@wji.ai/cli 0.1.0 → 0.2.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.
@@ -0,0 +1,465 @@
1
+ # Stage 5 — Storyboard
2
+
3
+ The first layer that faces the video model. Cuts are nearly free, so **the only hard ceiling is one
4
+ generation's length — and that belongs to the model, not to the story.** A segment is one video
5
+ generation, so it can only be as long as the provider accepts:
6
+
7
+ | `params.video` | model | one segment | refs | auto duration |
8
+ | --- | --- | --- | --- | --- |
9
+ | `h3` | `minimax-h3-ref` | 5–15 s | 9 | ✗ — always `fixed` |
10
+ | `seedance` | `seedance-2.5-ref` | 4–30 s | 30 | ✓ |
11
+ | `seedance2` | `seedance-2.0-ref` | 4–15 s | 9 | ✓ |
12
+ | `seedance2fast` | `seedance-2.0-fast-ref` | 4–15 s | 9 | ✓ |
13
+ | `wan` | `wan-3.0-ref` | 2–30 s | 10 | ✓ |
14
+
15
+ All five take **whole seconds only**. So decide the model first: the same 90-second episode is
16
+ seven segments on a 15-second model and three on a 30-second one, and a storyboard written for one
17
+ does not transfer to the other. `min`/`maxSegmentSeconds` follow `params.video` on their own — set
18
+ them by hand only to go *narrower*, never wider than the model.
19
+
20
+ **Cuts claim beats**: every beat of every scene is claimed by exactly one cut, in order — change
21
+ the script and `validate` names the cuts that broke.
22
+
23
+ ## Start from the seed
24
+
25
+ ```bash
26
+ node {baseDir}/scripts/short-drama.mjs seed <project> storyboard --ep 1
27
+ ```
28
+
29
+ Three worksheets, all deleted once the segments are written:
30
+
31
+ - **`storyboard.plan`** — the split this model forces. Per scene: its seconds, how many segments it
32
+ needs (`⌈seconds ÷ max⌉`, the fewest the model allows) and how long each should be, with a `note`
33
+ on any scene too short for the model's floor. Follow it. It is arithmetic, not taste, and it is
34
+ the step people skip — writing 30-second beats and then discovering the model tops out at 15.
35
+ - **`storyboard.seedScenes`** — every beat with its computed seconds and speaker, plus an
36
+ `axisHint` naming the pairs that need a declared axis.
37
+ - **`storyboard.visualPlans`** — one worksheet per key scene (the 爽点 scene and the cliff scene):
38
+ 戏剧转向 / 观众立场 / 空间压力 / 视觉推进 / 摄影节奏 / 反应落点 / 声音策略. Fill it before cutting the
39
+ scene (gate `visual-plan`); ordinary scenes get none.
40
+
41
+ ## Segment, then cut
42
+
43
+ 1. **Segment = one generation**: inside the model's range, never crosses a scene (new scene → new
44
+ segment), and a whole number of seconds when `videoDuration: fixed`. Group beats by dramatic
45
+ unit (one exchange, one entrance, one exit) and let `plan` tell you how many runs the scene
46
+ needs — a 15 s model wants 9–15 s runs, a 30 s model can hold a whole exchange in one.
47
+ 2. **Cut = 2–5 s**, aim at 3. A segment holds 3–5 cuts. The dialogue of the beats a cut claims
48
+ must fit in its seconds (`chars ÷ 4.5`) — a 4.4 s line gets a 5 s cut, or two cuts.
49
+ 3. **`frame` per cut** (the prompt for that moment; in `keyframes` mode it is also the keyframe
50
+ image's prompt, and in the default `sheets` mode it is the shot's description inside the segment
51
+ prompt): an English prompt for that moment — composition, this
52
+ moment's positions and poses (already on the boat / still on the pier — the model trusts the
53
+ picture over the text), the size phrase (`wide shot`, `close-up`…), the style's
54
+ `framePhrase` (`photorealistic cinematic film still` / `cinematic film still` /
55
+ `hand-painted anime film still`), `params.ratio`. No names: identity
56
+ comes from the sheet references.
57
+ 4. `camera` from the H3 vocabulary (`Static Shot`, `Push In`, `Tracking Shot`, `Pan Left`…);
58
+ `characters` / `props` ⊆ the scene's; > 3 people on screen needs a `note` (staging).
59
+
60
+ ## Shot language — purpose, axes, boundaries, motion, sound
61
+
62
+ The method is in `craft-shots.md` and `craft-motion.md`; the fields below are what the gates
63
+ check, in **both** reference modes — in `keyframes` the frame becomes an image, in `sheets` it is
64
+ the shot's description, and either way `prompt segment` composes frame + motion + camera + sound
65
+ into the prompt.
66
+
67
+ - **`purpose { kind, change }`** (gate `shot-purpose`) — why this cut exists (`establish / reveal /
68
+ evidence / reaction / pressure / decision / consequence / performance / transition`) and what
69
+ the audience knows or who holds power when it ends. Adjacent cuts differ in size, angle or
70
+ people. A cut on a script `turn` beat or the 爽点 is reveal-class or is followed by a `reaction`
71
+ on someone who did not speak it; the cliff cut is `reveal / consequence / reaction / pressure`.
72
+ - **`angle`, `height`, `axisSide`** (gate `framing-axis`) — three fields, not one: `front /
73
+ three-quarter / profile / rear-three-quarter / back`, `ground / low / eye / high / top`,
74
+ `working / cross / neutral` (`cross` with a `note`). A segment with two or more people declares
75
+ **`axis { between, workingSide, screenLeft, screenRight }`**; consecutive singles on the two axis
76
+ people face opposite screen directions.
77
+ - **`start` (first cut only) and `end`** (gate `continuity`) — `positions / facing / gaze / hands /
78
+ heldProps / state` for every person on screen, in screen terms (bare left / right is the
79
+ screen's). Later cuts begin where the previous ended; the next segment of the same scene begins
80
+ where this one ended. A prop changes hands only on an action beat. The `frame` is the start
81
+ instant: no end-only state in it, no sequence words.
82
+ - **`motion { trigger, action, result }`** and **`cameraNote`** (gate `motion`) — the one visible
83
+ state chain from cue to landing (or `hold`), common actions only, no names, no inferred inner
84
+ states, ≤ 3 hand verbs. A `Static Shot` writes `hold` (what still moves); any move writes
85
+ `motivation` + `endFraming` (and `trigger`, `path`).
86
+ - **`background`** — the third depth layer; on 9:16 a wide without it (or a `note`) is flagged by
87
+ `storyboard-advice`, as are size jumps of more than two steps, a segment that only tightens,
88
+ frames over `maxFrameChars` and negative-form actions.
89
+ - **`segment.sound { ambience, sfx: [{cut, event}], music }`** (gate `sound`) — the same
90
+ `ambience` string on every segment of a scene, sfx pinned to cuts, `music` `N/A` or its intent.
91
+ - **Prompt hygiene** (gate `prompt-hygiene`) — no `…` left from the skeleton, no ids / file names /
92
+ notes, the subtitles-off sentence in the prompt's language unless `params.subtitles`, on H3 the
93
+ other person on screen during a line has `lips remain completely closed`, `<d>` holds only the
94
+ line, and in sheets mode every person on screen is bound by `@imageK` inside their own shot.
95
+ - **`params.speechLeadSeconds`** (0.5) is added to the dialogue-fit check: a 4.0 s line needs a
96
+ 5 s cut.
97
+ - A prop may carry **`textPolicy { mode: readable | symbolic | blank, text }`** (gate
98
+ `text-policy`): readable text is verbatim in every frame and prompt that shows the prop and is
99
+ never overridden by "no text".
100
+
101
+ `render segment` prints one `### SHOT k` block per cut (职责 · 来源 · 时长 · 起点 · 唯一动作 · 终点 ·
102
+ 摄影机 · 光与前景 · 声音 · 视觉依据) — the reference 分镜 shape, published as the shot dataset's content.
103
+
104
+ ## Shoot it like a shoot — lens, light, foreground
105
+
106
+ Three things a real crew decides per set-up and a prompt usually forgets. All three are gated
107
+ (`photography`), and `prompt segment` writes them into the shot lines for you.
108
+
109
+ ### Lens and stop
110
+
111
+ `params.camera` is the production's package — one body, one lens family, one working stop:
112
+
113
+ ```json
114
+ "camera": { "body": "ARRI ALEXA Mini LF", "lens": "Zeiss Supreme Prime", "aperture": "T1.5" }
115
+ ```
116
+
117
+ Per cut, `focal` is the focal length and `aperture` an optional override. Leave `focal` out and the
118
+ shot size decides it, the way a crew would: `extreme-wide` 18mm, `wide` 25mm, `medium` 40mm,
119
+ `close` 75mm, `extreme-close` 100mm macro. Every `frame` must then carry the phrase verbatim:
120
+
121
+ ```
122
+ shot on an ARRI ALEXA Mini LF with a Zeiss Supreme Prime 75mm at T1.5
123
+ ```
124
+
125
+ Naming a real body and a real lens is not decoration: it is the shorthand these models were trained
126
+ on, and it pins sensor size, rendering and — through the stop — how much of the frame is sharp. One
127
+ package across the whole drama is also what makes two shots look like the same production. The stop
128
+ lives here rather than in `lookPhrase` because it is what buys the shallow focus the foreground
129
+ depends on; override it on a cut that genuinely needs depth (`"aperture": "T5.6"` for a wide the
130
+ audience has to read).
131
+
132
+ ### Key light, relative to the camera
133
+
134
+ The same lamp is a key from one side of the axis and a rim from the other, so the direction belongs
135
+ to the **shot**, not to the room:
136
+
137
+ ```json
138
+ "light": { "direction": "back-left",
139
+ "note": "the ON AIR box rims her left shoulder while the desk lamp fills the other side; as the lens pushes in the red creeps further onto her cheek" }
140
+ ```
141
+
142
+ `direction` is one of `frontal` `front-left` `front-right` `side-left` `side-right` `back-left`
143
+ `back-right` `backlight` `top` `underlight` `ambient`, each of which expands to a phrase the frame
144
+ must carry (`rim-lit from behind camera left`). `note` is the part that costs thought: what the key
145
+ picks out, what fills the shadow side, and **how it changes over the cut** — a push in that takes
146
+ the fill away, a pan that carries the face out of one source into another, a turn that trades a
147
+ front key for a rim. A cut where the light does nothing is a cut where the light was not directed.
148
+
149
+ ### Foreground
150
+
151
+ Depth needs something to be shallow *against*. `foreground` names what sits between the lens and
152
+ the subject — a console edge, a curtain of rain off an awning, a blurred shoulder, a doorframe —
153
+ and the frame prompt says it is thrown out of focus. `""` is a deliberately clean frame, and the
154
+ gate allows it for up to half the cuts of a segment: a segment of clean frames reads flat, and a
155
+ drama where every shot is clean reads like a corporate video.
156
+
157
+ Directing feel (not gated): reverse angles for dialogue (asker's close-up, answerer's close-up);
158
+ the first cut of an episode moves; a key action gets its own 2 s insert; a reaction shot after a
159
+ heavy line is free drama; cut on motion; the last cut of a segment leaves a hook; static is the
160
+ default and more than two camera moves per segment is showing off.
161
+
162
+ ## The video prompt (`prompt`)
163
+
164
+ Get the skeleton:
165
+
166
+ ```bash
167
+ node {baseDir}/scripts/short-drama.mjs prompt <project> segment E01-01
168
+ ```
169
+
170
+ It prints the parts that are derived and gated verbatim — the alignment line, `[Shot k] At
171
+ mm:ss.mmm,` stamps, `<d>[Chinese] …</d>` dialogue, camera words, the lens phrase, the light
172
+ direction and each speaker's delivery — and now the parts composed from the shot language: per
173
+ shot the frame, `Cue: … Action: … End: …` from `motion`, the camera clause from `cameraNote`
174
+ (`The camera holds; …` / `Push In (reveal), as his eyes drop: …, ending on …`), on H3 the
175
+ `while <other> does not speak, lips remain completely closed` clause on every line, and per
176
+ segment the sound block from `segment.sound` with the accent and the subtitles-off sentence —
177
+ plus the reference plan. Fill in the descriptions; do not touch the derived tokens. **Change a
178
+ cut's seconds and the prompt is stale** — re-run `prompt`, re-paste.
179
+
180
+ ### References are always `@image1` / `@video1` / `@audio1`
181
+
182
+ Whatever the model, a reference is addressed the way WJI labels it on the canvas: connected
183
+ references are numbered **per type**, in connection order — `@image1 @image2 …` for pictures,
184
+ `@video1 …` for clips, `@audio1 …` for the character voices. That is what the edge labels show
185
+ (the frontend's `referenceOrder.ts`), so the prompt and the canvas read the same, and the order the
186
+ nodes were connected in *is* the numbering — reconnect one and the whole legend shifts. The two
187
+ series are independent: a voice connected after three sheets is `@audio1`, not `@image4`.
188
+
189
+ **A label is used wherever it applies, not once in a legend.** `prompt segment` lists every
190
+ reference the node carries, and the rule is that each one is then referred to by its label
191
+ everywhere it is relevant:
192
+
193
+ | Reference | Legend line | Used again in |
194
+ | --- | --- | --- |
195
+ | `@imageN` keyframe | the alignment line, which pins it to a shot | **that shot's paragraph**, which anchors on it — gated |
196
+ | `@imageN` sheet | what it is and what to match | wherever that thing appears; every one must be mentioned |
197
+ | `@videoN` previous clip | what to carry over from it | the continuity sentence — gated when connected |
198
+ | `@audioN` voice | whose voice it is (by `@imageK` in sheets mode, else by their identity) | **every line that character speaks** — gated per line |
199
+
200
+ `segmentReferences` builds the list in connection order: environment sheet, lighting state, one
201
+ sheet per person, one per prop (or the keyframes, in `keyframes` mode) — then the **previous clip**
202
+ if `params.videoReferences: previous` — then one **voice** per character who *speaks in that
203
+ segment*, in speaking order. Each series numbers from 1 independently.
204
+
205
+ ### `@videoN` — continuity between clips
206
+
207
+ A segment is one generation, and a ten-segment episode is ten of them. Sheets hold the faces and
208
+ the space, but not the grade, the way someone walks, or how fast the camera drifts — which is why
209
+ separate clips of one scene can look like separate productions. `params.videoReferences: previous`
210
+ hands each segment the clip before it as `@video1`:
211
+
212
+ ```
213
+ @video1 is the clip immediately before this one — continue its look, wardrobe, lighting and camera
214
+ language, and pick up where it left off; do not repeat its action.
215
+ ```
216
+
217
+ It is **opt-in** (`none` by default) because it changes every clip after the first and chains them:
218
+ regenerate segment 2 and segments 3+ now reference a clip that no longer exists on screen. Turn it
219
+ on for a scene that has to feel continuous; leave it off for an episode that cuts between places.
220
+
221
+ Caps, from the model forms: **3** reference videos on `minimax-h3-ref` and the Seedance 2.0 pair,
222
+ **5** on `wan-3.0-ref`, **10** on `seedance-2.5-ref`; and a reference video may itself run at most
223
+ **15 s** (30 s on `seedance-2.5-ref`) — so a 30-second `wan` segment cannot be fed to the next one,
224
+ which the `timing` gate reports rather than letting the provider refuse it.
225
+
226
+ On the canvas the previous segment's **video node** is connected directly — it already holds the
227
+ clip, so there is no display node to make. It means segment k cannot be built until k−1 has
228
+ generated, which is the order `manifest` prints anyway.
229
+
230
+ ### Dialogue points at the voice
231
+
232
+ The character's voice sample from stage 2 is connected to the video node, so every line they speak
233
+ is attributed to it — `@audioN` — and carries their `voice.videoDirection`. The label says *which
234
+ voice to read in*; the direction says *how*:
235
+
236
+ ```
237
+ @audio1 is the voice of the person in @image2 — speak their lines in it, matching its timbre,
238
+ pitch, pace and accent.
239
+ …
240
+ @audio1 / @image2, slow and close-miked, low and chest-resonant with a light rasp, falling at the end
241
+ of every phrase, Taiwanese-accented Mandarin, restrained, says <d>[Chinese] 凌晨兩點,這裡是海風電台。</d>
242
+ ```
243
+
244
+ The **full** voice description is spelled out on that character's *first* line in the segment; after
245
+ that the `@audioN` label carries it and only the beat's own delivery is added. The beat's `delivery`
246
+ is script prose in the drama's language, so it goes into a `wan` prompt as written and is **left out
247
+ of an English one** — write the English acting note yourself if the line needs one
248
+ (`@audio1 / @image2, the voice catching on the last three words, says: …`). Repeating sixty
249
+ characters of timbre on every line buys nothing, and on `wan` it feeds the content filter, which
250
+ judges by length.
251
+
252
+ Three things are gated, and the third is the one people get wrong:
253
+
254
+ 1. every speaking character's `voice.videoDirection` appears verbatim (the drama-language voice
255
+ fields instead, for `wan`);
256
+ 2. every connected `@audioN` is mentioned, and no `@audioN` above the number connected;
257
+ 3. **every single line** names its voice reference within the sentence or two before it — not just
258
+ once in the legend. A model picks a voice per line; a line with no label attached gets read in
259
+ whatever the model feels like, which is exactly the drift the voice sample exists to stop.
260
+
261
+ Never the speaker's **name** — the name gate rejects it. Use `@audioN`, the `@imageK` sheet label,
262
+ or `identityEn`.
263
+
264
+ Voices are audio references and have their own, much smaller cap: **3** on `minimax-h3-ref` and the
265
+ Seedance 2.0 pair, **5** on `wan-3.0-ref`, **10** on `seedance-2.5-ref`. More speakers than that in
266
+ one segment is a gate failure — split the segment. `params.maxAudioRefs` tightens it further;
267
+ `params.voiceSamples: false` removes the voice references altogether.
268
+
269
+ **H3 mode** (`params.video: h3`, model `minimax-h3-ref`): alignment line first, then
270
+ `integrated_multimodal_description:` with one `[Shot k]` paragraph per cut (anchor on `@imagek`,
271
+ then action, camera word, dialogue in `<d>`; the speaker's identity, age and voice in English
272
+ outside `<d>`; voice-over as `says in an off-screen voiceover while their lips remain completely
273
+ closed`), then `overall_soundscape:` (ambient and action sounds — the soundscape is an action
274
+ instruction: change the picture, change the sound), then `non_diegetic_music:` (or `N/A`).
275
+
276
+ **Seedance modes** (`seedance` → `seedance-2.5-ref`, `seedance2` → `seedance-2.0-ref`,
277
+ `seedance2fast` → `seedance-2.0-fast-ref`): one prompt dialect for all three — follow the `sd25-pe`
278
+ skill's templates. The anchors are verbatim: `Use @image1 through @imageN as keyframes in that
279
+ order.`, `Use @image1 as the first frame.`, `Use @imageN as the last frame.`, then one sentence per
280
+ keyframe state, the event script, the dialogue verbatim, continuity. Only `@image1…N` may be
281
+ referenced. What differs is the size of the box: 2.5 takes 30 s and 30 references, the 2.0 pair
282
+ 15 s and 9 — which is the whole reason to plan the split per model. The 2–5 s cut band applies to
283
+ all of them. `seedance2fast` is the cheap draft of the same shot; it tops out at `720p`.
284
+
285
+ **Wan mode** (`params.video: wan`, model `wan-3.0-ref`) — an Alibaba model, so the prompt is
286
+ **Chinese**, the reference images are addressed as 图1…图10, and the model generates the
287
+ **audio with the picture**. What that changes:
288
+
289
+ - The first line is the derived legend (参考图说明:图1=镜头1关键帧;…), then one derived header per
290
+ cut: `镜头k(start–end秒,size,camera):` with Chinese size and camera words. Both are gated
291
+ verbatim — take them from `prompt segment`, never retype them.
292
+ - **Speakers are named by picture, never by name**: `台词(图2中的少年,低头):「…」`. Off-screen
293
+ lines are `台词(画外…)`.
294
+ - The last line is `音频:` and must name `params.speechAccent` verbatim (e.g. 台湾口音国语)
295
+ together with the ambient sound. There is **no separate voice stage** in this mode: the accent,
296
+ the delivery and the room tone all come from this one line.
297
+ - Write the shot descriptions in 简体 (the model's native register) and keep the dialogue in the
298
+ drama's own script — the gate only demands the line verbatim.
299
+ - **Whole seconds**: the segment total must be an integer, because `-d` is an integer and
300
+ `autoDuration` is switched off by passing it. At most **10** keyframes per segment.
301
+ - Duration times `params.videoResolution` is the price: 480P is a quarter the rate of 1080P, and
302
+ 480P at 9:16 is enough for a phone-first short drama.
303
+
304
+ **Wan's content filter rejects benign prompts.** `wan-3.0-ref` runs the prompt through DashScope's
305
+ green-net before generating:
306
+
307
+ ```
308
+ ✗ Generation failed: DataInspectionFailed: Green net check failed for text (input):
309
+ Input data may contain inappropriate content.
310
+ ```
311
+
312
+ A rejection happens **before** generation and **costs nothing**. What it actually does, measured on
313
+ an ordinary drama (four 15-second segments):
314
+
315
+ - It is **partly random**: one short string passed once and was rejected twice in three consecutive
316
+ submissions; one segment passed only on attempt 5. So **retry 8–12 times first** — it is free.
317
+ - It is **not only random**: two segments were rejected **24/24** across two rounds, while the same
318
+ prompts' legend, every `镜头k` block, the `整体` line and the `音频` line each **passed on their own**.
319
+ The verdict is on the assembled prompt, not on any phrase in it.
320
+ - **The dialogue is never the problem.** Dialogue-only text passed; the same segment with the
321
+ `台词` lines stripped was still rejected.
322
+ - **What fixed it: making the prompt shorter.** Drop the `整体` paragraph, cut each `镜头k` block to
323
+ one terse sentence of action, and keep `音频` to one clause. Both stuck segments then passed on
324
+ attempts 1 and 2. Rewriting *content* (removing stillness, facial detail, breathing) changed
325
+ nothing — only length did.
326
+
327
+ So: keep Wan segment prompts lean from the start (roughly 300 characters, one line per shot). If a
328
+ segment is rejected, retry, then trim — do not go hunting for a forbidden word.
329
+
330
+ To bisect anyway, use a scratch video node at `-d 2`, 480P: free when rejected, ~30 credits when it
331
+ passes, and sample each fragment several times because one pass proves nothing.
332
+
333
+ ## Sheets mode — no storyboard images (the default)
334
+
335
+ `params.references: sheets` drops stage 5's image generation entirely: the video node references
336
+ the scene sheet, each character sheet and each prop sheet the segment uses (`prompt segment`
337
+ prints the order), and the prompt carries one `Shot k (start–end s, size, camera)` line per cut
338
+ built from the cut's `frame`. **This is the default**, because a storyboard image is a second
339
+ generation per cut to pay for, wait for and approve, and the sheets already hold the faces, the
340
+ space and the props — which is what continuity actually depends on. It works in any mode (`h3`
341
+ then takes an `Image k is …` legend instead of the alignment line).
342
+
343
+ Switch to `references: keyframes` when a shot needs a composition the model would not invent — a
344
+ specific blocking, a prop in a specific place, an exact eyeline — or when a first pass drifts and
345
+ you want to pin each moment. Cuts exist either way and are gated either way: they are the timing
346
+ plan and the shot list, they just do not become images.
347
+
348
+ With `videoDuration: auto` (also the default) the node gets no `-d`; write the planned timing into
349
+ the shot lines and let the model match it (the Seedance models quote the cost for 5 s and bill the
350
+ real length). `minimax-h3-ref` has no auto duration, so h3 always sends `-d` = Σ cut seconds and
351
+ the cuts must sum to a whole number.
352
+
353
+ **Seedance and photoreal people.** By default the Seedance ref models (BytePlus) reject any reference
354
+ image its real-person check flags — with `style: photoreal` that is every character sheet and
355
+ every keyframe with a face, deterministically (HTTP 400, `input image 'content[k]' may contain
356
+ real person`; `content[0]` is the prompt, so `k` counts the references from 1). Environment and
357
+ prop sheets pass. The account can have the **real-person permission** enabled on the provider
358
+ side, after which the same sheets are accepted. So for seedance + photoreal the `params` gate
359
+ demands an explicit `humanReferences`: `false` (no permission — reference only scene and prop
360
+ sheets, put the people in a `Cast:` line of the prompt; the face will drift between clips) or
361
+ `true` (permission enabled — the character sheets are referenced and the identity holds).
362
+
363
+ A look that must hold across every clip — lens, depth of field, foreground blur — goes in
364
+ `params.lookPhrase`, which every segment prompt must carry verbatim.
365
+
366
+ ## Keyframes on WJI
367
+
368
+ Each keyframe is an image node whose references are the **sheet display nodes** on the episode
369
+ canvas, in the order the reference plan prints (environment first, then each person, then props),
370
+ with the plan's prefix in front of the frame prompt (`@image1 is the environment …; @image2 is the
371
+ character sheet of one person in this frame …`). Never reference a dataset node — `canvas gen`
372
+ refuses it; reference the image display node that shows the sheet asset.
373
+
374
+ Chain references when a face still drifts: use the best generated keyframe of that character as an
375
+ extra `--from` on the later keyframes (`the SAME face and clothes as @imageN`). Anything large
376
+ that enters the frame (a boat, a car, a gate) needs its own sheet reference or it is reinvented
377
+ every frame.
378
+
379
+ **First segment first.** Generate its keyframes, download them, check against the sheets (same
380
+ world, same faces, right size, nobody extra), fix, then continue. An episode is 30–40 keyframes;
381
+ a wrong direction costs the whole batch.
382
+
383
+ ## Soundtrack node — Seed Audio is not a TTS
384
+
385
+ `seed-audio-1.0` (the voice-sample model of stage 2) generates a whole **audio scene** from one
386
+ prompt: speech, sound effects, ambience and music in one track, up to 120 s, and a
387
+ **`[start s:end s]` timecode in front of an element pins it to that interval** (100 ms
388
+ precision). So every segment gets a soundtrack node next to its video node, and the prompt is
389
+ derived — not written — from `segment.sound` and the cut timing:
390
+
391
+ ```bash
392
+ node {baseDir}/scripts/short-drama.mjs prompt <project> audio E01-05 # bed (default)
393
+ node {baseDir}/scripts/short-drama.mjs prompt <project> audio E01-02 --full # + the lines, in the @AudioN voices
394
+ ```
395
+
396
+ ```
397
+ [0.0s:11.0s] Ambience, continuous: the low hum of the oden warmer under a faint fluorescent buzz.
398
+ [6.0s:9.0s] SFX: a single ring of the brass wind chimes, rain hammering the glass.
399
+ Music, instrumental: the low cello note.
400
+ [0.0s:6.0s] the music enters.
401
+ [6.0s:10.5s] the music swells — on the chime.
402
+ [10.5s:11.0s] the music falls silent — cuts off with the last line.
403
+ No dialogue, no vocals, no narration.
404
+ ```
405
+
406
+ `params.audioTrack` decides what the node makes: **`bed`** (default) — ambience + sfx + timed
407
+ music, no voices: the editor's music-and-effects track, and what the video model's same-pass sound
408
+ is checked against; **`full`** — the bed plus every line at its cut (start = cut start +
409
+ `speechLeadSeconds`, length from the reading rate), each read in the connected voice sample
410
+ (`--from` the voice display nodes in speaking order → `@Audio1..3`; Seed Audio takes at most 3, so a
411
+ segment with four speakers stays `bed`); **`none`** — no node. Music dynamics are written as cues on
412
+ `sound.music`:
413
+
414
+ ```json
415
+ "music": { "intent": "the low cello note",
416
+ "cues": [{ "at": 1, "do": "enter" }, { "at": 3, "do": "swell", "note": "on the chime" },
417
+ { "at": "10.5s", "do": "silence", "note": "cuts off with the last line" }] }
418
+ ```
419
+
420
+ `at` is a cut number (its start) or seconds inside the segment; `do` ∈ `enter / swell / duck /
421
+ drop / silence / out`. Cues cut the timeline into timecoded states; a plain string is an intent
422
+ with no cues (music under everything), `N/A` is no music. Gated (`sound`, `audio-track`): cues
423
+ valid, ≤ 120 s, prompt ≤ 2048 characters, ≤ 3 voices in `full`. The rules for the bed are the
424
+ `craft-motion.md` §1.6 rules — ambience identical across a scene, sfx on their cut, music a
425
+ timeline decision — the node just makes them audible. `render segment` prints the prompt under
426
+ `## 音轨提示词`; `manifest` asks for `wji.nodes.track` / `wji.assets.track` per segment and the asset
427
+ is attached to the shot dataset with `--add-images`. A same-pass video clip and a Seed Audio bed of
428
+ the same segment are two takes of one design: keep the video's sound section and the bed reading
429
+ the same `segment.sound`, and lay the bed under the clip in the edit when the clip's own sound is
430
+ the weaker of the two.
431
+
432
+ ## Video node
433
+
434
+ `--from` **in the exact order `prompt segment` lists under `references:`** — the pictures first
435
+ (sheet display nodes in sheets mode, keyframe nodes in cut order otherwise), then the **voice
436
+ display nodes** of everyone who speaks in the segment. The pictures become `@image1…N` and the
437
+ voices `@audio1…M`, numbered per type in connection order, which is why the order matters and why
438
+ `canvas connect` appending to the end can silently renumber a legend.
439
+
440
+ Then `-m` the mode's model (check `wji models get` for the exact nickname and its caps);
441
+ `-r params.ratio`; `-d` = Σ cut seconds **only in `videoDuration: fixed`**;
442
+ `--resolution params.videoResolution`; the prompt via `--prompt-file`. Cost it
443
+ (`wji canvas gen <node>` without `--yes`), ask, generate, poll. Then the shot dataset:
444
+ `--add-images <keyframes>`, `--set primaryVideo=<clip>`, `icon` = f1.
445
+
446
+ The voice display nodes are the same assets stage 2 attached to the character datasets
447
+ (`primaryAudio`), added to the episode canvas with `canvas add audio --asset <voiceAsset>` and
448
+ recorded in the episode's `wji.displayNodes` as `"C01:voice"`, exactly like the sheets.
449
+
450
+ ## Sync and bind
451
+
452
+ ```bash
453
+ wji fs mkdir <shot folder> E01 # bind --ep 1 wji.shotFolderId
454
+ wji dataset new <shot/E01> "E01-01" -t shot -d "<scene · seconds>" # bind --ep 1 storyboard.segments.E01-01.wji.datasetId
455
+ node {baseDir}/scripts/short-drama.mjs render <project> segment E01-01 > /tmp/E01-01.md
456
+ wji fs set <id> --merge content=@/tmp/E01-01.md --merge viewMode=content --merge video_prompt=@/tmp/E01-01-prompt.txt
457
+ node {baseDir}/scripts/short-drama.mjs bind <project> --ep 1 storyboard.segments.E01-01.wji.nodes.keyframes.0 <nodeId>
458
+ node {baseDir}/scripts/short-drama.mjs manifest <project> --ep 1 # must be clean before the episode is "done"
459
+ ```
460
+
461
+ ## Gates
462
+
463
+ `segments` · `timing` (durations, the speech lead-in **and** the reference caps) · `cut-content` ·
464
+ `photography` · `video-prompt` · `shot-purpose` · `framing-axis` · `continuity` · `motion` · `sound`
465
+ · `visual-plan` · `text-policy` · `prompt-hygiene` · `audio-track` · `storyboard-advice` (advisory).
@@ -0,0 +1,118 @@
1
+ # Workspace layout on WJI
2
+
3
+ One drama = one workspace. Data lives in the **Dataset** app; work happens on
4
+ **Infinite Canvas** — one canvas for characters, one for scenes and props, one
5
+ per episode. Everything below is done with the `wji` CLI (see the `wji` skill
6
+ for command semantics; this file only fixes *where things go*).
7
+
8
+ ```
9
+ workspace "<title>" wji ws create "<title>" -r <params.ratio> # default apps: Dataset, DatasetImageGeneration, InfiniteCanvas
10
+ ├── Dataset app (semantic folders are created by the backend with the app)
11
+ │ ├── story/ outline dataset · 创作指引 dataset (both type outline) · one screenplay dataset per episode
12
+ │ ├── character/ one dataset per character (type character)
13
+ │ ├── location/ one dataset per scene (type location)
14
+ │ ├── prop/ one dataset per prop (type prop)
15
+ │ ├── shot/E01/ one dataset per segment (type shot) ← subfolder per episode
16
+ │ └── other/
17
+ └── InfiniteCanvas app
18
+ └── canvases/
19
+ ├── characters wji canvas create characters
20
+ ├── scenes wji canvas create scenes
21
+ ├── E01, E02 … wji canvas create E01
22
+ └── canvas-001 (the default one — leave it)
23
+ ```
24
+
25
+ Find the semantic folders once and bind them. They live under the **Dataset
26
+ app's** tree, which `wji ws fs` alone does not list — pass the app id:
27
+
28
+ ```bash
29
+ DS_APP=$(wji --json ws get | jq -r '.apps[] | select(.app=="Dataset") | .id')
30
+ wji --json ws fs --app $DS_APP | jq -r '.. | objects | select(.content.semanticKey?) | "\(.content.semanticKey) \(.id)"'
31
+ node {baseDir}/scripts/short-drama.mjs bind <project> wji.datasetFolders.character <id> # etc.
32
+ ```
33
+
34
+ Folder names in the web are localized; match on `content.semanticKey`
35
+ (`story` `character` `location` `prop` `shot` `other`), never on the name.
36
+
37
+ ## Dataset field contract
38
+
39
+ A dataset is an fsnode with mimetype `workspace/dataset`. The pipeline writes:
40
+
41
+ | Dataset field | Set with | Holds |
42
+ | --- | --- | --- |
43
+ | `name` | `wji dataset new <folder> "<name>"` | entity name (`<title> 大纲`, `<title> 创作指引`, `林小夏`, `便利店店内`, `E01 剧本`, `E01-01`) |
44
+ | `content.type` | `dataset new -t <type>` | `outline` · `screenplay` · `character` · `location` · `prop` · `shot` |
45
+ | `content.description` | `dataset new -d` / `fs describe` | the one-liner (required by the web validator) |
46
+ | `content.content` | `fs set --merge content=@file.md` | the rendered markdown from `render` — what people read in the web |
47
+ | `content.viewMode` | `fs set --merge viewMode=content` | `content` for outline / screenplay / shot prompts, else leave (assets grid) |
48
+ | `images[]` | `fs set --add-images <assetId...>` | **the dataset's asset list, not only pictures** — an audio asset lands there with `category: audio`, so a character carries headshot + sheet + voice. `--add-images` appends and de-duplicates; `--images` would replace |
49
+ | `content.image_prompt` / `video_prompt` / `audio_prompt` | `fs set --merge image_prompt=@prompt.txt` | the machine prompt: the sheet prompt, the segment's video prompt, and on a character the composed voice brief (`prompt <project> voice C01`) |
50
+ | `content.metadata.shortDrama` | `fs set --merge metadata.shortDrama=@entity.json` | the entity's JSON as it is in `project.json` (minus `wji`), plus `{stage, version}` |
51
+ | `images[]` | `fs set --add-images <id…>` | the entity's assets — `--add-images` appends; `--images` **replaces** |
52
+ | `icon` | `fs set --icon <assetId>` | character: headshot; scene/prop: sheet; segment: first keyframe |
53
+ | `primaryAudio` | `fs set --set primaryAudio=<assetId>` | character: voice sample (top-level field, so `--set` is right) |
54
+ | `primaryVideo` | `fs set --set primaryVideo=<assetId>` | segment: the generated clip |
55
+
56
+ `--merge` reads the node and deep-merges into `content`, so the other dataset
57
+ fields survive; `--content` and `--set content.x` replace the whole content
58
+ object — never use them on a dataset. Several `--merge` pairs go in one call.
59
+ Reads right after a write can lag 10 s (server cache) — trust the write, or
60
+ re-read after a moment.
61
+
62
+ Character datasets must end up with the three assets the `wji` skill expects
63
+ (headshot as `icon`, sheet, voice as `primaryAudio`) — Ref video models take
64
+ the headshot + sheet as image references and the voice as audio.
65
+
66
+ ## Canvas conventions
67
+
68
+ ### `characters` canvas — one row per character
69
+
70
+ ```
71
+ [title C01 林小夏] → [image* headshot 1:1] → [image* sheet 16:9 @image1=headshot] → [audio* voice]
72
+ ```
73
+
74
+ - headshot: `canvas add image -n "C01 headshot" -m <image model> -r 1:1 -p "<image.prompt>"`
75
+ - sheet: `canvas add image -n "C01 sheet" --from <headshotNode> -m <image model> -r 16:9 -p "@image1 is the face to match. <image.sheet>"`
76
+ - voice: `canvas add audio -n "C01 voice" -m <audio/tts model> -p "<voice.prompt>"` (optional; skip when no voice model is enabled)
77
+ - title: `canvas add title -t "C01 林小夏" --at <x>,<y>` — one per row, rows 700 px apart
78
+
79
+ Bind the node ids (`wji.nodes.*`) and, after generation, the asset ids
80
+ (`wji.assets.*`); then attach the assets to the dataset.
81
+
82
+ ### `scenes` canvas — one row per scene, then one per prop
83
+
84
+ ```
85
+ [title S01] → [image* sheet 16:9] → [image* lighting:<state> @image1=sheet] …
86
+ [title P01] → [image* sheet 16:9 white bg] → [image* state:<state> @image1=sheet] …
87
+ ```
88
+
89
+ Variants (`variantOf`) reference the parent scene's sheet node as `@image1`.
90
+
91
+ ### `E<nn>` canvas — references on top, one block per segment
92
+
93
+ ```
94
+ row 0 [image display C01 sheet] [image display C02 sheet] [image display S01 sheet] [image display P01 sheet] [audio display C01 voice]
95
+ row 1 [title E01-01] [image* f1 (@image = sheets in frame)] [image* f2] … → [video* E01-01 (@image1..N = keyframes or sheets, @audio1..M = the voices of whoever speaks)] [text prompt]
96
+ row 2 [title E01-02] …
97
+ ```
98
+
99
+ - Display nodes: `canvas add image --asset <assetId> -n "C01 sheet"` and `canvas add audio --asset <voiceAsset> -n "C01 voice"` — record both in the episode's `wji.displayNodes` (`"C01:sheet"`, `"C01:voice"`) so every generate node can `--from` them. Pictures become `@imageN`, voices `@audioN`, numbered per type in connection order.
100
+ - Per segment, beside the video node, a **soundtrack audio node** (`canvas add audio -n "E01-01 track" -m seed-audio-1.0 --prompt-file …`, prompt from `prompt audio E01-01`) → `storyboard.segments.E01-01.wji.nodes.track`; its asset joins the shot dataset's `images[]`.
101
+ - Keyframe k: `canvas add image -n "E01-01 f<k>" --from <scene sheet display> <character sheet displays…> <prop sheet displays…> -m <image model> -r <params.ratio> -p "<frame prompt prefixed with what each @imageN is>"`.
102
+ - Video: `canvas add video -n "E01-01" --from <sheet display nodes, or f1…fN in keyframes mode> <voice display nodes of everyone who speaks> -m <the mode's model> -r <params.ratio> [-d <Σ cut seconds>] --prompt-file prompt.txt`. The model and its resolution spelling come from `prompt segment` (`video.model`, `video.resolution`). `-d` **only in `videoDuration: fixed`** — it switches auto-duration off, which pins the clip to the storyboard's timestamps but also fixes the price; in `auto` (the default) leave it out and the model matches the clip to the prompt.
103
+ - Prompt text node: `canvas add text -n "E01-01 prompt" --text-file prompt.txt` beside the video node, for people reading the canvas.
104
+ - Shot dataset: `dataset new <shot/E01 folder> "E01-01" -t shot -d "<one line>"`, `--set video_prompt=@prompt.txt`, images = keyframes, `primaryVideo` = the clip.
105
+
106
+ Placement: `canvas add` puts a node right of its `--from` nodes; pass `--at x,y`
107
+ for the row starts (title nodes) so blocks do not overlap. Take ids from the
108
+ command output (`wji --json canvas add … | jq -r .id`), never by name.
109
+
110
+ ## Idempotency
111
+
112
+ - Before creating anything, run `manifest` — it lists what is unbound. Only
113
+ create those; a bound id is trusted (verify with `wji fs get <id>` if in doubt).
114
+ - After every create, `bind` the id immediately. Losing the mapping means the
115
+ next run duplicates the node.
116
+ - Regenerating a node keeps its id; only `wji.assets.*` changes.
117
+ - Renaming or renumbering an entity after binding is a manual migration —
118
+ don't.