venice-video-harness 2.21.0 → 2.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/.agents/skills/venice-agent-guide/SKILL.md +1 -0
  2. package/AGENTS.md +1 -1
  3. package/CHANGELOG.md +107 -0
  4. package/README.md +139 -8
  5. package/capabilities.json +2 -2
  6. package/dist/agent/guide.d.ts.map +1 -1
  7. package/dist/agent/guide.js +1 -0
  8. package/dist/agent/guide.js.map +1 -1
  9. package/dist/mini-drama/cli.d.ts.map +1 -1
  10. package/dist/mini-drama/cli.js +72 -14
  11. package/dist/mini-drama/cli.js.map +1 -1
  12. package/dist/mini-drama/stream-choices.d.ts +46 -0
  13. package/dist/mini-drama/stream-choices.d.ts.map +1 -0
  14. package/dist/mini-drama/stream-choices.js +125 -0
  15. package/dist/mini-drama/stream-choices.js.map +1 -0
  16. package/dist/mini-drama/stream-engine.d.ts +81 -6
  17. package/dist/mini-drama/stream-engine.d.ts.map +1 -1
  18. package/dist/mini-drama/stream-engine.js +209 -40
  19. package/dist/mini-drama/stream-engine.js.map +1 -1
  20. package/dist/mini-drama/video-generator.d.ts +6 -1
  21. package/dist/mini-drama/video-generator.d.ts.map +1 -1
  22. package/dist/mini-drama/video-generator.js +8 -3
  23. package/dist/mini-drama/video-generator.js.map +1 -1
  24. package/dist/venice/client.d.ts +8 -0
  25. package/dist/venice/client.d.ts.map +1 -1
  26. package/dist/venice/client.js +7 -1
  27. package/dist/venice/client.js.map +1 -1
  28. package/dist/web/server.d.ts.map +1 -1
  29. package/dist/web/server.js +13 -1
  30. package/dist/web/server.js.map +1 -1
  31. package/dist/web/ui/dist/assets/index-W5lm3DN3.js +41 -0
  32. package/dist/web/ui/dist/index.html +1 -1
  33. package/package.json +4 -3
  34. package/dist/web/ui/dist/assets/index-_MI6v9ev.js +0 -41
@@ -43,6 +43,7 @@ in the other.
43
43
  - Loop mode (`venice-video loop -p <project> -e <n> --mode <looping|production>`): once a shot script exists, render the whole plan continuously and watch it as a live browser loop that hot-swaps fresh takes. It SKIPS the storyboard/QA gates and writes only under `episodes/episode-NNN/loop/`, so it never touches the canonical cut. `looping` = creative flow, lower quality (Turbo 480P, disposable); `production` = gather usable, identity-locked takes (Max R2V @768P). `--mode` is REQUIRED non-interactively; `--budget` caps spend (default $2). This is NOT in the `pipeline` stage list — it is under `branches` in `pipeline --json`.
44
44
  - **Loop mode plays AND renders at the same time — it does NOT pre-generate everything.** The browser loops the takes that already exist while the worker keeps generating new ones, swapping each shot's newer take in on the loop's **next pass** (never mid-clip). Two things make a running loop *look* pre-generated when it isn't: (a) it **pauses** when it hits `--budget` or you click Pause, then just replays what's on disk — raise `--budget` or pass `--unbounded` to keep it generating; (b) `--max-takes` is a per-shot ring buffer (default 3, older takes pruned), not a total.
45
45
  - **Which mode evolves while you watch:** `looping` (Turbo, 480P) renders faster than it plays, so it visibly changes as you watch. `production`/create renders each shot slowly on Max R2V — it's for **gathering keeper takes**, not a continuously-evolving watch, and on a short/few-shot plan it can look static even while running (one take takes longer to render than a full loop cycle takes to play). Want "watch it keep changing"? Use `looping` with a higher/unbounded budget.
46
+ - **Stream mode** (`venice-video stream -p <project> -e <n> --writer <model> [--direction "..."]`): an INFINITE, live-authored story — not a loop. The writer model authors one beat at a time; beat 1 renders t2v, every later beat i2v off the previous last frame. Needs only `series.json`. Two things to do before starting: (1) **ASK the operator which model writes the beats** — it is the voice of the whole story, bills from beat 1, and its speed sets the lag behind playback; a non-interactive new stream with no `--writer` is a hard error (`--writer default` = `deepseek-v4-flash-0731-fast`, the fastest reliable writer in the bakeoff; `kimi-k3` is 3x slower). Both the writer and the video family are dropdowns in the Stream tab; every family other than MiniMax H3 Max Turbo renders slower than playback. (2) If the show has human faces, put the camera rule in `--direction`: end every beat wide, never on a face close-up (MiniMax i2v dies on a face-filled start frame; the engine also enforces this in the writer prompt and falls back to a t2v soft reset after 2 chained failures). The command registers its episode in `series.json` so the Stream tab can show it.
46
47
  - Three video lanes, chosen per shot by the router (see the `venice-video-model-routing` skill): **t2v** (prompt only), **i2v** (animate a supplied START image via `image_url` — establishing/atmosphere shots, or chaining off a previous last frame), **R2V** (identity anchored to a reference stack — the default for character shots). "i2v" means a supplied first frame; "R2V" means `reference_image_urls`, not a start frame — do not conflate them.
47
48
  - To animate a single image you already have (plain i2v, no project), the routing skill's bundled `scripts/venice-video.py --image <file> --model <...-image-to-video>` is the standalone path; the project pipeline is for multi-shot, consistency-first work.
48
49
 
package/AGENTS.md CHANGED
@@ -482,7 +482,7 @@ Use `POST /video/quote` (via `quoteVideo()`) to estimate costs before committing
482
482
 
483
483
  59. **MiniMax H3 Max (simple-prompt) models IMPROVISE dialogue — the scripted line is intent, not a script (2026-09-04).** Unlike Seedance/Wan, which render the exact line you give them, the H3 Max family (`promptStyle: 'simple'`) performs markedly better carrying natural, continuous speech across a whole generation than reciting a verbatim quote — a fixed line fights the model the same way the directorial blocks do. So in **native-dialogue mode** the prompt builders render a speaker's line as INTENT — `[@ImageN, voice, delivery] conveys: "…"` plus a one-line "improvise naturally in character, keep the meaning and tone, don't recite word for word" note — instead of the directorial `[…]: "exact line"` quote. This is automatic via `shouldImproviseDialogue(modelId, series)` in `prompt-builder.ts` (`modelWantsSimplePrompt(modelId)` AND `videoDefaults.audioStrategy !== 'lip-sync'`), applied in both `buildVideoPrompt` (singles) and `buildMontagePrompt` (montage) — the two paths the H3 Max family renders through. Directorial models keep the exact quote; the legacy Seedance-native / Kling multi-shot builders (`buildMultiShotPrompt`) are untouched because those lanes are never simple-prompt. **The exception is exact-lip-sync**: there the `audio_url` drives the exact spoken words, so the line stays verbatim (the improv gate excludes `audioStrategy === 'lip-sync'`). **Consequence:** when a simple-prompt model improvises, burned/exported captions must be derived by transcribing the rendered audio (the editing pipeline's `silencedetect`/whisper path), NOT from `script.json` — the model will not say it word for word. When changing this, keep `shouldImproviseDialogue` / `formatDialogueLine` / `IMPROV_DIALOGUE_NOTE` in `prompt-builder.ts` and this rule in sync.
484
484
 
