@dsh-gis/dsh-video-studio-skill 0.9.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.
Files changed (55) hide show
  1. package/CHANGELOG.md +424 -0
  2. package/README.md +287 -327
  3. package/THIRD_PARTY_NOTICES.md +1 -1
  4. package/package.json +9 -10
  5. package/schemas/design.schema.json +722 -0
  6. package/schemas/film.schema.json +4 -15
  7. package/schemas/proposal.schema.json +133 -0
  8. package/skills/video-studio/SKILL.md +1316 -1123
  9. package/skills/video-studio/assets/design_fixture.json +141 -0
  10. package/skills/video-studio/assets/film.json +1 -2
  11. package/skills/video-studio/assets/golden_frames.json +11 -8
  12. package/skills/video-studio/assets/scenes.py +67 -22
  13. package/skills/video-studio/assets/voice_loudness.json +16 -0
  14. package/skills/video-studio/references/audio-variation.md +15 -11
  15. package/skills/video-studio/references/audio.md +36 -34
  16. package/skills/video-studio/references/change-impact.md +3 -3
  17. package/skills/video-studio/references/frame-qa.md +2 -2
  18. package/skills/video-studio/references/proposal.md +177 -0
  19. package/skills/video-studio/references/quality-gates.md +1 -1
  20. package/skills/video-studio/references/scene-grammar.md +28 -23
  21. package/skills/video-studio/references/sfx-design.md +1 -1
  22. package/skills/video-studio/references/style-packs.md +95 -257
  23. package/skills/video-studio/references/v08-quality.md +8 -8
  24. package/skills/video-studio/references/web-engine.md +1 -1
  25. package/skills/video-studio/references/word-timeline.md +2 -2
  26. package/skills/video-studio/scripts/designkit.py +2120 -0
  27. package/skills/video-studio/scripts/filmcheck.py +7 -33
  28. package/skills/video-studio/scripts/filmkit.py +153 -78
  29. package/skills/video-studio/scripts/grammar.py +67 -7
  30. package/skills/video-studio/scripts/musicgen.py +2043 -0
  31. package/skills/video-studio/scripts/preflight.py +10 -3
  32. package/skills/video-studio/scripts/previewkit.py +849 -0
  33. package/skills/video-studio/scripts/propose.py +825 -0
  34. package/skills/video-studio/scripts/qa_frames.py +45 -10
  35. package/skills/video-studio/scripts/qavmaf.py +77 -11
  36. package/skills/video-studio/scripts/render.py +472 -154
  37. package/skills/video-studio/scripts/scorekit.py +50 -1748
  38. package/skills/video-studio/scripts/selfcheck.py +658 -131
  39. package/skills/video-studio/scripts/stagewatch.py +21 -5
  40. package/skills/video-studio/scripts/stylekit.py +265 -741
  41. package/skills/video-studio/scripts/voicekit.py +502 -380
  42. package/skills/video-studio/scripts/webengine.py +15 -14
  43. package/tools/audio_similarity.py +57 -30
  44. package/tools/ci_check.py +7 -1
  45. package/tools/estimate.py +14 -8
  46. package/tools/golden_frames.py +56 -11
  47. package/tools/silhouette_qa.py +521 -168
  48. package/docs/demo-pil-sheet.png +0 -0
  49. package/docs/styles/demo-results.json +0 -76
  50. package/docs/styles/sheet-8-styles.png +0 -0
  51. package/docs/styles/sheet-ai.png +0 -0
  52. package/docs/styles/sheet-before-after.png +0 -0
  53. package/docs/styles/sheet-craft.png +0 -0
  54. package/docs/styles/sheet-night.png +0 -0
  55. package/skills/video-studio/assets/score_demo.json +0 -307
@@ -0,0 +1,141 @@
1
+ {
2
+ "schema": 1,
3
+ "id": "fixture-golden",
4
+ "label": "金帧基准设计",
5
+ "pitch": "验收基准:固定设计,用于像素回归(不是给人看的方案)",
6
+ "structure": {
7
+ "segments": 4,
8
+ "beats": [
9
+ "hook",
10
+ "body",
11
+ "body",
12
+ "outro"
13
+ ],
14
+ "hold_policy": "readable",
15
+ "target_sec": 30.0
16
+ },
17
+ "visual": {
18
+ "palette": {
19
+ "bg": [
20
+ 7,
21
+ 14,
22
+ 26
23
+ ],
24
+ "bg2": [
25
+ 17,
26
+ 33,
27
+ 54
28
+ ],
29
+ "surface": [
30
+ 26,
31
+ 42,
32
+ 64
33
+ ],
34
+ "ink": [
35
+ 230,
36
+ 216,
37
+ 207
38
+ ],
39
+ "muted": [
40
+ 178,
41
+ 157,
42
+ 143
43
+ ],
44
+ "dim": [
45
+ 153,
46
+ 135,
47
+ 122
48
+ ],
49
+ "accent": [
50
+ 230,
51
+ 106,
52
+ 23
53
+ ],
54
+ "accent2": [
55
+ 92,
56
+ 230,
57
+ 216
58
+ ],
59
+ "good": [
60
+ 71,
61
+ 178,
62
+ 98
63
+ ],
64
+ "bad": [
65
+ 178,
66
+ 97,
67
+ 89
68
+ ],
69
+ "rule": [
70
+ 255,
71
+ 255,
72
+ 255
73
+ ]
74
+ },
75
+ "family": "sans",
76
+ "typography": {
77
+ "tracking": 0.0,
78
+ "line_gap": 1.3,
79
+ "number": "dim",
80
+ "ratio": 1.5,
81
+ "align": "left"
82
+ },
83
+ "motif": {
84
+ "kind": "brackets",
85
+ "density": 1.0,
86
+ "detail": {}
87
+ },
88
+ "transition": {
89
+ "kind": "dip",
90
+ "dur": 0.28
91
+ },
92
+ "motion": {
93
+ "ease": "out3",
94
+ "enter": [
95
+ 0.45,
96
+ 0.85
97
+ ],
98
+ "push": 0.04,
99
+ "breathe_hz": 0.85
100
+ },
101
+ "bias": {
102
+ "x": 0,
103
+ "y": 0,
104
+ "inset": 0
105
+ },
106
+ "layout": "left-stack",
107
+ "hud": "chip-left",
108
+ "size": "1920x1080",
109
+ "fps": 30
110
+ },
111
+ "audio": {
112
+ "voice": {
113
+ "mode": "edge_id",
114
+ "id": "zh-CN-YunyangNeural",
115
+ "map": {},
116
+ "jitter": 0.0,
117
+ "max_distinct": 1
118
+ },
119
+ "music": {
120
+ "mode": "params",
121
+ "style": "tech-deep",
122
+ "scale": "minor",
123
+ "bpm": 96.0,
124
+ "key": "Dm",
125
+ "mood": "steady",
126
+ "seed": 7
127
+ },
128
+ "sfx": {
129
+ "enabled": false,
130
+ "density": "none",
131
+ "gain_db": -6
132
+ }
133
+ },
134
+ "pacing": {
135
+ "lead": 0.62,
136
+ "tail": 0.92,
137
+ "min_dur": 3.0,
138
+ "hold": 0.0,
139
+ "item_stagger": 0.3
140
+ }
141
+ }
@@ -1,8 +1,7 @@
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
+ "_comment": "自测/演示样例剧本(仓库测试夹具,不是给用户抄的模板)。外观由 work/design.json 决定(propose.py --brief 现场生成后 --confirm 冻结);v1.0.1 起 style 字段已废弃。文本仅用于跑通全链与金帧/门禁回归,换题材请自己写 lines[]。",
3
3
  "title": "测试片 demo",
