venice-video-harness 2.22.0 → 2.24.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.
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +80 -0
- package/README.md +99 -8
- package/capabilities.json +2 -2
- package/dist/agent/guide.js +1 -1
- package/dist/agent/guide.js.map +1 -1
- package/dist/mini-drama/cli.d.ts.map +1 -1
- package/dist/mini-drama/cli.js +55 -2
- package/dist/mini-drama/cli.js.map +1 -1
- package/dist/mini-drama/stream-choices.d.ts +1 -1
- package/dist/mini-drama/stream-choices.d.ts.map +1 -1
- package/dist/mini-drama/stream-choices.js +9 -9
- package/dist/mini-drama/stream-choices.js.map +1 -1
- package/dist/mini-drama/stream-engine.d.ts +134 -14
- package/dist/mini-drama/stream-engine.d.ts.map +1 -1
- package/dist/mini-drama/stream-engine.js +404 -63
- package/dist/mini-drama/stream-engine.js.map +1 -1
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +51 -1
- package/dist/web/server.js.map +1 -1
- package/dist/web/ui/dist/assets/index-BtGiSg73.js +41 -0
- package/dist/web/ui/dist/index.html +1 -1
- package/package.json +1 -1
- package/dist/web/ui/dist/assets/index-W5lm3DN3.js +0 -41
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; 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`.
|
|
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 (the default as of 2026-09-07 — Turbo read too low-quality; the faster/cheaper `minimax-h3-max-turbo` lane is one dropdown away); 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. The default `minimax-h3-max` is pinned to 480P for speed (~45s/beat, $0.22) — sharper than Turbo but still slower than playback; the look-ahead buffer removes the writer latency but not the render, and only the cheaper/lower-quality Turbo lane nearly keeps pace (the UI says so beside the selector). Per-beat cost comes from the family's quoted `usdPer15s` (verified via `POST /video/quote`: H3 Max is $0.22 at 480P and $0.36 at 768P; Turbo $0.11 at 480P). (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 whole `StreamView` is keyed by project slug in `App.tsx` so switching projects REMOUNTS it with fresh state — without the key, the previous project's beats stayed under the player and the writer/video/resolution selects stayed greyed out because the engine was still bound to the other project (fixed 2026-09-07). (j) **Every beat keeps its exact video prompt.** `StreamBeat.render` = model, prompt, resolution, duration, start frame — byte-for-byte what `renderVideoFile` sent. The Stream tab exposes it per beat ("Full prompt") and as a JSON/Markdown export of the whole stream (`/stream/export.json|md`), so an operator can fine-tune outside the harness. Older beats are backfilled from `.recipe.json` on resume. When the prompt builder changes, the recorded prompt is the evidence of what a given beat actually got. 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`. (k) **Pre-written beats: `stream --beats-file` (2026-09-07).** When the operator wants the beats authored up front — no live writer at all for the scripted span — pass `--beats-file <path>`: a JSON file holding a bare array of `AuthoredBeat` objects or the `{ "beats": [...] }` shape of `/stream/export.json` (entries with an `authored` object are unwrapped, so an exported stream replays as-is). `parseScriptedBeats()` unwraps and type-checks; `normalizeBeat()` runs per entry against the locked cast BEFORE anything bills, so a bad beat fails at load. Engine side, `scriptedBeats` on `StreamEngineOptions` wraps any author (injected override included) in `makeScriptedAuthor()`: beat N of the stream is served from file position N−1 until the file runs out, then the live writer takes over as the fallback — for a new stream that fallback defaults to `STREAM_DEFAULT_WRITER`, so `--beats-file` alone satisfies the writer decision of (g). A writer switch from the Stream tab (`configure`) or a resumed manifest changes ONLY the fallback; scripted beats keep serving, and already-rendered beats are never re-rendered. The continuity rules still bind the file's author: each beat is one continuous shot beginning where the previous ended, and every beat must END wide (anti-pattern 31 — the chain's start frame must not be a human-face close-up). (l) **Look-ahead writer buffer — the writer authors AHEAD of the render by default (2026-09-07, 2.24.0).** The stream runs the writer (producer) and renderer (consumer) concurrently: `runWriter()` keeps up to `lookahead` beats (default `STREAM_DEFAULT_LOOKAHEAD` = 15) authored and waiting in `buffer[]`, and `renderNext()` consumes `buffer[0]` — the in-flight beat, kept there across render retries (this replaced the old single `pendingBeat`) — so a render NEVER blocks on a writer-model call. This is the whole point: previously each beat's wall time was writer latency + render latency in series; now the writer stays ahead so it is just render latency, and a slower/better writer is free as long as it keeps ahead. Priming fills the buffer while the stream is paused, so Start renders back to back immediately. Controls: `--lookahead <n>` (0 = the old serial path, where the render worker authors each beat inline just before rendering it) and `--no-refill` (`autoRefill=false` — fill the buffer once, then author on demand as it drains; default keeps it topped up to the depth). Both are switchable at runtime via `engine.configure()` / `POST /stream/config` and the Stream tab's **Look-ahead buffer** control (depth field + "keep topped up" toggle); a live `buffered/depth` meter rides the `stream-updated` SSE (`buffered`, `lookahead`, `autoRefill`). Invariants: the writer's author context is built in-memory from the union of rendered + buffered beats (`authoredList()` / `buildAuthorContext()`), NOT `story-so-far.md`, so beat N sees the beats already queued ahead of the render (or the writer would repeat itself); the budget still bounds it — `authorTarget()` never authors beats the budget cannot render; the buffer persists as `pendingBeats` in the manifest and is restored on resume ONLY when the beats prefix is intact (a truncated prefix would misalign the chain); switching the writer drops the non-in-flight buffered beats so the new writer takes over from the next beat (preserving the "a switch applies to the next beat" contract); and the writer's idle/backoff timers are `unref`'d so a stopped engine can never hold the process open. Keep `stream-engine.ts`, `cli.ts` (`--lookahead`/`--no-refill`), `server.ts` (`/stream/config` body), `types.ts`, `api.ts`, and `StreamView.tsx` in sync. (m) **Always run and OPEN the local UI when streaming — 100% of the time, unprompted (2026-09-07).** A stream is a live broadcast; a running stream the operator cannot watch is a failure. Whenever the operator asks to start/run/kick off a stream, the agent ALWAYS launches the local web UI and opens it in the browser as soon as the opening beat is ready — the operator must NEVER have to ask for it. Run `venice-video stream -p <project> [-e N] [--direction …]` (it starts the server on port 3000, primes beat 1, and opens the browser by default); never pass `--no-open`. Always surface the `http://127.0.0.1:3000/?project=<slug>&tab=Stream` URL in the reply so the operator can reopen it, and if the browser cannot auto-open (headless/remote/SSH), print that URL prominently and tell them to open it. Free port 3000 first if it is taken.
|
|
486
486
|
|
|
487
487
|
## Learned Anti-Patterns (Production Issues Log)
|
|
488
488
|
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,85 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.24.0 — 2026-09-07
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Look-ahead writer buffer — the writer authors beats ahead of the render by
|
|
8
|
+
default.** The stream now runs the writer and the renderer as a
|
|
9
|
+
producer/consumer pair: the writer keeps up to `--lookahead` beats (default
|
|
10
|
+
**15**) authored and waiting in a buffer, so a render never blocks on a
|
|
11
|
+
writer-model call. Priming fills the buffer while the stream is paused, so
|
|
12
|
+
clicking Start renders back to back with no writer latency. This also lets a
|
|
13
|
+
slower, better writer keep pace as long as it stays ahead.
|
|
14
|
+
- `--lookahead <n>` sets the buffer depth. `0` restores the pre-2.24 serial
|
|
15
|
+
behaviour (author each beat just before it renders).
|
|
16
|
+
- `--no-refill` fills the buffer once, then authors on demand as it drains;
|
|
17
|
+
the default keeps the buffer topped up as the renderer consumes it.
|
|
18
|
+
- Both are switchable at runtime from the Stream tab (a **Look-ahead buffer**
|
|
19
|
+
control: depth input + "keep topped up" toggle) and via
|
|
20
|
+
`POST /stream/config` (`lookahead`, `autoRefill`). The Stream tab shows a
|
|
21
|
+
live `buffered / depth` meter.
|
|
22
|
+
- Switching the writer drops the beats the old writer had buffered (keeping
|
|
23
|
+
only the one on the wire) so the new writer takes over from the next beat.
|
|
24
|
+
- Budget still bounds it: the writer never authors beats the budget cannot
|
|
25
|
+
render. The buffer persists in the manifest (`pendingBeats`) so a resume
|
|
26
|
+
renders the pre-authored beats without paying for them again.
|
|
27
|
+
- Engine: `lookahead` / `autoRefill` on `StreamEngineOptions`, `lookahead` /
|
|
28
|
+
`autoRefill` / `buffered` / `pendingBeats` in the manifest, and
|
|
29
|
+
`STREAM_DEFAULT_LOOKAHEAD` (15), all exported.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- **Default stream video family is now `minimax-h3-max`, pinned to 480P.** Turbo
|
|
34
|
+
reads noticeably lower quality, so the default is the sharper MiniMax H3 Max
|
|
35
|
+
model — but kept at **480P** (not its 768P draft tier) so it still generates
|
|
36
|
+
fast: ~45 s/beat at $0.22 per 15 s (verified via `POST /video/quote`; 768P is
|
|
37
|
+
$0.36 and selectable). It renders slower than playback, but the look-ahead
|
|
38
|
+
buffer takes the writer latency out of the picture and the Stream tab shows
|
|
39
|
+
the hold honestly. Turbo ($0.11, ~30 s) is still one dropdown away for a
|
|
40
|
+
cheaper live watch. `STREAM_DEFAULT_VIDEO_FAMILY`, the default
|
|
41
|
+
`STREAM_MODEL_T2V` / `STREAM_MODEL_I2V` lanes, the H3 Max draft resolution,
|
|
42
|
+
and the CLI `--video-family` default all move to `minimax-h3-max` @ 480P.
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
|
|
46
|
+
- **Stream tab: switching projects no longer leaks the previous project's
|
|
47
|
+
beats or its disabled/attached controls.** The view is keyed by project, so
|
|
48
|
+
it remounts with fresh state on a project switch (the old project's beats
|
|
49
|
+
stayed under the player, and the writer/video/resolution selects stayed
|
|
50
|
+
greyed out because the engine was still bound to the other project).
|
|
51
|
+
|
|
52
|
+
## 2.23.0 — 2026-09-07
|
|
53
|
+
|
|
54
|
+
### Added
|
|
55
|
+
|
|
56
|
+
- **Pre-written beats for the stream: `stream --beats-file`.** Author beats up
|
|
57
|
+
front and the stream renders them without ever calling the writer model.
|
|
58
|
+
Accepts a bare JSON array of beats or the `{ "beats": [...] }` shape of
|
|
59
|
+
`/stream/export.json` (entries with an `authored` object are unwrapped, so an
|
|
60
|
+
exported stream replays as-is). Each entry is normalized against the locked
|
|
61
|
+
cast the same way a writer's output would be; a beat with no description
|
|
62
|
+
fails at load, before anything bills. The live writer (defaulting to
|
|
63
|
+
`STREAM_DEFAULT_WRITER` for a new stream) is only the fallback past the last
|
|
64
|
+
scripted beat, and a writer switch from the Stream tab changes only that
|
|
65
|
+
fallback. Engine side: `scriptedBeats` on `StreamEngineOptions`,
|
|
66
|
+
`makeScriptedAuthor()`, and `parseScriptedBeats()`, all exported.
|
|
67
|
+
|
|
68
|
+
## 2.22.1 — 2026-09-05
|
|
69
|
+
|
|
70
|
+
### Added
|
|
71
|
+
|
|
72
|
+
- **Full prompt per beat, and export.** Each stream beat now records `render`
|
|
73
|
+
— the exact model, prompt, resolution, duration, and start frame sent to the
|
|
74
|
+
video model — in `beat-NNNNN.json` and the manifest. The Stream tab's Story
|
|
75
|
+
So Far list has a **Full prompt** toggle per beat (authored beat + verbatim
|
|
76
|
+
video prompt, Copy prompt / Copy as JSON) and **JSON** / **Markdown** export
|
|
77
|
+
links for every beat plus the writer's system prompt
|
|
78
|
+
(`GET /api/projects/:slug/stream/export.json|md?episode=N`, engine attached
|
|
79
|
+
or not). Beats from before this field existed are backfilled on resume from
|
|
80
|
+
their `.recipe.json` sidecars. `exportStreamJson` / `exportStreamMarkdown`
|
|
81
|
+
are exported from `stream-engine.ts`.
|
|
82
|
+
|
|
3
83
|
## 2.22.0 — 2026-09-05
|
|
4
84
|
|
|
5
85
|
### Added
|
package/README.md
CHANGED
|
@@ -944,6 +944,58 @@ venice-video loop -p ~/VeniceVideos/my-film -e 1 --mode looping # or state it
|
|
|
944
944
|
venice-video loop -p ~/VeniceVideos/my-film -e 1 --mode production
|
|
945
945
|
```
|
|
946
946
|
|
|
947
|
+
### Pre-written beats: `stream --beats-file`
|
|
948
|
+
|
|
949
|
+
The stream writes every beat with a live writer model. To author the beats
|
|
950
|
+
yourself — or have an agent write them up front — pass `--beats-file`. The
|
|
951
|
+
first N beats of the stream are then served from the file and the writer model
|
|
952
|
+
is **never called** for them; only if the stream runs past the last scripted
|
|
953
|
+
beat does the live writer take over (defaulting to `STREAM_DEFAULT_WRITER`).
|
|
954
|
+
|
|
955
|
+
```bash
|
|
956
|
+
venice-video stream -p ~/VeniceVideos/my-film -e 1 \
|
|
957
|
+
--beats-file ~/VeniceVideos/my-film/beats.json \
|
|
958
|
+
--direction "live studio audience laugh track after every joke" \
|
|
959
|
+
--budget 2
|
|
960
|
+
```
|
|
961
|
+
|
|
962
|
+
With `--beats-file` a new stream needs no `--writer`: the file IS the writer
|
|
963
|
+
decision for the beats it covers. A `--writer` still overrides the fallback
|
|
964
|
+
used past the file. On resume the scripted lane re-attaches the same way —
|
|
965
|
+
beats already rendered are never re-rendered, and a writer switch from the
|
|
966
|
+
Stream tab changes only the fallback.
|
|
967
|
+
|
|
968
|
+
The file is JSON and accepts two shapes:
|
|
969
|
+
|
|
970
|
+
```jsonc
|
|
971
|
+
// 1. A bare array of beats.
|
|
972
|
+
[
|
|
973
|
+
{
|
|
974
|
+
"description": "The bell jingles as JAKE strides in and takes the couch.",
|
|
975
|
+
"characters": ["JAKE KELLER", "MEL"],
|
|
976
|
+
"dialogue": { "character": "JAKE KELLER", "line": "The usual.", "delivery": "cheerful" },
|
|
977
|
+
"sfx": "door bell, live studio audience applause",
|
|
978
|
+
"cameraMovement": "slow dolly in to a wide of the cafe",
|
|
979
|
+
"summary": "Jake arrives at the cafe."
|
|
980
|
+
}
|
|
981
|
+
]
|
|
982
|
+
```
|
|
983
|
+
|
|
984
|
+
```jsonc
|
|
985
|
+
// 2. The { "beats": [...] } shape of /stream/export.json — entries with an
|
|
986
|
+
// "authored" object are unwrapped, so an exported stream replays as-is.
|
|
987
|
+
{ "beats": [ { "n": 1, "authored": { "description": "…", … } } ] }
|
|
988
|
+
```
|
|
989
|
+
|
|
990
|
+
Beat fields match `AuthoredBeat` in `stream-engine.ts`. Each entry is
|
|
991
|
+
normalized against the locked cast (names snap to the cast's spelling, missing
|
|
992
|
+
fields are completed), and a beat with no `description` fails at load — before
|
|
993
|
+
anything bills. The stream's continuity rules still apply to what you write:
|
|
994
|
+
each beat is one continuous shot that begins where the previous beat ended,
|
|
995
|
+
and every beat should END on a wide or medium-wide frame, never a human-face
|
|
996
|
+
close-up (the next beat chains off that frame, and MiniMax i2v dies on a
|
|
997
|
+
face-filled start frame — anti-pattern 31).
|
|
998
|
+
|
|
947
999
|
Loop mode starts with one **required, deliberate decision** — **is this for
|
|
948
1000
|
LOOPING or for PRODUCTION?** — because it is a real quality-vs-flow tradeoff, not
|
|
949
1001
|
a default to fall through. In a terminal it asks; non-interactively you must pass
|
|
@@ -1061,7 +1113,7 @@ How it works:
|
|
|
1061
1113
|
writer and the per-beat cost print before beat 1 bills.
|
|
1062
1114
|
2. The writer writes beat 1 from the series bible: concept, setting, aesthetic,
|
|
1063
1115
|
and cast.
|
|
1064
|
-
3. Beat 1 renders text-to-video on MiniMax H3 Max Turbo.
|
|
1116
|
+
3. Beat 1 renders text-to-video on MiniMax H3 Max (the default; the faster, lower-quality Turbo lane is selectable).
|
|
1065
1117
|
4. The writer reads `story-so-far.md` (one line per prior beat) plus the last
|
|
1066
1118
|
6 beats verbatim, and writes beat 2 so it begins exactly where beat 1 ended.
|
|
1067
1119
|
5. Beat 2 renders image-to-video off beat 1's last frame.
|
|
@@ -1081,13 +1133,39 @@ venice-video stream -p <dir> \
|
|
|
1081
1133
|
-e 1 \ # episode the stream lives under (default 1)
|
|
1082
1134
|
--direction "<text>" \ # standing direction folded into every beat's writer prompt
|
|
1083
1135
|
--writer <model> \ # writer; asked for a new stream, required non-interactively (see the bakeoff table)
|
|
1084
|
-
--video-family <family> \ # minimax-h3-max
|
|
1136
|
+
--video-family <family> \ # minimax-h3-max (default) | minimax-h3-max-turbo | wan-3-0 | grok-imagine | seedance-2-0 | seedance-2-5 | kling-o3-standard
|
|
1085
1137
|
--resolution 480P \ # default: the family's draft tier
|
|
1086
1138
|
--duration 15s \ # per-beat length, snapped to the 5-15s ladder
|
|
1139
|
+
--lookahead 15 \ # beats authored AHEAD of the render (0 = serial)
|
|
1087
1140
|
--budget 2 # stop after ~$2; Continue authorizes another budget
|
|
1141
|
+
# --no-refill # fill the look-ahead buffer once, then author on demand
|
|
1088
1142
|
# --unbounded # no cap (streams until Ctrl-C)
|
|
1089
1143
|
```
|
|
1090
1144
|
|
|
1145
|
+
#### Look-ahead writer buffer
|
|
1146
|
+
|
|
1147
|
+
By default the writer runs **ahead** of the render. It is a producer/consumer
|
|
1148
|
+
pair: the writer keeps up to `--lookahead` beats (default **15**) authored and
|
|
1149
|
+
waiting in a buffer, and the renderer pulls from it — so a render never blocks
|
|
1150
|
+
on a writer-model call. While the stream is paused after priming, the writer is
|
|
1151
|
+
already filling the buffer, so clicking Start renders back to back with no
|
|
1152
|
+
writer latency between beats. It also lets you run a slower, better writer
|
|
1153
|
+
without stalling playback, as long as the writer stays ahead of the render.
|
|
1154
|
+
|
|
1155
|
+
- `--lookahead <n>` sets the depth. `0` is serial: each beat is authored just
|
|
1156
|
+
before it renders (the pre-2.24 behaviour), so every beat pays the writer
|
|
1157
|
+
latency.
|
|
1158
|
+
- `--no-refill` fills the buffer once and then authors on demand as it drains;
|
|
1159
|
+
the default keeps it topped up to the depth as the renderer consumes it.
|
|
1160
|
+
- Both are switchable live from the Stream tab (the **Look-ahead buffer**
|
|
1161
|
+
control — a depth field and a "keep topped up" toggle) and via
|
|
1162
|
+
`POST /stream/config`. The tab shows a live `buffered / depth` meter.
|
|
1163
|
+
- Switching the writer drops the beats the old writer had queued (keeping only
|
|
1164
|
+
the one on the wire) so the new writer takes over from the next beat.
|
|
1165
|
+
- The budget still bounds it — the writer never authors beats the budget cannot
|
|
1166
|
+
render — and the buffer is saved in `stream-manifest.json` (`pendingBeats`),
|
|
1167
|
+
so a resume renders the pre-authored beats without paying for them again.
|
|
1168
|
+
|
|
1091
1169
|
The stream is resumable: re-running `stream` continues from the last beat on
|
|
1092
1170
|
disk and chains off it. After 3 consecutive failures (write, chain, or render)
|
|
1093
1171
|
the engine stops rather than skip a beat — a stream cannot have a hidden cut.
|
|
@@ -1171,16 +1249,18 @@ Not offered, with the reason:
|
|
|
1171
1249
|
##### Video Family Matrix
|
|
1172
1250
|
|
|
1173
1251
|
Speed is the wall time to render one 15 s beat. "Lag" is what the viewer
|
|
1174
|
-
feels:
|
|
1175
|
-
|
|
1176
|
-
|
|
1252
|
+
feels: the default `minimax-h3-max` at 480P renders a 15 s beat in ~45 s, so the
|
|
1253
|
+
player holds ~30 s between beats once it has caught up; the faster Turbo lane
|
|
1254
|
+
cuts that to a ~20 s hold at lower quality. The look-ahead buffer takes the
|
|
1255
|
+
writer's time out of this — only the render remains. Every family other than
|
|
1256
|
+
Turbo holds for a minute or more. Cost is the quote for 15 s at the family's
|
|
1177
1257
|
draft resolution. Quality is relative to what the harness knows about each
|
|
1178
1258
|
family (see the model registry and AGENTS.md).
|
|
1179
1259
|
|
|
1180
1260
|
| Family | Privacy | Speed (15 s beat) | Cost / 15 s | Quality | Faces on start frame | Verdict |
|
|
1181
1261
|
|---|---|---|---|---|---|---|
|
|
1182
|
-
| `minimax-h3-max
|
|
1183
|
-
| `minimax-h3-max` | ●●● private |
|
|
1262
|
+
| `minimax-h3-max` **(default)** | ●●● private | ●●○ ~45 s @ 480P | ●●● $0.22 | ●●● sharper than Turbo, same model | ✗ dies after billing; engine soft-resets | The default. H3 Max quality pinned to 480P for speed; ~30 s hold. 768P selectable at $0.36. |
|
|
1263
|
+
| `minimax-h3-max-turbo` | ●●● private | ●●● ~30 s | ●●● $0.11 | ●●○ good motion, native audio, lower quality | ✗ same limit | Fastest and cheapest, the only lane that nearly keeps pace. Draft look at 480P; pick when a live watch matters more than fidelity. |
|
|
1184
1264
|
| `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
1265
|
| `grok-imagine` | ●○○ anonymized | ●○○ ~90 s | ●○○ $0.95 | ●●○ | ✓ | Faster than Wan, pricier, lower ceiling. |
|
|
1186
1266
|
| `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. |
|
|
@@ -1193,7 +1273,8 @@ family (see the model registry and AGENTS.md).
|
|
|
1193
1273
|
|---|---|---|---|
|
|
1194
1274
|
| 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
1275
|
| Watch it live, best sitcom writing | `mistral-small-2603` | `minimax-h3-max-turbo` | Same lag, warmer beats |
|
|
1196
|
-
| Sharper picture, still
|
|
1276
|
+
| Sharper picture, still fast (default) | `deepseek-v4-flash-0731-fast` | `minimax-h3-max` @ 480P | ~45 s per beat, ~30 s hold, $0.22/beat |
|
|
1277
|
+
| Max fidelity, will accept the wait | `deepseek-v4-flash-0731-fast` | `minimax-h3-max` @ 768P | ~65 s per beat, ~50 s hold, $0.36/beat |
|
|
1197
1278
|
| Human faces fill the frame often | any fast writer | `wan-3-0` | Faces never kill the chain; ~2 min per beat |
|
|
1198
1279
|
| 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
1280
|
| 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 |
|
|
@@ -1214,6 +1295,16 @@ Rules of thumb:
|
|
|
1214
1295
|
15 s. The old `TURBO_USD_PER_SEC` constant assumed $0.18; budgets now buy
|
|
1215
1296
|
more beats than they used to.
|
|
1216
1297
|
|
|
1298
|
+
**Take the prompts elsewhere.** Every beat records exactly what was sent to the
|
|
1299
|
+
video model (`render` in `beat-NNNNN.json` and the manifest: model, prompt,
|
|
1300
|
+
resolution, duration, start frame). In the Stream tab, **Full prompt** on any
|
|
1301
|
+
beat in the Story So Far list expands the authored beat and the verbatim video
|
|
1302
|
+
prompt with Copy buttons. **JSON** and **Markdown** at the top of the list
|
|
1303
|
+
download every beat's authored text and full prompt plus the writer's system
|
|
1304
|
+
prompt (`GET /api/projects/:slug/stream/export.json|md?episode=N`; works
|
|
1305
|
+
after the engine has stopped, too). Beats rendered before this field existed
|
|
1306
|
+
are backfilled on resume from their `.recipe.json` sidecars.
|
|
1307
|
+
|
|
1217
1308
|
The `stream` command registers its episode in `series.json` if it is missing, so
|
|
1218
1309
|
the Stream tab always has an episode to show. (Before 2.21.1 a stream under an
|
|
1219
1310
|
unregistered episode rendered beats the browser could not display.)
|
package/capabilities.json
CHANGED
package/dist/agent/guide.js
CHANGED
|
@@ -76,7 +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;
|
|
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; the default MiniMax H3 Max renders slower than playback (higher quality) — only the cheaper/lower-quality Turbo lane nearly keeps pace. (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.',
|
|
80
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.',
|
|
81
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.',
|
|
82
82
|
],
|
package/dist/agent/guide.js.map
CHANGED
|
@@ -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,
|
|
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,qrCAAqrC;YACrrC,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;AA2JpC,QAAA,MAAM,OAAO,SAAgB,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;AA2+J9B,OAAO,EAAE,OAAO,EAAE,CAAC"}
|
package/dist/mini-drama/cli.js
CHANGED
|
@@ -4565,13 +4565,16 @@ program
|
|
|
4565
4565
|
.option('-e, --episode <number>', 'Episode number the stream lives under', '1')
|
|
4566
4566
|
.option('--direction <text>', 'Standing direction folded into every beat (e.g. "live studio audience laugh track after every joke")')
|
|
4567
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('--
|
|
4568
|
+
.option('--beats-file <path>', 'JSON file of pre-written beats served in order before the live writer runs — no writer call for the beats it covers. Accepts an array of beats or { "beats": [...] } (the /stream/export.json shape; entries with an "authored" object are unwrapped). Each beat: { description, characters, dialogue: { character, line, delivery? } | null, sfx, cameraMovement, summary }.')
|
|
4569
|
+
.option('--video-family <family>', 'Video family for the beats: minimax-h3-max (default; higher fidelity, renders slower than playback) | minimax-h3-max-turbo (fastest and cheapest, nearly keeps pace, lower quality) | wan-3-0 | grok-imagine | seedance-2-0 | seedance-2-5 | kling-o3-standard. Switchable from the Stream tab.')
|
|
4569
4570
|
.option('--port <port>', 'Port to listen on', '3000')
|
|
4570
4571
|
.option('--host <host>', 'Host to bind (localhost only by default)', '127.0.0.1')
|
|
4571
4572
|
.option('--resolution <res>', 'Render resolution (default: the video family\'s draft tier, 480P on MiniMax)')
|
|
4572
4573
|
.option('--duration <dur>', 'Per-beat duration, snapped to the 5-15s ladder', '15s')
|
|
4573
4574
|
.option('--budget <usd>', 'Stop after this much estimated spend (each Start authorizes another budget)', '2')
|
|
4574
4575
|
.option('--unbounded', 'No budget cap — stream until stopped', false)
|
|
4576
|
+
.option('--lookahead <n>', 'How many beats the writer authors AHEAD of the renderer so a render never waits on a writer call. 0 = serial (author each beat just before it renders).', '15')
|
|
4577
|
+
.option('--no-refill', 'Fill the look-ahead buffer once at the start, then stop topping it up (author on demand after it drains). Default keeps the buffer full as it drains.')
|
|
4575
4578
|
.option('--no-open', 'Do not open the browser automatically')
|
|
4576
4579
|
.action(async (opts) => {
|
|
4577
4580
|
const json = wantsJson();
|
|
@@ -4599,6 +4602,11 @@ program
|
|
|
4599
4602
|
console.warn('⚠ No cast (add-character). The writer will invent characters; identity will drift more.');
|
|
4600
4603
|
}
|
|
4601
4604
|
const budgetUsd = Number.parseFloat(opts.budget);
|
|
4605
|
+
const lookahead = Number.parseInt(String(opts.lookahead), 10);
|
|
4606
|
+
if (!Number.isFinite(lookahead) || lookahead < 0) {
|
|
4607
|
+
failJson(json, `--lookahead must be a non-negative integer (0 = serial). Got "${opts.lookahead}".`);
|
|
4608
|
+
process.exit(1);
|
|
4609
|
+
}
|
|
4602
4610
|
const slug = series.slug;
|
|
4603
4611
|
const workspace = dirname(projectDir);
|
|
4604
4612
|
if (basename(projectDir) !== slug) {
|
|
@@ -4620,6 +4628,11 @@ program
|
|
|
4620
4628
|
else if (resumed) {
|
|
4621
4629
|
writer = undefined; // the engine keeps the manifest's writer
|
|
4622
4630
|
}
|
|
4631
|
+
else if (opts.beatsFile) {
|
|
4632
|
+
// Pre-written beats skip the writer for the beats they cover. The stored
|
|
4633
|
+
// model is only the fallback past the last scripted beat.
|
|
4634
|
+
writer = STREAM_DEFAULT_WRITER;
|
|
4635
|
+
}
|
|
4623
4636
|
else if (!json && stdin.isTTY) {
|
|
4624
4637
|
writer = await promptChoice('Which model writes the beats? (its speed sets how far the stream lags playback)', STREAM_WRITER_CHOICES.map(w => ({
|
|
4625
4638
|
label: `${w.label} — ~${w.medianSec}s/beat, ${w.reliability} valid, ${w.privacy}${w.id === STREAM_DEFAULT_WRITER ? ' (default)' : ''}`,
|
|
@@ -4637,6 +4650,38 @@ program
|
|
|
4637
4650
|
process.exit(1);
|
|
4638
4651
|
return;
|
|
4639
4652
|
}
|
|
4653
|
+
// Pre-written beats are normalized against the locked cast the same way a
|
|
4654
|
+
// writer's output would be: names snap to the cast's spelling, missing
|
|
4655
|
+
// fields are completed, and a beat with no description fails here — before
|
|
4656
|
+
// anything bills.
|
|
4657
|
+
let scriptedBeats;
|
|
4658
|
+
if (opts.beatsFile) {
|
|
4659
|
+
const beatsPath = resolve(opts.beatsFile);
|
|
4660
|
+
if (!existsSync(beatsPath)) {
|
|
4661
|
+
failJson(json, `Beats file not found: ${beatsPath}`);
|
|
4662
|
+
process.exit(1);
|
|
4663
|
+
return;
|
|
4664
|
+
}
|
|
4665
|
+
const { parseScriptedBeats, normalizeBeat } = await import('./stream-engine.js');
|
|
4666
|
+
try {
|
|
4667
|
+
const raw = JSON.parse(await readFile(beatsPath, 'utf-8'));
|
|
4668
|
+
scriptedBeats = parseScriptedBeats(raw).map((b, i) => {
|
|
4669
|
+
try {
|
|
4670
|
+
return normalizeBeat(b, series);
|
|
4671
|
+
}
|
|
4672
|
+
catch (err) {
|
|
4673
|
+
throw new Error(`beat ${i + 1}: ${err.message}`);
|
|
4674
|
+
}
|
|
4675
|
+
});
|
|
4676
|
+
}
|
|
4677
|
+
catch (err) {
|
|
4678
|
+
failJson(json, `Could not load --beats-file: ${err.message}`);
|
|
4679
|
+
process.exit(1);
|
|
4680
|
+
return;
|
|
4681
|
+
}
|
|
4682
|
+
if (!json)
|
|
4683
|
+
console.log(`Loaded ${scriptedBeats.length} pre-written beat(s) from ${beatsPath}.`);
|
|
4684
|
+
}
|
|
4640
4685
|
// The stream needs only series.json, but the browser builds its episode
|
|
4641
4686
|
// list FROM series.json. A stream under an unregistered episode rendered
|
|
4642
4687
|
// beats the Stream tab could not show (it fell through to "No episodes
|
|
@@ -4669,6 +4714,9 @@ program
|
|
|
4669
4714
|
budgetUsd: Number.isFinite(budgetUsd) ? budgetUsd : undefined,
|
|
4670
4715
|
unbounded: opts.unbounded,
|
|
4671
4716
|
direction: opts.direction,
|
|
4717
|
+
scriptedBeats,
|
|
4718
|
+
lookahead,
|
|
4719
|
+
autoRefill: opts.refill,
|
|
4672
4720
|
broadcaster: hub,
|
|
4673
4721
|
});
|
|
4674
4722
|
await engine.init();
|
|
@@ -4685,7 +4733,7 @@ program
|
|
|
4685
4733
|
// story. A resumed session (beats on disk) opens at once, also paused.
|
|
4686
4734
|
const before = engine.state();
|
|
4687
4735
|
if (!json && before.beats.length === 0) {
|
|
4688
|
-
const perBeat = (getStreamVideoChoice(before.videoFamily)?.usdPer15s ?? 0.
|
|
4736
|
+
const perBeat = (getStreamVideoChoice(before.videoFamily)?.usdPer15s ?? 0.22) * (Number.parseInt(before.duration, 10) / 15);
|
|
4689
4737
|
const fam = getStreamVideoChoice(before.videoFamily);
|
|
4690
4738
|
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
4739
|
console.log('Rendering the opening beat before opening the browser…');
|
|
@@ -4702,6 +4750,8 @@ program
|
|
|
4702
4750
|
resolution: status.resolution,
|
|
4703
4751
|
duration: status.duration,
|
|
4704
4752
|
budget: opts.unbounded ? 'unbounded' : budgetUsd,
|
|
4753
|
+
lookahead,
|
|
4754
|
+
autoRefill: opts.refill,
|
|
4705
4755
|
beats: status.beats.length,
|
|
4706
4756
|
});
|
|
4707
4757
|
}
|
|
@@ -4712,6 +4762,9 @@ program
|
|
|
4712
4762
|
console.log(` video: ${status.model.t2v} (beat 1) then ${status.model.i2v} chained off each last frame @ ${status.resolution || 'default'}, ${status.duration}/beat`);
|
|
4713
4763
|
console.log(' models: switch the writer or the video model any time from the Stream tab; changes apply to the next beat.');
|
|
4714
4764
|
console.log(` direction: ${opts.direction ?? '(none)'}`);
|
|
4765
|
+
console.log(` lookahead: ${lookahead > 0 ? `${lookahead} beats authored ahead of the render${opts.refill ? ', kept topped up' : ', filled once then on demand'} (no writer latency between beats)` : 'serial — each beat is authored just before it renders'}`);
|
|
4766
|
+
if (scriptedBeats?.length)
|
|
4767
|
+
console.log(` beats-file: ${scriptedBeats.length} pre-written beat(s) render before the live writer takes over`);
|
|
4715
4768
|
console.log(` budget: ${opts.unbounded ? 'unbounded (streams until you stop it)' : `$${budgetUsd.toFixed(2)} (Start authorizes another budget)`}`);
|
|
4716
4769
|
console.log(` beats: ${status.beats.length} on disk`);
|
|
4717
4770
|
console.log(' state: PAUSED — click Start in the browser to continue the story. New beats then render back to back.');
|