From 91eff6675657b62c94f2684240ab547948c2821f Mon Sep 17 00:00:00 2001 From: Bowen Xue Date: Wed, 14 Oct 2026 02:18:46 +0000 Subject: [PATCH 1/8] Package the image models' notice file with the engine - Installing the image generation models reads it without the file the install could not start --- freevideo_engine/image_gen/licenses/NOTICE.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 freevideo_engine/image_gen/licenses/NOTICE.txt diff --git a/freevideo_engine/image_gen/licenses/NOTICE.txt b/freevideo_engine/image_gen/licenses/NOTICE.txt new file mode 100644 index 00000000..1cddfd79 --- /dev/null +++ b/freevideo_engine/image_gen/licenses/NOTICE.txt @@ -0,0 +1 @@ +Qwen is licensed under the Qwen RESEARCH LICENSE AGREEMENT, Copyright (c) 2026 Hangzhou Tongyi Laboratory Technology Co., Ltd. All Rights Reserved. From bf15c70bf8291debffcfc880612efea55bbaef3d Mon Sep 17 00:00:00 2001 From: Bowen Xue Date: Wed, 14 Oct 2026 02:27:59 +0000 Subject: [PATCH 2/8] Agent: a job stopped on the page is reported as stopped by the user; works are called Creations --- freevideo_engine/agent/guide.py | 11 +++++++---- freevideo_engine/agent/jobs.py | 23 ++++++++++++++++++++++- freevideo_engine/agent/routes.py | 2 +- freevideo_engine/agent/schema.py | 2 ++ 4 files changed, 32 insertions(+), 6 deletions(-) diff --git a/freevideo_engine/agent/guide.py b/freevideo_engine/agent/guide.py index 29633761..92301670 100644 --- a/freevideo_engine/agent/guide.py +++ b/freevideo_engine/agent/guide.py @@ -176,14 +176,16 @@ 'job, never in a tight loop. The connection dropped or FreeVideo restarted: connect again, read the job list, ' 'and continue; resend an unanswered request with the same ref, and never generate again what is already done.', 'Do not stop to report progress. A long task is not a reason to stop or to ask whether to go on.', - 'When a video is done, tell the user it is in "Your creations" in FreeVideo, in plain words. Do not read out job, library ' + 'When a video is done, tell the user it is in "Creations" in FreeVideo, in plain words. Do not read out job, library ' 'or project ids; give a file path only when the user asks for one. If the length differs from what was asked, ' 'say the real length in seconds and do not explain frame counts.', 'Ask the user only when you would have to change the content or style they asked for, when the work will ' 'take clearly longer than you told them at the start, or when you would have to delete or overwrite their ' 'own works or edits.', 'Stay inside FreeVideo. Do not touch other files on this computer. Never change back something the user ' - 'edited. When the user tells you to stop, stop: cancel what you started and start nothing new.', + 'edited. When the user tells you to stop, stop: cancel what you started and start nothing new. ' + 'When cancelled_by is "user", the user stopped this job on the page and the page already shows it as stopped: ' + 'send no message about it, and do not retry or resume it unless they ask.', 'Cast before you shoot. Search the cast library first. Use the card the user named, or one that clearly ' 'matches; ask if you are unsure. Create a card only when none fits: from the user\'s photos if they gave any, ' 'otherwise make its reference pictures with POST /cast/{id}/generate. ' @@ -213,12 +215,13 @@ '连接中断或 FreeVideo 重启后,重新连接,读取任务列表并继续;未收到响应的请求用相同的 ref 重发,' '已经完成的内容不要重新生成。', '不要为了汇报进度而停下。任务耗时较长,不是停止工作或询问是否继续的理由。', - '视频做好后,用通俗的话告诉用户它已经放进 FreeVideo 的“我的作品”里。不要念任务、作品或剪辑的编号;' + '视频做好后,用通俗的话告诉用户它已经放进 FreeVideo 的“作品”里。不要念任务、作品或剪辑的编号;' '文件路径在用户问起时再给。实际长度和要求的不一样时,只说实际约几秒,不解释帧数。', '只有需要改变用户要求的内容或风格、预计耗时明显超过开始时告诉用户的时间,' '或者需要删除或覆盖用户自己的作品或修改时,才询问用户。', '只在 FreeVideo 内工作,不要改动本机的其他文件,不要撤销用户做的修改。' - '用户要求停止时,取消你启动的任务,不再启动新任务。', + '用户要求停止时,取消你启动的任务,不再启动新任务。' + 'cancelled_by 为 "user" 时,表示用户在页面上停止了该任务,页面已显示已停止:不要为此发消息;除非用户要求,否则不要重试或恢复。', '先选角,再拍摄。先搜索角色库,使用用户指定的卡片或明确符合要求的卡片;拿不准时询问用户。' '只有找不到合适卡片时才新建:用户提供了照片就用照片,否则通过 POST /cast/{id}/generate 生成参考图。' '开拍前用一句话告诉用户选角结果。后续镜头使用卡片当前的图片。保留你创建的卡片,方便以后拍片复用。', diff --git a/freevideo_engine/agent/jobs.py b/freevideo_engine/agent/jobs.py index 8d29db26..894198de 100644 --- a/freevideo_engine/agent/jobs.py +++ b/freevideo_engine/agent/jobs.py @@ -67,6 +67,17 @@ def image_reference(port, relative): raise JobError('invalid_media', IMAGE_ERRORS['invalid_media']) from None +USER_STOP_NEXT = ('The user stopped this job on the page, and the page already shows it as stopped. Send no message ' + 'about it, and do not retry or resume it unless they ask. Wait for what they say next.') + + +def user_stop_notice(row): + """What a job the user stopped on the page tells the agent. A job the agent cancelled, and one that finished + some other way before the stop took hold, read as before.""" + stopped = row.get('cancelled_by') == 'user' and row.get('status') not in ('succeeded', 'failed', 'interrupted') + return dict(cancelled_by='user', next=USER_STOP_NEXT) if stopped else {} + + class JobError(Exception): def __init__(self, code, message, **details): super().__init__(message) @@ -578,6 +589,8 @@ def finish(self, row, status, *, result=None, error=None, save=True): card = (row.get('parent') or {}).get('cast') if card and '/resume' in error['next']: error['next'] = CAST_AGAIN % card + if row.get('cancelled_by') == 'user' and status == 'cancelled': + error['next'] = USER_STOP_NEXT if row['status'] == status and row.get('error') == error and row.get('finished_at') is not None: if result is not None and row.get('result') != result: row['result'] = result @@ -739,11 +752,14 @@ def list(self, *, status=None, project=None, kind=None, limit=50): and (not project or r['spec'].get('project') == project)] return rows[:limit] - def cancel(self, identity): + def cancel(self, identity, *, by=None): row = self.get(identity) with self.lock: if row['status'] in DONE: return row + if by == 'user': + # Save before contacting an executor, including jobs still being dispatched. + self.touch(row, cancelled_by='user') if row['kind'] == 'generate' and not row['spec'].get('refine_from'): row['_cancel'] = True row['cancel_requested'] = True @@ -921,6 +937,7 @@ def resume(self, identity, *, key=None, value=None, replay_result=False, client= row.setdefault('receipts', []).append(dict(receipt, jobs=[identity])) row.pop('error', None) row.pop('_cancel', None) + row.pop('cancelled_by', None) self.touch(row, status='waiting', resume_pending=bool(row.get('chain_id')), cancel_requested=False, cancelled_in_jobs=False, chain_submission='linked' if row.get('chain_id') else None, @@ -1008,6 +1025,10 @@ def view(self, row, *, detail=True): if result['error'].get('next'): result['error'] = dict(result['error'], next=errors.recovery(result['error']['code'], kind=row['kind'], stage='terminal', resumable=not row['spec'].get('refine_from')).replace('{id}', row['id'])) + notice = user_stop_notice(row) + result.update(notice) + if notice and result.get('error'): + result['error'] = dict(result['error'], next=USER_STOP_NEXT) if detail: result['spec'] = {k: v for k, v in spec.items() if k not in ('depends_on',)} if row.get('comfy'): diff --git a/freevideo_engine/agent/routes.py b/freevideo_engine/agent/routes.py index 7eabc124..3a9af4a8 100644 --- a/freevideo_engine/agent/routes.py +++ b/freevideo_engine/agent/routes.py @@ -990,7 +990,7 @@ async def activity_cancel(request): return problem('unsupported_media_type', 'Send JSON', 415) value = await body(request) try: - row = await asyncio.to_thread(jobs.cancel, value.get('id') if isinstance(value, dict) else None) + row = await asyncio.to_thread(jobs.cancel, value.get('id') if isinstance(value, dict) else None, by='user') except KeyError: return problem('not_found', 'No such job', 404) return reply(dict(id=row['id'], status=row['status'])) diff --git a/freevideo_engine/agent/schema.py b/freevideo_engine/agent/schema.py index 5f38b8b3..db0ef907 100644 --- a/freevideo_engine/agent/schema.py +++ b/freevideo_engine/agent/schema.py @@ -1297,6 +1297,8 @@ def openapi(): job_view = {'type': 'object', 'properties': dict(id=JOB, kind=enum(KINDS), status=enum(('waiting', 'queued', 'running', 'cancelling', 'succeeded', 'failed', 'cancelled', 'interrupted')), chain_id=string(), last_heartbeat_at={'type': ['string', 'null']}, poll_after_ms={'type': ['integer', 'null']}, + cancelled_by=enum(('user',), description='Present when the user stopped this job on the page.'), + next=string(description='Instructions for the agent after a user stop.'), spec={'type': 'object'}, progress={'type': 'object'}, result={'type': 'object'}, waiting_for=array(string()), notice=string(), error={'$ref': '#/components/schemas/Error'})} From b035b4edb96ea3aeece708932588b6765a60e790 Mon Sep 17 00:00:00 2001 From: Bowen Xue Date: Wed, 14 Oct 2026 02:27:59 +0000 Subject: [PATCH 3/8] Agent: a note on a timeline block is trimmed before its length is checked --- 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 db0ef907..b1321648 100644 --- a/freevideo_engine/agent/schema.py +++ b/freevideo_engine/agent/schema.py @@ -813,7 +813,7 @@ def project_schema(name): 'move': (dict(clip_ids=ids, frames=number(-10**7, 10**7, integer=True)), ('clip_ids', 'frames'), 'Move selected clips by a signed relative frame offset. Use clip IDs from this revision; linked partners move together.'), 'describe': (dict(clip_ids=ids, text=dict(type='string', maxLength=8000)), ('clip_ids', 'text'), 'Set the description of selected clips. Use clip IDs from this revision and one paragraph of text.'), 'set_ref': (dict(clip_ids=ids, ref=dict(type='string', maxLength=128, pattern=r'^[A-Za-z0-9_-]{0,128}$')), ('clip_ids', 'ref'), 'Set an outside shot reference on selected clips. Use clip IDs from this revision; an empty ref removes it.'), - 'note': (dict(clip_ids=ids, text=dict(type='string', maxLength=120)), ('clip_ids', 'text'), 'Write the one line the user sees under selected picture clips; without it they see the start of the generation text. Do it after you add, generate or change a clip: in the user\'s language, what is in the picture, subject first ("Train through morning mist"); a short clip shows only the first few characters. Not the clip\'s position (opening, part 2) and not its progress. Up to 120 characters on one line; an empty text removes it. It never changes the description used for generation.'), + 'note': (dict(clip_ids=ids, text=dict(type='string', maxLength=400)), ('clip_ids', 'text'), 'Write the one line the user sees under selected picture clips; without it they see the start of the generation text. Do it after you add, generate or change a clip: in the user\'s language, what is in the picture, subject first ("Train through morning mist"); a short clip shows only the first few characters. Not the clip\'s position (opening, part 2) and not its progress. Up to 120 characters on one line; an empty text removes it. It never changes the description used for generation.'), 'use_version': (dict(clip_id=CLIP, index=number(0, 49, integer=True)), ('clip_id', 'index'), 'Restore a stored clip version. First get the clip ID and its versions array; index is zero based.'), 'subtitles': (dict(cues=array(obj(dict(start=EDITOR_FRAME, frames=number(1, 10**7, integer=True), text=dict(type='string', minLength=1, maxLength=400)), ('start', 'frames', 'text')), 1, 500), replace=dict(type='boolean')), ('cues',), From 007fc432580f46f3b68894e9d8a88dffe2e4d3c5 Mon Sep 17 00:00:00 2001 From: Bowen Xue Date: Wed, 14 Oct 2026 02:29:16 +0000 Subject: [PATCH 4/8] Offline installation: LAN access and its port are kept when it is reopened --- freevideo_engine/launcher_session.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/freevideo_engine/launcher_session.py b/freevideo_engine/launcher_session.py index 9a25faf7..98a56697 100644 --- a/freevideo_engine/launcher_session.py +++ b/freevideo_engine/launcher_session.py @@ -408,7 +408,13 @@ def activate_offline(self, root): from .portable_launcher import PortableController root = Path(plain(root)) controller = PortableController(root, bundled=self.source) - controller.restore({'url': self.form['url']}) + values = {'url': self.form['url']} + selected = self.selected or {} + # Imported environments must not inherit another installation's LAN access. + if (selected.get('engine') and Path(plain(resolved(selected['engine']))) + == Path(plain(resolved(root / 'engine')))): + values['installation'] = selected + controller.restore(values) self.controller.close() self.repair_request = None self.controller = controller From e5ddea86e0df4c60e54fe6f7dcc98d9957c4807b Mon Sep 17 00:00:00 2001 From: Bowen Xue Date: Wed, 14 Oct 2026 02:43:57 +0000 Subject: [PATCH 5/8] Agent: a video length this device cannot make is refused with the lengths it can make --- freevideo_engine/agent/jobs.py | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/freevideo_engine/agent/jobs.py b/freevideo_engine/agent/jobs.py index 894198de..89aa1cd6 100644 --- a/freevideo_engine/agent/jobs.py +++ b/freevideo_engine/agent/jobs.py @@ -107,9 +107,12 @@ def chain_error(code, message, next_action, *, details=None, retryable=False): def single_block(spec, seconds=None): - from ..chain_plan import plan - return plan(spec['seconds'] if seconds is None else seconds, max_frames=single_frame_limit(spec), - continued=bool(spec.get('continue_from')))['single'] + from ..chain_plan import PlanError, plan + try: + return plan(spec['seconds'] if seconds is None else seconds, max_frames=single_frame_limit(spec), + continued=bool(spec.get('continue_from')))['single'] + except PlanError: + return False # Too long for one generation and it cannot be cut into parts: parts_problems says so. def continuation_problems(spec, prefix='', *, editor=None): @@ -162,13 +165,13 @@ def _continued_part_problem(seconds, chain): return seconds < MIN_PART_SECONDS or any(block['frames'] < MIN_PART_FRAMES for block in chain['blocks']) -def length_problem(seconds, continued, prefix='', *, minimum=None): - """Suggest nearby whole-second lengths the same planner and minimum accept.""" +def length_problem(seconds, continued, prefix='', *, minimum=None, frames=None): + """Suggest nearby whole-second lengths the same planner, device limit (`frames`) and minimum accept.""" from ..chain_plan import PlanError, plan available = [] for candidate in range(max(1, int(seconds) - 30), int(seconds) + 31): try: - chain = plan(candidate, continued=continued) + chain = plan(candidate, max_frames=frames, continued=continued) except PlanError: continue if not continued or not _continued_part_problem(candidate, chain): @@ -178,8 +181,11 @@ def length_problem(seconds, continued, prefix='', *, minimum=None): nearby = [s for s in available if s < seconds][-2:] + [s for s in available if s > seconds][:2] rule = ('at least %d s per continued part' % minimum if minimum is not None else 'a length that can be split into supported parts') - allowed = rule + '; available lengths: ' + ', '.join('%d s' % s for s in nearby) + allowed = rule + ('; available lengths: ' + ', '.join('%d s' % s for s in nearby) if nearby + else '; this device makes no length near this one at this size') problem = schema.diagnostic(prefix + '/seconds', allowed, seconds, nearest=nearby) + if not nearby: + problem['next'] = 'Try a smaller width and height.' if minimum is not None: problem['message'] = ('Each continued part must be at least %d seconds. Make it longer. ' % minimum + problem['message']) @@ -204,9 +210,9 @@ def parts_problems(spec, runner, prefix='', *, editor=None, work=None, planned=N try: chain = plan(spec['seconds'], max_frames=frames, continued=continued) except PlanError: - return [length_problem(spec['seconds'], continued, prefix)] + return [length_problem(spec['seconds'], continued, prefix, frames=frames)] if continued and _continued_part_problem(spec['seconds'], chain): - return [length_problem(spec['seconds'], continued, prefix, minimum=MIN_PART_SECONDS)] + return [length_problem(spec['seconds'], continued, prefix, minimum=MIN_PART_SECONDS, frames=frames)] if runner is None: return [] limit = runner.max_parts @@ -1469,7 +1475,8 @@ def start_chain(self, row, *, payload=None, resume=False): except (PlanError, ChainPartTooShort) as error: row['resume_pending'] = False problem = length_problem(row['spec']['seconds'], bool(row['spec'].get('continue_from')), - minimum=error.limit if isinstance(error, ChainPartTooShort) else None) + minimum=error.limit if isinstance(error, ChainPartTooShort) else None, + frames=single_frame_limit(row['spec'])) self.finish(row, 'failed', error=errors.validation([problem])) return except Exception: From e2a227471ed0ea0aed704a4cbbe358eb3804c693 Mon Sep 17 00:00:00 2001 From: Bowen Xue Date: Wed, 14 Oct 2026 02:49:11 +0000 Subject: [PATCH 6/8] Agent requests: text with a broken character pair is refused at the entry with the reason --- freevideo_engine/agent/editor_runtime.py | 5 +++++ freevideo_engine/agent/routes.py | 15 ++++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/freevideo_engine/agent/editor_runtime.py b/freevideo_engine/agent/editor_runtime.py index 2f793a32..3d4c42ca 100644 --- a/freevideo_engine/agent/editor_runtime.py +++ b/freevideo_engine/agent/editor_runtime.py @@ -319,6 +319,11 @@ def _fresh(self): return context def _text(self, name, request): + try: + request.encode('utf-8') + except UnicodeEncodeError: + # QuickJS ends the whole program on half of a character pair, wherever the text came from. + raise ValueError('The text holds half of a character pair') from None with self._lock: if self._context is None: self._context = self._fresh() diff --git a/freevideo_engine/agent/routes.py b/freevideo_engine/agent/routes.py index 3a9af4a8..e7dd0902 100644 --- a/freevideo_engine/agent/routes.py +++ b/freevideo_engine/agent/routes.py @@ -623,7 +623,20 @@ async def read_body(request, limit, optional=False): data.extend(chunk) if len(data) > limit: raise JobError('too_large', 'Body exceeds limit', limit_bytes=limit) - return json.loads(data.decode('utf-8'), parse_constant=lambda _: (_ for _ in ()).throw(json.JSONDecodeError('Nonfinite JSON', '', 0))) + text = data.decode('utf-8') + value = json.loads(text, parse_constant=lambda _: (_ for _ in ()).throw(json.JSONDecodeError('Nonfinite JSON', '', 0))) + if '\\u' in text: + # An escape may spell half of a character pair. No file or page can hold that text, and the + # script runtime behind the editing rules ends the whole program on it. + try: + json.dumps(value, ensure_ascii=False).encode('utf-8') + except UnicodeEncodeError: + row = contract.diagnostic('/', 'text made of whole characters', None) + row.update(got='half of a character pair', next='Send whole characters and retry.', + message='The body holds half of a character pair (a lone \\ud800 to \\udfff escape); ' + 'no text can be stored that way.') + raise specs.SpecError(row) from None + return value async def body(request, **ignored): return request[body_key] From 99150518358bed44b14246e95fb8af6e8f4af459 Mon Sep 17 00:00:00 2001 From: Bowen Xue Date: Wed, 14 Oct 2026 02:49:12 +0000 Subject: [PATCH 7/8] Agent jobs: a submit does not hold the job lock while it waits for a size asked for the first time --- freevideo_engine/agent/jobs.py | 36 +++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/freevideo_engine/agent/jobs.py b/freevideo_engine/agent/jobs.py index 89aa1cd6..47eff87c 100644 --- a/freevideo_engine/agent/jobs.py +++ b/freevideo_engine/agent/jobs.py @@ -323,6 +323,17 @@ def iso(stamp): return None +class _Warmed: + """A submit's capability source inside Jobs.lock: the studio's answer as it stands, never a wait.""" + def __init__(self, port): + self.port = port + + def capability_snapshot(self, query): + table = self.port.capability_snapshot(query) + # Still being worked out after the wait before the lock: the unknown limit, as a timeout gives. + return dict(status='unavailable') if table.get('status') == 'computing' else table + + class _ReceiptWrites: """Windows receipt outbox. Backoff never holds this lock or Jobs.lock. @@ -657,16 +668,35 @@ def submit(self, value, *, client=None, takes=1, parent=None, editor=None): payload = dict(value, **({'takes': takes} if takes != 1 else {})) return self.submit_request(payload, client=client, parent=parent, editor=editor)[0] + def warm_capabilities(self, inputs): + """Before Jobs.lock: each row waits here (length.WAIT_SECONDS at most) for a size asked for the first time.""" + from .routes import substitute + names = {} + for raw in inputs: + item = substitute({k: v for k, v in raw.items() if k not in ('ref', 'takes')}, names) + if isinstance(raw.get('ref'), str): + names[raw['ref']] = specs.new_id() # Stands in for the id this row gets under the lock. + try: + specs.normalize(item, port=self.port) + except Exception: + pass # The locked pass reports it, in batch order. + def submit_request(self, value, *, client=None, key=None, parent=None, editor=None): batch = isinstance(value, dict) and 'jobs' in value schema.validate(schema.batch_schema(local=True) if batch else schema.job_schema(local=True), value) schema.preflight_submission(value) + if key is not None or value.get('ref') is not None: + with self.lock: + _, replay = self.idempotency('submit', value, key, client) + if replay is not None: + return replay, True + inputs = value['jobs'] if batch else [value] + self.warm_capabilities(inputs) with self.lock: + # Another submit may have committed the same key while its tables warmed. receipt, replay = self.idempotency('submit', value, key, client) if replay is not None: return replay, True - batch = 'jobs' in value - inputs = value['jobs'] if batch else [value] names, rows, problems = {}, [], [] for i, raw in enumerate(inputs): prefix = '/jobs/%d' % i if batch else '' @@ -679,7 +709,7 @@ def submit_request(self, value, *, client=None, key=None, parent=None, editor=No from .routes import substitute item = substitute(item, names) try: - normalized = specs.normalize(item, port=self.port) + normalized = specs.normalize(item, port=_Warmed(self.port)) except specs.SpecError as error: problems.extend(schema.prefix_diagnostic(e, prefix) for e in error.errors) continue From cb1ce3bb5dba9fef146ea9afed30a712d0ac4706 Mon Sep 17 00:00:00 2001 From: Bowen Xue Date: Wed, 14 Oct 2026 02:50:32 +0000 Subject: [PATCH 8/8] Release 0.4.3 --- freevideo_engine/release_notes.json | 67 +++++++++++++++++++---------- 1 file changed, 44 insertions(+), 23 deletions(-) diff --git a/freevideo_engine/release_notes.json b/freevideo_engine/release_notes.json index a7c70bf0..59cd2837 100644 --- a/freevideo_engine/release_notes.json +++ b/freevideo_engine/release_notes.json @@ -1,37 +1,58 @@ { "schema": 1, - "product_version": "0.4.2", + "product_version": "0.4.3", "en": { - "summary": "Adds optional image generation models: generate and edit pictures, make several views for character cards, and let the agent use them. Also fixes several install and launcher issues.", + "summary": "LAN access settings of offline-package installations are kept when the launcher is reopened, and the agent no longer replies after a job is stopped on the page. Also includes several fixes; the image generation models can now be downloaded and installed normally.", "changes": [ - "Adds optional image generation models. \"Generate with AI\" in the media panel makes a picture from one sentence on a blank canvas, and \"Edit this\" in the image editor changes the current picture, with up to two more pictures from your media as references.", - "With the image generation models installed, character cards start from a front view and use it as the reference for three-quarter, side, back and close-up views; scene cards are made the same way.", - "The agent can generate or edit pictures and add them to your media, and can first make a frame with people placed as described, then generate the video from it.", - "Setup can install the image generation models at the same time (about 16 GB), and existing installs can download them in the background from the launcher; the download pauses while a video is generating.", - "Online installs into a folder the system code page cannot spell, such as a Chinese folder on an English system, no longer fail at the kernel check.", - "On Linux, when the system compiler is newer than the installed GPU toolkit supports, setup now uses a compatible compiler instead of stopping while building the GPU attention component.", - "When a Windows security feature blocks files FreeVideo needs, the error card now says so and notes that the setting may need to be changed in Windows.", - "After a launcher error, an address or path being typed in the settings is no longer reset to its previous value.", - "When the agent makes a film, a shot that continues the previous one is at least 10 seconds long, 10 seconds by default; a length this computer cannot make in one run is reported before generation starts.", - "In AI Autopilot, the conversation list updates as soon as a job starts or stops, and switching conversations quickly stays on the one selected." + "With an offline-package installation, LAN access and its port now stay as set after the launcher is reopened.", + "After a job is stopped on the page, the agent adds no message about it and does not retry on its own.", + "When the agent asks for a video length this computer cannot make, it now gets a clear explanation instead of an error.", + "The image generation models can now be downloaded and installed normally." ] }, "zh": { - "summary": "新增可选的图像生成模型,可以生成和修改图片,为角色卡生成多个角度,Agent 也能使用;并修复了几处安装和启动器的问题。", + "summary": "用离线包安装时,局域网访问的设置在重开启动器后保留;在页面上停止任务后,Agent 不再另外回复;另有几处修复,图像生成模型现在可以正常下载和安装。", "changes": [ - "新增可选的图像生成模型。素材面板的“AI 生成”可以在空白画布上用一句话生成一张图;图片编辑器里的“改这张”可以修改当前图片,还可以再选最多两张素材图作参考。", - "装有图像生成模型时,角色卡先生成正面像,再以它为参考生成四分之三侧面、侧面、背面和脸部特写;场景卡也用它生成。", - "Agent 可以生成或修改图片并放进素材,也可以先按描述的人物站位生成一张首帧,再用它生成视频。", - "安装时可以选择同时安装图像生成模型(约 16 GB);已经安装 FreeVideo 的,可以在启动器里下载,下载在后台进行,生成视频时自动暂停。", - "英文系统上在线安装到中文文件夹时,内核检查不再因为路径无法识别而失败。", - "Linux 上系统编译器比 GPU 工具包支持的版本更新时,安装会换用兼容的编译器,不再停在编译 GPU 注意力加速组件这一步。", - "Windows 的安全功能拦下运行所需的文件时,错误卡会说明原因,并提示可能需要在 Windows 中调整这项设置。", - "启动器出错后,设置里正在输入的地址或路径不再被换回原值。", - "Agent 拍片时,接上一镜的镜头每段至少 10 秒,没写长度时默认 10 秒;这台电脑一次生成不了的长度会在开始前说明。", - "“AI 全自动”里任务开始或停止后,对话列表立刻更新;快速切换对话时停在所选的那一段。" + "用离线包安装时,局域网访问的开关和端口在重开启动器后保持不变。", + "在页面上停止任务后,Agent 不再另外回复,也不会自行重试。", + "Agent 请求的视频长度超出这台电脑能做的范围时,会收到明确的说明,不再出错。", + "图像生成模型现在可以正常下载和安装。" ] }, "history": [ + { + "product_version": "0.4.2", + "en": { + "summary": "Adds optional image generation models: generate and edit pictures, make several views for character cards, and let the agent use them. Also fixes several install and launcher issues.", + "changes": [ + "Adds optional image generation models. \"Generate with AI\" in the media panel makes a picture from one sentence on a blank canvas, and \"Edit this\" in the image editor changes the current picture, with up to two more pictures from your media as references.", + "With the image generation models installed, character cards start from a front view and use it as the reference for three-quarter, side, back and close-up views; scene cards are made the same way.", + "The agent can generate or edit pictures and add them to your media, and can first make a frame with people placed as described, then generate the video from it.", + "Setup can install the image generation models at the same time (about 16 GB), and existing installs can download them in the background from the launcher; the download pauses while a video is generating.", + "Online installs into a folder the system code page cannot spell, such as a Chinese folder on an English system, no longer fail at the kernel check.", + "On Linux, when the system compiler is newer than the installed GPU toolkit supports, setup now uses a compatible compiler instead of stopping while building the GPU attention component.", + "When a Windows security feature blocks files FreeVideo needs, the error card now says so and notes that the setting may need to be changed in Windows.", + "After a launcher error, an address or path being typed in the settings is no longer reset to its previous value.", + "When the agent makes a film, a shot that continues the previous one is at least 10 seconds long, 10 seconds by default; a length this computer cannot make in one run is reported before generation starts.", + "In AI Autopilot, the conversation list updates as soon as a job starts or stops, and switching conversations quickly stays on the one selected." + ] + }, + "zh": { + "summary": "新增可选的图像生成模型,可以生成和修改图片,为角色卡生成多个角度,Agent 也能使用;并修复了几处安装和启动器的问题。", + "changes": [ + "新增可选的图像生成模型。素材面板的“AI 生成”可以在空白画布上用一句话生成一张图;图片编辑器里的“改这张”可以修改当前图片,还可以再选最多两张素材图作参考。", + "装有图像生成模型时,角色卡先生成正面像,再以它为参考生成四分之三侧面、侧面、背面和脸部特写;场景卡也用它生成。", + "Agent 可以生成或修改图片并放进素材,也可以先按描述的人物站位生成一张首帧,再用它生成视频。", + "安装时可以选择同时安装图像生成模型(约 16 GB);已经安装 FreeVideo 的,可以在启动器里下载,下载在后台进行,生成视频时自动暂停。", + "英文系统上在线安装到中文文件夹时,内核检查不再因为路径无法识别而失败。", + "Linux 上系统编译器比 GPU 工具包支持的版本更新时,安装会换用兼容的编译器,不再停在编译 GPU 注意力加速组件这一步。", + "Windows 的安全功能拦下运行所需的文件时,错误卡会说明原因,并提示可能需要在 Windows 中调整这项设置。", + "启动器出错后,设置里正在输入的地址或路径不再被换回原值。", + "Agent 拍片时,接上一镜的镜头每段至少 10 秒,没写长度时默认 10 秒;这台电脑一次生成不了的长度会在开始前说明。", + "“AI 全自动”里任务开始或停止后,对话列表立刻更新;快速切换对话时停在所选的那一段。" + ] + } + }, { "product_version": "0.4.1", "en": {