@dsh-gis/dsh-video-studio-skill 1.0.2 → 1.0.4

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 (51) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +84 -108
  3. package/THIRD_PARTY_NOTICES.md +2 -2
  4. package/package.json +1 -1
  5. package/schemas/design.schema.json +5 -5
  6. package/schemas/film.schema.json +31 -31
  7. package/skills/video-studio/SKILL.md +168 -268
  8. package/skills/video-studio/assets/film.json +2 -3
  9. package/skills/video-studio/assets/golden_frames.json +11 -11
  10. package/skills/video-studio/assets/scenes.py +92 -49
  11. package/skills/video-studio/references/{style-packs.md → appearance.md} +17 -15
  12. package/skills/video-studio/references/audio-variation.md +73 -96
  13. package/skills/video-studio/references/audio.md +40 -44
  14. package/skills/video-studio/references/change-impact.md +2 -2
  15. package/skills/video-studio/references/{v08-quality.md → delivery-gates.md} +9 -11
  16. package/skills/video-studio/references/frame-qa.md +47 -15
  17. package/skills/video-studio/references/pipeline.md +4 -4
  18. package/skills/video-studio/references/pitfalls.md +22 -3
  19. package/skills/video-studio/references/proposal.md +19 -36
  20. package/skills/video-studio/references/quality-gates.md +11 -11
  21. package/skills/video-studio/references/scene-authoring.md +2 -2
  22. package/skills/video-studio/references/scene-grammar.md +17 -24
  23. package/skills/video-studio/references/sfx-design.md +7 -5
  24. package/skills/video-studio/references/web-engine.md +7 -7
  25. package/skills/video-studio/references/word-timeline.md +7 -7
  26. package/skills/video-studio/requirements.txt +9 -13
  27. package/skills/video-studio/scripts/designkit.py +84 -89
  28. package/skills/video-studio/scripts/filmcheck.py +19 -19
  29. package/skills/video-studio/scripts/filmkit.py +2949 -2948
  30. package/skills/video-studio/scripts/gates.py +4 -4
  31. package/skills/video-studio/scripts/grammar.py +10 -10
  32. package/skills/video-studio/scripts/musicgen.py +3 -4
  33. package/skills/video-studio/scripts/preflight.py +5 -5
  34. package/skills/video-studio/scripts/previewkit.py +11 -12
  35. package/skills/video-studio/scripts/propose.py +11 -11
  36. package/skills/video-studio/scripts/qa_frames.py +34 -35
  37. package/skills/video-studio/scripts/qaasr.py +26 -7
  38. package/skills/video-studio/scripts/qavmaf.py +19 -19
  39. package/skills/video-studio/scripts/render.py +137 -141
  40. package/skills/video-studio/scripts/scorekit.py +21 -23
  41. package/skills/video-studio/scripts/selfcheck.py +208 -135
  42. package/skills/video-studio/scripts/sfxkit.py +27 -26
  43. package/skills/video-studio/scripts/stagewatch.py +4 -4
  44. package/skills/video-studio/scripts/stylekit.py +77 -282
  45. package/skills/video-studio/scripts/voicekit.py +43 -44
  46. package/skills/video-studio/scripts/webengine.py +62 -64
  47. package/tools/audio_similarity.py +12 -12
  48. package/tools/ci_check.py +16 -16
  49. package/tools/estimate.py +24 -25
  50. package/tools/golden_frames.py +17 -18
  51. package/tools/silhouette_qa.py +13 -14
@@ -1,40 +1,36 @@
1
- # 音频多样性改造(v0.7)—— 接口契约(**已归档**)
1
+ # 音频差异机制 —— 接口契约
2
2
 
3
- > **v1.0.1 起本文描述的 scorekit 风格层已删除**(`MUSIC_STYLES` / `render_styled` /
4
- > 指纹结构变奏)。保留本文是因为音效侧契约(sfxkit / CueBus / duck / 旁白优先)仍然有效;
5
- > 「A 路配乐变奏」那一节只作历史记录——现在配乐由 `musicgen.py` 参数化编排,没有风格表。
3
+ > 当前机制一句话:配乐由 `musicgen.py` 参数化编排(bpm/key/scale/mood/seed + 编排参数,
4
+ > 内容指纹与编排参数共同决定和声/动机/鼓型);音效 seed 由 `content_seed(base, style, content_fp, recipe, t)`
5
+ > 按内容派生;提示音调性跟随配乐(`in_key`);段落边界前 2.2 s 内的转场 cue 自动 -3 dB
6
+ > (`sfx.duck_transitions`);混音旁白优先(配乐 -12 dB 基线 + 语音侧链 6/3 dB + SNR 兜底 12 dB)。
6
7
  >
7
- > 起草:lead,2026-11-09。本文件是本次改造的**唯一定义源**。
8
- > 三路并行改动的文件写域不重叠,但**接口必须按本文件逐字实现**,否则集成会失败。
9
- > 诊断证据见 `tools/audio_similarity.py` 的输出(改完后必须重跑,阈值见 §5)。
8
+ > 阈值由 `tools/audio_similarity.py` 按 §5 判定,诊断证据就是它的输出。
10
9
 
11
10
  ## 1. 要解决的两个问题(都有实测数据)
12
11
 
13
12
  | 问题 | 实测 | 根因 |
14
13
  |---|---|---|
15
- | A. 跨视频配乐雷同 | 同风格/同 bpm/同调、只换内容指纹:corr **0.966** | 指纹只改 seed(音色细节),和弦进行/动机/鼓型/段落骨架全固定 |
14
+ | A. 跨视频配乐雷同 | 同 bpm/同调/同编排参数、只换内容指纹:corr **0.966** | 指纹只改 seed(音色细节),和弦进行/动机/鼓型/段落骨架全固定 |
16
15
  | B. 跨视频音效雷同 | 同 cue 序列两次渲染逐样本相同 | `CueBus.build_audio` seed = `seed_base + i`,只跟 cue 顺序有关 |
17
16
  | C. 片内配乐与音效难分 | impact 质心 1001 Hz / whoosh 1589 Hz / music 5451 Hz;提示音硬编码 Dm 五声 | 提示音调性硬编码 + 配乐过门与 sfx 轨同边界同频段 + duck 仅 2.5–4.5 dB |
18
17
 
19
- ## 2. 改动总览(按写域分三路)
18
+ ## 2. 各层由谁负责
20
19
 
21
- | 路 | 负责文件 | 改什么 |
20
+ | 层 | 负责文件 | 做什么 |
22
21
  |---|---|---|
