@gitruck/cli 1.3.6 → 1.3.7
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/AGENT.md +564 -564
- package/CHANGELOG.md +9 -0
- package/README.en.md +50 -50
- package/README.md +50 -50
- package/assets/composite-kit/kit.css +57 -57
- package/assets/composite-kit/kit.js +246 -246
- package/assets/readme/wordmark.svg +6 -6
- package/contracts/README.md +16 -16
- package/contracts/gsap-emit-v1.md +163 -163
- package/contracts/handoff-contracts.json +10 -10
- package/dist/index.js +258 -254
- package/docs/development.en.md +47 -47
- package/docs/development.md +47 -47
- package/docs/reference.en.md +31 -1
- package/docs/reference.md +31 -1
- package/package.json +79 -79
- package/skills/gtrk-ai-drama/SKILL.md +10 -10
- package/skills/gtrk-ai-drama/references/sparse-key-scenes.md +38 -38
- package/skills/gtrk-ai-drama/scripts/validate-storyboard.mjs +119 -119
- package/skills/gtrk-composite/SKILL.md +194 -194
- package/skills/gtrk-cover/SKILL.md +3 -3
- package/skills/gtrk-cover/references/layout-recipes.md +337 -337
- package/skills/gtrk-cover/references/prompt-templates.md +119 -119
- package/skills/gtrk-food-recap/SKILL.md +9 -9
- package/skills/gtrk-mg/SKILL.md +391 -391
- package/skills/gtrk-music-visualizer/SKILL.md +3 -3
- package/skills/gtrk-narration/SKILL.md +23 -23
- package/skills/gtrk-splitter/SKILL.md +298 -298
- package/skills/gtrk-splitter/references/example-visual-split.json +645 -645
- package/skills/gtrk-splitter/references/example-visual-split.md +439 -439
- package/skills/gtrk-splitter/references/field-schema.md +171 -171
- package/skills/gtrk-style-maker/SKILL.md +1 -1
- package/skills/gtrk-style-maker/references/ammo.md +16 -16
- package/skills/gtrk-style-maker/references/contracts-ref.md +9 -9
- package/skills/gtrk-style-maker/references/seeds/README.md +4 -4
- package/skills/gtrk-style-maker/references/seeds/seed-ecom-home-goods.md +37 -37
- package/skills/gtrk-style-maker/references/seeds/seed-psych-humanities.md +35 -35
- package/skills/gtrk-talking-head/SKILL.md +205 -205
- package/skills/gtrk-transcript/SKILL.md +4 -4
- package/skills/gtrk-transcript/agents/openai.yaml +4 -4
- package/skills/gtrk-travel-recap/SKILL.md +51 -51
- package/skills/gtrk-voiceover/SKILL.md +2 -2
|
@@ -1,38 +1,38 @@
|
|
|
1
|
-
# 稀疏关键场景与角色参考
|
|
2
|
-
|
|
3
|
-
用户要求“隔一段挑关键场景、其余自己补实况”时使用。它不是通篇 AI 成片图纸,不套用零缝、片头铺满或自动封缝规则。
|
|
4
|
-
|
|
5
|
-
## 选镜与时基
|
|
6
|
-
|
|
7
|
-
先通读正文及上下文,再按叙事转折选场景。间隔是密度目标,不是机械切秒。每镜登记原句、源音频起点、母带起点、预计画面时长、人物、年龄阶段、场景与关键道具。母带含 OP/ED 时显式登记正文范围与 offset;用对应音频片段核对,不能把正文从零开始的 ASR 时码直接当母带时码。
|
|
8
|
-
|
|
9
|
-
一处独立关键场景对应一个短 beat 窗口,窗口只覆盖这条片段的计划时长;不要把直到下一处场景的整个间隔当窗口让末镜吃满。CLI beat 使用 B 开头数字编号;其他展示编号应显式映射,不私改接口。其余时间留给实况,禁止自动 seal、循环或拉伸填满。用户指定只做正文时,OP/ED 不派视觉生成。
|
|
10
|
-
|
|
11
|
-
## 原作资产与逐镜判断
|
|
12
|
-
|
|
13
|
-
- 从叙事语义识别人物,不能靠“他、父亲、刀”等关键词自动加入主角。角色随时间改变时分别绑定年龄、发型、服装参考。
|
|
14
|
-
- 实际查看参考图。只带本镜需要的角色;物证或空镜不要夹带多人图作为角色参考。原作 CG 可以锁角色/画风,但画中事件不等于当前镜头事件。
|
|
15
|
-
- 参考路径逐镜登记,禁止按镜号取模轮换 CG。用户指定宿主 Image2 时使用该能力,不擅自换成本地生图或云模型。
|
|
16
|
-
- 图片提示词描述一个明确瞬间;I2V 提示词描述该瞬间里有剧情含义的小动作,以及须保持稳定的脸、手、道具与空间。实际时长、尺寸、帧率以模型配置及成品探测为准,不硬编码样片参数到全部项目。
|
|
17
|
-
|
|
18
|
-
## 分阶段审阅
|
|
19
|
-
|
|
20
|
-
已生成、已选中、已审阅、已回轨是不同状态。逐镜看实际输出,记录人物与年龄、场景关系、道具比例、手部接触和微动可行性。审阅记录绑定具体文件与内容哈希;替换图片、分镜或人物设定后重新审阅,不能整批写 accepted 代替目视检查。工具可以强制记录完整,却不能证明记录内容真实。
|
|
21
|
-
|
|
22
|
-
外部图片通过工作台的 keyframe import 接口进入候选池;角色 refs 上传不是关键帧导入。具体命令见工作台 skill。视频检查至少看首、中、尾及运动过程;仅看抽帧要如实说明,不写成逐帧播放验收。
|
|
23
|
-
|
|
24
|
-
## 分批回轨
|
|
25
|
-
|
|
26
|
-
首次按现有 pack/lay 契约回轨;新增 beat 使用:
|
|
27
|
-
|
|
28
|
-
```sh
|
|
29
|
-
gtrk ai-drama lay --project <工程> --package <新包> --append --json
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
`--append` 为本次新包新增独立轨,逐对象保留已有轨、素材与手调;拒绝重复 beat,不与 `--replace-all` 同用。需替换已有镜头时,等时长/尺寸/帧率素材用 `swap`;改变时序须重新设计并用 patch 编辑,不以“备份后重铺再猜着恢复”冒充保留手调。媒体由增量路径按 beat 和内容哈希隔离。
|
|
33
|
-
|
|
34
|
-
执行前查看当前 CLI 的 `ai-drama --help` 是否已有 `--append`。skill 更新不等于已安装二进制更新;旧版本缺参数时使用已核实仓库的源码入口或完成构建,不退回覆盖手调的项目脚本。
|
|
35
|
-
|
|
36
|
-
阅读 lay 回执的 reprojection:降级使用派单快照时核对它是否属于当前母带。doctor 检查结构与文件存在,不能代替语义审阅;稀疏轨的空档警告不自动触发封缝。
|
|
37
|
-
|
|
38
|
-
交付明确计划数、已生成数、已审阅数和已回轨数。用户说先看样片时只交付约定样片;其他镜头保持未完成状态,不因有场景表就宣称批量完成。
|
|
1
|
+
# 稀疏关键场景与角色参考
|
|
2
|
+
|
|
3
|
+
用户要求“隔一段挑关键场景、其余自己补实况”时使用。它不是通篇 AI 成片图纸,不套用零缝、片头铺满或自动封缝规则。
|
|
4
|
+
|
|
5
|
+
## 选镜与时基
|
|
6
|
+
|
|
7
|
+
先通读正文及上下文,再按叙事转折选场景。间隔是密度目标,不是机械切秒。每镜登记原句、源音频起点、母带起点、预计画面时长、人物、年龄阶段、场景与关键道具。母带含 OP/ED 时显式登记正文范围与 offset;用对应音频片段核对,不能把正文从零开始的 ASR 时码直接当母带时码。
|
|
8
|
+
|
|
9
|
+
一处独立关键场景对应一个短 beat 窗口,窗口只覆盖这条片段的计划时长;不要把直到下一处场景的整个间隔当窗口让末镜吃满。CLI beat 使用 B 开头数字编号;其他展示编号应显式映射,不私改接口。其余时间留给实况,禁止自动 seal、循环或拉伸填满。用户指定只做正文时,OP/ED 不派视觉生成。
|
|
10
|
+
|
|
11
|
+
## 原作资产与逐镜判断
|
|
12
|
+
|
|
13
|
+
- 从叙事语义识别人物,不能靠“他、父亲、刀”等关键词自动加入主角。角色随时间改变时分别绑定年龄、发型、服装参考。
|
|
14
|
+
- 实际查看参考图。只带本镜需要的角色;物证或空镜不要夹带多人图作为角色参考。原作 CG 可以锁角色/画风,但画中事件不等于当前镜头事件。
|
|
15
|
+
- 参考路径逐镜登记,禁止按镜号取模轮换 CG。用户指定宿主 Image2 时使用该能力,不擅自换成本地生图或云模型。
|
|
16
|
+
- 图片提示词描述一个明确瞬间;I2V 提示词描述该瞬间里有剧情含义的小动作,以及须保持稳定的脸、手、道具与空间。实际时长、尺寸、帧率以模型配置及成品探测为准,不硬编码样片参数到全部项目。
|
|
17
|
+
|
|
18
|
+
## 分阶段审阅
|
|
19
|
+
|
|
20
|
+
已生成、已选中、已审阅、已回轨是不同状态。逐镜看实际输出,记录人物与年龄、场景关系、道具比例、手部接触和微动可行性。审阅记录绑定具体文件与内容哈希;替换图片、分镜或人物设定后重新审阅,不能整批写 accepted 代替目视检查。工具可以强制记录完整,却不能证明记录内容真实。
|
|
21
|
+
|
|
22
|
+
外部图片通过工作台的 keyframe import 接口进入候选池;角色 refs 上传不是关键帧导入。具体命令见工作台 skill。视频检查至少看首、中、尾及运动过程;仅看抽帧要如实说明,不写成逐帧播放验收。
|
|
23
|
+
|
|
24
|
+
## 分批回轨
|
|
25
|
+
|
|
26
|
+
首次按现有 pack/lay 契约回轨;新增 beat 使用:
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
gtrk ai-drama lay --project <工程> --package <新包> --append --json
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`--append` 为本次新包新增独立轨,逐对象保留已有轨、素材与手调;拒绝重复 beat,不与 `--replace-all` 同用。需替换已有镜头时,等时长/尺寸/帧率素材用 `swap`;改变时序须重新设计并用 patch 编辑,不以“备份后重铺再猜着恢复”冒充保留手调。媒体由增量路径按 beat 和内容哈希隔离。
|
|
33
|
+
|
|
34
|
+
执行前查看当前 CLI 的 `ai-drama --help` 是否已有 `--append`。skill 更新不等于已安装二进制更新;旧版本缺参数时使用已核实仓库的源码入口或完成构建,不退回覆盖手调的项目脚本。
|
|
35
|
+
|
|
36
|
+
阅读 lay 回执的 reprojection:降级使用派单快照时核对它是否属于当前母带。doctor 检查结构与文件存在,不能代替语义审阅;稀疏轨的空档警告不自动触发封缝。
|
|
37
|
+
|
|
38
|
+
交付明确计划数、已生成数、已审阅数和已回轨数。用户说先看样片时只交付约定样片;其他镜头保持未完成状态,不因有场景表就宣称批量完成。
|
|
@@ -1,119 +1,119 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { readFile } from "node:fs/promises";
|
|
4
|
-
import { basename } from "node:path";
|
|
5
|
-
|
|
6
|
-
const files = process.argv.slice(2);
|
|
7
|
-
if (files.length === 0) {
|
|
8
|
-
console.error("用法:bun scripts/validate-storyboard.mjs <分镜稿.md> [...]");
|
|
9
|
-
process.exit(2);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function firstMatchIndex(text, re) {
|
|
13
|
-
const m = re.exec(text);
|
|
14
|
-
return m ? m.index : -1;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function validate(md, file) {
|
|
18
|
-
const errors = [];
|
|
19
|
-
const beat = md.match(/[((]\s*beat\s+([A-Za-z0-9_-]+)\s*[))]/i)?.[1] ?? "";
|
|
20
|
-
if (!beat) errors.push("H1 标题缺少「(beat BXX)」");
|
|
21
|
-
|
|
22
|
-
const meta = md.match(/^[-*]\s*区间总时长\s*[::]\s*track_st\s+(-?\d+(?:\.\d+)?)\s*→\s*track_ed\s+(-?\d+(?:\.\d+)?)\s*≈\s*(\d+(?:\.\d+)?)\s*秒\s*$/m);
|
|
23
|
-
if (!meta) errors.push("缺少单行元信息「区间总时长:track_st … → track_ed … ≈ … 秒」");
|
|
24
|
-
|
|
25
|
-
const cnStart = firstMatchIndex(md, /^##\s*一、中文稿.*$/m);
|
|
26
|
-
const enStart = firstMatchIndex(md, /^##\s*二、English Storyboard.*$/mi);
|
|
27
|
-
if (cnStart < 0) errors.push("缺少「## 一、中文稿…」区块");
|
|
28
|
-
if (enStart < 0) errors.push("缺少「## 二、English Storyboard…」区块");
|
|
29
|
-
if (cnStart >= 0 && enStart >= 0 && cnStart >= enStart) errors.push("中文稿必须位于 English Storyboard 之前");
|
|
30
|
-
|
|
31
|
-
const cn = cnStart >= 0 ? md.slice(cnStart, enStart >= 0 ? enStart : md.length) : "";
|
|
32
|
-
const en = enStart >= 0 ? md.slice(enStart) : "";
|
|
33
|
-
for (const n of ["①", "②", "③", "④", "⑤"]) {
|
|
34
|
-
if (!new RegExp(`^#{1,6}\\s*${n}`, "m").test(cn)) errors.push(`中文稿缺少 ${n} 区块`);
|
|
35
|
-
}
|
|
36
|
-
for (const n of ["①", "②", "③", "④"]) {
|
|
37
|
-
if (!new RegExp(`^#{1,6}\\s*${n}`, "m").test(en)) errors.push(`English Storyboard 缺少 ${n} 区块`);
|
|
38
|
-
}
|
|
39
|
-
if (!/^>\s*禁忌\s*[::]/m.test(cn)) errors.push("① 视觉基调缺少独立的「> 禁忌:…」行");
|
|
40
|
-
if (!/^>\s*Avoid\s*[::]/mi.test(en)) errors.push("English ① Style Lock 缺少独立的「> Avoid: …」行");
|
|
41
|
-
|
|
42
|
-
const cnCharBlock = cn.match(/^#{1,6}\s*③[^\n]*\n([\s\S]*?)(?=^#{1,6}\s*④)/m)?.[1] ?? "";
|
|
43
|
-
const enCharBlock = en.match(/^#{1,6}\s*③[^\n]*\n([\s\S]*?)(?=^#{1,6}\s*④)/m)?.[1] ?? "";
|
|
44
|
-
const cnChars = [...cnCharBlock.matchAll(/^####\s+(.+?)\s*$/gm)].map((m) => m[1].trim());
|
|
45
|
-
const enChars = [...enCharBlock.matchAll(/^####\s+(.+?)\s*$/gm)].map((m) => m[1].trim());
|
|
46
|
-
if (cnChars.length === 0) errors.push("③ 角色描述中没有「#### 角色名(备注)」标题");
|
|
47
|
-
if (enChars.length === 0) errors.push("English ③ Characters 中没有角色四级标题");
|
|
48
|
-
|
|
49
|
-
const cnShotRe = /^####\s*分镜\s*(\d+)\s*[·•..]\s*(.*?)\s*[||]\s*建议\s*≈?\s*(\d+(?:\.\d+)?)\s*s\b.*[||]\s*角色\s*[::]\s*([^||\n]+)/gim;
|
|
50
|
-
const enShotRe = /^####\s*Shot\s*(\d+)\s*[·•..]\s*(.*?)\s*[||]\s*≈?\s*(\d+(?:\.\d+)?)\s*s\b.*[||]\s*cast\s*[::]\s*([^||\n]+)/gim;
|
|
51
|
-
const cnShots = [...cn.matchAll(cnShotRe)].map((m) => Number(m[1]));
|
|
52
|
-
const enShots = [...en.matchAll(enShotRe)].map((m) => Number(m[1]));
|
|
53
|
-
if (cnShots.length === 0) errors.push("④ 中没有可解析的「#### 分镜 01 · … |建议 ≈Xs … |角色:…」标题");
|
|
54
|
-
if (enShots.length === 0) errors.push("English ④ 中没有可解析的「#### Shot 01 · … |≈Xs … |cast: …」标题");
|
|
55
|
-
if (cnShots.length > 0 && enShots.length > 0 && JSON.stringify(cnShots) !== JSON.stringify(enShots)) {
|
|
56
|
-
errors.push(`中英文镜头序号不一致:CN=${cnShots.join(",")},EN=${enShots.join(",")}`);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const cnShotBlock = cn.match(/^#{1,6}\s*④[^\n]*\n([\s\S]*?)(?=^#{1,6}\s*⑤)/m)?.[1] ?? "";
|
|
60
|
-
const enShotBlock = en.match(/^#{1,6}\s*④[^\n]*\n([\s\S]*)/m)?.[1] ?? "";
|
|
61
|
-
const cnHeads = [...cnShotBlock.matchAll(/^####\s+(.+?)\s*$/gm)];
|
|
62
|
-
const enHeads = [...enShotBlock.matchAll(/^####\s+(.+?)\s*$/gm)];
|
|
63
|
-
const extraCnHeads = cnHeads.filter((m) => !/^分镜\s*\d+/i.test(m[1]));
|
|
64
|
-
const extraEnHeads = enHeads.filter((m) => !/^Shot\s*\d+/i.test(m[1]));
|
|
65
|
-
if (extraCnHeads.length > 0) errors.push(`④ 内存在非分镜四级标题:${extraCnHeads.map((m) => m[1]).join("、")};请把蒙太奇名放进「段」字段`);
|
|
66
|
-
if (extraEnHeads.length > 0) errors.push(`English ④ 内存在非 Shot 四级标题:${extraEnHeads.map((m) => m[1]).join("、")};请把 montage 名放进 segment 字段`);
|
|
67
|
-
|
|
68
|
-
function checkPrefixes(block, heads, label) {
|
|
69
|
-
for (let i = 0; i < heads.length; i += 1) {
|
|
70
|
-
const start = (heads[i].index ?? 0) + heads[i][0].length;
|
|
71
|
-
const end = i + 1 < heads.length ? heads[i + 1].index : block.length;
|
|
72
|
-
const firstBodyLine = block.slice(start, end).split(/\r?\n/).map((line) => line.trim()).find(Boolean) ?? "";
|
|
73
|
-
if (!firstBodyLine.startsWith("〔") || !firstBodyLine.includes("〕")) {
|
|
74
|
-
errors.push(`${label} ${heads[i][1].match(/\d+/)?.[0] ?? "?"} 的首个正文不是「〔视觉基调〕」前缀`);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
checkPrefixes(cnShotBlock, cnHeads.filter((m) => /^分镜\s*\d+/i.test(m[1])), "中文分镜");
|
|
79
|
-
checkPrefixes(enShotBlock, enHeads.filter((m) => /^Shot\s*\d+/i.test(m[1])), "English Shot");
|
|
80
|
-
|
|
81
|
-
if (/^\s*[-*]\s*(男性|女性|男主|女主|父亲|母亲|角色)\s*[::]/m.test(cnCharBlock)) {
|
|
82
|
-
errors.push("③ 角色仍使用项目符号;请改成四级标题并把描述另起段落");
|
|
83
|
-
}
|
|
84
|
-
if (/^\s*\*\*(镜头|分镜|Shot)\s*\d+/mi.test(md)) {
|
|
85
|
-
errors.push("镜头仍使用粗体行;请改成「#### 分镜…」/「#### Shot…」四级标题");
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (meta) {
|
|
89
|
-
const st = Number(meta[1]);
|
|
90
|
-
const ed = Number(meta[2]);
|
|
91
|
-
const total = Number(meta[3]);
|
|
92
|
-
if (!(ed > st)) errors.push("track_ed 必须大于 track_st");
|
|
93
|
-
if (Math.abs((ed - st) - total) > 0.11) errors.push(`区间总时长与 track_ed-track_st 不一致:${total} vs ${(ed - st).toFixed(3)}`);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return {
|
|
97
|
-
ok: errors.length === 0,
|
|
98
|
-
file: basename(file),
|
|
99
|
-
beatId: beat || null,
|
|
100
|
-
characters: cnChars.length,
|
|
101
|
-
shots: cnShots.length,
|
|
102
|
-
errors,
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
let failed = false;
|
|
107
|
-
for (const file of files) {
|
|
108
|
-
try {
|
|
109
|
-
const md = await readFile(file, "utf8");
|
|
110
|
-
const result = validate(md, file);
|
|
111
|
-
console.log(JSON.stringify(result));
|
|
112
|
-
failed ||= !result.ok;
|
|
113
|
-
} catch (error) {
|
|
114
|
-
failed = true;
|
|
115
|
-
console.log(JSON.stringify({ ok: false, file: basename(file), errors: [String(error?.message ?? error)] }));
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
process.exit(failed ? 1 : 0);
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { basename } from "node:path";
|
|
5
|
+
|
|
6
|
+
const files = process.argv.slice(2);
|
|
7
|
+
if (files.length === 0) {
|
|
8
|
+
console.error("用法:bun scripts/validate-storyboard.mjs <分镜稿.md> [...]");
|
|
9
|
+
process.exit(2);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function firstMatchIndex(text, re) {
|
|
13
|
+
const m = re.exec(text);
|
|
14
|
+
return m ? m.index : -1;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function validate(md, file) {
|
|
18
|
+
const errors = [];
|
|
19
|
+
const beat = md.match(/[((]\s*beat\s+([A-Za-z0-9_-]+)\s*[))]/i)?.[1] ?? "";
|
|
20
|
+
if (!beat) errors.push("H1 标题缺少「(beat BXX)」");
|
|
21
|
+
|
|
22
|
+
const meta = md.match(/^[-*]\s*区间总时长\s*[::]\s*track_st\s+(-?\d+(?:\.\d+)?)\s*→\s*track_ed\s+(-?\d+(?:\.\d+)?)\s*≈\s*(\d+(?:\.\d+)?)\s*秒\s*$/m);
|
|
23
|
+
if (!meta) errors.push("缺少单行元信息「区间总时长:track_st … → track_ed … ≈ … 秒」");
|
|
24
|
+
|
|
25
|
+
const cnStart = firstMatchIndex(md, /^##\s*一、中文稿.*$/m);
|
|
26
|
+
const enStart = firstMatchIndex(md, /^##\s*二、English Storyboard.*$/mi);
|
|
27
|
+
if (cnStart < 0) errors.push("缺少「## 一、中文稿…」区块");
|
|
28
|
+
if (enStart < 0) errors.push("缺少「## 二、English Storyboard…」区块");
|
|
29
|
+
if (cnStart >= 0 && enStart >= 0 && cnStart >= enStart) errors.push("中文稿必须位于 English Storyboard 之前");
|
|
30
|
+
|
|
31
|
+
const cn = cnStart >= 0 ? md.slice(cnStart, enStart >= 0 ? enStart : md.length) : "";
|
|
32
|
+
const en = enStart >= 0 ? md.slice(enStart) : "";
|
|
33
|
+
for (const n of ["①", "②", "③", "④", "⑤"]) {
|
|
34
|
+
if (!new RegExp(`^#{1,6}\\s*${n}`, "m").test(cn)) errors.push(`中文稿缺少 ${n} 区块`);
|
|
35
|
+
}
|
|
36
|
+
for (const n of ["①", "②", "③", "④"]) {
|
|
37
|
+
if (!new RegExp(`^#{1,6}\\s*${n}`, "m").test(en)) errors.push(`English Storyboard 缺少 ${n} 区块`);
|
|
38
|
+
}
|
|
39
|
+
if (!/^>\s*禁忌\s*[::]/m.test(cn)) errors.push("① 视觉基调缺少独立的「> 禁忌:…」行");
|
|
40
|
+
if (!/^>\s*Avoid\s*[::]/mi.test(en)) errors.push("English ① Style Lock 缺少独立的「> Avoid: …」行");
|
|
41
|
+
|
|
42
|
+
const cnCharBlock = cn.match(/^#{1,6}\s*③[^\n]*\n([\s\S]*?)(?=^#{1,6}\s*④)/m)?.[1] ?? "";
|
|
43
|
+
const enCharBlock = en.match(/^#{1,6}\s*③[^\n]*\n([\s\S]*?)(?=^#{1,6}\s*④)/m)?.[1] ?? "";
|
|
44
|
+
const cnChars = [...cnCharBlock.matchAll(/^####\s+(.+?)\s*$/gm)].map((m) => m[1].trim());
|
|
45
|
+
const enChars = [...enCharBlock.matchAll(/^####\s+(.+?)\s*$/gm)].map((m) => m[1].trim());
|
|
46
|
+
if (cnChars.length === 0) errors.push("③ 角色描述中没有「#### 角色名(备注)」标题");
|
|
47
|
+
if (enChars.length === 0) errors.push("English ③ Characters 中没有角色四级标题");
|
|
48
|
+
|
|
49
|
+
const cnShotRe = /^####\s*分镜\s*(\d+)\s*[·•..]\s*(.*?)\s*[||]\s*建议\s*≈?\s*(\d+(?:\.\d+)?)\s*s\b.*[||]\s*角色\s*[::]\s*([^||\n]+)/gim;
|
|
50
|
+
const enShotRe = /^####\s*Shot\s*(\d+)\s*[·•..]\s*(.*?)\s*[||]\s*≈?\s*(\d+(?:\.\d+)?)\s*s\b.*[||]\s*cast\s*[::]\s*([^||\n]+)/gim;
|
|
51
|
+
const cnShots = [...cn.matchAll(cnShotRe)].map((m) => Number(m[1]));
|
|
52
|
+
const enShots = [...en.matchAll(enShotRe)].map((m) => Number(m[1]));
|
|
53
|
+
if (cnShots.length === 0) errors.push("④ 中没有可解析的「#### 分镜 01 · … |建议 ≈Xs … |角色:…」标题");
|
|
54
|
+
if (enShots.length === 0) errors.push("English ④ 中没有可解析的「#### Shot 01 · … |≈Xs … |cast: …」标题");
|
|
55
|
+
if (cnShots.length > 0 && enShots.length > 0 && JSON.stringify(cnShots) !== JSON.stringify(enShots)) {
|
|
56
|
+
errors.push(`中英文镜头序号不一致:CN=${cnShots.join(",")},EN=${enShots.join(",")}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const cnShotBlock = cn.match(/^#{1,6}\s*④[^\n]*\n([\s\S]*?)(?=^#{1,6}\s*⑤)/m)?.[1] ?? "";
|
|
60
|
+
const enShotBlock = en.match(/^#{1,6}\s*④[^\n]*\n([\s\S]*)/m)?.[1] ?? "";
|
|
61
|
+
const cnHeads = [...cnShotBlock.matchAll(/^####\s+(.+?)\s*$/gm)];
|
|
62
|
+
const enHeads = [...enShotBlock.matchAll(/^####\s+(.+?)\s*$/gm)];
|
|
63
|
+
const extraCnHeads = cnHeads.filter((m) => !/^分镜\s*\d+/i.test(m[1]));
|
|
64
|
+
const extraEnHeads = enHeads.filter((m) => !/^Shot\s*\d+/i.test(m[1]));
|
|
65
|
+
if (extraCnHeads.length > 0) errors.push(`④ 内存在非分镜四级标题:${extraCnHeads.map((m) => m[1]).join("、")};请把蒙太奇名放进「段」字段`);
|
|
66
|
+
if (extraEnHeads.length > 0) errors.push(`English ④ 内存在非 Shot 四级标题:${extraEnHeads.map((m) => m[1]).join("、")};请把 montage 名放进 segment 字段`);
|
|
67
|
+
|
|
68
|
+
function checkPrefixes(block, heads, label) {
|
|
69
|
+
for (let i = 0; i < heads.length; i += 1) {
|
|
70
|
+
const start = (heads[i].index ?? 0) + heads[i][0].length;
|
|
71
|
+
const end = i + 1 < heads.length ? heads[i + 1].index : block.length;
|
|
72
|
+
const firstBodyLine = block.slice(start, end).split(/\r?\n/).map((line) => line.trim()).find(Boolean) ?? "";
|
|
73
|
+
if (!firstBodyLine.startsWith("〔") || !firstBodyLine.includes("〕")) {
|
|
74
|
+
errors.push(`${label} ${heads[i][1].match(/\d+/)?.[0] ?? "?"} 的首个正文不是「〔视觉基调〕」前缀`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
checkPrefixes(cnShotBlock, cnHeads.filter((m) => /^分镜\s*\d+/i.test(m[1])), "中文分镜");
|
|
79
|
+
checkPrefixes(enShotBlock, enHeads.filter((m) => /^Shot\s*\d+/i.test(m[1])), "English Shot");
|
|
80
|
+
|
|
81
|
+
if (/^\s*[-*]\s*(男性|女性|男主|女主|父亲|母亲|角色)\s*[::]/m.test(cnCharBlock)) {
|
|
82
|
+
errors.push("③ 角色仍使用项目符号;请改成四级标题并把描述另起段落");
|
|
83
|
+
}
|
|
84
|
+
if (/^\s*\*\*(镜头|分镜|Shot)\s*\d+/mi.test(md)) {
|
|
85
|
+
errors.push("镜头仍使用粗体行;请改成「#### 分镜…」/「#### Shot…」四级标题");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (meta) {
|
|
89
|
+
const st = Number(meta[1]);
|
|
90
|
+
const ed = Number(meta[2]);
|
|
91
|
+
const total = Number(meta[3]);
|
|
92
|
+
if (!(ed > st)) errors.push("track_ed 必须大于 track_st");
|
|
93
|
+
if (Math.abs((ed - st) - total) > 0.11) errors.push(`区间总时长与 track_ed-track_st 不一致:${total} vs ${(ed - st).toFixed(3)}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
ok: errors.length === 0,
|
|
98
|
+
file: basename(file),
|
|
99
|
+
beatId: beat || null,
|
|
100
|
+
characters: cnChars.length,
|
|
101
|
+
shots: cnShots.length,
|
|
102
|
+
errors,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let failed = false;
|
|
107
|
+
for (const file of files) {
|
|
108
|
+
try {
|
|
109
|
+
const md = await readFile(file, "utf8");
|
|
110
|
+
const result = validate(md, file);
|
|
111
|
+
console.log(JSON.stringify(result));
|
|
112
|
+
failed ||= !result.ok;
|
|
113
|
+
} catch (error) {
|
|
114
|
+
failed = true;
|
|
115
|
+
console.log(JSON.stringify({ ok: false, file: basename(file), errors: [String(error?.message ?? error)] }));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
process.exit(failed ? 1 : 0);
|