# Chengfeng Subtitle

> 给剪好的口播做字幕：直接用已有的逐词稿加账本算出剪后时间（不必导出、不必重新转录）、用词典和作者文稿改写听错的专名、按句子分屏、在 Studio 里逐屏复核。用户说做字幕、加字幕、改字幕、重新分屏、字幕不对时使用。不要用于删词剪辑、物理剪切、分镜动画或成片渲染。

- Skill: `gabrielmoreira/chengfeng-subtitle` (Agent Skill)
- Install (CLI): `npx skillmds@latest add gabrielmoreira/chengfeng-subtitle`
- Raw SKILL.md: https://api.skillmd.com/api/skills/gabrielmoreira/chengfeng-subtitle/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: gabrielmoreira (https://skillmd.com/u/gabrielmoreira)
- Updated: 2026-09-09
- Page: https://skillmd.com/skills/gabrielmoreira/chengfeng-subtitle

---


# 字幕

**这是一件事，不是流程的一段。** 用户什么时候喊它就什么时候做——刚剪完可以做，
做完又删了两句可以再做一遍。

它只有一个前提：**账本已经存在**。不是因为它排在剪辑后面，而是因为没有账本，
它不知道哪几句留着、各自落在成片的第几秒。

```text
需要   edit-list.json（账本）、transcript.json（逐词稿）
产出   subtitles.json
```

干完就停，**不指挥用户下一步**。

先读取并执行 [业务 Skill 的阶段合同](../../references/business-workflow-contract.md)，
判据见 [字幕校对规则](references/subtitle-correction.md)，专名写法见
[AI 用词词典](../../references/ai-term-dictionary.md)。

## 谁说了算什么

这是这一段的地基，其余都从这里推出来。

```text
剪口播   说了算「留哪些话、在什么时候」    ← 时间的唯一真相
字幕     说了算「屏幕上显示什么字」        ← 显示的唯一真相
```

一屏字幕存的是**词 id 列表 + 显示文字**，**不存秒数**。时间每次从账本现算。
于是两本账没有第二份时间可以走偏，失效也能精确到屏：「第 7、第 12 屏的词被剪掉了」，
而不是那句谁也点不动的「字幕可能已过期」。

**字幕存自己的文字是故意的。** 转录回答「他说了什么」，字幕回答「给人看什么」——
标点、去掉的口头禅、专名的正式写法，这些本来就该不一样。逼一份字符串同时干两件事，
字幕就永远加不了逗号。

## 0. 就绪

先执行 [检查更新](../chengfeng-check-updates/SKILL.md) 的「就绪检查」——skills 是否
最新、Runtime 是否配套；**插件根**也在那里定位（本文命令里的 `<插件根>` 都代入
那个字面路径）。只有「就绪」才继续；「需新会话」或「停」按它的处置执行
（含「禁止自制替代界面」禁令），业务 Skill 不自带环境逻辑。

若就绪结果为 `runtime.kind=desktop-managed`，直接复用桌面 App 已安装的稳定 CLI 与
同一 `launchd/windows-task` 服务；不要解析 Electron 路径、另装依赖或起第二个
Runtime。

## 1. 入口断言

```bash
node "<插件根>/scripts/ensure-running.cjs" --json
node "<插件根>/scripts/videocut-cli.cjs" inspect "<项目目录>" --json
```

| 断言 | 不成立时 |
| --- | --- |
| `edit-list.json` 存在且有片段 | 说清楚缺账本，让用户先喊剪口播。**不要自己去剪。** |
| `transcript.json` 存在 | 项目没准备好，停止。不要自己造一份。 |

**不要断言 `source_cut.mp4`，也不要去转录。** 字幕两样都不需要——见下一节。

## 2. 改字：先词典，后文稿

**不需要重新转录。** 原来那份逐词稿已经带着时间戳，账本知道哪几段留着——
每个词落在成片的第几秒是**算出来的**，不是再听一遍听出来的：

```text
词在源片的第几秒   转录里就有
哪几段留着         账本里就有
词在成片的第几秒   两者一算就出来
```

再送一遍 ASR 只是把同样的话重新听一次，花钱、花时间，而且听得更差——专名要重新改一遍。
（`transcript retranscribe` 是上一版设计的遗留，那时字幕打算靠转写剪后视频拿时间轴。
字幕改成锚词 id 之后这个问题就没了。**做字幕不要用它。**）

两件不同的工具，都要用。

```bash
# 词典：这个说话人的固定写法，不需要证据
node "<插件根>/scripts/videocut-cli.cjs" transcript dictionary "<项目目录>" --dictionary "<插件根>/references/ai-term-dictionary.md" --json

# 文稿：作者手上有稿子时才做，逐处要证据
node "<插件根>/scripts/videocut-cli.cjs" transcript align "<项目目录>" --script "<口播稿文件>" --json
```

```text
词典    永远这么写            不看上下文，直接改，列出改了哪几处
文稿    上下文对得上才这么写   逐处要证据，对不上的报「不敢定」，不猜
```

**为什么必须有词典，光靠文稿不行**：口播会重录。真实项目里同一句录了四遍，
于是「连接叉」「叉是」这些上下文全都出现不止一次，align 只能报「不敢定」——
它按规矩不猜，因为**名字写错比听错更糟**。三处「叉」它只敢改一处。
词典没有这个问题：在这个说话人的作品里，「叉」**就是**「X」。

**词典只有一份**，在插件级 `references/ai-term-dictionary.md`：它改的是转录，
而剪口播和字幕吃同一份转录。不要在这个 skill 下面再建一份。