4
4
  "end_text": "完",
5
- "style": "auto",
6
5
  "voice": "zh-CN-YunxiNeural",
7
6
  "rate": "+6%",
8
7
  "pitch": "+0Hz",
@@ -9,6 +9,7 @@
9
9
  "fps": 30,
10
10
  "script": "skills/video-studio/assets/film.json",
11
11
  "scenes": "skills/video-studio/assets/scenes.py",
12
+ "design": "skills/video-studio/assets/design_fixture.json",
12
13
  "font_fp": "05c853d0eb28bcca3125d81c1ce27c9436f66a55",
13
14
  "fonts": [
14
15
  {
@@ -33,11 +34,13 @@
33
34
  "platform": "Windows 11"
34
35
  },
35
36
  "inputs": {
36
- "script_sha1": "2f375a7298ea861684a2d553cb6617ed4351d3e5",
37
- "scenes_sha1": "aab9df135ce5a7a3f2750d275dfcdac48405d4a4",
38
- "filmkit_sha1": "a582ad1a2b53cf1ae163ab3052d054bc196c7d49",
39
- "stylekit_sha1": "4bcbce2048b89c66352455f6e82c0943cc2e0508",
40
- "golden_tool_sha1": "17cdc5d64705d54c157cf86f76899393a5d0b8c4"
37
+ "script_sha1": "9ad24749d19cf0fcec1b7fa62e906ed78ce5501b",
38
+ "scenes_sha1": "1ff8eaeed66c226a48875580d96d41ff6daad1e3",
39
+ "filmkit_sha1": "aca2f742ee4e7f7f2e99901a151864d8046d1547",
40
+ "stylekit_sha1": "869098c23a1d9e704a0ffacbc58da6c987712817",
41
+ "design_sha1": "f87cc13e4c55040d9de5675933c01aa8220134d1",
42
+ "designkit_sha1": "074b9705ae0f90a8b35162946142f8c28a3a39db",
43
+ "golden_tool_sha1": "da09795aaf3cb92bc9e14f9561bb64027917acbf"
41
44
  },
42
45
  "frames": [
43
46
  {
@@ -45,21 +48,21 @@
45
48
  "scene": "l01",
46
49
  "t_local": 1.2,
47
50
  "label": "hook",
48
- "sha256": "ee2a70067aeaee0c158c7eef6c26c1228354bf247f05f7330c991ef1946aaaae"
51
+ "sha256": "155eaa030a519b7ded1c49664e0007314f2a4dc4eb6ef1e4b69d3b70bae0b367"
49
52
  },
50
53
  {
51
54
  "frame": 270,
52
55
  "scene": "l03",
53
56
  "t_local": 1.6,
54
57
  "label": "body",
55
- "sha256": "ead954c01f5498782b3a31b8e42638d4f8f42e2faca97df476e0d78a1f9f72f8"
58
+ "sha256": "9d205ae43f2632c78446d372d8f3194ecfb7a9634ca42c36943ce31fd6be7981"
56
59
  },
57
60
  {
58
61
  "frame": 369,
59
62
  "scene": "l04",
60
63
  "t_local": 1.5,
61
64
  "label": "outro",
62
- "sha256": "10463a35429185e7b816fd5ede7107d529e26f4ec8d8640fba84d9897cc77f66"
65
+ "sha256": "c3ccdbedc567402728677f9f4d7ccf62bb98930bebf52934b8bb2becb0e9fb4f"
63
66
  }
64
67
  ]
65
68
  }
@@ -1,20 +1,26 @@
1
1
  # -*- coding: utf-8 -*-
