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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/CHANGELOG.md +1109 -0
  2. package/README.md +76 -10
  3. package/THIRD_PARTY_NOTICES.md +1 -0
  4. package/docs/styles/sheet-8-styles.png +0 -0
  5. package/docs/styles/sheet-before-after.png +0 -0
  6. package/index.js +4 -1
  7. package/package.json +7 -3
  8. package/schemas/film.schema.json +329 -2
  9. package/skills/video-studio/SKILL.md +270 -13
  10. package/skills/video-studio/assets/golden_frames.json +65 -0
  11. package/skills/video-studio/assets/scenes.py +2162 -1190
  12. package/skills/video-studio/assets/web/engine.js +63 -2
  13. package/skills/video-studio/assets/web/film.js +224 -101
  14. package/skills/video-studio/assets/web/index.html +4 -2
  15. package/skills/video-studio/assets/web/qa_text.js +36 -8
  16. package/skills/video-studio/references/audio-variation.md +4 -3
  17. package/skills/video-studio/references/audio.md +2 -2
  18. package/skills/video-studio/references/change-impact.md +1 -1
  19. package/skills/video-studio/references/quality-gates.md +25 -3
  20. package/skills/video-studio/references/scene-grammar.md +306 -0
  21. package/skills/video-studio/references/style-packs.md +108 -3
  22. package/skills/video-studio/references/v08-quality.md +256 -0
  23. package/skills/video-studio/references/web-engine.md +19 -2
  24. package/skills/video-studio/references/word-timeline.md +147 -0
  25. package/skills/video-studio/requirements.txt +14 -0
  26. package/skills/video-studio/scripts/filmcheck.py +218 -0
  27. package/skills/video-studio/scripts/filmkit.py +521 -38
  28. package/skills/video-studio/scripts/gates.py +88 -4
  29. package/skills/video-studio/scripts/grammar.py +559 -0
  30. package/skills/video-studio/scripts/preflight.py +774 -0
  31. package/skills/video-studio/scripts/qa_frames.py +49 -8
  32. package/skills/video-studio/scripts/qaasr.py +1563 -0
  33. package/skills/video-studio/scripts/qavmaf.py +1442 -0
  34. package/skills/video-studio/scripts/render.py +764 -32
  35. package/skills/video-studio/scripts/scorekit.py +16 -30
  36. package/skills/video-studio/scripts/selfcheck.py +483 -0
  37. package/skills/video-studio/scripts/sfxkit.py +130 -14
  38. package/skills/video-studio/scripts/stagewatch.py +19 -6
  39. package/skills/video-studio/scripts/stylekit.py +242 -12
  40. package/skills/video-studio/scripts/voicekit.py +15 -7
  41. package/skills/video-studio/scripts/webengine.py +1119 -72
  42. package/tools/ci_check.py +173 -31
  43. package/tools/estimate.py +130 -19
  44. package/tools/golden_frames.py +664 -0
  45. package/tools/silhouette_qa.py +910 -0
@@ -42,17 +42,24 @@ cp <SKILL_DIR>/assets/scenes.py . # 场景模板(改画面即可)
42
42
  cp <SKILL_DIR>/scripts/{filmkit.py,stylekit.py,render.py,selfcheck.py,gates.py,stagewatch.py} .
43
43
  # 可选(v0.3):要程序化音效/配乐就一并拷;要走浏览器渲染再拷 webengine.py + assets/web/
44
44
  cp <SKILL_DIR>/scripts/{sfxkit.py,scorekit.py,voicekit.py,qa_frames.py} .
45
+ # v0.8.0 起渲染前预检 / 全片 VMAF / ASR 回读是三个**可选模块**:
46
+ # 拷了才有对应门禁(G0 / G11 / G12);不拷就记 skipped 并写明原因,不会让渲染失败。
47
+ # 但"skipped != 通过"——交付前请确认这三项是被你**主动**跳过的。
48
+ cp <SKILL_DIR>/scripts/{preflight.py,qavmaf.py,qaasr.py} .
45
49
  cp -r <SKILL_DIR>/assets/web ./web
46
50
 
47
- # 2) 先跑一次自检:43 条回归护栏,20 秒内出结论(不联网;可选模块缺失会标 SKIP)
51
+ # 2) 先跑一次自检:55 条回归护栏(v0.9 新增 52–55),20 秒内出结论(不联网;可选模块缺失会标 SKIP)
48
52
  python selfcheck.py
49
53
 
50
54
  # 3) 【v0.5 新步骤】先确认风格,再改文字
51
55
  # film.json 的 "style": "auto" 会按内容自动选一套(画风+配乐+旁白一起变);
52
- # 想锁定就写死风格 id。这条命令打印"为什么选这套",并落盘 work/style.json。
56
+ # 想锁定就写死风格 id。这条命令打印"为什么选这套"与"视觉语言"(v0.9),并落盘 work/style.json。
53
57
  python render.py --script film.json --stage style
54
58
 
55
59
  # 3b) 改 film.json 的 lines[].text / sub / big / items
60
+ # v0.9:每段还能写 type(10 类构图之一)与 quote/by/stats/series/steps/sides;
61
+ # 不写就由 grammar 按内容选。排完轴看分镜:--stage layout 会打印"每段为什么是这个构图",
62
+ # 并落 work/storyboard.json(详见 <SKILL_DIR>/references/scene-grammar.md)。
56
63
  # text = 朗读文本(自然口语);sub = 上屏字幕(短,一行优先)
57
64
 
58
65
  # 4) 母版预览(P6):先只渲几张关键帧看构图/配色,别一上来渲全片
@@ -67,9 +74,26 @@ python render.py --script film.json --scenes scenes.py --work work --stage still
67
74
  # 注意:estimate.py 是**仓库/包里的工具**(<REPO>/tools/),不在 skills/video-studio/ 里,
68
75
  # quickstart 拷出来的 myfilm/ 也不会有它;想用就按包根路径调用,或从包/仓库里复制过来。
69
76
  python <REPO>/tools/estimate.py --script film.json --jobs 4
77
+ # 【v0.8 必看】estimate 已经算进新增的三道门禁,预演时间不再只是渲染时间:
78
+ # 29 s 模板片从 v0.7 的 total 28.4 s 变成 37.5–83.0 s(vmaf 单阶段 12.6 s 起)。
79
+ # 预演给的是**区间上界**(悲观侧),实测全链约 47.3 s —— 别把区间上界当承诺。
80
+ # 要压回原来的量级:在 film.json 写 "qa": {"vmaf": {"mode": "off"}}——estimate 与
81
+ # render.py 都读这个字段,G11 记 skipped。注意 estimate **没有** --vmaf 参数:
82
+ # --vmaf off|sample 是 render.py 的命令行开关,加在 estimate 上会直接报错;
83
+ # 而 --vmaf sample 只是每 5 帧取 1(仍然跑 VMAF,pass/fail 照判),只有 off 才 skipped。
70
84
 
