# 分镜协议 — atoms / beat / step

`分镜.js` 是单元视频的数据文件。它描述一条口播如何被拆成白板页、每页使用哪个模板、白板内容怎样随口播逐步出现。

具体模板规范不写在这里，统一放在：

```text
SKILL/templates/<模板目录>/README.md
SKILL/templates/<模板目录>/template.json
SKILL/src/templates/registry.ts
```

`src/templates/registry.ts` 是机器校验的权威来源；`templates/*/README.md` 是 agent 填分镜时的快速说明。新增模板时，先注册模板，再补渲染逻辑和说明。

## 一、核心模型

```text
atom  = 机器生成的全局口播锚点，不承载视觉判断
beat  = 一页白板，覆盖一段连续 atoms，使用一个模板
step  = 这一页内的一次语义推进，覆盖当前 beat 内一段连续 atoms
```

agent 的工作顺序必须是：

```text
读完整口播稿
→ 理解整条视频的叙事逻辑
→ 拆 beat：一页白板讲一个完整小意思
→ 拆 step：一页内按模板容量承载的语义推进
→ 用 atom id 绑定 beat/step 的口播范围
```

不要从 atom 局部往上凑结构。atom 可以细，因为它是时间锚；step 必须按语义结构切，不按句子或 atom 机械切。

## 二、文件结构

```js
const STORYBOARD = {
  book: "道德经",
  chapter: "ch01/01",
  assets: "道德经/水墨卷轴",
  scrollTitle: "道德經·第一章",
  audio: "口播.tts.m4a",

  jing: [
    { id: "s1", parts: [
      { k: "dao", t: "道可道" },
      { p: "，" },
      { k: "notNormal", t: "非常道" },
      { p: "；" }
    ] }
  ],

  atoms: [
    { id: "a001", narr: "这一章，" },
    { id: "a002", narr: "我先请你盯住一个字：道。" },
    { id: "a003", narr: "道不是一个能被你抓住的定义。" }
  ],

  beats: [
    {
      seg: "开题",
      tpl: "T3",
      take: ["a001", "a002"],
      base: { glyph: "道" },
      steps: [
        { take: ["a001", "a002"] }
      ]
    },
    {
      seg: "破题",
      tpl: "T2",
      take: ["a003"],
      base: { title: "先松开定义" },
      steps: [
        {
          take: ["a003"],
          show: { lead: ["道不是概念标签，而是进入方式。"] }
        }
      ]
    }
  ],

  durs: [],
  stepDurs: [],
};
if (typeof window !== "undefined") window.STORYBOARD = STORYBOARD;
if (typeof module !== "undefined") module.exports = STORYBOARD;
```

## 三、顶层字段

| 字段 | 作用 |
|---|---|
| `book` | 书名，用于展示和素材解析 |
| `chapter` | 单元标识，如 `ch01/01` |
| `assets` | 素材套，相对 `SKILL/assets/` |
| `scrollTitle` | 画面标题区小字 |
| `audio` | 同目录下 TTS 音轨，通常是 `口播.tts.m4a` |
| `jing` | 可展示和点亮的原文句子 |
| `atoms` | 全局口播锚点，机器生成，不手改 |
| `beats` | 分镜主数据 |
| `durs` | align 回填，每个 beat 的真实时长 |
| `stepDurs` | align 回填，每个 beat 内各 step 的真实时长 |

## 四、atoms

`atoms[]` 是确定性锚点，不承载视觉信息。

硬规则：

- `atoms[].narr` 拼接后必须逐字等于整条口播。
- agent 不改写 `atoms[].narr`。
- 要改讲法，回 `口播稿.md`，重跑 TTS，再重跑 `draft-storyboard`。
- beat 和 step 只能用 `take` 引用 atom id。

## 五、jing

`jing` 放会在经文区显示的原文。用 `{k,t}` 定义可点亮短语，用 `{p}` 定义标点。

```js
jing: [
  { id: "s1", parts: [
    { k: "a", t: "道可道" },
    { p: "，" },
    { k: "b", t: "非常道" },
    { p: "；" }
  ] }
]
```

点亮由模板的 `state.hi` 使用：

- `["a"]`：点亮 key 为 `a` 的短语。
- `["a", "b"]`：同时点亮多个短语。
- `"sweep"`：整句扫亮。

如果想点亮半句，就先在 `jing.parts` 里把半句拆成单独 `{k,t}`。