23
- | A 配乐变奏(**v1.0.1 已删除**) | 原 `skills/video-studio/scripts/scorekit.py` | 指纹参与**和声/动机/鼓型/结构**,不再只影响音色细节 |
24
- | B 音效差异化 | `skills/video-studio/scripts/sfxkit.py`、`assets/scenes.py`、`assets/film.json` | `in_key` 跟随配乐调性;`cues()` 按画面内容生成;模板 cue 去固定化 |
25
- | C 接线与混音 | `skills/video-studio/scripts/filmkit.py`(lead)、`render.py`(lead)、`stylekit.py`(只读)、`selfcheck.py`(lead) | CueBus 内容派生 seed、调性传给 sfxkit、duck 加深、风格过门避让 |
26
-
27
- **写域规则(历史)**:A 路只改 scorekit.py;B 路只改 sfxkit.py / assets/scenes.py / assets/film.json;
28
- C 路由 lead 改 filmkit.py / render.py / selfcheck.py。**任何一路不得改别人的文件**(包括顺手"优化")。
22
+ | 配乐编排 | `skills/video-studio/scripts/musicgen.py` | 按 bpm / key / scale / mood / seed 编排;`sections` / `progressions` / `motif` / `drums` / `voices` / `energy_curve` 真的参与事件展开;内容指纹进 seed 与变奏参数 |
23
+ | 音效差异化 | `skills/video-studio/scripts/sfxkit.py`、`assets/scenes.py`、`assets/film.json` | `in_key` 跟随配乐调性;`content_seed` 内容派生 seed;`cues()` 按画面内容生成;夹具 cue 不固定三条 |
24
+ | 接线与混音 | `skills/video-studio/scripts/filmkit.py`、`render.py`、`selfcheck.py` | CueBus 内容派生 seed、调性透传、duck 分级、过门避让、旁白优先 |
29
25
 
30
26
  ## 3. 接口契约(逐字实现,向后兼容)
31
27
 
32
- ### 3.1 sfxkit(B 路)
28
+ ### 3.1 sfxkit
33
29
 
34
30
  ```python
35
- # 1) 调性跟随:新增 key 参数,默认 None = 旧行为(Dm 五声)
31
+ # 1) 调性跟随:key 参数,默认 None = 固定 D 小调五声
36
32
  def in_key(freq: float, key: str | None = None) -> float:
37
- """key=None -> 旧行为(硬编码 D 小调五声,逐样本兼容)。
33
+ """key=None -> 固定 D 小调五声(不随配乐调性变,逐样本可复现)。
38
34
  key 给 "Dm"/"C"/"Am"/... -> 吸附到该调(小调用自然小调五声,大调用大调五声)。"""
39
35
 
40
36
  # 2) 所有走 in_key 的配方接受 key(通过 **kw 透传,签名末位加 key=None 或直接从 kw 取):
@@ -43,121 +39,102 @@ def in_key(freq: float, key: str | None = None) -> float:
43
39
 
44
40
  # 3) 内容派生 seed 的辅助函数(供 filmkit.CueBus 用)
45
41
  def content_seed(base: int, style: str, content_fp: str, recipe: str, t: float) -> int:
46
- """稳定的 32 位非负 int:sha1(\"v0.7|%d|%s|%s|%s|%.3f\") -> int(h[:8],16)%9000+1000。"""
42
+ """稳定的 32 位非负 int:sha1("<固定盐>|<base>|<style>|<content_fp>|<recipe>|<t 三位小数>")
43
+ -> int(h[:8], 16) % 9000 + 1000。"""
47
44
  ```
48
45
 
49
- ### 3.2 filmkit.CueBus.build_audio(C 路,lead)
46
+ `<固定盐>` 是代码里写死的字符串常量:`sfxkit.content_seed` 与 `filmkit.content_seed` 逐字相同。
47
+ 要复刻就照抄实现里的拼接串,**不要自己另拼一份**——换个盐等于换一套 seed,
48
+ "A1 同参数换指纹可区分"的验收直接失效。
49
+
50
+ ### 3.2 filmkit.CueBus.build_audio
50
51
 
51
52
  ```python
52
53
  def build_audio(self, total=None, sr=SR, base_db=None, default_gain_db=0.0,
53
54
  seed_base=1000, verbose=True,
54
- style=None, # 风格 id;None = 旧行为
55
- content_fp=None, # 内容指纹 full;None = 旧行为
56
- key=None): # 配乐调性;None = 旧行为(sfxkit 用 Dm)
55
+ style=None, # 设计标识(design:<hash>);None = 位置派生
56
+ content_fp=None, # 内容指纹 full;None = 位置派生
57
+ key=None): # 配乐调性;None = 提示音按 D 小调五声
57
58
  """每条 cue 的 seed:
58
- - style/content_fp 任一为 None -> seed = seed_base + i(旧路径,逐样本兼容)
59
+ - style / content_fp 任一为 None -> seed = seed_base + i(位置派生,逐样本可复现)
59
60
  - 都给 -> seed = sfxkit.content_seed(seed_base, style, content_fp, kind, t)
60
61
  并把 key 透传给配方(render(kind, seed=..., key=key, **params))。"""
61
62
  ```
62
63
 
63
- ### 3.3 scorekit.render_styled(A 路)——**v1.0.1 已删除,仅存档**
64
-
65
- 签名不变。**内部**行为变化:
66
-
67
- - `full` 指纹(已有 `fingerprint` 参数,当前只用于 seed)必须影响**和声与动机**:
68
- - 和弦替换:按指纹从该风格的 `progressions` 里选 tier,再对**每两小节**做一次稳定替换
69
- (从同大小的替代级数集里挑:i↔i7 / VI↔VI7 / III↔III7 / VII↔VII7;大调侧 I↔Imaj7 / IV↔IVmaj7)。
70
- 只在与乐句末/片尾规则不冲突时替换。
71
- - 动机变形:`motif` 的每个 (拍, 级数, 时值) 按指纹做 **±1 级数的稳定偏移**,
72
- 只改级数不改节奏;问题句偏移与答句偏移必须独立派生(保证两部片子不会同步变形)。
73
- - 鼓型:`drums[tier]` 的 kick 落点按指纹加 **最多一个 ghost 落点**(在 0.5 拍网格上,
74
- 不与现有落点重复);clap 位置按指纹 ±0.5 拍。
75
- - **必须保证**:同 `fingerprint` 逐采样一致;不同 fingerprint 的相关性显著下降(§5 阈值)。
76
- - 新增报告字段:`variation`(dict:`{"chord_swaps": n, "motif_shifts": n, "drum_ghosts": n}`),
77
- 写进 work/score.json,方便审计。
78
- - legacy 路径(style=None)**逐字节不变**(`scorekit.py --selftest` 的 legacy 条会验)。
79
- (v1.0.1:`render_styled` 已删除,这一条现在只说明历史契约。)
64
+ `render.py` 的 `_sfx_seed_context` 就是这条的调用方:它从 `resolve_style` 解出的设计计划里取
65
+ `style` / `fingerprint.full` / `music.key`;`sfx.content_seed=false`(或计划里没有指纹)时返回全 None,
66
+ `build_audio` 于是走位置派生。
80
67
 