71
85
  # 5) 一条命令出片(TTS→排版→混音→渲染→合成→质检)
72
86
  python render.py --script film.json --scenes scenes.py --out out/film.mp4 --work work
87
+ # 【v0.8 会变慢,别以为卡死】一条 29.011 s / 870 帧的模板片实测:
88
+ # · 全链 --stage all:47.3 s(v0.7 约 28 s),其中 frames 12.5 s、qa 23.9 s
89
+ # · 渲前资源预检(G0) < 1 s(查字形/字体/已有 TTS 音频是否可解析)
90
+ # · 全片 VMAF+SSIM(G11) 14.2 s(参考帧要按场景函数重渲一遍 870 帧)
91
+ # · ASR 回读(G12) 8.7 s(faster-whisper small / CPU;没装就 skipped)
92
+ # —— **qa 已经超过渲染本身**(23.9 s vs 12.5 s)。同一台机器负载不同能差近 2×,
93
+ # 别把单次秒数当承诺;12 分钟片按 0.4× 实时外推 VMAF ≈ 5 分钟。
94
+ # 想先快速看画面:--preview 15(只渲前 15 秒出 mp4+gif,不写交付账本);
95
+ # 想省掉 VMAF:--vmaf off 或 film.json 的 "qa": {"vmaf": {"mode": "off"}}(记 skipped)。
96
+ # web 引擎没有可重渲的参考帧,G11 恒 skipped——这不是漏查,是路线限制。
73
97
 
74
98
  # 6) 亲眼看质检抽出的帧
75
99
  # work/qa/sheet.png 联系表(全片调性)
@@ -90,7 +114,7 @@ python render.py --script film.json --work work --stage accept --gate release
90
114
  ```bash
91
115
  python render.py ... --stage style # 只解析风格 -> work/style.json(画风/配乐/旁白/节奏一次定死)
92
116
  python render.py ... --stage tts # 只合成旁白 + 实测时长(带输入校验的缓存)
93
- python render.py ... --stage layout # 只排时间轴 -> timeline.json / sub.srt / sub.ass(含 hold 停留窗口)
117
+ python render.py ... --stage layout # 只排时间轴 -> timeline.json / sub.srt / sub.ass(含 hold 停留窗口;v0.9 还选构图、打印分镜、落 work/storyboard.json)
94
118
  python render.py ... --stage cues # 只出提示点 -> work/cues.json(声音跟着画面走)
95
119
  python render.py ... --stage score # 只出配乐 -> work/music.wav(scorekit)
96
120
  python render.py ... --stage sfx # 只出音效轨 -> work/sfx.wav(sfxkit,按 cues.json)
@@ -354,6 +378,60 @@ python render.py ... --stage style # 用某份 film.json 解析风格并打印"
354
378
 
355
379
  字段、指纹规则、加第 9 套风格的步骤见 **references/style-packs.md**。
356
380
 
381
+ ### 5.5b 场景语法与视觉语言(v0.9):换风格不再只是换色
382
+
383
+ v0.5 解决了"画风/配乐/旁白一片一样",但只到**皮肤**层。骨架没变:当时 `assets/scenes.py`
384
+ 只有 3 个绘制器(hook / body / outro),quote / emphasis / calm 全部回落到 body,
385
+ 三个绘制器共用"背景板 → 眉标 → 大字 → 副标 → 强调线 → HUD"同一套结构。
386
+ 于是 8 套风格换完,灰度量出来的构图骨架(前景占比/重心/边缘/对称)几乎不动——
387
+ 用户说的"每个视频都差不多,只是主题和颜色不同"就是这么来的。
388
+
389
+ v0.9 把这件事拆成三层**正交**的:
390
+
391
+ | 层 | 决定什么 | 谁读它 | 产物 |
392
+ |---|---|---|---|
393
+ | 叙事 `kind` | 声线、语气、配乐能量 | voicekit / scorekit | 本版**没有新增 kind 值** |
394
+ | 场景 `type`(10 类构图) | 这段画面长什么样 | `assets/scenes.py` 的 10 个绘制器(`_DRAWERS`) | timeline 的 `scene_type`、`work/storyboard.json` |
395
+ | 视觉语言(风格 id) | 这套风格怎么画(母题/转场/字距/重心) | `stylekit.VISUAL_LANGUAGES` | `work/style.json` 的 `language` |
396
+
397
+ 场景类型有 10 个:`hero` 封面 / `statement` 金句 / `list` 要点 / `stat` 数字卡 /
398
+ `compare` 左右对比 / `process` 流程轴 / `quote` 引语 / `chapter` 章节卡 /
399
+ `chart` 数据条 / `outro` 收尾。**不写就不管**——grammar 按内容自动选(字段存在性 >
400
+ 关键词 > 结构位置,段长不足按固定表降级,连续同类型 ≤2 段);
401
+ 要锁定就写行级 `lines[].type`。
402
+
403
+ 看选型与环境:
404
+
405
+ ```bash
406
+ python render.py --script film.json --scenes scenes.py --work work --stage layout
407
+ # 分镜: 4 段 / 封面×1 要点列表×2 收尾×1
408
+ # l01 hook -> hero v0 d0 首段/开场
409
+ # l02 body -> list v0 d2 items>=3
410
+ # ...
411
+ # 同时落 work/storyboard.json:每段的 type / 变体 / 装饰密度 / 理由,可审计
412
+
413
+ python render.py --script film.json --stage style
414
+ # 视觉语言: 母题=brackets 转场=dip(0.28s) 字距=0.0 编号=dim 重心=(+0,+0)
415
+ # (坏配置在这里就拦,不等渲完 300 帧)
416
+ ```
417
+
418
+ **legacy 边界(必须记住)**:不写 `style` 字段 = 走 v0.4 三个老绘制器,逐字节不变,
419
+ timeline 里不会出现场景字段。想要新构图,写 `"style": "auto"` 或任意风格 id。
420
+
421
+ **三条代价与边界**:
422
+
423
+ 1. **比 v0.8.1 慢,但仍在预算内**:模板片 frames 约 **19 ms/帧(54–61 fps)**,预算 25 ms;
424
+ v0.8.1 约 13 ms/帧(约 75 fps)。慢在 10 类构图各自多了装饰与几何。
425
+ 0.9.0 首版还要慢一截(约 20–24 ms/帧),0.9.1 把装饰母题从"逐帧 paste(实测 5.6 ms)"
426
+ 改成"烘焙进背景底版(1.3–2.0 ms)"才回到现在这个数。换机必须重测。
427
+ 2. **同一部片仍锁一个明暗族**:段界跨族会整屏换底、触发闪烁门禁,也像换片。
428
+ 3. **web 引擎不跟随**:`--engine web` 的页面由你自己写,本次没有 10 类构图,
429
+ 画风仍只有主题色。
430
+
431
+ 选型规则、`storyboard.json` 字段、加第 11 类构图的步骤、常见误用,见
432
+ [scene-grammar.md](references/scene-grammar.md);五组视觉语言签名与加第 9 套风格要补什么,
433
+ 见 [style-packs.md](references/style-packs.md)。
434
+
357
435
  ### 5.6 混音链(照着做,别自创)
358
436
 
359
437
  ```
