dsh-motion 0.3.0 → 0.4.0

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/README.md CHANGED
@@ -48,6 +48,10 @@
48
48
 
49
49
  路由是显式的:`node scripts/route-style.mjs "<题材>"`(点名优先 → registry 打分 → 默认 `swiss-motion`;分数并列/为 0 时要求确认)。规则在 [styles/registry.json](styles/registry.json)。
50
50
 
51
+ 迭代预算(首帧 ≤60s、输出 ≤30k、上下文 ≤100k、读图 ≤10)见 [docs/performance-budget.md](docs/performance-budget.md)。
52
+
53
+ 开工前先过需求卡点:`npm run plan -- "竖屏抖音,讲导数,要旁白"`,会推断画幅、交付、风格的候选项,再交给 `ask_user_question`;能推断的不问。
54
+
51
55
  项目 `DESIGN.md` 头部写 `style: <slug>` + `style_version: 1.0.0`。风格来源、HF 集成、抽帧能力与 Lemo 调研见 [docs/ai-native-motion.md](docs/ai-native-motion.md)。
52
56
 
53
57
  ## 演示
@@ -62,6 +66,28 @@
62
66
 
63
67
  汇总见 [showcase/README.md](showcase/README.md)。
64
68
 
69
+ ## 开工前确认
70
+
71
+ 顺序是硬性的:
72
+
73
+ 1. 需求卡点(画幅 / 交付 / 风格 / 受众 / 旁白)
74
+ 2. 脚本 / 文案(逐句旁白、上屏文字、公式)
75
+ 3. 分镜(镜头、秒数、画面、运动、字幕)
76
+ 4. `DESIGN.md` + 3 张风格帧(只出帧,不渲染)
77
+ 5. 交付清单(尺寸 / fps / 时长 / 文件 / 音频 / 字幕)
78
+ 6. **用户明确说“可以开始 / 开始做 / 按这个执行”**
79
+
80
+ 没听到第 6 步的明确指令,不写完整场景、不出全片。清单模板见
81
+ [skills/animation-router/references/confirmation.md](skills/animation-router/references/confirmation.md)。
82
+
83
+ 按体量分档:`light`(≤15 s 无旁白:脚本+分镜合并)、`standard`(15–60 s 或旁白)、`full`(>60 s 或多段,加 animatic)。确认状态落在项目里的 `APPROVALS.json`:
84
+
85
+ ```bash
86
+ npm run approvals -- init <project> --mode light
87
+ npm run approvals -- set <project> start approved --note "用户说可以开始"
88
+ npm run approvals -- check <project>
89
+ ```
90
+
65
91
  ## 安装
66
92
 
