@wji.ai/cli 0.9.1 → 0.9.4

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.
Files changed (71) hide show
  1. package/README.md +6 -1
  2. package/dist/index.js +109 -32
  3. package/package.json +1 -1
  4. package/skill/backreel/SKILL.md +29 -15
  5. package/skill/backreel/reference/crowdfunding.md +61 -30
  6. package/skill/backreel/reference/episodes.md +7 -7
  7. package/skill/short-drama/SKILL.md +27 -4
  8. package/skill/short-drama/examples/night-shift/episodes/E01.json +173 -1
  9. package/skill/short-drama/examples/night-shift/project.json +6 -0
  10. package/skill/short-drama/reference/dialogue-craft.md +16 -11
  11. package/skill/short-drama/reference/prompt-audit.mjs +308 -0
  12. package/skill/short-drama/reference/reference-binding.md +84 -0
  13. package/skill/short-drama/reference/schema.md +14 -5
  14. package/skill/short-drama/reference/stage-characters.md +1 -1
  15. package/skill/short-drama/reference/stage-script.md +54 -7
  16. package/skill/short-drama/reference/stage-storyboard.md +29 -24
  17. package/skill/short-drama/scripts/selftest.mjs +154 -9
  18. package/skill/short-drama/scripts/short-drama.mjs +467 -23
  19. package/skill/w-film-dialogue-editor/CHANGELOG.md +44 -0
  20. package/skill/w-film-dialogue-editor/SKILL.md +124 -0
  21. package/skill/w-film-dialogue-editor/references/data/README.md +222 -0
  22. package/skill/w-film-dialogue-editor/references/data/en/core.json +1666 -0
  23. package/skill/w-film-dialogue-editor/references/data/en/slang.json +1602 -0
  24. package/skill/w-film-dialogue-editor/references/data/en/work.json +1722 -0
  25. package/skill/w-film-dialogue-editor/references/data/en-sentences.csv +134 -0
  26. package/skill/w-film-dialogue-editor/references/data/en-sentences.md +146 -0
  27. package/skill/w-film-dialogue-editor/references/data/ja/core.json +1978 -0
  28. package/skill/w-film-dialogue-editor/references/data/ja/slang.json +1927 -0
  29. package/skill/w-film-dialogue-editor/references/data/ja/work.json +1972 -0
  30. package/skill/w-film-dialogue-editor/references/data/ja-sentences.csv +134 -0
  31. package/skill/w-film-dialogue-editor/references/data/ja-sentences.md +146 -0
  32. package/skill/w-film-dialogue-editor/references/data/ko/core.json +1839 -0
  33. package/skill/w-film-dialogue-editor/references/data/ko/slang.json +1897 -0
  34. package/skill/w-film-dialogue-editor/references/data/ko/work.json +1829 -0
  35. package/skill/w-film-dialogue-editor/references/data/ko-sentences.csv +134 -0
  36. package/skill/w-film-dialogue-editor/references/data/ko-sentences.md +146 -0
  37. package/skill/w-film-dialogue-editor/references/data/zh-cn/core.json +1794 -0
  38. package/skill/w-film-dialogue-editor/references/data/zh-cn/slang.json +1609 -0
  39. package/skill/w-film-dialogue-editor/references/data/zh-cn/work.json +1648 -0
  40. package/skill/w-film-dialogue-editor/references/data/zh-cn-sentences.csv +134 -0
  41. package/skill/w-film-dialogue-editor/references/data/zh-cn-sentences.md +146 -0
  42. package/skill/w-film-dialogue-editor/references/data/zh-hk/core.json +2476 -0
  43. package/skill/w-film-dialogue-editor/references/data/zh-hk/gen-z.json +3263 -0
  44. package/skill/w-film-dialogue-editor/references/data/zh-hk/older.json +3632 -0
  45. package/skill/w-film-dialogue-editor/references/data/zh-hk/rough.json +1668 -0
  46. package/skill/w-film-dialogue-editor/references/data/zh-hk/workplace.json +3697 -0
  47. package/skill/w-film-dialogue-editor/references/data/zh-hk-sentences.csv +373 -0
  48. package/skill/w-film-dialogue-editor/references/data/zh-hk-sentences.md +391 -0
  49. package/skill/w-film-dialogue-editor/references/data/zh-tw/core.json +1640 -0
  50. package/skill/w-film-dialogue-editor/references/data/zh-tw/slang.json +1537 -0
  51. package/skill/w-film-dialogue-editor/references/data/zh-tw/work.json +1699 -0
  52. package/skill/w-film-dialogue-editor/references/data/zh-tw-sentences.csv +132 -0
  53. package/skill/w-film-dialogue-editor/references/data/zh-tw-sentences.md +144 -0
  54. package/skill/w-film-dialogue-editor/references/dialogue_revision_framework.md +84 -0
  55. package/skill/w-film-dialogue-editor/references/en-index.md +265 -0
  56. package/skill/w-film-dialogue-editor/references/ja-index.md +633 -0
  57. package/skill/w-film-dialogue-editor/references/ko-index.md +498 -0
  58. package/skill/w-film-dialogue-editor/references/lang-en.md +242 -0
  59. package/skill/w-film-dialogue-editor/references/lang-ja.md +312 -0
  60. package/skill/w-film-dialogue-editor/references/lang-ko.md +290 -0
  61. package/skill/w-film-dialogue-editor/references/lang-zh-cn.md +60 -0
  62. package/skill/w-film-dialogue-editor/references/lang-zh-hk-mandarin.md +34 -0
  63. package/skill/w-film-dialogue-editor/references/lang-zh-hk.md +306 -0
  64. package/skill/w-film-dialogue-editor/references/lang-zh-period.md +38 -0
  65. package/skill/w-film-dialogue-editor/references/lang-zh-tw.md +57 -0
  66. package/skill/w-film-dialogue-editor/references/zh-cn-index.md +636 -0
  67. package/skill/w-film-dialogue-editor/references/zh-hk-index.md +1450 -0
  68. package/skill/w-film-dialogue-editor/references/zh-tw-index.md +457 -0
  69. package/skill/w-film-dialogue-editor/scripts/rebuild.sh +13 -0
  70. package/skill/w-film-dialogue-editor/scripts/ref.mjs +539 -0
  71. package/skill/w-film-dialogue-editor/templates/dialogue_revision_report.md +43 -0