2
- """scenes.py — 场景绘制模板(v0.5.0:画风随内容变化)。
2
+ """scenes.py — 技能内置的场景渲染器(v1.0.2:默认渲染路径 + 自定义场景的范例)。
3
+
4
+ ⚠ 这**不是**给用户拷走改的模板:v1.0.2 起 render.py 不写 --scenes 时会自动回落到本文件,
5
+ 用户工程目录里只需要一个 film.json。想改画面时,在剧本同目录放一个自己的 scenes.py
6
+ (或 --scenes 显式指定),它会被优先发现;本文件就是那份自写场景的**参考实现**:
7
+ 10 类构图 / 8 种母题 / 6 种转场 / 35 字段主题 / 版式轮换 / 字幕参数全部从计划里读。
3
8
 
4
9
  唯一约定:提供 draw_frame(scene, t_local, dur, t_global, script, frame) -> PIL.Image
5
- scene : timeline.json 里的一段(含 id/text/sub/start/end/dur)
10
+ scene : timeline.json 里的一段(含 id/text/sub/start/end/dur 与 grammar 注入的
11
+ scene_type / scene_variant / scene_decor / scene_layout / scene_reason)
6
12
  t_local : 本段内已经过秒数
7
13
  dur : 本段总秒数
8
14
  t_global: 全片秒数
15
+ script : 剧本 dict(含 _style 计划、_total 总时长)
9
16
  frame : 全片帧号(确定性:同一帧号必须画出同一张图)
10
17
 
11
- v0.5.0 画风主题化
12
- ----------------
13
- 剧本里写了 style(或 render.py 把 stylekit 解析出的计划塞进 script["_style"])时,
14
- 本文件从 stylekit 的主题包里取配色 / 材质 / 字体族 / 版式 / 缓动 / HUD / 镜头缓推,
15
- 8 套风格各长一个样(深蓝科技 / 暖白杂志 / 手绘纸感 / 活力撞色 / 极简黑白 /
16
- 电影青橙 / 暖灰薄荷 / 深黑鎏金)。没写 style 的老项目 = legacy:
17
- 走下面原封不动的 v0.4 绘制函数,逐字节可复现(基线三帧 sha256 由 selfcheck 比对)。
18
+ 画风从哪来
19
+ ----------
20
+ render.py 把 designkit.design_to_plan() 的计划塞进 script["_style"](主题 / 视觉语言 /
21
+ 版式轮换 / 配乐 / 声线 / 节奏 / 字幕样式)。本文件从这份计划里取配色、材质、字体族、
22
+ 版式、缓动、HUD、镜头缓推——**没有任何内置风格表**(v1.0.1 已全部删除):
23
+ 同一份代码画出来是什么样,完全由现场采样并确认过的 design.json 决定。
18
24
 
19
25
  铁律(决定画质与可复现性):
20
26
  1. 不许用 random / time.time();要随机用 K.hash2 / K.hash3 / vnoise(seed)
@@ -271,11 +277,31 @@ def _plan(script):
271
277
  return got
272
278
 
273
279
 
280
+ # 主题里这些键的值是 RGB,统一要 (r,g,b) 元组。JSON 往返会把它变成 list,
281
+ # 而 list 不能当缓存键 —— _soft_dot / _bg 之类的 key 里都带着颜色,一旦错位就是
282
+ # "unhashable type: 'list'"。实测触发路径:--stage frames --jobs N 把 script 写成
283
+ # _script_snapshot.json,子进程读回来重建,于是 theme.bg 是 [8,8,10]。
284
+ # 修在**入口**(_scene_theme)而不是每个缓存点:缓存点会越加越多,入口只有一个。
285
+ _THEME_RGB_KEYS = ("bg", "bg2", "surface", "ink", "muted", "dim",
286
+ "accent", "accent2", "good", "bad", "rule", "vignette_tint")
287
+
288
+
289
+ def _freeze_theme(th):
290
+ """把主题里的颜色值统一成元组(就地不重复建字面量的前提下返回新 dict)。"""
291
+ out = dict(th)
292
+ for k in _THEME_RGB_KEYS:
293
+ v = out.get(k)
294
+ if isinstance(v, (list, tuple)) and len(v) >= 3:
295
+ out[k] = (int(v[0]), int(v[1]), int(v[2]))
296
+ # scale / card / qa 是嵌套 dict,外面只读不建缓存键,原样保留
297
+ return out
298
+
299
+
274
300
  def _scene_theme(plan, scene):
275
301
  """本段用哪套主题:全片主题 + 可选的 scene["theme"] 覆盖(只许同明暗族)。"""
276
302
  base = plan.get("theme")
277
303
  if isinstance(base, dict):
278
- th = dict(base)
304
+ th = _freeze_theme(base)
279
305
  else:
280
306
  th = dict(ST.THEMES[plan["theme_id"]])
281
307
  sid = scene.get("theme")
@@ -293,7 +319,7 @@ def _scene_theme(plan, scene):
293
319
  "场景 %r 想从主题 %s 换到 %s,两者明暗族不同(同片换族会触发闪烁门禁),已忽略"
294
320
  % (scene.get("id"), th.get("id"), sid))
295
321
  return th
296
- return dict(alt)
322
+ return _freeze_theme(alt)
297
323
 
298
324
 
299
325
  # ---------------- 包内小工具 ----------------
@@ -1194,13 +1220,19 @@ def _draw_hero(scene, t, dur, tg, script, frame, plan, th):
1194
1220
  lh = int(size * gap)
1195
1221
  ksize = int(th["scale"]["kicker"][0])
1196
1222
  kicker = scene.get("kicker") or "00 · 开场"
1223
+ y = float(lay["title_y"]) + bdy
1197
1224
 
