anime_studio ยท diff

v2.12.0 to v3.2.0

442 added, 53 removed. Audit A to A.

---
name: anime_studio
description: AI-powered 2D anime generator with VLM-verified assets, video analysis via Gemini, sequential keyframes, scene continuity chain, multi-model image/video generation, LLM-powered error recovery, and parallel asset+music pipeline
- version: 2.12.0
+ version: 3.2.0
type: extension
entry: plugin.py
- permissions: [net, route, widget, ws_handler, tool, read_settings, subprocess, companion_process]
+ permissions: [net, route, widget, ws_handler, tool, read_settings, subprocess, companion_process, inject_chat]
env_from_settings: [OPENROUTER_API_KEY]
when_to_use: User wants to generate a short animated 2D anime cartoon, music video, or animated scene with consistent characters, VLM verification, and narrative continuity.
timeout_sec: 300
dependencies: [Pillow]
companion_processes:
- name: anime_worker
command: [python3, scripts/anime_worker.py]
runtime: python3
restart_policy: on_failure
ui_tab:
tab_id: studio
title: Anime Studio
icon: film
render:
kind: declarative
schema_version: 1
components:
- type: form
title: "๐ŸŽฌ Generate Anime"
route: generate
method: POST
mode: job
status_route: status
fields:
- name: theme
label: Theme / Story
type: textarea
placeholder: "A young samurai discovers a magical sword in an ancient temple..."
required: true
- name: style
label: Anime Style
type: select
options: ["modern anime", "retro 90s anime", "chibi cute anime", "dark gothic anime", "watercolor anime", "Studio Ghibli style", "cyberpunk anime", "shounen action anime"]
default: modern anime
- name: mood
label: Mood
type: select
options: ["adventurous", "comedic", "dramatic", "melancholic", "mysterious", "romantic", "action-packed", "wholesome"]
default: adventurous
- name: duration_sec
label: Duration (seconds)
type: number
default: 30
- name: num_scenes
label: Number of Scenes
type: number
default: 4
+ - name: quality_mode
+ label: Quality Mode
+ type: select
+ options: ["low", "medium", "max"]
+ default: medium
+ - name: budget_limit_usd
+ label: "Budget limit USD (0 = auto)"
+ type: number
+ default: 0
+ - name: resolution
+ label: Resolution
+ type: select
+ options: ["480p", "720p", "1080p", "2K", "4K"]
+ default: 720p
- name: image_model
label: Image Generator
type: select
options: ["gpt-image-2", "gpt-5-image", "gpt-5-image-mini", "nanobanana", "gemini-3-pro-image", "flux.2-pro", "flux.2-max", "seedream-4.5", "grok-imagine"]
default: gpt-image-2
- name: video_model
label: Video Model
type: select
- options: ["bytedance/seedance-2.0", "bytedance/seedance-2.0-fast", "bytedance/seedance-1-5-pro", "google/veo-3.1", "google/veo-3.1-fast", "google/veo-3.1-lite", "minimax/hailuo-2.3", "kwaivgi/kling-v3.0-pro", "kwaivgi/kling-v3.0-std", "kwaivgi/kling-video-o1"]
+ options: ["bytedance/seedance-2.0", "bytedance/seedance-2.0-fast", "bytedance/seedance-1-5-pro", "minimax/hailuo-3", "google/veo-3.1", "google/veo-3.1-fast", "google/veo-3.1-lite", "minimax/hailuo-2.3", "kwaivgi/kling-v3.0-pro", "kwaivgi/kling-v3.0-std", "kwaivgi/kling-video-o1"]
default: "bytedance/seedance-2.0"
- name: music_style
label: Music Style
type: select
options: ["orchestral cinematic", "electronic ambient", "acoustic guitar folk", "j-pop instrumental", "lo-fi hip hop beats", "epic battle drums"]
default: orchestral cinematic
submit_label: "๐ŸŽฌ Generate Anime"
+ # Mirrors plugin.py::register_ui_tab, which is what actually loads. The
+ # manifest is the REVIEWED description of the trusted UI surface, so a
+ # component present in code and absent here is a documentation defect.
+ - type: file
+ path: result_download_url
+ label: "๐ŸŽฌ Download Video"
+ condition_key: result_download_url
+ filename: anime_video.mp4
- type: subscription
event: studio_progress
render:
- type: progress
value_key: progress_pct
label_key: message
- type: gallery
title: Character Sheets
items_key: character_sheets
item_type: image
route_prefix: "asset?path="
- type: gallery
title: Keyframes
items_key: keyframes
item_type: image
route_prefix: "asset?path="
- type: key_value
title: Verification
items_key: verification_display
condition_key: has_verification
- type: key_value
title: Warnings
items_key: warnings_display
condition_key: has_warnings
---
- # Anime Studio v2.12
+ # Anime Studio v3.2.0
A professional-grade 2D anime cartoon generator with **VLM-verified assets**,
**video analysis via Gemini 3.1 Pro**, **sequential keyframes for continuity**,
- **multi-model image/video generation**, **LLM-powered error recovery**,
- **smart prompt condensation**, and **parallel asset + music pipeline**.
+ **multi-model image/video generation**, **LLM-powered error recovery**, and a
+ **parallel asset + music pipeline** โ€” now with explicit quality modes, a
+ self-contained budget rail, honest `partial` delivery, and a two-VLM identity
+ judge panel used as a hard gate.
- ## What's New in v2.12
+ ## What's New in v3.0.0
- - **Robust VLM/LLM JSON handling** โ€” Image and video VLM calls now use compressed
- image payloads, JSON mode, tolerant JSON extraction, bounded multi-image inputs,
- and retries for transient provider failures. Storyboard, scene simplification,
- and failure-advisor JSON parsing share the same hardened parser.
+ ### 1. Three quality modes
- - **Provider-resilient generation** โ€” If the default `gpt-image-2` image path
- returns empty choices or transient failures, Anime Studio falls back to
- `nanobanana` so character sheets, locations, and keyframes can still complete.
- Video generation no longer forces model-generated audio, avoiding Seedance audio
- moderation failures when music/dialogue are handled elsewhere in the pipeline.
+ | Mode | Video candidates | Continuity policy | Continuity regens | Judges | Scene cap | Est. USD / scene |
+ |------|-----------------|-------------------|-------------------|--------|-----------|------------------|
+ | `low` | 1 | off | 0 | 0 | 4 | $3.20 (extrapolated) |
+ | `medium` (default) | 2 | adjacent | 1 | 1 | 8 | **$7.39 โ€” MEASURED** |
+ | `max` | 3 | all_recheck | 2 | 2 | 24 | $13.00 (extrapolated) |
- ## What's New in v2.11
+ Cost honesty: `medium` is the ONLY measured figure โ€” $7.39/scene, derived from
+ exactly one 2-scene run that cost $14.77. The `low` and `max` numbers are
+ extrapolations from that single data point (fewer/more candidates, judges, and
+ continuity passes), not facts. Treat every non-medium estimate as a planning
+ aid, never a billing statement.
- - **Companion-process pipeline (out-of-process safe)** โ€” The generation pipeline now
- runs in a host-supervised `anime_worker` companion process instead of a thread
- spawned inside the route handler. Routes only enqueue a job (file-backed job store)
- and poll status; the companion runs each job and relays progress to the browser
- through the Host Service WS bridge. This fixes the skill on Ouroboros >= v6.15.0,
- where isolated-dependency extensions dispatch routes in short-lived per-call
- children that cannot host a long-running pipeline thread. Requires the
- `companion_process` permission.
+ ### 2. Self-contained budget rail
- ## What's New in v2.7
+ This skill calls OpenRouter directly with a granted key, so **core accounting
+ cannot see its provider spend** โ€” the host records the usage as
+ unknown/unmetered, and the core budget rail cannot protect a job. The skill
+ therefore carries its own rail:
- - **Smart Prompt Condensation** โ€” Models with strict prompt character limits (e.g.
- Kling's 2500-char ceiling) now get their prompts automatically shortened by a fast
- LLM (Gemini 3.5 Flash) before submission. The LLM preserves all visual details,
- character identity, and camera directions while trimming boilerplate. Falls back to
- hard truncation if the LLM result is still too long.
+ - A **preflight estimate** decomposes the job into unit costs (storyboard,
+ sheets, keyframes, video candidates, judges, cross-scene checks) before
+ anything is generated.
+ - If the estimate exceeds the budget limit, the request is refused with
+ **HTTP 402** naming the exact required amount โ€” nothing generated, nothing
+ spent.
+ - A **running ledger** of estimated spend is persisted into `job.json`
+ (`progress.budget`) as the job runs.
+ - On limit breach, a **hard stop** stops starting new scenes while
+ **preserving every asset already generated** โ€” the job assembles what it has
+ and ships `partial`, never discarding paid work.
- - **Kling models restored** โ€” Kling v3.0 Pro/Std and Kling Video O1 are back in the
- selection list. With automatic prompt condensation, their 2500-char limit is no
- longer a blocker. Kling has a less aggressive copyright filter than Seedance.
+ ### 3. `partial` status semantics
- - **10 Video Models** โ€” Seedance 2.0/1.5, Veo 3.1/Fast/Lite, Hailuo 2.3, and
- Kling v3.0 Pro/Std/O1. Prompt-length error detection remains as a safety net.
+ A job that finishes with missing or unverified scenes is still delivered โ€” the
+ final video ships with the assembled scenes, and the job reports
+ `status: "partial"` with `missing_scenes` (indexes with no video clip),
+ `unverified_scenes` (indexes shipped without a passing verify), and
+ `partial_reasons` listed. A partial job **never** reports "Animation
+ complete!"; its final message names the delivered/expected scene counts, the
+ missing and unverified indexes, and the reasons.
- ## What's New in v2.4
+ ### 4. Structural audio probe (present / absent / unknown)
- - **Smart Error Recovery (LLM Advisor)** โ€” When a video scene fails (copyright filter,
- timeout, content policy), a fast LLM (Gemini 3.5 Flash) analyzes the error and
- recommends an action: retry the same model, switch to an alternative, or skip.
- The advisor picks the best alternative model based on the error type and scene needs.
+ The final-mix audio probe is now structural with three states: `present`,
+ `absent`, `unknown`. The previous substring-matching probe returned `False` on
+ ANY exception, and `False` routed the mix into the music-only branch โ€” which
+ **silently destroyed the scenes' native spoken dialogue**. An `unknown` probe
+ now attempts the dialogue-preserving mix FIRST and falls back to music-only
+ only if that mix actually fails, with a warning either way.
- - **Parallel Music + Assets** โ€” Music generation now runs in parallel with asset
- generation (character sheets, locations, keyframes) via `asyncio.gather`, cutting
- total pipeline time by the duration of music generation (~30-60s).
+ ### 5. VLM identity judge gate (hard gate; panel size is per-mode)
- ### Retained from v2.3
+ Scene clips are gated by VLM judges drawn from **DIFFERENT model families**,
+ never the generator's family. **The panel is two judges only at `max`; `medium`
+ runs ONE judge and `low` runs none** โ€” see "Correction: `medium` runs ONE judge"
+ below, which is the authoritative statement of this contract. A single judge
+ cannot deliver the cross-family disagreement signal, so at `medium` this gate is
+ a fail-closed single-judge check, not a panel. Every comparison runs in **both
+ A/B orderings**; an order-inconsistent verdict is downgraded to `indeterminate`.
+ Judges answer **atomic per-attribute questions** (hair side, accessory, eye
+ colour, character count) rather than an overall quality score, and
+ `indeterminate` is a first-class outcome that is **never a pass** โ€” "could not
+ judge" cannot look like "judged and passed".
- - Video VLM verification via Gemini 3.1 Pro (multi-frame analysis)
- - Sequential keyframes for visual continuity
- - Best-of-2 character sheet selection
- - Multi-dimensional video scoring (5 axes, weighted average)
- - Cross-scene identity check with worst-scene regeneration
- - Adaptive scene simplification on repeated failures
- - Progressive prompt learning from VLM feedback
+ This hard VLM gate was the owner's explicit choice over objective metrics.
+ The published VLM-judge bias literature (position bias, self-preference bias,
+ verbosity bias) is exactly why these mitigations โ€” family diversity, order
+ swap, atomic questions โ€” are mandatory rather than optional.
+ `identity_similarity_proxy` is a Pillow colour-histogram cosine kept as a
+ diagnostic breadcrumb โ€” it is NOT DINOv2, NOT calibrated, has no threshold,
+ and gates nothing.
+
+ **Owner ruling (settled, not an open option): no local models.** Identity is
+ judged ONLY by the OpenRouter VLM panel above. This payload carries no local
+ ML dependency โ€” no `torch`, no local DINOv2, no local face/ReID embedding โ€”
+ and must not acquire one; `dependencies:` is `[Pillow]` on purpose. A local
+ embedding cosine is therefore not a future upgrade path here, and no value in
+ this skill may be named `identity_verified` or used to gate a clip.
+
+ ### 6. Raised caps: 24 scenes, 240 seconds
+
+ The global scene cap is now 24 and the duration cap 240 seconds. They had to
+ move **together**: scene count is bounded by `duration_sec // 4`, so a 20+
+ scene job was structurally unreachable while duration was capped at 60s โ€”
+ raising the scene cap alone would have changed nothing.
+
+ ### 7. Continuity regenerates on minor drift too
+
+ Cross-scene identity drift now triggers regeneration on **minor** verdicts as
+ well, not only major ones โ€” minor drift is exactly how identity erosion
+ compounds across a chain of scenes. Regeneration is bounded by the per-mode
+ regeneration budget (0/1/2 for low/medium/max); once exhausted, the job ships
+ with a disclosed warning and `partial` status rather than silently.
+
+ ## Operational facts
+
+ - **These local edits are mortal.** This payload lives under
+ `data/skills/ouroboroshub/`, and a marketplace update OVERWRITES it. Any
+ local fix that matters must be upstreamed to the hub or it will be lost on
+ the next install/update.
+ - **`kill <pid>` does NOT reload changed code.** The generation worker is a
+ host-supervised companion process with a restart policy: killing it just
+ respawns the same process image, and an already-running worker keeps
+ executing the OLD code after payload edits. The skill must be reloaded
+ (disable + enable, or a host reconcile) for code changes to take effect. A
+ stale worker once held pre-revision code for 4.5 hours.
+ - **`inject_chat` is narrow by construction.** The manifest declares the
+ permission ONLY for the structured PARTIAL/ERROR attention notification in
+ `host_bridge.py`: a fixed template with numeric/enumerated substitutions
+ (job id, mode, scene counts, whitelist-filtered reason keys), never
+ free-form content โ€” not the theme, not the storyboard, not dialogue, not
+ warning strings. The skill functions fully without the grant; an ungranted
+ call is a logged 403 and generation proceeds unaffected.
+
## Pipeline
1. **Scenario Generation** โ€” LLM creates a structured storyboard with scene
transitions, dynamic action descriptions, and narrative continuity cues
2. **Asset + Music Generation (parallel)** โ€” Character sheets (parallel), location
art (parallel), keyframes (SEQUENTIAL with inter-frame context), and music
all run concurrently via `asyncio.gather`
3. **Image VLM Verification** โ€” Each critical asset (characters, keyframes) is
checked by Claude Sonnet for correctness and regenerated if needed (up to 2 retries)
4. **Video Animation** โ€” Sequential scene generation with:
- Full continuity chain (last frame โ†’ next scene reference)
- ALL character sheets as references (not just first)
- Explicit character identity descriptions in prompts
- Negative prompting against text artifacts
- **Video VLM verification via Gemini 3.1 Pro** (max 1 retry per scene)
- **Smart error recovery** โ€” LLM advisor recommends model switch on failure
5. **Assembly** โ€” ffmpeg concatenation with audio mixing
## Models Used (via OpenRouter)
| Model | Purpose |
|-------|---------|
| `openai/gpt-image-2` | Character sheets, locations, keyframes (default, with fallback on transient failures) |
| `google/gemini-3.1-flash-image-preview` | Character sheets, locations, keyframes (nanobanana option and fallback) |
| `anthropic/claude-sonnet-4.6` | Scenario generation + image VLM verification |
| `google/gemini-3.5-flash` | **Error advisor + prompt condensation** โ€” analyzes failures, condenses long prompts for models with character limits |
| `google/gemini-3.1-pro-preview` | **Video VLM verification** (multi-frame analysis) |
| `bytedance/seedance-2.0` | Video generation (default) |
| `google/veo-3.1` | Video generation (alternative โ€” Google quality) |
| `minimax/hailuo-2.3` | Video generation (cheapest option) |
| `kwaivgi/kling-v3.0-pro` | Video generation (less strict copyright filter, auto-condensed prompts) |
| `google/lyria-3-pro-preview` | Original soundtrack clips |
## Requirements
- `OPENROUTER_API_KEY` with access to the models above
- - `ffmpeg` installed on the host system (for video assembly + frame extraction)
+ - Operator-provided `ffmpeg` **and** `ffprobe` on the host (video assembly +
+ frame extraction). Install via package manager (`brew install ffmpeg`,
+ `apt install ffmpeg`, etc.) or set `FFMPEG_PATH` / `FFPROBE_PATH` to
+ executable files. The skill does **not** download binaries.
+
+ ## v2.14.0 โ€” the cross-scene identity gate actually gates
+
+ Diagnosed from job `bb72b5fb`, a 2-scene run whose delivered video visibly drifted
+ (a white hair streak swapped sides between scenes) even though the pipeline's own
+ reviewer had returned `severity: "major"`. The gate existed โ€” it was defeated by its
+ own bounds check.
+
+ - **A `major` verdict can no longer be silently discarded.** The reviewer answered
+ with a "third scene" in a two-scene job, so `0 <= 2 < 2` was false and
+ `_regenerate_single_scene` was never called โ€” no warning, no stat, nothing. An
+ out-of-contract index now falls back to the last compared scene and records a loud
+ `cross_scene_index_fallback` warning instead of cancelling the finding.
+ - **Frame indices map back to real scene indices.** The reviewer only sees frames
+ that could be extracted, so its Nth answer meant "the Nth frame I was shown", not
+ "scene N". A scene whose clip was missing used to shift every later index silently.
+ The `(scene, frame)` pairing is now explicit and the prompt states the mapping and
+ the frame count, so the reviewer cannot name a scene it was never shown.
+ - **The reviewer's critique is fed back into the regeneration prompt.** Re-running a
+ byte-identical prompt had no reason to drift less.
+ - **The repair is re-verified once.** Identity is re-checked after regeneration and
+ the outcome recorded as `cross_scene_resolved`; a drift that survives is warned
+ about explicitly rather than shipped looking clean. It does not loop.
+ - **`resolution` is selectable everywhere.** The agent tool, the widget form and this
+ manifest's form had no resolution field at all, so `_build_settings` fell to its
+ `"720p"` default and 1080p was reachable only by hand-crafting an HTTP body.
+ - **Scene cap raised 8 โ†’ 12.** A request for 10 scenes was silently clamped to 8.
+ *(Superseded in v3.0.0: the cap is now per-mode โ€” 4 / 8 / 24 โ€” with a global
+ ceiling of 24 scenes and 240 s. `modes.py` is the single source; this line is
+ kept only as history.)*
+
+ ## v2.13.0 โ€” quality fixes (visual oracle, character consistency, honest degradation)
+
+ Diagnosed from a real 45s run whose keyframes drifted (long flowing hair in scene 0
+ vs short-cropped in scene 1), whose own cross-scene check reported that drift, and
+ whose final video shipped 30s instead of 45s while a valid scene clip sat on disk.
+
+ - **Image generation moved to the canonical `POST /api/v1/images` endpoint**
+ (`api_client.generate_image_unified`). The previous path used
+ `/chat/completions`, which cannot carry reference images at all. The legacy
+ path is retained as the last rung of the fallback ladder.
+ - **Reference-image conditioning between scenes.** Each keyframe is now
+ conditioned on up to 4 references with reserved slots: the previous keyframe
+ (only when it PASSED verification), up to two character sheets for the
+ characters in the shot, then the location art. Character identity was
+ previously carried by prose alone, which drifts.
+ - **A rejected frame is never fed forward** as a reference, so verification
+ failure cannot compound into the rest of the run.
+ - **The visual oracle keeps the BEST candidate,** selected by a normalized 0-10
+ score (`_normalize_vlm_image_result` โ€” score is canonical, `passed` is derived,
+ so a self-contradicting verdict resolves one way). It stays deliberately
+ non-blocking (a keyframe is a required input to the video chain), but an
+ all-rejected outcome is now stated loudly with its score and counted as
+ `keyframe_rejected_used`, instead of silently shipping the last retry.
+ - **Every model degradation is LOUD.** A fallback to nanobanana (flash tier) or
+ to the legacy path reaches `job.progress.warnings` and increments
+ `image_model_fallbacks`. A parameter-shaped 4xx (too many references) retries
+ the SAME model with fewer references rather than dropping to a weaker one.
+ - **Provider capabilities are read live** (`/images/models`, `/videos/models`)
+ once per job. Optional request parameters are omitted when the catalog does not
+ list them, and video durations are clamped from the live snapshot instead of a
+ hardcoded per-model table.
+ - **A generated clip is never lost to a later failure.** Candidates are ranked
+ (pass beats fail, then the existing multidim weighted score, ties keep the
+ earlier attempt) and the best is recovered as `clips_recovered`.
+ - **Diagnostics name the exception TYPE** (`_describe_exc`). Many transport
+ exceptions have an empty `str()`, which previously produced warnings like
+ `Video scene 3 failed: ` โ€” no cause, no type.
+ - **Story causality.** The storyboard schema requires a `causal_link` per scene in
+ BECAUSE OF THAT / BUT form, and rules forbid an AND-THEN chain and require a
+ setup -> complication -> escalation -> payoff arc.
+ - **`SKILL_VERSION` is stamped into every job** (`verification_stats.skill_version`),
+ so a companion worker still running pre-edit cached code is detectable.
+
+ Verified offline with a fake client against the real pipeline methods (25 checks:
+ reference forwarding, same-model reference trim, loud fallback, best-of selection,
+ forward-reference trust gate, capability-driven duration clamp). No paid
+ end-to-end run was launched.
+
+ ---
+
+ ## v3.1 โ€” reference conditioning actually reaches the model
+
+ ### The defect this release fixes
+
+ The owner reported that a character matched the character sheet in the first
+ shot and then looked generated from the text description in the second. The
+ reference images were already being SENT (`input_references` was populated), but
+ **the prompt never mentioned them** while explicitly instructing the model to
+ follow the text description. Nothing bound image to instruction, so the model
+ was free to treat the attachments as decoration.
+
+ ### Seedance multi-reference conditioning โ€” what is documented vs measured
+
+ **Documented by the provider** (Seedance 2.0 API reference, docs.byteplus.com;
+ OpenRouter "Reference to Video" guide, openrouter.ai/docs/features/video-generation):
+
+ | Fact | Source |
+ |---|---|
+ | Reference images are accepted, 1โ€“9 of them | BytePlus Seedance 2.0 API reference (`image_urls`, 1โ€“9) |
+ | The prompt MUST address them as `@Image1`, `@Image2`, โ€ฆ | BytePlus Seedance 2.0 API reference |
+ | Over OpenRouter the transport field is `input_references` (`inputReferences` also documented) | OpenRouter reference-to-video guide |
+ | Reference support is per-model, not universal | OpenRouter reference-to-video guide |
+ | Hard first/last-frame anchoring is a SEPARATE field (`frame_images` + `frame_type`) from soft references | OpenRouter video docs + live `/videos/models` |
+
+ **Established empirically here, because no documentation states it.** OpenRouter
+ does not document a per-model maximum, an item schema for `input_references`, or
+ any signal that references were ignored:
+
+ - `input_references` is Zod-validated: each item must carry `image_url`,
+ `audio_url` or `video_url`, otherwise the request is rejected before routing.
+ - `data:` URLs are accepted as reference items.
+ - **A misspelled field is silently ignored.** Sending `image_urls` (Seedance's
+ own native name) through OpenRouter produced a normal accepted job with the
+ references simply absent โ€” no warning, no error. That silent-drop behaviour is
+ why the redacted request audit below exists.
+ - **`@ImageN` resolves to `input_references` submission order.** Verified by a
+ paid colour-discrimination probe: references `[solid RED, solid BLUE]` with a
+ prompt commanding `@Image2`; the produced frame measured mean RGB
+ `R=0 G=25 B=229` (blue). One-based, positional, and controllable.
+ - OpenRouter accepted 12 reference items without complaint, so its validator is
+ NOT the enforcer of Seedance's documented 9 โ€” the model is. The cap here is 9,
+ from the vendor documentation.
+
+ **No provider mechanism reports that references were ignored.** There is no
+ field, warning, or status for it. That absence is the reason this release logs a
+ structural self-check instead of trusting the provider.
+
+ ### How references are now assembled
+
+ `Pipeline._assemble_scene_references` is the SINGLE ordered source for both the
+ prompt labels and the `input_references` payload. Both are derived from the same
+ list after truncation, so the `@ImageN` number in the prompt cannot disagree with
+ submission order. Order is fixed: **character sheets first**, then the location
+ plate, then the previous approved frame โ€” so the reference cap eats the
+ environment or the continuity anchor before it eats an identity anchor.
+
+ Labels are index-redundant on purpose (`@Image2 โ€” the CHARACTER SHEET of Mochi:
+ โ€ฆ`), so even if numbering were ever mis-resolved the label still says WHO the
+ image shows. Authority is scoped **by kind**: a location plate governs
+ environment only, a continuity frame governs appearance across the cut, and the
+ scene text still decides action, camera and dialogue.
+
+ Reference state is explicit, never a silent text fallback:
+
+ - `complete` โ€” every scene character had a sheet and nothing was dropped.
+ - `partial` โ€” something is missing or was dropped by the cap; the job records a
+ warning naming exactly what.
+ - `none` โ€” nothing reached the request; the job records
+ `verification_stats["scenes_without_references"]` and warns that identity is
+ unanchored for that shot. Generation still proceeds; the state is disclosed.
+
+ ### Request audit (host-verifiable, redacted)
+
+ Every `/videos` POST logs `VIDEO_REQUEST_BODY` and appends a redacted summary to
+ `verification_stats["video_requests"]` (a list, capped at 60, with
+ `video_requests_omitted` counting anything beyond โ€” never a silent drop). The
+ audit sink is passed per call and fires BEFORE the POST, so a request that
+ raises still leaves evidence and retries cannot overwrite each other's record.
+
+ It carries counts, per-entry mime/byte-length/sha256 prefix, `frame_type`, and
+ `prompt_names_all_references` โ€” a structural check that the prompt really named
+ every reference it attached. It deliberately carries **no** base64 payload and
+ **no** prompt text (the prompt contains owner-authored theme and dialogue).
+
+ ### Reference cap raised 4 โ†’ 9
+
+ `VIDEO_MAX_INPUT_REFERENCES` was 4. With characters ordered first, a three-character
+ scene consumed the whole budget and the cap silently discarded the cross-scene
+ continuity frame โ€” the exact anchor that keeps identity stable between shots. It
+ is now 9, the vendor-documented Seedance maximum. A model accepting fewer replies
+ 4xx and the existing reference-rejection path trims and retries the same model.
+
+ ### minimax/hailuo-3 added
+
+ From the live `/videos/models` catalog (not guessed):
+
+ | | `minimax/hailuo-3` | `bytedance/seedance-2.0` |
+ |---|---|---|
+ | catalog id | `minimax/hailuo-3` ("MiniMax: H3") | `bytedance/seedance-2.0` |
+ | resolutions | **`["2K"]` โ€” 2K ONLY** | 480p / 720p / 1080p / 4K |
+ | durations | 5โ€“15s | 4โ€“15s |
+ | frame images | first_frame, last_frame | first_frame, last_frame |
+ | native audio | yes | yes |
+ | pricing SKUs | `duration_seconds: $0.13`, **`reference_images: $0.04`** | `video_tokens: $0.000007` |
+
+ **Does it support reference conditioning? Yes โ€” and this is catalog evidence, not
+ an assumption:** it publishes a `reference_images` price SKU, so references are a
+ billed, first-class input. It has NOT been exercised live here, so its
+ reference-following QUALITY relative to Seedance is unmeasured.
+
+ **Is it cheaper? Yes, on published prices.** A 10-second shot costs
+ `10 ร— $0.13 = $1.30` plus `$0.04` per reference image โ€” roughly `$1.46` with four
+ references, against a measured Seedance cost near `$3.3` per candidate at 720p/10s.
+ About 2ร— cheaper.
+
+ **Two honest caveats.**
+ 1. It is **2K-only**. Selecting it with any other resolution is reconciled
+ upward to 2K with a warning by the existing capability gate; `2K` was added to
+ that gate's ladder, without which this model raised "supports none of the
+ known resolutions" and was impossible to select at all.
+ 2. **Cost estimation is calibrated for Seedance's token pricing.** MiniMax bills
+ per second plus per reference image, which this estimator does not model, so
+ its estimate for MiniMax is too HIGH. That errs toward stopping early rather
+ than overspending, but it is an estimate mismatch and not a measurement.
+
+ It is **not** made the default for scenes with characters: Seedance's
+ reference-following is the behaviour actually measured here.
+
+ #### Independent corroboration from the model pages (read 2026-08-04)
+
+ The catalog table above was derived from `/videos/models`. The two OpenRouter
+ model pages were then read separately and agree with it, which is why these
+ numbers are stated as facts rather than as a single source's claim.
+
+ `minimax/hailuo-3` ("MiniMax: H3", released **2026-07-29**) โ€” its own FAQ:
+
+ > "H3 generates video at 2K, supports 5โ€“15 second clips, covers the 21:9, 16:9,
+ > 4:3, 1:1, 3:4 and 9:16 aspect ratios, can be steered with a first frame and
+ > last frame image and produces a matching audio track."
+
+ That independently confirms 2K-only, the 5โ€“15s window, first/last-frame
+ steering and native audio. Its overview also states "native audiovisual output
+ for reference-driven generation".
+
+ `bytedance/seedance-2.0` โ€” its own overview:
+
+ > "It supports text-to-video, image-to-video with first and last frame control,
+ > and multimodal reference-to-video. It is particularly strong at preserving
+ > character consistency, visual style, and camera movement from reference
+ > material. The number of tokens is given by
+ > (height of output video * width of output video * duration * 24) / 1024"
+
+ **Correction to the "cheaper" claim โ€” the headline prices invert it.** The pages
+ show Seedance "from **$0.06726**/second" against H3 "from **$0.13**/second", so
+ per advertised second H3 is the *more* expensive of the two. The comparison above
+ is still the right one, but only because it is stated at comparable quality:
+ Seedance bills by tokens that scale with `height ร— width`, so its "from" price is
+ the floor at its cheapest resolution, while H3's per-second price is flat and
+ already includes 2K. Against the **measured** Seedance cost near `$3.3` per
+ 720p/10s candidate, a 10-second H3 shot at 2K (`10 ร— $0.13` plus `$0.04` per
+ reference image โ‰ˆ `$1.46`) is cheaper at a higher resolution. Anyone comparing
+ the two headline `$/second` figures alone would reach the opposite conclusion,
+ which is exactly why this is written down.
+
+ **Minimum duration is 5s, not 4s.** `_clamp_duration` falls back to the global
+ 4โ€“15s range only when the capability snapshot is empty; with H3 selected and an
+ empty snapshot a 4-second scene would be sent to a model whose documented floor
+ is 5. The snapshot normally supplies `durations` and clamps correctly, so this is
+ a narrow unknown-capability window, disclosed rather than fixed.
+
+ ### Correction: `medium` runs ONE judge, not a panel of two
+
+ Earlier documentation implied a two-family judge panel at `medium`. That is true
+ only at `max`. At `medium` the identity gate runs a single judge
+ (`google/gemini-3.1-pro-preview`), which is a diagnostic gate: it can still fail
+ a shot and still refuses to green an unanswered checklist, but it provides no
+ cross-family disagreement signal. Two judges from different families remain a
+ `max` feature. This is a documentation correction, not a behaviour change.
+
+ ### Identity checklist keys are now short ids
+
+ The judge checklist previously used the **entire question text** โ€” which embedded
+ a character's whole `visual_traits` string โ€” as the JSON key the model had to
+ reproduce verbatim, twice, for the forward and reversed pass to be matched. No
+ model reproduces hundreds of characters byte-identically, so coverage never
+ matched and every scene of the last live run returned `indeterminate` with an
+ empty `attributes` map. The gate could not pass by construction.
+
+ Keys are now short ordinal ids (`char_01_hair`, `char_02_eyes`,
+ `character_count`), collision-proof by construction rather than by slugifying
+ names that are frequently non-ASCII. Full descriptions stay in the question body.
+ Refusal remains fail-closed โ€” partial coverage, order disagreement, a judge
+ error, or too few judges are all still `indeterminate`, never a pass โ€” a green
+ verdict merely became reachable.
+
+ A character whose sheet did not reach the judge is **not** asked about (there
+ would be no ground truth) but its ids stay in the expected set, so an
+ unverifiable character forces `indeterminate` instead of a free pass.
+
+ ### v3.2: ONE authority for the native-audio flag
+
+ `generate_audio` was recomputed independently in the main scene loop and again in
+ continuity regeneration, while the preflight capability check only **warned** โ€” so
+ a model reporting `generate_audio=false` still received `generate_audio=true` on
+ all four paid dispatch paths, and a gate later applied to one path would have
+ silently missed the others. `_effective_generate_audio(job)` is now the single
+ authority: the owner's dialogue setting is the intent, the live capability
+ snapshot is the veto, and a suppression is recorded as
+ `verification_stats["native_audio_suppressed"]` so a mute clip states its cause
+ instead of looking like a generation failure. An unknown model keeps the owner's
+ intent, because a permissive flag can at worst be ignored by the provider, while
+ a restrictive one would mute a model that does support native speech.
+
+ ### Known, not fixed in v3.1
+
+ - `LEASE_TTL_SEC` (900s) is still shorter than `TIMEOUT_VIDEO` (1320s): a single
+ long video call can outlive its lease and be requeued, paying twice. Low risk
+ at short durations; real at long ones.
+ - Budget is charged at the logical unit, not the physical POST; `api_client` can
+ issue up to three POSTs per charge, and condense/simplify/advisor calls do not
+ charge at all.
+ - MiniMax has not been run live from this skill.