Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
5f15a4e
Shorten Windows uv source build paths
KBRASK Oct 20, 2026
b1e9079
test: cover feedback cause extraction and redaction
KBRASK Oct 20, 2026
5dd7717
Prompt enhancement retries once when a rewrite leaves out an image re…
KBRASK Oct 20, 2026
1a011aa
Test the Windows setup memory guard below, at and above each threshold
KBRASK Oct 20, 2026
2c03c29
Bound generation lease waits and classify engine contention
KBRASK Oct 20, 2026
0c361b3
Give the failure card a fix for first/last frames combined with refer…
KBRASK Oct 20, 2026
03b0bf7
Tell the user when a full media list stops Generate with AI
KBRASK Oct 20, 2026
686b516
Cover the host-graph copy fallback in the creation panel, editor entr…
KBRASK Oct 20, 2026
756f8fd
Name the stage a failed video worker stopped in, not the last plannin…
KBRASK Oct 20, 2026
12e8a9b
Chat: stop key while waiting for a reply and a stage line beside the …
KBRASK Oct 20, 2026
e020f39
Cache the launcher QR rows again and resolve the LAN password module …
KBRASK Oct 20, 2026
da279ed
Show the failed-install update message in the interface language
KBRASK Oct 20, 2026
3d640fa
Use a formal, consistent register for interface text
KBRASK Oct 20, 2026
70f523d
Run installer Git from a short view with core.longpaths set on every …
KBRASK Oct 21, 2026
40f93a1
Run Git in a short real folder for deep Windows installs; log why a s…
KBRASK Oct 21, 2026
c40c6a7
Fall back to LOCALAPPDATA for the short Git work folder
KBRASK Oct 21, 2026
6009e85
Pass Diffusers patches to git apply through stdin, not a long-path-pr…
KBRASK Oct 21, 2026
a411691
Give uv venv and python commands the short view; unpack archives thro…
KBRASK Oct 21, 2026
cec97ba
Download into a short folder where curl cannot name a deep target
KBRASK Oct 21, 2026
b9bd89b
Read a deep checkout's tree from its commit object when Git rejects t…
KBRASK Oct 21, 2026
fc040db
Revert the consistent register change to interface text
KBRASK Oct 21, 2026
7fbfc93
Give the setup children that run uv the short path view
KBRASK Oct 21, 2026
a912efd
Run every installer Python child and a deep online installation throu…
KBRASK Oct 21, 2026
9ff60d0
Keep the original interface wording and drop only second-person and f…
KBRASK Oct 21, 2026
7d16d6a
Editor names known causes when subtitles, the subtitle download or a …
KBRASK Oct 21, 2026
7efc9e0
Library: say how many creation records could not be read
KBRASK Oct 21, 2026
bb3cc82
A failed part says so in one line and a missing media file is named
KBRASK Oct 21, 2026
1cd6d3a
Keep failure reasons for full disks and unknown export errors
KBRASK Oct 21, 2026
0bf3bc1
Show the engine's English failure reason on Autopilot cards
KBRASK Oct 21, 2026
1e5e68b
Unmatched launcher failures give a next step, and path, installer mem…
KBRASK Oct 21, 2026
839d4cc
A too-old NVIDIA driver gets its own notice with the installed version
KBRASK Oct 21, 2026
2309bc6
Name the reference-image waits: upload and read stages in image gener…
KBRASK Oct 21, 2026
6e5d9b0
Read LoRA factors named without .weight and explain acceleration LoRAs
KBRASK Oct 21, 2026
cbd1e3c
Let prompt enhancement skip audio and connected media and read refere…
KBRASK Oct 21, 2026
efa51e2
Name unreachable model sources and give slow networks a second chance
KBRASK Oct 21, 2026
c83ab42
Open Setup with a notice when the saved ComfyUI folder is unavailable
KBRASK Oct 21, 2026
a29b7f4
Show first-time setup and the one slow-run cause on the range line
KBRASK Oct 21, 2026
eb4f6f5
Chat: hidden tab keeps reading slowly and announces in its title, sta…
KBRASK Oct 21, 2026
2183333
Chat: a hidden tab's prefix is set once even when the page has no title
KBRASK Oct 21, 2026
8533de0
Mac encoder: slice limits follow the live encoder pool, and projectio…
KBRASK Oct 21, 2026
cf126aa
Say what a queued video waits for and when it starts
KBRASK Oct 21, 2026
7741d0e
Keep the Autopilot entry when its check fails
KBRASK Oct 21, 2026
babd277
Show prompt save/load failures and tell an unchecked source video fro…
KBRASK Oct 21, 2026
e311cd0
Name each source and reason when every download source fails
KBRASK Oct 21, 2026
3aa2435
Wording: replace the unclear 'FreeVideo tests' phrasing with 'tested'…
KBRASK Oct 21, 2026
3ef2cac
Show Queued on the export button while an export waits its turn
KBRASK Oct 21, 2026
76f1858
Keep an unreadable launcher settings file aside; startup survives unr…
KBRASK Oct 21, 2026
cce3f4a
Mac: limit only the wait for the new app to appear, never replace a r…
KBRASK Oct 21, 2026
d70120a
Keep resident encoder/engine models across requests without costing l…
KBRASK Oct 21, 2026
e175f03
Give the image generation models a download list of their own
KBRASK Oct 21, 2026
ff735cd
Check MLX image attention against Torch once and report why it fell back
KBRASK Oct 21, 2026
5a8ecfc
Placing a picture from the image history keeps the generation tool, s…
KBRASK Oct 21, 2026
77ba9f9
Creation panel: keys stay inside the panel
KBRASK Oct 21, 2026
0883553
Agent: multi-shot films write no per-shot music by default
KBRASK Oct 21, 2026
5001732
Let a Windows request spend its default RAM growth reserve before the…
KBRASK Oct 21, 2026
0848a94
Launcher: a missing prompt model list no longer fails the whole page
KBRASK Oct 21, 2026
13c7a9f
Give the full-disk failure reason an English line on Autopilot cards
KBRASK Oct 21, 2026
43ef727
Release 0.4.18
KBRASK Oct 21, 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
4 changes: 2 additions & 2 deletions .github/ISSUE_TEMPLATE/feature_request.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
name: Suggestion / 建议
description: Tell us what would help / 说说你希望改进什么
description: Tell us what would help / 说说希望改进什么
body:
- type: textarea
id: idea
attributes:
label: What would you like to do? / 你希望实现什么?
label: What would you like to do? / 希望实现什么?
validations:
required: true
2 changes: 1 addition & 1 deletion freevideo_engine/agent/builtin_director.py
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ def judge(shot_view, sheets, *, ask):
return Decision('keep', shot['id'], '保留已选定的版本。')
if shot.get('state') in ('waiting', 'shooting'):
return Decision('wait', shot['id'], '等待这一镜的各版完成。')
uncertain = Decision('ask_user', shot['id'], '拿不准,请你挑一版。')
uncertain = Decision('ask_user', shot['id'], '拿不准,请挑一版。')
takes = _selectable(shot)
if not takes:
return uncertain
Expand Down
4 changes: 2 additions & 2 deletions freevideo_engine/agent/cast.py
Original file line number Diff line number Diff line change
Expand Up @@ -807,7 +807,7 @@ def _land_image(self, identity, generating, hidden, rows):
code = (row.get('error') or {}).get('code') if row else 'not_found'
code = code or (row['status'] if row else 'not_found')
detail = errors.image_failure(code, (row.get('error') or {}).get('report_id') if row else None)
failure = failure or dict(code=code, job=job, error_zh=detail['reason']['zh'], message=(
failure = failure or dict(code=code, job=job, error_zh=detail['reason']['zh'], error_en=detail['reason']['en'], message=(
'The generation was cancelled.' if row and row['status'] == 'cancelled'
else detail['reason']['en']), **detail)
continue
Expand Down Expand Up @@ -843,7 +843,7 @@ def _land(self, identity, generating, hidden, rows):
if row is None or row['status'] != 'succeeded':
code = (row.get('error') or {}).get('code') if row else 'not_found'
failure = failure or dict(code=code or row['status'], job=job,
error_zh=errors.failure_reason((row.get('error') or dict(code=code or row['status'])) if row else dict(code=code)), message=(
**errors.reasons((row.get('error') or dict(code=code or row['status'])) if row else dict(code=code)), message=(
'The generation was cancelled.' if row and row['status'] == 'cancelled'
else 'The reference clip did not finish.'))
continue
Expand Down
4 changes: 2 additions & 2 deletions freevideo_engine/agent/drama.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@
'lines': [{'who': 'lin', 'line': '再见了。', 'how': 'says softly'}],
'sound': 'The hum of the subway, rain tapping on glass.'},
],
'music': 'A sparse, slow piano figure over soft sustained strings.',
'review': {'min_score': 7, 'retakes': 1, 'frames': 6},
'transition': 'crossfade', 'transition_seconds': .4,
}
Expand Down Expand Up @@ -258,7 +257,8 @@ def prompt(play, shot):
scene = ' '.join(shot.get(k) or '' for k in ('framing', 'action', 'camera')) + ' ' + (play.get('style') or '')
parts.append('No subtitles or captions.' if SHOWN_TEXT.search(scene) else 'No subtitles, no on-screen text.')
sound = shot.get('sound') or 'Natural ambience of the scene.'
music = shot.get('music') or play.get('music') or 'N/A'
# Only a shot's own music is written; a score for the whole film is imported and laid on the sound track.
music = shot.get('music') or 'N/A'
return ('integrated_multimodal_description: ' + ' '.join(p for p in parts if p) +
'\n\noverall_soundscape: ' + sound + '\n\nnon_diegetic_music: ' + music)

