Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
c2a56db
Chain seam notes: name the seams that failed before the ones with no …
KBRASK Oct 31, 2026
dc57ca5
Engine update: a refused connection is not a busy pool; setup spares …
KBRASK Oct 31, 2026
8208085
Long-video how-to: point to Extend below the finished video, as the t…
KBRASK Oct 31, 2026
e7528b8
docs: unify Chinese community table cells and reword the reference fo…
KBRASK Oct 31, 2026
7e86811
Write browser-playable lossless audio for kept original tracks
KBRASK Oct 31, 2026
b29a3f2
Show the phone password page in Chinese and unify password messages
KBRASK Oct 31, 2026
a104e97
Precompile status: preserve write order and responsive cancellation
KBRASK Oct 31, 2026
ea70d1f
Music add-on: BF16-only download, built-in int8 for small cards, mode…
KBRASK Oct 31, 2026
f558b32
Use the portable Git on Windows when the installed Git cannot load li…
KBRASK Oct 31, 2026
a6fbe5e
Keep a rejected entry explained until that field is accepted
KBRASK Oct 31, 2026
bdb061d
Record a decode retry that fails again so the compatibility level can…
KBRASK Oct 31, 2026
e1cbe61
Text conditioning cache keyed by encoder content and prompt, with LRU…
KBRASK Oct 31, 2026
d2943da
Record Agent generation polling in diagnostic reports
KBRASK Oct 31, 2026
5470950
Name offline package refusals and stop blaming the browser for a slow…
KBRASK Oct 31, 2026
346f222
Mac: a third sampling attempt with Torch attention after two Metal co…
KBRASK Oct 31, 2026
2a4754c
Keep the guide clip's sound on a preview too when requested
KBRASK Oct 31, 2026
5992fee
Linux setup: name the pacman command for missing basic tools on Arch
KBRASK Oct 31, 2026
7298f4c
Add community results row
KBRASK Oct 31, 2026
b21e6f0
Use the Windows certificate store for HTTPS checks
KBRASK Oct 31, 2026
8084dd1
Release notes for 0.4.27
KBRASK Nov 1, 2026
6613e68
Release notes for 0.4.27: decoding after a CUDA error
KBRASK Nov 1, 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
5 changes: 3 additions & 2 deletions docs/community-results.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,11 +123,12 @@ Thanks to the community members who sent diagnostic reports. FreeVideo already r
| 5070 Ti Laptop | 12 GB | 32 GB | 1344×768 | 10 s | Light | 0.4.23 | 373 s¹³ | 0.4.23 | Thermal‑limited¹ |
| 5060 | 8 GB | 48 GB | 768×1344 | 14 s | Medium | 0.3.25 | 3474 s | 0.3.25 | Already optimal |
| 3060 | 12 GB | 32 GB | 896×1120 | 10 s | Light | 0.3.25 | 1045 s | 0.4.0 | 1025 s (−20 s) |
| 3060 Laptop | 6 GB | 40 GB | 960×544 | 15 s | Light⁵ | 0.4.24 | 713 s | 0.4.24 | Thermal‑limited¹ |

