@dsh-gis/dsh-video-studio-skill 0.4.0 → 0.7.2

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.
Files changed (38) hide show
  1. package/README.md +93 -18
  2. package/docs/styles/demo-results.json +76 -0
  3. package/docs/styles/sheet-8-styles.png +0 -0
  4. package/docs/styles/sheet-ai.png +0 -0
  5. package/docs/styles/sheet-craft.png +0 -0
  6. package/docs/styles/sheet-night.png +0 -0
  7. package/index.js +20 -7
  8. package/package.json +13 -3
  9. package/schemas/film.schema.json +599 -0
  10. package/skills/video-studio/SKILL.md +252 -28
  11. package/skills/video-studio/assets/film.json +78 -24
  12. package/skills/video-studio/assets/scenes.py +1005 -42
  13. package/skills/video-studio/assets/web/engine.js +18 -1
  14. package/skills/video-studio/assets/web/film.js +29 -4
  15. package/skills/video-studio/assets/web/index.html +5 -0
  16. package/skills/video-studio/references/audio-variation.md +158 -0
  17. package/skills/video-studio/references/audio.md +96 -6
  18. package/skills/video-studio/references/change-impact.md +54 -0
  19. package/skills/video-studio/references/quality-gates.md +1 -1
  20. package/skills/video-studio/references/style-packs.md +178 -0
  21. package/skills/video-studio/references/web-engine.md +1 -1
  22. package/skills/video-studio/requirements.txt +13 -1
  23. package/skills/video-studio/scripts/filmcheck.py +725 -0
  24. package/skills/video-studio/scripts/filmkit.py +953 -76
  25. package/skills/video-studio/scripts/gates.py +509 -0
  26. package/skills/video-studio/scripts/qa_frames.py +109 -19
  27. package/skills/video-studio/scripts/render.py +942 -45
  28. package/skills/video-studio/scripts/scorekit.py +1887 -27
  29. package/skills/video-studio/scripts/selfcheck.py +648 -2
  30. package/skills/video-studio/scripts/sfxkit.py +132 -30
  31. package/skills/video-studio/scripts/stagewatch.py +405 -0
  32. package/skills/video-studio/scripts/stylekit.py +1012 -0
  33. package/skills/video-studio/scripts/voicekit.py +880 -0
  34. package/skills/video-studio/scripts/webengine.py +68 -14
  35. package/tools/audio_similarity.py +279 -0
  36. package/tools/ci_check.py +450 -0
  37. package/tools/estimate.py +657 -0
  38. package/tools/selftest_noscipy.py +70 -0
@@ -26,21 +26,33 @@ description: 用代码从零生成中文视频(科普片/纪录片/宣传片/
26
26
  > 下文的 `<SKILL_DIR>` 指**本次加载技能时 `skill` 工具回报的 `resourceBase.path`**。装成插件后它就是
27
27
  > 本插件包内的 `skills/video-studio/`(profile 的 `node_modules/@dsh-gis/dsh-video-studio-skill` 是链接)。
28
28
  > 找不到时按名字定位:`find ~ -path '*/skills/video-studio/scripts/filmkit.py'`。
29
+ >
30
+ > `<REPO>` 指**包根**(仓库根,或装好后 profile 里的 `node_modules/@dsh-gis/dsh-video-studio-skill/`),
31
+ > 即 `<SKILL_DIR>` 往上两级。**`tools/` 与 `schemas/` 在 `<REPO>` 下,不在 `<SKILL_DIR>` 下**:
32
+ > 本节的 `cp <SKILL_DIR>/scripts/...` 把技能拷成 `myfilm/` 之后,那个工作目录里**没有** `tools/`,
33
+ > 所以 `tools/estimate.py` 这类仓库工具要按 `<REPO>/tools/...` 调用(或从包/仓库里取一份过来)。
29
34
 
30
35
  ```bash
31
36
  # 1) 建工作目录,拷模板
32
37
  mkdir -p myfilm && cd myfilm
33
38
  cp <SKILL_DIR>/assets/film.json . # 剧本模板(改文字即可)
34
39
  cp <SKILL_DIR>/assets/scenes.py . # 场景模板(改画面即可)
35
- cp <SKILL_DIR>/scripts/{filmkit.py,render.py,selfcheck.py} .
40
+ # v0.6.0 起 gates.py(门禁账本)与 stagewatch.py(阶段新鲜度)是**必需**的:
41
+ # render.py 直接 import 它们,少一个就是 ModuleNotFoundError。
42
+ cp <SKILL_DIR>/scripts/{filmkit.py,stylekit.py,render.py,selfcheck.py,gates.py,stagewatch.py} .
36
43
  # 可选(v0.3):要程序化音效/配乐就一并拷;要走浏览器渲染再拷 webengine.py + assets/web/
37
- cp <SKILL_DIR>/scripts/{sfxkit.py,scorekit.py,qa_frames.py} .
44
+ cp <SKILL_DIR>/scripts/{sfxkit.py,scorekit.py,voicekit.py,qa_frames.py} .
38
45
  cp -r <SKILL_DIR>/assets/web ./web
39
46
 
40
- # 2) 先跑一次自检:24 条回归护栏,20 秒内出结论(不联网;可选模块缺失会标 SKIP)
47
+ # 2) 先跑一次自检:43 条回归护栏,20 秒内出结论(不联网;可选模块缺失会标 SKIP)
41
48
  python selfcheck.py
42
49
 
43
- # 3) 改 film.json 的 lines[].text / sub / big / items
50
+ # 3) 【v0.5 新步骤】先确认风格,再改文字
51
+ # film.json 的 "style": "auto" 会按内容自动选一套(画风+配乐+旁白一起变);
52
+ # 想锁定就写死风格 id。这条命令打印"为什么选这套",并落盘 work/style.json。
53
+ python render.py --script film.json --stage style
54
+
55
+ # 3b) 改 film.json 的 lines[].text / sub / big / items
44
56
  # text = 朗读文本(自然口语);sub = 上屏字幕(短,一行优先)
45
57
 
46
58
  # 4) 母版预览(P6):先只渲几张关键帧看构图/配色,别一上来渲全片
@@ -49,17 +61,34 @@ python selfcheck.py
49
61
  python render.py --script film.json --scenes scenes.py --work work --stage stills \
50
62
  --stills "l01:3,l02:5,l03:2.5"
51
63
 
64
+ # 4.5) 【v0.6】先算成本再开渲:帧数 / 各阶段耗时 / 磁盘 / 分块建议
65
+ # 项目原则是"成本敏感",而在此之前开渲前拿不到任何数字。
66
+ # (--json 可机读;有 work/timeline.json 时用真实时长,否则按字数粗估并标注)
67
+ # 注意:estimate.py 是**仓库/包里的工具**(<REPO>/tools/),不在 skills/video-studio/ 里,
68
+ # quickstart 拷出来的 myfilm/ 也不会有它;想用就按包根路径调用,或从包/仓库里复制过来。
69
+ python <REPO>/tools/estimate.py --script film.json --jobs 4
70
+
52
71
  # 5) 一条命令出片(TTS→排版→混音→渲染→合成→质检)
53
72
  python render.py --script film.json --scenes scenes.py --out out/film.mp4 --work work
54
73
 
55
74
  # 6) 亲眼看质检抽出的帧
56
75
  # work/qa/sheet.png 联系表(全片调性)
57
76
  # work/qa/snap_*.png 全分辨率静帧
77
+
78
+ # 7) 【v0.6 新步骤】看机器可读的交付结论,然后签核
79
+ # out/QC-REPORT.md 是**交付凭据**:gate matrix 分 pass / fail / skipped 三档,
80
+ # skipped 表示"本次没执行",不等于通过。以前这些只活在 stdout 里,会话一断就没了。
81
+ cat out/QC-REPORT.md
82
+ python render.py --script film.json --work work --stage accept --gate stills # 看过静帧了
83
+ python render.py --script film.json --work work --stage accept --gate release # 成片验收过了
84
+ # 签核绑内容指纹:改了剧本 / scenes.py / 引擎,旧签核自动失效,要重新看、重新签。
85
+ # 交付前想强制:加 --strict-gates(skipped 也算失败)。
58
86
  ```
59
87
 
60
88
  分阶段跑(**每个 stage 只做自己的事**,不会再顺手重渲全片):
61
89
 
62
90
  ```bash
91
+ python render.py ... --stage style # 只解析风格 -> work/style.json(画风/配乐/旁白/节奏一次定死)
63
92
  python render.py ... --stage tts # 只合成旁白 + 实测时长(带输入校验的缓存)
64
93
  python render.py ... --stage layout # 只排时间轴 -> timeline.json / sub.srt / sub.ass(含 hold 停留窗口)
65
94
  python render.py ... --stage cues # 只出提示点 -> work/cues.json(声音跟着画面走)
@@ -70,9 +99,11 @@ python render.py ... --stage frames # 只渲染帧 -> silent.mp4(--engine pi
70
99
  python render.py ... --stage mux # 只合成(视频流 copy,两遍 loudnorm)
71
100
  python render.py ... --stage qa # 只质检(只读输入,只写 work/qa/)+ 帧级深检
72
101
  python render.py ... --stage stills # 只渲母版静帧 + 联系表
102
+ python render.py ... --stage accept --gate stills # 【v0.6】人工签核一道闸(写 work/signoff.json)
103
+ python render.py ... --stage explain # 【v0.6】只报"哪些阶段过时了、为什么、该跑什么",不跑任何 stage
73
104
  ```
74
105
 
75
- `--stage all` 的完整顺序:`tts → layout → cues → score → sfx → audio → frames → mux → qa`。
106
+ `--stage all` 的完整顺序:`style → tts → layout → cues → score → sfx → audio → frames → mux → qa`。
76
107
  **任何一步出问题都会在该步停下**,不会带着坏输入继续往下跑。
77
108
 
78
109
  长片渲染(分块并行 + 断点续跑):
@@ -90,12 +121,18 @@ python render.py ... --stage frames --jobs 4 --resume # 中断后只补缺块
90
121
  > 里存着计划指纹),并提示你 `rm -rf work/segments`——这是刻意的:旧分块配新时间轴会拼出
