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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/CHANGELOG.md +473 -0
  2. package/README.md +263 -327
  3. package/THIRD_PARTY_NOTICES.md +2 -2
  4. package/package.json +9 -10
  5. package/schemas/design.schema.json +722 -0
  6. package/schemas/film.schema.json +31 -42
  7. package/schemas/proposal.schema.json +133 -0
  8. package/skills/video-studio/SKILL.md +1216 -1123
  9. package/skills/video-studio/assets/design_fixture.json +141 -0
  10. package/skills/video-studio/assets/film.json +2 -4
  11. package/skills/video-studio/assets/golden_frames.json +13 -10
  12. package/skills/video-studio/assets/scenes.py +153 -65
  13. package/skills/video-studio/assets/voice_loudness.json +16 -0
  14. package/skills/video-studio/references/appearance.md +123 -0
  15. package/skills/video-studio/references/audio-variation.md +74 -93
  16. package/skills/video-studio/references/audio.md +64 -66
  17. package/skills/video-studio/references/change-impact.md +5 -5
  18. package/skills/video-studio/references/{v08-quality.md → delivery-gates.md} +16 -18
  19. package/skills/video-studio/references/frame-qa.md +48 -16
  20. package/skills/video-studio/references/pipeline.md +4 -4
  21. package/skills/video-studio/references/pitfalls.md +22 -3
  22. package/skills/video-studio/references/proposal.md +160 -0
  23. package/skills/video-studio/references/quality-gates.md +11 -11
  24. package/skills/video-studio/references/scene-authoring.md +2 -2
  25. package/skills/video-studio/references/scene-grammar.md +40 -42
  26. package/skills/video-studio/references/sfx-design.md +8 -6
  27. package/skills/video-studio/references/web-engine.md +8 -8
  28. package/skills/video-studio/references/word-timeline.md +9 -9
  29. package/skills/video-studio/requirements.txt +9 -13
  30. package/skills/video-studio/scripts/designkit.py +2115 -0
  31. package/skills/video-studio/scripts/filmcheck.py +22 -48
  32. package/skills/video-studio/scripts/filmkit.py +2980 -2904
  33. package/skills/video-studio/scripts/gates.py +4 -4
  34. package/skills/video-studio/scripts/grammar.py +73 -13
  35. package/skills/video-studio/scripts/musicgen.py +2042 -0
  36. package/skills/video-studio/scripts/preflight.py +14 -7
  37. package/skills/video-studio/scripts/previewkit.py +848 -0
  38. package/skills/video-studio/scripts/propose.py +825 -0
  39. package/skills/video-studio/scripts/qa_frames.py +73 -39
  40. package/skills/video-studio/scripts/qaasr.py +26 -7
  41. package/skills/video-studio/scripts/qavmaf.py +89 -23
  42. package/skills/video-studio/scripts/render.py +545 -231
  43. package/skills/video-studio/scripts/scorekit.py +55 -1755
  44. package/skills/video-studio/scripts/selfcheck.py +815 -215
  45. package/skills/video-studio/scripts/sfxkit.py +27 -26
  46. package/skills/video-studio/scripts/stagewatch.py +24 -8
  47. package/skills/video-studio/scripts/stylekit.py +207 -888
  48. package/skills/video-studio/scripts/voicekit.py +508 -387
  49. package/skills/video-studio/scripts/webengine.py +71 -72
  50. package/tools/audio_similarity.py +64 -37
  51. package/tools/ci_check.py +20 -14
  52. package/tools/estimate.py +34 -29
  53. package/tools/golden_frames.py +72 -28
  54. package/tools/silhouette_qa.py +520 -168
  55. package/docs/demo-pil-sheet.png +0 -0
  56. package/docs/styles/demo-results.json +0 -76
  57. package/docs/styles/sheet-8-styles.png +0 -0
  58. package/docs/styles/sheet-ai.png +0 -0
  59. package/docs/styles/sheet-before-after.png +0 -0
  60. package/docs/styles/sheet-craft.png +0 -0
  61. package/docs/styles/sheet-night.png +0 -0
  62. package/skills/video-studio/assets/score_demo.json +0 -307
  63. package/skills/video-studio/references/style-packs.md +0 -283
