@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
|
@@ -2160,10 +2160,15 @@ def _parse_frames(text: str) -> list:
|
|
|
2160
2160
|
|
|
2161
2161
|
|
|
2162
2162
|
def _resolve_style_spec(text):
|
|
2163
|
-
"""解析 --style
|
|
2163
|
+
"""解析 --style 的**两种**写法:内联 JSON / JSON 文件路径。
|
|
2164
2164
|
|
|
2165
2165
|
返回 normalize_style 能吃的 dict;空字符串返回 None(= 不注入,旧行为)。
|
|
2166
2166
|
解析失败一律 SystemExit(CLI 场景,带可操作提示),不做静默回落。
|
|
2167
|
+
|
|
2168
|
+
v1.0.1:**不再接受"stylekit 风格 id"这种写法**。内置风格表已删除,
|
|
2169
|
+
`--style magazine` 这种用法没有对象了。早先这里会调 ST.get()/ST.theme_of(),
|
|
2170
|
+
而那两个函数现在只会抛 KeyError —— 与其让它抛一个"没有风格 X"的错,
|
|
2171
|
+
不如直接把写法和替代方案说清楚。
|
|
2167
2172
|
"""
|
|
2168
2173
|
s = str(text or "").strip()
|
|
2169
2174
|
if not s:
|
|
@@ -2179,24 +2184,20 @@ def _resolve_style_spec(text):
|
|
|
2179
2184
|
return json.loads(p.read_text(encoding="utf-8"))
|
|
2180
2185
|
except json.JSONDecodeError as exc:
|
|
2181
2186
|
raise SystemExit("[错误] --style 的 JSON 文件 %s 解析失败:%s" % (p, exc))
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
pack = ST.get(s)
|
|
2190
|
-
except KeyError as exc:
|
|
2191
|
-
raise SystemExit("[错误] %s" % (exc.args[0] if exc.args else exc))
|
|
2192
|
-
return {"style": s, "style_name": pack.get("name"), "legacy": ST.is_legacy(s),
|
|
2193
|
-
"theme": ST.theme_of(s), "layout_default": (pack.get("layouts") or ("left-stack",))[0]}
|
|
2187
|
+
raise SystemExit(
|
|
2188
|
+
"[错误] --style %r 不是 JSON、也不是存在的文件——而**按内置风格 id 取外观这条路已经没有了**。\n"
|
|
2189
|
+
" v1.0.1 删除了全部内置风格/主题表(本技能不提供模板)。现在注入外观只有两种写法:\n"
|
|
2190
|
+
" ① --style '{\"theme\": {...}, \"canvas\": {...}}' # 内联 JSON\n"
|
|
2191
|
+
" ② --style work/style.json # 由 render.py --stage style 落盘\n"
|
|
2192
|
+
" 想生成一套新外观:python propose.py --brief \"题材\" --script film.json\n"
|
|
2193
|
+
" 详细契约见 references/web-engine.md 的 __STYLE__ 段。" % (s,))
|
|
2194
2194
|
|
|
2195
2195
|
|
|
2196
2196
|
def _add_style_args(p, *, fps=True):
|
|
2197
2197
|
"""给子命令加风格参数(缺省 = v0.7 旧行为:不注入、1920x1080)。"""
|
|
2198
2198
|
p.add_argument("--style", default="",
|
|
2199
|
-
help="
|
|
2199
|
+
help="外观载荷:work/style.json / 内联 JSON;缺省不注入"
|
|
2200
|
+
"(v1.0.1 起不接受风格 id:内置风格表已删除)")
|
|
2200
2201
|
p.add_argument("--width", type=int, default=W, help="画布宽(只收 16:9 横屏偶数,默认 %d)" % W)
|
|
2201
2202
|
p.add_argument("--height", type=int, default=H, help="画布高(默认 %d)" % H)
|
|
2202
2203
|
if fps:
|
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
|
|
10
10
|
四组对照(阈值来自 skills/video-studio/references/audio-variation.md §5):
|
|
11
11
|
|
|
12
|
-
A 跨片配乐(
|
|
13
|
-
|
|
12
|
+
A 跨片配乐(v1.0.1 起用 musicgen 参数化编排):
|
|
13
|
+
同参数/同 bpm/同调、只换内容指纹 -> 相关性(目标 <= 0.85);
|
|
14
|
+
换编排参数(mood + 音色) -> 相关性(目标 <= 0.52)
|
|
14
15
|
B 跨片音效(v0.7):同 cue 序列,legacy(不传 style/content_fp)必须逐样本相同;
|
|
15
16
|
不同 content_fp 必须不同(否则"跨片音效都一样"没被修掉)
|
|
16
17
|
C 片内分离(v0.7):duck 默认深度(目标 >= 3.5 dB)与配乐/音效的频谱质心对照
|
|
@@ -21,6 +22,7 @@
|
|
|
21
22
|
from __future__ import annotations
|
|
22
23
|
|
|
23
24
|
import argparse
|
|
25
|
+
import hashlib
|
|
24
26
|
import re
|
|
25
27
|
import sys
|
|
26
28
|
from pathlib import Path
|
|
@@ -32,12 +34,12 @@ SCRIPTS = HERE.parent / "skills" / "video-studio" / "scripts"
|
|
|
32
34
|
sys.path.insert(0, str(SCRIPTS))
|
|
33
35
|
|
|
34
36
|
# 契约 §5 的阈值(改契约时两边一起改)
|
|
35
|
-
THRESH_A_SAME_STYLE_MAX = 0.85 #
|
|
36
|
-
THRESH_A_CROSS_STYLE_MAX = 0.52 #
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
#
|
|
37
|
+
THRESH_A_SAME_STYLE_MAX = 0.85 # 同参数、只换内容指纹的相关性上限
|
|
38
|
+
THRESH_A_CROSS_STYLE_MAX = 0.52 # 换编排参数的相关性上限(v0.7 改造前的 0.534)
|
|
39
|
+
# v1.0.1:配乐改由 musicgen 参数化编排(scorekit 的 9 套风格表已删除),这两条阈值
|
|
40
|
+
# 仍是「跨片不能太像」的验收线:同参数换指纹要 <=0.85,换编排参数要 <=0.52。
|
|
41
|
+
# 生成链是确定性的,换平台/换 numpy 若把实测值推过线,先查 musicgen 的参数有没有
|
|
42
|
+
# 变成死旋钮,别第一反应去抬阈值。
|
|
41
43
|
THRESH_C_DUCK_MIN = 3.5 # duck 默认深度下限(dB)
|
|
42
44
|
THRESH_D_SNR_MIN = 12.0 # 人声段 旁白/背景 响度差下限(dB)
|
|
43
45
|
|
|
@@ -101,10 +103,35 @@ def _utf8_stdio():
|
|
|
101
103
|
pass
|
|
102
104
|
|
|
103
105
|
|
|
104
|
-
def _music(dur,
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
106
|
+
def _music(dur, profile, fp):
|
|
107
|
+
"""用参数化编排器现场生成一条配乐(v1.0.1:scorekit 的 9 套风格表已删除)。
|
|
108
|
+
|
|
109
|
+
profile 是「一套编排参数」,对应旧版的一个 style id —— 换成 mood + 音色 + 和弦进行:
|
|
110
|
+
* "steady" -> 稳重:pad=saw / bass=sine / arp=pluck / lead=bell
|
|
111
|
+
* "magazine" -> 换一套色调:triangle 铺底 + bell 琶音 + hopeful
|
|
112
|
+
fp 除了派生 seed,还决定**和弦进行走哪一档**(结构层):旧版 scorekit 的指纹会换
|
|
113
|
+
进行档,这里用同一思路 —— 否则只换 seed 时两段配乐的相关性接近 1.0,A1 门禁
|
|
114
|
+
就成了「永远通过」的假门禁。
|
|
115
|
+
"""
|
|
116
|
+
import musicgen as MG
|
|
117
|
+
# 指纹 -> 两个**互相独立**的派生量:seed(音色/细节)与进行档(结构)。
|
|
118
|
+
# 早先用同一个 8 位片段同时取模,content-a / content-b 正好撞在一起(seed 与档位
|
|
119
|
+
# 全同 -> corr 恒 1.0,A1 门禁变成永远通过)。分开取片段才是真的"同一内容同一首、
|
|
120
|
+
# 换内容不像同一首"。
|
|
121
|
+
digest = hashlib.sha1(str(fp or "").encode("utf-8")).hexdigest()
|
|
122
|
+
seed = 7 + (int(digest[:12], 16) % (2 ** 31 - 1))
|
|
123
|
+
prog_idx = int(digest[12:20], 16) % 2
|
|
124
|
+
voices = {"steady": {"pad": "saw", "bass": "sine", "arp": "pluck", "lead": "bell"},
|
|
125
|
+
"magazine": {"pad": "triangle", "bass": "sine", "arp": "bell", "lead": "pluck"}}[profile]
|
|
126
|
+
progressions = [
|
|
127
|
+
{"calm": ["i", "VI", "iv", "v"], "body": ["i", "VI", "III", "VII"],
|
|
128
|
+
"drive": ["i", "VII", "VI", "V"], "climax": ["i", "VII", "VI", "V"]},
|
|
129
|
+
{"calm": ["i", "iv", "VI", "i"], "body": ["i", "III", "VII", "VI"],
|
|
130
|
+
"drive": ["i", "VI", "VII", "V"], "climax": ["i", "VI", "VII", "V"]},
|
|
131
|
+
][prog_idx]
|
|
132
|
+
samples, _rep = MG.render(float(dur), bpm=110.0, key="Dm", scale="minor",
|
|
133
|
+
mood="steady" if profile == "steady" else "hopeful",
|
|
134
|
+
seed=seed, voices=voices, progressions=progressions)
|
|
108
135
|
return samples
|
|
109
136
|
|
|
110
137
|
|
|
@@ -120,7 +147,7 @@ def _cue_track(content_fp=None, style=None, key=None):
|
|
|
120
147
|
|
|
121
148
|
|
|
122
149
|
def _voice_priority_snr(sr=48000, guard_snr_db=12.0):
|
|
123
|
-
"""量"人声比背景响多少":用真实
|
|
150
|
+
"""量"人声比背景响多少":用真实 musicgen 配乐 + 类人声,按 st_audio 的默认参数混音。
|
|
124
151
|
|
|
125
152
|
返回 (人声活跃区 SNR dB, 间隙背景 RMS, 人声处背景额外让位 dB)。
|
|
126
153
|
口径与 `filmkit.voice_snr` 一致:只统计人声活跃区(含间隙会稀释问题)。
|
|
@@ -130,16 +157,16 @@ def _voice_priority_snr(sr=48000, guard_snr_db=12.0):
|
|
|
130
157
|
"""
|
|
131
158
|
import filmkit as K
|
|
132
159
|
import numpy as np
|
|
133
|
-
import
|
|
160
|
+
import musicgen as MG
|
|
134
161
|
n = int(6.0 * sr)
|
|
135
162
|
t = np.arange(n) / float(sr)
|
|
136
163
|
voice = np.zeros(n, dtype=np.float64)
|
|
137
164
|
for a, b in ((0.2, 2.6), (3.4, 5.8)): # 两段"说话",中间留间隙
|
|
138
165
|
i, j = int(a * sr), int(b * sr)
|
|
139
166
|
voice[i:j] = 0.12 * np.sin(2 * np.pi * 220 * t[i:j])
|
|
140
|
-
# 真实配乐(
|
|
141
|
-
music, _ =
|
|
142
|
-
|
|
167
|
+
# 真实配乐(musicgen 输出峰值 -6 dBFS,比"类人声"响 —— 正是需要侧链/兜底的场景)
|
|
168
|
+
music, _ = MG.render(6.0, bpm=110.0, key="Am", scale="minor",
|
|
169
|
+
mood="steady", seed=7)
|
|
143
170
|
music = np.asarray(music, dtype=np.float32)
|
|
144
171
|
out = K.mix_film(voice.astype(np.float32), music, None, total=6.0, fade=0.0,
|
|
145
172
|
music_gain_db=-12.0, voice_duck_db=6.0,
|
|
@@ -174,29 +201,29 @@ def main(argv=None):
|
|
|
174
201
|
import sfxkit as SX
|
|
175
202
|
|
|
176
203
|
print("=" * 72)
|
|
177
|
-
print("A. 跨片配乐(差异来源 =
|
|
204
|
+
print("A. 跨片配乐(差异来源 = 编排参数 / bpm / 调 / 内容指纹 / seed)")
|
|
178
205
|
print("=" * 72)
|
|
179
|
-
a1 = _music(args.dur, "
|
|
180
|
-
a2 = _music(args.dur, "
|
|
206
|
+
a1 = _music(args.dur, "steady", "content-a")
|
|
207
|
+
a2 = _music(args.dur, "steady", "content-b")
|
|
181
208
|
a3 = _music(args.dur, "magazine", "content-a")
|
|
182
|
-
a4 = _music(args.dur, "
|
|
209
|
+
a4 = _music(args.dur, "steady", "content-a") # 同指纹复渲染,验证确定性
|
|
183
210
|
same_style = _corr(a1, a2)
|
|
184
211
|
cross_style = _corr(a1, a3)
|
|
185
212
|
rms_a1 = _rms(a1)
|
|
186
213
|
print(" 配乐非静音(RMS > -60 dBFS) : %s (rms=%.4f,静音时相关系数无定义)"
|
|
187
214
|
% (rms_a1 > 0.001, rms_a1))
|
|
188
|
-
print("
|
|
215
|
+
print(" 同参数·同bpm·同调,只换内容指纹 : corr = %.3f (目标 <= %.2f)"
|
|
189
216
|
% (same_style, THRESH_A_SAME_STYLE_MAX))
|
|
190
|
-
print("
|
|
217
|
+
print(" 换编排参数(steady -> magazine) : corr = %.3f (目标 <= %.2f)"
|
|
191
218
|
% (cross_style, THRESH_A_CROSS_STYLE_MAX))
|
|
192
219
|
print(" 同指纹两次渲染逐样本相同 : %s" % bool(np.array_equal(a1, a4)))
|
|
193
220
|
if args.strict:
|
|
194
221
|
# A0 是前置条件:若配乐渲染成静音/常量,A1/A2 的相关系数没有定义(NaN),
|
|
195
222
|
# 必须显式判 FAIL,而不是让 NaN 比较悄悄变成 False 后只剩一句 corr=nan。
|
|
196
223
|
_ok("A0 配乐非静音", rms_a1 > 0.001, "rms=%.4f" % rms_a1)
|
|
197
|
-
_ok("A1
|
|
224
|
+
_ok("A1 同参数换指纹可区分", same_style <= THRESH_A_SAME_STYLE_MAX,
|
|
198
225
|
"corr=%.3f" % same_style)
|
|
199
|
-
_ok("A2
|
|
226
|
+
_ok("A2 换编排参数更不像", cross_style <= THRESH_A_CROSS_STYLE_MAX,
|
|
200
227
|
"corr=%.3f" % cross_style)
|
|
201
228
|
_ok("A3 配乐可复现", bool(np.array_equal(a1, a4)), "同指纹逐样本一致")
|
|
202
229
|
|
|
@@ -256,10 +283,10 @@ def main(argv=None):
|
|
|
256
283
|
if args.out:
|
|
257
284
|
out = Path(args.out)
|
|
258
285
|
out.mkdir(parents=True, exist_ok=True)
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
286
|
+
import musicgen as MG
|
|
287
|
+
MG.save(out / "music_steady_a.wav", a1)
|
|
288
|
+
MG.save(out / "music_steady_b.wav", a2)
|
|
289
|
+
MG.save(out / "music_magazine.wav", a3)
|
|
263
290
|
for t in ("whoosh", "riser", "impact"):
|
|
264
291
|
SX.save(out / ("sfx_%s.wav" % t), SX.render(t, seed=1000))
|
|
265
292
|
print("\n对照样本已写入: %s" % out)
|
|
@@ -276,4 +303,4 @@ def main(argv=None):
|
|
|
276
303
|
|
|
277
304
|
|
|
278
305
|
if __name__ == "__main__":
|
|
279
|
-
raise SystemExit(main())
|
|
306
|
+
raise SystemExit(main())
|
package/tools/ci_check.py
CHANGED
|
@@ -59,7 +59,13 @@ LOCAL_MODULES = {"filmkit", "sfxkit", "scorekit", "webengine", "qa_frames", "ren
|
|
|
59
59
|
# v0.8.0:渲染前资源闸门、全片 VMAF/SSIM 门禁、ASR 回读门禁
|
|
60
60
|
"preflight", "qavmaf", "qaasr",
|
|
61
61
|
# v0.9.0:场景语法(内容 -> 构图选型)。纯标准库,可被 scenes.py 反向 import
|
|
62
|
-
"grammar"
|
|
62
|
+
"grammar",
|
|
63
|
+
# v1.0:设计 DSL / 离线预览 / 提案器(D3 改造的第一期)
|
|
64
|
+
"designkit", "previewkit", "propose",
|
|
65
|
+
# v1.0.1:presetkit 已删除(连同 presets/ 一起);这里不能再列它,
|
|
66
|
+
# 否则 ci_check 会去 import 一个不存在的模块
|
|
67
|
+
# v1.0 / P2:参数化配乐编排器(替代 9 套 MUSIC_STYLES 表)
|
|
68
|
+
"musicgen"}
|
|
63
69
|
MAX_FILE_BYTES = 400 * 1024
|
|
64
70
|
# 体积检查忽略的目录后缀:.team 是本地调研/自测工作区(gitignore),不随包发布;
|
|
65
71
|
# 媒体文件本来就该大(样例片/参考视频),不该拿"能不能塞进上下文"的尺子量。
|
package/tools/estimate.py
CHANGED
|
@@ -50,9 +50,14 @@ WEB_SEC_PER_FRAME = (0.080, 0.130) # web 引擎:Edge 无头 PNG 截图 80–1
|
|
|
50
50
|
WEB_STARTUP_SEC = 6.0 # 浏览器冷启动 + CDP 连接(12 s 片 28–30 s 含启动)
|
|
51
51
|
X264_FPS = 141.0 # libx264 -preset medium -crf 17 ≈ 4.7× 实时(仅 reencode_video 用)
|
|
52
52
|
TTS_SEC_PER_LINE = (2.0, 4.0) # edge-tts:每句每 worker 的墙钟(联网,波动大):实测 2.2 s,留到 4.0
|
|
53
|
-
|
|
54
|
-
#
|
|
55
|
-
#
|
|
53
|
+
# 配乐耗时锚点 (片长 s, 耗时低, 耗时高)。
|
|
54
|
+
#
|
|
55
|
+
# v1.0.1 换成 musicgen 实测值:30 s / 60 s / 120 s 分别 0.94 / 1.93 / 3.68 s(≈31× 实时),
|
|
56
|
+
# 比 scorekit 时代快一个数量级。为什么现在敢改:以前配乐走 scorekit 的 9 套风格表,
|
|
57
|
+
# 那个引擎要逐层展开事件并做多次 IIR,实测 29 s 片 3.6–6.5 s;现在的参数化编排器
|
|
58
|
+
# 是单遍事件合成,实测抖动也小(同参数逐采样确定,耗时只随片长线性涨)。
|
|
59
|
+
# 区间仍给 0.7–1.5 系数包络:CI 机器与共享 CPU 会慢,报小了会让人以为卡住。
|
|
60
|
+
SCORE_REFS = ((29.0, 0.9, 1.6), (58.0, 1.8, 3.0), (389.0, 11.5, 19.0))
|
|
56
61
|
SFX_SEC_PER_CUE = 0.20 # 单条短音效 合成+落位(24 配方单次 ≤1.5 s 是上限口径)
|
|
57
62
|
SFX_MIN_SEC = 0.8 # 实测 18 条 cue 的模板片 sfx 墙钟 0.8 s(含 ffmpeg 写盘)
|
|
58
63
|
LAYOUT_BASE, LAYOUT_PER_LINE = 0.05, 0.01 # 排轴 + srt/ass + 安全区/字形自查(实测 29 s 片 <0.15 s)
|
|
@@ -81,7 +86,7 @@ CALIBRATION = dict(
|
|
|
81
86
|
"(是否同一台未核实,只保证这些数字是在上述 24 逻辑核机器上测的)"),
|
|
82
87
|
measured_at="2026-11-10",
|
|
83
88
|
sdk="video-studio v0.7.1 scripts(ffmpeg 自带 x264)",
|
|
84
|
-
film="29.011 s / 870 帧 /
|
|
89
|
+
film="29.011 s / 870 帧 / 测试样片(assets/film.json + assets/scenes.py,仅用于基准校准)",
|
|
85
90
|
method="render.py --stage all --jobs 2,逐 stage 取 [time] 行墙钟;同片重复 2 次",
|
|
86
91
|
observed=dict(tts=2.2, layout=0.0, cues=0.0, score=6.3, sfx=0.8, audio=0.7,
|
|
87
92
|
frames=9.7, mux=1.9, qa=6.8, total=28.4),
|
|
@@ -152,8 +157,9 @@ def load_script(path, as_json=False):
|
|
|
152
157
|
"""读剧本;坏输入给一句话错误(退出码 2),不甩 traceback。"""
|
|
153
158
|
p = Path(path).expanduser()
|
|
154
159
|
if not p.exists():
|
|
155
|
-
_die("找不到剧本 %s(--script
|
|
156
|
-
|
|
160
|
+
_die("找不到剧本 %s(--script 指向你自己的 film.json;"
|
|
161
|
+
"仓库里只有一个用于测试的样例 skills/video-studio/assets/film.json,"
|
|
162
|
+
"它不是要抄的模板)" % p, as_json)
|
|
157
163
|
try:
|
|
158
164
|
raw = p.read_text(encoding="utf-8")
|
|
159
165
|
except OSError as e:
|
|
@@ -479,7 +485,7 @@ def build_report(script_path, script, args):
|
|
|
479
485
|
n_cues = len(sfx_cfg.get("cues") or [])
|
|
480
486
|
sfx_on = bool(sfx_cfg.get("enabled", True)) and n_cues > 0
|
|
481
487
|
# 配乐口径对齐 filmkit.music_plan:mode=off/none 或 enabled=false 才算关;
|
|
482
|
-
# 只有 mode=score(或没写 mode 但写了 bpm
|
|
488
|
+
# 只有 mode=score(或没写 mode 但写了 bpm)才会真跑配乐引擎(musicgen)。
|
|
483
489
|
mode = str(mus.get("mode") or "").strip().lower()
|
|
484
490
|
has_bpm = "bpm" in mus or "bpm" in script
|
|
485
491
|
music_on = bool(mus.get("enabled", True)) and mode not in ("off", "none")
|
|
@@ -570,7 +576,7 @@ def build_report(script_path, script, args):
|
|
|
570
576
|
dict(stage="score", lo=score_lo, hi=score_hi, measured=bool(score_on),
|
|
571
577
|
note=("以实测点 29 s≈3.6–6.3 s / 58 s≈4.5–5.0 s 为锚(--stage all 里与其它阶段争 CPU 会更慢;389 s 只是外推锚点)"
|
|
572
578
|
if score_on else
|
|
573
|
-
("mode=%s
|
|
579
|
+
("mode=%s:不跑配乐引擎(%s)"
|
|
574
580
|
% (mode or "-", "外部配乐/和弦床/关掉配乐")))),
|
|
575
581
|
dict(stage="sfx", lo=sfx_lo, hi=sfx_hi, measured=False,
|
|
576
582
|
note=("%d 条音效(实测 18 条 0.8 s;单条上限口径 ≤1.5 s,长环境声 wind/rain 另计)"
|
package/tools/golden_frames.py
CHANGED
|
@@ -69,6 +69,11 @@ SCRIPTS = os.path.join(SKILL, "scripts")
|
|
|
69
69
|
ASSETS = os.path.join(SKILL, "assets")
|
|
70
70
|
|
|
71
71
|
DEFAULT_SCRIPT = os.path.join(ASSETS, "film.json")
|
|
72
|
+
# v1.0 / D3:金帧基线绑"固定 fixture 设计"而不是内置风格。
|
|
73
|
+
# 为什么:金帧要守的是"同一份设计 + 同一份场景代码,像素不许漂",
|
|
74
|
+
# 不是"某套内置风格长什么样"——后者会随风格表下沉到 presets 而失去意义。
|
|
75
|
+
# 没有这个文件时回落到老口径(读 film.json 的 style),不阻塞老项目。
|
|
76
|
+
DEFAULT_DESIGN = os.path.join(ASSETS, "design_fixture.json")
|
|
72
77
|
DEFAULT_SCENES = os.path.join(ASSETS, "scenes.py")
|
|
73
78
|
DEFAULT_GOLDEN = os.path.join(ASSETS, "golden_frames.json")
|
|
74
79
|
|
|
@@ -216,11 +221,39 @@ def build_timeline(K, script):
|
|
|
216
221
|
return tl
|
|
217
222
|
|
|
218
223
|
|
|
219
|
-
def
|
|
220
|
-
"""
|
|
224
|
+
def _design_plan(design_path):
|
|
225
|
+
"""读 fixture 设计并转成计划;读不到返回 None(回落到 stylekit 老口径)。"""
|
|
226
|
+
if not design_path or not os.path.exists(str(design_path)):
|
|
227
|
+
return None
|
|
228
|
+
try:
|
|
229
|
+
with open(design_path, encoding="utf-8") as fh:
|
|
230
|
+
design = json.load(fh)
|
|
231
|
+
except (OSError, json.JSONDecodeError) as e:
|
|
232
|
+
raise RuntimeError("fixture 设计读不出来(%s):%s" % (design_path, e)) from e
|
|
233
|
+
sys.path.insert(0, SCRIPTS)
|
|
234
|
+
import designkit as DK
|
|
235
|
+
bad = DK.validate_design(design)
|
|
236
|
+
if bad:
|
|
237
|
+
raise RuntimeError("fixture 设计没过校验(%s):%s" % (design_path, ";".join(bad)))
|
|
238
|
+
return DK.design_to_plan(design)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def resolve_plan(ST, script, design_path=None):
|
|
242
|
+
"""解析计划并挂到 script["_style"]。
|
|
243
|
+
|
|
244
|
+
v1.0 / D3 起优先用 **fixture 设计**(design_path,默认 assets/design_fixture.json):
|
|
245
|
+
金帧守的是"固定设计 + 固定场景代码 -> 像素不漂",与"哪套内置风格"无关。
|
|
246
|
+
没有 fixture 时回落到 stylekit 老口径,不阻塞老项目。
|
|
221
247
|
|
|
222
248
|
主题体检失败在这里就抛错(不要等渲到第 3 帧才炸)。
|
|
223
249
|
"""
|
|
250
|
+
plan = _design_plan(design_path)
|
|
251
|
+
if plan is not None:
|
|
252
|
+
script["_style"] = plan
|
|
253
|
+
bad = ST.theme_lint(plan.get("theme") or {})
|
|
254
|
+
if bad:
|
|
255
|
+
raise RuntimeError("fixture 设计主题体检失败:%s" % ";".join(bad))
|
|
256
|
+
return plan
|
|
224
257
|
plan = ST.resolve_script_style(script)
|
|
225
258
|
bad = ST.theme_lint(plan.get("theme") or ST.THEMES[plan["theme_id"]])
|
|
226
259
|
if bad:
|
|
@@ -384,7 +417,7 @@ def default_frame_specs(tl):
|
|
|
384
417
|
|
|
385
418
|
|
|
386
419
|
# ---------------------------------------------------------------- 基线读写
|
|
387
|
-
def _golden_payload(script_path, scenes_path, K, plan, tl, rendered):
|
|
420
|
+
def _golden_payload(script_path, scenes_path, K, plan, tl, rendered, design_path=None):
|
|
388
421
|
font_fp, fonts = font_fingerprint(K, plan)
|
|
389
422
|
env_fp, env = env_fingerprint()
|
|
390
423
|
return {
|
|
@@ -398,6 +431,7 @@ def _golden_payload(script_path, scenes_path, K, plan, tl, rendered):
|
|
|
398
431
|
"fps": int(tl["fps"]),
|
|
399
432
|
"script": _rel(script_path),
|
|
400
433
|
"scenes": _rel(scenes_path),
|
|
434
|
+
"design": _rel(design_path) if design_path else "",
|
|
401
435
|
"font_fp": font_fp,
|
|
402
436
|
"fonts": fonts,
|
|
403
437
|
"env_fp": env_fp,
|
|
@@ -407,6 +441,9 @@ def _golden_payload(script_path, scenes_path, K, plan, tl, rendered):
|
|
|
407
441
|
"scenes_sha1": _sha1_file(scenes_path),
|
|
408
442
|
"filmkit_sha1": _sha1_file(os.path.join(SCRIPTS, "filmkit.py")),
|
|
409
443
|
"stylekit_sha1": _sha1_file(os.path.join(SCRIPTS, "stylekit.py")),
|
|
444
|
+
# 设计 fixture 进指纹:改了基准设计却不重录基线 = "改了但基线不报警"
|
|
445
|
+
"design_sha1": _sha1_file(design_path) if design_path else "",
|
|
446
|
+
"designkit_sha1": _sha1_file(os.path.join(SCRIPTS, "designkit.py")),
|
|
410
447
|
"golden_tool_sha1": _sha1_file(os.path.abspath(__file__)),
|
|
411
448
|
},
|
|
412
449
|
"frames": [dict(sp, sha256=sha, scene=scene_id, frame=int(f))
|
|
@@ -414,13 +451,13 @@ def _golden_payload(script_path, scenes_path, K, plan, tl, rendered):
|
|
|
414
451
|
}
|
|
415
452
|
|
|
416
453
|
|
|
417
|
-
def write_golden(script_path, scenes_path, golden_path, frames=None):
|
|
454
|
+
def write_golden(script_path, scenes_path, golden_path, frames=None, design_path=None):
|
|
418
455
|
"""--write:渲染金帧并写基线。返回 result(status=pass,written=True)。"""
|
|
419
456
|
K, ST = _modules()
|
|
420
457
|
script = _load_script(script_path)
|
|
421
458
|
scenes_mod = _load_scenes(scenes_path)
|
|
422
459
|
tl = build_timeline(K, script)
|
|
423
|
-
plan = resolve_plan(ST, script)
|
|
460
|
+
plan = resolve_plan(ST, script, design_path=design_path)
|
|
424
461
|
if frames:
|
|
425
462
|
specs = [dict(frame=int(f), scene=str(_scene_at(tl, int(f) / float(tl["fps"]))[0]
|
|
426
463
|
.get("id") or ""),
|
|
@@ -432,7 +469,8 @@ def write_golden(script_path, scenes_path, golden_path, frames=None):
|
|
|
432
469
|
specs = default_frame_specs(tl)
|
|
433
470
|
rendered = render_frames(script, scenes_mod, plan, tl, [sp["frame"] for sp in specs])
|
|
434
471
|
plan = dict(plan, specs=specs)
|
|
435
|
-
data = _golden_payload(script_path, scenes_path, K, plan, tl, rendered
|
|
472
|
+
data = _golden_payload(script_path, scenes_path, K, plan, tl, rendered,
|
|
473
|
+
design_path=design_path)
|
|
436
474
|
golden_path = os.path.abspath(str(golden_path))
|
|
437
475
|
d = os.path.dirname(golden_path)
|
|
438
476
|
if d:
|
|
@@ -475,7 +513,7 @@ def _fmt_fp_pair(actual, expect):
|
|
|
475
513
|
return "%s… vs %s…" % (str(actual)[:12], str(expect)[:12])
|
|
476
514
|
|
|
477
515
|
|
|
478
|
-
def check_golden(script_path, scenes_path, golden_path):
|
|
516
|
+
def check_golden(script_path, scenes_path, golden_path, design_path=None):
|
|
479
517
|
"""--check:渲染基线里的帧号并与基线比对(默认命令)。"""
|
|
480
518
|
golden, why = _load_golden(golden_path)
|
|
481
519
|
if golden is None:
|
|
@@ -498,7 +536,7 @@ def check_golden(script_path, scenes_path, golden_path):
|
|
|
498
536
|
script = _load_script(script_path)
|
|
499
537
|
scenes_mod = _load_scenes(scenes_path)
|
|
500
538
|
tl = build_timeline(K, script)
|
|
501
|
-
plan = resolve_plan(ST, script)
|
|
539
|
+
plan = resolve_plan(ST, script, design_path=design_path)
|
|
502
540
|
font_fp, fonts = font_fingerprint(K, plan)
|
|
503
541
|
env_fp, env = env_fingerprint()
|
|
504
542
|
# 先做"能不能比"的判定,再渲(省得换机时白渲三帧)
|
|
@@ -551,7 +589,9 @@ def check_golden(script_path, scenes_path, golden_path):
|
|
|
551
589
|
cur_inputs = dict(script_sha1=_sha1_file(script_path),
|
|
552
590
|
scenes_sha1=_sha1_file(scenes_path),
|
|
553
591
|
filmkit_sha1=_sha1_file(os.path.join(SCRIPTS, "filmkit.py")),
|
|
554
|
-
stylekit_sha1=_sha1_file(os.path.join(SCRIPTS, "stylekit.py"))
|
|
592
|
+
stylekit_sha1=_sha1_file(os.path.join(SCRIPTS, "stylekit.py")),
|
|
593
|
+
design_sha1=(_sha1_file(design_path) if design_path else ""),
|
|
594
|
+
designkit_sha1=_sha1_file(os.path.join(SCRIPTS, "designkit.py")))
|
|
555
595
|
stale = [k for k, v in cur_inputs.items()
|
|
556
596
|
if inputs.get(k) and inputs.get(k) != v]
|
|
557
597
|
if changed:
|
|
@@ -608,6 +648,9 @@ def main(argv=None):
|
|
|
608
648
|
g.add_argument("--check", action="store_true", help="校验(默认)")
|
|
609
649
|
g.add_argument("--write", action="store_true", help="生成/更新基线")
|
|
610
650
|
ap.add_argument("--script", default=DEFAULT_SCRIPT, help="film.json(默认模板片)")
|
|
651
|
+
ap.add_argument("--design", default=DEFAULT_DESIGN,
|
|
652
|
+
help="固定 fixture 设计(默认 assets/design_fixture.json);"
|
|
653
|
+
"传空串回落到老的 stylekit 口径")
|
|
611
654
|
ap.add_argument("--scenes", default=DEFAULT_SCENES, help="scenes.py(默认模板场景)")
|
|
612
655
|
ap.add_argument("--golden", default=DEFAULT_GOLDEN,
|
|
613
656
|
help="基线 JSON(默认 skills/video-studio/assets/golden_frames.json)")
|
|
@@ -627,12 +670,14 @@ def main(argv=None):
|
|
|
627
670
|
raise SystemExit("--frames 必须是逗号分隔的整数帧号,收到 %r" % a.frames)
|
|
628
671
|
if not frames:
|
|
629
672
|
raise SystemExit("--frames 里没有有效帧号")
|
|
630
|
-
res = write_golden(a.script, a.scenes, a.golden, frames
|
|
673
|
+
res = write_golden(a.script, a.scenes, a.golden, frames,
|
|
674
|
+
design_path=(a.design or None))
|
|
631
675
|
else:
|
|
632
676
|
if a.frames:
|
|
633
677
|
raise SystemExit("--frames 只用于 --write;--check 的帧号由基线决定"
|
|
634
678
|
"(要换帧号请 --write --frames ... 重录)")
|
|
635
|
-
res = check_golden(a.script, a.scenes, a.golden
|
|
679
|
+
res = check_golden(a.script, a.scenes, a.golden,
|
|
680
|
+
design_path=(a.design or None))
|
|
636
681
|
except SystemExit:
|
|
637
682
|
raise
|
|
638
683
|
except Exception as e: # noqa: BLE001
|