@@ -431,11 +509,62 @@ python <REPO>/tools/audio_similarity.py --strict # A/B/C 三组阈值,失败
431
509
  2. 出现了**要理解的结构**(网络层次、坐标轴、流程图)→ 留到能扫一眼看完。
432
510
  3. 两个镜头之间**没有内容连续性**(黑一下、空一拍)→ 让下一幕提前开始,而不是加长黑场。
433
511
 
512
+ ## 6.1 词级时间轴与词锚点(v0.8.0)
513
+
514
+ > **一句话**:句时长仍然是权威;词级时间戳是**附加值**,只用来「字幕按词折行 / cue 打到某个词上 /
515
+ > 门禁判它有没有变」——它不参与排轴,也**绝不**用词时长之和去推算句子时长。
516
+
517
+ **开与关**:`film.json` 顶层 `"words": true|false`,默认 **true**。
518
+ `--stage tts` 会额外收 edge-tts 的 `WordBoundary` 事件,落盘 `work/words.json`:
519
+
520
+ ```jsonc
521
+ { "schema": 1, "enabled": true,
522
+ "lines": { "l03": { "dur": 7.18, "n": 13, "covered": 5.2, "tail": 0.09,
523
+ "gaps": [0.18, 0.21, ...], "mono": true,
524
+ "words": [{"text":"画质","start":0.04,"end":0.32}, ...] } },
525
+ "stats": { "lines": 4, "words": 64, "empty": [] } }
526
+ ```
527
+
528
+ **它怎么来的(实现口径,改代码前先看)**:
529
+ - 必须显式 `edge_tts.Communicate(..., boundary="WordBoundary").stream()` 同时收音频与词事件。
530
+ 用 `save()` 会把词事件丢掉;不传 `boundary` 时默认是 `SentenceBoundary`,**一条词事件都收不到**
531
+ (实测 edge-tts 7.3.1 / zh-CN:默认设置下词事件数 = 0)。
532
+ - 音频先写 `<句>.mp3.part` 再 `os.replace`。旧写法直接 `open(..., "wb")`,一次断网就把**已缓存的好音频
533
+ 截成 0 字节**;现在失败只丢这次的新音频,旧缓存还在。
534
+ - 词事件 offset 是相对**原始 mp3** 的 100 ns 单位;`trim_silence=True` 时时间轴用的是裁剪后的语音,
535
+ 所以 `words_normalize(offset_shift=裁掉的句首秒数)` 会把词表整体平移。**不平移,词会整体提前 0.1–1 s**
536
+ (实测模板句 shift = 0.10 s 量级)。
537
+ - 词表有独立缓存键 `tts_words_key`,没并进 `tts_key_v2`:老音频照旧复用,只在"这次显式要词级"时补采一次。
538
+ - 词事件收不到**不是失败**:meta 里记 `words_ok=false`、`words_events=0`,打印 WARN 并复用已有音频;
539
+ G10 会记 `skipped`("词事件为空"),不许冒充 pass。
540
+
541
+ **下游怎么用**:
542
+
543
+ | 能力 | 用法 | 退化行为 |
544
+ |---|---|---|
545
+ | 字幕按词折行 | `fit_sub_lines(..., words=)` / `draw_subtitle(..., words=)` | 词表对不上原文或折出来超过 2 行 -> **回退字符折行**(老项目排版不变) |
546
+ | cue 词锚点 | `"at": "l03:w2"` / `"at": "l03:w2+0.1"`(**从 1 数**,w1 = 第一个词) | 该句没有那么多词 -> 退化为 `l03+偏移` 并打 WARN;场景 id 也不存在才报错 |
547
+ | 门禁判据 | G10 读 `work/words.json`(唯一事实来源,不再各自翻 `*.mp3.json`) | 没开/没数据 -> skipped |
548
+
549
+ **三个坑(都在真实跑里踩过)**:
550
+
551
+ 1. **词间有空隙**:实测 edge-tts 相邻词之间有 0.1–0.3 s 不在任何事件里。所以"词时长求和"永远小于句长,
552
+ G10 的覆盖率阈值才定在 45%(低于它才是"明显少了"),别拿 `sum(dur) == dur` 当判据。
553
+ 2. **句尾静音不在词事件里**:约 0.5 s,靠**整句实测时长**兜底。G10 只检查"末词终点离句尾不超过 0.40 s"
554
+ (模板片实测 0.07–0.12 s),超过它通常意味着词事件本身错位了。
555
+ 3. **词锚点会静默退化**:`l03:w99` 不会炸,只会 WARN 后退回句偏移。**改台词后先看 stdout 有没有这条 WARN**——
556
+ 否则你会以为自己精确打到了某个词。同样的,`words=false` 或词事件为空时,字幕不会报错,只是退回整句断行。
557
+
558
+ **老项目第一次升级**:默认 `words=true` 会让每句**重新联网合成一次**(老缓存里没有 `words_key`)。
559
+ 一次全片 TTS 的网络成本,换词级能力;不想付就把 `"words": false` 写进 `film.json`,行为回到 v0.7。
560
+
561
+ 细则(词级契约、时长权威性、裁剪平移公式、词锚点退化路径)见 [references/word-timeline.md](references/word-timeline.md)。
562
+
434
563
  ## 7. 质检门禁(error 必过 / warning 别当看不见)
435
564
 
436
565
  ```bash
437
566
  python render.py ... --stage qa # 只读质检:分辨率/音轨/时长/响度/真峰/锐度比/黑帧/字幕条数 + 帧级深检
438
- python selfcheck.py # 技能自身回归(43 条曾真的坏过的点)
567
+ python selfcheck.py # 技能自身回归(55 条曾真的坏过的点)
439
568
  python qa_frames.py out/film.mp4 --cues work/cues.json # 单独跑帧级深检,出 JSON + 图
440
569
  # v0.6:qa / all 收尾会自动写 out/QC-REPORT.md(机器可读交付结论)
441
570
  # 并落 work/gates.json(三档 gate matrix)与 work/cost.jsonl(实测耗时台账)
@@ -461,6 +590,11 @@ python render.py ... --stage qa --strict-gates # 交付前:skipped 也算失
461
590
  python render.py ... --stage accept --gate stills --note "看过联系表" # 人工签核
462
591
  ```