Expand Down
87 changes: 81 additions & 6 deletions freevideo_engine/agent/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
'needs_confirmation': (409, False, 'The user must approve this operation first.', 'Wait with freevideo_wait (no id) for the user\'s answer; once approved, repeat the same call unchanged.'),
'declined_by_user': (409, False, 'The user declined this operation.', 'Do not repeat this call; change the plan or ask the user what they want instead.'),
'stopped_by_user': (409, False, 'The user stopped this session.', 'Wait for the user to continue the session; do not retry or start a new one.'),
'turn_abandoned': (409, False, 'The user gave up on this turn.', 'Stop working on it. Wait for the next message from the user; do not retry.'),
'reshoot_limit': (409, False, 'This shot reached its generation limit.', 'Choose the best existing take, rewrite the shot, or ask the user.'),
'production_limit': (409, False, 'This film reached its child job limit.', 'Choose existing takes or ask the user how to reduce the film.'),
'managed_by_production': (409, False, 'This job belongs to a film and cannot be resumed on its own.', 'This take belongs to a film; reshoot the shot or wait for the user to continue.'),
Expand Down Expand Up @@ -131,6 +132,8 @@
'显卡暂时用不了,稍后再试。'),
'worker_exit': ('Image generation stopped unexpectedly. Try again.', '图像生成意外停止了,请再试一次。'),
'timeout': ('This image took too long and was stopped. Try again.', '这一张用时太长,已停止。请再试一次。'),
'disk_full': ('Saving the image failed: the disk is full. Free some space, then generate again.',
'保存图片时出错:磁盘空间不够,空出空间后再生成。'),
'generation_failed': ('Image generation ran into an error. Try again.', '图像生成出错了,请再试一次。'),
'invalid_media': ('The reference image could not be read. Choose another one.',
'参考图读不出来,请换一张。'),
Expand All @@ -148,7 +151,7 @@ def image_failure(code, report_id=None):
valid = isinstance(report_id, str) and re.fullmatch(r'[0-9a-f]{32}', report_id)
return dict(reason=dict(en=en, zh=zh), report_id=report_id if valid and code not in (
'busy', 'cancelled', 'invalid_request', 'invalid_media', 'invalid_prompt', 'invalid_dimensions',
'invalid_seed', 'too_many_images') else None)
'invalid_seed', 'too_many_images', 'disk_full') else None)