- 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.
- Already optimal: weight placement and transfers on this machine are already the fastest they can be; the time depends on the GPU's compute and the chosen quality level.
- ¹ Thermal-limited: the GPU spent most of the run in thermal slowdown (3070 Laptop: 229 MHz on average at 89 °C; A5000 Laptop: 882 MHz on average), so cooling limited the time.
- ¹ Thermal-limited: the GPU spent most of the run in thermal slowdown (3070 Laptop: 229 MHz on average at 89 °C; A5000 Laptop: 882 MHz on average; 3060 Laptop: 1496 MHz on average at 81 °C), so cooling limited the time.
- ² This machine still runs the FP8 model; the estimate is for the int8 model, a one-click upgrade in the launcher since 0.3.0.
- ³ Preview: only the first pass, at half resolution. With 16 GB of RAM most of the model is read from the drive at every step (about 80 MB/s on this machine), which takes most of the time.
- ⁴ The report had no diagnostic file; the estimate replays the plan for this machine's configuration and measures the same request on our test machines.
Expand All @@ -138,5 +139,5 @@ Thanks to the community members who sent diagnostic reports. FreeVideo already r
- ⁹ The drive holding the model files reads at about 80 MB/s; reading them took about 570 s of the 1676 s.
- ¹⁰ GPU shared: other programs were using about 29% of the graphics card during the run, so FreeVideo's steps ran at a little over 70% of their usual speed. With the card free, the same request takes about 7200 s.
- ¹¹ With 16 GB of RAM, the text encoder and part of the video model cannot stay in memory and are read from the drive; text encoding and the first pass, which wait on these reads, took about 460 s of the 887 s.
- ¹² Reference-video task: the reference video and audio roughly double the sequence the model processes, so each step takes about twice as long as a plain request of the same size.
- ¹² Task with a reference: every step also processes the reference video, images or audio, so each step takes longer than a plain request of the same size, up to about twice as long with a long reference.
- ¹³ Preview (first pass only).
13 changes: 7 additions & 6 deletions docs/zh-CN/community-results.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,12 @@
| 5060 笔⁠记⁠本 | 8 GB | 32 GB | 1344×768 | 3 s | 标⁠准 | 0.4.7 | 541 s | 0.4.7 | 已⁠是⁠最⁠优 |
| 5060 Ti | 16 GB | 34 GB | 1344×768 | 5.2 s | 标⁠准 | 0.4.8 | 2521 s | 0.4.8 | 已⁠是⁠最⁠优¹² |
| 4060 | 8 GB | 32 GB | 768×1344 | 17 s | 轻⁠量 | 0.4.8 | 1942 s | 0.4.8 | 已⁠是⁠最⁠优 |
| M1 Max | 统一内存 | 64 GB | 960×544 | 2.3 s | 轻⁠量 | 0.4.8 | 1226 s | 0.4.8 | 已⁠是⁠最⁠优 |
| M1 Max | 统⁠一⁠内⁠存 | 64 GB | 960×544 | 2.3 s | 轻⁠量 | 0.4.8 | 1226 s | 0.4.8 | 已⁠是⁠最⁠优 |
| 3070 | 8 GB | 32 GB | 1344×768 | 5.2 s | 轻⁠量⁵ | 0.4.8 | 285 s | 0.4.8 | 已⁠是⁠最⁠优 |
| 3060 | 12 GB | 34 GB | 1664×960 | 6.6 s | 标⁠准 | 0.4.8 | 4025 s | 0.4.8 | 已⁠是⁠最⁠优¹² |
| 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 | 已⁠是⁠最⁠优 |
| 4070 笔⁠记⁠本 | 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 笔⁠记⁠本 | 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 | 已⁠是⁠最⁠优 |
Expand Down Expand Up @@ -123,11 +123,12 @@
| 5070 Ti 笔⁠记⁠本 | 12 GB | 32 GB | 1344×768 | 10 s | 轻⁠量 | 0.4.23 | 373 s¹³ | 0.4.23 | 散⁠热⁠受⁠限¹ |
| 5060 | 8 GB | 48 GB | 768×1344 | 14 s | 标⁠准 | 0.3.25 | 3474 s | 0.3.25 | 已⁠是⁠最⁠优 |
| 3060 | 12 GB | 32 GB | 896×1120 | 10 s | 轻⁠量 | 0.3.25 | 1045 s | 0.4.0 | 1025 s⁠(−20 s⁠) |
| 3060 笔⁠记⁠本 | 6 GB | 40 GB | 960×544 | 15 s | 轻⁠量⁵ | 0.4.24 | 713 s | 0.4.24 | 散⁠热⁠受⁠限¹ |