81
- ### 3.4 assets/scenes.py 的 cues()(B 路)
68
+ ### 3.3 assets/scenes.py 的 cues()
82
69
 
83
- 现在只出 `ui_blip × items` + `answer_chime × outro`。改为按画面事件生成,并且**类型按场景 kind 与内容变化**:
70
+ 按画面事件生成,**类型按场景 kind 与内容变化**:
84
71
 
85
72
  - `hook`:段首 `impact`(size 按字数 0.9~1.3),落定 `riser`。
86
- - `body`:条目落位仍用 `ui_blip`,但 **pitch 由条目文本 sha1 派生**(json 的 items 文本);
73
+ - `body`:条目落位用 `ui_blip`,但 **pitch 由条目文本 sha1 派生**(json 的 items 文本);
87
74
  最后一个条目后加 `answer_chime`(若 kind=body 且 items≥3);段尾 `whoosh`(going 到下一段)。
88
75
  - `quote`:`reverse_swell` + 落定 `impact`(轻)。
89
76
  - `outro`:`answer_chime` + 片尾 `sub_drop`。
90
- - 不许再出现"每段都一样的三连击":同类型 cue 在一段内重复时,`gain` 必须递减(-2/-4/-6),pitch 必须不同。
91
- - **保持向后兼容**:仍有 `stagger` 对齐逻辑(与 _draw_items 同节拍),返回 schema 不变。
77
+ - 不许出现"每段都一样的三连击":同类型 cue 在一段内重复时,`gain` 必须递减(-2/-4/-6),pitch 必须不同。
78
+ - **对齐**:stagger 对齐逻辑(与 `_draw_items` 同节拍)保留,返回 schema 不变。
92
79
 
93
- ### 3.5 assets/film.json 模板(B 路)
80
+ ### 3.4 assets/film.json 夹具
94
81
 
95
- 模板里的 `sfx.cues` 固定三条 whoosh/riser/impact 是"跨片雷同"的直接来源之一。改为:
82
+ 模板里的 `sfx.cues` 只保留 1 条转场 whoosh 作为示例,并注明"具体提示点交给 `scenes.py` 的 `cues()` 钩子"
83
+ (固定三条 whoosh/riser/impact 是"跨片雷同"的直接来源之一)。
96
84
 
97
- - 模板只保留 1 条转场 whoosh 作为示例,并加注释说明"具体提示点建议交给 scenes.py 的 cues() 钩子"。
98
- - `sfx` 增加可选字段 `content_seed: true`(默认 true):走内容派生 seed;显式 false 回到旧行为。
85
+ - `sfx` 的 `content_seed: true`(默认):走内容派生 seed;显式 `false` 回到 `seed_base + i`。
99
86
 
100
- ## 4. render.py 接线(C 路,lead)
87
+ ## 4. render.py 接线
101
88
 
102
- - `st_score`:把 `plan_s["music"]["key"]` 与 `plan_s` 的 `fingerprint` 传给 sfx 阶段(经 `script["_style"]`)。
103
- - `st_sfx`:读 `script["_style"]`,把 `style` / `content_fp` / `key` / `sfx.content_seed` 传给
104
- `bus.build_audio(...)`;没有 `_style`(legacy)时不传,保持旧行为。
105
- - `st_audio`:duck 默认值改为按风格:在现有表基础上整体 +1.0 dB,且 `music.duck_db` 显式值仍然优先。
106
- 另外新增**过门避让**:配乐在 music.wav 里已经有段落过门,sfx 轨在
107
- [段落边界 - 2.2s, 边界] 区间内的 whoosh/riser/reverse_swell 需要自动 -3 dB(在 filmkit 里做,
108
- 或 st_sfx 后处理;由 lead 决定实现位置,**B 路不要碰**)。
89
+ - `_sfx_seed_context`:从设计计划取 `style` / `fingerprint.full` / `music.key`;
90
+ `sfx.content_seed=false` 或计划没有指纹时返回全 None -> `build_audio` 走位置派生。
91
+ - `st_sfx`:把上面三样传给 `bus.build_audio(...)`;`sfx.duck_transitions`(默认 true)
92
+ 决定是否做过门避让。
93
+ - `st_audio`:duck 默认值按风格分级——`mono` 3.0 / `lo-fi`、`magazine` 3.5 / `cinema`、`epic` 5.5 /
94
+ 其余 4.0;`music.duck_db` 显式值永远优先。
95
+ 另外**过门避让**:配乐在 music.wav 里已经有段落过门,sfx 轨在
96
+ [段落边界 - 2.2s, 边界] 区间内的 whoosh/riser/reverse_swell 需要自动 -3 dB
97
+ (`_duck_transition_sfx` 实现,`sfx.duck_transitions=false` 可关)。
109
98
 
110
99
  ## 5. 验收阈值(改完必须重跑 `python <REPO>/tools/audio_similarity.py`)
111
100
 
112
- | 指标 | 改前 | 目标 | 落地实测(2026-11-09) |
101
+ | 指标 | 目标 | 实测 | 不这么做时 |
113
102
  |---|---|---|---|
114
- | A 同风格换指纹 corr | 0.966 | **≤ 0.85**(仍要像同一风格,但不能像同一首) | **0.645** |
115
- | A 换风格 corr | 0.534 | ≤ 0.52(不能变得更像) | **0.514**(v0.7.2 复测,余量 0.006;连跑 3 次逐位相同。余量薄是有意的:抬到 0.53 就离改造前的 0.534 只剩 0.004,这条门禁就没意义了) |
116
- | B 同 cue 序列跨片逐样本相同 | True | **False**(不同内容指纹必须不同) | False(同 fp 仍逐样本相同) |
117
- | B legacy(不传 style/fp) | True | **必须仍为 True**(向后兼容) | True |
118
- | C duck 默认 | 2.5–4.5 dB | 3.5–5.5 dB | 4.0(mono 3.0 / cinema 5.5) |
119
- | D 人声段旁白/背景响度差 | 约 1.5 dB(配乐 -1.5 无侧链) | **≥ 12 dB** | **15.0 dB** |
120
- | D 旁白处背景额外让位 | 0 | ≥ 4 dB | 6.0 dB |
121
-
122
- > v0.7.1 追加 D 组(旁白优先):视频以旁白为主,BGM 与音效是可选层。`mix_film` 的
103
+ | A1 同参数换内容指纹 corr | **≤ 0.85**(仍要像同一部片子,但不能像同一首) | **0.645** | 0.966 |
104
+ | A2 换编排参数 corr | ≤ 0.52(不能变得更像) | **0.514**(连跑 3 次逐位相同) | 0.534 |
105
+ | B1 同 cue 序列跨片逐样本相同 | **False**(不同内容指纹必须不同) | False(同 fp 仍逐样本相同) | True |
106
+ | B2 位置派生(不传 style/fp) | **必须仍为 True**(向后兼容) | True | — |
107
+ | C1 duck 默认 | 3.5–5.5 dB | 4.0(mono 3.0 / cinema 5.5) | 2.5–4.5 dB |
108
+ | D1 人声段旁白/背景响度差 | **≥ 12 dB** | **15.0 dB** | 约 1.5 dB(配乐 -1.5、无侧链) |
109
+ | D2 旁白处背景额外让位 | ≥ 4 dB | 6.0 dB | 0 |
110
+
111
+ > A2 的余量只有 0.006,是有意的:抬到 0.53 就离 0.534 只剩 0.004,这条门禁就没意义了。
112
+ > D 组守的是"旁白优先":视频以旁白为主,BGM 与音效是可选层。`mix_film` 的
123
113
  > `voice_duck_db=6` / `voice_duck_sfx_db=3` + `music.gain_db=-12` 把"人声比背景响 12 dB"