# Recovery is a function of the committed task and phase, not just an error code.
Expand All @@ -171,6 +174,7 @@ def image_failure(code, report_id=None):
'timeout': IMAGE_REASONS['timeout'][0],
'generation_failed': IMAGE_REASONS['generation_failed'][0],
'worker_exit': 'The image worker stopped before finishing this picture.',
'disk_full': IMAGE_REASONS['disk_full'][0],
'cancelled': 'The picture job was cancelled.',
}
for _code, _message in IMAGE_ERRORS.items():
Expand Down Expand Up @@ -214,6 +218,8 @@ def recovery(code, *, kind=None, stage='request', resumable=True):
return SESSION_GONE[1]
if kind == 'image' and code == 'pack_missing':
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 == '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 Expand Up @@ -282,6 +288,7 @@ def safe_details(value):
'join_failed': '片段已生成,但拼接成片时出错',
'invalid_output': '生成的结果无法读取或保存',
'storage_unavailable': '任务记录或素材无法写入磁盘',
'disk_full': '磁盘空间不够,图片没能保存',
'submit_interrupted': '提交生成时服务中断,尚未确认任务是否开始',
'timeout': '生成等了太久没有结果',
'invalid_media': '参考素材无法读取',
Expand All @@ -299,6 +306,54 @@ def safe_details(value):
'generation_failed': '生成没有完成',
'internal_error': '没有完成',
}
FAILURE_REASONS_EN = {
'lan_unavailable': 'Phone access requires FreeVideo installed with the launcher',
'out_of_memory': 'Graphics or system memory ran out during generation',
'insufficient_memory': 'There is not enough free graphics or system memory for this generation',
'no_memory': 'There is not enough free graphics or system memory for this picture',
'ram_space': 'System memory ran out while loading the model',
'model_missing': 'A required model is not installed yet',
'pack_missing': 'The image model is not installed yet',
'not_ready': 'The generation environment is not installed yet',
'cancelled': 'Generation was stopped',
'stopped_by_user': 'Generation was stopped',
'interrupted': 'Generation was interrupted',
'worker_exit': 'Picture generation was interrupted',
'closed': 'The picture generation service has closed',
'image_unavailable': 'The picture generation service has not started yet',
'vlm_unavailable': 'The model that reads pictures has not started yet',
'vlm_failed': 'The picture could not be read',
'vlm_invalid_json': 'The reply could not be read',
'context_length': 'The description and reference pictures are too long',
'dependency_failed': 'A task this one depends on did not finish',
'lost': 'The generation service no longer has the result of this task',
'unknown_job': 'The picture generation service cannot find this task',
'not_found': 'This task or media file cannot be found',
'source_missing': 'The previous video needed to continue is no longer available',
'chain_missing': 'The generation record needed to continue is no longer available',
'chain_unavailable': 'The part-by-part generation service has not started yet',
'continuation_unavailable': 'The current generation mode cannot continue a video',
'join_failed': 'The parts were generated, but joining them into one video failed',
'invalid_output': 'The result could not be read or saved',
'storage_unavailable': 'The task record or media could not be written to disk',
'disk_full': 'Saving the image failed: the disk is full',
'submit_interrupted': 'The service stopped during submission, so it is not confirmed whether the task started',
'timeout': 'Generation took too long without a result',
'invalid_media': 'The reference media could not be read',
'clip_unreadable': 'The generated character reference video could not be read',
'invalid_request': 'The generation settings are outside the supported range',
'invalid_drama': 'The short film settings are invalid',
'reshoot_limit': 'This shot has reached its generation limit',
'production_limit': 'This short film has reached its task limit',
'export_failed': 'Exporting the video failed',
'invalid_prompt': 'The picture description is empty or too long',
'invalid_dimensions': 'The picture size is outside the supported range',
'too_many_images': 'There are more reference pictures than supported',
'chain_too_long': 'The continued video has more parts than supported',
'chain_rejected': 'The generation service cannot use these settings',
'generation_failed': 'Generation did not finish',
'internal_error': 'Did not finish',
}
FAILURE_MESSAGES_ZH = {
'The previous video is too short to continue; it needs to be at least 1.7 s long.': '接续的上一段视频太短,至少需要 1.7 秒',
'The previous video has no usable continuation data. Generate it again with this version of FreeVideo.': '上一段视频缺少可用的接续数据',
Expand All @@ -309,23 +364,43 @@ def safe_details(value):
}