91
122
  > "画面和旁白对不上"的片子,而且时长门禁查不出来。
92
123
 
124
+ > **v0.6.0 修了一个已发布版本里的真 bug**:`--jobs >1` 时子进程从 `_script_snapshot.json` 读剧本,
125
+ > JSON 把主题色的 `(r,g,b)` 元组变成 `list`,而 `scenes._bg()` 拿它当缓存键 → `TypeError: unhashable type: "list"`。
126
+ > 也就是说 **"长片并行渲染"这条 SKILL 自己教的路径此前是崩的**。现在子进程会按剧本重解风格(拿回元组),
127
+ > `scenes.py` 的 `_bg` 键也做了冻结兜底;`selfcheck` 第 32 条专门盯这个。
128
+
93
129
  ## 2. 标准八阶段(P0–P7)
94
130
 
95
131
  | 阶段 | 产出 | 关键动作 | 常见错误 |
96
132
  |---|---|---|---|
133
+ | **P0.5 定风格** | `work/style.json` | `--stage style`:按内容选风格包,打印"为什么选这套" | 跳过 → 画风/配乐/旁白全片一个样(v0.4 的通病) |
97
134
  | **P0 定基准** | `CONVENTIONS.md` | 分辨率/帧率/时长/BPM→帧换算一次算死,全项目引用同一份 | 各文件自己写 `150` 这种魔数 → 幕边界对不齐 |
98
- | **P1 写剧本** | `film.json` | 每句拆 `text`(朗读)与 `sub`(字幕);每段一个 `kind` | 一个字段两头用 → 字幕太长或读起来生硬 |
135
+ | **P1 写剧本** | `film.json` | 每句拆 `text`(朗读)与 `sub`(字幕);每段一个 `kind`;定 `style`(auto 或风格 id) | 一个字段两头用 → 字幕太长或读起来生硬;不写 style → 每部片长一个样 |
99
136
  | **P2 事实核查** | `FACTS.md` | 上屏的每个数字/年份有出处编号;建"不能上屏"黑名单 | 跳过这步 → 画面和旁白数字打架,评论区第一条就是你错了 |
100
137
  | **P3 TTS** | `work/tts/*.mp3` + `tts.json` | edge-tts 逐句合成,`ffmpeg -f f32le` 实测时长,磁盘缓存 | 按字数估时长 → 字幕漂移 |
101
138
  | **P4 排时间轴** | `work/timeline.json` + `sub.srt` | `dur = lead + 实测语音 + tail`,句读必须在本段内 | 段太短 → 旁白被下一段截断 |
@@ -116,6 +153,10 @@ python render.py ... --stage frames --jobs 4 --resume # 中断后只补缺块
116
153
  > 上面两行是**本机空载**实测口径(换机、重负载画面都会变;详见 §12)。别把区间当保证。
117
154
  | 纯数据动画 | **manim**(若已安装) | 视场景而定 | 数学/公式推导 |
118
155
 
156
+ > **v0.5 注意**:风格包的主题化目前只覆盖 **PIL 引擎**(`engine: pil`,默认)。
157
+ > `--engine web` 的页面(`assets/web/`)仍是 v0.4 的深蓝橙配色,不跟随 `style`;
158
+ > 用 web 引擎时画风不会随内容变化,但配乐与旁白**照样**跟随风格包(它们不依赖渲染引擎)。
159
+
119
160
  **默认走 PIL**:依赖少(numpy+Pillow 就够)、启动快、确定性强、调试成本低。
120
161
  需要"一镜到底的粒子形变 / 实时着色器质感"才上浏览器方案——**v0.3 起它是真的可用了**,不再是"文档里说有":
121
162
 