@@ -0,0 +1,16 @@
1
+ {
2
+ "schema": 1,
3
+ "_comment": "音色响度补偿(数据,不是身份):gain_db = 目标响度 - 该音色实测 rms_db,夹在 ±3 dB。换机/换 voice 必须重测(python voicekit.py --calibrate 会对表里的音色重新合成校准句)。designkit 与 voicekit 共用这一份:两处各存一份必然漂移——实测采样器会产出 4 把表里没有的音色。",
4
+ "target_dbfs": -20.97,
5
+ "voices": {
6
+ "zh-CN-YunyangNeural": 3.0,
7
+ "zh-CN-XiaoxiaoNeural": -2.7,
8
+ "zh-TW-HsiaoChenNeural": -3.0,
9
+ "zh-CN-XiaoyiNeural": -0.6,
10
+ "zh-CN-YunjianNeural": 1.0,
11
+ "zh-CN-YunxiNeural": 1.9,
12
+ "zh-CN-YunxiaNeural": 0.3,
13
+ "zh-CN-liaoning-XiaobeiNeural": 0.0,
14
+ "zh-CN-shaanxi-XiaoniNeural": -0.3
15
+ }
16
+ }
@@ -0,0 +1,123 @@
1
+ # 外观定制:设计 DSL 实操手册
2
+
3
+ > **外观由 `work/design.json` 决定:`propose.py` 现场采样,你确认之后冻结。**
4
+ > 没有现成外观目录可挑,也没有 `style` 字段——剧本不决定长相。
5
+ > 同一份稿子换一份 design.json 就是另一种画面,反之亦然。
6
+ > 这篇讲的是:**想控制画面的哪一块,就改 design.json 的哪个字段,怎么验证它真的生效。**
7
+
8
+ ## 0. 三条正确姿势
9
+
10
+ | 你想要 | 怎么做 | 不要这样做 |
11
+ |---|---|---|
12
+ | 换一种气质 | `propose.py --revise "更冷一点,少装饰,换女声"` 重出方案 | 去找"第 9 套风格"——没有这种目录 |
13
+ | 微调某个细节 | 手改 `work/design.json` 的字段 → `designkit.py --lint` 校验 → `render.py --stage stills` 看 | 直接改 `filmkit.py` 里的常量 |
14
+ | 换整个画面语言 | 写自己的 `scenes.py`(放剧本同目录),实现 `draw_frame(scene, t_local, dur, t_global, script, frame)`(完整签名见 SKILL.md §1) | 期望 `--engine web` 跟随 design(它不会) |
15
+
16
+ ## 1. design.json 的六块
17
+
18
+ | 块 | 决定什么 | 字段 |
19
+ |---|---|---|
20
+ | `structure` | 几段、目标时长 | `segments` / `beats` / `hold_policy` / `target_sec` |
21
+ | `visual` | 画面全部外观 | 见 §2 |
22
+ | `audio` | 配乐 / 声线 / 音效 | `music`(scale/bpm/key/mood/seed)/ `voice`(id/map/jitter/max_distinct)/ `sfx`(enabled/density/gain_db) |
23
+ | `pacing` | 节奏 | `lead` / `tail` / `min_dur` / `hold` / `item_stagger` |
24
+ | `id` | 指纹 | `design:<hash>`,由 `designkit.design_fingerprint()` 算出 |
25
+ | `label` / `pitch` | 中文标签 / 立意 | 提案卡上显示的那一句话 |
26
+
27
+ 校验:`python <SKILL_DIR>/scripts/designkit.py --lint work/design.json`;
28
+ 契约:`<REPO>/schemas/design.schema.json`。**fail-closed**:缺字段/越界/对比度不达标一律报错,
29
+ 不会静默回落——设计层不允许「改了不生效也不报错」这种状态存在。
30
+ (实测过 11 个静默失效的旋钮:`voice` 写字符串时 map/jitter 全失效、`pan` 对立体声配方无效……
31
+ 每个都"配置生效、画面不动",且不报错。设计层改成一律报错,这类问题在 `--lint` 就暴露。)
32
+
33
+ ## 2. visual 字段表(能控制什么)
34
+
35
+ | 字段 | 取值 | 改它会发生什么 |
36
+ |---|---|---|
37
+ | `palette` | 11 个颜色角色(bg/bg2/surface/ink/muted/dim/accent/accent2/good/bad/rule),每个 `[r,g,b]` | 整片色调。它不是"从 8 套配色里挑",而是**连续色相空间**里的一点:改 `bg` 的色相,其余角色会按对比度网格重新求解 |
38
+ | `family` | `sans` / `serif` / `kai` / `round` | 全片字族(标题优先,字幕默认跟随) |
39
+ | `typography` | `tracking`(字距)/ `line_gap`(行距)/ `number`(编号写法,8 种)/ `ratio`(字号比例)/ `align` | 文字气质;`number` 决定"01 / 壹 / I / 1.0"这类编号外观 |
40
+ | `motif` | `kind`(8 种:brackets / hairlines / handdrawn / slabs / minimal / letterbox / signal / rays)× `density`(0.55–1.00) | 装饰语言:四角括线 / 细线页眉 / 手绘框 / 色块 / 极简 / 遮幅 / 信号条 / 放射线 |
41
+ | `transition` | `kind`(dip / wash / paper / wipe / cut / iris)+ `dur`(秒) | 段与段之间的转场 |
42
+ | `motion` | `ease`(out3/out5/inOut3/outBack)/ `enter`(dx,dy) / `push`(镜头缓推)/ `breathe_hz` | 入场与镜头运动 |
43
+ | `bias` | `x` / `y` / `inset` | 整屏重心偏移与安全区收缩 |
44
+ | `layout` | `left-stack` / `center-stack` / `split-right` | 默认版式 |
45
+ | `layout_cycle` | 版式数组(≥2 种不同) | 按章节轮换版式;不写则自动派生 2–3 种 |
46
+ | `hud` | `chip-left` / `corner-line` / `none` | 页眉/角落装饰 |
47
+ | `size` / `fps` | `1920x1080` / `30` | 只支持 16:9 横屏(编码时可下采样到 720p) |
48
+ | `subtitle` | 见 §3 | 字幕字号/位置/对齐/背板/圆角/字族/颜色/行数 |
49
+
50
+ > **能组合出多少种画面?** 离散维度是 8 母题 × 6 转场 × 4 缓动 × 8 编号 × 3 版式 × 3 HUD × 4 字族
51
+ > × 7 情绪 × 2 调式(= 774,144 种),再乘配色这个连续量(色相 0–360° 任意取值)。
52
+ > 这个数字只说明"字段可组合空间足够大",**不是推荐你从中挑**——好看与否仍由对比度体检、
53
+ > 分镜语法和你的目检决定。
54
+
55
+ ## 3. 字幕样式(visual.subtitle)
56
+
57
+ 字幕的字号 / 位置 / 背板 / 圆角由设计 DSL 决定;写死的常量会让**任何两条片子**的字幕带一模一样。
58
+ **不写 = 用默认值**,老 `design.json` 不受影响、字幕与默认常量逐像素一致。
59
+
60
+ ```json
61
+ "subtitle": {"size": 46, "y": 560, "align": "left", "plate_alpha": 170,
62
+ "radius": 10, "family": "sans", "color": [245, 245, 245], "max_lines": 2}
63
+ ```
64
+
65
+ | 字段 | 默认 | 取值 | 备注 |
66
+ |---|---|---|---|
67
+ | `size` | 46 | 20–96 | 采样器默认跑 40–56 |
68
+ | `y` | null | 0–1030 | null = 贴底安全区;采样器 1/3 概率抬高到 560–880 |
69
+ | `align` | center | center / left | |
70
+ | `plate_alpha` | 170 | 0–255 | 采样器默认 96–194 |
71
+ | `radius` | 10 | 0–48 | 采样器默认 2–20 |
72
+ | `family` | null | sans / serif / kai / round | null = 跟随大标题字族 |
73
+ | `color` | null | [r,g,b] 或 #rrggbb | null = 默认墨色 |
74
+ | `max_lines` | 2 | 1–3 | 决定了折行上限 |
75
+
76
+ 验证:`render.py --stage style` 会打印一行「字幕: ... 对齐=... 背板α=... 圆角=... 最多N行」,
77
+ 改 design 后重跑这一行就能看重算结果,不用等渲完。
78
+
79
+ ## 4. 版式轮换(visual.layout_cycle)
80
+
81
+ ```json
82
+ "layout_cycle": ["split-right", "left-stack", "center-stack"]
83
+ ```
84
+
85
+ - 元素取 `left-stack` / `center-stack` / `split-right`;**至少 2 种不同版式**,只写一种会被校验拒绝。
86
+ - 由 `grammar.plan_scenes` 按**章节**轮换:同一章内固定,换章才换(观众视角一章是一段整体)。
87
+ - 不写时 `design_to_plan` 按设计指纹自动派生 2–3 种,**默认就不是全片一张版式**。
88
+ - 审计:`--stage layout` 打印每段构图;`work/storyboard.json` 每段都有 `layout` 字段。
89
+
90
+ ## 5. 配色怎么改才不出事
91
+
92
+ 配色不是"挑一个好看的颜色",而是 11 个角色要同时满足对比度:
93
+ `theme_lint` 的门槛是 ink≥7 / muted≥4.5 / dim≥3 / accent≥4.5 / accent2≥4.5(good/bad≥3),
94
+ 另外要求 bg 与 bg2 有可感知的亮度差(防纯色底)。
95
+
96
+ 推荐两条路:
97
+
98
+ 1. **让采样器算**(默认):采样器在 (hue, s, v) 网格里找满足全部门槛的点;
99
+ 实测纯随机取色通过率 35%,网格求解 100%。
100
+ 2. **手改色相**:只改 `palette.bg` 的色相/饱和度,然后跑 `designkit.py --lint`;
101
+ 过不了就调一档明度——报错会直接点名哪个角色不达标。
102
+
103
+ > 片内**不要跨明暗族**:同一部片锁一个明暗族,段界跨族会整屏换底、触发闪烁门禁,观众也看着像换片。
104
+ > 这条在采样器里是硬约束,手改 design 时请自己守住。
105
+
106
+ ## 6. 片内不重复:三层一起用
107
+
108
+ | 层 | 机制 | 防的是什么 |
109
+ |---|---|---|
110
+ | 段内 | `lines[].type`(10 类构图,grammar 自动选) | 每段都长一个样 |
111
+ | 章内 | `layout_cycle`(按章节轮换) | 全片一张版式 |
112
+ | 片间 | 采样 nonce + 候选 ≥2 维 + 底色色相分叉 + **片间去重台账** | 不同片撞衫 |
113
+
114
+ ## 7. 常见误用
115
+
116
+ | 现象 | 原因 | 修法 |
117
+ |---|---|---|
118
+ | 改了 `design.json` 画面没变 | 没重跑阶段 / 没重签核 | `render.py --stage explain` 看哪些阶段过时 |
119
+ | `--stage style` 报「找不到设计」 | 工程里没有 `work/design.json` | 先 `propose.py --brief ... --script film.json --work work` 再 `--confirm` |
120
+ | 手改 design 后 `--confirm` 报校验失败 | 字段越界/对比度不达标 | 按报错逐条修;或干脆 `--revise` 重出 |
121
+ | 想"用某套预设当起点" | 没有预设目录,外观只由 design.json 决定 | 现场采样 + `--revise` 表达要求,要复现用 `--seed` |
122
+ | `render.py --preset x` 报错 | render.py 没有 `--preset` 这个参数 | 用 `--brief` 或直接写 design.json |
123
+ | `stylekit.py --list` 只有一段说明、exit 2 | 没有可选外观清单(预期行为) | 看 `designkit.py --list-palettes` 的配色采样示例(那不是可选清单) |
@@ -1,36 +1,36 @@
1
- # 音频多样性改造(v0.7)—— 接口契约
1
+ # 音频差异机制 —— 接口契约
2
2
 