- 用时:从提交到视频保存完成的端到端用时。
- 优化后:预估用时,根据报告中的分段用时,结合测试机(Windows RTX 3090 和 RTX 5060 Ti、Linux RTX PRO 6000)在报告机器的显存和内存上限下运行完整规划的实测推算,不是在报告机器上重跑。
- 已是最优:这台机器的权重放置和数据传输已经是最快的方式,用时取决于显卡算力和所选档位。
- ¹ 散热受限:显卡在生成过程中大部分时间处于温控降频(3070 笔记本平均 229 MHz、89 °C;A5000 笔记本平均 882 MHz),用时主要受散热限制。
- ¹ 散热受限:显卡在生成过程中大部分时间处于温控降频(3070 笔记本平均 229 MHz、89 °C;A5000 笔记本平均 882 MHz;3060 笔记本平均 1496 MHz、81 °C),用时主要受散热限制。
- ² 这台机器仍在使用 FP8 模型;预估为在启动器中一键升级到 int8 模型(0.3.0 起提供)后的用时。
- ³ 预览:只运行第一遍(半分辨率)。16 GB 内存放不下大部分模型,每一步都要从硬盘读取(这台机器约 80 MB/s),大部分时间花在读盘上。
- ⁴ 报告未附诊断文件;预估按这台机器的配置重放规划,并在测试机上实测同样的请求。
Expand All @@ -138,5 +139,5 @@
- ⁹ 模型文件所在的硬盘读取约 80 MB/s,1676 秒里约 570 秒用于读取模型文件。
- ¹⁰ 显卡被占用:生成过程中其他程序占用了约 29% 的显卡,FreeVideo 每一步只有独占时七成多一点的速度;显卡空出来时,同样的请求约需 7200 秒。
- ¹¹ 16 GB 内存留不下文本编码器和视频模型的一部分,需要从硬盘读取;887 秒里约 460 秒是等待这些读取的文本编码和第一遍。
- ¹² 参考视频任务:参考视频和参考音频使模型处理的序列约增加一倍,每步耗时约为相同尺寸普通请求的两倍。
- ¹² 带参考的任务:每一步还要处理参考视频、参考图片或参考音频,所以每步耗时比相同尺寸的普通请求长;参考较长时约为两倍。
- ¹³ 预览(只做第一遍)。
35 changes: 23 additions & 12 deletions freevideo_engine/access_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,23 +38,34 @@
COOKIE = 'freevideo_access'
DAYS = 30
LOGIN_PATH = '/m/login'
# Temporary English copy; keep all password-page text here for Iris.
PASSWORD_COPY = dict(title='FreeVideo', password='Password', remember='Keep me signed in',
submit='Sign in', incorrect='Incorrect password', retry='Try again later ({minutes} minutes).',
phone='FreeVideo mobile')
# All password-page text lives here; the lockout line matches web/phone_model.js.
PASSWORD_COPY = dict(
en=dict(title='FreeVideo', password='Password', remember='Keep me signed in',
submit='Sign in', incorrect='Incorrect password', retry='Too many attempts. Try again in {minutes} min.',
phone='FreeVideo mobile'),
zh=dict(title='FreeVideo', password='密码', remember='保持登录',
submit='登录', incorrect='密码不正确', retry='尝试次数过多,请 {minutes} 分钟后再试。',
phone='FreeVideo 手机版'))


def password_page(*, error='', retry_after=0, status=401):
copy = PASSWORD_COPY
def password_language(request):
"""'zh' when the browser's most preferred language is Chinese, else 'en'."""
first = request.headers.get('Accept-Language', '').split(',')[0].split(';')[0].strip().lower()
return 'zh' if first == 'zh' or first.startswith('zh-') else 'en'