@@ -172,10 +213,16 @@ python webengine.py textqa ./web/index.html # 文字重叠/出画/一
172
213
  | 配乐 | `scorekit.py`(程序化) | `work/music.wav` | 和弦表 + 律动层 + 分段能量;段落边界对齐小节线 |
173
214
  | 音效 | `sfxkit.py`(物理建模) | `work/sfx.wav` | 按 `cues.json` 里登记的提示点逐条合成 |
174
215
 
175
- 三条总线在 `--stage audio` 里合成:**音效响起时配乐自动让位**(`duck_db`,默认 3 dB),
216
+ 三条总线在 `--stage audio` 里合成:**音效响起时配乐自动让位**(`duck_db`,v0.7 默认 4 dB,
217
+ cinema/epic 5.5 dB;旧版 3 dB 在段落过门时配乐几乎与音效等高),
176
218
  最后交给 ffmpeg 两遍 loudnorm 到 -16 LUFS / 真峰 ≤ -1 dBTP。**不要**自己在 numpy 里做最终响度标准化——
177
219
  你有 `loudnorm`,它比手写归一化准得多,而且真峰才是门禁。
178
220
 
221
+ > **v0.7 片内分离**:配乐在段落交界本来就有过门(scorekit 的 swell/riser/sub_boom),
222
+ > sfx 轨不再在同一个边界重复放 whoosh/riser——`--stage sfx` 会对落在
223
+ > [边界 - 2.2 s, 边界] 内的转场类 cue 自动 -3 dB(`sfx.duck_transitions=false` 可关)。
224
+ > 提示音的音高现在跟随配乐调性(`in_key(freq, key)`),不再硬编码 D 小调五声。
225
+
179
226
  ### 5.2 提示点:让声音跟着画面走
180
227
 
181
228
  **这是 v0.3 最重要的一个概念。** 画面在该响一下的地方登记一个提示点,音频阶段按提示点合成音效:
@@ -207,9 +254,14 @@ python render.py ... --engine web --stage cues # 自动导出 work/cues.json
207
254
 
208
255
  ### 5.3 配乐(scorekit)
209
256
 
257
+ > **v0.5 起先看 §5.5。** 配乐的"身份"(调式/调性/BPM/进行/音色/律动)由风格包决定,
258
+ > 你通常只需要在 film.json 写 `"style": "auto"` 或一个风格 id,不用手调下面这些参数。
259
+
210
260
  ```json
211
- "bpm": 120,
212
- "music": {"enabled": true, "mode": "score", "key": "Dm", "gain_db": -1.5, "duck_db": 3.0,
261
+ "bpm": "auto",
262
+ "music": {"enabled": true, "mode": "score", "style": "auto", "mood": "auto",
263
+ "key": "auto", "seed": 7, "seed_from_content": true,
264
+ "gain_db": -1.5, "duck_db": 3.0,
213
265
  "sections": [{"at": "l01", "name": "intro", "energy": 0.3},
214
266
  {"at": "l03", "name": "drive", "energy": 0.8}]}
215
267
  ```
@@ -251,24 +303,114 @@ python -c "import sfxkit; print(sfxkit.render('whoosh', dur=1.2).shape)"
251
303
  它的定位是**环境声**:基准电平要比同刻配乐低 10 dB 以上。
252
304
  3. **音量一律在成片混音里量**,不要按配方内部音量推断——混音时每个音效会先按峰值归一,配方里的"响"是没用的。
253
305
 
254
- ### 5.5 混音链(照着做,别自创)
306
+ ### 5.5 风格包:画风 + 配乐 + 旁白一起变(v0.5 新增)
307
+
308
+ **这是 v0.5 最重要的一节。** v0.4 及以前的通病是"内容换了、气质没换":
309
+ 画风锁死在 scenes.py 的深蓝橙、配乐锁死在 Dm/120、旁白锁死在同一个 voice。
310
+ 用户的原话是「背景音乐风格都差不多、旁白声音都差不多、整体画风也差不多」。
311
+
312
+ v0.5 引入 **风格包**:一个 `style` id **同时**决定画风、配乐、旁白声线和节奏。
313
+
314
+ ```json
315
+ {"style": "auto"}
316
+ ```
317
+
318
+ - `"auto"` = 按标题/章节/正文关键词自动选;零命中时按内容指纹稳定落桶
319
+ (同一份稿子永远同一个风格,不同稿子大概率不同)。
320
+ - 写死 id 就锁定:`tech-deep` / `magazine` / `paper` / `vivid` / `mono` / `cinema` / `lo-fi` / `epic`。
321
+ - **不写 `style` 字段 = `legacy`**:画风/配乐/旁白全部等于 v0.4,配乐逐样本一致(老项目无痛)。
322
+
323
+ 查表与环境:
255
324
 
325
+ ```bash
326
+ python stylekit.py --list # 8 套风格(含画风/配乐/旁白/节奏与对比度)
327
+ python stylekit.py --show cinema # 单套风格的完整字段
328
+ python render.py ... --stage style # 用某份 film.json 解析风格并打印"为什么选这套"
256
329
  ```
257
- 旁白(单声道, 居中) ─┐
258
- 配乐(立体声) ───────┼─> duck(音效包络, -3dB) ─> 求和 ─> 峰值保护(0.89) ─> ffmpeg loudnorm 两遍 ─> -16 LUFS / TP<=-1
259
- 音效(立体声) ───────┘
330
+
331
+ | 风格 | 画风 | 配乐 | 旁白 |
332
+ |---|---|---|---|
333
+ | tech-deep | 深蓝渐变 + 橙 | 小调 100–124 / 琶音 + 铃声 | 男声叙述 + 青年开场 |
334
+ | magazine | 米白 + 朱红,衬线 | 大调 88–100 / 电钢 | 女声叙述 + 权威引用 |
335
+ | paper | 牛皮纸 + 靛青,楷体 | 大调 84–92 / 马林巴 | 软糯女声 + 男声引用 |
336
+ | vivid | 深紫 + 柠檬黄,大字 | 大调 120–132 / 短促和弦 | 活泼女声开场 |
337
+ | mono | 纯白 + 黑,超宽字距 | 极简 60–68 / 钢片琴 | 沉稳男声 + 女声引用 |
338
+ | cinema | 近黑 + 青橙,扫描线 | 小调 76–84 / 弦乐铺底 | 纪录片男声 |
339
+ | lo-fi | 暖灰 + 薄荷 | 大调 72–84 / 电钢 + 轻鼓 | 温柔女声 |
340
+ | epic | 近黑 + 金,衬线 | 小调 88–96 / 铜管 + 人声铺 | 有力男声 |
341
+
342
+ **片内也要有呼吸**(不是整片一个死样子):
343
+
344
+ - 版式按章节轮换(同一章内不变):left-stack / center-stack / split-right;
345
+ - 镜头缓推、入场位移、条目落位步长都由主题决定(`item_stagger` 与 `cues()` 共用同一个值,音效不会错位);
346
+ - 旁白按 kind 换语气档(hook/body/quote/outro/emphasis/calm),引号句会自动换声线;
347
+ - 配乐逐小节有能量弧线、配器渐入渐出、主奏轮换。
348
+
349
+ **三条硬约束**(违反会让片子"花"或者被门禁拦):
350
+
351
+ 1. 同一部片**不许跨明暗族**换主题(深蓝段直接切暖白段会精确触发闪烁门禁,观众也看着像换片)。
352
+ 2. 主题必须过 `stylekit.theme_lint` 的对比度体检(ink≥7 / muted≥4.5 / dim≥3 / accent≥4.5)。
353
+ 3. 旁白全片最多 2–3 把声音(`voice.max_distinct`),相邻句不换人——否则像广播剧而不是旁白。
354
+
355
+ 字段、指纹规则、加第 9 套风格的步骤见 **references/style-packs.md**。
356
+
357
+ ### 5.6 混音链(照着做,别自创)
358
+
359
+ ```
360
+ 旁白(单声道, 居中) ─────────────────────────────┐
361
+ 配乐(立体声, -12dB 基线) ─> 音效duck(-4dB) ─> 旁白duck(-6dB) ─┤─> 求和 ─> 峰值保护(0.89) ─> loudnorm 两遍 ─> -16 LUFS / TP<=-1
362
+ 音效(立体声) ───────────────────> 旁白duck(-3dB) ─────────────┘
260
363
  ```
