Turn a topic or an SRT subtitle file into a themed text-animation web video — an independent HTML5 player with RAF-driven audio-synced scenes. First-class deliverable is the web player itself; MP4 is an optional export via CDP-automated headless Chrome (default) or OBS/FFmpeg (fallback). Use when the user asks to 根据主题生成口播视频, 从文案到视频, 根据SRT制作视频, 字幕生成视频, SRT分镜, 文字动画视频, text-motion-video, 或 improve/rebuild an existing project generated by this workflow.
Scanned 8/30/2026
Install to Claude Code
npx -y skills add huige-opc/text-motion-skill --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of text-motion-skill?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/huige-opc-text-motion-skill)More formats (shields.io, HTML) on the badges page.
---
name: text-motion-skill
description: Turn a topic or an SRT subtitle file into a themed text-animation web video — an independent HTML5 player with RAF-driven audio-synced scenes. First-class deliverable is the web player itself; MP4 is an optional export via CDP-automated headless Chrome (default) or OBS/FFmpeg (fallback). Use when the user asks to 根据主题生成口播视频, 从文案到视频, 根据SRT制作视频, 字幕生成视频, SRT分镜, 文字动画视频, text-motion-video, 或 improve/rebuild an existing project generated by this workflow.
---
# Text Motion Video(v2 · 独立网页 + 自动化 MP4)
> 🔀 **导航入口**:接到任务先看 [SKILL_INDEX.md](SKILL_INDEX.md)——总入口文件,按模式/阶段找"读什么、跑什么",不迷路。
## 一句话说清这个技能
**给我一个主题(或 SRT),我给你一个"网页版视频"**:
- 一个能在浏览器里播放的独立网页(`index.html` + `assets/` + `scenes/` + `audio.mp3`)
- 口播由 AI 克隆声音生成,画面由 AI 按详细口播稿设计
- **RAF 咬合齿轮**:每一帧读 `audio.currentTime`,画面元素跟着口播节奏出场
- 可选:一行命令导出 MP4(两种 CDP 方案:管道省磁盘 / 写文件更小)
## 核心原理:RAF 咬合齿轮
```
上齿轮:audio.mp3 的 currentTime ← 时间轴绝对真理
↓ 每帧咬合
下齿轮:CSS class 揭示(.reveal-XX) ← 画面跟着口播出场
```
`main.js` 每一帧读 `currentTime`,到点了就给 `#mount` 加上对应的 `reveal-*` class,CSS 里 `#mount.reveal-XX .element { animation: ... }` 命中,动画就跑。**自成一体,不依赖任何外部引擎**。
## 交付形态(按优先级)
| # | 交付物 | 状态 |
|---|---|---|
| 1 | **独立网页**(永远产出) | 打开 index.html 就能播 |
| 2 | **CDP 逐帧精确 MP4** | `scripts/render-mp4-seek.mjs` 逐帧 seek+截图(推荐,音画零漂移) |
| 3 | **CDP 管道实时截帧 MP4** | `scripts/render-mp4-pipe.mjs` 实时截帧走管道(快速,长视频有漂移风险) |
| 4 | **CDP 写文件 MP4** | `scripts/render-mp4.mjs` 截图写磁盘(同上原理,兜底) |
| 5 | **FFmpeg 自动录屏** | `scripts/render-mp4-gdigrab.mjs` 弹窗录屏(需可见窗口) |
| 6 | **OBS 手动录屏**(兜底) | 见 `references/RENDER_MP4_MANUAL.md` |
## 入口流程(强制,技能触发后第一件事)
```
技能触发
↓
🔴 **CHECKPOINT · 第一步:选布局模式(必须问用户)**
├── 1. 全屏模式(fullscreen)— 纯内容画面,无人像
└── 2. 人像模式(portrait-center)— 有真人出镜视频,居中全屏背景 + 左右玻璃浮层
↓
🔴 **CHECKPOINT · 第二步:选风格主题(必须问用户;仅全屏模式,人像模式跳过)**
├── 自动匹配 — 根据内容关键词 AI 推荐
├── 用户指定 — 如"我要科技风""暖色系"
└── 候选展示 — 列出 2-3 个匹配主题让用户挑
(人像模式背景是用户视频、画面不固定,不套主题——文字颜色按背景自适应,辉哥 2026-08-04 定)
↓
第三步:内容来源(🛑 必须问用户"你手上有什么素材",不能默认选择)
├── 全屏模式:
│ ├── 🔴 **先问"你手上有什么素材?"(四选含其他,辉哥 2026-08-05 定——问素材比问"有没有稿"更准,因为 SRT 本身就是文字稿,有成品音频的人不该再走录音)**:
│ │ ① **只有文字稿**(口播稿/文案,无音频)→ 让用户提供(粘贴/指定路径)→ 确认口播稿 → Step 1b 录音
│ │ ② **有成品 SRT + 音频**(配音+时间线已就绪)→ 直接用 → 跳过写稿配音 → 从场景描述稿开始做画面(素材进 Step 2)
│ │ ③ **只有主题**(什么都没有)→ 🔴 **引导用户输入主题或相关内容** → 用口播稿生成技能(koubo-script-writer)按结构生成口播稿 → 🔴 **生成后必须问用户"是否要修改"**,用户改完确认后 → 才进 Step 1b 录音
│ │ - AI 不得自己定主题:用户没给主题时**必须问**"这条视频要什么主题",给候选 + OTHER(血训 2026-08-05:AI 自造主题写稿 → 辉哥纠正"应该先问我要什么主题" → 返工)
│ │ ④ **其他**(都不符合)→ 🔴 **停下问用户具体情况**(如:有视频没稿?有稿是别的格式?只有图片/PPT?),按用户说的判断该走哪条分支或另起流程,**不擅自归类**
└── 人像模式(辉哥 2026-08-05 详细定):
1. **自动创建项目文件夹**(按规则命名 `{YYYYMMDD}-1` 当天第一个 / `-2` 第二个 / `-3` 第三个,辉哥 2026-08-05 定)
2. 🔴 **提示用户导入素材**(文件拖入 / 指定路径),不默认自造
3. 🔴 **检查素材条件是否具备**:
- **视频**(`background.mp4` / `portrait.mp4`)→ ✅ 满足(视频含音频):ffmpeg 提取音频 → 音频转写字幕(**字幕由音频转写生成,非用户输入**)
- **音频**(`audio.mp3`)→ 需转字幕带时间线(Whisper / 百度识别 → `audio.srt`)
- **已有 SRT + 音频** → 直接用(跳过转写)
- 条件不足 → **提示用户补充对应素材**,不擅自替用户决定
```
### 两种布局的流程差异
| 步骤 | 全屏 | 人像居中 |
|------|------|---------|
| 初始化 | `--layout fullscreen` | `--layout portrait-center` |
| 场景写法 | `#mount` 单挂载 | `#mount-left` + `#mount-right` + `#mount-bottom` 三挂载(底部大组件区可选) |
| 用户素材 | 不需要 | 放 `项目根目录/background.mp4` |
| 预览注意 | 常规 | 检查玻璃面板在背景上可读 |
| 渲染 MP4 | 正常 | 正常(render-mp4-seek 直接渲染 HTML,background.mp4 视频元素自动播放;渲染方式见 Step 5 引导选择) |
### 预览地址约定
三种布局使用不同端口避免冲突:
- 全屏:`localhost:3009`
- 人像居右:`localhost:3010`
- 人像居中:`localhost:3011`
## Defaults
- Default aspect ratio: infer `16:9` unless the user explicitly names a vertical destination; confirm before initialization.
- Default theme: run `select-theme.mjs`; all 31 themes are always available for auto-selection.
- 🔴 **CHECKPOINT**:Preview and obtain user approval before rendering MP4,并询问画质(high / standard / general)。用户确认后:
```powershell
node "{skillRoot}/scripts/check-gates.mjs" "{projectRoot}" confirm preview-ok
```
- 🔴 **渲染门禁**:渲染脚本启动时自动检查 `preview-ok` 关卡,未确认则拒绝渲染。
## Read Only What Applies
## 失败模式速查表
工作流中每一步都可能出问题。以下按阶段列出常见失败、触发条件和修复动作。
| 阶段 | 失败信号 | 一线修复 | 仍失败兜底 |
|------|---------|---------|-----------|
| **写稿** | AI 生成的口播稿质量差/跑题 | 退回 Step 1a,补充更多主题关键词和示例 | 用户手动改写关键段落后再让 AI 润色 |
| **压缩** | 压缩后某场景时长 < 6s | `compress-script.mjs --min-duration 6` 自动补静音 | 退回 Step 1a 补充该场景文案(至少 26 字),或增加新场景 |
| **TTS** | 百度 TTS 返回错误或无响应 | 检查 `BAIDU_API_KEY`/`BAIDU_SECRET` 环境变量是否设置、网络是否通 | 用户自己录音,跳过 AI 配音 |
| **SRT** | SRT 时间戳呈固定间隔(如全部 X.671 → X+2.671) | 这是伪造 SRT,不能用。用 `ffprobe silencedetect` 按真实音频重建 | 跑 Whisper 重转写 |
| **scene-timing** | 实际时长 vs 预计偏差 > 50% | `match-scene-timing.mjs` 会标记 `⚠` 场景,人工检查 SRT 是否漏句 | 重新跑 `build-timing-from-srt.mjs` |
| **写场景** | 场景 HTML 中文字符出现 `�`(U+FFFD) | grep 搜 `�` 定位截断位置,补全被截断的中文字 | 整段重写该场景 |
| **写场景** | 元素停在 `opacity:0` 不出现 | 检查场景 script 花括号是否平衡(grep `{` 和 `}` 数量) | 检查 CSS reveal 选择器路径是否正确 |
| **写场景** | 所有中文变成乱码(如"璋冪爺")" | Python 脚本以 GBK 编码读写 UTF-8 文件导致。**禁止用 Python 工具读写场景 HTML**。用 Node.js 或手动编辑。恢复方法:从 SRT 重建中文内容 | 重写该场景 HTML |
| **写场景** | `//` 注释吞掉同一行后续代码 | 编码损坏导致 `//` 注释和代码合并到一行。用工具或手写分离注释和代码,或改用 `/* */` 块注释 | 整段重写该场景脚本 |
| **预览** | 键盘切帧无效/进度条拖不动 | 用 `Invoke-WebRequest` 验证音频响应头含 `Accept-Ranges: bytes` | 换服务器(`_serve.js` / `npx serve`),不要改业务代码 |
| **渲染** | CDP 管道模式失败 | 降级到 CDP 写文件模式 `render-mp4.mjs` | 再降级到 gdigrab 自动录屏或 OBS 手动录屏 |
| **渲染** | **MP4 时长比音频短 / 越往后画面越超前(音画漂移)** | screencast 连续截帧节奏≠30fps,固定帧率编码导致累积漂移(20260730 验证:339s 视频只截到 331.67s)。**改用 `render-mp4-seek.mjs` 逐帧精确渲染**;渲染后 `ffprobe` 对比时长,偏差 >0.5s 即异常 | 重渲染前确认 `scene-timing.json` 末场景 `end` ≥ 音频时长 |
| **渲染** | 渲染前 `verify-timeline.mjs` 校验 FAIL(三方对不上) | SRT 与音频不一致 → 重新转写;末场景 `end` < 音频时长 → 重跑 `build-timing-from-srt.mjs` / `generate-full-audio.mjs` | 手动核对 SRT 末条时间戳与 `ffprobe` 音频时长 |
| **渲染** | 逐帧渲染出全黑视频 | 渲染脚本内置 HTTP 服务未支持 Range → Chrome 无法对音频 seek,`currentTime` 全被重置为 0,每帧截到空白初始画面。**服务必须响应 `Accept-Ranges: bytes` + 206** | 检查 `tryCreateServer` 是否实现了 Range 分支 |
| **渲染** | Chrome 版本不兼容/npm 装不上 | 跳过所有自动化渲染,走 OBS 兜底 | 参考 `references/RENDER_MP4_MANUAL.md` |
- `references/CREATIVE_RULES.md` — SRT storyboard、layout density、style、forbidden effects
- `references/AESTHETIC_GUARDRAILS.md` — 硬禁用清单
- `references/AESTHETIC_THEMES.md` — 主题美学
- `references/SYNC_ENGINE.md` — RAF 揭示原理和 beat 时间控制
- `references/scene-creator.md` — 写场景 HTML 的核心指引(v2 版)
- `references/components/INDEX.md` — 组件库索引(**优先读取**)
- `references/RENDER_MP4_MANUAL.md` — OBS 兜底流程
Optional:
- `references/THEME_SELECTOR.md` / `THEME_DEV_GUIDE.md` — 主题调试和扩展
- `references/AUDIO_SYNC_PREVIEW.md` — 音频驱动预览
- `references/MOTION-CANON.md` — 自定义动画
- `references/optional/*.md` — 外部素材分析和扩展主题
## Project Paths
- `skillRoot`:本文件所在目录
- `projectsRoot`:`{skillRoot}/projects/`(技能目录下的 projects/)
- `projectName`:自动生成 `{YYYYMMDD}-1`(当天第一个)、`{YYYYMMDD}-2`(第二个)、`{YYYYMMDD}-3`(第三个)…(辉哥 2026-08-05 定:第一个即带 -1,不用无后缀)
- `projectRoot`:`{projectsRoot}/{projectName}`
**扁平目录结构**(v2,不再有 v2/ 子目录):
```
projects/{projectName}/
├── script.md ← 口播稿(配音用稿,定稿原文,无压缩)
├── script-detailed.md ← 场景描述稿(整个场景描述,不参与配音)
├── audio.mp3 ← AI 配音(从口播稿生成,带时间线)
├── audio.srt ← 转写字幕(每句时间戳 = 时间线真相)
├── scene-plan.json ← 场景规划(按 SRT 时间线分解场景片段)
├── scene-timing.json ← 场景时间轴
├── project.config.json ← 项目元数据
├── theme.json ← 主题元数据
├── index.html ← 播放器入口(网页交付物)
├── assets/
│ ├── main.js ← RAF 播放引擎(含键盘导航 seeking 锁)
│ ├── theme-defaults.css ← 主题变量兜底(--hf-info/--hf-pos/--hf-bg-surface 等)
│ ├── tokens.css ← 主题 token(会被 init-project.mjs 用主题覆盖)
│ ├── tokens-extra.css ← --tvp-* 独有变量(不随主题变)
│ ├── chrome.css ← 播放器壳
│ ├── shared.css ← 通用组件
│ └── components.css ← 组件样式
├── scenes/
│ ├── 01-hook.html ← 每个场景一个独立 HTML 片段
│ ├── 02-xxx.html
│ └── ...
└── output.mp4 ← MP4 交付物(可选)
```
## Workflow
**项目位置说明**:初始化命令跑完后,项目自动创建在 `{skillRoot}/projects/` 下。`{projectRoot}` 就是项目的完整路径,脚本会打印出来。用户的口播录音、人像视频等素材都放到 `{projectRoot}/` 下。
### 全屏模式(fullscreen)— AI 写稿 + AI 配音
**主题生成模式**从 Step 1 开始。
---
### Step 1. 生成口播稿 + 音频(音频先于场景规划,辉哥 2026-08-02 强制)
**生产顺序(不可反,辉哥 2026-08-02 定):**
```
Step 1a 口播稿(script.md 定稿,配音用稿)→ script-ok
↓
Step 1b 音频(从口播稿直接生成,SRT 每句真实时间戳 = 时间线真相)→ audio-ok
↓
Step 1c 场景描述稿(原"详细稿",整个场景描述,从口播稿扩展)
↓
Step 1d 场景规划(基于 SRT 时间线,把场景描述稿分解成场景片段)
↓
Step 1e 写场景 HTML → 双层检查 → 渲染
```
**为什么音频先生成(辉哥 2026-08-02 定)**:音频是"口播稿配音 + 每句真实时间戳",是**时间线真相**。场景是**匹配层**,按音频时间段拆分。音频不生成,就没有时间线,场景规划没依据。以前"详细稿前置 + 按场景逐段生成音频"的逻辑已废弃——那是把场景切分放在音频之前,违背"音频固定、场景匹配"。
**概念对齐(辉哥 2026-08-02 定)**:
- **不存在"精简稿"**——以前压缩口播稿导致压缩过度、音频不完整。**AI 配音永远用定稿口播稿原文,绝不压缩。**
- **详细稿改名"场景描述稿"**——只做场景规划的语义骨架,**不参与配音**;做场景切分时,才把整个场景描述稿分解成场景片段
#### Step 1a:确定口播稿(配音用稿)
> 🔴 **入口素材分支(辉哥 2026-08-05 定,对应入口三选):**
> - **素材① 只有文字稿** → 让用户提供(粘贴文本 / 指定文件路径),保存为 `script.md` → 直接进"确认口播稿"(步骤 2)→ 进 Step 1b 录音
> - **素材② 已有成品 SRT + 音频** → **跳过本步和 Step 1b**(不写稿不配音)→ 从场景描述稿开始做画面(见 Step 2 入口;SRT 即时间线真相,直接基于它提炼)
> - **素材③ 只有主题** → 🔴 **引导用户输入主题或相关内容**(问"这条视频要讲什么",给候选 + OTHER)→ 读 `references/koubo-script-guide.md` 按口播稿规则生成口播稿 → 🔴 **生成后必须问用户"是否要修改"**,用户改完确认后才进 Step 1b
1. 生成**口播稿**(`script.md`)——配音/录音的最终文本。时长由内容结构自然决定(类型A 约 2-3 分钟,类型B 约 3-5 分钟),不设硬性字数目标。
🔴 **口播稿格式整理(辉哥 2026-08-07 强制)**:确认口播稿前必须先整理格式——
- **每行一个完整句子**(一个语义单元),**禁止一行几个字的碎片排版**(血训:碎片排版 + generate-full-audio 按标点断句 → 190 个碎句,TTS 念出来节奏很断)。碎片短句(如"找素材。""做动画。")合并成完整句(如"找素材、做动画、配字幕、想转场,一条3分钟口播视频,剪一天太正常了。")
- **标题/元数据行不进配音**:`script.md` 首行不要放"xxx 口播稿(最终版)"这类标题,AI 配音用稿从正文第一句开始。generate-full-audio 已内置"文件开头标题块跳过"逻辑兜底(2026-08-07 加),但写稿时仍应直接删掉标题
2. 🔴 **口播稿质量检查(去AI味 + 违禁词,2026-08-09 强制)** — 生成后、展示给用户前,必须逐条执行 `references/koubo-script-guide.md` 的「去AI味检查清单」和「合规安全自检」:
- **去AI味**:单字动词/排比句/缺主语/"其实"开头/"这事"/书面词/过度压缩成分/缺"可以·了",命中就改
- **违禁词**:极限词(最/第一/最佳/绝对)、导流违禁(微信/链接/私信)、虚假夸大、AI 生成标注
- **口播稿不合规不允许展示给用户**(血训 2026-08-09:口播稿没跑去AI味检查直接进配音 = 违规)
3. 🔴 **CHECKPOINT:确认口播稿(素材①③走这步)** — 生成后向用户展示,**用户说 OK 才继续,不满意就循环修改**。用户确认后:
```powershell
node "{skillRoot}/scripts/check-gates.mjs" "{projectRoot}" confirm script-ok
```
**确认后才进 Step 1b 录音**(血训 2026-08-05:口播稿未确认直接配音 = 违规)。
#### Step 1b:生成音频(带每句真实时间戳 = 时间线真相)
> 🔴 **血训(2026-08-05 全屏实测):录音方式必须问用户,AI 不得默认**——即使口播稿阶段已隐含"AI 配音",**Step 1b 开始前仍必须用选择项问一次"自己录音还是 AI 配音"**,不能跳。血训实例:AI 直接跑 generate-full-audio 被辉哥拦截"这里是否也要问用户呢"。这条规则早已在文档,AI 漏执行 = 规则≠执行,故前置到步骤开头并标注"机器强制"。
**Step 1b 强制流程:**
3. 🛑 **CHOICE(第一步就做):录音方式** — **必须用 AskUserQuestion 问用户选**"自己录音"还是"AI 配音",**不能默认**,选了再往下走:
**选项 A:用户自己录音**
> "请录制口播音频,保存为 `{projectRoot}/audio.mp3`。按稿子念,每段自然停顿。"
**选项 B:AI 自动配音(推荐)** — 直接从**定稿口播稿原文**生成,**不经过任何压缩/详细稿**(无压缩稿概念):
```powershell
node "{skillRoot}/scripts/generate-full-audio.mjs" ^
--script "{projectRoot}/script.md"
```
- 输入 `script.md`(口播稿定稿原文),**逐字保留、不压缩**(无压缩稿,不存在"精简稿")
- 逐句 TTS 生成 + ffprobe 实测每句时长 → **SRT 每句带真实时间戳 = 时间线真相**
- 生成 `audio.mp3` + `audio.srt`。**本阶段不生成 scene-timing.json**(场景时间轴由场景规划阶段基于 SRT 分解生成)
- **自动安全增益**:合并后测峰值并放大到 -1.5dB(约 +4~6dB 响度提升),不削波不损音质
- 音色读取自 `.env.local` 的 `VOICE_ID`(填入你自己的克隆音色 ID;**克隆音色 ID 属隐私,勿上传/勿公开**,可用 `--per` 覆盖)
4. 🛑 **CHOICE(选项 B 选 AI 配音后、生成前必须问):语速** — **必须用 AskUserQuestion 问用户选**"正常速度"还是"倍速",**不能默认**。若选倍速,给建议区间 **1x–1.5x**(AI 默认 1.4x,辉哥 2026-08-05 实测:AI 原始语速偏慢)。**引导用户时注明百度 spd 档位对应真实速率**:
| 百度 spd | 真实速率 |
|:---:|:---:|
| 5 | 1.0x(正常) |
| 7 | 1.2x |
| 9 | 1.4x(推荐) |
| 11 | 1.6x |
| 13 | 1.8x |
| 15 | 2.0x |
用户选定倍速后,AI 配音命令加 `--speed <x>`(如 `--speed 1.4`),**百度服务端直接按对应 spd 档位变速输出**,不二次转码、音质保证:
```powershell
node "{skillRoot}/scripts/generate-full-audio.mjs" --script "{projectRoot}/script.md" --speed 1.4
```
- `--speed` → 百度 spd 换算:`spd = clamp(5 + (speed-1)*10, 0, 15)`(实测 spd9=1.4x)。百度直接变速后 ffprobe 实测每句时长累积 SRT 时间戳 → SRT 时间线同步更新(时间线真相不破坏)
- **兜底**:用户换语音模型(非百度 TTS)时,加 `--atempo` 让脚本百度原速生成 + ffmpeg atempo 后期变速(保持音高),同样 ffprobe 实测后时间线正确。百度默认走 spd 直出,不需此参数
- **句间停顿保持不变**(0.35s 不动,只变速说话内容,更像人自然停顿)
- 用户自己录音(选项 A)时**不适用**倍速(语速由录音决定)
5. 🔴 **前置门禁**:配音前必须确认口播稿已通过(generate-full-audio 会自动检查):
```powershell
node "{skillRoot}/scripts/check-gates.mjs" "{projectRoot}" require script-ok
```
6. 生成成功后确认关卡:
```powershell
node "{skillRoot}/scripts/check-gates.mjs" "{projectRoot}" confirm audio-ok
```
> **核心原则**:视觉因素优先。音频是固定不变的"时间线真相",场景是匹配层。静音填充只是**兜底**(给动画留执行时间),**不靠静音硬撑**。`--max-silence` 是应急兜底参数,不是常规手段,最长不超过 5-6s。
7. 用户自己录音(选项 A)时,用 Whisper / 百度语音识别转写为带时间戳的 SRT,对照口播稿修正错别字,保存到 `{projectRoot}/audio.srt`。
#### Step 1c:生成场景描述稿(语义切分好的场景清单)
> 🔴 **血训(2026-08-05 全屏实测):场景描述稿 = 按语义切分好的场景清单,不是照 SRT 逐条排**。AI 曾直接把 SRT 19 条逐条拆成 12 个场景,其中多个 <4s(2.41s/2.89s/3.14s),违反 4-12s 护栏。**生成场景描述稿时就要保证每场景 4-12s**(低于 4s = 视觉没展开,高于 12s = 空洞撑不满),低于 4s 的场景必须与相邻场景按语义合并。
> 🔴 **血训(2026-08-05 全屏实测):锚点必须锚定场景的起始字幕**。build-timing 的锚点语义是"场景起点锚点"——把"从本锚点匹配的字幕到下一个锚点之前"归为一个场景。**锚点用场景内任意标志词 = 该词之前的字幕被吞进上一场景**(实测:scene-09 锚点用末句"机会比谁都多"→ 句17/18 被吞进 scene-08 变 13s 超标、scene-09 只剩 2.3s)。**每个场景的锚点必须是该场景第一条字幕中的短语(一字不差)**。
8. 基于口播稿 + SRT 时间线生成**场景描述稿**(`script-detailed.md`):
- **每场景 = 若干条 SRT 字幕按语义组合**(几段字幕讲同一件事 → 合并成一个场景),**不是一条字幕一个场景**
- 🔴 **每场景时长锁定 4-12s(护栏2)**:内容多/关键 → 8-12s;过渡/简短 → 4-6s。低于 4s 必须合并,高于 12s 必须拆(在 Step 1c 就完成,不等 Step 1d)
- 每场景:视觉类型 + 锚点词 + 画面细节 + 数字强调
- 视觉类型从预设清单选(钩子/痛点/数据/方案引出/演示/金句/对比/分类列举/场景切换/升华收尾)
- 锚点词必须提炼自口播稿原文,不能凭空造;**且必须与 SRT 转写文本一字不差(不能意译/改字)**——build-timing 用子串匹配 SRT,改一个字就找不到 → 静默吞场景错位(血训 2026-08-05:锚点错字 → 场景数减少、单场景时长爆炸吞掉相邻场景)
- **不参与配音**,只做场景规划的语义骨架
- 保存到 `{projectRoot}/script-detailed.md`
9. 🔴 **CHECKPOINT:确认场景描述稿** — 向用户展示场景描述稿(语义骨架 + 锚点),**用户说 OK 才继续**。不清楚的先问用户,不要自己默认。用户确认后:
```powershell
node "{skillRoot}/scripts/check-gates.mjs" "{projectRoot}" confirm detailed-ok
```
#### Step 1d:场景规划(基于 SRT 时间线,把场景描述稿分解成场景片段)
> 🔴 **先读 `references/SCENE_PLANNING.md`(场景规划方法论,辉哥 2026-08-02 定)**——它定义了规划的本质、流程、每场景 6 项要素、规划铁律。**规划对了后面不修补**(血训:35 碎片场景→18、声波动效漏规划→事后补)。不读就规划 = 违规。
> 🔴 **规划前强制先读规范(两层,仿人像 Step 2c1,辉哥 2026-08-05 补)** — **跑 generate-scene-plan 前必须读透以下文件,不读就规划 = 违规**(血训:规划前不读规范 → 密度/动效/死区规划不完整,写完 HTML 才被 R2/S30 拦 → 大量修补返工。视觉密度(元素数/动效数/活动效)必须规划时定死,不等写完 HTML 才查):
> **第一层 规范文件**(教 AI 怎么规划):
> - `references/SCENE_PLANNING.md`(方法论:6 项要素/规划铁律)
> - `references/MOTION_LIBRARY.md`(元素库 × 动效库:每场景元素 4-10 个、动效 ≥3 种、活动效 ≥1)
> - `references/SCENE_PREFLIGHT.md`(源头治理:深度理解口播稿 → expressionCore)
> - `references/QUALITY_GATES.md`(GATE 0:切分质量 / 视觉密度门禁)
> **第二层 项目内容文件**(告诉 AI 要规划什么):
> - `script.md`(口播稿,理解语义源头)+ `script-detailed.md`(场景语义骨架 + 锚点)
> - `audio.srt`(时间线真相)+ `scene-timing.json`(场景边界/时长)
> 读完后**逐场景自检视觉密度**:元素数量达推荐(recommendedElements)、动效 ≥3 种、持续活动效 ≥1、无 >2.5s 死区——不达标就在 expressionCore 里设计补足,**规划阶段就定死,不等写 HTML**。
> 🔴 **全屏无真人 → 内容展示为王,防"干巴巴"(辉哥 2026-08-11 强调)**:画面动态完全靠内容撑,三位一体防翻 PPT——① **布局分散铺满**(generate-scene-plan 按类型自动分配:分栏/网格/卡片铺满画面,禁居中挤中间、禁大片留白,垂直空间有限→横向铺展)② **元素高密度**(4-10 个/场景,铺满安全区,空背景也要加网格/标签/装饰线)③ **多动效**(≥3 种 + 持续活动效 + check-plan-motion 机器强制)。居中(sparse)只留给钩子/金句/收尾,且背景必须铺满。
> 🔴 **动效丰富度机器强制(辉哥 2026-08-10 定)**:expressionCore 补全后、确认前,必须跑 `node scripts/check-plan-motion.mjs --project "{projectRoot}"`——每场景 ≥2 种强动效(组件库/自创),**禁止 elastic-in/typewriter/stagger/fadeUp 普通组合拼凑**,通过(exit 0)才准写场景(血训:第一版全用基础动效被辉哥打回,MOTION_LIBRARY 规范是软要求时 AI 会偷懒,必须机器强制)。
10. **打开 `audio.srt`,这是场景切分的唯一依据**。字幕时间线很零碎,**根据字幕+时间线组合判断切分**(辉哥 2026-08-02 定):
**场景总数锚定基准(护栏4,血训 35→18)**:5-6 分钟口播 ≈ **15-20 个场景**(参考历史项目 373s/18 场景)。若切出 30+ 个 = 碎片化,重切。一段字幕 ≠ 一个场景。
**核心逻辑(不是一段字幕一个场景!)**:
- 字幕是零碎的,场景是**组合出来的**——几段字幕拼起来表达一个完整意思 → 合并成一个场景
- "这一段的真实表达是:这一段是为了表达一个场景",先判断这几段字幕合起来在表达什么
- 表达核心够大/关键 → 单独成场景(8-12s);连续小句讲同一件事 → 合并(4-6s)
- **参考场景规范约束切分**:元素数量(4-10)、动效数量(≥3)、**动效时长必须在对应音频时间段内**(元素出场+动效播放要赶在该场景的 SRT 时间窗里播完)
- 单场景时长锁 **4-12s**(防切太碎/切太长,保证动效有足够时间表现视觉)
10.5 🔴 **写锚点配置 + 生成时间轴**(generate-scene-plan 依赖 scene-timing.json,必须先有;辉哥 2026-08-05 补):
- 写 `timing-config.json`:`anchors` = **直接用 script-detailed.md 每场景的 [锚点: 词]**(同一来源两处复用),`scenes` = `["scene-01", "scene-02", ...]`
- ```powershell
node "{skillRoot}/scripts/build-timing-from-srt.mjs" --srt-path "{projectRoot}/audio.srt" --config "{projectRoot}/timing-config.json"
```
- 🔴 **禁止 `--segments` 均分**(会拦腰切断口播一句话,场景和内容对不上)
- 🔴 **跑完必检**:输出不能含 `"⚠ 未匹配锚点"`(有 = 锚点词与 SRT 转写不一致,回查修复再继续,否则静默吞场景错位);**场景数 = timing-config anchors 数**(不等 = 有锚点没匹配上)
10.6 🔴 **GATE 0:时间轴质量门禁(提前到规划前!辉哥 2026-08-05 定:时间轴先验证可靠再进规划,避免规划建立在坏时间轴上白做)** — 打开 `references/QUALITY_GATES.md`,**逐条执行 GATE 0 检查清单**:
- 每场景 start 锚定 SRT 语义断点(起始字幕),非等距切割
- 单场景时长 4-12s(护栏2;QUALITY_GATES 若写 ≥6s 以 SKILL.md 4-12s 为准——过渡/简短场景 4-6s 合法,这是护栏的明确区间,两者不冲突)
- 末场景 end ≥ audio.mp3 总长(ffprobe 实测)
- 边界不在语义断点 / 有 <4s 或 >12s / 等份切割 → 全部修完才准进规划
10.7 🔴 **时间轴校验**(规划前,辉哥 2026-08-05 提前:确认单给辉哥看之前时间轴必须已可靠):
- 场景边界锚定 SRT 字幕断点(不是等距切割)
- 实际时长 vs 预计时长偏差 > 50% 的场景(脚本用 `⚠` 标记),检查 SRT 是否有漏句或多句
- **校验通过 → 才进规划(步骤 11)**;不通过回 10.5 修锚点/切分
11. 生成 `{projectRoot}/scene-plan.json` + `scene-plan.md`(人可读确认单,辉哥 2026-08-05 补):
- 每个场景 = 一组 SRT 字幕片段(scene → SRT 起止时间戳)
- 跑 `generate-scene-plan.mjs` 生成自动骨架(type/duration/ctAnchors/elementBlueprint/keywords/动效建议),**keywords 已自动注入技能名全名(介绍+实操开头)**
- 🔴 **AI 深度读口播稿补全 `expressionCore`(必填,骨架为空)**:每场景一句话"这段在表达X(讲什么),传达Y感觉(情绪/重点)。动效——[关键元素的具体动效设计]"。**基于关键词 + 帧内详细口播稿语义**设计每个元素动效,不套模板(如 声波→跳动、进度条→随音频移动、对比→箭头反转)。expressionCore 空 = 规划不完整(generate-scene-plan 会提示),不填就写场景 = 违规
- 补全后**重跑 `generate-scene-plan.mjs` 刷新确认单**(确认单 scene-plan.md 是辉哥确认规划的人可读载体——JSON 没人会真读,确认看 md)
- 参考 `references/SCENE_PLANNING.md`(方法论)+ `references/MOTION_LIBRARY.md`(元素库×动效库)+ `references/SCENE_PREFLIGHT.md`(源头治理规范)
12. 🔴 **CHECKPOINT:辉哥确认规划**(看 **`scene-plan.md`**,不看 JSON)— 每场景关键词/元素/动效/表达核心已定死,确认后才写场景,不满意循环修改(回到步骤 11 改 expressionCore)
#### Step 1e-0:🔴 规范阅读证明(写场景第一道机器门禁,辉哥 2026-08-05 强制)
> 🔴 **血训根因**:不读规范就写场景 → 反复修补(scene-03 数字停滞 / scene-08 typewriter 打在有子元素的标题上排版乱 / 5 处 addClass 变色违规 / scene-06 容器 opacity:0 代码块跳动——全是没读透 SCENE_CHECKLIST / MOTION_LIBRARY / scene-creator 等核心规范造成的)。
>
> **机器强制**(不靠 AI 自觉):写场景前必须**逐份读懂 `references/SPEC_INDEX.md` 索引清单里的全部核心规范**,逐份填写 `{projectRoot}/spec-read.json` 必答问题答案,然后运行:
>
> ```bash
> node "{skillRoot}/scripts/check-spec-read.mjs" --project "{projectRoot}"
> ```
>
> **通过(exit 0)才准进 Step 1e 初检;不通过(exit 1)禁止写场景**——回到 SPEC_INDEX 对应文档重读、补答案,再跑直到通过。步骤:
>
> 1. 读 `references/SPEC_INDEX.md`(必读清单 + 必答问题 + 核心规则关键词)
> 2. 逐份读懂清单里的文档(SKILL.md 全屏写法 / scene-creator.md / SCENE_CHECKLIST / MOTION_LIBRARY / SYNC_ENGINE / 检查脚本规则等)
> 3. 填 `{projectRoot}/spec-read.json`(每份文档:readAt 时间戳 + 每题用自己的话复述规则,答案必须覆盖该文档核心关键词)
> 4. 跑 `check-spec-read.mjs` → 通过才继续
> 5. `check-scene-single.mjs` 每次运行也会前置 require 本检查(写场景过程中被拦截 = 回去重读,不是改场景糊弄)
>
> 6. 🔴 **门禁已全局化(2026-08-11 治本)**:统一门禁 `lib/stage-gate.mjs` 已挂到**全部**检查/生成/渲染脚本入口(check-scene-single / full-check / check-hard-rules / check-scene-prerequisites / check-layout-quality / check-viewport-overflow / check-component-diversity / check-plan-detail / check-plan-motion / 渲染脚本)。**无论 AI 跑哪个脚本,都会被这道门禁拦**——没有"省事通道"。门禁每次运行**重跑** spec-read / 规划 / 关卡 / 产物验证(不读伪造 JSON),手写 `.gates.json` / `spec-read.json` 无法过关。**单入口**:正常流程只跑 `node scripts/pipeline.mjs --project <root> --stage <plan|write|fullcheck|preview|render>`。
#### Step 1e:项目初检(写场景前的前置检查门,全过才准写,辉哥 2026-08-05 定/加固)
🔴 **场景规划确认后、写场景前,先做一次初检,全部通过才准进 Step 3**。这不仅是"文件存在检查",更是**执行顺序 + 检查逻辑 + 视觉密度的总闸门**(血训 2026-08-05:初检只查文件会漏掉"顺序错了、检查没跑、密度不够",写场景后才返工。**产物顺序可证明、检查逻辑必须有据**)。
| # | 初检项 | 检查内容 |
|---|--------|---------|
| ① | 素材齐全 | audio.mp3 + audio.srt 存在(SRT 是时间线唯一真相),且 audio.srt 时间戳**非等距**(等距=伪造 SRT) |
| ② | 时间轴 | scene-timing.json 存在;**场景数 = timing-config anchors 数**(不等 = 锚点没匹配上);单场景 4-12s;无"未匹配锚点"输出记录 |
| ③ | 规划完整 | scene-plan.json + scene-plan.md 存在;每场景 expressionCore 已填(空 = 规划不完整) |
| ④ | **执行顺序** | `.gates.json` 确认顺序:script-ok → audio-ok → detailed-ok 全绿,且 **confirm 时间戳递增**(乱序 = 跳步,回查) |
| ⑤ | **检查逻辑跑过** | GATE 0(切分质量)、时间轴校验(步骤 14)**必须已执行且通过**——不跳过、不口头说"检查了",用脚本/清单留痕 |
| ⑥ | **视觉密度** | 每场景:元素数 ≥ recommendedElements、动效 ≥3 种、持续活动效 ≥1、相邻 beat 无 >2.5s 死区;**动效丰富度 check-plan-motion.mjs 必须通过(机器强制,2026-08-10)**——不达标回 Step 1d 补 expressionCore |
| ⑦ | 音频时长 | 末场景 end ≥ audio.mp3 总长(ffprobe 校验) |
🔴 **CHECKPOINT:写场景前确认(辉哥 2026-08-05 加)** — 初检 7 项全过后,**向辉哥展示初检结果,辉哥说"开始写"才准进 Step 3**。不经辉哥确认直接写 = 违规(血训:初检是机器检查,规划确认单是辉哥看场景前最后一次把关,跳过 = 白写返工)。展示内容:① 初检 7 项通过情况 ② 将按 scene-plan.md 写的 9 个场景清单 ③ 提示辉哥"OK 就开始写场景"。
#### Step 1f:🔴 RAF 锚点铁律(核心中的核心,辉哥 2026-08-07 强制,凌驾其他所有写场景规则)
> **这不是"写场景时注意一下"的软规则——这是整个系统的运行原理。违反 = 场景和口播脱节 = 辉哥肉眼一看就废。**
**RAF 咬合齿轮**(main.js 运行原理):
```
audio.currentTime(字幕时间线 = 绝对真理)
↓ 每帧 RAF 读一次
runScene 的 beat 触发(ct >= beat.t → 执行 action)
↓
画面元素揭示/动画
```
**音频 currentTime 是唯一时间来源。beat 的 `t` 值就是"口播讲到哪个词的时刻"。**
**写场景时 beats 的 t 必须满足(机器强制):**
1. **t = 对应口播词在字幕时间线上的时刻**(看 `scene-plan.json` 每场景的 **`wordAnchors`** 字段:`[{t, text}]`)——不是随手写 4.6/7.6
2. **内容元素(标签/列表行/卡片内容/步骤/消息/typewriter/countUp)逐个跟口播**:每个并列内容项一个 beat,t 锚定它对应的那个口播词(找素材@7.64 / 做动画@8.26 / ...)——禁止容器 addClass 一次性出
3. **装饰/框架(眉标/主标题/容器壳/下划线/箭头/进度条/addClass 强调)可一次性**,但时间也尽量贴最近口播词
4. **任何内容 beat 的 t 不得超前它对应口播词 >2.5s**(R2d 硬拦);相邻视觉活动间隔 ≤4s(R12)
**为什么必须这样(血训 2026-08-07)**:scene-01 标签手写 4.6s 出场,口播 7.64s 才念"找素材"——超前 3s,辉哥一看"动画和口播完全脱节"。scene-04 右卡片列表(动态文字模式)整组弹出,但口播全程在讲人像模式——内容脱节。**这些全是我手写 beats 时间、没锚定字幕时间线造成的。**
**执行方式**:写场景前**必读 `scene-plan.json` 该场景的 `wordAnchors`**(词级时刻表),beats 的 t 直接照抄对应词的 t。**不准凭感觉定时间**。
**写场景流程(2026-08-11 定,血训:写一批查一批导致反复返工 + 生成1帧就检查)**:
- 🔴 **写场景阶段 = 全部场景写完,中途不跑检查**。只允许写完第一个场景跑一次 `node scripts/pipeline.mjs --project <root> --stage write` 验证格式/规则理解对(此后不再中途检查)。
- 写每个场景前对照 `references/scene-creator.md` 的「写场景硬约束清单」逐条自检(字号/布局/动效种类/间隔/多阶段/持续动效/变色/opacity/字体/keyframes/伪元素/双入场/beats递增/类名/wordAnchors/组件内部类名/照翻译实现细节蓝图)。
- 🔴 **写 HTML = 照翻译 scene-plan 的"正确性底线"(4 类防错)**:类名/组件结构/布局照抄 `implementationDetail`、文案照抄 `elementContent`(句子级,禁编造口播外文案)、时间锚点照 `wordAnchors`。**照翻译只管这 4 类防错底线,不管视觉表达**——动效组合、装饰细节、排版层次、视觉风格由 AI 自由发挥(在硬规则内即可)。规划 `expressionCore` 只定"这段讲什么 + 传达感觉 + 强动效方向",**不逐元素锁死动效**(血训 2026-08-11:过度定死 → 画面千篇一律,辉哥明确要求灵活放开)。写场景前 `check-plan-detail.mjs` 必须已通过(含 D6 elementContent 完整)。**照翻译由机器强制**:`check-scene-translation.mjs`(已挂进 check-scene-single/full-check/pipeline)验证类名/文案/布局/组件结构照抄蓝图,不照抄直接拦。
- 🔴 **写场景前决策确认(机器强制)**:每场景写前把确认行写进 `{root}/scene-confirm.json`——`组件[照components] | 元素数[照recommendedElements] | 动效[照recommendedEffects] | 文案[照elementContent] | 布局[照layout] | 类名[私有.sN-xxx无组件内部类名] | 规则自检[scene-creator约束表]`。`check-scene-decision.mjs` 逐字段比对 scene-plan,缺记录/填错直接拦(强制回答去读规则,生成时减少大部分问题)。
- 全部写完 → **单入口流水线**(不要单独挑脚本跑,门禁已挂所有检查入口,挑哪个都会先被拦):
- `node scripts/pipeline.mjs --project <root> --stage write`(自动跑全部场景 check-scene-single 全检查)→ 通过后 `--stage fullcheck`(full-check 第二层)。
- 不"写一个查一个"、不"生成 1 帧就检查"(血训 2026-08-10:中途检查浪费往返时间,且形成依赖检查兜底的坏习惯)。
**🔴 自动修循环(流水线引擎机器强制,辉哥 2026-08-11 定)**:
```
全部场景写完 → node scripts/pipeline.mjs --stage write(自动跑 check-scene-single 全场景)
→ 有报错:AI 逐个真实修复(不改功能不造假,辉哥铁律)
· 修复 = 读报错 → 定位根因 → 改对应场景/规划 → 重跑 `pipeline --stage write --fix`
→ 轮数由 .pipeline-state.json 机器计数,≤3 轮(辉哥铁律三:3 次不成功强制停)
→ 3 轮不过 → pipeline 输出"风险汇报"段并 exit 2,AI 把该段原样呈给辉哥,不继续盲改
→ 全过 → pipeline --stage fullcheck(full-check 第二层,同上循环)
→ 修复全程记录进 .pipeline-state.json 的 fixLog → 交付报告
```
**🔴 风险汇报清单("全自动+风险才汇报"落地,辉哥 2026-08-11 定)**:
以下情况**必须出现在辉哥面前**,其余静默通过(交付成品 + 报告)。**pipeline 遇到 ④ 类会输出"风险汇报"段并 exit 2,AI 必须原样转发给辉哥,不得自行处置**:
```
① 自动修 3 轮超限:场景 + 问题 + 已尝试的 3 轮修复
② 检查报错无法理解/无法修:标记跳过,说明原因
③ 渲染校验失败:时长偏差 / 黑帧 / 帧数不符
④ 机器判不了但明显矛盾:表达核心与场景类型/口播稿明显不符
```
交付报告固定含:改了什么(修复记录,来自 .pipeline-state.json fixLog)+ 检查结果(全绿/带 ⚠️)+ 风险清单(有则列)。
**🔴 检查脚本改完必自测(辉哥 2026-08-11 定,防死规则)**:修改/新增 `check-*.mjs` 规则后,跑 `node scripts/self-test-rules.mjs`——验证关键规则活着(能拦已知违规),防假绿(血训 R19/R20/S37 曾全绿漏检)。**新增规则时必须在 self-test-rules.mjs 的 cases 里加一条已知违规 case**(规则全绿没拦过一个该报的 = 没检查)。已知 S37 只查 id 选择器 typewriter(class 选择器场景由引擎 preMeasure 兜底)。**修改流水线/门禁(stage-gate / 各脚本门禁挂点 / check-gates confirm)后跑 `node scripts/self-test-pipeline.mjs`**——验证伪造/绕过必被拦、真合格放行(防门禁有洞)。
---
---
### 人像模式(portrait-center)— 用户口播 + AI 画面
用户已录好真人出镜口播视频,AI 只做画面叠加。**人像模式是独立逻辑,与全屏模式本质不同**(辉哥 2026-08-04 定)。
🔴 **人像不需要写口播稿、不需要 AI 配音**(辉哥 2026-08-05 强调):用户视频转写的 **SRT 就是文稿**(带时间线),直接基于它提炼关键点做场景。写口播稿 + AI 配音只是全屏模式(无人像、纯 AI 生成)的流程。**人像流程 = 用户给视频 → 提取音频 → 转写 SRT → 基于 SRT 做场景**。
**为什么整个文件体系完全独立(辉哥 2026-08-05 补充完整理由):**
1. **根本原因——表现方式本质不同**:人像模式是**真人口播为主**,真人说话本身撑起画面动态,左右两侧只是辅助;全屏模式**没有真人**,必须靠元素堆叠/动画/场景切换撑视觉动态,否则像翻 PPT。两者的场景时长标准、动效密度、画面逻辑都不一样,不能混用规则
2. **防污染**:两者都靠代码生成,文件若混在一起容易互相污染(组件库/变量/检查脚本互相引用),出错后很难定位是哪个模式的代码导致的
3. **易纠错**:文件隔离后,每个模式单独跑自己的检查,报错能直接定位到该模式
4. **内容差异大**:很多内容只适用于其中一个模式(有些适用有些不适用),混在一起会互相干扰
5. **执行效率高**:独立后单模式的生成/检查/维护各自高效,互不拖累
6. **组件库适用性差异**(辉哥 2026-08-05):全屏组件库(mac-window / eyebrow / badge / big-title / 复杂网格等)**部分完全不适用于人像**——人像是轻量提示卡 + 左右栏辅助,不需要全屏窗口/高密度视觉组件。人像组件库 `components-portrait.css` 独立设计(p-* 前缀),与全屏 components.css 零交集(check-portrait-rules P4 已禁止引用全屏组件 class)
7. **架构演进方向**(辉哥 2026-08-05):独立结构为后续把两种模式**拆成 2 个独立技能**(全屏模式技能 + 人像模式技能)铺路,现在保持文件独立即为此做准备
8. 所以**模板/组件库(components-portrait.css)/配色(portrait-tokens.css)/规划(portrait-plan.json)/检查(check-portrait-rules + check-portrait-plan + portrait-full-check)/字体全部独立**,与全屏零交集
- **不做全量字幕动画**——只提炼关键点做**轻量提示卡**(金句/数据/转折/结论),视频本身是主体。场景少、内容轻,抽帧分析成本可控
- **不搞固定主题**——背景是用户视频、画面不固定,无法套主题。文字颜色**根据视频画面底色自适应**(先量化展示内容 → 按展示时间点抽帧 → 分区分析亮度/主色调 → 生成场景配色)
- **透明背景**——文字直接浮在视频上,无大面积色板,靠颜色对比 + 微阴影保证可读,文字靠上(不垂直居中)
- **可展示形态**:文字提示、标签、管道流程图、高亮引用块、数据表、动态效果
#### Step 2a:素材条件检查 + 提取音频 + 转写(人像素材入口,辉哥 2026-08-05 详细定)
1. 🔴 **检查用户给的素材,分三种情况**:
- **视频**(`portrait.mp4` 居右 / `background.mp4` 居中)→ ✅ 满足:视频含音频,ffmpeg 提取音频 → 转字幕
- **音频**(`audio.mp3`)→ 直接转字幕带时间线 → `audio.srt`
- **已有 SRT + 音频** → 直接用,跳过转写
2. 视频时用 ffmpeg 提取音频:`ffmpeg -i video.mp4 -vn audio.mp3`
3. 🔴 **转写字幕(引导用户选方式,辉哥 2026-08-05 定)**:AI 不擅自替用户定工具,**提示用户选择转写方式**,目标是拿到**带时间线的字幕文件 `audio.srt`**:
- **百度语音识别**(video-image-reader-writer 技能)
- **阿里/其他云 ASR**
- **本地 whisper**
- **系统自带语音转文字**
- **用户已有 SRT**(直接用,跳过转写)
- 转写完成后**回读 audio.srt 核对时间戳真实**(不是等距),再继续
4. SRT 是时间轴唯一真相,禁止估算等距切割
#### Step 2b:从 SRT 提炼关键点 + 写场景描述稿(内容精简,非全量切场景)
1. 读 SRT 内容,**提炼要展示的关键点**(金句/数据/转折/结论),不是每句字幕都做场景
2. 每个关键点 = 一个轻量提示卡场景(眉标 + 一句主标题 + 辅助元素),展示形态按内容选(文字/标签/流程图/数据表/引用块/动态)
3. 场景数量少(几分钟视频约 4-8 个),每场景 4-12s
4. 🔴 **写 `script-detailed.md`(场景描述稿)** — 这是**从 SRT 提炼的类型表**,不是重写口播稿(人像不写口播稿,辉哥 2026-08-05 强调)。每场景一行:
```
## 场景01 [钩子] [锚点: 从零散的记录里捋出来]
场景02 [痛点] [锚点: 憋到十二点]
...
```
- **类型**从 7 种模板库选:钩子 / 痛点 / 方案引出 / 演示 / 数据 / 金句 / 升华收尾(决定生成规划时选哪套左右栏模板,见 P_TEMPLATES)
- **锚点** = 该场景 SRT 独有的标志词(4-12 字、优先实词),**必须从 SRT 原文精确取词(一字不差,不能意译/改字)**——build-timing 用子串匹配 SRT 文本,改一个字就找不到 → 静默吞场景错位(血训 2026-08-05 实测:锚点错字 → 场景数 7→6、单场景 27.7s 吞掉相邻场景)。用于 SRT 自动匹配场景边界
- **不写这份类型表 → 生成规划时所有场景 type=演示,左右栏模板全单调,风格轮换失效**(血训 2026-08-05)
5. 场景规划确认后生成 `scene-timing.json`
#### Step 2c:初始化项目 + 素材 + 时间轴 + 配色(项目文件夹一步到位,辉哥 2026-08-05 定)
🔴 **初始化 = 复制人像模板全套文件,项目文件夹即完整**(`init-project.mjs` 把 `templates/portrait-center/` 整个复制到项目根):
```
projects/{projectName}/
├── index.html ← 播放器入口(引用 portrait-tokens / scene-colors / chrome / components-portrait / main.js)
├── assets/
│ ├── main.js ← 播放引擎(双挂载 #mount-left/#mount-right + runScene + AX)
│ ├── components-portrait.css ← 人像独立组件库(p-* 16 基础 + hero 主角组件 r-code/p-chart/p-flow/p-step-block)
│ ├── portrait-tokens.css ← 双档色板(--pc-* 暗档 + --pc-*-light 亮档)
│ ├── chrome.css ← 播放器壳
│ └── *.ttf ← 字体(Noto Sans SC 400-800 / JetBrains Mono 400/500/700)
├── scenes/
│ └── _layout.html ← 写场景骨架(对照它改,不自创轮子)
├── scene-timing.json ← 场景时间轴(模板占位,build-timing-from-srt 覆盖)
└── project.config.json ← 项目元数据(layout=portrait-center)
```
**然后导入素材 + 生成配套文件(让人像项目文件夹完整):**
```powershell
# ① 初始化(复制模板全套文件到项目根)
node "{skillRoot}/scripts/init-project.mjs" --layout portrait-center
# ② 导入用户素材(提示用户放入 / 指定路径)
# 背景视频 background.mp4(居中)/ portrait.mp4(居右)
# 或音频 audio.mp3(需转字幕);或已有 audio.srt
# ③ 视频时提取音频
ffmpeg -i 视频.mp4 -vn audio.mp3
# 音频时直接转写:Whisper/百度 → audio.srt(字幕由音频生成,非用户输入)
# ④ 写锚点配置文件 + 按锚点生成时间轴(辉哥 2026-08-05 定:禁止均分)
# 先写 timing-config.json:anchors = 直接用 script-detailed.md 每场景的 [锚点: 词](同一来源,两处复用):
# { "anchors": ["从零散的记录里捋出来", "以前我写周报", ...], "scenes": ["scene-01", "scene-02", ...] }
node "{skillRoot}/scripts/build-timing-from-srt.mjs" --srt-path "{projectRoot}/audio.srt" --config "{projectRoot}/timing-config.json"
# 🔴 禁止 --segments 均分:会拦腰切断口播一句话,场景和内容对不上
# 🔴 跑完必检:输出不能含 "⚠ 未匹配锚点"(有 = 锚点词与 SRT 原文不一致,回查修复再继续,否则静默吞场景错位,血训 2026-08-05)
# ⑤ 配色分析(人像特有,必跑)
node "{skillRoot}/scripts/analyze-portrait-colors.mjs" --project "{projectRoot}"
# 生成 scene-colors.json + assets/scene-colors.css
```
🔴 **配色分析规则**:对每个场景在 start+0.3s 抽背景视频帧,按文字所在区域(左 30%/右 30%)分区分析亮度与主色调,生成 `scene-colors.json` + `assets/scene-colors.css`。配色规则:
- 亮度 L > 143 → 深色文字;L ≤ 143 → 浅色文字(实测校准值,可用 `--light-threshold` 调)
- 主文字用画面主色调,副文字用**互补色**(每场景至少 2 种 hue 组合)
- 强调色用互补/亮色;描边用同色系半透明(不用纯黑纯白、不用大光晕)
- 用 `--video <path>` 可对同一时间轴测不同视频
**静态图背景(无真人视频时)**:复制图片为 `background.png`(main.js 已支持 background.mp4 缺失时回退静态图),或转成静态视频 `background.mp4`——**时长必须 ≥ 音频总长**:
`ffmpeg -loop 1 -i bg.png -t <音频秒数> -c:v libx264 -pix_fmt yuv420p background.mp4`
#### Step 2c0:项目初检(素材导入完成后必做,通过后才准写规划,辉哥 2026-08-05 定)
🔴 **项目导入全部完成后,先做一次初检,全部通过才准进入场景规划**:
| # | 初检项 | 检查内容 |
|---|--------|---------|
| ① | 项目文件齐全 | index.html / assets/main.js / components-portrait.css / portrait-tokens.css / chrome.css / 字体 / scenes/_layout.html 都在 |
| ② | 素材就绪 | 视频(background.mp4)或 音频(audio.mp3)+ audio.srt 存在 |
| ③ | 时间轴生成 | scene-timing.json 存在,**场景数 = timing-config anchors 数**(不等 = 有锚点没匹配上,回查锚点词与 SRT 原文);**单场景 ≤20s**(超 = 锚点错位信号) |
| ④ | 配色分析 | scene-colors.json + assets/scene-colors.css 都在(index.html 引用它),每场景 light/dark 档位已定 |
| ⑤ | 音频时长 | 末场景 end ≥ audio.mp3 总长(ffprobe 校验) |
**初检通过 → 进入 Step 2c1**(先强制读规范文件,再生成场景规划文件)。
#### Step 2c1:场景规划(人像独立体系,辉哥 2026-08-05 定)
🔴 **写场景 HTML 之前必须先"读透规则 + 生成完整规划",全部定死,辉哥确认后才写场景**。这样写场景只是执行规划,后期零修改(2026-08-05 血训:规划只定元素类型、文案/数量/动效靠写场景时临场发挥 → 后期反复打回)。
**0. 🔴 强制先读规则(两层:规范文件 + 项目内容文件,AI 必须,防偷懒;**初检通过后、生成规划文件前必读**,不读就生成规划 = 违规)** — 辉哥 2026-08-05 强制:**这是决定后面成败、生成质量和效率的核心门禁**。项目初检(Step 2c0)通过、准备写场景规划文件时,必须读透以下两层文件、内化后再生成规划(血训:不读检查脚本就写场景/规划,打回十几轮浪费一半时间;读透一次生成就合规,效率和质量都靠这一步):
**第一层:规范文件(教 AI 怎么写,缺一不可)**
- `references/PORTRAIT_SCENE_GUIDE.md`(设计三原则 + 血训 #1-14 + 规划规范 + 组件清单 + 预览验收清单)
- `templates/portrait-center/assets/components-portrait.css`(组件初始态/AX 对应/双档选择器)
- `scripts/check-portrait-rules.mjs`(硬规则 M/I/P/S/T/A 组)
- `scripts/check-portrait-plan.mjs`(规划合规 R1-R9)
**第二层:项目内容文件(告诉 AI 要做什么,生成规划前必读)**
- `audio.srt`(SRT 转写文稿 = 内容来源,AI 深度读它理解口播说什么)
- `scene-timing.json`(时间轴:每场景边界/时长,build-timing-from-srt 产出)
- `scene-colors.json`(背景档位:每场景 left/right 的 light/dark,analyze-portrait-colors 产出)
**1. 生成规划骨架**:`node scripts/generate-portrait-plan.mjs --project "{projectRoot}"` → **`portrait-plan.json` + `portrait-plan.md`(人可读确认单)**
每场景含:类型 / 锚点 / 时长 / lightDark 档位 / **leftColumn(count+元素+文案)** / **rightColumn(count+hero 主角组件+元素+文案)** / **motionPlan(动效数量+actions)** / **rules(该场景要满足的检查+血训清单)** / ctAnchors 时间轴 / srtText
确认单 `portrait-plan.md` 是辉哥确认规划的人可读载体(每场景左右栏文案/动效/hero/时间锚点)——JSON 没人会真读,**确认看 md**(辉哥 2026-08-05 定)
**2. AI 深度读该场景 SRT 转写文稿(人像不另写口播稿——用户视频转写的 SRT 就是文稿,辉哥 2026-08-05 强调;300-500 字等字数是测试数字不是规则),补全每场景 expressionCore** — **必须定死**(不是一句话表达核心):
- 左栏每元素**具体文案**(从该场景 SRT 取词)
- 右栏每元素**具体文案**(取词分配、左右不重复)+ **主角组件 hero**
- 每元素**动效**(typewriter/countUp/progress 等,≥2 种)
- 入场时间**锚定字幕断点**(ct = 字幕绝对时间 - offset)
- **对照 rules 自检**:数量 / 动效数 / 主角组件 / 左右不重复 / 时间锚定 / 禁变色 / nowrap
- 补全后**重跑刷新确认单**(保留已填 expressionCore,只刷新确认单):`node scripts/generate-portrait-plan.mjs --project "{projectRoot}"`
**3. 🔴 CHECKPOINT:辉哥确认规划**(看 **`portrait-plan.md`**,不看 JSON)— 确认后才写场景 HTML,不满意循环修改(回到步骤 2 改 expressionCore)
**4. 按规划写场景 HTML**(执行规划,不临场发挥)→ 顶部写 SRT 注释逐句核对 beat.t
**5. 全链路检查循环**:`node scripts/portrait-full-check.mjs --project "{projectRoot}"`
(硬规则 check-portrait-rules + 规划合规 check-portrait-plan R1-R9 + 音频时长)——**报错就修复,直到 0 错 0 警告**
#### Step 2d:写场景 HTML → 预览 → 检查 → 渲染
- **场景根 class**:`.s{序号}l`(左挂载)/`.s{序号}r`(右挂载),配色用 `var(--pc-main)` / `var(--pc-sub)` / `var(--pc-accent)` / `var(--pc-stroke)`(由 scene-colors.css 注入)
- **透明背景**:文字直接浮在视频上,无大面积色板;文字位置靠上(`justify-content:flex-start`)
- **文字锐利**:只用微小 text-shadow(chrome.css 全局 `--pc-stroke` 控制),禁止 14px+ 大光晕
- 人像居中场景 HTML 用 `<!--R-->` 分隔左右内容(左放 mount-left、右放 mount-right)
**人像居中(portrait-center)**
- 三挂载:`#mount-left` + `#mount-right` + `#mount-bottom`(底部大组件区可选),场景 HTML 用 `<!--R-->` 分左右、`<!--B-->` 分底部
- 人像视频全屏背景(main.js 自动加载 `background.mp4`)
- 文字直接浮在视频上(透明),活性检查只查 `mount-left`(右侧可能为空)
- 预览 `localhost:3010`,肉眼确认文字在任何背景上可读
**人像居中场景写法标准(v5 runScene,辉哥 2026-08-04 定)**
🔴 **时序铁律**:场景用 `runScene({root, offset, duration, beats})`(main.js 内置),**每个 beat 的 `t` 直接锚定 SRT 字幕断点**(`ct = 字幕绝对时间 - offset`),**禁止估算**。场景文件顶部写 SRT 注释(逐句 `#序号 绝对时间 "字幕原文"`),写完对照注释核对每个 `t`。
🔴 **字幕取词去重**:左右栏内容**必须从 SRT 取词分配**,左右不重复——左栏放主信息(眉标/标题/正文),右栏放视觉强化(数字/清单/标签,是左栏的数据化)。**字幕用完了宁缺毋滥,不编字幕里没有的词**(血训:右栏 tag 编"每天重复"等字幕外的词 = 违规)。
🔴 **动效**:beat `action` 用 `fadeUp/pop/popUp/scaleX/scaleY/countUp/typewriter/progress`。**装饰元素(竖线 scaleY、装饰线 scaleX)也要走 beat reveal,不能最早暴露**(血训:装饰线 opacity:1 最先出现 = 时间点错乱)。**typewriter 支持双色标记:txt 内 `**关键词**` → 生成 `span.p-hl`(accent2 色),标题打字机出场也能双色**(辉哥 2026-08-05 检验:标题必须 2 种文字颜色)。
🔴 **字号铁律**:所有文字 ≥17px。本套:编号 64 / 标题 31 / 眉标 22 / 正文 20 / 标签 20(1920 基准)。
🔴 **颜色规则(档位机制,辉哥 2026-08-04 定)**:
1. **色度分析**:`scripts/analyze-portrait-colors.mjs` 逐场景抽帧分析左右区域亮度 → 输出 `scene-colors.json` 每场景 `{mode: light|dark}`(L≥120 → light)
2. **档位 class**:场景根元素加 `.light`(亮背景)或 `.dark`(暗背景)——生成器读 scene-colors.json 自动加
3. **双套选择器**:场景 CSS 写 `.sNl.light .t{color:var(--pc-accent-light)}` / `.sNl.dark .t{color:var(--pc-accent)}` 两套
4. **精心色板(非算法混合)**:`portrait-tokens.css` 每主题两档——`--pc-*-light`(亮背景深色档,同主题色相的干净深色版,如 KINE 爱马仕橙→#EA580C 饱和橙)、`--pc-*`(暗背景原色档)
5. **玻璃面板恒白字**:正文/清单/logo 等玻璃面板内文字**永远白色**(玻璃深底保证可读),玻璃透明度尽量低(0.3-0.55),白字可见即达标——亮背景玻璃稍深、暗背景玻璃更透
6. **裸字按档**:标题/眉标/编号/数字/tag(下划线式)亮背景用深色档、暗背景用原色档
7. **禁止全部压成同一色**:每元素保留搭配规则(标题=accent、关键词=accent2、眉标=eyebrow、正文=ink),双色标题两色要拉开对比(色相差距大)
8. 换背景视频跑一次 analyze 自动选档;辉哥看哪个色不满意 → 直接改色板值迭代
🔴 **组件库(2026-08-05 迭代)**:人像模式**全部用独立组件库 `assets/components-portrait.css`(p-* 组件:p-stage/p-head/p-title/p-tline/p-sub/p-num/p-bar/p-tag/p-list/p-steps/p-quote/p-pill/p-track/p-step-idx/p-state/p-card + hero 主角组件 r-code/p-chart/p-flow/p-step-block)**,不引用全屏 components.css。组件清单 + AX 对应见 `references/PORTRAIT_SCENE_GUIDE.md` §组件库。
🔴 **扁平混搭布局(2026-08-05 血训)**:右栏直接平铺 p-* 组件**兄弟元素混搭**(8~14 元素、5~8 种类型),**禁止外层大卡片容器**;全部左对齐(靠组件基类默认,不写 align-self/width/flex-direction 覆盖);自定义修饰类**只改配色**不改布局;beats selector **必须用唯一修饰类定位,禁止 `:nth-of-type`**(会按标签类型计数落空 → 元素永不入场)。
🔴 **防塌陷(2026-08-05 血训)**:`.p-stage` 固定高 flex 列 + overflow:hidden,内容溢出时带 overflow:hidden 的子元素(`.p-bar`/`.r-code`)被压到 0/塌陷 → 给不可压缩元素加 `flex-shrink:0`。长空档(相邻 beat >4s)要插填充(**辉哥 2026-08-05 检验:禁用变色强调 + 严禁字重**,改用下划线/装饰 / 元素错峰 / typewriter)。**叙事顺序必须跟 SRT 语义顺序**(先讲原因再讲结论,禁止先亮结论)。完整血训见 `references/PORTRAIT_SCENE_GUIDE.md` §血训与雷区。
🔴 **检验沉淀规则(辉哥 2026-08-05 逐帧检验沉淀,必须遵守)**:
- **🚫 严禁文字变色强调**(辉哥 2026-08-05 定:**"变色"指文字本身的颜色变化**,任何 addClass 让文字 color 变(橙→蓝、文字变灰、文字变白等)= 违规,很 LOW)。开场场景关键文字用 typewriter 出场。
- **🚫 严禁字重强调**(辉哥 2026-08-05 补:font-weight 突变也很 LOW,视觉呆板)。
- **✅ 强调手段可选,且不是必须**(辉哥 2026-08-05 补:下划线不是强制项)。**下划线的用途 = 强调重点词 + 装饰**:① 给关键词(如"抢饭碗")加下划线让眼睛聚焦 ② 标题下方独立横线做视觉分隔。**看内容需要决定加不加,不是每个词都要加**。**禁止用 box-shadow 冒充下划线**(细且无展开动效,辉哥 2026-08-05 检验)。边框高亮(border-color)、背景色块(background,**文字颜色保持不动**)也是可选强调手段。
- **📏 下划线技术实现统一用伪元素 + scaleX**(辉哥 2026-08-05 定:技术实现层面优先稳定):**`::after`/`::before` 伪元素 `{content:'';position:absolute;left:0;right:0;bottom:-Npx;height:3-5px;background:var(--hf-primary);transform:scaleX(0);transform-origin:left;transition:transform .5s ease-out}`**,用 `.on` 类触发 `transform:scaleX(1)`(有展开动效)。**伪元素 `position:absolute` 时对应元素必须加 `position:relative`**(R23 兜底,否则线贴到页面角落)。**禁止用 box-shadow 冒充下划线**(无展开动效,辉哥 2026-08-05 检验:细且难看)。若目标元素被 JS 动效驱动(fadeUp/pop/popUp 有入场 transform),下划线伪元素加在它的**子元素/独立 span** 上,不与入场 transform 冲突。装饰线(标题下方独立横线)用独立元素 + scaleX。
- **🚫 下划线 vs 装饰线必须样式区分**(辉哥 2026-08-05 检验:同一个画面里,下划线(跟词的)和分隔装饰线(独立横线)**不能长得一样**——否则叠在一起或错位平行,很难看)。同场景出现两种线时,必须明显区分:粗细不同(下划线 4-6px / 装饰线 2-3px 或更细)、颜色不同(下划线主色 / 装饰线 muted 淡色)、位置不同(下划线贴词底 / 装饰线独立留白)。**判断标准:一眼能看出"这条是强调词、那条是分隔"**。
- **🎨 标签/胶囊必须有底色分级**(辉哥 2026-08-05 补:**禁止透明底 + 只靠边框区分,视觉不明显**):
- **负面/过渡/次要标签 → 浅色底**:`background:color-mix(in srgb,var(--hf-bg) 92%,var(--hf-text-muted))` + muted 文字 + 淡边框(低存在感)
- **正向/核心/目标标签 → 主色底(可稍深)**:`background:color-mix(in srgb,var(--hf-bg) 88%,var(--hf-primary))` 或 `color-mix(in srgb,var(--hf-primary) 14%,transparent)` + primary 文字 + 主色边框(高存在感)
- **最深强调(关键目标/CTA)→ 主色实底**:`background:var(--hf-primary)` + 亮字(最高存在感)
- 原则:**用底色深浅制造层级,浅色不突兀、深色做强调**,同一屏标签底色有区分但不花哨
- **🎨 配色白名单(B,辉哥 2026-08-05 定)**:场景颜色**全部走主题变量 `var(--hf-*)`**(全屏模式有主题风格 tokens.css 兜底)。`color-mix()` 混色时**第一个参数必须是 `var(--hf-*)`**,禁止写死 `#hex`/`rgb()`/`rgba()`(写死=换主题翻车,S2c 检查)。允许中性色:`#fff`/`#000`/`transparent`/`rgba(0,0,0,.x)`/`rgba(255,255,255,.x)`。
- **📏 伪元素定位(C,辉哥 2026-08-05 定)**:下划线/装饰用伪元素 `::after`/`::before` 且 `position:absolute` 时,**对应元素必须加 `position:relative`**,否则线贴到页面角落(R23 检查)。
- **🔍 双色高亮词数量(D,辉哥 2026-08-05 定)**:**同一行文字最多 2 处双色高亮,通常 1 处**(重点词 1-2 个)。全行一半字变色 = 眼睛花、没重点。标题/金句保持 1 处双色最佳。
- **短词元素必须 nowrap**("下一步"拆成 2 行 = 违规),场景自建文字类必须自带 nowrap + 宽度足量
- **STEP 徽章必须与对应正文配对排列**(STEP 下方紧跟正文,禁止 STEP 全堆上、正文全在下的分两堆布局)
- **右栏视觉风格轮换**(数字 countUp / 状态 / 标签 / 进度条 / 数据图 / 流动管道 / 轨道 / 步骤 / 清单 / 卡片 / 药丸 / 代码块 轮换使用,相邻场景不重复堆叠同一风格)
🔴 **参考骨架**:`templates/portrait-center/scenes/_layout.html`(KINE 布局 + runScene 完整示例 + 双套选择器),写场景对照它改,不自创轮子。
两者最后都跑人像全链路检查 **`portrait-full-check.mjs`**(人像硬规则 check-portrait-rules + 规划合规 check-portrait-plan R1-R9 + 音频时长)→ 预览 → 渲染。注意:人像模式场景为轻量提示卡,**检查规则用独立的人像 check-portrait-rules(S18 人像 ≥2 动效、禁变色、禁全屏组件等),不套全屏硬规则**(辉哥 2026-08-05 定:两模式检查内容不同)。
---
### Step 3. 写场景 HTML
🔴 **强制第一步:深度理解口播稿(场景规划的源头,辉哥 2026-08-02 强制)**
场景文件的核心源头是口播稿。**只有深度理解口播稿内容和表达核心,才能写好场景文件。不深度理解 = 不写场景。**
1. **通读 `script.md`(口播稿)全文**——理解整体结构、分几块、每块讲什么、情绪怎么走
2. **场景↔口播对齐**——按 scene-timing 时间轴,定位每个场景对应的口播段落
3. **提炼每场景表达核心**(一句话)——"这段在表达 X(讲什么),要传达 Y 感觉(情绪/重点)"
4. **不明白就提问(不盲猜)**——对某段表达核心拿不准/有歧义,列出疑问问辉哥,确认后再进视觉规划。**提问用选择项方式(AskUserQuestion)**:给 2-4 个候选选项(含推荐的视觉方向)+ OTHER 供辉哥自定义,减少打字(辉哥交互偏好)
5. **表达核心 → 视觉翻译**——核心决定场景形态 + 动效/组件(内容驱动,不套模板;可自创动画)
6. **写入 scene-plan.json 的 `expressionCore` 字段**,写场景时按它执行
7. **源头配套检查(规划时就位,防"牵一发动全身")**:
| # | 配套 | 检查 | 缺了怎么办 |
|---|------|------|-----------|
| ① | 音频时间线 | SRT 锚点逐句对应场景 | 回场景描述稿/重新对齐 |
| ② | 场景描写 | 场景描述稿的视觉信息进规划 | 补充场景描述稿 |
| ③ | 元素规划 | 元素库(MOTION_LIBRARY §1)匹配 | 按表达核心选元素 |
| ④ | 组件核对 | components.css 有对应组件 → 用 | 没有 → 评估新建补库 |
| ⑤ | 动效匹配 | 强动效 Tier1,内容驱动 | 无匹配 → 自创(过 S18b/S32) |
| ⑥ | 字体规范 | 页眉页脚字体/字号/行距走 tokens | 用 tokens 变量,R14 |
🔴 **强制:写场景前必须先读完以下文件(不可跳过):**
1. `references/SYNC_ENGINE.md` — 理解 ct(音频时间) vs localT(挂钟时间)的分工
2. `references/scene-creator.md` — 完整阅读,尤其 §"⏱️ tick 契约" 和 §"🚫 ct 锚点必须按时间递增排列"
3. 当前主题的 `assets/tokens.css` — 知道有哪些 `--hf-*` 变量可用
4. `scripts/check-scene-prerequisites.mjs` — **逐条读全部检查规则**(R3动效种类/R10 DOM守卫/R12间隔≤2.5s/R14字号/R15对齐SRT/R18组件),写场景时一次满足,避免"写→被打回→修"反复
5. `scripts/check-hard-rules.mjs` — 重点读 S10 offset一致性 / S18动效≥3 / S22 beat递增 / S27多阶段≥2 / R2c至少1锚点
6. `scripts/check-layout-quality.mjs` — 读 R_LAYOUT2 布局判定逻辑,规划时就安排不同布局模式
7. `references/LAYOUT_ENGINE.md` — **overload 规则:内容超 92 字或每秒超 16 字 → 必须拆分 scene 或强制分栏**。规划时就按内容量决定布局,不要等渲染后溢出再改
8. `references/components/LAYOUTS/` — **场景骨架优先用组件库**(scene-two-column 分栏 / scene-demo 流程 / scene-stats 数据等),别手写竖排堆叠,最容易溢出视口
9. `references/MOTION_LIBRARY.md` — **元素库 × 动效库(辉哥 2026-08-02 强制)**。每种元素(文字/数据/卡片/标签/流程/图形/结构/状态/转场/界面模拟)必须配对应强动效,规划时按 §3 匹配矩阵分配,禁止 fadeUp 应付
10. `references/SCENE_PREFLIGHT.md` — **源头治理规范(辉哥 2026-08-02 强制)**。深度理解口播稿是场景规划源头:通读→场景对齐→提炼表达核心→不明用选择项提问→视觉翻译→写 expressionCore
11. `references/SCENE_CHECKLIST.md` — **写场景必过清单(辉哥 2026-08-02 强制)**。把 R1-R18/S1-S37/R_LAYOUT 全部规则浓缩成可勾选清单。**写第一个场景前逐条读内化,写时对着写,写完自查,一次写对**。批量写 + 批量查,不逐个往返。
12. `references/SCENE_PLANNING.md` — **场景规划方法论(辉哥 2026-08-02 强制)**。规划的本质(匹配层)、流程(深度读口播稿→语义切分→expressionCore 深度设计→布局)、每场景 6 项要素、规划铁律、动效匹配语义参考。**Step 1d 必读**,规划对了后面不修补。
> **血训(2026-08-02)**:不读检查脚本就写场景,被规则打回十几轮,浪费一半时间。**写第一个场景前先通读三条检查脚本的规则定义 + SCENE_CHECKLIST**,一次满足全部约束。
> **血训(2026-08-02)· 内容溢出**:scene-10/14 规划时没按内容量选布局,生成后竖排堆叠溢出 16:9 视口,事后分栏改很麻烦。**规划时内容多就分栏/加场景(LAYOUT_ENGINE overload 规则),别等预览后溢出再修。**
**写场景时禁止直接复制 `_layout.html` 或 `_type-*.html` 的 JS 部分作为标准写法。** 这些文件是"机制说明书",不是模板。照抄 = 违规。
> **规则和脚本检查都是辅助措施,画面效果才是目的(辉哥 2026-08-02 定)**:检查脚本过 ≠ 画面好——脚本只是最低门槛,真正要的是"这段音频内容用视觉表达得够不够有力"。写场景核心永远是"这段内容怎么用视觉表达最有力",检查是最后的兜底确认。**为画面效果做场景,不为过检查做场景**。规则是参考不是束缚——内容表达需要时,规则之外可自创(符合强动效标准)。
### 写入后的硬性强制检查链(双层检查机制,辉哥 2026-08-02 强制,跳步=违规)
**第一层:批量写完所有场景 → 全量检查统一修(高效节奏,辉哥 2026-08-09 改:取消单检逐个往返)**
> **关键**:写场景前先读 `references/SCENE_CHECKLIST.md`(写场景必过清单)内化全部规则,然后**批量写完全部场景,直接跑全量检查统一修复**。不要"写一个→打回→改一个"逐个往返,也不要用 check-scene-single 逐个检查(那是血训,18 个场景被打回十几轮 + 20260809 单检后 full-check 又暴露 R 系列 33 个错误,重复返工)。规则内化后一次写对率大幅提升,检查集中在全量一次做完。
```bash
# 批量写完所有场景后,直接跑全量检查(包含前置 R 系列 + 硬规则 S 系列 + 布局 + 视口 + 音频时长)
node "{skillRoot}/scripts/full-check.mjs" --project "{projectRoot}"
# 全量不通过 → 根据输出逐场景/逐规则统一修 → 再跑 full-check 直到全绿
```
**布局提前拦截**:full-check 的布局检查会读取全部场景,连续 3 个场景布局相同 → 报 R_LAYOUT2。批量写完一起修即可,不用边写边拦。
**第二层:全场景汇总检查(全部场景完成后必跑,全过才进入音频/渲染)**
所有场景写完 + 第一层全过后,跑完整汇总。不通过**不准通知辉哥**:
```bash
node "{skillRoot}/scripts/full-check.mjs" --project "{projectRoot}"
# 前置 + 硬规则全场景 + 布局 + 视口溢出 + 音频时长
# 输出场景级汇总表(每场景 ✅/❌ + 错误/警告数)
```
| 检查 | 检测什么 | 不通过的后果 |
|------|---------|------------|
| **check-scene-single.mjs**(写场景唯一入口) | 前置(R系列+R12间隔≤3s)+硬规则(S系列: S2色板/S27多阶段/S33 mockup/S26动画类等)+布局+视口 | 不修不能写下一个场景 |
| check-scene-prerequisites.mjs | 仅供 debug 用基础规则(`--file` 只查 R 系列,**不能替代 check-scene-single**,血训 2026-08-08 --file 假绿 S 系列全漏) | 不修不能写下一个场景 |
| check-layout-quality.mjs | 布局模式不重复/根容器居中/行距≥1.7/字号≥52px | 不修不能进入预览 |
| check-hard-rules.mjs | main.js正确性/索引结构/时间戳一致性 | 不修不能进入预览 |
| 浏览器预览 | 音画同步/内容匹配/活动效可见 | 不修不能通知用户 |
| check-viewport-overflow.mjs | 元素超出 16:9 视口(内容太多→分栏/加场景) | 不修不能确认 preview-ok |
**🔴 GATE 1:批量写完所有场景后,直接跑 `full-check.mjs` 全量检查统一修(辉哥 2026-08-09 改:不是"写一个查一个",也不是单检逐个往返;改为批量写完全部 → 全量检查一次修完)**
读 `references/scene-creator.md` 作为完整协议(含代码示例),然后对 `scene-timing.json` 中每个场景写 `scenes/NN-name.html`。
**每个场景是一个独立片段**:`<style>` + HTML + `<script>` 三段,不含 `<html>/<head>/<body>`。
`scenes/` 下有类型参考文件(`_type-*.html`),可直接复制改名后改内容。
### 组件库定位(参考手册,不是死规矩)
- ✅ **有合适的组件** → 抄现成的(`references/components/MICRO|MACRO|LAYOUTS|DECORATIONS/*.md`)
- ✅ **没合适的** → AI 自由发挥
- ✅ **发挥出好东西** → 反哺 `components/`,后续项目复用
- ✅ **用户指定** → 必须去 `components/` 找
- ⚠️ **特定语义动效不强行通用化**(齿轮咬合教训):动效是内容的表达不是套壳。从旧项目提炼动效看**通用性 > 特定性**——gear-spin(齿轮咬合)这类"AI 根据特定语义生成的动画"通用性低,只对匹配内容用,不归入通用 Tier 1。写场景先想"这段内容要表达什么动作",再从组件库匹配或自创。
### 场景切片原则(视频表现第一位 · 灵活但有护栏)
**核心**:场景是**匹配层**,按音频时间段拆分,用视觉表达每段音频内容。切分**灵活但有限制**(辉哥 2026-08-02 定,防 AI 乱切):
**🔒 三护栏(缺一不可,违反=违规):**
1. **断点必须锚定 SRT 语义断点** — 场景边界必须是 SRT 字幕语义断点(一段表达核心的起止),**禁止拍脑袋切**(等距/固定间隔/凭感觉都是违规)。拿不准就锚定后继场景首条字幕的开始时间。
2. **单场景时长锁定区间 4-12s** — 灵活只发生在区间内(内容多/关键 → 偏 8-12s;过渡/简短 → 偏 4-6s)。**低于 4s = 切太碎**(视觉没展开就走),**高于 12s = 切太长**(视觉撑不满空洞)。区间外先调切片,不是硬撑。
3. **视觉充实兜底(脚本强制)** — 每场景视觉表达必须填满时长:元素不够 → 补辅助元素(图标/标签/装饰/状态);动效不够 → 补动态元素(强动效/流动/扫描)。写完后 S18b(强动效≥3)/ R12(间隔≤2.5s)/ 视口溢出检查拦截,不过就打回。
**同一段音频(如 60s)可切 10 场景(5s/个,节奏快信息密)或 5 场景(10s/个,从容深入)或混合(5s/8s/10s)** —— 切分依据 = **每段音频表达的核心意思**,不是固定字数/固定条数。宁可 20 帧画面丰富,不要 8 帧每帧死长。
**🔒 护栏4(场景总数锚定基准,血训 2026-08-02:35→18):**
- **场景总数参考历史项目基准**:5-6 分钟口播视频 ≈ **15-20 个场景**(实测基准:20260802-de-ai-skills 项目 373s / 18 场景 ≈ 20.7s/场景)。
- **一段字幕 ≠ 一个场景**:字幕时间线零碎,必须**几段字幕组合成一个完整语义 → 一个场景**。若 5-6 分钟切出 30+ 个场景 = 碎片化(血训:首次规划 35 个 → 辉哥"切换太频繁、画面元素不高密度" → 重切 18 个)。
- **切换对观众无感**:靠**场景内高密度元素 + 多动效**撑满视觉、制造节奏,**不靠频繁切场景**。频繁切换反而显碎片化低质。
- **场景少 → 每个场景内容要更密**:场景数量 15-20 时,每个场景必须 5-8 元素 + ≥3 种动效撑满时长(护栏3 视觉充实兜底),不是靠切场景补密度。
**例外**:金句收尾帧(quote/finale)可以少于常规元素密度,但时长仍受区间约束。
### 场景质量自检清单(写完后强制逐条检查,不通过不能提交)
写完每个场景 HTML 后,必须逐条确认:
- [ ] 父容器无 `opacity:0` — 只对直接触发动画的叶子元素设 `opacity:0`,父容器不能设(子动画穿透不了父透明度)(count CSS class 中的子元素数量)
- [ ] 动画效果 ≥ 2 种(不能只有 fadeUp,必须有 pop / scaleX / stagger / countUp 中的至少一种)
- [ ] 标题字号 ≥ 36px(金句/升华场景允许 ≥ 32px)
- [ ] 元素出场间隔 ≤ 2.5s(无 > 2.5s 的视觉死区)
- [ ] CSS 无硬编码颜色值(全部用 `var(--hf-*)` 或 `color-mix`)
- [ ] 花括号平衡(grep `{` 和 `}` 数量相等)
- [ ] 类名前缀正确(使用 `.sN` 前缀,无裸 `.tag`/`.badge` 冲突)
- [ ] `SCENE_OFFSET` / `SCENE_DUR` 与 `scene-timing.json` 一致
### 写场景前的强制要求
写每个场景 HTML 前,必须:
1. **读 `_type-*.html` 骨架** — 找到对应的视觉类型文件,读它的 CSS 结构和动画模式,基于它改编,不从头写
2. **对照场景描述稿的元素清单** — 场景描述稿写了几条元素,场景里就必须做几个,不能少
3. **对照参考场景** — 从最近一个已验证通过的项目中找同类型场景,看它的密度和动画丰富度
4. **打开 QUALITY_GATES.md 逐条对照 GATE 1 清单** — 不凭记忆写,必须逐条看
5. **打开 audio.srt,列出当前场景的每句字幕时间戳** — 每个 `if(ct>=X)` 必须对应一条 SRT 时间戳,写在注释里
6. **布局规划** — 连续 3 个场景不能使用相同布局模式(grid/compare/timeline/cards-row/sparse 至少轮换)
### 写场景时的硬禁则
1. **🚫 严禁用 sed 修改场景 JS** — sed 多行替换会破坏 JS 语法。改 JS 一律用 Read + Write 完整覆盖文件(已坏 3 次,浪费数小时)"
2. **🚫 禁止用 Python 读写场景 HTML 文件** — Python 的 GBK 默认编码会彻底破坏 UTF-8 中文字符,导致 HTML 内容乱码、JS 注释吞代码、typewriter 字符串显示乱码。一律用 Node.js 脚本或手动编辑。
3. **🚫 严禁在 JS 中使用含中文的 `//` 单行注释** — 编码损坏时 `//` 注释可能丢失换行符,同一行后续代码被静默注释掉,造成花括号不匹配的语法错误。所有注释改用 `/* */` 块注释,或使用英文。
4. **每个 `if(ct>=X)` 必须对应 SRT 时间戳** — 在 tick 函数上方注释中写明 SRT 映射。禁止按固定间隔(0, 0.5, 1.5...)拍脑袋
5. **禁止硬编码 `background:#fff` 或任何色值** — 所有颜色走 `var(--hf-*)`,否则换主题后白底白字不可见
### 写场景后的双重检查
写完每个场景后:
1. **开发者自查**(执行者做)— 逐条过自检清单,不满足重写
2. **编码完整性检查** — 搜以下模式确认无乱码:
- `grep -P "[^\x00-\x7F]" scenes/scene-NN.html` 检查非 ASCII 字符是否都是正确的中文
- `grep "�" scenes/scene-NN.html" 检查 U+FFFD 替换字符
- `grep "txt:'" scenes/scene-NN.html" 检查 typewriter 参数字符串是否为正确中文(如发现 `txt:'鍙欓潰...'` 则是乱码,需替换)
3. **用户验收前不可提交** — 自查通过后才能让用户看,不能让用户当测试员
**不满足任一条件 → 必须重写该场景,不能跳过进入下一步。**
### ⚠️ 强制:普通人听得懂检测
**口播稿不是文章,是让人说的,也是让普通人听的。** 任何专业术语、书面表达、AI 腔必须清零。
写完后必须过 `references/koubo-script-guide.md` 中的"普通人听得懂检测"清单。但凡有一句让不做这行的人皱眉头,重写。
**重点清洗词:** 节奏卡点、特效动效、版式镜头、工具链、结构化的、可执行的、可量化的、提炼、提取、复刻、对标、视觉风格——全部换成大白话。**"我妈妈能不能听懂?"听不懂就换。**
### 硬规则(详见 `references/scene-creator.md`)
- **元素初始状态**:所有需要动画揭示的元素必须 CSS 写 `opacity:0`
- **CSS reveal 选择器**:fullscreen 用 `#mount.reveal-XX .element`;portrait-center 用 `#mount-left.reveal-XX .element` 或 `#mount-right.reveal-XX .element`
- **视觉领先**:元素出现时间控制在对应口播 beat 起始前 **0.08–0.18s**,最大 **0.35s**
- **动画时长**:入场 0.5s–0.7s(不短于 0.35s),infinite 全片不超过 2 处
- **同时运动元素**不超过 2 个
- **长场景(>5s)**:必须加 JS 驱动的持续进度/状态(数字计数、进度条、播放头走位)
- **🚫 position:absolute;inset:0 覆盖父级 padding(v2 血训)**:场景内子元素用 `position:absolute;inset:0` 做全容器覆盖时,父容器的 `padding` 对绝对定位子元素**完全无效**。错误写法:`.wrap{padding:60px}` → `.x{position:absolute;inset:0}` → 子元素覆盖到 padding 区域外。正确写法:`.x{position:absolute;inset:0;padding:60px}` 或在中间加一层 `position:relative` 容器。每次写 `inset:0` 后必须在浏览器肉眼确认元素位置。
- **严禁**:GSAP / Anime.js 等外部库、呼吸动效、抖动、场景内用 `id="mount"`、内嵌 `<audio>`
- **🎨 主题可切换性(v2 硬规则)**:场景 `<style>` 内**禁止硬编码颜色值**。任何 `#RRGGBB` / `rgb()` / `rgba()` 必须来自 `var(--hf-*)` 或 `color-mix(in srgb, var(--hf-primary) 20%, transparent)`。**唯一例外**:纯黑 `#000` / 纯白 `#fff` / `transparent` 用于中性阴影/描边可豁免。违反 = 切主题后白底白字,必须重写。
- **🎨 开场(opener)颜色规则(v4)**:index.html 内的开场动画 CSS 颜色**必须**用 `var(--hf-*)` + `color-mix()`,JS 粒子颜色**必须**从 `AX.theme.pri` / `AX.theme.sec` 读取(`main.js` 已加载 `loadThemeColors()`),需半透明用 `AX.hexRgba(AX.theme.pri, 0.3)` 转换。**禁止** JS 里写 `'#00e5ff'` / `'rgba(0,229,255,0.3)'` 等硬编码。示例:
```js
// 正确
spawnParticles(cx, cy, 60, AX.theme.pri, 8);
spawnParticles(cx, cy, 40, AX.theme.sec, 6);
spawnParticles(cx, cy, 2, AX.hexRgba(AX.theme.pri, 0.3), 2);
// 错误
spawnParticles(cx, cy, 60, '#00e5ff', 8);
```
**⚠️ 血训:开场闪文字("啪!")的 `color` 禁止硬编码 `#fff`。浅色主题下白字在浅底上完全不可见。必须用 `color:var(--hf-primary)`,深色主题自动变亮色,浅色主题自动变深色。**
- **🎯 SVG 图标必须描线化(v2 硬规则)**:每个内联 `<svg>` 必须显式声明 `fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"`,或外层 CSS 里 `.xxx svg { fill:none; stroke:currentColor }`。否则深色主题下变黑色实心块。
- **🚫 禁用手绘/涂鸦元素**:不用 sketch-arrow / scribble / hand-drawn / wobble 类的手绘感 SVG。用正式箭头字符 `→` / SVG 直线 / CSS 分割线代替。
- **📦 主题变量完整性**:场景只用 `references/AESTHETIC_GUARDRAILS.md` §8.2 列出的 `--hf-*` 清单。`theme-defaults.css` 已兜底所有扩展变量(`--hf-info` / `--hf-pos` / `--hf-bg-surface` 等),任何主题都能跑。
- **⌨️ 键盘导航契约**:`main.js` 的 goTo 必须**保持切换前的播放/暂停状态**(暂停中按 → 只切帧不自动播;播放中按 → 切帧后继续播)。加 `seeking` 锁避免 tick 竞态。
- **🈶 中文字符完整性自检(v3 升级版)**:写完场景 HTML 后,必须依次检查:
1. `grep "�" scenes/scene-NN.html` — 无 U+FFFD 替换字符
2. `grep "txt:'" scenes/scene-NN.html` — 检查 typewriter 文本参数是否为正确中文(发现 `鍙欓潰`、`鏁版嵁`、`娲诲姩` 等异常字符 → 整段重写)
3. 有 `//` 注释的行检查是否和后续代码在同一行(发现合并行 → 分离注释和代码)
4. 在浏览器 Console 确认无 SyntaxError(红色报错)
- **🚫 禁止使用 CSS animation-delay 替代 RAF 揭示(v2 铁律)**:所有场景元素的出场时机**必须**通过 RAF tick 函数读取 `audio.currentTime` 驱动,`beats` + `revealMap` 绑定到 SRT 时间轴。**绝对禁止**用纯 CSS `animation-delay` 控制元素出现时机。CSS `animation-delay` 只能用于同一次 reveal 内部的子元素错峰(如列表项的 `.1s` 间隔)。违反后果:音画不同步、键盘切帧乱跳、暂停切帧看不到终态。此规则优先级凌驾于所有其他动画规则之上。
- **⏱️ tick 契约:下界 `t >= 0` + 无上界 + 活性检查**(v2 硬规则终版):场景 tick 函数**只写 `if (t >= 0)`,不写 `t < SCENE_DUR`**。理由:`SCENE_DUR` 等于最后一条 beat 的 end,而末段 reveal 常带正 offset(如 +0.3s / +0.4s),触发时刻 > SCENE_DUR → **末段 reveal 永远丢帧**。停 tick 用**活性检查**替代 —— 每帧首行 `if(!document.querySelector('.xx')) return;`(.xx 是本场景根 class),场景切走后 DOM 消失 tick 自然停,不需要靠时间上界。**只写 `t > 0` 也不行**(键盘 goTo 精确落到 t=0 会漏 reveal)。portrait-center 场景用 `document.getElementById('mount-left')` + `.querySelector('.xx')` 做活性检查。
- **⚠️ 同一元素多个 animation shorthand 互相覆盖(v2 血训 / 07-personas 惨案)**:对同一个元素,后定义的 `animation` shorthand 会**完整覆盖**前一个所有子属性(name/duration/timing/delay/fill-mode),不等同于"只改自身属性"。后果:前一个 animation 控制的 `opacity` 回退到 CSS 初始值 0,元素消失。修复:后定义的 animation 的 keyframes **必须显式保持**前一个 animation 控制的关键属性(如 `opacity:1; transform:none` 写进 `from{...} to{...}`)。更优方案:用伪元素或子元素做附加效果,避免对同一元素用多个 animation。写完场景后 grep 搜同一 ID/class 是否有多条 animation 规则命中。
- **🕐 场景时间必须以 SRT 为唯一真相(v2 核心硬规则)**:`scene-timing.json` **必须**由 `build-timing-from-srt.mjs` 从真实音频 SRT 反推生成,禁止手写或用"目标时长累加"的估算。场景内 `SCENE_OFFSET` / `SCENE_DUR` **必须**与 `scene-timing.json` 完全一致。三者不同步 = 音画错位 ~1-2秒,画面提前切换。修改任何一处 = 三处同步修改。
- **📝 手写压缩 JS 必须花括号平衡自检(v2 血训)**:场景 <script> 内的 IIFE 如果花括号不平衡(多一个 `}` 或少一个),整个脚本静默不执行 —— RAF tick 不启动、reveal 不触发、所有元素停在 `opacity:0`。写完场景后必须:① grep 搜 `{` 和 `}` 数量是否相等;② 在浏览器 console 检查是否有语法错误(红色报错);③ 手动切换场景进出确认 reveal 正常。**禁止手写压缩场景脚本**:用 AI 生成后至少保留可读格式,花括号对齐,方便眼球检查。
- **🎧 音频总长兜底(对抗式自检)**:`scene-timing.json` 最后一个场景的 `end` **必须 ≥ audio.mp3 实际总长(ffprobe -show_entries format=duration 读到的秒数)**。如果 `end < 音频总长`,最后一段口播会在场景 tick 之外播完 = 用户听到声音看不到画面。修改 timing 后**必须**跑一次自检:`ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 audio.mp3`,把结果和 timing 末场景 `end` 对比。
- **📐 SRT 场景边界取"下一段开始时间"**:从 SRT 反推场景切点时,**用后继场景首条字幕的开始时间**作为前场景的 `end`,而不是前场景末条字幕的结束时间(两者可能不相等,会漏几百毫秒的静音)。例如场景 08 起点 = SRT #56 "说到底" 的 `start`(99.671),场景 07 的 `end` 也是 99.671,不是 #55 的 end。
- **🌐 HTTP 服务器必须支持 Range**(v2 最高优先级硬规则 / 血泪教训):**绝对禁止用 `python -m http.server` 预览**。Python 内置服务器不响应 `Accept-Ranges: bytes`,浏览器无法对 audio/video seek —— `audio.currentTime = X` 静默失败,键盘切帧无效,进度条拖不动,用户以为是"最后一帧看不到"或"音画错位",实际是**根本没跳到那个时间**。用项目内置的 `_serve.js`(Node)或 `npx serve`、`npx http-server`、Nginx。**验证**:`Invoke-WebRequest -Uri "http://localhost:3009/audio.mp3" -Method Head` 响应必须含 `Accept-Ranges: bytes`。**没有这个头 = 立刻换服务器,不要动业务代码**(我曾在这个陷阱上白白改了 5 次 timing/tick/SCENE_DUR/活性检查/CSS,全部无效)。
- **� MP4 渲染必须用逐帧精确模式,禁止用 screencast 连续截帧出正式视频(v2 血泪教训 / 20260730 项目验证)**:`Page.startScreencast` 连续截帧 + 固定 `-framerate` 编码,帧节奏由浏览器合成器决定(非精确 30fps),**长视频会累积漂移** —— 5 分 39 秒的视频实际只截到 331.67s,画面越到后面越超前、音画对不上。**正式渲染必须用 `scripts/render-mp4-seek.mjs`**:逐帧把 `audio.currentTime` seek 到 `k/FPS`,页面按音频时钟切换场景/触发动效,settle 后 `Page.captureScreenshot` 截帧,每帧都是精确时间点的画面、零累积漂移。渲染后必检:`ffprobe -v error -show_entries format=duration output.mp4`,时长必须 ≈ audio.mp3 时长,偏差 >0.5s 即异常。screencast 方式(`render-mp4-pipe.mjs` / `render-mp4.mjs`)只适合短视频或快速预览。
- **🔧 渲染脚本内置 HTTP 服务必须支持 Range**(配合逐帧精确渲染):`render-mp4-seek.mjs` 的 `tryCreateServer` 必须实现 `Accept-Ranges: bytes` + 206 响应。没有 Range → Chrome 无法对音频 seek,`audio.currentTime=X` 全部静默重置为 0 → 每一帧都截到空白初始画面 = **全黑视频**(20260730 实测)。改动渲染脚本时不要删掉这个分支。
- **⏱️ 动画必须用音频时间线驱动,禁止用 performance.now(辉哥 2026-08-08 血训)**:引擎动画(AX.fadeUp/pop/typewriter/countUp 等)进度一律用 `(a.currentTime - t0)/(dur/1000)`,`t0 = offset + beat.t`(字幕/场景定义的动画开始时间)。seek 逐帧渲染时 `performance.now`(电脑真实时钟)会让动画加速 3-6 倍——拍一帧电脑花 0.1-0.2s,动画按电脑时间提前走完(现象:字幕时间线对,动画超前)。**两个致命坑**:① **dur 单位**——`audio.currentTime` 是秒、`dur` 是毫秒,必须 `dur/1000`,否则 p 小 1000 倍、动画卡在 0、**画面空白**(2026-08-08 浪费一整天的元凶,别误判成"currentTime 读不到");② **CSS 循环动画**(`animation: xxx infinite`,管道流动/光点/脉冲)也要锚定音频相位——`syncActivity`(WAAPI `anim.currentTime = (currentTime - sceneStart) % durMs`),在 runScene 的 tk 每帧调用、全程 try-catch。同步位置:`projects/{项目}/assets/main.js` + `templates/fullscreen/assets/main.js` + `templates/portrait-center/assets/main.js`(**人像模板易漏**)。改引擎后**先渲染 20s 小片段给辉哥确认再全量**。
- **�️ SRT 时间戳必须是"真转写"而不是"估算等距切割"**(v2 血泪教训第 2 条):AI 生成脚本时若没有跑 Whisper/百度语音识别,可能会输出**每段固定 X 秒(如 2.000s)**的伪造 SRT。这种 SRT 相对真实音频**平均每句错位 0.3-0.7 秒**,画面按它切分就是"视频提前"或"音画对不上"。**自检方法**:`ffprobe -f lavfi -i "amovie=audio.mp3,silencedetect=n=-30dB:d=0.15"` 拿真实静音断点,跟 SRT 对比。若 SRT 相邻多条呈完全等距(如全部 `X.671 → X+2.671`),**一定是假的**,必须用 ffprobe 静音检测重建,或跑 Whisper 重转写。beats 数组里的时间也要跟着重算。
- **⏱️ 场景 beat.t 必须锚定 SRT 字幕断点,禁止估算 t(辉哥 2026-08-05 检验,全屏/人像通用)**:写场景时每个 beat 的 `t` 必须从 `audio.srt` 对应句的**绝对时间戳**精确算出(`ct = 字幕绝对时间 - offset`),**禁止随手估算**。血训实例(人像 20260805 项目):STEP03 徽章 t 被估算成 9.0s,实际字幕"第三步"在 10.75s → 元素提前出现、时间轴错位,辉哥逐帧检验才抓到。**自检动作**:场景文件顶部写逐句 SRT 注释(`#序号 绝对时间 "字幕原文"`),每个 beat.t 写完对照注释核对。**机器拦截**:检查脚本必须加"非 addClass 的内容 beat 绝对时间落在某句字幕时段内"校验(人像 check-portrait-plan R7 已实现,全屏 check-hard-rules 参照补)。
- **🚫 场景 HTML 在用户未点击前不自动揭示(v2 血训 / 20260721 项目验证)**:场景 HTML 的 RAF tick 在 `mount.innerHTML` 被赋值后立即执行。如果 `main.js` 在初始化时 `await show(0)` 加载了首帧,即使音频未播放,`audio.currentTime === 0` → 所有 `ct>=0` 的 reveal 全部触发 → 用户在空白页看到已揭示的画面。**正确做法**:`main.js` 初始化时不调用 `show(0)`,只做预加载 `loadScene(i)`。等用户点击播放后才首次 `show(0)` + `audio.play()`。`index.html` 的 `#mount` 保持空白(不加 `grid-bg` 初始类),`show()` 函数里再加 `grid-bg`。
- **🔒 CSS 类名安全原则(v2 血训 / 20260721 项目验证 + 20260811 scene-11 血训)**:`assets/components.css` 定义了全局类名 `.tag { opacity:0; transform:scale(0.7) }` 和 `.badge { opacity:0; transform:scale(0) rotate(-25deg) }`,还定义了组件内部类名(mac-win/term/code-block/browser 的 `.body` `.bar` `.dot` `.tt` `.lines` `.row` `.pf` `.out` `.ln` `.kw` `.str` `.fn` `.dots` `.addr` 等)。场景 HTML 内**绝对禁止**使用裸 `.tag` / `.badge` / `.scene` 等未加场景前缀的类名 —— 全局 CSS 的 `opacity:0` 和 `transform` 会直接覆盖场景样式,导致元素不显示或变形。**同时禁止**用组件内部类名(`.body`/`.bar`/`.row` 等)做**场景自有元素**的类名 —— 场景内嵌 `<style>` 在 `<link>` 之后加载、等优先级时场景选择器(如 `.s11 .body`)会覆盖组件内部样式 → 布局错乱(血训 20260811:scene-11 主网格叫 `.body`,覆盖 `.mac-win .body` → 文件列表排成两列)。**所有场景内类名必须带场景前缀且唯一**(如 `.s5 .ptag` / `.s22 .tg` / `.s11 .grid`),类名本身要避免和 global names、组件内部类名重复。写完场景后 grep 搜 `class="tag\|class="badge\|class="scene"`,并跑 check-hard-rules S14d 确认无组件内部类名复用(该规则自动拦截)。
- **⚠️ 先清 inline transform 再切 CSS class transform(v2 血训 / 20260721 项目验证)**:`X.fadeUp()` 在 JS 动画过程中持续写入 `el.style.transform = 'translateY(...)'`。动画完成后 inline 残留 `style.transform: translateY(0px)`,此时切换 CSS class(如 `.think { transform: translateY(-3px) }`)**不生效**—— inline 样式优先级高于 class。**修复**:在触发 CSS class 变换前,先 `el.style.transform = ''` 清除内联残留。模式:`元素.style.transform=''; 元素.classList.add('think')`。
- **📊 相邻 reveal 间隔 ≤ 2.5s(v2 节奏规范 / 20260721 项目验证)**:场景内相邻 element reveal 触发之间的时间间隔**不得超过 2.5 秒**。超过此阈值观众会感受"死区"(画面静止、无新元素)。自检方法:绘制 reveal 链时间线,计算每对相邻 `ct` 值之差。如果有 > 2.5s 的间隙,必须在中间插入填充揭示(一个轻量标签、呼吸灯、或状态文字),或者在 `scene-timing.json` 里减少对应的 `silenceToAdd` 避免死区。**语音播完就播完,不在静默段加 artificial filler 动画**(不要为 padding 而制造假揭示)。
- **📦 通用组件优先从 shared.css 引用(v2 共享策略 / 20260721 项目验证)**:`assets/shared.css` 已包含 `.br`(底部进度条+场景计数)和 `.eb`(眉标圆点标签)两个跨场景共用组件。**新场景不应重复定义这两个组件的 CSS**,直接从 shared.css 继承。如果某个场景需要不同的 margin spacing,只需写一行 `margin-bottom` 覆盖(如 `.s5 .eb{margin-bottom:24px}`),而不是重写整个组件。后续 `.stepno`、色卡网格等高频模式也应逐步提到 shared.css 或 components.css。
---
### 🔴 GATE 1:场景质量门禁(批量写完统一执行)
批量写完全部场景后,打开 `references/QUALITY_GATES.md`,**逐条执行 GATE 1 检查清单**。结构正确性/主题安全性/RAF安全性/视觉效果 四组全部通过,才算写完。**不是"写一个查一个"**(逐个往返浪费时间,血训 18 场景打回十几轮;批量写→批量查→统一过 GATE 1,效率高且一次写对率靠规则内化保证)。
**硬约束**:连续 3 次不通过 → 停,反思方案假设,重读 QUALITY_GATES.md 的"血训记录"章节。
---
### Step 4. 预览
**⚠️ 绝对禁止用 `python -m http.server`**:Python 内置服务器不响应 `Accept-Ranges`,浏览器无法对 audio/video 做 seek。表现为:键盘切帧无效、进度条拖不动、`audio.currentTime = X` 静默失败、"最后一帧看不到"。**必须**用支持 HTTP Range 请求的服务器。
推荐用项目根目录里的 `_serve.js`(本技能模板已内置):
```powershell
cd {projectRoot}
node _serve.js 3009
```
或用其他支持 Range 的静态服务器:`npx serve`、`npx http-server -p 3009`、Nginx。**验证方法**:
```powershell
Invoke-WebRequest -Uri "http://localhost:3009/audio.mp3" -Method Head |
Select-Object -ExpandProperty Headers
```
响应头必须包含 `Accept-Ranges: bytes`。**没有 = 换服务器**。
浏览器打开 `http://localhost:3009/`,验收网页交付物。
**这就是"网页交付形态"**——独立可播放,永远存在。
---
### 🔴 GATE 2 + GATE 3:main.js 正确性 + 音画同步(预览前执行)
预览前,打开 `references/QUALITY_GATES.md`:
1. **逐条执行 GATE 2**(main.js 正确性)— 主 tick 不死的验证路径、`started` 不是 kill switch、show() 不杀 tick
2. 运行 `node scripts/check-hard-rules.mjs --project "{projectRoot}"`,**结果必须 0 error**
3. **逐条执行 GATE 3**(音画同步)— 浏览器逐帧检查内容匹配、播放模式验证
GATE 2 或 GATE 3 任一不通过 → **修复,重新运行检查**,不能跳过。
---
### Step 5. 渲染 MP4(可选)
🔴 **渲染前先引导用户选择渲染方式(辉哥 2026-08-05 定):**
**流程:**
1. 渲染前向用户展示几种渲染方式 + 各自**效果和用时**,让用户选择(AI 不擅自替用户定)
2. **默认 = 标准质量 MP4(FFMPEG 逐帧精确)**——质量与体积均衡
3. **用户不选择 → 等待 30 秒 → 用浏览器录屏方式渲染**(兜底,先出片给用户看)
4. 渲染后**效果不满意 → 用户选择更高质量方式重渲**
| 方式 | 质量 | 用时(5 分钟视频) | 适用 |
|------|------|------|------|
| **标准质量 FFMPEG**(默认) | 标准 | 20-30 分钟 | 默认推荐,质量与体积均衡 |
| **高质量** | 近乎无损 | 更慢 | 追求画质、正式交付 |
| **浏览器录屏** | 一般 | 最快 | 快速出片 / 兜底(用户 30s 不选择自动用) |
**渲染方式有五种,按优先顺序:**
#### 方式 A:CDP 逐帧精确渲染(推荐 · 音画零漂移)
`scripts/render-mp4-seek.mjs`
**只有这一种方式能保证长视频音画精确同步**。screencast 连续截帧(方式 B/C)的帧节奏由浏览器合成器决定,固定 30fps 编码后会**累积漂移**——5 分 39 秒的视频只截到 331.67s,画面越到后面越超前(20260730 血训)。正式交付必须用本方式。
```powershell
node "{skillRoot}/scripts/render-mp4-seek.mjs" "{projectRoot}" --output output.mp4
```
🔴 **CHECKPOINT:渲染前必须问用户画质**——三档预设:
| 画质 | 参数 | 效果 |
|---|---|---|
| `high`(高质量) | CRF 16 · JPEG 95 · preset slow | 视觉近乎无损,文件大,编码稍慢 |
| `standard`(标准,默认) | CRF 20 · JPEG 90 · preset medium | 质量与体积均衡 |
| `general`(一般) | CRF 23 · JPEG 85 · preset veryfast | 快速出片,文件小,适合预览 |
```powershell
node "{skillRoot}/scripts/render-mp4-seek.mjs" "{projectRoot}" --output output.mp4 --quality high
```
可选参数:
```
--width 1920 --height 1080 # 输出尺寸
--fps 30 # 帧率
--quality high|standard|general # 画质预设(默认 standard)
--settle 45 # seek 后等待页面渲染的毫秒数(渲染提前量随动)
--limit 6 # 只渲染前 N 秒(冒烟测试用)
--output output-sync.mp4 # 输出文件名
```
原理:逐帧 `audio.currentTime = k/FPS`(音频时钟精确驱动)→ 页面按音频时钟切换场景/触发动效 → settle 后 `Page.captureScreenshot` 截帧 → 管道送 FFmpeg → mux audio.mp3。**渲染脚本内置 HTTP 服务已实现 Range 支持**(无 Range 时音频 seek 全被重置、视频全黑)。
**缺点**:逐帧 seek+截图较慢(5 分钟视频约 20-30 分钟)。可以先 `--limit 10` 冒烟测试再全量。
**渲染前自动校验(fail-fast)**:脚本启动时自动运行 `verify-timeline.mjs`,校验 **audio.mp3 时长 ↔ audio.srt 最后时间戳 ↔ scene-timing.json 边界** 三方对齐(含场景边界对齐 SRT 字幕起点、场景边界连续性、场景文件齐全)。任一 FAIL → 直接拒绝渲染,不浪费时间。也可手动提前跑:
```powershell
node "{skillRoot}/scripts/verify-timeline.mjs" --project "{projectRoot}"
```
**渲染中时钟漂移检测(浏览器问题主力)**:逐帧 seek 后回读 `audio.currentTime`,对比期望帧时间。漂移 >0.3s 记警告、>0.5s 记失败,失败帧占比 ≥5% → 直接拒绝出片。浏览器 seek 不精确 / 音频未就绪 / HTTP Range 缺失都会触发(这是画面与字幕时间线错位的根源)。
**渲染后必检(脚本自动)**:
1. 时长:`ffprobe` 视频时长必须 ≈ audio.mp3 时长,偏差 >0.5s 即异常
2. 帧数:`ffprobe -count_frames` 视频帧数必须 ≈ 预期帧数(±1%,抓管道丢帧导致时间轴缩短)
**渲染后一键验证**:`node "{skillRoot}/scripts/check-render-frames.mjs" "{projectRoot}"` — 自动完成 时长/帧数对齐 + 抽帧 OCR 验证画面内容 + PIL 像素分析网格线清晰度(间隔 56px、对比度 >4 即清晰)。无网络加 `--no-ocr`,无 python/PIL 加 `--no-grid`。
#### 方式 B:CDP 管道实时截帧(快速 · 长视频有漂移风险)
`scripts/render-mp4-pipe.mjs`
⚠️ **仅适合短视频(<1 分钟)或快速预览**。长视频会累积音画漂移(原因见方式 A)。
无窗口无声音,后台默默截图走管子。
一次性依赖安装(每个工作区装一次即可):
```powershell
npm i chrome-remote-interface --save-dev --prefix "{skillRoot}"
```
一行命令渲染:
```powershell
node "{skillRoot}/scripts/render-mp4-pipe.mjs" "{projectRoot}"
```
可选参数:
```
--width 1920 --height 1080 # 输出尺寸
--fps 30 # 帧率
--port 9226 # CDP 端口起始
--http-port 3022 # 静态服务端口起始
--chrome "path/to/chrome.exe" # 显式指定 Chrome 路径
--output output-pipe.mp4 # 输出文件名
```
原理:起本地 HTTP → headless Chrome + CDP 截图 → **管道直送 FFmpeg(不写磁盘)** → mux audio.mp3 → output.mp4
**优点**:不写临时文件,省约 1 分钟(无需 FFmpeg 二次编码)。
**缺点**:管道在 Windows 上可能偶发背压问题。
输出:`{projectRoot}/{output文件名}`
#### 方式 C:CDP 写文件模式(兜底 · 有漂移风险)
`scripts/render-mp4.mjs`
⚠️ **仅适合短视频或快速预览**(漂移原因见方式 A)。当管道模式异常时,用这个:
```powershell
node "{skillRoot}/scripts/render-mp4.mjs" "{projectRoot}"
```
可选参数:
```
--width 1920 --height 1080 # 输出尺寸
--fps 30 # 帧率
--port 9222 # CDP 端口
--http-port 3009 # 静态服务端口
--chrome "path/to/chrome.exe" # 显式指定 Chrome 路径
--keep-frames # 保留临时帧不删(调试用)
```
原理:起本地 HTTP → headless Chrome + CDP 逐帧收 JPEG → **写磁盘** → FFmpeg 拼帧 + 塞 audio.mp3 → output.mp4
**优点**:多一道 FFmpeg 重编码,文件更小(CRF 20 质量可控)。
**缺点**:多写 14000 个临时文件(~1.5GB),多约 1 分钟。
输出:`{projectRoot}/output.mp4`
#### 方式 D:FFmpeg gdigrab 自动录屏(需弹窗 · 有漂移风险)
`scripts/render-mp4-gdigrab.mjs`
⚠️ **仅适合短视频或快速预览**(漂移原因见方式 A)。
弹一个可见 Chrome 窗口在 (0,0) 位置,FFmpeg 用 gdigrab 直接录窗口画面。
```powershell
node "{skillRoot}/scripts/render-mp4-gdigrab.mjs" "{projectRoot}"
```
可选参数:
```
--width 1920 --height 1080 # 输出尺寸
--fps 30 # 帧率
--chrome "path/to/chrome.exe" # 显式指定 Chrome 路径
--output output-gdigrab.mp4 # 输出文件名
```
原理:开可见 Chrome 窗口 → FFmpeg gdigrab 录窗口画面 → CDP 触发播放 → 播完停录 → mux 音频
**优点**:录制期间画面即时可见,编码效率由 FFmpeg 显卡/CPU 决定。
**缺点**:必须弹窗且不能被遮挡,录制时屏幕 (0,0)-(1920,1080) 区域被占用。
输出:`{projectRoot}/{输出文件名}`
#### 兜底路径:OBS 手动录屏
见 `references/RENDER_MP4_MANUAL.md`。用于 CDP 自动化异常(Chrome 版本不兼容、npm 装不上)时。
---
## 字体与布局系统
### 字体方案
所有字体从 Google Fonts 下载到本地,存放在 `assets/*.ttf`,通过 `@font-face` 加载,**不依赖外部 CDN**。
**基础字体(默认继承):**
| 字体 | Weight | 角色 |
|------|--------|------|
| Inter | 400/500/600/700/800 | 英文、数字、标签、页码(通用) |
| Noto Sans SC | 400/500/600/700/800 | 中文正文、标题(跨平台统一) |
**扩展字体(按场景匹配):**
| 字体 | 感觉 | 使用场景 | 下载 |
|------|------|---------|------|
| Archivo Black | 极粗冲击 | 开场 hook、CTA 号召 | 400 一个 weight |
| Space Grotesk | 几何科技 | 维度展示、步骤流程、结构说明 | 400/500/600/700 |
| Anton | 扁宽震撼 | 过渡场景、大数字 | 400 一个 weight |
| Playfair Display | 优雅衬线 | 引文、金句、收尾升华 | 400/700/400i |
| JetBrains Mono | 等宽硬核 | 数据、证据、统计数字 | 400/500/700 |
**字体分配原则:**
- **每个场景根元素显式设置 font-family**(不依赖 body 继承,防止动态加载丢失)
- 眉标(eyebrow badge)、标签(tags)等 UI 辅助元素统一用 Inter
- 中文统一用 Noto Sans SC(或衬线搭配时用 Noto Serif SC)
- 大数字/标题用对应风格字体,正文保持不变
**index.html 必须包含:**
```html
<style>
/* 全部 @font-face 规则在此,不依赖外部 CSS 文件 */
@font-face { font-family:'Inter'; font-weight:400; font-display:swap; src:url('assets/Inter-400.ttf') format('truetype'); }
/* ... 所有字体 ... */
html, body { font-family:'Inter','Noto Sans SC','PingFang SC','Microsoft YaHei',system-ui,sans-serif; }
</style>
```
### 布局模式
**根据内容量选择布局,不预设固定规则:**
| 布局 | 适合 | 特点 |
|------|------|------|
| **左竖线 + 单栏左对齐** | 内容量少的场景(1-2个元素) | 左侧 3px 竖线装饰,内容靠左,留白在右 |
| **中间竖线 + 左右分栏** | 左右内容均衡的场景 | 统一背景 + 中间 1px 渐变竖线分隔 |
| **居中布局** | 大数字过渡、纯文本标题 | 简洁有力,适合 6 秒内的过渡场景 |
**基础 HTML 模板(左竖线单栏):**
```html
<style>
.sN{position:absolute;inset:0;font-family:'对应字体','Noto Sans SC',sans-serif;background:var(--hf-bg);overflow:hidden}
.sN .accent-rule{position:absolute;left:80px;top:10%;bottom:10%;width:3px;background:linear-gradient(180deg,transparent,var(--hf-primary),transparent);border-radius:2px;opacity:0;z-index:1}
.sN .content{position:absolute;left:120px;right:80px;top:50%;transform:translateY(-50%);display:flex;flex-direction:column}
</style>
<div class="sN">
<div class="accent-rule"></div>
<div class="content">...</div>
</div>
```
**基础 HTML 模板(中间竖线分栏):**
```html
<style>
.sN{position:absolute;inset:0;display:flex;background:var(--hf-bg);overflow:hidden}
.sN::after{content:'';position:absolute;left:38%;top:12%;bottom:12%;width:1px;background:linear-gradient(180deg,transparent,color-mix(in srgb,var(--hf-text-muted) 18%,transparent),transparent);z-index:1}
.sN .left{width:38%;display:flex;flex-direction:column;align-items:flex-end;justify-content:center;padding:0 40px 0 72px}
.sN .right{flex:1;display:flex;flex-direction:column;justify-content:center;padding:0 72px 0 44px}
</style>
<div class="sN">
<div class="left">...</div>
<div class="right">...</div>
</div>
```
### 关键原则
1. **视觉冲击优先** — 不拘泥固定形式,根据内容选择最能突出信息的布局和字体
2. **布局服从内容** — 左边内容少就单栏竖线,左右均衡就分栏,过渡场景居中
3. **字体区分角色** — 标题用风格字体(Archivo/Anton/Playfair),正文用 Noto Sans SC,UI 元素用 Inter
4. **全部本地加载** — 字体下载到项目 `assets/` 目录,不在线引用 CDN
## 改进已有项目指南
用户可能会要求对已生成的项目做修改。以下是常见场景和操作流程:
### 场景 A:换主题
```powershell
# 1. 选择新主题
node "{skillRoot}/scripts/select-theme.mjs" --query "暖色杂志风"
# 2. 重新应用主题到项目
node "{skillRoot}/scripts/init-project.mjs" `
--theme {newThemeId} `
--layout {layout} `
--project "{projectRoot}"
```
🔴 **CHECKPOINT**:换主题后必须检查所有场景的 CSS 颜色变量是否正常(greo `#RRGGBB` 确认无硬编码),预览确认无白底白字。
### 场景 B:修改场景内容
1. 找到对应场景的 `scenes/NN-name.html`
2. 修改内容后,用 `_serve.js` 预览局部修改
3. 🔴 **CHECKPOINT**:确认修改后的元素无 syntax error(console 检查花括号平衡)
### 场景 C:重新渲染
```powershell
# 逐帧精确渲染(推荐,音画零漂移)
node "{skillRoot}/scripts/render-mp4-seek.mjs" "{projectRoot}" --output output-v2.mp4
```
🛑 **Note**:重新渲染前确认音频时间轴未变,否则需同步更新 `scene-timing.json`。渲染后 `ffprobe` 对比输出时长与 audio.mp3 时长。
---
## 组件反哺机制
当你在场景里做出一个**通用性强、视觉效果好**的组件,完成后要做两件事:
1. **提取**:把 HTML/CSS 剥离出来写成 `references/components/MICRO/xxx.md` 或 `MACRO/xxx.md`
2. **记录**:更新 `references/components/INDEX.md`,加上组件名 + 用途 + 使用示例
**每个项目都在为下一个项目积累素材,组件库越用越强。**
## 引擎脚本一览
**已内置(v2 生态)**:
| 脚本 | 作用 |
|---|---|
| `init-project.mjs` | 初始化项目(复制模板 + 应用主题 + 扁平结构) |
| `render-mp4-seek.mjs` | **逐帧精确 MP4 渲染(推荐,音画零漂移)**,启动时自动跑时间线校验 |
| `verify-timeline.mjs` | **渲染前时间线校验(音频↔SRT↔scene-timing 三方对齐)**,FAIL 拒绝渲染 |
| `render-mp4.mjs` | CDP 写文件模式 MP4 渲染(快速预览/兜底,长视频有漂移风险) |
| `render-mp4-pipe.mjs` | CDP 管道模式 MP4 渲染(快速预览/兜底,长视频有漂移风险) |
| `render-mp4-gdigrab.mjs` | FFmpeg gdigrab 自动录屏(弹窗可见) |
| `select-theme.mjs` | 根据文本推荐主题 |
| `install-optional-theme.mjs` | 安装扩展主题包 |
| `compress-script.mjs` | ⚠️ 已废弃(无压缩稿概念)——仅保留给用户自录场景做录音稿参考,不参与 AI 配音流程 |
| `parse-srt.mjs` | SRT 解析 |
| `build-timing-from-srt.mjs` | SRT → scene-timing.json(auto-split 或 anchor) |
| `match-scene-timing.mjs` | 锚点词映射 SRT 到场景 ID |
| `generate-tts-audio.mjs` | 百度声音克隆 TTS |
| `generate-full-audio.mjs` | 一站式:口播稿 → TTS + SRT(音频先生成,每句真实时间戳) |
| `test-tts.mjs` | TTS 音色试听 |
**人像模式独立体系(与全屏完全隔离,辉哥 2026-08-05 定)**:
| 脚本 | 作用 |
|---|---|
| `analyze-portrait-colors.mjs` | 人像背景抽帧配色分析 → scene-colors.json(人像特有,必跑) |
| `generate-portrait-plan.mjs` | 人像场景规划生成器 → portrait-plan.json + **portrait-plan.md 人可读确认单**(左右栏元素 + hero 主角 + 档位 + 时间轴 + motionPlan + rules,独立于全屏 generate-scene-plan) |
| `check-portrait-rules.mjs` | 人像硬规则检查(M/I/P/S/T/A 组,独立于全屏 check-hard-rules) |
| `check-portrait-plan.mjs` | 人像规划合规检查 R1-R9(按 portrait-plan.json 验证左右栏元素/档位/时间轴/时间锚定字幕) |
| `portrait-full-check.mjs` | 人像全链路汇总检查(硬规则 + 规划合规 + 音频时长,全过才允许预览/渲染) |
**内部库(scripts/lib/)**:
- `text-analysis.mjs` — 场景类型推断、标题提取
- `visual-planning.mjs` — 语义动作 / 视觉家族 / 布局变体推断
- `storyboard-planner.mjs` — SRT 分组
- `layout-planner.mjs` — 密度与安全区
- `sync-planner.mjs` — Beat 揭示时间计算(视觉领先 0.12s)
- `layout-configs.mjs` — 布局模式常量 + SYNC_CONFIG
- `theme-configs.mjs` / `base-themes.mjs` / `installed-extended-themes.mjs` — 主题元数据
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!