def password_page(request, *, error='', retry_after=0, status=401):
language = password_language(request)
copy = PASSWORD_COPY[language]
message = copy['retry'].format(minutes=math.ceil(retry_after / 60)) if retry_after else error
response = web.Response(status=status, content_type='text/html', text=(
'<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" '
'<!doctype html><html lang="{language}"><head><meta charset="utf-8"><meta name="viewport" '
'content="width=device-width,initial-scale=1"><title>{title}</title></head><body>'
'<h1>{title}</h1><p data-retry-after="{retry_after}">{message}</p><form method="post" action="{action}">'
'<label>{password}<input type="password" name="password" autocomplete="current-password" required></label>'
'<label><input type="checkbox" name="remember" value="1" checked>{remember}</label>'
'<button type="submit">{submit}</button></form></body></html>').format(
**{k: html.escape(v) for k, v in copy.items()}, message=html.escape(message), retry_after=retry_after, action=LOGIN_PATH))
**{k: html.escape(v) for k, v in copy.items()}, language=language, message=html.escape(message), retry_after=retry_after, action=LOGIN_PATH))
response.headers['Cache-Control'] = 'no-store'
# Chromium sends Origin: null from a no-referrer page, which the login would refuse.
response.headers['Referrer-Policy'] = 'same-origin'
Expand Down Expand Up @@ -191,17 +202,17 @@ async def login(request):
await asyncio.sleep(max(0, LOGIN_DELAY - (time.monotonic() - started)))
auth.reload()
if not auth.enabled or auth.record != record:
return password_page(error=PASSWORD_COPY['incorrect'])
return password_page(request, error=PASSWORD_COPY[password_language(request)]['incorrect'])
now = time.monotonic()
remaining = auth.attempts.remaining(ip, now)
if remaining:
auth.log(ip, 'locked')
return password_page(retry_after=remaining, status=429)
return password_page(request, retry_after=remaining, status=429)
if not correct:
remaining = auth.attempts.failed(ip, now)
if remaining:
auth.log(ip, 'locked')
return password_page(error=PASSWORD_COPY['incorrect'], retry_after=remaining,
return password_page(request, error=PASSWORD_COPY[password_language(request)]['incorrect'], retry_after=remaining,
status=429 if remaining else 401)
value = auth.issue(remember, time.time())
auth.attempts.success(ip)
Expand Down Expand Up @@ -284,7 +295,7 @@ async def handle(request):
header = request.headers.get('Authorization', '')
if not (header[:7].lower() == 'bearer ' and matches(header[7:].strip(), token)):
remaining = auth.attempts.remaining(request.remote or '', time.monotonic())
return password_page(retry_after=remaining, status=429 if remaining else 401)
return password_page(request, retry_after=remaining, status=429 if remaining else 401)
else:
offered = request.rel_url.query.get('token')
if offered is not None:
Expand Down
33 changes: 30 additions & 3 deletions freevideo_engine/adaptive.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,26 @@ def second_pass_resumable(error, failure, metrics, attempts):
return not any(row.get('decision', {}).get('second_pass_resume') for row in attempts)


def decode_resumable(error, failure, metrics, attempts):
"""A CUDA error after sampling finished and its latents were saved and validated.

Any CUDA error qualifies, including a lost or unknown device state: the
retry is one fresh process that loads only the retained latents and decodes
them with the same placement, so no earlier context is reused and the
sampling is not repeated. It is not repeated itself, and a worker that may
still own the device never gets it. Out-of-memory keeps its own path.
"""
from .decode_resume import completed_sampling
if failure.get('kind') not in ('cuda_error', 'code_error') or getattr(error, 'worker_stop_confirmed', None) is False:
return False
recorded = str((metrics or {}).get('error', '')).lower()
if not (cuda_runtime_error(error) or 'cuda error' in recorded) or 'out of memory' in recorded:
return False
if not completed_sampling(metrics):
return False
return not any(row.get('decision', {}).get('decode_resume') for row in attempts)


def classify_failure(error, metrics=None):
"""Only positively identified resource failures are retriable.

Expand Down Expand Up @@ -651,16 +671,23 @@ def execute_attempts(history, identity, profile, canvas, launch, *,
history.finish(attempt, failure['outcome'], details=details)
row.update(state=failure['outcome'], failure=failure)
persist()
resume = (automatic and index < max_retries and not failure['retryable']
and second_pass_resumable(error, failure, metrics, report['resource_attempts']))
decode_resume = (automatic and index < max_retries and not failure['retryable']
and decode_resumable(error, failure, metrics, report['resource_attempts']))
resume = decode_resume or (automatic and index < max_retries and not failure['retryable']
and second_pass_resumable(error, failure, metrics, report['resource_attempts']))
if not automatic or not (failure['retryable'] or resume) or index == max_retries:
raise
previous_ram_retries = sum(r.get('failure', {}).get('kind') == 'ram_pressure'
for r in report['resource_attempts'][:-1])
previous_gpu_retries = sum(r.get('failure', {}).get('kind') == 'gpu_oom'
for r in report['resource_attempts'][:-1])
from .decode_resume import completed_sampling
if resume:
if decode_resume:
updated, decision = copy.deepcopy(current), dict(
reason='A CUDA error ended the request after sampling finished; decode only the '
'retained latents in a fresh worker with the same placement.',
numerical_class='unchanged', decode_resume=True)
elif resume:
updated, decision = copy.deepcopy(current), dict(
reason='A CUDA call was rejected in the second pass; repeat only the second pass '
'from the retained first pass with the same placement.',
Expand Down
7 changes: 7 additions & 0 deletions freevideo_engine/agent/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,11 @@
'Do not retry. Write the subtitle lines yourself with the subtitles edit, or tell the user the model is not installed.'),
'listen_failed': (409, True, 'Listening to the sound did not finish.',
'Read the project, then submit the listening job again with a new ref; if it fails again, write the lines yourself.'),
'music_download_failed': (409, True, 'The music model could not be downloaded.',
'Tell the user the music model needs about 20 GB of free disk and a network connection, then '
'submit the music job again with a new ref; if it fails again, write the score yourself.'),
'music_failed': (409, True, 'Composing the music did not finish.',
'Read the project, then submit the music job again with a new ref; if it fails again, write the score yourself.'),
'clip_source_unusable': (409, False, 'A clip\'s source is missing, or cannot be read as far as the clip uses it.',
'Read the project and check each clip\'s source: it must exist and play, and in + frames must stay '
'within its real length. Correct the project, then export the new revision.'),
Expand Down Expand Up @@ -221,6 +226,8 @@ def recovery(code, *, kind=None, stage='request', resumable=True):
return ERRORS[code][3]
if kind == 'image' and code == 'disk_full':
return 'Tell the user to free disk space, then submit the picture again with a new ref.'
if kind == 'music' and code == 'out_of_memory':
return 'Close other GPU applications, then submit the music job again with a new ref.'
if kind == 'image' and code == 'no_memory':
return 'Lower the picture dimensions or use fewer reference pictures, then submit with a new ref.'
if kind in RECOVERY and stage == 'terminal':
Expand Down
Loading