Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
fe02255
Linux drops an idle text encoder without first copying its weights to…
KBRASK Oct 11, 2026
f0cfe03
Keep only the cat ready-made scene; answer progress questions about a…
KBRASK Oct 18, 2026
dcd804c
White-model scene: say which failure happened and show rendering prog…
KBRASK Oct 18, 2026
47f76d4
Preserve reference video early steps in exported cache identities
KBRASK Oct 18, 2026
ac5a7e0
LoRA check: say when a file with another model's layer names is used …
KBRASK Oct 18, 2026
bc0b215
Image generation history and size tiers
KBRASK Oct 18, 2026
c88f309
Image generation on 6-8 GB cards: lighter decoding and more DiT block…
KBRASK Oct 18, 2026
196e700
Image generation: a card that holds the whole model keeps no host cop…
KBRASK Oct 18, 2026
aea443e
Image generation: falling back to streaming no longer stops half-way …
KBRASK Oct 18, 2026
3a84876
Image generation: a failed fall-back to streaming keeps the card able…
KBRASK Oct 18, 2026
b88c158
Run workflows saved by older versions: reset unusable optional inputs…
KBRASK Oct 18, 2026
fa025b9
Use one sentence shape for white-model scene failures
KBRASK Oct 18, 2026
ca67320
Align cast back-view body direction and bound orientation retries
KBRASK Oct 18, 2026
aaccdf2
Report a problem next to failures everywhere
KBRASK Oct 18, 2026
7f94937
Mac: generate long videos in parts and continue a video
KBRASK Oct 18, 2026
064153f
Faster Windows updates in mainland China: signed domestic mirror and …
KBRASK Oct 18, 2026
babd96d
Launcher install review page fits narrow windows
KBRASK Oct 18, 2026
5d54025
Align the prompt model card padding with the cards above it
KBRASK Oct 18, 2026
a1245e4
Keep attention outputs on the GPU on small Linux cards by trimming th…
KBRASK Oct 18, 2026
342720e
Let the 16-step easycache pass reuse at threshold 0.30
KBRASK Oct 18, 2026
b674f7b
Load the update modules next to the engine when ComfyUI imports the n…
KBRASK Oct 18, 2026
cce4c5e
Report the engine budget with the same trimmed GPU reserve the small-…
KBRASK Oct 18, 2026
d4c1bcb
Image history: open the folder without waiting on the Windows shell
KBRASK Oct 18, 2026
287c70c
Report the text-encode room readings and resident models
KBRASK Oct 18, 2026
ddf886d
Planner: fund the decoder from the request budget, not beside samplin…
KBRASK Oct 18, 2026
9246c19
docs: add community result rows
KBRASK Oct 19, 2026
0189e13
Release 0.4.15
KBRASK Oct 19, 2026
e27a67a
Export the Node resolver hook that the public entry test imports
KBRASK Oct 19, 2026
0aed4c6
Keep eight earlier versions in the release notes so the update metada…
KBRASK Oct 19, 2026
4128f51
Write the English release notes in plain ASCII
KBRASK Oct 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,5 @@ prune benchmarks
prune distribution
prune .github
exclude AGENTS.md CHANGELOG.md

include freevideo_source_bootstrap.py freevideo_ed25519.py freevideo_update_keys.py
4 changes: 4 additions & 0 deletions docs/community-results.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ Thanks to the community members who sent diagnostic reports. FreeVideo already r
| 4070 Laptop | 8 GB | 34 GB | 1344×768 | 15 s | Light⁵ | 0.4.8 | 1444 s | 0.4.8 | Already optimal¹² |
| 5060 Ti | 16 GB | 56 GB | 960×544 | 10 s | Light | 0.4.6 | 904 s | 0.4.9 | 805 s (−99 s) |
| 3070 Laptop | 8 GB | 64 GB | 1344×768 | 10 s | Light | 0.4.9 | 1267 s | 0.4.9 | Already optimal |
| 3060 | 12 GB | 64 GB | 768×1344 | 5 s | Light | 0.4.12 | 639 s | 0.4.12 | Already optimal |
| 5080 | 16 GB | 64 GB | 768×1344 | 3 s | Max | 0.3.20 | 216 s | 0.3.20 | Already optimal |
| 4060 Ti | 8 GB | 32 GB | 960×544 | 5 s | Light | 0.4.12 | 333 s | 0.4.12 | Already optimal |
| 5070 Ti | 16 GB | 64 GB | 1184×672 | 15 s | Medium | 0.4.13 | 487 s | 0.4.13 | Already optimal |