## 六、beat

每个 beat 是一页白板。

| 字段 | 作用 |
|---|---|
| `seg` | 段落名，连续同名 beat 会在预览进度点归为一组 |
| `tpl` | 模板 id，必须存在于模板 registry |
| `take` | 这一页覆盖的连续 atom id |
| `base` | 进入 beat 时立即存在的白板对象，字段由模板定义 |
| `steps` | 这一页里的渐进板书节点 |

硬规则：

- 所有 `beats[].take` 必须按 `atoms[]` 原顺序完整覆盖全部 atoms。
- 每个 atom 必须被一个 beat 使用且只使用一次。
- 一个 beat 只能引用连续 atoms。
- 不写 `beat.narr`；renderer 和 align 会从 `beat.take` 自动拼出文本。

beat 的切法看语义，不看句数。一个 beat 不是“几句话”，而是“一页白板最多能舒服承载的一小层意思”。

## 七、steps

每个 step 引用当前 beat 内的一组连续 atoms：

```js
steps: [
  { take: ["a004", "a005"], state: { hi: ["a"] }, show: { aux: [...] } },
  { take: ["a006"], state: { hi: ["b"] }, show: { aux: [...] } }
]
```

硬规则：

- 每个 step 的 `take` 必须是当前 `beat.take` 的连续切片。
- 当前 beat 内所有 atoms 必须被 steps 按顺序完整覆盖。
- 一个 step 是一个语义推进，不是一个句子 reveal。
- 模板内部层级固定；reveal 必须先上层、再下层，不能先露出下层内容再补标题。
- 不写秒数；校验通过后由 `align-durs.js` 从 TTS 时间轴推导 step 时长。

很多模板会用 step 做渐进板书，但不是每个模板都必须有 reveal。像 T3 这种纯题眼模板，steps 只负责覆盖 atoms，不写 `state/show`。

## 八、模板怎么查

当前模板：

| 模板 | 目录 | 用途 |
|---|---|---|
| `T0` | `templates/T0-intro/` | 片头字幕，不写白板 |
| `T1` | `templates/T1-jing-aux/` | 原文 + 渐进注释 |
| `T2` | `templates/T2-title-lead/` | 标题 + 引 |
| `T3` | `templates/T3-glyph/` | 中央大字 |
| `T4` | `templates/T4-closing-line/` | 收束一句话 |

填某个 beat 前，只读对应模板目录的 `README.md`。机器校验以 `src/templates/registry.ts` 为准；改模板后跑 `node SKILL/dist/cli/validate-templates.js`。

## 九、白板内容标准

白板是观看辅助层，不是字幕复述。

好白板：

- 关键词
- 结构提示
- 概念压缩
- 判断句
- 当前 step 只显示当前讲到的信息

坏白板：

- 复制或近似复制口播原句
- 还没讲到就提前显示后面要点
- 一页塞满所有解释
- 为了看起来丰富而加无关词

自检：

```text
观众不听声音，只看当前白板，能不能知道这一小段正在讲什么？
观众听声音再看白板，会不会觉得白板只是在重复字幕？
```

如果第二个问题答案是「会」，就继续压缩。

## 十、模板容量

每个模板的容量定义写在对应目录的 `template.json` 和 `README.md`：

```text
templates/<模板目录>/template.json
templates/<模板目录>/README.md
```

分镜输出必须在模板容量内。容量分两层：

- `perStep`：当前 step 这次最多新增多少内容。
- `perBeat`：当前 beat 作为一页白板，最终最多累积多少内容。

超过 `perBeat` 时，不继续追加 step，必须拆成下一个 beat。T1 可以在多个连续 beat 里重复同一句经文和同一组 highlight，但左侧注释每个 beat 重新开始。

容量样张见：

```text
SKILL/runtime/capacity-review.html
```

## 十一、校验

渲染前必须跑：

```bash
node SKILL/dist/cli/validate-storyboard.js <out>/分镜.js
node SKILL/dist/cli/align-durs.js <out>/分镜.js
node SKILL/dist/cli/layout-check.js <out>/分镜.js
```

`validate-storyboard` 先挡住上文硬规则和 capacity 超限；`align-durs` 回填时长；`layout-check` 检查真实 DOM 溢出。失败时按错误修分镜；不要靠改秒数、改 atom 文本或跳过检查来出片。