463
592
 
593
+ **报告头部有「运行编号」与「输入指纹」两行**(v0.8):前者是 `--run-id`(不给就按时间戳生成),
594
+ 后者是 script / scenes / words / filmkit 四个内容的指纹。同一部片跑了好几版之后,
595
+ "上一版的门禁结论到底是什么"就靠 `work/runs/<runId>/` 里的快照回答(只归档小文件,
596
+ 视频/音频/分块就地保留)。
597
+
464
598
  **四道人工签核闸**(`style` 风格与声线 / `stills` 母版静帧 / `facts` 上屏数字 / `release` 成片验收):
465
599
 
466
600
  - 签核前会检查"证据产物存在"(如 `stills` 要求 `work/qa/sheet.png`),**没有证据不许签**——
@@ -489,6 +623,23 @@ python scripts/stagewatch.py --work work --json
489
623
  下次该 stage 开始前重算上游输入与自己的产物指纹。**默认只告警**(`--strict-stale` 升级为失败,
490
624
  `--allow-stale` 显式放行并留痕)。改一句要重跑什么,见 [references/change-impact.md](references/change-impact.md)。
491
625
 
626
+ **v0.8 的四道新门禁**(账本 id:`G0` / `G10` / `G11` / `G12`,报告里按记的顺序排在 `G4` 前后):
627
+
628
+ | 门禁 | 查什么 | 三档语义(`pass` / `fail` / `skipped`) |
629
+ |---|---|---|
630
+ | `G0` 渲染前资源预检 | 剧本结构、全部上屏文本逐字过字形检测(按字段选字重)、`work/tts/*.mp3` 能否被 ffprobe 解析、字体候选能否加载、web 入口页是否存在 | 有 error -> `fail`;只有 warning -> `pass`(warning 仍进账本);没拷 `preflight.py` -> `skipped` |
631
+ | `G10` 词级时间戳 | `work/words.json` 的覆盖率/单调性/末词与首词偏差 | 判据不过 -> `fail`;`words=false`、没有 `words.json`、词事件为空 -> `skipped`(写明原因) |
632
+ | `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` |
633
+ | `G12` ASR 回读 | 成片音轨转文字,按 timeline 每段窗口算**字符级多重集召回**(中文没有词边界,字符级才成立;多重集是因为"是"这种字要计次数) | 召回 < 0.72 -> `fail`;[0.72, 0.85) -> `pass` + warning;两个引擎都不可用 / 没装 `qaasr.py` / 显式关闭 -> `skipped` |
634
+
635
+ **为什么 G11 值得全片跑**:锐度比只在 4 个时刻采样,能发现"整体发虚",发现不了"某 30 秒里字幕糊了"。
636
+ VMAF 是唯一覆盖整条时间轴的客观感知指标。代价是**每片多渲一遍参考帧**(29 s 片实测 14.2 s 全跑完;
637
+ > 机器被其它任务占满时同一份代码测到过 28.8 s,同机不同负载差近 2×,只认"这一跑"的数字)。
638
+ **为什么 G12 值得跑**:容器/响度/帧级质检全都默认"音轨里念的就是剧本那几句"——缓存串了、漏句、被压没了,
639
+ 从波形和容器上都看不出来。代价约 6 s/片(small 模型,CPU)。
640
+
641
+ G0 / G11 / G12 三个新模块(外加 `G10` 词级)的实测数据、阈值来源与关闭方式见 [references/v08-quality.md](references/v08-quality.md)。
642
+
492
643
  **质检分三层**,一层比一层更接近"观众真的看到了什么":
493
644
 
494
645
  | 层 | 查什么 | 工具 | 抓得住什么 |
@@ -509,7 +660,7 @@ python scripts/stagewatch.py --work work --json
509
660
 
510
661
  | 判据 | 阈值 | 失败怎么办 |
511
662
  |---|---|---|
512
- | 分辨率/帧率 | 严格等于设定值 | 检查 FrameSink 参数;`strict=True` 会直接报错而不是静默缩放 |
663
+ | 分辨率/帧率 | 严格等于**交付规格**(v0.8:`timeline.video` 的 out_w/out_h/fps;缺省回落 1080p/30) | 检查 `FrameSink` 参数与 `film.json` 的 `video` 块是否一致;`strict=True` 会直接报错而不是静默缩放 |
513
664
  | 音轨缺失 | 必须有音轨 | mux 参数漏了 `-i audio.wav` |
514
665
  | 时长 | 与 `frames/FPS` 差 < 0.5 s | 断点续渲漏帧;分块模式检查缺口检测 |
515
666
  | **真峰** | **> -1.0 dBTP**(-1.0 ~ -1.4 之间只 warn) | 混流前目标设 -1.5(AAC 会把真峰抬高 ~0.9 dB) |
@@ -519,6 +670,10 @@ python scripts/stagewatch.py --work work --json
519
670
  | **音画同步(v0.3)** | 判到超差(`dt > 60 ms`)即 error;**但默认可能根本没在查**(见下方两小节) | 要给 cue 写 `visual_at`,或设 `QA_REQUIRE_SYNC=1` 把"没查"也变成 error |
520
671
  | **字幕条数** | **= 剧本句数** | 少一条就是静默丢字幕,查 `sub_window` 与 `write_srt` |
521
672
  | **时长(时间层复核)** | 与预期差 > 0.5 s | `qa_frames` 会独立再查一次时长 |
673
+ | **渲染前资源预检(v0.8,G0)** | 有任何 error(缺字形/坏 TTS 音频/字体全不可加载/web 入口页不存在) | 按报错里的「字符 + 字段 + 句 id」改:换说法、改用矢量画、`--force-tts` 重合成。**这一步在渲第一帧之前**,改起来最便宜;`--no-preflight` 可跳过(会在 QC 报告留痕,不算通过) |
674
+ | **词级时间戳(v0.8,G10)** | 词覆盖率 < 45%、词偏移非单调、末词终点离句尾 > 0.40 s、首词起点离句首 > 0.20 s、或有句子一条词事件都收不到 | 多为 edge-tts 服务端行为变了或该句被裁坏了:先看 `work/words.json` 的 `stats.empty`,再 `--force-tts` 重采;长期不可靠就把 `"words": false`(退回整句字幕,G10 记 skipped) |
675
+ | **全片 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(没有可重渲的参考帧) |
676
+ | **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) |
522
677
 
523
678
  #### warning 档(不阻断,但别当看不见)
524
679
 
@@ -607,6 +762,84 @@ v0.4 给了两条真正能用的路:
607
762
  两条都没做时,`deep.json` 里 `sync` 恒为 `[]`,同时会有一条 warning 明说"同步对照跳过"。
608
763
  **看到 `sync: []` 先确认是"真的同步"还是"根本没查"**——`warnings` 里有没有那句话就是判据。