def failure_reason(error):
if isinstance(error.get('error_zh'), str) and error['error_zh'].strip():
return error['error_zh']
def _failure_line(error):
lines = str(error.get('message') or '').strip().splitlines()
line = lines[0] if lines else ''
if line.startswith('FreeVideo generation failed') and len(lines) > 1:
line = lines[1].strip()
line = re.sub(r'^(?:[\w.]*Error|[\w.]*Exception):\s*', '', line)
return re.sub(r'^(?:[\w.]*Error|[\w.]*Exception):\s*', '', line)


def failure_reason(error):
if isinstance(error.get('error_zh'), str) and error['error_zh'].strip():
return error['error_zh']
line = _failure_line(error)
if line in FAILURE_MESSAGES_ZH:
return FAILURE_MESSAGES_ZH[line]
if line.startswith(('CUDA out of memory', 'MPS backend out of memory')):
return FAILURE_REASONS_ZH['out_of_memory']
return FAILURE_REASONS_ZH.get(error.get('code'), '没有完成')


# The same lookup in English for the page: the specific sentences are already written for people.
def failure_reason_en(error):
if isinstance(error.get('error_en'), str) and error['error_en'].strip():
return error['error_en']
line = _failure_line(error)
if line in FAILURE_MESSAGES_ZH:
return line
if line.startswith(('CUDA out of memory', 'MPS backend out of memory')):
return FAILURE_REASONS_EN['out_of_memory']
return FAILURE_REASONS_EN.get(error.get('code'), 'Did not finish')


