Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
fbf84c2
Feedback diagnostics: include the machine's hardware before the first…
KBRASK Nov 11, 2026
02cea96
Model upgrade: let the download start an unresumable partial file again
KBRASK Nov 11, 2026
764cb96
An engine update downloads the pinned uv again when it is missing fro…
KBRASK Nov 11, 2026
e22028c
Setup waits briefly for the installation lock and names what holds it
KBRASK Nov 11, 2026
07187d9
Image generation progress follows time like video: one percentage fro…
KBRASK Nov 11, 2026
b44b75b
Prompt enhancement reads the picture or video a connected load node h…
KBRASK Nov 11, 2026
3e380b3
Editor: music on the second sound track starts from its own descripti…
KBRASK Nov 11, 2026
91c84ab
Lost connection while submitting: the report records whether the engi…
KBRASK Nov 11, 2026
e7e7b3a
H3 Image: show the picture after each step while it generates
KBRASK Nov 11, 2026
303cb97
Long video join: say whether the engine kept no seam record or the re…
KBRASK Nov 11, 2026
53616a2
Carry a library character's voice into the shot with its pictures
KBRASK Nov 11, 2026
fe97fb1
Image page: the step picture and the finished picture use the same fr…
KBRASK Nov 11, 2026
051ff6f
Name the Reserved VRAM setting in a GPU refusal it alone causes, with…
KBRASK Nov 11, 2026
c5117c2
H3 image: file model-load failures under the load stage
KBRASK Nov 12, 2026
7a9e8a5
Image failure card: stage reason and retry inside the card, chat card…
KBRASK Nov 12, 2026
94cf606
First installation offers MiniMax H3 Video or MiniMax H3 Image
KBRASK Nov 12, 2026
654b0a3
GPU package downloads keep trying through a slow or unstable connection
KBRASK Nov 12, 2026
27f173d
Chat and image pages: Enter, Ctrl+Enter and paste reach their handler…
KBRASK Nov 12, 2026
7f0e9a2
Edit page quality control: the creative panel's quality setting besid…
KBRASK Nov 12, 2026
7d25ec8
The editing dialog keeps its grid layout when the studio stylesheet l…
KBRASK Nov 12, 2026
59dde68
Chat picture failure: the report button takes the video failure card'…
KBRASK Nov 12, 2026
7cd5ba4
The report button reads "Download report" on the image page, in the A…
KBRASK Nov 12, 2026
9325461
Chat picture failure: the reason stands on its own line with the butt…
KBRASK Nov 12, 2026
5da9d28
Image view at 400 px: the model name ends before the dropdown arrow, …
KBRASK Nov 12, 2026
6fe08bf
Picture failures for lack of memory use consistent formal Chinese wor…
KBRASK Nov 12, 2026
b59d41a
Tutorial: image generation chapter with real H3 Image pictures
KBRASK Nov 12, 2026
e5fc352
Show technical details on picture failure cards in Autopilot, as on v…
KBRASK Nov 12, 2026
3aa8609
Keep the Autopilot message hint on one line on phones
KBRASK Nov 12, 2026
1edb772
Use formal wording for memory failure reasons
KBRASK Nov 12, 2026
aff9cfc
Image view: a Seed field under Advanced, empty draws a new seed for e…
KBRASK Nov 12, 2026
9ee171a
Entry tests: stub the video model check and give text nodes children
KBRASK Nov 12, 2026
097f7d2
Autopilot hint names Autopilot and fits one line on phones; failure r…
KBRASK Nov 12, 2026
b8e418c
What's new page draws its own segmented control so the editor code st…
KBRASK Nov 12, 2026
ab7f252
Image failure reports record the GPU reading the admission check plan…
KBRASK Nov 12, 2026
14a47c2
Node view top bar fits narrow windows: entries step aside in the head…
KBRASK Nov 12, 2026
4b46e14
Image result menu: opens upward when the box below would clip it
KBRASK Nov 12, 2026
fe930c6
Browser tests: phone entry and view switch follow the five-view header
KBRASK Nov 12, 2026
92b9a88
A kept engine now serves the next request when only its pass tiling c…
KBRASK Nov 12, 2026
ec32921
Report real work counts for the long silent spans of a video run (dec…
KBRASK Nov 11, 2026
f6a9c83
The text encoder's weights are read ahead in pieces on several handle…
KBRASK Nov 12, 2026
904ba47
Place the view switch the same way in every view
KBRASK Nov 12, 2026
b2c5667
Keep long titles inside the running Autopilot job card
KBRASK Nov 12, 2026
eb0668e
Tutorial book: add middle-frames and save-frame chapters
KBRASK Nov 12, 2026
bf5d0c4
Keep a readable job title in the Autopilot header before other entries
KBRASK Nov 12, 2026
b9c59e6
Image editor: wrap tools and tool options on narrow screens
KBRASK Nov 12, 2026
88a88cd
Connection card: when the address is refused to a page not opened wit…
KBRASK Nov 12, 2026
232151c
Editor: whole track names in the narrow head column up to 520 px
KBRASK Nov 12, 2026
d65aded
The native GPU check on a Mac install now exits normally when the pro…
KBRASK Nov 12, 2026
54d238c
Release 0.5.2: update notes
KBRASK Nov 12, 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
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ recursive-include example_workflows *.json
recursive-include web *.js *.css
include web/vendor/three/LICENSE
include web/assets/*.svg web/assets/*.png web/assets/*.webp
recursive-include web/assets/image *.webp
recursive-include web/howto *.png
recursive-include web/fonts *.woff2
recursive-include web/tutorial *.webp *.mp4
Expand Down
1 change: 0 additions & 1 deletion docs/community-results.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,6 @@ Thanks to the community members who sent diagnostic reports. FreeVideo already r
| 3070 Laptop | 8 GB | 64 GB | 960×544 | 5 s | Light | 0.4.33 | 260 s | 0.4.33 | Already optimal |
| 3070 | 8 GB | 64 GB | 736×736 | 10 s | Light | 0.4.35 | 471 s | 0.4.35 | Already optimal |
| 5060 | 8 GB | 64 GB | 1088×1920 | 5.9 s | Medium | 0.4.31 | 1259 s | 0.4.31 | Already optimal¹² |
| 5060 | 8 GB | 64 GB | 960×1664 | 11.5 s | Light⁵ | 0.4.38 | 1035 s | 0.4.38 | 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
1 change: 0 additions & 1 deletion docs/zh-CN/community-results.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,6 @@
| 3070 笔⁠记⁠本 | 8 GB | 64 GB | 960×544 | 5 s | 轻⁠量 | 0.4.33 | 260 s | 0.4.33 | 已⁠是⁠最⁠优 |
| 3070 | 8 GB | 64 GB | 736×736 | 10 s | 轻⁠量 | 0.4.35 | 471 s | 0.4.35 | 已⁠是⁠最⁠优 |
| 5060 | 8 GB | 64 GB | 1088×1920 | 5.9 s | 标⁠准 | 0.4.31 | 1259 s | 0.4.31 | 已⁠是⁠最⁠优¹² |
| 5060 | 8 GB | 64 GB | 960×1664 | 11.5 s | 轻⁠量⁵ | 0.4.38 | 1035 s | 0.4.38 | 已⁠是⁠最⁠优 |

- 用时:从提交到视频保存完成的端到端用时。
- 优化后:预估用时,根据报告中的分段用时,结合测试机(Windows RTX 3090 和 RTX 5060 Ti、Linux RTX PRO 6000)在报告机器的显存和内存上限下运行完整规划的实测推算,不是在报告机器上重跑。
Expand Down
11 changes: 7 additions & 4 deletions freevideo_engine/agent/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import math
import re
import uuid
from ..image_gen.failure import STAGES as IMAGE_STAGES
from ..image_gen.loras import ERRORS as LORA_ERRORS, MESSAGE_EN as LORA_MESSAGE_EN, MESSAGE_ZH as LORA_MESSAGE_ZH

REQUEST_ID = ContextVar('agent_request_id', default=None)
Expand Down Expand Up @@ -149,7 +150,7 @@
'small_memory': ('This Mac has less than 16 GB of memory and cannot generate pictures.',
'这台 Mac 的内存不足 16 GB,无法生成图片。'),
'no_memory': ('Not enough GPU memory, even at the smallest size. Close other apps using the GPU, then try again.',
'显存不够,最小尺寸也没能生成。关掉其他占用显卡的程序后再试。'),
'显存不足,最小尺寸也无法生成。请关闭其他占用显卡的程序后重试。'),
'busy_video': ('A video is generating. Try again when it finishes.', '视频正在生成,完成后再试。'),
'pack_missing': ('Download "Image generation models" in the launcher first.',
'请先在启动器里下载“图像生成模型”。'),
Expand Down Expand Up @@ -297,8 +298,8 @@ def safe_details(value):
'lan_unavailable': '局域网设置需要启动器安装记录',
'capabilities_computing': '正在计算这台电脑的生成能力,稍后再试',
'out_of_memory': '生成时显存或内存不足',
'insufficient_memory': '可用显存或内存不够生成这段内容',
'no_memory': '可用显存或内存不够生成这张图片',
'insufficient_memory': '可用显存或内存不足,无法生成这段内容',
'no_memory': '可用显存或内存不足,无法生成这张图片',
'ram_space': '加载模型时系统内存不足',
'model_missing': '需要的模型还没有安装好',
'pack_missing': '图片模型还没有安装好',
Expand Down Expand Up @@ -466,7 +467,7 @@ def with_reason(error):

def build(code, *, details=None, request=None, validation=False, message=None, next_action=None,
action_required=True, retry_after_seconds=None, owner=None, kind=None, stage='request', resumable=True,
report_id=None, memory=None, other_usage=_UNREAD_USAGE):
report_id=None, memory=None, image_stage=None, other_usage=_UNREAD_USAGE):
code = code if code in ERRORS else 'internal_error'
_, retryable, default_message, _ = ERRORS[code]
action = recovery(code, kind=kind, stage=stage, resumable=resumable)
Expand All @@ -482,6 +483,8 @@ def build(code, *, details=None, request=None, validation=False, message=None, n
result.update(image_failure(code, report_id))
if code == 'no_memory' and memory == 'host':
result['memory'] = 'host'
if image_stage in IMAGE_STAGES:
result['stage'] = image_stage
if other_usage is not _UNREAD_USAGE:
result['other_usage'] = other_usage
result = capture_other_usage(result)
Expand Down
5 changes: 3 additions & 2 deletions freevideo_engine/agent/jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -713,7 +713,7 @@ def finish(self, row, status, *, result=None, error=None, save=True):
error = errors.build(code, details=error.get('details'),
request=row.get('request_id'), action_required=code != 'cancelled',
kind=row['kind'], stage='terminal', resumable=not row['spec'].get('refine_from'),
report_id=error.get('report_id'), memory=error.get('memory'),
report_id=error.get('report_id'), memory=error.get('memory'), image_stage=error.get('stage'),
other_usage=error.get('other_usage'))
if row['kind'] == 'image':
reason_zh, reason_en = error['error_zh'], error['error_en']
Expand Down Expand Up @@ -1264,6 +1264,7 @@ def view(self, row, *, detail=True):
result['error'] = errors.build(result['error']['code'], kind='image', stage='terminal',
details=result['error'].get('details'), request=row.get('request_id'),
action_required=result['error']['code'] != 'cancelled', report_id=row['error'].get('report_id'),
memory=row['error'].get('memory'), image_stage=row['error'].get('stage'),
other_usage=row['error'].get('other_usage'))
else:
result['error'] = dict(result['error'], **errors.reasons(row['error']))
Expand Down Expand Up @@ -2168,7 +2169,7 @@ def poll_image(self, row):
code = code or 'generation_failed'
if not self.wait_image(row, code):
self.finish(row, 'failed', error=dict(code=code, report_id=state.get('report_id'), memory=state.get('memory'),
other_usage=state.get('other_usage')))
stage=state.get('stage'), other_usage=state.get('other_usage')))

# Local VLM -----------------------------------------------------------------------------------------
def start_vlm(self, row):
Expand Down
59 changes: 35 additions & 24 deletions freevideo_engine/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -1136,6 +1136,32 @@ def unpack(archive, destination):
tar.extractall(io_path(destination))


def prepare_uv(system, root, uv_spec, networking, env, fetch):
"""Put the pinned uv under root/tools and return its path: from a verified local copy when one exists, else downloaded.

The installer and the launcher's source update share it, so a missing uv is restored the same way in both.
"""
root = Path(root)
if system == 'Windows':
from .uv_bootstrap import prepare_windows
return prepare_windows(root, uv_spec, networking, env, fetch)
if system == 'Darwin':
from . import macos_bootstrap
return macos_bootstrap.prepare_uv(root, networking, env, fetch)
archive = root / 'downloads' / 'uv.tar.gz'
from .system import bootstrap_root
previous = bootstrap_root(root) / ('uv-' + uv_spec['version'] + '.tar.gz')
if not archive.exists() and previous.is_file() and digest(previous) == uv_spec['sha256']:
archive.parent.mkdir(parents=True, exist_ok=True)
try:
os.link(previous, archive)
except OSError:
shutil.copyfile(previous, archive)
fetch(uv_spec['url'], archive, uv_spec['sha256'])
unpack_folder(archive, root / 'tools', 'uv-x86_64-unknown-linux-gnu')
return root / 'tools' / 'uv-x86_64-unknown-linux-gnu' / 'uv'


def unpack_folder(archive, destination, name):
"""Unpack the archive's top-level folder `name` to destination/name, replacing what is there.

Expand Down Expand Up @@ -1265,9 +1291,9 @@ def initialize(self, value, ui):
self.system = value['inventory'].get('hardware', {}).get('system', platform.system())
self.pythons = role_pythons(self.root, self.layout, self.system)
self.root.mkdir(parents=True, exist_ok=True)
self.lock = self.locks.enter_context(runtime_lock(self.root / 'setup.lock', inherit=False))
self.lock = self.locks.enter_context(runtime_lock(self.root / 'setup.lock', inherit=False, wait_seconds=3))
lock_path = os.environ.get('FREEVIDEO_LOCK_PATH', str(self.root / 'engine.lock'))
self.runtime_fd = self.locks.enter_context(runtime_lock(lock_path))
self.runtime_fd = self.locks.enter_context(runtime_lock(lock_path, wait_seconds=3))
self.run_dir = self.root / 'setup-runs' / (time.strftime('%Y%m%dT%H%M%SZ', time.gmtime()) + '-' + str(os.getpid()))
self.run_dir.mkdir(parents=True, exist_ok=False)
save(self.run_dir / 'plan.json', value)
Expand Down Expand Up @@ -2264,24 +2290,7 @@ def record_kernel_validation(self, results, kernel_report):
save(sage_manifest, sage_record)

def prepare_tools(self):
uv_spec = self.versions['uv']
if self.system == 'Windows':
from .uv_bootstrap import prepare_windows
uv = prepare_windows(self.root, uv_spec, self.network, self.env, self.fetch)
else:
archive = self.root / 'downloads' / 'uv.tar.gz'
from .system import bootstrap_root
previous = bootstrap_root(self.root) / ('uv-' + uv_spec['version'] + '.tar.gz')
if not archive.exists() and previous.is_file() and digest(previous) == uv_spec['sha256']:
archive.parent.mkdir(parents=True, exist_ok=True)
try:
os.link(previous, archive)
except OSError:
shutil.copyfile(previous, archive)
self.fetch(uv_spec['url'], archive, uv_spec['sha256'])
unpack_folder(archive, self.root / 'tools', 'uv-x86_64-unknown-linux-gnu')
uv = self.root / 'tools' / 'uv-x86_64-unknown-linux-gnu' / 'uv'
return uv
return prepare_uv(self.system, self.root, self.versions['uv'], self.network, self.env, self.fetch)

def execute(self):
if self.int8_check_first:
Expand Down Expand Up @@ -2618,10 +2627,12 @@ def curl_progress(message):
except BlockingIOError:
prepare_end(ui, False)
print('Setup, testing or generation is using this installation/GPU lock. Retry after it finishes.', file=sys.stderr)
from .locking import lock_holders
owners = lock_holders([Path(value['root']) / 'setup.lock', os.environ.get('FREEVIDEO_LOCK_PATH', str(Path(value['root']) / 'engine.lock'))])
if owners:
print('Processes with open lock handles: %s. Stop the old task before retrying; do not delete lock files.' % ', '.join(map(str, owners)), file=sys.stderr)
from .locking import describe_lock_holders
try:
for line in describe_lock_holders([Path(value['root']) / 'setup.lock', os.environ.get('FREEVIDEO_LOCK_PATH', str(Path(value['root']) / 'engine.lock'))]):
print(line, file=sys.stderr)
except Exception:
pass
return 1
except (OSError, ValueError) as error:
prepare_end(ui, False)
Expand Down
4 changes: 4 additions & 0 deletions freevideo_engine/chain_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -726,6 +726,10 @@ def _seams_kept(self, snapshot, reasons=None):
kept += 1
else:
reason = self._lead_failure(identity, row['index'], row['output'], neighbour['output'], True)
told = row['continuation'].get('record_not_kept') if isinstance(row['continuation'], dict) else None
if reason == 'no_record' and isinstance(told, str):
# The engine already said when the block finished that it could not keep this record.
reason = 'not_saved:' + told[:64]
if reason is None:
kept += 1
elif reasons is not None:
Expand Down
3 changes: 3 additions & 0 deletions freevideo_engine/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@ def main():
help='With --continue-from: also give the model the first frame of the whole film as a '
'reference picture. Reduces drift over many parts; holds framing and motion close to '
'the opening, so leave it off when later parts should move away from it')
generate.add_argument('--ten-bit', choices=('h264', 'hevc'),
help='Also write a 10-bit copy next to the MP4 (name ends -10bit), from the decoder\'s own '
'frames. The usual 8-bit file is unchanged. Direct generation only.')
generate.add_argument('--hide-tail', action='store_true',
help='Generate and discard 17 extra frames for dialogue segments.')
generate.add_argument('--voice-reference', action='store_true',
Expand Down
22 changes: 22 additions & 0 deletions freevideo_engine/comfy_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,9 @@ def _engine_version():


# Names emitted by both decoders; forecast keys use the same suffixes.
WORK_LABELS = {'encoding': 'Encoding text and images', 'load': 'Preparing offloaded weight storage',
'decode': 'Decoding the video'}
WORK_UNITS = ('bytes', 'layers', 'files', 'frames', 'chunks')
_DECODE_PARTS = {'Loading video VAE': 'vae_load', 'Decoding the video': 'video_decode',
'Loading and decoding audio': 'audio_load_decode', 'Saving MP4 and audio': 'encode'}

Expand Down Expand Up @@ -1124,6 +1127,15 @@ def progress_message(event):
return dict(label='Decoding the video', timing_phase='decode', phase='decode', stage='video',
done=event.get('done'), total=event.get('total'), unit='frames',
elapsed_seconds=event.get('elapsed_seconds'))
if name == 'work_progress':
# Real completed counts inside a long step that otherwise prints nothing.
group, stage = event.get('group'), event.get('stage')
done, total, unit = event.get('done'), event.get('total'), event.get('unit')
if (group not in WORK_LABELS or type(done) is not int or type(total) is not int
or not 0 <= done <= total or total < 1 or unit not in WORK_UNITS):
return None
return dict(label=WORK_LABELS[group], timing_phase=group, phase=group, stage=stage,
done=done, total=total, unit=unit, elapsed_seconds=event.get('elapsed_seconds'))
if name == 'media_encode_phase':
return {'label': str(event.get('phase', 'Encoding input media')), 'timing_phase': 'encoding'}
if name == 'media_encode_progress':
Expand Down Expand Up @@ -1419,6 +1431,9 @@ def check_ending():
command += ['--' + field.replace('_', '-'), str(reserve)]
if preview:
command.append('--preview')
elif continue_from is None:
from .output_settings import command_option
command += command_option(root)
if hide_tail and '--hide-tail' not in command:
command.append('--hide-tail')
state = {'status': 'starting', 'geometry': canvas, 'seed': seed, 'installation': str(root),
Expand Down Expand Up @@ -1718,6 +1733,13 @@ def asset_progress(message):
# The ending was encoded by the engine, which is where the real counters come from.
state['continuation'] = dict(receipt)
state['continuation']['audio_lead_in_samples'] = receipt.get('audio_lead_in_samples', 0)
# When the engine could not keep this block's seam record, say why, so a join that
# later finds no record can name it. Only a short word, never a path or a text.
stored = engine.get('continuation_store')
if isinstance(stored, dict) and stored.get('saved') is False:
why = stored.get('reason') or stored.get('error')
if isinstance(why, str) and why.isascii() and why.isidentifier() and len(why) <= 64:
state['continuation']['record_not_kept'] = why
if comfy_metadata:
# Dropping the video on the canvas restores this graph, as with ComfyUI's own video nodes.
# Written before the result cache records the file's size and hash.
Expand Down
14 changes: 12 additions & 2 deletions freevideo_engine/comfy_continuation.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,18 @@ def audio_lead_in(video, previous=None):
return audio_lead_in_checked(video, previous)[0]


def _record_file_exists(path, directory):
"""Whether the record file of this video is on disk, however it reads. Only asked after a failed read."""
try:
from .continuation_store import _path, video_id
return _path(video_id(path), directory).is_file()
except Exception:
return False


def audio_lead_in_checked(video, previous=None):
"""(the seam audio, None) as audio_lead_in reads it, or (None, why it could not be read): no_video,
no_record, old_record, source_changed or read_failed:<error type>. Never a path or a text."""
no_record, unreadable_record, old_record, source_changed or read_failed:<error type>. Never a path or a text."""
try:
import numpy as np
from av import FFmpegError
Expand Down Expand Up @@ -161,7 +170,8 @@ def located(name):
return None, 'no_video'
value = load(path, directory=directory)
if value is None:
return None, 'no_record'
# load() also answers None for a record file that is there but fails to read or check.
return None, 'unreadable_record' if _record_file_exists(path, directory) else 'no_record'
if 'audio_lead_in' not in value:
return None, 'old_record'
if previous is not None:
Expand Down
Loading