1225
+ # kicker 的纵向位置必须**跟着标题字号走**,不能直接用布局表里的固定 y。
1226
+ # 为什么:布局表给的是 left-stack 的 title_y=300 / kicker_y=250,只差 50px;
1227
+ # 而 hero 的标题字号是 96px(display 档),字形往上顶穿 kicker —— 实测封面上
1228
+ # "00 · 开场"直接压在"价值与代价来自同一个选择"中间。
1229
+ # 按字号算间距后,换字体/换字号档(visual 语言包会改 ratio)都不会再压。
1230
+ _ky = min(float(lay["kicker_y"]), y - size * 1.05 - ksize * 0.35)
1198
1231
  p0 = ease(rng(t, 0.05, 0.85))
1199
1232
  _draw_text(img, th, v, kicker, ax + _entrance_x(th, p0, align),
1200
- float(lay["kicker_y"]) + _entrance_y(th, p0), ksize, "bold",
1233
+ _ky + _entrance_y(th, p0), ksize, "bold",
1201
1234
  th["accent"], align, p0, trk * 0.5)
1202
1235
 
1203
- y = float(lay["title_y"]) + bdy
1204
1236
  for i, ln in enumerate(lines):
1205
1237
  p = ease(rng(t, 0.12 + 0.13 * i, 0.88 + 0.13 * i))
