@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.
- package/README.md +10 -1
- package/dist/commands/canvas.js +91 -12
- package/dist/commands/fsnode.js +30 -1
- package/dist/commands/gen.js +4 -19
- package/dist/commands/workspace.js +6 -2
- package/dist/index.js +4 -1
- package/package.json +1 -1
- package/skill/short-drama/SKILL.md +305 -0
- package/skill/short-drama/examples/night-shift/episodes/E01.json +1883 -0
- package/skill/short-drama/examples/night-shift/project.json +774 -0
- package/skill/short-drama/reference/canvas-recipes.md +199 -0
- package/skill/short-drama/reference/craft-motion.md +116 -0
- package/skill/short-drama/reference/craft-shots.md +177 -0
- package/skill/short-drama/reference/craft-story.md +188 -0
- package/skill/short-drama/reference/dialogue-craft.md +120 -0
- package/skill/short-drama/reference/schema.md +435 -0
- package/skill/short-drama/reference/stage-art.md +56 -0
- package/skill/short-drama/reference/stage-characters.md +206 -0
- package/skill/short-drama/reference/stage-outline.md +103 -0
- package/skill/short-drama/reference/stage-script.md +113 -0
- package/skill/short-drama/reference/stage-storyboard.md +465 -0
- package/skill/short-drama/reference/workspace-layout.md +118 -0
- package/skill/short-drama/scripts/selftest.mjs +1411 -0
- package/skill/short-drama/scripts/short-drama.mjs +3371 -0
- package/skill/wji/SKILL.md +97 -22
- package/skill/wji/reference/canvas-recipes.md +16 -2
- package/skill/wji/reference/commands.md +15 -6
|
@@ -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.
|