@wji.ai/cli 0.2.0 → 0.4.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.
@@ -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` canvas
18
+ canvas `scenes` + `props` canvases
19
19
  ```
20
20
 
21
- `{baseDir}` = this directory. Script: `node {baseDir}/scripts/short-drama.mjs <cmd> …` (zero
22
- dependencies). Read `{baseDir}/reference/schema.md` before writing any JSON and
23
- `{baseDir}/reference/workspace-layout.md` before touching WJI. The `wji` skill governs every CLI
24
- call (auth, confirmations, spending policy, model choice) — this skill only says what to build.
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. Choose `keyframes` (one generated
72
- image per cut) only when a shot has to be composed exactly; h3 needs it for its keyframe-pinned
73
- prompt. **Duration** (default `auto`): the model matches the clip to the prompt; `fixed` sends
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 / prop
170
- (16:9), variant and state nodes `--from` the parent sheet. Generate, inspect (no people in a
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`). Then, in the default `sheets` mode,
240
- the video node per segment straight from those display nodes (`--from` them in the order
241
- `prompt segment` prints the legend — the order is the legend, so a reconnect must redo the whole
242
- set). With `references: keyframes`, first a keyframe node per cut (`--from` the display nodes,
243
- ratio = `params.ratio`), then the video node `--from` the keyframes in cut order.
244
- Either way the video node takes the mode's model, `--resolution params.videoResolution`,
245
- `--prompt-file`, and `-d` = Σ cut seconds **only in `fixed` duration mode** — in `auto` leave
246
- `-d` off and let the model match the clip to the prompt.
247
- 4. In `sheets` mode go straight to generating the clips. With `keyframes`, **generate the first
248
- segment's keyframes only**, download, check: same world as the scene sheet, same faces as the
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…> <track> --set primaryVideo=<clip>`;
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 / hands, one space across panels), attach
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 # reference plan per keyframe + video prompt skeleton
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 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 \
84
- --prompt-file /tmp/E01-01-f1.txt # → K1 … K5
85
- SD bind <project> --ep 1 storyboard.segments.E01-01.wji.nodes.keyframes.0 K1
86
- wji canvas gen K1 --yes --wait # first segment: generate, download, inspect, fix, then the rest of the segment
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
- The keyframe prompt file is the plan's prefix (`@image1 is the environment …; @image2 is the
94
- character sheet of one person …`) followed by the cut's `frame`. Reference order in `--from` must
95
- match the plan's labels — `wji canvas get K1` shows `@image1 ← D_S02` etc.; fix with
96
- disconnect/connect if it does not.
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): one 16:9 **sheet** node per scene and per prop; per
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** (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
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 on WJI
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, then one per prop
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] [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]
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
- - 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.
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
- - Shot dataset: `dataset new <shot/E01 folder> "E01-01" -t shot -d "<one line>"`, `--set video_prompt=@prompt.txt`, images = keyframes, `primaryVideo` = the clip.
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 + (project.props || []).length && !w.canvases?.scenes) add('art', 'canvas', 'wji.canvases.scenes', 'wji canvas create scenes');
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 <scenes> -n "${p.id} sheet" -r 16:9`);
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
- function printGates(results, { all }) {
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) console.log(` – ${r.stage}/${r.id} · ${r.label} (skipped: ${r.skipped})`); continue; }
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) console.log(` ${warnings.length ? '⚠' : '✓'} ${r.stage}/${r.id} · ${r.label} (advisory)`);
3168
- for (const w of warnings) console.log(` ⚠ ${w}`);
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) console.log(` ${ok ? '✓' : '✗'} ${r.stage}/${r.id} · ${r.label}`);
3174
- for (const f of r.failures) console.log(` - ${f}`);
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
- return { failed, notes };
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, notes } = printGates(results, { all: cmd === 'checkup' });
3222
- const hard = results.filter((r) => !r.skipped && !r.advisory).length;
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
 
@@ -302,6 +302,15 @@ System keys currently include `image`, `document`, `workspace-dataset`,
302
302
  When authoring, edit a pulled folder and `skill push` it again, or
303
303
  `skill update <id> --dir ./my-skill` to replace its files.
304
304
 
305
+ **Crews** are skill packs with agent seats (a `crew.json` + `agents/<seat>.md`
306
+ next to `skills/`). `wji crew install <name> --for claude|codex|openclaw|grok|all`
307
+ installs one into the host (Claude Code: `~/.claude/skills` + `~/.claude/agents`;
308
+ Codex: `~/.agents/skills` + `~/.codex/agents/*.toml`; OpenClaw: workspaces under
309
+ `~/.openclaw/workspaces/<crew>/` plus a JSON5 fragment to merge by hand; Grok bot:
310
+ an import zip). `--dry-run` shows the plan, `--from <dir|zip|url>` bypasses the
311
+ registry, `wji crew status` / `uninstall` track what was written, and
312
+ `wji crew check <dir>` validates a pack. The `short-drama` crew is the known one.
313
+
305
314
  Bundle limits: at most 200 markdown files, 2MB per file, 20MB in total and a
306
315
  5MB zip; 50 user skills per account. `skill update --dir` sends files in the
307
316
  request body, where each file is capped far lower, at **200,000 characters** —
@@ -332,12 +341,17 @@ open). Every `wji canvas` command then targets that canvas; pass
332
341
  ```bash
333
342
  wji canvas status # linked canvas + selected nodes (ids, asset URLs, settings)
334
343
  wji canvas nodes # every node; * marks generate nodes
344
+ wji canvas nodes --tag C01 # nodes with the color tag labelled C01 (or --tag red, --untagged)
345
+ wji canvas nodes -n '^C0[1-3] ' # nodes whose name matches a regex (case-insensitive); combines with --tag/-k
346
+ wji canvas tags # color tags on the canvas: color, label, node count
335
347
  wji canvas get <nodeId> # full detail incl. references
336
348
  wji asset download <assetId…> -o <dir> # then look at the images yourself
337
349
 
338
350
  wji canvas create E01 -w <workspaceId> # new empty canvas; then --canvas <id>
339
351
  wji canvas add video --from <imgNode> <imgNode> -m <model> -r 16:9 -d 8 -p "@image1 … @image2 …"
340
352
  wji canvas add text -t "shot list…" --at 0,600
353
+ wji canvas add text --text-file prompt.txt --plain --size 600,900 # plain text keeps its line breaks; node 600×900
354
+ wji canvas set <nodeId> --text-file notes.md --size 520,700 # replace the text, resize
341
355
  wji canvas add image --asset <assetId> -n "Ada sheet" # display node for an existing asset
342
356
  wji canvas add dataset --dataset <fsnodeId> # card bound to a workspace dataset
343
357
  wji canvas set <nodeId> -p "new prompt" # merges: other settings stay
@@ -387,6 +401,10 @@ wji canvas rm <nodeId…> # destructive — see Rules
387
401
  - Take new ids from the command's own output (`wji --json canvas add … | jq
388
402
  -r .id`), not by searching names — names repeat on a canvas. In zsh a
389
403
  variable holding several ids is not word-split; pass the ids literally.
404
+ - Text, title and comment nodes are **Markdown** on the web: a single line break
405
+ joins the lines into one paragraph (the CLI warns). For prompts and other plain
406
+ text pass `--plain` (line breaks become Markdown hard breaks); `--size w,h`
407
+ sets the node's box so a long prompt is readable without scrolling.
390
408
  - Changes show up live in the user's open canvas; there's no need to ask them
391
409
  to reload.
392
410
  - `canvas gen` sends exactly what the canvas's own Generate button sends