@wji.ai/cli 0.2.0 → 0.3.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/package.json +1 -1
- package/skill/short-drama/SKILL.md +41 -24
- package/skill/short-drama/reference/canvas-recipes.md +47 -27
- package/skill/short-drama/reference/operations-cli.md +43 -0
- package/skill/short-drama/reference/operations-wji.md +82 -0
- package/skill/short-drama/reference/schema.md +1 -1
- package/skill/short-drama/reference/stage-art.md +2 -1
- package/skill/short-drama/reference/stage-storyboard.md +7 -4
- package/skill/short-drama/reference/workspace-layout.md +18 -6
- package/skill/short-drama/scripts/selftest.mjs +4 -0
- package/skill/short-drama/scripts/short-drama.mjs +30 -14
package/package.json
CHANGED
|
@@ -15,13 +15,24 @@ outline ──▶ characters ──▶ art ──▶ script (per episode) ──
|
|
|
15
15
|
story/ character/ location/ story/ shot/E01/ datasets
|
|
16
16
|
dataset datasets + prop/ screenplay + E01 canvas: keyframes → video
|
|
17
17
|
`characters` datasets + dataset
|
|
18
|
-
canvas `scenes`
|
|
18
|
+
canvas `scenes` + `props` canvases
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
`{baseDir}` = this directory.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
`{baseDir}` = this directory. Read `{baseDir}/reference/schema.md` before writing any JSON and
|
|
22
|
+
`{baseDir}/reference/workspace-layout.md` before touching WJI.
|
|
23
|
+
|
|
24
|
+
**First, decide which surface you are on — it decides how every operation below is spelled.**
|
|
25
|
+
|
|
26
|
+
- `short_drama_validate` is among your tools → you are in **WJI chat**. Read
|
|
27
|
+
`{baseDir}/reference/operations-wji.md`. Every `node {baseDir}/scripts/short-drama.mjs <cmd>` and
|
|
28
|
+
`wji <cmd>` line in this skill is the CLI spelling of an operation — translate it with the tables
|
|
29
|
+
there. There is no shell and no project directory.
|
|
30
|
+
- Otherwise you are in **Claude Code with the `wji` CLI**. Read
|
|
31
|
+
`{baseDir}/reference/operations-cli.md`; the commands in this skill are literal. The script is
|
|
32
|
+
`node {baseDir}/scripts/short-drama.mjs <cmd> …` (zero dependencies), and the `wji` skill governs
|
|
33
|
+
every CLI call (auth, confirmations, spending policy, model choice).
|
|
34
|
+
|
|
35
|
+
Either way, this skill only says **what to build**; the operations doc says how to say it.
|
|
25
36
|
|
|
26
37
|
**How the drama is written** comes from `{baseDir}/reference/craft-story.md` (the 短剧 method:
|
|
27
38
|
brief, three phases, 分集目录, 爽点 / 钩子 libraries, the episode formula, the character bible) and
|
|
@@ -68,9 +79,10 @@ ending), change the guideline first, then the episodes.
|
|
|
68
79
|
**Style**: `photoreal` for a live-action drama, `realistic` for semi-realistic painterly,
|
|
69
80
|
`ghibli` for hand-painted anime. **References** (default `sheets`): the video node references
|
|
70
81
|
the character/scene sheets directly and no storyboard images are generated — fewer images to
|
|
71
|
-
pay for and approve, and continuity comes from the sheets.
|
|
72
|
-
|
|
73
|
-
|
|
82
|
+
pay for and approve, and continuity comes from the sheets. Every mode works this way, h3
|
|
83
|
+
included (its sheets-mode prompt opens with an `@imageK is …` legend instead of the alignment
|
|
84
|
+
line). Choose `keyframes` (one generated image per cut) only when the user asks for it or a shot
|
|
85
|
+
has to be composed exactly — never switch a project to it on your own. **Duration** (default `auto`): the model matches the clip to the prompt; `fixed` sends
|
|
74
86
|
`-d` = Σ cuts and is forced on models without an auto duration.
|
|
75
87
|
|
|
76
88
|
**The camera package** is `params.camera` — body, lens family and working stop, default
|
|
@@ -166,8 +178,9 @@ Read `{baseDir}/reference/stage-art.md`.
|
|
|
166
178
|
1. Scenes: `summary`, 3–5 `anchors`, `lighting` states derived from the episodes the scene appears
|
|
167
179
|
in, empty-scene `image.*`; variants via `variantOf` + `changes`. Props (only narrative ones):
|
|
168
180
|
`scale`, `anchors`, `states`, white-background `image.*`. `validate --stage art`.
|
|
169
|
-
2. Sync to `location/` and `prop/` datasets; `scenes` canvas: one sheet node per scene
|
|
170
|
-
|
|
181
|
+
2. Sync to `location/` and `prop/` datasets; `scenes` canvas: one sheet node per scene (16:9),
|
|
182
|
+
lighting / variant nodes `--from` the parent sheet; `props` canvas (`wji canvas create props`):
|
|
183
|
+
one white-background sheet node per prop, state nodes `--from` it. Generate, inspect (no people in a
|
|
171
184
|
scene, no hands on a prop, one space across all panels), attach as `icon`, bind.
|
|
172
185
|
|
|
173
186
|
## Stage 4 — Script (per episode, batches of ≤3)
|
|
@@ -236,20 +249,24 @@ must be connected: pictures, then clips, then voices.
|
|
|
236
249
|
2. Sync: shot folder `shot/E0N`, one `shot` dataset per segment (`video_prompt` = the prompt,
|
|
237
250
|
content = `render segment E0N-01` — the 分镜 with one `### SHOT k` block per cut).
|
|
238
251
|
3. `E0N` canvas (`wji canvas create E0N`): display nodes for every sheet the episode uses
|
|
239
|
-
(`canvas add image --asset …`, keyed in `wji.displayNodes`)
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
character sheets, the right shot size, nobody extra in frame. Regenerate what is off, then
|
|
250
|
-
continue segment by segment. Keyframes for one segment are a batch (quote the total); each video
|
|
251
|
-
is a video (ask, or `-auto`). Stay under the queue cap (`wji` skill *Limits*): submit up to the
|
|
252
|
+
(`canvas add image --asset …`, keyed in `wji.displayNodes`), plus the voice display nodes of
|
|
253
|
+
whoever speaks. **No storyboard images are generated**: the video node per segment references
|
|
254
|
+
the sheets directly — `--from` the scene / character / prop display nodes then the voices, in
|
|
255
|
+
the order `prompt segment` prints the legend (the order *is* the numbering, so a reconnect must
|
|
256
|
+
redo the whole set). The video node takes the mode's model, `--resolution
|
|
257
|
+
params.videoResolution`, `--prompt-file`, and `-d` = Σ cut seconds **only in `fixed` duration
|
|
258
|
+
mode** — in `auto` leave `-d` off and let the model match the clip to the prompt.
|
|
259
|
+
4. Generate the clips segment by segment — each video is a video (ask, or `-auto`); download and
|
|
260
|
+
check the first one against the sheets (same world, same faces, right size, nobody extra)
|
|
261
|
+
before submitting the rest. Stay under the queue cap (`wji` skill *Limits*): submit up to the
|
|
252
262
|
cap, `wji history get <id> --wait`, continue.
|
|
263
|
+
|
|
264
|
+
*Only if the user set `references: keyframes`* (a shot that must be composed exactly): a keyframe
|
|
265
|
+
image node per cut first (`--from` the display nodes, ratio = `params.ratio`, prompt from
|
|
266
|
+
`prompt segment`), the video node `--from` the keyframes in cut order; generate the first
|
|
267
|
+
segment's keyframes only, inspect, then continue. Keyframes for one segment are a batch (quote
|
|
268
|
+
the total). Do not switch a project to keyframes on your own — it doubles the images to pay for
|
|
269
|
+
and approve.
|
|
253
270
|
5. **Soundtrack node** per segment (`params.audioTrack`, default `bed`): `prompt audio E0N-01`
|
|
254
271
|
derives a Seed Audio prompt from `segment.sound` — timecoded `[start s:end s]` ambience, sfx on
|
|
255
272
|
their cuts, music that enters / swells / ducks / falls silent at the cues you wrote on
|
|
@@ -257,7 +274,7 @@ must be connected: pictures, then clips, then voices.
|
|
|
257
274
|
credits a minute, no `--from`; `--full` adds the lines in the connected `@Audio1..3` voices).
|
|
258
275
|
Seed Audio is the voice-sample model, but it is an audio-scene generator: one prompt is a whole
|
|
259
276
|
track. Attach the asset to the shot dataset; bind `wji.nodes.track` / `wji.assets.track`.
|
|
260
|
-
6. After each clip: `fs set <shot dataset> --add-images <keyframes…>
|
|
277
|
+
6. After each clip: `fs set <shot dataset> --add-images <track> [<keyframes…>] --set primaryVideo=<clip>`;
|
|
261
278
|
bind asset ids. `manifest --ep N` must come back clean before you call the episode done.
|
|
262
279
|
|
|
263
280
|
- **A failed generation is not always a bad prompt.** `wan-3.0-ref` rejections from DashScope's
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Canvas recipes for short drama
|
|
2
2
|
|
|
3
|
+
**On WJI** (`canvas_*` tools, no shell): the layouts below are the contract — which node sits in
|
|
4
|
+
which row, and in what order the references are connected. Read them as written and substitute the
|
|
5
|
+
tool calls from `operations-wji.md`; `--from A B C` is `fromNodeIds: ['A','B','C']` and the array
|
|
6
|
+
order is the reference order. Everything about rows, `@imageN` order and prompts is identical.
|
|
7
|
+
|
|
3
8
|
Command sequences for the three canvas types. All ids are placeholders — take real ones from the
|
|
4
9
|
command output (`wji --json canvas add … | jq -r .id`) and `bind` them at once. `SD` below is
|
|
5
10
|
`node {baseDir}/scripts/short-drama.mjs`.
|
|
@@ -46,7 +51,7 @@ wji fs set <C01 dataset> --add-images <headshot> <sheet> <voice> --icon <headsho
|
|
|
46
51
|
Reference for the sheet prompt file: `@image1 is the face to match exactly — same features, same
|
|
47
52
|
hairstyle. ` + `image.sheet`.
|
|
48
53
|
|
|
49
|
-
## 2. `scenes` canvas
|
|
54
|
+
## 2. `scenes` canvas — one row per scene
|
|
50
55
|
|
|
51
56
|
```bash
|
|
52
57
|
wji canvas create scenes --json | jq -r .id # → SC; bind wji.canvases.scenes
|
|
@@ -54,18 +59,33 @@ wji canvas add title -c SC -t "S01 便利店店内" --at 0,0
|
|
|
54
59
|
wji canvas add image -c SC -n "S01 sheet" -m "<image model>" -r 16:9 --at 400,0 --prompt-file /tmp/S01-sheet.txt # → SS1
|
|
55
60
|
wji canvas add image -c SC -n "S01 lighting 监控回放绿" --from SS1 -m "<image model>" -r 16:9 \
|
|
56
61
|
-p "@image1 is this exact space; keep structure, materials and wear identical; change only the lighting: <lighting prompt>. Empty scene, no people, cinematic film still"
|
|
57
|
-
# props: same, white background, `handheld scale` etc. in the prompt
|
|
58
|
-
wji canvas add image -c SC -n "P01 sheet" -m "<image model>" -r 16:9 --at 400,700 --prompt-file /tmp/P01-sheet.txt
|
|
59
|
-
wji canvas add image -c SC -n "P01 state 接在主机上" --from <P01 sheet> -m "<image model>" -r 16:9 -p "@image1 is the prop to reproduce exactly. <state prompt>, pure white background, no people, no hands"
|
|
60
62
|
```
|
|
61
63
|
|
|
62
64
|
Variants: `--from` the parent scene's sheet node, prompt = parent master view + `changes` +
|
|
63
65
|
`keep the structure, materials and wear identical to @image1`.
|
|
64
66
|
|
|
65
|
-
After generation: inspect (no people
|
|
67
|
+
After generation: inspect (no people, one space across panels), attach
|
|
66
68
|
(`--add-images <sheet> <variants…> --icon <sheet>`), bind `scenes.S01.wji.nodes.sheet`,
|
|
67
69
|
`scenes.S01.wji.assets.sheet`, `scenes.S01.wji.assets.lighting.<state>`.
|
|
68
70
|
|
|
71
|
+
## 2b. `props` canvas — one row per prop
|
|
72
|
+
|
|
73
|
+
Props live on their own canvas: white background, `handheld scale` / `tabletop scale` /
|
|
74
|
+
`furniture scale` in the prompt, one **state** node per `states[]` entry referencing the sheet.
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
wji canvas create props --json | jq -r .id # → PC; bind wji.canvases.props
|
|
78
|
+
wji canvas add title -c PC -t "P01 旧监控硬盘" --at 0,0
|
|
79
|
+
wji canvas add image -c PC -n "P01 sheet" -m "<image model>" -r 16:9 --at 400,0 --prompt-file /tmp/P01-sheet.txt # → PS1
|
|
80
|
+
wji canvas add image -c PC -n "P01 state 接在主机上" --from PS1 -m "<image model>" -r 16:9 \
|
|
81
|
+
-p "@image1 is the prop to reproduce exactly. <state prompt>, pure white background, no people, no hands"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Inspect (no hands, no people, the same object in every panel), attach
|
|
85
|
+
(`--add-images <sheet> <states…> --icon <sheet>`), bind `props.P01.wji.nodes.sheet`,
|
|
86
|
+
`props.P01.wji.assets.sheet`, `props.P01.wji.assets.states.<state>`. On the episode canvas the
|
|
87
|
+
prop's sheet becomes a display node like any other (`wji.displayNodes["P01:sheet"]`).
|
|
88
|
+
|
|
69
89
|
## 3. `E01` canvas
|
|
70
90
|
|
|
71
91
|
```bash
|
|
@@ -77,23 +97,33 @@ wji canvas add image -c E1 -n "S02 sheet" --asset <S02 sheet asset> --at 500,0
|
|
|
77
97
|
wji canvas add audio -c E1 -n "C01 voice" --asset <C01 voice asset> --at 2000,0 # D_C01v (only if a voice exists)
|
|
78
98
|
wji canvas add dataset -c E1 --dataset <C01 dataset id> --at 0,300 # optional: the character card, for people reading the canvas
|
|
79
99
|
|
|
80
|
-
# row 1 — segment E01-01
|
|
81
|
-
SD prompt <project> segment E01-01
|
|
100
|
+
# row 1 — segment E01-01 (default: reference images → video, no storyboard images)
|
|
101
|
+
SD prompt <project> segment E01-01 > /tmp/E01-01-prompt.txt # the header line lists refs: in the order to connect them
|
|
82
102
|
wji canvas add title -c E1 -t "E01-01 · 雨棚 · 15 s" --at 0,700
|
|
83
|
-
wji canvas add
|
|
84
|
-
--prompt-file /tmp/E01-01-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
wji canvas add video -c E1 -n "E01-01" --from K1 K2 K3 K4 K5 D_C01v -m minimax-h3-ref -r 9:16 -d 15 \
|
|
88
|
-
--prompt-file /tmp/E01-01-prompt.txt --json | jq -r .id # → VID1
|
|
103
|
+
wji canvas add video -c E1 -n "E01-01" --from D_S02 D_C01 D_C03 D_P01 D_C03v D_C01v -m seedance-2.5-ref -r 9:16 \
|
|
104
|
+
--resolution 480p --prompt-file /tmp/E01-01-prompt.txt --json | jq -r .id # → VID1
|
|
105
|
+
# D_S02 D_C01 D_C03 D_P01 → @image1 … @image4 · D_C03v D_C01v → @audio1 @audio2 (pictures, then clips, then voices)
|
|
106
|
+
SD bind <project> --ep 1 storyboard.segments.E01-01.wji.nodes.video VID1
|
|
89
107
|
wji canvas add text -c E1 -n "E01-01 prompt" --text-file /tmp/E01-01-prompt.txt
|
|
90
108
|
wji canvas gen VID1 # prints the cost and refuses → tell the user → then --yes --wait (or -auto)
|
|
91
109
|
```
|
|
92
110
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
111
|
+
`wji canvas get VID1` must print `@image1 ← D_S02`, `@image2 ← D_C01` … exactly as the prompt's
|
|
112
|
+
legend says; fix with disconnect/connect if it does not. With `videoDuration: auto` (the default)
|
|
113
|
+
there is no `-d`; `minimax-h3-ref` has no auto duration, so h3 projects pass `-d` = Σ cut seconds.
|
|
114
|
+
|
|
115
|
+
**Only with `params.references: keyframes`** — one generated image per cut, referenced by the video
|
|
116
|
+
node instead of the sheets (an opt-in for shots that must be composed exactly; twice the images to
|
|
117
|
+
pay for and approve):
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
wji canvas add image -c E1 -n "E01-01 f1" --from D_S02 D_C01 D_C03 D_P01 -m "<image model>" -r 9:16 --at 400,700 \
|
|
121
|
+
--prompt-file /tmp/E01-01-f1.txt # → K1 … K5 (prompt = plan prefix + cut.frame)
|
|
122
|
+
SD bind <project> --ep 1 storyboard.segments.E01-01.wji.nodes.keyframes.0 K1
|
|
123
|
+
wji canvas gen K1 --yes --wait # first segment: generate, download, inspect, fix, then the rest
|
|
124
|
+
wji canvas add video -c E1 -n "E01-01" --from K1 K2 K3 K4 K5 D_C03v D_C01v -m minimax-h3-ref -r 9:16 -d 15 \
|
|
125
|
+
--resolution 768P --prompt-file /tmp/E01-01-prompt.txt --json | jq -r .id # → VID1
|
|
126
|
+
```
|
|
97
127
|
|
|
98
128
|
Video models, one per `params.video`: `h3` → `minimax-h3-ref`, `seedance` → `seedance-2.5-ref`,
|
|
99
129
|
`seedance2` → `seedance-2.0-ref`, `seedance2fast` → `seedance-2.0-fast-ref`, `wan` → `wan-3.0-ref`.
|
|
@@ -114,16 +144,6 @@ that character's lines to the label. Caps: 3 audio references on `minimax-h3-ref
|
|
|
114
144
|
pictures then voices, in the order to connect them; drop them only if the model shows no audio
|
|
115
145
|
reference support (`params.voiceSamples: false`).
|
|
116
146
|
|
|
117
|
-
Sheets mode (`params.references: sheets`, no keyframes — **the default**) — the video node's
|
|
118
|
-
`--from` are the sheet display nodes then the voice display nodes, in the order `prompt segment`
|
|
119
|
-
prints, and with `videoDuration: auto` there is no `-d`:
|
|
120
|
-
|
|
121
|
-
```bash
|
|
122
|
-
wji canvas add video -c E1 -n "E01-01" --from D_S01 D_C01 D_P01 D_C01_VOICE -m seedance-2.5-ref -r 9:16 \
|
|
123
|
-
--resolution 480p --prompt-file /tmp/E01-01-prompt.txt # resolution values are case-sensitive per model
|
|
124
|
-
# D_S01 D_C01 D_P01 → @image1 @image2 @image3 · D_C01_VOICE → @audio1
|
|
125
|
-
```
|
|
126
|
-
|
|
127
147
|
The segment's **soundtrack node** (Seed Audio; `params.audioTrack`, default `bed`) sits beside the
|
|
128
148
|
video node — no `--from` for a bed, the voice display nodes in speaking order for `--full`:
|
|
129
149
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Operations — Claude Code / `wji` CLI
|
|
2
|
+
|
|
3
|
+
This is the **CLI binding** of the operations the stage docs name. You are on this surface when you
|
|
4
|
+
have a shell and a project directory. If `short_drama_validate` is among your tools instead, you are
|
|
5
|
+
on WJI — read `operations-wji.md` and ignore this file.
|
|
6
|
+
|
|
7
|
+
`{baseDir}` = the skill directory. `{project}` = the project directory (the one holding
|
|
8
|
+
`project.json` and `episodes/E0N.json`). The engine is zero-dependency:
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
node {baseDir}/scripts/short-drama.mjs <cmd> {project} …
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## The engine
|
|
15
|
+
|
|
16
|
+
| Operation | Command |
|
|
17
|
+
| --- | --- |
|
|
18
|
+
| `init` | `short-drama.mjs init {project} --title "<t>" --episodes N --minutes M --genre "<g>" --register 台湾国语 [--adapt 抽核] [--ratio 9:16] [--video h3\|seedance\|seedance2\|seedance2fast\|wan] [--resolution 480P…] [--accent "<accent>"] [--style photoreal\|realistic\|ghibli] [--references sheets\|keyframes] [--duration auto\|fixed] [--look "<lens>"] [--lang zh]` |
|
|
19
|
+
| `validate` | `short-drama.mjs validate {project} [--stage <stage>] [--ep N]` — exits 1 on a hard failure, 0 on advisories alone |
|
|
20
|
+
| `checkup` | `short-drama.mjs checkup {project}` — same, but prints the passing and skipped rows too |
|
|
21
|
+
| `seed` | `short-drama.mjs seed {project} script --ep N [--force]` · `… seed {project} storyboard --ep N [--force]` |
|
|
22
|
+
| `render` | `short-drama.mjs render {project} outline \| guideline \| character C01 \| scene S01 \| prop P01 \| script --ep N \| dialogue --ep N \| segment E01-01` |
|
|
23
|
+
| `prompt` | `short-drama.mjs prompt {project} segment E01-01 [--json]` · `… prompt {project} audio E01-01 [--full\|--bed]` · `… prompt {project} voice C01` |
|
|
24
|
+
| `manifest` | `short-drama.mjs manifest {project} [--stage <stage>] [--ep N] [--json]` — exit 2 while anything is unbound |
|
|
25
|
+
| `bind` | `short-drama.mjs bind {project} [--ep N] <path> <value>` — record a WJI id in the JSON |
|
|
26
|
+
|
|
27
|
+
Read and write `project.json` / `episodes/E0N.json` with your ordinary file tools; `bind` exists only
|
|
28
|
+
so the WJI ids land on the right path without hand-editing.
|
|
29
|
+
|
|
30
|
+
## WJI, through the `wji` CLI
|
|
31
|
+
|
|
32
|
+
The `wji` skill governs auth, confirmations and spending. The calls this skill makes:
|
|
33
|
+
|
|
34
|
+
| Purpose | Command |
|
|
35
|
+
| --- | --- |
|
|
36
|
+
| preflight | `wji auth whoami --json`, `wji profile wallet` |
|
|
37
|
+
| workspace | `wji ws create "<title>" -r <ratio>` → `wji ws use <id>` |
|
|
38
|
+
| datasets | `wji dataset new … --app <appId>`, `wji fs set <id> --merge` (never `--content`), `--add-images` (never `--images` on an existing node) |
|
|
39
|
+
| canvases | `wji canvas create <name>` · `wji canvas add <kind> -c <cv> …` · `wji canvas connect <a> <b>` · `wji canvas get <node>` · `wji canvas gen <node> [--yes --wait]` · `wji canvas rm` |
|
|
40
|
+
| assets | `wji asset download <id> -o <dir>` — **look at what you generated** before building on it |
|
|
41
|
+
| history | `wji history get <id> --wait` |
|
|
42
|
+
|
|
43
|
+
Full canvas layouts, with the node rows and the reference order, are in `canvas-recipes.md`.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Operations — WJI chat (`short_drama_*` / `canvas_*` tools)
|
|
2
|
+
|
|
3
|
+
This is the **WJI binding** of the operations the stage docs name. You are on this surface when
|
|
4
|
+
`short_drama_validate` is among your tools: there is no shell, no project directory and no `wji`
|
|
5
|
+
command — everything below is a tool call. If you have a shell instead, read `operations-cli.md`.
|
|
6
|
+
|
|
7
|
+
Whenever a stage doc shows a `node …/short-drama.mjs <cmd>` or `wji <cmd>` line, translate it with
|
|
8
|
+
the tables here. The rules, the gates and the JSON schema are identical on both surfaces.
|
|
9
|
+
|
|
10
|
+
## Where the project lives
|
|
11
|
+
|
|
12
|
+
There is no `project.json` on disk. The project is a small set of datasets in the workspace's
|
|
13
|
+
**story** folder:
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
story/<title> · 短劇工程/
|
|
17
|
+
project ← the whole project.json, in content.shortDrama.data
|
|
18
|
+
E01, E02 … ← one per episode, the episodes/E0N.json
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`short_drama_create` makes them and returns a **`project` id**. Every other tool takes that id;
|
|
22
|
+
episodes are addressed with `ep: N`, never by node id.
|
|
23
|
+
|
|
24
|
+
## The engine
|
|
25
|
+
|
|
26
|
+
| Operation | Tool |
|
|
27
|
+
| --- | --- |
|
|
28
|
+
| `init` | `short_drama_create { workspace, title, episodes, minutes, genre, register, … }` → `{ projectId, project }` |
|
|
29
|
+
| read the JSON | `short_drama_get { project, ep?, section? }` — `section` trims a big branch (`characters`, `storyboard` …) |
|
|
30
|
+
| write the JSON | `short_drama_put { project, ep?, data }` for a whole document, or `{ project, ep?, path, value }` for one field (this is `bind`) |
|
|
31
|
+
| `validate` | `short_drama_validate { project, stage?, ep? }` |
|
|
32
|
+
| `checkup` | `short_drama_validate { project, all: true }` |
|
|
33
|
+
| `seed` | `short_drama_seed { project, what: 'script' \| 'storyboard', ep, force? }` |
|
|
34
|
+
| `render` | `short_drama_render { project, what, ep?, id? }` → markdown |
|
|
35
|
+
| `prompt` | `short_drama_prompt { project, what: 'segment' \| 'audio' \| 'voice', id, mode? }` |
|
|
36
|
+
| `manifest` | `short_drama_manifest { project, stage?, ep? }` |
|
|
37
|
+
|
|
38
|
+
**`short_drama_put` validates as it writes** and returns the gates for the stage you touched — read
|
|
39
|
+
that result instead of calling `short_drama_validate` again. A turn is capped at 20 tool calls, so
|
|
40
|
+
write a whole document at a time rather than field by field.
|
|
41
|
+
|
|
42
|
+
## WJI
|
|
43
|
+
|
|
44
|
+
| Purpose | Tool |
|
|
45
|
+
| --- | --- |
|
|
46
|
+
| workspace | `workspace_search`, `workspace_get`, `workspace_create`, `workspace_add_app` |
|
|
47
|
+
| semantic folders | `resolve_dataset_folder { id, key: 'story' \| 'character' \| 'location' \| 'prop' \| 'shot' }` |
|
|
48
|
+
| datasets | `fsnode_create_file { parent, name, dataset: true, datasetType, description, content, tags }`, `fsnode_update`, `fsnode_upsert_segment` |
|
|
49
|
+
| assets | `asset_get`, `asset_check_status`, `asset_create_from_url` |
|
|
50
|
+
| canvases | the `canvas_*` tools below |
|
|
51
|
+
|
|
52
|
+
## Canvases
|
|
53
|
+
|
|
54
|
+
Every canvas tool takes an explicit `canvas` id, so you can build the `characters`, `scenes`, `props`
|
|
55
|
+
and per-episode canvases in one conversation. The layouts — which node in which row, and in what
|
|
56
|
+
order to connect the references — are in `canvas-recipes.md`; read the CLI recipe there and
|
|
57
|
+
substitute:
|
|
58
|
+
|
|
59
|
+
| `canvas-recipes.md` shows | call |
|
|
60
|
+
| --- | --- |
|
|
61
|
+
| `wji canvas create <name>` | `canvas_create { workspace, name }` → canvas id |
|
|
62
|
+
| `wji canvas add <kind> -c CV -n "<name>" --at x,y …` | `canvas_create_node { canvas, kind, name, position: {x, y}, … }` |
|
|
63
|
+
| `--prompt-file /tmp/x.txt` / `-p "<prompt>"` | `generate: { prompt, model, ratio, resolution, duration }` on `canvas_create_node` |
|
|
64
|
+
| `--asset <id>` (a display node) | `assetId` on `canvas_create_node` |
|
|
65
|
+
| `--from A B C` | `fromNodeIds: ['A','B','C']` — **the array order is the reference order** |
|
|
66
|
+
| `wji canvas connect <a> <b>` | `canvas_connect_nodes { canvas, fromId, toId }` |
|
|
67
|
+
| `wji canvas get <node>` | `canvas_get_node { canvas, id }` — references, label, status, asset |
|
|
68
|
+
| `wji canvas gen <node>` (cost preview) | `canvas_generate { canvas, nodeIds, estimateOnly: true }` |
|
|
69
|
+
| `wji canvas gen <node> --yes --wait` | `canvas_generate { canvas, nodeIds, wait: true }` |
|
|
70
|
+
| `wji canvas rm` | `canvas_delete_nodes { canvas, ids }` |
|
|
71
|
+
|
|
72
|
+
Reference labels (`@image1`, `@audio1` …) are numbered per type in connection order, exactly as the
|
|
73
|
+
canvas UI numbers them. After connecting, call `canvas_get_node` and check the labels match what the
|
|
74
|
+
prompt names **before** generating — a mislabelled reference is a wasted generation.
|
|
75
|
+
|
|
76
|
+
**Spending.** Generation costs the user credits. Call `canvas_generate` with `estimateOnly: true`
|
|
77
|
+
first, tell the user the cost, and only generate for real once they say yes. The `wji` skill's
|
|
78
|
+
spending policy applies here in full.
|
|
79
|
+
|
|
80
|
+
**In a canvas chat** (the chat panel beside an open canvas) the browser supplies its own `canvas_*`
|
|
81
|
+
tools, which always act on the canvas the user is looking at and take no `canvas` argument. A short
|
|
82
|
+
drama needs several canvases, so do that work in a workspace chat; in a canvas chat, say so.
|
|
@@ -212,7 +212,7 @@ prompt bans hands**). At most `thresholds.maxProps` (8).
|
|
|
212
212
|
{
|
|
213
213
|
"endpoint": "https://api.wji.ai", "workspaceId": "…",
|
|
214
214
|
"datasetFolders": { "story": "…", "character": "…", "location": "…", "prop": "…", "shot": "…" },
|
|
215
|
-
"canvases": { "characters": "…", "scenes": "…", "episodes": { "1": "…" } },
|
|
215
|
+
"canvases": { "characters": "…", "scenes": "…", "props": "…", "episodes": { "1": "…" } },
|
|
216
216
|
"outlineDatasetId": "…", "guidelineDatasetId": "…"
|
|
217
217
|
}
|
|
218
218
|
```
|
|
@@ -40,7 +40,8 @@ node {baseDir}/scripts/short-drama.mjs render <project> scene S01 > /tmp/S01.md
|
|
|
40
40
|
wji fs set <id> --merge content=@/tmp/S01.md --merge image_prompt=@/tmp/S01-sheet.txt --merge metadata.shortDrama=@/tmp/S01.json
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
`scenes` canvas (`canvas-recipes.md` §2)
|
|
43
|
+
`scenes` canvas (`canvas-recipes.md` §2) for the scenes and a separate `props` canvas (§2b) for
|
|
44
|
+
the props: one 16:9 **sheet** node per scene and per prop; per
|
|
44
45
|
extra lighting state / prop state one node `--from` the sheet (`@image1 is this exact space —
|
|
45
46
|
keep structure, materials and wear identical; change only: <state prompt>`); variants `--from` the
|
|
46
47
|
parent scene's sheet with the `changes` text. Sheets are single images (run), a whole batch of
|
|
@@ -46,8 +46,8 @@ Three worksheets, all deleted once the segments are written:
|
|
|
46
46
|
needs — a 15 s model wants 9–15 s runs, a 30 s model can hold a whole exchange in one.
|
|
47
47
|
2. **Cut = 2–5 s**, aim at 3. A segment holds 3–5 cuts. The dialogue of the beats a cut claims
|
|
48
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**
|
|
50
|
-
|
|
49
|
+
3. **`frame` per cut** — the shot's description inside the segment prompt (by default no image
|
|
50
|
+
is generated from it; only with `references: keyframes` does it also become the keyframe image's
|
|
51
51
|
prompt): an English prompt for that moment — composition, this
|
|
52
52
|
moment's positions and poses (already on the boat / still on the pier — the model trusts the
|
|
53
53
|
picture over the text), the size phrase (`wide shot`, `close-up`…), the style's
|
|
@@ -363,7 +363,9 @@ sheets, put the people in a `Cast:` line of the prompt; the face will drift betw
|
|
|
363
363
|
A look that must hold across every clip — lens, depth of field, foreground blur — goes in
|
|
364
364
|
`params.lookPhrase`, which every segment prompt must carry verbatim.
|
|
365
365
|
|
|
366
|
-
## Keyframes
|
|
366
|
+
## Keyframes — only with `params.references: keyframes`
|
|
367
|
+
|
|
368
|
+
Not the default. Read this section only when the user has chosen keyframes for a project.
|
|
367
369
|
|
|
368
370
|
Each keyframe is an image node whose references are the **sheet display nodes** on the episode
|
|
369
371
|
canvas, in the order the reference plan prints (environment first, then each person, then props),
|
|
@@ -378,7 +380,8 @@ every frame.
|
|
|
378
380
|
|
|
379
381
|
**First segment first.** Generate its keyframes, download them, check against the sheets (same
|
|
380
382
|
world, same faces, right size, nobody extra), fix, then continue. An episode is 30–40 keyframes;
|
|
381
|
-
a wrong direction costs the whole batch
|
|
383
|
+
a wrong direction costs the whole batch — which is why this is opt-in and the default references
|
|
384
|
+
the sheets directly.
|
|
382
385
|
|
|
383
386
|
## Soundtrack node — Seed Audio is not a TTS
|
|
384
387
|
|
|
@@ -18,6 +18,7 @@ workspace "<title>" wji ws create "<title>" -r <params.rati
|
|
|
18
18
|
└── canvases/
|
|
19
19
|
├── characters wji canvas create characters
|
|
20
20
|
├── scenes wji canvas create scenes
|
|
21
|
+
├── props wji canvas create props
|
|
21
22
|
├── E01, E02 … wji canvas create E01
|
|
22
23
|
└── canvas-001 (the default one — leave it)
|
|
23
24
|
```
|
|
@@ -79,29 +80,40 @@ the headshot + sheet as image references and the voice as audio.
|
|
|
79
80
|
Bind the node ids (`wji.nodes.*`) and, after generation, the asset ids
|
|
80
81
|
(`wji.assets.*`); then attach the assets to the dataset.
|
|
81
82
|
|
|
82
|
-
### `scenes` canvas — one row per scene
|
|
83
|
+
### `scenes` canvas — one row per scene
|
|
83
84
|
|
|
84
85
|
```
|
|
85
86
|
[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
87
|
```
|
|
88
88
|
|
|
89
89
|
Variants (`variantOf`) reference the parent scene's sheet node as `@image1`.
|
|
90
90
|
|
|
91
|
+
### `props` canvas — one row per prop
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
[title P01] → [image* sheet 16:9 white bg] → [image* state:<state> @image1=sheet] …
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Bound as `wji.canvases.props`; `manifest` asks for it as soon as the project has a prop.
|
|
98
|
+
|
|
91
99
|
### `E<nn>` canvas — references on top, one block per segment
|
|
92
100
|
|
|
93
101
|
```
|
|
94
102
|
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]
|
|
103
|
+
row 1 [title E01-01] → [video* E01-01 (@image1..N = the sheet display nodes, @audio1..M = the voices of whoever speaks)] [audio* E01-01 track] [text prompt]
|
|
96
104
|
row 2 [title E01-02] …
|
|
97
105
|
```
|
|
98
106
|
|
|
107
|
+
By default the video node references the sheets directly — **no storyboard images**. Only with
|
|
108
|
+
`params.references: keyframes` does row 1 gain `[image* f1] [image* f2] …` between the title and the
|
|
109
|
+
video node, and the video node then references those instead of the sheets.
|
|
110
|
+
|
|
99
111
|
- 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
112
|
- 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
|
-
-
|
|
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.
|
|
113
|
+
- Video: `canvas add video -n "E01-01" --from <sheet display nodes in legend order> <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
114
|
- Prompt text node: `canvas add text -n "E01-01 prompt" --text-file prompt.txt` beside the video node, for people reading the canvas.
|
|
104
|
-
-
|
|
115
|
+
- Keyframes (opt-in only): `canvas add image -n "E01-01 f<k>" --from <sheet display nodes> -m <image model> -r <params.ratio> -p "<plan prefix + frame>"`, and the video node then `--from f1…fN` instead of the sheets.
|
|
116
|
+
- Shot dataset: `dataset new <shot/E01 folder> "E01-01" -t shot -d "<one line>"`, `--set video_prompt=@prompt.txt`, images = the soundtrack (and keyframes, if any), `primaryVideo` = the clip.
|
|
105
117
|
|
|
106
118
|
Placement: `canvas add` puts a node right of its `--from` nodes; pass `--at x,y`
|
|
107
119
|
for the row starts (title nodes) so blocks do not overlap. Take ids from the
|
|
@@ -1359,6 +1359,10 @@ const breaks = (gateId, project, episode, needle) => {
|
|
|
1359
1359
|
const items = manifest(PROJECT, { 1: E01 });
|
|
1360
1360
|
ok(items.some((i) => i.item === 'wji.workspaceId'), 'manifest asks for the workspace first');
|
|
1361
1361
|
ok(items.some((i) => i.item === 'characters.C01.wji.datasetId'), 'manifest lists character datasets');
|
|
1362
|
+
ok(items.some((i) => i.item === 'wji.canvases.scenes') && items.some((i) => i.item === 'wji.canvases.props'), 'manifest asks for a scenes canvas and a separate props canvas');
|
|
1363
|
+
ok(items.find((i) => i.item === 'props.P01.wji.nodes.sheet').hint.includes('-c <props>'), 'prop sheets go on the props canvas');
|
|
1364
|
+
const noProps = clone(PROJECT); noProps.props = []; noProps.outline.episodes.forEach((e) => (e.propIds = []));
|
|
1365
|
+
ok(!manifest(noProps, {}).some((i) => i.item === 'wji.canvases.props'), 'no props, no props canvas');
|
|
1362
1366
|
ok(items.some((i) => i.item === 'wji.displayNodes.S02:sheet'), 'manifest lists display nodes the episode needs');
|
|
1363
1367
|
ok(items.some((i) => i.item === 'storyboard.segments.E01-01.wji.nodes.keyframes.4'), 'manifest lists every keyframe node');
|
|
1364
1368
|
ok(items.some((i) => i.item === 'storyboard.segments.E01-05.wji.nodes.video'), 'manifest lists video nodes');
|
|
@@ -2975,7 +2975,9 @@ export function manifest(project, episodes, opts = {}) {
|
|
|
2975
2975
|
}
|
|
2976
2976
|
}
|
|
2977
2977
|
if (stages.includes('art')) {
|
|
2978
|
-
if ((project.scenes || []).length
|
|
2978
|
+
if ((project.scenes || []).length && !w.canvases?.scenes) add('art', 'canvas', 'wji.canvases.scenes', 'wji canvas create scenes');
|
|
2979
|
+
// Props get their own canvas: white-background sheets and state variants, one row per prop.
|
|
2980
|
+
if ((project.props || []).length && !w.canvases?.props) add('art', 'canvas', 'wji.canvases.props', 'wji canvas create props');
|
|
2979
2981
|
for (const s of project.scenes || []) {
|
|
2980
2982
|
const sw = s.wji || {};
|
|
2981
2983
|
if (!sw.datasetId) add('art', s.id, `scenes.${s.id}.wji.datasetId`, `wji dataset new <location> "${s.name}" -t location`);
|
|
@@ -2985,7 +2987,7 @@ export function manifest(project, episodes, opts = {}) {
|
|
|
2985
2987
|
for (const p of project.props || []) {
|
|
2986
2988
|
const pw = p.wji || {};
|
|
2987
2989
|
if (!pw.datasetId) add('art', p.id, `props.${p.id}.wji.datasetId`, `wji dataset new <prop> "${p.name}" -t prop`);
|
|
2988
|
-
if (!pw.nodes?.sheet) add('art', p.id, `props.${p.id}.wji.nodes.sheet`, `wji canvas add image -c <
|
|
2990
|
+
if (!pw.nodes?.sheet) add('art', p.id, `props.${p.id}.wji.nodes.sheet`, `wji canvas add image -c <props> -n "${p.id} sheet" -r 16:9`);
|
|
2989
2991
|
else if (!pw.assets?.sheet) add('art', p.id, `props.${p.id}.wji.assets.sheet`, `wji canvas gen ${pw.nodes.sheet} → asset`);
|
|
2990
2992
|
}
|
|
2991
2993
|
}
|
|
@@ -3156,24 +3158,41 @@ function positional(args) {
|
|
|
3156
3158
|
return out;
|
|
3157
3159
|
}
|
|
3158
3160
|
|
|
3159
|
-
|
|
3161
|
+
/**
|
|
3162
|
+
* The gate report as text. One writer for every surface: the CLI prints it, the
|
|
3163
|
+
* WJI tool returns it. `all` keeps the passing and skipped rows (checkup).
|
|
3164
|
+
*/
|
|
3165
|
+
export function formatGates(results, { all, stage } = {}) {
|
|
3166
|
+
const lines = [];
|
|
3160
3167
|
let failed = 0;
|
|
3161
3168
|
let notes = 0;
|
|
3162
3169
|
for (const r of results) {
|
|
3163
|
-
if (r.skipped) { if (all)
|
|
3170
|
+
if (r.skipped) { if (all) lines.push(` – ${r.stage}/${r.id} · ${r.label} (skipped: ${r.skipped})`); continue; }
|
|
3164
3171
|
if (r.advisory) {
|
|
3165
3172
|
const warnings = r.warnings || [];
|
|
3166
3173
|
notes += warnings.length;
|
|
3167
|
-
if (all || warnings.length)
|
|
3168
|
-
for (const w of warnings)
|
|
3174
|
+
if (all || warnings.length) lines.push(` ${warnings.length ? '⚠' : '✓'} ${r.stage}/${r.id} · ${r.label} (advisory)`);
|
|
3175
|
+
for (const w of warnings) lines.push(` ⚠ ${w}`);
|
|
3169
3176
|
continue;
|
|
3170
3177
|
}
|
|
3171
3178
|
const ok = !r.failures.length;
|
|
3172
3179
|
if (!ok) failed += 1;
|
|
3173
|
-
if (all || !ok)
|
|
3174
|
-
for (const f of r.failures)
|
|
3180
|
+
if (all || !ok) lines.push(` ${ok ? '✓' : '✗'} ${r.stage}/${r.id} · ${r.label}`);
|
|
3181
|
+
for (const f of r.failures) lines.push(` - ${f}`);
|
|
3175
3182
|
}
|
|
3176
|
-
|
|
3183
|
+
const hard = results.filter((r) => !r.skipped && !r.advisory).length;
|
|
3184
|
+
const advisory = results.filter((r) => !r.skipped && r.advisory).length;
|
|
3185
|
+
const tail = advisory ? `, ${advisory} advisory (${notes} note${notes === 1 ? '' : 's'})` : '';
|
|
3186
|
+
const summary = failed
|
|
3187
|
+
? `✗ ${failed}/${hard} gate(s) failed${tail}`
|
|
3188
|
+
: `✓ ${hard} gate(s) passed${stage ? ` (through ${stage})` : ''}${tail}`;
|
|
3189
|
+
return { lines, text: [...lines, summary].join('\n'), summary, failed, notes, hard, advisory };
|
|
3190
|
+
}
|
|
3191
|
+
|
|
3192
|
+
function printGates(results, opts) {
|
|
3193
|
+
const out = formatGates(results, opts);
|
|
3194
|
+
for (const line of out.lines) console.log(line);
|
|
3195
|
+
return out;
|
|
3177
3196
|
}
|
|
3178
3197
|
|
|
3179
3198
|
function main(argv) {
|
|
@@ -3218,11 +3237,8 @@ function main(argv) {
|
|
|
3218
3237
|
const { project, episodes } = loadProject(input, { eps });
|
|
3219
3238
|
const ctx = makeContext(project, episodes, { eps });
|
|
3220
3239
|
const results = runGates(ctx, { stage });
|
|
3221
|
-
const { failed,
|
|
3222
|
-
|
|
3223
|
-
const advisory = results.filter((r) => !r.skipped && r.advisory).length;
|
|
3224
|
-
const tail = advisory ? `, ${advisory} advisory (${notes} note${notes === 1 ? '' : 's'})` : '';
|
|
3225
|
-
console.log(failed ? `✗ ${failed}/${hard} gate(s) failed${tail}` : `✓ ${hard} gate(s) passed${stage ? ` (through ${stage})` : ''}${tail}`);
|
|
3240
|
+
const { failed, summary } = printGates(results, { all: cmd === 'checkup', stage });
|
|
3241
|
+
console.log(summary);
|
|
3226
3242
|
return cmd === 'validate' && failed ? 1 : 0;
|
|
3227
3243
|
}
|
|
3228
3244
|
|