261
364
 
262
- - **旁白优先**(听清台词是第一优先):`mix_film` 里旁白固定 `voice_gain=1.0` 居中,配乐默认
263
- `music.gain_db=-1.5`、音效按 `CUE_BASE_DB` 逐条定电平。**"高 12 dB"是人耳经验值,代码不测量也不保证**——
264
- 交付前用 `filmkit.loudness` 或回听确认旁白压得住,别把它当门禁。
265
- - 音效响时配乐自动让位 3 dB(`mix_film(duck_db=3.0)`;注意 `duck()` 函数自身默认是 4.0,
266
- `mix_film` 传的是 3.0——看源码时别被两个默认值绕住)。让得太多会"音乐一抖一抖"。
365
+ - **旁白优先是硬行为(v0.7.1)**,不是文档口号:`mix_film` 里旁白固定 `voice_gain=1.0` 居中、不被任何侧链影响;
366
+ 配乐基线 `music.gain_db=-12`(旧版 -1.5,导致背景与台词几乎等高);有人声的地方再按旁白包络压
367
+ 配乐 6 dB、音效 3 dB(`music.voice_duck_db` / `music.voice_duck_sfx_db`,间隙自动恢复)。
368
+ 实测人声段"旁白/背景"响度差 **12.9 dB**(真实配乐素材下;合成信号口径 15 dB),
369
+ selfcheck 第 35/36 条与 `audio_similarity.py --strict` 的 D 组守着这条。
370
+ - **SNR 兜底(v0.7.1 最后一道闸)**:混音后按台词活跃区实测"人声/背景",不足 12 dB 就整体再压背景
371
+ (`music.guard_snr_db`,最多 `guard_cap_db=30`;补压 >18 dB 会提示检查素材电平)。
372
+ 这是唯一一条"混音后自己改电平"的门禁,保证换任何配乐素材都不会把旁白盖住。
373
+ - **BGM 与音效是可选层**:它们的作用是铺情绪与打重音,不该抢字。想更安静就调低 `music.gain_db`
374
+ 或设 `music.enabled=false`;"让观众听清旁白"永远优先于"背景好听"。
375
+ - 音效响时配乐自动让位:v0.7 起风格表默认 4.0 dB(mono 3.0 / lo-fi·magazine 3.5 /
376
+ cinema·epic 5.5),`film.json` 的 `music.duck_db` 显式值永远优先。旧版 `mix_film` 默认 3.0 dB,
377
+ 在段落过门时配乐几乎与音效等高——这是"配乐和音效听着像同一套"的混音侧成因,已由 `duck_default` 抬高。
378
+ 让得太多会"音乐一抖一抖",所以按风格分级而不是一律压狠。
267
379
  - **本项目不做母带压缩**:链路是 求和 → duck → 峰值保护 0.89 → `ffmpeg loudnorm` 两遍。
268
380
  代码里**没有压缩器**(想加请自行在 `mix_film` 之后接),所以也不存在"压太狠"的问题;
269
381
  要让重击前后有能量差,靠的是配乐分段 energy 与 `suck()`,不是压缩。
270
382
  - 成片音轨必须 48 kHz(`mux` 已显式 `-ar 48000`,selfcheck 第 12 条守着)。
271
383
 
384
+ ### 5.7 声音的片间差异与片内分离(v0.7 新增)
385
+
386
+ > v0.5 解决的是"配乐风格都差不多",但**同一风格下每部片的配乐仍像同一首**——
387
+ > 内容指纹只进了 seed(改音色细节),和弦进行/动机/鼓型全固定。
388
+ > 实测:同风格、同 bpm、同调、只换内容指纹,两段配乐相关性 **0.966**。
389
+
390
+ v0.7 把指纹推进到**结构层**(详见 **references/audio-variation.md**):
391
+
392
+ | 层 | v0.6 | v0.7 |
393
+ |---|---|---|
394
+ | 配乐和声 | 风格固定进行 | 指纹决定**每两小节一次的稳定和弦替换**(不影响乐句末与片尾规则) |
395
+ | 配乐进行 | 风格固定一套进行 | 指纹在风格自带的 calm/body/drive 三档里选一档(同风格素材,只换功能顺序) |
396
+ | 配乐动机 | 风格固定动机 | 指纹对级数做 **±1 稳定偏移**(节奏不变,问句/答句独立派生;跨主音带八度修正) |
397
+ | 配乐鼓型 | 固定落点 | 指纹加 ghost kick(不与现有落点重复),clap 位置 ±0.5 拍 |
398
+ | 出片接线 | 指纹只进 seed | `render.py` 的 `st_score` 把 `fingerprint.full` 传进 `scorekit.render()`;legacy 不传 |
399
+ | 音效 seed | `seed_base + i`(只跟 cue 顺序) | `content_seed(seed_base, style, content_fp, type, t)`(跟内容走) |
400
+ | 提示音音高 | 硬编码 D 小调五声 | `in_key(freq, key)` 跟随配乐调性 |
401
+ | 段落边界 | 配乐过门 + sfx 转场各响各的 | 边界前 2.2 s 内的 whoosh/riser **自动 -3 dB**(不与过门撞车) |
402
+ | duck | 3 dB 一刀切 | 按风格 3.0–5.5 dB,配乐让得更彻底 |
403
+ | 旁白优先(v0.7.1) | 配乐 -1.5 dB、无语音侧链 | 配乐 -12 dB 基线 + 人声处再压 6 dB(音效 3 dB),人声段 SNR 实测 15 dB |
404
+
405
+ **向后兼容**:不写 `style` 字段(legacy)或 `sfx.content_seed: false` 时,音效走旧的
406
+ `seed_base + i` 路径,老片逐样本不变(selfcheck 第 33 条守着这条)。
407
+
408
+ 验收工具(改音频相关代码后必须跑):
409
+
410
+ ```bash
411
+ python <REPO>/tools/audio_similarity.py --strict # A/B/C 三组阈值,失败 exit 1(在包根执行)
412
+ ```
413
+
272
414
  ## 6. 停留窗口:让观众看清楚,而不是整片放慢(v0.3.0)