124
114
  > 从文档口号变成可执行断言(selfcheck 35/36 条 + 本脚本 D 组)。
125
115
 
126
- **已知边界(不是回归,是设计取舍)**:同风格不同内容指纹有约 1/3 概率落到同一个
127
- 和弦进行档位偏移上,此时两段配乐 corr 约 0.93(只剩七和弦色彩与鼓型 ghost 的差别)。
128
- 要再压下去需要"每段独立选档"或"同档时轮转",会削弱"片内是一条曲子"的连贯性,
129
- 本轮不做;`--strict` 用固定的 content-a / content-b 做验收,两者落在不同档。
130
-
131
116
  另有硬性回归(任何一条红都不算完成):
132
117
 
133
- > 下面 1–4 的路径都按**仓库根**写(`python skills/video-studio/scripts/…`);如果你已经 `cd` 进技能目录,
118
+ > 下面 1–5 的路径都按**仓库根**写(`python skills/video-studio/scripts/…`);如果你已经 `cd` 进技能目录,
134
119
  > 去掉 `skills/video-studio/` 前缀即可。`<REPO>` = 仓库根 / 装好后的包根。
135
120
 
136
- 1. `python skills/video-studio/scripts/selfcheck.py` 全 PASS(现在 **67 条**;v0.7.2 时的 43 条、v0.8 的 51 条是旧的)。
137
- 2. `python skills/video-studio/scripts/musicgen.py --selftest` 全 PASS(原第 2 条要求 scorekit 风格层自测,v1.0.1 已随风格层删除)。
138
- 3. `python skills/video-studio/scripts/sfxkit.py --selftest` 全 PASS。
121
+ 1. `python skills/video-studio/scripts/selfcheck.py` 全 PASS(**69 条**)。
122
+ 2. `python skills/video-studio/scripts/musicgen.py --selftest` 全 PASS(26 项)。
123
+ 3. `python skills/video-studio/scripts/sfxkit.py --selftest` 全 PASS(16 条)。
139
124
  4. `python <REPO>/tools/ci_check.py` 全 PASS(在包根执行)。
140
- 5. 音效 legacy 逐样本不变:`selfcheck.py` 的音效 legacy 断言(v1.0.1 起风格 legacy 断言随风格层一并删除)。
141
-
142
- ## 6. 协作规则
125
+ 5. 音效位置派生的逐样本可复现:`selfcheck.py` 守着这条(`sfx.content_seed=false` 时与 `seed_base + i` 同值)。
143
126
 
144
- - 每个 teammate 只跑自己文件的 `--selftest`,**全量 selfcheck / ci_check 由 lead 统一跑**(避免并行抢 CPU 和临时目录)。
145
- - 改完在 `CHANGELOG.md` 追加的条目由 lead 统一写(teammate 不要改 CHANGELOG)。
146
- - 任何接口拿不准的地方,按本文件逐字实现;本文件没写的自由发挥请控制在"不改变签名"的范围内。
147
- - 完成后向 lead 报告:改了哪些函数、自测输出摘要、与契约的偏差(如果有)。
127
+ ## 6. 实现备注
148
128
 
149
- ## 7. 实现备注(落地后回填,2026-11-09)
129
+ 几处落地细节(不改契约本身):
150
130
 
151
- 三路改动全部落盘后,实测与契约之间的**已知偏差**都记在这里(不改契约本身):
152
-
153
- | 项 | 契约原文 | 实际实现 | 判断 |
154
- |---|---|---|---|
155
- | 配乐 tier 选择 | §3.3 只列了和弦替换/动机偏移/鼓型 ghost | 额外让指纹从 `progressions` 的 calm/body/drive 三档里选一档(该风格自带的三套进行,不发明新和弦) | 接受:单靠七和弦替换压不到 0.85,实测换 tier 后 0.966 → 0.645;仍是"同风格同一套素材" |
156
- | `roulette_tick` 调性 | §3.1 列进"走 in_key 的配方" | v0.6 本来没走 in_key;实现为 key=None 保持旧公式、key 非空才吸附 | 接受:不破坏 legacy 逐样本 |
157
- | 同段重复 cue 的 gain | §3.4 写 -2/-4/-6 | 每次再降 2 dB 不封顶(items 很多时会到 -2n) | 待观察:模板 5 条时最末一条 -8 dB;若听感偏轻改 `_cue_gain_decay` 一行 |
158
- | `content_seed` 实现位置 | §3.1 放 sfxkit | filmkit 与 sfxkit 各有一份逐字相同的实现(filmkit 走函数内延迟 import,模块级 from 会改导入时机) | 接受:两侧同参同值已核对;注释互相指认 |
131
+ | 项 | 现状 |
132
+ |---|---|
133
+ | `content_seed` 实现位置 | `filmkit` 与 `sfxkit` 各有一份逐字相同的实现(`filmkit` 走函数内延迟 import,模块级 `from` 会改导入时机);两侧同参同值 |
134
+ | 同段重复 cue 的 gain | 每次再降 2 dB、不封顶(items 很多时会到 -2n);模板 5 条时最末一条 -8 dB;要改听感就改 `_cue_gain_decay` 一行 |
135
+ | `roulette_tick` 调性 | `key=None` 走原公式(D 小调五声);`key` 非空才吸附 |
159
136
 
160
- **回归基线**(改前先录、改后逐条比对):
137
+ **回归基线**:
161
138
 
162
139
  - sfxkit 9 条渲染的 sha256(ui_blip ×3 / answer_chime / text_stream ×2 / data_chirp / roulette_tick ×2)逐条相同。
