@dsh-gis/dsh-video-studio-skill 0.4.0 → 0.6.0

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.
@@ -32,15 +32,22 @@ description: 用代码从零生成中文视频(科普片/纪录片/宣传片/
32
32
  mkdir -p myfilm && cd myfilm
33
33
  cp <SKILL_DIR>/assets/film.json . # 剧本模板(改文字即可)
34
34
  cp <SKILL_DIR>/assets/scenes.py . # 场景模板(改画面即可)
35
- cp <SKILL_DIR>/scripts/{filmkit.py,render.py,selfcheck.py} .
35
+ # v0.6.0 起 gates.py(门禁账本)与 stagewatch.py(阶段新鲜度)是**必需**的:
36
+ # render.py 直接 import 它们,少一个就是 ModuleNotFoundError。
37
+ cp <SKILL_DIR>/scripts/{filmkit.py,stylekit.py,render.py,selfcheck.py,gates.py,stagewatch.py} .
36
38
  # 可选(v0.3):要程序化音效/配乐就一并拷;要走浏览器渲染再拷 webengine.py + assets/web/
37
- cp <SKILL_DIR>/scripts/{sfxkit.py,scorekit.py,qa_frames.py} .
39
+ cp <SKILL_DIR>/scripts/{sfxkit.py,scorekit.py,voicekit.py,qa_frames.py} .
38
40
  cp -r <SKILL_DIR>/assets/web ./web
39
41
 
40
- # 2) 先跑一次自检:24 条回归护栏,20 秒内出结论(不联网;可选模块缺失会标 SKIP)
42
+ # 2) 先跑一次自检:32 条回归护栏,20 秒内出结论(不联网;可选模块缺失会标 SKIP)
41
43
  python selfcheck.py
42
44
 
43
- # 3) 改 film.json 的 lines[].text / sub / big / items
45
+ # 3) 【v0.5 新步骤】先确认风格,再改文字
46
+ # film.json 的 "style": "auto" 会按内容自动选一套(画风+配乐+旁白一起变);
47
+ # 想锁定就写死风格 id。这条命令打印"为什么选这套",并落盘 work/style.json。
48
+ python render.py --script film.json --stage style
49
+
50
+ # 3b) 改 film.json 的 lines[].text / sub / big / items
44
51
  # text = 朗读文本(自然口语);sub = 上屏字幕(短,一行优先)
45
52
 
46
53
  # 4) 母版预览(P6):先只渲几张关键帧看构图/配色,别一上来渲全片
@@ -49,17 +56,32 @@ python selfcheck.py
49
56
  python render.py --script film.json --scenes scenes.py --work work --stage stills \
50
57
  --stills "l01:3,l02:5,l03:2.5"
51
58
 
59
+ # 4.5) 【v0.6】先算成本再开渲:帧数 / 各阶段耗时 / 磁盘 / 分块建议
60
+ # 项目原则是"成本敏感",而在此之前开渲前拿不到任何数字。
61
+ # (--json 可机读;有 work/timeline.json 时用真实时长,否则按字数粗估并标注)
62
+ python <SKILL_DIR>/../../../tools/estimate.py --script film.json --jobs 4
63
+
52
64
  # 5) 一条命令出片(TTS→排版→混音→渲染→合成→质检)
53
65
  python render.py --script film.json --scenes scenes.py --out out/film.mp4 --work work
54
66
 
55
67
  # 6) 亲眼看质检抽出的帧
56
68
  # work/qa/sheet.png 联系表(全片调性)
57
69
  # work/qa/snap_*.png 全分辨率静帧
70
+
71
+ # 7) 【v0.6 新步骤】看机器可读的交付结论,然后签核
72
+ # out/QC-REPORT.md 是**交付凭据**:gate matrix 分 pass / fail / skipped 三档,
73
+ # skipped 表示"本次没执行",不等于通过。以前这些只活在 stdout 里,会话一断就没了。
74
+ cat out/QC-REPORT.md
75
+ python render.py --script film.json --work work --stage accept --gate stills # 看过静帧了
76
+ python render.py --script film.json --work work --stage accept --gate release # 成片验收过了
77
+ # 签核绑内容指纹:改了剧本 / scenes.py / 引擎,旧签核自动失效,要重新看、重新签。
78
+ # 交付前想强制:加 --strict-gates(skipped 也算失败)。
58
79
  ```
59
80
 
60
81
  分阶段跑(**每个 stage 只做自己的事**,不会再顺手重渲全片):
61
82
 
62
83
  ```bash
84
+ python render.py ... --stage style # 只解析风格 -> work/style.json(画风/配乐/旁白/节奏一次定死)
63
85
  python render.py ... --stage tts # 只合成旁白 + 实测时长(带输入校验的缓存)
64
86
  python render.py ... --stage layout # 只排时间轴 -> timeline.json / sub.srt / sub.ass(含 hold 停留窗口)
65
87
  python render.py ... --stage cues # 只出提示点 -> work/cues.json(声音跟着画面走)
@@ -70,9 +92,11 @@ python render.py ... --stage frames # 只渲染帧 -> silent.mp4(--engine pi
70
92
  python render.py ... --stage mux # 只合成(视频流 copy,两遍 loudnorm)
71
93
  python render.py ... --stage qa # 只质检(只读输入,只写 work/qa/)+ 帧级深检
72
94
  python render.py ... --stage stills # 只渲母版静帧 + 联系表
95
+ python render.py ... --stage accept --gate stills # 【v0.6】人工签核一道闸(写 work/signoff.json)
96
+ python render.py ... --stage explain # 【v0.6】只报"哪些阶段过时了、为什么、该跑什么",不跑任何 stage
73
97
  ```
74
98
 
75
- `--stage all` 的完整顺序:`tts → layout → cues → score → sfx → audio → frames → mux → qa`。
99
+ `--stage all` 的完整顺序:`style → tts → layout → cues → score → sfx → audio → frames → mux → qa`。
76
100
  **任何一步出问题都会在该步停下**,不会带着坏输入继续往下跑。
77
101
 
78
102
  长片渲染(分块并行 + 断点续跑):
@@ -90,12 +114,18 @@ python render.py ... --stage frames --jobs 4 --resume # 中断后只补缺块
90
114
  > 里存着计划指纹),并提示你 `rm -rf work/segments`——这是刻意的:旧分块配新时间轴会拼出
91
115
  > "画面和旁白对不上"的片子,而且时长门禁查不出来。
92
116
 
117
+ > **v0.6.0 修了一个已发布版本里的真 bug**:`--jobs >1` 时子进程从 `_script_snapshot.json` 读剧本,
118
+ > JSON 把主题色的 `(r,g,b)` 元组变成 `list`,而 `scenes._bg()` 拿它当缓存键 → `TypeError: unhashable type: "list"`。
119
+ > 也就是说 **"长片并行渲染"这条 SKILL 自己教的路径此前是崩的**。现在子进程会按剧本重解风格(拿回元组),
120
+ > `scenes.py` 的 `_bg` 键也做了冻结兜底;`selfcheck` 第 32 条专门盯这个。
121
+
93
122
  ## 2. 标准八阶段(P0–P7)
94
123
 
95
124
  | 阶段 | 产出 | 关键动作 | 常见错误 |
96
125
  |---|---|---|---|
126
+ | **P0.5 定风格** | `work/style.json` | `--stage style`:按内容选风格包,打印"为什么选这套" | 跳过 → 画风/配乐/旁白全片一个样(v0.4 的通病) |
97
127
  | **P0 定基准** | `CONVENTIONS.md` | 分辨率/帧率/时长/BPM→帧换算一次算死,全项目引用同一份 | 各文件自己写 `150` 这种魔数 → 幕边界对不齐 |
98
- | **P1 写剧本** | `film.json` | 每句拆 `text`(朗读)与 `sub`(字幕);每段一个 `kind` | 一个字段两头用 → 字幕太长或读起来生硬 |
128
+ | **P1 写剧本** | `film.json` | 每句拆 `text`(朗读)与 `sub`(字幕);每段一个 `kind`;定 `style`(auto 或风格 id) | 一个字段两头用 → 字幕太长或读起来生硬;不写 style → 每部片长一个样 |
99
129
  | **P2 事实核查** | `FACTS.md` | 上屏的每个数字/年份有出处编号;建"不能上屏"黑名单 | 跳过这步 → 画面和旁白数字打架,评论区第一条就是你错了 |
100
130
  | **P3 TTS** | `work/tts/*.mp3` + `tts.json` | edge-tts 逐句合成,`ffmpeg -f f32le` 实测时长,磁盘缓存 | 按字数估时长 → 字幕漂移 |
101
131
  | **P4 排时间轴** | `work/timeline.json` + `sub.srt` | `dur = lead + 实测语音 + tail`,句读必须在本段内 | 段太短 → 旁白被下一段截断 |
@@ -116,6 +146,10 @@ python render.py ... --stage frames --jobs 4 --resume # 中断后只补缺块
116
146
  > 上面两行是**本机空载**实测口径(换机、重负载画面都会变;详见 §12)。别把区间当保证。
117
147
  | 纯数据动画 | **manim**(若已安装) | 视场景而定 | 数学/公式推导 |
118
148
 
149
+ > **v0.5 注意**:风格包的主题化目前只覆盖 **PIL 引擎**(`engine: pil`,默认)。
150
+ > `--engine web` 的页面(`assets/web/`)仍是 v0.4 的深蓝橙配色,不跟随 `style`;
151
+ > 用 web 引擎时画风不会随内容变化,但配乐与旁白**照样**跟随风格包(它们不依赖渲染引擎)。
152
+
119
153
  **默认走 PIL**:依赖少(numpy+Pillow 就够)、启动快、确定性强、调试成本低。
120
154
  需要"一镜到底的粒子形变 / 实时着色器质感"才上浏览器方案——**v0.3 起它是真的可用了**,不再是"文档里说有":
121
155
 
@@ -207,9 +241,14 @@ python render.py ... --engine web --stage cues # 自动导出 work/cues.json
207
241
 
208
242
  ### 5.3 配乐(scorekit)
209
243
 
244
+ > **v0.5 起先看 §5.5。** 配乐的"身份"(调式/调性/BPM/进行/音色/律动)由风格包决定,
245
+ > 你通常只需要在 film.json 写 `"style": "auto"` 或一个风格 id,不用手调下面这些参数。
246
+
210
247
  ```json
211
- "bpm": 120,
212
- "music": {"enabled": true, "mode": "score", "key": "Dm", "gain_db": -1.5, "duck_db": 3.0,
248
+ "bpm": "auto",
249
+ "music": {"enabled": true, "mode": "score", "style": "auto", "mood": "auto",
250
+ "key": "auto", "seed": 7, "seed_from_content": true,
251
+ "gain_db": -1.5, "duck_db": 3.0,
213
252
  "sections": [{"at": "l01", "name": "intro", "energy": 0.3},
214
253
  {"at": "l03", "name": "drive", "energy": 0.8}]}
215
254
  ```
@@ -251,7 +290,58 @@ python -c "import sfxkit; print(sfxkit.render('whoosh', dur=1.2).shape)"
251
290
  它的定位是**环境声**:基准电平要比同刻配乐低 10 dB 以上。
252
291
  3. **音量一律在成片混音里量**,不要按配方内部音量推断——混音时每个音效会先按峰值归一,配方里的"响"是没用的。
253
292
 
254
- ### 5.5 混音链(照着做,别自创)
293
+ ### 5.5 风格包:画风 + 配乐 + 旁白一起变(v0.5 新增)
294
+
295
+ **这是 v0.5 最重要的一节。** v0.4 及以前的通病是"内容换了、气质没换":
296
+ 画风锁死在 scenes.py 的深蓝橙、配乐锁死在 Dm/120、旁白锁死在同一个 voice。
297
+ 用户的原话是「背景音乐风格都差不多、旁白声音都差不多、整体画风也差不多」。
298
+
299
+ v0.5 引入 **风格包**:一个 `style` id **同时**决定画风、配乐、旁白声线和节奏。
300
+
301
+ ```json
302
+ {"style": "auto"}
303
+ ```
304
+
305
+ - `"auto"` = 按标题/章节/正文关键词自动选;零命中时按内容指纹稳定落桶
306
+ (同一份稿子永远同一个风格,不同稿子大概率不同)。
307
+ - 写死 id 就锁定:`tech-deep` / `magazine` / `paper` / `vivid` / `mono` / `cinema` / `lo-fi` / `epic`。
308
+ - **不写 `style` 字段 = `legacy`**:画风/配乐/旁白全部等于 v0.4,配乐逐样本一致(老项目无痛)。
309
+
310
+ 查表与环境:
311
+
312
+ ```bash
313
+ python stylekit.py --list # 8 套风格(含画风/配乐/旁白/节奏与对比度)
314
+ python stylekit.py --show cinema # 单套风格的完整字段
315
+ python render.py ... --stage style # 用某份 film.json 解析风格并打印"为什么选这套"
316
+ ```
317
+
318
+ | 风格 | 画风 | 配乐 | 旁白 |
319
+ |---|---|---|---|
320
+ | tech-deep | 深蓝渐变 + 橙 | 小调 100–124 / 琶音 + 铃声 | 男声叙述 + 青年开场 |
321
+ | magazine | 米白 + 朱红,衬线 | 大调 88–100 / 电钢 | 女声叙述 + 权威引用 |
322
+ | paper | 牛皮纸 + 靛青,楷体 | 大调 84–92 / 马林巴 | 软糯女声 + 男声引用 |
323
+ | vivid | 深紫 + 柠檬黄,大字 | 大调 120–132 / 短促和弦 | 活泼女声开场 |
324
+ | mono | 纯白 + 黑,超宽字距 | 极简 60–68 / 钢片琴 | 沉稳男声 + 女声引用 |
325
+ | cinema | 近黑 + 青橙,扫描线 | 小调 76–84 / 弦乐铺底 | 纪录片男声 |
326
+ | lo-fi | 暖灰 + 薄荷 | 大调 72–84 / 电钢 + 轻鼓 | 温柔女声 |
327
+ | epic | 近黑 + 金,衬线 | 小调 88–96 / 铜管 + 人声铺 | 有力男声 |
328
+
329
+ **片内也要有呼吸**(不是整片一个死样子):
330
+
331
+ - 版式按章节轮换(同一章内不变):left-stack / center-stack / split-right;
332
+ - 镜头缓推、入场位移、条目落位步长都由主题决定(`item_stagger` 与 `cues()` 共用同一个值,音效不会错位);
333
+ - 旁白按 kind 换语气档(hook/body/quote/outro/emphasis/calm),引号句会自动换声线;
334
+ - 配乐逐小节有能量弧线、配器渐入渐出、主奏轮换。
335
+
336
+ **三条硬约束**(违反会让片子"花"或者被门禁拦):
337
+
338
+ 1. 同一部片**不许跨明暗族**换主题(深蓝段直接切暖白段会精确触发闪烁门禁,观众也看着像换片)。
339
+ 2. 主题必须过 `stylekit.theme_lint` 的对比度体检(ink≥7 / muted≥4.5 / dim≥3 / accent≥4.5)。
340
+ 3. 旁白全片最多 2–3 把声音(`voice.max_distinct`),相邻句不换人——否则像广播剧而不是旁白。
341
+
342
+ 字段、指纹规则、加第 9 套风格的步骤见 **references/style-packs.md**。
343
+
344
+ ### 5.6 混音链(照着做,别自创)
255
345
 
256
346
  ```
257
347
  旁白(单声道, 居中) ─┐
@@ -293,10 +383,59 @@ python -c "import sfxkit; print(sfxkit.render('whoosh', dur=1.2).shape)"
293
383
 
294
384
  ```bash
295
385
  python render.py ... --stage qa # 只读质检:分辨率/音轨/时长/响度/真峰/锐度比/黑帧/字幕条数 + 帧级深检
296
- python selfcheck.py # 技能自身回归(24 条曾真的坏过的点)
386
+ python selfcheck.py # 技能自身回归(32 条曾真的坏过的点)
297
387
  python qa_frames.py out/film.mp4 --cues work/cues.json # 单独跑帧级深检,出 JSON + 图
388
+ # v0.6:qa / all 收尾会自动写 out/QC-REPORT.md(机器可读交付结论)
389
+ # 并落 work/gates.json(三档 gate matrix)与 work/cost.jsonl(实测耗时台账)
390
+ ```
391
+
392
+ ### 7.0 交付凭据:`out/QC-REPORT.md` 与人工签核(v0.6.0)
393
+
394
+ > 这一节解决的是"**质检结论只活在控制台里**"——会话一断,"我查过了"就无从复现。
395
+
396
+ **三档状态**(这是最关键的语义,不许混):
397
+
398
+ | 状态 | 含义 | 退出码 |
399
+ |---|---|---|
400
+ | `pass` | 本次**真的查了**且通过 | 0 |
401
+ | `fail` | 查了没过 | 非 0(除非 `--no-gate` 绕过,且会留痕) |
402
+ | `skipped` | 本次**没有执行**(没装 cv2 / 没写 visual_at / 没签核) | 默认 0,`--strict-gates` 下非 0 |
403
+
404
+ **"skipped ≠ 通过"** 是这套机制的全部意义:以前"没查"和"查了没事"在输出上都叫"没报错"。
405
+
406
+ ```bash
407
+ cat out/QC-REPORT.md # gate matrix + 实测值 + 签核 + 耗时,交付时一起给
408
+ python render.py ... --stage qa --strict-gates # 交付前:skipped 也算失败
409
+ python render.py ... --stage accept --gate stills --note "看过联系表" # 人工签核
298
410
  ```
299
411
 
412
+ **四道人工签核闸**(`style` 风格与声线 / `stills` 母版静帧 / `facts` 上屏数字 / `release` 成片验收):
413
+
414
+ - 签核前会检查"证据产物存在"(如 `stills` 要求 `work/qa/sheet.png`),**没有证据不许签**——
415
+ 否则签核就是橡皮图章。
416
+ - 签核绑**内容指纹**(剧本 + `scenes.py` + 引擎)。改了其中任何一个,旧签核**自动失效**,
417
+ 报告里会标成 `skipped` 并写明"内容已变"。这不是麻烦,是防止"我签过了"变成空头支票。
418
+ - 逃生阀是 `--no-gate`(整体跳过签核检查),但会写进 QC 报告的"本次绕过的门禁"一节,
419
+ **留痕不等于通过**。
420
+
421
+ **耗时台账** `work/cost.jsonl`:每个 stage 一行实测墙钟(`--cost` 开启,`--stage all` 里自动记)。
422
+ 它是 SKILL §12 那张"排期公式"的对照——公式从来没被验证过,现在有数据了。
423
+
424
+ ### 7.0b 阶段新鲜度:别让旧产物混进新片(v0.6.0)
425
+
426
+ > 与 §1 的分块指纹同源:那里只修了 `frames` 一个点,其余 stage 此前是裸奔的
427
+ > (改了 `style` 后 `--stage audio` 仍读旧 `music.wav`;改了 `hold` 后 `--stage frames` 仍用旧 `cues.json`,**都不报错**)。
428
+
429
+ ```bash
430
+ python render.py ... --stage explain # 哪些阶段过时了、为什么、最小重跑命令
431
+ python render.py ... --stage explain --json # 机读
432
+ python render.py ... --stage qa --strict-stale # 有过时就失败(CI / 交付前)
433
+ ```
434
+
435
+ 机制:每个 stage 结束往 `work/manifest.json` 记一行 `{stage, at, inputs:{名:指纹}, outputs:{名:指纹}}`;
436
+ 下次该 stage 开始前重算上游输入与自己的产物指纹。**默认只告警**(`--strict-stale` 升级为失败,
437
+ `--allow-stale` 显式放行并留痕)。改一句要重跑什么,见 [references/change-impact.md](references/change-impact.md)。
438
+
300
439
  **质检分三层**,一层比一层更接近"观众真的看到了什么":
301
440
 
302
441
  | 层 | 查什么 | 工具 | 抓得住什么 |
@@ -454,6 +593,7 @@ v0.4 给了两条真正能用的路:
454
593
  - `references/web-engine.md` — **(v0.3)浏览器引擎**:页面契约、确定性时间轴、分块渲染、崩溃重开
455
594
  - `references/frame-qa.md` — **(v0.3)帧级质检**:单帧闪烁判据、画面空窗、音画同步、报告怎么读
456
595
  - `references/quality-gates.md` — G0–G6 全部门禁、可复制代码、验收清单
596
+ - `references/change-impact.md` — **(v0.6)改动影响表**:改了 text/sub/hold/style/scenes.py 各要重跑什么
457
597
  - `references/pitfalls.md` — 20 条最佳实践 + 16 条反模式 + 3 个非确定性 bug 复盘
458
598
 
459
599
  ## 10. 脚本资产
@@ -461,14 +601,19 @@ v0.4 给了两条真正能用的路:
461
601
  | 文件 | 作用 |
462
602
  |---|---|
463
603
  | `scripts/filmkit.py` | 生产内核:字体/字形自查/缓存排版/字幕安全区/缓动/确定性随机/音频总线/**CueBus 提示点**/**立体声混音 mix_film**/TTS/时间轴/**hold 停留窗口**/FrameSink/合成/质检 |
464
- | `scripts/render.py` | 编排器:**10 个独立 stage**、两种引擎(`--engine pil\|web`)、分块并行(`--jobs`)、断点续跑(`--resume`)、母版预览(`stills`) |
465
- | `scripts/selfcheck.py` | 回归自检:**24 条**"曾真的坏过"的断言,不联网 20 秒出结论(可选模块缺失标 SKIP) |
604
+ | `scripts/render.py` | 编排器:**13 个 stage**(含 `accept` / `explain`)、两种引擎(`--engine pil\|web`)、分块并行(`--jobs`)、断点续跑(`--resume`)、母版预览(`stills`) |
605
+ | `scripts/gates.py` | **(v0.6)门禁账本**:pass / fail / skipped 三档、`out/QC-REPORT.md` 渲染、人工签核(绑内容指纹)、原子写 |
606
+ | `scripts/stagewatch.py` | **(v0.6)阶段新鲜度**:每 stage 记录输入/输出指纹到 `work/manifest.json`;上游一改,下游报过时并给出最小重跑命令 |
607
+ | `scripts/filmcheck.py` | **(v0.6)film.json 契约校验**:未知键提示"是不是想写 X"(以前静默忽略)、枚举与范围、跨字段(cue 引用的 id 必须存在) |
608
+ | `scripts/selfcheck.py` | 回归自检:**32 条**"曾真的坏过"的断言,不联网 20 秒出结论(可选模块缺失标 SKIP) |
466
609
  | `scripts/sfxkit.py` | **(v0.3)物理建模音效库**:模态合成/纹理/空间/动力学/母带 + 24 个配方 + `CUE_ALIASES` |
467
610
  | `scripts/scorekit.py` | **(v0.3)程序化配乐**:和弦表/分段能量/律动层/吸气/自动电平,输出 `music.wav` |
468
611
  | `scripts/webengine.py` | **(v0.3)浏览器逐帧渲染**:Edge/Chromium + CDP 截图、分块并行、崩溃重开、cue 导出、文字质检 |
469
612
  | `scripts/qa_frames.py` | **(v0.3)帧级质检**:单帧闪烁/画面空窗/音画同步/亮度与响度曲线(JSON + PNG) |
470
613
  | `assets/scenes.py` | 场景模板(开场/正文卡片/收尾 + **`cues()` 提示点钩子示范**),含缓存排版与预烘焙背景的正确写法 |
471
614
  | `assets/film.json` | 剧本模板(含 `hold`/`bpm`/`music`/`sfx.cues`/`engine` 等新字段) |
615
+ | `../../schemas/film.schema.json` | **(v0.6)film.json 的契约**(JSON Schema draft-07 子集)。校验器见 `scripts/filmcheck.py`,模板由 `tools/ci_check.py` 第 12 项守着 |
616
+ | `../../tools/estimate.py` | **(v0.6)开渲前预演**:帧数 / 各阶段耗时 / 磁盘峰值 / 分块建议,`--json` 可机读。基准常量集中在文件顶部,换机请重测后回填 |
472
617
  | `assets/web/` | **(v0.3)浏览器页面模板**:原生 Canvas2D 确定性时间轴、场景示范、文字质检脚本 |
473
618
  | `requirements.txt` | **(v0.4)Python 依赖清单**:必需(numpy/Pillow/edge-tts)/ 强烈建议(cv2)/ 可选(scipy、playwright)三档,带注释说明"缺了会怎样"。`pip install -r requirements.txt` 即可 |
474
619
 
@@ -498,13 +643,16 @@ v0.4 给了两条真正能用的路:
498
643
  7. **(v0.3)帧级深检的三个数**:单帧闪烁 N 条 / 画面空窗 N 段 / 音画同步超差 N 条
499
644
  (有 web 引擎时再加页面层的 文字重叠 / 出画 / 一闪而过 / 从没露面 四项)
500
645
 
646
+ > **v0.6 起不要口头复述这些数字**:`out/QC-REPORT.md` 是机器生成的交付凭据,
647
+ > 照抄报告里的"实测值"即可;报告里的 `skipped` 项必须逐项说明为什么没执行。
648
+ >
501
649
  > 完整示例(本技能自带模板片的实测,可以照这个格式抄;**数字每次跑会差一点**——
502
650
  > TTS 实测时长、AAC 编码都会让真峰/响度/锐度在小范围内浮动,别 chasing 小数点):
503
651
  > ```
504
652
  > 容器:1920x1080 / 30 fps / h264+aac / 48 kHz / 时长 30.10 s / 1.00 Mbps
505
653
  > 信号:-16.0 LUFS / 真峰 -1.2 dBTP / 锐度比 1.01 / 黑帧 0 / 字幕 4 条 = 剧本 4 句
506
654
  > 时间:单帧闪烁 0 / 画面空窗 0 / 音画同步超差 0(本条为 web 引擎或写了 visual_at 时才有意义)
507
- > 自检:selfcheck PASS 24 / FAIL 0 / SKIP 0;tools/ci_check.py PASS 11 / FAIL 0
655
+ > 自检:selfcheck PASS 32 / FAIL 0 / SKIP 0;tools/ci_check.py PASS 12 / FAIL 0
508
656
  > 目检:work/qa/sheet.png,四帧分别为开场星射线、五步成片卡片、画质三件事卡片、收尾居中大字;
509
657
  > 字幕黑底白字不压正文,大字边距均匀,无空洞帧。
510
658
  > ```
@@ -614,7 +762,7 @@ Pillow 路线: 墙钟秒数 ≈ 帧数 / 40 / 并行度 + 30% 余量
614
762
 
615
763
  ```bash
616
764
  cd <SKILL_DIR>/scripts
617
- python selfcheck.py # 24 条断言;FAIL 就照提示修环境
765
+ python selfcheck.py # 32 条断言;FAIL 就照提示修环境
618
766
  python sfxkit.py --selftest # 音效库(可选模块,装了才跑)
619
767
  python scorekit.py --selftest # 配乐
620
768
  python webengine.py --selftest # 浏览器引擎(24 项,会真起无头浏览器,约 15 秒)
@@ -630,6 +778,10 @@ selfcheck 自己造测试信号、现场调 ffmpeg 编码,**不联网、不依
630
778
  > `python tools/ci_check.py -v` 只查"这个仓库作为可分发包是否完整"(语法/JSON/依赖白名单/
631
779
  > 打包卫生/文档路径引用),**不需要 ffmpeg、字体或网络**,任何机器都能跑;
632
780
  > selfcheck 查的是"技能还能不能真的出片"。两层都在 CI 里跑。
781
+ >
782
+ > v0.6.0 起 ci_check 多了第 12 项「film.json 契约」:直接调 `scripts/filmcheck.py` 校验模板,
783
+ >并把 `schemas/film.schema.json` 的存在与合法性一起断言——**校验规则只允许有一份**,
784
+ >不在这里重写一遍。
633
785
 
634
786
  ### v0.2 在本机实测(2026-11-07,同一台机器)
635
787
 
@@ -1,13 +1,12 @@
1
1
  {
2
+ "_comment": "v0.5 模板:style 决定画风+配乐+旁白+节奏。'auto' = 按内容自动选;写死风格 id 可锁定(tech-deep/magazine/paper/vivid/mono/cinema/lo-fi/epic)。删掉 style 字段 = 回到 v0.4 行为(逐样本兼容)。节奏(lead/tail/min_dur/hold)默认由风格给,想覆盖就自己加字段。",
2
3
  "title": "测试片 demo",
3
4
  "end_text": "完",
5
+ "style": "auto",
4
6
  "voice": "zh-CN-YunxiNeural",
5
7
  "rate": "+6%",
6
- "lead": 0.62,
7
- "tail": 0.92,
8
- "min_dur": 3.2,
9
- "hold": 0.0,
10
- "bar": 0.0,
8
+ "pitch": "+0Hz",
9
+ "bar": 0,
11
10
  "sub_max_hold": null,
12
11
  "trim_silence": true,
13
12
  "tts_workers": 4,
@@ -19,28 +18,63 @@
19
18
  "music": {
20
19
  "enabled": true,
21
20
  "mode": "score",
22
- "key": "Dm",
21
+ "style": "auto",
22
+ "mood": "auto",
23
+ "bpm": "auto",
24
+ "key": "auto",
23
25
  "seed": 7,
26
+ "seed_from_content": true,
24
27
  "gain_db": -1.5,
25
- "duck_db": 3.0,
26
- "seg": 4.0,
28
+ "duck_db": 3,
29
+ "seg": 4,
27
30
  "amp": 0.05,
28
31
  "gain": 0.9,
29
32
  "xfade": 1.2,
30
- "chords": [[220, 261.63, 329.63], [174.61, 220, 261.63], [196, 246.94, 293.66]]
33
+ "chords": [
34
+ [
35
+ 220,
36
+ 261.63,
37
+ 329.63
38
+ ],
39
+ [
40
+ 174.61,
41
+ 220,
42
+ 261.63
43
+ ],
44
+ [
45
+ 196,
46
+ 246.94,
47
+ 293.66
48
+ ]
49
+ ]
31
50
  },
32
51
  "sfx": {
33
52
  "enabled": true,
34
- "gain_db": 0.0,
53
+ "gain_db": 0,
35
54
  "cues": [
36
- {"at": "l02-0.15", "type": "whoosh", "dur": 0.9, "gain": -2},
37
- {"at": "l03", "type": "riser", "dur": 2.0, "gain": -4},
38
- {"at": "l03+0.05", "type": "impact", "size": 1.0, "gain": -3}
55
+ {
56
+ "at": "l02-0.15",
57
+ "type": "whoosh",
58
+ "dur": 0.9,
59
+ "gain": -2
60
+ },
61
+ {
62
+ "at": "l03",
63
+ "type": "riser",
64
+ "dur": 2,
65
+ "gain": -4
66
+ },
67
+ {
68
+ "at": "l03+0.05",
69
+ "type": "impact",
70
+ "size": 1,
71
+ "gain": -3
72
+ }
39
73
  ]
40
74
  },
41
75
  "web": {
42
76
  "entry": "",
43
- "chunk_s": 30.0
77
+ "chunk_s": 30
44
78
  },
45
79
  "whoosh": true,
46
80
  "whoosh_amp": 0.055,
@@ -63,11 +97,26 @@
63
97
  "big": "五步成片",
64
98
  "hold": 0.6,
65
99
  "items": [
66
- {"text": "剧本 film.json", "ok": true},
67
- {"text": "旁白 edge-tts 实测时长", "ok": true},
68
- {"text": "时间轴 timeline.json", "ok": true},
69
- {"text": "逐帧 PIL 渲染", "ok": true},
70
- {"text": "numpy 采样级混音", "ok": true}
100
+ {
101
+ "text": "剧本 film.json",
102
+ "ok": true
103
+ },
104
+ {
105
+ "text": "旁白 edge-tts 实测时长",
106
+ "ok": true
107
+ },
108
+ {
109
+ "text": "时间轴 timeline.json",
110
+ "ok": true
111
+ },
112
+ {
113
+ "text": "逐帧 PIL 渲染",
114
+ "ok": true
115
+ },
116
+ {
117
+ "text": "numpy 采样级混音",
118
+ "ok": true
119
+ }
71
120
  ]
72
121
  },
73
122
  {
@@ -79,10 +128,22 @@
79
128
  "sub": "直通编码 / 安全区 / 响度",
80
129
  "big": "画质三件事",
81
130
  "items": [
82
- {"text": "rawvideo -> libx264 CRF17", "ok": true},
83
- {"text": "字幕不越出安全区", "ok": true},
84
- {"text": "-16 LUFS / -1.5 dBTP", "ok": true},
85
- {"text": "每帧确定性可复现", "ok": true}
131
+ {
132
+ "text": "rawvideo -> libx264 CRF17",
133
+ "ok": true
134
+ },
135
+ {
136
+ "text": "字幕不越出安全区",
137
+ "ok": true
138
+ },
139
+ {
140
+ "text": "-16 LUFS / -1.5 dBTP",
141
+ "ok": true
142
+ },
143
+ {
144
+ "text": "每帧确定性可复现",
145
+ "ok": true
146
+ }
86
147
  ]
87
148
  },
88
149
  {