3
- > 起草:lead,2026-11-09。本文件是本次改造的**唯一定义源**。
4
- > 三路并行改动的文件写域不重叠,但**接口必须按本文件逐字实现**,否则集成会失败。
5
- > 诊断证据见 `tools/audio_similarity.py` 的输出(改完后必须重跑,阈值见 §5)。
3
+ > 当前机制一句话:配乐由 `musicgen.py` 参数化编排(bpm/key/scale/mood/seed + 编排参数,
4
+ > 内容指纹与编排参数共同决定和声/动机/鼓型);音效 seed 由 `content_seed(base, style, content_fp, recipe, t)`
5
+ > 按内容派生;提示音调性跟随配乐(`in_key`);段落边界前 2.2 s 内的转场 cue 自动 -3 dB
6
+ > (`sfx.duck_transitions`);混音旁白优先(配乐 -12 dB 基线 + 语音侧链 6/3 dB + SNR 兜底 12 dB)。
7
+ >
8
+ > 阈值由 `tools/audio_similarity.py` 按 §5 判定,诊断证据就是它的输出。
6
9
 
7
10
  ## 1. 要解决的两个问题(都有实测数据)
8
11
 
9
12
  | 问题 | 实测 | 根因 |
10
13
  |---|---|---|
11
- | A. 跨视频配乐雷同 | 同风格/同 bpm/同调、只换内容指纹:corr **0.966** | 指纹只改 seed(音色细节),和弦进行/动机/鼓型/段落骨架全固定 |
14
+ | A. 跨视频配乐雷同 | 同 bpm/同调/同编排参数、只换内容指纹:corr **0.966** | 指纹只改 seed(音色细节),和弦进行/动机/鼓型/段落骨架全固定 |
12
15
  | B. 跨视频音效雷同 | 同 cue 序列两次渲染逐样本相同 | `CueBus.build_audio` seed = `seed_base + i`,只跟 cue 顺序有关 |