273
415
 
274
416
  > 来自同一部成片的复盘:"部分画面没看清就被切走了"。**解决办法不是把整片放慢,而是按内容加停留。**
@@ -293,10 +435,60 @@ python -c "import sfxkit; print(sfxkit.render('whoosh', dur=1.2).shape)"
293
435
 
294
436
  ```bash
295
437
  python render.py ... --stage qa # 只读质检:分辨率/音轨/时长/响度/真峰/锐度比/黑帧/字幕条数 + 帧级深检
296
- python selfcheck.py # 技能自身回归(24 条曾真的坏过的点)
438
+ python selfcheck.py # 技能自身回归(43 条曾真的坏过的点)
297
439
  python qa_frames.py out/film.mp4 --cues work/cues.json # 单独跑帧级深检,出 JSON + 图
440
+ # v0.6:qa / all 收尾会自动写 out/QC-REPORT.md(机器可读交付结论)
441
+ # 并落 work/gates.json(三档 gate matrix)与 work/cost.jsonl(实测耗时台账)
298
442
  ```
299
443
 
444
+ ### 7.0 交付凭据:`out/QC-REPORT.md` 与人工签核(v0.6.0)
445
+
446
+ > 这一节解决的是"**质检结论只活在控制台里**"——会话一断,"我查过了"就无从复现。
447
+
448
+ **三档状态**(这是最关键的语义,不许混):
449
+
450
+ | 状态 | 含义 | 退出码 |
451
+ |---|---|---|
452
+ | `pass` | 本次**真的查了**且通过 | 0 |
453
+ | `fail` | 查了没过 | 非 0(除非 `--no-gate` 绕过,且会留痕) |
454
+ | `skipped` | 本次**没有执行**(没装 cv2 / 没写 visual_at / 没签核) | 默认 0,`--strict-gates` 下非 0 |
455
+
456
+ **"skipped ≠ 通过"** 是这套机制的全部意义:以前"没查"和"查了没事"在输出上都叫"没报错"。
457
+
458
+ ```bash
459
+ cat out/QC-REPORT.md # gate matrix + 实测值 + 签核 + 耗时,交付时一起给
460
+ python render.py ... --stage qa --strict-gates # 交付前:skipped 也算失败
461
+ python render.py ... --stage accept --gate stills --note "看过联系表" # 人工签核
462
+ ```
463
+
464
+ **四道人工签核闸**(`style` 风格与声线 / `stills` 母版静帧 / `facts` 上屏数字 / `release` 成片验收):
465
+
466
+ - 签核前会检查"证据产物存在"(如 `stills` 要求 `work/qa/sheet.png`),**没有证据不许签**——
467
+ 否则签核就是橡皮图章。
468
+ - 签核绑**内容指纹**(剧本 + `scenes.py` + 引擎)。改了其中任何一个,旧签核**自动失效**,
469
+ 报告里会标成 `skipped` 并写明"内容已变"。这不是麻烦,是防止"我签过了"变成空头支票。
470
+ - 逃生阀是 `--no-gate`(整体跳过签核检查),但会写进 QC 报告的"本次绕过的门禁"一节,
471
+ **留痕不等于通过**。
472
+
473
+ **耗时台账** `work/cost.jsonl`:每个 stage 一行实测墙钟(`--cost` 开启,`--stage all` 里自动记)。
474
+ 它是 SKILL §12 那张"排期公式"的对照——公式从来没被验证过,现在有数据了。
475
+
476
+ ### 7.0b 阶段新鲜度:别让旧产物混进新片(v0.6.0)
477
+
478
+ > 与 §1 的分块指纹同源:那里只修了 `frames` 一个点,其余 stage 此前是裸奔的
479
+ > (改了 `style` 后 `--stage audio` 仍读旧 `music.wav`;改了 `hold` 后 `--stage frames` 仍用旧 `cues.json`,**都不报错**)。
480
+
481
+ ```bash
482
+ python render.py ... --stage explain # 哪些阶段过时了、为什么、最小重跑命令
483
+ python render.py ... --stage qa --strict-stale # 有过时就失败(CI / 交付前)
484
+ # 机读:问 stagewatch(render.py 的 explain 只打人读文本,不接受 --json)
485
+ python scripts/stagewatch.py --work work --json
486
+ ```
487
+
488
+ 机制:每个 stage 结束往 `work/manifest.json` 记一行 `{stage, at, inputs:{名:指纹}, outputs:{名:指纹}}`;
489
+ 下次该 stage 开始前重算上游输入与自己的产物指纹。**默认只告警**(`--strict-stale` 升级为失败,
490
+ `--allow-stale` 显式放行并留痕)。改一句要重跑什么,见 [references/change-impact.md](references/change-impact.md)。
491
+
300
492
  **质检分三层**,一层比一层更接近"观众真的看到了什么":
301
493
 
302
494
  | 层 | 查什么 | 工具 | 抓得住什么 |
@@ -450,10 +642,13 @@ v0.4 给了两条真正能用的路:
450
642
  - `references/pipeline.md` — 八阶段详细规格、契约 JSON 格式、目录约定、并行分工
451
643
  - `references/scene-authoring.md` — 场景函数写法、构图规则、Canvas/CDP 方案、超采样与后处理
452
644
  - `references/audio.md` — 采样级混音、程序化配乐/音效、响度链路、循环环境音、提示点系统
645
+ - `references/audio-variation.md` — **(v0.7)片间差异**:内容指纹怎么进和声/进行/动机/鼓型,legacy 兼容与验收阈值
453
646
  - `references/sfx-design.md` — **(v0.3)音效设计**:物理拆解流程、配方表、听感诊断表、QC 规则
454
647
  - `references/web-engine.md` — **(v0.3)浏览器引擎**:页面契约、确定性时间轴、分块渲染、崩溃重开
455
648
  - `references/frame-qa.md` — **(v0.3)帧级质检**:单帧闪烁判据、画面空窗、音画同步、报告怎么读
456
649
  - `references/quality-gates.md` — G0–G6 全部门禁、可复制代码、验收清单
650
+ - `references/change-impact.md` — **(v0.6)改动影响表**:改了 text/sub/hold/style/scenes.py 各要重跑什么
651
+ - `references/style-packs.md` — **(v0.5)风格包**:主题/配乐/声线字段、三级指纹规则、加第 9 套风格的步骤
457
652
  - `references/pitfalls.md` — 20 条最佳实践 + 16 条反模式 + 3 个非确定性 bug 复盘