163
- - 配乐 legacy 路径(scorekit,style=None)6 s 渲染逐样本与 v0.6 相同(v1.0.1 起该路径只保留合成原语,不再承担成片配乐)。
140
+ - scorekit 的 `render()` 只承担合成原语(成片配乐走 musicgen):同 seed 逐样本一致(`scorekit.py --selftest` 第 18 条)。
@@ -1,8 +1,8 @@
1
1
  # 音频:采样级混音、配乐与音效
2
2
 
3
- > **v0.3.0 新增三节**(§10–§12 是新的推荐做法,§1–§9.5 是底层原理与手写示例,仍然有效):
3
+ > 读法:§1–§9.5 是底层原理与手写示例;§10–§12 是推荐做法——
4
4
  > §10 提示点系统(声音跟着画面走)、§11 程序化配乐(musicgen 参数化编排)、§12 程序化音效(sfxkit)。
5
- > 先读 §10——它决定了另外两节怎么用。
5
+ > 先读 §10,它决定了另外两节怎么用。
6
6
 
7
7
  ## 1. 为什么不用 ffmpeg 混音
8
8
 
@@ -28,8 +28,8 @@ wav = bus.mix(normalize=True, fade=0.6)
28
28
  | 声道 | 单声道总线 → 双声道输出 | 旁白单声道足够 |
29
29
  | 母带响度 | **-16 LUFS ± 1** | 网络视频标准 |
30
30
  | 真峰 | **≤ -1 dBTP** | AAC 编码会推高约 0.9 dB,混流前留 1.4 dB 余量 |
31
- | 旁白与间隙比 | ≥ 12 dB | 人声必须明显突出(v0.7.1 起有代码保障:`mix_film` 旁白侧链) |
32
- | 配乐电平 | 旁白以下 12–18 dB | 否则听不清字(v0.7.1 默认 `music.gain_db=-12`,旧版 -1.5 不合格) |
31
+ | 旁白与间隙比 | ≥ 12 dB | 人声必须明显突出(有代码保障:`mix_film` 旁白侧链) |
32
+ | 配乐电平 | 旁白以下 12–18 dB | 否则听不清字(默认 `music.gain_db=-12`;-1.5 时背景与台词几乎等高,不合格) |
33
33
 
34
34
  ## 3. 响度链路(三步)
35
35
 
@@ -42,7 +42,7 @@ wav = bus.mix(normalize=True, fade=0.6)
42
42
  af = K.loudnorm_two_pass("work/audio.wav", target_i=-16.0, target_tp=-1.5)
43
43
  ```
44
44
 
45
- > **下面是"设计建议",本仓库没有实现**(以前这段被写成既有链路,容易误导):
45
+ > **下面是"设计建议",本仓库没有实现**:
46
46
  > 「各轨目标 LUFS 旁白 -13.5 / 音效 -24.5 / 配乐 -26.5」+「前瞻限幅器补差额」——
47
47
  > 这套是专业分轨母带流程,本项目用"相对增益 + 最终 loudnorm"替代,**代码里没有逐轨 LUFS 对齐,
48
48
  > 也没有前瞻限幅器**。要那套效果得自己在 `mix_film` 之后接。
@@ -130,20 +130,20 @@ def loop_noise(dur=8.0, sr=48000, lo=180, hi=900, seed=5):
130
130
 
131
131
  ```python
132
132
  # edge-tts 每条尾部约 0.95 s 静音,55 条累计 61.5 s
133
- # v0.2 起 filmkit.tts(trim=True) 默认裁掉首尾静音,并把裁剪后的 wav 作为 decode 源:
133
+ # filmkit.tts(trim=True) 默认裁掉首尾静音,并把裁剪后的 wav 作为 decode 源:
134
134
  # meta.path -> xxx.trim.wav, meta.dur = 裁剪后实测时长;原 mp3 保留在 meta.mp3
135
- # 缓存 key = sha1(text|voice|rate|pitch);改台词会自动重新合成(v0.1 只判文件存在)
135
+ # 缓存 key = sha1(text|voice|rate|pitch);改台词会自动重新合成
136
136
  pcm = K.decode_pcm(mp3)
137
137
  trimmed = K.trim_silence(pcm, thresh_db=-42.0, keep_ms=80) # 不是 trim_tail:本仓库没这个函数
138
138
 
139
139
  # 轻 EQ:200 Hz 以下衰减 2 dB(去闷),3 kHz 提 1 dB(清晰度)
140
140
  ```
141
141
 
142
- ### 7.1 声线(edge voice id)与语气(tone)—— v1.0.1 起没有 persona 身份表
142
+ ### 7.1 声线(edge voice id)与语气(tone):没有 persona 身份表
143
143
 
144
- > **v0.5 的 persona 表(`bright_female` / `narrator_male` 之类 9 个身份)在 v1.0.0 已删除。**
145
- > 现在 `voice` 只认**裸 edge-tts voice id**(如 `zh-CN-YunxiNeural`)+ 实测响度补偿表
146
- > `assets/voice_loudness.json`;tone 的 6 档(hook/body/quote/outro/emphasis/calm)保留。
144
+ > **没有 persona 身份表**(`bright_female` / `narrator_male` 之类 9 个身份):`voice` 只认
145
+ > **裸 edge-tts voice id**(如 `zh-CN-YunxiNeural`)+ 实测响度补偿表 `assets/voice_loudness.json`;
146
+ > tone 的 6 档(hook/body/quote/outro/emphasis/calm)照旧。
147
147
 
148
148
  ```json