485
- 60. **`stream` is not `loop` — "infinite" means the STORY never ends, not that the playback cycles (2026-09-04).** When the operator asks for an infinite / never-ending / always-on story, use `venice-video stream -p <project> [--direction "…"]`, not `loop`. The two are different products: `loop` takes a fixed shot plan and re-renders the same N shots forever so the *playback* cycles (`--max-takes` is a ring buffer of candidate renders per shot); `stream` (`src/mini-drama/stream-engine.ts`) never repeats and never re-renders. It authors the story live: the intelligence model (`series.intelligence`, or `--writer`) writes ONE beat at a time from the series bible + `story-so-far.md` (one summary line per prior beat) + the last `STREAM_RECENT_BEATS` (6) beats verbatim, with a hard continuity rule ("begins EXACTLY where the previous beat ended, the camera does not cut"). Beat 1 renders t2v on MiniMax H3 Max Turbo; every later beat renders i2v off the previous beat's last frame (`extractLastFrame`). Invariants: (a) **no re-anchoring, ever** — every frame descends from the frame before it, identity drifts slowly by design, and there is no t2v reset cadence; (b) **no ring buffer** — every beat stays on disk in order (`stream/beat-NNNNN.mp4` + `.json`), disk is the only limit; (c) **a stream cannot skip a beat** — after `MAX_CONSECUTIVE_ERRORS` (3) failures at write, chain-frame, or render the engine STOPS (a skipped beat would be a hidden cut), where the loop would give up on one shot and move on; (d) it needs only `series.json` — no script, storyboard, QA, or references; a locked aesthetic and a cast (`--skip-images` is fine) make the writer much better; (e) `--direction` is standing direction folded into every writer prompt (the place for "laugh track after every joke"), and `openingBeat` lets a caller pin beat 1 verbatim; (f) budget/resume semantics match the loop (billed at queue time, `--budget` stops, Start/Continue authorizes another, `--unbounded` lifts the cap; `stream-manifest.json` resumes from the longest unbroken prefix of beats whose files exist and chains off the last one). The writer is `AuthorFn` and the renderer `RenderFn`, both injectable (`tests/stream-engine.test.mjs`). Output for the browser is the **Stream** tab (`StreamView.tsx`, `stream-updated` SSE, `/api/projects/:slug/stream/{state,start,stop}`), which plays forward from beat 1 and holds on the newest beat until the next lands. When changing stream behavior, keep `stream-engine.ts`, the `stream` command in `cli.ts`, `server.ts` (`WebServerOptions.stream`), `state.ts`, `StreamView.tsx`, `api.ts` (`names` list), `types.ts`, and this rule in sync. Note the `-e` option on `stream` is a string parsed by hand — passing Commander's `parseInt` collided with the `applyContextDefaults` hook and yielded `episode-NaN`.
485
+ 60. **`stream` is not `loop` — "infinite" means the STORY never ends, not that the playback cycles (2026-09-04).** When the operator asks for an infinite / never-ending / always-on story, use `venice-video stream -p <project> [--direction "…"]`, not `loop`. The two are different products: `loop` takes a fixed shot plan and re-renders the same N shots forever so the *playback* cycles (`--max-takes` is a ring buffer of candidate renders per shot); `stream` (`src/mini-drama/stream-engine.ts`) never repeats and never re-renders. It authors the story live: the intelligence model (`series.intelligence`, or `--writer`) writes ONE beat at a time from the series bible + `story-so-far.md` (one summary line per prior beat) + the last `STREAM_RECENT_BEATS` (6) beats verbatim, with a hard continuity rule ("begins EXACTLY where the previous beat ended, the camera does not cut"). Beat 1 renders t2v on MiniMax H3 Max Turbo; every later beat renders i2v off the previous beat's last frame (`extractLastFrame`). Invariants: (a) **no re-anchoring, ever** — every frame descends from the frame before it, identity drifts slowly by design, and there is no t2v reset cadence; (b) **no ring buffer** — every beat stays on disk in order (`stream/beat-NNNNN.mp4` + `.json`), disk is the only limit; (c) **a stream cannot skip a beat** — after `MAX_CONSECUTIVE_ERRORS` (3) failures at write, chain-frame, or render the engine STOPS (a skipped beat would be a hidden cut), where the loop would give up on one shot and move on; (d) it needs only `series.json` — no script, storyboard, QA, or references; a locked aesthetic and a cast (`--skip-images` is fine) make the writer much better; the command registers its episode in `series.json` when missing (the browser builds its episode list from there — an unregistered episode rendered beats the Stream tab could not show, and hid the Start button behind "No episodes yet."); (e) `--direction` is standing direction folded into every writer prompt (the place for "laugh track after every joke"), and `openingBeat` lets a caller pin beat 1 verbatim; (f) budget/resume semantics match the loop (billed at queue time, `--budget` stops, Start/Continue authorizes another, `--unbounded` lifts the cap; `stream-manifest.json` resumes from the longest unbroken prefix of beats whose files exist and chains off the last one); (g) **the writer is the session's first decision — ASK the operator which model writes the beats before starting a new stream.** It is the voice of the whole story and it bills from beat 1. Interactive runs get a `promptChoice` over `selectableTextModels()`; a non-interactive new stream with no `--writer` is a hard error (pass `--writer <model>` or `--writer default`), mirroring `loop --mode`. A resumed stream keeps the project default. The writer and the per-beat cost print before beat 1 bills. Do not treat `series.intelligence` as the operator's choice for the stream — it was chosen for QA and scripting, not latency. The stream's own default is `STREAM_DEFAULT_WRITER` (`deepseek-v4-flash-0731-fast`, 3.8s median, 9/9 valid, thinking off) from the 2026-09-05 bakeoff (`scripts/bakeoff-stream-writer.ts`; results in `src/mini-drama/stream-choices.ts` and the README). `chatJson` takes `disableThinking`; the stream writer sets it per choice — with thinking on, the same models are 3-10x slower and reasoning-only models burn the token budget and return nothing. Both the writer and the video family are dropdowns in the Stream tab (`POST /stream/config`, `engine.configure()`); a change applies to the NEXT beat, the i2v chain survives a family switch (the start frame is a PNG), and a resumed stream keeps the models in its manifest. Every video family other than Turbo renders slower than playback; the UI says so beside the selector. Per-beat cost comes from the family's quoted `usdPer15s` (Turbo is $0.11 at 480P, not the $0.18 the old constant assumed). (h) **A face-ending beat must not kill the stream.** MiniMax i2v dies server-side on a face-filled start frame after billing (anti-pattern 31), and the chain makes one bad frame poison every retry. Three defenses, in order: the writer's system prompt carries a MANDATORY camera rule — end every beat wide, never on a human face close-up; a failed chained render steps the start frame back (`STREAM_CHAIN_STEP_BACK_SEC`); and after `STREAM_CHAIN_FAILURES_BEFORE_RESET` (2) chained failures on one beat, the engine renders that beat **t2v as a soft reset** (`lane: 't2v-reset'`), with the previous beat's summary prepended so the prompt re-establishes the scene. Identity drifts for one beat; a skipped beat or a dead stream is worse. The Stream tab shows `lastError` while a retry is in flight and marks reset beats. (i) **The Stream tab merges, never replaces.** The on-disk manifest is re-read on every `state-changed` (the workspace watcher fires on each beat's files); it is a fallback. `stream-updated` SSE is the truth. The view merges beats by number so a stale disk snapshot can never remove a beat the SSE already delivered — that race is what made new beats appear only after a reload. Only the `<video>` element swaps source (keyed by file); the page never reloads, and playback is kicked explicitly after each swap. The writer is `AuthorFn` and the renderer `RenderFn`, both injectable (`tests/stream-engine.test.mjs`). Output for the browser is the **Stream** tab (`StreamView.tsx`, `stream-updated` SSE, `/api/projects/:slug/stream/{state,start,stop}`), which plays forward from beat 1 and holds on the newest beat until the next lands. When changing stream behavior, keep `stream-engine.ts`, the `stream` command in `cli.ts`, `server.ts` (`WebServerOptions.stream`), `state.ts`, `StreamView.tsx`, `api.ts` (`names` list), `types.ts`, and this rule in sync. Note the `-e` option on `stream` is a string parsed by hand — passing Commander's `parseInt` collided with the `applyContextDefaults` hook and yielded `episode-NaN`.
486
486
 
