@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.
- package/CHANGELOG.md +49 -0
- package/README.md +84 -108
- package/THIRD_PARTY_NOTICES.md +2 -2
- package/package.json +1 -1
- package/schemas/design.schema.json +5 -5
- package/schemas/film.schema.json +31 -31
- package/skills/video-studio/SKILL.md +168 -268
- package/skills/video-studio/assets/film.json +2 -3
- package/skills/video-studio/assets/golden_frames.json +11 -11
- package/skills/video-studio/assets/scenes.py +92 -49
- package/skills/video-studio/references/{style-packs.md → appearance.md} +17 -15
- package/skills/video-studio/references/audio-variation.md +73 -96
- package/skills/video-studio/references/audio.md +40 -44
- package/skills/video-studio/references/change-impact.md +2 -2
- package/skills/video-studio/references/{v08-quality.md → delivery-gates.md} +9 -11
- package/skills/video-studio/references/frame-qa.md +47 -15
- package/skills/video-studio/references/pipeline.md +4 -4
- package/skills/video-studio/references/pitfalls.md +22 -3
- package/skills/video-studio/references/proposal.md +19 -36
- package/skills/video-studio/references/quality-gates.md +11 -11
- package/skills/video-studio/references/scene-authoring.md +2 -2
- package/skills/video-studio/references/scene-grammar.md +17 -24
- package/skills/video-studio/references/sfx-design.md +7 -5
- package/skills/video-studio/references/web-engine.md +7 -7
- package/skills/video-studio/references/word-timeline.md +7 -7
- package/skills/video-studio/requirements.txt +9 -13
- package/skills/video-studio/scripts/designkit.py +84 -89
- package/skills/video-studio/scripts/filmcheck.py +19 -19
- package/skills/video-studio/scripts/filmkit.py +2949 -2948
- package/skills/video-studio/scripts/gates.py +4 -4
- package/skills/video-studio/scripts/grammar.py +10 -10
- package/skills/video-studio/scripts/musicgen.py +3 -4
- package/skills/video-studio/scripts/preflight.py +5 -5
- package/skills/video-studio/scripts/previewkit.py +11 -12
- package/skills/video-studio/scripts/propose.py +11 -11
- package/skills/video-studio/scripts/qa_frames.py +34 -35
- package/skills/video-studio/scripts/qaasr.py +26 -7
- package/skills/video-studio/scripts/qavmaf.py +19 -19
- package/skills/video-studio/scripts/render.py +137 -141
- package/skills/video-studio/scripts/scorekit.py +21 -23
- package/skills/video-studio/scripts/selfcheck.py +208 -135
- package/skills/video-studio/scripts/sfxkit.py +27 -26
- package/skills/video-studio/scripts/stagewatch.py +4 -4
- package/skills/video-studio/scripts/stylekit.py +77 -282
- package/skills/video-studio/scripts/voicekit.py +43 -44
- package/skills/video-studio/scripts/webengine.py +62 -64
- package/tools/audio_similarity.py +12 -12
- package/tools/ci_check.py +16 -16
- package/tools/estimate.py +24 -25
- package/tools/golden_frames.py +17 -18
- package/tools/silhouette_qa.py +13 -14
|
@@ -1,40 +1,36 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 音频差异机制 —— 接口契约
|
|
2
2
|
|
|
3
|
-
>
|
|
4
|
-
>
|
|
5
|
-
>
|
|
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
|
-
>
|
|
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. 跨视频配乐雷同 |
|
|
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
|
-
|
|
|
24
|
-
|
|
|
25
|
-
|
|
|
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
|
|
28
|
+
### 3.1 sfxkit
|
|
33
29
|
|
|
34
30
|
```python
|
|
35
|
-
# 1)
|
|
31
|
+
# 1) 调性跟随:key 参数,默认 None = 固定 D 小调五声
|
|
36
32
|
def in_key(freq: float, key: str | None = None) -> float:
|
|
37
|
-
"""key=None ->
|
|
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(
|
|
42
|
+
"""稳定的 32 位非负 int:sha1("<固定盐>|<base>|<style>|<content_fp>|<recipe>|<t 三位小数>")
|
|
43
|
+
-> int(h[:8], 16) % 9000 + 1000。"""
|
|
47
44
|
```
|
|
48
45
|
|
|
49
|
-
|
|
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, #
|
|
55
|
-
content_fp=None, # 内容指纹 full;None =
|
|
56
|
-
key=None): # 配乐调性;None =
|
|
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
|
-
|
|
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.
|
|
68
|
+
### 3.3 assets/scenes.py 的 cues()
|
|
82
69
|
|
|
83
|
-
|
|
70
|
+
按画面事件生成,**类型按场景 kind 与内容变化**:
|
|
84
71
|
|
|
85
72
|
- `hook`:段首 `impact`(size 按字数 0.9~1.3),落定 `riser`。
|
|
86
|
-
- `body
|
|
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
|
-
-
|
|
91
|
-
-
|
|
77
|
+
- 不许出现"每段都一样的三连击":同类型 cue 在一段内重复时,`gain` 必须递减(-2/-4/-6),pitch 必须不同。
|
|
78
|
+
- **对齐**:stagger 对齐逻辑(与 `_draw_items` 同节拍)保留,返回 schema 不变。
|
|
92
79
|
|
|
93
|
-
### 3.
|
|
80
|
+
### 3.4 assets/film.json 夹具
|
|
94
81
|
|
|
95
|
-
模板里的 `sfx.cues`
|
|
82
|
+
模板里的 `sfx.cues` 只保留 1 条转场 whoosh 作为示例,并注明"具体提示点交给 `scenes.py` 的 `cues()` 钩子"
|
|
83
|
+
(固定三条 whoosh/riser/impact 是"跨片雷同"的直接来源之一)。
|
|
96
84
|
|
|
97
|
-
-
|
|
98
|
-
- `sfx` 增加可选字段 `content_seed: true`(默认 true):走内容派生 seed;显式 false 回到旧行为。
|
|
85
|
+
- `sfx` 的 `content_seed: true`(默认):走内容派生 seed;显式 `false` 回到 `seed_base + i`。
|
|
99
86
|
|
|
100
|
-
## 4. render.py
|
|
87
|
+
## 4. render.py 接线
|
|
101
88
|
|
|
102
|
-
- `
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
| 指标 |
|
|
101
|
+
| 指标 | 目标 | 实测 | 不这么做时 |
|
|
113
102
|
|---|---|---|---|
|
|
114
|
-
|
|
|
115
|
-
|
|
|
116
|
-
|
|
|
117
|
-
|
|
|
118
|
-
|
|
|
119
|
-
|
|
|
120
|
-
|
|
|
121
|
-
|
|
122
|
-
>
|
|
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–
|
|
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
|
|
137
|
-
2. `python skills/video-studio/scripts/musicgen.py --selftest` 全 PASS
|
|
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.
|
|
141
|
-
|
|
142
|
-
## 6. 协作规则
|
|
125
|
+
5. 音效位置派生的逐样本可复现:`selfcheck.py` 守着这条(`sfx.content_seed=false` 时与 `seed_base + i` 同值)。
|
|
143
126
|
|
|
144
|
-
|
|
145
|
-
- 改完在 `CHANGELOG.md` 追加的条目由 lead 统一写(teammate 不要改 CHANGELOG)。
|
|
146
|
-
- 任何接口拿不准的地方,按本文件逐字实现;本文件没写的自由发挥请控制在"不改变签名"的范围内。
|
|
147
|
-
- 完成后向 lead 报告:改了哪些函数、自测输出摘要、与契约的偏差(如果有)。
|
|
127
|
+
## 6. 实现备注
|
|
148
128
|
|
|
149
|
-
|
|
129
|
+
几处落地细节(不改契约本身):
|
|
150
130
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
|
154
|
-
|
|
155
|
-
|
|
|
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
|
-
-
|
|
140
|
+
- scorekit 的 `render()` 只承担合成原语(成片配乐走 musicgen):同 seed 逐样本一致(`scorekit.py --selftest` 第 18 条)。
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# 音频:采样级混音、配乐与音效
|
|
2
2
|
|
|
3
|
-
>
|
|
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 |
|
|
32
|
-
| 配乐电平 | 旁白以下 12–18 dB |
|
|
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
|
-
#
|
|
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)
|
|
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
|
|
142
|
+
### 7.1 声线(edge voice id)与语气(tone):没有 persona 身份表
|
|
143
143
|
|
|
144
|
-
>
|
|
145
|
-
>
|
|
146
|
-
>
|
|
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`
|
|
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
|
-
|
|
170
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
|
307
|
+
## 11. 程序化配乐(musicgen)
|
|
310
308
|
|
|
311
|
-
>
|
|
312
|
-
>
|
|
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
|
|
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.
|
|
422
|
+
## 13. 片间差异与片内分离
|
|
427
423
|
|
|
428
|
-
>
|
|
424
|
+
> 完整契约见 **references/audio-variation.md**;这里只记"怎么用、怎么验"。
|
|
429
425
|
|
|
430
|
-
### 13.1 为什么
|
|
426
|
+
### 13.1 为什么"差不多"
|
|
431
427
|
|
|
432
|
-
|
|
428
|
+
"每片都不一样"不能只靠随机,得让**内容进结构**;下面三种做法就会让片子"差不多":
|
|
433
429
|
|
|
434
430
|
- 配乐:内容指纹只进 `seed`(音色细节),和弦进行/动机/鼓型/段落骨架全固定
|
|
435
|
-
→
|
|
436
|
-
- 音效:`CueBus.build_audio` 的 seed
|
|
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
|
-
| 配乐和声/动机/鼓型 |
|
|
445
|
-
| 音效 seed | `content_seed(base, style, fp, type, t)`,随内容变 | `sfx.content_seed`(默认 true;false =
|
|
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
|
-
|
|
446
|
+
**兼容开关**:`sfx.content_seed: false` 时音效退回 `seed_base + i`(逐样本可复现);`key` 为空时提示音回落 D 小调五声。
|
|
451
447
|
|
|
452
|
-
### 13.2.1
|
|
448
|
+
### 13.2.1 旁白优先
|
|
453
449
|
|
|
454
450
|
视频以旁白为主,**BGM 与音效是可选层**:它们负责情绪与重音,不能抢字。具体做法:
|
|
455
451
|
|
|
456
452
|
| 手段 | 默认值 | 说明 |
|
|
457
453
|
|---|---|---|
|
|
458
|
-
| 配乐基线 | `music.gain_db=-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
|
|
466
|
-
|
|
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 组量"
|
|
476
|
-
|
|
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` —— **
|
|
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
|
-
> 这张表的存在理由:**"我就改一句台词,要重渲全片吗?"**
|
|
4
|
-
>
|
|
3
|
+
> 这张表的存在理由:**"我就改一句台词,要重渲全片吗?"** ——别靠猜。
|
|
4
|
+
> 有两个依据:`render.py --stage explain`(机器判断哪些阶段过时)与本表(人读的因果)。
|
|
5
5
|
> 两者不一致时**以 `--stage explain` 为准**——它看的是文件的真实指纹,不是经验。
|
|
6
6
|
|
|
7
7
|
## 0. 先记住三条
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 渲染前预检 / 全片 VMAF / ASR 回读三道门禁
|
|
2
2
|
|
|
3
|
-
> 本页只讲这三个**模块**(`G0` / `G11` / `G12
|
|
3
|
+
> 本页只讲这三个**模块**(`G0` / `G11` / `G12`)。账本里还有第四行 `G10` 词级时间戳,
|
|
4
4
|
> 它的判据与关闭方式在 [word-timeline.md](word-timeline.md)。
|
|
5
5
|
|
|
6
|
-
>
|
|
7
|
-
>
|
|
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
|
-
|
|
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
|
-
**
|
|
140
|
-
其中 `frames` 12.5 s、`qa` 23.9 s(VMAF 14.2 + ASR 8.7 +
|
|
141
|
-
|
|
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
|
-
|
|
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>
|