@@ -0,0 +1,84 @@
1
+ # Reference binding — 參考圖綁定
2
+
3
+ A video prompt is **one string** read by a model that has never seen the storyboard, the project
4
+ or the canvas. It cannot tell `@image2` from `@image3`, cannot tell the room from a prop, and
5
+ defaults to whatever it saw in the previous shot for anything the shot does not name. So the
6
+ picture plan has to be written twice: once as a legend that says **what each picture is**, and once
7
+ inside every shot that depends on it.
8
+
9
+ Gate: `ref-binding` (stage `storyboard`, sheets mode). Proofreader for text that is not in the
10
+ project file: `prompt-audit.mjs` (shipped in `reference/`).
11
+
12
+ ## The six roles — and what may not be copied from each
13
+
14
+ | Role | Legend says | Take | Explicitly NOT take |
15
+ | --- | --- | --- | --- |
16
+ | 場景 environment | `@image1` 係邊個場景 | 佈局、材質、磨損、比例 | 人物(場景圖本身冇人) |
17
+ | 光影 lighting | `@image5` 係光影參考 | 光比、反差、色溫、光源位置 | 構圖、人物、衣著 |
18
+ | 人物 character | `@image2` 係邊個人 | 面容、髮型、衣著 | 其他鏡頭嘅表情/姿勢 |
19
+ | 道具 prop | `@image4` 係邊件道具 | 外形、材質、尺寸、標記 | 枱面、背景、手 |
20
+ | 瞬間 moment | `@image6` 係「撞擊一瞬間」 | 打光方向、反差、剪影形狀 | 背景細節、其他鏡頭嘅走位 |
21
+ | 風格 style | `@image7` 係風格板 | 質感、顆粒、調色 | 題材、人物、構圖 |
22
+
23
+ Two consequences the model actually obeys:
24
+
25
+ - **一張圖一行。** 兩張人物圖寫在同一句(`@image2、@image3 是人物參考圖`)= 兩張圖冇分別 = 面孔互換。
26
+ - **唔係人物嘅圖,一定要寫「唔照」。** 光影圖/瞬間圖如果唔寫「只照光比,不要照它的構圖與人物」,
27
+ 模型會連圖裡面嘅人一齊抄落去。
28
+
29
+ ## Legend — good and bad
30
+
31
+ ```
32
+ ✗ @image2、@image3 是人物參考圖,照他們的面容、髮型與衣著
33
+ ✗ @image4 is the character sheet of one person in this segment — match face, hair and clothes exactly
34
+ ```
35
+
36
+ ```
37
+ ✓ @image2 是女主角三視圖(二十五六歲,齊蔭 bob 頭、oversized 黑 tee、白波鞋):只照面容、髮型、衣著
38
+ ✓ @image3 是男主角三視圖(二十七八歲,豎起黑髮、小鬍子、白 tee):只照面容、髮型、衣著
39
+ ✓ @image1 是車廂場景(右軚電動車、近黑、中控屏微光):只照空間、材質、磨損,場景圖裡沒有人
40
+ ✓ @image4 是白衣女鬼服裝與妝容參考:濕透白袍、長濕黑髮、赤腳
41
+ ✓ @image5 是車廂光影參考:只照光比、反差、色溫、光源位置,不要照它的構圖與人物
42
+ ✓ @image6 是「車撞女鬼一瞬間」:只照打光方向、反差與剪影形狀,不要照它的背景
43
+ ✓ @image7 是風格板:只照質感、顆粒、調色
44
+ ```
45
+
46
+ Every character line must carry at least two facts from the character bible (age / gender / hair /
47
+ clothes), because that is the only thing that makes two sheets distinguishable.
48
+
49
+ ## Shot binding — the shot names what it depends on
50
+
51
+ ```
52
+ ✓ 鏡頭 1(…):@image2 坐直指住屏幕,@image3 單手扶軚伸手去點…
53
+ ✓ 鏡頭 4(…):@image4 走出馬路正中間(照 @image6 的剪影與打光)…
54
+ ✓ 鏡頭 8(…):@image4 的白臉貼喺玻璃外;屏幕上的導航界面照 @image5 的 UI
55
+ ✓ 整段都在 @image1 的空間裡:佈局、材質與舊痕不變,不要重新設計場景。
56
+ ```
57
+
58
+ - 每個有角色/道具嘅鏡頭,**逐個 label 寫一次**;唔係寫喺 legend 就當夠。
59
+ - 場景 label 每段講一次就夠(「整段都在 @image1 的空間裡」)。
60
+ - Handheld / 行車感 / 鏡頭語言呢類**全段共通**嘅指令,用獨立一段寫,唔好混入逐鏡描述。
61
+ - Hand-written production prompts: run `prompt-audit.mjs` before generation; it reports unbound
62
+ people and props per shot, indistinguishable legend lines, unused pictures, and refs that are not
63
+ in the derived plan (so the canvas will renumber them).
64
+
65
+ ```bash
66
+ node reference/prompt-audit.mjs --project <project> --ep 1 --segment E01-02 --prompt prompt.txt
67
+ ```
68
+
69
+ ## Ref budget
70
+
71
+ - **≤ 4 pictures per segment** unless every extra picture has a role from the table above.
72
+ - More than 3 people in one shot → every person in that shot is named; a group sheet still needs
73
+ the individual labels if faces must be right.
74
+ - The engine caps a segment by model (`minimax-h3-ref` 9, `seedance-2.5-ref` 30, `wan-3.0-ref` 10);
75
+ the gate `ref-binding` now also fails a sheets segment over the model's cap.
76
+
77
+ ## Checklist before generating
78
+
79
+ 1. 每張圖有一行 legend,寫明係邊個/邊件/邊度/邊種參考。
80
+ 2. 兩張人物圖唔會讀起來一樣(年齡、性別、髮型、衣著至少兩樣)。
81
+ 3. 非人物圖有「唔照」句。
82
+ 4. 場景每段講一次;每個出場人物、每件道具喺自己嘅鏡頭有 label。
83
+ 5. 額外接嘅圖(光影/瞬間/風格)寫入 `segment.refOverride`,否則 canvas 一接線全部編號會移。
84
+ 6. `$SD validate --stage storyboard` 綠;或者 `prompt-audit.mjs` 零 error。
@@ -65,7 +65,7 @@ All commands accept either the directory or the `project.json` path.
65
65
  | `maxVideoRefs` / `maxAudioRefs` | per mode | how many clip / voice references the model takes; set them lower to tighten, never higher |
66
66
  | `lookPhraseZh` | empty | the same look phrase in the drama's language, gated in a **Chinese** prompt (`wan`) where the English `lookPhrase` would be both out of place and extra length for the content filter to judge |
67
67
  | `speechLeadSeconds` | 0.5 | added to a cut's dialogue seconds before the fit check — the mouth opens after the cut starts; also where a line starts in the `full` soundtrack prompt |
68
- | `audioTrack` | `post` | where the soundtrack is made. **`post`** (the default): the clips carry dialogue, ambience and in-picture sfx but **no music** (`No music.` / `non_diegetic_music: N/A` / `无配乐`) and no `post: true` sfx — music and sound design cannot carry across separately generated segments, so they are made once over the **edited episode**, on its timecode (`prompt score E01`, then `mix-over-video.md`); no per-segment audio node. `bed` (ambience + sfx + timed music, no voices) / `full` (the bed plus every line in the connected `@Audio1..3` voices — at most three speakers): a Seed Audio node per segment, music in the clip prompt. `none`: no soundtrack work |
68
+ | `audioTrack` | `post` | where the soundtrack is made. **`post`** (the default): the clips carry dialogue, ambience and in-picture sfx but **no music** (`No music.` / `non_diegetic_music: N/A` / `无配乐`) and no `post: true` sfx — music and sound design cannot carry across separately generated segments, so they are made once over the **edited episode**, on its timecode (`prompt score E01`, then `../short-drama-sound/reference/mix-over-video.md`); no per-segment audio node. `bed` (ambience + sfx + timed music, no voices) / `full` (the bed plus every line in the connected `@Audio1..3` voices — at most three speakers): a Seed Audio node per segment, music in the clip prompt. `none`: no soundtrack work |
69
69
  | `foregroundLevel` | `medium`; `high` for 唯美 / 美感优先 (`composition: art-first`, `cinematography: sweet-romance` or `costume-idol`, a genre naming 唯美 or 美感) | how often a cut shoots past a foreground, measured across the episode: `low` up to 25 % of the cuts, `medium` 25–50 %, `high` 50 % or more (gate `photography`; `--foreground` at init). Which cuts: `stage-storyboard.md` |
70
70
  | `scoreFps` | 24 | frame rate `prompt score` writes SMPTE `HH:MM:SS:FF` at (`--fps` overrides); set it to the edit's real fps |
71
71
  | `subtitles` | false | `true` drops the subtitles-off sentence requirement from the prompts |
@@ -87,6 +87,7 @@ All commands accept either the directory or the `project.json` path.
87
87
  | `turnSeconds` | 30 | the longest stretch allowed between turn points — the hook, every beat marked `turn: true`, the 爽点 and the cliff |
88
88
  | `maxConsecutiveLines` | 3 | line beats in a row without an action beat between them |
89
89
  | `minActionRatio` | 0.35 | action beats ÷ line beats over an episode with ≥ 5 lines (谈话+行动) |
90
+ | `dialogueReview` | `required` | `required`: every episode's lines are reviewed against the `w-film-dialogue-editor` guide of their 语体 before the storyboard (gate `dialogue-review`); `off`: no review — only when the user asks |
90
91
  | `maxLineWords` / `maxNameCallsPerScene` | 20 / 2 | English registers only: words per line, and how often another character is addressed by name in one scene (advisory) |
91
92
  | `charsPerSecond` / `actionSeconds` | 4.5 / 2.5 | script duration math: a line takes `nonWhitespaceChars ÷ charsPerSecond` s, an action beat takes `actionSeconds` |
92
93
  | `tolerance` | 0.15 | ±15 % for episode duration gates |
