@gitruck/cli 1.1.2 → 1.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitruck/cli",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
4
4
  "description": "同合云成片流水线 CLI —— agent 驱动云端任务、产物拉回本地、三方工程文件(客户端/剪映/PR)互通。",
5
5
  "license": "MIT",
6
6
  "author": "Gitruck (同合云)",
@@ -50,7 +50,7 @@
50
50
  "build": "bun build ./src/index.ts --target=node --minify --outfile dist/index.js",
51
51
  "typecheck": "tsc --noEmit",
52
52
  "prepublishOnly": "npm run typecheck && npm test && bun run build",
53
- "test": "esbuild src/lib/compliance-notice.ts src/lib/first-run-tutorial.ts src/lib/desktop-client-hint.ts src/lib/chunk-upload.ts src/lib/upload-cache.ts src/lib/upload-submit.ts src/lib/render.ts src/lib/cloud.ts src/lib/materialize.ts src/lib/jianying.ts src/lib/clip-brief.ts src/lib/media.ts src/lib/projection.ts src/lib/reproject.ts src/lib/splitdoc.ts src/lib/gtrk-writeback.ts src/lib/gtrk-patch.ts src/lib/column-config.ts src/lib/matrix.ts src/lib/matrix-material.ts src/lib/matrix-fetch.ts src/lib/matrix-lay.ts src/lib/arrange-metering.ts src/lib/arrange-wire.ts src/lib/arrange-apply.ts src/lib/arrange-client.ts src/lib/arrange-gate.ts src/lib/arrange-decision-pin.ts src/lib/arrange-qc.ts src/lib/arrange-qc-bind.ts src/lib/rate-limit-wait.ts src/lib/embed-client.ts src/lib/describe.ts src/lib/local-index.ts src/lib/index-decode.ts src/lib/local-search.ts src/lib/qc.ts src/lib/alignment-qc.ts src/lib/bgm-history.ts src/lib/image-move.ts src/lib/cloud-render-guard.ts src/lib/material-integrity.ts src/lib/solid-png.ts src/lib/mg-lint.ts src/lib/mg-lay.ts src/lib/mg-render.ts src/lib/tool-descriptors.ts src/lib/tool-runner.ts src/lib/tool-pricing.ts src/lib/transcript.ts src/lib/runtime-assets.ts src/lib/subtitle-lay.ts src/lib/audio-align.ts src/lib/audio-tighten.ts --bundle --platform=node --format=esm --external:bun:sqlite --outdir=.test-build --out-extension:.js=.mjs && esbuild src/commands/split.ts --bundle --platform=node --format=esm --outdir=.test-build --out-extension:.js=.mjs && esbuild src/commands/patch.ts --bundle --platform=node --format=esm --external:commander --outfile=.test-build/patch-cmd.mjs && esbuild src/commands/matrix.ts --bundle --platform=node --format=esm --external:bun:sqlite --outfile=.test-build/matrix-cmd.mjs && esbuild src/commands/mg.ts --bundle --platform=node --format=esm --outfile=.test-build/mg-cmd.mjs && esbuild src/commands/tool.ts --bundle --platform=node --format=esm --outfile=.test-build/tool-cmd.mjs && esbuild src/commands/transcript.ts --bundle --platform=node --format=esm --outfile=.test-build/transcript-cmd.mjs && esbuild src/commands/project.ts --bundle --platform=node --format=esm --outfile=.test-build/project-cmd.mjs && esbuild src/commands/audio.ts --bundle --platform=node --format=esm --outfile=.test-build/audio-cmd.mjs && esbuild src/commands/subtitle.ts --bundle --platform=node --format=esm --outfile=.test-build/subtitle-cmd.mjs && esbuild src/commands/feedback.ts --bundle --platform=node --format=esm --outfile=.test-build/feedback-cmd.mjs && esbuild src/commands/music-visualizer.ts --bundle --platform=node --format=esm --external:commander --outfile=.test-build/music-visualizer-cmd.mjs && esbuild src/commands/long2short.ts --bundle --platform=node --format=esm --external:commander --outfile=.test-build/long2short-cmd.mjs && esbuild src/commands/oralcut.ts --bundle --platform=node --format=esm --external:commander --outfile=.test-build/oralcut-cmd.mjs && esbuild src/commands/skills.ts --bundle --platform=node --format=esm --outfile=.test-build/skills-cmd.mjs && esbuild src/commands/qc.ts --bundle --platform=node --format=esm --external:commander --external:bun:sqlite --outfile=.test-build/qc-cmd.mjs && esbuild src/lib/convert/ir_to_jsx.ts src/lib/convert/ir_to_am.ts src/lib/convert/ir_to_nv.ts src/lib/convert/am_parse.ts src/lib/convert/nv_parse.ts src/lib/convert/parse.ts src/lib/convert/download.ts src/lib/convert/bake_ops.ts --bundle --platform=node --format=esm --outdir=.test-build/convert --out-extension:.js=.mjs && esbuild src/lib/mad/selector.ts src/lib/mad/beat.ts src/lib/mad/scan.ts src/lib/mad/data.ts src/lib/mad/pool.ts src/lib/mad/cloud-beat.ts src/lib/mad/mad.ts --bundle --platform=node --format=esm --outdir=.test-build/mad --out-extension:.js=.mjs && node --import ./test/helpers/test-home.mjs --test \"test/*.test.mjs\"",
53
+ "test": "esbuild src/lib/compliance-notice.ts src/lib/first-run-tutorial.ts src/lib/desktop-client-hint.ts src/lib/chunk-upload.ts src/lib/upload-cache.ts src/lib/upload-submit.ts src/lib/render.ts src/lib/cloud.ts src/lib/materialize.ts src/lib/jianying.ts src/lib/clip-brief.ts src/lib/media.ts src/lib/projection.ts src/lib/reproject.ts src/lib/splitdoc.ts src/lib/gtrk-writeback.ts src/lib/gtrk-patch.ts src/lib/column-config.ts src/lib/matrix.ts src/lib/matrix-material.ts src/lib/matrix-fetch.ts src/lib/matrix-lay.ts src/lib/arrange-metering.ts src/lib/arrange-wire.ts src/lib/arrange-apply.ts src/lib/arrange-client.ts src/lib/arrange-gate.ts src/lib/arrange-decision-pin.ts src/lib/arrange-qc.ts src/lib/arrange-qc-bind.ts src/lib/rate-limit-wait.ts src/lib/embed-client.ts src/lib/describe.ts src/lib/local-index.ts src/lib/index-decode.ts src/lib/local-search.ts src/lib/qc.ts src/lib/alignment-qc.ts src/lib/bgm-history.ts src/lib/image-move.ts src/lib/cloud-render-guard.ts src/lib/material-integrity.ts src/lib/solid-png.ts src/lib/mg-lint.ts src/lib/mg-lay.ts src/lib/mg-render.ts src/lib/tool-descriptors.ts src/lib/tool-runner.ts src/lib/tool-pricing.ts src/lib/transcript.ts src/lib/runtime-assets.ts src/lib/subtitle-lay.ts src/lib/audio-align.ts src/lib/audio-tighten.ts src/lib/ai-drama-lay.ts --bundle --platform=node --format=esm --external:bun:sqlite --outdir=.test-build --out-extension:.js=.mjs && esbuild src/commands/split.ts --bundle --platform=node --format=esm --outdir=.test-build --out-extension:.js=.mjs && esbuild src/commands/patch.ts --bundle --platform=node --format=esm --external:commander --outfile=.test-build/patch-cmd.mjs && esbuild src/commands/matrix.ts --bundle --platform=node --format=esm --external:bun:sqlite --outfile=.test-build/matrix-cmd.mjs && esbuild src/commands/mg.ts --bundle --platform=node --format=esm --outfile=.test-build/mg-cmd.mjs && esbuild src/commands/tool.ts --bundle --platform=node --format=esm --outfile=.test-build/tool-cmd.mjs && esbuild src/commands/transcript.ts --bundle --platform=node --format=esm --outfile=.test-build/transcript-cmd.mjs && esbuild src/commands/project.ts --bundle --platform=node --format=esm --outfile=.test-build/project-cmd.mjs && esbuild src/commands/audio.ts --bundle --platform=node --format=esm --outfile=.test-build/audio-cmd.mjs && esbuild src/commands/subtitle.ts --bundle --platform=node --format=esm --outfile=.test-build/subtitle-cmd.mjs && esbuild src/commands/ai-drama.ts --bundle --platform=node --format=esm --outfile=.test-build/ai-drama-cmd.mjs && esbuild src/commands/feedback.ts --bundle --platform=node --format=esm --outfile=.test-build/feedback-cmd.mjs && esbuild src/commands/music-visualizer.ts --bundle --platform=node --format=esm --external:commander --outfile=.test-build/music-visualizer-cmd.mjs && esbuild src/commands/long2short.ts --bundle --platform=node --format=esm --external:commander --outfile=.test-build/long2short-cmd.mjs && esbuild src/commands/oralcut.ts --bundle --platform=node --format=esm --external:commander --outfile=.test-build/oralcut-cmd.mjs && esbuild src/commands/skills.ts --bundle --platform=node --format=esm --outfile=.test-build/skills-cmd.mjs && esbuild src/commands/qc.ts --bundle --platform=node --format=esm --external:commander --external:bun:sqlite --outfile=.test-build/qc-cmd.mjs && esbuild src/lib/convert/ir_to_jsx.ts src/lib/convert/ir_to_am.ts src/lib/convert/ir_to_nv.ts src/lib/convert/am_parse.ts src/lib/convert/nv_parse.ts src/lib/convert/parse.ts src/lib/convert/download.ts src/lib/convert/bake_ops.ts --bundle --platform=node --format=esm --outdir=.test-build/convert --out-extension:.js=.mjs && esbuild src/lib/mad/selector.ts src/lib/mad/beat.ts src/lib/mad/scan.ts src/lib/mad/data.ts src/lib/mad/pool.ts src/lib/mad/cloud-beat.ts src/lib/mad/mad.ts --bundle --platform=node --format=esm --outdir=.test-build/mad --out-extension:.js=.mjs && node --import ./test/helpers/test-home.mjs --test \"test/*.test.mjs\"",
54
54
  "openspec:gate": "node openspec/gate.mjs"