487
487
  ## Learned Anti-Patterns (Production Issues Log)
488
488
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,112 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.22.0 — 2026-09-05
4
+
5
+ ### Added
6
+
7
+ - **Stream model selectors.** The Stream tab has two dropdowns: the writer
8
+ (which model authors each beat) and the video model family (which t2v/i2v
9
+ pair renders it), each with speed and cost in the label and a plain verdict
10
+ beside it ("keeps up" / "falls behind" / "much slower"). A change applies to
11
+ the next beat; the beat in flight finishes on the models it started with.
12
+ `POST /api/projects/:slug/stream/config`, `StreamEngine.configure()`,
13
+ `--video-family` on the CLI, and `choices` in the manifest. The i2v chain
14
+ survives a family switch. A resumed stream keeps the models in its manifest.
15
+ - **Stream writer bakeoff** (`scripts/bakeoff-stream-writer.ts`): times each
16
+ candidate on the real stream prompt against a real project and checks the
17
+ beat parses. Results are in `src/mini-drama/stream-choices.ts` and the README.
18
+ - `VeniceClient.chatJson` takes `disableThinking`, which sends
19
+ `venice_parameters.disable_thinking`. The stream writer sets it per choice.
20
+ - README "Choosing Stream Models": why the stream has its own writer default,
21
+ why thinking is off, how the numbers were measured, and two decision
22
+ matrices (writer, video family) scored on privacy, speed, cost, and quality,
23
+ plus a pick-by-goal table.
24
+
25
+ ### Changed
26
+
27
+ - **Default stream writer is `deepseek-v4-flash-0731-fast`** (3.8 s median,
28
+ 9/9 valid beats, private), not the project's intelligence model. Kimi K3
29
+ took 35 s per beat with thinking on and ~10 s without, dropping 1 in 9. With
30
+ Turbo's ~30 s render, the fast writer brings a beat to ~35 s wall time for
31
+ 15 s of video. `--writer default` means this model now.
32
+ - **Per-beat cost is quote-derived.** Turbo at 480P is $0.11 per 15 s
33
+ (`POST /video/quote`), not the $0.18 the old `TURBO_USD_PER_SEC` constant
34
+ assumed. Budgets buy more beats than before.
35
+ - `--resolution` on `stream` defaults to the family's draft tier instead of a
36
+ hardcoded `480P`; values are validated against the family.
37
+ - **The stream writer is an explicit decision.** A new stream asks which model
38
+ writes the beats in a terminal; a non-interactive new stream with no
39
+ `--writer` is a hard error (pass `--writer <model>` or `--writer default`),
40
+ mirroring `loop --mode`. A resumed stream keeps the project default. The
41
+ writer and the per-beat cost print before beat 1 bills. AGENTS.md rule 60
42
+ (g)-(i) records the operating rules.
43
+
44
+ ### Fixed
45
+
46
+ - **Stream tab showed no Start button on a fresh project.** `stream` needs only
47
+ `series.json`, but the browser builds its episode list from
48
+ `series.episodes`, and `stream` never registered its episode. The view fell
49
+ through to "No episodes yet." with the engine attached and beat 1 billed.
50
+ `stream` now registers the episode in `series.json` before anything bills,
51
+ and the view renders with a synthetic episode if the list is still empty.
52
+ - **New beats appeared only after a page reload.** Every beat's files fire the
53
+ workspace watcher, which pushes `state-changed`; `App` re-fetches the project
54
+ state, and `StreamView` REPLACED its stream with the on-disk manifest — which
55
+ can lag the `stream-updated` SSE that already delivered the beat. The view
56
+ now merges by beat number (SSE is the truth, disk is a fallback), fetches the
57
+ full state if an event arrives before the initial load, and kicks `play()`
58
+ after each `<video>` source swap.
59
+ - **A face-ending beat stalled the whole stream.** MiniMax i2v dies
60
+ server-side on a face-filled start frame after billing (anti-pattern 31); the
61
+ chain made that one frame poison every retry, and the engine stopped after
62
+ three. Now: the writer's system prompt carries a MANDATORY camera rule (end
63
+ every beat wide, never on a human face close-up); and after
64
+ `STREAM_CHAIN_FAILURES_BEFORE_RESET` (2) chained failures on one beat the
65
+ engine renders that beat t2v as a soft reset (`lane: "t2v-reset"`) with the
66
+ previous beat's summary prepended. The Stream tab shows the retry error and
67
+ marks reset beats.
68
+ - `renderVideoFile` no longer warns "No start image available" on
69
+ text-to-video models, where no start image is expected.
70
+ - **The stream manifest is written atomically** (temp file + rename), so a
71
+ concurrent reader — a resuming engine, or the web server serving `/stream/state`
72
+ — can never parse a half-written manifest and silently fall back to defaults
73
+ (it would otherwise drop the configured writer/family on a restart under load).
74
+
75
+ ### Known gap (feature request)
76
+
77
+ - **No path for the driving agent to author beats.** An operator running the
78
+ agent on a model outside the Venice text registry (e.g. Claude Fable 5.1)
79
+ cannot make that agent the writer. `AuthorFn` is injectable in code only.
80
+ Proposed: `--writer external`, where the engine writes
81
+ `stream/next-beat-request.json` and waits; `venice-video stream-beat -p <dir>
82
+ -e <n> --file beat.json` (or `POST /api/projects/:slug/stream/beat`) accepts
83
+ an `AuthoredBeat`, runs `normalizeBeat`, and renders it. Same shape for
84
+ pinning beat 1 from the CLI.
85
+
86
+ ## 2.21.1 — 2026-09-04
87
+
88
+ ### Changed
89
+
90
+ - **`stream` starts paused after the opening beat.** `prime()` writes and renders
91
+ beat 1, then the engine waits — the browser opens with one beat ready and the
92
+ stream paused. Start in the UI renders the next beat immediately and keeps
93
+ going. A resumed session (beats already on disk) opens paused at once.
94
+ - **Stream tab UI.** The Loop tab is hidden (the `loop` command still exists),
95
+ beat rows are clickable and load that beat, and the error banner is replaced
96
+ with a `paused — ready to continue` badge.
97
+
98
+ ### Fixed
99
+
100
+ - **Chained render failure now steps the start frame back through the previous
101
+ clip instead of retrying the doomed frame.** A chained render that dies
102
+ server-side is almost always the start frame (MiniMax i2v rejects some frames
103
+ after billing — anti-pattern 31); retrying the same frame is a guaranteed
104
+ repeat and re-writing the text is wasted. Each retry keeps the written beat and
105
+ steps the start frame back through the previous clip (0, 0.5, 1.5, 3.0s from
106
+ the end). `extractLastFrame` gains an optional `secondsFromEnd` (default 0 — no
107
+ behavior change for existing callers). Verified live: a beat that died twice on
108
+ the true last frame landed 1.5s back, and the next beat chained off it normally.
109
+
3
110
  ## 2.21.0 — 2026-09-04
4
111
 
5
112
  ### Added
package/README.md CHANGED
@@ -1053,13 +1053,19 @@ venice-video stream -p ~/VeniceVideos/my-show \
1053
1053
 
1054
1054
  How it works:
1055
1055
 