@@ -203,7 +204,7 @@ id or list) and `gag` (`setup` / `build` / `punchline`) with `runningGag`.
203
204
  ```json
204
205
  {
205
206
  "id": "S01", "name": "便利店店内", "primary": true, "reusePlan": "",
206
- "summary": "设计意图…", "anchors": [{ "name": "闪烁的关东煮灯箱", "desc": "…" }],
207
+ "summary": "设计意图…", "condition": "lived-in", "conditionReason": "这家店每晚都在营业,是用旧了而不是残破", "anchors": [{ "name": "闪烁的关东煮灯箱", "desc": "…" }],
207
208
  "lighting": [{ "state": "凌晨冷白光", "prompt": "…" }],
208
209
  "image": { "prompt": "…", "negativePrompt": "…", "sheet": "…", "tags": [] },
209
210
  "variantOf": "", "changes": "",
@@ -217,13 +218,20 @@ paintable and checkable), `lighting` (≥1, prompts in `imagePromptLang`, derive
217
218
  episodes it appears in), `image.*` (`imagePromptLang`, **empty scene — negative prompt
218
219
  must ban people**), `variantOf` + `changes` for variants of a parent scene.
219
220
  `wji.nodes.lighting` / `wji.assets.lighting` are keyed by lighting `state`.
221
+ `condition` is `new`, `lived-in` or `worn`, and `conditionReason` says why the story gives it
222
+ that condition — the same two fields are required on every prop. Realism is materials, light and
223
+ detail, not wear. A luxury interior, a set just built, a prop the organisers have just issued,
224
+ and every national or regional flag are `new` and accurate. Anchors follow the condition: do not
225
+ add peeling paint, rust or fading by default. Only `worn` may ban `brand-new` or `showroom-clean`
226
+ in `negativePrompt`. A `new` asset fails when an anchor or prompt says worn, rust, faded, chipped,
227
+ torn, frayed, peeling, stain, 殘舊, 生鏽, 褪色 or 剝落.
220
228
 
221
229
  ### props[]
222
230
 
223
231
  ```json
224
232
  {
225
233
  "id": "P01", "name": "旧监控硬盘", "function": "全剧悬念核心:里面到底录了什么",
226
- "beatIds": ["B02"], "scale": "handheld", "summary": "…",
234
+ "beatIds": ["B02"], "scale": "handheld", "summary": "…", "condition": "worn", "conditionReason": "三年前的硬盘,标签撕掉、接口生锈",
227
235
  "anchors": [{ "name": "…", "desc": "…" }], "states": [{ "state": "封在防静电袋里", "prompt": "…" }],
228
236
  "relatedScenes": ["S01"], "carriedBy": ["林小夏"],
229
237
  "image": { "prompt": "…", "negativePrompt": "…", "sheet": "…", "tags": [] },
@@ -295,6 +303,7 @@ prompt bans hands**). At most `thresholds.maxProps` (8).
295
303
  | `flow[].turn` | `true` on a beat that brings new information, an obstacle, a counter or a choice; the `rhythm` gate measures hook → turns → 爽点 → cliff against `turnSeconds` |
296
304
  | `scenes[].sceneId` / `lighting` / `characters` / `props` | must exist in `project.json`; `lighting` must be a state of that scene |
297
305
  | `flow[]` | alternating **action beats** `{action}` (narrative, one thing per beat, **common actions only**, no quotes) and **line beats** `{speaker, line, delivery?}`; `speaker` ∈ scene `characters` or `VO` (an off-screen voice: it gets no `@audio` reference, the prompt calls it off-screen and keeps every visible mouth closed while it speaks); `line` ≤ `maxLineChars` non-whitespace characters; `delivery` names nobody when the video prompt carries it (a Chinese `wan` prompt, or an English delivery) — gate `beat-flow` |
306
+ | `dialogueReview` | the 对白校对 record (gate `dialogue-review`, required unless `params.dialogueReview: off`), seeded by `seed dialogue`: `{ level: 轻修 \| 中修 \| 重修, guides: ["lang-zh-hk.md", …] (every speaker's 语体 guide of `w-film-dialogue-editor`), diagnosis: { goal, conflict, subtext, voice, rhythm }, rows: [{ scene, beat, speaker, original, issue, revised, reason, note, delivery? }] (one per line beat, 1-based positions; `revised` must equal the script's line — `apply dialogue` writes it; `note` a playable action), selfCheck: [{ item, ok, note }] (the guide's 自检清单, ≥ 3) }` |
298
307
 
299
308
  Duration: `Σ lines (chars ÷ charsPerSecond) + Σ actions × actionSeconds`.
300
309
 
@@ -322,7 +331,7 @@ Duration: `Σ lines (chars ÷ charsPerSecond) + Σ actions × actionSeconds`.
322
331
  | `motion` | `{ trigger, action, result }` — cue, one visible state chain, landing (or `hold`); English unless the prompt language is Chinese; no names, no inferred states, ≤ 3 hand verbs — gate `motion` |
323
332
  | `cameraNote` | `{ motivation, trigger, path, endFraming }` on any move (`motivation` ∈ `reveal / pressure / alignment / relationship / transition / stillness`), `{ hold }` on a `Static Shot` — gate `motion` |
324
333
  | `frame` | English prompt for the moment: composition, this moment's positions and poses, the size phrase, the style's `framePhrase` (`photorealistic cinematic film still` / `cinematic film still` / `hand-painted anime film still`), **and verbatim** the camera phrase (`shot on an ARRI ALEXA Mini LF with a Zeiss Supreme Prime 75mm at T1.5`), the light-direction phrase and the foreground, and under a `cinematography` preset also its `framePhrase` and the `light.source`; **no names** — identity comes from the sheet references |
325
- | `prompt` | the video prompt, shape by `params.video` (below); gate `prompt-hygiene` (no `…`, no ids / paths / notes, subtitles off unless `params.subtitles`, H3 lips-closed clause, sheets-mode `@imageK` per shot) |
334
+ | `prompt` | the video prompt, shape by `params.video` (below); gate `prompt-hygiene` (no `…`, no ids / paths / notes, subtitles off unless `params.subtitles`, H3 lips-closed clause, sheets-mode `@imageK` per shot) and gate `ref-binding` (one legend line per picture saying **what** it is — a person by age / hair / clothes from the bible, a room by its name, a prop by its name; every non-person picture carries a *take only … / not its composition or its people* clause; every prop and person is bound by its label again inside its own shot; the room is named once per segment; no connected picture unused; sheets-mode picture cap) |
326
335
  | `segments[].axis` | `{ between: [C01, C02], workingSide, screenLeft, screenRight }`, required when two or more people appear in the segment |
327
336
  | `segments[].grammar` / `exceptions` | `grammar` (a string or an array) says how the segment is cut — `drama` · `action` · `faceslap` · `comedy`; missing, it is `params.grammar`; the old `action: true` means `action`. Each grammar's rules apply (gates `grammar`, `action`, `faceslap`, `comedy`; advisory `action-advice`, `grammar-advice`) and the prompt carries each grammar's line. `exceptions: [{ rule, reason }]` waives one rule for this segment when the user asks — rules `open-wide` `one-move` `impact-reaction` `no-graphic` `reveal-insert` `reaction-chain` `punchline-reaction` `setup-first` (the old `actionExceptions` still works) |
328
337
  | `cuts[].idle` | sheets mode: `{ <id>: "shifts her weight" }`, a small motion in place for a person other than the one carrying the action, or `"still"` (breathing and blinking only). In a cut with two or more visible people, all but one need one (gate `idle-motion`) |
@@ -330,7 +339,7 @@ Duration: `Σ lines (chars ÷ charsPerSecond) + Σ actions × actionSeconds`.
330
339
  | `cuts[].ots` | the person whose shoulder is near the lens in an over-the-shoulder; `characters` holds both people; counts as a foreground; that person needs no `eyeline` |