67
93
  ```bash
@@ -50,7 +50,7 @@ HF 的卖点是 "same input, same frames, same output":HTML + 可 seek 的动
50
50
 
51
51
  - Lemo-Opuscar 明确写着风格是为 Claude Opus 5.5 调的,别的模型不一定复现得好;代码 MIT、demo 与静帧 CC BY 4.0;
52
52
  - 它的 59 个风格覆盖纸感、印刷、像素、3D、纪录片等,适合当"语言菜单",不适合直接当品牌规范;
53
- - 可借鉴的三件事:`STYLE.md` 作为 style prompt、三张风格帧验收、分镜只在被要求时做;
53
+ - 可借鉴的三件事:`STYLE.md` 作为 style prompt、三张风格帧验收、分镜作为确认材料;
54
54
  - 授权上,demo 静帧可商用但需署名 LemoLab;风格文件本身是 MIT。
55
55
 
56
56
  本仓库的选择:删除自创的 `dark-editorial`,改为 4 套 Lemo 风格(`swiss-motion` / `dataviz` / `blueprint` / `whiteboard`)+ 1 套 Manim 社区的 `3b1b`,每套按 `STYLE.md` + `tokens.json` + `theme.py` + `preview.jpg` 落地;外部参考统一指向 HF。
@@ -125,7 +125,7 @@ manimgx inspect scene.py -t 1.2,4.5,end
125
125
  DESIGN.md
126
126
  → motion_frames(3–6 帧:开场 / 转折 / 结论)
127
127
  → 改场景 → 再出帧(秒级)
128
- → 需要给用户看分镜时:6–9 帧(真实代码)
128
+ → 确认门:脚本 → 分镜 → DESIGN/风格帧 → 用户明确“开始”
129
129
  → formula_check
130
130
  → motion_render(定稿后)
131
131
  → 需要字幕/旁白/混音:进 HF
@@ -150,15 +150,7 @@ DESIGN.md
150
150
  - 需要交付时才升级到 `motion_render` / HF;
151
151
  - 成片检查(`motion_probe`)只在已经有 mp4 时运行,不做"为了检查而渲染"。
152
152
 
153
- ## 参考
154
-
155
- - [HyperFrames design systems](https://hyperframes.heygen.com/prompting/design-systems)
156
- - [HyperFrames examples](https://hyperframes.heygen.com/examples) · [catalog](https://hyperframes.heygen.com/catalog)
157
- - [HyperFrames CLI rendering](https://hyperframes.heygen.com/guides/rendering)
158
- - [Lemo-Opuscar](https://github.com/lemomo-ai/lemo-opuscar) · [DIRECTOR.md](https://github.com/lemomo-ai/lemo-opuscar/blob/main/DIRECTOR.md) · [TECHNIQUE.md](https://github.com/lemomo-ai/lemo-opuscar/blob/main/TECHNIQUE.md)
159
- - 本地 `manimgx inspect --help`(manimgx 0.1.1)
160
-
161
- ## 4.5 效率:大家怎么缩短时间
153
+ ## 5. 效率:大家怎么缩短时间
162
154
 
163
155
  共识是**把检查点前移、把渲染延后**:
164
156
 
@@ -178,7 +170,7 @@ DESIGN.md
178
170
  - 关键不是"帧比视频快多少",而是**看帧成本与片长无关**,且不经过编码、起浏览器、抽帧三次搬运;
179
171
  - 如果已经在 HF 里,用 `hyperframes snapshot` 而不是"先 render 再 ffmpeg 截图"。
180
172
 
181
- ## 4.6 开发调试
173
+ ## 6. 开发调试
182
174
 
183
175
  服务端插件没有 HMR,但也不需要每次重装:
184
176
 
@@ -189,7 +181,50 @@ DESIGN.md
189
181
 
190
182
  细节见 [dev-loop.md](dev-loop.md)。
191
183
 
192
- ## 4.7 风格路由:目前怎么做,通用标准是什么
184
+ ## 7. 需求卡点:先问,再猜
185
+
186
+ 默认流程多了一个“卡点”,避免在没有信息时自己替用户决定:
187
+
188
+ ```bash
189
+ node scripts/plan-questions.mjs "<brief>" --json
190
+ ```
191
+
192
+ 输出可直接交给 `ask_user_question`:
193
+
194
+ - **画幅**:brief 已有“竖屏/横屏/16:9”就直接写进 assumptions,不再问;没有才给推断的 2 个;
195
+ - **交付**:brief 已有“旁白/静音”就跳过;没有才问;
196
+ - **风格**:`route-style` 的 top 2–3;点名风格时直接省略;
197
+ - **受众**:只对数学/科学、且 brief 未提“高中/大学/大众/专业”时问;选项按平台排(竖屏→大众科普,横屏→高中/大学);
198
+ - **旁白**:只在交付为旁白、且 brief 未提“中文/英文/双语”时问;选项是语言 + 声音;
199
+ - **时长/语言**:从 brief 提取,不单独问;未回答的写进 `assumptions`。
200
+
201
+ 最多 4 题。关键不是“问满四道”,而是**能推断的不问、选项按上下文生成、问完一次就开做**。同时输出 `assumptions.gateMode`:≤15s 且无旁白为 `light`,15–60s 或有旁白为 `standard`,>60s 或多段为 `full`。
202
+
203
+ ### 确认门(本仓库的硬规则,分档)
204
+
205
+ 需求卡点问完,**不等于开工**。按体量选一档:
206
+
207
+ | 模式 | 适用 | 阶段 |
208
+ |---|---|---|
209
+ | `light` | ≤15 s、无旁白 | 需求 → **脚本+分镜(合并)** → 视觉 → 开工 |
210
+ | `standard` | 15–60 s,或有旁白 | 需求 → 脚本 → 分镜 → 视觉 → 交付 → 开工 |
211
+ | `full` | >60 s、多段、复杂旁白 | 需求 → 脚本 → 分镜 → **animatic** → 视觉 → 交付 → 开工 |
212
+
213
+ 确认状态落在项目里的 `APPROVALS.json`:
214
+
215
+ ```bash
216
+ node scripts/approvals.mjs init <project> --mode light
217
+ node scripts/approvals.mjs set <project> script_storyboard approved --note "用户确认"
218
+ node scripts/approvals.mjs check <project>
219
+ ```
220
+
221
+ `check` 未全部 approved 时退出 1;不能跳过前置阶段;`start` 必须是用户明确授权。
222
+
223
+ **是不是太严?** 对 6 秒、无旁白的单公式短片,强制“脚本、分镜、交付清单”三份独立材料确实过重;所以 `light` 把脚本和分镜合并成一张表,保留“明确开工”这一道。对 30 秒以上或带旁白的片子,`standard` 的独立脚本/分镜是值得的;`full` 才加 animatic。
224
+
225
+ 模板在 [skills/animation-router/references/confirmation.md](../skills/animation-router/references/confirmation.md)。
226
+
227
+ ## 8. 风格路由:目前怎么做,通用标准是什么
193
228
 
194
229
  ### 本仓库
195
230
 
@@ -245,7 +280,7 @@ DESIGN.md
245
280
  - registry 的 `keywords` 需要随新题材维护;
246
281
  - 画幅/音频/复杂度字段目前只记录,还没参与硬约束(比如 9:16 未验证的风格应直接拒绝)。
247
282
 
248
- ## 5. 现成的风格体系(2026-10 可直接采用)
283
+ ## 9. 现成的风格体系(2026-10 可直接采用)
249
284
 
250
285
  不必从零写风格文档。这个生态已经沉淀出几套公开、可复用的体系:
251
286
 
@@ -263,7 +298,7 @@ DESIGN.md
263
298
  1. **风格 = 不变量 + 自由故事。** `STYLE.md` 锁死 look、颜色、字体、运动、声音;故事、镜头、节奏由导演决定(Lemo 的 AGENTS.md 原话:`STYLE.md` is fixed;everything else is yours to direct)。
264
299
  2. **风格按名字选,不按形容词选。** 用户说 "swiss-motion" / "瑞士动态排版",agent 查索引、读 `STYLE.md`;没选就推荐 2–3 个最接近的。
265
300
  3. **风格文档有固定骨架。** Lemo 是 11 节;mg-styles-15 是四段式,其中 signature features 必须可检验("cubic-bezier(0.34,1.56,0.64,1),错峰 2–4 帧",不是"要有弹性")。
266
- 4. **先风格帧,后分镜,最后成片。** 三张真实代码出的风格帧 → 用户要看才做 6–9 张分镜 → 成片;风格帧不过不往下走。
301
+ 4. **先确认,后执行。** 脚本 → 分镜 → DESIGN/风格帧 → 交付清单,逐项确认;用户明确说“开始”才写完整场景、出片。
267
302
  5. **独立评审。** 另一个 AI 用 rubric 按 7 个维度打分(风格保真、概念、运动、排版、质感、声画同步、技术),给 5–10 条带时间码的修改,再改两轮。
268
303
  6. **确定性。** 帧是 `render(t)` 的纯函数;同一输入同一输出,便于批量与回归。
269
304
 
@@ -299,3 +334,11 @@ DESIGN.md
299
334
  3. 画面仍由 manimgx 画,不复制它们的 Canvas/WebGL demo 代码,只取语法与 prompt;
300
335
  4. 交付层继续交给 HF,`frame.md` 只做品牌/版式契约;
301
336
  5. 标注来源与许可:Lemo 代码/文档 MIT,demo 静帧 CC BY 4.0(署名 LemoLab);mg-styles-15 MIT;HF Apache-2.0。
337
+
338
+ ## 参考
339
+
340
+ - [HyperFrames design systems](https://hyperframes.heygen.com/prompting/design-systems)
341
+ - [HyperFrames examples](https://hyperframes.heygen.com/examples) · [catalog](https://hyperframes.heygen.com/catalog)
342
+ - [HyperFrames CLI rendering](https://hyperframes.heygen.com/guides/rendering)
343
+ - [Lemo-Opuscar](https://github.com/lemomo-ai/lemo-opuscar) · [DIRECTOR.md](https://github.com/lemomo-ai/lemo-opuscar/blob/main/DIRECTOR.md) · [TECHNIQUE.md](https://github.com/lemomo-ai/lemo-opuscar/blob/main/TECHNIQUE.md)
344
+ - 本地 `manimgx inspect --help`(manimgx 0.1.1)
@@ -0,0 +1,73 @@
1
+ # 迭代预算(P0)
2
+
3
+ 依据:30.02 s 成片、882.1 s 端到端的一次真实诊断。
4
+
5
+ | 指标 | 那次 | 目标 |
6
+ |---|---|---|
7
+ | 首帧 | 261 s | **≤60 s** |
8
+ | 端到端 | 882 s | **≤400 s** |
9
+ | 模型时间 | 740.6 s(84%) | 不单独设目标,随步数/token 降 |
10
+ | 输出 tokens | 126,839 | **≤30k** |
11
+ | 上下文峰值 | 222k / 256k | **≤100k** |
12
+ | 读图 | 32 张 1080p | **≤10 张 sheet/局部** |
13
+ | 全文件重写 | 2 次 | **0 次**(例外见下) |
14
+ | `motion_probe` / `motion_render` | 3 / 4 次 | **1 / 2 次** |
15
+
16
+ ## P0 四条
17
+
18
+ ### 1. 先构图,后动画
19
+
20
+ - 先写“最小场景”:只放 6–8 个关键构图,不写完整动画;
21
+ - `motion_frames` 出 sheet,确认构图与版式;
22
+ - **60 s 内看不到第一帧就停下来**,检查是不是在盲写;
23
+ - 构图确认后才补时间轴、运动、转场。
24
+
25
+ ### 2. 只做局部 edit
26
+
27
+ - 默认用 `edit`,不整文件重写;
28
+ - 只有改动超过文件 50%,或文件 <40 行时,才允许重写;
29
+ - 重写必须在回复里说明原因,不能“顺手重写”。
30
+
31
+ ### 3. 图片预算
32
+
33
+ - 每次迭代只看 **1 张 contact sheet**,宽度约 960 px;
34
+ - 只有可疑格才读全分辨率;
35
+ - 同一张图不重复读;改了场景才重新出图;
36
+ - 目标:整个任务 ≤10 次读图。
37
+
38
+ ### 4. 上下文预算
39
+
40
+ - 只保留当前场景版本,旧版本用一两句话摘要;
41
+ - 不把旧代码、旧图、已经确认过的长文重复贴回上下文;
42
+ - 目标峰值 ≤100k;接近 100k 时先摘要再继续。
43
+
44
+ ## P1(工具)
45
+
46
+ 已完成:
47
+
48
+ - [x] `motion_frames` 按场景 fps 取帧:各风格 `theme.py` 在模块层设置 `m.config.frame_rate`;
49
+ - [x] post 强制 CFR,并校验成片时长(`tpad + CFR` 修正后复验);
50
+ - [x] storyboard 顶部 caption 条加提示,避免误判“裁切”。
51
+
52
+ 明确不做:
53
+
54
+ - CJK 字符级 `ReplacementTransform` / `Transform` 的字形变形。manimgx 核心按点插值,汉字轮廓点数与顺序不一致,修起来是字形级工程;本仓库按“不支持”处理。
55
+ - 中文标题/标签:`Write`、`FadeIn/Out`、`Indicate`、改颜色;
56
+ - 换词:`FadeOut(old)` + `FadeIn(new)`,或按词/行 stagger;
57
+ - 公式:`TransformMatchingTex`;中文注释放在 `Text`,不混进 MathTex。
58
+
59
+ 待做:
60
+
61
+ - [ ] `motion_probe` 只在最终成片跑一次;
62
+ - [ ] `--draft` 低分辨率/低帧率、分段渲染、`manimgx preview` 包装、animatic。
63
+
64
+ ## 怎么量
65
+
66
+ 每个项目记录:
67
+
68
+ ```text
69
+ first_frame_s, total_s, model_s, tool_s, render_s,
70
+ output_tokens, context_peak, image_reads, full_rewrites, probes, renders
71
+ ```
72
+
73
+ 复测时用同一台机器、同一支 30 s 片,先对比“首帧”和“输出 tokens”,再看总时长。
package/lib/ops.js CHANGED
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import { execFile } from 'node:child_process'
11
- import { existsSync, statSync } from 'node:fs'
11
+ import { existsSync, renameSync, statSync } from 'node:fs'
12
12
  import { basename, dirname, join, resolve } from 'node:path'
13
13
  import { promisify } from 'node:util'
14
14
 
@@ -104,10 +104,47 @@ export async function renderScene(config, args) {
104
104
  summary.video = finalOut
105
105
  }
106
106
 
107
+ // post/ffmpeg 可能因 tmix/丢帧把时长压短;用 CFR 补齐并校验,避免"30s 变 27.4s"。
108
+ if (summary.durationSec) {
109
+ const fixed = await ensureDuration(config, summary, fps)
110
+ if (fixed) summary.normalized = true
111
+ }
112
+
107
113
  summary.wallSec = Number(((Date.now() - started) / 1000).toFixed(1))
108
114
  return summary
109
115
  }
110
116
 
117
+ /** 成片时长与渲染器报告的时长不一致时,用 tpad + CFR 修正;返回是否修过。 */
118
+ async function ensureDuration(config, summary, fps) {
119
+ const expected = Number(summary.durationSec)
120
+ if (!Number.isFinite(expected) || expected <= 0) return false
121
+ const file = summary.output
122
+ const before = await probeOutput(config, file)
123
+ const delta = expected - before.durationSec
124
+ if (!Number.isFinite(delta) || Math.abs(delta) <= 0.08) return false
125
+
126
+ const tmp = `${file}.cfr.mp4`
127
+ const chain = []
128
+ if (delta > 0.05) chain.push(`tpad=stop_mode=clone:stop_duration=${delta.toFixed(3)}`)
129
+ chain.push(`fps=${fps}`)
130
+ const args = [
131
+ '-y', '-loglevel', 'error', '-i', file,
132
+ '-vf', chain.join(','), '-fps_mode', 'cfr',
133
+ '-c:v', 'libx264', '-crf', '18', '-pix_fmt', 'yuv420p', '-an',
134
+ ]
135
+ if (delta < -0.05) args.push('-t', expected.toFixed(3))
136
+ args.push(tmp)
137
+ await exec(binaryOf(config, 'ffmpeg'), args, { timeout: 900_000, maxBuffer: 8 * 1024 * 1024 })
138
+
139
+ const after = await probeOutput(config, tmp)
140
+ if (!Number.isFinite(after.durationSec) || Math.abs(after.durationSec - expected) > 0.12) {
141
+ throw new Error(`CFR 修正后时长仍不对:期望 ${expected}s,实际 ${after.durationSec}s(${tmp})`)
142
+ }
143
+ renameSync(tmp, file)
144
+ Object.assign(summary, after)
145
+ return true
146
+ }
147
+
111
148
  /** 归一化后期参数:枚举/上限之外的值一律夹到合法区间,交给 ffmpeg 之前就把脏输入挡掉。 */
112
149
  export function normalizePost(raw) {
113
150
  const p = raw && typeof raw === 'object' ? raw : {}
@@ -135,7 +172,8 @@ async function applyPost(config, input, output, fps, post) {
135
172
  if (chain.length === 0) return
136
173
  await exec(binaryOf(config, 'ffmpeg'), [
137
174
  '-y', '-loglevel', 'error', '-i', input,
138
- '-vf', chain.join(','), '-c:v', 'libx264', '-crf', '19', '-pix_fmt', 'yuv420p',
175
+ '-vf', chain.join(','), '-fps_mode', 'cfr', '-r', String(fps),
176
+ '-c:v', 'libx264', '-crf', '19', '-pix_fmt', 'yuv420p',
139
177
  '-an', output,
140
178
  ], { timeout: 900_000, maxBuffer: 8 * 1024 * 1024 })
141
179
  }
@@ -252,6 +290,10 @@ export async function inspectFrames(config, args) {
252
290
  mode: 'frames',
253
291
  requestedTimes: times,
254
292
  ...parsed,
293
+ // 每格顶部约 28px 是 caption 条,不属于画面;不要据此判断"上沿被裁"。
294
+ storyboardNote: parsed.storyboard.length
295
+ ? '每格顶部是 caption 条,不是画面;怀疑裁切时看单帧或全图。'
296
+ : null,
255
297
  wallSec: Number(((Date.now() - started) / 1000).toFixed(1)),
256
298
  }
257
299
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-motion",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Code-generated math & science animations for DeepSeek Harness: frames-first preview, Manim clips, formula checks.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -38,10 +38,12 @@
38
38
  },
39
39
  "scripts": {
40
40
  "check": "node --check lib/index.js && node --check lib/ops.js && node --check lib/env.js && node --check lib/doctor.js",
41
- "test": "node scripts/test-ops.mjs && node scripts/test-route.mjs",
41
+ "test": "node scripts/test-ops.mjs && node scripts/test-route.mjs && node scripts/test-plan.mjs && node scripts/test-approvals.mjs",
42
42
  "smoke": "node scripts/smoke.mjs",
43
43
  "dev": "node scripts/dev.mjs",
44
44
  "route": "node scripts/route-style.mjs",
45
+ "plan": "node scripts/plan-questions.mjs",
46
+ "approvals": "node scripts/approvals.mjs",
45
47
  "styles:index": "node scripts/build-style-index.mjs",
46
48
  "styles:check": "node scripts/build-style-index.mjs --check",
47
49
  "check:meta": "node scripts/check-meta.mjs",
@@ -5,6 +5,27 @@ description: 做代码动画/科普视频的总入口:先判题材(关系还
5
5
 
6
6
  # 总入口:先分题材,再选画面层
7
7
 
8
+ ## 0. 需求卡点(只问一次)
9
+
10
+ 开工前先推断;能从 brief 看出来的,不要问。需要问时:
11
+
12
+ ```bash
13
+ node scripts/plan-questions.mjs "<brief>" --json
14
+ ```
15
+
16
+ 把返回的 `questions` 原样交给 `ask_user_question`,`assumptions` 作为默认值。规则:
17
+
18
+ - **画幅**:brief 已说"竖屏/横屏/16:9"就跳过;没说才问,只给推断出的 2 个;
19
+ - **交付**:brief 已说"要旁白"或"静音"就跳过;没说才问;
20
+ - **风格**:从 `styles/registry.json` 路由出的 top 2–3;用户点名则跳过;
21
+ - **受众**:只对数学/科学题材、且 brief 没提"高中/大学/大众"时问;选项按平台排(抖音→大众科普,B站→高中);
22
+ - **旁白**:只在交付为旁白、且 brief 没说"中文/英文/双语"时问;选项是"语言 + 声音";
23
+ - **时长**:从 brief 提取("30 秒"/"1 分钟"),不单独问;
24
+ - 最多 4 题,问完不再追问;没回答的按 `assumptions` 执行。**问完不等于开工**,接着进 §4 确认门。
25
+
26
+ 选项是**推出来的**,不是固定清单:画幅看平台线索,交付看有无旁白,风格看 registry 命中,受众看题材与平台。
27
+ 用户也可以直接在 `Other` 里写别的。
28
+
8
29
  ## 1. 判题材
9
30
 
10
31
  | 题材 | 观众记住 | 判据 |
@@ -34,25 +55,59 @@ description: 做代码动画/科普视频的总入口:先判题材(关系还
34
55
 
35
56
  三者都只是素材层;**需要交付时**再统一进 HyperFrames 工程,默认的极简模式不经过 HF。
36
57
 
37
- ## 4. 固定顺序(默认极简,先帧后片)
58
+ ## 4. 确认门(阻塞,分档)
59
+
60
+ 写完整场景、出全片之前,按项目体量选一档,逐项确认。**不要跳步,不要用“我先做起来”代替确认。**
61
+
62
+ | 模式 | 适用 | 阶段 |
63
+ |---|---|---|
64
+ | `light`(默认) | ≤15 s、无旁白、一个结论 | 需求 → **脚本+分镜(合并)** → 视觉 → 开工 |
65
+ | `standard` | 15–60 s,或有旁白 | 需求 → 脚本 → 分镜 → 视觉 → 交付 → 开工 |
66
+ | `full` | >60 s、多段、复杂旁白 | 需求 → 脚本 → 分镜 → **animatic** → 视觉 → 交付 → 开工 |
67
+
68
+ - 脚本:逐句旁白、上屏文字、公式(先过 `formula_check`);
69
+ - 分镜:镜头号、起止秒、画面、运动、字幕/旁白、验收点;
70
+ - 视觉:`DESIGN.md` + 3 张风格帧(真实代码,只出帧);
71
+ - 交付清单:画幅 / fps / 时长 / 文件 / 音频 / 字幕;
72
+ - animatic:带时长的分镜 + 临时旁白,只在 `full` 或用户要求时做。
73
+
74
+ 用账本记录,别靠上下文记忆:
75
+
76
+ ```bash
77
+ node scripts/approvals.mjs init <project> --mode light
78
+ node scripts/approvals.mjs set <project> script_storyboard approved --note "用户确认"
79
+ node scripts/approvals.mjs check <project> # 未全部 approved 时退出 1
80
+ ```
81
+
82
+ - `check` 不通过,不进入执行;
83
+ - `start` 是独立阶段,只有用户**明确**说“可以开始 / 开始做 / 按这个执行 / 开工”才能置为 approved;
84
+ - “好 / 可以 / 行”如果只是回答某个选项,**不算**整体开工;
85
+ - 不确定就问:“脚本、分镜、风格帧都确认了,现在开始执行吗?”
86
+
87
+ 模板与清单见 [references/confirmation.md](references/confirmation.md)。
88
+
89
+ ## 5. 执行顺序(确认后才开始)
38
90
 
39
91
  ```