609
764
 
765
+ ## 7.5 v0.8 交付生产线:画幅 / 批量 / 预览 / 运行版本化
766
+
767
+ ### 7.5.1 画幅与帧率(只做 16:9 横屏)
768
+
769
+ ```jsonc
770
+ "video": { "width": 1280, "height": 720, "fps": 24 } // 不写 = 1920x1080@30(v0.7 行为)
771
+ ```
772
+
773
+ | 规则 | 实际行为 |
774
+ |---|---|
775
+ | 内部构图 | **恒为 1920x1080**;输出更小时在编码阶段用 ffmpeg `scale`(lanczos)下采样(`FrameSink(out_w=, out_h=)`) |
776
+ | 允许的输出尺寸 | 1920x1080 / 1280x720 / 960x540;2560x1440 在"允许列表"里但**会被上采样规则拒掉**(见下) |
777
+ | 允许的 fps | 24 / 25 / 30 / 50 / 60 |
778
+ | 竖屏 | **直接报错**。安全区、版式几何、HUD、字幕折行全部按 16:9 的像素坐标写死,竖屏是另一套版式 |
779
+ | 非 16:9 | **直接报错**(换宽高比要重做版式几何与字幕安全区,不是把宽高换一下) |
780
+ | 上采样 | **直接报错**。输出大于 1920x1080 = 把 1080p 包装成"更高分辨率"(假 4K);要真高清得改内部构图 |
781
+ | 非法 fps | **直接报错**(报错里列出可用值) |
782
+
783
+ 链路:`filmkit.video_plan(script)` 在 `layout` 阶段就解析并校验(错了不拖到渲帧),
784
+ 结果写进 `timeline.json` 的 `video` 块;`G4` 容器门禁改成**按交付规格**比对(不是恒看 1920x1080);
785
+ `encode_quality` 的参考帧会先缩到交付分辨率再比锐度。
786
+
787
+ > **跨分辨率别比锐度比**:同一片 1080p 交付实测 1.009、720p 交付实测 1.206——数值不可横向比较
788
+ > (参考帧也被缩到交付分辨率,笔画占画面的比例变了)。它只用来判"这一版有没有比上一版糊",别拿它当"720p 更清晰"。
789
+ >
790
+ > 720p 交付已实测通过全部门禁:`G4 = 1280x720@24`、`G11 = VMAF 97.10 / SSIM 0.9925`(696 帧,7.2 s)。
791
+ > `filmkit.qa_report` 为此新增 `expect_w/expect_h/expect_fps`——容器门禁比的是**交付规格**,
792
+ > 不是死比 1920x1080;改规格时记得让 `timeline.video` 与之一致(qavmaf 会核对两者是否同源)。
793
+
794
+ ### 7.5.2 批量生产(`--batch`)
795
+
796
+ ```jsonc
797
+ // films.json —— 两种写法都接受
798
+ ["a/film.json", "b/film.json"]
799
+ {"films": [{"script": "a/film.json", "scenes": "a/scenes.py",
800
+ "work": "a/work", "out": "out/a.mp4", "label": "A"}]}
801
+ ```
802
+
803
+ ```bash
804
+ python render.py --batch films.json
805
+ ```
806
+
807
+ | 行为 | 细节 |
808
+ |---|---|
809
+ | 上限 | **100 条 / 1 MiB**(清单本身不该是个大 JSON);超了直接报错让你拆份 |
810
+ | 开跑前 | **全量 `filmcheck` 预检**:任一条不通过就**一条都不跑**,把明细一次性报全(`batch-report.json` 里 `phase="preflight"`) |
811
+ | 执行 | 逐条起**子进程**渲染(每条都是完整的 `--stage all`),**单条失败不阻断其余** |
812
+ | 收尾 | `batch-report.json`(`phase="render"`,含每条 `exit` / `wall_s` / 日志尾)+ 退出码:全成功 `0`,**部分失败 `1`** |
813
+ | 预检失败的退出码 | 也是 `1`(`SystemExit` 带文字,Python 的默认行为);区别看 `batch-report.json` 的 `phase` |
814
+
815
+ 实测(本机,2 条 29 s 模板片,v0.8 早期一轮):**67.2 s / 60.1 s**,各 `pass 11 · fail 0 · skipped 1`。
816
+ 注意每条都要付 G11 的参考帧成本——批量不是"摊销",是线性叠加。**而且瓶颈已经转到 qa**:
817
+ 单条全链实测 47.3 s 里 `qa` 占 23.9 s(见 §1 的时间表),批量时这个比例会原样复制。
818
+
819
+ ### 7.5.3 预览片(`--preview N`)
820
+
821
+ ```bash
822
+ python render.py ... --preview 15 # 只渲前 15 秒
823
+ ```
824
+
825
+ - 产物:`out/preview/<片名>_preview.mp4` + 同名 `.gif`(480 px / 10 fps,两遍调色板生成)。
826
+ - 音轨:有 `work/audio.wav` 就**先截到预览时长再混入**。直接拿全长音轨去配短片,`mux` 会判
827
+ "音视频时长差 ≥ 0.05 s" 而失败(v0.8 开发期实测踩过);没有音轨就出无声版,不阻断。
828
+ - **不落交付账本**:它是给人快速看效果的,不是交付证据;正式结论仍以 `--stage qa` 的报告为准。
829
+
830
+ ### 7.5.4 运行版本化(`--run-id` + `work/runs/`)
831
+
832
+ ```bash
833
+ python render.py ... --run-id hero-v3 # 不给就按时间戳生成 rYYYYMMDD-HHMMSS
834
+ ```
835
+
836
+ - `work/gates.json` 增 `runId` 与 `inputs`(**script / scenes / words / filmkit** 四个内容指纹)。
837
+ 为什么要逐项指纹:`contentHash` 只说"变了没",说不出"变了哪个";批量出片最常见的事故就是
838
+ "这一条用的还是上一版的词级时间戳"。
839
+ - 每次收尾(`gates.archive_run`)把**账本快照 + QC-REPORT.md + run.json** 复制到
840
+ `work/runs/<runId>/`。只归档小文件;视频/音频/分块体积大,就地保留。
841
+ - `out/QC-REPORT.md` 头部多两行「运行编号」「输入指纹」,交付时一眼能对上是哪一版跑的。
842
+
610
843
  ## 8. 常见故障速查
611
844
 
612
845
  | 现象 | 根因 | 修法 |
@@ -646,7 +879,9 @@ v0.4 给了两条真正能用的路:
646
879
  - `references/sfx-design.md` — **(v0.3)音效设计**:物理拆解流程、配方表、听感诊断表、QC 规则
647
880
  - `references/web-engine.md` — **(v0.3)浏览器引擎**:页面契约、确定性时间轴、分块渲染、崩溃重开