331
340
  | `segments[].sound` | `{ ambience, sfx: [{ cut, event, post? }], music }` — ambience identical across a scene's segments; `music` is `N/A`, an intent string, or `{ intent, cues: [{ at: <cut number \| "10.5s">, do: enter \| swell \| duck \| drop \| silence \| out, note }] }` — gate `sound`. An sfx with `post: true` is **sound design outside the story world** (stinger, riser, whoosh, impact hit, reverse cymbal) — never in the clip prompt, made over the edit. In `audioTrack: post` the `music` is the **spotting intent** for the episode score: it never enters the clip, and `prompt score E01` lays it, the post sfx, the lines and the scene beds on the episode timeline. In `bed` / `full`, `prompt audio E01-01` derives the segment's Seed Audio soundtrack prompt from it (timecoded `[start s:end s]` ambience / sfx / music, plus the lines with `--full`) — gate `audio-track` |
332
341
  | `wji.assets.edit` / `wji.assets.mix` (episode) | `audioTrack: post` only — the locked edit of the episode (clips cut together, no music) and the final mixed episode (edit + score + sound design). `manifest` asks for the edit once every clip is bound, then for the mix |
333
- | `segments[].refOverride[]` | optional — the Director pins this segment's **picture** references instead of the derived set (environment, people, props): `[{ entity, asset?: "sheet" \| "headshot" \| "lighting:<state>", faceOf?, why?, use?, zh? }]` in the order they will be connected (`@image1…`). `faceOf: "C01"` says this picture supplies that character's face, so their lines bind to it even when a body sheet of theirs comes later. Voices and the previous clip are still appended after the pictures. An empty array means "derived" |
342
+ | `segments[].refOverride[]` | optional — the Director pins this segment's **picture** references instead of the derived set (environment, people, props): `[{ entity, asset?: "sheet" \| "headshot" \| "lighting:<state>", role?, faceOf?, why?, take?, takeZh?, notTake?, use?, zh? }]` in the order they will be connected (`@image1…`). `role` is `environment` · `lighting` · `character` · `prop` · `moment` · `style`; `take` / `notTake` write the legend line for a reference that is not simply the place, a person or the prop — e.g. `{ role: "lighting", take: "only its light ratios, contrast and colour temperature", notTake: "its composition and its people" }` renders as *take only what it is for, not its composition and its people* (gate `ref-binding` requires the negative clause on every non-person picture). `use` / `zh` override the whole line. `faceOf: "C01"` says this picture supplies that character's face, so their lines bind to it even when a body sheet of theirs comes later. Voices and the previous clip are still appended after the pictures. An empty array means "derived" |
334
343
  | `storyboard.visualPlans[]` | `{ sceneIndex, turn, alignment, pressure, progression, rhythm, landing, sound }` for the 爽点 scene and the cliff scene (seeded) — gate `visual-plan` |
335
344
  | `props[].textPolicy` | optional `{ mode: readable \| symbolic \| blank, text }` — gate `text-policy` |
336
345
 
@@ -40,7 +40,7 @@ relationship carries a `secret` — that is where the reversals come from.
40
40
 
41
41
  **`bible.speech` is the source of the voice.** `voice.pace` / `videoDirection` and every line's
42
42
  `delivery` are written from `rhythm`, `silence` and `forbiddenWords`; `seed script` prints them in
43
- the `seedNote` and `render dialogue` puts them beside each line for the polish pass.
43
+ the `seedNote` and `render dialogue` puts them beside each line for the dialogue review.
44
44
 
45
45
  ## Image prompts (English, always)
46
46
 
@@ -19,6 +19,20 @@ budget. **Before writing: `render guideline`, then the scene bibles (anchors, li
19
19
  the speakers' `render character` — the bible's `speech` is where the lines come from** (see
20
20
  `dialogue-craft.md`). Delete `seedNote` when the scenes are written.
21
21
 
22
+ `seedNote.dialogue` points at the user's dialogue skill, **`w-film-dialogue-editor`** (installed
23
+ beside this one): the revision framework, the language guide of every 语体 in the episode
24
+ (`lang-zh-hk.md`, `lang-zh-cn.md`, `lang-zh-tw.md`, `lang-zh-hk-mandarin.md`, `lang-zh-period.md`,
25
+ `lang-en.md`) and, per speaker, the corpus query for someone of their age:
26
+
27
+ ```bash
28
+ node <skills>/w-film-dialogue-editor/scripts/ref.mjs --lang zh-hk list --age young --limit 30
29
+ node <skills>/w-film-dialogue-editor/scripts/ref.mjs --lang zh-hk search 搞掂 # one word
30
+ ```
31
+
32
+ Read the guide before writing the first line and pull phrasing from the corpus — particles, slang,
33
+ swearing levels and trade talk come from there, never from memory. The corpus is a vocabulary of
34
+ real usage, not lines to paste: a line still grows from the person and the moment.
35
+
22
36
  ## Hard rules (all gated)
23
37
 
24
38
  1. **The duration budget is the wall.** Lines cost `chars ÷ 4.5` s, action beats 2.5 s; the
@@ -95,13 +109,46 @@ scene rather than hopping four.
95
109
  | omniscience | lines explain everything | move information into action and delivery |
96
110
  | flat ending | ends calm | last beat becomes the cliff, even just a whistle |
97
111
 
98
- ## Polish (`render dialogue --ep N`)
112
+ ## Dialogue review — required after the script (`dialogue-review`)
113
+
114
+ A script is not done when its structural gates are green: every line is then reviewed against the
115
+ 语体 guide, the way `w-film-dialogue-editor` reviews a scene, and the review is kept in the episode.
116
+ The storyboard stage cannot start until it is (`validate --stage storyboard` runs the script gates
117
+ too). `params.dialogueReview: off` skips it — only when the user asks.
118
+
119
+ ```bash
120
+ node {baseDir}/scripts/short-drama.mjs seed <project> dialogue --ep 1 # dialogueReview: one row per line + the guides to read
121
+ node {baseDir}/scripts/short-drama.mjs apply <project> dialogue --ep 1 # write the revised lines (and deliveries) into the flow
122
+ node {baseDir}/scripts/short-drama.mjs validate <project> --stage script --ep 1
123
+ node {baseDir}/scripts/short-drama.mjs render <project> dialogue --ep 1 # the filled 对白校对 table
124
+ ```
99
125
 
100
- When the user asks for a polish, when the dialogue is the visible weakness, or when the register
101
- gate keeps noting the same scene: `render dialogue --ep 1` prints the 逐句校对表 (原句 / 问题 / 修改句 /
102
- 修改理由 / 导演/演员提示) with each speaker's speech rules in the last column. Fill it per
103
- `dialogue-craft.md` §3, apply the changes to the flow, re-validate, and append the filled table to
104
- the screenplay dataset under `## 对白校对`.
126
+ 1. **Read** the framework (`references/dialogue_revision_framework.md`) and the guide of every 语体
127
+ `seed dialogue` prints; query the corpus for each speaker it lists.
128
+ 2. **Diagnose the episode first** (`diagnosis`: `goal` 场面目标, `conflict` 主要冲突, `subtext` 潜台词核心,
129
+ `voice` 角色声音问题, `rhythm` 节奏问题) and pick `level` 轻修 / 中修 / 重修. A scene with no goal,
130
+ obstacle or turn is a script problem — fix the beats, not the words.
131
+ 3. **Every row**: `issue` (the problem, or `无:<why it stands>`), `revised` (the new line, or the
132
+ same line), `reason` when it changed, `note` — the 导演/演员提示 as a playable action
133
+ ("你不是解释,你是在逼对方认错"), never a mood (自然一点 / 生气 are refused). A row may set
134
+ `delivery` to replace the beat's parenthetical.
135
+ 4. **`selfCheck`**: the guide's 自检清单, item by item (`{ item, ok, note }`, at least three); an
136
+ item that fails says why it stands or the lines are fixed. `lang-zh-hk.md` has no section called
137
+ 自检: for 粤语 the checklist is its §九 (书面中文 → 港式粤语 and the 三个硬指标) plus the skill's
138
+ `SKILL.md` 品质检查.
139
+ 5. `apply dialogue`, then re-validate — a shorter or longer line moves the duration and the dialogue
140
+ fit downstream.
141
+
142
+ Editing a line later reopens it: the gate compares every row's `revised` with the script, so a
143
+ changed line fails until it is reviewed again. `seed dialogue` re-run keeps the rows of unchanged
144
+ lines and opens only the moved ones (`--force` resets them all). Cutting or adding a line is done in
145
+ the flow first, then re-seed.
146
+
147
+ The gate checks: a review exists, the level, all five diagnosis fields, the guide of every speaker's
148
+ 语体 listed in `guides`, one row per line and none for a beat that is not a line, the revision
149
+ applied, an issue on every row, a reason on every change, a playable note on every row, and the
150
+ self-check. `render dialogue` prints the filled table — append it to the screenplay dataset under
151
+ `## 对白校对`; `render script`'s 创作自检 gains the row 对白是否已按语体 guide 逐句校对.
105
152
 