def reasons(error):
return dict(error_zh=failure_reason(error), error_en=failure_reason_en(error))


def with_reason(error):
return dict(error, error_zh=failure_reason(error)) if error else error
return dict(error, **reasons(error)) if error else error


def build(code, *, details=None, request=None, validation=False, message=None, next_action=None,
Expand Down
23 changes: 19 additions & 4 deletions freevideo_engine/agent/guide.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,21 @@
],
'sound': [
'overall_soundscape: 1-4 sentences of ambience, action sounds and breaths; N/A only for silence.',
'non_diegetic_music: 1-3 sentences of instruments, tempo and dynamics, or N/A.',
'non_diegetic_music: 1-3 sentences of instruments, tempo and dynamics, or N/A. By default a long video or a '
'film of several shots has no music: write N/A in every shot, so each has only ambience, action sounds and '
'dialogue, and do not add music on your own. Whatever the case, never write a different non_diegetic_music '
'for each shot: the shots are generated separately, so the music would not be continuous and would jump at '
'every cut. Write music in a shot only when the user asks for it there.',
'When the user wants music over the film, keep every shot at N/A and lay one score on one sound track after '
'the cut is joined. If the user gives a music file, import it. If there is none, compose it yourself: write '
'a program that synthesizes one audio file (WAV, using the standard wave module and numpy or whatever is '
'already installed, nothing to install) as long as the finished cut, with its sections, changes and ending '
'on the cut points. Import it with POST /imports (MCP freevideo_import_media, CLI imports add), wait until it '
'is ready, and insert import:<id> at frame 0 of the project. There is no volume or fade operation, so build '
'them into the file: keep the music clearly quieter than the dialogue and ambience, lower it further while '
'someone speaks (take the line times from your own screenplay and shot lengths, or run a listen job in align '
'mode to time them), and fade it in at the start and out at the end. The audio goes onto a free sound track by '
'itself; read the project afterwards and check that one clip covers the whole film.',
],
'references': [
'Tags are numbered per kind in reference order: <Picture 1>, <Picture 2>, <Video 1>, <Audio 1>. '
Expand Down Expand Up @@ -355,6 +369,7 @@
('POST', '/sessions/{id}/events'): '向用户发送消息或计划;操作步骤由服务端记录',
('POST', '/sessions/{id}/stop'): '只有页面用户可以停止工作会话',
('POST', '/sessions/{id}/resume'): '只有页面用户可以继续工作会话',
('POST', '/sessions/{id}/stage'): '说明你正在做回答的哪个阶段;页面用自己的话显示,你开口或开始任务后自动清除',
('GET', '/confirmations'): '查看哪些 Agent 操作需要等待用户确认',
('PUT', '/confirmations'): '只有页面用户可以修改需要确认的操作',
}
Expand Down Expand Up @@ -480,7 +495,7 @@ def connection_guide(root, base, lang=None, offers=None, resumed=False):
'连接后先调一次等待接口 GET ' + base + PREFIX + '/inbox?wait=50(MCP 用不带 id 的 freevideo_wait),'
'回应里的 session 就是用户现在所在的对话(用户新开或换到另一段对话时它会变,话总是发到最近一次等待给的那个 session);往 POST ' + base + PREFIX + '/sessions/{session}/events 发 '
'{"type":"message","text":"…"}(MCP 用 freevideo_session_post)。在这里先只说一句话,不超过 20 个字,'
'不列条目、不举例(页面上已经有例句),例如“连上了。告诉我想做什么视频。”'
'不列条目、不举例(页面上已经有例句),例如“连上了。请说明想做什么视频。”'
'你自己窗口里的那段介绍照常说,但它不是这一轮的结尾:说完紧接着去调等待接口。'
'然后守在这段对话里:再调等待接口,它最多约 50 秒返回;"again": true 表示用户还没说话,'
'马上再调下一次,一次接一次,不限次数。没有消息不是结束的理由:不要结束这一轮回复,'
Expand All @@ -498,7 +513,7 @@ def connection_guide(root, base, lang=None, offers=None, resumed=False):
'without id). Its "session" is the conversation the user is in (it changes when the user opens or returns to another one; always post to the session the latest wait named); POST ' + base + PREFIX + '/sessions/{session}/events '
'with {"type":"message","text":"..."} (MCP: freevideo_session_post). Say only one short sentence '
'there first, at most 12 words, no list and no examples (the page already shows examples), for example '
'"Connected. Tell me what video you want.", in the language the user writes to you in. Give the introduction in your own window as usual, but it '
'"Connected. Describe the video you want.", in the language the user writes to you in. Give the introduction in your own window as usual, but it '
'does not end your turn: right after it, call the wait. Then stay in this conversation: call the wait again; it answers within about 50 seconds, and '
'"again": true means the user has not spoken yet, so call it again at once, time after time, with no '
'limit. Nothing new is not a reason to finish: do not end your reply and do not stop to ask the '
Expand All @@ -515,7 +530,7 @@ def connection_guide(root, base, lang=None, offers=None, resumed=False):
'Stop only when the user clearly tells you to.',
rules=RULES_ZH if zh else RULES,
fallback='如果你无法执行命令、读取本地文件或连接到这台电脑,请如实告诉用户:'
'“我连接不到你电脑上的 FreeVideo,请改用安装在这台电脑上的 AI 助手。”'
'“连接不到您电脑上的 FreeVideo,请改用安装在这台电脑上的 AI 助手。”'
'用中文说明;如果用户改用其他语言,就跟随用户。不要另装程序来绕过这个限制。' if zh else
'If you cannot run commands, read local files, or reach this computer, tell the user honestly '
'in their language: I cannot connect to FreeVideo on your computer; please use an AI assistant '
Expand Down
Loading