@wji.ai/cli 0.9.0 → 0.9.2

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 (70) hide show
  1. package/README.md +7 -1
  2. package/dist/index.js +156 -11
  3. package/package.json +1 -1
  4. package/skill/backreel/SKILL.md +6 -2
  5. package/skill/backreel/reference/episodes.md +45 -7
  6. package/skill/short-drama/SKILL.md +27 -4
  7. package/skill/short-drama/examples/night-shift/episodes/E01.json +173 -1
  8. package/skill/short-drama/examples/night-shift/project.json +6 -0
  9. package/skill/short-drama/reference/dialogue-craft.md +16 -11
  10. package/skill/short-drama/reference/prompt-audit.mjs +308 -0
  11. package/skill/short-drama/reference/reference-binding.md +84 -0
  12. package/skill/short-drama/reference/schema.md +14 -5
  13. package/skill/short-drama/reference/stage-characters.md +1 -1
  14. package/skill/short-drama/reference/stage-script.md +54 -7
  15. package/skill/short-drama/reference/stage-storyboard.md +29 -24
  16. package/skill/short-drama/scripts/selftest.mjs +154 -9
  17. package/skill/short-drama/scripts/short-drama.mjs +467 -23
  18. package/skill/w-film-dialogue-editor/CHANGELOG.md +44 -0
  19. package/skill/w-film-dialogue-editor/SKILL.md +124 -0
  20. package/skill/w-film-dialogue-editor/references/data/README.md +222 -0
  21. package/skill/w-film-dialogue-editor/references/data/en/core.json +1666 -0
  22. package/skill/w-film-dialogue-editor/references/data/en/slang.json +1602 -0
  23. package/skill/w-film-dialogue-editor/references/data/en/work.json +1722 -0
  24. package/skill/w-film-dialogue-editor/references/data/en-sentences.csv +134 -0
  25. package/skill/w-film-dialogue-editor/references/data/en-sentences.md +146 -0
  26. package/skill/w-film-dialogue-editor/references/data/ja/core.json +1978 -0
  27. package/skill/w-film-dialogue-editor/references/data/ja/slang.json +1927 -0
  28. package/skill/w-film-dialogue-editor/references/data/ja/work.json +1972 -0
  29. package/skill/w-film-dialogue-editor/references/data/ja-sentences.csv +134 -0
  30. package/skill/w-film-dialogue-editor/references/data/ja-sentences.md +146 -0
  31. package/skill/w-film-dialogue-editor/references/data/ko/core.json +1839 -0
  32. package/skill/w-film-dialogue-editor/references/data/ko/slang.json +1897 -0
  33. package/skill/w-film-dialogue-editor/references/data/ko/work.json +1829 -0
  34. package/skill/w-film-dialogue-editor/references/data/ko-sentences.csv +134 -0
  35. package/skill/w-film-dialogue-editor/references/data/ko-sentences.md +146 -0
  36. package/skill/w-film-dialogue-editor/references/data/zh-cn/core.json +1794 -0
  37. package/skill/w-film-dialogue-editor/references/data/zh-cn/slang.json +1609 -0
  38. package/skill/w-film-dialogue-editor/references/data/zh-cn/work.json +1648 -0
  39. package/skill/w-film-dialogue-editor/references/data/zh-cn-sentences.csv +134 -0
  40. package/skill/w-film-dialogue-editor/references/data/zh-cn-sentences.md +146 -0
  41. package/skill/w-film-dialogue-editor/references/data/zh-hk/core.json +2476 -0
  42. package/skill/w-film-dialogue-editor/references/data/zh-hk/gen-z.json +3263 -0
  43. package/skill/w-film-dialogue-editor/references/data/zh-hk/older.json +3632 -0
  44. package/skill/w-film-dialogue-editor/references/data/zh-hk/rough.json +1668 -0
  45. package/skill/w-film-dialogue-editor/references/data/zh-hk/workplace.json +3697 -0
  46. package/skill/w-film-dialogue-editor/references/data/zh-hk-sentences.csv +373 -0
  47. package/skill/w-film-dialogue-editor/references/data/zh-hk-sentences.md +391 -0
  48. package/skill/w-film-dialogue-editor/references/data/zh-tw/core.json +1640 -0
  49. package/skill/w-film-dialogue-editor/references/data/zh-tw/slang.json +1537 -0
  50. package/skill/w-film-dialogue-editor/references/data/zh-tw/work.json +1699 -0
  51. package/skill/w-film-dialogue-editor/references/data/zh-tw-sentences.csv +132 -0
  52. package/skill/w-film-dialogue-editor/references/data/zh-tw-sentences.md +144 -0
  53. package/skill/w-film-dialogue-editor/references/dialogue_revision_framework.md +84 -0
  54. package/skill/w-film-dialogue-editor/references/en-index.md +265 -0
  55. package/skill/w-film-dialogue-editor/references/ja-index.md +633 -0
  56. package/skill/w-film-dialogue-editor/references/ko-index.md +498 -0
  57. package/skill/w-film-dialogue-editor/references/lang-en.md +242 -0
  58. package/skill/w-film-dialogue-editor/references/lang-ja.md +312 -0
  59. package/skill/w-film-dialogue-editor/references/lang-ko.md +290 -0
  60. package/skill/w-film-dialogue-editor/references/lang-zh-cn.md +60 -0
  61. package/skill/w-film-dialogue-editor/references/lang-zh-hk-mandarin.md +34 -0
  62. package/skill/w-film-dialogue-editor/references/lang-zh-hk.md +306 -0
  63. package/skill/w-film-dialogue-editor/references/lang-zh-period.md +38 -0
  64. package/skill/w-film-dialogue-editor/references/lang-zh-tw.md +57 -0
  65. package/skill/w-film-dialogue-editor/references/zh-cn-index.md +636 -0
  66. package/skill/w-film-dialogue-editor/references/zh-hk-index.md +1450 -0
  67. package/skill/w-film-dialogue-editor/references/zh-tw-index.md +457 -0
  68. package/skill/w-film-dialogue-editor/scripts/rebuild.sh +13 -0
  69. package/skill/w-film-dialogue-editor/scripts/ref.mjs +539 -0
  70. package/skill/w-film-dialogue-editor/templates/dialogue_revision_report.md +43 -0