13
16
  | C. 片内配乐与音效难分 | impact 质心 1001 Hz / whoosh 1589 Hz / music 5451 Hz;提示音硬编码 Dm 五声 | 提示音调性硬编码 + 配乐过门与 sfx 轨同边界同频段 + duck 仅 2.5–4.5 dB |
14
17
 
15
- ## 2. 改动总览(按写域分三路)
18
+ ## 2. 各层由谁负责
16
19
 
17
- | 路 | 负责文件 | 改什么 |
20
+ | 层 | 负责文件 | 做什么 |
18
21
  |---|---|---|
19
- | A 配乐变奏 | `skills/video-studio/scripts/scorekit.py` | 指纹参与**和声/动机/鼓型/结构**,不再只影响音色细节 |
20
- | B 音效差异化 | `skills/video-studio/scripts/sfxkit.py`、`assets/scenes.py`、`assets/film.json` | `in_key` 跟随配乐调性;`cues()` 按画面内容生成;模板 cue 去固定化 |
21
- | C 接线与混音 | `skills/video-studio/scripts/filmkit.py`(lead)、`render.py`(lead)、`stylekit.py`(只读)、`selfcheck.py`(lead) | CueBus 内容派生 seed、调性传给 sfxkit、duck 加深、风格过门避让 |
22
-
23
- **写域规则**:A 路只改 scorekit.py;B 路只改 sfxkit.py / assets/scenes.py / assets/film.json;
24
- C 路由 lead 改 filmkit.py / render.py / selfcheck.py。**任何一路不得改别人的文件**(包括顺手"优化")。
22
+ | 配乐编排 | `skills/video-studio/scripts/musicgen.py` | 按 bpm / key / scale / mood / seed 编排;`sections` / `progressions` / `motif` / `drums` / `voices` / `energy_curve` 真的参与事件展开;内容指纹进 seed 与变奏参数 |
23
+ | 音效差异化 | `skills/video-studio/scripts/sfxkit.py`、`assets/scenes.py`、`assets/film.json` | `in_key` 跟随配乐调性;`content_seed` 内容派生 seed;`cues()` 按画面内容生成;夹具 cue 不固定三条 |
24
+ | 接线与混音 | `skills/video-studio/scripts/filmkit.py`、`render.py`、`selfcheck.py` | CueBus 内容派生 seed、调性透传、duck 分级、过门避让、旁白优先 |
25
25
 