106
153
  ## Sync
107
154
 
@@ -115,7 +162,7 @@ wji fs set <id> --merge content=@/tmp/E01-script.md --merge viewMode=content --m
115
162
  ## Gates
116
163
 
117
164
  `script-shape` · `beat-flow` · `duration` · `hook-cliff` · `beats-claimed` · `script-design` ·
118
- `rhythm` · `filmable` · `dialogue-register` (advisory).
165
+ `rhythm` · `filmable` · `dialogue-review` · `dialogue-register` (advisory).
119
166
 
120
167
  `render script` writes the reference's episode format — `△(from-to秒)` action lines with the
121
168
  seconds derived, `角色(delivery):line`, `【爽点标记】` after the 爽点, `【钩子标记】` at the end — and
@@ -23,7 +23,7 @@ the script and `validate` names the cuts that broke.
23
23
  ## Start from the seed
24
24
 
25
25
  ```bash
26
- node {baseDir}/scripts/short-drama.mjs seed <project> storyboard --ep 1
26
+ $SD seed <project> storyboard --ep 1
27
27
  ```
28
28
 
29
29
  Three worksheets, all deleted once the segments are written:
@@ -92,6 +92,16 @@ into the prompt.
92
92
  notes, the subtitles-off sentence in the prompt's language unless `params.subtitles`, on H3 the
93
93
  other person on screen during a line has `lips remain completely closed`, `<d>` holds only the
94
94
  line, and in sheets mode every person on screen is bound by `@imageK` inside their own shot.
95
+ - **Reference binding** (gate `ref-binding`) — the picture plan has to survive into the delivery
96
+ text. Every picture gets **one** legend line saying what it is: a person by age / hair / clothes
97
+ from the character bible (two people described in the same words is how faces get swapped), the
98
+ room by its name, a prop by its name. Every picture that is *not* a person also says what may
99
+ **not** be copied from it (*take only its light ratios, contrast and colour temperature — never
100
+ its composition or its people*). Every person and every prop is bound by its label again **inside
101
+ its own shot**, the room is named once per segment (`Every shot stays inside @image1 …`), no
102
+ connected picture may go unused, and a sheets segment may not carry more pictures than the model
103
+ takes. Wording patterns and the six roles: `reference/reference-binding.md`; proofread text that
104
+ is not yet in the project file with `reference/prompt-audit.mjs`.
95
105
  - **`params.speechLeadSeconds`** (0.5) is added to the dialogue-fit check: a 4.0 s line needs a
96
106
  5 s cut.
97
107
  - A prop may carry **`textPolicy { mode: readable | symbolic | blank, text }`** (gate
@@ -152,13 +162,13 @@ preset's banned directions (e.g. `low-key` / `costume-dark`: `frontal` / `ambien
152
162
  `sweet-romance` / `costume-idol`: `underlight` / `top`), at least half the cuts with people in a
153
163
  segment use its preferred ones, the stop stays within the preset's limit, and a period or costume
154
164
  preset refuses a source its era did not have. Stage the person against the source first, then pick the camera side; the
155
- tables are in `cinematography.md` and the preset's guide.
165
+ tables are in `../../short-drama-core/reference/cinematography.md` and the preset's guide.
156
166
 
157
167
  **Each segment is cut in its grammar** (`segment.grammar`, else `params.grammar`; see
158
- `grammar.md`): `action` segments open on a wide, take one move per cut and answer every impact
159
- (`action.md`); `faceslap` segments show the proof and a cascade of reactions
160
- (`grammar-faceslap.md`); `comedy` segments set up every punchline and give it laugh room
161
- (`grammar-comedy.md`). Each grammar adds its line to the prompt (`Action:` / `Payoff:` /
168
+ `../../short-drama-core/reference/grammar.md`): `action` segments open on a wide, take one move per cut and answer every impact
169
+ (`../../short-drama-core/reference/action.md`); `faceslap` segments show the proof and a cascade of reactions
170
+ (`../../short-drama-core/reference/grammar-faceslap.md`); `comedy` segments set up every punchline and give it laugh room
171
+ (`../../short-drama-core/reference/grammar-comedy.md`). Each grammar adds its line to the prompt (`Action:` / `Payoff:` /
162
172
  `Comedy:`).
163
173
 
164
174
  ### Foreground
@@ -205,7 +215,7 @@ default and more than two camera moves per segment is showing off.
205
215
  Get the skeleton:
206
216
 
207
217
  ```bash
208
- node {baseDir}/scripts/short-drama.mjs prompt <project> segment E01-01
218
+ $SD prompt <project> segment E01-01
209
219
  ```
210
220
 
211
221
  It prints the parts that are derived and gated verbatim — the alignment line, `[Shot k] At
@@ -371,12 +381,6 @@ segment is rejected, retry, then trim — do not go hunting for a forbidden word
371
381
  To bisect anyway, use a scratch video node at `-d 2`, 480P: free when rejected, ~30 credits when it
372
382
  passes, and sample each fragment several times because one pass proves nothing.
373
383
 
374
- **Last resort: the uncensored twin.** `wan-3.0-ref-uncensored` (MuleRouter, no green-net) takes the
375
- same prompt and the same 10 images + 5 videos + 5 audios, at ~1.3× the price; note that it bills the
376
- reference-video seconds as well as the output. Switch a stuck segment to it only with the
377
- Producer's approval, and only after retry-then-trim has failed. `short-drama.mjs` does not know it
378
- as a mode, so set it as the node's model directly.
379
-
380
384
  ## Sheets mode — no storyboard images (the default)
381
385
 
382
386
  `params.references: sheets` drops stage 5's image generation entirely: the video node references
@@ -438,9 +442,10 @@ score. So by default (`params.audioTrack: post`) **the clips carry no music**: t
438
442
  says `No music.` (H3 `non_diegetic_music: N/A`, Wan `无配乐`), keeps the dialogue, the ambience and
439
443
  the in-picture sfx, and drops every sfx marked `post: true` (stingers, risers, whooshes, impact
440
444
  hits — sound design outside the story world). The storyboard still writes `sound.music` (intent and
441
- cues) and the `post: true` hits: they are the **spotting notes**. When every clip is in, cut the
442
- episode as in `edit-tools.md` (FilmCraft, or ffmpeg if `filmcraft-cli` is missing; the `.fcproj`
443
- stays in the project), and score that locked picture as one piece on its timecode:
445
+ cues) and the `post: true` hits: they are the **spotting notes**. When every clip is in, the Operator
446
+ cuts the episode as in `../../short-drama-core/reference/edit-tools.md` (FilmCraft, or ffmpeg if
447
+ `filmcraft-cli` is missing; the `.fcproj` stays in the project), and the Sound Designer scores that
448
+ locked picture as one piece on its timecode:
444
449
 
445
450
  ```bash
446
451
  $SD prompt <project> score E01 # planned spotting sheet: SMPTE rows + ≤120 s Seed Audio sections