- Time: end to end, from submission until the video is saved.
- After: the estimated time, derived from the report's stage times and from complete planned runs on our test machines (RTX 3090 and RTX 5060 Ti on Windows, RTX PRO 6000 on Linux) under the reporting machine's VRAM and RAM limits, not from reruns on that machine.
Expand Down
4 changes: 4 additions & 0 deletions docs/zh-CN/community-results.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@
| 4070 Laptop | 8 GB | 34 GB | 1344×768 | 15 s | 轻⁠量⁵ | 0.4.8 | 1444 s | 0.4.8 | 已⁠是⁠最⁠优¹² |
| 5060 Ti | 16 GB | 56 GB | 960×544 | 10 s | 轻⁠量 | 0.4.6 | 904 s | 0.4.9 | 805 s (−99 s) |
| 3070 Laptop | 8 GB | 64 GB | 1344×768 | 10 s | 轻⁠量 | 0.4.9 | 1267 s | 0.4.9 | 已⁠是⁠最⁠优 |
| 3060 | 12 GB | 64 GB | 768×1344 | 5 s | 轻⁠量 | 0.4.12 | 639 s | 0.4.12 | 已⁠是⁠最⁠优 |
| 5080 | 16 GB | 64 GB | 768×1344 | 3 s | 极⁠致 | 0.3.20 | 216 s | 0.3.20 | 已⁠是⁠最⁠优 |
| 4060 Ti | 8 GB | 32 GB | 960×544 | 5 s | 轻⁠量 | 0.4.12 | 333 s | 0.4.12 | 已⁠是⁠最⁠优 |
| 5070 Ti | 16 GB | 64 GB | 1184×672 | 15 s | 标⁠准 | 0.4.13 | 487 s | 0.4.13 | 已⁠是⁠最⁠优 |

- 用时:从提交到视频保存完成的端到端用时。
- 优化后:预估用时,根据报告中的分段用时,结合测试机(Windows RTX 3090 和 RTX 5060 Ti、Linux RTX PRO 6000)在报告机器的显存和内存上限下运行完整规划的实测推算,不是在报告机器上重跑。
Expand Down
4 changes: 2 additions & 2 deletions docs/zh-CN/使用说明.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
### 限制

- 两种做法一样,一条视频最多 5 段(第一段加 4 次接着生成),所以创作面板里时长最多填 60 秒。接续的每一段至少 10 秒,整条约 70 秒。剪辑视图里逐段生成同样适用。段数越多,衔接处越容易出现变化,适合切换镜头处,可直接切镜头,用对应的参考图开始新的一段。
- 接着生成使用轻量档,需要 NVIDIA 显卡,也不能和“继续二采”同时使用。
- 接着生成使用轻量档,需要 NVIDIA 显卡或 Mac,也不能和“继续二采”同时使用。
- 单次生成的最长时长随画幅和显卡而不同,在创作面板的“可生成范围”中可查看各尺寸的最长时长和预计用时。
- 分段生成同样需要 NVIDIA 显卡,不支持 Mac。
- 分段生成同样需要 NVIDIA 显卡或 Mac。
- 分段生成过程中取消,只会停止当前这一段,已经完成的段落会保留,之后可以从第一个未完成的段落继续。
95 changes: 95 additions & 0 deletions freevideo_ed25519.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
"""Dependency-free Ed25519 (RFC 8032), with strict encodings/subgroup checks.

Verification handles public inputs. Signing is an offline release utility, not
constant-time: keep signing seeds on a trusted release machine.
"""
import hashlib