26
26
  ## 3. 接口契约(逐字实现,向后兼容)
27
27
 
28
- ### 3.1 sfxkit(B 路)
28
+ ### 3.1 sfxkit
29
29
 
30
30
  ```python
31
- # 1) 调性跟随:新增 key 参数,默认 None = 旧行为(Dm 五声)
31
+ # 1) 调性跟随:key 参数,默认 None = 固定 D 小调五声
32
32
  def in_key(freq: float, key: str | None = None) -> float:
33
- """key=None -> 旧行为(硬编码 D 小调五声,逐样本兼容)。
33
+ """key=None -> 固定 D 小调五声(不随配乐调性变,逐样本可复现)。
34
34
  key 给 "Dm"/"C"/"Am"/... -> 吸附到该调(小调用自然小调五声,大调用大调五声)。"""
35
35
 
36
36
  # 2) 所有走 in_key 的配方接受 key(通过 **kw 透传,签名末位加 key=None 或直接从 kw 取):
@@ -39,121 +39,102 @@ def in_key(freq: float, key: str | None = None) -> float:
39
39
 
40
40
  # 3) 内容派生 seed 的辅助函数(供 filmkit.CueBus 用)
41
41
  def content_seed(base: int, style: str, content_fp: str, recipe: str, t: float) -> int:
42
- """稳定的 32 位非负 int:sha1(\"v0.7|%d|%s|%s|%s|%.3f\") -> int(h[:8],16)%9000+1000。"""
42
+ """稳定的 32 位非负 int:sha1("<固定盐>|<base>|<style>|<content_fp>|<recipe>|<t 三位小数>")
43
+ -> int(h[:8], 16) % 9000 + 1000。"""
43
44
  ```
44
45
 
45
- ### 3.2 filmkit.CueBus.build_audio(C 路,lead)
46
+ `<固定盐>` 是代码里写死的字符串常量:`sfxkit.content_seed` 与 `filmkit.content_seed` 逐字相同。
47
+ 要复刻就照抄实现里的拼接串,**不要自己另拼一份**——换个盐等于换一套 seed,
48
+ "A1 同参数换指纹可区分"的验收直接失效。
49
+
50
+ ### 3.2 filmkit.CueBus.build_audio
46
51
 
47
52
  ```python
48
53
  def build_audio(self, total=None, sr=SR, base_db=None, default_gain_db=0.0,
49
54
  seed_base=1000, verbose=True,
50
- style=None, # 风格 id;None = 旧行为
51
- content_fp=None, # 内容指纹 full;None = 旧行为
52
- key=None): # 配乐调性;None = 旧行为(sfxkit 用 Dm)
55
+ style=None, # 设计标识(design:<hash>);None = 位置派生
56
+ content_fp=None, # 内容指纹 full;None = 位置派生
57
+ key=None): # 配乐调性;None = 提示音按 D 小调五声
53
58
  """每条 cue 的 seed:
54
- - style/content_fp 任一为 None -> seed = seed_base + i(旧路径,逐样本兼容)
59
+ - style / content_fp 任一为 None -> seed = seed_base + i(位置派生,逐样本可复现)
55
60
  - 都给 -> seed = sfxkit.content_seed(seed_base, style, content_fp, kind, t)
56
61
  并把 key 透传给配方(render(kind, seed=..., key=key, **params))。"""
57
62
  ```
58
63
 