@@ -588,6 +588,8 @@
588
588
  "primary": true,
589
589
  "reusePlan": "",
590
590
  "summary": "凌晨两点的便利店:亮得刺眼、空得发冷,收银台是全店唯一有人气的地方,所有的戏都在那一米柜台前后发生。",
591
+ "condition": "lived-in",
592
+ "conditionReason": "这家便利店每晚都在营业:柜台和抽屉有日常使用的痕迹,不是残破,也不是刚搭好的布景。",
591
593
  "anchors": [
592
594
  {
593
595
  "name": "关东煮灯箱",
@@ -647,6 +649,8 @@
647
649
  "primary": true,
648
650
  "reusePlan": "",
649
651
  "summary": "店门外一米宽的雨棚:里面是灯,外面是雨,站在这里的人一半亮一半黑——所有来历不明的人都从这里进出。",
652
+ "condition": "lived-in",
653
+ "conditionReason": "雨棚在雨里站了很多年,红白条已经褪色,但店还在开,不是新搭的布景。",
650
654
  "anchors": [
651
655
  {
652
656
  "name": "褪色的红白雨棚",
@@ -709,6 +713,8 @@
709
713
  ],
710
714
  "scale": "handheld",
711
715
  "summary": "一块旧的 3.5 寸机械硬盘,装在防静电袋里,标签被撕掉一半。",
716
+ "condition": "worn",
717
+ "conditionReason": "这是三年前的监控硬盘:标签撕掉一半,接口生锈,不是主办方刚发的新道具。",
712
718
  "anchors": [
713
719
  {
714
720
  "name": "撕掉一半的标签",
@@ -1,9 +1,12 @@
1
1
  # Dialogue craft — lines as action, in the right 语体
2
2
 
3
- Distilled from the user's `w-film-dialogue-editor` skill. Read it before writing a script's
4
- lines and before any polish pass (`render dialogue --ep N`). What can be checked is the
5
- **advisory** `dialogue-register` gate — it reports, it never blocks, because a 穿越 character
6
- speaking 古装 in a modern drama is right, not wrong.
3
+ A summary of the user's `w-film-dialogue-editor` skill, which is installed beside this one and is
4
+ the source of truth: its revision framework, one language guide per 语体 and a corpus of real
5
+ usage queried with `scripts/ref.mjs`. This page is the short version to keep in mind; the guide of
6
+ the drama's 语体 is read in full before writing lines and again in the dialogue review (§3). Two
7
+ gates: the **advisory** `dialogue-register` notes lines that break the register (a 穿越 character
8
+ speaking 古装 in a modern drama is right, not wrong), and the **required** `dialogue-review` holds
9
+ the storyboard until every line has been reviewed.
7
10
 
8
11
  ## 1. A line is an action under pressure
9
12
 
@@ -94,14 +97,16 @@ a 6–18-month half-life: two or three signature words per character, no more. P
94
97
  vertical short drama allows damn / hell / ass / limited bitch, avoids the f-word; the first three
95
98
  seconds are clean.
96
99
 
97
- ## 3. The polish pass — `render dialogue --ep N`
100
+ ## 3. The dialogue review — `seed dialogue` → fill → `apply dialogue`
98
101
 
99
- Produces the reference's 逐句校对表: 原句 filled, 问题 / 修改句 / 修改理由 / 导演/演员提示 to fill,
100
- with each speaker's `bible.speech` summary in the last column. Levels: 轻修 (keep the sentence,
101
- fix tone, rhythm, filler), 中修 (restructure, cut explanation, sharpen the attack and defence),
102
- 重修 (re-design the beats and strategies, keep the story intent). Do it when the user asks, when
103
- dialogue is the visible weakness, or when the register gate keeps noting the same scene. The
104
- filled table is appended to the screenplay dataset under `## 对白校对`.
102
+ Required after every script (`stage-script.md` § Dialogue review). `seed dialogue --ep N` writes
103
+ `episode.dialogueReview` — one row per line with 原句 filled — and prints the guide and the corpus
104
+ query for each speaker. Fill it the way the dialogue skill reviews a scene: the diagnosis first,
105
+ then per line 问题 / 修改句 / 修改理由 / 导演/演员提示, then the guide's 自检清单. Levels: 轻修 (keep
106
+ the sentence, fix tone, rhythm, filler), 中修 (restructure, cut explanation, sharpen the attack and
107
+ defence), 重修 (re-design the beats and strategies, keep the story intent). `apply dialogue` writes
108
+ the revised lines into the script; `render dialogue` prints the filled table, which is appended to
109
+ the screenplay dataset under `## 对白校对`.
105
110
 
106
111
  | 原句 | 问题 | 修改句 | 修改理由 |
107
112
  | --- | --- | --- | --- |
@@ -0,0 +1,308 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * prompt-audit.mjs — reference-binding proofreader for AI short-drama video prompts.
4
+ *
5
+ * It does NOT rewrite the prompt. It reads the prompt as TEXT, compares it with what the
6
+ * project says must be on screen (cuts → characters / props / scene), and reports every place
7
+ * where the model could mistake which @imageN is a person, which is the room, which is a prop,
8
+ * or where a shot simply never mentions the reference it depends on.
9
+ *
10
+ * usage:
11
+ * node prompt-audit.mjs --project <dir> --ep 1 --segment E01-02 --prompt <file> [--json]
12
+ *
13
+ * exit code: 0 clean (warnings allowed), 1 errors found.
14
+ */
15
+
16
+ import { readFileSync } from 'node:fs';
17
+ import { resolve, join } from 'node:path';
18
+
19
+ // ── args ────────────────────────────────────────────────────────────────────
20
+ const argv = process.argv.slice(2);
21
+ const arg = (name, fallback = null) => {
22
+ const i = argv.indexOf(`--${name}`);
23
+ return i >= 0 && argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[i + 1] : fallback;
24
+ };
25
+ const PROJECT = arg('project');
26
+ const EP = Number(arg('ep', 1));
27
+ const SEGMENT = arg('segment');
28
+ const PROMPT_FILE = arg('prompt');
29
+ const AS_JSON = argv.includes('--json');
30
+ if (!PROJECT || !SEGMENT || !PROMPT_FILE) {
31
+ console.error('usage: node prompt-audit.mjs --project <dir> --ep 1 --segment E01-02 --prompt <file> [--json]');
32
+ process.exit(2);
33
+ }
34
+
35
+ const project = JSON.parse(readFileSync(resolve(PROJECT, 'project.json'), 'utf8'));
36
+ const epFile = join(resolve(PROJECT, 'episodes'), `E${String(EP).padStart(2, '0')}.json`);
37
+ const episode = JSON.parse(readFileSync(epFile, 'utf8'));
38
+ const prompt = readFileSync(resolve(PROMPT_FILE), 'utf8')
39
+ .split('\n')
40
+ .filter((l) => !l.startsWith('#')) // drop `prompt` command's header comment line
41
+ .join('\n')
42
+ .trim();
43
+
44
+ const seg = (episode.storyboard?.segments || []).find((s) => s.id === SEGMENT);
45
+ if (!seg) {
46
+ console.error(`no segment ${SEGMENT} in ${epFile}`);
47
+ process.exit(2);
48
+ }
49
+ const scene = (episode.scenes || [])[Number(seg.sceneIndex) - 1] || {};
50
+ const params = project.params || {};
51
+ const charById = (id) => (project.characters || []).find((c) => c.id === id);
52
+ const sceneById = (id) => (project.scenes || []).find((s) => s.id === id);
53
+ const propById = (id) => (project.props || []).find((p) => p.id === id);
54
+
55
+ // ── what the project says the segment needs ────────────────────────────────
56
+ /** Reference plan in connection order, the same way the engine derives it (env → people → props → voices). */
57
+ function planOf(seg) {
58
+ const imgs = [];
59
+ if (Array.isArray(seg.refOverride) && seg.refOverride.length) {
60
+ for (const r of seg.refOverride) imgs.push({ entity: r.entity, asset: r.asset || 'sheet', declared: r.role || null });
61
+ } else {
62
+ if (scene.sceneId) {
63
+ imgs.push({ entity: scene.sceneId, asset: 'sheet', declared: 'scene' });
64
+ const se = sceneById(scene.sceneId);
65
+ if (scene.lighting && se?.wji?.assets?.lighting?.[scene.lighting]) imgs.push({ entity: scene.sceneId, asset: `lighting:${scene.lighting}`, declared: 'lighting' });
66
+ }
67
+ const seen = new Set();
68
+ for (const c of seg.cuts || []) for (const id of c.characters || []) if (!seen.has(id)) { seen.add(id); imgs.push({ entity: id, asset: 'sheet', declared: 'character' }); }
69
+ for (const c of seg.cuts || []) for (const id of c.props || []) if (!seen.has(id)) { seen.add(id); imgs.push({ entity: id, asset: 'sheet', declared: 'prop' }); }
70
+ }
71
+ imgs.forEach((r, i) => (r.label = `@image${i + 1}`));
72
+ const speakers = [];
73
+ for (const c of seg.cuts || []) for (const id of c.characters || []) if (id !== 'VO' && !speakers.includes(id)) speakers.push(id);
74
+ const voices = speakers.map((id, i) => ({ entity: id, label: `@audio${i + 1}`, kind: 'audio' }));
75
+ return { imgs, voices };
76
+ }
77
+
78
+ const roleOfEntity = (id, asset, declared) => {
79
+ if (declared) return declared;
80
+ if (/^lighting:/.test(String(asset))) return 'lighting';
81
+ if (/^S/i.test(id)) return 'scene';
82
+ if (/^P/i.test(id)) return 'prop';
83
+ if (/^C/i.test(id)) return 'character';
84
+ return 'unknown';
85
+ };
86
+
87
+ const plan = planOf(seg);
88
+ for (const r of plan.imgs) r.role = roleOfEntity(r.entity, r.asset, r.declared);
89
+
90
+ // ── identity tokens: how a legend can prove WHICH person a picture is ──────
91
+ const CN_STOP = new Set(['男的', '女的', '一個', '一个', 'the', 'and', 'with', 'her', 'his', 'man', 'woman']);
92
+ function tokensOf(ch) {
93
+ const t = new Set();
94
+ const add = (s) => {
95
+ for (const w of String(s || '').toLowerCase().split(/[^a-z0-9\u4e00-\u9fff]+/)) if (w.length > 2 && !CN_STOP.has(w)) t.add(w);
96
+ for (const run of String(s || '').match(/[\u4e00-\u9fff]+/g) || []) {
97
+ if (run.length <= 3) t.add(run);
98
+ for (let i = 0; i + 1 < run.length && run.length > 3; i++) t.add(run.slice(i, i + 2));
99
+ for (let i = 0; i + 2 < run.length; i++) t.add(run.slice(i, i + 3));
100
+ }
101
+ };
102
+ add(ch.identityEn);
103
+ add(ch.profile?.appearance);
104
+ add(ch.profile?.identity);
105
+ const g = ch.profile?.gender;
106
+ if (g) t.add(g === '男' ? '男' : '女');
107
+ return t;
108
+ }
109
+
110
+ // ── prompt parsing ─────────────────────────────────────────────────────────
111
+ const SHOT_RE = /(?:^|\n)[ \t]*(?:镜头|鏡頭|Shot)[ \t]*(\d+)/g;
112
+ const shotMarks = [];
113
+ for (let m; (m = SHOT_RE.exec(prompt)); ) shotMarks.push({ k: Number(m[1]), at: m.index + (m[0].startsWith('\n') ? 1 : 0) });
114
+ const firstShotAt = shotMarks.length ? shotMarks[0].at : prompt.length;
115
+ const head = prompt.slice(0, firstShotAt); // legend + global blocks
116
+ const shots = shotMarks.map((s, i) => ({
117
+ k: s.k,
118
+ text: prompt.slice(s.at, i + 1 < shotMarks.length ? shotMarks[i + 1].at : prompt.length),
119
+ }));
120
+ const legend = head.split('\n').filter((l) => /@image\d+/.test(l)).join('\n');
121
+
122
+ const IMG_RE = /@image(\d+)/g;
123
+ const labelPositions = new Map(); // label -> { first, count, span }
124
+ const legendLines = head.split('\n').filter((l) => /^@image\d+\b/.test(l.trim()));
125
+ {
126
+ const all = [...head.matchAll(IMG_RE)];
127
+ for (let i = 0; i < all.length; i++) {
128
+ const label = `@image${all[i][1]}`;
129
+ const from = all[i].index;
130
+ const to = i + 1 < all.length ? all[i + 1].index : head.length;
131
+ const prev = labelPositions.get(label);
132
+ const mine = legendLines.filter((l) => l.trim().startsWith(label));
133
+ const lines = mine.length;
134
+ const span = lines ? mine[0].trim().slice(label.length) : head.slice(from, to);
135
+ labelPositions.set(label, { first: from, count: (prev?.count || 0) + 1, lines, span });
136
+ }
137
+ }
138
+
139
+ const countIn = (text, label) => (text.match(new RegExp(`${label}(?!\\d)`, 'g')) || []).length;
140
+
141
+ // ── checks ─────────────────────────────────────────────────────────────────
142
+ const issues = [];
143
+ const add = (level, code, msg) => issues.push({ level, code, msg });
144
+ const ROLE_EN = /\b(scene|environment|character|person|prop|lighting|light reference|moment|action reference|style)\b/i;
145
+ const ROLE_ZH = /(場景|场景|環境|环境|人物|角色|道具|光影|光線|光线|瞬間|瞬间|撞擊|撞击|動作參考|动作参考|風格|风格|車廂|车厢)/;
146
+ const BIND_EN = /\b(match|as in|from|same as|use)\b/i;
147
+ const BIND_ZH = /(照|參照|参照|跟|用|依|係|是)/;
148
+ const NEG_EN = /\b(not|only|do not|don't)\b/i;
149
+ const NEG_ZH = /(唔照|不要照|唔好照|只照|只取|唔要|不要|只係|只是|只)/;
150
+ const hasCJK = (t) => /[\u4e00-\u9fff]/.test(String(t || ''));
151
+ const ROLE_WORDS = { test: (t) => (hasCJK(t) ? ROLE_ZH : ROLE_EN).test(t) };
152
+ const BIND_WORDS = { test: (t) => (hasCJK(t) ? BIND_ZH : BIND_EN).test(t) };
153
+ const NEG_WORDS = { test: (t) => (hasCJK(t) ? NEG_ZH : NEG_EN).test(t) };
154
+
155
+ // R1 legend exists
156
+ if (!/@image\d+/.test(head)) add('error', 'R1-legend-missing', 'prompt 首段冇 @imageN legend:模型唔知邊張圖係咩');
157
+
158
+ // R2 every planned image is in the legend, exactly once
159
+ for (const r of plan.imgs) {
160
+ const p = labelPositions.get(r.label);
161
+ if (!p) add('error', 'R2-legend-missing-label', `${r.label}(${r.role}:${r.entity})已接但 legend 冇提`);
162
+ else if (p.lines > 1) add('warn', 'R2-legend-duplicate', `${r.label} 有 ${p.lines} 條 legend 行 — 一張圖一行,重覆會令模型重新解讀角色`);
163
+ else if (p.lines === 0) add('warn', 'R2-legend-inline', `${r.label} 夾在一句裡面,冇自己一行 — 一張圖一行,「@image2、@image3 是人物圖」係模型撈亂嘅溫床`);
164
+ }
165
+
166
+ // R3 prompt uses labels the plan does not have (extra hand-connected refs)
167
+ const maxPlanned = plan.imgs.length;
168
+ for (const label of labelPositions.keys()) {
169
+ const n = Number(label.slice(6));
170
+ if (n > maxPlanned) add('warn', 'R3-extra-ref', `${label} 唔在項目派生嘅引用次序(只有 ${maxPlanned} 張圖)— 額外接嘅圖要寫入 segment.refOverride,否則重跑 prompt 就對唔上`);
171
+ }
172
+
173
+ // R4 role words per label + R5 negative instruction on non-character refs
174
+ for (const r of plan.imgs) {
175
+ const p = labelPositions.get(r.label);
176
+ if (!p) continue;
177
+ if (!ROLE_WORDS.test(p.span)) add('warn', 'R4-role-unsaid', `${r.label} 冇講明係咩角色(場景/人物/道具/光影/瞬間)— 模型只會當「一張參考圖」`);
178
+ if (r.role !== 'character' && !NEG_WORDS.test(p.span)) add('warn', 'R5-no-negative', `${r.label}(${r.role})冇寫「只照…唔照…」:非人物圖一定要講明唔好抄佢嘅構圖/人物,唔係模型會連人一齊抄`);
179
+ }
180
+
181
+ // R6 legend must let the model tell one person from another
182
+ const segChars = [];
183
+ for (const c of seg.cuts || []) for (const id of c.characters || []) if (id !== 'VO' && !segChars.includes(id)) segChars.push(id);
184
+ const charImgs = plan.imgs.filter((r) => r.role === 'character');
185
+ if (segChars.length >= 2 && charImgs.length >= 2) {
186
+ const total = new Map();
187
+ for (const id of segChars) for (const t of tokensOf(charById(id) || {})) total.set(t, (total.get(t) || 0) + 1);
188
+ const assigned = new Map();
189
+ for (const r of charImgs) {
190
+ const span = (labelPositions.get(r.label)?.span || '').toLowerCase();
191
+ let best = null, bestScore = 0;
192
+ for (const id of segChars) {
193
+ if (assigned.has(id)) continue;
194
+ let score = 0;
195
+ for (const t of tokensOf(charById(id) || {})) if (span.includes(t) && (total.get(t) || 0) <= 1) score += 2;
196
+ const g = charById(id)?.profile?.gender;
197
+ if (g && span.includes(g) && segChars.filter((x) => charById(x)?.profile?.gender === g).length === 1) score += 1;
198
+ if (score > bestScore) { best = id; bestScore = score; }
199
+ }
200
+ if (best && bestScore >= 2) assigned.set(best, r.label);
201
+ }
202
+ if (assigned.size < segChars.length) {
203
+ const missing = segChars.filter((id) => !assigned.has(id)).map((id) => charById(id)?.name || id);
204
+ add('error', 'R6-identity-unresolvable', `legend 冇寫到分辨特徵,無法對應邊張圖係邊個人(對唔到:${missing.join('、')})。` +
205
+ `人物圖必須逐張寫「係邊個」——性別、年齡、髮型、衣著各一句;寫成「@image2、@image3 是人物參考圖」模型就會互換面孔`);
206
+ }
207
+ // two people described in one legend sentence, with no per-picture role
208
+ for (const line of legendLines) {
209
+ const labels = line.match(/@image\d+/g) || [];
210
+ const people = labels.filter((l) => charImgs.some((r) => r.label === l));
211
+ if (people.length >= 2 && !/not its|never its|只照|不要照/i.test(line.replace(/@image\d+/g, ''))) {
212
+ add('warn', 'R6b-shared-clause', `legend 一句寫咗幾個人(${people.join('、')})又冇逐個講明 — 一張圖一句,否則模型會互換`);
213
+ }
214
+ }
215
+ }
216
+
217
+ // R7 per-shot binding: person / prop / scene
218
+ const cuts = seg.cuts || [];
219
+ if (shots.length !== cuts.length) add('warn', 'R0-shot-count', `prompt 有 ${shots.length} 個鏡頭,分鏡有 ${cuts.length} 個 cut — 冇法逐鏡核對(人名/道具綁定會漏)`);
220
+ const deriveOrder = (() => {
221
+ // when the legend is ambiguous, fall back to the engine's derivation order to still give per-shot advice
222
+ const m = new Map();
223
+ for (const r of plan.imgs) if (r.role === 'character') m.set(r.entity, r.label);
224
+ return m;
225
+ })();
226
+ const unboundChar = new Map(); // entity -> {label, shots: []}
227
+ for (let i = 0; i < cuts.length && i < shots.length; i++) {
228
+ const c = cuts[i];
229
+ const s = shots[i];
230
+ for (const id of c.characters || []) {
231
+ const label = deriveOrder.get(id);
232
+ if (!label) continue;
233
+ if (!countIn(s.text, label)) {
234
+ if (!unboundChar.has(id)) unboundChar.set(id, { label, shots: [] });
235
+ unboundChar.get(id).shots.push(s.k);
236
+ }
237
+ }
238
+ for (const id of c.props || []) {
239
+ const label = plan.imgs.find((r) => r.entity === id)?.label;
240
+ if (label && !countIn(s.text, label)) add('error', 'R7-prop', `鏡頭 ${s.k}:道具 ${propById(id)?.name || id} 在畫面內,但鏡頭文冇 ${label} — 道具外觀會由模型自創`);
241
+ }
242
+ if (i === 0) {
243
+ const sceneLabel = plan.imgs.find((r) => r.role === 'scene')?.label;
244
+ if (sceneLabel && !countIn(head + '\n' + shots.map((x) => x.text).join('\n'), sceneLabel) && !countIn(prompt.slice(firstShotAt), sceneLabel)) {
245
+ add('warn', 'R7-scene', `${sceneLabel} 只在 legend 出現,冇任何鏡頭講明照佢嘅空間/材質 — 每個 segment 至少講一次「整段都在 ${sceneLabel} 的空間裡」`);
246
+ }
247
+ }
248
+ }
249
+
250
+ for (const [id, bad] of unboundChar) {
251
+ add('error', 'R7-char', `${charById(id)?.name || id} 在鏡頭 ${bad.shots.join('、')} 畫面內,但鏡頭文冇 ${bad.label}` +
252
+ ` — 模型會沿用上一鏡嘅樣或者自己編。每個有佢嘅鏡頭都要寫一次 ${bad.label}`);
253
+ }
254
+ // R8 every image ref is actually used somewhere outside the legend (people are covered by R7)
255
+ const bodyText = prompt.slice(firstShotAt);
256
+ for (const r of plan.imgs) {
257
+ if (r.role === 'character' || r.role === 'scene') continue;
258
+ if (!countIn(bodyText, r.label)) add('error', 'R8-unused-ref', `${r.label}(${r.role}:${r.entity})接咗但鏡頭文一次都冇用 — 等於冇參考`);
259
+ }
260
+
261
+ // R9 dialogue still carries its voice label (mirrors the engine's own rule)
262
+ const dialogues = [];
263
+ for (const c of cuts) {
264
+ for (let b = (c.beats?.[0] || 1) - 1; b <= (c.beats?.[1] || 1) - 1; b++) {
265
+ const beat = (scene.flow || [])[b];
266
+ if (beat?.speaker) dialogues.push({ speaker: beat.speaker, line: beat.line });
267
+ }
268
+ }
269
+ const norm = (t) => String(t).replace(/[…⋯]/g, '...').replace(/[“”「」『』"]/g, '').replace(/\s+/g, '');
270
+ const flat = norm(prompt);
271
+ for (const l of dialogues) {
272
+ const needle = norm(l.line);
273
+ const at = flat.indexOf(needle);
274
+ if (at < 0) { add('error', 'R9-dialogue-missing', `台詞「${l.line}」逐字唔見(引擎逐字對,標點差異都算唔同)`); continue; }
275
+ const around = flat.slice(Math.max(0, at - 160), at + needle.length + 60);
276
+ if (!/@audio\d/.test(around)) add('error', 'R9-voice-unbound', `台詞「${l.line}」附近冇 @audioN — 模型會用預設聲`);
277
+ }
278
+
279
+ // R10 hygiene: no names, subtitles off
280
+ for (const ch of project.characters || []) if (ch.name && prompt.includes(ch.name)) add('warn', 'R10-name-leak', `prompt 出現角色名「${ch.name}」(引擎 prompt-hygiene 會當錯;描述性名字可以留,但要預期被擋)`);
281
+ if (!params.subtitles && !/(全程无字幕|no subtitles)/i.test(prompt)) add('warn', 'R10-subtitles', 'prompt 冇寫「無字幕/no subtitles」');
282
+
283
+ // R11 ambiguity sweep: a whole sentence that names several pictures without saying what each is for
284
+ for (const s of shots) {
285
+ for (const sentence of s.text.split(/[。;;\n]/)) {
286
+ const body = sentence.replace(/^\s*(?:Shot|镜头|鏡頭)\s*\d+\s*\([^)]*\)\s*:?/, '');
287
+ const labels = body.match(/@image\d+/g) || [];
288
+ if (labels.length >= 2 && !BIND_WORDS.test(body.replace(/@image\d+/g, '')) && body.length > 40) {
289
+ add('warn', 'R11-ambiguous-clause', `鏡頭 ${s.k}「${body.trim().slice(0, 44)}…」一句有 ${labels.length} 張圖但冇逐張講用途 — 拆開寫`);
290
+ }
291
+ }
292
+ }
293
+
294
+ // ── report ─────────────────────────────────────────────────────────────────
295
+ const errors = issues.filter((x) => x.level === 'error');
296
+ const warns = issues.filter((x) => x.level === 'warn');
297
+ if (AS_JSON) {
298
+ console.log(JSON.stringify({ segment: SEGMENT, shots: shots.length, cuts: cuts.length, plan: plan.imgs, issues }, null, 2));
299
+ } else {
300
+ console.log(`\n== prompt-audit · ${SEGMENT} ==`);
301
+ console.log(`refs (derived order): ${plan.imgs.map((r) => `${r.label}=${r.role}:${r.entity}`).join(' ')}`);
302
+ console.log(`shots in prompt: ${shots.length} · cuts in storyboard: ${cuts.length} · images declared: ${labelPositions.size}\n`);
303
+ for (const x of errors) console.log(`❌ [${x.code}] ${x.msg}`);
304
+ for (const x of warns) console.log(`⚠️ [${x.code}] ${x.msg}`);
305
+ if (!issues.length) console.log('✅ 冇發現引用綁定問題');
306
+ console.log(`\n${errors.length} error(s), ${warns.length} warning(s)`);
307
+ }
308
+ process.exit(errors.length ? 1 : 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