648
881
  - `references/frame-qa.md` — **(v0.3)帧级质检**:单帧闪烁判据、画面空窗、音画同步、报告怎么读
649
- - `references/quality-gates.md` — G0–G6 全部门禁、可复制代码、验收清单
882
+ - `references/quality-gates.md` — 方法论 G0–G8 全部门禁、可复制代码、验收清单(文内另有「两套编号别混用」的映射说明:账本是 `G0`/`G4–G12`)
883
+ - `references/word-timeline.md` — **(v0.8)词级时间轴**:词级契约、句时长为何仍是权威、词锚点写法与退化路径、四个真实坑
884
+ - `references/v08-quality.md` — **(v0.8)三道新门禁**:VMAF/SSIM、ASR 回读、渲前预检的判据、阈值来源、实测数据与关闭方式(词级 G10 见 `word-timeline.md`)
650
885
  - `references/change-impact.md` — **(v0.6)改动影响表**:改了 text/sub/hold/style/scenes.py 各要重跑什么
651
886
  - `references/style-packs.md` — **(v0.5)风格包**:主题/配乐/声线字段、三级指纹规则、加第 9 套风格的步骤
652
887
  - `references/pitfalls.md` — 20 条最佳实践 + 16 条反模式 + 3 个非确定性 bug 复盘
@@ -656,11 +891,14 @@ v0.4 给了两条真正能用的路:
656
891
  | 文件 | 作用 |
657
892
  |---|---|
658
893
  | `scripts/filmkit.py` | 生产内核:字体/字形自查/缓存排版/字幕安全区/缓动/确定性随机/音频总线/**CueBus 提示点**/**立体声混音 mix_film**/TTS/时间轴/**hold 停留窗口**/FrameSink/合成/质检 |
659
- | `scripts/render.py` | 编排器:**13 个 stage**(含 `accept` / `explain`)、两种引擎(`--engine pil\|web`)、分块并行(`--jobs`)、断点续跑(`--resume`)、母版预览(`stills`) |
894
+ | `scripts/render.py` | 编排器:**13 个 stage**(含 `accept` / `explain`)、两种引擎(`--engine pil\|web`)、分块并行(`--jobs`)、断点续跑(`--resume`)、母版预览(`stills`)。v0.8 增:`--batch` 批量、`--preview N` 前 N 秒试片、`--run-id` 运行标签(`work/runs/<runId>/`)、`--vmaf full\|sample\|off`、`--no-preflight` |
660
895
  | `scripts/gates.py` | **(v0.6)门禁账本**:pass / fail / skipped 三档、`out/QC-REPORT.md` 渲染、人工签核(绑内容指纹)、原子写 |
661
896
  | `scripts/stagewatch.py` | **(v0.6)阶段新鲜度**:每 stage 记录输入/输出指纹到 `work/manifest.json`;上游一改,下游报过时并给出最小重跑命令 |
662
897
  | `scripts/filmcheck.py` | **(v0.6)film.json 契约校验**:未知键提示"是不是想写 X"(以前静默忽略)、枚举与范围、跨字段(cue 引用的 id 必须存在) |
663
- | `scripts/selfcheck.py` | 回归自检:**43 条**"曾真的坏过"的断言,不联网 20 秒内出结论(可选模块缺失标 SKIP) |
898
+ | `scripts/selfcheck.py` | 回归自检:**55 条**"曾真的坏过"的断言,不联网 20 秒内出结论(可选模块缺失标 SKIP)。v0.8 新增 8 条:44 词级时间戳 / 45 画幅规划 / 46 金帧确定性 / 47 依赖漂移体检 / 48 批量清单解析 / 49 TTS 后端可替换 / 50 web 风格载荷(legacy 必须回落)/ 51 金帧回归(调 `tools/golden_frames.py`)。v0.9 新增 4 条:52 场景语法(10 类分流/确定性/段长门)/ 53 视觉语言包(8 套签名无重复)/ 54 场景类型单真值(4 处集合相等)/ 55 剪影可区分(调 `tools/silhouette_qa.py`) |
899
+ | `scripts/preflight.py` | **(v0.8)渲染前资源闸门**:剧本结构 + 全部上屏文本逐字过字形检测(按字段选字重)+ 已有 TTS 音频可解析 + 字体可加载 + web 入口页存在。`render.py` 在 `frames` 之前调用,记门禁 `G0`;也可单独跑 `python preflight.py --script film.json [--json] [--strict]` |
900
+ | `scripts/qavmaf.py` | **(v0.8)全片 VMAF/SSIM 门禁**:参考帧由场景函数**实时重渲**(不是成片抽帧),rawvideo 喂 ffmpeg 与成片逐帧比;一次调用同时算 VMAF 与 SSIM;门禁 `G11` |
901
+ | `scripts/qaasr.py` | **(v0.8)ASR 回读门禁**:成片音轨转文字与剧本对账(字符级多重集召回 + 每句偏移);引擎优先 faster-whisper,其次 ffmpeg whisper 滤镜,都不可用记 skipped;门禁 `G12` |
664
902
  | `scripts/sfxkit.py` | **(v0.3)物理建模音效库**:模态合成/纹理/空间/动力学/母带 + 24 个配方 + `CUE_ALIASES` |
665
903
  | `scripts/scorekit.py` | **(v0.3)程序化配乐**:和弦表/分段能量/律动层/吸气/自动电平,输出 `music.wav` |
666
904
  | `scripts/webengine.py` | **(v0.3)浏览器逐帧渲染**:Edge/Chromium + CDP 截图、分块并行、崩溃重开、cue 导出、文字质检 |
@@ -669,6 +907,7 @@ v0.4 给了两条真正能用的路:
669
907
  | `assets/film.json` | 剧本模板(含 `hold`/`bpm`/`music`/`sfx.cues`/`engine` 等新字段) |
670
908
  | `<REPO>/schemas/film.schema.json` | **(v0.6)film.json 的契约**(JSON Schema draft-07 子集)。校验器见 `scripts/filmcheck.py`,模板由 `tools/ci_check.py` 第 12 项守着 |
671
909
  | `<REPO>/tools/estimate.py` | **(v0.6)开渲前预演**:帧数 / 各阶段耗时 / 磁盘峰值 / 分块建议,`--json` 可机读。基准常量集中在文件顶部,换机请重测后回填 |
910
+ | `<REPO>/tools/golden_frames.py` | **(v0.8)金帧像素回归**:渲 3 帧算 SHA-256 与 `assets/golden_frames.json` 比对(`--write` 重录 / `--check` 校验)。同环境像素变了是 fail,换了机器(字体指纹不同)是 skipped——跨机不误报 |
672
911
  | `assets/web/` | **(v0.3)浏览器页面模板**:原生 Canvas2D 确定性时间轴、场景示范、文字质检脚本 |