59
- ### 3.3 scorekit.render_styled(A 路)
60
-
61
- 签名不变。**内部**行为变化:
62
-
63
- - `full` 指纹(已有 `fingerprint` 参数,当前只用于 seed)必须影响**和声与动机**:
64
- - 和弦替换:按指纹从该风格的 `progressions` 里选 tier,再对**每两小节**做一次稳定替换
65
- (从同大小的替代级数集里挑:i↔i7 / VI↔VI7 / III↔III7 / VII↔VII7;大调侧 I↔Imaj7 / IV↔IVmaj7)。
66
- 只在与乐句末/片尾规则不冲突时替换。
67
- - 动机变形:`motif` 的每个 (拍, 级数, 时值) 按指纹做 **±1 级数的稳定偏移**,
68
- 只改级数不改节奏;问题句偏移与答句偏移必须独立派生(保证两部片子不会同步变形)。
69
- - 鼓型:`drums[tier]` 的 kick 落点按指纹加 **最多一个 ghost 落点**(在 0.5 拍网格上,
70
- 不与现有落点重复);clap 位置按指纹 ±0.5 拍。
71
- - **必须保证**:同 `fingerprint` 逐采样一致;不同 fingerprint 的相关性显著下降(§5 阈值)。
72
- - 新增报告字段:`variation`(dict:`{"chord_swaps": n, "motif_shifts": n, "drum_ghosts": n}`),
73
- 写进 work/score.json,方便审计。
74
- - legacy 路径(style=None)**逐字节不变**(`scorekit.py --selftest` 的第 20 条会验;
75
- 别和 `selfcheck.py` 的编号搞混——那边第 21 条只查 scorekit 同 seed 确定性)。
64
+ `render.py` 的 `_sfx_seed_context` 就是这条的调用方:它从 `resolve_style` 解出的设计计划里取
65
+ `style` / `fingerprint.full` / `music.key`;`sfx.content_seed=false`(或计划里没有指纹)时返回全 None,
66
+ `build_audio` 于是走位置派生。
76
67
 
77
- ### 3.4 assets/scenes.py 的 cues()(B 路)
68
+ ### 3.3 assets/scenes.py 的 cues()
78
69
 
79
- 现在只出 `ui_blip × items` + `answer_chime × outro`。改为按画面事件生成,并且**类型按场景 kind 与内容变化**:
70
+ 按画面事件生成,**类型按场景 kind 与内容变化**:
80
71
 
81
72
  - `hook`:段首 `impact`(size 按字数 0.9~1.3),落定 `riser`。
82
- - `body`:条目落位仍用 `ui_blip`,但 **pitch 由条目文本 sha1 派生**(json 的 items 文本);
73
+ - `body`:条目落位用 `ui_blip`,但 **pitch 由条目文本 sha1 派生**(json 的 items 文本);
83
74
  最后一个条目后加 `answer_chime`(若 kind=body 且 items≥3);段尾 `whoosh`(going 到下一段)。
84
75
  - `quote`:`reverse_swell` + 落定 `impact`(轻)。
85
76
  - `outro`:`answer_chime` + 片尾 `sub_drop`。
86
- - 不许再出现"每段都一样的三连击":同类型 cue 在一段内重复时,`gain` 必须递减(-2/-4/-6),pitch 必须不同。
87
- - **保持向后兼容**:仍有 `stagger` 对齐逻辑(与 _draw_items 同节拍),返回 schema 不变。
77
+ - 不许出现"每段都一样的三连击":同类型 cue 在一段内重复时,`gain` 必须递减(-2/-4/-6),pitch 必须不同。
78
+ - **对齐**:stagger 对齐逻辑(与 `_draw_items` 同节拍)保留,返回 schema 不变。
88
79
 
89
- ### 3.5 assets/film.json 模板(B 路)
80
+ ### 3.4 assets/film.json 夹具
90
81
 
91
- 模板里的 `sfx.cues` 固定三条 whoosh/riser/impact 是"跨片雷同"的直接来源之一。改为:
82
+ 模板里的 `sfx.cues` 只保留 1 条转场 whoosh 作为示例,并注明"具体提示点交给 `scenes.py` 的 `cues()` 钩子"
83
+ (固定三条 whoosh/riser/impact 是"跨片雷同"的直接来源之一)。
92
84
 
93
- - 模板只保留 1 条转场 whoosh 作为示例,并加注释说明"具体提示点建议交给 scenes.py 的 cues() 钩子"。
94
- - `sfx` 增加可选字段 `content_seed: true`(默认 true):走内容派生 seed;显式 false 回到旧行为。
85
+ - `sfx` 的 `content_seed: true`(默认):走内容派生 seed;显式 `false` 回到 `seed_base + i`。
95
86
 
96
- ## 4. render.py 接线(C 路,lead)
87
+ ## 4. render.py 接线
97
88
 
