@dsh-gis/dsh-video-studio-skill 0.9.1 → 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 +473 -0
- package/README.md +263 -327
- package/THIRD_PARTY_NOTICES.md +2 -2
- package/package.json +9 -10
- package/schemas/design.schema.json +722 -0
- package/schemas/film.schema.json +31 -42
- package/schemas/proposal.schema.json +133 -0
- package/skills/video-studio/SKILL.md +1216 -1123
- package/skills/video-studio/assets/design_fixture.json +141 -0
- package/skills/video-studio/assets/film.json +2 -4
- package/skills/video-studio/assets/golden_frames.json +13 -10
- package/skills/video-studio/assets/scenes.py +153 -65
- package/skills/video-studio/assets/voice_loudness.json +16 -0
- package/skills/video-studio/references/appearance.md +123 -0
- package/skills/video-studio/references/audio-variation.md +74 -93
- package/skills/video-studio/references/audio.md +64 -66
- package/skills/video-studio/references/change-impact.md +5 -5
- package/skills/video-studio/references/{v08-quality.md → delivery-gates.md} +16 -18
- package/skills/video-studio/references/frame-qa.md +48 -16
- 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 +160 -0
- 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 +40 -42
- package/skills/video-studio/references/sfx-design.md +8 -6
- package/skills/video-studio/references/web-engine.md +8 -8
- package/skills/video-studio/references/word-timeline.md +9 -9
- package/skills/video-studio/requirements.txt +9 -13
- package/skills/video-studio/scripts/designkit.py +2115 -0
- package/skills/video-studio/scripts/filmcheck.py +22 -48
- package/skills/video-studio/scripts/filmkit.py +2980 -2904
- package/skills/video-studio/scripts/gates.py +4 -4
- package/skills/video-studio/scripts/grammar.py +73 -13
- package/skills/video-studio/scripts/musicgen.py +2042 -0
- package/skills/video-studio/scripts/preflight.py +14 -7
- package/skills/video-studio/scripts/previewkit.py +848 -0
- package/skills/video-studio/scripts/propose.py +825 -0
- package/skills/video-studio/scripts/qa_frames.py +73 -39
- package/skills/video-studio/scripts/qaasr.py +26 -7
- package/skills/video-studio/scripts/qavmaf.py +89 -23
- package/skills/video-studio/scripts/render.py +545 -231
- package/skills/video-studio/scripts/scorekit.py +55 -1755
- package/skills/video-studio/scripts/selfcheck.py +815 -215
- package/skills/video-studio/scripts/sfxkit.py +27 -26
- package/skills/video-studio/scripts/stagewatch.py +24 -8
- package/skills/video-studio/scripts/stylekit.py +207 -888
- package/skills/video-studio/scripts/voicekit.py +508 -387
- package/skills/video-studio/scripts/webengine.py +71 -72
- package/tools/audio_similarity.py +64 -37
- package/tools/ci_check.py +20 -14
- package/tools/estimate.py +34 -29
- package/tools/golden_frames.py +72 -28
- package/tools/silhouette_qa.py +520 -168
- package/docs/demo-pil-sheet.png +0 -0
- package/docs/styles/demo-results.json +0 -76
- package/docs/styles/sheet-8-styles.png +0 -0
- package/docs/styles/sheet-ai.png +0 -0
- package/docs/styles/sheet-before-after.png +0 -0
- package/docs/styles/sheet-craft.png +0 -0
- package/docs/styles/sheet-night.png +0 -0
- package/skills/video-studio/assets/score_demo.json +0 -307
- package/skills/video-studio/references/style-packs.md +0 -283
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# 音频:采样级混音、配乐与音效
|
|
2
2
|
|
|
3
|
-
>
|
|
4
|
-
> §10 提示点系统(声音跟着画面走)、§11 程序化配乐(
|
|
5
|
-
> 先读 §10
|
|
3
|
+
> 读法:§1–§9.5 是底层原理与手写示例;§10–§12 是推荐做法——
|
|
4
|
+
> §10 提示点系统(声音跟着画面走)、§11 程序化配乐(musicgen 参数化编排)、§12 程序化音效(sfxkit)。
|
|
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,44 +130,45 @@ 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 声线(
|
|
142
|
+
### 7.1 声线(edge voice id)与语气(tone):没有 persona 身份表
|
|
143
143
|
|
|
144
|
-
|
|
145
|
-
|
|
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)照旧。
|
|
146
147
|
|
|
147
148
|
```json
|
|
148
|
-
{"voice": {"
|
|
149
|
+
{"voice": {"id": "zh-CN-YunxiNeural",
|
|
150
|
+
"map": {"hook": "zh-CN-YunxiaNeural", "body": "zh-CN-YunxiNeural",
|
|
151
|
+
"quote": "zh-CN-YunyangNeural", "outro": "zh-CN-YunxiNeural"},
|
|
149
152
|
"jitter": 0.35, "max_distinct": 3}}
|
|
150
153
|
```
|
|
151
154
|
|
|
152
155
|
```bash
|
|
153
|
-
python voicekit.py --list
|
|
154
|
-
python voicekit.py --plan film.json
|
|
156
|
+
python <SKILL_DIR>/scripts/voicekit.py --list # 音色响度表(裸 id + 实测 gain_db)与 6 档 tone
|
|
157
|
+
python <SKILL_DIR>/scripts/voicekit.py --plan film.json # 打印逐句声线计划(不发请求)
|
|
155
158
|
```
|
|
156
159
|
|
|
157
|
-
- **
|
|
160
|
+
- **voice id** 直接就是 edge-tts 的音色名;`--list` 里带的是**实测响度补偿**(不同音色天然差 3–7 dB,
|
|
161
|
+
实测跨度 6.0 dB)。写了表里没有的音色,补偿回落 0 = 旁白电平没校准。
|
|
158
162
|
- **tone** 决定相对偏移与句前/句后停顿:hook / body / quote / outro / emphasis / calm
|
|
159
|
-
(`body`
|
|
160
|
-
- **内容规则**会自动升级语气:引号句
|
|
161
|
-
问句/感叹
|
|
162
|
-
- **jitter**(默认 0.35)用 sha1 派生 ±1.5% / ±1Hz / ±0.04s 的微扰,去掉"每句等距等音量"
|
|
163
|
+
(`body` 是基准档:lead 0.62 / tail 0.92 / 所有 delta=0)。
|
|
164
|
+
- **内容规则**会自动升级语气:引号句 → quote(并换到 quote 声线)、数字密集 → emphasis、
|
|
165
|
+
问句/感叹 → 稍快稍亮、长句 → 降速加尾部留白;显式 `lines[].voice/tone` 优先级最高。
|
|
166
|
+
- **jitter**(默认 0.35)用 sha1 派生 ±1.5% / ±1Hz / ±0.04s 的微扰,去掉"每句等距等音量"的机械感。
|
|
163
167
|
- **governor** 限制全片最多 `max_distinct` 把声音、相邻句不换人 —— 超过就变成广播剧,不是旁白。
|
|
164
168
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
|
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,25 +304,25 @@ 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. 程序化配乐(
|
|
307
|
+
## 11. 程序化配乐(musicgen)
|
|
308
|
+
|
|
309
|
+
> **没有「配乐模板」。** 配乐由 `musicgen.py` 按参数**现场编排**:没有可选曲风,也没有风格表,
|
|
310
|
+
> 只有参数。下面仍是「一张和弦表贯穿全片」的技术思路,实现落在参数化编排器上。
|
|
310
311
|
|
|
311
312
|
```python
|
|
312
|
-
import
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
seed=7)
|
|
316
|
-
|
|
313
|
+
import musicgen as MG
|
|
314
|
+
# 参数就是「设计」:bpm / key / scale / mood / seed,加可选的编排参数
|
|
315
|
+
samples, report = MG.render(389.0, bpm=120, key="Dm", scale="minor",
|
|
316
|
+
mood="steady", seed=7) # 同参数逐采样一致
|
|
317
|
+
MG.save("work/music.wav", samples)
|
|
317
318
|
```
|
|
318
319
|
|
|
319
|
-
|
|
320
|
+
要更细的控制就传 `sections` / `progressions` / `motif` / `drums` / `voices` ——
|
|
321
|
+
它们真的参与事件展开,不是死旋钮(接口见 `musicgen.py` 模块 docstring)。
|
|
320
322
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
sc.section("finale", 360.0, 389.0, energy=0.95)
|
|
325
|
-
sc.suck(360.0, db=-6, dur=0.22) # 最终重击前"吸一口气"
|
|
326
|
-
samples = sc.render()
|
|
327
|
-
```
|
|
323
|
+
底层原语(吸气、分段能量、过门)仍在 `scorekit.py` 里,但**它不是配乐引擎**:
|
|
324
|
+
它只提供和声表 / 律动层 / 混音 / 响度测量这些可复用件。要自己拼一条非常规配乐可以直接
|
|
325
|
+
调它的 `Score` 再用 `scorekit.save()` 落盘;成片链路用的是 musicgen。
|
|
328
326
|
|
|
329
327
|
### 11.1 为什么"一张和弦表贯穿全片"
|
|
330
328
|
|
|
@@ -354,7 +352,7 @@ samples = sc.render()
|
|
|
354
352
|
- 同 seed 必须逐采样一致(`np.random.default_rng(seed)`,不许用全局 `np.random`)。
|
|
355
353
|
- 配乐只保证"不削波、不直流、峰值 ≤ -1 dBFS";**最终响度交给 ffmpeg loudnorm**。
|
|
356
354
|
|
|
357
|
-
## 12. 程序化音效(sfxkit
|
|
355
|
+
## 12. 程序化音效(sfxkit)
|
|
358
356
|
|
|
359
357
|
### 12.1 为什么不"画波形"
|
|
360
358
|
|
|
@@ -421,47 +419,47 @@ out.append({"at": "%s+0.3" % s["id"], "type": "text_stream",
|
|
|
421
419
|
`sfxkit` 的算法思想改编自 `ai-history-film`(MIT)的 `sfx/` 包,实现为本项目单文件重写;
|
|
422
420
|
文件头已注明出处。配方名、参数名保持与上游一致,方便对照上游文档。
|
|
423
421
|
|
|
424
|
-
## 13.
|
|
422
|
+
## 13. 片间差异与片内分离
|
|
425
423
|
|
|
426
|
-
>
|
|
424
|
+
> 完整契约见 **references/audio-variation.md**;这里只记"怎么用、怎么验"。
|
|
427
425
|
|
|
428
|
-
### 13.1 为什么
|
|
426
|
+
### 13.1 为什么"差不多"
|
|
429
427
|
|
|
430
|
-
|
|
428
|
+
"每片都不一样"不能只靠随机,得让**内容进结构**;下面三种做法就会让片子"差不多":
|
|
431
429
|
|
|
432
430
|
- 配乐:内容指纹只进 `seed`(音色细节),和弦进行/动机/鼓型/段落骨架全固定
|
|
433
|
-
→
|
|
434
|
-
- 音效:`CueBus.build_audio` 的 seed
|
|
431
|
+
→ 两段 20 s 配乐相关性 **0.966**(实测)。
|
|
432
|
+
- 音效:`CueBus.build_audio` 的 seed 取 `seed_base + i`(只跟 cue 顺序)
|
|
435
433
|
→ 同一批 cue 跨片逐样本相同。
|
|
436
434
|
- 片内:提示音硬编码 D 小调五声、配乐过门与 sfx 转场在同一段落边界撞车、duck 只有 3 dB。
|
|
437
435
|
|
|
438
|
-
### 13.2
|
|
436
|
+
### 13.2 怎么变
|
|
439
437
|
|
|
440
438
|
| 层 | 行为 | 开关 |
|
|
441
439
|
|---|---|---|
|
|
442
|
-
| 配乐和声/动机/鼓型 |
|
|
443
|
-
| 音效 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`) |
|
|
444
442
|
| 提示音调性 | `in_key(freq, key)` 跟随配乐调性 | `CueBus.build_audio(key=...)`,render.py 自动传 |
|
|
445
443
|
| 段落边界 | [边界-2.2s, 边界] 内的 whoosh/riser 自动 -3 dB | `sfx.duck_transitions`(默认 true) |
|
|
446
444
|
| duck | 按风格 3.0–5.5 dB(默认 4.0) | `music.duck_db` 显式覆盖 |
|
|
447
445
|
|
|
448
|
-
|
|
446
|
+
**兼容开关**:`sfx.content_seed: false` 时音效退回 `seed_base + i`(逐样本可复现);`key` 为空时提示音回落 D 小调五声。
|
|
449
447
|
|
|
450
|
-
### 13.2.1
|
|
448
|
+
### 13.2.1 旁白优先
|
|
451
449
|
|
|
452
450
|
视频以旁白为主,**BGM 与音效是可选层**:它们负责情绪与重音,不能抢字。具体做法:
|
|
453
451
|
|
|
454
452
|
| 手段 | 默认值 | 说明 |
|
|
455
453
|
|---|---|---|
|
|
456
|
-
| 配乐基线 | `music.gain_db=-12` |
|
|
454
|
+
| 配乐基线 | `music.gain_db=-12` | -1.5 时背景与台词几乎等高;-12 是"人声明显压住背景"的经验线 |
|
|
457
455
|
| 旁白侧链(配乐) | `voice_duck_db=6` | 有人声的地方再压 6 dB;间隙由包络自动恢复(不会整片闷住) |
|
|
458
456
|
| 旁白侧链(音效) | `voice_duck_sfx_db=3` | 提示音/转场也让人声 |
|
|
459
457
|
| 语音阈值 | `voice_thresh_db=-42` / `voice_floor_db=-60` | 10 ms 块 RMS 映射成 0~1 的活动度,起音快、释放慢 |
|
|
460
458
|
| 旁白自身 | `voice_gain=1.0` | **不被任何侧链影响**——旁白优先不等于"压人声" |
|
|
461
459
|
| SNR 兜底 | `guard_snr_db=12` / `guard_cap_db=30` | 混音后实测人声活跃区的人声/背景差,不足就整体补压背景(只压背景) |
|
|
462
460
|
|
|
463
|
-
实测(真实 edge-tts 旁白 + scorekit
|
|
464
|
-
|
|
461
|
+
实测(真实 edge-tts 旁白 + scorekit 配乐):不做上面这些处理时 **SNR -4.2 dB**(背景比台词还响,
|
|
462
|
+
就是"听不清"的原因);按上表默认值 **SNR 12.0 dB**;合成信号口径 15.0 dB,真实配乐素材口径 12.9 dB。
|
|
465
463
|
设 `voice_duck_db=0` / `voice_duck_sfx_db=0` / `guard_snr_db=0` 可分别关掉对应机制。
|
|
466
464
|
|
|
467
465
|
### 13.3 验收:别靠耳朵,跑脚本
|
|
@@ -470,13 +468,13 @@ v0.5 的 style pack 决定的是**风格**(8 套),不是**这一部的具
|
|
|
470
468
|
python <REPO>/tools/audio_similarity.py --strict # 在包根执行
|
|
471
469
|
```
|
|
472
470
|
|
|
473
|
-
A 组量"
|
|
474
|
-
|
|
471
|
+
A 组量"同参数换内容指纹"和"换编排参数"的相关性;B 组量"跨片音效是否还逐样本相同"(同时验
|
|
472
|
+
位置派生可复现);C 组读 `render.py` 的 duck 默认值并打印质心对照;
|
|
475
473
|
D 组构造"有人声段 + 间隙",量人声段旁白/背景响度差。任何一项不达标 exit 1。
|
|
476
474
|
|
|
477
475
|
### 13.4 改音频代码后的自检清单
|
|
478
476
|
|
|
479
|
-
1. `python <REPO>/tools/audio_similarity.py --strict` —— A/B/C
|
|
480
|
-
2. `python selfcheck.py` ——
|
|
481
|
-
3. `python
|
|
482
|
-
4. 改 `assets/scenes.py` 的 `cues()` 后,跑一次 `--stage cues` 看类型分布是否覆盖 hook/body/quote/outro。
|
|
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 条盯音频边界)。
|
|
479
|
+
3. `python musicgen.py --selftest` / `python sfxkit.py --selftest` —— 各自全 PASS(musicgen 的 26 项覆盖编排与确定性)。
|
|
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. 先记住三条
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
| `lines[].sub`(字幕文案) | sub.srt / sub.ass / timeline 的字幕窗口 / frames / mux / qa | `layout → frames → mux → qa`(TTS 可复用) | 帧数÷40 s |
|
|
24
24
|
| `lines[].hold` / `lead` / `tail` / `bar` | timeline / cues / frames / mux / qa | 同上 | 帧数÷40 s |
|
|
25
25
|
| `lines[].kind` | 声线计划 / 配乐分段 / 画面版式(scenes 按 kind 选绘制函数) | `style → tts → layout → …` 全链 | 全片 |
|
|
26
|
-
| `
|
|
26
|
+
| `work/design.json`(设计变了:配色/字体/母题/版式/字幕/配乐/声线) | style.json / storyboard / voice_plan / tts(换声线时) / score / audio / frames / qa | 看 `--stage explain`:通常是全链 | 全片 |
|
|
27
27
|
| `bpm` / `music.*` | score / audio / mux | `score → audio → mux → qa` | 秒级 |
|
|
28
28
|
| `sfx.cues` / `scenes.py 的 cues()` | cues.json / sfx.wav / audio.wav / mux | `cues → sfx → audio → mux → qa` | 秒级 |
|
|
29
29
|
| `scenes.py`(画面代码) | frames / qa(参考帧失效) | `frames → mux → qa`(音轨可复用) | 帧数÷40 s |
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
| 错误做法 | 后果 | 正确做法 |
|
|
37
37
|
|---|---|---|
|
|
38
38
|
| 改了 `text` 只重跑 `tts` | 新语音配旧时间轴 → 字幕漂移、音效错位 | 跑 `--stage explain`,按它给的最小重跑命令 |
|
|
39
|
-
| 改了 `
|
|
39
|
+
| 改了 `design.json`(或重新 `--confirm` 了另一份)只重跑 `frames` | 画风换了但配乐/声线还是旧的(或反之) | 设计是所有下游的上游,通常整链重跑;`--stage style` 先看解析结果 |
|
|
40
40
|
| 手工删 `work/segments/_plan.json` 绕过指纹 | 拼接出的片子画面和旁白对不上,且**不报错** | 删 `work/segments` 整目录重渲 |
|
|
41
41
|
|
|
42
42
|
## 3. 一条命令代替这张表
|
|
@@ -51,4 +51,4 @@ python scripts/stagewatch.py --work work --json # 机读(给脚本/agent 用
|
|
|
51
51
|
|
|
52
52
|
签核(`--stage accept`)绑的是**内容指纹**(剧本 + scenes.py + 引擎)。
|
|
53
53
|
所以:**改了上面表里任何一项,之前的签核都会自动失效**,需要重新看、重新签。
|
|
54
|
-
这不是麻烦,是这套机制存在的意义——否则"我签过了"会变成一张永远有效的空头支票。
|
|
54
|
+
这不是麻烦,是这套机制存在的意义——否则"我签过了"会变成一张永远有效的空头支票。
|
|
@@ -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
|
```
|
|
@@ -80,7 +80,7 @@ python preflight.py --script film.json --work work --engine pil --json
|
|
|
80
80
|
|
|
81
81
|
### 2.3 阈值 92 / 96 是怎么来的(**只有一份权威定义**)
|
|
82
82
|
|
|
83
|
-
判据来自 qavmaf
|
|
83
|
+
判据来自 qavmaf 作者对**同一部示例片**(参考帧含字幕与片头片尾淡入淡出)的三组实测:
|
|
84
84
|
crf17 → **97.24**(pass)、crf30 → **95.74**(pass + warning)、二次编码 + 缩放坏片 → **29.45**(fail)。
|
|
85
85
|
|
|
86
86
|
**阈值只有一份**:权威定义是 `qavmaf.py` 的 `VMAF_PASS=92.0` / `VMAF_WARN=96.0`
|
|
@@ -124,7 +124,7 @@ qavmaf 会在报告里打 warning 提醒这一点,但**不因此改判**——
|
|
|
124
124
|
|
|
125
125
|
| 项 | 实测 |
|
|
126
126
|
|---|---|
|
|
127
|
-
| 29.011 s
|
|
127
|
+
| 29.011 s 示例片全片(870 帧,1080p 交付) | **14.2 s**(同机负载高时同一份代码测到过 28.8 s,见下"成本口径") |
|
|
128
128
|
| 720p@24 交付(696 帧) | **7.2 s**(帧数少 + libvmaf 比较分辨率低) |
|
|
129
129
|
| 分项吞吐 | 参考帧渲染受 ffmpeg 消费速度限制;libvmaf threads=min(8, CPU) 时约 124–160 帧/秒(threads=0 只有约 20 帧/秒) |
|
|
130
130
|
| 抽样模式 | `--vmaf sample`(每 5 帧取 1)实测 8.0 s 跑完 184 帧,VMAF 与全片差 0.34 分 |
|
|
@@ -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
|
|
|
@@ -188,13 +188,13 @@ v0.7 及以前的质检只看"容器 / 响度 / 帧 / 音画同步",都建立
|
|
|
188
188
|
|
|
189
189
|
### 3.4 为什么默认线是 0.85 而不是 0.95
|
|
190
190
|
|
|
191
|
-
实测(29 s
|
|
191
|
+
实测(29 s 示例片,含 "DeepSeek 4.1 Flash / DeepSeek Harness" 这类中英混排专名):
|
|
192
192
|
|
|
193
193
|
| 场景 | 召回 |
|
|
194
194
|
|---|---|
|
|
195
|
-
|
|
|
196
|
-
|
|
|
197
|
-
|
|
|
195
|
+
| 干净示例片 1080p(4 段全对上、缺句 0) | **0.803** |
|
|
196
|
+
| 干净示例片 720p@24(同剧本) | **0.853** |
|
|
197
|
+
| 干净示例片(另一轮) | **0.811 – 0.828** |
|
|
198
198
|
| 丢句反证:把 l03 从音轨剪掉、timeline 仍写 3 句 | **0.677**(`missing_lines=["l03"]`、gate=fail、exit 1) |
|
|
199
199
|
|
|
200
200
|
0.803–0.853 不是流水线坏了,是 ASR 对中英混排专名的正常误差——实测把 "DeepSeek 4.1 Flash"
|
|
@@ -203,7 +203,7 @@ v0.7 及以前的质检只看"容器 / 响度 / 帧 / 音画同步",都建立
|
|
|
203
203
|
|
|
204
204
|
- **< 0.72 才说明"整句丢了 / 音轨跟剧本不是一套"**——这是真正要阻断的;
|
|
205
205
|
- 0.72–0.85 给 warning:仍然出片,但提示可能是缓存串了;
|
|
206
|
-
- **≥ 0.95
|
|
206
|
+
- **≥ 0.95 会把干净的示例片判成 fail**,所以不能拿短测试片的 0.985(19.87 s 纯中文素材,
|
|
207
207
|
没有专名)当默认线。
|
|
208
208
|
|
|
209
209
|
### 3.5 实测耗时
|
|
@@ -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>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 帧级深度质检(qa_frames)
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> 成片质检的容器 / 响度 / 黑帧 / 锐度几项只看"能出片",看不出"画面里有一帧白闪"——本模块补的正是这一层。
|
|
4
4
|
> 这份文档讲清楚 **判据、阈值来源、报告字段、怎么调参、以及这套方法的边界**。
|
|
5
5
|
|
|
6
6
|
实现:`skills/video-studio/scripts/qa_frames.py`(只依赖 stdlib + numpy + Pillow,全部解码走 ffmpeg)。
|
|
@@ -102,10 +102,10 @@ s[t] = max over 8x8 blocks of mean(d[t]) # 分块取最大均值(块内
|
|
|
102
102
|
|
|
103
103
|
实测正常运动画面 `s <= 6.4`,真实异常 `s = 10–15`,所以阈值默认 `6.0`。
|
|
104
104
|
|
|
105
|
-
>
|
|
106
|
-
>
|
|
105
|
+
> **不要加回"严格孤立"**(要求 `s[t-1] < thresh` 且 `s[t+1] < thresh`):它只会制造假阳性
|
|
106
|
+
> (16.933 s 案例就是两侧恰好低谷),判据只保留比值。
|
|
107
107
|
|
|
108
|
-
**第三步:必须"回到原位"**(`FLICKER_RETURN_RATIO = 1.6
|
|
108
|
+
**第三步:必须"回到原位"**(`FLICKER_RETURN_RATIO = 1.6`):
|
|
109
109
|
|
|
110
110
|
```
|
|
111
111
|
r[t] = |F[t+1] - F[t-1]| # 同一块上的"前后两帧彼此差多少"
|
|
@@ -122,7 +122,7 @@ r[t] = |F[t+1] - F[t-1]| # 同一块上的"前后两帧彼此差多少"
|
|
|
122
122
|
| 判定 | 记 error | 跳过 |
|
|
123
123
|
|
|
124
124
|
没有这道闸时,**每一段"缓动入场 / 逐字入场 / 转场黑场起坡"的最快帧都会被报成闪烁**。
|
|
125
|
-
实测反例(本技能自带的 `assets/web/`
|
|
125
|
+
实测反例(本技能自带的 `assets/web/` 示例片):12 s 的片子里报了 3 处——
|
|
126
126
|
|
|
127
127
|
| 时刻 | score | 真相 |
|
|
128
128
|
|---|---|---|
|
|
@@ -175,9 +175,9 @@ r[t] = |F[t+1] - F[t-1]| # 同一块上的"前后两帧彼此差多少"
|
|
|
175
175
|
**绝对响度是否合规(-16 LUFS ±、真峰 ≤ -1 dBTP)以 `filmkit.loudness()`(ffmpeg ebur128)为门禁**,
|
|
176
176
|
本曲线只回答"哪段掉下去了、重击比环境高多少、空窗处声音是不是也停了"。
|
|
177
177
|
|
|
178
|
-
### 2.4b `_static_warnings` —
|
|
178
|
+
### 2.4b `_static_warnings` — 画面长时间静止
|
|
179
179
|
|
|
180
|
-
|
|
180
|
+
判据(**不要用**"全帧平均亮度 3 s 内波动 < 0.002"这类公式):
|
|
181
181
|
|
|
182
182
|
```
|
|
183
183
|
drift[t] = max over 8x8 blocks of mean |F[t] - F[t-lag]| * 255 (lag = FLAT_LAG_S = 0.5 s)
|
|
@@ -188,8 +188,8 @@ drift[t] = max over 8x8 blocks of mean |F[t] - F[t-lag]| * 255 (lag = FLAT_L
|
|
|
188
188
|
人眼看到的就是一张静止的图。用"最活跃块"而不是"全帧均值",是因为逐字入场、数字滚动、卡片切换
|
|
189
189
|
只占全屏几个百分点,全帧均值几乎不动,而观众显然看得到它们在动。
|
|
190
190
|
|
|
191
|
-
|
|
192
|
-
(411/814
|
|
191
|
+
为什么不用全帧均值:它对局部动画天生不敏感。自带示例片(30.10 s)实测误报 **4 段**
|
|
192
|
+
(411/814 个滑动窗口命中),而画面一直在动。现判据在同一片上 **0 段**。
|
|
193
193
|
|
|
194
194
|
实测分布(160×90 解码,H.264):
|
|
195
195
|
|
|
@@ -214,7 +214,7 @@ drift[t] = max over 8x8 blocks of mean |F[t] - F[t-lag]| * 255 (lag = FLAT_L
|
|
|
214
214
|
2. 否则**时间最近**:任意事件里取最近者,**但只有 `|dt| <= tol` 才认可**——用来佐证"确实同步了";
|
|
215
215
|
3. 都找不到 → 视为"这个 cue 没有对应画面事件",**不判超差**。
|
|
216
216
|
|
|
217
|
-
第 2
|
|
217
|
+
第 2 条为什么只认 `|dt| <= tol` 的最近事件:画面事件和音频 cue 本来就不是一一对应。
|
|
218
218
|
实测反例:4.100 s 的 `data_chirp` 被 3.700 s 的"交界黑场起坡"事件配对,报 `dt = -0.400 s` 超差;
|
|
219
219
|
8.020 s 的 `reverse_swell` 被 7.700 s 的事件配成 `-0.320 s`。两者都是**假 FAIL**——
|
|
220
220
|
跨 kind 的"最近事件"根本不是"预期事件"。
|
|
@@ -229,9 +229,9 @@ drift[t] = max over 8x8 blocks of mean |F[t] - F[t-lag]| * 255 (lag = FLAT_L
|
|
|
229
229
|
`flash / shake / ring` 而 cue 是 `impact / whoosh`,同 kind 永远配不上,跨 kind 又只做确认,
|
|
230
230
|
结果就是 **`sync` 恒为空、检查静默全过**——看起来"零超差",其实是"一条都没在查"。
|
|
231
231
|
|
|
232
|
-
####
|
|
232
|
+
#### PIL 路线的两条补救(否则这条门禁不会真的在查)
|
|
233
233
|
|
|
234
|
-
`--engine pil` 根本没有页面、没有 `events
|
|
234
|
+
`--engine pil` 根本没有页面、没有 `events`,所以上面的配对必然配不上。两条补救:
|
|
235
235
|
|
|
236
236
|
1. **cue 自带画面锚点**:给 cue 加 `"visual_at"`(或 `"visual_t"`)字段,值是画面上该事件
|
|
237
237
|
**真正发生的绝对秒数**。`sync_check` 会直接算 `dt = visual_at - cue.t` 并判超差,
|
|
@@ -462,9 +462,9 @@ JSON 里 `flicker` 两项:
|
|
|
462
462
|
|
|
463
463
|
---
|
|
464
464
|
|
|
465
|
-
## 9. 案例:16.933 s
|
|
465
|
+
## 9. 案例:16.933 s「小元素动画」误报
|
|
466
466
|
|
|
467
|
-
PIL 模板演示片(30 s / 1080p30
|
|
467
|
+
PIL 模板演示片(30 s / 1080p30)报过 `t=16.933s score=6.61`,bbox `96,40–104,48`。
|
|
468
468
|
逐层复现(160×90、8×8 块、area 采样)后,数据是这样的:
|
|
469
469
|
|
|
470
470
|
| 帧号(t) | 504 (16.800) | 505 (16.833) | 506 (16.867) | 507 (16.900) | 508 (16.933) | 509 (16.967) | 510 (17.000) |
|
|
@@ -481,8 +481,8 @@ PIL 模板演示片(30 s / 1080p30)早期版本报过 `t=16.933s score=6.61`
|
|
|
481
481
|
3. **真相是"小元素在动"**:卡片上一个绿色光点在漂移(全分辨率二阶差分 > 30 的像素只占全屏 0.33%),
|
|
482
482
|
504–510 帧连续都在动;508 只是恰好被两侧 507/509 的低谷夹住,`ratio = 6.48 / max(5.66,3.52) = 1.15 < 1.5`。
|
|
483
483
|
|
|
484
|
-
|
|
485
|
-
|
|
484
|
+
**所以判据不要求"邻居低于阈值"**:那条严格路径只会制造这种假阳性,只保留比值判据;
|
|
485
|
+
另加"回到原位"闸(§2.2)处理加速运动。
|
|
486
486
|
复核这类案例最快的两个线索:**`median`(这里是 1.0,真白闪是 143–199)** 和
|
|
487
487
|
**变化像素占全屏的比例(这里是 0.33%)**。
|
|
488
488
|
|
|
@@ -500,3 +500,35 @@ PIL 模板演示片(30 s / 1080p30)早期版本报过 `t=16.933s score=6.61`
|
|
|
500
500
|
| 空窗漏报 | 近黑但没到 0.045(例如深灰 0.06)→ 调高 `BLACK_LUMA_MAX`;或时长不足 0.8 s |
|
|
501
501
|
| 同步全不报 | ① `cues.json` 没有 `events`(页面未导出);② **events.kind 与 cue 配方名不同名**(最常见,且不会任何报错);③ cue 与事件时间差 > 同 kind 搜索窗 2 s |
|
|
502
502
|
| 想确认同步检查真的在工作 | 故意把某个 cue 的时刻改错 0.3 s 重跑,**必须**报出超差;不报就是没配上(见上一行) |
|
|
503
|
+
|
|
504
|
+
## 11. 取景抖动(噪声沸腾)—— 单帧闪烁判据抓不到的一类
|
|
505
|
+
|
|
506
|
+
**现象**:成片看起来"一直在抖",但 `flicker_scan` 报 0、`ssim`/`vmaf` 都很好。
|
|
507
|
+
用户的一句话往往是"生成的视频有明显的抖动感,这是为啥?"
|
|
508
|
+
|
|
509
|
+
**根因**:底图把颗粒(`_grain_layer`)和材质一起烘进了 1920×1080 的静态图层,逐帧再用
|
|
510
|
+
`Image.EXTENT + NEAREST` 按镜头缓推的缩放比 `s` 取景。NEAREST 的采样网格随 `s` 整像素跳变,
|
|
511
|
+
颗粒图案跟着**整体错位 1px**——这就是"满屏噪点在沸腾"。它是**位移**型瑕疵,不是亮度型,
|
|
512
|
+
所以按"前后帧平均亮度差"定义的 `flicker_scan` 对它完全不敏感。
|
|
513
|
+
|
|
514
|
+
**探测方法**(不用整片,10 秒就够):
|
|
515
|
+
|
|
516
|
+
```python
|
|
517
|
+
import numpy as np
|
|
518
|
+
from PIL import Image
|
|
519
|
+
# 同一主题,连续取 s = 1.0 -> 1+push 的 20~40 帧
|
|
520
|
+
frames = [mod._bg_plate(th, 1.0 + th["push"] * i / 39.0) for i in range(40)]
|
|
521
|
+
worst = max(int(np.abs(np.asarray(a, np.int16) - np.asarray(b, np.int16)).max())
|
|
522
|
+
for a, b in zip(frames, frames[1:]))
|
|
523
|
+
print(worst) # NEAREST 直采 4~6 灰阶(可见沸腾);端点预烘 + blend 后 0~1
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
**修法**:不要用 NEAREST 直采缩放(BILINEAR/BICUBIC 整帧重采样 29/64 ms,超单帧预算)。
|
|
527
|
+
改成**端点预烘 + 逐帧 blend**:`s=1.0` 与 `s=1+push` 各烘一张 BILINEAR 底版(每张约 60ms,
|
|
528
|
+
只做一次),逐帧用 `Image.blend(lo, hi, (s-1)/push)` 插值(约 4.4 ms/帧)。
|
|
529
|
+
端点内部是平滑重采样,没有网格跳变;颗粒质感保留。上限 `1+push` 是精确边界
|
|
530
|
+
(`_push_scale` 恒 <= 1+push),超出就钳到端点上,不做外推。
|
|
531
|
+
|
|
532
|
+
**护栏**:`selfcheck` 第 68 条(`t_zoom_stability`)断言相邻帧最大差 <= 2 灰阶。
|
|
533
|
+
改回 NEAREST 直采在这条上直接 fail——这就是"再也不会悄悄退回 NEAREST"的机制。
|
|
534
|
+
|
|
@@ -45,7 +45,7 @@ BPM 96, 4/4 → 1 小节 = 2.5 s = 75 帧 @30fps
|
|
|
45
45
|
2. 单独一节"不能上屏"黑名单:写清为什么不行、实际是什么
|
|
46
46
|
3. 口径表:同一数字在报告/字幕/画面里表述完全一致
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
真实收益(一次实际核查的账):75 条主张 + 25 条黑名单,抓出 8 处硬伤,其中"疫情在拆把手后很快平息"这类因果讲反的错误会被评论区第一条指出。
|
|
49
49
|
|
|
50
50
|
## P3 TTS(唯一正确的时长来源)
|
|
51
51
|
|
|
@@ -73,10 +73,10 @@ tl = K.build_timeline(lines, lead=0.55, tail=0.85, min_dur=3.0)
|
|
|
73
73
|
字幕窗口规则(防交叠,来自实战):
|
|
74
74
|
```
|
|
75
75
|
t0 = voice_start - 0.12
|
|
76
|
-
t1 = voice_end + 0.45 #
|
|
76
|
+
t1 = voice_end + 0.45 # 没有 5.2 s 硬钳;要钳短显式传 max_hold
|
|
77
77
|
t1 = min(t1, next.voice_start - 0.12) # 绝不压住下一句
|
|
78
78
|
```
|
|
79
|
-
停留时长参考 `0.5 + 字数/9`
|
|
79
|
+
停留时长参考 `0.5 + 字数/9` 秒。**没有默认上限**——
|
|
80
80
|
真要缩短就显式传 `max_hold` / `sub_max_hold`。按字符比例平均分配会产生大量交叠。
|
|
81
81
|
|
|
82
82
|
## P5 混音(numpy 总线)
|
|
@@ -109,7 +109,7 @@ python render.py ... --stage qa # 只读质检,不重
|
|
|
109
109
|
```python
|
|
110
110
|
sink = K.FrameSink("work/silent.mp4", crf=18, preset="medium") # rawvideo 直通
|
|
111
111
|
...
|
|
112
|
-
# 视频流默认 -c:v copy
|
|
112
|
+
# 视频流默认 -c:v copy(不二次编码);需要改分辨率时 reencode_video=True
|
|
113
113
|
K.mux("work/silent.mp4", "work/audio.wav", "out/film.mp4")
|
|
114
114
|
```
|
|
115
115
|
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
| 3 | **音频实测反推时间,绝不按字数估算** | 字幕漂移是观众最先察觉的瑕疵 |
|
|
10
10
|
| 4 | **字幕文本与朗读文本分离**(`sub` 短 / `text` 长) | 上屏可读性与朗读自然度是两个约束 |
|
|
11
11
|
| 5 | **提示点由画面声明、由声音消费** | 音画对齐从"感觉"变成"数据" |
|
|
12
|
-
| 6 | **分块键用绝对帧号**(`seg_025200_025560.mp4`),拼接时显式检缺口;再存一份**计划指纹**(`_plan.json`) | 唯一一次"少了 183 秒还不报错"的根因。只按"文件在不在"跳过还会导致"改了台词却复用旧画面"
|
|
12
|
+
| 6 | **分块键用绝对帧号**(`seg_025200_025560.mp4`),拼接时显式检缺口;再存一份**计划指纹**(`_plan.json`) | 唯一一次"少了 183 秒还不报错"的根因。只按"文件在不在"跳过还会导致"改了台词却复用旧画面"——计划指纹不符会**拒绝拼接**并提示删目录 |
|
|
13
13
|
| 7 | **JPEG q94 截图替代 PNG**(浏览器方案) | 215 → 55 ms/帧,PSNR 41–44 dB 肉眼无差 |
|
|
14
14
|
| 8 | **worker 复用浏览器、出错才重建、finally 必关** | 每块开新页会把进程堆到 28 个、内存 2.7 GB |
|
|
15
15
|
| 9 | **前瞻限幅器补响度,不整体拉高** | 只压峰值不改主体响度 |
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
| 19 | **写入边界 + 接口先写死 + 一份施工文档** | 多 agent 并行不打架的三个前提 |
|
|
26
26
|
| 20 | **交付文档如实声明已知边界** | 把"局限"变成可信度,而不是藏起来 |
|
|
27
27
|
|
|
28
|
-
## 二、
|
|
28
|
+
## 二、17 条反模式(全部有实证)
|
|
29
29
|
|
|
30
30
|
| # | 反模式 | 后果 | 正确做法 |
|
|
31
31
|
|---|---|---|---|
|
|
@@ -45,8 +45,9 @@
|
|
|
45
45
|
| 14 | 音频用单声道总线后复制成双声道 | 损失立体声场 | 全程 float32 立体声(配乐做宽度) |
|
|
46
46
|
| 15 | 静态图叠加忘了 `-loop 1` | 时间轴被截成 1 帧 | `-loop 1 -framerate ` |
|
|
47
47
|
| 16 | 靠"注意准确性"这种叮嘱做质量 | 无法验收 | 事实核查有编号、出处、黑名单三件套 |
|
|
48
|
+
| 17 | 把颗粒烘进底图,再用 NEAREST 逐帧缩放取景 | **满屏噪点沸腾**(用户原话"明显的抖动感"),而单帧闪烁门禁全程报 0 | 端点预烘 + `Image.blend` 取景;颗粒留在底图但只走平滑重采样(见复盘 ④) |
|
|
48
49
|
|
|
49
|
-
##
|
|
50
|
+
## 三、四个 bug 复盘(最值得读)
|
|
50
51
|
|
|
51
52
|
### ① 音轨中断:成片只有前 15 秒有声
|
|
52
53
|
|
|
@@ -81,3 +82,21 @@ if (inP < 1 && act.i > 0) { ... } // 永远不成立
|
|
|
81
82
|
字段从 `t0` 改名成 `start` 后,全片十幕的淡入特效与所有图名框**静默失效**。没有任何报错,画面照常渲染,只是"少了一点东西"。
|
|
82
83
|
|
|
83
84
|
**教训**:质检分两类——**"不能有"**(闪烁、重叠、出画、削波)和**"必须有"**(图名框、淡入、字幕、每段至少一个动态元素)。后者更难,因为需要一份"应然清单"。
|
|
85
|
+
|
|
86
|
+
### ④ 整片"发抖":颗粒 + NEAREST 取景
|
|
87
|
+
|
|
88
|
+
**现象**:成片从头到尾有轻微抖动感,用户说"有明显的抖动感",但 `flicker_scan` 0 条、
|
|
89
|
+
VMAF 97+、SSIM 0.999——所有现有门禁都说没问题。
|
|
90
|
+
|
|
91
|
+
**根因**:`_bg()` 把颗粒(逐像素 hash 噪声)烘进了静态底图,逐帧再用
|
|
92
|
+
`EXTENT + NEAREST` 按缓推比例 `s` 取景。NEAREST 的采样网格随 `s` 整像素跳变,
|
|
93
|
+
颗粒图案整体错位 1px。实测背景-only 相邻帧差 **4~6 灰阶**(整屏低幅位移),
|
|
94
|
+
而"单帧闪烁"判据比的是前后帧平均亮度——压根看不到这种位移。
|
|
95
|
+
|
|
96
|
+
**修复**:`s=1.0` 与 `s=1+push` 两个端点各烘一张 BILINEAR 底版(每张约 60 ms,一次性),
|
|
97
|
+
逐帧 `Image.blend(lo, hi, (s-1)/push)`(约 4.4 ms/帧)。实测相邻帧差降到 **0~1 灰阶**。
|
|
98
|
+
为什么不用 BILINEAR/BICUBIC 直采:整帧重采样 29/64 ms,超 15 ms 单帧预算。
|
|
99
|
+
|
|
100
|
+
**教训**:**"能出片 + 指标好看"不等于"看着不抖"**。质检判据要按**瑕疵的物理类型**分类:
|
|
101
|
+
亮度型(闪烁/黑帧)、位移型(抖动/漂移)、时序型(同步/丢句)。缺哪一类,
|
|
102
|
+
那一类就会一直漏。补判据的同时补一条 `selfcheck` 断言(第 68 条),否则下次改回 NEAREST 没人发现。
|