458
653
 
459
654
  ## 10. 脚本资产
@@ -461,14 +656,19 @@ v0.4 给了两条真正能用的路:
461
656
  | 文件 | 作用 |
462
657
  |---|---|
463
658
  | `scripts/filmkit.py` | 生产内核:字体/字形自查/缓存排版/字幕安全区/缓动/确定性随机/音频总线/**CueBus 提示点**/**立体声混音 mix_film**/TTS/时间轴/**hold 停留窗口**/FrameSink/合成/质检 |
464
- | `scripts/render.py` | 编排器:**10 个独立 stage**、两种引擎(`--engine pil\|web`)、分块并行(`--jobs`)、断点续跑(`--resume`)、母版预览(`stills`) |
465
- | `scripts/selfcheck.py` | 回归自检:**24 条**"曾真的坏过"的断言,不联网 20 秒出结论(可选模块缺失标 SKIP) |
659
+ | `scripts/render.py` | 编排器:**13 个 stage**(含 `accept` / `explain`)、两种引擎(`--engine pil\|web`)、分块并行(`--jobs`)、断点续跑(`--resume`)、母版预览(`stills`) |
660
+ | `scripts/gates.py` | **(v0.6)门禁账本**:pass / fail / skipped 三档、`out/QC-REPORT.md` 渲染、人工签核(绑内容指纹)、原子写 |
661
+ | `scripts/stagewatch.py` | **(v0.6)阶段新鲜度**:每 stage 记录输入/输出指纹到 `work/manifest.json`;上游一改,下游报过时并给出最小重跑命令 |
662
+ | `scripts/filmcheck.py` | **(v0.6)film.json 契约校验**:未知键提示"是不是想写 X"(以前静默忽略)、枚举与范围、跨字段(cue 引用的 id 必须存在) |
663
+ | `scripts/selfcheck.py` | 回归自检:**43 条**"曾真的坏过"的断言,不联网 20 秒内出结论(可选模块缺失标 SKIP) |
466
664
  | `scripts/sfxkit.py` | **(v0.3)物理建模音效库**:模态合成/纹理/空间/动力学/母带 + 24 个配方 + `CUE_ALIASES` |
467
665
  | `scripts/scorekit.py` | **(v0.3)程序化配乐**:和弦表/分段能量/律动层/吸气/自动电平,输出 `music.wav` |
468
666
  | `scripts/webengine.py` | **(v0.3)浏览器逐帧渲染**:Edge/Chromium + CDP 截图、分块并行、崩溃重开、cue 导出、文字质检 |
469
667
  | `scripts/qa_frames.py` | **(v0.3)帧级质检**:单帧闪烁/画面空窗/音画同步/亮度与响度曲线(JSON + PNG) |
470
668
  | `assets/scenes.py` | 场景模板(开场/正文卡片/收尾 + **`cues()` 提示点钩子示范**),含缓存排版与预烘焙背景的正确写法 |
471
669
  | `assets/film.json` | 剧本模板(含 `hold`/`bpm`/`music`/`sfx.cues`/`engine` 等新字段) |
670
+ | `<REPO>/schemas/film.schema.json` | **(v0.6)film.json 的契约**(JSON Schema draft-07 子集)。校验器见 `scripts/filmcheck.py`,模板由 `tools/ci_check.py` 第 12 项守着 |
671
+ | `<REPO>/tools/estimate.py` | **(v0.6)开渲前预演**:帧数 / 各阶段耗时 / 磁盘峰值 / 分块建议,`--json` 可机读。基准常量集中在文件顶部,换机请重测后回填 |
472
672
  | `assets/web/` | **(v0.3)浏览器页面模板**:原生 Canvas2D 确定性时间轴、场景示范、文字质检脚本 |
473
673
  | `requirements.txt` | **(v0.4)Python 依赖清单**:必需(numpy/Pillow/edge-tts)/ 强烈建议(cv2)/ 可选(scipy、playwright)三档,带注释说明"缺了会怎样"。`pip install -r requirements.txt` 即可 |
474
674
 
@@ -498,13 +698,17 @@ v0.4 给了两条真正能用的路:
498
698
  7. **(v0.3)帧级深检的三个数**:单帧闪烁 N 条 / 画面空窗 N 段 / 音画同步超差 N 条
499
699
  (有 web 引擎时再加页面层的 文字重叠 / 出画 / 一闪而过 / 从没露面 四项)
500
700
 
701
+ > **v0.6 起不要口头复述这些数字**:`out/QC-REPORT.md` 是机器生成的交付凭据,
702
+ > 照抄报告里的"实测值"即可;报告里的 `skipped` 项必须逐项说明为什么没执行。
703
+ >
501
704
  > 完整示例(本技能自带模板片的实测,可以照这个格式抄;**数字每次跑会差一点**——
502
705
  > TTS 实测时长、AAC 编码都会让真峰/响度/锐度在小范围内浮动,别 chasing 小数点):
503
706
  > ```
504
707
  > 容器:1920x1080 / 30 fps / h264+aac / 48 kHz / 时长 30.10 s / 1.00 Mbps
505
708
  > 信号:-16.0 LUFS / 真峰 -1.2 dBTP / 锐度比 1.01 / 黑帧 0 / 字幕 4 条 = 剧本 4 句
506
709
  > 时间:单帧闪烁 0 / 画面空窗 0 / 音画同步超差 0(本条为 web 引擎或写了 visual_at 时才有意义)
507
- > 自检:selfcheck PASS 24 / FAIL 0 / SKIP 0;tools/ci_check.py PASS 11 / FAIL 0
710
+ > 自检:selfcheck PASS 43 / FAIL 0 / SKIP 0;tools/ci_check.py PASS 12 / FAIL 0
711
+ > 音频:work/score.json 的 variation 非 0(指纹参与了结构变奏)、audio_similarity --strict 全 PASS
508
712
  > 目检:work/qa/sheet.png,四帧分别为开场星射线、五步成片卡片、画质三件事卡片、收尾居中大字;
509
713
  > 字幕黑底白字不压正文,大字边距均匀,无空洞帧。
510
714
  > ```
@@ -546,7 +750,7 @@ v0.4 给了两条真正能用的路:
546
750
  | `scorekit` | 389 s 全片 **4–8 s**(v0.3 首测 6.7–8.2 s,v0.4 复测 3.9–5.8 s,随负载);60 s 实测 2–4 s;输出 -18.5 LUFS / -1.44 dBTP(母带交给 loudnorm)。**selftest 会打印当下实测**,以那个为准 |
547
751
  | `webengine` | **80–130 ms/帧**(1080p30,Edge 无头,PNG 截图);12 s / 360 帧 **28–30 s**(2 workers,含启动);**同一页面会话内**同帧两次截图 byte 完全一致(跨会话默认 GPU 光栅有 ≤4 像素、±1 级抗锯齿差异,要跨会话字节级一致请开 `--deterministic`,代价约 +22%/帧) |
