Skip to content

v1.0.37

Choose a tag to compare

@github-actions github-actions released this 30 Sep 08:14
· 222 commits to main since this release

Added

  • codex model dropdowns follow the official catalog. The codex model lists (top bar and per-agent model dialog) are now read from codex debug models via the new GET /api/v1/engines/codex/models (models with visibility=list, sorted by official priority, cached for 5 minutes), so newly released codex models appear automatically once the installed codex CLI is new enough to receive them. If the CLI is missing or the call fails, the dropdown falls back to a built-in list and its tooltip shows the error. Smart Assignment tiers are unchanged and still set by hand.

  • Checked fancy-text types are now expected to appear. In manual caption mode a checked type used to mean "allowed, not required", so the caption agent could silently drop a whole type (fengshen3 ep07: a creature appeared on screen but no creature name card was made although 「生物 / 妖兽名牌」 was checked). Now every checked type that has matching content in the episode (a creature's first appearance, a prop's first reveal, a faction's first showing, …) is expected to get at least one caption; if a type is left out, the reason must be written per type in the new top-level type_skips field of captions.json (e.g. {"creature_card": "no creature appears in this episode"}). A missing template is not a valid reason: the agent creates one or reuses a similar template. New design-stage machine check caption_types_covered (manual mode only; types unavailable for the project are not counted). The caption SOUL, the caption-styling skill, WORKFLOW §9A and the injected caption prompt are updated. Existing episodes are not re-run and will fail this check until their caption design is redone.

  • Insert-friendly numbering for new projects. Projects created from now on number scenes, generation groups and shots in steps of ten — S010, S020… / grp0010, grp0020… / sh0010, sh0020… — so a scene, group or shot can later be inserted between two existing ones (S011, grp0011, sh0011) without renumbering anything downstream. The scheme is stamped into the project's settings.json#numbering (step: 10) at creation and cannot be changed afterwards; the host injects a 「编号制:预留插入位」 section into every agent's run prompt for these projects only, genmedia's grpsh_id_3digits check expects four digits there, and check_generation_groups.py --propose numbers its draft groups accordingly (new modules/id_scheme.py). Existing projects have no numbering block and keep S01 / grp001 / sh001 unchanged.

  • Project music library. Every project now keeps a library of its score under assets/audio/library/music/ (index.json + MUS-NNNN.<ext>, new modules/music_library.py). When the music workstation (09-audio/music, p8-music) delivers an episode, the tracks it added — newly generated ones and the user-provided ones it picked — are copied into the library together with their data (mood, genre, tempo, instruments, duration, generation prompt, license chain), and each cue of the episode is recorded as a use of its track (episode, cue, in/out, scene). Later episodes look in the library first: the workstation lists / searches it with the new host CLI code/music_library.py (list, search, use, sync, annotate, add, check), reuses a fitting track with use (the cue's license becomes source: library + track_id and carries the original license chain) and generates new music only when nothing fits, stating the reason per cue in the cue sheet's music_library_report. Selection order is now user music in refs/music/ → project library → new generation. A track its source episode no longer uses after a rework is marked replaced and is not offered any more (the file stays). New machine check music_library_synced on p8-music; episodes delivered before this feature are backfilled and only get a reminder. Project settings → 「音乐库」 opens the new /music-library page: all tracks with playback, their data, source episode and the episodes / cues using them, filters by episode, source and keyword, and 「同步入库」 to backfill projects that already have scores (nothing is generated). New endpoints GET /api/v1/projects/{project}/previews/music-library and POST /api/v1/projects/{project}/music-library/sync ({ep?}); music and copyright SOULs, WORKFLOW Phase 8 and workflow.yaml updated; UI strings translated in all 11 dictionaries. Requires a service restart and a hard refresh.

  • 「+」attachment button on the console chat box and the preview pages' 「修改」 popup. A round 「+」 at the top-right corner of the input (the voice-input microphone, when enabled, sits below it at the bottom-right) (apps/web/static/file-attach.js, shared by index.html and edit-popup.js) lets the user pick local files to send along with the message. Nothing is uploaded: the agent runs on the same machine as the browser, so only the absolute paths are appended to the message as a trailing 「附件(本机文件绝对路径,请直接读取):\n- /abs/path」 paragraph (header follows the UI language) and the agent reads the files itself. Paths come from ① the desktop client's native open dialog (videoagentsDesktop.pickFiles, new desktop:pick-files IPC), ② in the browser, a native dialog opened by the web server on the host (POST /actions/pick-files: macOS osascript, Windows PowerShell, Linux zenity/kdialog; 409 while one is open, 501 when no dialog is available), or ③ a manual paste box (one absolute path per line; also via Shift+click, and used automatically for a remote backend or when no dialog can be shown). Selected files show as removable chips above the input; the console keeps them with the draft (localStorage webui_draft_files) and hands them back if sending fails; the popup clears them on every open. Requires a service restart (new web-server endpoint) and a hard refresh; the desktop bridge takes effect with the next desktop build.

  • Script preview: 「🗒 注释」 buttons and 「📨 提交注释」. Every 「✏️ 反馈」 button on the script preview page (whole episode, scene, each action / dialogue / narration / transition block, narration entries, hooks, event cards, pacing, trim suggestions, episode plan) now has a 「🗒 注释」 button next to it that opens the same note dialog as the storyboard page; notes are stored in story/episodes/epNN/script_notes.json keyed by a stable object id (*, S01, S01/b3, nar:ID, hook:ID, event:ID, pacing:S01, trim:S01/n, plan) together with the button's location text (object description + file path), shown in yellow next to the object and in the button tooltip. 「📨 提交注释」 at the right end of the episode row sends all of the episode's notes to the reviser as one revision order (kind=script, files = the episode's screenplay / narration / hooks / pacing / breakdown plus story/events.json / story/episode_plan.json when referenced) and clears the file once the run is dispatched. New endpoints POST /api/v1/projects/{project}/script/{ep}/notes ({key, text, label?}, empty text deletes) and POST …/script/{ep}/notes/submit; the reviser SOUL lists the file as must-read for script-layer objects. Requires a service restart and a hard refresh.

  • Storyboard page and storyboard preview: 「📨 提交注释」 submits every note of the episode to the reviser in one go. A new button at the right end of the episode row (both pages) collects all of the episode's notes — the storyboard page's whole-episode / scene / draft-shot notes from directing/epNN/storyboard_notes.json, or the storyboard preview's per-group director notes from assets/notes/epNN/<grp>.json — and the host composes a single revision order for the reviser (kind=storyboard or kind=group), listing each note together with the object it is attached to (scene number / location / scene id; draft shot order, content summary and final shot ids; for groups: scene, shot ids, duration, beat and the group-prompt path; stale notes whose object is gone are marked as such). Once the run is dispatched the notes are cleared: the storyboard notes file is deleted and each group note is removed the same way as 「清空保存」 (its injected Director's note sentence leaves the video_prompt). The button shows the note count, is disabled when the episode has none, asks for confirmation, and the order text follows the UI language (Chinese / English). New endpoints POST /api/v1/projects/{project}/board/{ep}/notes/submit and POST /api/v1/storyboard/notes/submit ({project, ep}); reviser SOUL and WORKFLOW §5 updated. Requires a service restart and a hard refresh.

  • Transition design stage 2: the 07-directing/transition-design workstation and generated establishing shots. A new per-episode DAG node p6-transition-design (condition transition_design_enabled = effective transition mode ≠ minimal; after shot-planning / continuity / plates, feeding the H3A gate) runs the host CLI code/transition_design.py propose and then reviews every group boundary: it checks the change class against the director's transition list and the screenplay's 「转场:」 lines, fixes inferred card text with card, and picks between candidates with the new design --alt N | --design '<json>' --note … verb — proposals stay proposed; accepting is the user's decision on the storyboard preview's transition cards, and signing H3A now accepts every remaining proposal into shot_list (the sign card shows the pending count, same precedent as H3W). Boundaries decided by the user or the director's list are locked against the workstation (feedback only), and re-running propose no longer overwrites a workstation design. When the mode allows generative transitions (cinematic / custom), 「换场景 → 定场空镜 + 叠地点字幕」 is now a real image-to-video establishing clip (source.mode = i2v): the host renders the first-frame still from the scene panorama anchor or the first shot's plate (clips --prepare) and composes the prompt; because this involves video generation, the clip itself is produced in Phase 7 by the new per-episode node p7-transition-clips (video-generation, condition transition_clips_requested, after H3A and before H3B) into assets/transitions/epNN/<B-id>.establishing.mp4, checked by clips --check (transition_clips_ready); render_transitions.py build consumes the file (trim to the insert length, location overlay composited as before) and reports missing / inserts_built FAIL instead of silently falling back to a panorama sweep. Classic / minimal modes keep the ffmpeg pan / Ken Burns establishing and never dispatch the clip node. Also: transition_design.py check gains transition_design_generative_allowed and a transition_clips_ready warning, accept-all for re-running the H3A acceptance, the storyboard transition card shows 「🤖 工位建议」, the first-frame still and a 已生成/待生成 chip for i2v inserts, and the agent system prompt states the project's transition mode and whether the two nodes are instantiated. Design note: docs/transition_design.md.

  • Transition design stages 3 and 4: generative bridges, paired camera moves and audio leads (J-cut). In modes that allow generative transitions, entering a flashback / dream / imagination block now proposes a bridge insert (2 s image-to-video morph from the previous group's last frame to the next group's first frame, host-composed prompt keyed by block kind, white-dip fallback kept as a candidate; leaving a block offers a "bridge back" candidate; custom mode's bridge option is honoured). The two stills are extracted by transition_design.py clips --prepare from the adjacent group clips, so p7-transition-clips now also depends on the episode's p7-video; the contract requires file and forbids combining with motion_pair. Paired camera moves are a new transition_in.motion_pair {out, in, speed} field with a fixed legal-pair table (MOTION_PAIRS, check motion_pair_valid); they are realised as two host-written sentences in the adjacent group prompts — code/sync_motion_pairs.py --write appends 「【运镜对接】…带出画面…」 to the previous group's last Shot N: paragraph and 「【运镜对接】…接入…」 to the next group's Shot 1: paragraph (English Motion pair: sentences for non-Chinese UIs), idempotently, and motion_pair_bound joins the p7-prompt acceptance list. Audio lead audio_lead_s (0–1 s) is now enforced (transition_audio_lead_valid: hard_cut / dissolve only, no inserts or black hold, not on the first group) and surfaced to the mixer: mix_basis.py sources lists boundaries that only carry a lead, includes the lead in the boundary fingerprint (unchanged for projects without leads) and prints a LEAD line; the audio-mixing SOUL specifies the J-cut (next group's native track starts lead early with crossfades, BGM / narration / total length unchanged), and mix_basis_current reports a lead-only change as stale with a dedicated message. Cinematic mode offers "paired move + audio lead" and "hard cut + audio lead" candidates and puts a 0.5 s lead on the overlay-only scene-change option; custom mode gains motion_pair and j_cut options in the 视频节奏 dialog; the storyboard transition card shows the paired move, the lead and bridge first/last stills. Classic / minimal modes never propose any of these.

  • Scene preview: the per-scene 「背景图模式」 selector shows 「全景图」 and 「世界模型」 again. The scene-level plate-mode dropdown on the scene preview page now lists all three modes (跟随全局 / 九宫格 / 全景图 / 世界模型) instead of hiding the panorama and world-model entries; the project-level Output-settings dropdowns still expose only 九宫格 and the default stays grid. Static file only (no service restart), hard-refresh the page.

  • Shot plate 「✏️ 修改」 now regenerates from the current plate instead of re-deciding the camera. Sending feedback on a shot plate from the storyboard preview goes to the reviser as before (kind=shot_plate), but the reviser now runs the new host CLI code/revise_shot_plate.py --shot <shNNN> --role <start|end> --change "<change in English>" --note "<user's words>": the shot's current plate is passed as [Image 1] (authoritative for camera, framing, layout, dressing and light; only the requested change may differ), one new plate is generated into the scene library as <key>_rev<N> (revised: true, pano_ref.kind = revision with the source key/kind and the request) and only this shot's start/end entry in directing/<ep>/shot_plates.json is pointed at it (reuse: revised, revised_from kept), followed by the usual group-prompt sync. The original image and its library entry are never touched — whether it came from a nine-grid tile, a nine-grid fill-in, a panorama/world-model capture, a master plate or a hand capture — and other shots that reference the same original are unaffected; 「🔁 换图」 can swap back at any time. Non---force render_shot_plates keeps the revised plate (fresh record) and --status does not flag it as legacy. The storyboard preview labels it 「起点 · 按修改意见重出」, the scene preview and the swap dialog show 「按修改意见重出(基于 <原 key>)」, and the feedback popup now carries the current file plus this command instead of render_shot_plates.py --force. shot-plates / reviser SOULs and docs/shot_plates.md updated. Requires a service restart (preview data) and a hard refresh.

  • Scene preview 3D whitebox: the orbit view is now driven like the world-model viewer. The 「旋转视角」 viewport on the scene preview page no longer orbits around the scene centre: drag turns the view in place (yaw/pitch), the wheel moves forward/back along the line of sight, and after clicking the view W/S move along the line of sight, A/D strafe, Q/E raise/lower and Shift triples the speed (movement scales with the scene's framing radius). The initial framing is unchanged. The focused viewport shows a blue inset ring, and the hint line under the toolbar lists the controls. Static files only (no service restart), hard-refresh the page.

  • /stop all chat command. Sending /stop all to any agent from any channel (web chat, Feishu, WeChat, WhatsApp) is intercepted in api_chat the same way as /auto — no run is dispatched and no engine quota is used — and stops every running/queued agent task across all projects via the same entry point as the run panel's 「⏹ 停止」 button (api_stop_all, so downstream sees 「已被用户手动停止」 rather than an error). /stop alone reports the current running/queued counts; the reply follows the UI language and warns when the project's auto-run watchdog is still on (it would dispatch again once idle — send /auto off to stop it for good). Requires a service restart.

  • Hand-captured plates from the panorama and world-model viewers. The scene preview page's 360° panorama viewer gets a 「💾 背景图」 button (top right, with a dashed frame showing the saved area) and the world-model (Spark) viewer gets the same button in its toolbar; the world viewer now starts with the whitebox wireframe off and no longer draws the panorama-camera marker sphere. Pressing the button renders the current view off-screen at the project plate size and posts it with the derived whitebox camera (position / target / fov) to the new POST /projects/<p>/scenes/<sid>/plates/manual, which validates the aspect, computes the camera facts, stores plates/<plate_key>_hand<stamp>.png (+ sidecar) and adds a master / manual library entry (pano_ref.kind = pano_manual | world_manual, view angles recorded). The plate shows up in the scene's plate list (caption 「手工截取(全景 A3)」 / 「手工截取(世界模型)」) and in the storyboard 「换图」 candidates. Requires a service restart.

  • Plate mode defaults to nine-grid; panorama and world-model options hidden. output.plate_mode now defaults to grid for new projects, the two Output-settings dropdowns and the per-scene selector on the scene preview page show only 「九宫格」 (the pano / world options remain as hidden entries so existing projects and scenes keep displaying and running their stored value; the backend still accepts all three). Grid tiles split from the 3×3 sheet are now stored as JPEG under their .png names like every other library plate (was true PNG, ~1.8 MB per tile; existing tiles converted in place). Requires a service restart for the new-project default.

  • Nine-grid plate mode now includes automatic per-shot fallback plates (default; render_shot_plates.py --no-grid-fallback disables). In plate_mode = grid each shot still picks the nearest of the nine tiles first; when that tile is unsuitable (bearing delta > 30°, pitch delta > 20°, camera distance > 6 m or height class delta ≥ 2 — GRID9_FIT) the script now renders a dedicated plate for that shot's own whitebox camera, using the scene's top-down plan as [Image 1] and the whole 3×3 grid sheet as [Image 2] (build_grid9_fallback_prompt), stored in the scene plate library as grid9_fallback entries (<plate_key>_g9fb) and reused for nearby cameras (GRID9_FB_TOLERANCE). The episode index records reuse = grid9_fallback plus the rejected tile's scores and the reasons; shots already indexed on an unsuitable tile are re-decided on the next run (the grid sheet itself is never re-rendered; --force redoes only the targeted fallback plates, --no-grid-fallback --force re-renders the grid). code/grid9_compare.py --project <slug> --ep <ep> --scene <sid> writes a comparison page qa/grid9_compare/<sid>.html (whitebox frame / picked tile with scores / fallback plate side by side). Also fixes the plate-prompt inventory counting an object's parts (*_top, *_leg_1…) as several objects ("the glass table (4 of them)").

  • NSFW mode (Settings → Advanced → 「NSFW 模式」, default off). When enabled, the user configures three fallback models — a language model (engine + model), an image model (provider + model) and a video model (provider + model) — plus an 「审核拒收后自动切换」 failover switch (default on). Routing is three-layered and host-side (members never pick engines or providers for it): ① explicit marks first — p5-breakdown writes events[].content_flags (nudity / sex / gore / extreme_violence / drugs / self_harm, [] when none) into script_breakdown.json, shot-planning derives a per-group nsfw: true|false in shot_list.json from the scene's events, the user can toggle 🔞 on a group card in the storyboard page (assets/group_settings/epNN/grpNNN.json#nsfw, nsfw_reason: manual, overriding the derived value), and the orchestrator dispatches flagged work with the new optional work-order field nsfw: true / dispatch.py --nsfw — the host then runs that member on the fallback language model and sets VIDEOAGENTS_NSFW=1 so its genmedia image/video calls use the fallback providers (genmedia.py info --group shows 「🔞 NSFW 路由」); ② moderation-rejection failover — when a provider rejects a call for content (Ark *SensitiveContentDetected, MiniMax 1026, Fal/OpenRouter content_policy) genmedia re-submits once on the fallback provider within the same call, logs [genmedia] NSFW 兜底, records meta.json#nsfw_route, and this switch does not count toward the user's retry budget; a language-model refusal is not switched in-process — the host marks the run nsfw_suspect, records the task_id in runs/nsfw_suspect.json, and the second dispatch of the same task_id automatically uses the fallback language model (「第二次尝试才换」); ③ stickiness — a successful failover writes nsfw: true, nsfw_reason: failover to the group's settings so later re-rolls go straight to the fallback provider. A configured-but-unusable fallback (no key) raises instead of silently falling back to the primary model; a missing fallback for a modality only warns. New WARN-only check nsfw_hint (code/check_nsfw_hint.py) flags groups whose shot text / prompt looks sensitive but is not marked, and the storyboard page shows a yellow hint on them. The mode only decides which models are used; content-safety and platform-rating checks are unchanged, and agents must not fabricate content_flags. All agent clauses are conditional on the injected 「## NSFW 模式:开启」 section (WORKFLOW §5/§6/§9, timeline-story and shot-planning SOULs, docs/script_breakdown.md, code/README.md). Requires a service restart.

  • Upscale settings: Agentics tab. The "🔍 Upscale" card on the Generation Models page gains an Agentics tab (genconfig.upscale.provider = agentics, upscale.agentics.profile_code, default scale-seedvr2). It reads the same Agentics media catalogue as the video section, filtered to media_type = Scale profiles, and bills the signed-in desktop account. genmedia.py upscale uploads the source clip to the profile's declared video slot, submits the target tier / size / seed through the fixed profile fields, rescales a mismatched output to the delivery size, and records upscale{provider: agentics, model, params.task_id…} in the clip's meta.json; the task is a long task like video, so an interrupted process resumes with reclaim --task-id <UUID> instead of resubmitting. No fallback, as with the other upscale providers.

  • Episode close: the cut no longer stops dead on its last frame. A new episode close setting (Video Rhythm dialog and new-project wizard, settings.json#transitions.episode_close, present in every transition mode) decides how the main body ends: fade to black (default: 1.0 s fade + 0.5 s hold), fade to white, cut to black (cut_black, a sudden blackout for suspense: no fade, the last frame cuts straight to a black hold and the audio stops at the same instant), cut to white, or none (hard_cut, the previous behaviour). An episode can override the project value from the new "⏹ Episode end" row in the storyboard page's transition summary bar (POST /transitions/<ep>/close, written to a new optional top-level shot_list.episode_close field; the director's transition list may also name it and shot-planning writes the field; machine check transition_close_valid). render_transitions.py plan appends one to_group: null entry to timeline.transitions[]; render re-encodes only the last group (fade-out / cut + tpad colour hold; a cut's own audio track is faded or cut at the same moment and padded with silence); check folds the hold into duration_as_planned / audio_stream_intact, verifies the last frame and hold midpoint (for cuts also that the last content frame was left untouched), whitelists the window for no_black_frames and records transitions_render.json#episode_close {fade_end_s, end_s}; preview --boundary episode_close renders a tail preview (also from the storyboard row). The hold sits after the end of the picture, so it is deliberately not a timemap op and not part of the mix boundary layer (mix manifests and stamped fingerprints are unaffected); finalize_episode.py assemble reads the ledger and applies the matching afade (fade types) or a 20 ms de-click cut (cut types / hold_audio: mute) to the external mix at the moment the picture is fully black, pads the hold with silence, and no longer warns about the picture being longer than the audio by exactly the hold. Existing projects pick up the default fade on their next transition render (cut_v2 is now produced even for an all-hard-cut episode unless the close is set to none). WORKFLOW §9C, transition / shot-planning / director / audio-mixing SOULs, all 11 UI dictionaries. Requires a service restart.

  • Third background-plate mode: nine-grid (output.plate_mode = grid). Next to Panorama and World model (project output settings and the per-scene override on the scene preview page) a whitebox project can now have render_shot_plates.py generate, per scene and lighting scheme, one 3×3 grid image from the nine semantic camera positions in the scene's layout pack (layout.json#views, tile 1..9) with the top-down plan and a host-made blank tiling template as references, split it into nine background plates filed in the plate library (pano_ref.kind = grid9; tiles are not master plates and never go through homography derivation), and then pick for every shot the tile whose synthesised camera (landmark → whitebox coordinates, height by angle, field of view by size) is closest to the shot's whitebox camera (bearing, horizontal distance, height class, pitch, field of view; the score breakdown is logged per shot and stored in the episode index). The grid sheet <scheme>_grid9.png (+ .json ledger) is never attached to video prompts and deliberately avoids the retired grid_9views* file prefix that three checks blacklist; a layout without nine views raises Grid9LayoutError. docs/shot_plates.md "背景图模式", shot-plates SOUL, WORKFLOW §9. Requires a service restart. Exercised once for real on fengshen3 ep06 SCN-0121 (38 shots → one grid, nine tiles).

  • Fancy text (花字) gets its own section on the Post-production page, with an auto/manual mode and a per-type picker. The packaging layer used to have one row of five switches ("Intro/outro / caption switches"); it is now two rows: "Intro/outro" (intro, outro, teaser, subtitle burn-in) and "Fancy text" (enable switch, a mode dropdown and a type picker). Mode auto (default) lets the caption agent choose usage types by genre; manual opens a dialog grouped by four categories (narrative info labels, structure and pacing, emotion and variety-show style, functional and compliance) with select-all/clear per category and a "suggest by genre" preset menu, and only the checked types may appear in the episode's captions (checked = allowed, not required). Switching to manual opens the dialog first and falls back to auto if it is cancelled; saving manual with nothing checked is refused by the server. Types that a project cannot use are greyed out with the reason: in audio-to-video projects every caption must come from the master transcript and align to speech, so types that are usually not spoken (inner voice, onomatopoeia, footnotes, disclaimers, …) are unavailable, and types that need upstream data (cultivation table, prop/creature cards, dictionary, world bible) are unavailable until that data exists. The single source is the new modules/caption_catalog.json (35 usage types; the seven legacy type values remain valid; four usages are listed as excluded for now: show-title corner bug/watermark because it persists across groups, teaser hook and credits which belong to the title station, transition cards which belong to the transition station). New settings output.caption_mode / output.caption_types; captions.json gains an optional per-caption tier (headline/keyword/label size band, inferred from the type when absent, em_pct is now validated per band) and a top-level caption_policy stamp; new design-stage machine checks caption_types_allowed (manual mode: every type is in the checked set) and caption_policy_fresh (stamp equals current settings, so changing the selection without re-running the design fails); new CLI render_captions.py policy [--stamp] prints the effective policy and writes the stamp. The caption prompt injected into caption/edit agents was also brought up to date (it still described schema v2 with ≤4 style presets and the old density rules) and now lists the allowed types. WORKFLOW §9A, workflow.yaml, the caption SOUL and the caption-styling skill are updated; all 11 UI dictionaries gain the catalog labels.

  • Issue feedback reports now carry the user's client version. The metadata block at the end of an auto-filed GitHub issue (Settings → Advanced → Diagnostics, "Issue feedback") gains a "客户端" line next to the server version: the desktop app passes its own version and build (VIDEOAGENTS_CLIENT_VERSION, VIDEOAGENTS_CLIENT_BUILD = channel/buildHash) to the Python service it launches, so a report reads e.g. desktop 1.0.36 (release/abc123); a browser talking to a locally started service records web. Stored as client_version in the local ledger; the prefilled "submit in browser" link includes it too. Requires a service restart (and a rebuilt desktop app for the desktop value).

  • Claude Opus 5.5 in the language-model menus (claude-opus-5-5, listed as opus-5.5 right above opus-5 in both the top-bar model dropdown and the per-agent model dialog). The sonnet alias is now labelled "sonnet (latest)" and sits directly below "opus (latest)", followed by a new "fable (latest)" entry (the CLI fable alias, currently resolving to claude-fable-5-1); model ids are unchanged, so existing settings keep working. Not a default: Smart Assignment still maps to opus / sonnet.

Changed

  • Sample-reel dialogue now keeps pace with the shots. Dialogue voiced by the dialogue TTS library ran long in the whitebox reel and the animatic — seed-audio speaks at about 3 characters per second and pauses 0.6–1.7 s at punctuation, while shots are timed from est_duration_s (about 4.2 characters per second) — so lines dragged into the next shot and talked over the next line. The library now also writes a paced copy of every line that runs longer than its est_duration_s (assets/audio/voice/<ep>/tts/_paced/, per-line pace{file,duration_s,tempo,pause_s,target_s} in tts_manifest.json): pauses inside the line are first shortened to 0.3 s, and only what still does not fit is sped up without changing pitch, capped by the new project setting output.dialogue_tts_max_tempo (1.0–2.0, default 1.5; 1.0 turns pacing off; CLI code/dialogue_tts.py --max-tempo). This is post-processing of audio already synthesized — no TTS call is made and nothing is billed. Both sample reels use the paced copies (line_audio(paced=True)); the library files themselves stay at natural speed, and dubbing (code/dub_group.py) keeps using those. When a line still runs past its shot, the first line of the next shot now waits for it to finish (delay_s, at most half of that shot) instead of overlapping. Reels built before this change are rebuilt with the 「白模样片」/「动态样片」 button.

Fixed

  • Network fail-fast message now follows the interface language. The run error, the chat entry and the live run log written when an agent run is killed after Connection dropped / connection timeout (🔌 net_error) were Chinese-only; with a non-Chinese interface language they now read in English (Network interruption: … (CLI api_retry #n, error: …) (network fail-fast after Ns of running)), same rule as other server-side texts. The 🔌 marker and the net_error field, which the orchestrator relies on, are unchanged. Requires a service restart.

  • Fancy-text (花字) rendering now follows the post-production timeline (code/render_captions.py, new modules/caption_timeline.py; fengshen3 ep06 DEF-ep06-caption-0001). The caption tool only knew the v0 master clips and the original rough-cut timeline: copies were burned from the masters (adopted post versions such as graded or cut-down groups were lost) and the SFX track / speech alignment used the rough-cut clock, so a final with cut-outs (−19.24 s) and title-card inserts (+10 s) drifted. One resolver now answers "which file, which second" for every caption: the burn source is each group's current adopted version (master when none), local_start/local_end stay in master-v0 coordinates and are mapped through the version's time_ops (a caption inside a deleted range is dropped with a warning), episode-level start/end, word_track.json and speech-lookup are all on the cut basis (adopted versions + boundary layer, no intro), and the final composition adds the intro offset from final_layout.json. The caption-final work order becomes one host command, render_captions.py final: it composites all stickers onto the clean final.mp4 (same EDL by construction, so post versions, transition cards and intro line up), builds the SFX track on the same clock, muxes a:0 premix / a:1 archive, and writes edit/epNN/final_caption.json (idempotent; sfx-track + mux --video remain as the legacy path). speech-align folds the transcript and the mixed audio to the same clock before aligning (the mix may or may not already carry the boundary layer, decided by the mix manifest exactly as finalize_episode.py does, whose load_timemap moved to modules/timemap_layers.py); speech-snap writes local back in v0 coordinates. check_captions.py: caption_time_consistent now verifies the cut-basis start and fails captions that fall inside a deleted range, render receipts are keyed to the adopted version (src_version), and a new final-stage caption_final_fresh compares the receipt with the current clean final / captions / audio / timeline. New render_captions.py timeline prints the resolved axis. WORKFLOW §9A, the caption and edit SOULs, workflow.yaml and the injected caption prompt describe the new contract. Existing projects: re-run speech-align → speech-snap → render → final (old copies and word tracks are reported stale).

  • BGM no longer drops out inside transition inserts (title cards / establishing shots / holds). The mix (assets/audio/final/epNN.wav) was built on the pre-transition timeline and finalize_episode.py then cut it open at every boundary insert and filled the gap according to the op's audio policy (mute → digital silence; sustain only loops the last second), so the music stopped for the whole card (fengshen3 ep06: −91 dB for 2.5 s under cue 04 and cue 06). The boundary layer is now part of the mix basis: code/mix_basis.py sources derives it from shot_list.transition_in (freeze / hold / inserts, frame-quantised like render_transitions.pad_ops) and reports it as boundaries[] with each group's cum_start_s already including the preceding boundaries, so the mixing agent lays BGM and narration on the final, transition-inclusive timeline (a cue spanning an insert keeps playing) while the native track leaves a gap per the boundary's audio policy; stamp records the boundary fingerprint and checks wav length = Σgroups + Σboundaries. finalize_episode.py compares that fingerprint with the cut's transitions_render.json#timemap: when they match the external audio is no longer remapped through the boundary layer (subtitles still are); a mismatch, an old manifest without the layer while the cut has inserts, or a mix with the layer but a cut that never went through render_transitions fails mix_basis_current / refuses to assemble and asks for a p8-mix rerun. WORKFLOW §8B ④ and the audio-mixing SOUL describe the new placement rule.

  • Transition title cards and overlay captions now use the project's fonts (refs/fonts/, WORKFLOW §2 rule 9). render_transitions.py build used to pick the first usable face from the global data/fonts/ directory or the system, ignoring fonts uploaded on the Reference Files page, and settings.json#transitions.card_style was never read at render time. The host now resolves the card font once per project (modules/transition_design.py resolve_card_font): the optional new transitions.card_font setting (a project-relative path such as refs/fonts/huiwen.ttf, or a caption-engine id such as proj:Huiwen-mincho) wins, otherwise the first file in refs/fonts/ is used for every card and overlay in the project, otherwise the previous global/system order applies. The font file (name / size / mtime) is part of the build fingerprint whenever a boundary renders text, so a changed or newly uploaded font rebuilds the affected cards on the next build / render without --force; projects without project fonts keep their existing fingerprints. Build logs and meta.json record the face that was used. render_transitions.py check gains a staleness test inside inserts_built: a segment or overlay built after the transition cut was rendered (e.g. build after a font change without render) now fails with "构建晚于成片(重跑 render)" instead of passing and showing the boundary as rendered on the storyboard page.