@dsh-gis/dsh-video-studio-skill 0.8.1 → 1.0.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.
- package/CHANGELOG.md +582 -0
- package/README.md +287 -304
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/package.json +11 -10
- package/schemas/design.schema.json +722 -0
- package/schemas/film.schema.json +171 -15
- package/schemas/proposal.schema.json +133 -0
- package/skills/video-studio/SKILL.md +1316 -1066
- package/skills/video-studio/assets/design_fixture.json +141 -0
- package/skills/video-studio/assets/film.json +1 -2
- package/skills/video-studio/assets/golden_frames.json +12 -9
- package/skills/video-studio/assets/scenes.py +2207 -1190
- package/skills/video-studio/assets/voice_loudness.json +16 -0
- package/skills/video-studio/references/audio-variation.md +15 -11
- package/skills/video-studio/references/audio.md +36 -34
- package/skills/video-studio/references/change-impact.md +3 -3
- package/skills/video-studio/references/frame-qa.md +2 -2
- package/skills/video-studio/references/proposal.md +177 -0
- package/skills/video-studio/references/quality-gates.md +2 -2
- package/skills/video-studio/references/scene-grammar.md +311 -0
- package/skills/video-studio/references/sfx-design.md +1 -1
- package/skills/video-studio/references/style-packs.md +107 -168
- package/skills/video-studio/references/v08-quality.md +8 -8
- package/skills/video-studio/references/web-engine.md +1 -1
- package/skills/video-studio/references/word-timeline.md +2 -2
- package/skills/video-studio/scripts/designkit.py +2120 -0
- package/skills/video-studio/scripts/filmcheck.py +104 -33
- package/skills/video-studio/scripts/filmkit.py +153 -78
- package/skills/video-studio/scripts/grammar.py +619 -0
- package/skills/video-studio/scripts/musicgen.py +2043 -0
- package/skills/video-studio/scripts/preflight.py +10 -3
- package/skills/video-studio/scripts/previewkit.py +849 -0
- package/skills/video-studio/scripts/propose.py +825 -0
- package/skills/video-studio/scripts/qa_frames.py +45 -10
- package/skills/video-studio/scripts/qavmaf.py +77 -11
- package/skills/video-studio/scripts/render.py +505 -142
- package/skills/video-studio/scripts/scorekit.py +50 -1748
- package/skills/video-studio/scripts/selfcheck.py +851 -115
- package/skills/video-studio/scripts/stagewatch.py +21 -5
- package/skills/video-studio/scripts/stylekit.py +293 -570
- package/skills/video-studio/scripts/voicekit.py +502 -380
- package/skills/video-studio/scripts/webengine.py +15 -14
- package/tools/audio_similarity.py +57 -30
- package/tools/ci_check.py +9 -1
- package/tools/estimate.py +14 -8
- package/tools/golden_frames.py +56 -11
- package/tools/silhouette_qa.py +1263 -0
- 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-craft.png +0 -0
- package/docs/styles/sheet-night.png +0 -0
- package/skills/video-studio/assets/score_demo.json +0 -307
|
@@ -1,1066 +1,1316 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: video-studio
|
|
3
|
-
description: 用代码从零生成中文视频(科普片/纪录片/宣传片/动画短片):写剧本→edge-tts 旁白→实测时长排时间轴→PIL 或 Canvas 逐帧渲染→ffmpeg 编码→numpy 采样级混音→自动质检。当用户要"做一个视频 / 生成宣传片 / 把报告变成视频 / 做动画 / 配音配字幕 / 出成片 / 修视频音画不同步 / 视频画质糊",或提到 ffmpeg、edge-tts、逐帧渲染、字幕、分镜、配乐、混音时使用。 | Code-generated video production (Chinese narration, deterministic frame rendering, ffmpeg, edge-tts, QA gates). Use when asked to make a video, turn a report into a video, animate a story, fix A/V sync or blurry text in a generated video.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# 视频工作室(video-studio)
|
|
7
|
-
|
|
8
|
-
**一句话:视频 = 剧本 + 音频实测时长 + 确定性逐帧函数 + ffmpeg 编码 + 自动质检。**
|
|
9
|
-
画面与声音全部由代码生成,零素材、可复现、可断点续跑、可脚本化验收。
|
|
10
|
-
|
|
11
|
-
## 0. 铁律(违反任意一条,成片一定会出问题)
|
|
12
|
-
|
|
13
|
-
1. **音频先做,画面后做**。时间轴必须由 TTS 实测时长反推,**绝不按字数估算**。按字数排的时间轴会在第 20 句开始明显漂移。
|
|
14
|
-
2. **每帧是纯函数**:`draw(t, frame) -> Image`。禁 `random`/`time()`,要随机用 `filmkit.hash2/hash3/vnoise`。同一帧号必须永远画出同一张图。
|
|
15
|
-
3. **字幕与大字必须落在安全区**:正文 `y ∈ [58, 974]`,字幕带 `y ∈ [872, 1030]`,永不超过 1030。
|
|
16
|
-
4. **原始帧直通编码**:PIL 帧 → `rawvideo` 管道 → `libx264`。**不要**用 `cv2.VideoWriter` 做中间文件(慢 2 倍且二次有损)。
|
|
17
|
-
5. **混音用 numpy 采样级叠加**,不要用一条 ffmpeg 命令喂上百个输入(Windows 上会非确定性静音)。
|
|
18
|
-
6. **交付前必须跑质检脚本 + 亲眼看 3 张静帧**(用 `read_image`)。"能出图"不等于"没问题"。
|
|
19
|
-
7. **(v0.3)声音跟着画面走**:该响一下的地方在**场景代码里登记提示点**(`K.CueBus` / `scenes.py` 的 `cues()` 钩子),
|
|
20
|
-
不要事后手工对时间。画面改了(换停留窗口、调节奏),提示点自动跟着改。
|
|
21
|
-
8. **(v0.3)没看清就加停留,别整片放慢**:`film.json` 的 `hold`(全局)或 `lines[].hold`(单句)
|
|
22
|
-
在语音结束后加停留;配乐用 `bpm` 对齐小节。
|
|
23
|
-
|
|
24
|
-
##
|
|
25
|
-
|
|
26
|
-
>
|
|
27
|
-
>
|
|
28
|
-
>
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
#
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
#
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
python
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
python render.py
|
|
188
|
-
python render.py
|
|
189
|
-
python
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
>
|
|
227
|
-
>
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
|
234
|
-
|
|
235
|
-
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
>
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
`
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
|
314
|
-
|
|
315
|
-
|
|
|
316
|
-
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
{"
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
|
649
|
-
|
|
650
|
-
|
|
|
651
|
-
|
|
|
652
|
-
|
|
|
653
|
-
|
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
1.
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
|
719
|
-
|
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
`
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
|
754
|
-
|
|
|
755
|
-
|
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
|
801
|
-
|
|
802
|
-
|
|
|
803
|
-
|
|
|
804
|
-
|
|
|
805
|
-
|
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
- `
|
|
829
|
-
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
|
837
|
-
|
|
|
838
|
-
|
|
|
839
|
-
|
|
|
840
|
-
|
|
|
841
|
-
|
|
|
842
|
-
|
|
|
843
|
-
|
|
|
844
|
-
|
|
|
845
|
-
|
|
|
846
|
-
| `
|
|
847
|
-
|
|
|
848
|
-
|
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
|
853
|
-
|
|
854
|
-
|
|
|
855
|
-
|
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
|
860
|
-
|
|
861
|
-
| `
|
|
862
|
-
|
|
|
863
|
-
|
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
###
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
|
1022
|
-
|
|
1023
|
-
|
|
|
1024
|
-
|
|
|
1025
|
-
|
|
|
1026
|
-
|
|
|
1027
|
-
|
|
|
1028
|
-
|
|
|
1029
|
-
|
|
|
1030
|
-
|
|
|
1031
|
-
|
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1
|
+
---
|
|
2
|
+
name: video-studio
|
|
3
|
+
description: 用代码从零生成中文视频(科普片/纪录片/宣传片/动画短片):写剧本→edge-tts 旁白→实测时长排时间轴→PIL 或 Canvas 逐帧渲染→ffmpeg 编码→numpy 采样级混音→自动质检。当用户要"做一个视频 / 生成宣传片 / 把报告变成视频 / 做动画 / 配音配字幕 / 出成片 / 修视频音画不同步 / 视频画质糊",或提到 ffmpeg、edge-tts、逐帧渲染、字幕、分镜、配乐、混音时使用。 | Code-generated video production (Chinese narration, deterministic frame rendering, ffmpeg, edge-tts, QA gates). Use when asked to make a video, turn a report into a video, animate a story, fix A/V sync or blurry text in a generated video.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 视频工作室(video-studio)
|
|
7
|
+
|
|
8
|
+
**一句话:视频 = 剧本 + 音频实测时长 + 确定性逐帧函数 + ffmpeg 编码 + 自动质检。**
|
|
9
|
+
画面与声音全部由代码生成,零素材、可复现、可断点续跑、可脚本化验收。
|
|
10
|
+
|
|
11
|
+
## 0. 铁律(违反任意一条,成片一定会出问题)
|
|
12
|
+
|
|
13
|
+
1. **音频先做,画面后做**。时间轴必须由 TTS 实测时长反推,**绝不按字数估算**。按字数排的时间轴会在第 20 句开始明显漂移。
|
|
14
|
+
2. **每帧是纯函数**:`draw(t, frame) -> Image`。禁 `random`/`time()`,要随机用 `filmkit.hash2/hash3/vnoise`。同一帧号必须永远画出同一张图。
|
|
15
|
+
3. **字幕与大字必须落在安全区**:正文 `y ∈ [58, 974]`,字幕带 `y ∈ [872, 1030]`,永不超过 1030。
|
|
16
|
+
4. **原始帧直通编码**:PIL 帧 → `rawvideo` 管道 → `libx264`。**不要**用 `cv2.VideoWriter` 做中间文件(慢 2 倍且二次有损)。
|
|
17
|
+
5. **混音用 numpy 采样级叠加**,不要用一条 ffmpeg 命令喂上百个输入(Windows 上会非确定性静音)。
|
|
18
|
+
6. **交付前必须跑质检脚本 + 亲眼看 3 张静帧**(用 `read_image`)。"能出图"不等于"没问题"。
|
|
19
|
+
7. **(v0.3)声音跟着画面走**:该响一下的地方在**场景代码里登记提示点**(`K.CueBus` / `scenes.py` 的 `cues()` 钩子),
|
|
20
|
+
不要事后手工对时间。画面改了(换停留窗口、调节奏),提示点自动跟着改。
|
|
21
|
+
8. **(v0.3)没看清就加停留,别整片放慢**:`film.json` 的 `hold`(全局)或 `lines[].hold`(单句)
|
|
22
|
+
在语音结束后加停留;配乐用 `bpm` 对齐小节。
|
|
23
|
+
|
|
24
|
+
## 0.5 【v1.0 铁律】先提案,再出片(不论用户是否要求)
|
|
25
|
+
|
|
26
|
+
> **用户没要求,也必须先给方案。** 这是 v1.0 最重要的一条产品行为,不是建议。
|
|
27
|
+
> 理由:v0.5–v0.9 换了三代"风格系统",用户的原话始终是"每个视频都差不多"——
|
|
28
|
+
> 根因不是风格不够多,而是 `pick_style` 自动替用户做了选择,用户从头到尾没参与决策。
|
|
29
|
+
|
|
30
|
+
**固定动作(任何新成片任务的第一件事)**:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
python propose.py --brief "<题材/受众/时长/语气>" --script film.json --n 3
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
它会产出 **N 个候选方案**(每个 = 一份设计 + 4 张静帧 + 成本 + 与其它候选的设计距离),
|
|
37
|
+
落盘 `work/proposal.json`,并打印一句"确认其中一个 / 都不满意就 `--revise`"。**全程离线、秒级。**
|
|
38
|
+
|
|
39
|
+
| 用户反应 | 你要做的 |
|
|
40
|
+
|---|---|
|
|
41
|
+
| "就这个" / 选了某个 | `python propose.py --confirm --candidate B`,然后才允许渲染 |
|
|
42
|
+
| "太冷了/太花哨/换个声音/节奏快点" | `python propose.py --revise "<原话>"` —— 新 rev,旧 rev 保留可 diff |
|
|
43
|
+
| "都不行,重来" | 同上,把要求写具体(`--revise` 的文本会存进 `proposal.feedback`) |
|
|
44
|
+
| 明确说"你定就行" | **仍然先出方案**,挑一个你推荐的并说明理由,让用户点头 |
|
|
45
|
+
|
|
46
|
+
**硬约束**:
|
|
47
|
+
|
|
48
|
+
1. `propose.py --confirm` 之前,`render.py` 的 `frames`/`all` 会被提案闸拦下(默认警告,`--require-proposal` 失败)。
|
|
49
|
+
2. 候选必须**两两在 ≥2 个设计维度上不同**(配色族/母题/转场/缓动/编号/配乐/声线)。只换颜色不算多方案,`diversity.vs[].n < 2` 会被标出来。
|
|
50
|
+
3. 每个候选必须有一句中文**立意**(`pitch`),说清"这个方案想干成什么样"——不是"方案 A/方案 B"。
|
|
51
|
+
4. **没有内置模板可套**(v1.0.1):`design.json` 的所有字段都开放(35 字段主题 + 五组视觉语言签名 +
|
|
52
|
+
声音/配乐/节奏),配色是**连续色相空间**里的任意一点,不是从若干套现成外观里挑。
|
|
53
|
+
每次提案都是现场生成——同一个 brief 连拍两次会得到不同设计(要复现就传 `--seed`)。
|
|
54
|
+
5. **片间去重台账(v1.0.2,默认开启)**:`--confirm` 会把冻死的设计记进
|
|
55
|
+
`~/.dsh/video-studio/design-ledger.json`(最多 500 条),下次提案主动避开最近交付过的外观——
|
|
56
|
+
候选与台账里任一部片的**设计距离 < 2 维**时自动换 nonce 重采并告警。
|
|
57
|
+
这是"不同片不重复"从"靠随机"变成"有记忆"的落地机制。`--show-ledger` 查台账、
|
|
58
|
+
`--no-ledger` 本次忽略、环境变量 `VIDEO_STUDIO_LEDGER` 改路径或设 `off` 关闭。
|
|
59
|
+
台账读写失败一律当空表,**永不阻塞出片**。
|
|
60
|
+
|
|
61
|
+
**只有一种情况可以跳过提案**:纯修复 / 纯重渲(不改内容、不改设计,只修音画不同步或重出片)。
|
|
62
|
+
这种情况下用 `--require-proposal` 之外的方式跑,并在回复里说明"本次未提案,因为…"。
|
|
63
|
+
|
|
64
|
+
### 0.5.1 v1.0 / D3 的破坏性变更(升级必读)
|
|
65
|
+
|
|
66
|
+
| 以前 | 现在 |
|
|
67
|
+
|---|---|
|
|
68
|
+
| 不写 `style` → 静默走 v0.4 老外观(固定皮肤 + 120BPM/Dm) | **报错**,并给两步迁移指引(先 `propose.py` 再 `--confirm`) |
|
|
69
|
+
| `style: "legacy"` 是合法值 | **报错**(同一个坑的另一种写法) |
|
|
70
|
+
| `style: "auto"` 自动挑一套内置风格 | **已删除**(v1.0.1):内置风格表已清空,任何 style 值都会报错 |
|
|
71
|
+
| 8 套风格写死在 `stylekit.py` 里 | **全部删除**:没有 `THEMES`/`STYLES`/`VISUAL_LANGUAGES`,也没有 `presets/` |
|
|
72
|
+
| 8 套配乐风格表在 `scorekit.py` | **已删除**:配乐由 `musicgen.py` 按参数现场编排 |
|
|
73
|
+
| `scenes.draw_frame` 有 legacy 分支 | 已删(不可达的死分支) |
|
|
74
|
+
|
|
75
|
+
**没有"以某套预设为起点"这回事了**(v1.0.1)。要特定外观只有两条路:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
# ① 自然语言提要求(认得 must_have/avoid 里的关键词)
|
|
79
|
+
python propose.py --brief "科技发布会,深蓝,要遮幅" --script film.json
|
|
80
|
+
|
|
81
|
+
# ② 直接手写 design.json(字段全开放,配色是任意 RGB)
|
|
82
|
+
python render.py --script film.json --stage all # 前提:work/design.json 已存在
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
详细契约见 [references/proposal.md](references/proposal.md) 与 `schemas/design.schema.json`。
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## 1. 五步上手(零拷贝:不拷脚本,不拷模板)
|
|
90
|
+
|
|
91
|
+
> **v1.0.2 起这条路径没有任何"拷贝"动作。** 技能脚本留在技能目录里直接执行;
|
|
92
|
+
> 你的工程目录里只需要一个 `film.json`(外加运行产生的 `work/` 与 `out/`)。
|
|
93
|
+
>
|
|
94
|
+
> `<SKILL_DIR>` 指**本次加载技能时 `skill` 工具回报的 `resourceBase.path`**。装成插件后它就是
|
|
95
|
+
> 本插件包内的 `skills/video-studio/`(profile 的 `node_modules/@dsh-gis/dsh-video-studio-skill` 是链接)。
|
|
96
|
+
> 找不到时按名字定位:`find ~ -path '*/skills/video-studio/scripts/filmkit.py'`。
|
|
97
|
+
>
|
|
98
|
+
> `<REPO>` 指**包根**(仓库根,或装好后 profile 里的 `node_modules/@dsh-gis/dsh-video-studio-skill/`),
|
|
99
|
+
> 即 `<SKILL_DIR>` 往上两级。`tools/` 与 `schemas/` 在 `<REPO>` 下,**不在** `<SKILL_DIR>` 下。
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
# 1) 只写一个 film.json(工程目录里唯一需要你手写的文件)
|
|
103
|
+
mkdir -p myfilm && cd myfilm
|
|
104
|
+
cat > film.json <<'JSON'
|
|
105
|
+
{
|
|
106
|
+
"title": "我的片子",
|
|
107
|
+
"lines": [
|
|
108
|
+
{"id": "l01", "kind": "hook", "chapter": "01 · 开场",
|
|
109
|
+
"text": "这是用代码从零生成的画面与声音。", "sub": "零素材,全代码生成", "big": "零素材,全代码生成"},
|
|
110
|
+
{"id": "l02", "kind": "body", "chapter": "02 · 正文", "kicker": "PIPELINE",
|
|
111
|
+
"text": "整条管线只有五步:写剧本、生成旁白、排时间轴、逐帧渲染、混音合成。",
|
|
112
|
+
"sub": "五步成片,每步落盘", "big": "五步成片",
|
|
113
|
+
"items": [{"text": "剧本 film.json"}, {"text": "旁白"}, {"text": "时间轴"}, {"text": "逐帧渲染"}, {"text": "混音"}]},
|
|
114
|
+
{"id": "l03", "kind": "outro", "chapter": "99 · 收尾",
|
|
115
|
+
"text": "先定脚本,再定声音,最后才是画面。", "sub": "顺序比努力重要", "big": "顺序比努力重要"}
|
|
116
|
+
]
|
|
117
|
+
}
|
|
118
|
+
JSON
|
|
119
|
+
# lines[].text = 朗读文本(自然口语);sub = 上屏字幕(短);kind = hook/body/quote/outro/emphasis/calm
|
|
120
|
+
# v1.0 起画风/配乐/声线/节奏全部由 design 决定,film.json 里**没有** style 字段可写。
|
|
121
|
+
# 契约校验(可选,几秒):
|
|
122
|
+
python <SKILL_DIR>/scripts/filmcheck.py film.json
|
|
123
|
+
|
|
124
|
+
# 2) 先自检一次环境(可选;67 条回归护栏,约 10–20 秒,不联网;缺可选模块会标 SKIP)
|
|
125
|
+
python <SKILL_DIR>/scripts/selfcheck.py
|
|
126
|
+
|
|
127
|
+
# 3) 【v1.0 铁律】先出方案,再出片——同一个 brief 每次都不一样,所以必须让用户看过
|
|
128
|
+
python <SKILL_DIR>/scripts/propose.py \
|
|
129
|
+
--brief "AI 大模型的三年,30 秒科普,深蓝" \
|
|
130
|
+
--script film.json --work work
|
|
131
|
+
# 产出 3 个候选:各自一份 design + 静帧联系表(work/proposals/rev1/_compare.png)+ 设计距离,
|
|
132
|
+
# 2 秒级出结果、全程离线。看并排图,挑一个。
|
|
133
|
+
|
|
134
|
+
# 4) 确认候选,冻结设计(work/design.json)——此后渲染只认这份设计
|
|
135
|
+
python <SKILL_DIR>/scripts/propose.py --confirm --candidate A --work work
|
|
136
|
+
# 不满意就换一个:--revise "再冷一点,少装饰,换女声"(新 rev,旧 rev 留着眼 diff)
|
|
137
|
+
# 想复现某一次:首次提案时加 --seed 42(不传 seed = 每次新 nonce,这是"每片不同"的来源)
|
|
138
|
+
|
|
139
|
+
# 5) 先看静帧,再出全片;--scenes 不写就用技能内置渲染器
|
|
140
|
+
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage stills \
|
|
141
|
+
--stills "l01:3,l02:5,l03:2.5"
|
|
142
|
+
# → work/stills/_sheet.png:用 read_image 亲眼看,别跳过
|
|
143
|
+
|
|
144
|
+
python <SKILL_DIR>/scripts/render.py --script film.json --work work \
|
|
145
|
+
--out out/film.mp4 --stage all
|
|
146
|
+
# TTS→排版→配乐→音效→混音→逐帧→编码→质检 一条龙;中间产物全在 work/,可断点续跑。
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**关于 `--scenes`(v1.0.2 的零拷贝关键)**:`render.py` 不写 `--scenes` 时的定位顺序是
|
|
150
|
+
**① 剧本同目录的 `scenes.py` → ② 技能内置 `assets/scenes.py`**。所以默认情况下你什么都不用写、
|
|
151
|
+
也不用拷,内置渲染器直接出片;**只有想改画面**时才在剧本同目录放一个自己的 `scenes.py`
|
|
152
|
+
(实现下面的 `draw_frame` 与可选的 `cues()`),它会被自动发现——也可以显式 `--scenes my_scenes.py`。
|
|
153
|
+
内置渲染器源码就是最好的范例:`<SKILL_DIR>/assets/scenes.py`。
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
# 真实签名(六个参数,少一个都会崩):`scenes.draw_frame`
|
|
157
|
+
def draw_frame(scene, t_local, dur, t_global, script, frame):
|
|
158
|
+
# scene : 这一段在 timeline 里的字典(id / kind / scene_type / sub / big / items / stats ...)
|
|
159
|
+
# t_local : 段内时间(秒,0..dur)——入场/持续动效都按它在段内推进
|
|
160
|
+
# dur : 这一段的总时长(秒)
|
|
161
|
+
# t_global : 全片时间(秒)——跨段的连续动效(镜头缓推、呼吸)用它
|
|
162
|
+
# script : 整份剧本/计划字典(plan、主题色、words 等;有缓存,别原地改)
|
|
163
|
+
# frame : 绝对帧号(整数)——需要确定性噪声/相位时用它,别用 time()
|
|
164
|
+
return img # PIL.Image,必须 1920x1080 RGB(用 K.new_frame() 起手最稳)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
> 写自定义 `scenes.py` 时先抄 `<SKILL_DIR>/assets/scenes.py` 的一个绘制器(`_draw_hero` 之类),
|
|
168
|
+
> 它已经把纯函数、缓存排版、预烘焙底版这些坑都绕过一遍了。
|
|
169
|
+
|
|
170
|
+
**为什么先提案**:v1.0.1 起没有"内置风格"可选,外观由 `design.json` 决定。
|
|
171
|
+
同一个 brief 连拍两次会得到不同设计(配色/母题/转场/配乐/声线都可能不同),
|
|
172
|
+
所以流程必须是"生成方案 → 人确认 → 冻结 → 出片";不确认就渲染,`render.py` 会打提案警告
|
|
173
|
+
(加 `--require-proposal` 则直接失败)。设计契约见 [references/proposal.md](references/proposal.md)。
|
|
174
|
+
|
|
175
|
+
**分阶段跑**(每个 stage 只做自己的事,落盘后可单独重跑):
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage style # 解析已确认的 design,打印主题/视觉语言/版式轮换/字幕样式
|
|
179
|
+
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage tts # 只合成旁白 + 实测时长(带输入校验的缓存)
|
|
180
|
+
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage layout # 只排时间轴 + 选构图,落 timeline.json / sub.srt / storyboard.json
|
|
181
|
+
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage cues # 只出提示点 -> work/cues.json
|
|
182
|
+
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage score # 只出配乐 -> work/music.wav(musicgen 现场编排)
|
|
183
|
+
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage sfx # 只出音效轨(sfxkit,按 cues.json)
|
|
184
|
+
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage audio # 只混音(旁白+配乐+音效,旁白优先)
|
|
185
|
+
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage frames # 只渲染帧(--engine pil|web,--jobs N 分块并行)
|
|
186
|
+
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage mux # 只合成(视频流 copy,两遍 loudnorm)
|
|
187
|
+
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage qa # 只质检(只读输入,只写 work/qa/)
|
|
188
|
+
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage stills # 只渲母版静帧 + 联系表
|
|
189
|
+
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage accept --gate stills # 人工签核一道闸(写 work/signoff.json)
|
|
190
|
+
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage explain # 只报"哪些阶段过时、为什么、该跑什么",不跑任何 stage
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
`--stage all` 的完整顺序:`style → tts → layout → cues → score → sfx → audio → frames → mux → qa`。
|
|
194
|
+
**任何一步出问题都会在该步停下**,不会带着坏输入继续往下跑。
|
|
195
|
+
|
|
196
|
+
**开渲前先算成本**(可选;`estimate.py` 是仓库工具,按 `<REPO>` 调用):
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
python <REPO>/tools/estimate.py --script film.json --jobs 4
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
它给帧数/各阶段耗时/磁盘/分块建议(`--json` 可机读),是**区间上界**不是承诺;
|
|
203
|
+
有 `work/timeline.json` 时用真实时长,否则按字数粗估。要省掉全片 VMAF 就在 `film.json` 写
|
|
204
|
+
`"qa": {"vmaf": {"mode": "off"}}`(estimate 与 render 都读这个字段,G11 记 skipped)。
|
|
205
|
+
|
|
206
|
+
**看交付凭据再签核**:
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
cat out/QC-REPORT.md # gate matrix 分 pass / fail / skipped 三档;skipped = 本次没执行,不等于通过
|
|
210
|
+
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage accept --gate release
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
签核绑内容指纹:改了剧本 / `scenes.py` / 设计,旧签核自动失效,要重新看、重新签。
|
|
214
|
+
交付前想强制(skipped 也算失败):加 `--strict-gates`。
|
|
215
|
+
|
|
216
|
+
**长片渲染**(分块并行 + 断点续跑):
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage frames --jobs 4
|
|
220
|
+
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage frames --jobs 4 --resume
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
> 分块落在 `work/segments/`,按 `seg_<起始帧>_<结束帧>.mp4` 命名;拼接前会**显式检测缺口**,
|
|
224
|
+
> 缺块直接报错,不允许"少一段也能出片"。默认 chunk 按 `总帧数 / jobs / 4` 向整秒取整(jobs=4、
|
|
225
|
+
> 900 帧时约 8 秒一段),可用 `film.json` 的 `chunk_frames` 覆盖。
|
|
226
|
+
>
|
|
227
|
+
> **改台词 / 改 scenes.py / 改设计 / 改编码参数之后,`--resume` 会拒绝复用旧分块**(`work/segments/_plan.json`
|
|
228
|
+
> 里存着计划指纹),并提示你 `rm -rf work/segments`——这是刻意的:旧分块配新时间轴会拼出
|
|
229
|
+
> "画面和旁白对不上"的片子,而且时长门禁查不出来。
|
|
230
|
+
|
|
231
|
+
## 2. 标准八阶段(P0–P7)
|
|
232
|
+
|
|
233
|
+
| 阶段 | 产出 | 关键动作 | 常见错误 |
|
|
234
|
+
|---|---|---|---|
|
|
235
|
+
| **P0.5 定设计** | `work/design.json` + `work/style.json` | `propose.py` 出方案 → `--confirm` 冻结设计;`--stage style` 只解析已确认的设计并打印主题/视觉语言/版式轮换/字幕 | 跳过 → `render.py` 报错(没有 design 就没有外观);不确认就渲染会打提案警告 |
|
|
236
|
+
| **P0 定基准** | `CONVENTIONS.md` | 分辨率/帧率/时长/BPM→帧换算一次算死,全项目引用同一份 | 各文件自己写 `150` 这种魔数 → 幕边界对不齐 |
|
|
237
|
+
| **P1 写剧本** | `film.json` | 每句拆 `text`(朗读)与 `sub`(字幕);每段一个 `kind`;可选 `lines[].type` 锁定构图 | 一个字段两头用 → 字幕太长或读起来生硬;**别写 `style`**(不再影响画面,外观由 `work/design.json` 决定) |
|
|
238
|
+
| **P2 事实核查** | `FACTS.md` | 上屏的每个数字/年份有出处编号;建"不能上屏"黑名单 | 跳过这步 → 画面和旁白数字打架,评论区第一条就是你错了 |
|
|
239
|
+
| **P3 TTS** | `work/tts/*.mp3` + `tts.json` | edge-tts 逐句合成,`ffmpeg -f f32le` 实测时长,磁盘缓存 | 按字数估时长 → 字幕漂移 |
|
|
240
|
+
| **P4 排时间轴** | `work/timeline.json` + `sub.srt` | `dur = lead + 实测语音 + tail`,句读必须在本段内 | 段太短 → 旁白被下一段截断 |
|
|
241
|
+
| **P5 混音** | `work/audio.wav` | numpy 总线:旁白 + 配乐 + 音效按采样点叠加 | ffmpeg 多输入混流 → 非确定性静音 |
|
|
242
|
+
| **P6 母版** | 3 张静帧 | 先渲 3 帧看构图/配色/字重,定了再渲全片 | 直接渲全片 → 36000 帧后发现字体丑 |
|
|
243
|
+
| **P7 成片** | `out/film.mp4` | rawvideo→libx264 CRF18 + 两遍 loudnorm 到 -16 LUFS | 码率过低/响度 -25 LUFS → "像草稿" |
|
|
244
|
+
|
|
245
|
+
> 注:`filmkit` / `render.py` 的默认是 `crf=18`;
|
|
246
|
+
> `assets/film.json` 只是**测试夹具**(跑通全链与金帧/门禁回归用,不是给你抄的模板),它写的是 17。
|
|
247
|
+
> 自己写剧本时不写 `crf` 就是 18;追求更锐利的文字可以写 17(体积约 +30%)。
|
|
248
|
+
|
|
249
|
+
## 3. 渲染引擎怎么选
|
|
250
|
+
|
|
251
|
+
| 场景 | 选型 | 实测速度 | 适合 |
|
|
252
|
+
|---|---|---|---|
|
|
253
|
+
| 图文/数据/科普/UI 风 | **PIL(numpy+cv2 辅助)** — `filmkit` 默认 | 40–80 fps @1080p30(示例片 75) | 绝大多数中文科普片、报告转视频 |
|
|
254
|
+
| 需要流畅矢量/粒子/3D/纸纹质感 | **无头浏览器 Canvas2D/WebGL + CDP 截图 → image2pipe**(`webengine.py`) | 80–130 ms/帧(核显,PNG 截图) | 纪录片级长片、需要复杂视觉语言 |
|
|
255
|
+
|
|
256
|
+
> 上面两行是**本机空载**实测口径(换机、重负载画面都会变;详见 §12)。别把区间当保证。
|
|
257
|
+
| 纯数据动画 | **manim**(若已安装) | 视场景而定 | 数学/公式推导 |
|
|
258
|
+
|
|
259
|
+
> **设计只覆盖 PIL 引擎**(`engine: pil`,默认):`work/design.json` 的配色/字体/版式/母题
|
|
260
|
+
> 由 `assets/scenes.py` 读取。`--engine web` 的页面由**你自己**写(`assets/web/` 只是能跑通的示例页,
|
|
261
|
+
> 不是出片模板),画面不跟随设计;但配乐与旁白**照样**跟随 `design.json`(它们不依赖渲染引擎)。
|
|
262
|
+
|
|
263
|
+
**默认走 PIL**:依赖少(numpy+Pillow 就够)、启动快、确定性强、调试成本低。
|
|
264
|
+
需要"一镜到底的粒子形变 / 实时着色器质感"才上浏览器方案——**v0.3 起它是真的可用了**,不再是"文档里说有":
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
cp -r <SKILL_DIR>/assets/web ./web # 页面模板(原生 Canvas2D,零三方库)
|
|
268
|
+
pip install playwright && python -m playwright install chromium # 只在走 web 引擎时需要
|
|
269
|
+
python render.py ... --engine web --stage cues # 从页面导出提示点
|
|
270
|
+
python render.py ... --engine web --stage frames --jobs 3 --resume
|
|
271
|
+
python webengine.py textqa ./web/index.html # 文字重叠/出画/一闪而过/从没露面
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
两条路线的取舍(**先想清楚再选,换引擎要重做画面**):
|
|
275
|
+
|
|
276
|
+
| | PIL(`engine: pil`) | 浏览器(`engine: web`) |
|
|
277
|
+
|---|---|---|
|
|
278
|
+
| 依赖 | numpy + Pillow(+cv2 做锐度比) | + playwright + Chromium/Edge |
|
|
279
|
+
| 速度 | 40–80 帧/秒 | 8–12 帧/秒(逐帧截图+编码,慢一个数量级) |
|
|
280
|
+
| 画面语言 | 位图合成、渐变、几何、文字 | Canvas2D/WebGL:粒子、形变、着色器、矢量动画 |
|
|
281
|
+
| 确定性 | 天然(纯函数画图) | 靠"把时间轴精确 seek 到 f/FPS"保证(`window.renderFrame`) |
|
|
282
|
+
| 调试 | 改一帧跑一帧 | 浏览器里 `index.html?dev=1&t=秒` 直接看,出了 bug 好查 |
|
|
283
|
+
| 适合 | 图文/数据/科普/宣传片 | 纪录片级长片、需要一镜到底的视觉语言 |
|
|
284
|
+
|
|
285
|
+
**浏览器引擎的三条硬约束**(`webengine.py` 已实现,自己写页面时也要守):
|
|
286
|
+
1. 所有补间放在一条**暂停的**主时间轴上,渲染第 f 帧 = 把它 seek 到 `f / FPS` 秒;**不许依赖播放**。
|
|
287
|
+
2. 循环/粒子/波浪这类程序化内容用"按时间直算"的更新器(`update(fn)`),不许用 `setInterval` 累加。
|
|
288
|
+
3. 页面必须暴露 `window.ready / DURATION / renderFrame(f) / getCues()`;**不连 CDN、不请求网络**。
|
|
289
|
+
|
|
290
|
+
要**跨浏览器会话**的字节级可复现(崩溃重开、多次渲染做 diff、质检要比对两张图)时开确定性模式:
|
|
291
|
+
`--deterministic` 或环境变量 `WEBENGINE_DETERMINISTIC=1`(关 GPU 光栅,实测 8/8 会话一致,
|
|
292
|
+
代价约 +22%/帧)。默认不开:同一会话内本来就一致,跨会话只有 ≤4 像素、±1 级的抗锯齿差异。
|
|
293
|
+
|
|
294
|
+
## 4. 画质清单(这几项决定"像不像专业片")
|
|
295
|
+
|
|
296
|
+
- **亮度与层次**:暗底 (`#0a1420`) + 顶部亮 accent;不要纯黑纯白,用户屏幕会吃掉渐变。
|
|
297
|
+
- **字重与字号**:大字 84–96px、副标题 34–38px、字幕 46px;中文用 `msyhbd.ttc`(微软雅黑粗体)。
|
|
298
|
+
- **字幕**:≤ 2 行、单行约 24 个中文字、带半透明背板、`y ≥ 872`。
|
|
299
|
+
实际折行看的是**像素宽度**(`fit_sub_lines` 按 `max_w=1560 px` 自动折),24 字是"大致上限"并非硬校验
|
|
300
|
+
——`SUB_MAX_CHARS=24` 这个常量目前**没有被任何代码引用**,别指望它拦你。
|
|
301
|
+
- **节奏**:每 3–6 秒要有视觉变化(新卡片 / 位移 / 数字滚动);静止超过 6 秒观众就走。
|
|
302
|
+
- **镜头**:只做 3–15% 缓推,不要大幅拉远拉近。
|
|
303
|
+
- **调色**:整片一个主色 + 一个强调色,别超过 4 个色相。
|
|
304
|
+
- **音频**:母带 -16 LUFS / 真峰 ≤ -1 dBTP;旁白比间隙响 12 dB 以上;AAC 前留 1.4 dB 余量。
|
|
305
|
+
|
|
306
|
+
## 5. 声音:配乐 + 音效 + 提示点(v0.3.0)
|
|
307
|
+
|
|
308
|
+
> 这一节的思路来自一部真实成片(`ai-history-film`)的复盘:**声音不好不是"混得不够响",而是"声音不认识画面"**。
|
|
309
|
+
> 解决顺序永远是:先让声音知道画面上发生了什么(提示点),再谈音色,最后才是母带。
|
|
310
|
+
|
|
311
|
+
### 5.1 三层声音
|
|
312
|
+
|
|
313
|
+
| 层 | 谁做 | 落盘 | 说明 |
|
|
314
|
+
|---|---|---|---|
|
|
315
|
+
| 旁白 | `filmkit.tts`(edge-tts) | `work/tts/*.mp3` | **唯一的时长权威**:时间轴由它反推 |
|
|
316
|
+
| 配乐 | `musicgen.py`(参数化编排) | `work/music.wav` | 按设计里的 bpm/key/scale/mood/seed + 编排参数现场生成;**没有现成曲风可挑** |
|
|
317
|
+
| 音效 | `sfxkit.py`(物理建模) | `work/sfx.wav` | 按 `cues.json` 里登记的提示点逐条合成 |
|
|
318
|
+
|
|
319
|
+
三条总线在 `--stage audio` 里合成:**音效响起时配乐自动让位**(`duck_db`,v0.7 默认 4 dB,
|
|
320
|
+
cinema/epic 5.5 dB;旧版 3 dB 在段落过门时配乐几乎与音效等高),
|
|
321
|
+
最后交给 ffmpeg 两遍 loudnorm 到 -16 LUFS / 真峰 ≤ -1 dBTP。**不要**自己在 numpy 里做最终响度标准化——
|
|
322
|
+
你有 `loudnorm`,它比手写归一化准得多,而且真峰才是门禁。
|
|
323
|
+
|
|
324
|
+
> **v0.7 片内分离**:配乐在段落交界本来就有过门(编排器写的 swell / riser / sub_boom),
|
|
325
|
+
> sfx 轨不再在同一个边界重复放 whoosh/riser——`--stage sfx` 会对落在
|
|
326
|
+
> [边界 - 2.2 s, 边界] 内的转场类 cue 自动 -3 dB(`sfx.duck_transitions=false` 可关)。
|
|
327
|
+
> 提示音的音高现在跟随配乐调性(`in_key(freq, key)`),不再硬编码 D 小调五声。
|
|
328
|
+
|
|
329
|
+
### 5.2 提示点:让声音跟着画面走
|
|
330
|
+
|
|
331
|
+
**这是 v0.3 最重要的一个概念。** 画面在该响一下的地方登记一个提示点,音频阶段按提示点合成音效:
|
|
332
|
+
|
|
333
|
+
```python
|
|
334
|
+
# 方式一:写在 film.json 里(好改,适合少量关键音效)
|
|
335
|
+
"sfx": {"cues": [
|
|
336
|
+
{"at": "l03+0.05", "type": "impact", "size": 1.1},
|
|
337
|
+
{"at": "abs:12.5", "type": "whoosh", "dur": 0.9}
|
|
338
|
+
]}
|
|
339
|
+
|
|
340
|
+
# 方式二:写在 scenes.py 的 cues() 钩子里(推荐——画面改了声音自动跟着改)
|
|
341
|
+
def cues(tl, script):
|
|
342
|
+
out = []
|
|
343
|
+
for s in tl["scenes"]:
|
|
344
|
+
for i, _it in enumerate(s.get("items") or []):
|
|
345
|
+
out.append({"at": "%s+%.2f" % (s["id"], 0.48 + 0.32 * i), "type": "ui_blip"})
|
|
346
|
+
return out
|
|
347
|
+
|
|
348
|
+
# 方式三:web 引擎里由页面导出(提示点跟着动画代码走)
|
|
349
|
+
S.cue(t, 'impact', {size: 1.1}); // 页面里的时刻是"本幕局部秒"
|
|
350
|
+
python render.py ... --engine web --stage cues # 自动导出 work/cues.json
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
`at` 有三种写法:`"abs:12.5"`(绝对秒)、`12.5`(数字同义)、`"l03+0.35"` / `"l03-0.2"`(段 id ± 偏移)。
|
|
354
|
+
|
|
355
|
+
**为什么值得多写这一层**:改一句台词、加一个停留窗口、把 BPM 从 120 改成 100,
|
|
356
|
+
所有音效仍然贴着画面,不需要重新对时间。纯手工"在 12.5 秒放一声"的方案,改一次节奏就全废。
|
|
357
|
+
|
|
358
|
+
### 5.3 配乐(musicgen 编排;scorekit 只是合成原语)
|
|
359
|
+
|
|
360
|
+
> 配乐的"身份"(调式/调性/BPM/情绪)由 `design.json` 的 `audio.music` 决定(§5.5),
|
|
361
|
+
> 下面这些都是**可选**的底层覆盖;不写就听设计的。
|
|
362
|
+
|
|
363
|
+
`film.json` 里仍可手调这些底层参数(`bpm: "auto"` 表示按 design 的 BPM 走):
|
|
364
|
+
|
|
365
|
+
```json
|
|
366
|
+
"bpm": "auto",
|
|
367
|
+
"music": {"enabled": true, "mode": "score", "mood": "auto",
|
|
368
|
+
"key": "auto", "seed": 7, "seed_from_content": true,
|
|
369
|
+
"gain_db": -12, "duck_db": 4.0,
|
|
370
|
+
"sections": [{"at": "l01", "name": "intro", "energy": 0.3},
|
|
371
|
+
{"at": "l03", "name": "drive", "energy": 0.8}]}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
> `music.style` 是已删除字段(v1.0.1 起没有配乐风格表),写了也不参与渲染;
|
|
375
|
+
> 配乐的调式/调性/BPM/情绪/seed 由 `design.json` 的 `audio.music` 决定,`musicgen` 现场编排。
|
|
376
|
+
|
|
377
|
+
- **一张和弦表贯穿全片**,段落之间只换"色彩与能量",不换调——这是"听起来像一首曲子"而不是"一堆配乐"的关键。
|
|
378
|
+
- `energy` 控制律动层数(pad / bass / kick / hat / arp)与亮度;安静段比律动段低 4–8 dB。
|
|
379
|
+
- 段落边界**对齐小节线**(按 `bpm` 算),画面重音才落在拍点上。
|
|
380
|
+
- 重击前用 `Score.suck()` 让配乐"吸一口气"(压低 3–8 dB、0.2 s 内恢复),落差立刻出来——
|
|
381
|
+
**不要**用静音断层,那会让声音像断了。
|
|
382
|
+
- `"mode": "bed"` 是 v0.2 的和弦床兜底;`"mode": "off"` 不要配乐。
|
|
383
|
+
|
|
384
|
+
### 5.4 音效(sfxkit)
|
|
385
|
+
|
|
386
|
+
`sfxkit` 是**物理建模**而不是"画波形":撞击 = 材质(衰减率)× 形状(模态比)× 敲击位置 × 接触时间;
|
|
387
|
+
风 = 噪声 + 时变滤波(参数由慢变控制曲线驱动);雨/火 = 泊松事件 + 幂律幅度 + 多核散布。
|
|
388
|
+
|
|
389
|
+
```bash
|
|
390
|
+
python sfxkit.py --list # 全部配方与参数
|
|
391
|
+
python sfxkit.py --render impact --seed 3 --set size=1.2 --out /tmp/i.wav
|
|
392
|
+
python -c "import sfxkit; print(sfxkit.render('whoosh', dur=1.2).shape)"
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
常用配方(**cue 的 `type` 就是这些名字**):
|
|
396
|
+
|
|
397
|
+
| 用途 | 配方 |
|
|
398
|
+
|---|---|
|
|
399
|
+
| 界面/提示 | `ui_blip` `ui_click` `relay_click` `roulette_tick` `answer_chime` |
|
|
400
|
+
| 文字 | `typing` `typing_at`(逐字对齐,`times` 是相对提示点的秒数组) `text_stream` |
|
|
401
|
+
| 转场 | `whoosh` `riser` `impact` `sub_drop` `reverse_swell` `glitch` `tape_rewind` |
|
|
402
|
+
| 环境 | `wind` `rain` `campfire` |
|
|
403
|
+
| 撞击 | `knock_wood` `metal_hit` `glass_clink` `go_stone` |
|
|
404
|
+
|
|
405
|
+
**音效设计的三条经验**(都是踩过坑才写下的):
|
|
406
|
+
|
|
407
|
+
1. **"叮叮咚咚"是头号污染源**:有音高的小提示音多了就变成八音盒。全片只留几个**关键**的叮
|
|
408
|
+
(开场任务完成、认出猫、片尾),其余换成不带音高的"咔嗒"。
|
|
409
|
+
2. **"嗖"要从物理出发**:多普勒 + 1/r 音量钟形 + 越远越闷 + 地面反射的梳状相位。
|
|
410
|
+
合成器式的扫频"咻"听起来像电子音,物理飞过模型才像"有东西飞过去"。
|
|
411
|
+
它的定位是**环境声**:基准电平要比同刻配乐低 10 dB 以上。
|
|
412
|
+
3. **音量一律在成片混音里量**,不要按配方内部音量推断——混音时每个音效会先按峰值归一,配方里的"响"是没用的。
|
|
413
|
+
|
|
414
|
+
### 5.5 设计 DSL:外观的唯一来源(v1.0 起)
|
|
415
|
+
|
|
416
|
+
**没有"内置风格"可选。** v1.0.1 删掉了全部风格表(`STYLES` / `THEMES` / `VISUAL_LANGUAGES` / `presets/`),
|
|
417
|
+
v1.0.2 也没有 `style` 字段:`film.json` 里写任何 `style` 值都不再影响画面。
|
|
418
|
+
外观的唯一来源是 `work/design.json`——由 `propose.py` 现场采样、你 `--confirm` 之后冻结。
|
|
419
|
+
|
|
420
|
+
```bash
|
|
421
|
+
python <SKILL_DIR>/scripts/propose.py --brief "科技发布会,深蓝,要遮幅" --script film.json --work work
|
|
422
|
+
python <SKILL_DIR>/scripts/propose.py --confirm --candidate A --work work # -> work/design.json
|
|
423
|
+
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage style # 看这份设计到底定成什么样
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
`design.json` 的六块(完整契约见 [references/proposal.md](references/proposal.md) 与
|
|
427
|
+
`<REPO>/schemas/design.schema.json`):
|
|
428
|
+
|
|
429
|
+
| 块 | 决定什么 | 关键字段 |
|
|
430
|
+
|---|---|---|
|
|
431
|
+
| `structure` | 段落数与目标时长 | `segments` / `beats` / `hold_policy` / `target_sec` |
|
|
432
|
+
| `visual` | 画面:配色、字体、版式、动效、装饰、字幕 | 见下面的字段表 |
|
|
433
|
+
| `audio` | 配乐参数、声线、音效密度 | `music`(调式/BPM/调性/情绪/seed)/ `voice` / `sfx` |
|
|
434
|
+
| `pacing` | 节奏 | `lead` / `tail` / `min_dur` / `hold` / `item_stagger` |
|
|
435
|
+
| `id` / `label` | 指纹与中文标签 | `design:<hash>` / 一句中文立意 |
|
|
436
|
+
| `pitch` | 说清"这个方案想干成什么样" | 中文一句话 |
|
|
437
|
+
|
|
438
|
+
`visual` 的字段(`design.json` 可手改,改完用 `python <SKILL_DIR>/scripts/designkit.py --lint work/design.json` 校验):
|
|
439
|
+
|
|
440
|
+
| 字段 | 取值 | 说明 |
|
|
441
|
+
|---|---|---|
|
|
442
|
+
| `palette` | 11 个颜色角色 | **连续色相空间**里的一点(`hsv` 求解),每套都必须过 `theme_lint` 对比度体检 |
|
|
443
|
+
| `family` | `sans` / `serif` / `kai` / `round` | 字族 |
|
|
444
|
+
| `typography` | `tracking` / `line_gap` / `number`(8 种编号写法)/ `ratio` / `align` | 排版 |
|
|
445
|
+
| `motif` | 8 种装饰母题 × 密度 | 母题决定"框长什么样"(`brackets` / `hairlines` / `handdrawn` / `slabs` / `minimal` / `letterbox` / `signal` / `rays`) |
|
|
446
|
+
| `transition` | 6 种段界转场 + 时长 | `dip` / `wash` / `paper` / `wipe` / `cut` / `iris` |
|
|
447
|
+
| `motion` | `ease`(4 档)/ `enter` / `push` / `breathe_hz` | 镜头缓推与入场 |
|
|
448
|
+
| `bias` | `x` / `y` / `inset` | 整体重心偏移 |
|
|
449
|
+
| `layout` | `left-stack` / `center-stack` / `split-right` | 默认版式 |
|
|
450
|
+
| `layout_cycle` | 版式数组(v1.0.2,可选) | 按章节轮换,见下 |
|
|
451
|
+
| `hud` | `chip-left` / `corner-line` / `none` | 页眉装饰 |
|
|
452
|
+
| `size` / `fps` | `1920x1080` / `30` | 本版只做 16:9 横屏(可下采样到 720p) |
|
|
453
|
+
| `subtitle` | 字幕样式(v1.0.2,可选) | 见下 |
|
|
454
|
+
|
|
455
|
+
离散维度合计 **8 母题 × 6 转场 × 4 缓动 × 8 编号 × 3 版式 × 3 HUD × 4 字族 × 7 情绪 × 2 调式 =
|
|
456
|
+
774,144 种组合**,再乘配色这个连续量(色相 0–360° 任意取值,只是采样器会在锚点附近取);
|
|
457
|
+
**这个数是"字段能组合出多少种",不是"推荐你从中挑一套"**——好看与否仍由对比度体检、分镜语法和你的目检决定。
|
|
458
|
+
|
|
459
|
+
#### 5.5.1 字幕样式 `visual.subtitle`(v1.0.2,可选)
|
|
460
|
+
|
|
461
|
+
以前字幕的字号 / 位置 / 背板透明度 / 圆角全是 `filmkit` 里的写死常量,于是**任何两条片子**的字幕带一模一样。
|
|
462
|
+
现在它们进设计 DSL;**不写 = 补齐默认值,与过去逐像素一致**(老 `design.json` 不受影响)。
|
|
463
|
+
|
|
464
|
+
```json
|
|
465
|
+
"subtitle": {"size": 46, "y": 560, "align": "left", "plate_alpha": 170,
|
|
466
|
+
"radius": 10, "family": "sans", "color": [245, 245, 245], "max_lines": 2}
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
| 字段 | 默认 | 范围 / 取值 | 作用 |
|
|
470
|
+
|---|---|---|---|
|
|
471
|
+
| `size` | `46` | 20–96 | 字幕字号 |
|
|
472
|
+
| `y` | `null` | 0–1030 | 字幕带基准线;`null` = 默认贴底安全区 |
|
|
473
|
+
| `align` | `center` | `center` / `left` | 对齐 |
|
|
474
|
+
| `plate_alpha` | `170` | 0–255 | 字幕背板透明度(0 = 无背板) |
|
|
475
|
+
| `radius` | `10` | 0–48 | 背板圆角 |
|
|
476
|
+
| `family` | `null` | `sans` / `serif` / `kai` / `round`;`null` = 跟随大标题字族 | 字幕字族 |
|
|
477
|
+
| `color` | `null` | `[r,g,b]` 或 `"#rrggbb"`;`null` = 默认墨色 | 字幕颜色 |
|
|
478
|
+
| `max_lines` | `2` | 1–3 | 最多折几行 |
|
|
479
|
+
|
|
480
|
+
采样器默认就会在这些区间里变化(字号 40–56、背板透明度 96–194、圆角 2–20、
|
|
481
|
+
多数居中偶尔左对齐、1/3 概率抬高),所以**默认出的片字幕也不会两条一模一样**。
|
|
482
|
+
|
|
483
|
+
#### 5.5.2 片内版式轮换 `visual.layout_cycle`(v1.0.2,可选)
|
|
484
|
+
|
|
485
|
+
以前 `visual.layout` 是单值 → 全片所有章节同一个骨架。现在可以给一串版式,
|
|
486
|
+
`grammar` 在**章节**维度上轮换:同一章内版式固定(观众视角里一章是一段整体),**换章才换版式**。
|
|
487
|
+
这就是"同一视频内部不要大面积重复"的落地方式。
|
|
488
|
+
|
|
489
|
+
```json
|
|
490
|
+
"layout_cycle": ["split-right", "left-stack", "center-stack"]
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
- 元素只能取 `left-stack` / `center-stack` / `split-right`;**至少 2 种不同版式**(只写一种等于不轮换,
|
|
494
|
+
`designkit.validate_design` 直接报错,不静默放过)。
|
|
495
|
+
- **不写**:`design_to_plan` 按设计指纹自动派生 **2–3 种**的轮换序列——默认就不会"全片一张版式"。
|
|
496
|
+
- 可审计:`--stage layout` 打印每段选了哪类构图,并落 `work/storyboard.json`,
|
|
497
|
+
每段都带 `layout` 字段(哪一章用了哪个骨架一眼可查)。
|
|
498
|
+
|
|
499
|
+
#### 5.5.2b 时长要求:关于「2 分钟」这类 brief 的真实语义(v1.0.2)
|
|
500
|
+
|
|
501
|
+
`--brief` 里写「2 分钟」「90 秒」「一分半」「3 分 30 秒」会被解析成 `duration_target`,并落进
|
|
502
|
+
`design.structure.target_sec`。它真正决定两件事:
|
|
503
|
+
|
|
504
|
+
1. **候选的段落数**(`designkit` 按目标时长决定 `structure.segments`),所以 90 秒的 brief 与 30 秒的 brief,
|
|
505
|
+
候选结构本身不同;
|
|
506
|
+
2. **渲染时的对账**:`render.py` 会打印「目标时长 vs 实测」,差 > 15% 时告警。
|
|
507
|
+
|
|
508
|
+
**但它不会截断旁白**:成片长度仍由 TTS 实测时长决定。写完 `film.json` 的台词后如果总时长和目标差太多,
|
|
509
|
+
要改的是台词量(或 `pacing` 的停留),不是指望渲染器「压到 90 秒」。
|
|
510
|
+
|
|
511
|
+
#### 5.5.3 设计多样性:候选之间、以及片与片之间
|
|
512
|
+
|
|
513
|
+
`propose.py` 的候选不是"换个颜色"就叫多方案。两个设计在 **10 个离散轴**上比较
|
|
514
|
+
(`designkit.design_distance`):配色 / 母题 / 转场 / 缓动 / 编号 / **配乐(调式 + BPM 档 + 情绪复合键)** /
|
|
515
|
+
声线 / 情绪 / 版式 / 调式。规则:
|
|
516
|
+
|
|
517
|
+
1. **候选之间**至少 **2 个轴**不同,否则重新采样(`propose` 会把"距离"打在候选表里,一眼能看出谁和谁像)。
|
|
518
|
+
2. **底色必须拉开色相**:两个候选的底色色相差 < 6° 一律重采(并排看就是同一片底色);
|
|
519
|
+
色相差 < 12° 时,只有**结构差异 ≥ 3 个轴**才接受。
|
|
520
|
+
v1.0.2 修掉了旧实现的两个坑:以前按"配色族"量化到 24° 分桶判重,带 `--brief "深蓝"` 这类约束时
|
|
521
|
+
可选色相窗口只有 52°,n=3 有近一半概率直接采样失败;现在直接比色相距离,
|
|
522
|
+
实测带约束的 brief 连跑 10 次 **10/10 成功**(含"深蓝 + 遮幅")。
|
|
523
|
+
3. **配乐轴**以前恒失效(读的是已删除的 `audio.music.style`),现在按
|
|
524
|
+
**调式 / BPM 档(±6 归一)/ 情绪** 的复合键判定——"两个候选配乐不一样"是真的在比。
|
|
525
|
+
|
|
526
|
+
#### 5.5.4 片间去重台账(v1.0.2,默认开启)
|
|
527
|
+
|
|
528
|
+
"每条视频都不一样"不能只靠随机:同一个题材连拍三部,仍可能撞到相近的配色/版式组合。
|
|
529
|
+
`propose.py` 现在把**确认过(= 真要出片)的设计**记进用户级台账,提案时主动避开:
|
|
530
|
+
|
|
531
|
+
| 项 | 值 |
|
|
532
|
+
|---|---|
|
|
533
|
+
| 默认路径 | `~/.dsh/video-studio/design-ledger.json`(最多保留 500 条) |
|
|
534
|
+
| 环境变量 | `VIDEO_STUDIO_LEDGER` 可改路径;设为 `off` / `0` / `false` / `no` 即关闭 |
|
|
535
|
+
| CLI | `--no-ledger`(本次不读不写)/ `--show-ledger`(打印路径与最近 20 条) |
|
|
536
|
+
| 判据 | 这批候选与最近交付设计的最小距离 < **2 维** → 换新 nonce 重采(最多 4 轮),并在输出里告警 |
|
|
537
|
+
| 记录时机 | **`--confirm` 时**才记(草稿候选不进台账) |
|
|
538
|
+
|
|
539
|
+
读不到 / 写坏台账都当空表处理——**台账永远不会因为文件损坏而阻塞出片**。
|
|
540
|
+
写文档或做回归时把 `VIDEO_STUDIO_LEDGER` 指到临时目录,别动用户真实台账。
|
|
541
|
+
|
|
542
|
+
#### 5.5.5 三条硬约束(违反会让片子"花"或者被门禁拦)
|
|
543
|
+
|
|
544
|
+
1. 同一部片**不许跨明暗族**换主题(深蓝段直接切暖白段会精确触发闪烁门禁,观众也看着像换片)。
|
|
545
|
+
2. 主题必须过 `stylekit.theme_lint` 的对比度体检(ink≥7 / muted≥4.5 / dim≥3 / accent≥4.5 / accent2≥4.5)。
|
|
546
|
+
3. 旁白全片最多 2–3 把声音(`voice.max_distinct`),相邻句不换人——否则像广播剧而不是旁白。
|
|
547
|
+
|
|
548
|
+
#### 5.5.6 已删除的东西(别照着旧文档试)
|
|
549
|
+
|
|
550
|
+
| 旧写法 | 现在 |
|
|
551
|
+
|---|---|
|
|
552
|
+
| `film.json` 写 `"style": "auto"` / 风格 id / `"legacy"` | **不再影响画面**;外观只认 `work/design.json`。没有 design 就渲染会报错并给两步指引 |
|
|
553
|
+
| `stylekit.py --list` / `--show <id>` / `--lint` | **已移除**:命令会打印"内置风格表已删除"并以 **exit 2** 结束(以前的 `--list` 直接 NameError、`--lint` 在空表上假绿) |
|
|
554
|
+
| `render.py --preset <id>` | 已删除(v1.0.1)。要指定外观只有两条路:自然语言 `--brief` / 直接写 `design.json` |
|
|
555
|
+
| persona 声线表(`bright_female` 之类) | 已删除:`voice` 只认裸 edge voice id + 响度补偿表;`voicekit.py --list` 可看当前音色与补偿 |
|
|
556
|
+
| `filmkit.resolve_theme()` / `style_plan()` 在无设计时"回落默认主题" | **不再静默回落**:直接抛错并告诉你去跑 `propose.py` → `--confirm` → `render.py --stage all` |
|
|
557
|
+
|
|
558
|
+
外观细节(配色怎么解、主题 35 个字段、如何手写一份设计)见
|
|
559
|
+
[references/style-packs.md](references/style-packs.md)——那篇现在讲的是**怎么用设计 DSL 定制外观**,
|
|
560
|
+
不是"8 套风格选一套"。提案流程与契约见 [references/proposal.md](references/proposal.md)。
|
|
561
|
+
|
|
562
|
+
### 5.5b 场景语法:同一部片内部的骨架也要变(v0.9)
|
|
563
|
+
|
|
564
|
+
v0.9 起画面不只有"皮肤"(配色/字体),还有三层正交结构:
|
|
565
|
+
|
|
566
|
+
| 层 | 决定什么 | 谁读它 | 产物 |
|
|
567
|
+
|---|---|---|---|
|
|
568
|
+
| 叙事 `kind` | 声线、语气、配乐能量 | voicekit / musicgen | 本版**没有新增 kind 值** |
|
|
569
|
+
| 场景 `type`(10 类构图) | 这段画面长什么样 | `assets/scenes.py` 的 10 个绘制器(`_DRAWERS`) | timeline 的 `scene_type`、`work/storyboard.json` |
|
|
570
|
+
| 设计 `visual` 五组签名 | 这套设计怎么画(母题/转场/字距/重心) | 设计 DSL → `work/style.json` | `work/style.json` 的 `language` 与 `layout_cycle` |
|
|
571
|
+
|
|
572
|
+
场景类型有 10 个:`hero` 封面 / `statement` 金句 / `list` 要点 / `stat` 数字卡 /
|
|
573
|
+
`compare` 左右对比 / `process` 流程轴 / `quote` 引语 / `chapter` 章节卡 /
|
|
574
|
+
`chart` 数据条 / `outro` 收尾。**不写就不管**——grammar 按内容自动选(字段存在性 >
|
|
575
|
+
关键词 > 结构位置,段长不足按固定表降级,连续同类型 ≤2 段);要锁定就写行级 `lines[].type`。
|
|
576
|
+
|
|
577
|
+
看选型(命令都不需要 `--scenes`,除非你要用自己的渲染器):
|
|
578
|
+
|
|
579
|
+
```bash
|
|
580
|
+
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage layout
|
|
581
|
+
# 分镜: 3 段 / 封面×1 要点列表×1 收尾×1
|
|
582
|
+
# l01 hook -> hero v0 d0 首段/开场
|
|
583
|
+
# l02 body -> list v1 d1 items>=3
|
|
584
|
+
# l03 outro -> outro v0 d0 末段/收尾
|
|
585
|
+
# 同时落 work/storyboard.json:每段的 type / variant / decor / layout / 理由,可审计
|
|
586
|
+
|
|
587
|
+
python <SKILL_DIR>/scripts/render.py --script film.json --work work --stage style
|
|
588
|
+
# 版式轮换: split-right -> left-stack(按章节轮换,防全片一张版式)
|
|
589
|
+
# 视觉语言: 母题=brackets 转场=paper(0.40s) 字距=3.0 编号=cn 重心=(-60,+60)
|
|
590
|
+
# 字幕: 底部安全区 44px 对齐=center 背板α=138 圆角=2 最多1行
|
|
591
|
+
# (坏配置在这里就拦,不等渲完 300 帧)
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
**边界(必须记住)**:
|
|
595
|
+
|
|
596
|
+
1. **比 v0.8.1 慢,但仍在预算内**:示例片 frames 约 **19 ms/帧(54–61 fps)**,预算 25 ms;
|
|
597
|
+
v0.8.1 约 13 ms/帧(约 75 fps)。慢在 10 类构图各自多了装饰与几何。
|
|
598
|
+
0.9.1 把装饰母题从"逐帧 paste(实测 5.6 ms)"改成"烘焙进背景底版(1.3–2.0 ms)"才回到现在这个数。
|
|
599
|
+
2. **同一部片仍锁一个明暗族**:段界跨族会整屏换底、触发闪烁门禁,也像换片。
|
|
600
|
+
3. **web 引擎不跟随场景语法**:`--engine web` 的页面由你自己写,没有 10 类构图,
|
|
601
|
+
画风也只有你页面里的颜色。
|
|
602
|
+
|
|
603
|
+
选型规则、`storyboard.json` 字段、加第 11 类构图的步骤、常见误用,见
|
|
604
|
+
[scene-grammar.md](references/scene-grammar.md);配色/母题/字幕等设计字段见
|
|
605
|
+
[style-packs.md](references/style-packs.md)。
|
|
606
|
+
|
|
607
|
+
### 5.6 混音链(照着做,别自创)
|
|
608
|
+
|
|
609
|
+
```
|
|
610
|
+
旁白(单声道, 居中) ─────────────────────────────┐
|
|
611
|
+
配乐(立体声, -12dB 基线) ─> 音效duck(-4dB) ─> 旁白duck(-6dB) ─┤─> 求和 ─> 峰值保护(0.89) ─> loudnorm 两遍 ─> -16 LUFS / TP<=-1
|
|
612
|
+
音效(立体声) ───────────────────> 旁白duck(-3dB) ─────────────┘
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
- **旁白优先是硬行为(v0.7.1)**,不是文档口号:`mix_film` 里旁白固定 `voice_gain=1.0` 居中、不被任何侧链影响;
|
|
616
|
+
配乐基线 `music.gain_db=-12`(旧版 -1.5,导致背景与台词几乎等高);有人声的地方再按旁白包络压
|
|
617
|
+
配乐 6 dB、音效 3 dB(`music.voice_duck_db` / `music.voice_duck_sfx_db`,间隙自动恢复)。
|
|
618
|
+
实测人声段"旁白/背景"响度差 **12.9 dB**(真实配乐素材下;合成信号口径 15 dB),
|
|
619
|
+
selfcheck 第 35/36 条与 `audio_similarity.py --strict` 的 D 组守着这条。
|
|
620
|
+
- **SNR 兜底(v0.7.1 最后一道闸)**:混音后按台词活跃区实测"人声/背景",不足 12 dB 就整体再压背景
|
|
621
|
+
(`music.guard_snr_db`,最多 `guard_cap_db=30`;补压 >18 dB 会提示检查素材电平)。
|
|
622
|
+
这是唯一一条"混音后自己改电平"的门禁,保证换任何配乐素材都不会把旁白盖住。
|
|
623
|
+
- **BGM 与音效是可选层**:它们的作用是铺情绪与打重音,不该抢字。想更安静就调低 `music.gain_db`
|
|
624
|
+
或设 `music.enabled=false`;"让观众听清旁白"永远优先于"背景好听"。
|
|
625
|
+
- 音效响时配乐自动让位:`design.json` 的 `audio.music.duck_db` 决定档位(默认 4.0 dB),
|
|
626
|
+
`film.json` 的 `music.duck_db` 显式值永远优先。旧版 `mix_film` 默认 3.0 dB,
|
|
627
|
+
在段落过门时配乐几乎与音效等高——这是"配乐和音效听着像同一套"的混音侧成因,已由 `duck_default` 抬高。
|
|
628
|
+
让得太多会"音乐一抖一抖",所以按设计分级而不是一律压狠。
|
|
629
|
+
- **本项目不做母带压缩**:链路是 求和 → duck → 峰值保护 0.89 → `ffmpeg loudnorm` 两遍。
|
|
630
|
+
代码里**没有压缩器**(想加请自行在 `mix_film` 之后接),所以也不存在"压太狠"的问题;
|
|
631
|
+
要让重击前后有能量差,靠的是配乐分段 energy 与 `suck()`,不是压缩。
|
|
632
|
+
- 成片音轨必须 48 kHz(`mux` 已显式 `-ar 48000`,selfcheck 第 12 条守着)。
|
|
633
|
+
|
|
634
|
+
### 5.7 声音的片间差异与片内分离(v0.7 新增)
|
|
635
|
+
|
|
636
|
+
> v0.5 解决的是"配乐风格都差不多",但**同一风格下每部片的配乐仍像同一首**——
|
|
637
|
+
> 内容指纹只进了 seed(改音色细节),和弦进行/动机/鼓型全固定。
|
|
638
|
+
> 实测:同风格、同 bpm、同调、只换内容指纹,两段配乐相关性 **0.966**。
|
|
639
|
+
|
|
640
|
+
v0.7 把指纹推进到**结构层**(详见 **references/audio-variation.md**):
|
|
641
|
+
|
|
642
|
+
| 层 | v0.6 | v0.7 |
|
|
643
|
+
|---|---|---|
|
|
644
|
+
| 配乐和声 | 固定进行 | 指纹决定**每两小节一次的稳定和弦替换**(不影响乐句末与片尾规则) |
|
|
645
|
+
| 配乐进行 | 固定一套进行 | 指纹在 calm/body/drive 三档里选一档(同素材,只换功能顺序) |
|
|
646
|
+
| 配乐动机 | 固定动机 | 指纹对级数做 **±1 稳定偏移**(节奏不变,问句/答句独立派生;跨主音带八度修正) |
|
|
647
|
+
| 配乐鼓型 | 固定落点 | 指纹加 ghost kick(不与现有落点重复),clap 位置 ±0.5 拍 |
|
|
648
|
+
| 出片接线 | 指纹只进 seed | `render.py` 的 `st_score` 把 `seed_effective`(含指纹)传给 `musicgen.render()` |
|
|
649
|
+
| 音效 seed | `seed_base + i`(只跟 cue 顺序) | `content_seed(seed_base, style, content_fp, type, t)`(跟内容走) |
|
|
650
|
+
| 提示音音高 | 硬编码 D 小调五声 | `in_key(freq, key)` 跟随配乐调性 |
|
|
651
|
+
| 段落边界 | 配乐过门 + sfx 转场各响各的 | 边界前 2.2 s 内的 whoosh/riser **自动 -3 dB**(不与过门撞车) |
|
|
652
|
+
| duck | 3 dB 一刀切 | 按设计分级(默认 4.0 dB),配乐让得更彻底 |
|
|
653
|
+
| 旁白优先(v0.7.1) | 配乐 -1.5 dB、无语音侧链 | 配乐 -12 dB 基线 + 人声处再压 6 dB(音效 3 dB),人声段 SNR 实测 15 dB |
|
|
654
|
+
|
|
655
|
+
**向后兼容**:`sfx.content_seed: false` 时音效走旧的 `seed_base + i` 路径,老片逐样本不变
|
|
656
|
+
(selfcheck 第 33 条守着这条)。注意:v1.0.1 起**没有 legacy 外观**——外观永远来自 `design.json`。
|
|
657
|
+
|
|
658
|
+
验收工具(改音频相关代码后必须跑):
|
|
659
|
+
|
|
660
|
+
```bash
|
|
661
|
+
python <REPO>/tools/audio_similarity.py --strict # A/B/C 三组阈值,失败 exit 1(在包根执行)
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
## 6. 停留窗口:让观众看清楚,而不是整片放慢(v0.3.0)
|
|
665
|
+
|
|
666
|
+
> 来自同一部成片的复盘:"部分画面没看清就被切走了"。**解决办法不是把整片放慢,而是按内容加停留。**
|
|
667
|
+
|
|
668
|
+
```jsonc
|
|
669
|
+
"hold": 0.4, // 每句语音结束后统一多停 0.4 s
|
|
670
|
+
"lines": [{"id": "l03", "hold": 1.2, ...}] // 这一句单独停 1.2 s
|
|
671
|
+
```
|
|
672
|
+
|
|
673
|
+
- `hold` 只加在**语音结束后**,不影响旁白节奏;句子读得还是那么快,只是停在画面上的时间变长。
|
|
674
|
+
- 加长量取 `0.5 s`(120 BPM 下的**四分音符**=一拍)的整数倍,画面重音仍落在拍点上。
|
|
675
|
+
- 配乐 `sections` 用 `at: "l03"` 而不是绝对秒——停留窗口一改,段落边界自动跟着走。
|
|
676
|
+
- `bar` 字段可以让每段时长自动对齐小节(`"bar": 2.0` = 120 BPM 一小节),**代价是段落时长不再精确等于
|
|
677
|
+
语音+停留**,只在对齐更重要时才用。
|
|
678
|
+
|
|
679
|
+
**判断哪里需要停留**(按顺序看这三条):
|
|
680
|
+
1. 画面上出现了**要读的字**(数字滚完、卡片落定、字幕出齐)→ 至少留 `0.5 s + 字数/9`。
|
|
681
|
+
2. 出现了**要理解的结构**(网络层次、坐标轴、流程图)→ 留到能扫一眼看完。
|
|
682
|
+
3. 两个镜头之间**没有内容连续性**(黑一下、空一拍)→ 让下一幕提前开始,而不是加长黑场。
|
|
683
|
+
|
|
684
|
+
## 6.1 词级时间轴与词锚点(v0.8.0)
|
|
685
|
+
|
|
686
|
+
> **一句话**:句时长仍然是权威;词级时间戳是**附加值**,只用来「字幕按词折行 / cue 打到某个词上 /
|
|
687
|
+
> 门禁判它有没有变」——它不参与排轴,也**绝不**用词时长之和去推算句子时长。
|
|
688
|
+
|
|
689
|
+
**开与关**:`film.json` 顶层 `"words": true|false`,默认 **true**。
|
|
690
|
+
`--stage tts` 会额外收 edge-tts 的 `WordBoundary` 事件,落盘 `work/words.json`:
|
|
691
|
+
|
|
692
|
+
```jsonc
|
|
693
|
+
{ "schema": 1, "enabled": true,
|
|
694
|
+
"lines": { "l03": { "dur": 7.18, "n": 13, "covered": 5.2, "tail": 0.09,
|
|
695
|
+
"gaps": [0.18, 0.21, ...], "mono": true,
|
|
696
|
+
"words": [{"text":"画质","start":0.04,"end":0.32}, ...] } },
|
|
697
|
+
"stats": { "lines": 4, "words": 64, "empty": [] } }
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
**它怎么来的(实现口径,改代码前先看)**:
|
|
701
|
+
- 必须显式 `edge_tts.Communicate(..., boundary="WordBoundary").stream()` 同时收音频与词事件。
|
|
702
|
+
用 `save()` 会把词事件丢掉;不传 `boundary` 时默认是 `SentenceBoundary`,**一条词事件都收不到**
|
|
703
|
+
(实测 edge-tts 7.3.1 / zh-CN:默认设置下词事件数 = 0)。
|
|
704
|
+
- 音频先写 `<句>.mp3.part` 再 `os.replace`。旧写法直接 `open(..., "wb")`,一次断网就把**已缓存的好音频
|
|
705
|
+
截成 0 字节**;现在失败只丢这次的新音频,旧缓存还在。
|
|
706
|
+
- 词事件 offset 是相对**原始 mp3** 的 100 ns 单位;`trim_silence=True` 时时间轴用的是裁剪后的语音,
|
|
707
|
+
所以 `words_normalize(offset_shift=裁掉的句首秒数)` 会把词表整体平移。**不平移,词会整体提前 0.1–1 s**
|
|
708
|
+
(实测模板句 shift = 0.10 s 量级)。
|
|
709
|
+
- 词表有独立缓存键 `tts_words_key`,没并进 `tts_key_v2`:老音频照旧复用,只在"这次显式要词级"时补采一次。
|
|
710
|
+
- 词事件收不到**不是失败**:meta 里记 `words_ok=false`、`words_events=0`,打印 WARN 并复用已有音频;
|
|
711
|
+
G10 会记 `skipped`("词事件为空"),不许冒充 pass。
|
|
712
|
+
|
|
713
|
+
**下游怎么用**:
|
|
714
|
+
|
|
715
|
+
| 能力 | 用法 | 退化行为 |
|
|
716
|
+
|---|---|---|
|
|
717
|
+
| 字幕按词折行 | `fit_sub_lines(..., words=)` / `draw_subtitle(..., words=)` | 词表对不上原文或折出来超过 2 行 -> **回退字符折行**(老项目排版不变) |
|
|
718
|
+
| cue 词锚点 | `"at": "l03:w2"` / `"at": "l03:w2+0.1"`(**从 1 数**,w1 = 第一个词) | 该句没有那么多词 -> 退化为 `l03+偏移` 并打 WARN;场景 id 也不存在才报错 |
|
|
719
|
+
| 门禁判据 | G10 读 `work/words.json`(唯一事实来源,不再各自翻 `*.mp3.json`) | 没开/没数据 -> skipped |
|
|
720
|
+
|
|
721
|
+
**三个坑(都在真实跑里踩过)**:
|
|
722
|
+
|
|
723
|
+
1. **词间有空隙**:实测 edge-tts 相邻词之间有 0.1–0.3 s 不在任何事件里。所以"词时长求和"永远小于句长,
|
|
724
|
+
G10 的覆盖率阈值才定在 45%(低于它才是"明显少了"),别拿 `sum(dur) == dur` 当判据。
|
|
725
|
+
2. **句尾静音不在词事件里**:约 0.5 s,靠**整句实测时长**兜底。G10 只检查"末词终点离句尾不超过 0.40 s"
|
|
726
|
+
(示例片实测 0.07–0.12 s),超过它通常意味着词事件本身错位了。
|
|
727
|
+
3. **词锚点会静默退化**:`l03:w99` 不会炸,只会 WARN 后退回句偏移。**改台词后先看 stdout 有没有这条 WARN**——
|
|
728
|
+
否则你会以为自己精确打到了某个词。同样的,`words=false` 或词事件为空时,字幕不会报错,只是退回整句断行。
|
|
729
|
+
|
|
730
|
+
**老项目第一次升级**:默认 `words=true` 会让每句**重新联网合成一次**(老缓存里没有 `words_key`)。
|
|
731
|
+
一次全片 TTS 的网络成本,换词级能力;不想付就把 `"words": false` 写进 `film.json`,行为回到 v0.7。
|
|
732
|
+
|
|
733
|
+
细则(词级契约、时长权威性、裁剪平移公式、词锚点退化路径)见 [references/word-timeline.md](references/word-timeline.md)。
|
|
734
|
+
|
|
735
|
+
## 7. 质检门禁(error 必过 / warning 别当看不见)
|
|
736
|
+
|
|
737
|
+
```bash
|
|
738
|
+
python render.py ... --stage qa # 只读质检:分辨率/音轨/时长/响度/真峰/锐度比/黑帧/字幕条数 + 帧级深检
|
|
739
|
+
python selfcheck.py # 技能自身回归(67 条曾真的坏过的点)
|
|
740
|
+
python qa_frames.py out/film.mp4 --cues work/cues.json # 单独跑帧级深检,出 JSON + 图
|
|
741
|
+
# v0.6:qa / all 收尾会自动写 out/QC-REPORT.md(机器可读交付结论)
|
|
742
|
+
# 并落 work/gates.json(三档 gate matrix)与 work/cost.jsonl(实测耗时台账)
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
### 7.0 交付凭据:`out/QC-REPORT.md` 与人工签核(v0.6.0)
|
|
746
|
+
|
|
747
|
+
> 这一节解决的是"**质检结论只活在控制台里**"——会话一断,"我查过了"就无从复现。
|
|
748
|
+
|
|
749
|
+
**三档状态**(这是最关键的语义,不许混):
|
|
750
|
+
|
|
751
|
+
| 状态 | 含义 | 退出码 |
|
|
752
|
+
|---|---|---|
|
|
753
|
+
| `pass` | 本次**真的查了**且通过 | 0 |
|
|
754
|
+
| `fail` | 查了没过 | 非 0(除非 `--no-gate` 绕过,且会留痕) |
|
|
755
|
+
| `skipped` | 本次**没有执行**(没装 cv2 / 没写 visual_at / 没签核) | 默认 0,`--strict-gates` 下非 0 |
|
|
756
|
+
|
|
757
|
+
**"skipped ≠ 通过"** 是这套机制的全部意义:以前"没查"和"查了没事"在输出上都叫"没报错"。
|
|
758
|
+
|
|
759
|
+
```bash
|
|
760
|
+
cat out/QC-REPORT.md # gate matrix + 实测值 + 签核 + 耗时,交付时一起给
|
|
761
|
+
python render.py ... --stage qa --strict-gates # 交付前:skipped 也算失败
|
|
762
|
+
python render.py ... --stage accept --gate stills --note "看过联系表" # 人工签核
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
**报告头部有「运行编号」与「输入指纹」两行**(v0.8):前者是 `--run-id`(不给就按时间戳生成),
|
|
766
|
+
后者是 script / scenes / words / filmkit 四个内容的指纹。同一部片跑了好几版之后,
|
|
767
|
+
"上一版的门禁结论到底是什么"就靠 `work/runs/<runId>/` 里的快照回答(只归档小文件,
|
|
768
|
+
视频/音频/分块就地保留)。
|
|
769
|
+
|
|
770
|
+
**四道人工签核闸**(`style` 风格与声线 / `stills` 母版静帧 / `facts` 上屏数字 / `release` 成片验收):
|
|
771
|
+
|
|
772
|
+
- 签核前会检查"证据产物存在"(如 `stills` 要求 `work/qa/sheet.png`),**没有证据不许签**——
|
|
773
|
+
否则签核就是橡皮图章。
|
|
774
|
+
- 签核绑**内容指纹**(剧本 + `scenes.py` + 引擎)。改了其中任何一个,旧签核**自动失效**,
|
|
775
|
+
报告里会标成 `skipped` 并写明"内容已变"。这不是麻烦,是防止"我签过了"变成空头支票。
|
|
776
|
+
- 逃生阀是 `--no-gate`(整体跳过签核检查),但会写进 QC 报告的"本次绕过的门禁"一节,
|
|
777
|
+
**留痕不等于通过**。
|
|
778
|
+
|
|
779
|
+
**耗时台账** `work/cost.jsonl`:每个 stage 一行实测墙钟(`--cost` 开启,`--stage all` 里自动记)。
|
|
780
|
+
它是 SKILL §12 那张"排期公式"的对照——公式从来没被验证过,现在有数据了。
|
|
781
|
+
|
|
782
|
+
### 7.0b 阶段新鲜度:别让旧产物混进新片(v0.6.0)
|
|
783
|
+
|
|
784
|
+
> 与 §1 的分块指纹同源:那里只修了 `frames` 一个点,其余 stage 此前是裸奔的
|
|
785
|
+
> (改了 `style` 后 `--stage audio` 仍读旧 `music.wav`;改了 `hold` 后 `--stage frames` 仍用旧 `cues.json`,**都不报错**)。
|
|
786
|
+
|
|
787
|
+
```bash
|
|
788
|
+
python render.py ... --stage explain # 哪些阶段过时了、为什么、最小重跑命令
|
|
789
|
+
python render.py ... --stage qa --strict-stale # 有过时就失败(CI / 交付前)
|
|
790
|
+
# 机读:问 stagewatch(render.py 的 explain 只打人读文本,不接受 --json)
|
|
791
|
+
python scripts/stagewatch.py --work work --json
|
|
792
|
+
```
|
|
793
|
+
|
|
794
|
+
机制:每个 stage 结束往 `work/manifest.json` 记一行 `{stage, at, inputs:{名:指纹}, outputs:{名:指纹}}`;
|
|
795
|
+
下次该 stage 开始前重算上游输入与自己的产物指纹。**默认只告警**(`--strict-stale` 升级为失败,
|
|
796
|
+
`--allow-stale` 显式放行并留痕)。改一句要重跑什么,见 [references/change-impact.md](references/change-impact.md)。
|
|
797
|
+
|
|
798
|
+
**v0.8 的四道新门禁**(账本 id:`G0` / `G10` / `G11` / `G12`,报告里按记的顺序排在 `G4` 前后):
|
|
799
|
+
|
|
800
|
+
| 门禁 | 查什么 | 三档语义(`pass` / `fail` / `skipped`) |
|
|
801
|
+
|---|---|---|
|
|
802
|
+
| `G0` 渲染前资源预检 | 剧本结构、全部上屏文本逐字过字形检测(按字段选字重)、`work/tts/*.mp3` 能否被 ffprobe 解析、字体候选能否加载、web 入口页是否存在 | 有 error -> `fail`;只有 warning -> `pass`(warning 仍进账本);没拷 `preflight.py` -> `skipped` |
|
|
803
|
+
| `G10` 词级时间戳 | `work/words.json` 的覆盖率/单调性/末词与首词偏差 | 判据不过 -> `fail`;`words=false`、没有 `words.json`、词事件为空 -> `skipped`(写明原因) |
|
|
804
|
+
| `G11` 全片 VMAF/SSIM | 参考帧由**场景函数实时重渲**,经 rawvideo 喂 ffmpeg 与成片逐帧比(不是从成片抽帧——那样恒等于 100) | VMAF < **92** -> `fail`;[92, 96) -> `pass` + warning;缺 `qavmaf.py` / web 引擎 / `--vmaf off` / 没 libvmaf -> `skipped`。阈值只有一份,定义在 `qavmaf.VMAF_PASS / VMAF_WARN` |
|
|
805
|
+
| `G12` ASR 回读 | 成片音轨转文字,按 timeline 每段窗口算**字符级多重集召回**(中文没有词边界,字符级才成立;多重集是因为"是"这种字要计次数) | 召回 < 0.72 -> `fail`;[0.72, 0.85) -> `pass` + warning;两个引擎都不可用 / 没装 `qaasr.py` / 显式关闭 -> `skipped` |
|
|
806
|
+
|
|
807
|
+
**为什么 G11 值得全片跑**:锐度比只在 4 个时刻采样,能发现"整体发虚",发现不了"某 30 秒里字幕糊了"。
|
|
808
|
+
VMAF 是唯一覆盖整条时间轴的客观感知指标。代价是**每片多渲一遍参考帧**(29 s 片实测 14.2 s 全跑完;
|
|
809
|
+
> 机器被其它任务占满时同一份代码测到过 28.8 s,同机不同负载差近 2×,只认"这一跑"的数字)。
|
|
810
|
+
**为什么 G12 值得跑**:容器/响度/帧级质检全都默认"音轨里念的就是剧本那几句"——缓存串了、漏句、被压没了,
|
|
811
|
+
从波形和容器上都看不出来。代价约 6 s/片(small 模型,CPU)。
|
|
812
|
+
|
|
813
|
+
G0 / G11 / G12 三个新模块(外加 `G10` 词级)的实测数据、阈值来源与关闭方式见 [references/v08-quality.md](references/v08-quality.md)。
|
|
814
|
+
|
|
815
|
+
**质检分三层**,一层比一层更接近"观众真的看到了什么":
|
|
816
|
+
|
|
817
|
+
| 层 | 查什么 | 工具 | 抓得住什么 |
|
|
818
|
+
|---|---|---|---|
|
|
819
|
+
| 容器层 | 分辨率/帧率/音轨/时长/码率 | `ffprobe`(`filmkit.probe`) | 参数写错、音轨丢了 |
|
|
820
|
+
| 信号层 | 响度/真峰/锐度比/黑帧/静音/字幕条数 | `filmkit.qa_report` | 像草稿、文字发虚、静默丢字幕 |
|
|
821
|
+
| **时间层(v0.3)** | **单帧闪烁 / 画面空窗 / 音画同步 / 响度曲线** | `qa_frames.py` | **"一帧白闪""两秒没东西看""重击没声音"** |
|
|
822
|
+
| 页面层(web 引擎) | 文字重叠/出画/一闪而过/从没露面 | `webengine.py textqa` | 页面里被别的元素压住的字 |
|
|
823
|
+
|
|
824
|
+
时间层需要单独说,因为它是**唯一靠"看完全片"才能发现**的一类问题:单看任何一帧都正常。
|
|
825
|
+
|
|
826
|
+
**先分清两档**(这张表以前混在一起,标题写"必过",实际 8 行里 5 行只是 warning):
|
|
827
|
+
|
|
828
|
+
- **error 档**:出现即 `qa` 返回非 0、CI 红。这些是"片子坏了"。
|
|
829
|
+
- **warning 档**:只提示,不阻断交付。这些是"能更好"或"门禁没跑"。
|
|
830
|
+
|
|
831
|
+
#### error 档(必须过)
|
|
832
|
+
|
|
833
|
+
| 判据 | 阈值 | 失败怎么办 |
|
|
834
|
+
|---|---|---|
|
|
835
|
+
| 分辨率/帧率 | 严格等于**交付规格**(v0.8:`timeline.video` 的 out_w/out_h/fps;缺省回落 1080p/30) | 检查 `FrameSink` 参数与 `film.json` 的 `video` 块是否一致;`strict=True` 会直接报错而不是静默缩放 |
|
|
836
|
+
| 音轨缺失 | 必须有音轨 | mux 参数漏了 `-i audio.wav` |
|
|
837
|
+
| 时长 | 与 `frames/FPS` 差 < 0.5 s | 断点续渲漏帧;分块模式检查缺口检测 |
|
|
838
|
+
| **真峰** | **> -1.0 dBTP**(-1.0 ~ -1.4 之间只 warn) | 混流前目标设 -1.5(AAC 会把真峰抬高 ~0.9 dB) |
|
|
839
|
+
| **编码锐度比** | **< 0.85**(0.85–0.92 只 warn;没装 cv2 则门禁跳过 + warn) | 降 CRF 或提 preset |
|
|
840
|
+
| **单帧闪烁(v0.3)** | **0 条** | 位移/推力没归零、粒子贴镜头过曝、转速突变;见 §8 故障速查 |
|
|
841
|
+
| **画面空窗(v0.3)** | 首尾以外 0 段(≥0.8 s 近黑) | 转场之间没有内容连续性:让下一幕提前开始 |
|
|
842
|
+
| **音画同步(v0.3)** | 判到超差(`dt > 60 ms`)即 error;**但默认可能根本没在查**(见下方两小节) | 要给 cue 写 `visual_at`,或设 `QA_REQUIRE_SYNC=1` 把"没查"也变成 error |
|
|
843
|
+
| **字幕条数** | **= 剧本句数** | 少一条就是静默丢字幕,查 `sub_window` 与 `write_srt` |
|
|
844
|
+
| **时长(时间层复核)** | 与预期差 > 0.5 s | `qa_frames` 会独立再查一次时长 |
|
|
845
|
+
| **渲染前资源预检(v0.8,G0)** | 有任何 error(缺字形/坏 TTS 音频/字体全不可加载/web 入口页不存在) | 按报错里的「字符 + 字段 + 句 id」改:换说法、改用矢量画、`--force-tts` 重合成。**这一步在渲第一帧之前**,改起来最便宜;`--no-preflight` 可跳过(会在 QC 报告留痕,不算通过) |
|
|
846
|
+
| **词级时间戳(v0.8,G10)** | 词覆盖率 < 45%、词偏移非单调、末词终点离句尾 > 0.40 s、首词起点离句首 > 0.20 s、或有句子一条词事件都收不到 | 多为 edge-tts 服务端行为变了或该句被裁坏了:先看 `work/words.json` 的 `stats.empty`,再 `--force-tts` 重采;长期不可靠就把 `"words": false`(退回整句字幕,G10 记 skipped) |
|
|
847
|
+
| **全片 VMAF/SSIM(v0.8,G11)** | VMAF 均值 **< 92** fail;**[92, 96)** 只记 warning(能出片,提示复核/别降档);SSIM(0.95/0.98)是诊断项不单独阻断 | 先看是不是 CRF/preset 调高了或走了二次编码;阈值只有一份(`qavmaf.py` 的 `VMAF_PASS/WARN`),按片覆盖写 `"qa": {"vmaf": {"pass": 92, "warn": 96}}`;`--vmaf off` 可关(记 skipped)。web 引擎恒 skipped(没有可重渲的参考帧) |
|
|
848
|
+
| **ASR 回读(v0.8,G12)** | 字符召回 **< 0.72** fail;**[0.72, 0.85)** pass + warning;**≥ 0.85** 干净 | 抓的是"音轨念的不是剧本":TTS 缓存串了、换声线漏句、混音压没了。低于 0.72 先 `--force-tts` + 检查 `work/tts.json`;`"qa": {"asr": {"enabled": false}}` 可关(记 skipped) |
|
|
849
|
+
|
|
850
|
+
#### warning 档(不阻断,但别当看不见)
|
|
851
|
+
|
|
852
|
+
| 判据 | 触发条件 | 怎么办 |
|
|
853
|
+
|---|---|---|
|
|
854
|
+
| 音轨采样率 | ≠ 48 kHz(**音轨缺失才是 error**) | mux 的 `-ar 48000` 被漏了 |
|
|
855
|
+
| 响度略偏 | -17.2 ~ -14.5 之外但仍在 -19 ~ -12 内 | 目标 -16 ± 1 是"舒适区";重跑 loudnorm |
|
|
856
|
+
| 真峰余量不足 | -1.4 ~ -1.0 dBTP | AAC 编码后可能更高,留 1.4 dB 余量 |
|
|
857
|
+
| 锐度比偏低 | 0.85 ~ 0.92 | 可接受但可更好 |
|
|
858
|
+
| 长黑帧 | 首尾以外出现 ≥0.4 s 黑帧 | 检查素材/淡入淡出 |
|
|
859
|
+
| 长静音 | 出现 ≥1.2 s 静音 | 确认是否有意留白 |
|
|
860
|
+
| 字幕安全区 | `qa_text_safe` 有警告(>2 行、越界、触底) | 缩短字幕或调 `SUB_BAND` |
|
|
861
|
+
| 缺字形 | `check_glyphs` 有警告 | 改文字或用矢量线条画(msyhbd 缺 ✓ × ↵ 等) |
|
|
862
|
+
| **画面长时间静止(v0.4)** | 连续 6 s 内最活跃 8×8 块的 0.5 s 漂移 ≤ 1.5 级灰度 | 加视觉变化(§4「每 3–6 秒要有变化」);确实要静止就接受它 |
|
|
863
|
+
| 门禁没执行 | 没装 cv2 / 测不到响度 / 找不到 sub.srt / 没 events 也没锚点 | **这一条最重要:它说明"看起来通过的项,其实没在查"** |
|
|
864
|
+
|
|
865
|
+
**三个 v0.2 才修对的坑(v0.1 的"门禁"其实是摆设):**
|
|
866
|
+
|
|
867
|
+
1. **锐度比的参考帧不能是成片自己抽的帧。** 那样 `sharp_ref` 与 `sharp_dec` 几乎相同,
|
|
868
|
+
ratio 恒 ≈ 1.0。必须用**场景函数重新渲染**同一时刻的原图(`st_qa` 已这么做)。
|
|
869
|
+
2. **真峰必须显式开 `ebur128=peak=true`。** 默认参数下解析不到 `Peak:`,
|
|
870
|
+
"真峰 ≤ -1 dBTP" 这条永远不触发。
|
|
871
|
+
3. **码率不是质量指标。** 暗底大字内容 0.5–1 Mbps 是正常的;把它当"下限"会天天误报。
|
|
872
|
+
|
|
873
|
+
**"不能有"和"必须有"两类都要查**:闪烁/重叠/出画/削波是"不能有";图名框/淡入/字幕/每段至少一个动态元素是"必须有"——后者最容易静默失效。
|
|
874
|
+
|
|
875
|
+
**时间层门禁怎么读**(`work/qa/deep.json`):
|
|
876
|
+
|
|
877
|
+
| 字段 | 意义 | 判据 |
|
|
878
|
+
|---|---|---|
|
|
879
|
+
| `flicker[]` | 只亮一帧的格子:`|F[t]-(F[t-1]+F[t+1])/2|` 分块超阈值 **且** `|F[t+1]-F[t-1]|` 很小("回到原位"——加速运动不满足这条) | 0 条;有的话 `bbox` 告诉你在画面哪个位置 |
|
|
880
|
+
| `black_windows[]` | ≥0.8 s 近黑(片头片尾不算) | 0 段;有就补内容连续性 |
|
|
881
|
+
| `sync[]` | cue 与视觉事件的时间差 | 全部 `|dt| ≤ 0.06 s` |
|
|
882
|
+
| `luma[]` / `loudness[]` | 逐帧亮度 / 短时响度曲线 | 看形状:有没有"该有起伏的地方一条直线" |
|
|
883
|
+
|
|
884
|
+
**别把"重击"当闪烁**:设计好的闪白/震屏会出现在 `events` 里(web 引擎由页面导出,pil 引擎写进 `cues.json`),
|
|
885
|
+
`qa_frames` 会先排除它们再报 `flicker`。没登记却又闪的,才是 bug。
|
|
886
|
+
|
|
887
|
+
**也别把"运动最快的一帧"当闪烁**:二阶差分对加速运动天生过敏(逐字入场、缓动推近、
|
|
888
|
+
交界黑场的起坡帧)。所以判据还有第三道闸——**单帧事件必须"回到原位"**:
|
|
889
|
+
`|F[t+1] - F[t-1]|` 要远小于中间帧的偏离量(`FLICKER_RETURN_RATIO = 1.6`)。
|
|
890
|
+
高分(≥10)但被判成运动的候选**会进 warning**("高分异常被判为运动,建议人工复核")——
|
|
891
|
+
**门禁可以保守,但不能静默**。
|
|
892
|
+
|
|
893
|
+
实测反例(本技能自带的 web 示例页):报了 3 处闪烁,逐一逐帧查下来**全部是**
|
|
894
|
+
"最后一个字入场"和"交界黑场起坡"这两类加速运动;加上这道闸后归零,
|
|
895
|
+
而 selftest 里注入的单帧白闪仍然能被检出——**没被削弱的门禁才叫门禁**。
|
|
896
|
+
调阈值之前先确认这条闸在不在:没有它,你会把正常运动当 bug,改到死。
|
|
897
|
+
|
|
898
|
+
### 音画同步的命名约定(门禁会不会静默失效就看这条)
|
|
899
|
+
|
|
900
|
+
`sync_check` **先按 `events.kind == cue 的配方名` 配对**。所以画面里"该响一下"的事件,
|
|
901
|
+
kind 要写成**同一个配方名**:
|
|
902
|
+
|
|
903
|
+
```js
|
|
904
|
+
S.cue(0.62, 'impact', {size: 1.15}); // 音频:配方名 impact
|
|
905
|
+
S.event(0.62, 'impact', {}); // 画面:同名的 visual event
|
|
906
|
+
S.event(0.62, 'ring', {}); // 纯装饰事件:用通用名,不参与配对
|
|
907
|
+
```
|
|
908
|
+
|
|
909
|
+
**不这么写会怎样**:页面只导出 `flash / shake / ring` 而 cue 是 `impact / whoosh` 时,
|
|
910
|
+
同 kind 永远配不上、跨 kind 又只做确认,`sync` 会恒为空 —— **看起来"零超差",其实一条都没在查**。
|
|
911
|
+
|
|
912
|
+
两道保护:
|
|
913
|
+
1. `qa_frames` 发现"cues 与 events 没有任何同名项"时**主动给 warning**("音画同步检查可能没生效");
|
|
914
|
+
2. 自查:故意把某个 cue 的时刻改错 0.3 s 重跑,**必须**能报出超差。不报就是没配上。
|
|
915
|
+
|
|
916
|
+
### PIL 路线的同步门禁怎么真的生效(v0.4 新增)
|
|
917
|
+
|
|
918
|
+
`--engine pil` 没有页面,也就没有 `events`,"同 kind 配对"永远配不上——**这条门禁默认是没在查的**。
|
|
919
|
+
v0.4 给了两条真正能用的路:
|
|
920
|
+
|
|
921
|
+
1. **给 cue 写画面锚点**(推荐):在 `film.json` 的 `sfx.cues` 或 `scenes.py` 的 `cues()` 里,
|
|
922
|
+
给每条 cue 加一个可选字段 `"visual_at"`(或 `"visual_t"`),值是**画面上那个事件真正发生的绝对秒数**:
|
|
923
|
+
|
|
924
|
+
```json
|
|
925
|
+
{ "at": "l03+0.05", "type": "impact", "visual_at": 15.77 }
|
|
926
|
+
```
|
|
927
|
+
|
|
928
|
+
`qa_frames` 会直接算 `|cue 时刻 - visual_at|` 并报超差,不依赖 events / 命名约定。
|
|
929
|
+
锚点必须写画面上的真实时刻——随便填一个等于自己骗自己。
|
|
930
|
+
|
|
931
|
+
2. **把"没查"变成失败**:设环境变量 `QA_REQUIRE_SYNC=1`(或 `qa_frames.py --require-sync`),
|
|
932
|
+
"音画同步门禁没执行"就从 warning 升级成 error。CI / 交付前建议开着。
|
|
933
|
+
|
|
934
|
+
两条都没做时,`deep.json` 里 `sync` 恒为 `[]`,同时会有一条 warning 明说"同步对照跳过"。
|
|
935
|
+
**看到 `sync: []` 先确认是"真的同步"还是"根本没查"**——`warnings` 里有没有那句话就是判据。
|
|
936
|
+
|
|
937
|
+
## 7.5 v0.8 交付生产线:画幅 / 批量 / 预览 / 运行版本化
|
|
938
|
+
|
|
939
|
+
### 7.5.1 画幅与帧率(只做 16:9 横屏)
|
|
940
|
+
|
|
941
|
+
```jsonc
|
|
942
|
+
"video": { "width": 1280, "height": 720, "fps": 24 } // 不写 = 1920x1080@30(v0.7 行为)
|
|
943
|
+
```
|
|
944
|
+
|
|
945
|
+
| 规则 | 实际行为 |
|
|
946
|
+
|---|---|
|
|
947
|
+
| 内部构图 | **恒为 1920x1080**;输出更小时在编码阶段用 ffmpeg `scale`(lanczos)下采样(`FrameSink(out_w=, out_h=)`) |
|
|
948
|
+
| 允许的输出尺寸 | 1920x1080 / 1280x720 / 960x540;2560x1440 在"允许列表"里但**会被上采样规则拒掉**(见下) |
|
|
949
|
+
| 允许的 fps | 24 / 25 / 30 / 50 / 60 |
|
|
950
|
+
| 竖屏 | **直接报错**。安全区、版式几何、HUD、字幕折行全部按 16:9 的像素坐标写死,竖屏是另一套版式 |
|
|
951
|
+
| 非 16:9 | **直接报错**(换宽高比要重做版式几何与字幕安全区,不是把宽高换一下) |
|
|
952
|
+
| 上采样 | **直接报错**。输出大于 1920x1080 = 把 1080p 包装成"更高分辨率"(假 4K);要真高清得改内部构图 |
|
|
953
|
+
| 非法 fps | **直接报错**(报错里列出可用值) |
|
|
954
|
+
|
|
955
|
+
链路:`filmkit.video_plan(script)` 在 `layout` 阶段就解析并校验(错了不拖到渲帧),
|
|
956
|
+
结果写进 `timeline.json` 的 `video` 块;`G4` 容器门禁改成**按交付规格**比对(不是恒看 1920x1080);
|
|
957
|
+
`encode_quality` 的参考帧会先缩到交付分辨率再比锐度。
|
|
958
|
+
|
|
959
|
+
> **跨分辨率别比锐度比**:同一片 1080p 交付实测 1.009、720p 交付实测 1.206——数值不可横向比较
|
|
960
|
+
> (参考帧也被缩到交付分辨率,笔画占画面的比例变了)。它只用来判"这一版有没有比上一版糊",别拿它当"720p 更清晰"。
|
|
961
|
+
>
|
|
962
|
+
> 720p 交付已实测通过全部门禁:`G4 = 1280x720@24`、`G11 = VMAF 97.10 / SSIM 0.9925`(696 帧,7.2 s)。
|
|
963
|
+
> `filmkit.qa_report` 为此新增 `expect_w/expect_h/expect_fps`——容器门禁比的是**交付规格**,
|
|
964
|
+
> 不是死比 1920x1080;改规格时记得让 `timeline.video` 与之一致(qavmaf 会核对两者是否同源)。
|
|
965
|
+
|
|
966
|
+
### 7.5.2 批量生产(`--batch`)
|
|
967
|
+
|
|
968
|
+
```jsonc
|
|
969
|
+
// films.json —— 两种写法都接受
|
|
970
|
+
["a/film.json", "b/film.json"]
|
|
971
|
+
{"films": [{"script": "a/film.json", "scenes": "a/scenes.py",
|
|
972
|
+
"work": "a/work", "out": "out/a.mp4", "label": "A"}]}
|
|
973
|
+
```
|
|
974
|
+
|
|
975
|
+
```bash
|
|
976
|
+
python render.py --batch films.json
|
|
977
|
+
```
|
|
978
|
+
|
|
979
|
+
| 行为 | 细节 |
|
|
980
|
+
|---|---|
|
|
981
|
+
| 上限 | **100 条 / 1 MiB**(清单本身不该是个大 JSON);超了直接报错让你拆份 |
|
|
982
|
+
| 开跑前 | **全量 `filmcheck` 预检**:任一条不通过就**一条都不跑**,把明细一次性报全(`batch-report.json` 里 `phase="preflight"`) |
|
|
983
|
+
| 执行 | 逐条起**子进程**渲染(每条都是完整的 `--stage all`),**单条失败不阻断其余** |
|
|
984
|
+
| 收尾 | `batch-report.json`(`phase="render"`,含每条 `exit` / `wall_s` / 日志尾)+ 退出码:全成功 `0`,**部分失败 `1`** |
|
|
985
|
+
| 预检失败的退出码 | 也是 `1`(`SystemExit` 带文字,Python 的默认行为);区别看 `batch-report.json` 的 `phase` |
|
|
986
|
+
|
|
987
|
+
实测(本机,2 条 29 s 示例片,v0.8 早期一轮):**67.2 s / 60.1 s**,各 `pass 11 · fail 0 · skipped 1`。
|
|
988
|
+
注意每条都要付 G11 的参考帧成本——批量不是"摊销",是线性叠加。**而且瓶颈已经转到 qa**:
|
|
989
|
+
单条全链实测 47.3 s 里 `qa` 占 23.9 s(见 §1 的时间表),批量时这个比例会原样复制。
|
|
990
|
+
|
|
991
|
+
### 7.5.3 预览片(`--preview N`)
|
|
992
|
+
|
|
993
|
+
```bash
|
|
994
|
+
python render.py ... --preview 15 # 只渲前 15 秒
|
|
995
|
+
```
|
|
996
|
+
|
|
997
|
+
- 产物:`out/preview/<片名>_preview.mp4` + 同名 `.gif`(480 px / 10 fps,两遍调色板生成)。
|
|
998
|
+
- 音轨:有 `work/audio.wav` 就**先截到预览时长再混入**。直接拿全长音轨去配短片,`mux` 会判
|
|
999
|
+
"音视频时长差 ≥ 0.05 s" 而失败(v0.8 开发期实测踩过);没有音轨就出无声版,不阻断。
|
|
1000
|
+
- **不落交付账本**:它是给人快速看效果的,不是交付证据;正式结论仍以 `--stage qa` 的报告为准。
|
|
1001
|
+
|
|
1002
|
+
### 7.5.4 运行版本化(`--run-id` + `work/runs/`)
|
|
1003
|
+
|
|
1004
|
+
```bash
|
|
1005
|
+
python render.py ... --run-id hero-v3 # 不给就按时间戳生成 rYYYYMMDD-HHMMSS
|
|
1006
|
+
```
|
|
1007
|
+
|
|
1008
|
+
- `work/gates.json` 增 `runId` 与 `inputs`(**script / scenes / words / filmkit** 四个内容指纹)。
|
|
1009
|
+
为什么要逐项指纹:`contentHash` 只说"变了没",说不出"变了哪个";批量出片最常见的事故就是
|
|
1010
|
+
"这一条用的还是上一版的词级时间戳"。
|
|
1011
|
+
- 每次收尾(`gates.archive_run`)把**账本快照 + QC-REPORT.md + run.json** 复制到
|
|
1012
|
+
`work/runs/<runId>/`。只归档小文件;视频/音频/分块体积大,就地保留。
|
|
1013
|
+
- `out/QC-REPORT.md` 头部多两行「运行编号」「输入指纹」,交付时一眼能对上是哪一版跑的。
|
|
1014
|
+
|
|
1015
|
+
## 8. 常见故障速查
|
|
1016
|
+
|
|
1017
|
+
| 现象 | 根因 | 修法 |
|
|
1018
|
+
|---|---|---|
|
|
1019
|
+
| 后半段全程静音 | ffmpeg 多输入混流竞争 | 改 numpy 总线 `bus[i:i+n] += src*gain` |
|
|
1020
|
+
| 混音爆音/削波 | 先 `astype(float32)` 再判 dtype,归一化分支不执行 | **先判 dtype 再转**,并打印中间峰值 |
|
|
1021
|
+
| 字幕越出画面被裁 | 字幕带贴底 | `SUB_BAND` 上移到 872,`base_y` 夹紧 |
|
|
1022
|
+
| 文字发虚 | cv2 中间文件二次压缩 | 改 rawvideo 直通 + CRF18,用锐度比验证 |
|
|
1023
|
+
| 音画不同步 | 按字数估时长 | 改实测时长驱动 |
|
|
1024
|
+
| 字幕交叠 | 按字符比例分配字幕时长 | 以 TTS 句边界为窗,窗内按标点切 |
|
|
1025
|
+
| 全片该有的淡入/图名框消失 | 字段改名没跑冒烟 | 改名后必跑 `snap`;质检加"元素存在性" |
|
|
1026
|
+
| 渲染越跑越慢 / 内存涨 | 每块开新浏览器没关 | 复用会话 + `finally` 关闭 |
|
|
1027
|
+
| 拼出的片子缺一段还不报错 | 分块按"第几块"命名 | 按**绝对起始帧**命名 + 显式缺口检测 |
|
|
1028
|
+
| **请求 524 帧却只出 488 帧** | `-shortest` 按较短一路(音频)截断 | 去掉 `-shortest`,显式 `-t <视频时长>` + 校验音视频时长差 < 0.05 s |
|
|
1029
|
+
| 产出 13 小时的怪物文件 | `-stream_loop -1` 让 `-shortest` 失效 | 显式 `-t`,别依赖 `-shortest` |
|
|
1030
|
+
| BGM 混音后掉 6 dB / 听不清 | `amix` 默认 `normalize=1` 会按输入数均分 | `amix=...:normalize=0` 再 `loudnorm=I=-16` |
|
|
1031
|
+
| 旁白整体偏移 ~0.6 s | `adelay` 铺轨有系统性偏移 | 别用 adelay,改 numpy 按采样点写位 |
|
|
1032
|
+
| 烧录字幕字号小得看不清 | SRT 交 libass 按 `PlayResY=288` 排版 | `FontSize/MarginV` 要 ×3.75,或直接用 ASS 指定 PlayRes |
|
|
1033
|
+
| ASS 字幕位置全乱 | Format 行漏了 `MarginL/MarginR/MarginV` 字段 | ASS 头 Format 行字段顺序必须与 Dialogue 完全对应 |
|
|
1034
|
+
| 渲染出上一部片的内容 | `sys.path` 顺序导致 import 到旧模块 | 用绝对路径 + 每次全新进程;子进程别继承父进程模块缓存 |
|
|
1035
|
+
| 画面里 ✓ × ↵ 显示成方框 | msyhbd 缺这些字形 | 用矢量画(ImageDraw 线条),不要用符号字符;或先跑 `K.check_glyphs` |
|
|
1036
|
+
| **改了台词但配音没变** | TTS 缓存只判断"文件存在" | v0.2 已把 text/voice/rate/pitch 哈希写进 `*.mp3.json`;旧缓存需 `--force-tts` 一次 |
|
|
1037
|
+
| **长句字幕读到一半消失** | `sub_window` 把 t1 钳在 `voice_start+5.2` | v0.2 默认覆盖整句;要钳短显式传 `max_hold` |
|
|
1038
|
+
| **--stage qa 也重渲全片** | stage 分发写成 `in ("all","frames","mux","qa")` | v0.2 每个 stage 独立;qa 只读成片 |
|
|
1039
|
+
| **concatenation 打不开分块** | concat.txt 里写相对路径,ffmpeg 按自己 cwd 解析 | 写**绝对路径**(v0.2 已修) |
|
|
1040
|
+
| 成片比 silent.mp4 慢一倍 | mux 对已编码视频又编一遍 | `-c:v copy`(v0.2 默认);只在要改分辨率时 `reencode_video=True` |
|
|
1041
|
+
| 每帧渲染慢 2–3 倍 | 每帧重新 `wrap_cn`(中文逐字测宽) | 用 `K.wrap_cn_cached` / `K.text_w_cached` |
|
|
1042
|
+
| 混音偶发爆表 | 归一化分支没执行 / 叠加峰值 >1 | `AudioBus.mix()` 会打印叠加峰值并软限幅,峰值 >0.97 报警 |
|
|
1043
|
+
| **成片音轨变成 96kHz** | mux 没指定 `-ar`,loudnorm 内部升到 192kHz 后 AAC 取了 96k | mux 显式 `-ar 48000`(v0.2 已内置,selfcheck 第 12 条守着) |
|
|
1044
|
+
|
|
1045
|
+
## 9. 参考资料(按需加载)
|
|
1046
|
+
|
|
1047
|
+
- `references/proposal.md` — **(v1.0)提案优先**:为什么先出方案、`design.json` 契约、候选采样与设计距离、`visual.subtitle` / `layout_cycle` 字段、片间去重台账、提案闸五档、常见误用
|
|
1048
|
+
- `references/scene-grammar.md` — **(v0.9)场景语法**:10 类构图怎么选、`storyboard.json` 字段、设计五组视觉签名、加第 11 类构图的步骤、常见误用
|
|
1049
|
+
- `references/pipeline.md` — 八阶段详细规格、契约 JSON 格式、目录约定、并行分工
|
|
1050
|
+
- `references/scene-authoring.md` — 场景函数写法、构图规则、Canvas/CDP 方案、超采样与后处理
|
|
1051
|
+
- `references/audio.md` — 采样级混音、程序化配乐/音效、响度链路、循环环境音、提示点系统
|
|
1052
|
+
- `references/audio-variation.md` — **(v0.7)片间差异**:内容指纹怎么进和声/进行/动机/鼓型与验收阈值
|
|
1053
|
+
- `references/sfx-design.md` — **(v0.3)音效设计**:物理拆解流程、配方表、听感诊断表、QC 规则
|
|
1054
|
+
- `references/web-engine.md` — **(v0.3)浏览器引擎**:页面契约、确定性时间轴、分块渲染、崩溃重开
|
|
1055
|
+
- `references/frame-qa.md` — **(v0.3)帧级质检**:单帧闪烁判据、画面空窗、音画同步、报告怎么读
|
|
1056
|
+
- `references/quality-gates.md` — 方法论 G0–G8 全部门禁、可复制代码、验收清单(文内另有「两套编号别混用」的映射说明:账本是 `G0`/`G4–G12`)
|
|
1057
|
+
- `references/word-timeline.md` — **(v0.8)词级时间轴**:词级契约、句时长为何仍是权威、词锚点写法与退化路径、四个真实坑
|
|
1058
|
+
- `references/v08-quality.md` — **(v0.8)三道新门禁**:VMAF/SSIM、ASR 回读、渲前预检的判据、阈值来源、实测数据与关闭方式(词级 G10 见 `word-timeline.md`)
|
|
1059
|
+
- `references/change-impact.md` — **(v0.6)改动影响表**:改了 text/sub/hold/design.json/scenes.py 各要重跑什么
|
|
1060
|
+
- `references/style-packs.md` — **(v1.0.2 重写)设计 DSL 外观定制**:35 字段主题、连续色相配色、母题/转场/字幕/版式轮换怎么写、常见误用(不再是「风格包选一套」)
|
|
1061
|
+
- `references/pitfalls.md` — 20 条最佳实践 + 16 条反模式 + 3 个非确定性 bug 复盘
|
|
1062
|
+
|
|
1063
|
+
## 10. 脚本资产
|
|
1064
|
+
|
|
1065
|
+
> **所有脚本都在技能目录里,直接按路径调用即可**(如 `python <SKILL_DIR>/scripts/propose.py`)。
|
|
1066
|
+
> 不需要拷到工程目录——v1.0.2 起的主路径是零拷贝(§1)。
|
|
1067
|
+
|
|
1068
|
+
| 文件 | 作用 |
|
|
1069
|
+
|---|---|
|
|
1070
|
+
| `scripts/propose.py` | **(v1.0)提案器**:`--brief` 出 N 个候选(各带静帧联系表 + 设计距离)/ `--revise` 重出 / `--diff` 对比 / `--confirm` 冻结 `work/design.json` / `--show` 查看;v1.0.2 增片间去重台账(`--no-ledger` / `--show-ledger` / `VIDEO_STUDIO_LEDGER`) |
|
|
1071
|
+
| `scripts/designkit.py` | **(v1.0)设计 DSL**:`sample_designs`(确定性采样、两两 ≥2 维不同、配色色相分叉)/ `validate_design`(fail-closed)/ `design_to_plan` / `design_distance` / `subtitle_of`;CLI:`--sample` / `--lint` / `--list-palettes` |
|
|
1072
|
+
| `scripts/previewkit.py` | **(v1.0)离线预览**:合成时间轴 + `grammar.plan_scenes` + 场景绘制 + 联系表,不联网、不合成旁白 |
|
|
1073
|
+
| `scripts/grammar.py` | **(v0.9)场景语法**:按内容从 10 类构图里选 `type`,落 `work/storyboard.json`;`--help` 可查用法 |
|
|
1074
|
+
| `scripts/voicekit.py` | **(v1.0)旁白声线/语气计划**:裸 edge voice id + 实测响度补偿表 + 6 档 tone;`--list` 查音色表与语气档 |
|
|
1075
|
+
| `scripts/filmkit.py` | 生产内核:字体/字形自查/缓存排版/字幕安全区(读 `design.visual.subtitle`)/缓动/确定性随机/音频总线/**CueBus 提示点**/**立体声混音 mix_film**/TTS/时间轴/**hold 停留窗口**/FrameSink/合成/质检 |
|
|
1076
|
+
| `scripts/render.py` | 编排器:**14 个 stage**(`style`/`tts`/`layout`/`cues`/`score`/`sfx`/`audio`/`frames`/`mux`/`qa`/`stills`/`accept`/`explain`/`all`)、两种引擎(`--engine pil|web`)、分块并行(`--jobs`)、断点续跑(`--resume`)、母版预览(`stills`)、`--batch` 批量、`--preview N` 前 N 秒试片、`--run-id` 运行标签、`--vmaf full|sample|off`、`--no-preflight`。**`--scenes` 缺省 = 剧本同目录 `scenes.py` → 技能内置 `assets/scenes.py`** |
|
|
1077
|
+
| `scripts/gates.py` | **(v0.6)门禁账本**:pass / fail / skipped 三档、`out/QC-REPORT.md` 渲染、人工签核(绑内容指纹)、原子写 |
|
|
1078
|
+
| `scripts/stagewatch.py` | **(v0.6)阶段新鲜度**:每 stage 记录输入/输出指纹到 `work/manifest.json`;上游一改,下游报过时并给出最小重跑命令 |
|
|
1079
|
+
| `scripts/filmcheck.py` | **(v0.6)film.json 契约校验**:未知键提示「是不是想写 X」(以前静默忽略)、枚举与范围、跨字段(cue 引用的 id 必须存在)。`style` 键现在是 **inert 警告**(已废弃、不参与渲染),不会再误报「找不到 stylekit.py」 |
|
|
1080
|
+
| `scripts/selfcheck.py` | 回归自检:**67 条**「曾真的坏过」的断言,不联网、约 10–20 秒出结论(可选模块缺失标 SKIP)。v0.8 新增 8 条(44–51)、v0.9 新增 4 条(52–55)、v1.0 新增 3 条(56–58)、v1.0.2 新增 8 条:**59 逐 stage 可单独跑 / 60 brief 里的要求真的生效 / 61 字幕样式可定制(`visual.subtitle` 真的生效)/ 62 片内版式轮换(`layout_cycle`:换章即换骨架)/ 63 带约束的 brief 不会高概率采样失败 / 64 已删除 API 的 fail-closed(不许 NameError / 假绿 / 静默回落)/ 65 `design.audio` 的音量/音效参数真的落进 script / 66 片间去重台账(确认记账 / 提案避开 / 可关可查)/ 67 时长要求真的被接住(brief 里的「2 分钟 / 90 秒」→ duration_target:决定候选段落数 + 渲染对账,成片长度仍由实测旁白决定)** |
|
|
1081
|
+
| `scripts/preflight.py` | **(v0.8)渲染前资源闸门**:剧本结构 + 全部上屏文本逐字过字形检测(按字段选字重)+ 已有 TTS 音频可解析 + 字体可加载 + web 入口页存在。`render.py` 在 `frames` 之前调用,记门禁 `G0`;也可单独跑 `python preflight.py --script film.json [--json] [--strict]` |
|
|
1082
|
+
| `scripts/qavmaf.py` | **(v0.8)全片 VMAF/SSIM 门禁**:参考帧由场景函数**实时重渲**(不是成片抽帧),rawvideo 喂 ffmpeg 与成片逐帧比;一次调用同时算 VMAF 与 SSIM;门禁 `G11` |
|
|
1083
|
+
| `scripts/qaasr.py` | **(v0.8)ASR 回读门禁**:成片音轨转文字与剧本对账(字符级多重集召回 + 每句偏移);引擎优先 faster-whisper,其次 ffmpeg whisper 滤镜,都不可用记 skipped;门禁 `G12` |
|
|
1084
|
+
| `scripts/sfxkit.py` | **(v0.3)物理建模音效库**:模态合成/纹理/空间/动力学/母带 + 24 个配方 + `CUE_ALIASES` |
|
|
1085
|
+
| `scripts/musicgen.py` | **(v1.0.1)参数化配乐编排器**:和弦进行/动机/鼓型/音色全可传参,现场生成 `music.wav`(**没有风格表**) |
|
|
1086
|
+
| `scripts/scorekit.py` | **配乐合成原语**:和声/律动/吸气/自动电平/混音/响度测量/24-bit 落盘。v1.0.1 删掉了 9 套 `MUSIC_STYLES` 风格表(模板),留下的只是可复用的底层技术 |
|
|
1087
|
+
| `scripts/webengine.py` | **(v0.3)浏览器逐帧渲染**:Edge/Chromium + CDP 截图、分块并行、崩溃重开、cue 导出、文字质检 |
|
|
1088
|
+
| `scripts/qa_frames.py` | **(v0.3)帧级质检**:单帧闪烁/画面空窗/音画同步/亮度与响度曲线(JSON + PNG) |
|
|
1089
|
+
| `assets/scenes.py` | **技能内置渲染器**(v1.0.2 起是 `--scenes` 的默认回退):10 类构图绘制器 + `draw_frame` + **`cues()` 提示点钩子示范**,含缓存排版与预烘焙背景的正确写法。要改画面就把它拷成你自己的 `scenes.py` 再改,或直接参考它写 |
|
|
1090
|
+
| `assets/film.json` | **测试夹具**(跑通全链 / 金帧 / 门禁回归用,**不是给用户抄的模板**;里面没有 `style` 字段,外观由 `work/design.json` 决定) |
|
|
1091
|
+
| `assets/design_fixture.json` | **金帧基准设计**(像素回归绑它,不绑任何内置风格) |
|
|
1092
|
+
| `assets/voice_loudness.json` | 音色响度补偿实测表(voicekit 与 designkit 共用,避免两处漂移) |
|
|
1093
|
+
| `<REPO>/schemas/design.schema.json` | **(v1.0)设计 DSL 契约**(draft-07 子集):六块字段、`visual.subtitle` / `layout_cycle`、fail-closed 规则 |
|
|
1094
|
+
| `<REPO>/schemas/proposal.schema.json` | **(v1.0)提案契约**:候选 + 确认状态 + `designHash` / `scriptHash` |
|
|
1095
|
+
| `<REPO>/schemas/film.schema.json` | **(v0.6)film.json 的契约**(JSON Schema draft-07 子集)。校验器见 `scripts/filmcheck.py`,契约由 `tools/ci_check.py` 第 12 项守着 |
|
|
1096
|
+
| `<REPO>/tools/estimate.py` | **(v0.6)开渲前预演**:帧数 / 各阶段耗时 / 磁盘峰值 / 分块建议,`--json` 可机读。基准常量集中在文件顶部,换机请重测后回填 |
|
|
1097
|
+
| `<REPO>/tools/golden_frames.py` | **(v0.8)金帧像素回归**:渲 3 帧算 SHA-256 与 `assets/golden_frames.json` 比对(`--write` 重录 / `--check` 校验)。同环境像素变了是 fail,换了机器(字体指纹不同)是 skipped——跨机不误报 |
|
|
1098
|
+
| `<REPO>/tools/silhouette_qa.py` | **(v0.9)剪影可区分门禁(G13)**:不同设计渲染出的画面骨架必须两两可区分,防止「换设计只是换色」 |
|
|
1099
|
+
| `assets/web/` | **(v0.3)浏览器页面示例**:原生 Canvas2D 确定性时间轴、场景示范、文字质检脚本。**不是出片模板**:`--engine web` 需要你自己的页面(见 §3) |
|
|
1100
|
+
| `requirements.txt` | **(v0.4)Python 依赖清单**:必需(numpy/Pillow/edge-tts)/ 强烈建议(cv2)/ 可选(scipy、playwright)三档,带注释说明「缺了会怎样」。`pip install -r requirements.txt` 即可 |
|
|
1101
|
+
|
|
1102
|
+
### 关键内置能力(v0.2)
|
|
1103
|
+
|
|
1104
|
+
| API | 用途 |
|
|
1105
|
+
|---|---|
|
|
1106
|
+
| `K.tts(...)` | 逐句合成;缓存 key = text+voice+rate+pitch 哈希;网络重试 4 次;可选尾静音裁剪 |
|
|
1107
|
+
| `K.loudnorm_two_pass(...)` | 两遍响度标准化,返回可给 `-af` 的滤镜串 |
|
|
1108
|
+
| `K.loudness(path)` | 返回 `{I, LRA, TP}`(真峰已开 `ebur128=peak=true`) |
|
|
1109
|
+
| `K.AudioBus.music_bed(...)` | 连续配乐床(和弦交叉淡化,无接缝爆点) |
|
|
1110
|
+
| `K.AudioBus.mix()` | 归一化 + 软限幅,打印叠加峰值,>0.97 报警 |
|
|
1111
|
+
| `K.wrap_cn_cached / text_w_cached` | 场景每帧排版必用(实测单帧 12ms → 4.6ms) |
|
|
1112
|
+
| `K.glyph_missing_chars / check_glyphs` | 缺字形自查(✓ × ↵ 会画成方块) |
|
|
1113
|
+
| `K.write_ass(...)` | 直接输出 ASS,头里声明 PlayResX/Y=1920/1080 |
|
|
1114
|
+
| `K.encode_quality(film, ref_img=...)` | 锐度比;参考帧必须来自场景函数,不能是成片抽帧 |
|
|
1115
|
+
|
|
1116
|
+
## 11. 验收话术(不要自我宣称"做好了")
|
|
1117
|
+
|
|
1118
|
+
交付时必须能贴出:
|
|
1119
|
+
1. `ffprobe` 的时长/分辨率/码率
|
|
1120
|
+
2. `ebur128` 的积分响度与**真峰**(两个数都要,只有响度不算)
|
|
1121
|
+
3. 编码锐度比(≥0.85)**并说明参考帧来自场景函数重新渲染**
|
|
1122
|
+
4. 联系表路径 + "已用 read_image 亲眼确认"的**具体观察**(不是"看起来不错")
|
|
1123
|
+
5. 字幕条目数 = 剧本句数(无静默丢失)
|
|
1124
|
+
6. `python selfcheck.py` 的 PASS 计数
|
|
1125
|
+
7. **(v0.3)帧级深检的三个数**:单帧闪烁 N 条 / 画面空窗 N 段 / 音画同步超差 N 条
|
|
1126
|
+
(有 web 引擎时再加页面层的 文字重叠 / 出画 / 一闪而过 / 从没露面 四项)
|
|
1127
|
+
8. **(v0.8)新门禁的数**:G10 词级(N 句 / M 词、末词偏差)、G11 的 **VMAF 与 SSIM**(外加用了多少帧)、
|
|
1128
|
+
G12 的**字符召回**。这三项若在报告里是 `skipped`,要说明为什么没跑(没拷模块 / web 引擎 / 显式关闭),
|
|
1129
|
+
**不能只字不提**。
|
|
1130
|
+
|
|
1131
|
+
> **v0.6 起不要口头复述这些数字**:`out/QC-REPORT.md` 是机器生成的交付凭据,
|
|
1132
|
+
> 照抄报告里的"实测值"即可;报告里的 `skipped` 项必须逐项说明为什么没执行。
|
|
1133
|
+
>
|
|
1134
|
+
> 完整示例(本技能自带示例片的实测,可以照这个格式抄;**数字每次跑会差一点**——
|
|
1135
|
+
> TTS 实测时长、AAC 编码都会让真峰/响度/锐度在小范围内浮动,别 chasing 小数点):
|
|
1136
|
+
> ```
|
|
1137
|
+
> 容器:1920x1080 / 30 fps / h264+aac / 48 kHz / 时长 30.10 s / 1.00 Mbps
|
|
1138
|
+
> 信号:-16.0 LUFS / 真峰 -1.2 dBTP / 锐度比 1.01 / 黑帧 0 / 字幕 4 条 = 剧本 4 句
|
|
1139
|
+
> 时间:单帧闪烁 0 / 画面空窗 0 / 音画同步超差 0(本条为 web 引擎或写了 visual_at 时才有意义)
|
|
1140
|
+
> 词级:4 句 / 64 词 / 末词偏差最大 0.09 s(G10)
|
|
1141
|
+
> 画质:VMAF 97.22 / SSIM 0.9928 / 870 帧(G11,参考帧由场景函数重渲)
|
|
1142
|
+
> 回读:字符召回 0.803(G12,faster-whisper small;中英混排专名会掉分,< 0.72 才 fail)
|
|
1143
|
+
> 自检:selfcheck PASS 67 / FAIL 0 / SKIP 0;tools/ci_check.py PASS 12 / FAIL 0
|
|
1144
|
+
> 音频:work/score.json 的 variation 非 0(指纹参与了结构变奏)、audio_similarity --strict 全 PASS
|
|
1145
|
+
> 目检:work/qa/sheet.png,四帧分别为开场星射线、五步成片卡片、画质三件事卡片、收尾居中大字;
|
|
1146
|
+
> 字幕黑底白字不压正文,大字边距均匀,无空洞帧。
|
|
1147
|
+
> ```
|
|
1148
|
+
|
|
1149
|
+
> 自查口令:如果一条门禁从来没失败过,先确认它是不是根本测不到。
|
|
1150
|
+
|
|
1151
|
+
## 12. 作者机器已验证的环境事实(2026-11-07 实测)
|
|
1152
|
+
|
|
1153
|
+
> **本节是作者机器(Windows 11 + Ryzen AI 9 HX 370 + OSGeo4W Python 3.12)的实测基准,不是跨机器承诺。**
|
|
1154
|
+
> 换机后请先用 5 分钟重测:`ffmpeg -version`(是否有 libx264/aac/loudnorm)、
|
|
1155
|
+
> `python -c "import numpy,PIL,cv2"`、`dir C:\Windows\Fonts\msyhbd.ttc`、`edge-tts --list-voices`。
|
|
1156
|
+
> 任何一项不符,先改 `filmkit.FONT_CANDIDATES` / `FILMKIT_FFMPEG` 环境变量,别硬跑。
|
|
1157
|
+
|
|
1158
|
+
**这些数字是实测的,不是文档抄的。**
|
|
1159
|
+
|
|
1160
|
+
| 项 | 实测结论 |
|
|
1161
|
+
|---|---|
|
|
1162
|
+
| ffmpeg | **9.0.2 full_build**(gyan.dev),路径 `C:\Users\Administrator\AppData\Local\Microsoft\WinGet\Links\ffmpeg.exe` |
|
|
1163
|
+
| 编码器 | 实测可用:libx264 / libx265 / libvpx-vp9 / **aac(native)** / h264_amf;**无 libfdk_aac**。ffmpeg 构建里**带 nvenc 编码器**(h264/hevc/av1_nvenc),但本机无 NVIDIA GPU,跑不起来——别看到 `ffmpeg -encoders` 里有就以为能用 |
|
|
1164
|
+
| 滤镜 | drawtext(支持 fontfile)、subtitles/ass、zoompan、xfade、afade、amix、loudnorm 全在 |
|
|
1165
|
+
| ⚠ 第二套 ffmpeg | `C:\Program Files\EVCapture\ffmpeg.exe` 是 2019 旧版且**没有 xfade**,别混用 |
|
|
1166
|
+
| Python | OSGeo4W 3.12.16;numpy 2.5 / PIL 13 / cv2 5.0 / scipy 1.18 / edge-tts 7.3.1 / matplotlib / faster-whisper 已装 |
|
|
1167
|
+
| Python 缺 | **moviepy / pydub / soundfile / librosa / manim 都没装** → 代码里别 import 它们 |
|
|
1168
|
+
| 字体 | msyh/msyhbd/simhei/simsun/simkai/simfang/Deng + Noto Sans/Serif SC;**PIL 加载 ttc 必须带 `index=0`**(index=2 会抛 OSError) |
|
|
1169
|
+
| 字形缺口 | msyhbd 缺 ✓ × ↵ → 用矢量线条画,不要用符号字符 |
|
|
1170
|
+
| TTS | edge-tts 可用,zh-CN 8 个音色;单句 4 s 语音约 4 s 合成 |
|
|
1171
|
+
| 硬件 | Ryzen AI 9 HX 370 / 12C24T / 59.6 GB / **Radeon 890M 无 CUDA** |
|
|
1172
|
+
| 编码速度 | x264 `-preset medium -crf 17` = **141 fps(4.7× 实时)**;x265 94 fps;vp9 65 fps |
|
|
1173
|
+
| 硬件编码 | h264_amf 127 fps —— **不比 x264 快,体积大 2.5 倍,不用** |
|
|
1174
|
+
| 端到端 | 17.47 s 成片(524 帧 + 3 句旁白)编码 4.99 s = **3.5× 实时** |
|
|
1175
|
+
| 实测渲染 | PIL 中等复杂度场景 **40–54 fps**(1080p30);自带示例片 **68–82 fps**(多次实测区间) |
|
|
1176
|
+
| 磁盘 | 当时约 C 300 GB / D 520 GB(会持续变动,别当基准) |
|
|
1177
|
+
|
|
1178
|
+
### v0.3.0 各模块实测(同机,2026-11-07)
|
|
1179
|
+
|
|
1180
|
+
| 模块 | 实测数据 |
|
|
1181
|
+
|---|---|
|
|
1182
|
+
| `sfxkit` | 24 配方单次渲染 ≤ 1.5 s(**selftest 断言的门槛**;空载最慢 wind ≈0.8 s,并发负载下 ~1.3 s);**389 s 长环境声** wind 5.7–7.8 / rain 7.3–7.9 / campfire 6.3–6.7 s(预算 8 s,随负载浮动);真峰与 ffmpeg 差 **0.05 dB**;响度与 ebur128 差 **0.01 LU** |
|
|
1183
|
+
| `musicgen` | 12 s 合成 **0.30 s**、30 s **0.78 s**(v1.0.1 实测);输出峰值 -6 dBFS(母带交给 loudnorm)。参数全部参与编排(seed/mood/voices/progressions 相关性均 < 0.95) |
|
|
1184
|
+
| `scorekit` | 作为合成原语:389 s 全片 **4–8 s**(v0.3 实测)、60 s 2–4 s;24-bit 落盘与响度测量仍被复用。**它不再是配乐引擎**,`--selftest` 只验原语与 legacy 逐样本兼容 |
|
|
1185
|
+
| `webengine` | **80–130 ms/帧**(1080p30,Edge 无头,PNG 截图);12 s / 360 帧 **28–30 s**(2 workers,含启动);**同一页面会话内**同帧两次截图 byte 完全一致(跨会话默认 GPU 光栅有 ≤4 像素、±1 级抗锯齿差异,要跨会话字节级一致请开 `--deterministic`,代价约 +22%/帧) |
|
|
1186
|
+
| `qa_frames` | 30 s 片全量深检 **约 1 s**(160×90 流式解码 903 帧);闪烁/空窗/静止/同步 |
|
|
1187
|
+
| 端到端 | 30.10 s 示例片:TTS 4 句 → cues 由 scenes.py 按 kind 生成(v0.7,示例片 17 条)→ 配乐 3.3 s → 音效逐条落位 → 混音 → PIL 75 fps 渲染 12 s → mux → 深检 PASS |
|
|
1188
|
+
| Python 依赖 | **playwright 已装**(chromium + msedge 均可用);scipy 1.18 已装(sfxkit 可选,缺了走 FFT 兜底);`soundfile` **没装**(本技能不依赖它) |
|
|
1189
|
+
|
|
1190
|
+
### 排期公式(保守)
|
|
1191
|
+
|
|
1192
|
+
```
|
|
1193
|
+
Pillow 路线: 墙钟秒数 ≈ 帧数 / 40 / 并行度 + 30% 余量
|
|
1194
|
+
浏览器路线: 墙钟秒数 ≈ 帧数 × 0.3 / 并行度 + 30% 余量
|
|
1195
|
+
重负载画面(多图/模糊/纹理):按 12–23 fps 估,而不是 40–54 fps
|
|
1196
|
+
```
|
|
1197
|
+
|
|
1198
|
+
### 三条来自实战的"隐形坑"
|
|
1199
|
+
|
|
1200
|
+
1. **`-shortest` 会按较短一路截断**:音频比视频短 1.2 s 时,请求 524 帧只出 488 帧,且不报错。**永远显式 `-t` 并校验两路时长差 < 0.05 s**(`filmkit.mux` 已内置此检查)。
|
|
1201
|
+
2. **`-stream_loop -1` 让 `-shortest` 失效**:会产生 13 小时的怪物文件。循环输入必须配显式 `-t`。
|
|
1202
|
+
3. **`amix` 默认 `normalize=1`**:按输入数均分,2 路输入直接掉 6 dB。要么 `normalize=0`,要么走 numpy 总线(推荐)。
|
|
1203
|
+
|
|
1204
|
+
## 13. 升级须知(从旧版迁移要动的地方)
|
|
1205
|
+
|
|
1206
|
+
### 13.0 v0.4 → 当前版本(v0.5 … v0.8.1 / v0.9 / v1.0 / v1.0.1 / v1.0.2 要做什么)
|
|
1207
|
+
|
|
1208
|
+
**一句话**:v1.0 是**破坏性变更**——不再有 `style`、没有内置风格表、外观必须来自 `work/design.json`。
|
|
1209
|
+
v0.4–v0.9 的老项目要出片,必须走一遍 `propose.py` → `--confirm`。按版本逐条过:
|
|
1210
|
+
|
|
1211
|
+
| 从 → 到 | 你要做的动作 | 依据 |
|
|
1212
|
+
|---|---|---|
|
|
1213
|
+
| v0.4 → **v0.5** | `--stage` 多出 `style`,`--stage all` 顺序变为 `style → tts → …`(**这一步的 style 体系已在 v1.0.1 删除**,见下面 v1.0.1 行) | CHANGELOG 0.5.0 |
|
|
1214
|
+
| v0.5 → **v0.6** | 首次跑会多出 `work/manifest.json` / `work/gates.json` / `out/QC-REPORT.md`,属新增文件 | CHANGELOG 0.6.0 |
|
|
1215
|
+
| v0.6 → **v0.7** | 内容指纹参与配乐结构;想更丰富就把提示点写进 `scenes.py` 的 `cues()` | CHANGELOG 0.7.0 |
|
|
1216
|
+
| v0.7 → **v0.7.1** | 配乐基线 `music.gain_db` 从 `-1.5` 变成 **-12**,有人声处再压配乐 6 dB / 音效 3 dB;**手调过 `gain_db`/`duck_db` 的片子要重新听一遍** | CHANGELOG 0.7.1 |
|
|
1217
|
+
| v0.7.1 → **v0.7.2** | `QC-REPORT.md` 只落在 `--out` 的目录 | `render._report_path()` |
|
|
1218
|
+
| v0.7.2 → **v0.8.0** | ① 可选模块 `preflight.py` / `qavmaf.py` / `qaasr.py` 决定 G0/G11/G12 跑不跑(缺了记 skipped,`skipped != 通过`);② `words` 默认 true,首次 `--stage tts` 会重新联网合成一遍;③ qa 变慢(全片 VMAF 要重渲参考帧);④ 输出仍默认 1080p@30 | CHANGELOG 0.8.0 |
|
|
1219
|
+
| v0.8.0 → **v0.8.1** | ① 删一次 `work/segments`(分块指纹变了);② `--stage style/stills/frames/mux` 的门禁 fail 现在真的返回 1;③ `ci_check` 汇总变三档;④ `filmcheck` 变严;⑤ `webengine` 改 fail-closed | CHANGELOG 0.8.1 |
|
|
1220
|
+
| v0.8.1 → **v0.9** | 场景语法(10 类构图)与视觉语言上线;新增 `work/storyboard.json`;G13 剪影可区分门禁 | CHANGELOG 0.9.0 |
|
|
1221
|
+
| v0.9 → **v1.0** | **提案优先**:`propose.py --brief 题材 --script film.json --work work` → `--confirm --candidate A` → `render.py --stage all`。删掉 `style: auto` 的自动选型 | CHANGELOG 1.0.0 |
|
|
1222
|
+
| v1.0 → **v1.0.1** | **删除全部模板/风格表**:`presets/`、`--preset`、`stylekit` 的 `THEMES/STYLES/VISUAL_LANGUAGES`、`scorekit` 的 `MUSIC_STYLES`、voicekit 的 persona 表全部删除。`film.json` 里写 `style` 不再生效;**同一个 brief 连拍两次会得到不同设计**(要复现就传 `--seed`) | CHANGELOG 1.0.1 |
|
|
1223
|
+
| v1.0.1 → **v1.0.2** | ① **零拷贝**:不用再拷 `scripts/*.py` / `assets/scenes.py`,直接跑技能目录里的脚本;`--scenes` 缺省按「剧本同目录 → 技能内置」回落。② 设计 DSL 新增 `visual.subtitle`(字幕样式,不写 = 逐像素不变)与 `visual.layout_cycle`(按章节轮换版式,默认派生 2–3 种)。③ 带约束的 brief(如「深蓝」)不再有约一半概率采样失败。④ 已删除 API 改为明确报错:`stylekit --list/--show/--lint` → 说明已删除 + **exit 2**;`--stage style` 不再 KeyError;`--stage cues` 可单跑。⑤ 片间去重台账默认开启(`~/.dsh/video-studio/design-ledger.json`,`--no-ledger` / `--show-ledger` / `VIDEO_STUDIO_LEDGER`)。⑥ 自检 **67 条** | CHANGELOG 1.0.2 |
|
|
1224
|
+
|
|
1225
|
+
> 查当前版本:`python <REPO>/tools/ci_check.py -v` 里的 version 断言、或 `cat package.json` 的 `version`。
|
|
1226
|
+
> 升级后建议按 §14 重跑一次自检(67 条),再按 §7 重新签核(改了代码/默认值,旧签核会自动失效,这是设计)。
|
|
1227
|
+
|
|
1228
|
+
> **v1.0.1 → v1.0.2 的具体迁移动作**:
|
|
1229
|
+
> 1. 删掉工程目录里拷过去的 `scripts/*.py`、`scenes.py`、`assets/web/`——不再需要;命令改成
|
|
1230
|
+
> `python <SKILL_DIR>/scripts/render.py --script film.json --work work --out out/film.mp4 --stage all`。
|
|
1231
|
+
> 2. 如果你想保留自己改过的 `scenes.py`:放在**剧本同目录**即可被自动发现(或显式 `--scenes`)。
|
|
1232
|
+
> 3. 字幕样式不再写死在 `filmkit` 里:想要自定义就在 `work/design.json` 的 `visual.subtitle` 里写
|
|
1233
|
+
> 那几个字段(或直接改 design 再 `designkit.py --lint` 校验);不写 = 与 v1.0.1 逐像素一致。
|
|
1234
|
+
> 4. 以前跑 `stylekit.py --list` 的脚本会拿到 exit 2 和一条说明——那是预期行为(表已删除)。
|
|
1235
|
+
> 5. 自检条数从 58 → **66**;`grep 'PASS 58'` 之类的交付脚本要改。
|
|
1236
|
+
|
|
1237
|
+
### 13.1 v0.3.0 → v0.4.0
|
|
1238
|
+
|
|
1239
|
+
**多数项目不用改任何东西**(默认行为向后兼容),但下面几处会**改变你看到的输出或报错**:
|
|
1240
|
+
|
|
1241
|
+
| 变更 | 影响 | 你要做什么 |
|
|
1242
|
+
|---|---|---|
|
|
1243
|
+
| `--resume` 增加计划指纹 | 改过台词 / scenes.py / crf·preset·chunk 之后再用 `--resume`,会**报错拒绝拼接**(旧行为是静默复用错画面) | 按提示 `rm -rf work/segments` 重渲;没改过东西时 `--resume` 照旧只补缺块 |
|
|
1244
|
+
| `music.mode=off` 真的不混配乐 | 以前 `work/music.wav` 存在就会被混进去;现在 off 时既不读也不删 | 想用外部配乐:显式 `"mode": "file"`,或不写 `mode`/`bpm` 让兼容链生效 |
|
|
1245
|
+
| `--stage qa` 报错更严 | 缺 `scenes.py` 时不再崩,改为丢参考帧 + 两条 WARN;锐度比参考帧按时刻配对 | 无(行为更合理) |
|
|
1246
|
+
| 新增"画面长时间静止"WARN | 连续 6 s 无视觉变化会告警(默认不 fail) | 设计上确实要静止 >6 s 就接受它;误报可调 `FLAT_DRIFT_EPS` |
|
|
1247
|
+
| 音画同步可真的生效 | cue 支持 `visual_at`;`QA_REQUIRE_SYNC=1` 把"没查"升级为 error | 想用门禁就加锚点 + 环境变量;不想要就保持默认(仅 warning) |
|
|
1248
|
+
| `film.json` 校验变严 | 缺 `text` / `lines` 空 / id 重复 / bpm 非法 会直接报错 | 按提示修剧本 |
|
|
1249
|
+
| 可选 peer 改成按需 import | 缺 `@deepseek-ai/dsh-skill-filesystem` 时插件不再整体加载失败,只是不挂载技能并打日志 | 想用技能就按日志里的命令装依赖 |
|
|
1250
|
+
|
|
1251
|
+
### 13.2 v0.2 → v0.3.0
|
|
1252
|
+
|
|
1253
|
+
**兼容性优先:老项目不做任何改动也能跑**(`engine` 默认 `pil`、`music.mode` 缺省时按老的和弦床走、`whoosh` 行为保留)。
|
|
1254
|
+
|
|
1255
|
+
| 变更 | v0.2 | v0.3.0 | 你要做什么 |
|
|
1256
|
+
|---|---|---|---|
|
|
1257
|
+
| stage 列表 | 7 个 | **10 个**(+cues/score/sfx) | `--stage all` 顺序变了;单独跑多阶段时补 `--stage cues sfx` |
|
|
1258
|
+
| 渲染引擎 | 只有 PIL | `--engine pil\|web` | 想用浏览器就拷 `assets/web/` 并装 playwright |
|
|
1259
|
+
| 配乐 | 只有和弦床 | 有 `bpm` 时由 musicgen 现场编排 | 想保留旧声音:`"music": {"mode": "bed"}` 或不写 `bpm` |
|
|
1260
|
+
| 音效 | 每段一声 whoosh | 按 `cues.json` 合成 | 想保留旧行为:不写 `sfx.cues`、不写 `scenes.cues()`,且不跑 `--stage cues` |
|
|
1261
|
+
| 混音 | 旁白+配乐两路 | 旁白+配乐+音效三路(带 duck) | 无(自动) |
|
|
1262
|
+
| 时间轴 | lead/tail/min_dur | + `hold` / `bar` | 想加停留就写 `hold` |
|
|
1263
|
+
| 质检 | 成片级 | + 帧级深检(有 `qa_frames.py` 时) | 无(自动;没这个文件就跳过) |
|
|
1264
|
+
| qa 退出码 | 恒 0 | **有 FAIL 就返回 1** | CI 里会开始真的失败——这是故意的 |
|
|
1265
|
+
|
|
1266
|
+
**新增文件**:`sfxkit.py` / `scorekit.py` / `webengine.py` / `qa_frames.py` / `assets/web/`(都是可选,缺失时对应能力自动降级并打印 WARN)。**v1.0.1 起配乐改由 `musicgen.py` 现场编排**,scorekit 只作为它的合成原语库保留。
|
|
1267
|
+
|
|
1268
|
+
### 13.3 v0.1 → v0.2(历史)
|
|
1269
|
+
|
|
1270
|
+
| 变更 | v0.1 | v0.2 | 迁移动作 |
|
|
1271
|
+
|---|---|---|---|
|
|
1272
|
+
| 阶段语义 | `--stage mux/qa/frames` 都会重渲全片 | 每个 stage 只做自己的事 | 脚本里连续跑多阶段时去掉重复的 `--stage all` |
|
|
1273
|
+
| TTS 缓存 | 只判断 mp3 存在 | text/voice/rate/pitch 哈希必须匹配 | 首次升级加 `--force-tts` 跑一次 |
|
|
1274
|
+
| 字幕窗口 | 硬钳在 `voice_start+5.2` | 默认覆盖整句(`end-0.06` 封顶) | 确实要短字幕就传 `sub_max_hold` |
|
|
1275
|
+
| 视频合成 | 对 silent.mp4 再编一遍 libx264 | `-c:v copy`(不重编码) | 要改分辨率时显式 `"reencode_video": true` |
|
|
1276
|
+
| 帧尺寸 | 不符时静默 resize | `strict=True` 直接报错 | 场景函数必须返回 1920×1080 |
|
|
1277
|
+
| 码率门禁 | `min_bitrate=3Mbps` 算错误 | 默认不当门禁,只做参考 | 需要就显式传 `min_bitrate` |
|
|
1278
|
+
| 真峰门禁 | 解析不到 Peak → 永不触发 | `ebur128=peak=true`,真测 | 无(自动生效) |
|
|
1279
|
+
| 锐度比 | 参考帧用成片抽帧 → 恒 ≈1.0 | 参考帧由场景函数重新渲染 | 无(`st_qa` 已改) |
|
|
1280
|
+
| TTS 音频 | 保留 edge-tts 尾部 ~0.95s 静音 | 默认裁掉首尾静音 | 想保留:`"trim_silence": false` |
|
|
1281
|
+
|
|
1282
|
+
## 14. 换机 60 秒自检(比读文档快)
|
|
1283
|
+
|
|
1284
|
+
```bash
|
|
1285
|
+
cd <SKILL_DIR>/scripts
|
|
1286
|
+
python selfcheck.py # 67 条断言;FAIL 就照提示修环境
|
|
1287
|
+
python sfxkit.py --selftest # 音效库(可选模块,装了才跑)
|
|
1288
|
+
python musicgen.py --selftest # 配乐编排器(参数化)
|
|
1289
|
+
python scorekit.py --selftest # 配乐合成原语(不是配乐引擎)
|
|
1290
|
+
python webengine.py --selftest # 浏览器引擎(35 项,会真起无头浏览器,约 60 秒)
|
|
1291
|
+
python qa_frames.py --selftest # 帧级质检(自己造测试视频)
|
|
1292
|
+
```
|
|
1293
|
+
|
|
1294
|
+
selfcheck 自己造测试信号、现场调 ffmpeg 编码,**不联网、不依赖任何成片**。
|
|
1295
|
+
每条 FAIL 都对应一个具体环境缺口(缺字体 / ffmpeg 无 libx264 / 无 numpy / 缺 sfxkit),
|
|
1296
|
+
报错信息直接写该改哪里。可选模块缺失时 selfcheck 标 `SKIP` 而不是 FAIL——
|
|
1297
|
+
**别把"没装可选件"和"坏了"混为一谈**。
|
|
1298
|
+
|
|
1299
|
+
> **仓库级体检**(v0.4 起)是另一层,别和 selfcheck 搞混:
|
|
1300
|
+
> `python <REPO>/tools/ci_check.py -v`(在包根执行)只查"这个仓库作为可分发包是否完整"(语法/JSON/依赖白名单/
|
|
1301
|
+
> 打包卫生/文档路径引用),**不需要 ffmpeg、字体或网络**,任何机器都能跑;
|
|
1302
|
+
> selfcheck 查的是"技能还能不能真的出片"。两层都在 CI 里跑。
|
|
1303
|
+
>
|
|
1304
|
+
> v0.6.0 起 ci_check 多了第 12 项「film.json 契约」:直接调 `scripts/filmcheck.py` 校验模板,
|
|
1305
|
+
>并把 `schemas/film.schema.json` 的存在与合法性一起断言——**校验规则只允许有一份**,
|
|
1306
|
+
>不在这里重写一遍。
|
|
1307
|
+
|
|
1308
|
+
### v0.2 在本机实测(2026-11-07,同一台机器)
|
|
1309
|
+
|
|
1310
|
+
| 项 | 数据 |
|
|
1311
|
+
|---|---|
|
|
1312
|
+
| 场景渲染(缓存排版 + 预烘焙背景) | **单帧 12.3ms → 4.6ms**;885 帧整片约 8s(103 fps) |
|
|
1313
|
+
| 分块并行(--jobs 3,4 段) | 885 帧 **7s** 完成分段,拼接后时长误差 0 |
|
|
1314
|
+
| mux(视频流 copy) | 两遍 loudnorm 后 −16.5 LUFS / 真峰 −1.5 dBTP,不二次编码 |
|
|
1315
|
+
| 11 分钟真实片 | 帧渲染 69–80 fps |
|
|
1316
|
+
| selfcheck | 14/14 PASS(含真峰、ASS PlayRes、尾静音裁剪、mux 48kHz) |
|