From 5c77402b337c0dbe1d5701db109c7f88c7aa8d5b Mon Sep 17 00:00:00 2001 From: Bowen Xue Date: Thu, 15 Oct 2026 15:53:04 +0000 Subject: [PATCH 01/21] Clarify refinement step limits and unavailable time estimates --- README.md | 2 +- README.zh-CN.md | 4 +- docs/Mac.md | 2 +- docs/Mac.zh-CN.md | 2 +- web/freevideo.js | 2 +- web/library.js | 14 +++--- web/result_actions.js | 14 +++--- web/sampling_effort.js | 2 +- web/studio.js | 98 +++++++++++++++++++++--------------------- 9 files changed, 69 insertions(+), 71 deletions(-) diff --git a/README.md b/README.md index 7c841d18..926a950c 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ It coordinates VRAM, system memory and disk, adapting weight placement, compute - **Low-memory inference**: Weight streaming, asynchronous prefetching and chunked computation keep peak memory low, enabling inference with as little as 8 GB of VRAM and 16 GB of RAM. - **Multimodal inputs**: Text prompts, first and last frames, and image, video and audio references. - **Community LoRAs**: Use MiniMax H3 LoRAs in your workflow. See [examples](docs/LoRA.md). -- **ComfyUI integration**: A dedicated creative workspace inside ComfyUI that supports two-pass sampling and batch generation and keeps a history of past creations. For finer control, switch to the node view to add LoRAs or customize the workflow. +- **ComfyUI integration**: A dedicated creative workspace inside ComfyUI that supports two-step generation and batch generation and keeps a history of past creations. For finer control, switch to the node view to add LoRAs or customize the workflow. - **One-click deployment**: The Windows, Mac and Linux launchers set up ComfyUI, the runtime environment and the models, reuse existing models, and support offline installation. ## Getting Started diff --git a/README.zh-CN.md b/README.zh-CN.md index 43d10c0a..37b16585 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -20,7 +20,7 @@ https://github.com/user-attachments/assets/ecda7d0d-7fbe-4e0c-8c29-8f3315bafc15 ## 更新动态 - **2026-10-13** · **[v0.4.0](https://github.com/FlashML-org/FreeVideo/releases/tag/v0.4.0):Agent 全自动生成、分钟级长视频和剪辑视图。** 连接 Claude Code、Codex 等 AI Agent,让它全自动生成视频;一条视频可以生成到一分钟以上;在时间线上剪辑。 -- **2026-10-07** · **[v0.3.2](https://github.com/FlashML-org/FreeVideo/releases/tag/v0.3.2):先出预览。** 先看半分辨率预览,满意的再继续二采。 +- **2026-10-07** · **[v0.3.2](https://github.com/FlashML-org/FreeVideo/releases/tag/v0.3.2):先出预览。** 先看半分辨率预览,满意的再继续生成。 - **2026-10-07** · **[v0.3.0](https://github.com/FlashML-org/FreeVideo/releases/tag/v0.3.0):更快的 int8 模型。** GeForce 和 RTX 30 系显卡改用 int8,RTX 3090 生成 5 秒视频快约 2.3 倍。 - **2026-10-06** · **[作品展示](https://freevideo-community.pages.dev/#gallery)上线。** 观看 FreeVideo 生成的 20 秒视频,以及四档质量的并排对比。 - **2026-10-06** · **[v0.2.3](https://github.com/FlashML-org/FreeVideo/releases/tag/v0.2.3):视频自带工作流。** 把 FreeVideo 生成的视频拖到 ComfyUI 画布上,即可还原提示词、种子和全部参数,之前的视频也能补上。 @@ -39,7 +39,7 @@ FreeVideo 是面向消费级显卡的 MiniMax H3 本地推理引擎,基于 [Op - **低显存推理**:通过权重流式加载、异步预取与分块计算降低峰值显存,最低只需 8GB 显存和 16GB 内存。 - **多模态输入**:支持文本、首帧、尾帧,以及图像、视频、音频参考输入。 - **社区 LoRA**:支持在工作流中使用 MiniMax H3 社区 LoRA。[查看效果对比](docs/LoRA.zh-CN.md)。 -- **ComfyUI 集成**:在 ComfyUI 中提供专门的创作面板,支持二次采样和批量生成,并可浏览历史作品;需要更精细的控制时,可切换到节点视图,接入 LoRA 或自定义工作流。 +- **ComfyUI 集成**:在 ComfyUI 中提供专门的创作面板,支持两步生成和批量生成,并可浏览历史作品;需要更精细的控制时,可切换到节点视图,接入 LoRA 或自定义工作流。 - **一键部署**:Windows、Mac 和 Linux 启动器自动完成 ComfyUI、运行环境与模型的部署,可复用已有模型,并支持离线安装。 ## 开始使用 diff --git a/docs/Mac.md b/docs/Mac.md index b63cc83a..71c3980a 100644 --- a/docs/Mac.md +++ b/docs/Mac.md @@ -26,7 +26,7 @@ This approves FreeVideo only. It doesn't turn off Gatekeeper or change your othe - Before each stage, attention head groups, chunk sizes and the number of resident transformer layers are set from the unified memory available at that moment. - Macs download ConvRot int8 weights. On Apple M5 and newer, the large projections run as int8 on the GPU's Metal 4 tensor units; earlier Macs decode them to BF16 on the GPU as each layer loads. - Layers that are not kept in memory stream from disk with read-ahead. -- Text prompts, first-frame input and two-pass sampling are supported. +- Text prompts, first-frame input and two-step generation are supported. ## Generation time diff --git a/docs/Mac.zh-CN.md b/docs/Mac.zh-CN.md index eb406e06..49531d0b 100644 --- a/docs/Mac.zh-CN.md +++ b/docs/Mac.zh-CN.md @@ -26,7 +26,7 @@ Mac 预览版暂未进行 Apple 公证,因此首次打开时 macOS 会拦截 - 每个阶段开始前,根据当前可用的统一内存确定注意力头分组、分块大小和常驻的 Transformer 层数。 - Mac 下载 ConvRot int8 权重。M5 及更新的芯片在 GPU 的 Metal 4 张量单元上以 int8 计算大矩阵;更早的芯片在每层加载时于 GPU 上解码为 BF16 计算。 - 未常驻内存的层从磁盘流式加载并提前预取。 -- 支持文本、首帧输入和二次采样。 +- 支持文本、首帧输入和两步生成。 ## 生成时间 diff --git a/web/freevideo.js b/web/freevideo.js index e11c54be..5957ada0 100644 --- a/web/freevideo.js +++ b/web/freevideo.js @@ -508,7 +508,7 @@ function resultPanel(node) { stats.hidden = !!value.result_cache_hit; const number = (value, scale, unit) => Number.isFinite(value) && value >= 0 ? `${(value / scale).toFixed(1)} ${unit}` : "—"; const unified = value.memory_model === 'unified'; - for (const [label, shown] of [[text("Sampling", "采样"), number(value.sample_seconds, 1, "s")], [text("Request total", "请求总计"), number(value.request_seconds, 1, "s")], [unified ? text("Unified memory", "统一内存总量") : text("VRAM peak", "显存峰值"), number(unified ? value.unified_total_bytes : value.vram_peak_bytes, 2 ** 30, "GiB")], [unified ? text("Process RAM peak", "进程内存峰值") : text("RAM peak", "内存峰值"), number(value.ram_peak_bytes, 2 ** 30, "GiB")]]) { + for (const [label, shown] of [[text("Generation", "生成用时"), number(value.sample_seconds, 1, "s")], [text("Total time", "总用时"), number(value.request_seconds, 1, "s")], [unified ? text("Unified memory", "统一内存总量") : text("Peak VRAM", "最高显存"), number(unified ? value.unified_total_bytes : value.vram_peak_bytes, 2 ** 30, "GiB")], [unified ? text("Process peak RAM", "进程最高内存") : text("Peak RAM", "最高内存"), number(value.ram_peak_bytes, 2 ** 30, "GiB")]]) { const stat = el("div", undefined, "fv-stat"); stat.append(el("strong", shown), el("span", label)); stats.append(stat); } const links = el("div", undefined, "fv-links"); diff --git a/web/library.js b/web/library.js index 90f4298c..2570e158 100644 --- a/web/library.js +++ b/web/library.js @@ -175,10 +175,10 @@ export function openLibrary(t, {upscale = null, selectedWork = null, extend = nu const number = (value, scale, unit) => measured(value) ? `${(value / scale).toFixed(1)} ${unit}` : '—'; stats.replaceChildren(); for (const [label, value, scale, unit] of [ - [t('Sampling', '采样耗时'), row.sample_seconds, 1, 's'], - [t('Request total', '请求总计'), row.request_seconds, 1, 's'], - [t('VRAM peak', '显存峰值'), row.vram_peak_bytes, 2 ** 30, 'GiB'], - [t('RAM peak', '内存峰值'), row.ram_peak_bytes, 2 ** 30, 'GiB'], + [t('Generation', '生成用时'), row.sample_seconds, 1, 's'], + [t('Total time', '总用时'), row.request_seconds, 1, 's'], + [t('Peak VRAM', '最高显存'), row.vram_peak_bytes, 2 ** 30, 'GiB'], + [t('Peak RAM', '最高内存'), row.ram_peak_bytes, 2 ** 30, 'GiB'], ]) { const item = el('div', null, 'fv-stat'); item.append(el('strong', number(value, scale, unit)), el('span', label)); stats.append(item); @@ -195,7 +195,7 @@ export function openLibrary(t, {upscale = null, selectedWork = null, extend = nu await upscale(row); if (!disposed && selected === row.video) notice.textContent = t('Queued. It appears in "Creations" when done.', '已加入队列,完成后在“作品”里。'); } catch (error) { - if (!disposed && selected === row.video) notice.textContent = t('Could not start the second pass: ', '无法开始二采:') + error.message; + if (!disposed && selected === row.video) notice.textContent = t('Could not continue: ', '无法继续生成:') + error.message; throw error; } } @@ -217,7 +217,7 @@ export function openLibrary(t, {upscale = null, selectedWork = null, extend = nu : error.message === 'editor-in-use' ? t('Not deleted: these edits use it: ', '未删除:这些剪辑正在使用它:') + (value.kept || []).join(', ') : error.message === 'editor-unavailable' ? t('Not deleted: saved edits could not be checked. Try again in a moment.', '未删除:暂时无法检查已保存的剪辑,请稍后再试。') : error.message === 'continuation-in-use' ? t('Not deleted: a video continuing from it is running or queued. Try again after it finishes.', '未删除:接在这条视频后面的生成正在运行或排队,请等它完成后再试。') - : error.message === 'in-use' ? t('Not deleted: its second pass is running, or the video is still open. Try again in a moment.', '未删除:正在用它跑二采,或视频仍被占用,请稍后再试。') + : error.message === 'in-use' ? t('Not deleted: it is still being generated or the video is open. Try again in a moment.', '未删除:它还在继续生成,或视频仍被占用,请稍后再试。') : t('Could not delete this creation.', '无法删除这条作品。')); } // Other views of the library (the studio's Recent videos) drop it too. @@ -250,7 +250,7 @@ export function openLibrary(t, {upscale = null, selectedWork = null, extend = nu image.dataset.src = videoThumbnailURL(row); image.onerror = () => { image.hidden = true; }; picture.append(image); - if (row.preview) picture.append(el('span', t('First-pass preview', '一采预览'), 'fv-preview-badge')); + if (row.preview) picture.append(el('span', t('Preview', '预览'), 'fv-preview-badge')); // Made by an AI agent: say so, with the name the agent gave the shot. if (row.agent) { const badge = el('span', 'Agent', 'fv-agent-badge'); diff --git a/web/result_actions.js b/web/result_actions.js index e0562482..68d56908 100644 --- a/web/result_actions.js +++ b/web/result_actions.js @@ -29,8 +29,8 @@ export function icon(name) { export function previewBadge(record, t) { const badge = document.createElement('span'); badge.className = 'fv-preview-badge'; const g = record.preview_geometry; - badge.textContent = g?.width ? t(`First-pass preview · ${g.width} × ${g.height}`, `一采预览 · ${g.width} × ${g.height}`) - : t('First-pass preview', '一采预览'); + badge.textContent = g?.width ? t(`Preview · ${g.width} × ${g.height}`, `预览 · ${g.width} × ${g.height}`) + : t('Preview', '预览'); return badge; } @@ -59,10 +59,10 @@ function popover(menu, summary, onClose = () => {}) { // The next step of a first-pass preview. `run` reports its own errors and throws. export function secondPassAction(t, run) { - const button = action('button', t('Run second pass', '继续二采'), 'expand', 'fv-action-primary fv-action-second-pass'); + const button = action('button', t('Continue', '继续生成'), 'expand', 'fv-action-primary fv-action-second-pass'); const label = button.lastElementChild; - button.title = t('Continue from this preview: latent upscale, the second pass and a full-resolution decode.', - '从这个预览接着跑:潜空间放大、二采,再以全分辨率解码。'); + button.title = t('Continue from this preview to the full-resolution video.', + '从这个预览继续生成全分辨率视频。'); button.onclick = async () => { if (button.disabled) return; button.disabled = true; button.setAttribute('aria-busy', 'true'); @@ -70,7 +70,7 @@ export function secondPassAction(t, run) { try { await run(); label.textContent = button.title = t('Queued', '已加入队列'); } catch { button.disabled = false; - label.textContent = button.title = t('Second pass did not start · Retry', '二采没有开始 · 重试'); + label.textContent = button.title = t('Could not continue · Retry', '未能继续生成 · 重试'); } finally { button.removeAttribute('aria-busy'); } }; return button; @@ -86,7 +86,7 @@ function deleteAction(t, remove) { const title = document.createElement('strong'); title.textContent = t('Delete this creation?', '删除这条作品?'); const hint = document.createElement('small'); hint.textContent = t('The video, its generation records and preview data are removed. This cannot be undone.', - '视频、生成记录和一采中间结果都会删除,无法恢复。'); + '视频、生成记录和预览数据都会删除,无法恢复。'); const problem = document.createElement('small'); problem.className = 'fv-confirm-problem'; problem.hidden = true; const row = document.createElement('div'); row.className = 'fv-confirm-actions'; const cancel = action('button', t('Cancel', '取消'), null, 'fv-action-secondary'); diff --git a/web/sampling_effort.js b/web/sampling_effort.js index d7360286..40ffcd9f 100644 --- a/web/sampling_effort.js +++ b/web/sampling_effort.js @@ -111,5 +111,5 @@ export function createSamplingEffort(t, {onChange, onPreview = () => {}}) { : t('Same model. Higher effort uses more sampling steps; results vary by scene.', '使用同一模型,提高档位会增加采样步数;效果因场景而异。'); for (const name of nameButtons) { name.disabled = disabled; name.title = ''; } } - return {element, update, setEstimate(text, title = '') { estimate.textContent = text; estimate.title = title; }, dispose: cancel}; + return {element, update, setEstimate(text, title = '') { estimate.textContent = text; if (title) estimate.title = title; else estimate.removeAttribute('title'); }, dispose: cancel}; } diff --git a/web/studio.js b/web/studio.js index b7d91ad5..732da5ec 100644 --- a/web/studio.js +++ b/web/studio.js @@ -83,8 +83,8 @@ function createChainPartList(t, save, {onShow = () => {}, onHide = () => {}} = { row.saving = null; if (rows.get(row.part.id) !== row) return; row.part = {...row.part, text: before}; draw(row); - flash(row, error.status === 409 ? t('This part has started. Your edit was not saved.', '这一段已开始,修改未保存') - : t('Your edit was not saved.', '修改未保存'), 'error'); + flash(row, error.status === 409 ? t('This part is already generating. Your edit was not saved.', '这一段已在生成,改动没有保存') + : t('Your edit was not saved.', '改动没有保存'), 'error'); } } function open(row) { @@ -128,7 +128,7 @@ function createChainPartList(t, save, {onShow = () => {}, onHide = () => {}} = { // The part began while it was open: the words typed so far cannot reach it. const changed = row.editor.value !== row.part.text; editing = null; row.part = part; draw(row); - if (changed) flash(row, t('This part has started. Your edit was not saved.', '这一段已开始,修改未保存'), 'error'); + if (changed) flash(row, t('This part is already generating. Your edit was not saved.', '这一段已在生成,改动没有保存'), 'error'); return; } if (editing === row) { row.part = {...part, text: row.part.text}; return; } @@ -356,7 +356,7 @@ export function loraPanel(node, panel, {promptNode = null} = {}) { } const add = button(t('Add', '添加'), async () => { await refresh(); // The folder may have changed since the list was last read. - try { const rows = read(); if (rows.length >= 32) throw new Error(t('Use at most 32 LoRAs', '最多使用 32 个 LoRA')); if (!names.length) throw new Error(t('Put LoRA files (.safetensors) in the models/loras folder, then choose "Add".', '请把 LoRA 文件(.safetensors)放进 models/loras 文件夹,再点“添加”。')); const next = recentLoras().find(n => names.includes(n) && !rows.some(row => row.name === n)) ?? names[0]; rememberLora(next); rows.push({name: next, strength: 1, enabled: true}); write(rows); } catch (error) { note.textContent = error.message; } + try { const rows = read(); if (rows.length >= 32) throw new Error(t('Use at most 32 LoRAs', '最多使用 32 个 LoRA')); if (!names.length) throw new Error(t('Put LoRA files (.safetensors) in the models/loras folder, then choose "Add".', '把 LoRA 文件(.safetensors)放进 models/loras 文件夹,再点“添加”。')); const next = recentLoras().find(n => names.includes(n) && !rows.some(row => row.name === n)) ?? names[0]; rememberLora(next); rows.push({name: next, strength: 1, enabled: true}); write(rows); } catch (error) { note.textContent = error.message; } }); async function refresh() { try { const response = await api.fetchApi('/models/loras', {cache: 'no-store'}); if (!response.ok) throw new Error(response.statusText); const listed = await response.json(); names = (Array.isArray(listed) ? listed : []).filter(n => typeof n === 'string' && n.toLowerCase().endsWith('.safetensors')); render(); } @@ -445,7 +445,7 @@ export async function openExtendEditor(record, node = current?.node || app.graph node = app.graph?._nodes?.find(row => row.type === 'FreeVideoGenerate'); if (!node) { node = globalThis.LiteGraph?.createNode('FreeVideoGenerate'); - if (!node || !app.graph) throw new Error(t('Could not add the creation node. Try again.', '无法添加创作节点,请重试。')); + if (!node || !app.graph) throw new Error(t('Could not open the creative panel. Try again.', '没能打开创作面板,请重试。')); app.graph.add(node); app.graph.change?.(); } } @@ -588,7 +588,7 @@ export function openStudio(node, {extendFrom = null} = {}) { const promptHelp = el('a', 'Skill ↗', 'fv-prompt-help'); promptHelp.href = 'https://github.com/MiniMax-AI/MiniMax-H3/blob/main/skills/h3-prompt-writing/SKILL.md'; promptHelp.target = '_blank'; promptHelp.rel = 'noopener noreferrer'; - promptHelp.title = t('MiniMax H3 prompt guide', 'MiniMax H3 提示词指南'); + promptHelp.title = t('Prompt guide', '提示词指南'); promptSection.querySelector('.fv-label').append(promptHelp); promptSection.append(enhancer.element); const canvas = section(t('Frame & duration', '画幅与时长')); @@ -607,16 +607,16 @@ export function openStudio(node, {extendFrom = null} = {}) { let pixels = selected !== 'custom' && Number.isFinite(node.properties?.freevideo_pixels) ? node.properties.freevideo_pixels : (initialWidth * initialHeight) / 1024 ** 2; let disposed = false; - const mp = el('select'); mp.setAttribute('aria-label', t('Total pixels', '总像素')); + const mp = el('select'); mp.setAttribute('aria-label', t('Resolution', '清晰度')); const presetRatio = {'16:9': 16 / 9, '9:16': 9 / 16, '1:1': 1}[selected] || ratio; const sizeSelection = imageSizeSelection(pixels, selected === 'custom' ? null : presetRatio, Number(initialWidth), Number(initialHeight)); if (pixels === 1 && sizeSelection.value === .98) pixels = .98; - for (const n of sizeSelection.options) { const option = el('option', n === .98 ? t('0.98 MP · Native', '0.98 MP · 原生') : `${n.toFixed(2)} MP`); option.value = n; mp.append(option); } + for (const n of sizeSelection.options) { const option = el('option', n === .98 ? t('0.98 MP · Recommended', '0.98 MP · 推荐') : `${n.toFixed(2)} MP`); option.value = n; mp.append(option); } mp.value = sizeSelection.value; const legacyOption = [...mp.options].find(o => Number(o.value) === 1); - mp.title = t('1 MP = 1024 × 1024, as in ComfyUI.', '1 MP = 1024 × 1024,与 ComfyUI 一致。'); mp.disabled = dimensionsLinked; + mp.disabled = dimensionsLinked; const seconds = el('input'); seconds.type = 'number'; seconds.required = true; seconds.min = '1.625'; seconds.step = 'any'; seconds.value = savedSeconds(node, value(node, 'seconds')); seconds.disabled = linked(node, 'seconds'); seconds.setAttribute('aria-label', t('Duration in seconds', '时长(秒)')); - const fields = el('div', null, 'fv-fields'); fields.append(field(t('Total pixels', '总像素'), mp), field(t('Duration · seconds', '时长 · 秒'), seconds)); canvas.append(fields); + const fields = el('div', null, 'fv-fields'); fields.append(field(t('Resolution', '清晰度'), mp), field(t('Duration · seconds', '时长 · 秒'), seconds)); canvas.append(fields); const canvasNote = el('div', null, 'fv-canvas-note'), dimensions = el('span'), duration = el('span'); canvasNote.append(dimensions, duration); canvas.append(canvasNote); const rangeMount = el('div'); canvas.append(rangeMount); let capabilityView; @@ -626,18 +626,18 @@ export function openStudio(node, {extendFrom = null} = {}) { // Two-pass switched off only because first-pass steps left 8; returning to 8 restores it. let autoSinglePass = false; const twoPassLabel = el('div', null, 'fv-two-pass fv-pass-row'); - twoPassLabel.title = t('Generate at a lower resolution, then upscale and finish sampling at the target size to save time.', '先以低分辨率生成,再放大到目标分辨率完成采样,缩短生成时间。'); + twoPassLabel.title = t('Generates at a lower resolution, then upscales. Faster.', '先用低分辨率生成,再放大成片,更快。'); const twoPassStatus = el('small', '', 'fv-two-pass-status'); // Off, on, or on and paused after the first pass: a preview is one way to run two-pass. const passMode = el('div', null, 'fv-pass-mode'); passMode.setAttribute('role', 'radiogroup'); - passMode.setAttribute('aria-label', t('Two-pass sampling', '二次采样')); + passMode.setAttribute('aria-label', t('Two-step', '两步生成')); const passOptions = [['off', t('Off', '关闭')], ['on', t('On', '开启')], ['preview', t('Preview first', '先出预览')]].map(([mode, label]) => { const option = el('button', label); option.type = 'button'; option.dataset.mode = mode; option.setAttribute('role', 'radio'); option.onclick = () => choosePassMode(mode); return option; }); - passOptions[2].title = t('Two-pass, paused after the half-resolution first pass so you can look at it. Run the second pass from the result when it looks right: the video matches generating it directly.', - '二次采样跑完半分辨率的一采先停下,给你看预览;满意后在结果里点「继续二采」,成片与直接生成一致。'); + passOptions[2].title = t('Stops at a half-resolution preview. Choose "Continue" in the result when it looks right; the video matches a direct run.', + '先出半分辨率预览,满意后在结果里点“继续生成”,成片与直接生成一致。'); const previewPassHint = passOptions[2].title; passMode.append(...passOptions); passMode.addEventListener('keydown', event => { @@ -649,7 +649,7 @@ export function openStudio(node, {extendFrom = null} = {}) { const next = enabled[(current + step + enabled.length) % enabled.length]; if (next) { next.click(); next.focus(); } }); - twoPassLabel.append(el('span', t('Two-pass', '二次采样')), twoPassStatus, passMode); + twoPassLabel.append(el('span', t('Two-step', '两步生成')), twoPassStatus, passMode); let effortEstimate = {}, effortEstimateKey = '', effortEstimateTimer = null, effortEstimateRequest = null, lengthControl = null, chainController = null; const effort = createSamplingEffort(t, {onChange: tier => { baseSteps.value = tier.steps; @@ -740,12 +740,12 @@ export function openStudio(node, {extendFrom = null} = {}) { } const samplingFields = el('div', null, 'fv-fields'); samplingFields.style.marginTop = '14px'; const baseSteps = el('input'), refineSteps = el('input'); - const samplingWarning = el('p', t('Changing sampling steps may reduce generation quality. Defaults: 8 + 3 steps.', '修改采样步数可能降低生成质量。默认一采 8 步、二采 3 步。'), 'fv-muted'); + const samplingWarning = el('p', t('Changing steps may lower quality. Default: 8 + 3 steps.', '改步数可能降低质量。默认 8 + 3 步。'), 'fv-muted'); samplingWarning.id = `fv-sampling-warning-${node.id}`; samplingWarning.style.color = 'var(--fv-warning)'; for (const [input, name, label, fallback, maximum] of [ - [baseSteps, 'base_steps', t('First-pass steps', '一采步数'), 8, 32], - [refineSteps, 'refine_steps', t('Second-pass steps', '二采步数'), 3, 31], + [baseSteps, 'base_steps', t('First-pass steps', '首轮步数'), 8, 32], + [refineSteps, 'refine_steps', t('Refine steps', '细化步数'), 3, 31], ]) { input.type = 'number'; input.min = '1'; input.max = String(maximum); input.step = '1'; input.required = true; input.value = value(node, name) ?? fallback; @@ -789,9 +789,9 @@ export function openStudio(node, {extendFrom = null} = {}) { baseSteps.min = twoPass.checked ? '2' : '1'; refineSteps.max = baseLinked ? '31' : String(Math.max(3, Math.min(31, Number(baseSteps.value || 8) - 1))); refineSteps.setCustomValidity(twoPass.checked && !baseLinked && !refineSteps.disabled && Number(refineSteps.value) !== 3 && Number(refineSteps.value) >= Number(baseSteps.value) - ? t('Other second-pass counts must be fewer than first-pass steps.', '其他二采步数需小于一采步数。') : ''); - refineSteps.title = twoPass.checked ? t('Three steps use the independent refinement schedule.', '3 步使用独立二采时间表。') - : t('Turn on two-pass acceleration to use this setting.', '开启二次采样加速后生效。'); + ? t('Use 3 refine steps, or fewer steps than in the first pass.', '细化步数须为 3,或少于首轮步数。') : ''); + if (twoPass.checked) refineSteps.removeAttribute('title'); + else refineSteps.title = t('Available when two-step is on.', '开启两步生成后可用。'); for (const [input, name] of [[baseSteps, 'base_steps'], [refineSteps, 'refine_steps']]) { if (!input.disabled && input.checkValidity() && Number(input.value) !== value(node, name)) { set(node, name, Number(input.value)); @@ -829,7 +829,7 @@ export function openStudio(node, {extendFrom = null} = {}) { const estimate = effortEstimate[mode]?.[steps]; // A previewed level always refines three steps; the current plan may not. if (estimate?.status !== 'estimated' || (mode === 'two_pass' && !arguments.length && Number(refineSteps.value) !== 3)) { - effort.setEstimate('', t('Estimates appear after a matching generation on this computer.', '本机有相近生成记录后显示预计耗时。')); + effort.setEstimate('', t('No estimate is available for these settings.', '当前设置暂无预计用时。')); return; } const minutes = estimate.high_seconds >= 90, scale = minutes ? 60 : 1; @@ -837,9 +837,7 @@ export function openStudio(node, {extendFrom = null} = {}) { const high = Math.max(low + 1, Math.ceil(estimate.high_seconds / scale)); const unit = minutes ? t('min', '分钟') : t('s', '秒'); const extra = estimate.includes_text_encoding ? '' : t(' + prompt', ' + 提示词准备'); - effort.setEstimate(`${t('~', '约 ')}${low}–${high} ${unit}${extra}`, - t('Based on complete local runs. Includes loading and decoding; first use or other applications may take longer.', - '根据本机完整生成记录估算,包含加载与解码;首次准备或其他程序占用可能延长时间。')); + effort.setEstimate(`${t('~', '约 ')}${low}–${high} ${unit}${extra}`); } function refreshEffortEstimate(force = false) { capabilityView?.update(); @@ -983,7 +981,7 @@ export function openStudio(node, {extendFrom = null} = {}) { const stats = el('div', null, 'fv-stats'), budget = el('div', '', 'fv-budget'), links = el('div', null, 'fv-result-links'); stats.hidden = true; const metrics = [], metricLabels = []; - for (const label of [t('Sampling', '采样耗时'), t('Request total', '请求总计'), t('VRAM peak', '显存峰值'), t('RAM peak', '内存峰值')]) { const box = el('div', null, 'fv-stat'), n = el('strong', '—'), caption = el('span', label); box.append(n, caption); stats.append(box); metrics.push(n); metricLabels.push(caption); } + for (const label of [t('Generation', '生成用时'), t('Total time', '总用时'), t('Peak VRAM', '最高显存'), t('Peak RAM', '最高内存')]) { const box = el('div', null, 'fv-stat'), n = el('strong', '—'), caption = el('span', label); box.append(n, caption); stats.append(box); metrics.push(n); metricLabels.push(caption); } const prewarm = el('div', node.freevideoPrewarm || '', 'fv-prewarm'); const reuseRow = el('div', null, 'fv-reuse-result'); reuseRow.hidden = true; const reuseNotice = el('span', t('Reused previous result', '已复用上次结果')); @@ -1008,7 +1006,7 @@ export function openStudio(node, {extendFrom = null} = {}) { try { await node.freevideoUpscaleResult(saved); await syncQueue(); } catch (error) { resultActionFailed(error); throw error; } }; - const memoryNote = el('div', t('This video used the low-memory mode for this GPU, so it took longer than usual. A lower resolution or a shorter video runs faster.', '这段视频使用了省显存方式,所以比平时慢。降低分辨率或缩短时长会更快。'), 'fv-note'); memoryNote.hidden = true; + const memoryNote = el('div', t('This run used the low-memory mode, so it was slower. A lower resolution or a shorter video is faster.', '这次用了省显存方式,所以较慢。降低分辨率或缩短时长会更快。'), 'fv-note'); memoryNote.hidden = true; const thermalNote = el('div', '', 'fv-note'); thermalNote.hidden = true; const sharedGpuNote = el('div', '', 'fv-note'); sharedGpuNote.hidden = true; const encoderNote = el('div', '', 'fv-note'); encoderNote.hidden = true; @@ -1053,8 +1051,8 @@ export function openStudio(node, {extendFrom = null} = {}) { if (encoderPath) encoderNote.append(el('pre', encoderPath, 'fv-encoder-path')); regenerate.disabled = false; regenerate.textContent = t('Regenerate', '重新生成'); const unified = r.memory_model === 'unified'; - metricLabels[2].textContent = unified ? t('Unified memory', '统一内存总量') : t('VRAM peak', '显存峰值'); - metricLabels[3].textContent = unified ? t('Process RAM peak', '进程内存峰值') : t('RAM peak', '内存峰值'); + metricLabels[2].textContent = unified ? t('Unified memory', '统一内存总量') : t('Peak VRAM', '最高显存'); + metricLabels[3].textContent = unified ? t('Process peak RAM', '进程最高内存') : t('Peak RAM', '最高内存'); const shown = [number(r.sample_seconds), number(r.request_seconds), number(unified ? r.unified_total_bytes : r.vram_peak_bytes, 2 ** 30, 'GiB'), number(r.ram_peak_bytes, 2 ** 30, 'GiB')]; metrics.forEach((e, i) => e.textContent = shown[i]); budget.textContent = r.result_cache_hit || r.preview ? '' : unified ? (Number.isFinite(r.unified_reserve_bytes) ? `${t('Reserved unified memory', '预留统一内存')} ${number(r.unified_reserve_bytes, 2 ** 30, 'GiB')}` : '') @@ -1156,7 +1154,7 @@ export function openStudio(node, {extendFrom = null} = {}) { const optionsPanel = el('div', null, 'fv-run-menu'); optionsPanel.append(el('strong', t('Generation mode', '生成方式'))); const modeButtons = []; - for (const [id, label] of [['single', t('One video', '单次生成')], ['batch', t('Batch', '批量生成')], ['loop', t('Loop', '循环生成')]]) { + for (const [id, label] of [['single', t('Single', '单次')], ['batch', t('Batch', '批量')], ['loop', t('Loop', '循环')]]) { const b = button(label, () => { runMode = id; node.properties.freevideo_run_mode = id; rememberPromptDraft(api, node); updateRunButton(); @@ -1198,13 +1196,13 @@ export function openStudio(node, {extendFrom = null} = {}) { queueActions.append(queueDetails, cancelCurrent, stopLoop, resumeChain); runbar.append(requestAssets.element, runrow, queueActions); controls.append(runbar); const queueError = error => ({ - queue_unavailable: t('Could not read the queue. Any loop has been stopped; already submitted videos stay queued.', '暂时无法读取队列。循环已停止追加,已提交的任务仍在队列中。'), - cancel_failed: t('Could not cancel. Try again.', '暂时未能取消,请重试。'), - move_failed: t('Could not move it. Try again.', '暂时未能移动,请重试。'), - submit_failed: t('The workflow was not queued. Check the node settings.', '工作流未能提交,请检查节点设置。'), + queue_unavailable: t('Could not read the queue. The loop has stopped; videos already added stay queued.', '暂时读不到队列,循环已停止;已加入的视频还在队列中。'), + cancel_failed: t('Could not cancel. Try again.', '没有取消成功,请重试。'), + move_failed: t('Could not move it. Try again.', '没有移动成功,请重试。'), + submit_failed: t('Could not add to the queue. Check the settings in node view.', '没能加入队列,请在节点视图检查设置。'), loop_active: t('Stop the current loop before starting another.', '请先停止当前循环。'), - loop_stopped: t('The last request did not complete. The loop has stopped.', '上一条任务未完成,循环已停止。'), - linked_seed: t('Use the seed in this panel for batch or loop generation.', '批量和循环生成需使用面板内的种子,请先断开种子输入的连接。'), + loop_stopped: t('The last video did not finish. The loop has stopped.', '上一个视频没有生成完,循环已停止。'), + linked_seed: t('Batch and loop use the seed in this panel. Disconnect the seed link in node view.', '批量和循环要用面板里的种子,请在节点视图断开种子的连线。'), missing_node: t('This node is disabled or no longer in the workflow.', '该节点已禁用或已从工作流移除。'), chain_cancel_failed: t('Could not cancel. Refresh the page and try again.', '没有取消成功,请刷新页面后再试。'), chain_resume_failed: t('Could not continue. Refresh the page and try again.', '没有继续生成,请刷新页面后再试。'), @@ -1218,7 +1216,7 @@ export function openStudio(node, {extendFrom = null} = {}) { if (['loop_stopped', 'queue_unavailable'].includes(error?.message) && node.freevideoFailureReport && node.freevideoFailureReport.stage !== 'queue') return; status.textContent = error?.schema === 'freevideo.error-report' - ? t('Submission failed. See the details below.', '提交失败,具体原因见下方。') + ? t('Could not start. See the details below.', '没能开始生成,原因见下方。') : extensionCause(error) ? t('A browser extension is interfering with FreeVideo. See details below.', '浏览器扩展干扰了 FreeVideo,详见下方。') : queueError(error); // Recoverable chain actions need one visible sentence, not a second error card. @@ -1244,7 +1242,7 @@ export function openStudio(node, {extendFrom = null} = {}) { : t(`Generating part ${position.index + 1}…`, `正在生成第 ${position.index + 1} 段…`); generate.disabled = submitting || chainController?.active || lengthControl.blocked || (runMode === 'loop' && queueState?.looping); generate.textContent = submitting ? t('Adding…', '正在添加…') - : chainReason || (runMode === 'batch' ? t(`Add ${count.value || '…'} videos`, `添加 ${count.value || '…'} 条任务`) + : chainReason || (runMode === 'batch' ? t(`Add ${count.value || '…'} videos`, `添加 ${count.value || '…'} 个视频`) : runMode === 'loop' ? (queueState?.looping ? t('Looping', '循环中') : t('Start loop', '开始循环')) : busy ? t('Add to queue', '添加到队列') : previewFirst.checked && !previewFirst.disabled ? t('Generate preview', '生成预览') : t('Generate video', '生成视频')); @@ -1253,9 +1251,9 @@ export function openStudio(node, {extendFrom = null} = {}) { for (const b of modeButtons) if (b.dataset.mode !== 'single') b.disabled = multiple; countField.hidden = runMode !== 'batch'; count.disabled = runMode !== 'batch'; modeHelp.hidden = runMode === 'single' && !multiple; - modeHelp.textContent = multiple ? t('Batch and loop are unavailable for videos made in parts.', '分段视频暂不支持批量和循环生成。') : runMode === 'loop' - ? t('Use these settings with a new seed each time. Closing this page stops adding videos.', '固定当前设置,每条使用不同种子。关闭网页后不再追加。') - : t('Use these settings with a different seed for each video.', '固定当前设置,每条使用不同种子。'); + modeHelp.textContent = multiple ? t('Videos made in parts cannot use batch or loop.', '分段视频不能批量或循环生成。') : runMode === 'loop' + ? t('Same settings, a new seed each time. Stops when this page closes.', '用当前设置,每次换一个种子;关闭网页后停止。') + : t('Same settings, a new seed for each video.', '用当前设置,每个视频换一个种子。'); cancelCurrent.hidden = !queueState?.running && !chainController?.active; cancelCurrent.disabled = cancelling; cancelCurrent.textContent = cancelling ? t('Cancelling…', '正在取消…') : t('Cancel current', '取消当前'); @@ -1280,7 +1278,7 @@ export function openStudio(node, {extendFrom = null} = {}) { // The right-hand frame belongs to the submitted video. Draft edits on // the left must never reshape a video currently being generated. if (Number.isFinite(g?.width) && Number.isFinite(g?.height)) previewSize(g.width, g.height); - queueToggle.textContent = t(`Queue · ${state.pending.length}`, `等待 ${state.pending.length} 条`); + queueToggle.textContent = t(`Queued · ${state.pending.length}`, `排队 ${state.pending.length} 个`); queueDetails.hidden = state.pending.length === 0; queueActions.hidden = !busy && !state.looping && resumeChain.hidden; const signature = JSON.stringify(state.pending.map(row => [row.id, row.inputs.width, row.inputs.height])); @@ -1294,7 +1292,7 @@ export function openStudio(node, {extendFrom = null} = {}) { try { await queueController.cancel(row.id); } catch (error) { showQueueError(error); remove.disabled = false; } }, 'fv-quiet'); - remove.setAttribute('aria-label', t(`Remove queued video ${index + 1}`, `移除第 ${index + 1} 条排队任务`)); + remove.setAttribute('aria-label', t(`Remove queued video ${index + 1}`, `移除排队的第 ${index + 1} 个`)); item.append(description); if (index > 0) { const first = button(t('Move to front', '排到最前'), async () => { @@ -1302,7 +1300,7 @@ export function openStudio(node, {extendFrom = null} = {}) { try { await queueController.moveFirst(row.id); } catch (error) { showQueueError(error); first.disabled = remove.disabled = false; } }, 'fv-quiet'); - first.setAttribute('aria-label', t(`Move queued video ${index + 1} to front`, `把第 ${index + 1} 条排队任务排到最前`)); + first.setAttribute('aria-label', t(`Move queued video ${index + 1} to front`, `把排队的第 ${index + 1} 个排到最前`)); item.append(first); } item.append(remove); queueList.append(item); @@ -1339,8 +1337,8 @@ export function openStudio(node, {extendFrom = null} = {}) { const message = new Map([[seconds, t('Fill in the duration.', '请填写时长。')], [width, t('Fill in the width.', '请填写宽度。')], [height, t('Fill in the height.', '请填写高度。')], [seed, t('Fill in the seed.', '请填写种子。')], - [baseSteps, t('Fill in the first-pass steps.', '请填写一采步数。')], - [refineSteps, t('Fill in the second-pass steps.', '请填写二采步数。')]]).get(invalid); + [baseSteps, t('Fill in the first-pass steps.', '请填写首轮步数。')], + [refineSteps, t('Fill in the refine steps.', '请填写细化步数。')]]).get(invalid); if (message) { status.dataset.error = 'true'; status.textContent = message; } } invalid.focus(); invalid.reportValidity(); return; @@ -1358,7 +1356,7 @@ export function openStudio(node, {extendFrom = null} = {}) { const lengthPlan = await lengthControl.ready(); if (disposed || lengthControl.blocked) return; if (lengthControl.absent && !seconds.reportValidity()) return; - if (lengthPlan?.count > 1 && chosenMode !== 'single') { status.textContent = t('Batch and loop are unavailable for videos made in parts.', '分段视频暂不支持批量和循环生成。'); return; } + if (lengthPlan?.count > 1 && chosenMode !== 'single') { status.textContent = t('Videos made in parts cannot use batch or loop.', '分段视频不能批量或循环生成。'); return; } const lengthOptions = lengthControl.options(); const source = extension.source; if (source && !await extendSourceExists(api, source)) throw new Error(extendText(t, 'unavailable')); @@ -1368,7 +1366,7 @@ export function openStudio(node, {extendFrom = null} = {}) { for (const section of controls.querySelectorAll('.fv-section')) section.inert = true; const snapshot = singleRunSnapshot(extendSnapshot(cleanOptionalPrompt(structuredClone(await app.graphToPrompt())), node.id, source, {multiple: lengthPlan?.count > 1}), node.id, lengthPlan); - if (!snapshot) { status.textContent = t('Could not plan this single run. Try again.', '暂时无法计算这次单次生成,请重试。'); return; } + if (!snapshot) { status.textContent = t('Could not prepare this run. Try again.', '没能准备这次生成,请重试。'); return; } const seedIndex = node.widgets.findIndex(w => w.name === 'seed'); capturing = false; for (const section of controls.querySelectorAll('.fv-section')) section.inert = false; @@ -1430,7 +1428,7 @@ export function openStudio(node, {extendFrom = null} = {}) { function applySize() { if (dimensionsLinked || extension.source) return; const [w, h] = canvasDimensions(pixels, ratio); - if (w < 256 || h < 256 || w > 4096 || h > 4096) throw new Error(t('This aspect ratio and pixel count exceed the supported dimensions. Use custom dimensions.', '该比例与像素数超出尺寸范围,请使用自定义宽高。')); + if (w < 256 || h < 256 || w > 4096 || h > 4096) throw new Error(t('This aspect ratio and resolution are out of range. Use a custom width and height.', '这个比例和清晰度超出范围,请改用自定义宽高。')); set(node, 'width', w); set(node, 'height', h); node.properties.freevideo_aspect = selected; node.properties.freevideo_pixels = pixels; syncCanvasSize(); } async function inputRatio(required = false) { @@ -1577,7 +1575,7 @@ export function openStudio(node, {extendFrom = null} = {}) { // Do not change any saved width/height simply by opening a workflow. syncQueue().then(async () => { if (disposed) return; - if (busy) { showProgress(node.freevideoProgress || {label: queued ? t('Waiting in queue', '正在排队') : t('Syncing generation progress', '正在同步生成进度')}); } + if (busy) { showProgress(node.freevideoProgress || {label: queued ? t('Waiting in queue', '正在排队') : t('Loading progress', '正在读取进度')}); } else if (!result) { const saved = (await recentLoad)[0]; if (!disposed && !busy && !result && saved) showResult(saved); From 716392845cf4a44978605a2f88c92c1aa307ea9c Mon Sep 17 00:00:00 2001 From: Bowen Xue Date: Thu, 15 Oct 2026 15:53:04 +0000 Subject: [PATCH 02/21] Clarify image model damage messages and test bilingual failure copy --- freevideo_engine/failure_details.py | 79 +++++++++++++++-------------- 1 file changed, 41 insertions(+), 38 deletions(-) diff --git a/freevideo_engine/failure_details.py b/freevideo_engine/failure_details.py index f57b9121..ada2678b 100644 --- a/freevideo_engine/failure_details.py +++ b/freevideo_engine/failure_details.py @@ -16,8 +16,8 @@ IMAGE_PACK_FAILURES = { 'disk_space': ('Not enough disk space to install the image model.', '磁盘空间不足,无法安装图像模型。'), 'offline': ('The network is unavailable; the image model could not be downloaded.', '网络不通,无法下载图像模型。'), - 'source_failed': ('The download source is unreachable or could not complete the image model download.', '下载源不可达或未能完成图像模型下载。'), - 'integrity_failed': ('The image model files failed verification; repair them in the launcher.', '图像模型文件校验失败,请在启动器里修复。'), + 'source_failed': ("The image model download didn't finish. Check your network and try again.", '图像模型没有下载完成,请检查网络后重试。'), + 'integrity_failed': ('The image model files are incomplete or damaged. Repair them in the launcher.', '图像模型文件不完整或已损坏,请在启动器里修复。'), 'write_failed': ('The image model files are in use or cannot be written or deleted.', '图像模型文件被占用,或无法写入/删除。'), 'invalid_catalog': ('The image model package version is incompatible; update FreeVideo and try again.', '图像模型包版本不匹配,请更新 FreeVideo 后重试。'), 'busy': ('Another task is using the image model package; try again when it finishes.', '图像模型包被其他任务占用,完成后再试。'), @@ -94,12 +94,12 @@ def sampling_table_failure(value, *, zh=False): # A custom step count is not a quality level (Iris): no "档" / "quality level" for it. if zh: if index is None: - title = '“自定义(%d 步)”的采样表还没有下载' % details['base_steps'] - what = '这个步数的采样表' + title = '“自定义(%d 步)”还没有下载' % details['base_steps'] + what = '这个步数需要的文件' else: - title = '“%s”档的采样表还没有下载' % QUALITY_LEVELS[index][1] - what = '这一档的采样表' - detail = ('这次生成需要先下载%s(约 %s),但这次没能下载。' + title = '“%s”还没有下载' % QUALITY_LEVELS[index][1] + what = '这一档需要的文件' + detail = ('这次生成要先下载%s(约 %s),但没能下载完。' '请确认网络连接后再生成一次,FreeVideo 会自动下载。') % (what, size) if available: detail += '也可以改用已经可以使用的%s档。' % available @@ -203,14 +203,14 @@ def environment_python_removed(args, pythons): ('This disk cannot hold the models', '这块硬盘放不下模型'), ('It is formatted as FAT32, which stores files up to 4 GB; the text encoder alone is 15.7 GB. ' 'Choose a folder on an APFS or Mac OS Extended disk.', - '它是 FAT32 格式,单个文件最大 4 GB,而文本编码器一个文件就有 15.7 GB。' - '请选择 APFS 或 Mac OS 扩展格式硬盘上的文件夹。')), + '它不能存放 4 GB 以上的文件。' + '请选择其他硬盘上的文件夹。')), (r'The installation folder is on an exFAT disk', 'disk-format', ('This disk cannot hold the FreeVideo environment', '这块硬盘装不了 FreeVideo 的运行环境'), ("It is formatted as exFAT, where FreeVideo's Python environment does not work. " 'Choose a folder on an APFS or Mac OS Extended disk.', - '它是 exFAT 格式,FreeVideo 的 Python 运行环境在这种格式上无法正常工作。' - '请选择 APFS 或 Mac OS 扩展格式硬盘上的文件夹。')), + '它不能安装 FreeVideo。' + '请选择其他硬盘上的文件夹。')), (r'The installation folder is on a disk that is not connected', 'disk-missing', ('The disk is not connected', '这块硬盘没有连接'), ('Connect the disk that holds the installation folder, then check again, or choose another folder.', @@ -218,7 +218,7 @@ def environment_python_removed(args, pythons): (r'The installation folder is on a read-only disk', 'disk-read-only', ('This disk is read-only', '这块硬盘是只读的'), ('This Mac can read the disk but not write to it, as with NTFS disks. Choose a folder on another disk.', - '这台 Mac 只能读取这块硬盘,不能写入(例如 NTFS 格式的硬盘)。请选择其他硬盘上的文件夹。')), + '这块硬盘只能读取、不能写入,请选择其他硬盘上的文件夹。')), ) @@ -263,7 +263,7 @@ def _model_unchanged(tail): for kind, why in (('plan-changed-files', 'files'), ('plan-changed', 'other'))}, 'dependencies': dict( busy=('When the current task ends, a Repair button appears here; it checks this installation and restores what is missing.', - '当前任务结束后,这里会出现“修复”按钮,点击后 FreeVideo 将检查这份安装并补装缺失的组件。'), + '当前任务结束后,这里会出现“修复”按钮,点击后会补装缺失的组件。'), bundled=('The portable package is missing components it needs and cannot restore them in one step. ' + _EXTRACT[0], '整合包缺少运行所需的组件,不能一键补齐。' + _EXTRACT[1]), offline=('An imported offline environment is missing components it needs and cannot restore them in one step. ' + _IMPORT[0], @@ -271,7 +271,7 @@ def _model_unchanged(tail): none=WITHOUT_REPAIR_NONE), 'runtime-libraries': dict( busy=('When the current task ends, a Repair button appears here; it reinstalls a complete copy of PyTorch and keeps your models and videos.', - '当前任务结束后,这里会出现“修复”按钮,点击后 FreeVideo 将重新安装完整的 PyTorch,模型和视频都会保留。'), + '当前任务结束后,这里会出现“修复”按钮,点击后会重新安装运行组件,模型和视频都会保留。'), bundled=('The portable package cannot be repaired in one step. ' + _EXTRACT[0], '整合包版不能一键修复。' + _EXTRACT[1]), offline=('An imported offline environment cannot be repaired in one step. ' + _IMPORT[0], @@ -282,13 +282,13 @@ def _model_unchanged(tail): 'update-unfinished': dict( busy=('FreeVideo is back on the previous version and works as before. When the current task ends, a Repair button appears here; ' 'clicking it reinstalls the engine environment and updates again. The details give the reason.', - 'FreeVideo 已退回原来的版本,可以照常使用。当前任务结束后,这里会出现“修复”按钮;点击后会重新安装引擎环境并再次更新。原因写在详情里。'), + 'FreeVideo 已退回原来的版本,可以照常使用。当前任务结束后,点这里出现的“修复”可以再次更新。'), **dict.fromkeys(('bundled', 'offline'), ( 'FreeVideo is back on the previous version and works as before; the details give the reason.', 'FreeVideo 已退回原来的版本,可以照常使用;原因写在详情里。')), none=('FreeVideo is back on the previous version and works as before. To update again, open Installation in the sidebar, keep the same folder, ' 'click Continue and then Check & continue, and finally click Install & launch on the confirmation page; the details give the reason.', - 'FreeVideo 已退回原来的版本,可以照常使用。要再次更新,请在左栏的“安装”页保持同一个安装目录,依次点击“继续”和“检查并继续”,最后在确认页点击“安装并启动”;原因写在详情里。')), + 'FreeVideo 已退回原来的版本,可以照常使用。要再次更新,请在“安装”页保持同一个安装目录,依次点“继续”“检查并继续”和“安装并启动”。')), } @@ -411,8 +411,8 @@ def launcher_failure(value, *, zh=False, other_disk=False, disk_name='', can_rep if can_repair and repairs_comfy and (_stopped_on_missing_module(summary) or ENVIRONMENT_FILES in summary): return dict(title=DEPENDENCIES[0][zh], detail='', action=DEPENDENCIES[1][zh], kind='dependencies') return dict(title='ComfyUI 启动失败' if zh else 'ComfyUI could not start', - detail='ComfyUI 进程在启动时退出。' if zh else 'The ComfyUI process exited during startup.', - action='展开详情查看退出码与插件错误;已有 ComfyUI 可使用原启动器检查。' if zh else + detail='ComfyUI 启动时退出了。' if zh else 'ComfyUI quit while starting.', + action='原因在详情里;使用已有的 ComfyUI 时,可以用原来的启动器检查。' if zh else 'Open details for the exit code and plugin errors. For an existing ComfyUI, also check its original launcher.', kind='comfy-startup') if 'Failed step: kernels (' in summary: @@ -420,9 +420,12 @@ def launcher_failure(value, *, zh=False, other_disk=False, disk_name='', can_rep detail = detail.partition('\nRetained step log:')[0] if zh: detail = detail.replace('Failed step: kernels (exit code ', '失败步骤:GPU 检查(退出码 ').replace(')\n', ')\n', 1) - detail = detail.replace('Required linear kernel did not pass.', '必需的 linear 运算检查未通过。') - detail = detail.replace('No attention backend passed.', '没有可用的 attention 后端。') - detail = detail.replace('Required path missing', '缺少必需目录') + detail = detail.replace('Required linear kernel did not pass.', + '显卡必需的运算检查没有通过。' if zh else 'A required GPU check did not pass.') + detail = detail.replace('No attention backend passed.', + '显卡加速检查没有通过。' if zh else 'The GPU acceleration check did not pass.') + detail = detail.replace('Required path missing', + '缺少必需的文件夹' if zh else 'A required folder is missing') return dict(title='GPU 检查未通过' if zh else 'GPU validation failed', detail=detail, action='展开详情或导出报告,查看完整检查结果。' if zh else 'Open the details or export the report for the complete results.', kind='kernels') @@ -471,7 +474,7 @@ def launcher_failure(value, *, zh=False, other_disk=False, disk_name='', can_rep (r'Editable install failed for|Editable verification failed for|The engine.s uv is missing', 'update-unfinished', ('The update did not finish', '这次更新没有完成'), ('FreeVideo is back on the previous version and works as before. Click Repair to reinstall the engine environment and update again; the details give the reason.', - 'FreeVideo 已退回原来的版本,可以照常使用。点击“修复”会重新安装引擎环境并再次更新,原因写在详情里。')), + 'FreeVideo 已退回原来的版本,可以照常使用。点“修复”可以再次更新,原因在详情里。')), (r'Git \d+\.\d+ is too old to download the pinned sources', 'git-version', ('Git is too old', 'Git 版本过旧'), ('Click Retry installation; FreeVideo then installs its own Git. On macOS, update the Command Line Tools.', @@ -485,15 +488,15 @@ def launcher_failure(value, *, zh=False, other_disk=False, disk_name='', can_rep (r'ComfyUI is still starting or could not load FreeVideo', 'comfy-nodes', ('ComfyUI did not load FreeVideo', 'ComfyUI 没有加载 FreeVideo'), ('Click Retry. If it stops here again, the ComfyUI log in the details shows why the FreeVideo nodes did not load; export the report to send it.', - '请点击重试。如果仍停在这里,详情里的 ComfyUI 日志会显示 FreeVideo 节点没有加载的原因,可以导出报告反馈。')), + '请点“重试”。仍然停在这里时,可以导出报告。')), (r'ComfyUI is already running\. Restart it once', 'comfy-restart', - ('ComfyUI must restart to load FreeVideo', 'ComfyUI 需要重启才能加载 FreeVideo'), + ('Restart ComfyUI to load FreeVideo', '需要重启 ComfyUI 才能加载 FreeVideo'), ('Close the ComfyUI that is running now, then click Retry.', '请关闭正在运行的 ComfyUI,然后点击重试。')), # web/health.js lists the failed files and what to do on the page itself. (r'The browser page did not load FreeVideo completely|浏览器页面没有完整加载 FreeVideo', 'page', ('FreeVideo did not load completely in the browser', '浏览器里的 FreeVideo 没有完整加载'), ('The browser page names the files that failed and what to do. Follow it, then reload the page; if it happens again, export the report.', - '浏览器页面上列出了出错的文件和处理方法。按提示处理后刷新页面;仍然出现时请导出报告。')), + '请按浏览器页面上的提示处理后刷新;仍然出现时,可以导出报告。')), (r'The browser has not opened FreeVideo yet|浏览器还没有打开 FreeVideo', 'page-silent', ('The browser has not opened FreeVideo', '浏览器还没有打开 FreeVideo'), ('Click “Open FreeVideo”. If the page stays blank or keeps loading, open it in a current Chrome or Edge.', @@ -506,29 +509,29 @@ def launcher_failure(value, *, zh=False, other_disk=False, disk_name='', can_rep (r'CUDA_ARCH_UNSUPPORTED', 'gpu-architecture', ('This GPU lacks the required compute support', '这张显卡不满足当前计算要求'), ('The detected GPU and compute capability are shown below. The CUDA runtime requires native BF16 computation (SM80 or newer). Use a compatible GPU; changing the driver does not change its hardware capability.', - '下方显示实际显卡和计算能力。当前 CUDA 引擎要求原生 BF16 计算(SM80 及以上);需要使用兼容显卡,更新驱动无法改变显卡的硬件能力。')), + '下方是这张显卡的信息。FreeVideo 需要较新的显卡,更新驱动无法解决。')), (r'CUDA_CAPABILITY_UNKNOWN', 'gpu-detection', - ('The GPU architecture could not be read', '未能读取显卡架构'), + ('GPU information could not be read', '未能读取显卡信息'), ('Retry hardware detection. If it still fails, export the report; it includes the GPU identity and the raw driver query.', - '请重试硬件检测。仍失败时导出报告,其中包含显卡信息和驱动查询的原始结果。')), + '请点“重新检查”。仍然失败时,可以导出报告。')), (r'CURL_REPAIR_(?:TIMEOUT|HTTP|TLS|DNS|FAILED|PROXY_UNSUPPORTED|PROXY_MISSING|REDIRECT|SIZE|INTEGRITY)', 'download-tool-network', ('The download tool could not be prepared', '下载工具自动补齐失败'), ('Check the connection mode in Downloads and retry. Export the report for the source, route and failure code.', - '请在“下载”中检查连接方式后重试。导出报告已包含来源、连接方式和失败码。')), + '请在“下载”里检查连接方式后重试。')), (r'CURL_START_DENIED|CURL_REPAIR_STORAGE', 'download-tool-access', ('Windows blocked the download tool', '下载工具启动或保存被拒绝'), ('Check folder permissions and Windows Security protection history, then retry. Export the report for the Windows error code.', - '请检查安装目录权限和 Windows 安全中心的保护历史记录后重试。导出报告包含 Windows 错误码。')), + '请检查安装目录的权限,并查看 Windows 安全中心的保护历史记录,然后重试。')), (r'CURL_[A-Z_]+|Windows curl\.exe was not found', 'download-tool', ('The download tool is unavailable', '下载工具不可用'), ('Retry installation to repair the tool automatically. If it still fails, export the report for the complete tool diagnostics.', - '请重试安装,启动器会自动修复下载工具。仍失败时导出报告,其中包含完整工具诊断。')), + '请点“重试安装”,启动器会自动修复下载工具。仍然失败时,可以导出报告。')), (r'ComfyUI is still running a job', 'busy', ('A video is still generating', '还有视频正在生成'), ('The update is installed. Click Connect again after the job finishes to restart ComfyUI with it.', '更新已装好。等当前任务完成后点“重新连接”,ComfyUI 会重启并使用新版。')), (r'Installation paused: Windows commit headroom', 'memory-commit', - ('Windows memory headroom is low', 'Windows 内存提交余量不足'), + ('Windows is low on available memory', 'Windows 可用内存不足'), ('Close memory-heavy applications, then retry. Downloaded files are retained.', '关闭占用内存较多的程序后重试,已下载文件会保留。')), (r'Installation paused: system RAM is nearly exhausted', 'memory-system', @@ -536,9 +539,9 @@ def launcher_failure(value, *, zh=False, other_disk=False, disk_name='', can_rep ('Close memory-heavy applications, then retry. Downloaded files are retained.', '关闭占用内存较多的程序后重试,已下载文件会保留。')), (r'Installation RAM monitoring failed|Cannot read process-tree memory', 'memory-monitor', - ('Memory usage could not be read', '暂时无法读取进程内存'), + ('Memory usage could not be read', '暂时无法读取内存用量'), ('Download progress is saved. Retry installation; copy the details if it happens again.', - '下载进度已保留。点击“重试安装”;若再次出现,请复制详情反馈。')), + '下载进度已保留,请点“重试安装”。')), (r'No space left on device|WinError 112|disk (?:is )?full|Not enough disk space|Insufficient disk space', 'disk', ('Not enough disk space', '磁盘空间不足'), ('Free up space on the installation drive, then retry.', '清理安装盘空间后,点击重试。')), @@ -556,13 +559,13 @@ def launcher_failure(value, *, zh=False, other_disk=False, disk_name='', can_rep (r'The installation plan changed\. Detect configuration again', 'plan-changed', CHANGED_TITLE, _joined(CHANGED_WHY['other'], CHANGED_REPAIR)), (ENVIRONMENT_PYTHON_REMOVED, 'environment-python-removed', - ('A Python file is missing', '一个 Python 文件不见了'), + ('A program file is missing', '缺少一个程序文件'), ('It may have been quarantined by security software. Restore the file or allow the FreeVideo folder, ' 'then click Retry installation.', '可能被安全软件隔离了。恢复这个文件或允许 FreeVideo 所在的文件夹后,点“重试安装”。')), # Windows refuses PyTorch's unsigned DLLs under Smart App Control; reinstalling changes nothing. (r'WinError 4551|Application Control policy has blocked', 'app-control', - ('Windows blocked the PyTorch files', 'Windows 拦截了 PyTorch 的文件'), + ('Windows blocked some runtime files', 'Windows 拦截了运行组件'), ('Smart App Control in Windows blocked these files. Search Windows for "Smart App Control", or open Windows Security → App & browser control → Smart App Control settings.', 'Windows 的“智能应用控制”拦截了这些文件。在 Windows 中搜索“智能应用控制”,或打开“Windows 安全中心 → 应用和浏览器控制 → 智能应用控制设置”。')), (r'PermissionError|Access is denied|Permission denied|WinError 5\b', 'permission', @@ -570,9 +573,9 @@ def launcher_failure(value, *, zh=False, other_disk=False, disk_name='', can_rep ('Choose an installation folder you can write to, then retry.', '选择有写入权限的安装目录后重试。')), # A worker's own check of the cuDNN files it loaded (runtime_libraries). (r"FreeVideo's PyTorch library files do not match", 'runtime-libraries', - ('PyTorch files have been changed', 'PyTorch 文件已被更改'), + ('Runtime files have been changed', '运行组件已被更改'), ('Click Repair to reinstall a complete copy of PyTorch. Models and videos are kept.', - '请点击“修复”,FreeVideo 将重新安装完整的 PyTorch,模型和视频都会保留。')), + '请点“修复”,FreeVideo 会重新安装运行组件,模型和视频都会保留。')), (r'ModuleNotFoundError|No module named|DLL load failed', 'dependencies', *DEPENDENCIES), (r'CUDA (?:error: )?out of memory|torch\.OutOfMemoryError|Insufficient currently available memory|crossed its RAM budget|paging file is too small|WinError 1455', 'memory', ('Memory is unavailable for this step', '这一步可用内存不足'), From cf7ac2324c0c71d175b5d33666e017270512207f Mon Sep 17 00:00:00 2001 From: Bowen Xue Date: Thu, 15 Oct 2026 15:53:04 +0000 Subject: [PATCH 03/21] The starter prompt's shot times read as seconds in the box --- freevideo_engine/comfy_nodes.py | 14 ++-- freevideo_engine/continuation.py | 2 +- freevideo_engine/default_prompt.py | 14 ++++ freevideo_engine/failure_details.py | 6 +- web/error_panel.js | 88 ++++++++++---------- web/freevideo.js | 55 ++++++------- web/generation_progress.js | 123 ++++++++++++---------------- web/studio.js | 4 +- 8 files changed, 150 insertions(+), 156 deletions(-) diff --git a/freevideo_engine/comfy_nodes.py b/freevideo_engine/comfy_nodes.py index c918a344..92fe8886 100644 --- a/freevideo_engine/comfy_nodes.py +++ b/freevideo_engine/comfy_nodes.py @@ -6,7 +6,7 @@ from . import comfy_bridge from .comfy_continuation import optional_text, request_options -from .default_prompt import DEFAULT_PROMPT +from .default_prompt import DEFAULT_PROMPT, DEFAULT_PROMPT_DISPLAY References = io.Custom('FREEVIDEO_REFERENCES') LoRAs = io.Custom('FREEVIDEO_LORAS') @@ -22,7 +22,7 @@ def define_schema(cls): description='Generate a video with audio using your FreeVideo installation. Memory placement, ' 'caches and validated optimizations are automatic. Outputs and diagnostics are retained.', inputs=[ - io.String.Input('text', display_name='Prompt', multiline=True, default=DEFAULT_PROMPT, + io.String.Input('text', display_name='Prompt', multiline=True, default=DEFAULT_PROMPT, placeholder=DEFAULT_PROMPT_DISPLAY, tooltip='Describe the scene, motion, dialogue, sound and music.'), io.Int.Input('width', default=1344, min=256, max=4096, step=32), io.Int.Input('height', default=768, min=256, max=4096, step=32), @@ -42,7 +42,7 @@ def define_schema(cls): 'When it is connected, the prompt and media inputs are not used. ' 'Conditioning from generic CLIP nodes is rejected.'), Media.Input('media', optional=True, tooltip='Unified Media panel: keyframes or ordered references.'), - io.Boolean.Input('two_pass', display_name='Two-pass acceleration', default=True, optional=True, + io.Boolean.Input('two_pass', display_name='Two-step generation', default=True, optional=True, tooltip='Usually faster: generate at a lower resolution, then upscale and finish sampling at the target size.'), io.Int.Input('base_steps', display_name='First-pass steps', default=8, min=1, max=32, optional=True, tooltip='Default: 8. Changing sampling steps may reduce generation quality.'), @@ -50,8 +50,8 @@ def define_schema(cls): tooltip='Default: 3. Three steps use the independent refinement schedule. Changing sampling steps may reduce generation quality.'), io.Boolean.Input('force_regenerate', display_name='Force regeneration', default=False, optional=True, tooltip='Generate again even when an identical completed video is saved locally.'), - io.Boolean.Input('preview', display_name='Two-pass: preview first pass', default=False, optional=True, - tooltip='Part of two-pass acceleration: pause after the half-resolution first pass and save it as a preview. ' + io.Boolean.Input('preview', display_name='Two-step: preview first', default=False, optional=True, + tooltip='Part of two-step generation: pause after the half-resolution first pass and save it as a preview. ' 'Run the second pass from the result when it looks right; it continues from the same latents.'), io.String.Input('refine_from', display_name='Second pass of preview', default='', optional=True, advanced=True, socketless=True, tooltip='Set by the Run second pass button: the preview video to continue. ' @@ -92,7 +92,7 @@ def validate_inputs(cls, text, width, height, seconds, seed, continue_from='', f from .two_pass import validate_steps validate_steps(base_steps, refine_steps, two_pass) if preview and not two_pass: - return 'Preview first needs two-pass acceleration.' + return 'Preview first needs two-step generation.' comfy_bridge.installation() except (OSError, ValueError, KeyError) as error: from .continuation import UNSUPPORTED @@ -123,7 +123,7 @@ def execute(cls, text, width, height, seconds, seed, first=None, last=None, notes = [] if options.get('continue_from') and not two_pass: two_pass = True - notes.append('Continuing a video needs "Two-pass acceleration", so it is on for this part.') + notes.append('Continuing a video needs "Two-step generation", so it is on for this part.') if options.get('continue_from') and preview: preview = False notes.append('A continued part is made in full, without stopping at the preview.') diff --git a/freevideo_engine/continuation.py b/freevideo_engine/continuation.py index 139ab931..22a04a43 100644 --- a/freevideo_engine/continuation.py +++ b/freevideo_engine/continuation.py @@ -35,7 +35,7 @@ # Every refusal starts with UNSUPPORTED, then says what to change. UNSUPPORTED = 'Continue from video cannot be used with these settings.' UNSUPPORTED_DEVICE = UNSUPPORTED + ' Continuing a video needs an NVIDIA GPU. To make a new video, clear "Continue from video".' -UNSUPPORTED_PASSES = UNSUPPORTED + ' Turn on "Two-pass acceleration" and turn off the first-pass preview.' +UNSUPPORTED_PASSES = UNSUPPORTED + ' Turn on "Two-step generation" and turn off the first-pass preview.' UNSUPPORTED_REFINE = UNSUPPORTED + ' Clear either "Second pass of preview" or "Continue from video".' UNSUPPORTED_MEDIA = UNSUPPORTED + ' Connect up to 3 pictures as references, and remove video, audio and pre-encoded inputs.' # A continued part keeps the cast's pictures, one per person and at most three people in a frame. diff --git a/freevideo_engine/default_prompt.py b/freevideo_engine/default_prompt.py index 66997b0a..8125c222 100644 --- a/freevideo_engine/default_prompt.py +++ b/freevideo_engine/default_prompt.py @@ -4,6 +4,8 @@ references/base-en.txt. This is creative copy, not a benchmark input. """ +import re + # Used only as a new-node default; saved and connected prompts stay authoritative. DEFAULT_PROMPT = ( 'integrated_multimodal_description: [Shot 1] A premium cinematic, photorealistic ' @@ -35,3 +37,15 @@ 'harmonics, expands into full strings and deep brass as the observatory opens, then ' 'resolves into one bright, confident chord under the final title.' ) + + +def display_prompt(prompt=DEFAULT_PROMPT): + """The same text as plain prose for the studio box; the structured labels stay in the prompt sent to the model.""" + text = re.sub(r'^integrated_multimodal_description:\s*', '', prompt) + text = re.sub(r'\[Shot \d+\]\s*', '', text) + text = re.sub(r'At 00:0?(\d+)\.000,', lambda m: f'At {int(m.group(1))} seconds,', text) + text = re.sub(r'\n\noverall_soundscape:\s*', '\n\nSound: ', text) + return re.sub(r'\n\nnon_diegetic_music:\s*', '\n\nMusic: ', text) + + +DEFAULT_PROMPT_DISPLAY = display_prompt() diff --git a/freevideo_engine/failure_details.py b/freevideo_engine/failure_details.py index ada2678b..465a8005 100644 --- a/freevideo_engine/failure_details.py +++ b/freevideo_engine/failure_details.py @@ -452,9 +452,9 @@ def launcher_failure(value, *, zh=False, other_disk=False, disk_name='', can_rep (r'Continue from video cannot be used with these settings\. Continuing a video needs an NVIDIA', 'continuation-route', ("This computer can't continue a video", '这台电脑无法接续视频'), ('Continuing a video needs an NVIDIA GPU. To make a new video, clear "Continue from video".', '接续视频需要 NVIDIA 显卡。要生成新视频,请清空“Continue from video”。')), - (r'Continue from video cannot be used with these settings\. Turn on "Two-pass', 'continuation-passes', - ('Continuing needs two-pass acceleration', '接续需要二次采样加速'), - ('Turn on "Two-pass acceleration" and turn off the first-pass preview.', '请打开“二次采样加速”,并关闭首轮预览。')), + (r'Continue from video cannot be used with these settings\. Turn on "Two-(?:pass|step)', 'continuation-passes', + ('Continuing needs two-step generation', '接续需要两步生成'), + ('Turn on "Two-step generation" and turn off the first-pass preview.', '请打开“两步生成”,并关闭首轮预览。')), (r'Continue from video cannot be used with these settings\. Clear either', 'continuation-refine', ('A continued part is made in full', '接续会完整生成这一段'), ('Clear either "Second pass of preview" or "Continue from video".', '请清空“Second pass of preview”或“Continue from video”其中一项。')), diff --git a/web/error_panel.js b/web/error_panel.js index 3df01e3d..36e2f7a4 100644 --- a/web/error_panel.js +++ b/web/error_panel.js @@ -24,9 +24,9 @@ const PLAIN_ERRORS = [ {key: 'continuation-route', message: 'Continue from video cannot be used with these settings. Continuing a video needs an NVIDIA GPU. To make a new video, clear "Continue from video".', title: ['This computer can\'t continue a video', '这台电脑无法接续视频'], summary: ['Continuing a video needs an NVIDIA GPU. To make a new video, clear "Continue from video".', '接续视频需要 NVIDIA 显卡。要生成新视频,请清空“Continue from video”。']}, - {key: 'continuation-passes', message: 'Continue from video cannot be used with these settings. Turn on "Two-pass acceleration" and turn off the first-pass preview.', - title: ['Continuing needs two-pass acceleration', '接续需要二次采样加速'], - summary: ['Turn on "Two-pass acceleration" and turn off the first-pass preview.', '请打开“二次采样加速”,并关闭首轮预览。']}, + {key: 'continuation-passes', message: 'Continue from video cannot be used with these settings. Turn on "Two-step generation" and turn off the first-pass preview.', + title: ['Continuing needs two-step generation', '接续需要两步生成'], + summary: ['Turn on "Two-step generation" and turn off the first-pass preview.', '请打开“两步生成”,并关闭首轮预览。']}, {key: 'continuation-refine', message: 'Continue from video cannot be used with these settings. Clear either "Second pass of preview" or "Continue from video".', title: ['A continued part is made in full', '接续会完整生成这一段'], summary: ['Clear either "Second pass of preview" or "Continue from video".', '请清空“Second pass of preview”或“Continue from video”其中一项。']}, @@ -51,7 +51,7 @@ const PLAIN_ERRORS = [ title: ['This LoRA cannot be used with the int8 model yet', '这个 LoRA 暂时不能用于 int8 模型'], summary: ['This LoRA changes parts of the model that the int8 version cannot merge yet, so it cannot be used for now. LoRAs that change only linear layers work.', '这个 LoRA 修改了 int8 模型目前还不能合并的部分,暂时无法使用。只修改线性层的 LoRA 可以正常使用。']}, {key: 'fa4-update', message: 'The attention speed-up component on this computer needs an update. Run freevideo setup --fa4-guard in a terminal, then restart FreeVideo.', - title: ['The attention speed-up component needs an update', '需要更新注意力加速组件'], + title: ['Runtime components need updating', '运行组件需要更新'], summary: ['The attention speed-up component on this computer needs an update. Run freevideo setup --fa4-guard in a terminal, then restart FreeVideo.', '注意力加速组件需要更新。请在终端运行 freevideo setup --fa4-guard,然后重新启动 FreeVideo。']}, {key: 'duration-too-short', message: 'The video must be at least 1.625 s long.', title: ['The video is too short', '视频时长太短'], @@ -82,11 +82,11 @@ function knownError(text) { // When a diagnostic report exists the card hides Export report; the same advice // then names Download report in the preview or node panel. -const DOWNLOAD_ADVICE = ['If it still fails, click Download report in the preview or node panel and attach the report to a GitHub issue.', - '仍然失败时,请在预览区或节点面板中点击“下载报告”,并提交到 GitHub issue。']; +const DOWNLOAD_ADVICE = ['If it still fails, use Download report in the preview and submit it in a GitHub issue.', + '仍然失败时,可点预览区的“下载报告”,提交到 GitHub issue。']; const EXPORT_TO_DOWNLOAD = [ ['Export the report if it still fails.', DOWNLOAD_ADVICE[0]], - ['If it still fails, export the report and attach it to a GitHub issue.', DOWNLOAD_ADVICE[0]], + ['If it still fails, export the report and submit it in a GitHub issue.', DOWNLOAD_ADVICE[0]], ['仍失败时请导出报告。', DOWNLOAD_ADVICE[1]], ['仍然失败时,请导出报告并提交到 GitHub issue。', DOWNLOAD_ADVICE[1]], ['If it still fails, export the report.', DOWNLOAD_ADVICE[0]], @@ -294,8 +294,8 @@ export function failureAdvice(value, t, known = knownError(value)) { return {...row(key, title, summary, ['', '']), plain: true}; } if (/Prompt outputs failed validation|Required input is missing|Value not in list|Return type mismatch|Failed to validate prompt|value (?:\S+ )?(?:smaller|bigger) than/i.test(text)) - return row('validation', ['Check the workflow inputs', '请检查工作流输入'], - ['ComfyUI rejected the workflow before generation. The affected nodes and reasons are listed below.', 'ComfyUI 在生成开始前拒绝了工作流,相关节点和具体原因列在下方。'], + return row('validation', ['Check generation settings', '检查生成设置'], + ['The generation settings did not pass validation; see the error details below.', '生成设置未通过检查,可查看下方错误详情。'], ['Check the named inputs, connections and model choices. Copy the details or export the error report when asking for help.', '检查下方指出的输入、节点连接和模型选项;反馈时可复制详情或导出错误报告。']); if (/FreeVideo resource planning could not admit/i.test(text)) { // The planner names smaller requests it admitted on this GPU just now. @@ -304,46 +304,46 @@ export function failureAdvice(value, t, known = knownError(value)) { if (fits.length) { const en = fits.map(f => f.longest ? `${f.width}\u00a0×\u00a0${f.height} up to ${f.seconds.toFixed(1)}\u00a0s` : `${f.width}\u00a0×\u00a0${f.height} at ${f.seconds.toFixed(1)}\u00a0s`).join(', or '); const zh = fits.map(f => f.longest ? `${f.width}\u00a0×\u00a0${f.height} 最长 ${f.seconds.toFixed(1)}\u00a0秒` : `${f.width}\u00a0×\u00a0${f.height} 生成 ${f.seconds.toFixed(1)}\u00a0秒`).join(',或 '); - return row('resources', ['This video is too large for the free GPU memory', '这段视频超出了当前可用显存'], - ['Before generating, FreeVideo estimated the GPU memory this video needs, and it is more than the GPU has free right now.', 'FreeVideo 在开始生成前估算了所需显存,这段视频超过了显卡现在空闲的显存。'], - [`This computer can make ${en} right now. Change the duration or total pixels and generate again; closing other programs that use the GPU also frees memory.`, - `现在可以生成:${zh}。请调整时长或总像素后重新生成;关闭其他占用显卡的程序也可以腾出显存。`]); + return row('resources', ['Not enough GPU memory', '显存不够'], + ['Generation needs more GPU memory than is currently available.', '生成所需显存超过当前可用量。'], + [`This computer can make ${en} right now. Change the duration or resolution and generate again; closing other programs that use the GPU also frees memory.`, + `现在可以生成:${zh}。请调整时长或清晰度后重新生成;关闭其他占用显卡的程序也可以腾出显存。`]); } return row('resources', ['The request exceeds available memory', '当前任务的可用内存不足'], - ['Before generating, FreeVideo estimated the GPU memory and RAM this task needs, and it is more than is available right now.', 'FreeVideo 在开始生成前估算了所需的显存和内存,这次任务超过了现在可用的部分。'], - ['Close other memory-heavy programs, or lower the resolution or shorten the duration, and retry. The details show whether GPU memory or RAM ran short, and by how much.', '请关闭其他占用内存的程序,或调低分辨率、缩短时长后重试。详情里写明了是显存还是内存不足,以及还差多少。']); + ['Generation did not start because there is not enough available RAM or GPU memory.', '可用内存或显存不足,本次生成未开始。'], + ['Close memory-heavy applications, or lower the resolution or duration, then retry.', '可关闭占用内存较多的程序,或调低分辨率、缩短时长后重试。']); } if (/commit headroom exhausted|paging file is too small|WinError 1455/i.test(text)) - return row('commit', ['Windows memory allocation limit reached', 'Windows 内存提交额度不足'], - ['Windows cannot back another memory allocation, even if physical RAM is still available.', 'Windows 已没有足够的内存提交额度;即使物理内存还有空闲,也无法再分配内存。'], + return row('commit', ['Not enough Windows memory', 'Windows 内存不足'], + ['Windows cannot provide enough memory for generation right now.', 'Windows 暂时无法提供生成所需的内存。'], ['Close memory-heavy applications and retry. In Windows virtual memory settings, use a system-managed paging file on a drive with free space.', '关闭占用内存较多的程序后重试。在 Windows 虚拟内存设置中,使用系统管理的分页文件,并确保所在磁盘有空闲空间。']); // Refused before it starts: more than this Mac can allocate at all, so closing // other applications cannot help (macos_generate.request_admission). if (/needs at least [\d.]+ GiB of unified memory for .*this Mac can allocate at most/i.test(text)) return row('unified-memory-limit', ['Not enough unified memory', '统一内存不足'], - ['These settings need more unified memory than this Mac can allocate, even with other applications closed.', '这组设置需要的统一内存超过了这台 Mac 可分配的上限,关闭其他程序也无法满足。'], + ['These settings need more memory than the device can provide.', '这组设置需要的内存超过设备上限。'], ['Choose a lower resolution or a shorter duration and retry.', '请降低分辨率或缩短时长后重试。']); if (/MPS backend out of memory|MPS (?:generation|encoding|decoding) reached|No unified-memory allowance|Native generation needs at least .* unified memory/i.test(text)) return row('unified-memory', ['Not enough unified memory', '统一内存不足'], - ['On a Mac, the CPU and GPU share unified memory. This step needed more unified memory than was available at the time.', 'Mac 的 CPU 和 GPU 共用统一内存。这一步需要的统一内存超过了当时可用的量。'], - ['Close memory-heavy applications and retry, or choose a lower resolution or a shorter duration. If you set a memory limit or reserve in Settings, check it as well. If it still fails, export the report.', '请关闭占用内存较多的程序后重试,或降低分辨率、缩短时长。如果在设置中手动设置了内存上限或预留,请一并检查。仍然失败时,请导出报告。']); + ['Generation needed more memory than was available.', '生成所需内存超过当时可用量。'], + ['Close memory-heavy applications, or lower the resolution or duration, then retry. Export the report if it still fails.', '可关闭占用内存较多的程序,或调低分辨率、缩短时长后重试。仍然失败时,请导出报告。']); if (/CUDA out of memory|torch\.OutOfMemoryError|cudaErrorMemoryAllocation/i.test(text)) - return row('vram', ['GPU memory allocation failed', '显存分配失败'], - ['The GPU could not allocate the memory needed at this stage.', '这一阶段的显存申请没有成功。'], - ['Close other GPU workloads and retry so FreeVideo can replan using the newly available memory. Your resolution, duration and steps are kept.', '关闭其他占用显卡的任务后重试,FreeVideo 会根据新的可用显存重新规划,保留你的分辨率、时长和步数。']); + return row('vram', ['GPU memory unavailable', '显存暂不可用'], + ['', ''], + ['Close other programs using the GPU, then retry.', '关闭其他占用显卡的程序后重试。']); const ramReasons = /RAM guard:\s*([a-z_, ]+)\./i.exec(text)?.[1] || ''; if (ramReasons.includes('system_or_commit_pressure') && !ramReasons.includes('working_memory_budget')) return row('ram', ['System memory headroom is too low', '系统可用内存不足'], - ['Memory headroom fell below the safety threshold during execution, so generation stopped. Earlier planning values may be higher.', '运行时测得的系统内存余量低于保护线,生成已停止。此前资源规划时的数值可能更高。'], + ['Generation stopped because available memory was too low.', '可用内存不足,生成已停止。'], ['Close memory-heavy applications and duplicate FreeVideo/ComfyUI instances, then retry. Export the report if it still fails.', '关闭占用内存较多的程序及重复运行的 FreeVideo/ComfyUI 后重试。仍失败时请导出报告。']); if (ramReasons.includes('working_memory_budget') && !ramReasons.includes('system_or_commit_pressure')) - return row('ram', ['The configured RAM limit was reached', '已触及设置的 RAM 限额'], - ['The worker exceeded the enforced RAM budget from a manual limit or selected profile.', '生成进程超过了手动限额或所选配置规定的 RAM 预算。'], - ['Review the RAM limit or selected profile. Automatic mode replans from available memory. Export the report if it still fails.', '检查 RAM 限额或所选配置;自动模式会根据可用内存重新规划。仍失败时请导出报告。']); + return row('ram', ['Memory limit reached', '已达到内存上限'], + ['This generation exceeded the configured memory limit.', '本次生成超过了设置的内存上限。'], + ['Check the memory limit in Settings, or switch back to automatic. Export the report if it still fails.', '请在设置里检查内存上限,或改回自动。仍然失败时,请导出报告。']); if (/RAM guard:|crossed its RAM budget|Insufficient currently available memory/i.test(text)) - return row('ram', ['Available memory or an explicit RAM limit reached', '可用内存或手动 RAM 限额不足'], - ['Automatic RAM estimates can be exceeded while memory is available. This stop indicates system pressure or an explicitly enforced limit.', '自动模式允许在系统仍有余量时超出预估 RAM;停止意味着系统内存压力,或触及了手动设置的硬限制。'], - ['Close memory-heavy applications and retry. If you set a RAM limit for testing, remove or raise it for normal generation. Copy the details below if it still fails.', '关闭占用内存较多的程序后重试。如果设置过测试用 RAM 限制,正常生成时可取消或调高。仍失败时可复制下方详情。']); + return row('ram', ['Memory is low or its limit was reached', '内存不足或已达到上限'], + ['Generation stopped because memory was low or its configured limit was reached.', '内存不足或已达到设置的上限,生成已停止。'], + ['Close memory-heavy applications and retry. If you set a memory limit, remove or raise it. Copy the details below if it still fails.', '关闭占用内存较多的程序后重试;设置过内存上限的,请取消或调高。仍然失败时,可复制下方详情。']); if (/No space left on device|WinError 112|disk (?:is )?full/i.test(text)) return row('disk', ['Disk space is insufficient', '磁盘空间不足'], ['FreeVideo could not write a required file.', 'FreeVideo 无法写入所需文件。'], @@ -351,14 +351,14 @@ export function failureAdvice(value, t, known = knownError(value)) { if (/Model download paused|unexpected-transfer-size|ConnectionError|ConnectTimeout|ReadTimeout|Every download source failed|All Git sources failed|All package sources failed|curl-28/i.test(text)) return row('download', ['Download needs attention', '下载未完成'], ['A transfer failed or its received data did not match the expected file.', '传输中断,或收到的数据与预期文件不一致。'], - ['Check your connection or select another download source in Settings, then retry. Existing download fragments are retained.', '检查网络,或在设置中切换下载源后重试。已有下载片段会保留。']); + ['Choose another download source in Settings and retry; download progress is kept.', '可在设置中切换下载源后重试,下载进度会保留。']); if (/ModuleNotFoundError|No module named|DLL load failed/i.test(text)) - return row('dependencies', ['A runtime dependency could not load', '运行依赖无法加载'], - ['A component FreeVideo needs could not load. This usually means the environment is incomplete or an incompatible environment was selected.', 'FreeVideo 运行所需的组件没能加载,通常是运行环境不完整,或者选择了不兼容的环境。'], - ['In the FreeVideo launcher, open Settings › Environment and click Repair; it checks this installation and restores what is missing. If it still fails, export the report and attach it to a GitHub issue.', '请在 FreeVideo 启动器的“设置 → 环境”里点击“修复”,它会检查这份安装并补齐缺少的组件。仍然失败时,请导出报告并提交到 GitHub issue。']); + return row('dependencies', ['A runtime component could not load', '运行组件无法加载'], + ['A component needed to run FreeVideo could not load.', 'FreeVideo 所需的运行组件未能加载。'], + ['Select "Repair" in the launcher under Settings > Environment. If it still fails, export the report and submit it in a GitHub issue.', '可在启动器“设置 → 环境”里点“修复”。仍然失败时,请导出报告并提交到 GitHub issue。']); return row('unknown', ['Generation stopped', '生成已停止'], - ['FreeVideo hit an internal error, and this generation did not finish.', 'FreeVideo 内部出错,这次生成没有完成。'], - ['Click Export report and attach the report to a GitHub issue; we will look into it and fix the problem for you.', '请点击“导出报告”,并把报告提交到 GitHub issue,我们会专门排查,为您解决这个问题。']); + ['This generation did not finish.', '这次生成未完成。'], + ['Click Export report and submit it in a GitHub issue; we will look into it.', '请点“导出报告”,提交到 GitHub issue,我们会专门排查。']); } let activeDialog = null; @@ -462,7 +462,7 @@ export function createErrorPanel(t) { // disabled by the browser or the page is served over plain HTTP. disclosure.open = true; details.value = JSON.stringify(currentReport, null, 2); details.focus(); details.select(); - downloadStatus.textContent = t('Download unavailable. Use Copy error to copy the report below.', '浏览器未能下载,请点击“复制错误”复制下方完整报告。'); + downloadStatus.textContent = t('The download did not finish; select "Copy error" to copy the report below.', '下载未完成,可点“复制错误”复制下方报告。'); } finally { link?.remove(); if (url) setTimeout(() => URL.revokeObjectURL(url), 30000); @@ -477,15 +477,15 @@ export function createErrorPanel(t) { : failureAdvice(report.details, t, report.known_error ?? null); if (report.stage === 'submission' && advice.kind === 'unknown') { advice.title = t('The task could not be submitted', '任务提交失败'); - advice.summary = t('The task submission returned an error. Its details are shown below.', '提交任务时返回了错误,具体原因显示在下方。'); + advice.summary = t('The task could not be submitted; see the error details below.', '任务未提交成功,可查看下方错误详情。'); } if (report.stage === 'queue' && advice.kind === 'unknown') { advice.title = t('The queue operation did not complete', '队列操作未完成'); advice.summary = t('Check that ComfyUI is still running and connected, then retry.', '请确认 ComfyUI 仍在运行、连接正常后重试。'); } if (advice.kind === 'unknown' && ['submission', 'queue'].includes(report.stage)) - advice.action = t('Copy the error details when reporting the issue; they include the failed stage when available.', '反馈问题时请复制错误详情,其中会保留已记录的失败阶段。'); - title.textContent = advice.title; explanation.textContent = advice.summary; action.textContent = advice.action; + advice.action = t('Select "Copy error" to keep the error details.', '可点“复制错误”保留错误详情。'); + title.textContent = advice.title; explanation.textContent = advice.summary; explanation.hidden = !advice.summary; action.textContent = advice.action; element.querySelector('.fv-extension-steps')?.remove(); if (advice.kind === 'browser-extension') action.after(extensionSteps(advice)); element.freevideoKind = advice.kind; reload.hidden = advice.kind !== 'browser-extension'; @@ -497,7 +497,7 @@ export function createErrorPanel(t) { } action.hidden = !advice.action; const stage = report.stage === 'submission' ? t('Submitting task', '提交任务') - : report.stage === 'queue' ? t('Queue operation', '队列操作') : t('Executing workflow', '执行工作流'); + : report.stage === 'queue' ? t('Queue operation', '队列操作') : t('Generating', '正在生成'); location.textContent = stage + (report.http_status ? ` · HTTP ${report.http_status}` : '') + report.nodes.map(node => ` · ${node.type || t('Node', '节点')} #${node.id}`).join(''); const reasons = report.nodes.flatMap(node => node.errors.map(error => @@ -516,15 +516,15 @@ export function createErrorPanel(t) { downloadStatus.textContent = ''; download.hidden = diagnosticReports.has(report); if (internal && download.hidden) - action.textContent = t('Click Download report in the preview or node panel and attach the report to a GitHub issue; we will look into it and fix the problem for you.', - '请在预览区或节点面板中点击“下载报告”,并把报告提交到 GitHub issue,我们会专门排查,为您解决这个问题。'); + action.textContent = t('Use Download report in the preview and submit it in a GitHub issue; we will look into it.', + '请点预览区的“下载报告”,提交到 GitHub issue,我们会专门排查。'); else if (download.hidden) { // Advice that ends with "export the report" points at the report this card does offer. for (const [from, to] of EXPORT_TO_DOWNLOAD) if (action.textContent.includes(from)) action.textContent = action.textContent.replace(from, to); } privacy.textContent = download.hidden - ? t('Use Download report in the preview or node panel for the diagnostic report.', '完整诊断请使用预览区或节点面板中的“下载报告”。') + ? t('Select "Download report" in the preview to save the full diagnostic information.', '可在预览区点“下载报告”保存完整诊断信息。') : t('Paths and typed text are removed.', '已去掉路径和输入的文字。'); copy.textContent = advice.kind === 'sampling-table' ? t('Copy details', '复制详情') : t('Copy error', '复制错误'); if (popup) showFailureDialog(report, t); diff --git a/web/freevideo.js b/web/freevideo.js index 5957ada0..e6b301f4 100644 --- a/web/freevideo.js +++ b/web/freevideo.js @@ -73,7 +73,7 @@ function mediaKind(name) { throw new Error(text("Choose an image, video or audio file", "请选择图片、视频或音频文件")); } -const referenceHelp = () => text('When one input receives several images, each counts as one reference.', '一个接口接入多张图片时,每张各算一个参考素材。'); +const referenceHelp = () => text('Each reference image counts as one media item.', '每张参考图片各占一个素材名额。'); // Input names stay English like every other node's; the panel's chips are translated. const referenceSocket = index => `Reference image ${index + 1}`; const referenceLabel = index => text(`Reference image ${index + 1}`, `参考图片 ${index + 1}`); @@ -184,7 +184,7 @@ function mediaPanel(node, mount = null) { try { outcome = await openImageEditor({source, name: source, target: videoSize(), edit: original.edit || agent?.edit || null, role: original.role, t: text, resolve: viewURL, upload: uploadOne, library, tool}); - } catch (error) { message(text('The image could not be opened for editing.', '这张图片无法打开编辑。'), true); return; } + } catch (error) { message(text('This image cannot be edited right now; choose an image again.', '图片暂时无法编辑,可重新选择图片。'), true); return; } if (!outcome) return; // The cards may have changed meanwhile; find this one again by its file. const rows = read(), at = rows.findIndex(row => row.file === file); @@ -202,8 +202,8 @@ function mediaPanel(node, mount = null) { if (index < 0) return; const hasOriginal = outcome.edit.layers.some(layer => layer.id === 'base'); latest[index] = {...latest[index], file: result.file, ...(hasOriginal ? {source} : {}), edit: outcome.edit}; update(latest); - message(hasOriginal ? text(`Saved the edited image, ${outcome.width}×${outcome.height}. The original is kept.`, - `已保存编辑后的图片(${outcome.width}×${outcome.height}),原图保留。`) : ''); + message(hasOriginal ? text('The edited image is saved, and the original is kept.', + '编辑后的图片已保存,原图已保留。') : ''); } catch (error) { message(error.message, true); } finally { node.isUploading = false; add.disabled = false; addAudio.disabled = false; } } @@ -215,7 +215,7 @@ function mediaPanel(node, mount = null) { try { outcome = await openImageEditor({target: videoSize(), role: role.value, t: text, resolve: viewURL, upload: uploadOne, library}); - } catch { message(text('The image could not be opened for editing.', '这张图片无法打开编辑。'), true); return; } + } catch { message(text('This image cannot be edited right now; choose an image again.', '图片暂时无法编辑,可重新选择图片。'), true); return; } if (!outcome?.blob || read().length >= 32) return; node.isUploading = true; add.disabled = addAudio.disabled = generateImage.disabled = true; try { @@ -252,8 +252,8 @@ function mediaPanel(node, mount = null) { const audioRefs = active.filter(row => row.role === 'reference' && mediaKind(row.file) === 'audio').length + Number(linked('reference_audio')); const refs = active.filter(row => row.role === "reference").length + referenceInputs.filter(linked).length + Number(linked('reference_audio')); audioHelp.hidden = !audioRefs; - audioHelp.textContent = text('Use