40
- 1. DESIGN.md(design spec) ← 先定颜色/字体/禁用项/参考
41
- 2. formula_check(数学题材) ← 每条上屏公式先验
42
- 3. 画面 + motion_frames ← 默认:按秒出 3–6 帧,看帧改,不渲染视频
43
- 4. 分镜表(仅用户要时) ← 关键镜头,用真实代码出帧,等确认
44
- 5. motion_render ← 定稿后才编码 mp4
45
- 6. HyperFrames composition ← 字幕/旁白/混音/排版(需要时才进)
46
- 7. hyperframes check / motion_probe ← 只检查已经生成的成片
92
+ 0. 最小场景:只放 6–8 个关键构图,motion_frames 出 sheet(首帧预算 ≤60s)
93
+ 1. 补全场景代码 + formula_check
94
+ 2. motion_frames(3–6 帧,局部改动后)
95
+ 3. 确认过的分镜帧(6–9 帧,若要)
96
+ 4. motion_render(draft → final)
97
+ 5. HyperFrames composition(字幕/旁白/混音;需要时才进)
98
+ 6. hyperframes check / motion_probe(只对最终成片跑一次)
47
99
  ```
48
100
 
49
- **默认不使用 HyperFrames**:只有需要字幕、旁白、混音、复杂图文编排时才进第 6 步。
50
- `motion_frames` 内部是 `manimgx inspect`,只画请求的时刻并做版式检查,几秒出图;
101
+ `motion_frames` 内部是 `manimgx inspect`,只画请求的时刻并做版式检查;
51
102
  `motion_probe` 是给已有 mp4 做体检的,不要用它替代看帧。
52
103
 
53
- ## 5. 四条硬规则
104
+ ## 6. 五条硬规则
54
105
 
55
- 1. **没有 DESIGN.md 或参考语法,不开工**——否则每次都是同一个默认 look;
56
- 2. **默认先帧后片**:先用 `motion_frames` 把画面改对,再决定要不要渲染;
57
- 3. **字幕放在 composition 层**,不烧进 Manim 或 canvas;
58
- 4. **公式先过 `formula_check`**,再进画面。
106
+ 1. **没有明确开工指令,不执行**:没听到“可以开始”就不写完整场景、不出全片;
107
+ 2. **没有 DESIGN.md 或参考语法,不开工**——否则每次都是同一个默认 look;
108
+ 3. **默认先帧后片**:先用 `motion_frames` 把画面改对,再决定要不要渲染;
109
+ 4. **字幕放在 composition 层**,不烧进 Manim 或 canvas;
110
+ 5. **公式先过 `formula_check`**,再进画面;
111
+ 6. **首帧预算 ≤60s**:先最小场景 + 构图 sheet,60s 看不到第一帧就停;
112
+ 7. **只做局部 edit**:改动 <50% 不重写整个文件;
113
+ 8. **图片/上下文预算**:每次迭代 1 张 sheet(约 960px),上下文 ≤100k。
@@ -0,0 +1,82 @@
1
+ # 确认门清单
2
+
3
+ 在写完整场景、出全片之前,逐项确认。每一轮只推进一项;用户改了哪项,就回到哪项重确认。
4
+
5
+ ## 0. 选模式
6
+
7
+ | 模式 | 适用 | 阶段 |
8
+ |---|---|---|
9
+ | `light` | ≤15 s、无旁白、一个结论 | 需求 → 脚本+分镜 → 视觉 → 开工 |
10
+ | `standard` | 15–60 s,或有旁白 | 需求 → 脚本 → 分镜 → 视觉 → 交付 → 开工 |
11
+ | `full` | >60 s、多段、复杂旁白 | 需求 → 脚本 → 分镜 → animatic → 视觉 → 交付 → 开工 |
12
+
13
+ `light` 把“脚本”和“分镜”合成一张表:每个镜头一行,写秒数、画面、上屏文字/公式。
14
+
15
+ ## 1. 需求
16
+
17
+ - [ ] 画幅与像素:16:9 / 9:16 / 1:1
18
+ - [ ] 时长
19
+ - [ ] 交付:先出帧 / 静音 mp4 / 旁白 + 字幕(HF)
20
+ - [ ] 风格(或“你决定”)
21
+ - [ ] 受众(数学/科学题材)
22
+ - [ ] 旁白语言与声音(仅旁白交付)
23
+
24
+ ## 2. 脚本 / 文案
25
+
26
+ | # | 旁白(逐句) | 上屏文字 | 公式 | 备注 |
27
+ |---|---|---|---|---|
28
+ | 1 | | | | |
29
+
30
+ - 公式先过 `formula_check`;
31
+ - 旁白按“一句一个意思”拆,标出预计秒数;
32
+ - 没有旁白就只填上屏文字与公式。
33
+
34
+ ## 3. 分镜
35
+
36
+ | # | 起止秒 | 画面 | 运动 | 字幕 / 旁白 | 验收点 |
37
+ |---|---|---|---|---|---|
38
+ | 1 | 0.0–2.0 | | | | |
39
+
40
+ - 镜头数按内容定,不为了凑时长加镜头;
41
+ - 每个镜头写清“观众看到什么变化”;
42
+ - 结论镜头要留够阅读时间(≥1.2 s)。
43
+
44
+ ## 4. 视觉
45
+
46
+ - [ ] `DESIGN.md`:颜色、字体、禁用项、参考
47
+ - [ ] 3 张风格帧:开场 / 转折 / 结论(真实代码出帧,不渲染视频)
48
+ - [ ] 风格帧通过 `motion_frames` 的 layout 检查
49
+
50
+ ## 5. 交付清单
51
+
52
+ - [ ] 尺寸 / fps / 时长
53
+ - [ ] 文件:mp4 / `.srt` / 场景源码 / DESIGN.md / 分镜表
54
+ - [ ] 音频:无 / 旁白 / 音乐;响度目标
55
+ - [ ] 字幕:烧录 / 外挂 / 两者
56
+
57
+ ## 6. 账本
58
+
59
+ ```bash
60
+ node scripts/approvals.mjs init <project> --mode light
61
+ node scripts/approvals.mjs set <project> <stage> approved --note "用户确认"
62
+ node scripts/approvals.mjs check <project>
63
+ ```
64
+
65
+ `check` 未全部 approved 时退出 1;不能跳过前置阶段;`start` 必须是用户明确授权。
66
+
67
+ ## 7. 开工指令
68
+
69
+ 只有用户**明确**说出下面这类话,才进入执行:
70
+
71
+ - “可以开始”
72
+ - “开始做”
73
+ - “按这个执行”
74
+ - “开工”
75
+
76
+ 以下**不算**整体开工:
77
+
78
+ - 对某个选项说“好 / 可以 / 行”;
79
+ - 确认了脚本或分镜;
80
+ - 说“看着办”但没有明确授权执行。
81
+
82
+ 不确定时问一句:“脚本、分镜、风格帧都确认了,现在开始执行吗?”
@@ -21,7 +21,13 @@ HyperFrames 的 look 文档原话:**不给外部输入,相同的 brief 每
21
21
 
22
22
  ## 先路由
23
23
 
24
- 写 spec 之前先确定风格:`node scripts/route-style.mjs "<题材>"`。点名风格优先;没有点名就按 `styles/registry.json` 打分;`high` 才直接写,`medium`/`low` 先给用户 top 2–3 确认。
24
+ 写 spec 之前先过需求卡点:`node scripts/plan-questions.mjs "<brief>" --json`。它会推断画幅、交付、风格的候选项(选项来自 `styles/registry.json`,不是固定清单),能推断的不问。
25
+
26
+ 风格:点名优先;没有点名就按 `styles/registry.json` 打分;`high` 才直接写,`medium`/`low` 先给用户 top 2–3 确认。
27
+
28
+ ## 确认门
29
+
30
+ `DESIGN.md` 和 3 张风格帧属于确认门的“视觉”阶段:先有确认过的脚本与分镜,再做视觉。按 `gateMode` 分档(`light` 合并脚本+分镜,`standard` 分开,`full` 加 animatic)。用户明确说“可以开始”之前,不要写完整场景、不要渲染成片。流程与账本见 [animation-router](../animation-router/SKILL.md) 的 §4。
25
31
 
26
32
  ## 最小形态
27
33
 
@@ -5,6 +5,15 @@ description: 代码动画的自检清单:环境、公式、画面、音频、
5
5
 
6
6
  # 交付前自检
7
7
 
8
+ ## 0. 迭代预算
9
+
10
+ - 首帧 ≤60 s;总时长 ≤400 s(30 s 片);
11
+ - 输出 tokens ≤30k,上下文 ≤100k;
12
+ - 每次迭代 1 张 contact sheet,读图 ≤10 次;
13
+ - 全文件重写 0 次;`motion_probe` 1 次、`motion_render` 2 次。
14
+
15
+ 详见 [performance-budget.md](../../docs/performance-budget.md)。
16
+
8
17
  ## 1. 环境
9
18
 
10
19
  先跑 `motion_doctor`,`ready.math` 与 `ready.delivery` 都为 true 再继续。
@@ -34,6 +43,8 @@ npx hyperframes check
34
43
  ```
