@wji.ai/cli 0.7.1 → 0.8.1

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/dist/index.js CHANGED
@@ -292,6 +292,10 @@ var MIME_TYPES = {
292
292
  pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation"
293
293
  };
294
294
  var mimeTypeOf = (filename) => MIME_TYPES[filename.split(".").pop()?.toLowerCase() || ""] || "application/octet-stream";
295
+ function clientVersion(version, env = process.env) {
296
+ const agent = (env.WJI_AGENT || "").trim() || (env.CLAUDECODE === "1" ? "claude-code" : "");
297
+ return agent ? `${version}; agent=${agent.replace(/[^\w.@-]/g, "_").slice(0, 40)}` : version;
298
+ }
295
299
  var ApiError = class extends Error {
296
300
  constructor(message, status, path, body) {
297
301
  super(message);
@@ -346,6 +350,8 @@ var ApiClient = class {
346
350
  headers(opts) {
347
351
  const headers = {
348
352
  Accept: "application/json",
353
+ "X-Client": this.spec.client ?? "cli",
354
+ "X-Client-Version": clientVersion(this.spec.version),
349
355
  "X-Locale": this.globals.locale || readEnv(this.spec, "LOCALE") || this.profile.locale || "cht"
350
356
  };
351
357
  if (!opts.anonymous) headers.Authorization = `Bearer ${this.requireToken()}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wji.ai/cli",
3
- "version": "0.7.1",
3
+ "version": "0.8.1",
4
4
  "description": "Command line interface for the WJI AI platform",
5
5
  "license": "UNLICENSED",
6
6
  "author": "WJI AI",
@@ -30,17 +30,16 @@
30
30
  "engines": {
31
31
  "node": ">=20"
32
32
  },
33
- "scripts": {
34
- "build": "tsup && chmod +x dist/index.js",
35
- "dev": "tsup --watch",
36
- "typecheck": "tsc -p tsconfig.json --noEmit",
37
- "start": "node dist/index.js",
38
- "prepublishOnly": "pnpm run build"
39
- },
40
33
  "dependencies": {
41
34
  "commander": "^12.1.0"
42
35
  },
43
36
  "publishConfig": {
44
37
  "access": "public"
38
+ },
39
+ "scripts": {
40
+ "build": "tsup && chmod +x dist/index.js",
41
+ "dev": "tsup --watch",
42
+ "typecheck": "tsc -p tsconfig.json --noEmit",
43
+ "start": "node dist/index.js"
45
44
  }
46
- }
45
+ }
@@ -159,6 +159,30 @@ write JSON → validate --stage <stage> → fix until green → render → sync
159
159
  approving a sheet or moving to the next segment. The reference-repo lesson: one wrong sheet
160
160
  corrupts every shot after it.
161
161
 
162
+ ## Production dashboard
163
+
164
+ Generate the dashboard whenever the user wants to review the project, and refresh it after a
165
+ stage or episode is locked:
166
+
167
+ ```bash
168
+ node {baseDir}/scripts/short-drama.mjs dashboard {project}
169
+ # custom location, and a folder of downloaded WJI images:
170
+ node {baseDir}/scripts/short-drama.mjs dashboard {project} \
171
+ --output {project}/dashboard.html --assets {project}/assets --force
172
+ ```
173
+
174
+ The output is one static HTML file: production totals and gate health, brief and guideline,
175
+ episode directory, character bibles, scene / prop sheets, scripts, cut timing and storyboard
176
+ plans. It does not call WJI and costs no credits. Every character, scene, prop and storyboard
177
+ slot keeps an image placeholder until a picture or video is bound. A bound value may be a WJI
178
+ server URL (`https://…` image or video) or a local file: set `image.url` / `video.url`, or
179
+ `wji.assets.sheet|headshot|keyframe|video`, to that URL or to a path relative to the project
180
+ (or an absolute path). Files whose name contains the entity or asset id are also picked up from
181
+ `{project}/assets`, `{project}/output`, the project tree, or `--assets <dir>`. A video URL or
182
+ `.mp4` / `.webm` / `.mov` file renders as a player; if the address fails to load, the
183
+ placeholder stays. Open `dashboard.html` in a browser. Do not paste the HTML into a dataset or
184
+ upload it as an asset.
185
+
162
186
  ## Stage 1 — Outline
163
187
 
164
188
  Read `{baseDir}/reference/stage-outline.md`.
@@ -211,7 +235,7 @@ Read `{baseDir}/reference/stage-characters.md`.
211
235
  sheet prompt prefixed with `@image1 is the face to match`), and a **voice** audio node
212
236
  (`seed-audio-1.0`, `--from` the headshot — the model reads the face and invents a voice for it;
213
237
  the composed prompt asks for a dry voice, because the same model also makes rooms, scores and
214
- sound effects — that is what its stage-5 soundtrack nodes are for;
238
+ sound effects — that is what the episode score over the edit is for;
215
239
  `prompt voice C01` composes the text: a parenthetical carrying age, identity, personality,
216
240
  timbre, pitch, pace, accent and emotion, then the acted line).
217
241
  Recipes in `reference/canvas-recipes.md`.
@@ -345,15 +369,27 @@ must be connected: pictures, then clips, then voices.
345
369
  segment's keyframes only, inspect, then continue. Keyframes for one segment are a batch (quote
346
370
  the total). Do not switch a project to keyframes on your own — it doubles the images to pay for
347
371
  and approve.
348
- 5. **Soundtrack node** per segment (`params.audioTrack`, default `bed`): `prompt audio E0N-01`
349
- derives a Seed Audio prompt from `segment.sound` — timecoded `[start s:end s]` ambience, sfx on
350
- their cuts, music that enters / swells / ducks / falls silent at the cues you wrote on
351
- `sound.music.cues` — and `canvas add audio -m seed-audio-1.0 --prompt-file …` makes it (~0.15
352
- credits a minute, no `--from`; `--full` adds the lines in the connected `@Audio1..3` voices).
353
- Seed Audio is the voice-sample model, but it is an audio-scene generator: one prompt is a whole
354
- track. Attach the asset to the shot dataset; bind `wji.nodes.track` / `wji.assets.track`.
355
- 6. After each clip: `fs set <shot dataset> --add-images <track> [<keyframes…>] --set primaryVideo=<clip>`;
356
- bind asset ids. `manifest --ep N` must come back clean before you call the episode done.
372
+ 5. **No BGM in the clips** (`params.audioTrack: post`, the default). Each segment is a separate
373
+ generation, so music or sound design made inside a clip stops at its last frame and cannot carry
374
+ across a cut. The video prompt keeps the lines, the ambience and the sfx the world makes, says
375
+ there is no music, and leaves out every sfx marked `post: true` (stingers, risers, whooshes,
376
+ impact hits). Still write `sound.music` (intent + cues) and the `post: true` hits — they are the
377
+ spotting notes for the score.
378
+ 6. After each clip: `fs set <shot dataset> [--add-images <keyframes…>] --set primaryVideo=<clip>`;
379
+ bind asset ids.
380
+ 7. **Score the edited episode.** When every clip is in, edit them into a locked cut (picture lock),
381
+ `wji asset upload` it and bind `wji.assets.edit`. `prompt score E0N [--fps 24]` prints the planned
382
+ spotting sheet — SMPTE rows for the scene beds, every line (where the music ducks), each
383
+ segment's music cues at episode time, the `post` sound design, the sfx already in the clips, the
384
+ hook / turns / 爽点 / cliff (起承转合) and the ≤120 s Seed Audio sections. Conform it to the edit's
385
+ real cuts and write the cue sheet and the section prompts (`reference/mix-over-video.md`; craft in
386
+ `reference/bgm-sfx.md`, loudness and terms in `reference/film-mixing-guideline.md`): BGM, BGE /
387
+ ambience and sound design only — never what the clips already carry. One `canvas add audio -n
388
+ "E0N score K" -m seed-audio-1.0 --prompt-file …` per section (~0.15 credits a minute — quote
389
+ first), mix the sections under the edit at their offsets, upload and bind `wji.assets.mix`.
390
+ `manifest --ep N` must come back clean before you call the episode done. (`audioTrack: bed` /
391
+ `full` instead make a Seed Audio node per segment from `prompt audio E0N-01` and put the music in
392
+ the clip.)
357
393
 
358
394
  - **A failed generation is not always a bad prompt.** `wan-3.0-ref` rejections from DashScope's
359
395
  content filter (`DataInspectionFailed`) charge nothing and are partly random — retry the same node
@@ -0,0 +1,124 @@
1
+ # BGM timed to video + short-drama SFX
2
+
3
+ ## Segment `sound` SoT
4
+
5
+ ```json
6
+ "sound": {
7
+ "ambience": "neon KTV booth hum, leather sofa creak",
8
+ "sfx": [
9
+ { "cut": 2, "event": "wine glass tip and splash on table" },
10
+ { "cut": 7, "event": "SPECIAL: cliff freeze sting — single high piano hit then silence", "post": true }
11
+ ],
12
+ "music": {
13
+ "intent": "romantic neon slow pulse, soft keys, restrained satin bass",
14
+ "cues": [
15
+ { "at": 1, "do": "enter", "note": "under dialogue" },
16
+ { "at": "10.5s", "do": "swell", "note": "rise into kiss handoff" },
17
+ { "at": 4, "do": "duck", "note": "under roast lines" },
18
+ { "at": 6, "do": "drop", "note": "after climax into plea" },
19
+ { "at": 7, "do": "silence", "note": "before P02 reveal" },
20
+ { "at": "end", "do": "out" }
21
+ ]
22
+ }
23
+ }
24
+ ```
25
+
26
+ Where it goes depends on `params.audioTrack`:
27
+
28
+ - **`post` (default)** — the clip gets only the ambience, the in-picture sfx and the lines; `music`
29
+ and every `post: true` sfx stay out of it. They are the spotting notes for the episode score:
30
+ `$SD prompt <project> score E01` lays them on the episode timeline (SMPTE) for scoring over the
31
+ edit (`mix-over-video.md`). A cue `at` is still a cut of *this* segment — the sheet adds the
32
+ segment's offset.
33
+ - **`bed` / `full`** — `prompt audio … --bed` / `--full` expands this into `[start s:end s]` lines
34
+ for a Seed Audio node per segment, and the music intent also goes into the clip prompt.
35
+
36
+ ## BGM cue verbs
37
+
38
+ `enter | swell | duck | drop | silence | out`
39
+
40
+ Put mood vocabulary in `intent` + `note`:
41
+
42
+ | Want | How to write |
43
+ | --- | --- |
44
+ | rise / build | `swell` + note "intensity climb / rise into …" |
45
+ | drone / dread | intent "low dread drone, barely moving" + `enter`/`drop` |
46
+ | romantic | intent "romantic …" + `swell` on romantic beats |
47
+ | comedy | intent "light comedy pizzicato" + duck under punchlines |
48
+ | cliff | `drop` or `silence` then one special sting SFX |
49
+ | triumphant | intent "bright brass sting" — use sparingly |
50
+
51
+ Rules: one music intent per segment (or continuous across a scene); instrumental unless vocals are a story beat; **duck under dialogue**; hit 爽点 / cliff on purpose. Continuous texture belongs in **ambience**, not twenty music mood words.
52
+
53
+ ### With a `params.cinematography` preset (sound is half the look)
54
+
55
+ Craft in `cinematography-<preset>.md`; not gated.
56
+
57
+ | preset | music / sfx |
58
+ | --- | --- |
59
+ | `low-key` | nothing extra — the usual dread / tension vocabulary |
60
+ | `sweet-romance` | the OST is the segment's `sound.music`, cues on the cut: `enter` on the eye-lock's last cut, `swell` on the look back / the carry / the gap closing, `silence` as the hand lands or just before a confession line (so it lands in a hush), `drop` on the 壁咚; rain on the umbrella, a heartbeat if wanted |
61
+ | `period-nostalgia` | diegetic music of the era — the radio, a period song on the cassette player — in `sound.sfx` or as the segment's `music` intent; cicadas in the courtyard, the bicycle bell, a street hawker, a distant train |
62
+ | `costume-idol` | follows `sweet-romance` for the 心动 set pieces (执手, 簪花, 共乘一马, 月下对饮, 灯会) — `enter` / `swell` / `silence`; wind, chimes, the lantern festival crowd as ambience |
63
+ | `costume-dark` | fire crackle, wind, rain on tiles, armour creaking, distant drums; **`silence`** in the court before a verdict; violence carried by sound and consequence rather than choreography |
64
+ | `scifi-hard` | hums, fans, relays, alarms, radio chatter with squelch, breathing in a helmet; **silence in vacuum** (only the suit's breathing and radio); music sparse — a low drone, a single piano line |
65
+ | `scifi-soft` | rain, crowds in many languages, synth hums, holographic chimes, drones passing; music synthwave / ambient pads |
66
+
67
+ ### By segment grammar (`segment.grammar`, else `params.grammar`)
68
+
69
+ Craft in `grammar-faceslap.md` §3, `grammar-comedy.md` and
70
+ `action.md` / `action-<style>.md` ("Sound"); gate `grammar-advice` checks the 打脸 music hit.
71
+
72
+ | grammar | music / sfx |
73
+ | --- | --- |
74
+ | `faceslap` | `silence` or `drop` under the insult, **`enter` / `swell` on the entrance or the proof**, full through the reaction cascade; sfx: the document slapped on the table, the gasp, the cup falling, the murmur rising |
75
+ | `comedy` | **silence before the punchline** (a `silence` cue or a pause in the ambience); `slapstick` — cartoon sfx on every beat (boing, whoosh, record scratch) and a comic sting after the punchline; `deadpan` — no comic sfx, often no music, the ambience (clock, fly, fridge hum) fills the hold; `romcom` — playful pops and plinks, a heartbeat, a cute sting, the OST entering on the blush |
76
+ | `action` | **every impact gets an sfx on its cut**; the music `drop`s out before the decisive blow; per style: `bullet-time` stretched / pitched-down sound then snap back, `gun-fu` driving low synth, `hk-kungfu` exaggerated whooshes and snaps with a comic sting, `historical-realist` metal on metal and often no score, `street-brawl` none or a low drone, `car-chase` engines then the silence after the crash |
77
+
78
+ ## SFX + special short-drama FX
79
+
80
+ Every story-visible hit needs `sfx[]` tied to a **cut** (or absolute time in a hand-written prompt). Prefer concrete / onomatopoeic events.
81
+
82
+ Mark hero moments with `SPECIAL:` in `event` so QC can hear them.
83
+
84
+ **In the clip or in post?** An sfx the world makes — a glass, a door, a slap, footsteps, rain — is
85
+ in-clip: the video model makes it in sync with the picture. Sound design the world does not make —
86
+ stinger, riser, whoosh, impact hit, reverse cymbal, drone hit, the silence drop — gets `"post": true`:
87
+ it stays out of the clip prompt and is made over the edit, where it can land across a cut. A hit that
88
+ is both (the slap *and* a stinger on it) is two entries.
89
+
90
+ | Beat | Typical special FX |
91
+ | --- | --- |
92
+ | 打脸 / reveal | sharp whoosh + muted impact + brief silence |
93
+ | 身份揭露 | rising drone → glass-ring sting |
94
+ | 强宠 / romantic turn | soft shimmer / heartbeat under VO |
95
+ | Comedy mishap | spill / choke / bonk — diegetic if live-action |
96
+ | Cliffhanger | near-silence + one iconic prop sound |
97
+ | Transition | whip-pan whoosh (sparingly) |
98
+
99
+ Few high-value FX per segment. If the beat is only atmosphere, put it in ambience.
100
+
101
+ ## Intent vocabulary bank (examples — not a closed list)
102
+
103
+ Write these into `music.intent` and cue `note`. Pair with `enter | swell | duck | drop | silence | out`. Mix freely.
104
+
105
+ **Tension / suspense:** suspense pad · ticking urgency · dread drone · cold empty room · stalking pulse · pressure build · quiet danger · held breath
106
+
107
+ **Rise / climax:** slow rise · intensity climb · heartbeat acceleration · approaching climax · swelling strings · peak hit
108
+
109
+ **Romantic:** romantic neon · soft satin · afterglow haze · tender piano · yearning guitar · candlelit hush · close whisper hush
110
+
111
+ **Comedy / farce:** light comedy pizzicato · clumsy slapstick · awkward silence bed · wink-and-nudge · drunken wobble · karaoke parody · punchline duck-and-sting
112
+
113
+ **Face-slap / twist / 爽点:** smug turnaround · reveal sting · power-flip brass · gotcha hit · vindication swell · identity-drop silence then sting
114
+
115
+ **Sad / bruised:** lonely night · soft regret · bruised tenderness · rain-on-window · hollow after
116
+
117
+ **Anger / control:** cold command · sharp authority · icy control · simmering rage · light disciplinary march
118
+
119
+
120
+ **Place texture:** neon booth haze · muffled corridor bass · office night hum · leather sofa creak bed · fluorescent buzz · city window night
121
+
122
+ **Transition / end:** whip cut · hard reset · freeze-frame sting · cliff hang · unresolved chord · fade-to-black
123
+
124
+ Example: `intent: "awkward comedy pizzicato under neon booth haze"` · note: `swell — intensity climb into climax` · note: `drop — freeze-frame sting then silence`
@@ -162,8 +162,11 @@ that character's lines to the label. Caps: 3 audio references on `minimax-h3-ref
162
162
  pictures then voices, in the order to connect them; drop them only if the model shows no audio
163
163
  reference support (`params.voiceSamples: false`).
164
164
 
165
- The segment's **soundtrack node** (Seed Audio; `params.audioTrack`, default `bed`) sits beside the
166
- video node — no `--from` for a bed, the voice display nodes in speaking order for `--full`:
165
+ **Soundtrack.** By default (`params.audioTrack: post`) there is **no per-segment soundtrack node**:
166
+ the clips carry no music, the episode is edited first, and the score + sound design are made over the
167
+ edit, one Seed Audio node per ≤ 120 s section (see the post recipe below). Only in `bed` / `full`
168
+ does each segment get a **soundtrack node** beside its video node — no `--from` for a bed, the voice
169
+ display nodes in speaking order for `--full`:
167
170
 
168
171
  ```bash
169
172
  SD prompt <project> audio E01-01 > /tmp/E01-01-audio.txt # timecoded ambience / sfx / music
@@ -173,6 +176,20 @@ wji canvas gen A1 --yes --wait #
173
176
  # --full: wji canvas add audio … --from D_C02_VOICE D_C01_VOICE (→ @Audio1 @Audio2, the order the prompt names them)
174
177
  ```
175
178
 
179
+ **Post soundtrack (`audioTrack: post`)** — after every clip is bound and the episode is edited:
180
+
181
+ ```bash
182
+ wji asset upload ./E01-edit.mp4 --json | jq -r .id # the locked edit, no music → EDIT
183
+ SD bind <project> --ep 1 wji.assets.edit EDIT
184
+ SD prompt <project> score E01 > /tmp/E01-score.md # planned spotting sheet + ≤120 s sections
185
+ # Sound Designer conforms it to the edit's real cuts and writes one bed prompt per section
186
+ wji canvas add audio -c E1 -n "E01 score 1" -m seed-audio-1.0 --prompt-file /tmp/E01-score-1.txt --json | jq -r .id
187
+ wji canvas gen <node> --yes --wait # ask first; ~0.15 credits a minute
188
+ # mix the sections under the edit at their offsets (ffmpeg), then:
189
+ wji asset upload ./E01-mix.mp4 --json | jq -r .id # → MIX
190
+ SD bind <project> --ep 1 wji.assets.mix MIX
191
+ ```
192
+
176
193
  `wan` mode, one clip per segment (the model speaks the dialogue itself, so no voice nodes):
177
194
 
178
195
  ```bash