149
149
  {"voice": {"id": "zh-CN-YunxiNeural",
@@ -160,14 +160,15 @@ python <SKILL_DIR>/scripts/voicekit.py --plan film.json # 打印逐句声线
160
160
  - **voice id** 直接就是 edge-tts 的音色名;`--list` 里带的是**实测响度补偿**(不同音色天然差 3–7 dB,
161
161
  实测跨度 6.0 dB)。写了表里没有的音色,补偿回落 0 = 旁白电平没校准。
162
162
  - **tone** 决定相对偏移与句前/句后停顿:hook / body / quote / outro / emphasis / calm
163
- (`body` 严格等于 v0.4 基准:lead 0.62 / tail 0.92 / 所有 delta=0)。
163
+ (`body` 是基准档:lead 0.62 / tail 0.92 / 所有 delta=0)。
164
164
  - **内容规则**会自动升级语气:引号句 → quote(并换到 quote 声线)、数字密集 → emphasis、
165
165
  问句/感叹 → 稍快稍亮、长句 → 降速加尾部留白;显式 `lines[].voice/tone` 优先级最高。
166
166
  - **jitter**(默认 0.35)用 sha1 派生 ±1.5% / ±1Hz / ±0.04s 的微扰,去掉"每句等距等音量"的机械感。
167
167
  - **governor** 限制全片最多 `max_distinct` 把声音、相邻句不换人 —— 超过就变成广播剧,不是旁白。
168
168
 
169
- **缓存键 v2**:`sha1(v2|text|voice|rate|pitch|volume|trim|trim_db)`。v0.4 的 key 不含 trim,
170
- 于是把 `trim_silence` 改成 false 仍会命中旧的裁剪版 —— "关了裁剪其实没关"。v1 key 仍然保留给老缓存。
169
+ **缓存键**:`sha1(v2|text|voice|rate|pitch|volume|trim|trim_db)`。key 必须含 trim——
170
+ 不含 trim 时把 `trim_silence` 改成 false 仍会命中裁剪版,"关了裁剪其实没关"。
171
+ `v1` key 仍给既有缓存保留。
171
172
 
172
173
  ## 8. 混音自检(必做)
173
174
 
@@ -187,7 +188,7 @@ print("peak:", np.max(np.abs(bus.bus))) # 期望 < 0.99
187
188
  |---|---|---|
188
189
  | 后半段静音 | ffmpeg 多输入混流 | numpy 总线 |
189
190
  | 爆音/削波 | 先 astype 再判 dtype | 先判 dtype 再转;打印中间峰值 |
190
- | 听起来糊 | 配乐电平过高 | 配乐压到旁白以下 12–18 dB(v0.7.1 默认 -12 且带旁白侧链) |
191
+ | 听起来糊 | 配乐电平过高 | 配乐压到旁白以下 12–18 dB(默认 -12 且带旁白侧链) |
191
192
  | 听不清台词 | 背景与人声响度接近 | `music.gain_db` 降到 -12、`voice_duck_db` 开 6;别用"整体拉高"救 |
192
193
  | 响度上不去 | AAC 推高真峰后限幅 | 混流前留 1.4 dB 余量 |
193
194
  | 环境音接缝咔 | 直接拼接 | 频域整周期噪声 |
@@ -196,9 +197,6 @@ print("peak:", np.max(np.abs(bus.bus))) # 期望 < 0.99
196
197
 
197
198
  ## 9.5 两条混音路线的取舍(含实测坑)
198
199
 
199
- > 编号说明:本小节原为「§10」,与后加的「§10 提示点系统」重号(v0.3 加节时的历史遗留)。
200
- > 现改为 §9.5,保证 §10–§12 三段新内容编号唯一。
201
-
202
200
  ### 路线 A:numpy 采样级叠加(推荐)
203
201
 
204
202
  完全确定、精度 1/48000 s、可单层重放、可中途打印峰值。`filmkit.AudioBus` 就是这条。
@@ -234,13 +232,13 @@ bus[i0:i0+len(src)] += src * gain # 精确到 1/48000 秒
234
232
 
235
233
  ### 首镜必须从 0 开始
236
234
 
237
- 历史事故:第一个镜头 `start ≠ 0`,导致全片音画错位 1.2 s(画面先跑,声音从 0 开始)。**时间轴第一段 start 必须严格等于 0**,且 `voice_start ≥ start`。
235
+ 典型事故:第一个镜头 `start ≠ 0`,全片音画错位(实测 1.2 s:画面先跑,声音从 0 开始)。**时间轴第一段 start 必须严格等于 0**,且 `voice_start ≥ start`。
238
236
 
239
237
  ### BGM 循环的淡出陷阱
240
238
 
241
239
  BGM 素材自带淡出尾音,被 `-stream_loop` 循环后会在每个接缝处"死一下"。做法:循环时只取素材的**稳态段**(裁掉头尾淡入淡出),或改用频域整周期噪声。
242
240
 
243
- ## 10. 提示点系统(v0.3.0,先读这一节)
241
+ ## 10. 提示点系统(先读这一节)
244
242
 
245
243
  **问题**:手工把音效放到"第 12.5 秒",只要片子里改一句台词、加一个停留窗口、把 BPM 从 120 改成 100,
246
244
  所有音效就全错位。改一次要重新对一遍时间——这就是为什么很多程序化视频"声音和画面对不上"。
@@ -306,12 +304,10 @@ python qa_frames.py out/film.mp4 --cues work/cues.json # 看音画同步 dt
306
304
  **音画同步 dt > 60 ms 就是错**:人眼/耳对 40–60 ms 以内的错位不敏感,超过就会"感觉哪里不对但说不出来"。
307
305
  dt 大的典型原因:cue 挂在字幕出现的时刻上,而画面重击晚 100 ms——应该挂在动画时刻上。
308
306
 
309
- ## 11. 程序化配乐(musicgen,v1.0.1)
307
+ ## 11. 程序化配乐(musicgen)
310
308
 
311
- > **v1.0.1 起没有「配乐模板」了。** 以前这一节讲的是 scorekit 的 9 套 `MUSIC_STYLES`
312
- > 风格表(选一个 id 得到一种配乐性格)——那套表已删除。现在配乐由 `musicgen.py`
313
- > 按参数**现场编排**:没有可选曲风,只有参数。下面仍然是「一张和弦表贯穿全片」的技术思路,
314
- > 实现落到了参数化编排器上。
309
+ > **没有「配乐模板」。** 配乐由 `musicgen.py` 按参数**现场编排**:没有可选曲风,也没有风格表,
310
+ > 只有参数。下面仍是「一张和弦表贯穿全片」的技术思路,实现落在参数化编排器上。
315
311
 
316
312
  ```python
317
313
  import musicgen as MG
@@ -356,7 +352,7 @@ MG.save("work/music.wav", samples)
356
352
  - 同 seed 必须逐采样一致(`np.random.default_rng(seed)`,不许用全局 `np.random`)。
357
353
  - 配乐只保证"不削波、不直流、峰值 ≤ -1 dBFS";**最终响度交给 ffmpeg loudnorm**。
358
354
 
359
- ## 12. 程序化音效(sfxkit,v0.3.0)
355
+ ## 12. 程序化音效(sfxkit)
360
356
 
361
357
  ### 12.1 为什么不"画波形"
362
358
 
@@ -423,47 +419,47 @@ out.append({"at": "%s+0.3" % s["id"], "type": "text_stream",
423
419
  `sfxkit` 的算法思想改编自 `ai-history-film`(MIT)的 `sfx/` 包,实现为本项目单文件重写;
424
420
  文件头已注明出处。配方名、参数名保持与上游一致,方便对照上游文档。
425
421
 
426
- ## 13. 片间差异与片内分离(v0.7 新增)
422
+ ## 13. 片间差异与片内分离
427
423
 
428
- > 完整契约与改动清单见 **references/audio-variation.md**;这里只记"怎么用、怎么验"。
424
+ > 完整契约见 **references/audio-variation.md**;这里只记"怎么用、怎么验"。
429
425
 
430
- ### 13.1 为什么 v0.5 之后还会"差不多"
426
+ ### 13.1 为什么"差不多"
431
427
 
432
- v0.5 的 style pack 决定的是**风格**(8 套),不是**这一部的具体样子**:
428
+ "每片都不一样"不能只靠随机,得让**内容进结构**;下面三种做法就会让片子"差不多":
433
429
 
434
430
  - 配乐:内容指纹只进 `seed`(音色细节),和弦进行/动机/鼓型/段落骨架全固定
435
- → 同风格下两段 20 s 配乐相关性 **0.966**(实测)。
436
- - 音效:`CueBus.build_audio` 的 seed 是 `seed_base + i`(只跟 cue 顺序)
431
+ → 两段 20 s 配乐相关性 **0.966**(实测)。
432
+ - 音效:`CueBus.build_audio` 的 seed 取 `seed_base + i`(只跟 cue 顺序)
437
433
  → 同一批 cue 跨片逐样本相同。
438
434
  - 片内:提示音硬编码 D 小调五声、配乐过门与 sfx 转场在同一段落边界撞车、duck 只有 3 dB。
439
435
 
440
- ### 13.2 现在怎么变
436
+ ### 13.2 怎么变
441
437
 
442
438
  | 层 | 行为 | 开关 |
443
439
  |---|---|---|
444
- | 配乐和声/动机/鼓型 | v0.7 时代由内容指纹驱动结构变奏(每两小节、±1 级数、ghost 击);**v1.0.1 起改由 musicgen 的编排参数直接决定**(没有可挑的曲风表) | `music.seed_from_content` 仍控制 seed 是否掺指纹;结构差来自 `music.sections` / `music.progressions` 等参数,而不是「换一套风格」 |
445
- | 音效 seed | `content_seed(base, style, fp, type, t)`,随内容变 | `sfx.content_seed`(默认 true;false = 旧 seed_base+i) |
440
+ | 配乐和声/动机/鼓型 | 由 musicgen 的编排参数直接决定:`sections` / `progressions` / `motif` / `drums` 参与事件展开(没有可挑的曲风表) | `music.seed_from_content` 控制 seed 是否掺内容指纹;结构差来自上述编排参数,而不是「换一套风格」 |
441
+ | 音效 seed | `content_seed(base, style, fp, type, t)`,随内容变 | `sfx.content_seed`(默认 true;false = 退回 `seed_base + i`) |
446
442
  | 提示音调性 | `in_key(freq, key)` 跟随配乐调性 | `CueBus.build_audio(key=...)`,render.py 自动传 |
447
443
  | 段落边界 | [边界-2.2s, 边界] 内的 whoosh/riser 自动 -3 dB | `sfx.duck_transitions`(默认 true) |
448
444
  | duck | 按风格 3.0–5.5 dB(默认 4.0) | `music.duck_db` 显式覆盖 |
449
445
 
450
- **legacy 兼容**:不写 `style` 字段 = 全部旧行为(配乐逐样本一致、音效 seed_base+i、提示音 Dm 五声)。
446
+ **兼容开关**:`sfx.content_seed: false` 时音效退回 `seed_base + i`(逐样本可复现);`key` 为空时提示音回落 D 小调五声。
451
447
 
452
- ### 13.2.1 旁白优先(v0.7.1)
448
+ ### 13.2.1 旁白优先
453
449
 
454
450
  视频以旁白为主,**BGM 与音效是可选层**:它们负责情绪与重音,不能抢字。具体做法:
455
451
 
456
452
  | 手段 | 默认值 | 说明 |
457
453
  |---|---|---|
458
- | 配乐基线 | `music.gain_db=-12` | 旧版 -1.5 时背景与台词几乎等高;-12 是"人声明显压住背景"的经验线 |
454
+ | 配乐基线 | `music.gain_db=-12` | -1.5 时背景与台词几乎等高;-12 是"人声明显压住背景"的经验线 |
459
455
  | 旁白侧链(配乐) | `voice_duck_db=6` | 有人声的地方再压 6 dB;间隙由包络自动恢复(不会整片闷住) |
460
456
  | 旁白侧链(音效) | `voice_duck_sfx_db=3` | 提示音/转场也让人声 |
461
457
  | 语音阈值 | `voice_thresh_db=-42` / `voice_floor_db=-60` | 10 ms 块 RMS 映射成 0~1 的活动度,起音快、释放慢 |
462
458
  | 旁白自身 | `voice_gain=1.0` | **不被任何侧链影响**——旁白优先不等于"压人声" |
463
459
  | SNR 兜底 | `guard_snr_db=12` / `guard_cap_db=30` | 混音后实测人声活跃区的人声/背景差,不足就整体补压背景(只压背景) |
464
460
 
465
- 实测(真实 edge-tts 旁白 + scorekit 配乐):旧混音 **SNR -4.2 dB**(背景比台词还响,就是"听不清"的原因),
466
- 新混音 **SNR 12.0 dB**;合成信号口径 15.0 dB,真实配乐素材口径 12.9 dB。
461
+ 实测(真实 edge-tts 旁白 + scorekit 配乐):不做上面这些处理时 **SNR -4.2 dB**(背景比台词还响,
462
+ 就是"听不清"的原因);按上表默认值 **SNR 12.0 dB**;合成信号口径 15.0 dB,真实配乐素材口径 12.9 dB。
467
463
  设 `voice_duck_db=0` / `voice_duck_sfx_db=0` / `guard_snr_db=0` 可分别关掉对应机制。
468
464
 
469
465
  ### 13.3 验收:别靠耳朵,跑脚本
@@ -472,13 +468,13 @@ v0.5 的 style pack 决定的是**风格**(8 套),不是**这一部的具
472
468
  python <REPO>/tools/audio_similarity.py --strict # 在包根执行
473
469
  ```
474
470
 
475
- A 组量"同风格换内容指纹"和"换风格"的相关性;B 组量"跨片音效是否还逐样本相同"(同时验
476
- legacy 兼容);C 组读 `render.py` 的 duck 默认值并打印质心对照;
471
+ A 组量"同参数换内容指纹"和"换编排参数"的相关性;B 组量"跨片音效是否还逐样本相同"(同时验
472
+ 位置派生可复现);C 组读 `render.py` 的 duck 默认值并打印质心对照;
477
473
  D 组构造"有人声段 + 间隙",量人声段旁白/背景响度差。任何一项不达标 exit 1。
478
474
 
479
475
  ### 13.4 改音频代码后的自检清单
480
476
 
481
- 1. `python <REPO>/tools/audio_similarity.py --strict` —— A/B/C 三组达标(在包根执行)。
482
- 2. `python <SKILL_DIR>/scripts/selfcheck.py` —— **67 条**全 PASS(第 33 条盯 legacy 逐样本兼容,第 35–38 条盯旁白优先,第 39 条盯音频边界)。
477
+ 1. `python <REPO>/tools/audio_similarity.py --strict` —— A/B/C/D 四组达标(在包根执行)。
478
+ 2. `python <SKILL_DIR>/scripts/selfcheck.py` —— **69 条**全 PASS(第 33 条盯内容派生 seed、第 35–38 条盯旁白优先,第 39 条盯音频边界)。
483
479
  3. `python musicgen.py --selftest` / `python sfxkit.py --selftest` —— 各自全 PASS(musicgen 的 26 项覆盖编排与确定性)。
484
480
  4. 改 `assets/scenes.py` 的 `cues()` 后,跑一次 `--stage cues` 看类型分布是否覆盖 hook/body/quote/outro。
@@ -1,7 +1,7 @@
1
1
  # 改了东西要重跑什么(改动影响表)
2
2
 
3
- > 这张表的存在理由:**"我就改一句台词,要重渲全片吗?"** 这个问题以前只能靠 agent 猜。
4
- > 现在有两个更硬的依据:`render.py --stage explain`(机器判断哪些阶段过时)与本表(人读的因果)。
3
+ > 这张表的存在理由:**"我就改一句台词,要重渲全片吗?"** ——别靠猜。
4
+ > 有两个依据:`render.py --stage explain`(机器判断哪些阶段过时)与本表(人读的因果)。
5
5
  > 两者不一致时**以 `--stage explain` 为准**——它看的是文件的真实指纹,不是经验。
6
6
 
7
7
  ## 0. 先记住三条
@@ -1,10 +1,10 @@
1
- # v0.8 三道新门禁:渲前预检 / 全片 VMAF / ASR 回读
1
+ # 渲染前预检 / 全片 VMAF / ASR 回读三道门禁
2
2
 
3
- > 本页只讲这三个**模块**(`G0` / `G11` / `G12`)。账本里 v0.8 还有第四行 `G10` 词级时间戳,
3
+ > 本页只讲这三个**模块**(`G0` / `G11` / `G12`)。账本里还有第四行 `G10` 词级时间戳,
4
4
  > 它的判据与关闭方式在 [word-timeline.md](word-timeline.md)。
5
5
 
6
- > 三个模块、三道账本门禁,都是**可选**的(不拷对应 .py 就记 `skipped` 并写明原因,不会让渲染失败)。
7
- > 三档语义沿用 v0.6:`pass` = 这次真的查了且通过;`fail` = 查了没过;
6
+ > 三个模块、三道账本门禁,都是**可选**的(模块不可用、web 引擎或显式关闭就记 `skipped` 并写明原因,不会让渲染失败)。
7
+ > 三档语义:`pass` = 这次真的查了且通过;`fail` = 查了没过;
8
8
  > `skipped` = **这次没执行**——它**不等于通过**,交付时要逐项说明为什么没跑。
9
9
 
10
10
  | 模块 | 门禁 id | 什么时候跑 | 实测代价 | 关掉 |
@@ -20,7 +20,7 @@
20
20
  ### 1.1 为什么要有它
21
21
 
22
22
  缺字形(✓ / emoji / 生僻字)和坏 TTS 音频都是"**渲染完才报**"的问题:`filmkit.glyph_missing_chars()`
23
- 本身是准的,但它之前在 `layout` / `qa` 才被调用——等 870 帧渲完,才发现某句 `big` 里有个方块。
23
+ 本身是准的,但只在 `layout` / `qa` 才被调用——等 870 帧渲完,才发现某句 `big` 里有个方块。
24
24
  preflight 不新写检测器,只把现成检测**前移**,并把结论整理成「字符 + 字段 + 句 id」:
25
25
 
26
26
  ```
@@ -136,9 +136,9 @@ qavmaf 会在报告里打 warning 提醒这一点,但**不因此改判**——
136
136
  所以这里只写"这一跑"的数,不写承诺。纯 libvmaf 比较(参考帧已就绪)约 1.2× 实时——
137
137
  **别把"纯比较的 1.2×"当成端到端成本**。
138
138
 
139
- **v0.8 之后 qa 比渲染还贵**:29 s 示例片 `--stage all --cost` 实测总 **47.3 s**,
140
- 其中 `frames` 12.5 s、`qa` 23.9 s(VMAF 14.2 + ASR 8.7 + 其余);v0.7 的全链约 28 s。
141
- 决策影响:**批量出片的瓶颈从渲染转到质检**——想压回渲染量级就 `--vmaf off` 或 `--vmaf sample`。
139
+ **qa 比渲染还贵**:29 s 示例片 `--stage all --cost` 实测总 **47.3 s**,
140
+ 其中 `frames` 12.5 s、`qa` 23.9 s(VMAF 14.2 + ASR 8.7 + 其余)。
141
+ 决策影响:**批量出片的瓶颈在质检而不是渲染**——想压回渲染量级就 `--vmaf off` 或 `--vmaf sample`。
142
142
 
143
143
  ### 2.6 单独跑 / 可证伪性
144
144
 
@@ -158,7 +158,7 @@ python qavmaf.py --selftest # 内置自检:验证三档语义不许"没查
158
158
 
159
159
  ### 3.1 为什么
160
160
 
161
- v0.7 及以前的质检只看"容器 / 响度 / 帧 / 音画同步",都建立在一个假设上:
161
+ 容器 / 响度 / 帧 / 音画同步这几项质检都建立在一个假设上:
162
162
  **成片音轨里念的就是剧本那几句话**。可是 TTS 缓存串了、换声线时漏了某句、混音把某段压没了、
163
163
  重新渲了但音轨是旧的——这些从容器和波形上都看不出来。把音轨转回文字逐句对账才能发现。
164
164
 
@@ -233,8 +233,6 @@ python qaasr.py out/film.mp4 --timeline work/timeline.json --script film.json \
233
233
  # 退出码:0=pass;1=fail;2=skipped
234
234
  # 阈值只有一份权威:qaasr.py 的 QA_RECALL_PASS=0.85 / QA_RECALL_WARN=0.72,
235
235
  # render.py 的 G12 直接引用同一组常量(CLI 默认值与 --recall-pass/--recall-warn 同源)。
236
- # v0.8.0 之前 CLI 默认是 0.95/0.90,于是同一部片 render 判 pass、单独跑 CLI 判 fail——
237
- # 两套权威已合并,别再按旧口径传 0.95。
238
236
  python qaasr.py --selftest # 不联网自检
239
237
  # 离线回放(CI 用,不加载模型):
240
238
  python qaasr.py film.mp4 --timeline ... --asr-json <已有 ASR 结果.json>