@wji.ai/cli 0.9.1 → 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.
- package/README.md +6 -1
- package/dist/index.js +4 -5
- package/package.json +1 -1
- package/skill/backreel/SKILL.md +1 -1
- package/skill/backreel/reference/episodes.md +7 -7
- package/skill/short-drama/SKILL.md +27 -4
- package/skill/short-drama/examples/night-shift/episodes/E01.json +173 -1
- package/skill/short-drama/examples/night-shift/project.json +6 -0
- package/skill/short-drama/reference/dialogue-craft.md +16 -11
- package/skill/short-drama/reference/prompt-audit.mjs +308 -0
- package/skill/short-drama/reference/reference-binding.md +84 -0
- package/skill/short-drama/reference/schema.md +14 -5
- package/skill/short-drama/reference/stage-characters.md +1 -1
- package/skill/short-drama/reference/stage-script.md +54 -7
- package/skill/short-drama/reference/stage-storyboard.md +29 -24
- package/skill/short-drama/scripts/selftest.mjs +154 -9
- package/skill/short-drama/scripts/short-drama.mjs +467 -23
- package/skill/w-film-dialogue-editor/CHANGELOG.md +44 -0
- package/skill/w-film-dialogue-editor/SKILL.md +124 -0
- package/skill/w-film-dialogue-editor/references/data/README.md +222 -0
- package/skill/w-film-dialogue-editor/references/data/en/core.json +1666 -0
- package/skill/w-film-dialogue-editor/references/data/en/slang.json +1602 -0
- package/skill/w-film-dialogue-editor/references/data/en/work.json +1722 -0
- package/skill/w-film-dialogue-editor/references/data/en-sentences.csv +134 -0
- package/skill/w-film-dialogue-editor/references/data/en-sentences.md +146 -0
- package/skill/w-film-dialogue-editor/references/data/ja/core.json +1978 -0
- package/skill/w-film-dialogue-editor/references/data/ja/slang.json +1927 -0
- package/skill/w-film-dialogue-editor/references/data/ja/work.json +1972 -0
- package/skill/w-film-dialogue-editor/references/data/ja-sentences.csv +134 -0
- package/skill/w-film-dialogue-editor/references/data/ja-sentences.md +146 -0
- package/skill/w-film-dialogue-editor/references/data/ko/core.json +1839 -0
- package/skill/w-film-dialogue-editor/references/data/ko/slang.json +1897 -0
- package/skill/w-film-dialogue-editor/references/data/ko/work.json +1829 -0
- package/skill/w-film-dialogue-editor/references/data/ko-sentences.csv +134 -0
- package/skill/w-film-dialogue-editor/references/data/ko-sentences.md +146 -0
- package/skill/w-film-dialogue-editor/references/data/zh-cn/core.json +1794 -0
- package/skill/w-film-dialogue-editor/references/data/zh-cn/slang.json +1609 -0
- package/skill/w-film-dialogue-editor/references/data/zh-cn/work.json +1648 -0
- package/skill/w-film-dialogue-editor/references/data/zh-cn-sentences.csv +134 -0
- package/skill/w-film-dialogue-editor/references/data/zh-cn-sentences.md +146 -0
- package/skill/w-film-dialogue-editor/references/data/zh-hk/core.json +2476 -0
- package/skill/w-film-dialogue-editor/references/data/zh-hk/gen-z.json +3263 -0
- package/skill/w-film-dialogue-editor/references/data/zh-hk/older.json +3632 -0
- package/skill/w-film-dialogue-editor/references/data/zh-hk/rough.json +1668 -0
- package/skill/w-film-dialogue-editor/references/data/zh-hk/workplace.json +3697 -0
- package/skill/w-film-dialogue-editor/references/data/zh-hk-sentences.csv +373 -0
- package/skill/w-film-dialogue-editor/references/data/zh-hk-sentences.md +391 -0
- package/skill/w-film-dialogue-editor/references/data/zh-tw/core.json +1640 -0
- package/skill/w-film-dialogue-editor/references/data/zh-tw/slang.json +1537 -0
- package/skill/w-film-dialogue-editor/references/data/zh-tw/work.json +1699 -0
- package/skill/w-film-dialogue-editor/references/data/zh-tw-sentences.csv +132 -0
- package/skill/w-film-dialogue-editor/references/data/zh-tw-sentences.md +144 -0
- package/skill/w-film-dialogue-editor/references/dialogue_revision_framework.md +84 -0
- package/skill/w-film-dialogue-editor/references/en-index.md +265 -0
- package/skill/w-film-dialogue-editor/references/ja-index.md +633 -0
- package/skill/w-film-dialogue-editor/references/ko-index.md +498 -0
- package/skill/w-film-dialogue-editor/references/lang-en.md +242 -0
- package/skill/w-film-dialogue-editor/references/lang-ja.md +312 -0
- package/skill/w-film-dialogue-editor/references/lang-ko.md +290 -0
- package/skill/w-film-dialogue-editor/references/lang-zh-cn.md +60 -0
- package/skill/w-film-dialogue-editor/references/lang-zh-hk-mandarin.md +34 -0
- package/skill/w-film-dialogue-editor/references/lang-zh-hk.md +306 -0
- package/skill/w-film-dialogue-editor/references/lang-zh-period.md +38 -0
- package/skill/w-film-dialogue-editor/references/lang-zh-tw.md +57 -0
- package/skill/w-film-dialogue-editor/references/zh-cn-index.md +636 -0
- package/skill/w-film-dialogue-editor/references/zh-hk-index.md +1450 -0
- package/skill/w-film-dialogue-editor/references/zh-tw-index.md +457 -0
- package/skill/w-film-dialogue-editor/scripts/rebuild.sh +13 -0
- package/skill/w-film-dialogue-editor/scripts/ref.mjs +539 -0
- package/skill/w-film-dialogue-editor/templates/dialogue_revision_report.md +43 -0
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
159
|
-
(
|
|
160
|
-
(
|
|
161
|
-
(
|
|
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
|
-
|
|
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,
|
|
442
|
-
episode as in
|
|
443
|
-
stays in the project), and
|
|
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
|
-
(
|
|
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
|
-
|
|
467
|
-
|
|
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
|
-
|
|
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
|
-
|
|
532
|
-
|
|
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('
|
|
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
|
|
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,
|
|
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,
|
|
2420
|
-
eq(GATES.filter((g) => !g.advisory).length,
|
|
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, '✓
|
|
2543
|
-
eq(rep.failed, 0, 'nothing failed'); eq(rep.hard,
|
|
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');
|