548
752
  | `qa_frames` | 30 s 片全量深检 **约 1 s**(160×90 流式解码 903 帧);闪烁/空窗/静止/同步 |
549
- | 端到端 | 30.10 s 模板片:TTS 4 句 → cues 13 条 → 配乐 3.3 s → 音效 13 条落位 → 混音 → PIL 75 fps 渲染 12 s → mux → 深检 PASS |
753
+ | 端到端 | 30.10 s 模板片:TTS 4 句 → cues 由 scenes.py 按 kind 生成(v0.7,模板片 17 条)→ 配乐 3.3 s → 音效逐条落位 → 混音 → PIL 75 fps 渲染 12 s → mux → 深检 PASS |
550
754
  | Python 依赖 | **playwright 已装**(chromium + msedge 均可用);scipy 1.18 已装(sfxkit 可选,缺了走 FFT 兜底);`soundfile` **没装**(本技能不依赖它) |
551
755
 
552
756
  ### 排期公式(保守)
@@ -565,7 +769,23 @@ Pillow 路线: 墙钟秒数 ≈ 帧数 / 40 / 并行度 + 30% 余量
565
769
 
566
770
  ## 13. 升级须知(从旧版迁移要动的地方)
567
771
 
568
- ### 13.0 v0.3.0 → v0.4.0
772
+ ### 13.0 v0.4 → 当前版本(v0.5 / v0.6 / v0.7 / v0.7.1 / v0.7.2 要做什么)
773
+
774
+ **一句话**:v0.4 的老项目**不写新字段就能继续跑**(不写 `style` = legacy 行为,配乐逐样本不变),
775
+ 但**工作目录要多拷文件**,且有两处默认值变了要重新听。按版本逐条过:
776
+
777
+ | 从 → 到 | 你要做的动作 | 依据 |
778
+ |---|---|---|
779
+ | v0.4 → **v0.5** | 新工作目录多拷 `scripts/stylekit.py`(**必需**,render 直接 import)、`scripts/voicekit.py`(可选,缺了单声线降级);`--stage` 多出 `style`,`--stage all` 顺序变为 `style → tts → …`;改用 `style: "auto"` 才会自动选风格,不写仍是 legacy | CHANGELOG 0.5.0「升级须知(v0.4 -> v0.5)」 |
780
+ | v0.5 → **v0.6** | **必需**多拷 `scripts/gates.py`、`scripts/stagewatch.py`(少一个 render.py 直接 `ModuleNotFoundError`);首次跑会多出 `work/manifest.json` / `work/gates.json` / `out/QC-REPORT.md`,属新增文件 | CHANGELOG 0.6.0「升级须知(v0.5 → v0.6)」 |
781
+ | v0.6 → **v0.7** | 想吃到「配乐/音效随内容变化」就保持 `style: "auto"`,**不要**写死 `music.seed` 或关掉 `seed_from_content`;音效想更丰富就把提示点写进 `scenes.py` 的 `cues()` | CHANGELOG 0.7.0「升级须知(v0.6 → v0.7)」 |
782
+ | v0.7 → **v0.7.1** | 配乐基线默认值从 `music.gain_db=-1.5` 变成 **-12**(模板也已改),有人声处再压配乐 6 dB / 音效 3 dB;**之前手调过 `gain_db`/`duck_db` 的片子要重新听一遍**(现在背景整体更低)。显式写 0 可关掉对应侧链 | CHANGELOG 0.7.1「修了什么」+ `assets/film.json` 的 `music` 默认值 |
783
+ | v0.7.1 → **v0.7.2** | `QC-REPORT.md` 现在**只落在 `--out` 的目录**(以前 `accept` 分支按 `work` 的兄弟目录算,`--out deliver/film.mp4` 时签核会更新到另一份没人交付的文件)。如果你用 `--out` 指到了非 `out/` 的目录,交付时请看新位置;旧的 `out/QC-REPORT.md` 不会被再写 | `render._report_path()` 的 docstring + `selfcheck` 第 40 条(本轮新增) |
784
+
785
+ > 查当前版本:`python <REPO>/tools/ci_check.py -v` 里的 version 断言、或 `cat package.json` 的 `version`。
786
+ > 升级后建议按 §14 重跑一次自检(43 条),再按 §7 重新签核(改了代码/默认值,旧签核会自动失效,这是设计)。
787
+
788
+ ### 13.1 v0.3.0 → v0.4.0
569
789
 
570
790
  **多数项目不用改任何东西**(默认行为向后兼容),但下面几处会**改变你看到的输出或报错**:
571
791
 
@@ -579,7 +799,7 @@ Pillow 路线: 墙钟秒数 ≈ 帧数 / 40 / 并行度 + 30% 余量
579
799
  | `film.json` 校验变严 | 缺 `text` / `lines` 空 / id 重复 / bpm 非法 会直接报错 | 按提示修剧本 |
580
800
  | 可选 peer 改成按需 import | 缺 `@deepseek-ai/dsh-skill-filesystem` 时插件不再整体加载失败,只是不挂载技能并打日志 | 想用技能就按日志里的命令装依赖 |
581
801
 
582
- ### 13.1 v0.2 → v0.3.0
802
+ ### 13.2 v0.2 → v0.3.0
583
803
 
584
804
  **兼容性优先:老项目不做任何改动也能跑**(`engine` 默认 `pil`、`music.mode` 缺省时按老的和弦床走、`whoosh` 行为保留)。
585
805
 
@@ -596,7 +816,7 @@ Pillow 路线: 墙钟秒数 ≈ 帧数 / 40 / 并行度 + 30% 余量
596
816
 
597
817
  **新增文件**:`sfxkit.py` / `scorekit.py` / `webengine.py` / `qa_frames.py` / `assets/web/`(都是可选,缺失时对应能力自动降级并打印 WARN)。
598
818
 
599
- ### 13.2 v0.1 → v0.2(历史)
819
+ ### 13.3 v0.1 → v0.2(历史)
600
820
 
601
821
  | 变更 | v0.1 | v0.2 | 迁移动作 |
602
822
  |---|---|---|---|
@@ -614,7 +834,7 @@ Pillow 路线: 墙钟秒数 ≈ 帧数 / 40 / 并行度 + 30% 余量
614
834
 