P = 2**255 - 19
L = 2**252 + 27742317777372353535851937790883648493
D = -121665 * pow(121666, P-2, P) % P
I = pow(2, (P-1)//4, P)
IDENTITY = (0, 1, 1, 0)


def _decode(data):
if len(data) != 32:
raise ValueError('Invalid point length')
encoded = int.from_bytes(data, 'little')
y, sign = encoded & (2**255-1), encoded >> 255
if y >= P:
raise ValueError('Noncanonical point')
x2 = (y*y-1) * pow(D*y*y+1, P-2, P) % P
x = pow(x2, (P+3)//8, P)
if x*x % P != x2:
x = x*I % P
if x*x % P != x2 or (x == 0 and sign):
raise ValueError('Invalid point')
if x & 1 != sign:
x = P-x
return x, y, 1, x*y % P


def _add(a, b):
x, y, z, t = a
u, v, w, s = b
aa, bb = (y-x)*(v-u) % P, (y+x)*(v+u) % P
cc, dd = 2*D*t*s % P, 2*z*w % P
e, f, g, h = bb-aa, dd-cc, dd+cc, bb+aa
return e*f % P, g*h % P, f*g % P, e*h % P


def _mul(point, scalar):
result = IDENTITY
while scalar:
if scalar & 1:
result = _add(result, point)
point = _add(point, point)
scalar >>= 1
return result


def _encode(point):
x, y, z, _ = point
inverse = pow(z, P-2, P)
return ((y*inverse % P) | ((x*inverse % P & 1) << 255)).to_bytes(32, 'little')


B = _decode(bytes.fromhex('58' + '66'*31))


def verify(public_key, message, signature):
try:
if len(public_key) != 32 or len(signature) != 64:
return False
a, r = _decode(public_key), _decode(signature[:32])
scalar = int.from_bytes(signature[32:], 'little')
neutral = _encode(IDENTITY)
if scalar >= L or any(_encode(q) == neutral or _encode(_mul(q, L)) != neutral for q in (a, r)):
return False
challenge = int.from_bytes(hashlib.sha512(signature[:32]+public_key+message).digest(), 'little') % L
return _encode(_mul(B, scalar)) == _encode(_add(r, _mul(a, challenge)))
except (ValueError, TypeError, OverflowError):
return False


def _secret(seed):
if len(seed) != 32:
raise ValueError('Ed25519 seed must contain 32 bytes')
hashed = hashlib.sha512(seed).digest()
scalar = (int.from_bytes(hashed[:32], 'little') & (2**254-8)) | 2**254
return scalar, hashed[32:]


def public_key(seed):
scalar, _ = _secret(seed)
return _encode(_mul(B, scalar))


def sign(seed, message):
scalar, prefix = _secret(seed)
public = _encode(_mul(B, scalar))
nonce = int.from_bytes(hashlib.sha512(prefix+message).digest(), 'little') % L
r = _encode(_mul(B, nonce))
challenge = int.from_bytes(hashlib.sha512(r+public+message).digest(), 'little') % L
return r + ((nonce+challenge*scalar) % L).to_bytes(32, 'little')
11 changes: 9 additions & 2 deletions freevideo_engine/agent/guide.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,10 @@
'continuing again and again is not recommended. One video has at most 5 parts (the first and 4 '
'continuations), each continued part at least 10 seconds, about 70 seconds in all; filling that is not '
'recommended. Past it, cut to a new shot.',
'A question is not a request. When the user asks about progress or status ("is it working?", "what happened?") '
'answer it in one sentence from what you know or from the job and white-model status, and start nothing; never '
'use a question as a prompt. When a row carries "whitebox", the user was starting a white-model scene that is '
'still being prepared or failed: say which, and wait for the answer.',
]


Expand Down Expand Up @@ -265,6 +269,9 @@
'默认切镜头。能切镜头的地方就切:新镜头直接用对应的参考图(人物卡、场景图)生成,不接上一段视频。'
'只有不能切镜的长镜头、必须连贯的动作、必须连贯的声音,才接着上一段生成,而且不建议一段接一段地连续接。'
'一条视频最多 5 段(第一段加 4 次接着生成),接着生成的每一段至少 10 秒,总长约 70 秒;不建议接满,超过就切镜头。',
'问话不是要求。用户问进度或状态(“在做吗”“怎么了”)时,用一句话照实回答,依据是你所知道的、任务和白模的状态,'
'什么都不要开始;不要把问句当提示词。某条 inbox 带有 whitebox 时,说明用户刚启动了白模场景,'
'它还在准备或已经失败:照实说是哪一种,再等用户回话。',
]