98
- - `st_score`:把 `plan_s["music"]["key"]` 与 `plan_s` 的 `fingerprint` 传给 sfx 阶段(经 `script["_style"]`)。
99
- - `st_sfx`:读 `script["_style"]`,把 `style` / `content_fp` / `key` / `sfx.content_seed` 传给
100
- `bus.build_audio(...)`;没有 `_style`(legacy)时不传,保持旧行为。
101
- - `st_audio`:duck 默认值改为按风格:在现有表基础上整体 +1.0 dB,且 `music.duck_db` 显式值仍然优先。
102
- 另外新增**过门避让**:`scorekit` 的过门音效已经在 music.wav 里,sfx 轨在
103
- [段落边界 - 2.2s, 边界] 区间内的 whoosh/riser/reverse_swell 需要自动 -3 dB(在 filmkit 里做,
104
- 或 st_sfx 后处理;由 lead 决定实现位置,**B 路不要碰**)。
89
+ - `_sfx_seed_context`:从设计计划取 `style` / `fingerprint.full` / `music.key`;
90
+ `sfx.content_seed=false` 或计划没有指纹时返回全 None -> `build_audio` 走位置派生。
91
+ - `st_sfx`:把上面三样传给 `bus.build_audio(...)`;`sfx.duck_transitions`(默认 true)
92
+ 决定是否做过门避让。
93
+ - `st_audio`:duck 默认值按风格分级——`mono` 3.0 / `lo-fi`、`magazine` 3.5 / `cinema`、`epic` 5.5 /
94
+ 其余 4.0;`music.duck_db` 显式值永远优先。
95
+ 另外**过门避让**:配乐在 music.wav 里已经有段落过门,sfx 轨在
96
+ [段落边界 - 2.2s, 边界] 区间内的 whoosh/riser/reverse_swell 需要自动 -3 dB
97
+ (`_duck_transition_sfx` 实现,`sfx.duck_transitions=false` 可关)。
105
98
 
106
99
  ## 5. 验收阈值(改完必须重跑 `python <REPO>/tools/audio_similarity.py`)
107
100
 
108
- | 指标 | 改前 | 目标 | 落地实测(2026-11-09) |
101
+ | 指标 | 目标 | 实测 | 不这么做时 |
109
102
  |---|---|---|---|
110
- | A 同风格换指纹 corr | 0.966 | **≤ 0.85**(仍要像同一风格,但不能像同一首) | **0.645** |
111
- | A 换风格 corr | 0.534 | ≤ 0.52(不能变得更像) | **0.514**(v0.7.2 复测,余量 0.006;连跑 3 次逐位相同。余量薄是有意的:抬到 0.53 就离改造前的 0.534 只剩 0.004,这条门禁就没意义了) |
112
- | B 同 cue 序列跨片逐样本相同 | True | **False**(不同内容指纹必须不同) | False(同 fp 仍逐样本相同) |
113
- | B legacy(不传 style/fp) | True | **必须仍为 True**(向后兼容) | True |
114
- | C duck 默认 | 2.5–4.5 dB | 3.5–5.5 dB | 4.0(mono 3.0 / cinema 5.5) |
115
- | D 人声段旁白/背景响度差 | 约 1.5 dB(配乐 -1.5 无侧链) | **≥ 12 dB** | **15.0 dB** |
116
- | D 旁白处背景额外让位 | 0 | ≥ 4 dB | 6.0 dB |
117
-
118
- > v0.7.1 追加 D 组(旁白优先):视频以旁白为主,BGM 与音效是可选层。`mix_film` 的
103
+ | A1 同参数换内容指纹 corr | **≤ 0.85**(仍要像同一部片子,但不能像同一首) | **0.645** | 0.966 |
104
+ | A2 换编排参数 corr | ≤ 0.52(不能变得更像) | **0.514**(连跑 3 次逐位相同) | 0.534 |
105
+ | B1 同 cue 序列跨片逐样本相同 | **False**(不同内容指纹必须不同) | False(同 fp 仍逐样本相同) | True |
106
+ | B2 位置派生(不传 style/fp) | **必须仍为 True**(向后兼容) | True | — |
107
+ | C1 duck 默认 | 3.5–5.5 dB | 4.0(mono 3.0 / cinema 5.5) | 2.5–4.5 dB |
108
+ | D1 人声段旁白/背景响度差 | **≥ 12 dB** | **15.0 dB** | 约 1.5 dB(配乐 -1.5、无侧链) |
109
+ | D2 旁白处背景额外让位 | ≥ 4 dB | 6.0 dB | 0 |
110
+
111
+ > A2 的余量只有 0.006,是有意的:抬到 0.53 就离 0.534 只剩 0.004,这条门禁就没意义了。
112
+ > D 组守的是"旁白优先":视频以旁白为主,BGM 与音效是可选层。`mix_film` 的
119
113
  > `voice_duck_db=6` / `voice_duck_sfx_db=3` + `music.gain_db=-12` 把"人声比背景响 12 dB"
120
114
  > 从文档口号变成可执行断言(selfcheck 35/36 条 + 本脚本 D 组)。
121
115
 
122
- **已知边界(不是回归,是设计取舍)**:同风格不同内容指纹有约 1/3 概率落到同一个
123
- 和弦进行档位偏移上,此时两段配乐 corr 约 0.93(只剩七和弦色彩与鼓型 ghost 的差别)。
124
- 要再压下去需要"每段独立选档"或"同档时轮转",会削弱"片内是一条曲子"的连贯性,
125
- 本轮不做;`--strict` 用固定的 content-a / content-b 做验收,两者落在不同档。
126
-
127
116
  另有硬性回归(任何一条红都不算完成):
128
117
 