35
44
 
36
45
  - 返回的 storyboard PNG 逐张看过:无叠字、无出画、字号可读;
46
+ - sheet 每格顶部是 caption 条,不是画面;不要把 caption 当成"上沿被裁";
47
+ - 中文文本没有用字符级 `Transform` / `ReplacementTransform`;换词用 FadeOut + FadeIn;
37
48
  - `layoutProblems` 每一条都要处理或明确接受;
38
49
  - 已经生成了 mp4 时,才用 `motion_probe` 抽 contact sheet + 查 `blankFrames`;
39
50
  - 用了 HyperFrames 的项目,用 `npx hyperframes snapshot <project> --at 2.9,10.4` 抽帧,不要先 render 再截图。
@@ -53,3 +64,5 @@ npx hyperframes check
53
64
  ## 7. 交付
54
65
 
55
66
  mp4 + `.srt` + 场景源码 + `DESIGN.md` + 分镜表。
67
+
68
+ 开工前跑过 `node scripts/approvals.mjs check <project>`;确保 `APPROVALS.json` 里 `start` 是用户明确授权,而不是 agent 自己填的。
Binary file
@@ -13,6 +13,9 @@ import manimgx as m
13
13
 
14
14
  TOKENS = json.loads(Path(__file__).with_name("tokens.json").read_text(encoding="utf-8"))