1056
- 1. The project's intelligence model (`series.intelligence`, or `--writer`)
1057
- writes beat 1 from the series bible: concept, setting, aesthetic, and cast.
1058
- 2. Beat 1 renders text-to-video on MiniMax H3 Max Turbo.
1059
- 3. The writer reads `story-so-far.md` (one line per prior beat) plus the last
1056
+ 1. You pick the writer. A new stream asks which model writes the beats (it is
1057
+ the voice of the whole story, and its speed sets how far the stream lags
1058
+ playback). Non-interactive runs must pass `--writer <model>` (or `--writer
1059
+ default` = `deepseek-v4-flash-0731-fast`, the fastest reliable writer in
1060
+ the bakeoff); a resumed stream keeps the writer it last ran with. The
1061
+ writer and the per-beat cost print before beat 1 bills.
1062
+ 2. The writer writes beat 1 from the series bible: concept, setting, aesthetic,
1063
+ and cast.
1064
+ 3. Beat 1 renders text-to-video on MiniMax H3 Max Turbo.
1065
+ 4. The writer reads `story-so-far.md` (one line per prior beat) plus the last
1060
1066
  6 beats verbatim, and writes beat 2 so it begins exactly where beat 1 ended.
1061
- 4. Beat 2 renders image-to-video off beat 1's last frame.
1062
- 5. Repeat forever, until Pause or the budget.
1067
+ 5. Beat 2 renders image-to-video off beat 1's last frame.
1068
+ 6. Repeat forever, until Pause or the budget.
1063
1069
 
1064
1070
  There is no re-anchoring and no ring buffer. Every beat descends from the frame
1065
1071
  before it, and every beat stays on disk in order under
@@ -1074,8 +1080,9 @@ a cast (`add-character`, `--skip-images` is fine) make the writer much better.
1074
1080
  venice-video stream -p <dir> \
1075
1081
  -e 1 \ # episode the stream lives under (default 1)
1076
1082
  --direction "<text>" \ # standing direction folded into every beat's writer prompt
1077
- --writer kimi-k3 \ # writer model (default: the project's intelligence model)
1078
- --resolution 480P \ # 480P or 768P
1083
+ --writer <model> \ # writer; asked for a new stream, required non-interactively (see the bakeoff table)
1084
+ --video-family <family> \ # minimax-h3-max-turbo (default) | minimax-h3-max | wan-3-0 | grok-imagine | seedance-2-0 | seedance-2-5 | kling-o3-standard
1085
+ --resolution 480P \ # default: the family's draft tier
1079
1086
  --duration 15s \ # per-beat length, snapped to the 5-15s ladder
1080
1087
  --budget 2 # stop after ~$2; Continue authorizes another budget
1081
1088
  # --unbounded # no cap (streams until Ctrl-C)
@@ -1087,6 +1094,130 @@ the engine stops rather than skip a beat — a stream cannot have a hidden cut.
1087
1094
  Identity drifts slowly over many hops, by design; that is the trade for a
1088
1095
  continuous, unbroken picture.
1089
1096
 