@@ -450,7 +455,7 @@ The sheet lays the segments end to end — scene beds (BGE), every line (where t
450
455
  segment's music cues at their episode time, the `post` sound design, the sfx already in the clips
451
456
  (never doubled) and the hook / turns / 爽点 / cliff as the 起承转合 anchors. It is the *planned*
452
457
  timeline: the Sound Designer conforms it to the edit's real cuts
453
- (`mix-over-video.md`) before any prompt is written. `manifest`
458
+ (`../../short-drama-sound/reference/mix-over-video.md`) before any prompt is written. `manifest`
454
459
  then asks for the episode's `wji.assets.edit` and `wji.assets.mix`.
455
460
 
456
461
  ### Per-segment soundtrack node (`audioTrack: bed` / `full`)
@@ -463,8 +468,8 @@ music goes into the clip prompt too; the prompt is derived — not written — f
463
468
  the cut timing:
464
469
 
465
470
  ```bash
466
- node {baseDir}/scripts/short-drama.mjs prompt <project> audio E01-05 # bed (default)
467
- node {baseDir}/scripts/short-drama.mjs prompt <project> audio E01-02 --full # + the lines, in the @AudioN voices
471
+ $SD prompt <project> audio E01-05 # bed (default)
472
+ $SD prompt <project> audio E01-02 --full # + the lines, in the @AudioN voices
468
473
  ```
469
474
 
470
475
  ```
@@ -526,14 +531,14 @@ recorded in the episode's `wji.displayNodes` as `"C01:voice"`, exactly like the
526
531
  ```bash
527
532
  wji fs mkdir <shot folder> E01 # bind --ep 1 wji.shotFolderId
528
533
  wji dataset new <shot/E01> "E01-01" -t shot -d "<scene · seconds>" # bind --ep 1 storyboard.segments.E01-01.wji.datasetId
529
- node {baseDir}/scripts/short-drama.mjs render <project> segment E01-01 > /tmp/E01-01.md
534
+ $SD render <project> segment E01-01 > /tmp/E01-01.md
530
535
  wji fs set <id> --merge content=@/tmp/E01-01.md --merge viewMode=content --merge video_prompt=@/tmp/E01-01-prompt.txt
531
- node {baseDir}/scripts/short-drama.mjs bind <project> --ep 1 storyboard.segments.E01-01.wji.nodes.keyframes.0 <nodeId>
532
- node {baseDir}/scripts/short-drama.mjs manifest <project> --ep 1 # must be clean before the episode is "done"
536
+ $SD bind <project> --ep 1 storyboard.segments.E01-01.wji.nodes.keyframes.0 <nodeId>
537
+ $SD manifest <project> --ep 1 # must be clean before the episode is "done"
533
538
  ```
534
539
 
535
540
  ## Gates
536
541
 
537
542
  `segments` · `timing` (durations, the speech lead-in **and** the reference caps) · `cut-content` ·
538
- `photography` · `video-prompt` · `shot-purpose` · `framing-axis` · `continuity` · `motion` · `sound`
539
- · `visual-plan` · `text-policy` · `prompt-hygiene` · `audio-track` · `storyboard-advice` (advisory).
543
+ `photography` · `video-prompt` · `shot-purpose` · `boundary-contrast` · `framing-axis` · `continuity` · `motion` · `sound`
544
+ · `visual-plan` · `text-policy` · `prompt-hygiene` · `ref-binding` · `audio-track` · `boundary-contrast-episode` (advisory) · `storyboard-advice` (advisory).
@@ -15,7 +15,7 @@ import {
15
15
  renderSegment, runGates, seedScript, seedStoryboard, seedanceAnchors, slug, timestamp, writeJsonFile,
16
16
  wanLegendLine, wanSheetLegendLine, wanShotHeader, segmentReferences, imageReferences, audioReferences, voiceSamplePrompt, voiceSampleSeconds, nonWhitespace,
17
17
  speechDirection, cameraPhrase, lightPhrase, idlePhrase, idleSentence, IDLE_MAX_CHARS, eyelinePhrase, eyelineValid, onlyInFramePhrase, overShoulderPhrase, sidesLine, foregroundPhrase, foregroundBand, foregroundForLook, focalOf, LIGHT_DIRECTIONS, identityOf, isEnglish,
18
- renderGuideline, renderDialogue, scriptSelfCheck, beatOffsets, turnPoints, parseAge, talkAndAction, normalizeRegister, REGISTERS, REGISTER_ACCENT,
18
+ renderGuideline, renderDialogue, seedDialogue, applyDialogue, dialogueRefs, DIALOGUE_GUIDES, corpusAge, scriptSelfCheck, beatOffsets, turnPoints, parseAge, talkAndAction, normalizeRegister, REGISTERS, REGISTER_ACCENT,
19
19
  cutStart, segmentCharacters, keyScenes, shotTexts, motionSentence, cameraClause, PURPOSE_KINDS, ANGLES, HEIGHTS, NO_SUBTITLES, LIPS_CLOSED,
20
20
  audioPrompt, musicOf, cueSeconds, AUDIO_MODEL, AUDIO_MAX_CHARS, AUDIO_MAX_SECONDS, scoreSheet, smpte,
21
21
  VIDEO_MODELS, VIDEO_MODES, DEFAULT_PARAMS, videoModel, videoResolutionFor, nearestResolution, segmentPlan, videoReferences,
@@ -262,6 +262,35 @@ const breaks = (gateId, project, episode, needle) => {
262
262
  const p5 = clone(PROJECT); p5.props[0].states = [];
263
263
  breaks('prop-design', p5, null, 'state');
264
264
  }
265
+ {
266
+ const missing = clone(PROJECT);
267
+ delete missing.scenes[0].condition;
268
+ delete missing.scenes[0].conditionReason;
269
+ breaks('scene-design', missing, null, 'condition must be new, lived-in or worn');
270
+ const fresh = clone(PROJECT);
271
+ fresh.scenes[0].condition = 'new';
272
+ fresh.scenes[0].conditionReason = 'the organisers finished this set this morning';
273
+ breaks('scene-design', fresh, null, 'condition is new but');
274
+ const clean = clone(PROJECT);
275
+ clean.scenes[1].condition = 'new';
276
+ clean.scenes[1].conditionReason = 'a pavilion built for this episode and struck the next day';
277
+ const scrub = (text) => String(text || '').replace(/\b(worn|rust|rusty|rusted|faded|chipped|torn|frayed|peeling|stain|stained|stains)\b/gi, 'plain').replace(/殘舊|残旧|生鏽|生锈|褪色|剝落|剥落/g, '');
278
+ for (const a of clean.scenes[1].anchors) { a.name = scrub(a.name); a.desc = scrub(a.desc); }
279
+ clean.scenes[1].image.prompt = scrub(clean.scenes[1].image.prompt);
280
+ clean.scenes[1].image.sheet = scrub(clean.scenes[1].image.sheet);
281
+ for (const l of clean.scenes[1].lighting) l.prompt = scrub(l.prompt);
282
+ passes('scene-design', clean, null);
283
+ const ban = clone(PROJECT);
284
+ ban.scenes[0].image.negativePrompt += ', brand-new, showroom-clean';
285
+ breaks('scene-design', ban, null, 'only condition worn may ban');
286
+ const propMissing = clone(PROJECT);
287
+ delete propMissing.props[0].conditionReason;
288
+ breaks('prop-design', propMissing, null, 'conditionReason');
289
+ const propNew = clone(PROJECT);
290
+ propNew.props[0].condition = 'new';
291
+ propNew.props[0].conditionReason = 'a drive the organisers just issued';
292
+ breaks('prop-design', propNew, null, 'condition is new but');
293
+ }
265
294
 
266
295
  // ── script gates ────────────────────────────────────────────────────────────
267
296
  {
@@ -479,7 +508,8 @@ const breaks = (gateId, project, episode, needle) => {
479
508
  const results = runGates(makeContext(PROJECT, { 1: e }));
480
509
  const adv = results.find((r) => r.id === 'dialogue-register');
481
510
  ok(adv.advisory && adv.failures.length === 0 && adv.warnings.length > 0, 'advisory findings come back as warnings, not failures');
482
- ok(results.filter((r) => !r.advisory && r.stage !== 'storyboard').every((r) => !r.failures.length), 'a register note never fails validate (the storyboard is stale on purpose here)');
511
+ ok(results.filter((r) => !r.advisory && r.stage !== 'storyboard' && r.id !== 'dialogue-review').every((r) => !r.failures.length), 'a register note never fails validate (the storyboard is stale on purpose here)');
512
+ ok(results.find((r) => r.id === 'dialogue-review').failures.some((m) => m.includes('differs from the reviewed one')), 'an edited line is no longer reviewed — dialogue-review asks for it again');
483
513
  const p2 = clone(PROJECT); p2.params.register = '粤语';
484
514
  ok(failures('dialogue-register', p2, E01).some((m) => m.includes('书面中文硬转')), 'a 粤语 drama with written-Chinese lines is noted');
485
515
  const p3 = clone(PROJECT); p3.params.register = '古装';
@@ -1681,19 +1711,23 @@ const breaks = (gateId, project, episode, needle) => {
1681
1711
  const script = renderScript(PROJECT, E01);
1682
1712
  ok(script.includes('穿雨衣的男人(') && script.includes('VO'), 'script markdown names speakers and VO');
1683
1713
  ok(script.includes('△(0-3秒)') && script.includes('【爽点标记】') && script.includes('【钩子标记】'), 'script markdown is in the 短剧 episode format with derived time marks');
1684
- ok(script.includes('## 本集功能') && script.includes('## 本集任务') && script.includes('## 创作自检') && script.includes('16/16'), 'script markdown carries the episode design and a full self-check');
1714
+ ok(script.includes('## 本集功能') && script.includes('## 本集任务') && script.includes('## 创作自检') && script.includes('18/18') && script.includes('对白是否已按语体 guide 逐句校对'), 'script markdown carries the episode design and a full self-check');
1685
1715
  const guide = renderGuideline(PROJECT);
1686
1716
  ok(guide.includes('创作指引') && guide.includes('## 9. 本项目绝对要注意的重点') && !guide.includes('{{'), 'guideline markdown is complete with no placeholders');
1687
1717
  ok(guide.includes(PROJECT.outline.brief.logline), 'guideline carries the logline');
1688
- const dlg = renderDialogue(PROJECT, E01);
1718
+ const blank = clone(E01); delete blank.dialogueReview;
1719
+ const dlg = renderDialogue(PROJECT, blank);
1689
1720
  eq((dlg.match(/^\| [^|]*:/gm) || []).length, 13, 'dialogue worksheet has one row per line');
1690
1721
  ok(dlg.includes('禁用:其实、我觉得'), 'dialogue worksheet carries the speaker\'s speech rules');
1722
+ ok(dlg.includes('lang-zh-cn.md') && dlg.includes('ref.mjs --lang zh-cn list --age elder'), 'dialogue worksheet points each speaker at their guide and corpus query');
1723
+ const filled = renderDialogue(PROJECT, E01);
1724
+ ok(filled.includes('| 修改幅度 | 轻修 |') && filled.includes('(保留)') && filled.includes('## 自检'), 'a filled review renders as the 对白校对 table');
1691
1725
  const c1 = renderCharacter(PROJECT, 'C01');
1692
1726
  ok(c1.includes('## 人物圣经') && c1.includes('### 过去的关键选择') && c1.includes('## 人物关系网') && c1.includes('### 写作备忘'), 'character markdown has the bible sections');
1693
1727
  const out = renderOutline(PROJECT);
1694
1728
  ok(out.includes('## 创作简报') && out.includes('## 全剧结构') && out.includes('## 分集目录'), 'outline markdown has brief, structure and episode index');
1695
1729
  const check = scriptSelfCheck(PROJECT, E01);
1696
- eq(check.score, 16, 'fixture episode scores 16/16');
1730
+ eq(check.score, 18, 'fixture episode scores 18/18');
1697
1731
  const segMd = renderSegment(PROJECT, E01, 'E01-02');
1698
1732
  ok(segMd.includes('| 00:02.000 |') && segMd.includes('integrated_multimodal_description'), 'segment markdown has cut table and prompt');
1699
1733
  ok(segMd.includes('### SHOT 1 · establish') && segMd.includes('| 唯一动作 |') && segMd.includes('| 视觉依据 |') && segMd.includes('轴线:'), 'segment markdown has the SHOT blocks, the axis and the sound line');
@@ -1701,6 +1735,86 @@ const breaks = (gateId, project, episode, needle) => {
1701
1735
  assert.throws(() => renderCharacter(PROJECT, 'C99'), /no character/, 'render refuses unknown ids');
1702
1736
  }
1703
1737
 
1738
+ // ── dialogue review (w-film-dialogue-editor) ────────────────────────────────
1739
+ {
1740
+ passes('dialogue-review', PROJECT, E01);
1741
+ ok(GATES.find((g) => g.id === 'dialogue-review').advisory === false, 'dialogue-review blocks');
1742
+ eq(DIALOGUE_GUIDES['粤语'].lang, 'zh-hk', '粤语 reads the zh-hk guide and corpus');
1743
+ eq(DIALOGUE_GUIDES['古装'].corpus, false, '古装 has a guide but no corpus');
1744
+ eq(corpusAge(24), 'young', 'a 24-year-old queries the young band');
1745
+ eq(corpusAge(61), 'elder', 'a 61-year-old queries the elder band');
1746
+ const none = clone(E01); delete none.dialogueReview;
1747
+ breaks('dialogue-review', PROJECT, none, 'seed <project> dialogue');
1748
+ const pOff = clone(PROJECT); pOff.params.dialogueReview = 'off';
1749
+ passes('dialogue-review', pOff, none);
1750
+ const lvl = clone(E01); lvl.dialogueReview.level = '';
1751
+ breaks('dialogue-review', PROJECT, lvl, 'level');
1752
+ const diag = clone(E01); diag.dialogueReview.diagnosis.subtext = '';
1753
+ breaks('dialogue-review', PROJECT, diag, '潜台词核心');
1754
+ const guide = clone(E01); guide.dialogueReview.guides = [];
1755
+ breaks('dialogue-review', PROJECT, guide, 'lang-zh-cn.md');
1756
+ const yue = clone(PROJECT); yue.characters.find((c) => c.id === 'C02').bible.speech.register = '粤语';
1757
+ breaks('dialogue-review', yue, E01, 'lang-zh-hk.md');
1758
+ const miss = clone(E01); miss.dialogueReview.rows.pop();
1759
+ breaks('dialogue-review', PROJECT, miss, 'no review row');
1760
+ const stale = clone(E01); stale.scenes[1].flow[2].line = '又轮到你值夜班。';
1761
+ breaks('dialogue-review', PROJECT, stale, 'differs from the reviewed one');
1762
+ const noIssue = clone(E01); noIssue.dialogueReview.rows[0].issue = '';
1763
+ breaks('dialogue-review', PROJECT, noIssue, 'issue is empty');
1764
+ const noWhy = clone(E01); noWhy.dialogueReview.rows[0].revised = '帮我收着,明天有人拿。'; noWhy.scenes[0].flow[1].line = '帮我收着,明天有人拿。';
1765
+ breaks('dialogue-review', PROJECT, noWhy, 'without a reason');
1766
+ const mood = clone(E01); mood.dialogueReview.rows[0].note = '自然一点';
1767
+ breaks('dialogue-review', PROJECT, mood, 'a mood, not an action');
1768
+ const emo = clone(E01); emo.dialogueReview.rows[1].note = '生气';
1769
+ breaks('dialogue-review', PROJECT, emo, 'a mood, not an action');
1770
+ const check = clone(E01); check.dialogueReview.selfCheck = check.dialogueReview.selfCheck.slice(0, 2);
1771
+ breaks('dialogue-review', PROJECT, check, '自检清单');
1772
+ const checkNo = clone(E01); checkNo.dialogueReview.selfCheck[0] = { item: '读出声', ok: false, note: '' };
1773
+ breaks('dialogue-review', PROJECT, checkNo, 'failed with no note');
1774
+ const ghost = clone(E01); ghost.dialogueReview.rows.push({ scene: 1, beat: 1, speaker: 'C01', original: 'x', issue: 'x', revised: 'x', reason: '', note: '逼' });
1775
+ breaks('dialogue-review', PROJECT, ghost, 'no longer a line');
1776
+
1777
+ // seed keeps filled rows for unchanged lines and opens only the edited one
1778
+ const edited = clone(E01); edited.scenes[1].flow[2].line = '又轮到你值夜班。';
1779
+ const reseeded = seedDialogue(PROJECT, edited, { skillDir: null });
1780
+ eq(reseeded.rows.length, 13, 're-seed keeps one row per line');
1781
+ eq(reseeded.rows.filter((r) => !r.issue).length, 1, 're-seed reopens only the edited line');
1782
+ eq(reseeded.level, '轻修', 're-seed keeps the level and diagnosis');
1783
+ eq(seedDialogue(PROJECT, edited, { skillDir: null, force: true }).rows.filter((r) => !r.issue).length, 13, '--force resets every row');
1784
+ ok(!('refs' in reseeded), 'the stored review carries no machine paths');
1785
+ const refs = dialogueRefs(PROJECT, E01, { skillDir: '/x' });
1786
+ ok(refs.speakers.every((x) => x.guide === '/x/references/lang-zh-cn.md'), 'every speaker gets the zh-cn guide');
1787
+ ok(refs.speakers.find((x) => x.id === 'C02').query.includes('--age elder'), "the sixty-year-old's corpus query is the elder band");
1788
+ ok(!refs.speakers.some((x) => x.id === 'VO'), 'VO is not a speaker with a guide');
1789
+
1790
+ // apply writes revised lines and deliveries back, then the gate is green again
1791
+ const app = clone(E01); app.dialogueReview.rows[0].revised = '帮我收着,明天有人拿。'; app.dialogueReview.rows[0].reason = '更短,一口气说完'; app.dialogueReview.rows[0].delivery = '压着嗓子,塞完就走';
1792
+ breaks('dialogue-review', PROJECT, app, 'differs from the reviewed one');
1793
+ eq(applyDialogue(app), 2, 'apply writes the line and the delivery');
1794
+ eq(app.scenes[0].flow[1].line, '帮我收着,明天有人拿。', 'the revised line is in the script');
1795
+ passes('dialogue-review', PROJECT, app);
1796
+ const bad = clone(E01); bad.dialogueReview.rows[0].revised = '';
1797
+ assert.throws(() => applyDialogue(bad), /revised is empty/, 'apply refuses an empty line'); passed += 1;
1798
+ ok(scriptSelfCheck(PROJECT, none).rows.some((r) => r[0].includes('逐句校对') && r[1] === false), 'the 创作自检 says 否 without a review');
1799
+
1800
+ // seed / apply through the CLI
1801
+ const dir = mkdtempSync(join(tmpdir(), 'sd-dlg-'));
1802
+ try {
1803
+ writeFileSync(join(dir, 'project.json'), JSON.stringify(PROJECT));
1804
+ spawnSync('mkdir', ['-p', join(dir, 'episodes')]);
1805
+ writeFileSync(join(dir, 'episodes/E01.json'), JSON.stringify(none));
1806
+ const engine = join(here, 'short-drama.mjs');
1807
+ const r1 = spawnSync(process.execPath, [engine, 'seed', dir, 'dialogue', '--ep', '1'], { encoding: 'utf8' });
1808
+ eq(r1.status, 0, `seed dialogue runs: ${r1.stderr}`);
1809
+ ok(r1.stdout.includes('13 lines (13 to review)') && r1.stdout.includes('lang-zh-cn.md'), 'seed dialogue prints the guide per speaker');
1810
+ const r2 = spawnSync(process.execPath, [engine, 'apply', dir, 'dialogue', '--ep', '1'], { encoding: 'utf8' });
1811
+ eq(r2.status, 0, `apply dialogue runs: ${r2.stderr}`);
1812
+ ok(r2.stdout.includes('0 change(s)'), 'apply on an untouched review changes nothing');
1813
+ } finally {
1814
+ rmSync(dir, { recursive: true, force: true });
1815
+ }
1816
+ }
1817
+
1704
1818
  // ── manifest & bind ─────────────────────────────────────────────────────────
1705
1819
  {
1706
1820
  const items = manifest(PROJECT, { 1: E01 });
@@ -2414,10 +2528,41 @@ const breaks = (gateId, project, episode, needle) => {
2414
2528
  eq(slug('E01-01 便利店 / 夜班'), 'e01-01-便利店-夜班', 'slug keeps CJK, drops separators');
2415
2529
  }
2416
2530
 
2531
+ // ── ref-binding (参考图绑定) ─────────────────────────────────────────────────
2532
+ {
2533
+ // The picture plan has to survive into the delivery text: a legend line that names WHO/WHAT each
2534
+ // picture is, a "not its composition / its people" clause on everything that is not a person, and
2535
+ // the label named again inside every shot that depends on it.
2536
+ const p = clone(PROJECT);
2537
+ p.params.video = 'seedance';
2538
+ p.params.references = 'sheets';
2539
+ p.params.videoDuration = 'auto';
2540
+ const e = clone(E01);
2541
+ const seg = e.storyboard.segments[3];
2542
+ const refs = segmentReferences(p, e, seg).filter((r) => r.kind === 'image');
2543
+ const legend = refs.map((r) => `${r.label} is ${r.use}.`).join('\n');
2544
+ const shots = seg.cuts
2545
+ .map((c, k) => `Shot ${k + 1} (${k}.0\u2013${k + 1}.0 s, medium shot, Static Shot): ${refs.map((r) => r.label).join(' and ')} hold.`)
2546
+ .join('\n');
2547
+ const good = `${legend}\n\n${shots}`;
2548
+ const bound = clone(e);
2549
+ bound.storyboard.segments[3].prompt = good;
2550
+ eq(failures('ref-binding', p, bound).filter((m) => m.startsWith('E01-04')).length, 0, 'ref-binding passes a fully bound prompt');
2551
+ const generic = clone(e);
2552
+ generic.storyboard.segments[3].prompt = good.replace(/@image2 is the character sheet of [^\n]*/, '@image2 is the character sheet of one person in this segment \u2014 match face, hair and clothes exactly.');
2553
+ breaks('ref-binding', p, generic, 'never says WHICH person');
2554
+ const noNegative = clone(e);
2555
+ noNegative.storyboard.segments[3].prompt = good.replace(/@image1 is the environment[^\n]*/, '@image1 is the environment \u2014 match its layout, materials and wear exactly.');
2556
+ breaks('ref-binding', p, noNegative, 'never says what NOT');
2557
+ const unused = clone(e);
2558
+ unused.storyboard.segments[3].prompt = `${legend}\n\n${shots.replaceAll('@image2', 'a person')}`;
2559
+ breaks('ref-binding', p, unused, 'used in no shot');
2560
+ }
2561
+
2417
2562
  // ── gate inventory (crew engine) ────────────────────────────────────────────
2418
2563
  {
2419
- eq(GATES.length, 69, 'gate count');
2420
- eq(GATES.filter((g) => !g.advisory).length, 63, 'hard gate count');
2564
+ eq(GATES.length, 71, 'gate count');
2565
+ eq(GATES.filter((g) => !g.advisory).length, 65, 'hard gate count');
2421
2566
  eq(GATES.filter((g) => g.advisory).length, 6, 'advisory gate count');
2422
2567
  ok(!GATES.find((g) => g.id === 'boundary-contrast').advisory, 'boundary-contrast is hard');
2423
2568
  ok(GATES.find((g) => g.id === 'boundary-contrast-episode').advisory, 'boundary-contrast-episode is advisory');
@@ -2539,8 +2684,8 @@ const breaks = (gateId, project, episode, needle) => {
2539
2684
  const results = runGates(makeContext(PROJECT, { 1: E01 }));
2540
2685
  const rep = formatGates(results);
2541
2686
  eq(Object.keys(rep).sort().join(','), 'advisory,failed,hard,lines,notes,summary,text', 'formatGates shape');
2542
- eq(rep.summary, '✓ 63 gate(s) passed, 6 advisory (1 note)', 'fixture summary');
2543
- eq(rep.failed, 0, 'nothing failed'); eq(rep.hard, 63, 'hard count'); eq(rep.advisory, 6, 'advisory count');
2687
+ eq(rep.summary, '✓ 65 gate(s) passed, 6 advisory (1 note)', 'fixture summary');
2688
+ eq(rep.failed, 0, 'nothing failed'); eq(rep.hard, 65, 'hard count'); eq(rep.advisory, 6, 'advisory count');
2544
2689
  // The fixture now raises one advisory (E01-03 carries the 爽点 and is not cut as faceslap),
2545
2690
  // exactly as it does in the wji-cli copy — advisory lines print, failures still must not.
2546
2691
  eq(rep.lines.filter((l) => l.includes('✗')).length, 0, 'a green run prints no failed gates');