15
15
 
16
+ # inspect 在 import 时读 config.frame_rate;必须放在模块层,不能只在 construct 里。
17
+ m.config.frame_rate = int(TOKENS["motion"]["fps"])
18
+
16
19
  BG = TOKENS["colors"]["bg"]
17
20
  TEXT = TOKENS["colors"]["text"]
18
21
  LABEL = TOKENS["colors"]["label"]
package/styles/README.md CHANGED
@@ -17,6 +17,8 @@
17
17
  - 每个风格目录:`style.json`(元数据)+ `STYLE.md`(不变量)+ `tokens.json` + `theme.py` + `preview.jpg`;
18
18
  - `STYLE.md` 是**改编**,必须在上游基础上写清楚差异;不复制 demo 代码;
19
19
  - 颜色、字体、时间表、音频以 `tokens.json` 为准;版面坐标与组件配方写在 `STYLE.md`;
20
+ - 中文文本不做字符级 `ReplacementTransform` / `Transform`(会出字形碎片);用 Fade/Write/Indicate,换词用 FadeOut+FadeIn;
21
+ - `theme.py` 必须在**模块 import 时**设置 `m.config.frame_rate`(不能只在 `construct` 里),否则 `manimgx inspect` 会按默认 60fps 取帧,30fps 场景的后半段会报 no frame;
20
22
  - 路由:`styles/registry.json` 是元数据,`node scripts/route-style.mjs "<题材>"` 打分;点名优先,低置信度/并列先问用户;