1206
1238
  _draw_text(img, th, v, ln, ax + _entrance_x(th, p, align),
@@ -1513,10 +1545,23 @@ def _draw_stat(scene, t, dur, tg, script, frame, plan, th):
1513
1545
  d.rectangle([v.x(bx + i * (bw + gapb)), v.y(760), v.x(bx + i * (bw + gapb) + bw),
1514
1546
  v.y(768)], fill=_rgba(th["accent"] if on else th["rule"],
1515
1547
  int((150 if on else 34))))
1548
+ # 次要统计:**数值 + 单位 + 标签**都要画出来。
1549
+ # v1.0 修:早先这里只画了 s["label"],于是 stats[] 里的第 2、3 项(例如
1550
+ # "338207 次 / 工具调用"、"22.62 B / Token 重放")在画面上只剩两个光秃秃的词,
1551
+ # 并且行距 30px < 字号,两行直接叠在一起 —— 观众看到的是"工具调用/Token 重放"两行字
1552
+ # 糊在一块、数字完全消失。数字卡的价值就在数字,丢数字等于这段白做。
1553
+ nsize = int(th["scale"]["body"][0] - 6)
1554
+ step = int(nsize * 1.75) # 行距必须大于字号,否则相邻两行会压在一起
1516
1555
  for j, s in enumerate(notes):
1517
1556
  pn = ease(rng(t, 0.6 + 0.15 * j, 1.3 + 0.15 * j))
1518
- txt = str(s.get("label") or "")
1519
- _draw_text(img, th, v, txt, W * 0.5, 800.0 + j * 30, int(th["scale"]["body"][0] - 6),
1557
+ val = str(s.get("value") if s.get("value") is not None else "").strip()
1558
+ unit = str(s.get("unit") or "").strip()
1559
+ lbl = str(s.get("label") or "").strip()
1560
+ head = val + ((" " + unit) if unit else "")
1561
+ txt = (head + " " + lbl) if head else lbl
1562
+ if not txt:
1563
+ continue
1564
+ _draw_text(img, th, v, txt, W * 0.5, 796.0 + j * step, nsize,
1520
1565
  "reg", th["muted"], "center", pn)
1521
1566
 
1522
1567
  img = _apply_veil(img, th, lang, scene, t, s_bg)
@@ -2003,18 +2048,18 @@ _KIND_TO_TYPE = {
2003
2048
  def draw_frame(scene, t_local, dur, t_global, script, frame):
2004
2049
  """按帧画一张 1920x1080 RGB 图。
2005
2050
 
2006
- 派发顺序(v0.9):
2007
- 1. legacy(film.json 没有 style 字段)-> v0.4 三个老绘制器,逐字节不变;
2008
- 2. 非 legacy -> 按 scene_type(grammar.py 选好、render.py 写进 timeline)派发到
2009
- 10 类构图之一;没有 scene_type 的旧 timeline 按 kind 映射到兼容类型。
2051
+ v1.0 / D3 起只有一条路:按 scene_type(grammar.py 选好、render.py 写进 timeline)
2052
+ 派发到 10 类构图之一;没有 scene_type 的旧 timeline 按 kind 映射到兼容类型。
2053
+
2054
+ 原来的第 1 条分支(legacy —— film.json 不写 style 就走 v0.4 三个老绘制器)**已删除**:
2055
+ stylekit 不再产出 legacy 计划(不写设计会直接报错),那条分支永远走不到。
2056
+ 留着死分支比删掉更危险——它会让"legacy 还能用"的错觉一直存在。
2057
+
2010
2058
  未命中的类型回落到 list("有条目就画条目"是最不会丢信息的兜底),
2011
2059
  并 warn 一次——静默画错构图比报错更糟。
2012
2060
  """
2013
2061
  plan = _plan(script)
2014
2062
  kind = scene.get("kind") or "body"
2015
- if plan.get("legacy"):
2016
- fn = _LEGACY_DRAWERS.get(kind) or _legacy_scene_body
2017
- return fn(scene, t_local, dur, t_global, script, frame)
2018
2063
  th = _scene_theme(plan, scene)
2019
2064
  stype = str(scene.get("scene_type") or "").strip()
2020
2065
  if not stype:
@@ -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
+ }
@@ -1,5 +1,9 @@
1
- # 音频多样性改造(v0.7)—— 接口契约
1
+ # 音频多样性改造(v0.7)—— 接口契约(**已归档**)
2
2
 
3
+ > **v1.0.1 起本文描述的 scorekit 风格层已删除**(`MUSIC_STYLES` / `render_styled` /
4
+ > 指纹结构变奏)。保留本文是因为音效侧契约(sfxkit / CueBus / duck / 旁白优先)仍然有效;
5
+ > 「A 路配乐变奏」那一节只作历史记录——现在配乐由 `musicgen.py` 参数化编排,没有风格表。
6
+ >
3
7
  > 起草:lead,2026-11-09。本文件是本次改造的**唯一定义源**。
4
8
  > 三路并行改动的文件写域不重叠,但**接口必须按本文件逐字实现**,否则集成会失败。
5
9
  > 诊断证据见 `tools/audio_similarity.py` 的输出(改完后必须重跑,阈值见 §5)。
@@ -16,11 +20,11 @@
16
20
 
17
21
  | 路 | 负责文件 | 改什么 |
18
22
  |---|---|---|
19
- | A 配乐变奏 | `skills/video-studio/scripts/scorekit.py` | 指纹参与**和声/动机/鼓型/结构**,不再只影响音色细节 |
23
+ | A 配乐变奏(**v1.0.1 已删除**) | 原 `skills/video-studio/scripts/scorekit.py` | 指纹参与**和声/动机/鼓型/结构**,不再只影响音色细节 |
20
24
  | B 音效差异化 | `skills/video-studio/scripts/sfxkit.py`、`assets/scenes.py`、`assets/film.json` | `in_key` 跟随配乐调性;`cues()` 按画面内容生成;模板 cue 去固定化 |
21
25
  | C 接线与混音 | `skills/video-studio/scripts/filmkit.py`(lead)、`render.py`(lead)、`stylekit.py`(只读)、`selfcheck.py`(lead) | CueBus 内容派生 seed、调性传给 sfxkit、duck 加深、风格过门避让 |
22
26
 
23
- **写域规则**:A 路只改 scorekit.py;B 路只改 sfxkit.py / assets/scenes.py / assets/film.json;
27
+ **写域规则(历史)**:A 路只改 scorekit.py;B 路只改 sfxkit.py / assets/scenes.py / assets/film.json;
24
28
  C 路由 lead 改 filmkit.py / render.py / selfcheck.py。**任何一路不得改别人的文件**(包括顺手"优化")。
25
29
 
26
30
  ## 3. 接口契约(逐字实现,向后兼容)
@@ -56,7 +60,7 @@ def build_audio(self, total=None, sr=SR, base_db=None, default_gain_db=0.0,
56
60
  并把 key 透传给配方(render(kind, seed=..., key=key, **params))。"""
57
61
  ```
58
62
 
59
- ### 3.3 scorekit.render_styled(A 路)
63
+ ### 3.3 scorekit.render_styled(A 路)——**v1.0.1 已删除,仅存档**
60
64
 
61
65
  签名不变。**内部**行为变化:
62
66
 
@@ -71,8 +75,8 @@ def build_audio(self, total=None, sr=SR, base_db=None, default_gain_db=0.0,
71
75
  - **必须保证**:同 `fingerprint` 逐采样一致;不同 fingerprint 的相关性显著下降(§5 阈值)。
72
76
  - 新增报告字段:`variation`(dict:`{"chord_swaps": n, "motif_shifts": n, "drum_ghosts": n}`),
73
77
  写进 work/score.json,方便审计。
74
- - legacy 路径(style=None)**逐字节不变**(`scorekit.py --selftest` 的第 20 条会验;
75
- 别和 `selfcheck.py` 的编号搞混——那边第 21 条只查 scorekit 同 seed 确定性)。
78
+ - legacy 路径(style=None)**逐字节不变**(`scorekit.py --selftest` 的 legacy 条会验)。
79
+ (v1.0.1:`render_styled` 已删除,这一条现在只说明历史契约。)
76
80
 
77
81
  ### 3.4 assets/scenes.py 的 cues()(B 路)
78
82
 
@@ -99,7 +103,7 @@ def build_audio(self, total=None, sr=SR, base_db=None, default_gain_db=0.0,
99
103
  - `st_sfx`:读 `script["_style"]`,把 `style` / `content_fp` / `key` / `sfx.content_seed` 传给
100
104
  `bus.build_audio(...)`;没有 `_style`(legacy)时不传,保持旧行为。
101
105
  - `st_audio`:duck 默认值改为按风格:在现有表基础上整体 +1.0 dB,且 `music.duck_db` 显式值仍然优先。
102
- 另外新增**过门避让**:`scorekit` 的过门音效已经在 music.wav 里,sfx 轨在
106
+ 另外新增**过门避让**:配乐在 music.wav 里已经有段落过门,sfx 轨在
103
107
  [段落边界 - 2.2s, 边界] 区间内的 whoosh/riser/reverse_swell 需要自动 -3 dB(在 filmkit 里做,
104
108
  或 st_sfx 后处理;由 lead 决定实现位置,**B 路不要碰**)。
105
109
 
@@ -129,11 +133,11 @@ def build_audio(self, total=None, sr=SR, base_db=None, default_gain_db=0.0,
129
133
  > 下面 1–4 的路径都按**仓库根**写(`python skills/video-studio/scripts/…`);如果你已经 `cd` 进技能目录,
130
134
  > 去掉 `skills/video-studio/` 前缀即可。`<REPO>` = 仓库根 / 装好后的包根。
131
135
 
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。
136
+ 1. `python skills/video-studio/scripts/selfcheck.py` 全 PASS(现在 **67 条**;v0.7.2 时的 43 条、v0.8 的 51 条是旧的)。
137
+ 2. `python skills/video-studio/scripts/musicgen.py --selftest` 全 PASS(原第 2 条要求 scorekit 风格层自测,v1.0.1 已随风格层删除)。
134
138
  3. `python skills/video-studio/scripts/sfxkit.py --selftest` 全 PASS。
135
139
  4. `python <REPO>/tools/ci_check.py` 全 PASS(在包根执行)。
136
- 5. legacy 逐样本不变:`scorekit.py --selftest` 第 20 条 + `selfcheck.py` 第 26(风格 legacy)/33(音效 legacy)条,**这些断言不要改**。
140
+ 5. 音效 legacy 逐样本不变:`selfcheck.py` 的音效 legacy 断言(v1.0.1 起风格 legacy 断言随风格层一并删除)。
137
141
 
138
142
  ## 6. 协作规则
139
143
 
@@ -156,4 +160,4 @@ def build_audio(self, total=None, sr=SR, base_db=None, default_gain_db=0.0,
156
160
  **回归基线**(改前先录、改后逐条比对):
157
161
 
158
162
  - 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 相同。
163
+ - 配乐 legacy 路径(scorekit,style=None)6 s 渲染逐样本与 v0.6 相同(v1.0.1 起该路径只保留合成原语,不再承担成片配乐)。
@@ -1,7 +1,7 @@
1
1
  # 音频:采样级混音、配乐与音效
2
2
 
3
3
  > **v0.3.0 新增三节**(§10–§12 是新的推荐做法,§1–§9.5 是底层原理与手写示例,仍然有效):
4
- > §10 提示点系统(声音跟着画面走)、§11 程序化配乐(scorekit)、§12 程序化音效(sfxkit)。
4
+ > §10 提示点系统(声音跟着画面走)、§11 程序化配乐(musicgen 参数化编排)、§12 程序化音效(sfxkit)。
5
5
  > 先读 §10——它决定了另外两节怎么用。
6
6
 
7
7
  ## 1. 为什么不用 ffmpeg 混音
@@ -139,33 +139,33 @@ trimmed = K.trim_silence(pcm, thresh_db=-42.0, keep_ms=80) # 不是 trim_tail
139
139
  # 轻 EQ:200 Hz 以下衰减 2 dB(去闷),3 kHz 提 1 dB(清晰度)
140
140
  ```
141
141
 
142
- ### 7.1 声线(persona)与语气(tone)—— v0.5.0
142
+ ### 7.1 声线(edge voice id)与语气(tone)—— v1.0.1 起没有 persona 身份表
143
143
 
144
- v0.4 及以前全片只有一个 `voice/rate/pitch`,于是"每部片的旁白都差不多"。
145
- v0.5 引入 **persona(声线)× tone(语气档)**,由 `voicekit.py` 解析成逐句 spec:
144
+ > **v0.5 的 persona 表(`bright_female` / `narrator_male` 之类 9 个身份)在 v1.0.0 已删除。**
145
+ > 现在 `voice` 只认**裸 edge-tts voice id**(如 `zh-CN-YunxiNeural`)+ 实测响度补偿表
146
+ > `assets/voice_loudness.json`;tone 的 6 档(hook/body/quote/outro/emphasis/calm)保留。
146
147
 
147
148
  ```json
148
- {"voice": {"map": {"hook": "bright_female", "body": "narrator_female", "quote": "authority_male"},
149
+ {"voice": {"id": "zh-CN-YunxiNeural",
150
+ "map": {"hook": "zh-CN-YunxiaNeural", "body": "zh-CN-YunxiNeural",
151
+ "quote": "zh-CN-YunyangNeural", "outro": "zh-CN-YunxiNeural"},
149
152
  "jitter": 0.35, "max_distinct": 3}}
150
153
  ```
151
154
 
152
155
  ```bash
153
- python voicekit.py --list # 9 个 persona + 6 个 tone(含实测响度补偿 gain_db)
154
- python voicekit.py --plan film.json # 打印某份剧本的逐句声线计划(不发请求)
156
+ python <SKILL_DIR>/scripts/voicekit.py --list # 音色响度表(裸 id + 实测 gain_db)与 6 档 tone
157
+ python <SKILL_DIR>/scripts/voicekit.py --plan film.json # 打印逐句声线计划(不发请求)
155
158
  ```
156
159
 
157
- - **persona** 决定 edge-tts 音色 id + 基线 rate/pitch/volume/gain_db;
160
+ - **voice id** 直接就是 edge-tts 的音色名;`--list` 里带的是**实测响度补偿**(不同音色天然差 3–7 dB,
161
+ 实测跨度 6.0 dB)。写了表里没有的音色,补偿回落 0 = 旁白电平没校准。
158
162
  - **tone** 决定相对偏移与句前/句后停顿:hook / body / quote / outro / emphasis / calm
159
- (`body` 严格等于 v0.4 基准:lead 0.62 / tail 0.92 / 所有 delta=0);
160
- - **内容规则**会自动升级语气:引号句 -> quote(并换到 quote 声线)、数字密集 -> emphasis、
161
- 问句/感叹 -> 稍快稍亮、长句 -> 降速加尾部留白;显式 `lines[].voice/tone` 优先级最高;
162
- - **jitter**(默认 0.35)用 sha1 派生 ±1.5% / ±1Hz / ±0.04s 的微扰,去掉"每句等距等音量"的机械感;
163
+ (`body` 严格等于 v0.4 基准:lead 0.62 / tail 0.92 / 所有 delta=0)。
164
+ - **内容规则**会自动升级语气:引号句 → quote(并换到 quote 声线)、数字密集 → emphasis、
165
+ 问句/感叹 → 稍快稍亮、长句 → 降速加尾部留白;显式 `lines[].voice/tone` 优先级最高。
166
+ - **jitter**(默认 0.35)用 sha1 派生 ±1.5% / ±1Hz / ±0.04s 的微扰,去掉"每句等距等音量"的机械感。
163
167
  - **governor** 限制全片最多 `max_distinct` 把声音、相邻句不换人 —— 超过就变成广播剧,不是旁白。
164
168
 
165
- **响度校准**:不同音色的响度天然差 3–7 dB(实测 narrator_female 比 narrator_male 响约 5.7 dB),
166
- persona 表里的 `gain_db` 是**实测回填**的(`voicekit.py --calibrate` 可复测)。
167
- 混音侧每句只应用一次增益(v0.5 顺手修掉了 v0.4 `voice_gain` 被乘两次的静默 bug)。
168
-
169
169
  **缓存键 v2**:`sha1(v2|text|voice|rate|pitch|volume|trim|trim_db)`。v0.4 的 key 不含 trim,
170
170
  于是把 `trim_silence` 改成 false 仍会命中旧的裁剪版 —— "关了裁剪其实没关"。v1 key 仍然保留给老缓存。
171
171
 
@@ -306,25 +306,27 @@ python qa_frames.py out/film.mp4 --cues work/cues.json # 看音画同步 dt
306
306
  **音画同步 dt > 60 ms 就是错**:人眼/耳对 40–60 ms 以内的错位不敏感,超过就会"感觉哪里不对但说不出来"。
307
307
  dt 大的典型原因:cue 挂在字幕出现的时刻上,而画面重击晚 100 ms——应该挂在动画时刻上。
308
308
 
309
- ## 11. 程序化配乐(scorekit,v0.3.0)
309
+ ## 11. 程序化配乐(musicgen,v1.0.1)
310
+
311
+ > **v1.0.1 起没有「配乐模板」了。** 以前这一节讲的是 scorekit 的 9 套 `MUSIC_STYLES`
312
+ > 风格表(选一个 id 得到一种配乐性格)——那套表已删除。现在配乐由 `musicgen.py`
313
+ > 按参数**现场编排**:没有可选曲风,只有参数。下面仍然是「一张和弦表贯穿全片」的技术思路,
314
+ > 实现落到了参数化编排器上。
310
315
 
311
316
  ```python
312
- import scorekit as sk
313
- samples, report = sk.render(389.0, bpm=120, key="Dm",
314
- anchors=timeline["scenes"], # [{id,kind,start,end}]
315
- seed=7) # 同 seed 逐采样一致
316
- sk.save("work/music.wav", samples)
317
+ import musicgen as MG
318
+ # 参数就是「设计」:bpm / key / scale / mood / seed,加可选的编排参数
319
+ samples, report = MG.render(389.0, bpm=120, key="Dm", scale="minor",
320
+ mood="steady", seed=7) # 同参数逐采样一致
321
+ MG.save("work/music.wav", samples)
317
322
  ```
318
323
 
319
- 或者用面向对象的写法(要更细的控制时):
324
+ 要更细的控制就传 `sections` / `progressions` / `motif` / `drums` / `voices` ——
325
+ 它们真的参与事件展开,不是死旋钮(接口见 `musicgen.py` 模块 docstring)。
320
326
 
321
- ```python
322
- sc = sk.Score(389.0, bpm=120, key="Dm", seed=7)
323
- sc.plan_from_anchors(timeline["scenes"], default_energy=0.5) # 自动分段,边界对齐小节
324
- sc.section("finale", 360.0, 389.0, energy=0.95)
325
- sc.suck(360.0, db=-6, dur=0.22) # 最终重击前"吸一口气"
326
- samples = sc.render()
327
- ```
327
+ 底层原语(吸气、分段能量、过门)仍在 `scorekit.py` 里,但**它不是配乐引擎**:
328
+ 它只提供和声表 / 律动层 / 混音 / 响度测量这些可复用件。要自己拼一条非常规配乐可以直接
329
+ 调它的 `Score` 再用 `scorekit.save()` 落盘;成片链路用的是 musicgen。
328
330
 
329
331
  ### 11.1 为什么"一张和弦表贯穿全片"
330
332
 
@@ -439,7 +441,7 @@ v0.5 的 style pack 决定的是**风格**(8 套),不是**这一部的具
439
441
 
440
442
  | 层 | 行为 | 开关 |
441
443
  |---|---|---|
442
- | 配乐和声/动机/鼓型 | 内容指纹做稳定局部替换(每两小节、±1 级数、ghost 击) | `style: auto`(默认);`seed_from_content: false` **只关 seed 派生**(音符细节不再随内容变),结构变奏照旧——`render.py` 只要不是 legacy 就把 `fingerprint` 传给 scorekit(`st_score` 里 `fp_hex` 那段)。想连结构一起回到旧行为只有不写 `style`(legacy) |
444
+ | 配乐和声/动机/鼓型 | v0.7 时代由内容指纹驱动结构变奏(每两小节、±1 级数、ghost 击);**v1.0.1 起改由 musicgen 的编排参数直接决定**(没有可挑的曲风表) | `music.seed_from_content` 仍控制 seed 是否掺指纹;结构差来自 `music.sections` / `music.progressions` 等参数,而不是「换一套风格」 |
443
445
  | 音效 seed | `content_seed(base, style, fp, type, t)`,随内容变 | `sfx.content_seed`(默认 true;false = 旧 seed_base+i) |
444
446
  | 提示音调性 | `in_key(freq, key)` 跟随配乐调性 | `CueBus.build_audio(key=...)`,render.py 自动传 |
445
447
  | 段落边界 | [边界-2.2s, 边界] 内的 whoosh/riser 自动 -3 dB | `sfx.duck_transitions`(默认 true) |
@@ -477,6 +479,6 @@ D 组构造"有人声段 + 间隙",量人声段旁白/背景响度差。任何
477
479
  ### 13.4 改音频代码后的自检清单
478
480
 
479
481
  1. `python <REPO>/tools/audio_similarity.py --strict` —— A/B/C 三组达标(在包根执行)。
480
- 2. `python selfcheck.py` —— 55 条全 PASS(第 33 条盯 legacy 逐样本兼容,第 35–38 条盯旁白优先,第 39 条盯音频边界)。
481
- 3. `python scorekit.py --selftest` / `python sfxkit.py --selftest` —— 各自全 PASS。
482
- 4. 改 `assets/scenes.py` 的 `cues()` 后,跑一次 `--stage cues` 看类型分布是否覆盖 hook/body/quote/outro。
482
+ 2. `python <SKILL_DIR>/scripts/selfcheck.py` —— **67 条**全 PASS(第 33 条盯 legacy 逐样本兼容,第 35–38 条盯旁白优先,第 39 条盯音频边界)。
483
+ 3. `python musicgen.py --selftest` / `python sfxkit.py --selftest` —— 各自全 PASS(musicgen 的 26 项覆盖编排与确定性)。
484
+ 4. 改 `assets/scenes.py` 的 `cues()` 后,跑一次 `--stage cues` 看类型分布是否覆盖 hook/body/quote/outro。
@@ -23,7 +23,7 @@
23
23
  | `lines[].sub`(字幕文案) | sub.srt / sub.ass / timeline 的字幕窗口 / frames / mux / qa | `layout → frames → mux → qa`(TTS 可复用) | 帧数÷40 s |
24
24
  | `lines[].hold` / `lead` / `tail` / `bar` | timeline / cues / frames / mux / qa | 同上 | 帧数÷40 s |
25
25
  | `lines[].kind` | 声线计划 / 配乐分段 / 画面版式(scenes 按 kind 选绘制函数) | `style → tts → layout → …` 全链 | 全片 |
26
- | `style`(含 `"auto"` 结果变了) | style.json / voice_plan / tts(换声线时) / score / audio / frames / qa | 看 `--stage explain`:通常是全链 | 全片 |
26
+ | `work/design.json`(设计变了:配色/字体/母题/版式/字幕/配乐/声线) | style.json / storyboard / voice_plan / tts(换声线时) / score / audio / frames / qa | 看 `--stage explain`:通常是全链 | 全片 |
27
27
  | `bpm` / `music.*` | score / audio / mux | `score → audio → mux → qa` | 秒级 |
28
28
  | `sfx.cues` / `scenes.py 的 cues()` | cues.json / sfx.wav / audio.wav / mux | `cues → sfx → audio → mux → qa` | 秒级 |
29
29
  | `scenes.py`(画面代码) | frames / qa(参考帧失效) | `frames → mux → qa`(音轨可复用) | 帧数÷40 s |
@@ -36,7 +36,7 @@
36
36
  | 错误做法 | 后果 | 正确做法 |
37
37
  |---|---|---|
38
38
  | 改了 `text` 只重跑 `tts` | 新语音配旧时间轴 → 字幕漂移、音效错位 | 跑 `--stage explain`,按它给的最小重跑命令 |
39
- | 改了 `style` 只重跑 `frames` | 画风换了但配乐/声线还是旧的(或反之) | `style` 是所有下游的上游,通常整链重跑 |
39
+ | 改了 `design.json`(或重新 `--confirm` 了另一份)只重跑 `frames` | 画风换了但配乐/声线还是旧的(或反之) | 设计是所有下游的上游,通常整链重跑;`--stage style` 先看解析结果 |
40
40
  | 手工删 `work/segments/_plan.json` 绕过指纹 | 拼接出的片子画面和旁白对不上,且**不报错** | 删 `work/segments` 整目录重渲 |
41
41
 
42
42
  ## 3. 一条命令代替这张表
@@ -51,4 +51,4 @@ python scripts/stagewatch.py --work work --json # 机读(给脚本/agent 用
51
51
 
52
52
  签核(`--stage accept`)绑的是**内容指纹**(剧本 + scenes.py + 引擎)。
53
53
  所以:**改了上面表里任何一项,之前的签核都会自动失效**,需要重新看、重新签。
54
- 这不是麻烦,是这套机制存在的意义——否则"我签过了"会变成一张永远有效的空头支票。
54
+ 这不是麻烦,是这套机制存在的意义——否则"我签过了"会变成一张永远有效的空头支票。
@@ -122,7 +122,7 @@ r[t] = |F[t+1] - F[t-1]| # 同一块上的"前后两帧彼此差多少"
122
122
  | 判定 | 记 error | 跳过 |
123
123
 
124
124
  没有这道闸时,**每一段"缓动入场 / 逐字入场 / 转场黑场起坡"的最快帧都会被报成闪烁**。
125
- 实测反例(本技能自带的 `assets/web/` 模板片):12 s 的片子里报了 3 处——
125
+ 实测反例(本技能自带的 `assets/web/` 示例片):12 s 的片子里报了 3 处——
126
126
 
127
127
  | 时刻 | score | 真相 |
128
128
  |---|---|---|
@@ -188,7 +188,7 @@ drift[t] = max over 8x8 blocks of mean |F[t] - F[t-lag]| * 255 (lag = FLAT_L
188
188
  人眼看到的就是一张静止的图。用"最活跃块"而不是"全帧均值",是因为逐字入场、数字滚动、卡片切换
189
189
  只占全屏几个百分点,全帧均值几乎不动,而观众显然看得到它们在动。
190
190
 
191
- 为什么旧判据要废:全帧均值对局部动画天生不敏感。自带模板片(30.10 s)实测误报 **4 段**
191
+ 为什么旧判据要废:全帧均值对局部动画天生不敏感。自带示例片(30.10 s)实测误报 **4 段**
192
192
  (411/814 个滑动窗口命中),而画面一直在动。新判据在同一片上 **0 段**。
193
193
 
194
194
  实测分布(160×90 解码,H.264):