**改完要看那份清单。** 词典不看上下文，所以它也会改错——比如把说话人真的说的
「Skills」（Claude Skills，复数专名）normalize 成「Skill」。看到不对就改词典，
不要在命令这边打补丁。

`transcript align` 只报不改。要应用它的提案，把 `corrections` 转成 `[{wordId, text}]` 再跑：

```bash
node "<插件根>/scripts/videocut-cli.cjs" transcript correct "<项目目录>" --file "<修正文件>" --json
```

改完转录，**引用了这些词的字幕屏会在同一步里跟着改写**，回报 `subtitles.updated`。
改不动的（那一行被人重写过）进 `needsAttention`，报出来让人看，不猜。

**词典里没有、文稿里也没有任何一个写法能确认正确时，不许猜。** 报出来让用户补词典——
补进词典下一条视频就自动对了，猜对一次下次还要再猜。

## 3. 分屏

```bash
node "<插件根>/scripts/videocut-cli.cjs" subtitle build "<项目目录>" --json
```

四条规则，按顺序：

```text
① 这里删掉过话    删掉的两边在时间轴上挨着，意思上毫无关系。
                 删掉的是「静音」不算 —— 把句子中间的停顿剪掉，不能把句子劈开。
② 任何标点        火山给的，句号和逗号都算 —— **和剪口播分段是同一个粒度**，
                 两边断在同一处。标点比任何时间证据都强：说话人可以两句连着
                 说不喘气，也可以在一句中间停顿。
③ 段落边界 + 停顿  **只在这个词没有标点时才用**。它是「没标点时靠段落猜句子结束了」，
                 有标点就该由②和逗号那条管 —— 它们会考虑屏够不够长，③ 不会。
                 （在带标点的边界上也触发③，真实项目从 40 屏变 43 屏，切出更多碎屏）
④ 观众听到长停顿   句子内部要断，得有更长的静音才够格。
```

**剪口播和字幕吃同一份标点，粒度也一样**：一个逗号一段 / 一屏。

碎片（一两个字）会被并掉，但**不跨句号并** —— 「你看」开启新的一句，
不是上一句的尾巴，往前并会得到「…调用Grok CLI你看」，两句糊在一起。

说话人说得快的短句会留下来（真实项目上「每天早上」「执行任务」各 0.6 秒）。
**那是他真的这么说的，不报警。** 一个逗号一屏必然产生这种屏，
为它报警等于对正常说话报警——报多了就没人看警告了。

剩下比一屏长的，均摊拆成几屏，**一屏一行**。切出一两个字的碎片，说明②那个边界
其实不是句号，把碎片并回上一屏。

命令回报四件事，都要看：

```text
stale              被剪辑改动的屏 —— 精确到第几屏、丢了哪几个字
tooFast            字太多、时间不够读
transcriptMoved    转录已经不是当初那一份了
```

### 已经有字幕时 —— 这里要停下来问

不加 `--replace` 它会直接拒绝。**这是这个 skill 里唯一要征求用户同意的地方**，
因为 `--replace` 冲掉的是人花时间做的分屏和措辞，产品自己恢复不了。

```text
用户想改几个字     不要 build。让他在 Studio 里直接改，或者告诉你改哪几屏。
用户要推倒重来     确认过了再加 --replace。
```

## 4. 复核：在 Studio 里逐屏看

```bash
node "<插件根>/scripts/videocut-cli.cjs" open "<项目目录>" --json
```

打开后切到左栏的**「字幕」标签页**。一行一屏：左边序号和时间，右边就是字，点进去直接改。

- **回车** = 从光标处另起一屏
- **行首退格** = 并入上一屏
- 右栏「参数 → 字幕」四个预设选一个，全片一个样式
- 画面上能直接看到字幕，位置和字号跟画面成比例——**预览和成片是同一套数**

复核时至少让用户确认：专名对不对、有没有吞字、断句读起来顺不顺、一屏停留够不够读完。

## 5. 到 `subtitles.json` 为止

**这一段做完了。** 不做物理剪切、不做分镜、不做动画、不渲染成片，**也不催用户下一步**。

报告必须分开写：Product 结构化 readback 为 **API/readback PASS**；真实同项目浏览器帧
审核才是 **visual frame PASS**；没有人实际看过字幕跟画面对不对时一律为
**human listening UNVERIFIED**，不得用 DOM、截图或文件探测替代。

## 边界：烧进画面不是这一段的事

产出方式定的是**烧进画面**，而画面是导出那一段画的。字幕的产出就是 `subtitles.json` ——
里面的尺寸全是画面的百分比，导出按输出分辨率换算成像素即可，两边同一套数。

**今天导出的 mp4 里没有字幕**，那一步还没做。报告里要写清楚这个后果，
不要说「字幕做好了」就完事——用户会以为成片里有。

## 恢复与失败

- `subtitles_exist`：已经有字幕了。**先问，别直接 `--replace`。**
- `revision_conflict`：另一处也在写。重新读取，不自动覆盖。
- 词典报「第 N 行读不懂」：那是一行格式坏掉的表格行。规则只认表格行（`| 正确写法 | 常见误识别 |`），
  散文和列表都会被安静忽略。
- `transcript correct` 报「must not change any word id, time or gap flag」：
  改字的提案里混进了改时间的东西。改字就只改字。
- `runtime_unhealthy`：不要循环重装。
- 任何失败都不得把「转写完成」说成「字幕做好了」。**ASR 出来的是原料，不是产物。**