Expand Down Expand Up @@ -480,7 +487,7 @@ def connection_guide(root, base, lang=None, offers=None, resumed=False):
'不要停下来问用户——你一停,用户在 FreeVideo 里说的话就没人听了。'
'用命令行调接口时,每次等待单独发一条命令(一条命令里不写循环,超时设到 60 秒以上),'
'返回后接着发下一条。'
'收到话(inbox 里每条的 text)先回一句再动手,例如“好的,开始生成。”:只一句,不复述尺寸、'
'收到要求(inbox 里每条的 text)先回一句再动手,例如“好的,开始生成。”:只一句,不复述尺寸、'
'时长这些参数,也不说预计时间(任务卡上都有)。做完再说一句短的,例如“好了。”,'
'或者一句用户需要知道的话,发回同一个 session,再接着等。先把话发出去,再调等待接口;不要写成“等待返回后才发话”,那样这句话会晚到将近一分钟。中文用全角标点。'
'做完一条视频也不是结尾:这句话就是汇报,不要再在自己窗口里写总结(一写总结这一轮就结束了,'
Expand All @@ -498,7 +505,7 @@ def connection_guide(root, base, lang=None, offers=None, resumed=False):
'user, because once you stop nobody hears what the user says in FreeVideo. From a command line, '
'each wait is its own command (no loop inside one command, timeout above 60 seconds); when it '
'returns, run the next one. '
'When words arrive (text of each inbox row), answer with one sentence before you start, for example '
'When a request arrives (text of each inbox row; a question about progress is answered, not started), answer with one sentence before you start, for example '
'"OK, starting.": one sentence only, without repeating size or length and without an estimate (the '
'job card shows them). When it is done say one short sentence, for example "Done.", or one thing the '
'user needs to know, to the same session, and wait again. Post your words first and call the wait after; never wait and then post, or they reach the user up to a minute late. Chinese text takes full-width punctuation. '
Expand Down
3 changes: 3 additions & 0 deletions freevideo_engine/agent/jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -1841,6 +1841,9 @@ def start_image(self, row):
else:
action = 'generate'
value.update(prompt=spec['prompt'], images=[image_reference(self.port, name) for name in spec['images']])
metadata = spec.get('metadata') or {}
if metadata.get('cast_card') and metadata.get('cast_part') == 'back':
value['cast_view'] = 'back'
if row.get('_cancel'):
raise JobError('cancelled', IMAGE_ERRORS['cancelled'])
row['_image'] = self.port.image_start(action, value)
Expand Down
26 changes: 23 additions & 3 deletions freevideo_engine/agent/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@
RESUMED = ('This conversation is already under way; its last lines are in "before". Do not introduce yourself again: '
'say nothing until the user speaks, then carry on from there.')
BEFORE_LINES, BEFORE_TEXT = 8, 600
# The user started a white-model scene and then spoke: the words may be about it, not a new request.
WHITEBOX_PENDING = ('The user started a white-model scene in this conversation ("whitebox" lists it). If the words ask about '
'it or about progress, answer from its status in one sentence (waiting or rendering: still being made; '
'failed: say it was not made, and why) and start nothing. A question is never a prompt.')


