From 3442d3f173e03a7f72b6431c5fd15e8804dfde91 Mon Sep 17 00:00:00 2001 From: Bowen Xue Date: Wed, 21 Oct 2026 05:23:10 +0000 Subject: [PATCH 1/8] Page check: a dimming or click-away layer over the toolbar is not a fault The toolbar was reported as covered whenever any element sat on top of it that was not a dialog, menu or toast. ComfyUI's own shade behind its Templates dialog and the click-away layer of its Help panel are not in that list, yet the toolbar stays visible beneath both. Only a layer that paints over the toolbar now counts as covering it: an opaque background, a background image, or a canvas, video, image or frame. The page check details also name the element that covers the toolbar. --- web/health.js | 38 ++++++++++++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/web/health.js b/web/health.js index 81add2c2..55ba9eff 100644 --- a/web/health.js +++ b/web/health.js @@ -344,12 +344,36 @@ function onScreen(element) { // canvas: inside the window, sized, and invisible. Look at what is on top of // its view switch; a dialog, menu, toast or this card only passes over it. const OVERLAYS = 'dialog, [role="dialog"], [role="menu"], .p-dialog-mask, .p-overlay-mask, .p-toast, .litecontextmenu, .fv-health'; -function covered(bar) { +// A layer hides the toolbar only if it paints over it. ComfyUI's dimming +// backdrops and click-away layers (a dialog's shade, the help panel's) leave it +// visible underneath, so they are not a fault. +const SOLID = /^(canvas|video|img|iframe|object|embed|svg)$/i; +function alphaOf(color) { + if (color === 'transparent') return 0; + const slash = color.match(/\/\s*([\d.]+)(%?)\s*\)/); + if (slash) return Number(slash[1]) / (slash[2] ? 100 : 1); + const legacy = color.match(/^rgba\(([^)]*)\)/); + const parts = legacy ? legacy[1].split(',') : []; + return parts.length === 4 ? Number(parts[3]) : 1; +} +function paints(element) { + const style = getComputedStyle(element); + if (style.visibility === 'hidden' || Number(style.opacity) === 0) return false; + return SOLID.test(element.tagName) || style.backgroundImage !== 'none' + || alphaOf(style.backgroundColor) * Number(style.opacity) >= 0.9; +} +function coveredBy(bar) { const box = (bar.querySelector('.fv-view-switch') || bar).getBoundingClientRect(); - const top = document.elementFromPoint(Math.min(innerWidth - 1, Math.max(0, box.left + box.width / 2)), + const stack = document.elementsFromPoint(Math.min(innerWidth - 1, Math.max(0, box.left + box.width / 2)), Math.min(innerHeight - 1, Math.max(0, box.top + box.height / 2))); - return !!top && !bar.contains(top) && !top.closest(OVERLAYS); + for (const top of stack) { + if (bar.contains(top) || top.closest(OVERLAYS)) return null; + if (paints(top)) return top; + } + return null; } +const nameOf = element => `${element.tagName.toLowerCase()}${element.id ? `#${element.id}` : ''}` + + `${[...element.classList].slice(0, 4).map(name => `.${name}`).join('')} (z-index ${getComputedStyle(element).zIndex})`; // A video enlarged to full screen (the maximize button on a node's video, or // the player's own) hides the rest of the page until it is left again. @@ -376,10 +400,12 @@ function workspaceProblem(app) { : {text: t('The creative workspace is open but cannot be seen on the page.', '创作面板已经打开,但页面上看不到。'), advice: 'reload'}; const bar = document.querySelector('.fv-view-navigation'); if (!bar) return {text: t('The FreeVideo toolbar that opens the creative workspace is missing from the page.', '页面上没有打开创作面板的 FreeVideo 工具栏。'), advice: 'reload'}; - if (onScreen(bar) && !covered(bar)) return null; + const cover = onScreen(bar) ? coveredBy(bar) : null; + if (onScreen(bar) && !cover) return null; // Its own stylesheet places it above the canvas. return sheetLoaded('view_navigation.css') - ? {text: t('The FreeVideo toolbar that opens the creative workspace is hidden or covered by other page elements.', '打开创作面板的 FreeVideo 工具栏被页面上的其他元素隐藏或遮挡了。'), advice: 'plugin'} + ? {text: t('The FreeVideo toolbar that opens the creative workspace is hidden or covered by other page elements.', '打开创作面板的 FreeVideo 工具栏被页面上的其他元素隐藏或遮挡了。'), advice: 'plugin', + ...(cover && {cover: nameOf(cover)})} : {text: t('The FreeVideo toolbar that opens the creative workspace cannot be seen: its stylesheet view_navigation.css did not load.', '看不到打开创作面板的 FreeVideo 工具栏:样式文件 view_navigation.css 没有加载。'), advice: 'reload', sheet: 'view_navigation.css'}; } @@ -451,7 +477,7 @@ function details(problems) { `ComfyUI frontend: ${window.__COMFYUI_FRONTEND_VERSION__ || 'unknown'}`, `FreeVideo UI: ${JSON.stringify(window.FreeVideoUI || {})}`, `Automatic reload: ${retried ? new Date(Number(retryRecord.at)).toISOString() : retryRecord ? 'not needed' : 'unavailable (no session storage)'}`, - '', 'Problems:', ...problems.map(p => `- ${p.text}`), + '', 'Problems:', ...problems.flatMap(p => [`- ${p.text}`, ...(p.cover ? [` covered by: ${p.cover}`] : [])]), ...(checks.length ? ['', 'Load check:', ...checks.map(row => `- ${row}`)] : []), ...(cache.length ? ['', 'Browser cache:', ...cache.map(row => `- ${row}`)] : []), ...(before.length ? ['', 'Browser cache before the automatic reload:', ...before.map(row => `- ${row}`)] : []), From 2d1a7ffd12d524cfc40cd75e3805914df754d2d5 Mon Sep 17 00:00:00 2001 From: Bowen Xue Date: Wed, 21 Oct 2026 07:45:12 +0000 Subject: [PATCH 2/8] Add a note on security software blocking the Windows launcher --- README.md | 2 ++ README.zh-CN.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/README.md b/README.md index 7c841d18..95d443ec 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,8 @@ It coordinates VRAM, system memory and disk, adapting weight placement, compute FreeVideo creative workspace +**Security software:** FreeVideo.exe is code-signed. If security software blocks it or quarantines files in the installation folder, compare the file's SHA-256 with the value shown for the download on the release page, restore the file, and add the FreeVideo folder to the software's exclusion list. + **Offline installation:** Download the packages from [Quark](https://pan.quark.cn/s/c51235b84618) and drag the ZIP files into the launcher without extracting them. A fully offline installation needs the four model packs (video model, text encoder, video & audio decoder, and sampling caches), plus the environment package for a new ComfyUI installation. After importing only the environment package, you can also choose **Automatic download** for the models (v0.3.12 or newer). The audio reference cache is optional and is used only when a reference includes audio. ### macOS (Apple silicon preview) diff --git a/README.zh-CN.md b/README.zh-CN.md index 5d40f748..7807fcde 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -54,6 +54,8 @@ FreeVideo 是面向消费级显卡的 MiniMax H3 本地推理引擎,基于 [Op FreeVideo 创作面板 +**安全软件:** FreeVideo.exe 已进行代码签名。如果安全软件拦截该文件,或隔离了安装目录中的文件,请先将文件的 SHA-256 与发布页中该下载文件显示的值核对,一致后恢复文件,并把 FreeVideo 文件夹加入该软件的排除列表。 + **离线安装:** 从[夸克网盘](https://pan.quark.cn/s/c51235b84618)下载离线包,将 ZIP 文件直接拖入启动器,无需解压。完全离线安装需要「视频模型」「文本编码器」「视频与音频解码器」「采样缓存」四个模型包,全新安装 ComfyUI 时还需要「运行环境」包。也可以只导入「运行环境」包,再在模型来源中选择**自动下载**(v0.3.12 及以上)。「音频参考缓存」为可选包,仅在参考素材带有音频时使用。 ### macOS(Apple 芯片预览版) From 7da6fdfa698eb2a1c6364ed81ac5c127e8278ace Mon Sep 17 00:00:00 2001 From: Bowen Xue Date: Fri, 23 Oct 2026 19:18:44 +0000 Subject: [PATCH 3/8] Name FreeVideo.exe in the security software note and say which files to restore --- README.md | 2 +- README.zh-CN.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 95d443ec..b4d52a8b 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ It coordinates VRAM, system memory and disk, adapting weight placement, compute FreeVideo creative workspace -**Security software:** FreeVideo.exe is code-signed. If security software blocks it or quarantines files in the installation folder, compare the file's SHA-256 with the value shown for the download on the release page, restore the file, and add the FreeVideo folder to the software's exclusion list. +**Security software:** FreeVideo.exe is code-signed. If security software blocks FreeVideo.exe or quarantines files in the FreeVideo folder, check that the SHA-256 of FreeVideo.exe matches the value on the release page, then restore the files and add the FreeVideo folder to the software's exclusion list. **Offline installation:** Download the packages from [Quark](https://pan.quark.cn/s/c51235b84618) and drag the ZIP files into the launcher without extracting them. A fully offline installation needs the four model packs (video model, text encoder, video & audio decoder, and sampling caches), plus the environment package for a new ComfyUI installation. After importing only the environment package, you can also choose **Automatic download** for the models (v0.3.12 or newer). The audio reference cache is optional and is used only when a reference includes audio. diff --git a/README.zh-CN.md b/README.zh-CN.md index 7807fcde..c1f7a693 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -54,7 +54,7 @@ FreeVideo 是面向消费级显卡的 MiniMax H3 本地推理引擎,基于 [Op FreeVideo 创作面板 -**安全软件:** FreeVideo.exe 已进行代码签名。如果安全软件拦截该文件,或隔离了安装目录中的文件,请先将文件的 SHA-256 与发布页中该下载文件显示的值核对,一致后恢复文件,并把 FreeVideo 文件夹加入该软件的排除列表。 +**安全软件:** FreeVideo.exe 已进行代码签名。如果安全软件拦截 FreeVideo.exe,或隔离了 FreeVideo 文件夹中的文件,请先核对 FreeVideo.exe 的 SHA-256 与发布页列出的值一致,再恢复文件,并把 FreeVideo 文件夹加入该软件的排除列表。 **离线安装:** 从[夸克网盘](https://pan.quark.cn/s/c51235b84618)下载离线包,将 ZIP 文件直接拖入启动器,无需解压。完全离线安装需要「视频模型」「文本编码器」「视频与音频解码器」「采样缓存」四个模型包,全新安装 ComfyUI 时还需要「运行环境」包。也可以只导入「运行环境」包,再在模型来源中选择**自动下载**(v0.3.12 及以上)。「音频参考缓存」为可选包,仅在参考素材带有音频时使用。 From 89da9ea29efdff5d372d121c4b2c783b0ac71807 Mon Sep 17 00:00:00 2001 From: Bowen Xue Date: Wed, 21 Oct 2026 06:44:25 +0000 Subject: [PATCH 4/8] Agent first-frame staging: scene reference, character cards, framing and facing, with a... Agent first-frame staging: scene reference, character cards, framing and facing, with a scene-first prompt in place of the pasted layout --- freevideo_engine/agent/guide.py | 4 +- freevideo_engine/agent/jobs.py | 25 ++++++++ freevideo_engine/agent/mcp.py | 2 +- freevideo_engine/agent/schema.py | 25 +++++--- freevideo_engine/agent/spec.py | 8 ++- freevideo_engine/image_gen/compose.py | 83 ++++++++++----------------- freevideo_engine/image_gen/staging.py | 65 +++++++++++++++++++++ 7 files changed, 146 insertions(+), 66 deletions(-) create mode 100644 freevideo_engine/image_gen/staging.py diff --git a/freevideo_engine/agent/guide.py b/freevideo_engine/agent/guide.py index 4bada091..09ed9a1d 100644 --- a/freevideo_engine/agent/guide.py +++ b/freevideo_engine/agent/guide.py @@ -242,7 +242,9 @@ 'Pictures. When the user wants a picture, or a first frame or reference picture that does not exist yet, make ' 'it with freevideo_generate_image; to change a picture, pass it in images. The result is an input file: use ' 'result.file as a first frame, as a reference, or in images again. When where people stand in the shot matters, ' - 'make the first frame with people (cast pictures and places) and generate the video from it.', + 'make the first frame with scene_image and people (cast card IDs or pictures, x, height, facing left/right, ' + 'framing full/half), then generate the video from result.file. Use dimensions divisible by 32. ' + 'This controls the scene, requested framing and approximate positions; face similarity is not guaranteed.', 'Cut by default. Wherever the film can change shot, make the new shot from the reference pictures it should ' 'have (cast cards, scene pictures) instead of continuing the previous video. Continue a video only for a ' 'long take that cannot be cut, an action that must stay unbroken, or sound that must stay unbroken, and ' diff --git a/freevideo_engine/agent/jobs.py b/freevideo_engine/agent/jobs.py index 827e42b8..73da1fd6 100644 --- a/freevideo_engine/agent/jobs.py +++ b/freevideo_engine/agent/jobs.py @@ -797,6 +797,7 @@ def submit_request(self, value, *, client=None, key=None, parent=None, editor=No item = substitute(item, names) try: normalized = specs.normalize(item, port=_Warmed(self.port)) + self.resolve_image_cast(normalized) except specs.SpecError as error: problems.extend(schema.prefix_diagnostic(e, prefix) for e in error.errors) continue @@ -1853,6 +1854,30 @@ def generation_failure(self, row, chain): return failure # Local image generation ---------------------------------------------------------------------------- + def resolve_image_cast(self, spec): + """Pin character card pictures before saving the job, including across restarts/replays.""" + if spec['kind'] != 'image': + return + for i, person in enumerate(spec.get('people', [])): + identity = person.get('cast') + if identity is None: + continue + path = '/people/%d/cast' % i + try: + card = self.cast.get(identity) + except JobError as error: + if error.code != 'not_found': + raise + specs.fail(path, 'an existing character card ID from GET /freevideo/agent/v1/cast') + if card['kind'] != 'character': + specs.fail(path, 'a character card, rather than a setting card') + picture = next((row for row in card['images'] if row['label'] == 'front'), None) + if picture is None: + specs.fail(path, 'a character card with a front picture; add or generate one first') + person.pop('cast') + person['image'] = picture['media']['file'] + person.setdefault('name', card['name'][:40]) + def wait_image(self, row, code, *, waiting_for=None): if code not in IMAGE_WAIT_ERRORS: return False diff --git a/freevideo_engine/agent/mcp.py b/freevideo_engine/agent/mcp.py index e94b1f88..956f4e1e 100644 --- a/freevideo_engine/agent/mcp.py +++ b/freevideo_engine/agent/mcp.py @@ -50,7 +50,7 @@ def tool_list(): dict(name='freevideo_schema', title='Job JSON Schemas', description='JSON Schemas of every job kind and of image edit recipes.', annotations=read), dict(name='freevideo_plan', title='Plan a job', description="Validate a job without running it: actual frames/seconds, sampling passes, warnings and a time estimate from this computer's history. Same fields as freevideo_generate.", annotations=read), dict(name='freevideo_generate', title='Generate a video', description='Generate one video from an H3 prompt, up to 60 seconds. Under 30 s defaults to one generation if the device supports this size and quality; unknown limits use 15 s. Otherwise the video is cut into as few parts as possible, each at least 10 s, and how many parts depends on how long this computer can generate in one run; the parts continue and join automatically. Prefer a new shot with its reference pictures wherever the film can cut; continue only a take, action or sound that must stay unbroken. Use this for any single video, also after freevideo_enhance_prompt; the drama tools are only for a film of several separately chosen shots. Optionally from first/last frames or references (files, local paths, or frames of earlier jobs). Returns the job; with wait, its result (video path and URL).', annotations=write), - dict(name='freevideo_generate_image', title='Generate a picture', description='Generate a picture from a prompt; with images, change or combine input pictures; with people, make a first frame with cast members standing where you say. The result is an input file to use as a first frame, as a reference, or in images again.', annotations=write), + dict(name='freevideo_generate_image', title='Generate a picture', description='Generate a picture from a prompt; with images, change or combine input pictures; with people and scene_image, make a first frame with the scene and the people near the places you say. The result is an input file to use as a first frame, as a reference, or in images again.', annotations=write), dict(name='freevideo_submit_batch', title='Submit a storyboard', description='Submit several jobs at once. Each row is a job object with "kind" (generate default, assemble, vlm, enhance, image) and an optional "ref"; later rows use "@ref" wherever a job id is expected (from_job, after, clips, refine_from). FreeVideo runs them in dependency order.', annotations=write), dict(name='freevideo_job', title='Job status', description='Status, progress, result or error of a job.', annotations=read), dict(name='freevideo_wait', title='Wait for a job or the user', description="Wait at most 25 seconds for a job (with id) or, without id, at most 50 seconds for what the user says in FreeVideo (inbox). again=true means nothing new yet: call it again. A job wait returns early when the user says something. If the job is still active, call it again with the same ID; do not resubmit.", annotations=read), diff --git a/freevideo_engine/agent/schema.py b/freevideo_engine/agent/schema.py index eb07103d..50552475 100644 --- a/freevideo_engine/agent/schema.py +++ b/freevideo_engine/agent/schema.py @@ -228,20 +228,28 @@ def vlm(*, local=False): def image(*, local=False): return obj(dict(kind={'const': 'image', 'type': 'string'}, - prompt=dict(type='string', minLength=1, maxLength=2000), + prompt=dict(type='string', minLength=1, maxLength=2000, + description='Scene description; required unless scene_image supplies the scene.'), width=number(256, 2048, integer=True), height=number(256, 2048, integer=True), aspect=enum(IMAGE_ASPECTS, description='Instead of width and height; 1:1 when all three are omitted.'), seed=nullable(number(-2**63, 2**64 - 1, integer=True)), images=array({'oneOf': [FILE, obj(dict(file=FILE), ('file',))]}, high=4, description='Pictures to change ' 'or combine: input files, such as result.file of an earlier picture job. The prompt calls them image 1, image 2.'), - people=array(obj(dict(image=FILE, name=string(40), x=number(0, 1), height=number(.05, 1), feet=number(0, 1)), - ('image',)), 1, 3, description='Makes a first frame with people standing where you say. image is ' - "a full-body picture on a plain backdrop, such as a cast card's front picture. From 0 to 1: x is the person's " - 'centre across the width (default 0.5), height their size (0.62) and feet where they stand down the height ' - '(0.9). The prompt describes the scene, or scene_image shows it. Not together with images.'), + people=array(obj(dict(image=FILE, cast=CAST, name=string(40), x=number(0, 1), height=number(.05, 1), + feet=number(0, 1), facing=enum(('left', 'right')), + framing=enum(('full', 'half'), default='full')), + oneOf=[{'required': ['image']}, {'required': ['cast']}]), 1, 3, + description='First frame with people near the places you say; faces may differ. Give image (an input ' + 'file) or cast (a character card ID; its front picture is used). From 0 to 1: x is the ' + "centre across the width (default 0.5), height the visible figure over the frame height (0.62; head to waist " + 'for half), feet the figure bottom from the top (default max(0.9, height)). facing is the screen left/right; ' + 'framing is full or half. Width and height: multiples of 32. The prompt or scene_image gives the scene. Not with images.'), scene_image=FILE, **common()), - ('kind', 'prompt'), allOf=[{'if': {'required': ['width']}, 'then': {'required': ['height']}}, + ('kind',), allOf=[{'if': {'not': {'required': ['scene_image']}}, 'then': {'required': ['prompt']}}, + {'if': {'required': ['people']}, 'then': {'properties': { + key: number(256, 2048, integer=True, multipleOf=32) for key in ('width', 'height')}}}, + {'if': {'required': ['width']}, 'then': {'required': ['height']}}, {'if': {'required': ['height']}, 'then': {'required': ['width']}}, {'if': {'required': ['aspect']}, 'then': {'not': {'anyOf': [{'required': ['width']}, {'required': ['height']}]}}}], @@ -1180,6 +1188,9 @@ def bad(field, allowed, got=None): bad('images', 'omitted when people is supplied', value['images']) if value.get('scene_image') and not value.get('people'): bad('scene_image', 'omitted unless people is supplied', value['scene_image']) + for i, person in enumerate(value.get('people', [])): + if 'feet' in person and person['feet'] < person.get('height', .62): + bad('people/%d/feet' % i, 'at least the figure height, so the figure fits inside the frame', person['feet']) if kind == 'generate': if value.get('continue_from') and isinstance(value.get('media'), dict) and any( v is not None for k, v in value['media'].items() if k != 'references'): diff --git a/freevideo_engine/agent/spec.py b/freevideo_engine/agent/spec.py index a5d51c08..72d221fd 100644 --- a/freevideo_engine/agent/spec.py +++ b/freevideo_engine/agent/spec.py @@ -269,13 +269,15 @@ def normalize(value, *, port=None): elif kind == 'image': width, height = ((value['width'], value['height']) if 'width' in value else contract.IMAGE_ASPECTS[value.get('aspect', '1:1')]) - result.update(prompt=value['prompt'], width=width, height=height, + result.update(prompt=value.get('prompt', ''), width=width, height=height, images=[item if isinstance(item, str) else item['file'] for item in value.get('images', [])]) if value.get('seed') is not None: result['seed'] = value['seed'] if value.get('people'): - result['people'] = [dict(image=person['image'], x=person.get('x', .5), height=person.get('height', .62), - feet=person.get('feet', .9), **({'name': person['name']} if person.get('name') else {})) + result['people'] = [dict({k: person[k] for k in ('image', 'cast', 'name', 'facing') if k in person}, + x=person.get('x', .5), height=person.get('height', .62), + framing=person.get('framing', 'full'), + feet=person.get('feet', max(.9, person.get('height', .62)))) for person in value['people']] if value.get('scene_image'): result['scene_image'] = value['scene_image'] diff --git a/freevideo_engine/image_gen/compose.py b/freevideo_engine/image_gen/compose.py index 5272b6bd..0dc06538 100644 --- a/freevideo_engine/image_gen/compose.py +++ b/freevideo_engine/image_gen/compose.py @@ -1,14 +1,13 @@ -"""First frames with placed people: a scene, some character pictures and where each one stands. +"""First frames from a scene followed by character references and numeric positions. -With a scene picture the people are cut out of their plain backdrops and pasted at the asked places; -the result is a rough layout, and the image model renders it as one clean photograph (the layout is -passed as a reference, so the places and sizes carry over). With only a scene description the same -places are written into the prompt. Either way the answer is a normal generate request. +The default uses a scene-first prompt (pos2), without writing a collage. It aims +for the scene, requested framing and approximate positions; face similarity and +exact coordinates are not guaranteed. Collage helpers remain available for experiments. """ from pathlib import Path -import uuid from PIL import Image +from .staging import position_prompt, scene_prompt MAX_PEOPLE = 3 # the model takes 4 pictures: the people (or none) plus the layout or the scene TOLERANCE = 9 @@ -78,51 +77,39 @@ def _check(people): raise ValueError('invalid_media') x = person.get('x', .5) tall = person.get('height', .62) - feet = person.get('feet', .9) + feet = person.get('feet', max(.9, tall) if type(tall) in (int, float) else .9) if any(type(v) not in (int, float) or not 0 <= v <= 1 for v in (x, tall, feet)) or tall < .05: raise ValueError('invalid_request') + if feet < tall or person.get('facing', 'right') not in ('left', 'right') \ + or person.get('framing', 'full') not in ('full', 'half'): + raise ValueError('invalid_request') rows.append(dict(image=person['image'], x=float(x), height=float(tall), feet=float(feet), - name=str(person.get('name') or '')[:40])) + name=str(person.get('name') or '')[:40], + framing=person.get('framing', 'full'), + **({'facing': person['facing']} if 'facing' in person else {}))) return rows -def _where(x): - return 'on the left' if x < .38 else 'on the right' if x > .62 else 'in the middle' - - -def _positions(people): - """Left-to-right sentence; ordering words place the people better than percentages do.""" - order = sorted(range(len(people)), key=lambda i: people[i]['x']) - if len(people) == 1: - return 'The person shown in image 1 stands %s of the frame, full body, feet on the floor.' % _where(people[0]['x']) - words = ['on the left', 'in the middle', 'on the right'] if len(people) == 3 else ['on the left', 'on the right'] - rows = ['%s stands the person shown in image %d' % (word, i + 1) for word, i in zip(words, order)] - rows[0] = rows[0][0].upper() + rows[0][1:] - return '; '.join(rows) + '. Show everyone head to toe, standing apart with empty floor between them, with natural shadows.' - - def build(value, input_directory): """A generate request for a first frame. value: width, height, people=[{image, x, height, feet, name}], and the scene as scene_image (a file name) or scene (text). Pictures are file names in the input directory - (or freevideo-image/); the layout picture, if any, is written to freevideo-image/.""" + (or freevideo-image/). facing=left/right and framing=full/half are optional per person.""" if not isinstance(value, dict): raise ValueError('invalid_request') people = _check(value.get('people')) width, height = value.get('width'), value.get('height') - if any(type(v) is not int or not 256 <= v <= 2048 for v in (width, height)): + if any(type(v) is not int or not 256 <= v <= 2048 or v % 32 for v in (width, height)): raise ValueError('invalid_dimensions') - width, height = width // 32 * 32, height // 32 * 32 text = value.get('scene') if isinstance(value.get('scene'), str) else '' directory = Path(input_directory).resolve() def load(name): folder, _, leaf = name.rpartition('/') - if not leaf or folder not in ('', 'freevideo-image') or any(c in name for c in ('\\', ':', '\0')): + if not leaf or leaf in ('.', '..') or folder not in ('', 'freevideo-image') or any(c in name for c in ('\\', ':', '\0')): raise ValueError('invalid_media') path = (directory / folder / leaf) if folder else directory / leaf - parent = directory / folder if folder else directory - if (parent.is_symlink() or path.is_symlink() or not path.is_file() - or path.resolve().parent != parent): + if path.parent.is_symlink() or path.is_symlink() or not path.is_file() \ + or path.resolve().parent != (directory / folder if folder else directory): raise ValueError('invalid_media') try: with Image.open(path) as image: @@ -132,34 +119,22 @@ def load(name): names = [person['image'] for person in people] scene_name = value.get('scene_image') - intro = ' '.join('Image %d is the character %s.' % (i + 1, p['name'] or str(i + 1)) for i, p in enumerate(people)) if isinstance(scene_name, str) and scene_name: - layout = collage(load(scene_name), [(load(p['image']), p['x'], p['height'], p['feet']) for p in people], - width, height) - folder = directory / 'freevideo-image' - if folder.is_symlink() or folder.resolve().parent != directory: - raise ValueError('invalid_media') - leaf = 'layout-' + uuid.uuid4().hex + '.png' - try: - folder.mkdir(exist_ok=True) - layout.save(folder / leaf) - except OSError as error: - # A runtime layout failure must reach the service's diagnostic-report - # path, rather than being returned as a malformed request. - raise ValueError('generation_failed') from error - names.append('freevideo-image/' + leaf) - n = len(people) - prompt = ('%s Image %d is a rough layout with the characters pasted on the scene. Render image %d as one clean ' - 'photograph of the same place with the same people. Keep the scene, keep everyone at exactly the same ' - 'position and size, keep each face, hairstyle and clothes as in their own image. Remove every cut-out ' - 'edge and white outline. Relight everyone to the lighting of the room, ground them with soft contact ' - 'shadows on the floor and slight floor reflections so they stand in the space instead of floating.' - % (intro, n + 1, n + 1)) + names.insert(0, scene_name) + prompt = scene_prompt(people) else: if not text.strip(): raise ValueError('invalid_prompt') - prompt = '%s %s Keep each face and outfit exactly as in their own image. %s' % ( - intro, _positions(people), 'The scene: ' + text.strip().rstrip('.') + '.') + first_image = 1 + prompt = 'The scene: ' + text.strip().rstrip('.') + '. ' + prompt += ' '.join('Image %d is a character reference%s.' % + (i, ' for ' + p['name'] if p['name'] else '') for i, p in enumerate(people, first_image)) + prompt += ' ' + position_prompt(people, first_image=first_image) + prompt += (' Render one natural photograph with only these %d people, matching the scene lighting, ' + 'with soft contact shadows and no cut-out edges. Use the character references for appearance and clothing.' + % len(people)) + for name in names: + load(name) if len(prompt) > 2000: raise ValueError('invalid_prompt') return dict(prompt=prompt, width=width, height=height, seed=value.get('seed'), images=names) diff --git a/freevideo_engine/image_gen/staging.py b/freevideo_engine/image_gen/staging.py new file mode 100644 index 00000000..b0fdca2a --- /dev/null +++ b/freevideo_engine/image_gen/staging.py @@ -0,0 +1,65 @@ +"""Pure coordinate-to-prompt mapping for first frames; no files or model calls. + +Keep this policy separate from compose's reference loading so later staging +experiments can replace it without changing the Agent request/result contract. +People are validated by compose before reaching this function. +""" + + +def scene_prompt(people): + """The measured pos2 wording, with scene first and integer percentages. + + Equal feet and unequal full-body heights express the two-person depth cue + from pos2 shot 8. Reference order never changes to describe that depth. + Explicit framing/facing extensions follow the complete baseline prompt. + """ + labels = [person.get('name', '').strip() or f'person {i}' + for i, person in enumerate(people, 1)] + sentences = [ + 'Image 1 is an empty scene; the background of the result must be exactly this scene ' + '(its walls, floor, windows and lighting), not the plain backdrop of the character photos.'] + sentences.extend(f'Image {i} is the character {name}.' for i, name in enumerate(labels, 2)) + positions = [f'{name} centred at {person["x"] * 100:.0f} percent of the image width, ' + f'full body, about {person["height"] * 100:.0f} percent of the image height' + for name, person in zip(labels, people)] + feet = [f'{person["feet"] * 100:.0f}' for person in people] + if len(set(feet)) == 1: + positions.append(f'all feet at about {feet[0]} percent of the image height, whole body from head to shoes') + else: + positions.extend(f'{name} feet at about {foot} percent of the image height, whole body from head to shoes' + for name, foot in zip(labels, feet)) + sentences.append('Place the characters in the scene: ' + '; '.join(positions) + '.') + heights = [round(person['height'] * 100) for person in people] + if len(people) == 2 and len(set(feet)) == 1 and heights[0] != heights[1] \ + and all(person.get('framing', 'full') == 'full' for person in people): + near, far = (0, 1) if heights[0] > heights[1] else (1, 0) + sentences.append(f'{labels[near]} is closer to the camera and larger, ' + f'{labels[far]} is farther away and smaller.') + sentences.append("Keep each face and the clothes as in the character's own image and match the lighting of the scene.") + for index, person in enumerate(people, 2): + if 'facing' in person or person.get('framing', 'full') != 'full': + sentences.append(position_prompt([person], first_image=index)) + return ' '.join(sentences) + + +def position_prompt(people, *, first_image=2): + """Describe positions in screen coordinates, preserving input/reference order. + + Height means the visible figure's fraction of frame height. For a half-body + shot this is the head-to-waist figure, rather than an off-screen full body. + """ + sentences = [] + for index, person in enumerate(people, first_image): + x, height = person['x'], person['height'] + side = 'on the screen left' if x < .5 else 'on the screen right' if x > .5 else 'in the screen centre' + full = person.get('framing', 'full') == 'full' + figure = 'full-body' if full else 'visible upper-body' + sentences.append( + f'The person in image {index} stands {side}, centred at x={x:g} ' + f'({x * 100:g}% of frame width from the left edge), with {figure} height ' + f'{height:g} ({height * 100:g}% of frame height), facing screen {person.get("facing", "right")}. ' + + ('Show their full body from head to shoes, with both shoes inside the frame.' if full else + 'Show them from head to waist, a half-body view.')) + if 'feet' in person: + sentences.append(f'The bottom of this visible figure is at {person["feet"] * 100:g}% of frame height from the top.') + return ' '.join(sentences) From 380a6cc19c038a3a39dc9d22b94d82024afa919d Mon Sep 17 00:00:00 2001 From: Bowen Xue Date: Fri, 23 Oct 2026 18:06:39 +0000 Subject: [PATCH 5/8] Clarify the first-frame tool descriptions: scene optional, face wording, height and feet, facing --- freevideo_engine/agent/mcp.py | 2 +- freevideo_engine/agent/schema.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/freevideo_engine/agent/mcp.py b/freevideo_engine/agent/mcp.py index 956f4e1e..ff1f1815 100644 --- a/freevideo_engine/agent/mcp.py +++ b/freevideo_engine/agent/mcp.py @@ -50,7 +50,7 @@ def tool_list(): dict(name='freevideo_schema', title='Job JSON Schemas', description='JSON Schemas of every job kind and of image edit recipes.', annotations=read), dict(name='freevideo_plan', title='Plan a job', description="Validate a job without running it: actual frames/seconds, sampling passes, warnings and a time estimate from this computer's history. Same fields as freevideo_generate.", annotations=read), dict(name='freevideo_generate', title='Generate a video', description='Generate one video from an H3 prompt, up to 60 seconds. Under 30 s defaults to one generation if the device supports this size and quality; unknown limits use 15 s. Otherwise the video is cut into as few parts as possible, each at least 10 s, and how many parts depends on how long this computer can generate in one run; the parts continue and join automatically. Prefer a new shot with its reference pictures wherever the film can cut; continue only a take, action or sound that must stay unbroken. Use this for any single video, also after freevideo_enhance_prompt; the drama tools are only for a film of several separately chosen shots. Optionally from first/last frames or references (files, local paths, or frames of earlier jobs). Returns the job; with wait, its result (video path and URL).', annotations=write), - dict(name='freevideo_generate_image', title='Generate a picture', description='Generate a picture from a prompt; with images, change or combine input pictures; with people and scene_image, make a first frame with the scene and the people near the places you say. The result is an input file to use as a first frame, as a reference, or in images again.', annotations=write), + dict(name='freevideo_generate_image', title='Generate a picture', description='Generate a picture from a prompt; with images, change or combine input pictures; with people, make a first frame with them near the places you say, in the scene from scene_image or the prompt. The result is an input file to use as a first frame, as a reference, or in images again.', annotations=write), dict(name='freevideo_submit_batch', title='Submit a storyboard', description='Submit several jobs at once. Each row is a job object with "kind" (generate default, assemble, vlm, enhance, image) and an optional "ref"; later rows use "@ref" wherever a job id is expected (from_job, after, clips, refine_from). FreeVideo runs them in dependency order.', annotations=write), dict(name='freevideo_job', title='Job status', description='Status, progress, result or error of a job.', annotations=read), dict(name='freevideo_wait', title='Wait for a job or the user', description="Wait at most 25 seconds for a job (with id) or, without id, at most 50 seconds for what the user says in FreeVideo (inbox). again=true means nothing new yet: call it again. A job wait returns early when the user says something. If the job is still active, call it again with the same ID; do not resubmit.", annotations=read), diff --git a/freevideo_engine/agent/schema.py b/freevideo_engine/agent/schema.py index 50552475..f41d0386 100644 --- a/freevideo_engine/agent/schema.py +++ b/freevideo_engine/agent/schema.py @@ -239,11 +239,11 @@ def image(*, local=False): feet=number(0, 1), facing=enum(('left', 'right')), framing=enum(('full', 'half'), default='full')), oneOf=[{'required': ['image']}, {'required': ['cast']}]), 1, 3, - description='First frame with people near the places you say; faces may differ. Give image (an input ' + description='First frame with people near the places you say; faces may not match the pictures exactly. Give image (an input ' 'file) or cast (a character card ID; its front picture is used). From 0 to 1: x is the ' - "centre across the width (default 0.5), height the visible figure over the frame height (0.62; head to waist " - 'for half), feet the figure bottom from the top (default max(0.9, height)). facing is the screen left/right; ' - 'framing is full or half. Width and height: multiples of 32. The prompt or scene_image gives the scene. Not with images.'), + "centre across the width (default 0.5), height the visible figure's share of the frame height (0.62; head to waist " + "for half), feet where the figure's lower edge sits, from the top (default: the larger of 0.9 and height). facing is left or right on screen; " + 'Width and height: multiples of 32. Not with images.'), scene_image=FILE, **common()), ('kind',), allOf=[{'if': {'not': {'required': ['scene_image']}}, 'then': {'required': ['prompt']}}, From 9ced68a2a7c86dd745762c3ad899d9e5fe4447ec Mon Sep 17 00:00:00 2001 From: Bowen Xue Date: Fri, 23 Oct 2026 19:18:39 +0000 Subject: [PATCH 6/8] End the facing sentence with a period in the first-frame description --- freevideo_engine/agent/schema.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freevideo_engine/agent/schema.py b/freevideo_engine/agent/schema.py index f41d0386..1ac9c104 100644 --- a/freevideo_engine/agent/schema.py +++ b/freevideo_engine/agent/schema.py @@ -242,7 +242,7 @@ def image(*, local=False): description='First frame with people near the places you say; faces may not match the pictures exactly. Give image (an input ' 'file) or cast (a character card ID; its front picture is used). From 0 to 1: x is the ' "centre across the width (default 0.5), height the visible figure's share of the frame height (0.62; head to waist " - "for half), feet where the figure's lower edge sits, from the top (default: the larger of 0.9 and height). facing is left or right on screen; " + "for half), feet where the figure's lower edge sits, from the top (default: the larger of 0.9 and height). facing is left or right on screen. " 'Width and height: multiples of 32. Not with images.'), scene_image=FILE, **common()), From 26a90aff21c423cac190f93e9ced5e7be976a5f4 Mon Sep 17 00:00:00 2001 From: Bowen Xue Date: Sat, 24 Oct 2026 00:44:15 +0000 Subject: [PATCH 7/8] Keep the MCP tool list under 200 KB and update the first-frame entry tests for the scene-first... Keep the MCP tool list under 200 KB and update the first-frame entry tests for the scene-first request --- freevideo_engine/agent/schema.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/freevideo_engine/agent/schema.py b/freevideo_engine/agent/schema.py index 1ac9c104..2ee30dc4 100644 --- a/freevideo_engine/agent/schema.py +++ b/freevideo_engine/agent/schema.py @@ -228,8 +228,7 @@ def vlm(*, local=False): def image(*, local=False): return obj(dict(kind={'const': 'image', 'type': 'string'}, - prompt=dict(type='string', minLength=1, maxLength=2000, - description='Scene description; required unless scene_image supplies the scene.'), + prompt=dict(type='string', minLength=1, maxLength=2000), width=number(256, 2048, integer=True), height=number(256, 2048, integer=True), aspect=enum(IMAGE_ASPECTS, description='Instead of width and height; 1:1 when all three are omitted.'), seed=nullable(number(-2**63, 2**64 - 1, integer=True)), @@ -239,11 +238,10 @@ def image(*, local=False): feet=number(0, 1), facing=enum(('left', 'right')), framing=enum(('full', 'half'), default='full')), oneOf=[{'required': ['image']}, {'required': ['cast']}]), 1, 3, - description='First frame with people near the places you say; faces may not match the pictures exactly. Give image (an input ' - 'file) or cast (a character card ID; its front picture is used). From 0 to 1: x is the ' - "centre across the width (default 0.5), height the visible figure's share of the frame height (0.62; head to waist " - "for half), feet where the figure's lower edge sits, from the top (default: the larger of 0.9 and height). facing is left or right on screen. " - 'Width and height: multiples of 32. Not with images.'), + description="First frame with people near the places you say; faces may not match the pictures exactly. " + "x, height and feet run 0 to 1: x the centre across the width (0.5), height the visible figure's share of the frame " + "height (0.62; head to waist for half), feet the figure's lower edge from the top (the larger of 0.9 and height). " + "facing is left or right on screen. Not with images."), scene_image=FILE, **common()), ('kind',), allOf=[{'if': {'not': {'required': ['scene_image']}}, 'then': {'required': ['prompt']}}, @@ -1482,7 +1480,7 @@ def openapi(): SESSION_ARGUMENT = dict(type='string', pattern='^s[0-9a-f]{12}$', - description='Work session for this call; omit to use your latest one.') + description='Work session; omit for your latest.') SHAPE_MIN_BYTES = 400 From fd7a12ebf5f98b41f39badbe046a9685e0302195 Mon Sep 17 00:00:00 2001 From: Bowen Xue Date: Sat, 24 Oct 2026 11:49:43 +0000 Subject: [PATCH 8/8] Release notes for 0.4.21 --- freevideo_engine/release_notes.json | 117 ++++++++++------------------ 1 file changed, 40 insertions(+), 77 deletions(-) diff --git a/freevideo_engine/release_notes.json b/freevideo_engine/release_notes.json index fee21a7c..3888e749 100644 --- a/freevideo_engine/release_notes.json +++ b/freevideo_engine/release_notes.json @@ -1,37 +1,54 @@ { "schema": 1, - "product_version": "0.4.20", + "product_version": "0.4.21", "en": { - "summary": "One continuous shot can now be split into up to 8 parts; generated pictures can be downloaded directly and the path of a video can be copied.", + "summary": "Agent image tasks can place characters in a scene by position, size and facing; the page check no longer reports a covered toolbar behind ComfyUI dialogs.", "changes": [ - "Long videos: one continuous shot can now be split into up to 8 parts instead of 5. The line below the duration in the creation panel states that each run makes one continuous shot and points to Edit or Autopilot for a storyboard; a length that needs more parts than allowed is stopped before it is submitted, and a refusal at submission is shown as a plain notice instead of an unknown error.", - "Reference video: fixed generation failing when a reference video is combined with a non-default upscale factor such as 1.5x.", - "Pictures and videos: picture cards in the Autopilot chat and the enlarged picture view have a Download button; results in the library and the creation panel have Copy path.", - "The desktop notification for a finished generation is now on by default: permission is requested once, the first time Generate is clicked; it stays off for anyone who has turned it off.", - "Edit: while the generation service has not answered, the generation bar stays visible with Generate dimmed; when the automatic checks get no answer, the status line shows \"Generation unavailable\" with a retry link, which checks again when clicked.", - "The edits, imports, characters, Agent conversations and Agent jobs lists now end with a line such as \"N edits could not be read\" when some records cannot be read.", - "Windows graphics cards with 8 to 12 GB of video memory: the first pass of FP8 two-pass videos on RTX 40 and 50 series cards, and the second pass of int8 videos longer than 10 seconds on 12 GB cards, now run attention 8 heads at a time instead of 4, within the planned memory budget. LoRA, Linux and Mac are unchanged. The same seed can give a slightly different video.", - "Retry after one-time errors: a second pass that stops with a one-time CUDA error resumes once from the finished first pass instead of failing or repeating it; on a Mac, a pass that stops with a Metal command-buffer error is retried once with smaller memory partitions. Failure reports name the machine, settings and retry result, and report ids stay retrievable after the service restarts.", - "Decoding the same request with the same seed now writes identical pixels every time. Compared with earlier versions, the same seed can give slightly different pixels.", - "The card shown for an unsupported graphics card now names the detected card and its compute capability, and states that FreeVideo needs compute capability 8.0 or higher (GeForce RTX 30 series or newer) regardless of VRAM. Error text no longer replaces a slash inside a sentence with a path placeholder." + "Agent images: an image task can take a scene picture and one to three characters (character card or picture) with a position, a height, a full-body or half-body framing and a facing direction. The picture is generated with the scene as the first reference instead of a pasted collage, and the result can be used directly as a first frame.", + "Page check: the FreeVideo toolbar is no longer reported as covered when ComfyUI shows a dimming or click-away layer over it, such as the Templates dialog on a first start or the Help panel; the details now name what covers the toolbar." ] }, "zh": { - "summary": "一个连续镜头最多可分 8 段;生成的图片可以直接下载,作品可以复制路径。", + "summary": "Agent 图片任务可按位置、大小和朝向把人物放进场景;页面检查不再因 ComfyUI 的对话框遮罩报告工具栏被遮挡。", "changes": [ - "长视频:一个连续镜头最多可分 8 段(原为 5 段)。创作面板时长下方写明每次生成一个连续镜头,有分镜剧本时可转到剪辑视图或 AI 全自动;所需段数超过上限时在提交前提示缩短时长,提交时仍被拒绝也显示为普通提示,不再显示为未知错误。", - "参考视频:修复参考视频配合 1.5 倍等非默认放大倍数时生成失败。", - "图片和作品:AI 全自动对话中的图片卡和放大查看窗增加“下载”;作品库和创作面板的结果按钮增加“复制路径”。", - "生成完成的桌面通知改为默认开启:第一次点生成时请求一次通知权限;已经关闭通知的保持关闭。", - "剪辑视图:生成服务暂时没有回答时,生成栏不再被藏起,“生成”按钮置灰;几次自动检查都没有回答时,状态行显示“生成服务暂不可用 · 重试”,点击后重新检查。", - "剪辑、已导入、角色、Agent 对话和 Agent 任务列表中有记录无法读取时,列表末尾写明“另有 N 个……无法读取”。", - "Windows 上显存 8–12 GB 的显卡:RTX 40 和 50 系列 FP8 两遍视频的第一遍,以及 12 GB 显卡上超过 10 秒的 int8 视频的第二遍,每次计算的注意力头由 4 个增加到 8 个,显存占用保持在计划预算内。LoRA、Linux 和 Mac 不变。同一个种子生成的视频可能略有不同。", - "一次性错误自动重试:第二遍遇到一次性的 CUDA 错误时,自动从已完成的第一遍续跑一次,不再整个请求失败或重做第一遍;Mac 上采样阶段遇到 Metal 命令缓冲区错误时,会用更小的内存分块重试一次。失败报告写明机器、设置和重试结果,报告编号在服务重启后仍可取回。", - "同一请求、同一种子,解码出的画面每次逐位一致。与之前的版本相比,同一种子的画面可能有细微差别。", - "不受支持的显卡的提示卡现在写出检测到的显卡和计算能力,并说明 FreeVideo 需要计算能力 8.0 及以上的显卡(GeForce RTX 30 系列及更新),与显存大小无关。错误文字中句子里的斜杠不再被替换成路径占位符。" + "Agent 图片:图片任务可以指定一张场景图和一至三个人物(角色卡或图片),并设定位置、人物高度、全身或半身景别和朝向。图片以场景为第一参考图生成,不再拼贴粘图,结果可直接作首帧。", + "页面检查:ComfyUI 的调暗或点击遮罩(首次启动的模板对话框、帮助面板等)盖在 FreeVideo 工具栏上时,不再报告工具栏被遮挡;诊断详情会写出遮挡工具栏的元素。" ] }, "history": [ + { + "product_version": "0.4.20", + "en": { + "summary": "One continuous shot can now be split into up to 8 parts; generated pictures can be downloaded directly and the path of a video can be copied.", + "changes": [ + "Long videos: one continuous shot can now be split into up to 8 parts instead of 5. The line below the duration in the creation panel states that each run makes one continuous shot and points to Edit or Autopilot for a storyboard; a length that needs more parts than allowed is stopped before it is submitted, and a refusal at submission is shown as a plain notice instead of an unknown error.", + "Reference video: fixed generation failing when a reference video is combined with a non-default upscale factor such as 1.5x.", + "Pictures and videos: picture cards in the Autopilot chat and the enlarged picture view have a Download button; results in the library and the creation panel have Copy path.", + "The desktop notification for a finished generation is now on by default: permission is requested once, the first time Generate is clicked; it stays off for anyone who has turned it off.", + "Edit: while the generation service has not answered, the generation bar stays visible with Generate dimmed; when the automatic checks get no answer, the status line shows \"Generation unavailable\" with a retry link, which checks again when clicked.", + "The edits, imports, characters, Agent conversations and Agent jobs lists now end with a line such as \"N edits could not be read\" when some records cannot be read.", + "Windows graphics cards with 8 to 12 GB of video memory: the first pass of FP8 two-pass videos on RTX 40 and 50 series cards, and the second pass of int8 videos longer than 10 seconds on 12 GB cards, now run attention 8 heads at a time instead of 4, within the planned memory budget. LoRA, Linux and Mac are unchanged. The same seed can give a slightly different video.", + "Retry after one-time errors: a second pass that stops with a one-time CUDA error resumes once from the finished first pass instead of failing or repeating it; on a Mac, a pass that stops with a Metal command-buffer error is retried once with smaller memory partitions. Failure reports name the machine, settings and retry result, and report ids stay retrievable after the service restarts.", + "Decoding the same request with the same seed now writes identical pixels every time. Compared with earlier versions, the same seed can give slightly different pixels.", + "The card shown for an unsupported graphics card now names the detected card and its compute capability, and states that FreeVideo needs compute capability 8.0 or higher (GeForce RTX 30 series or newer) regardless of VRAM. Error text no longer replaces a slash inside a sentence with a path placeholder." + ] + }, + "zh": { + "summary": "一个连续镜头最多可分 8 段;生成的图片可以直接下载,作品可以复制路径。", + "changes": [ + "长视频:一个连续镜头最多可分 8 段(原为 5 段)。创作面板时长下方写明每次生成一个连续镜头,有分镜剧本时可转到剪辑视图或 AI 全自动;所需段数超过上限时在提交前提示缩短时长,提交时仍被拒绝也显示为普通提示,不再显示为未知错误。", + "参考视频:修复参考视频配合 1.5 倍等非默认放大倍数时生成失败。", + "图片和作品:AI 全自动对话中的图片卡和放大查看窗增加“下载”;作品库和创作面板的结果按钮增加“复制路径”。", + "生成完成的桌面通知改为默认开启:第一次点生成时请求一次通知权限;已经关闭通知的保持关闭。", + "剪辑视图:生成服务暂时没有回答时,生成栏不再被藏起,“生成”按钮置灰;几次自动检查都没有回答时,状态行显示“生成服务暂不可用 · 重试”,点击后重新检查。", + "剪辑、已导入、角色、Agent 对话和 Agent 任务列表中有记录无法读取时,列表末尾写明“另有 N 个……无法读取”。", + "Windows 上显存 8–12 GB 的显卡:RTX 40 和 50 系列 FP8 两遍视频的第一遍,以及 12 GB 显卡上超过 10 秒的 int8 视频的第二遍,每次计算的注意力头由 4 个增加到 8 个,显存占用保持在计划预算内。LoRA、Linux 和 Mac 不变。同一个种子生成的视频可能略有不同。", + "一次性错误自动重试:第二遍遇到一次性的 CUDA 错误时,自动从已完成的第一遍续跑一次,不再整个请求失败或重做第一遍;Mac 上采样阶段遇到 Metal 命令缓冲区错误时,会用更小的内存分块重试一次。失败报告写明机器、设置和重试结果,报告编号在服务重启后仍可取回。", + "同一请求、同一种子,解码出的画面每次逐位一致。与之前的版本相比,同一种子的画面可能有细微差别。", + "不受支持的显卡的提示卡现在写出检测到的显卡和计算能力,并说明 FreeVideo 需要计算能力 8.0 及以上的显卡(GeForce RTX 30 系列及更新),与显存大小无关。错误文字中句子里的斜杠不再被替换成路径占位符。" + ] + } + }, { "product_version": "0.4.19", "en": { @@ -250,60 +267,6 @@ "安装:Windows 和 Linux 安装包变小;提示词增强模型也可以从魔搭下载;GPU 加速组件构建时,名称和说明保持不变。" ] } - }, - { - "product_version": "0.4.12", - "en": { - "summary": "Preview white-model scenes for Autopilot, LoRA compatibility checks and drag-to-reorder, clearer failure messages, deleting edits, a faster start for a busy machine, and a Medium level that samples two passes.", - "changes": [ - "Autopilot: an optional white-model preview, with ready-made scenes, shows the scene before the video is made; the entry showcase has an example.", - "LoRA: a compatibility check before generating, and LoRAs can be reordered by dragging.", - "Failure messages: submission, queue and browser-extension failures share one layout and say what to do next; failed requests can be packaged with Export report or Submit to GitHub.", - "Editor: an edit can be deleted, an empty untouched edit is discarded, and reopening an edit restores its cover and shows its finishing stage; continuations are queued on the server.", - "Characters: each character carries a sheet of all views plus a face close-up; a picture that cannot be read is refused with a message on its own tile.", - "Medium: samples two passes from a larger first pass; Max runs all 20 steps without a cache.", - "Install: the GPU acceleration build has a visible share of the progress, the model line stays steady while groups download, and torch shows as Runtime components.", - "Downloads: before a download starts, the launcher, the page and the editor say what it is and how long it takes.", - "Guide: the first-run guide shows where to add reference images and first or last frames, the length field shows the part limit, and the Chinese interface links a user guide beside the guide in Settings.", - "Reusing reference pictures and clips when only the prompt changes skips encoding them again; on a busy machine the small CPU table work of loading runs on one thread.", - "Mac: a device that cannot continue a video refuses lengths over one block up front.", - "The feedback button opens the issue form." - ] - }, - "zh": { - "summary": "AI 全自动新增白模预览场景,LoRA 兼容性检查和拖动排序,失败提示更清楚,可以删除剪辑,繁忙的机器上启动更快,标准档改为两遍采样。", - "changes": [ - "AI 全自动:可选的白模预览和现成场景,在生成视频前先看到场景;入口展示里有示例。", - "LoRA:生成前做兼容性检查,LoRA 可以拖动排序。", - "失败提示:提交、排队和浏览器扩展的失败用同一种版式,并说明下一步该做什么;失败的请求可以打包为导出报告或提交到 GitHub。", - "编辑器:可以删除剪辑,没改动的空剪辑自动丢弃;重新打开剪辑会恢复封面并显示收尾阶段;续接在服务端排队。", - "角色:每个角色带有全部视角的角色图和脸部特写;读不出来的图片会被拒绝,并在对应的格子上给出提示。", - "标准档:从更大的第一遍起采样两遍;极致档跑满 20 步且不使用缓存。", - "安装:GPU 加速组件的构建在进度中占有可见的份额,模型下载分组并行时进度行保持稳定,torch 显示为“运行组件”。", - "下载:开始下载前,启动器、页面和编辑器会说明这是什么下载、需要多久。", - "指引:首次指引新增一步,说明在哪里添加参考图和首帧、尾帧;时长栏直接显示分段上限;中文界面在设置里可以打开使用说明。", - "只改提示词时,参考图和片段不再重复编码;机器繁忙时,加载过程中的小型 CPU 表运算改为单线程。", - "Mac:不能续接视频的设备会在一开始就拒绝超过一个分段的长度。", - "反馈按钮直接打开问题表单。" - ] - } - }, - { - "product_version": "0.4.11", - "en": { - "summary": "Fixes requests being rejected with a frame count error, and LoRAs with extra parts generating on the int8 build.", - "changes": [ - "Generating from a workflow or a restored tab where Total frames holds a number that is not a frame count is no longer rejected with a frame count error; the number is read as the duration.", - "int8: a LoRA that also contains modulation parts no longer stops the generation; those parts are skipped one by one and the number skipped appears in the report." - ] - }, - "zh": { - "summary": "修复请求被帧数错误拒绝的问题,并让带额外部分的 LoRA 能在 int8 版本上正常生成。", - "changes": [ - "用 Total frames 里填的不是帧数的工作流或恢复的标签页生成时,不再被帧数错误拒绝,这个数会按时长读取。", - "int8:带调制部分的 LoRA 不再让生成中断,这些部分会逐个跳过,跳过的数量写进报告。" - ] - } } ] }