55
55
  },
56
56
  "devDependencies": {
@@ -1,23 +1,24 @@
1
1
  ---
2
2
  name: gtrk-ai-drama
3
- description: AI 再现分镜稿生成器——把成片 SOP 第③步(B-roll 底轨阶段)分到 AI_DRAMA 车道的 beat,逐个产**四段朴素描述(故事背景 / 角色 / 分镜 / 原文文稿)+ 独立视觉基调段 + 时长预算**,中英双版,可直接拿去任意外部 AI 漫剧管线(可灵 / 即梦 / Vidu / Veo / Runway / LTX / 本地开源,或 LibTV / OiiOii / TapNow 这类成片 agent 平台)上手用。这是**纯创作 skill、无 gtrk 命令**(产物即描述文本、出片在外部平台、用户手动拼回,同 /gtrk-style-maker)。当用户想「上 AI 再现 / 做 AI 分镜 / AI 动画描述 / 把这段做成 AI 视频 / 给这段配 AI 再现 / 弗洛伊德这段怎么做视频」时使用本 skill。通用拆镜 craft(蒙太奇段分层 / 四段描述结构 / 中英双语 / 时长预算)由本 skill 自持;栏目视觉 DNA(Style Lock)从栏目配置 style.skills 里 produces==AI_DRAMA 的条目解析注入,绝不硬编某栏目风格。凡把口播派单里 AI_DRAMA 段落转成可出片的描述稿,优先用本 skill,别手搓。
3
+ description: AI 再现分镜稿生成器——把成片 SOP 第③步(B-roll 底轨阶段)分到 AI_DRAMA 车道的 beat,逐个产**四段朴素描述(故事背景 / 角色 / 分镜 / 原文文稿)+ 独立视觉基调段 + 时长预算**,中英双版;Markdown 外壳默认严格兼容 gitruck-ai-drama-desk 解析器,可直接导入工作台,也可拿去任意外部 AI 漫剧管线(可灵 / 即梦 / Vidu / Veo / Runway / LTX / 本地开源,或 LibTV / OiiOii / TapNow 这类成片 agent 平台)上手用。创作与出片仍在 skill / 外部工作台;工作台导出的 return-v1 包用 `gtrk ai-drama lay` 确定性回填独立 AI 轨。当用户想「上 AI 再现 / 做 AI 分镜 / AI 动画描述 / 把这段做成 AI 视频 / 给这段配 AI 再现 / 弗洛伊德这段怎么做视频」时使用本 skill。通用拆镜 craft(蒙太奇段分层 / 四段描述结构 / 中英双语 / 时长预算)由本 skill 自持;栏目视觉 DNA(Style Lock)从栏目配置 style.skills 里 produces==AI_DRAMA 的条目解析注入,绝不硬编某栏目风格。凡把口播派单里 AI_DRAMA 段落转成可出片的描述稿,优先用本 skill,别手搓。
4
4
  ---
5
5
 
6
6
  # AI 再现分镜稿生成器(gtrk-ai-drama)
7
7
 
8
- 把口播派单里分到 **`AI_DRAMA` 车道**的 beat,逐个拆成分镜、为每个 beat 产 **四段朴素描述 + 独立视觉基调 + 时长预算**(中英双版),落到 `<project>/ai-drama/<beat_id>.md`,再交棒用户拿去任意外部平台出片、手动拼回时间线。
8
+ 把口播派单里分到 **`AI_DRAMA` 车道**的 beat,逐个拆成分镜、为每个 beat 产 **四段朴素描述 + 独立视觉基调 + 时长预算**(中英双版),落到 `<project>/ai-drama/<beat_id>.md`,再交棒 AI Drama Desk 或任意外部平台出片;标准工作台导出包由 CLI 自动回填时间线。
9
9
 
10
- > **这是纯创作 skill、没有 gtrk 命令**——AI_DRAMA 车道的产物就是**描述文本本身**,出片在外部生成平台、拼回时间线由用户手动做,没有确定性机械尾巴可下沉成 CLI 命令(性质同 `/gtrk-style-maker`)。所以「脑」是你(拆镜 + 注入栏目风格 + 管用户交互),「手」在外部平台和用户手里,不是 gtrk。别去找 `gtrk ai-drama` 这条命令,它不存在也不该存在。
10
+ > **脑手分工**:本 skill 负责创作(拆镜 + 注入栏目风格 + 管用户交互),外部工作台负责生成;`gtrk ai-drama lay` 只消费 AI Drama Desk 的 return-v1 导出包,把已经生成好的片段复制进工程并铺到一条独立 AI 视频轨。该命令不生成内容、不调用模型、不计费,也不改既有 A-roll / BGM / 三条 B-roll 候选轨。
11
11
 
12
12
  ## 为什么是「四段朴素描述」而不是「平台优化提示词」
13
13
 
14
- 因为**责权利要清**:无论用户拿去哪个管线,一致性的可移植载体是**角色/场景描述**,不是某平台的提示词语法("assets model-agnostic, prompts model-specific")。而且下游 agent 成片平台(LibTV / OiiOii / TapNow 等)拿到分镜稿会**自己重拆分镜、重写 prompt**,角色圣经(三视图)也在那些产品里生成——所以本 skill **不产平台优化提示词、不产角色圣经、不产参考图、不产成片**,只产四段可移植描述 + 视觉基调 + 时长,把「怎么出片、怎么保角色一致」交给下游平台,把「拼回时间线」交给用户。
14
+ 因为**责权利要清**:无论用户拿去哪个管线,一致性的可移植载体是**角色/场景描述**,不是某平台的提示词语法("assets model-agnostic, prompts model-specific")。而且下游 agent 成片平台(LibTV / OiiOii / TapNow 等)拿到分镜稿会**自己重拆分镜、重写 prompt**,角色圣经(三视图)也在那些产品里生成——所以本 skill **不产平台优化提示词、不产角色圣经、不产参考图、不产成片**,只产四段可移植描述 + 视觉基调 + 时长,把「怎么出片、怎么保角色一致」交给下游平台。**「拼回时间线」不再交给用户手工**:AI Drama Desk 的
15
+ return-v1 标准包由 `gtrk ai-drama lay` 确定性回填;其他平台没有标准包时才降级为用户手动回铺。
15
16
 
16
17
  ## SOP 定位:第 ③ 步 · B-roll 底轨阶段的 AI 情景片段腿(**不是最后**)
17
18
 
18
19
  成片是**有序 SOP + 用户检查点**,不是并行一把铺。次序:
19
20
 
20
- > ① oralcut 剪口播 → ② split 视觉拆分派单 → ③ **B-roll 底轨全铺齐**(`matrix` 铺影视/本地素材 + **本 skill 产稿→外部出片→用户手动回铺**,**两条腿同一阶段**)→ ④ **全局抽帧检查画面构图**(用户确认)→ ⑤ **mg 铺 MG(含 ov,最后叠上)** → `gtrk subtitle lay` 上字幕(快速成片模式必做)→ `gtrk render` 收口
21
+ > ① oralcut 剪口播 → ② split 视觉拆分派单 → ③ **B-roll 底轨全铺齐**(`matrix` 铺影视/本地素材 + **本 skill 产稿→外部出片→`gtrk ai-drama lay` 回填**,**两条腿同一阶段**)→ ④ **全局抽帧检查画面构图**(用户确认)→ ⑤ **mg 铺 MG(含 ov,最后叠上)** → `gtrk subtitle lay` 上字幕(快速成片模式必做)→ `gtrk render` 收口
21
22
 
22
23
  **你与 `matrix` 同属第 ③ 步,MUST 在 MG 之前落位。** 理由:**AI 情景片段属于底轨 B-roll 画面家族,不是叠加层**——整条管线里唯一的叠加层是 MG(含 ov)。MG 的排版决策是「因势象形避主体」、**依赖底轨的最终画面构图**;若你的片段在 MG 之后才回铺,底轨构图就变了,先产的 MG 是对着「还没有 AI 画面的底轨」做的避让,你的片段落位后必然被避让错位、甚至被 MG 盖住画面主体。
23
24
 
@@ -25,13 +26,14 @@ description: AI 再现分镜稿生成器——把成片 SOP 第③步(B-roll
25
26
 
26
27
  **异步等待的口子(你造成的那个)**:你产完描述后,用户要去外部平台出片、抽卡可能数天。严格串行会把 ④⑤ 无限期卡住,故约定:进 ⑤ 的硬门是「**AI 片段已回铺 ∨ 用户明示先跳过**」。走「明示跳过」时,下游 `/gtrk-mg` MUST 把与你的 beat 相邻/重叠区间的 MG 颗粒标记为「AI 回铺后待复查构图」并在收口时复述——**你在交代进度时也要把这个代价说清**,让用户知道跳过意味着将来可能返工那几颗。
27
28
 
28
- **你的下一步**:产完描述 → 用户拿去外部平台出片 → 手动回铺进工程 → 三源齐了进 **④ 全局抽帧检查构图**(对最终底轨抽帧、用户确认)→ 过了才交棒 ⑤ `/gtrk-mg`。**MUST NOT 直接从你这里跳到 `render`** ——后面还有构图检查与 MG 两步。
29
+ **你的下一步**:产完描述 → 用户拿去外部平台出片 → 若拿到 AI Drama Desk return-v1 导出目录,立即用 `gtrk ai-drama lay` 回填;其他平台没有标准包时才降级为用户手动回铺 → 三源齐了进 **④ 全局抽帧检查画面构图**(对最终底轨抽帧、用户确认)→ 过了才交棒 ⑤ `/gtrk-mg`。**MUST NOT 直接从你这里跳到 `render`** ——后面还有构图检查与 MG 两步。
29
30
 
30
- ## 三条铁律(先记死)
31
+ ## 四条铁律(先记死)
31
32
 
32
33
  1. **业务分离:通用 craft 归本 skill,栏目风格从配置解析。** 本 skill 只持**对任何栏目都通用的拆镜工艺**(蒙太奇段分层、四段描述结构、中英双语、时长预算)。**栏目专属的视觉 DNA / Style Lock(底色、单点强调色、影视参考锚点、材质、禁令)从有效栏目配置解析注入**(见下节),**绝不硬编某栏目风格**——尤其别把《实在界漫游指南》那套「黑白二元 + 单点朱红」当默认塞给所有人,那是它的栏目资产,该从配置来。未绑 AI_DRAMA 风格资产的栏目 → 用本 skill 的**中性默认**兜底。
33
34
  2. **只产描述,不产成片、不产角色圣经、不产参考图。** 本 skill 的交付物是四段可移植描述文本。出片(渲染成视频片段)、角色三视图/角色圣经、参考图,全在下游平台或用户手里。不要假装能帮他渲染或建角色资产。
34
35
  3. **中英双版地位平等,不是主辅。** ①视觉基调、②故事背景、③角色、④分镜四段每一段都给完整的中文版 + 英文版(不是把中文翻一句了事)。中文喂可灵 / 即梦 / Vidu / 通义,英文喂 Veo / Runway / LTX / Luma,用户按目标平台选。**唯独 ⑤ 原文文稿是中文口播原文、不翻译**(它是源真相 + 手动拼接的对位锚,翻译反而失真)。
36
+ 4. **内容可移植,Markdown 外壳不可自由改写。** 默认产物 MUST 通过 `gitruck-ai-drama-desk` 的 Markdown 解析契约:标题必须含 `(beat BXX)`;两大块必须叫 `## 一、中文稿…` / `## 二、English Storyboard…`;角色必须用 `#### 角色名(备注)` 标题;中文镜头必须用 `#### 分镜 01 · …`,英文镜头必须用 `#### Shot 01 · …`;元信息必须含 `区间总时长:track_st … → track_ed … ≈ … 秒`。`镜头`、粗体行、项目符号角色、`## 中文` / `## English`、`工程时间位` 都不是等价写法。**人读友好不能牺牲机读契约。**
35
37
 
36
38
  > **产物落点纪律(MUST · 全文见随包 `AGENT.md` 同名一节)**:
37
39
  > 分镜稿只落**工程目录**(`<project>/ai-drama/<beat_id>.md`)或**用户显式指定处**;
@@ -68,7 +70,8 @@ description: AI 再现分镜稿生成器——把成片 SOP 第③步(B-roll
68
70
  2. **每镜 3–8 秒**:短于 3 秒观众来不及呼吸;长于 8 秒 AI 视频模型会塌(人物表情 / 手部漂移)。
69
71
  3. **长文段拆蒙太奇段**:一个 AI_DRAMA beat 可能覆盖很长一段文稿、含多个场景/情境。此时**先拆蒙太奇段**(每段锚一个场景/情境),**段内 3–6 镜**按情感颗粒切、**段间可换场**。镜数上限按**段级**判定,**不再因整段总镜数超 6 就回退 splitter**——长区间整段做 AI 再现是合法创作,不是拆分错误。
70
72
  4. **切分触发器**(任一出现才考虑切):时间 / 空间改变、主体改变(例:理论家 → 当事人)、抽象概念登场(从「人物戏」切到「象征物戏」)、情绪转折(安静→刺点 / 爆发→凝滞)。**不要因一个句号、一个逗号就切。**
71
- 5. **控整体不控单镜**:你对**区间总时长**负责(各镜建议秒数之和量级贴合 `track_st→track_ed`),但不必把每一镜的秒数抠死——AI 平台交付本就不保证逐帧准,最终由用户手动拼时对齐区间。
73
+ 5. **控整体不控单镜**:你对**区间总时长**负责(各镜建议秒数之和量级贴合 `track_st→track_ed`),但不必把每一镜的秒数抠死——AI 平台交付本就不保证逐帧准,最终对齐由回填这一步兜:走 `gtrk ai-drama lay` 时它按窗口顺排、
74
+ 末镜尽量吸收到 `track_ed`;**素材短于窗口就留空档**(MUST NOT 拉伸/循环去填满),没有标准包才由用户手动拼。
72
75
 
73
76
  ### B. 视觉基调:独立成段 + 融入各描述(一源多处)
74
77
 
@@ -108,12 +111,33 @@ description: AI 再现分镜稿生成器——把成片 SOP 第③步(B-roll
108
111
 
109
112
  **Step 4 · 写四段描述(中英分块)**:先出**中文稿**(② 故事背景末句锁调 → ③ 角色结构化 + 禁变项 + 质感落点,复现理论家 pin 复用同段文字 → ④ 分镜每镜前缀复述基调 + 建议秒数 + 对应原文句 + 蒙太奇级意图 → ⑤ 原文文稿中文逐句不翻译),再整块出 **English Storyboard**(① Style Lock + ②③④ 英文原生化,不含 ⑤)。中英**分两大块、不逐段交错**(craft C)。
110
113
 
111
- **Step 5 · 落盘**:写到 `<project>/ai-drama/<beat_id>.md`(一 beat 一文件,靠 beat id 认得是哪段、互不覆盖)。用户直投单段时落到用户指定处或 `<cwd>/ai-drama/<自拟标题>.md`。
114
+ **Step 5 · 校验后落盘**:先把每个 beat 的候选稿写到系统 temp,运行随 skill 分发的 `scripts/validate-storyboard.mjs <候选稿.md>`;退出码为 0 才写到 `<project>/ai-drama/<beat_id>.md`。失败就按输出逐项修正,MUST NOT 把未通过稿件落进工程。校验器不可用时,至少逐项执行下面「解析器硬契约」与交付前自检。用户直投单段时同样先校验,再落到用户指定处或 `<cwd>/ai-drama/<自拟标题>.md`。临时稿无论成功失败都删掉。
112
115
 
113
116
  ## 输出契约(落盘 Markdown 结构 · 中英分块)
114
117
 
115
118
  顶部放一次责权利声明 + 元信息(不分语言);然后 **`## 一、中文稿`** 一整块、**`## 二、English Storyboard`** 一整块,用户整块复制。
116
119
 
120
+ ### 解析器硬契约(默认必须满足)
121
+
122
+ 下列是 `gitruck-ai-drama-desk` 的机器接口,不是排版建议:
123
+
124
+ - H1 标题含半角或全角括号包住的 `beat <beat_id>`,例如 `(beat B03)`。
125
+ - 元信息使用同一行:`- 区间总时长:track_st 37.685 → track_ed 65.665 ≈ 27.980 秒`;不要拆成「工程时间位」和「目标时长」。
126
+ - 中文与英文块标题分别以 `## 一、中文稿`、`## 二、English Storyboard` 开头;不要简写。否则英文 ①–④ 会被当成中文段并覆盖前面的中文段。
127
+ - ③ 内每个角色都用 `#### <角色名>(<备注>)` 开头,描述写在下一段;不要把角色写成 `- 男性:……` 项目符号。
128
+ - ④ 中文镜头头必须匹配 `#### 分镜 <数字> · <标题> |建议 ≈<秒数>s |段:… |场景:… |角色:… |对应原文:…`;英文镜头头必须匹配 `#### Shot <数字> · <title> |≈<seconds>s |segment: … |scene: … |cast: … |source lines: …`。必须用 Markdown 四级标题,中文关键词是「分镜」而不是「镜头」。
129
+ - 每镜标题后的首个非空正文必须以 `〔本镜视觉基调〕` 开头,解析器据此填 `stylePrefix`;不要写成 `- 视觉基调前缀:……` 普通项目符号。① 的负面词必须单独使用 `> 禁忌:…`,英文用 `> Avoid: …`,否则会被吞进 Style Lock 正文而不是负面字段。
130
+ - 蒙太奇分段只写进镜头头的 `|段:…` / `|segment: …` 字段;④ 内不要另插 `#### 蒙太奇段…` / `#### Montage…` 标题,解析器会把它吞进前一镜描述。
131
+ - 中英文镜头序号一一对应;至少一镜;每镜建议秒数写在头部字段,不要藏在正文项目符号里。
132
+
133
+ 校验命令(从本 `SKILL.md` 所在目录解析相对路径):
134
+
135
+ ```bash
136
+ bun scripts/validate-storyboard.mjs <候选稿.md>
137
+ ```
138
+
139
+ 通过时输出单行 JSON 并退出 0;不通过时逐条列出违约项并退出非零。它校验的是生产端硬契约;工作台仍是最终权威,仓库可用时可再用其 `server/lib/parse.ts` 做回归。
140
+
117
141
  ```
118
142
  # AI 再现分镜稿 · [自动标题](beat <beat_id>)
119
143
 
@@ -197,6 +221,8 @@ face / hair / eyes / wardrobe / signature prop + do-not-change + one line on how
197
221
 
198
222
  ## 交付前自检
199
223
 
224
+ - [ ] `scripts/validate-storyboard.mjs` 已对**每个**候选稿返回 `ok:true`,或在校验器不可用时完成了「解析器硬契约」逐项核对?
225
+ - [ ] 标题、两大块、③角色标题、④分镜标题与区间元信息使用契约原词,没有用“更好看”的同义排版替换?
200
226
  - [ ] 五区块齐备(元信息 + ①视觉基调 + ②故事背景 + ③角色 + ④分镜 + ⑤原文文稿)?
201
227
  - [ ] 每镜有**明确主语 + 明确动作**?(没有就退回)
202
228
  - [ ] 是**真的按情感颗粒切镜**,不是按句号切?长文段拆了**蒙太奇段**、段内 3–6 镜、段划分合理?
@@ -213,8 +239,10 @@ face / hair / eyes / wardrobe / signature prop + do-not-change + one line on how
213
239
  描述稿落盘后**别收工**,把接力讲清(但这一步的「手」在外部平台和用户手里、必须停下等用户):
214
240
 
215
241
  1. **交代出片**:让用户拿 `<project>/ai-drama/<beat_id>.md` 里的描述,去任意外部平台出片——中文版喂可灵 / 即梦 / Vidu;英文版喂 Veo / Runway / LTX / Luma;本地开源(Wan / Hunyuan 等)或成片 agent 平台(LibTV / OiiOii / TapNow)皆可,按下游平台的角色资产机制上传角色描述保一致。
216
- 2. **回铺时间线**:出好的 AI 片段**由用户手动**拖回 opencut 的 **AI_DRAMA 车道**,对齐该 beat 的 `track_st→track_ed`(**它属底轨 B-roll 画面家族,与影视/本地素材同层级;唯一在它之上的叠加层是 MG,而 MG 要等它落位后才产**),按区间总时长裁齐。
217
- 3. **停下等用户**:AI 出片与拼回是**外部异步、用户驱动**的——本 skill 在此停,等所有 AI_DRAMA beat 的片段都回铺齐。这是 SOP 里的用户检查点,别替他假装出片 / 回轨完成。
242
+ 2. **优先标准包回填**:AI Drama Desk 出片后,找到每个项目的 `exports/aidrama/manifest.json`,执行:
243
+ `gtrk ai-drama lay --project "<工程目录>" --package "<B03 exports/aidrama>" --package "<B05 exports/aidrama>" --json`。
244
+ 命令会先按当刻口播工程重投影 beat 窗口,再把所有镜头复制到 `<gtrk目录>/assets/ai-drama/<slug>/`,共用一条新的 `video_track`;每个 beat 从起点顺排,最后一镜吸收到 beat 终点。既有 A-roll、BGM 与 B-roll 轨零改动。若已铺 AI 轨被用户手调过,缺省拒绝覆盖;只有用户明确同意丢弃手调时才加 `--replace-all`。
245
+ 3. **非标准平台才手动回铺**:外部平台没有 return-v1 清单时,才由用户把片段拖回 opencut 的 **AI_DRAMA 车道**,对齐该 beat 的 `track_st→track_ed`。AI 出片仍是外部异步、用户驱动;未拿到完整片段前要停下等用户,别替他假装出片 / 回轨完成。
218
246
  4. **回铺齐 ≠ 收口,还有两步**:你的片段回铺齐只意味着 **③ B-roll 底轨阶段**完成。接着 MUST 走 **④ 全局抽帧检查画面构图**(对三源合并后的最终底轨抽帧,看主体位置 / 安全区 / 画面朝向 / 明暗,**停下等用户确认**)→ 过了才交棒 **⑤ `/gtrk-mg`** 把 MG(含 ov)叠上 → 最后才 `gtrk render` 收口。
219
247
  ⚠️ **MUST NOT 从你这里直接跳到 `render`**(旧文档曾这么写,那是错的——它把 AI 再现当成了最后一层)。
220
248
 
@@ -0,0 +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);