def local_address(request):
Expand Down Expand Up @@ -1074,12 +1078,24 @@ def write():
raise JobError('not_found', 'No conversation')
# Work in progress still writes into its conversation; finished videos stay in the library.
# New work is refused first, so what is counted below cannot grow before the file goes.
def busy(leaving=()):
return (any(r.get('session') == sid and r['status'] in ACTIVE and r['status'] not in leaving for r in jobs.rows())
or any(f['session'] == sid and not f['paused']
for f in jobs.productions.list(status='active', limit=None)))
stop = value.get('stop') is True
if stop: # The user confirmed: stop its jobs and pause its films first, as the stop button does.
jobs.stop_session(sid)
for _ in range(20): # A running job takes a moment to reach its cancelled state.
with jobs.lock:
if not busy():
break
time.sleep(0.25)
store.begin_delete(sid)
try:
with jobs.lock: # Productions share it.
if (any(r.get('session') == sid and r['status'] in ACTIVE for r in jobs.rows())
or any(f['session'] == sid and not f['paused']
for f in jobs.productions.list(status='active', limit=None))):
# After the user's stop, a job still winding down no longer holds the conversation: its
# writes are refused once deletion begins, so a stuck one cannot block deleting forever.
if busy(('cancelling',) if stop else ()):
raise JobError('conflict', 'Work is in progress in this conversation')
store.delete(sid)
except BaseException:
Expand Down Expand Up @@ -1725,6 +1741,10 @@ async def inbox(request):
follow = store is not None and not request.headers.get('X-FreeVideo-Session', '').strip()
def answer(value):
told, chat['told'][client] = chat['told'].get(client), value['session']
scenes = jobs.whitebox.open_for(value['session']) if value['inbox'] else []
if scenes:
value['whitebox'] = scenes
value['next'] = (WHITEBOX_PENDING + ' ' + value.get('next', '')).strip()
if follow and told != value['session']:
first = value['inbox'][0]['seq'] if value['inbox'] else store.get(value['session'])['last_seq'] + 1
if first > 1:
Expand Down
2 changes: 1 addition & 1 deletion freevideo_engine/agent/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,7 @@ def route(summary, body=None, query=None, **kw):
('/activity/cancel', 'POST', obj({'id': JOB}, ('id',))), ('/connect', 'GET', None),
('/connect/reset', 'POST', EMPTY), ('/chat', 'GET', None), ('/chat/now', 'GET', None),
('/chat/video', 'GET', None), ('/chat/list', 'GET', None), ('/chat/new', 'POST', EMPTY),
('/chat/delete', 'POST', obj({'session': SESSION}, ('session',))),
('/chat/delete', 'POST', obj({'session': SESSION, 'stop': {'type': 'boolean'}}, ('session',))),
('/chat', 'POST', obj({'text': string(8000), 'id': CHAT_ID, 'session': SESSION, 'whitebox': {'type': 'boolean'}}, ('text', 'id'))),
('/chat/confirm', 'POST', obj({'id': string(12, pattern=r'^cf[0-9a-f]{10}$'), 'approve': {'type': 'boolean'},
'session': SESSION}, ('id', 'approve')))):
Expand Down
12 changes: 12 additions & 0 deletions freevideo_engine/agent/whitebox.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,18 @@ def create(self, code, summary, session=None, notice=None):
self._save(record)
return self.view(record)

def open_for(self, session):
"""The scenes this conversation started that are still being made, or failed without the user being told."""
with self.lock:
rows = []
for record in sorted(self.records.values(), key=lambda r: r['created_at']):
self._expire(record)
if record.get('session') == session and record.get('notice') and record['status'] != 'ready' \
and self.clock() - record['created_at'] < 1800:
rows.append(dict(id=record['id'], status=record['status'], summary=record['summary'],
**(dict(error=record['error']) if record['status'] == 'failed' else {})))
return rows

def take_notice(self, identity):
"""What the server should tell the user's conversation once the scene is ready; handed out once."""
with self.lock:
Expand Down
Loading