1097
+ **Faces and the chain.** MiniMax i2v accepts a start frame that is filled by a
1098
+ human face, bills it, and then fails server-side (anti-pattern 31). Because
1099
+ every beat chains off the previous last frame, one face-ending beat could stall
1100
+ the whole stream. Three things keep it alive: the writer is told to end every
1101
+ beat on a wide shot with no face close-up; a failed chained render first steps
1102
+ the start frame back into the previous clip (0.5s, then 1.5s); and after
1103
+ `STREAM_CHAIN_FAILURES_BEFORE_RESET` (2) chained failures on one beat, that beat
1104
+ renders text-to-video as a **soft reset** (`lane: "t2v-reset"`) — the prompt
1105
+ restates the scene from the previous beat's summary, identity drifts for one
1106
+ beat, and the story keeps going. The Stream tab shows the retry error while it
1107
+ happens and marks reset beats in the story list.
1108
+
1109
+ #### Choosing Stream Models
1110
+
1111
+ A stream is a live broadcast whose producer must keep pace with the viewer.
1112
+ Each beat costs two latencies: the **writer** authors the beat, then the
1113
+ **video model** renders it. The viewer watches 15 s of video per beat. Nothing
1114
+ on Venice renders 15 s of video in under 15 s, so every stream eventually
1115
+ catches up to its newest beat and holds on the last frame. The two model
1116
+ choices decide how long that hold is. Both are dropdowns in the **Stream** tab;
1117
+ a change applies to the next beat, and the i2v chain survives a switch.
1118
+
1119
+ **Why the stream does not use the project's intelligence model.** The
1120
+ intelligence model (`series.intelligence`, default `kimi-k3`) is picked for the
1121
+ workshop, the shot script, and vision QA. Those tasks reward depth and reading
1122
+ panels; a 35 s think is fine. A stream beat is a 100-word paragraph in
1123
+ character, once every 30 s, forever. Latency is the whole game, so the stream
1124
+ has its own default and its own list.
1125
+
1126
+ **Why thinking is off.** The harness never sent `disable_thinking`. On the same
1127
+ model that flag is the difference between 35 s and 4 s per beat, and reasoning-
1128
+ only models spend the entire token budget thinking and return nothing.
1129
+ `chatJson` now takes `disableThinking`; the stream sets it per writer.
1130
+
1131
+ **How the numbers were measured.** `scripts/bakeoff-stream-writer.ts` sends the
1132
+ real stream prompt (system + user, from `stream-engine.ts`) for a real project
1133
+ at beat ~28, 3-6 rounds per model, and checks that the reply parses and
1134
+ normalizes to a beat. Video prices are `POST /video/quote` for a 15 s render at
1135
+ the family's draft resolution. Render times are observed on drop-the-beat
1136
+ (MiniMax) or vendor-typical (others). All measured 2026-09-05. Re-run:
1137
+
1138
+ ```bash
1139
+ npx tsx scripts/bakeoff-stream-writer.ts -p <project> --no-thinking [--rounds 6] [--show]
1140
+ ```
1141
+
1142
+ ##### Writer Matrix
1143
+
1144
+ Scores: ●●● best, ●●○ fine, ●○○ weak, ○○○ not usable. Speed is the bakeoff
1145
+ median per beat with thinking off (unless noted). Cost is USD per million
1146
+ input / output tokens; a beat is ~1.5 k in, ~0.4 k out, so even the priciest
1147
+ writer here is under a cent per beat. Quality is a judgment from reading the
1148
+ beats: does it follow the camera rule, keep one joke per beat, stay in voice.
1149
+
1150
+ | Writer | Privacy | Speed | Cost | Quality | Valid | Verdict |
1151
+ |---|---|---|---|---|---|---|
1152
+ | `deepseek-v4-flash-0731-fast` **(default)** | ●●● private | ●●● 3.8 s | ●●● $0.35 / $0.70 | ●●○ tight, follows rules, drier humor | 9/9 | Use this unless you have a reason not to. |
1153
+ | `mistral-small-2603` | ●●● private | ●●● 5.2 s | ●●● $0.19 / $0.75 | ●●● warmest, most physical comedy | 9/9 | Best prose of the fast tier. Sometimes puts the robot's display name in `characters` (normalized away). |
1154
+ | `seed-2-1-turbo` | ●○○ anonymized | ●●○ 9.2 s | ●●○ $0.63 / $3.13 | ●●○ | 9/9 | Reliable, but slower and not private. Pick only if the first two misbehave on your bible. |
1155
+ | `kimi-k3` | ●●● private | ●●○ 9.8 s (35 s thinking on) | ●○○ $3.75 / $18.75 | ●●● richest callbacks and structure | 8/9 | The intelligence default. Best writer; too slow and 1 in 9 beats fails. Use for a stream you will curate, not one you watch live. |
1156
+ | `deepseek-v4-flash` | ●●● private | ●○○ 11.0 s | ●●● $0.14 / $0.28 | ●●○ | 6/6 | Cheapest. 3x slower than its `-fast` sibling for no quality gain. |
1157
+ | `minimax-m27` | ●●● private | ●○○ 10.1 s (thinking on) | ●●● $0.38 / $1.50 | ●●○ | 5/6 | Needs thinking on to be reliable, which caps its speed. |
1158
+ | `gemini-3-8-flash` | ●○○ anonymized | ○○○ 19.1 s (thinking on) | ●●○ $0.94 / $4.69 | ●●● vivid prose | 4/6 | Good writer, wrong tool: slow, breaks JSON with thinking off, not private. |
1159
+
1160
+ Not offered, with the reason:
1161
+
1162
+ | Model | Why not |
1163
+ |---|---|
1164
+ | `z-ai-glm-5-3` | Thinking-only (400 on `disable_thinking`). With thinking on, 0/3 valid: it spent the whole 1500-token budget reasoning. |
1165
+ | `z-ai-glm-5-3-flash` | 2/3 at 21 s with thinking; 0/3 without: answers with prose ("Let me look..."), not JSON. |
1166
+ | `grok-4-6` | 3/3 valid but 67 s median. |
1167
+ | `qwen3-235b-a22b-instruct-2507` | 3/3 valid but 39 s median. |
1168
+ | `qwen3-6-35b-a3b` | 2 s, but truncates the JSON 1 in 3. |
1169
+ | `kimi-k3-fast` | HTTP 500 "Inference processing failed" on every call. |
1170
+
1171
+ ##### Video Family Matrix
1172
+
1173
+ Speed is the wall time to render one 15 s beat. "Lag" is what the viewer
1174
+ feels: with the default writer (~4 s) added, Turbo makes a 15 s beat in ~35 s,
1175
+ so the player holds ~20 s between beats once it has caught up. Every other
1176
+ family holds for a minute or more. Cost is the quote for 15 s at the family's
1177
+ draft resolution. Quality is relative to what the harness knows about each
1178
+ family (see the model registry and AGENTS.md).
1179
+
1180
+ | Family | Privacy | Speed (15 s beat) | Cost / 15 s | Quality | Faces on start frame | Verdict |
1181
+ |---|---|---|---|---|---|---|
1182
+ | `minimax-h3-max-turbo` **(default)** | ●●● private | ●●● ~30 s | ●●● $0.11 | ●●○ good motion, native audio, improvises dialogue | ✗ dies after billing; engine soft-resets | The only lane that nearly keeps pace. Draft look at 480P; 768P selectable. |
1183
+ | `minimax-h3-max` | ●●● private | ●●○ ~60 s | ●●● $0.22 | ●●● sharper than Turbo, same model family | ✗ same limit | Pick when you want the Turbo look at finish quality and will accept a 1-minute hold. |
1184
+ | `wan-3-0` | ●○○ anonymized | ●○○ ~120 s | ●●○ $0.68 | ●●● strong, up to 1080p, 30 s ladder | ✓ accepts faces | Best choice if the show is face-heavy and the camera rule is not enough. Slow. |
1185
+ | `grok-imagine` | ●○○ anonymized | ●○○ ~90 s | ●○○ $0.95 | ●●○ | ✓ | Faster than Wan, pricier, lower ceiling. |
1186
+ | `seedance-2-0` | ●○○ anonymized | ○○○ ~180 s | ●○○ $1.32 | ●●● the harness production look, native lip-synced dialogue | ✓ | Production fidelity. The viewer waits ~3 min per beat. Use for a stream you export, not one you watch. |
1187
+ | `seedance-2-5` | ●○○ anonymized | ○○○ ~180 s | ○○○ $1.93 | ●●● newest Seedance, up to 30 s beats | ✓ | Same trade as 2.0 at a higher price. |
1188
+ | `kling-o3-standard` | ●○○ anonymized | ○○○ ~150 s | ○○○ $1.84 | ●●● | ✓ | No resolution parameter. Expensive for a chain that re-renders forever. |
1189
+
1190
+ ##### Pick By Goal
1191
+
1192
+ | You want | Writer | Video | Expect |
1193
+ |---|---|---|---|
1194
+ | Watch it live, cheapest, private | `deepseek-v4-flash-0731-fast` | `minimax-h3-max-turbo` @ 480P | ~35 s per beat, ~20 s hold, ~$0.11/beat, ~$13/hour of story |
1195
+ | Watch it live, best sitcom writing | `mistral-small-2603` | `minimax-h3-max-turbo` | Same lag, warmer beats |
1196
+ | Sharper picture, still private | `deepseek-v4-flash-0731-fast` | `minimax-h3-max` @ 768P | ~65 s per beat, ~50 s hold, $0.22/beat |
1197
+ | Human faces fill the frame often | any fast writer | `wan-3-0` | Faces never kill the chain; ~2 min per beat |
1198
+ | Production look to export later | `kimi-k3` | `seedance-2-0` or `-2-5` | ~3.5 min per beat, $1.32-1.93/beat; run it overnight, do not watch it live |
1199
+ | Strict privacy for both text and pixels | `deepseek-v4-flash-0731-fast` or `mistral-small-2603` | `minimax-h3-max-turbo` or `minimax-h3-max` | The only fully private pairing; MiniMax is the sole private video family here |
1200
+
1201
+ Rules of thumb:
1202
+
1203
+ - The render is the floor. A faster writer cannot make the stream faster than
1204
+ the video model. Below ~5 s of writer time, nothing more is gained.
1205
+ - Switching the video family mid-stream is safe. The start frame is a PNG; any
1206
+ i2v lane can continue from it. Identity will shift a little across the
1207
+ switch, as it does across every hop.
1208
+ - Switching the writer mid-stream is free and instant. The new writer reads the
1209
+ same `story-so-far.md` and the same last six beats.
1210
+ - Do not chase 768P on Turbo for a live watch. It roughly doubles the render
1211
+ time for the same model; pick `minimax-h3-max` at 768P instead if fidelity
1212
+ is the goal, since you are paying the time either way.
1213
+ - Budget arithmetic is per beat, from the quote. Turbo at 480P is $0.11 for
1214
+ 15 s. The old `TURBO_USD_PER_SEC` constant assumed $0.18; budgets now buy
1215
+ more beats than they used to.
1216
+
1217
+ The `stream` command registers its episode in `series.json` if it is missing, so
1218
+ the Stream tab always has an episode to show. (Before 2.21.1 a stream under an
1219
+ unregistered episode rendered beats the browser could not display.)
1220
+
1090
1221
  ### Interrupted renders are resumable
1091
1222
 
1092
1223
  The harness records each Venice `queue_id` to disk *before* it starts polling, so
