Skip to content

v1.0.38

Choose a tag to compare

@github-actions github-actions released this 08 Oct 05:07
· 142 commits to main since this release

Added

  • Music library: themes, variations and a reuse quota (2026-10-07). With the library in place the music station had started scoring whole episodes by reuse alone: in one project episodes 7 and 8 took every cue from episode 6's ten tracks, one track was used five times, two tracks twice in the same episode, a 28-second opening cue was clipped to 6 seconds for a build-up beat, and not one reuse carried a reason. The rule said "reuse when something fits" and only new music had to be justified, so nothing was ever generated. The library is now two-tiered: a track with a theme_id is a season theme (a motif that runs through the season, registered in the library's theme table with a name, what it stands for and an English motif description) and may be reused as is across episodes; a track without one is a one-off cue and stays in its episode. Each cue is now one of three things: reuse as is (use), a theme variation (variant --track shows the base track's motif and prompt; the station regenerates the same motif with a different tempo, instrumentation, intensity or length, and the cue records variant_of; the new track inherits the theme), or new music. A per-episode quota is enforced by music_library_synced: at most 40% of cues reused as is (at least one allowed), at least 30% new or variations, a track at most once per episode and in at most three episodes, theme tracks only, a reused cue at least 60% of the track's length, and the cue's intensity (the 0–1 figures in mood or intensity) within 0.25 of the track's; reuse, variations and new music all need a reason in music_library_report. use refuses a one-off track, a track already reused in three episodes, or (with --need-duration) a cue shorter than 60% of the track, with exit code 2. New CLI commands theme (list / --add), variant, and annotate --theme / --variant-of; search --theme. The 音乐库 page gains a 主题 column, a 复用集数 column (n/3, red when full), a variation badge, a theme filter and the quota in the toolbar. Episodes delivered before this change fail the check as they stand (episodes 7 and 8 of the project above); rescoring them is the user's call. Music SOUL, WORKFLOW Phase 8, workflow.yaml, code/README.md and all 11 dictionaries updated; tests cover the quota rules. Restart the service for the page data.
  • Fal model discovery (2026-10-06). 生成模型 › 图像 / 视频 › Fal gains a 搜索 Fal 模型 box. It searches Fal's public model catalog by keyword (empty lists the popular ones), merges a model's task endpoints (text-to-video, image-to-video, reference-to-video, edit…) into one row, and 选用 fills the model field: models already in the built-in list are selected, others go into 自定义. Until now a model outside the built-in list was sent a fixed set of field names, which most endpoints reject (Veo 3.1 wants duration: "8s", Kling 2.5 wants "5" and tail_image_url). genmedia now reads that endpoint's parameter list from the catalog and builds the request from it: only fields it accepts are sent, duration, resolution and aspect ratio use its closest allowed value (logged), the endpoint is chosen by input (including a separate first-last-frame endpoint when there is an end frame), and an input it cannot take (end frame, reference images, video or audio) fails with a clear error instead of being dropped. Models with a built-in mapping are unchanged; an endpoint the catalog does not list (a private app) or an unreachable catalog falls back to the old generic mapping. The catalog needs no key but rate-limits anonymous use, so the Fal key is sent when one is configured; endpoint lookups are cached for 24 hours. New module modules/fal_models.py, endpoint GET /api/v1/providers/fal/models. Request shapes were checked against the live catalog; no paid generation was run with a discovered model. Restart the service to get the endpoint.
  • Fal jobs survive an interrupted run (2026-10-06). A Fal image, video or sound-effect job used to be lost if the genmedia.py process was killed while waiting (tool timeout, agent lifetime): the request id only went to the log, and running the command again submitted and billed a second job. The request id and its status/result URLs are now written to data/.videoagents/pending_tasks/ as soon as the job is created, before polling starts, and removed once the result is downloaded. Running the same command again picks the unfinished job up instead of submitting a new one. "Same" means same output path, endpoint and request body; the seed and the signature part of presigned URLs are ignored, and a changed prompt or reference submits a new job. If the old job can no longer be queried, or is still unfinished six hours after submission, it is dropped and a new one is submitted. genmedia.py reclaim --output <path> now also works for Fal video jobs without --task-id; it only queries and downloads. Other channels are unchanged. Covered by tests with a fake queue; not yet run against the real Fal service.
  • Sound effect generation: genmedia.py sfx (2026-10-06). The sound-effect and ambience stations were told to make a fallback patch when a defect ticket asks for one, but there was no way to generate it. python3 modules/genmedia.py sfx --prompt "<sound description>" --output <file.wav> [--duration s] [--loop] [--count 1-4] now produces it with ElevenLabs Sound Effects v2. There is no new settings tab: it uses the ElevenLabs key already entered for music or TTS (direct API, 0.5–30 s), and otherwise the Fal key entered for image or video (hosted endpoint, 0.5–22 s); --provider forces one. --loop returns a seamlessly looping bed and leaves it untouched. Other sounds have leading and trailing silence trimmed (pauses in the middle are kept) and the peak normalised to -3 dBFS; this needs ffmpeg. Each file gets a .meta.json with the source, the prompt and the measured duration and levels. Agents only call it when a defect ticket asks for a patch. Not yet run against a paid account. Restart the service for the agent instructions to pick it up.
  • Post page: letterbox bars to a final output aspect ratio (2026-10-06). 后期处理 → 包装 gains a 上下黑边 card under 花字: tick it and pick a final output aspect ratio (16:9, 9:16, 4:3, 3:4, 1:1; output.letterbox_enabled / output.letterbox_aspect, off by default). When the final is rendered, finalize_episode.py assemble scales the picture to fit and pads it with black bars to that ratio, so a 2.35:1 film can ship as 16:9 and a 16:9 film as 9:16. The canvas keeps the cut's short side (2520×1080 becomes 1920×1080; 1920×1080 becomes 1080×1920). Production stays at the project aspect ratio: group clips and cuts never carry bars. The layout is recorded in final_layout.json#letterbox, finalize_episode.py check gains letterbox_applied (fails when the setting changed and the final was not rendered again), and the fancy-text final sizes and anchors its captions to the picture area instead of the bars. Render the final again after changing the setting.
  • Output settings: cinema aspect ratio (2.35:1) (2026-10-06). 输出设置 → 输出画幅 (and the new-project wizard) gain a third preset, 电影画幅(2.35:1) (output.aspect_preset: "cinema"). Image and video models only accept a fixed list of ratios, and their widescreen entry is 21:9 (≈2.33:1), so the project is produced at 21:9 end to end: agents pass --aspect 21:9, whitebox renders at 952×408, the 1080p master is 2520×1080, and newly generated anchor images use 2968×1272. Also fixes transition insert-clip first frames, which read a settings key that never existed and so were always 1920×1080 regardless of the project aspect (portrait projects now get 1080×1920).
  • Director's console: bulk approve and bulk answer from the top bar (2026-10-05). The "✓ Approved n/N" pill is now a menu with Approve all, which approves every group in the episode that has been through whitebox staging and is not yet approved (or whose approval went stale); groups still on an auto-inferred draft are skipped and reported. A new "⚠ Questions a/N" pill next to it shows how many of the episode's Agent questions have an answer (chosen, applied, or accepted with a group approval) out of the total; its menu has Pick recommended for all, which answers every unanswered question — blocking and advisory alike — with the Agent's recommended option, falling back to the provisional choice where no option is recommended, and leaving questions with neither for you to answer one by one. Answers you already gave are not overwritten, and the picks are applied when the episode is submitted, like any other answer. Note that approving a group accepts its whitebox as it stands, so answers that have not been applied yet give way to the approval — pick and submit first if you want the recommended options carried out. New endpoints: POST /director/<ep>/approve-all, POST /whitebox/<ep>/issues/recommended.
  • Video prompt sign-off (H3V, DAG gate g7p, per episode). Once every generation group's video prompt in an episode is written (p7-prompt), the pipeline now stops for a user sign-off before any anchor pack (p7-image) or group video (p7-video) is produced. Review the prompts (text plus reference images / videos / audio) on the Shot preview page; the episode row shows the sign-off state with "Sign off / Hold" buttons that answer the same card as the console. The host enforces it twice: signing is refused (409) while any group still has no video prompt, and genmedia.py video refuses to write assets/clips/<ep>/grpNNN*.mp4 while the episode's g7p node exists in the DAG and is not passed (video_prompt_signed, modules/prompt_gate.py). The sign-off releases the first run rather than freezing the prompts: rewriting individual groups afterwards (your own edits, the reviser, defect rework) does not require signing again — the page only notes how many groups changed since sign-off (assets/prompts/<ep>/prompt_signoff.json). Episodes whose DAG was expanded before this change are not blocked; the orchestrator inserts the gate only for episodes that have not started video generation (2026-10-05).
  • Sound split default is now off for new projects (and for existing projects missing the key); auto must be chosen explicitly in 输出设置 (2026-10-03).
  • Sound split: off-screen (O.S.) and voice-over (V.O.) dialogue lines. Until now every line in a shot list implied that the speaker was on screen with a moving mouth, so storyboards never used off-screen dialogue, a reaction shot could not carry the other person's line, and a character's inner voice had no channel at all (narration is narrator-only). A new project output setting 「声画分离」 (output.sound_split: auto default / script_only / off) adds a placement field to every dialogue_lines[] entry — on (default), os (speaker in the scene but out of frame: phone, behind a door, walked out of frame) or vo (inner monologue, a letter being read, a remembered voice) — plus heard_in (the shots in the same generation group where the line is heard), source_fx (plain / phone / door / distance / inner / memory), offset_s and a placement_reason {trigger, evidence}. Two layers decide placement: the screenwriter marks objective off-screen sources in the speaker parenthetical ((O.S.) / (V.O.), whitelist S-phone / S-door / S-exit / S-remote / S-inner / S-letter / S-memory; downstream may not move them back on screen), and in auto mode shot-planning may move an on-screen line off screen only for whitelisted directing reasons D1–D5 (reaction-shot carry-over, time-budget overflow instead of trimming text, a fourth speaker's interjection, speaking while leaving frame, an establishing shot before the speaker appears), never for a line that carries the speaker's own performance beat. Off-screen lines are never written as {} into the group video prompt and never get the speaker's audio_ref; they are synthesized in post with the character's cast voice by the new host CLI code/offscreen_lines.py plan|synth|check (new DAG node p8-offscreen, also in native-voice projects — the TTS red line only ever protected lip sync), processed per source_fx, fitted into the heard_in window, and mixed by audio-mixing as a fourth 「画外对白」 track listed in mix_basis.py sources (offscreen_lines[], fingerprinted into mix.json; changing a line without remixing fails mix_basis_current). Group audio_plan is now judged on on-screen lines only (dialogue / voice_over — the old narration_over stays accepted as an alias / ambient_only), speakers_le_3 is now a real machine check that counts only on-screen speakers, dialogue-fit and time-budget checks no longer charge off-screen lines against mouth time, and dub_group.py skips them. The setting is orthogonal to the narration switch: narration covers the narrator's voice, a character's V.O. belongs here; a V.O. line must be in the character's own voice, not narration in disguise. Subtitles still appear for off-screen lines, prefixed (画外) / (V.O.); the shot-preview 「对白语音」 panel can change a line's placement, effect and offset. Spec: WORKFLOW.md §8D, docs/sound_split.md.
  • Transition design stage 4 redefined: sound bridges (J-cut / L-cut) replace audio_lead_s. The old audio lead laid the next group's whole native track lead seconds early, which put every dialogue group out of lip sync — that semantics is dropped, and legacy audio_lead_s values are normalized on read into transition_in.sound_bridge = {kind: "j", s, carry: "bed"} (the boundary fingerprint changes, so one remix is required; no compatibility switch). kind is j (next group's sound enters over the current tail) or l (current group's sound carries over the next head), s ∈ (0, 1.5], and carry is bed (a de-vocalized ambience bed pre-rolled or sustained by the new host CLI code/sound_bridge.py build|check into edit/epNN/sound_bridges/) or line (an off-screen line from sound split crosses the cut; only when 「声画分离」 is on and such a line sits next to the cut, otherwise it falls back to bed). New 「声桥默认」 settings transitions.sound_bridge_s (0.5) / sound_bridge_carry (bed); cinematic mode proposes a J bridge on scene changes and time jumps and offers an L candidate when the previous tail has no on-screen dialogue and the next head is an establishing / wide or silent shot; custom maps gain l_cut beside j_cut. The transition card gets a 「声桥」 row (none / J / L + seconds + carry). Audio-mixing now runs the build before laying tracks and keeps every group's native track in sync at its own cum_start_s (J bed: pre-roll file from cum_start_s − s; L bed: sustain file over the first s of the next group with its native track fading in); checks transition_sound_bridge_valid (replaces transition_audio_lead_valid) and sound_bridge_built (p8-mix). Spec: WORKFLOW.md §9C, docs/transition_design.md, docs/sound_split.md.
  • Sound split stage 3: native lead-in (native_lead) — in-group J-cuts generated natively by Seedance 2.5. An A/B on fengshen3 ep07 grp008 showed the model will start a line off screen in the preceding shot (lead-in 1.075 s, no repeat, speaker kept out of frame, same voice) but refuses to carry a line's tail past the cut into a shot without the speaker (0/2 — it pulls dialogue earlier and finishes it in the speaker's own shot). So only the J shape ships: an on-screen dialogue_lines[] entry may carry native_lead {shot: the previous shot in the same group, s ≤ min(prev shot, 1.0, 40% of the line), reason {trigger N1 insert/POV shot, N2 listener-first, N3 establishing opener, or U manual}, source}; the line stays on screen for every other rule ({}, audio_ref, speakers_le_3, timing). Prerequisites are machine-checked (native_lead_valid: sound_split=auto, native dialogue voice, effective model Seedance 2.5, previous shot in the same group with no speaker / lines / narration, first line of its shot, lead text without the performance trigger word). The host CLI code/sync_native_leads.py plan|apply|set|clear|check|sync|audible suggests candidates, writes the field, and injects two fixed marker sentences into the group prompt (previous shot: 「【原生先入】… {lead} …」; the line's shot: {full} → {rest} plus a continue-without-repeating sentence; idempotent, restorable, English Native lead: for non-Chinese UIs; prompt writers never hand-write them) — checked by native_lead_bound on p7-prompt. After generation audible verifies the voice onset lands in the previous shot (≥0.3 s) and only logs a WARN otherwise. L-cuts and fully off-screen lines stay with stage 1 (post-synthesized lines) and group boundaries with stage 2 (sound bridges). The 「对白语音」 panel gets a per-line 「原生先入」 toggle and the storyboard page a 🎧 badge. Spec: WORKFLOW.md §8D ⑨, docs/sound_split.md 三期.
  • Time scale: dialogue and action beats are costed in seconds, and the budget is checked at four stages. On fengshen3 ep07 group grp011 (28 s, 21 shots) the generated video dropped a 5-second line and scrambled its second half. The cause was not prompt length: three upstream layers were over-allocating time at once — line estimates counted only characters ÷ speech rate (about 30 % short against the dialogue speech library), dialogue shots were sized exactly to the estimate (34 of 44 shots at 95–100 %), and nobody had ever put a number of seconds on action beats (pacing split 28 s top-down, blocking put three beats into a 1.2 s shot, the prompt expanded them into five actions). modules/time_cost.py is now the single scale: a line costs 0.7 s onset/tail + characters ÷ (the character's speech rate × a pace factor fast 1.19 / medium 1 / slow 0.81) + 0.4 s per inner pause, and dialogue-rewrite writes the pace tier pace: fast|medium|slow next to the emotion so shot planning sizes shots by it; an action beat costs its minimum physical time from a keyword table (glance 0.5, throw 0.7, turn 0.7, step 0.8, fall 1.2, rise 1.5, effect 1.0, hold 0.3, default 0.7). code/check_time_budget.py applies it at four points: --scope script before pacing allocates (each scene's alloc_s must cover Σ line estimates with pre/post-speech margins + Σ beats; when the episode total exceeds the budget the host applies the user's order — first extend the episode up to the new 「视频节奏 → 可加长单集」 setting (default 50 %, settings.json#duration.episode_extend_pct, pacing records duration_budget_s + duration_policy), then trim dialogue (trim_targets), then merge short action shots (merge_candidates)); --scope shots for storyboard and shot planning (shot length ≥ beats + lines + margins; dense groups with an average shot under 1.5 s are capped at 15 s, also built into check_generation_groups.py); --scope blocking (Σ beat costs ≤ shot length − line estimate, beats of one character ≥ 0.3 s apart); --scope prompt (sequence connectors per Shot paragraph ≤ blocking beats + 1 — the prompt may detail a beat, never add one). check_dialogue_fit.py and dialogue_direction.py use the same scale, and a dialogue shot must now hold Σ estimate + 0.4 s before speech + 0.3 s after. After generation, code/check_dialogue_audible.py finds voiced segments without transcription and flags a dialogue shot whose longest voiced run inside its planned window is under 30 % of the estimate. Episodes written before 2026-10-03 only get warnings and keep the old estimate for consistency checks.
  • Line direction: director-style prompts and target durations for dialogue speech. Dialogue speech used to be synthesized line by line with at most a two-word emotion label, so delivery was flat and durations had to be squeezed into the shot afterwards. Each dialogue line in shot_list.json can now carry a delivery — direction (state + how to play it + pace / volume / pauses, naming the stressed words and how the line ends), scene (where, to whom, what just happened) and target_s — written right after shot planning by the new workstation 07-directing/dialogue-direction (workflow node p6-dialogue-direction, after p6-dialogue-fit, before H3A; only when 「生成对白语音」 is on or dialogue is dubbed). The workstation works only through the host CLI code/dialogue_direction.py (plan gives per-line context: script emotion, shot brief, the speaker's action, previous line, shot length, seconds for each pace; apply --file writes the batch; check is the dialogue_direction_bound machine check); it picks a pace (fast / medium / slow) and the host computes the target duration from the character count and clamps it to what the shot can hold. A direction is voided when the line's text changes later. With a valid direction, 火山 Doubao-音频生成 1.0 is prompted director-style — a one-sentence timbre taken from the voice card instead of the full acoustic spec, the direction, the scene, the target duration, and the reference sample declared as timbre-only — and speaks to the target duration; other providers receive the direction as a tone instruction only. Lines without a direction are synthesized as before and keep their cache keys. The 「对白语音」 panel on the storyboard preview page now lists every line with its script emotion, the library audio and an editable direction / scene / pace or seconds; saving marks only that line stale. dub_group.py passes the direction along and, on Doubao-音频生成 1.0, uses the mouth-open segment as the target duration instead of re-synthesizing at another speed. In a six-line test on fengshen3 ep07 the durations landed within about 0.4 s of the targets and five of six lines fit their shots, against one of six for the two earlier variants.
  • TTS voice modes: 「音色设计」 or 「音色库」 per provider. Every provider tab of the TTS card on the 「🎨 生成模型」 page now starts with a 「模式」 selector, and the fields below follow it. 音色设计 (voice design) asks for two models — a Voice Design model that turns the character's voice card into a voice sample, and a Voice Clone model that speaks dialogue and narration with that sample as the reference; 音色库 (voice library) asks for a voice library and a model, and each character gets one voice from the library, recorded in casting.json. Agentics and ComfyUI support both modes (their library is the local reference-audio library data/TimbreModel, used with the Voice Clone profile / workflow); 火山引擎 supports both (design: both slots are Doubao-音频生成 1.0, the only Volcengine model that takes a reference clip per call; library: Doubao-Seed-TTS 2.0 / 1.0); OpenRouter, ElevenLabs and MiniMax are library-only (OpenRouter speech models accept text only, so the local library cannot be used there; voices come from each model's own list). The mode is stored as tts.<provider>.voice_mode; existing configurations keep their behaviour (Volcengine on 音频生成 1.0 and ComfyUI with a Voice Design workflow become 音色设计, everything else 音色库). genmedia.py info prints the active mode.
  • Voice libraries are fetched and matched automatically. The new modules/voice_library.py pulls each provider's voice list on demand (Volcengine ListSpeakers, ElevenLabs account voices, MiniMax get_voice, the OpenRouter model catalog's supported_voices, the local TimbreModel catalog), caches it for 24 hours and ranks voices for a character from the voice card: hard gender filter, age band read from the card's age fields, language, shared timbre words, and voices already cast to another character pushed to the bottom. The dubbing workstation gets candidates with python3 modules/genmedia.py voices --character <CHAR-ID> [--variant …] [--search …] [--top N], registers its pick in casting.json, then synthesizes with --voice; the host never picks a character's voice silently at synthesis time. ElevenLabs matching only uses voices already in the account — the public Voice Library is search-only (voices --scope library --search …) and adding a voice to the account (voices --add <voice_id> --owner <public_owner_id>, which takes an account voice slot) happens only when a work order explicitly asks for it. Calls with neither a character nor a voice (narration without a narrator card, live lip-sync) take a narrator-type voice from the library automatically.
  • codex and pi Smart Assignment high tier now resolves to gpt-6.1-sol (was gpt-6-sol, which the official catalog now lists as the previous generation; needs codex CLI 0.159 or newer); the low tier stays gpt-6-luna. The tier is resolved at dispatch time, so existing installs on the codex smart policy pick it up after a restart; the top-bar tooltip and all 11 i18n dictionaries are updated. The pi engine's smart policy high tier moves the same way, to openai-codex/gpt-6.1-sol (low tier stays openai-codex/gpt-6-luna); it requires a pi version whose pi --list-models includes that model.

Changed

  • Publishing platforms moved from 输出设置 to the 成片发布 page, with per-channel publish buttons; Xiaohongshu is back (2026-10-07). The "发布平台" checkboxes are gone from 输出设置 and the new-project wizard (nothing about channels is shown there any more; new projects start with no channel selected). 🎞️ 成片发布 gains a 📡 发布渠道 section listing every channel — YouTube, Bilibili, TikTok, 抖音 and 小红书 — as a card: tick it to put it on the publish-stage platform list (output.platforms, the same key as before, so aspect-ratio / thumbnail / subtitle / platform-adapter keep reading it), and press 发布到 X to dispatch a work order for that episode and that channel to 12-publishing/publisher (POST /projects/{project}/publish/{ep}/dispatch; POST /projects/{project}/publish/channels saves the ticks). The work order names the channel's skill (skill-youtube/bilibili/tiktok/douyin/xhs-cdp-draft), says whether the publish/<platform>/package/<ep>/ bundle exists (otherwise the master edit/<ep>/final*.mp4 + thumbnail are uploaded as-is, noted in the receipt), and keeps the semi-automatic red line: the draft is filled in the local Chrome, the user presses the platform's publish button themselves. Publishing to an unticked channel ticks it. Each card shows whether the bundle is ready and the latest receipt (publish/receipts/<ep>_<platform>_receipt.json: 待人工提交 / 已由人工提交 / 成功 / 失败); the button is disabled until the episode has a final cut and while a run is in flight.
  • Real-face moderation workaround removed (2026-10-07). The conditional "colored-pencil face" salvage rule (character-concept / video-generation SOULs, WORKFLOW §9 notes) and the "real-face references trigger moderation" rationale in the prop SOUL are gone. Photorealistic faces are generated as-is; an Ark PrivacyInformation rejection means the reference image is not in the 虚拟人像资产库 (asset:// submissions pass), so the fix is to enrol the image, never to repaint it.
  • Output settings › Publishing platforms: selection is now optional, Xiaohongshu is hidden, and the per-platform aspect-ratio label is gone (2026-10-06). Both the new-project wizard and 输出设置 list YouTube / Bilibili / TikTok / 抖音 only, without the "16:9 / 9:16" suffix after each name. Leaving every platform unchecked is allowed: the project then does no per-platform publishing — aspect_ratio.json carries the master entry only, thumbnail / subtitle are produced once for the master, platform-adapter and publisher are not dispatched, and seo / metadata produce a single platform-neutral copy. Xiaohongshu is no longer part of the default list; a value already stored in an existing project is still honoured until that project's output settings are saved again.
  • Shot plates: the scene nine-grid sheet is now generated at the selected image model's maximum resolution (2026-10-06). The 3×3 grid used to be capped at 4.6 MP regardless of model (the Seedream 5.0 Pro limit). The cap now follows the model chosen for scenes (Scene preview › 🎨 image model, or the global one): Seedream 5.0 Pro stays at 4,624,220 px (≈2832×1632 for 16:9), while Seedream 5.0 / 5.0 Lite / 4.5 / 4.0 on Volcengine and BytePlus go up to 16,777,216 px (≈5386×3106, each tile ≈1734×974 instead of ≈910×510); Fal-hosted Seedream and Nano Banana use their 4K tier. Channels whose limit is unknown keep the previous size. The layout template, gutters and the per-tile crop boxes are derived from the same geometry, so splitting follows the new sheet size. Existing grids are reused as before; regenerate one with --no-grid-fallback --force.
  • Video prompts no longer quote dialogue words outside the {} line, and a line is never split across several {} (2026-10-05). With performance direction on, the prompt agent was required to repeat the trigger word outside the braces ("说到『X』时…") and allowed to cut one line into several {} with acting notes in between; once per-line delivery notes existed in the shot list, it also copied their quoted stress words into the prompt. Seedance 2.5 spoke a quoted word as dialogue (fengshen3 ep08 sh006: "…死后愚弄百姓!" came out with "愚弄百姓" said twice). Now one shot-list line is exactly one {}; the trigger moment, stress and pauses are pointed at by position ("the second half of the line", "the last four characters") and that phrase is recorded as performance[].trigger_anchor; delivery notes (dialogue_lines[].delivery) are for speech synthesis only and are not copied into video prompts. performance_bound check ② is inverted accordingly: it verifies the anchor phrase, and reports quoted line words (line_words_quoted) and split lines (line_split). Prompts written before this change only get warnings — nothing is rewritten or regenerated automatically; --fresh makes the same findings fail for groups written in the current task.
  • Props get one image each; the size-comparison image is gone and prop size is carried by text (2026-10-05). The prop stage (p6-prop-concept) used to produce two images per plot prop: the prop image main_01.png and a scale anchor scale_ref_01.png showing the prop next to everyday objects, and video prompts attached the scale anchor by default. The scale anchor is no longer produced: the objects it placed beside the prop (table tops, bowls, boards) tended to leak into the generated video, and it cost a second generation per prop. Each plot prop now gets only main_01.png, on a plain neutral backdrop with nothing else in frame, and that image is what group prompts and anchor packs attach by default (one per prop). Size is expressed only by the prop card's scale.prompt_token, copied verbatim after the <prop>@Image N: binding sentence; the phrase must carry a comparable reference (a body part of the holder or a common object, plus how it is held), and the binding sentence now also says that only the prop's own shape, material and colour are taken from the image. The concept-coverage audit (§6A) and prop_scale_defined count main_01.png only. code/refs_referenced_check.py (prop_ref_single) now warns when a group attaches a scale anchor or more than one image of the same prop. Existing projects keep their scale_ref_01.png files; already-written group prompts that attach them are not rewritten and only get a warning. Restart the service for the prompt-stage system text to pick this up.
  • Scene top-down plans are drawn in neutral lighting, not in a lighting scheme (2026-10-04). layout_top.png is only a spatial reference — the whitebox is measured from it, panoramas and nine-grid plates use it to tell what lies in each direction, and the host reads its colours to tell water from land; nothing downstream uses it as a lighting reference (per-scheme lighting reaches the plates as text from lighting.json). The environment-concept spec still told the agent to follow lighting.json, so plans came out moonlit or dusk-toned (liaozhai3 SCN-0088), and dark plans were misread as water (fengshen3 SCN-0141). The spec now requires flat, even, neutral lighting with no time of day, no cast shadows and no lit lamps, and the checklist field becomes lighting_neutral. The front scene plate main_01.png (whitebox off) still follows its registered master lighting scheme. Existing plans are not regenerated.
  • Nine-grid shot plates are now generated from one centre standpoint, and the plate mode splits into automatic and manual fill-in (2026-10-04). The 3×3 grid used to be nine semantic camera positions taken from layout.json#views; in a test on alices ep01 SCN-long-hall only 22 of 67 plate demands found a suitable tile. The grid is now, by default, one standpoint that only turns: the standpoint is the horizontal median of this episode's whitebox cameras in the scene, the camera height is their median height clamped to 0.9–1.6 m, tiles 1–8 look level every 45°, and tile 9 tilts up toward the direction most up-tilted shots face. The reference is the top-down plan with the standpoint and eight arrows marked. On the same scene this raised suitable tiles to 51 of 67. Existing libraries keep their tiles; --grid-layout views restores the old method. 输出设置「背景图模式」now offers 九宫格自动补图 (grid, default: a shot with no suitable tile gets its own plate rendered, as before) and 九宫格手动补图 (grid_manual: nothing is rendered; the shot keeps the nearest tile as a placeholder until you capture a plate with 「💾 背景图」 in the panorama or world-model viewer, and render_shot_plates.py exits 5 with [manual_plate_needed]; --status reports those shots as manual_needed). The scene preview page lists the shots waiting for a manual capture with their bearing, tilt and height, and its per-scene selector now shows only 跟随全局 / 九宫格自动补图 / 九宫格手动补图; 全景图 and 世界模型 are hidden there as they already were in the output settings (stored values keep working). Known limits, left as they are for now: tiles that face a nearby flat wall are often drawn as an enclosed bay or a receding corridor, and a tile one height class away from a ground-level shot still counts as suitable.
  • Scene panoramas: land anchors no longer get water under the camera. In a top-down plan, shaded woodland and rock are often the same dark teal as water, and the ground beyond the plan's edge was drawn by stretching the edge pixels to the horizon, so an anchor near a map corner read as surrounded by water; the prompt's ground rule also only described the "standing in water" case (fengshen3 SCN-0036 A5). The host now classifies water in the plan by colour and texture (water_mask): beyond the plan's edge, land sides are drawn in a warm earth tone while water sides still extend the edge pixels, and when the ground under the anchor is classified as land the prompt states that the nadir and near foreground are dry land. The heuristic mistakes blue-grey paving and shaded rock for water, so it only ever asserts land; anchors classified as water or shoreline keep the previous wording. A scene can override the classification with ground_water.png (white = water) next to its plan. Exterior blockout panoramas are re-rendered locally before the next generation (guides version 3); existing panoramas stay valid (2026-10-04).
  • Scene panoramas are always generated independently; chained hole-filling is removed. A new panorama used to take the nearest finished panorama of the same lighting scheme, re-project it to the new anchor and hand it to the image model as an authoritative reference. Outside the mapped floor there is no geometry, so most of that reference was the parent copied by direction, and a parent drawn from the wrong viewpoint was reproduced wholesale (fengshen3 SCN-0036: anchor A5, placed high inside the town, came out as its parent A1's view from the water outside the wall). generate_pano now uses only the anchor's own blockout panorama, the top-down plan and the text; relighting an anchor that already has another scheme is unchanged. Removed with it: the chain-parent guards, the parent.consistency score and render_scene_panos.py --trust. Existing panoramas recorded as mode: chain stay valid and become independent when redone (2026-10-04).
  • The regression suite is versioned again. tests/ had been local-only since v1.0.4, with a hand-maintained allowlist in .gitignore that let 13 files through; the other 57 files (about 8,300 lines) ran only on the maintainer's machine, so CI covered a fraction of the suite. The ignore block is removed and the whole suite is tracked. The tests are self-contained (temporary project directories, no private project data, no network); cases that need ffmpeg / ffprobe skip when the binaries are missing, and CI now installs ffmpeg so they run there. The three Node checks under tests/*.mjs (whitebox renderer, whitebox i18n, director world) were never run by CI; they now run in the web job through the new npm run test:web.
  • New regression tests for the HTTP layer and for intro-offset finalization. tests/test_api_routes.py checks that no route is shadowed by an earlier one with the same method (the /projects/import vs /projects/{project} class of bug), runs every GET route whose parameters can be filled against an empty data directory in an isolated subprocess (temporary DATA_DIR / HOME, narrowed PATH, network disabled) and requires no unhandled 500, a detail on every error response and no side effects for a non-existent project, and checks that every Web console page and preview page is served. tests/test_finalize_intro_offset.py covers code/finalize_episode.py (§9B): SRT / ASS shifting, segment offsets, per-cue comparison, and an end-to-end assemble → shift → check on synthetic clips, including the failure cases intro_offset_ok exists for (subtitles copied without shifting, audio track still starting at 0 s, intro missing from the final file).
  • Regression tests for the fancy-text and generation-group machine checks. tests/test_generation_groups_check.py covers rules 1–5 of code/check_generation_groups.py (every shot covered exactly once, contiguous shots within one scene instance, integer duration within the project's group cap and equal to the sum of its shots, at most four characters, the continuity_from chain) plus the greedy --propose draft and the CLI exit codes. tests/test_captions_validate.py covers validate_captions, the message-to-check bucketing, the template protocol, project font resolution and glyph coverage, anchor placement and the render-receipt fingerprint. tests/test_check_captions_cli.py runs code/check_captions.py end to end over a minimal project for all three stages (design / render / final) and asserts, check by check, which problem fails which check and when a receipt goes stale; burned copies and receipts are forged from the engine's own fingerprint functions, so no browser is launched.
  • Post-production dubbing: vocal removal, shot-window onset alignment, and dub-aware mixing. The p7-dub helper code/dub_group.py (Output Settings → Dialogue voice = Post-production dubbing) used to keep the video model's own voice under the TTS (only ducked by −26 dB inside the detected speech spans, full level anywhere the detector missed), located speech with silencedetect on the mixed native track (sound effects and ambience counted as speech, lines could land in the wrong shot), and treated "does not fit the mouth span" as a hard overflow. Now the native track is first split by the local MDX-Net separation model already used by the Post page (modules/audio_separation.py): the vocal-free bed becomes the mix base (--keep-native-voice-db, default −60 = removed; --no-separation / --resplit), and the onset of every line is detected on the voice stem with the pitch-based voiced-segment detector shared with dialogue_audible (modules/voice_activity.py), constrained to the line's own shot window from the clip's boundary_map (--pad), with per-line manual override unchanged (--detect-only now prints shot_windows, raw_voiced and notes; --segments). Fitting is onset-first: each line starts at its detected onset, speed ±25 % + tempo ±10 % still try to match the mouth span, but a line only counts as overflow when it would collide with the next line's onset or the end of the clip (segment.room_s; a longer/shorter line is just loose_fit). When the separation model is unavailable the old ducking path is used and vocal_removal_ok reports it. --dry-run now writes dub_manifest.dryrun.json instead of overwriting the real manifest. On the mixing side code/mix_basis.py sources adds a per-group dub_fp (dub timestamp + per-line spans + vocal-removal status) that is stamped into the mix manifest — re-dubbing after the mix makes mix_basis_current FAIL, and an adopted Post version whose base was built before the dub (so its file has no dub) is reported as dub_predates_version (also listed by dub_group.py as post_versions_predate_dub). The Post page's Native lane plays the vocal-free bed (grpNNN.bed.wav) for dubbed groups while the <video> element is muted, so the native and dub lanes no longer play the TTS twice; the Remove vocals / Remove ambience buttons are hidden on those entries. WORKFLOW §8C, workflow.yaml p7-dub (new auto check vocal_removal_ok), voice-generation / audio-mixing SOULs and 11 language dictionaries updated. Unit tests in tests/test_dub_group.py; verified offline on fengshen3 ep07 grp011 with synthetic TTS (vocal spans drop ~20 dB in the bed, picture stream and duration unchanged, idempotent re-run).
  • TTS settings no longer show a default voice, a voice list or previews. Because voice selection is automatic, the 「默认音色」 field and the 「拉取音色库 / 搜索 / 试听 / 设为默认 / 加入」 controls are gone from the Volcengine, ElevenLabs, MiniMax and OpenRouter tabs (previously saved default voices are kept in the configuration as a fallback but are not shown). The Volcengine voice library needs the account's Access Key / Secret Key; these are now entered on the TTS › 火山引擎 tab in 音色库 mode (with a link to the console's 「API 访问密钥」 page next to the API Key hint) instead of being borrowed from 文件托管 › 火山引擎 TOS — existing installs have the pair copied over. 「声音复刻 2.0」 is removed from the Volcengine model list (cast entries that already use an S_ cloned voice still synthesize). ElevenLabs defaults to Eleven v3 for new configurations.
  • 音色设计 mode no longer falls back to the local voice library. On Agentics and ComfyUI, synthesizing a character's dialogue before that character has a voice sample used to pick a TimbreModel clip silently, mixing the two modes; it now fails with 「先出样本」, and the dialogue voice library marks such lines as unbound with the reason. Narration without a frozen narrator sample is spoken from the voice description by the Voice Design model. The dialogue voice library and dub_group.py decide per mode (one shared rule instead of provider-name checks), which also fixes Agentics lines being skipped as 「未选角」 when the character had a sample but no casting.json entry. In local-library mode a casting.json voice (a catalog file name) is now honoured instead of being re-picked automatically.

Fixed

  • GET /api/v1/projects/{project}/prompt-skill no longer creates the project. The handler refreshed the effective-skill snapshot on every read, which wrote settings.json and the refs/ skeleton even when the project did not exist, so a mistyped or already deleted project name came back as an empty project in the project list. For a non-existent project it now returns the defaults without writing anything.
  • code/check_generation_groups.py crashed with IndexError on a group whose shots list was empty or missing, so none of the other groups' problems were reported either; it now reports group_shots_contiguous: 组内没有镜头 for that group and keeps checking.
  • code/check_captions.py: when the fonts or sound-effect manifest was missing, caption_assets_resolved was printed twice, first FAIL and then PASS (the exit code was already 1); only the FAIL line is printed now. A caption id that happened to contain a keyword of another check (for example group_id) no longer moves its messages into that check's bucket. validate_captions reported the unsupported cards field twice.
  • Multi-form characters now speak with the right age form. Dialogue TTS (the dialogue voice library used by the animatic / whitebox reel, and dub_group.py) decided a speaker's voice form only from the sample file name in the group prompt's audio_refs, using a pattern that did not accept underscores — a form named pre_lotus_child never matched, the speaker silently fell back to default, and the lines were synthesized from the other age's description with no reference sample attached, so the voice drifted from line to line (fengshen3 ep06/ep07, 哪吒). Form resolution now lives in one place, modules/voice_variants.py: group prompt sample name → the voice card's age_variants[].chapter_range against the episode's chapters in story/episode_plan.json (works before any prompt exists) → the form used by the episode's other groups → the only registered form → default. If the form cannot be decided, or the decided form has neither a sample nor a casting entry and there is no base sample to fall back to, the line is skipped with a warning instead of being synthesized with the wrong voice. Each library line records variant_source; the manifest's checks list speaker_variants, variant_warnings (prompt and chapter range disagree) and unanchored_speakers; code/dialogue_tts.py --variants prints the per-group decision without synthesizing; genmedia.py tts warns when a character has form samples but the requested form matches none. Existing lines of affected speakers are re-synthesized on the next sync; other speakers are untouched.