615
835
  ```bash
616
836
  cd <SKILL_DIR>/scripts
617
- python selfcheck.py # 24 条断言;FAIL 就照提示修环境
837
+ python selfcheck.py # 43 条断言;FAIL 就照提示修环境
618
838
  python sfxkit.py --selftest # 音效库(可选模块,装了才跑)
619
839
  python scorekit.py --selftest # 配乐
620
840
  python webengine.py --selftest # 浏览器引擎(24 项,会真起无头浏览器,约 15 秒)
@@ -627,9 +847,13 @@ selfcheck 自己造测试信号、现场调 ffmpeg 编码,**不联网、不依
627
847
  **别把"没装可选件"和"坏了"混为一谈**。
628
848
 
629
849
  > **仓库级体检**(v0.4 起)是另一层,别和 selfcheck 搞混:
630
- > `python tools/ci_check.py -v` 只查"这个仓库作为可分发包是否完整"(语法/JSON/依赖白名单/
850
+ > `python <REPO>/tools/ci_check.py -v`(在包根执行)只查"这个仓库作为可分发包是否完整"(语法/JSON/依赖白名单/
631
851
  > 打包卫生/文档路径引用),**不需要 ffmpeg、字体或网络**,任何机器都能跑;
632
852
  > selfcheck 查的是"技能还能不能真的出片"。两层都在 CI 里跑。
853
+ >
854
+ > v0.6.0 起 ci_check 多了第 12 项「film.json 契约」:直接调 `scripts/filmcheck.py` 校验模板,
855
+ >并把 `schemas/film.schema.json` 的存在与合法性一起断言——**校验规则只允许有一份**,
856
+ >不在这里重写一遍。
633
857
 
634
858
  ### v0.2 在本机实测(2026-11-07,同一台机器)
635
859
 
@@ -1,13 +1,12 @@
1
1
  {
2
+ "_comment": "v0.5 模板:style 决定画风+配乐+旁白+节奏。'auto' = 按内容自动选;写死风格 id 可锁定(tech-deep/magazine/paper/vivid/mono/cinema/lo-fi/epic)。删掉 style 字段 = 回到 v0.4 行为(逐样本兼容)。节奏(lead/tail/min_dur/hold)默认由风格给,想覆盖就自己加字段。",
2
3
  "title": "测试片 demo",
3
4
  "end_text": "完",
5
+ "style": "auto",
4
6
  "voice": "zh-CN-YunxiNeural",
5
7
  "rate": "+6%",
6
- "lead": 0.62,
7
- "tail": 0.92,
8
- "min_dur": 3.2,
9
- "hold": 0.0,
10
- "bar": 0.0,
8
+ "pitch": "+0Hz",
9
+ "bar": 0,
11
10
  "sub_max_hold": null,
12
11
  "trim_silence": true,
13
12
  "tts_workers": 4,
@@ -19,28 +18,56 @@
19
18
  "music": {
20
19
  "enabled": true,
21
20
  "mode": "score",
22
- "key": "Dm",
21
+ "style": "auto",
22
+ "mood": "auto",
23
+ "bpm": "auto",
24
+ "key": "auto",
23
25
  "seed": 7,
24
- "gain_db": -1.5,
25
- "duck_db": 3.0,
26
- "seg": 4.0,
26
+ "seed_from_content": true,
27
+ "gain_db": -12,
28
+ "duck_db": 4,
29
+ "voice_duck_db": 6,
30
+ "voice_duck_sfx_db": 3,
31
+ "guard_snr_db": 12,
32
+ "seg": 4,
27
33
  "amp": 0.05,
28
34
  "gain": 0.9,
29
35
  "xfade": 1.2,
30
- "chords": [[220, 261.63, 329.63], [174.61, 220, 261.63], [196, 246.94, 293.66]]
36
+ "chords": [
37
+ [
38
+ 220,
39
+ 261.63,
40
+ 329.63
41
+ ],
42
+ [
43
+ 174.61,
44
+ 220,
45
+ 261.63
46
+ ],
47
+ [
48
+ 196,
49
+ 246.94,
50
+ 293.66
51
+ ]
52
+ ]
31
53
  },
32
54
  "sfx": {
33
55
  "enabled": true,
34
- "gain_db": 0.0,
56
+ "gain_db": 0,
57
+ "content_seed": true,
58
+ "_comment": "v0.7 去固定化(references/audio-variation.md §3.5):模板只留 1 条转场 whoosh 作示例,具体提示点建议交给 assets/scenes.py 的 cues() 钩子按画面内容生成(hook=impact+riser;body=ui_blip 按条目文本定音高 + answer_chime + 段尾 whoosh;quote=reverse_swell+impact;outro=answer_chime+sub_drop)。content_seed=true(默认)= 每条 cue 的 seed 由 style/content_fp 派生(不同片子同一位置也不同),显式 false 回到 v0.6 的 seed_base+i 旧行为。",
35
59
  "cues": [
36
- {"at": "l02-0.15", "type": "whoosh", "dur": 0.9, "gain": -2},
37
- {"at": "l03", "type": "riser", "dur": 2.0, "gain": -4},
38
- {"at": "l03+0.05", "type": "impact", "size": 1.0, "gain": -3}
60
+ {
61
+ "at": "l02-0.15",
62
+ "type": "whoosh",
63
+ "dur": 0.9,
64
+ "gain": -2
65
+ }
39
66
  ]
40
67
  },
41
68
  "web": {
42
69
  "entry": "",
43
- "chunk_s": 30.0
70
+ "chunk_s": 30
44
71
  },
45
72
  "whoosh": true,
46
73
  "whoosh_amp": 0.055,
@@ -63,11 +90,26 @@
63
90
  "big": "五步成片",
64
91
  "hold": 0.6,
65
92
  "items": [
66
- {"text": "剧本 film.json", "ok": true},
67
- {"text": "旁白 edge-tts 实测时长", "ok": true},
68
- {"text": "时间轴 timeline.json", "ok": true},
69
- {"text": "逐帧 PIL 渲染", "ok": true},
70
- {"text": "numpy 采样级混音", "ok": true}
93
+ {
94
+ "text": "剧本 film.json",
95
+ "ok": true
96
+ },
97
+ {
98
+ "text": "旁白 edge-tts 实测时长",
99
+ "ok": true
100
+ },
101
+ {
102
+ "text": "时间轴 timeline.json",
103
+ "ok": true
104
+ },
105
+ {
106
+ "text": "逐帧 PIL 渲染",
107
+ "ok": true
108
+ },
109
+ {
110
+ "text": "numpy 采样级混音",
111
+ "ok": true
112
+ }
71
113
  ]
72
114
  },
73
115
  {
@@ -79,10 +121,22 @@
79
121
  "sub": "直通编码 / 安全区 / 响度",
80
122
  "big": "画质三件事",
81
123
  "items": [
82
- {"text": "rawvideo -> libx264 CRF17", "ok": true},
83
- {"text": "字幕不越出安全区", "ok": true},
84
- {"text": "-16 LUFS / -1.5 dBTP", "ok": true},
85
- {"text": "每帧确定性可复现", "ok": true}
124
+ {
125
+ "text": "rawvideo -> libx264 CRF17",
126
+ "ok": true
127
+ },
128
+ {
129
+ "text": "字幕不越出安全区",
130
+ "ok": true
131
+ },
132
+ {
133
+ "text": "-16 LUFS / -1.5 dBTP",
134
+ "ok": true
135
+ },
136
+ {
137
+ "text": "每帧确定性可复现",
138
+ "ok": true
139
+ }
86
140
  ]
87
141
  },
88
142
  {