package/capabilities.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "harnessVersion": "2.21.0",
4
- "generatedAt": "2026-09-04T19:52:48-03:00",
3
+ "harnessVersion": "2.22.0",
4
+ "generatedAt": "2026-09-05T14:38:23-03:00",
5
5
  "videoModels": [
6
6
  {
7
7
  "id": "wan-2.6-image-to-video",
@@ -1 +1 @@
1
- {"version":3,"file":"guide.d.ts","sourceRoot":"","sources":["../../src/agent/guide.ts"],"names":[],"mappings":"AAkBA,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,eAAO,MAAM,WAAW,EAAE,SAAS,YAAY,EA0E9C,CAAC;AAEF,wBAAgB,WAAW,IAAI;IAAE,OAAO,EAAE,CAAC,CAAC;IAAC,QAAQ,EAAE,SAAS,YAAY,EAAE,CAAA;CAAE,CAE/E;AAED,wBAAgB,WAAW,IAAI,MAAM,CAWpC"}
1
+ {"version":3,"file":"guide.d.ts","sourceRoot":"","sources":["../../src/agent/guide.ts"],"names":[],"mappings":"AAkBA,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,eAAO,MAAM,WAAW,EAAE,SAAS,YAAY,EA2E9C,CAAC;AAEF,wBAAgB,WAAW,IAAI;IAAE,OAAO,EAAE,CAAC,CAAC;IAAC,QAAQ,EAAE,SAAS,YAAY,EAAE,CAAA;CAAE,CAE/E;AAED,wBAAgB,WAAW,IAAI,MAAM,CAWpC"}
@@ -76,6 +76,7 @@ export const AGENT_GUIDE = [
76
76
  'Loop mode (`venice-video loop -p <project> -e <n> --mode <looping|production>`): once a shot script exists, render the whole plan continuously and watch it as a live browser loop that hot-swaps fresh takes. It SKIPS the storyboard/QA gates and writes only under episodes/episode-NNN/loop/, so it never touches the canonical cut. LOOPING = creative flow, lower quality (Turbo 480P, disposable); PRODUCTION = gather usable, identity-locked takes (Max R2V @768P). --mode is REQUIRED non-interactively; --budget caps spend (default $2). This is not in the `pipeline` stage list — it is under `branches`.',
77
77
  'Loop mode plays AND renders at the same time — it does NOT pre-generate everything. The browser loops the takes that already exist while the worker keeps generating new ones, swapping each shot\'s newer take in on the loop\'s NEXT pass (never mid-clip). Two things make a running loop LOOK pre-generated when it is not: (a) it PAUSES when it hits --budget or you click Pause in the UI, and then just replays what is on disk — raise --budget or pass --unbounded to keep it generating; (b) --max-takes is a per-shot ring buffer (default 3, older takes pruned), not a total, so once every shot has its cap the change per cycle is subtle.',
78
78
  'Which mode evolves while you watch: LOOPING (Turbo, 480P) renders faster than it plays, so the video visibly changes as you watch. PRODUCTION/create renders each shot slowly on Max R2V — it is for GATHERING keeper takes, not a continuously-evolving watch, and on a short or few-shot plan it can look static even while running because one take takes longer to render than a full loop cycle takes to play. If the user wants to "watch it keep changing", pick LOOPING and a higher/unbounded budget.',
79
+ 'Stream mode (`venice-video stream -p <project> -e <n> --writer <model> [--direction "..."]`): an INFINITE, live-authored story — not a loop. The writer model authors one beat at a time; beat 1 renders t2v, every later beat i2v off the previous last frame. Needs only series.json. TWO things to do before starting: (1) ASK the operator which model writes the beats — it is the voice of the whole story, bills from beat 1, and its speed sets the lag behind playback; a non-interactive new stream with no --writer is a hard error (--writer default = deepseek-v4-flash-0731-fast, the fastest reliable writer in the bakeoff; kimi-k3 is 3x slower). Both the writer and the video family are dropdowns in the Stream tab; every family other than MiniMax H3 Max Turbo renders slower than playback. (2) Put the camera rule in --direction if the show has human faces: end every beat wide, never on a face close-up (MiniMax i2v dies on a face-filled start frame; the engine also enforces this in the writer prompt and falls back to a t2v soft reset after 2 chained failures). The command registers its episode in series.json so the Stream tab can show it.',
79
80
  'Three video lanes, chosen per shot by the router (see the venice-video-model-routing skill): t2v (prompt only), i2v (animate a supplied START image via image_url — establishing/atmosphere shots, or chaining off a previous last frame), R2V (identity anchored to a reference stack — the default for character shots). "i2v" means a supplied first frame; "R2V" means reference_image_urls, not a start frame — do not conflate them.',
80
81
  'To animate a single image you already have (plain i2v, no project), the routing skill\'s bundled `scripts/venice-video.py --image <file> --model <...-image-to-video>` is the standalone path; the project pipeline is for multi-shot, consistency-first work.',
81
82
  ],
@@ -1 +1 @@
1
- {"version":3,"file":"guide.js","sourceRoot":"","sources":["../../src/agent/guide.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,uDAAuD;AACvD,EAAE;AACF,2EAA2E;AAC3E,8EAA8E;AAC9E,sEAAsE;AACtE,8EAA8E;AAC9E,sDAAsD;AACtD,EAAE;AACF,6EAA6E;AAC7E,8EAA8E;AAC9E,8EAA8E;AAC9E,4EAA4E;AAC5E,EAAE;AACF,gFAAgF;AAChF,8DAA8D;AAC9D,8EAA8E;AAO9E,MAAM,CAAC,MAAM,WAAW,GAA4B;IAClD;QACE,KAAK,EAAE,kCAAkC;QACzC,MAAM,EAAE;YACN,2GAA2G;YAC3G,sGAAsG;YACtG,0GAA0G;SAC3G;KACF;IACD;QACE,KAAK,EAAE,8BAA8B;QACrC,MAAM,EAAE;YACN,sGAAsG;YACtG,wNAAwN;YACxN,kJAAkJ;YAClJ,wGAAwG;SACzG;KACF;IACD;QACE,KAAK,EAAE,yCAAyC;QAChD,MAAM,EAAE;YACN,2FAA2F;YAC3F,4JAA4J;YAC5J,6FAA6F;SAC9F;KACF;IACD;QACE,KAAK,EAAE,2BAA2B;QAClC,MAAM,EAAE;YACN,mGAAmG;YACnG,8JAA8J;YAC9J,kJAAkJ;SACnJ;KACF;IACD;QACE,KAAK,EAAE,4CAA4C;QACnD,MAAM,EAAE;YACN,4PAA4P;YAC5P,kNAAkN;YAClN,kKAAkK;YAClK,6MAA6M;SAC9M;KACF;IACD;QACE,KAAK,EAAE,8BAA8B;QACrC,MAAM,EAAE;YACN,oHAAoH;YACpH,+JAA+J;YAC/J,gKAAgK;YAChK,8JAA8J;YAC9J,6SAA6S;YAC7S,kHAAkH;SACnH;KACF;IACD;QACE,KAAK,EAAE,+DAA+D;QACtE,MAAM,EAAE;YACN,+LAA+L;YAC/L,ylBAAylB;YACzlB,4nBAA4nB;YAC5nB,gfAAgf;YAChf,4aAA4a;YAC5a,gQAAgQ;SACjQ;KACF;IACD;QACE,KAAK,EAAE,gCAAgC;QACvC,MAAM,EAAE;YACN,+EAA+E;YAC/E,gJAAgJ;YAChJ,sFAAsF;YACtF,oIAAoI;SACrI;KACF;CACF,CAAC;AAEF,MAAM,UAAU,WAAW;IACzB,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,QAAQ,EAAE,WAAW,EAAE,CAAC;AAC/C,CAAC;AAED,MAAM,UAAU,WAAW;IACzB,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,CAAC,IAAI,CAAC,sEAAsE,CAAC,CAAC;IACnF,KAAK,CAAC,IAAI,CAAC,mFAAmF,CAAC,CAAC;IAChG,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,MAAM,OAAO,IAAI,WAAW,EAAE,CAAC;QAClC,KAAK,CAAC,IAAI,CAAC,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC;QAClC,KAAK,MAAM,KAAK,IAAI,OAAO,CAAC,MAAM;YAAE,KAAK,CAAC,IAAI,CAAC,OAAO,KAAK,EAAE,CAAC,CAAC;QAC/D,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACjB,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,CAAC;AACpC,CAAC"}
1
+ {"version":3,"file":"guide.js","sourceRoot":"","sources":["../../src/agent/guide.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,uDAAuD;AACvD,EAAE;AACF,2EAA2E;AAC3E,8EAA8E;AAC9E,sEAAsE;AACtE,8EAA8E;AAC9E,sDAAsD;AACtD,EAAE;AACF,6EAA6E;AAC7E,8EAA8E;AAC9E,8EAA8E;AAC9E,4EAA4E;AAC5E,EAAE;AACF,gFAAgF;AAChF,8DAA8D;AAC9D,8EAA8E;AAO9E,MAAM,CAAC,MAAM,WAAW,GAA4B;IAClD;QACE,KAAK,EAAE,kCAAkC;QACzC,MAAM,EAAE;YACN,2GAA2G;YAC3G,sGAAsG;YACtG,0GAA0G;SAC3G;KACF;IACD;QACE,KAAK,EAAE,8BAA8B;QACrC,MAAM,EAAE;YACN,sGAAsG;YACtG,wNAAwN;YACxN,kJAAkJ;YAClJ,wGAAwG;SACzG;KACF;IACD;QACE,KAAK,EAAE,yCAAyC;QAChD,MAAM,EAAE;YACN,2FAA2F;YAC3F,4JAA4J;YAC5J,6FAA6F;SAC9F;KACF;IACD;QACE,KAAK,EAAE,2BAA2B;QAClC,MAAM,EAAE;YACN,mGAAmG;YACnG,8JAA8J;YAC9J,kJAAkJ;SACnJ;KACF;IACD;QACE,KAAK,EAAE,4CAA4C;QACnD,MAAM,EAAE;YACN,4PAA4P;YAC5P,kNAAkN;YAClN,kKAAkK;YAClK,6MAA6M;SAC9M;KACF;IACD;QACE,KAAK,EAAE,8BAA8B;QACrC,MAAM,EAAE;YACN,oHAAoH;YACpH,+JAA+J;YAC/J,gKAAgK;YAChK,8JAA8J;YAC9J,6SAA6S;YAC7S,kHAAkH;SACnH;KACF;IACD;QACE,KAAK,EAAE,+DAA+D;QACtE,MAAM,EAAE;YACN,+LAA+L;YAC/L,ylBAAylB;YACzlB,4nBAA4nB;YAC5nB,gfAAgf;YAChf,wnCAAwnC;YACxnC,4aAA4a;YAC5a,gQAAgQ;SACjQ;KACF;IACD;QACE,KAAK,EAAE,gCAAgC;QACvC,MAAM,EAAE;YACN,+EAA+E;YAC/E,gJAAgJ;YAChJ,sFAAsF;YACtF,oIAAoI;SACrI;KACF;CACF,CAAC;AAEF,MAAM,UAAU,WAAW;IACzB,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,QAAQ,EAAE,WAAW,EAAE,CAAC;AAC/C,CAAC;AAED,MAAM,UAAU,WAAW;IACzB,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,CAAC,IAAI,CAAC,sEAAsE,CAAC,CAAC;IACnF,KAAK,CAAC,IAAI,CAAC,mFAAmF,CAAC,CAAC;IAChG,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,MAAM,OAAO,IAAI,WAAW,EAAE,CAAC;QAClC,KAAK,CAAC,IAAI,CAAC,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC;QAClC,KAAK,MAAM,KAAK,IAAI,OAAO,CAAC,MAAM;YAAE,KAAK,CAAC,IAAI,CAAC,OAAO,KAAK,EAAE,CAAC,CAAC;QAC/D,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACjB,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,CAAC;AACpC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/mini-drama/cli.ts"],"names":[],"mappings":";AAEA,OAAO,eAAe,CAAC;AACvB,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA0JpC,QAAA,MAAM,OAAO,SAAgB,CAAC;AAm4J9B,OAAO,EAAE,OAAO,EAAE,CAAC"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/mini-drama/cli.ts"],"names":[],"mappings":";AAEA,OAAO,eAAe,CAAC;AACvB,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA2JpC,QAAA,MAAM,OAAO,SAAgB,CAAC;AA67J9B,OAAO,EAAE,OAAO,EAAE,CAAC"}
@@ -37,7 +37,7 @@ import { multiEditImage, loadImageAsDataUri } from '../venice/multi-edit.js';
37
37
  import { assembleEpisode, collectShotVideos } from './assembler.js';
38
38
  import { buildGenerationPlan, saveGenerationPlan } from './generation-planner.js';
39
39
  import { AUDIO_STRATEGY_CHOICES, INTELLIGENCE_CHOICES, RENDER_ROUTE_CHOICES, VIDEO_FAMILY_CHOICES, } from './choices.js';
40
- import { DEFAULT_INTELLIGENCE_MODEL, describeIntelligence, resolveIntelligence, } from '../venice/text-models.js';
40
+ import { DEFAULT_INTELLIGENCE_MODEL, describeIntelligence, getTextModel, resolveIntelligence, } from '../venice/text-models.js';
41
41
  import { getProjectLanguage } from '../series/project-language.js';
42
42
  import { approveWorkshop, generateWorkshop, getWorkshopPath, inventoryReferencePath, loadWorkshop, saveWorkshop, } from './workshop.js';
43
43
  import { hasTreatment, refreshTreatment } from './treatment.js';
@@ -82,6 +82,10 @@ function montageModeForRoute(route) {
82
82
  * then the harness default. Projects created before 2.9.0 have no saved
83
83
  * choice and land on the default.
84
84
  */
85
+ function describeStreamWriter(id) {
86
+ const spec = getTextModel(id);
87
+ return spec ? `${spec.label} (${spec.privacy})` : id;
88
+ }
85
89
  function intelligenceFor(series, override) {
86
90
  if (override)
87
91
  return resolveIntelligence(override);
@@ -4560,10 +4564,11 @@ program
4560
4564
  .requiredOption('-p, --project <dir>', 'Project output directory')
4561
4565
  .option('-e, --episode <number>', 'Episode number the stream lives under', '1')
4562
4566
  .option('--direction <text>', 'Standing direction folded into every beat (e.g. "live studio audience laugh track after every joke")')
4563
- .option('--writer <model>', 'Intelligence model that writes beats (default: the project\'s intelligence model)')
4567
+ .option('--writer <model>', 'Model that writes beats. Asked interactively for a new stream; required (or "default" = deepseek-v4-flash-0731-fast, the fastest reliable writer in the bakeoff) in a non-interactive run. A resumed stream keeps the writer it last ran with. Switchable from the Stream tab.')
4568
+ .option('--video-family <family>', 'Video family for the beats: minimax-h3-max-turbo (default; the only one that keeps pace with playback) | minimax-h3-max | wan-3-0 | grok-imagine | seedance-2-0 | seedance-2-5 | kling-o3-standard. Switchable from the Stream tab.')
4564
4569
  .option('--port <port>', 'Port to listen on', '3000')
4565
4570
  .option('--host <host>', 'Host to bind (localhost only by default)', '127.0.0.1')
4566
- .option('--resolution <res>', 'Render resolution (480P or 768P)', '480P')
4571
+ .option('--resolution <res>', 'Render resolution (default: the video family\'s draft tier, 480P on MiniMax)')
4567
4572
  .option('--duration <dur>', 'Per-beat duration, snapped to the 5-15s ladder', '15s')
4568
4573
  .option('--budget <usd>', 'Stop after this much estimated spend (each Start authorizes another budget)', '2')
4569
4574
  .option('--unbounded', 'No budget cap — stream until stopped', false)
@@ -4580,10 +4585,6 @@ program
4580
4585
  failJson(json, `--episode must be a positive integer. Got "${opts.episode}".`);
4581
4586
  process.exit(1);
4582
4587
  }
4583
- if (!['480P', '768P'].includes(opts.resolution.toUpperCase())) {
4584
- failJson(json, `--resolution must be 480P or 768P. Got "${opts.resolution}".`);
4585
- process.exit(1);
4586
- }
4587
4588
  const projectDir = resolve(opts.project);
4588
4589
  const series = await loadSeries(projectDir);
4589
4590
  if (!series) {
@@ -4603,9 +4604,53 @@ program
4603
4604
  if (basename(projectDir) !== slug) {
4604
4605
  console.warn(`⚠ Project directory basename (${basename(projectDir)}) does not match the series slug (${slug}); browser media/deeplink may not resolve.`);
4605
4606
  }
4607
+ // The writer is the session's first decision, like `loop --mode`: it is
4608
+ // the voice of the whole story, it costs money from beat 1, and its speed
4609
+ // sets how far the stream falls behind playback. Ask in a terminal; in a
4610
+ // non-interactive run require --writer so no agent silently commits the
4611
+ // operator to a model they did not choose. The list is the bakeoff list
4612
+ // (stream-choices.ts), fastest reliable first — NOT the project's
4613
+ // intelligence model, which is tuned for QA and scripting, not latency.
4614
+ const { STREAM_WRITER_CHOICES, STREAM_DEFAULT_WRITER, STREAM_VIDEO_CHOICES, getStreamVideoChoice } = await import('./stream-choices.js');
4615
+ const resumed = existsSync(join(getEpisodeDir(series, episodeNumber), 'stream', 'stream-manifest.json'));
4616
+ let writer;
4617
+ if (opts.writer) {
4618
+ writer = opts.writer === 'default' ? STREAM_DEFAULT_WRITER : opts.writer;
4619
+ }
4620
+ else if (resumed) {
4621
+ writer = undefined; // the engine keeps the manifest's writer
4622
+ }
4623
+ else if (!json && stdin.isTTY) {
4624
+ writer = await promptChoice('Which model writes the beats? (its speed sets how far the stream lags playback)', STREAM_WRITER_CHOICES.map(w => ({
4625
+ label: `${w.label} — ~${w.medianSec}s/beat, ${w.reliability} valid, ${w.privacy}${w.id === STREAM_DEFAULT_WRITER ? ' (default)' : ''}`,
4626
+ value: w.id,
4627
+ description: w.note,
4628
+ })), 0);
4629
+ }
4630
+ else {
4631
+ failJson(json, `A writer is required for a new stream: pass --writer <model>, or --writer default (${STREAM_DEFAULT_WRITER}). Tested writers, fastest first: ${STREAM_WRITER_CHOICES.map(w => `${w.id} (~${w.medianSec}s)`).join(', ')}.`);
4632
+ process.exit(1);
4633
+ return;
4634
+ }
4635
+ if (opts.videoFamily && !getStreamVideoChoice(opts.videoFamily)) {
4636
+ failJson(json, `Unknown --video-family "${opts.videoFamily}". Choices: ${STREAM_VIDEO_CHOICES.map(v => v.id).join(', ')}.`);
4637
+ process.exit(1);
4638
+ return;
4639
+ }
4640
+ // The stream needs only series.json, but the browser builds its episode
4641
+ // list FROM series.json. A stream under an unregistered episode rendered
4642
+ // beats the Stream tab could not show (it fell through to "No episodes
4643
+ // yet." with no Start button). Register the episode before anything bills.
4644
+ if (!series.episodes.some(ep => ep.number === episodeNumber)) {
4645
+ while (series.episodes.length < episodeNumber) {
4646
+ addEpisode(series, series.episodes.length + 1 === episodeNumber ? 'Stream' : `Episode ${series.episodes.length + 1}`);
4647
+ }
4648
+ await saveSeries(series);
4649
+ if (!json)
4650
+ console.log(`Registered episode ${episodeNumber} in series.json so the browser can show the stream.`);
4651
+ }
4606
4652
  const apiKey = await getVeniceApiKey();
4607
4653
  const client = new VeniceClient(apiKey);
4608
- const writer = intelligenceFor(series, opts.writer).model;
4609
4654
  const { startWebServer } = await import('../web/server.js');
4610
4655
  const { EventHub } = await import('../web/events.js');
4611
4656
  const { StreamEngine } = await import('./stream-engine.js');
@@ -4618,7 +4663,8 @@ program
4618
4663
  episodeDir: getEpisodeDir(series, episodeNumber),
4619
4664
  slug,
4620
4665
  writerModel: writer,
4621
- resolution: opts.resolution.toUpperCase(),
4666
+ videoFamily: opts.videoFamily,
4667
+ resolution: opts.resolution,
4622
4668
  duration: opts.duration,
4623
4669
  budgetUsd: Number.isFinite(budgetUsd) ? budgetUsd : undefined,
4624
4670
  unbounded: opts.unbounded,
@@ -4633,14 +4679,25 @@ program
4633
4679
  hub,
4634
4680
  stream: { slug, episode: episodeNumber, engine },
4635
4681
  });
4636
- const status = await engine.start();
4637
4682
  const url = `http://${opts.host}:${server.port}/?project=${encodeURIComponent(slug)}&tab=Stream`;
4683
+ // A new session renders the opening beat and then WAITS. The browser opens
4684
+ // with one beat ready and the stream paused; Start in the UI continues the
4685
+ // story. A resumed session (beats on disk) opens at once, also paused.
4686
+ const before = engine.state();
4687
+ if (!json && before.beats.length === 0) {
4688
+ const perBeat = (getStreamVideoChoice(before.videoFamily)?.usdPer15s ?? 0.11) * (Number.parseInt(before.duration, 10) / 15);
4689
+ const fam = getStreamVideoChoice(before.videoFamily);
4690
+ console.log(`Writer: ${describeStreamWriter(before.model.writer)}. Video: ${before.model.t2v} @ ${before.resolution || 'default'}, ${before.duration}/beat, about $${perBeat.toFixed(2)} per beat (billed at queue time)${fam && fam.speed !== 'keeps up' ? ` — ${fam.label} renders slower than playback (~${fam.renderSecApprox}s per beat)` : ''}.`);
4691
+ console.log('Rendering the opening beat before opening the browser…');
4692
+ }
4693
+ const status = await engine.prime();
4638
4694
  if (json) {
4639
4695
  emitJson({
4640
4696
  ok: true,
4641
4697
  url,
4642
4698
  project: slug,
4643
4699
  episode: episodeNumber,
4700
+ paused: true,
4644
4701
  model: status.model,
4645
4702
  resolution: status.resolution,
4646
4703
  duration: status.duration,
@@ -4651,12 +4708,13 @@ program
4651
4708
  else {
4652
4709
  console.log(`venice-video stream running at ${url}`);
4653
4710
  console.log(` project: ${slug} (episode ${episodeNumber})`);
4654
- console.log(` writer: ${describeIntelligence(status.model.writer)}`);
4655
- console.log(` video: ${status.model.t2v} (beat 1) then ${status.model.i2v} chained off each last frame @ ${status.resolution}, ${status.duration}/beat`);
4711
+ console.log(` writer: ${describeStreamWriter(status.model.writer)}`);
4712
+ console.log(` video: ${status.model.t2v} (beat 1) then ${status.model.i2v} chained off each last frame @ ${status.resolution || 'default'}, ${status.duration}/beat`);
4713
+ console.log(' models: switch the writer or the video model any time from the Stream tab; changes apply to the next beat.');
4656
4714
  console.log(` direction: ${opts.direction ?? '(none)'}`);
4657
4715
  console.log(` budget: ${opts.unbounded ? 'unbounded (streams until you stop it)' : `$${budgetUsd.toFixed(2)} (Start authorizes another budget)`}`);
4658
- if (status.beats.length > 0)
4659
- console.log(` resuming: ${status.beats.length} beats already on disk`);
4716
+ console.log(` beats: ${status.beats.length} on disk`);
4717
+ console.log(' state: PAUSED — click Start in the browser to continue the story. New beats then render back to back.');
4660
4718
  console.log(' The story never repeats and never re-renders. Every beat is kept under the episode\'s stream/ directory.');
4661
4719
  console.log(' Ctrl-C to stop.');
4662
4720
  if (opts.open)