@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.
- package/CHANGELOG.md +424 -0
- package/README.md +287 -327
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/package.json +9 -10
- package/schemas/design.schema.json +722 -0
- package/schemas/film.schema.json +4 -15
- package/schemas/proposal.schema.json +133 -0
- package/skills/video-studio/SKILL.md +1316 -1123
- package/skills/video-studio/assets/design_fixture.json +141 -0
- package/skills/video-studio/assets/film.json +1 -2
- package/skills/video-studio/assets/golden_frames.json +11 -8
- package/skills/video-studio/assets/scenes.py +67 -22
- package/skills/video-studio/assets/voice_loudness.json +16 -0
- package/skills/video-studio/references/audio-variation.md +15 -11
- package/skills/video-studio/references/audio.md +36 -34
- package/skills/video-studio/references/change-impact.md +3 -3
- package/skills/video-studio/references/frame-qa.md +2 -2
- package/skills/video-studio/references/proposal.md +177 -0
- package/skills/video-studio/references/quality-gates.md +1 -1
- package/skills/video-studio/references/scene-grammar.md +28 -23
- package/skills/video-studio/references/sfx-design.md +1 -1
- package/skills/video-studio/references/style-packs.md +95 -257
- package/skills/video-studio/references/v08-quality.md +8 -8
- package/skills/video-studio/references/web-engine.md +1 -1
- package/skills/video-studio/references/word-timeline.md +2 -2
- package/skills/video-studio/scripts/designkit.py +2120 -0
- package/skills/video-studio/scripts/filmcheck.py +7 -33
- package/skills/video-studio/scripts/filmkit.py +153 -78
- package/skills/video-studio/scripts/grammar.py +67 -7
- package/skills/video-studio/scripts/musicgen.py +2043 -0
- package/skills/video-studio/scripts/preflight.py +10 -3
- package/skills/video-studio/scripts/previewkit.py +849 -0
- package/skills/video-studio/scripts/propose.py +825 -0
- package/skills/video-studio/scripts/qa_frames.py +45 -10
- package/skills/video-studio/scripts/qavmaf.py +77 -11
- package/skills/video-studio/scripts/render.py +472 -154
- package/skills/video-studio/scripts/scorekit.py +50 -1748
- package/skills/video-studio/scripts/selfcheck.py +658 -131
- package/skills/video-studio/scripts/stagewatch.py +21 -5
- package/skills/video-studio/scripts/stylekit.py +265 -741
- package/skills/video-studio/scripts/voicekit.py +502 -380
- package/skills/video-studio/scripts/webengine.py +15 -14
- package/tools/audio_similarity.py +57 -30
- package/tools/ci_check.py +7 -1
- package/tools/estimate.py +14 -8
- package/tools/golden_frames.py +56 -11
- package/tools/silhouette_qa.py +521 -168
- package/docs/demo-pil-sheet.png +0 -0
- package/docs/styles/demo-results.json +0 -76
- package/docs/styles/sheet-8-styles.png +0 -0
- package/docs/styles/sheet-ai.png +0 -0
- package/docs/styles/sheet-before-after.png +0 -0
- package/docs/styles/sheet-craft.png +0 -0
- package/docs/styles/sheet-night.png +0 -0
- package/skills/video-studio/assets/score_demo.json +0 -307
|
@@ -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": "
|
|
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": "
|
|
37
|
-
"scenes_sha1": "
|
|
38
|
-
"filmkit_sha1": "
|
|
39
|
-
"stylekit_sha1": "
|
|
40
|
-
"
|
|
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": "
|
|
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": "
|
|
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": "
|
|
65
|
+
"sha256": "c3ccdbedc567402728677f9f4d7ccf62bb98930bebf52934b8bb2becb0e9fb4f"
|
|
63
66
|
}
|
|
64
67
|
]
|
|
65
68
|
}
|
|
@@ -1,20 +1,26 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
|
-
"""scenes.py —
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1519
|
-
|
|
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
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
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
|
|
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
|
-
|
|
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`
|
|
75
|
-
|
|
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
|
-
|
|
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(现在
|
|
133
|
-
2. `python skills/video-studio/scripts/
|
|
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 逐样本不变:`
|
|
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
|
-
-
|
|
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 程序化配乐(
|
|
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 声线(
|
|
142
|
+
### 7.1 声线(edge voice id)与语气(tone)—— v1.0.1 起没有 persona 身份表
|
|
143
143
|
|
|
144
|
-
v0.
|
|
145
|
-
|
|
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": {"
|
|
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
|
|
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
|
-
- **
|
|
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
|
-
- **内容规则**会自动升级语气:引号句
|
|
161
|
-
问句/感叹
|
|
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. 程序化配乐(
|
|
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
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
seed=7)
|
|
316
|
-
|
|
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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
-
| 配乐和声/动机/鼓型 |
|
|
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` ——
|
|
481
|
-
3. `python
|
|
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
|
-
| `
|
|
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
|
-
| 改了 `
|
|
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/`
|
|
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
|
-
|
|
191
|
+
为什么旧判据要废:全帧均值对局部动画天生不敏感。自带示例片(30.10 s)实测误报 **4 段**
|
|
192
192
|
(411/814 个滑动窗口命中),而画面一直在动。新判据在同一片上 **0 段**。
|
|
193
193
|
|
|
194
194
|
实测分布(160×90 解码,H.264):
|