21
23
  - 新片在自己的项目目录写 `DESIGN.md`,头部 `style: <slug>` + `style_version: 1.0.0`;
22
24
  - 参考与示例统一用 HyperFrames 的 [examples](https://hyperframes.heygen.com/examples) / [catalog](https://hyperframes.heygen.com/catalog),不在仓库里放样片(`showcase/` 只放可复现的演示)。
@@ -15,6 +15,9 @@ import manimgx as m
15
15
 
16
16
  TOKENS = json.loads(Path(__file__).with_name("tokens.json").read_text(encoding="utf-8"))
17
17
 
18
+ # inspect 在 import 时读 config.frame_rate;必须放在模块层,不能只在 construct 里。
19
+ m.config.frame_rate = int(TOKENS["motion"]["fps"])
20
+
18
21
  TABLE = TOKENS["colors"]["table"]
19
22
  PAPER = TOKENS["colors"]["paper"]
20
23
  PAPER_LIGHT = TOKENS["colors"]["paper_light"]
@@ -29,6 +29,7 @@
29
29
  "趋势",
30
30
  "比较",
31
31
  "分布",
32
+ "导数",
32
33
  "斜率",
33
34
  "切线",
34
35
  "割线",
@@ -13,6 +13,9 @@ import manimgx as m
13
13
 
14
14
  TOKENS = json.loads(Path(__file__).with_name("tokens.json").read_text(encoding="utf-8"))
15
15
 
16
+ # inspect 在 import 时读 config.frame_rate;必须放在模块层,不能只在 construct 里。
17
+ m.config.frame_rate = int(TOKENS["motion"]["fps"])
18
+
16
19
  PAPER = TOKENS["colors"]["paper"]
17
20
  PAPER_SHADOW = TOKENS["colors"]["paper_shadow"]
18
21
  INK = TOKENS["colors"]["ink"]
@@ -67,6 +67,7 @@
67
67
  "趋势",
68
68
  "比较",
69
69
  "分布",
70
+ "导数",
70
71
  "斜率",
71
72
  "切线",
72
73
  "割线",
@@ -13,6 +13,9 @@ import manimgx as m
13
13
 
14
14
  TOKENS = json.loads(Path(__file__).with_name("tokens.json").read_text(encoding="utf-8"))
15
15
 
16
+ # inspect 在 import 时读 config.frame_rate;必须放在模块层,不能只在 construct 里。
17
+ m.config.frame_rate = int(TOKENS["motion"]["fps"])
18
+
16
19
  GROUND = TOKENS["colors"]["ground"]
17
20
  PAPER = TOKENS["colors"]["paper"]
18
21
  INK = TOKENS["colors"]["ink"]
@@ -15,6 +15,9 @@ import manimgx as m
15
15
 
16
16
  TOKENS = json.loads(Path(__file__).with_name("tokens.json").read_text(encoding="utf-8"))
17
17
 
18
+ # inspect 在 import 时读 config.frame_rate;必须放在模块层,不能只在 construct 里。
19
+ m.config.frame_rate = int(TOKENS["motion"]["fps"])
20
+
18
21
  BOARD = TOKENS["colors"]["board"]
19
22
  BOARD_EDGE = TOKENS["colors"]["board_edge"]
20
23
  INK = TOKENS["colors"]["ink"]