673
912
  | `requirements.txt` | **(v0.4)Python 依赖清单**:必需(numpy/Pillow/edge-tts)/ 强烈建议(cv2)/ 可选(scipy、playwright)三档,带注释说明"缺了会怎样"。`pip install -r requirements.txt` 即可 |
674
913
 
@@ -697,6 +936,9 @@ v0.4 给了两条真正能用的路:
697
936
  6. `python selfcheck.py` 的 PASS 计数
698
937
  7. **(v0.3)帧级深检的三个数**:单帧闪烁 N 条 / 画面空窗 N 段 / 音画同步超差 N 条
699
938
  (有 web 引擎时再加页面层的 文字重叠 / 出画 / 一闪而过 / 从没露面 四项)
939
+ 8. **(v0.8)新门禁的数**:G10 词级(N 句 / M 词、末词偏差)、G11 的 **VMAF 与 SSIM**(外加用了多少帧)、
940
+ G12 的**字符召回**。这三项若在报告里是 `skipped`,要说明为什么没跑(没拷模块 / web 引擎 / 显式关闭),
941
+ **不能只字不提**。
700
942
 
701
943
  > **v0.6 起不要口头复述这些数字**:`out/QC-REPORT.md` 是机器生成的交付凭据,
702
944
  > 照抄报告里的"实测值"即可;报告里的 `skipped` 项必须逐项说明为什么没执行。
@@ -707,7 +949,10 @@ v0.4 给了两条真正能用的路:
707
949
  > 容器:1920x1080 / 30 fps / h264+aac / 48 kHz / 时长 30.10 s / 1.00 Mbps
708
950
  > 信号:-16.0 LUFS / 真峰 -1.2 dBTP / 锐度比 1.01 / 黑帧 0 / 字幕 4 条 = 剧本 4 句
709
951
  > 时间:单帧闪烁 0 / 画面空窗 0 / 音画同步超差 0(本条为 web 引擎或写了 visual_at 时才有意义)
710
- > 自检:selfcheck PASS 43 / FAIL 0 / SKIP 0;tools/ci_check.py PASS 12 / FAIL 0
952
+ > 词级:4 句 / 64 词 / 末词偏差最大 0.09 s(G10)
953
+ > 画质:VMAF 97.22 / SSIM 0.9928 / 870 帧(G11,参考帧由场景函数重渲)
954
+ > 回读:字符召回 0.803(G12,faster-whisper small;中英混排专名会掉分,< 0.72 才 fail)
955
+ > 自检:selfcheck PASS 55 / FAIL 0 / SKIP 0;tools/ci_check.py PASS 12 / FAIL 0
711
956
  > 音频:work/score.json 的 variation 非 0(指纹参与了结构变奏)、audio_similarity --strict 全 PASS
712
957
  > 目检:work/qa/sheet.png,四帧分别为开场星射线、五步成片卡片、画质三件事卡片、收尾居中大字;
713
958
  > 字幕黑底白字不压正文,大字边距均匀,无空洞帧。
@@ -769,7 +1014,7 @@ Pillow 路线: 墙钟秒数 ≈ 帧数 / 40 / 并行度 + 30% 余量
769
1014
 
770
1015
  ## 13. 升级须知(从旧版迁移要动的地方)
771
1016
 
772
- ### 13.0 v0.4 → 当前版本(v0.5 / v0.6 / v0.7 / v0.7.1 / v0.7.2 要做什么)
1017
+ ### 13.0 v0.4 → 当前版本(v0.5 / v0.6 / v0.7 / v0.7.1 / v0.7.2 / v0.8.0 / v0.8.1 要做什么)
773
1018
 
774
1019
  **一句话**:v0.4 的老项目**不写新字段就能继续跑**(不写 `style` = legacy 行为,配乐逐样本不变),
775
1020
  但**工作目录要多拷文件**,且有两处默认值变了要重新听。按版本逐条过:
@@ -781,9 +1026,21 @@ Pillow 路线: 墙钟秒数 ≈ 帧数 / 40 / 并行度 + 30% 余量
781
1026
  | v0.6 → **v0.7** | 想吃到「配乐/音效随内容变化」就保持 `style: "auto"`,**不要**写死 `music.seed` 或关掉 `seed_from_content`;音效想更丰富就把提示点写进 `scenes.py` 的 `cues()` | CHANGELOG 0.7.0「升级须知(v0.6 → v0.7)」 |
782
1027
  | v0.7 → **v0.7.1** | 配乐基线默认值从 `music.gain_db=-1.5` 变成 **-12**(模板也已改),有人声处再压配乐 6 dB / 音效 3 dB;**之前手调过 `gain_db`/`duck_db` 的片子要重新听一遍**(现在背景整体更低)。显式写 0 可关掉对应侧链 | CHANGELOG 0.7.1「修了什么」+ `assets/film.json` 的 `music` 默认值 |
783
1028
  | v0.7.1 → **v0.7.2** | `QC-REPORT.md` 现在**只落在 `--out` 的目录**(以前 `accept` 分支按 `work` 的兄弟目录算,`--out deliver/film.mp4` 时签核会更新到另一份没人交付的文件)。如果你用 `--out` 指到了非 `out/` 的目录,交付时请看新位置;旧的 `out/QC-REPORT.md` 不会被再写 | `render._report_path()` 的 docstring + `selfcheck` 第 40 条(本轮新增) |
1029
+ | v0.7.2 → **v0.8.0** | ① 新工作目录**多拷三个文件**:`scripts/preflight.py`、`scripts/qavmaf.py`、`scripts/qaasr.py`(都是**可选**:不拷对应门禁记 `skipped` 并写明原因,不会让渲染失败——但 `skipped != 通过`)。② **默认行为变了**:`film.json` 的 `words` 默认 **true**,老项目第一次跑 `--stage tts` 会**把每句重新联网合成一次**(老缓存没有词级记录);不想付这次网络成本就写 `"words": false`。③ **qa 变慢**:全片 VMAF 每片多渲一遍参考帧(29 s 片实测 14.2 s),ASR 8.7 s——全链从约 28 s 涨到 **47.3 s**,qa(23.9 s)已超过渲染本身(12.5 s)。④ **输出仍默认 1080p@30**:不写 `video` 块行为与 v0.7 完全一致。⑤ 自检 43 → **51** 条,三档门禁多了 `G0/G10/G11/G12` 四行 | CHANGELOG 0.8.0「升级须知(v0.7.2 -> v0.8.0)」+ 本节下方逐条说明 |
1030
+ | v0.8.0 → **v0.8.1** | ① **删一次 `work/segments`**:`--resume` 的分块指纹新增 timeline 内容,旧计划会判"已变化"。② **退出码变严**:`--stage style/stills/frames/mux` 的门禁 fail(或 `--strict-gates` 下有 skipped)现在真的返回 1——靠退出码判定的交付脚本会开始"失败"。③ **`ci_check` 汇总变三档**:`PASS n / FAIL n / SKIP n`,用 `grep 'PASS 12'` 的脚本要改;要"没执行也算失败"加 `--strict`。④ **`filmcheck` 变严**:`video` 块走 `filmkit.video_plan`(1600x900 之类报 error),`qa.vmaf.enabled`/`qa.require_vmaf`/`qa.ssim.enabled` 三个惰性键报 WARN。⑤ **`webengine` 两处改成 fail-closed**:`style=None` + 720p 不再静默裁切、页面 FPS 与注入值不一致会报错。⑥ 金帧基线**不用重录**(像素逐位一致)。 | CHANGELOG 0.8.1「升级须知(v0.8.0 → v0.8.1)」+「行为变化」表 |
784
1031
 