129
- > 下面 1–4 的路径都按**仓库根**写(`python skills/video-studio/scripts/…`);如果你已经 `cd` 进技能目录,
118
+ > 下面 1–5 的路径都按**仓库根**写(`python skills/video-studio/scripts/…`);如果你已经 `cd` 进技能目录,
130
119
  > 去掉 `skills/video-studio/` 前缀即可。`<REPO>` = 仓库根 / 装好后的包根。
131
120
 
132
- 1. `python skills/video-studio/scripts/selfcheck.py` 全 PASS(现在 55 条;v0.7.2 时的 43 条、v0.8 的 51 条是旧的)。
133
- 2. `python skills/video-studio/scripts/scorekit.py --selftest` 全 PASS。
134
- 3. `python skills/video-studio/scripts/sfxkit.py --selftest` 全 PASS。
121
+ 1. `python skills/video-studio/scripts/selfcheck.py` 全 PASS(**69 条**)。
122
+ 2. `python skills/video-studio/scripts/musicgen.py --selftest` 全 PASS(26 项)。
123
+ 3. `python skills/video-studio/scripts/sfxkit.py --selftest` 全 PASS(16 条)。
135
124
  4. `python <REPO>/tools/ci_check.py` 全 PASS(在包根执行)。
136
- 5. legacy 逐样本不变:`scorekit.py --selftest` 第 20 条 + `selfcheck.py` 第 26(风格 legacy)/33(音效 legacy)条,**这些断言不要改**。
137
-
138
- ## 6. 协作规则
125
+ 5. 音效位置派生的逐样本可复现:`selfcheck.py` 守着这条(`sfx.content_seed=false` 时与 `seed_base + i` 同值)。
139
126
 
140
- - 每个 teammate 只跑自己文件的 `--selftest`,**全量 selfcheck / ci_check 由 lead 统一跑**(避免并行抢 CPU 和临时目录)。
141
- - 改完在 `CHANGELOG.md` 追加的条目由 lead 统一写(teammate 不要改 CHANGELOG)。
142
- - 任何接口拿不准的地方,按本文件逐字实现;本文件没写的自由发挥请控制在"不改变签名"的范围内。
143
- - 完成后向 lead 报告:改了哪些函数、自测输出摘要、与契约的偏差(如果有)。
127
+ ## 6. 实现备注
144
128
 
145
- ## 7. 实现备注(落地后回填,2026-11-09)
129
+ 几处落地细节(不改契约本身):
146
130
 
147
- 三路改动全部落盘后,实测与契约之间的**已知偏差**都记在这里(不改契约本身):
148
-
149
- | 项 | 契约原文 | 实际实现 | 判断 |
150
- |---|---|---|---|
151
- | 配乐 tier 选择 | §3.3 只列了和弦替换/动机偏移/鼓型 ghost | 额外让指纹从 `progressions` 的 calm/body/drive 三档里选一档(该风格自带的三套进行,不发明新和弦) | 接受:单靠七和弦替换压不到 0.85,实测换 tier 后 0.966 → 0.645;仍是"同风格同一套素材" |
152
- | `roulette_tick` 调性 | §3.1 列进"走 in_key 的配方" | v0.6 本来没走 in_key;实现为 key=None 保持旧公式、key 非空才吸附 | 接受:不破坏 legacy 逐样本 |
153
- | 同段重复 cue 的 gain | §3.4 写 -2/-4/-6 | 每次再降 2 dB 不封顶(items 很多时会到 -2n) | 待观察:模板 5 条时最末一条 -8 dB;若听感偏轻改 `_cue_gain_decay` 一行 |
154
- | `content_seed` 实现位置 | §3.1 放 sfxkit | filmkit 与 sfxkit 各有一份逐字相同的实现(filmkit 走函数内延迟 import,模块级 from 会改导入时机) | 接受:两侧同参同值已核对;注释互相指认 |
131
+ | 项 | 现状 |
132
+ |---|---|
133
+ | `content_seed` 实现位置 | `filmkit` 与 `sfxkit` 各有一份逐字相同的实现(`filmkit` 走函数内延迟 import,模块级 `from` 会改导入时机);两侧同参同值 |
134
+ | 同段重复 cue 的 gain | 每次再降 2 dB、不封顶(items 很多时会到 -2n);模板 5 条时最末一条 -8 dB;要改听感就改 `_cue_gain_decay` 一行 |
135
+ | `roulette_tick` 调性 | `key=None` 走原公式(D 小调五声);`key` 非空才吸附 |
155
136
 
156
- **回归基线**(改前先录、改后逐条比对):
137
+ **回归基线**:
157
138
 
158
139
  - sfxkit 9 条渲染的 sha256(ui_blip ×3 / answer_chime / text_stream ×2 / data_chirp / roulette_tick ×2)逐条相同。
159
- - scorekit legacy 路径(style=None)6 s 渲染逐样本与 v0.6 相同。
140
+ - scorekit 的 `render()` 只承担合成原语(成片配乐走 musicgen):同 seed 逐样本一致(`scorekit.py --selftest` 第 18 条)。