785
1032
  > 查当前版本:`python <REPO>/tools/ci_check.py -v` 里的 version 断言、或 `cat package.json` 的 `version`。
786
- > 升级后建议按 §14 重跑一次自检(43 条),再按 §7 重新签核(改了代码/默认值,旧签核会自动失效,这是设计)。
1033
+ > 升级后建议按 §14 重跑一次自检(55 条),再按 §7 重新签核(改了代码/默认值,旧签核会自动失效,这是设计)。
1034
+ >
1035
+ > **v0.7.2 → v0.8.0 的具体迁移动作**(表里那一行的展开):
1036
+ > 1. 从 `<SKILL_DIR>/scripts/` 拷 `preflight.py` / `qavmaf.py` / `qaasr.py` 过来(不拷也能出片,只是三项门禁 `skipped`)。
1037
+ > 2. 决定要不要词级:默认 `words=true` 会让老项目**首次**重合成全部旁白(每句多一次词事件采集)。
1038
+ > 不想重合成、或线上环境不允许额外网络请求,就在 `film.json` 写 `"words": false`。
1039
+ > 3. 交付前预留时间:全片 VMAF 的参考帧要按场景函数重渲一遍(模板片 14.2 s),ASR 8.7 s/片;
1040
+ > 29 s 片全链 47.3 s,**qa 比渲染还贵**。
1041
+ > 着急出片用 `--vmaf off`,但请把 `skipped` 项在报告里说明白。
1042
+ > 4. 想要别的画幅/帧率才写 `video` 块;**默认输出仍是 1920x1080@30**,老项目不写就完全不变。
1043
+ > 5. 有历史交付脚本在 grep 输出的话:`PASS 43` → `PASS 51`,`webengine --selftest 24 项` → `35 项`。
787
1044
 
788
1045
  ### 13.1 v0.3.0 → v0.4.0
789
1046
 
@@ -834,10 +1091,10 @@ Pillow 路线: 墙钟秒数 ≈ 帧数 / 40 / 并行度 + 30% 余量
834
1091
 
835
1092
  ```bash
836
1093
  cd <SKILL_DIR>/scripts
837
- python selfcheck.py # 43 条断言;FAIL 就照提示修环境
1094
+ python selfcheck.py # 55 条断言;FAIL 就照提示修环境
838
1095
  python sfxkit.py --selftest # 音效库(可选模块,装了才跑)
839
1096
  python scorekit.py --selftest # 配乐
840
- python webengine.py --selftest # 浏览器引擎(24 项,会真起无头浏览器,约 15 秒)
1097
+ python webengine.py --selftest # 浏览器引擎(35 项,会真起无头浏览器,约 60 秒)
841
1098
  python qa_frames.py --selftest # 帧级质检(自己造测试视频)
842
1099
  ```
843
1100
 
@@ -0,0 +1,65 @@
1
+ {
2
+ "version": 1,
3
+ "generator": "golden_frames.py v0.8.0",
4
+ "comment": "金帧基线:字体指纹+环境指纹一致时,每一帧像素 sha256 必须相同;换机/字体/Pillow 版本不同判 skipped(不误报),同环境像素变了才是 fail。改动模板(film.json / scenes.py / stylekit 主题)后请 --write 重录并提交本文件。",
5
+ "canvas": [
6
+ 1920,
7
+ 1080
8
+ ],
9
+ "fps": 30,
10
+ "script": "skills/video-studio/assets/film.json",
11
+ "scenes": "skills/video-studio/assets/scenes.py",
12
+ "font_fp": "05c853d0eb28bcca3125d81c1ce27c9436f66a55",
13
+ "fonts": [
14
+ {
15
+ "name": "msyh.ttc",
16
+ "sha1": "e5a993b85dd99da4930bb8d7b9da53ae89fef3c6"
17
+ },
18
+ {
19
+ "name": "msyhbd.ttc",
20
+ "sha1": "e142fd01e96fea9365b5c6e053a0a1e0d309e218"
21
+ },
22
+ {
23
+ "name": "msyhl.ttc",
24
+ "sha1": "5444a8bab4b57604f6420ea4e60194f41459f9f8"
25
+ }
26
+ ],
27
+ "env_fp": "e4bca653965cf664adf46784abe93e8519df8d00",
28
+ "env": {
29
+ "python": "3.12.16",
30
+ "pillow": "13.0.0",
31
+ "numpy": "2.5.4",
32
+ "os": "Windows",
33
+ "platform": "Windows 11"
34
+ },
35
+ "inputs": {
36
+ "script_sha1": "2f375a7298ea861684a2d553cb6617ed4351d3e5",
37
+ "scenes_sha1": "aab9df135ce5a7a3f2750d275dfcdac48405d4a4",
38
+ "filmkit_sha1": "a582ad1a2b53cf1ae163ab3052d054bc196c7d49",
39
+ "stylekit_sha1": "4bcbce2048b89c66352455f6e82c0943cc2e0508",
40
+ "golden_tool_sha1": "17cdc5d64705d54c157cf86f76899393a5d0b8c4"
41
+ },
42
+ "frames": [
43
+ {
44
+ "frame": 36,
45
+ "scene": "l01",
46
+ "t_local": 1.2,
47
+ "label": "hook",
48
+ "sha256": "ee2a70067aeaee0c158c7eef6c26c1228354bf247f05f7330c991ef1946aaaae"
49
+ },
50
+ {
51
+ "frame": 270,
52
+ "scene": "l03",
53
+ "t_local": 1.6,
54
+ "label": "body",
55
+ "sha256": "ead954c01f5498782b3a31b8e42638d4f8f42e2faca97df476e0d78a1f9f72f8"
56
+ },
57
+ {
58
+ "frame": 369,
59
+ "scene": "l04",
60
+ "t_local": 1.5,
61
+ "label": "outro",
62
+ "sha256": "10463a35429185e7b816fd5ede7107d529e26f4ec8d8640fba84d9897cc77f66"
63
+ }
64
+ ]
65
+ }