@dsh-gis/dsh-video-studio-skill 0.6.0 → 0.7.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/README.md +41 -16
- package/index.js +20 -7
- package/package.json +4 -2
- package/schemas/film.schema.json +75 -4
- package/skills/video-studio/SKILL.md +95 -23
- package/skills/video-studio/assets/film.json +7 -14
- package/skills/video-studio/assets/scenes.py +115 -10
- package/skills/video-studio/assets/web/engine.js +18 -1
- package/skills/video-studio/assets/web/film.js +29 -4
- package/skills/video-studio/assets/web/index.html +5 -0
- package/skills/video-studio/references/audio-variation.md +158 -0
- package/skills/video-studio/references/audio.md +67 -5
- package/skills/video-studio/references/change-impact.md +1 -1
- package/skills/video-studio/references/quality-gates.md +1 -1
- package/skills/video-studio/references/web-engine.md +1 -1
- package/skills/video-studio/requirements.txt +2 -1
- package/skills/video-studio/scripts/filmcheck.py +81 -12
- package/skills/video-studio/scripts/filmkit.py +271 -18
- package/skills/video-studio/scripts/gates.py +75 -12
- package/skills/video-studio/scripts/qa_frames.py +109 -19
- package/skills/video-studio/scripts/render.py +420 -51
- package/skills/video-studio/scripts/scorekit.py +289 -46
- package/skills/video-studio/scripts/selfcheck.py +302 -2
- package/skills/video-studio/scripts/sfxkit.py +132 -30
- package/skills/video-studio/scripts/stagewatch.py +141 -31
- package/skills/video-studio/scripts/stylekit.py +94 -6
- package/skills/video-studio/scripts/voicekit.py +8 -0
- package/skills/video-studio/scripts/webengine.py +68 -14
- package/tools/audio_similarity.py +279 -0
- package/tools/ci_check.py +145 -34
- package/tools/estimate.py +133 -54
- package/tools/selftest_noscipy.py +70 -0
|
@@ -29,6 +29,7 @@ v0.5.0 画风主题化
|
|
|
29
29
|
"""
|
|
30
30
|
from __future__ import annotations
|
|
31
31
|
|
|
32
|
+
import hashlib
|
|
32
33
|
import math
|
|
33
34
|
import os
|
|
34
35
|
import sys
|
|
@@ -1055,7 +1056,57 @@ def draw_frame(scene, t_local, dur, t_global, script, frame):
|
|
|
1055
1056
|
return fn(scene, t_local, dur, t_global, script, frame, plan, th)
|
|
1056
1057
|
|
|
1057
1058
|
|
|
1058
|
-
# ---------------- 提示点(v0.3.0
|
|
1059
|
+
# ---------------- 提示点(v0.3.0 起可选;v0.7 按 kind 与内容生成) ----------------
|
|
1060
|
+
def _sha1_int(*parts):
|
|
1061
|
+
"""把若干文本片段拼起来取 sha1 前 8 位(提示点"内容派生"的稳定来源)。"""
|
|
1062
|
+
raw = "|".join(str(p) for p in parts).encode("utf-8")
|
|
1063
|
+
return int(hashlib.sha1(raw).hexdigest()[:8], 16)
|
|
1064
|
+
|
|
1065
|
+
|
|
1066
|
+
def _item_pitch(text, base=1760.0):
|
|
1067
|
+
"""条目文本 -> ui_blip 音高(sha1 派生,不用序号)。
|
|
1068
|
+
|
|
1069
|
+
v0.7 契约 §3.4:body 条目的 pitch 必须由**条目文本**派生,不再按序号
|
|
1070
|
+
(旧实现 1760+110*i 每片都一样)——同一句文案换到别的片子里仍是同一个音高,
|
|
1071
|
+
不同文案落到 -5~+7 半音的不同音级;万一两条碰撞,_dedupe_segment 会再升半音。
|
|
1072
|
+
"""
|
|
1073
|
+
off = _sha1_int("pitch", text) % 13 - 5
|
|
1074
|
+
return round(float(base) * 2.0 ** (off / 12.0), 2)
|
|
1075
|
+
|
|
1076
|
+
|
|
1077
|
+
def _cue_gain_decay(gain, repeat):
|
|
1078
|
+
"""同段内同类型 cue 第 repeat 次重复(0 起)时的增益:每次再低 2 dB(-2/-4/-6/…)。
|
|
1079
|
+
|
|
1080
|
+
v0.7 契约 §3.4:不许再出现"每段都一模一样的三连击"——同类型重复必须**严格递减**
|
|
1081
|
+
(第 2 次 -2、第 3 次 -4、第 4 次 -6……),且 pitch 必须不同。
|
|
1082
|
+
"""
|
|
1083
|
+
return round(float(gain) - 2.0 * int(repeat), 2)
|
|
1084
|
+
|
|
1085
|
+
|
|
1086
|
+
def _dedupe_segment(seg):
|
|
1087
|
+
"""同一段内:同类型重复的 gain 递减、pitch 去重(保证"pitch 必须不同")。
|
|
1088
|
+
|
|
1089
|
+
正文条目多时 sha1 可能把两条吸附到同一个半音;碰撞就把后一个再升半音——
|
|
1090
|
+
仍然由文本派生,只是保证同段里不出现两个一模一样的高音(听着像卡带)。
|
|
1091
|
+
"""
|
|
1092
|
+
seen = {} # type -> {"n": 已出现次数, "pitch": 已用音高集}
|
|
1093
|
+
for c in seg:
|
|
1094
|
+
st = seen.setdefault(c["type"], {"n": 0, "pitch": set()})
|
|
1095
|
+
if st["n"]:
|
|
1096
|
+
c["gain"] = _cue_gain_decay(c.get("gain", 0.0), st["n"])
|
|
1097
|
+
st["n"] += 1
|
|
1098
|
+
p = c.get("pitch")
|
|
1099
|
+
if p is not None:
|
|
1100
|
+
p = float(p)
|
|
1101
|
+
guard = 0
|
|
1102
|
+
while round(p, 2) in st["pitch"] and guard < 12:
|
|
1103
|
+
p *= 2.0 ** (1.0 / 12.0)
|
|
1104
|
+
guard += 1
|
|
1105
|
+
c["pitch"] = round(p, 2)
|
|
1106
|
+
st["pitch"].add(round(p, 2))
|
|
1107
|
+
return seg
|
|
1108
|
+
|
|
1109
|
+
|
|
1059
1110
|
def cues(tl, script):
|
|
1060
1111
|
"""画面提示点钩子:render.py --stage cues 会调它(没有这个函数就不调)。
|
|
1061
1112
|
|
|
@@ -1065,7 +1116,16 @@ def cues(tl, script):
|
|
|
1065
1116
|
v0.5:条目落位步长与场景绘制共用 plan["pacing"]["item_stagger"],
|
|
1066
1117
|
两边读同一个数,否则音效会错位(这是 v0.4 写死 0.32 留下的坑)。
|
|
1067
1118
|
|
|
1068
|
-
|
|
1119
|
+
v0.7(references/audio-variation.md §3.4):提示点改为按**场景 kind 与内容**生成,
|
|
1120
|
+
不再是"每段 ui_blip 三连 + 结尾 answer_chime"的同质化固定套路(跨片音效雷同的
|
|
1121
|
+
来源之一):
|
|
1122
|
+
hook : 段首 impact(size 按字数 0.9~1.3)+ 落定 riser
|
|
1123
|
+
body : 条目落位 ui_blip(pitch 由条目文本 sha1 派生)+ items>=3 补 answer_chime
|
|
1124
|
+
+ 段尾 whoosh(转场到下一段)
|
|
1125
|
+
quote : reverse_swell + 落定轻 impact
|
|
1126
|
+
outro : answer_chime + 片尾 sub_drop
|
|
1127
|
+
同段内同类型 cue 重复时 gain 逐次再降 2 dB(-2/-4/-6)、pitch 必须不同。
|
|
1128
|
+
返回 schema 不变:[{"at": 时刻, "type": 配方名, ...参数}];
|
|
1069
1129
|
at 可以写 "abs:12.5" / 12.5 / "l03+0.35"(相对该段 start)/"l03"
|
|
1070
1130
|
type 见 references/sfx-design.md 的配方表,或 sfxkit.list_recipes()
|
|
1071
1131
|
"""
|
|
@@ -1073,13 +1133,58 @@ def cues(tl, script):
|
|
|
1073
1133
|
stagger = 0.32
|
|
1074
1134
|
if not plan.get("legacy"):
|
|
1075
1135
|
stagger = float((plan.get("pacing") or {}).get("item_stagger") or 0.32)
|
|
1136
|
+
scenes = list(tl.get("scenes") or [])
|
|
1076
1137
|
out = []
|
|
1077
|
-
for s in
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1138
|
+
for i, s in enumerate(scenes):
|
|
1139
|
+
sid = str(s.get("id") or ("l%02d" % (i + 1)))
|
|
1140
|
+
kind = str(s.get("kind") or "body")
|
|
1141
|
+
end = float(s.get("end") or 0.0)
|
|
1142
|
+
dur = float(s.get("dur") or max(0.0, end - float(s.get("start") or 0.0)))
|
|
1143
|
+
has_next = (i + 1) < len(scenes)
|
|
1144
|
+
seg = []
|
|
1145
|
+
|
|
1146
|
+
def _at(off):
|
|
1147
|
+
"""本段相对时刻 -> cue 的 at 写法(id+偏移,保持返回 schema 不变)。"""
|
|
1148
|
+
return "%s+%.2f" % (sid, max(0.0, float(off)))
|
|
1149
|
+
|
|
1150
|
+
if kind == "hook":
|
|
1151
|
+
# 开场重击按文案字数给"体量":字数越多 size 越大(0.9~1.3),避免每片同一记闷响
|
|
1152
|
+
text = str(s.get("text") or s.get("big") or "")
|
|
1153
|
+
n_char = len("".join(text.split()))
|
|
1154
|
+
size = round(0.9 + 0.4 * min(1.0, n_char / 40.0), 2)
|
|
1155
|
+
seg.append({"at": _at(0.10), "type": "impact", "size": size, "gain": -2})
|
|
1156
|
+
seg.append({"at": _at(min(1.2, max(0.35, dur * 0.28))),
|
|
1157
|
+
"type": "riser", "dur": 1.2, "gain": -6})
|
|
1158
|
+
elif kind == "quote":
|
|
1159
|
+
# 引语段:先来一层反向渐强铺底,落定时轻一点的一击
|
|
1160
|
+
seg.append({"at": _at(0.05), "type": "reverse_swell", "dur": 1.2, "gain": -5})
|
|
1161
|
+
seg.append({"at": _at(min(1.4, max(0.5, dur * 0.30))),
|
|
1162
|
+
"type": "impact", "size": 0.6, "gain": -8})
|
|
1163
|
+
elif kind == "outro":
|
|
1164
|
+
# 收尾:答对钟声 + 片尾 sub_drop(尾巴正好落在段尾,不留半截低频)
|
|
1165
|
+
seg.append({"at": _at(0.35), "type": "answer_chime", "gain": -4})
|
|
1166
|
+
seg.append({"at": _at(max(0.30, dur - 1.60)),
|
|
1167
|
+
"type": "sub_drop", "dur": 1.6, "gain": -5})
|
|
1168
|
+
else:
|
|
1169
|
+
# body(以及 emphasis/calm 等未单独设计的 kind:按正文处理,向后兼容)
|
|
1170
|
+
items = list(s.get("items") or [])
|
|
1171
|
+
for j, it in enumerate(items):
|
|
1172
|
+
# 条目逐条落位:第一项在 0.48s 出现,之后每 stagger 秒一个(与 _draw_items 同一拍)
|
|
1173
|
+
at = 0.48 + stagger * j
|
|
1174
|
+
if at > dur - 0.12: # 极短段:放不下的条目不再排提示点(避免响到下一段)
|
|
1175
|
+
break
|
|
1176
|
+
txt = it.get("text") if isinstance(it, dict) else it
|
|
1177
|
+
seg.append({"at": _at(at), "type": "ui_blip",
|
|
1178
|
+
"pitch": _item_pitch(txt), "gain": -6})
|
|
1179
|
+
blips = [c for c in seg if c["type"] == "ui_blip"]
|
|
1180
|
+
if len(items) >= 3 and blips and dur > 0.6:
|
|
1181
|
+
# 最后一个条目落定后的下一拍补一声"猜中了",给列表收尾
|
|
1182
|
+
last_at = float(blips[-1]["at"].split("+", 1)[1])
|
|
1183
|
+
t_chime = min(last_at + stagger, max(last_at + 0.20, dur - 0.30))
|
|
1184
|
+
seg.append({"at": _at(t_chime), "type": "answer_chime", "gain": -6})
|
|
1185
|
+
if has_next and dur > 0.6:
|
|
1186
|
+
# 段尾 whoosh:从本段扫进下一段(C 路会在段界附近给转场类音效让位)
|
|
1187
|
+
seg.append({"at": _at(max(0.0, dur - 0.45)),
|
|
1188
|
+
"type": "whoosh", "dur": 0.9, "gain": -6})
|
|
1189
|
+
out.extend(_dedupe_segment(seg))
|
|
1085
1190
|
return out
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
'use strict';
|
|
15
15
|
|
|
16
16
|
var W = 1920, H = 1080, FPS = 30, BG = '#0b0e14';
|
|
17
|
+
var DEFAULT_DURATION = 12.0; // 页面没给时长时的兜底;film.js 用它区分"显式时长"与"兜底值"
|
|
17
18
|
var FAMILY = '"Microsoft YaHei", "PingFang SC", "Noto Sans CJK SC", sans-serif';
|
|
18
19
|
var TAU = Math.PI * 2;
|
|
19
20
|
|
|
@@ -409,6 +410,20 @@
|
|
|
409
410
|
function getCues() { return copySorted(cueList); }
|
|
410
411
|
function getEvents() { return copySorted(eventList); }
|
|
411
412
|
|
|
413
|
+
/* 场景表推出的时间轴总长(最后一个场景的 end)。
|
|
414
|
+
* 为什么要它:页面契约里的 DURATION 必须等于真正的成片秒数,而 v0.3 的模板在 film.js
|
|
415
|
+
* 开头写死 global.DURATION = 12.0 —— 加一幕/改一幕时长后忘了同步这个常量,就会
|
|
416
|
+
* "明明改了场景、成片还是 12 秒"或者直接和 timeline.json 对不上(render.py 报
|
|
417
|
+
* 页面 DURATION 与 timeline 总时长不一致)。让 DURATION 从场景表推,就不会漏改。 */
|
|
418
|
+
function timelineEnd() {
|
|
419
|
+
var end = 0;
|
|
420
|
+
for (var i = 0; i < scenes.length; i++) {
|
|
421
|
+
var e = Number(scenes[i].start) + Number(scenes[i].dur);
|
|
422
|
+
if (isFinite(e) && e > end) end = e;
|
|
423
|
+
}
|
|
424
|
+
return Math.round(end * 1000) / 1000;
|
|
425
|
+
}
|
|
426
|
+
|
|
412
427
|
/* ---------------------------------------------------------------- 逐帧渲染 */
|
|
413
428
|
var currentFrame = -1;
|
|
414
429
|
|
|
@@ -467,11 +482,13 @@
|
|
|
467
482
|
/* ---------------------------------------------------------------- 导出 */
|
|
468
483
|
var FK = {
|
|
469
484
|
W: W, H: H, FPS: FPS, BG: BG, TAU: TAU, FAMILY: FAMILY,
|
|
485
|
+
defaultDuration: DEFAULT_DURATION,
|
|
470
486
|
canvas: canvas, ctx: ctx,
|
|
471
487
|
clamp: clamp, lerp: lerp, seg: seg, E: E, hash: hash, rng: rng,
|
|
472
488
|
g: g,
|
|
473
489
|
scene: scene, always: always, watch: watch, qaReset: qaReset,
|
|
474
490
|
texts: texts, watched: watched, drawnKeys: drawnKeyList,
|
|
491
|
+
timelineEnd: timelineEnd,
|
|
475
492
|
getCues: getCues, getEvents: getEvents, renderFrame: renderFrame, boot: boot,
|
|
476
493
|
frame: function () { return currentFrame; }
|
|
477
494
|
};
|
|
@@ -480,6 +497,6 @@
|
|
|
480
497
|
global.renderFrame = renderFrame;
|
|
481
498
|
global.getCues = getCues;
|
|
482
499
|
global.getEvents = getEvents;
|
|
483
|
-
if (typeof global.DURATION === 'undefined') global.DURATION =
|
|
500
|
+
if (typeof global.DURATION === 'undefined') global.DURATION = DEFAULT_DURATION;
|
|
484
501
|
global.ready = false;
|
|
485
502
|
})(window);
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
/* film.js — 场景模板(v0.3.0)
|
|
2
2
|
*
|
|
3
|
-
*
|
|
3
|
+
* 3 个场景 / 默认 12 秒(global.DURATION 由场景表推出,见下方 resolveDuration:
|
|
4
|
+
* 加一幕、改一幕时长都不用手工同步常量;要锁死就写 window.WINDOW_DURATION)。
|
|
5
|
+
* 这是**独立的 12 秒演示片**,不是"随便配一份 film.json 就能出全片"的成品页面:
|
|
6
|
+
* 换剧本必须自己重写场景,让画面覆盖到片尾,再按需写 WINDOW_DURATION。
|
|
7
|
+
*
|
|
8
|
+
* 示范 5 件事:
|
|
4
9
|
* ① 逐字入场(0–4s) ② 数字滚动(4–8s) ③ 粒子 / 发光(8–12s)
|
|
5
10
|
* ④ HUD 标注(贯穿全片) ⑤ cue / event 登记(由 window.getCues()/getEvents() 导出)
|
|
6
11
|
*
|
|
@@ -15,7 +20,21 @@
|
|
|
15
20
|
var W = F.W, H = F.H, g = F.g, E = F.E;
|
|
16
21
|
var seg = F.seg, lerp = F.lerp, clamp = F.clamp;
|
|
17
22
|
|
|
18
|
-
|
|
23
|
+
/* 成片秒数(webengine.py / render.py 会读它做分块与校验)。
|
|
24
|
+
* 默认由场景表推出 = 最后一个场景的 end(加/删/改场景后自动跟着变,不会忘记同步);
|
|
25
|
+
* 要显式锁死(例如页面里还有场景以外的动画),在 index.html 里先写
|
|
26
|
+
* <script>window.WINDOW_DURATION = 12.0;</script>
|
|
27
|
+
* 或在引入 film.js 之前赋值即可覆盖。使用说明见 references/web-engine.md §3。 */
|
|
28
|
+
function resolveDuration(F) {
|
|
29
|
+
var explicit = Number(global.WINDOW_DURATION);
|
|
30
|
+
if (isFinite(explicit) && explicit > 0) return explicit;
|
|
31
|
+
// 也认"在 film.js 之前直接写 window.DURATION = 秒数"的写法:render.py 的报错就是
|
|
32
|
+
// 教用户改 window.DURATION(页面里的时长必须等于 timeline.total)。
|
|
33
|
+
// engine.js 自己写的兜底值不算显式,否则模板永远锁死在 12 秒,加场景也不生效。
|
|
34
|
+
var pre = Number(global.DURATION);
|
|
35
|
+
if (isFinite(pre) && pre > 0 && pre !== Number(F.defaultDuration)) return pre;
|
|
36
|
+
return F.timelineEnd();
|
|
37
|
+
}
|
|
19
38
|
|
|
20
39
|
var C = {
|
|
21
40
|
ink: '#eaf2ff',
|
|
@@ -190,8 +209,14 @@
|
|
|
190
209
|
});
|
|
191
210
|
});
|
|
192
211
|
|
|
212
|
+
/* ================================================================ 时间轴总长(DURATION) */
|
|
213
|
+
/* 必须在所有 F.scene(...) 注册完之后求值:resolveDuration 读的就是场景表。
|
|
214
|
+
* 放在 HUD 之前、注册之后,这样 HUD 里读 global.DURATION 时它已经是正确值(boot 在最后)。 */
|
|
215
|
+
global.DURATION = resolveDuration(F);
|
|
216
|
+
|
|
193
217
|
/* ================================================================ HUD 层(最后注册 → 最后画,压在所有场景之上) */
|
|
194
218
|
F.always(function (t) {
|
|
219
|
+
var dur = Number(global.DURATION) || 12.0;
|
|
195
220
|
g.text('DSH VIDEO STUDIO', 64, 60, {
|
|
196
221
|
size: 24, weight: '500', color: 'rgba(234,242,255,0.55)', key: 'hud.brand'
|
|
197
222
|
});
|
|
@@ -200,14 +225,14 @@
|
|
|
200
225
|
size: 22, weight: '500', color: 'rgba(90,210,255,0.8)', key: 'hud.ch', group: 'hud.ch'
|
|
201
226
|
});
|
|
202
227
|
// 时间码每帧都在变 → 也保证「不同帧渲染结果不同」
|
|
203
|
-
var tc = 'T ' + (t < 10 ? '0' : '') + t.toFixed(2) + ' /
|
|
228
|
+
var tc = 'T ' + (t < 10 ? '0' : '') + t.toFixed(2) + ' / ' + dur.toFixed(2);
|
|
204
229
|
g.text(tc, W - 64, 60, {
|
|
205
230
|
size: 22, weight: '500', align: 'right', color: 'rgba(234,242,255,0.45)',
|
|
206
231
|
key: 'hud.tc', group: 'hud.tc'
|
|
207
232
|
});
|
|
208
233
|
// 顶部进度线
|
|
209
234
|
g.rect(64, 122, W - 128, 1, 'rgba(90,210,255,0.16)');
|
|
210
|
-
g.rect(64, 122, (W - 128) * clamp(t /
|
|
235
|
+
g.rect(64, 122, (W - 128) * clamp(t / dur, 0, 1), 2, 'rgba(255,140,66,0.85)');
|
|
211
236
|
// 底部品牌行
|
|
212
237
|
g.text('v0.3.0 · Canvas2D 逐帧渲染 · 不使用 GSAP / three.js', 64, H - 52, {
|
|
213
238
|
size: 20, weight: '400', color: 'rgba(234,242,255,0.3)', key: 'hud.foot'
|
|
@@ -12,6 +12,11 @@
|
|
|
12
12
|
<canvas id="stage" width="1920" height="1080"></canvas>
|
|
13
13
|
|
|
14
14
|
<script src="engine.js"></script>
|
|
15
|
+
<!-- 页面契约(references/web-engine.md §3):DURATION 必须等于成片秒数。
|
|
16
|
+
模板默认由场景表自动推导(film.js 的 resolveDuration);
|
|
17
|
+
要锁死就取消下面这行注释并改成 timeline.json 的 total:
|
|
18
|
+
<script>window.WINDOW_DURATION = 12.0;</script>
|
|
19
|
+
-->
|
|
15
20
|
<script src="film.js"></script>
|
|
16
21
|
<script src="qa_text.js"></script>
|
|
17
22
|
<script>
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# 音频多样性改造(v0.7)—— 接口契约
|
|
2
|
+
|
|
3
|
+
> 起草:lead,2026-11-09。本文件是本次改造的**唯一定义源**。
|
|
4
|
+
> 三路并行改动的文件写域不重叠,但**接口必须按本文件逐字实现**,否则集成会失败。
|
|
5
|
+
> 诊断证据见 `tools/audio_similarity.py` 的输出(改完后必须重跑,阈值见 §5)。
|
|
6
|
+
|
|
7
|
+
## 1. 要解决的两个问题(都有实测数据)
|
|
8
|
+
|
|
9
|
+
| 问题 | 实测 | 根因 |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| A. 跨视频配乐雷同 | 同风格/同 bpm/同调、只换内容指纹:corr **0.966** | 指纹只改 seed(音色细节),和弦进行/动机/鼓型/段落骨架全固定 |
|
|
12
|
+
| B. 跨视频音效雷同 | 同 cue 序列两次渲染逐样本相同 | `CueBus.build_audio` seed = `seed_base + i`,只跟 cue 顺序有关 |
|
|
13
|
+
| C. 片内配乐与音效难分 | impact 质心 1001 Hz / whoosh 1589 Hz / music 5451 Hz;提示音硬编码 Dm 五声 | 提示音调性硬编码 + 配乐过门与 sfx 轨同边界同频段 + duck 仅 2.5–4.5 dB |
|
|
14
|
+
|
|
15
|
+
## 2. 改动总览(按写域分三路)
|
|
16
|
+
|
|
17
|
+
| 路 | 负责文件 | 改什么 |
|
|
18
|
+
|---|---|---|
|
|
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。**任何一路不得改别人的文件**(包括顺手"优化")。
|
|
25
|
+
|
|
26
|
+
## 3. 接口契约(逐字实现,向后兼容)
|
|
27
|
+
|
|
28
|
+
### 3.1 sfxkit(B 路)
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
# 1) 调性跟随:新增 key 参数,默认 None = 旧行为(Dm 五声)
|
|
32
|
+
def in_key(freq: float, key: str | None = None) -> float:
|
|
33
|
+
"""key=None -> 旧行为(硬编码 D 小调五声,逐样本兼容)。
|
|
34
|
+
key 给 "Dm"/"C"/"Am"/... -> 吸附到该调(小调用自然小调五声,大调用大调五声)。"""
|
|
35
|
+
|
|
36
|
+
# 2) 所有走 in_key 的配方接受 key(通过 **kw 透传,签名末位加 key=None 或直接从 kw 取):
|
|
37
|
+
# ui_blip / answer_chime / text_stream(voice="tone") / data_chirp / roulette_tick
|
|
38
|
+
# 未知调名必须 raise ValueError(不静默降级,与 _enum 同口径)。
|
|
39
|
+
|
|
40
|
+
# 3) 内容派生 seed 的辅助函数(供 filmkit.CueBus 用)
|
|
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。"""
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### 3.2 filmkit.CueBus.build_audio(C 路,lead)
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
def build_audio(self, total=None, sr=SR, base_db=None, default_gain_db=0.0,
|
|
49
|
+
seed_base=1000, verbose=True,
|
|
50
|
+
style=None, # 风格 id;None = 旧行为
|
|
51
|
+
content_fp=None, # 内容指纹 full;None = 旧行为
|
|
52
|
+
key=None): # 配乐调性;None = 旧行为(sfxkit 用 Dm)
|
|
53
|
+
"""每条 cue 的 seed:
|
|
54
|
+
- style/content_fp 任一为 None -> seed = seed_base + i(旧路径,逐样本兼容)
|
|
55
|
+
- 都给 -> seed = sfxkit.content_seed(seed_base, style, content_fp, kind, t)
|
|
56
|
+
并把 key 透传给配方(render(kind, seed=..., key=key, **params))。"""
|
|
57
|
+
```
|
|
58
|
+
|
|
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)**逐字节不变**(selfcheck 第 20 条会验)。
|
|
75
|
+
|
|
76
|
+
### 3.4 assets/scenes.py 的 cues()(B 路)
|
|
77
|
+
|
|
78
|
+
现在只出 `ui_blip × items` + `answer_chime × outro`。改为按画面事件生成,并且**类型按场景 kind 与内容变化**:
|
|
79
|
+
|
|
80
|
+
- `hook`:段首 `impact`(size 按字数 0.9~1.3),落定 `riser`。
|
|
81
|
+
- `body`:条目落位仍用 `ui_blip`,但 **pitch 由条目文本 sha1 派生**(json 的 items 文本);
|
|
82
|
+
最后一个条目后加 `answer_chime`(若 kind=body 且 items≥3);段尾 `whoosh`(going 到下一段)。
|
|
83
|
+
- `quote`:`reverse_swell` + 落定 `impact`(轻)。
|
|
84
|
+
- `outro`:`answer_chime` + 片尾 `sub_drop`。
|
|
85
|
+
- 不许再出现"每段都一样的三连击":同类型 cue 在一段内重复时,`gain` 必须递减(-2/-4/-6),pitch 必须不同。
|
|
86
|
+
- **保持向后兼容**:仍有 `stagger` 对齐逻辑(与 _draw_items 同节拍),返回 schema 不变。
|
|
87
|
+
|
|
88
|
+
### 3.5 assets/film.json 模板(B 路)
|
|
89
|
+
|
|
90
|
+
模板里的 `sfx.cues` 固定三条 whoosh/riser/impact 是"跨片雷同"的直接来源之一。改为:
|
|
91
|
+
|
|
92
|
+
- 模板只保留 1 条转场 whoosh 作为示例,并加注释说明"具体提示点建议交给 scenes.py 的 cues() 钩子"。
|
|
93
|
+
- `sfx` 增加可选字段 `content_seed: true`(默认 true):走内容派生 seed;显式 false 回到旧行为。
|
|
94
|
+
|
|
95
|
+
## 4. render.py 接线(C 路,lead)
|
|
96
|
+
|
|
97
|
+
- `st_score`:把 `plan_s["music"]["key"]` 与 `plan_s` 的 `fingerprint` 传给 sfx 阶段(经 `script["_style"]`)。
|
|
98
|
+
- `st_sfx`:读 `script["_style"]`,把 `style` / `content_fp` / `key` / `sfx.content_seed` 传给
|
|
99
|
+
`bus.build_audio(...)`;没有 `_style`(legacy)时不传,保持旧行为。
|
|
100
|
+
- `st_audio`:duck 默认值改为按风格:在现有表基础上整体 +1.0 dB,且 `music.duck_db` 显式值仍然优先。
|
|
101
|
+
另外新增**过门避让**:`scorekit` 的过门音效已经在 music.wav 里,sfx 轨在
|
|
102
|
+
[段落边界 - 2.2s, 边界] 区间内的 whoosh/riser/reverse_swell 需要自动 -3 dB(在 filmkit 里做,
|
|
103
|
+
或 st_sfx 后处理;由 lead 决定实现位置,**B 路不要碰**)。
|
|
104
|
+
|
|
105
|
+
## 5. 验收阈值(改完必须重跑 `python <REPO>/tools/audio_similarity.py`)
|
|
106
|
+
|
|
107
|
+
| 指标 | 改前 | 目标 | 落地实测(2026-11-09) |
|
|
108
|
+
|---|---|---|---|
|
|
109
|
+
| A 同风格换指纹 corr | 0.966 | **≤ 0.85**(仍要像同一风格,但不能像同一首) | **0.645** |
|
|
110
|
+
| A 换风格 corr | 0.534 | ≤ 0.52(不能变得更像) | **0.514**(v0.7.2 复测,余量 0.006;连跑 3 次逐位相同。余量薄是有意的:抬到 0.53 就离改造前的 0.534 只剩 0.004,这条门禁就没意义了) |
|
|
111
|
+
| B 同 cue 序列跨片逐样本相同 | True | **False**(不同内容指纹必须不同) | False(同 fp 仍逐样本相同) |
|
|
112
|
+
| B legacy(不传 style/fp) | True | **必须仍为 True**(向后兼容) | True |
|
|
113
|
+
| C duck 默认 | 2.5–4.5 dB | 3.5–5.5 dB | 4.0(mono 3.0 / cinema 5.5) |
|
|
114
|
+
| D 人声段旁白/背景响度差 | 约 1.5 dB(配乐 -1.5 无侧链) | **≥ 12 dB** | **15.0 dB** |
|
|
115
|
+
| D 旁白处背景额外让位 | 0 | ≥ 4 dB | 6.0 dB |
|
|
116
|
+
|
|
117
|
+
> v0.7.1 追加 D 组(旁白优先):视频以旁白为主,BGM 与音效是可选层。`mix_film` 的
|
|
118
|
+
> `voice_duck_db=6` / `voice_duck_sfx_db=3` + `music.gain_db=-12` 把"人声比背景响 12 dB"
|
|
119
|
+
> 从文档口号变成可执行断言(selfcheck 35/36 条 + 本脚本 D 组)。
|
|
120
|
+
|
|
121
|
+
**已知边界(不是回归,是设计取舍)**:同风格不同内容指纹有约 1/3 概率落到同一个
|
|
122
|
+
和弦进行档位偏移上,此时两段配乐 corr 约 0.93(只剩七和弦色彩与鼓型 ghost 的差别)。
|
|
123
|
+
要再压下去需要"每段独立选档"或"同档时轮转",会削弱"片内是一条曲子"的连贯性,
|
|
124
|
+
本轮不做;`--strict` 用固定的 content-a / content-b 做验收,两者落在不同档。
|
|
125
|
+
|
|
126
|
+
另有硬性回归(任何一条红都不算完成):
|
|
127
|
+
|
|
128
|
+
> 下面 1–4 的路径都按**仓库根**写(`python skills/video-studio/scripts/…`);如果你已经 `cd` 进技能目录,
|
|
129
|
+
> 去掉 `skills/video-studio/` 前缀即可。`<REPO>` = 仓库根 / 装好后的包根。
|
|
130
|
+
|
|
131
|
+
1. `python skills/video-studio/scripts/selfcheck.py` 全 PASS(现在 43 条)。
|
|
132
|
+
2. `python skills/video-studio/scripts/scorekit.py --selftest` 全 PASS。
|
|
133
|
+
3. `python skills/video-studio/scripts/sfxkit.py --selftest` 全 PASS。
|
|
134
|
+
4. `python <REPO>/tools/ci_check.py` 全 PASS(在包根执行)。
|
|
135
|
+
5. legacy 逐样本不变:selfcheck 第 20/26/33 条不得改动其断言。
|
|
136
|
+
|
|
137
|
+
## 6. 协作规则
|
|
138
|
+
|
|
139
|
+
- 每个 teammate 只跑自己文件的 `--selftest`,**全量 selfcheck / ci_check 由 lead 统一跑**(避免并行抢 CPU 和临时目录)。
|
|
140
|
+
- 改完在 `CHANGELOG.md` 追加的条目由 lead 统一写(teammate 不要改 CHANGELOG)。
|
|
141
|
+
- 任何接口拿不准的地方,按本文件逐字实现;本文件没写的自由发挥请控制在"不改变签名"的范围内。
|
|
142
|
+
- 完成后向 lead 报告:改了哪些函数、自测输出摘要、与契约的偏差(如果有)。
|
|
143
|
+
|
|
144
|
+
## 7. 实现备注(落地后回填,2026-11-09)
|
|
145
|
+
|
|
146
|
+
三路改动全部落盘后,实测与契约之间的**已知偏差**都记在这里(不改契约本身):
|
|
147
|
+
|
|
148
|
+
| 项 | 契约原文 | 实际实现 | 判断 |
|
|
149
|
+
|---|---|---|---|
|
|
150
|
+
| 配乐 tier 选择 | §3.3 只列了和弦替换/动机偏移/鼓型 ghost | 额外让指纹从 `progressions` 的 calm/body/drive 三档里选一档(该风格自带的三套进行,不发明新和弦) | 接受:单靠七和弦替换压不到 0.85,实测换 tier 后 0.966 → 0.645;仍是"同风格同一套素材" |
|
|
151
|
+
| `roulette_tick` 调性 | §3.1 列进"走 in_key 的配方" | v0.6 本来没走 in_key;实现为 key=None 保持旧公式、key 非空才吸附 | 接受:不破坏 legacy 逐样本 |
|
|
152
|
+
| 同段重复 cue 的 gain | §3.4 写 -2/-4/-6 | 每次再降 2 dB 不封顶(items 很多时会到 -2n) | 待观察:模板 5 条时最末一条 -8 dB;若听感偏轻改 `_cue_gain_decay` 一行 |
|
|
153
|
+
| `content_seed` 实现位置 | §3.1 放 sfxkit | filmkit 与 sfxkit 各有一份逐字相同的实现(filmkit 走函数内延迟 import,模块级 from 会改导入时机) | 接受:两侧同参同值已核对;注释互相指认 |
|
|
154
|
+
|
|
155
|
+
**回归基线**(改前先录、改后逐条比对):
|
|
156
|
+
|
|
157
|
+
- sfxkit 9 条渲染的 sha256(ui_blip ×3 / answer_chime / text_stream ×2 / data_chirp / roulette_tick ×2)逐条相同。
|
|
158
|
+
- scorekit legacy 路径(style=None)6 s 渲染逐样本与 v0.6 相同。
|
|
@@ -28,15 +28,17 @@ wav = bus.mix(normalize=True, fade=0.6)
|
|
|
28
28
|
| 声道 | 单声道总线 → 双声道输出 | 旁白单声道足够 |
|
|
29
29
|
| 母带响度 | **-16 LUFS ± 1** | 网络视频标准 |
|
|
30
30
|
| 真峰 | **≤ -1 dBTP** | AAC 编码会推高约 0.9 dB,混流前留 1.4 dB 余量 |
|
|
31
|
-
| 旁白与间隙比 | ≥ 12 dB |
|
|
32
|
-
| 配乐电平 | 旁白以下 12–18 dB |
|
|
31
|
+
| 旁白与间隙比 | ≥ 12 dB | 人声必须明显突出(v0.7.1 起有代码保障:`mix_film` 旁白侧链) |
|
|
32
|
+
| 配乐电平 | 旁白以下 12–18 dB | 否则听不清字(v0.7.1 默认 `music.gain_db=-12`,旧版 -1.5 不合格) |
|
|
33
33
|
|
|
34
34
|
## 3. 响度链路(三步)
|
|
35
35
|
|
|
36
36
|
```python
|
|
37
37
|
# 本仓库**实际**链路(filmkit.mix_film + mux):
|
|
38
|
-
# 旁白 voice_gain=1.0
|
|
39
|
-
# →
|
|
38
|
+
# 旁白 voice_gain=1.0 居中(不被侧链动)
|
|
39
|
+
# → 音效按 CUE_BASE_DB 逐条,再按旁白包络压 3 dB
|
|
40
|
+
# → 配乐 music.gain_db(默认 -12) → 音效响时 duck(按风格 3.0–5.5) → 旁白包络再压 6 dB
|
|
41
|
+
# → 求和 → 峰值保护 0.89 → ffmpeg loudnorm 两遍到 -16 LUFS / 目标真峰 -1.5
|
|
40
42
|
af = K.loudnorm_two_pass("work/audio.wav", target_i=-16.0, target_tp=-1.5)
|
|
41
43
|
```
|
|
42
44
|
|
|
@@ -185,7 +187,8 @@ print("peak:", np.max(np.abs(bus.bus))) # 期望 < 0.99
|
|
|
185
187
|
|---|---|---|
|
|
186
188
|
| 后半段静音 | ffmpeg 多输入混流 | numpy 总线 |
|
|
187
189
|
| 爆音/削波 | 先 astype 再判 dtype | 先判 dtype 再转;打印中间峰值 |
|
|
188
|
-
| 听起来糊 | 配乐电平过高 | 配乐压到旁白以下 12–18 dB |
|
|
190
|
+
| 听起来糊 | 配乐电平过高 | 配乐压到旁白以下 12–18 dB(v0.7.1 默认 -12 且带旁白侧链) |
|
|
191
|
+
| 听不清台词 | 背景与人声响度接近 | `music.gain_db` 降到 -12、`voice_duck_db` 开 6;别用"整体拉高"救 |
|
|
189
192
|
| 响度上不去 | AAC 推高真峰后限幅 | 混流前留 1.4 dB 余量 |
|
|
190
193
|
| 环境音接缝咔 | 直接拼接 | 频域整周期噪声 |
|
|
191
194
|
| 配音机械 | 语速超过 +15% | 降到 +6% 左右,加自然停顿 |
|
|
@@ -418,3 +421,62 @@ out.append({"at": "%s+0.3" % s["id"], "type": "text_stream",
|
|
|
418
421
|
`sfxkit` 的算法思想改编自 `ai-history-film`(MIT)的 `sfx/` 包,实现为本项目单文件重写;
|
|
419
422
|
文件头已注明出处。配方名、参数名保持与上游一致,方便对照上游文档。
|
|
420
423
|
|
|
424
|
+
## 13. 片间差异与片内分离(v0.7 新增)
|
|
425
|
+
|
|
426
|
+
> 完整契约与改动清单见 **references/audio-variation.md**;这里只记"怎么用、怎么验"。
|
|
427
|
+
|
|
428
|
+
### 13.1 为什么 v0.5 之后还会"差不多"
|
|
429
|
+
|
|
430
|
+
v0.5 的 style pack 决定的是**风格**(8 套),不是**这一部的具体样子**:
|
|
431
|
+
|
|
432
|
+
- 配乐:内容指纹只进 `seed`(音色细节),和弦进行/动机/鼓型/段落骨架全固定
|
|
433
|
+
→ 同风格下两段 20 s 配乐相关性 **0.966**(实测)。
|
|
434
|
+
- 音效:`CueBus.build_audio` 的 seed 是 `seed_base + i`(只跟 cue 顺序)
|
|
435
|
+
→ 同一批 cue 跨片逐样本相同。
|
|
436
|
+
- 片内:提示音硬编码 D 小调五声、配乐过门与 sfx 转场在同一段落边界撞车、duck 只有 3 dB。
|
|
437
|
+
|
|
438
|
+
### 13.2 现在怎么变
|
|
439
|
+
|
|
440
|
+
| 层 | 行为 | 开关 |
|
|
441
|
+
|---|---|---|
|
|
442
|
+
| 配乐和声/动机/鼓型 | 内容指纹做稳定局部替换(每两小节、±1 级数、ghost 击) | `style: auto`(默认);写死 `music.seed` + `seed_from_content: false` 可关 |
|
|
443
|
+
| 音效 seed | `content_seed(base, style, fp, type, t)`,随内容变 | `sfx.content_seed`(默认 true;false = 旧 seed_base+i) |
|
|
444
|
+
| 提示音调性 | `in_key(freq, key)` 跟随配乐调性 | `CueBus.build_audio(key=...)`,render.py 自动传 |
|
|
445
|
+
| 段落边界 | [边界-2.2s, 边界] 内的 whoosh/riser 自动 -3 dB | `sfx.duck_transitions`(默认 true) |
|
|
446
|
+
| duck | 按风格 3.0–5.5 dB(默认 4.0) | `music.duck_db` 显式覆盖 |
|
|
447
|
+
|
|
448
|
+
**legacy 兼容**:不写 `style` 字段 = 全部旧行为(配乐逐样本一致、音效 seed_base+i、提示音 Dm 五声)。
|
|
449
|
+
|
|
450
|
+
### 13.2.1 旁白优先(v0.7.1)
|
|
451
|
+
|
|
452
|
+
视频以旁白为主,**BGM 与音效是可选层**:它们负责情绪与重音,不能抢字。具体做法:
|
|
453
|
+
|
|
454
|
+
| 手段 | 默认值 | 说明 |
|
|
455
|
+
|---|---|---|
|
|
456
|
+
| 配乐基线 | `music.gain_db=-12` | 旧版 -1.5 时背景与台词几乎等高;-12 是"人声明显压住背景"的经验线 |
|
|
457
|
+
| 旁白侧链(配乐) | `voice_duck_db=6` | 有人声的地方再压 6 dB;间隙由包络自动恢复(不会整片闷住) |
|
|
458
|
+
| 旁白侧链(音效) | `voice_duck_sfx_db=3` | 提示音/转场也让人声 |
|
|
459
|
+
| 语音阈值 | `voice_thresh_db=-42` / `voice_floor_db=-60` | 10 ms 块 RMS 映射成 0~1 的活动度,起音快、释放慢 |
|
|
460
|
+
| 旁白自身 | `voice_gain=1.0` | **不被任何侧链影响**——旁白优先不等于"压人声" |
|
|
461
|
+
| SNR 兜底 | `guard_snr_db=12` / `guard_cap_db=30` | 混音后实测人声活跃区的人声/背景差,不足就整体补压背景(只压背景) |
|
|
462
|
+
|
|
463
|
+
实测(真实 edge-tts 旁白 + scorekit 配乐):旧混音 **SNR -4.2 dB**(背景比台词还响,就是"听不清"的原因),
|
|
464
|
+
新混音 **SNR 12.0 dB**;合成信号口径 15.0 dB,真实配乐素材口径 12.9 dB。
|
|
465
|
+
设 `voice_duck_db=0` / `voice_duck_sfx_db=0` / `guard_snr_db=0` 可分别关掉对应机制。
|
|
466
|
+
|
|
467
|
+
### 13.3 验收:别靠耳朵,跑脚本
|
|
468
|
+
|
|
469
|
+
```bash
|
|
470
|
+
python <REPO>/tools/audio_similarity.py --strict # 在包根执行
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
A 组量"同风格换内容指纹"和"换风格"的相关性;B 组量"跨片音效是否还逐样本相同"(同时验
|
|
474
|
+
legacy 兼容);C 组读 `render.py` 的 duck 默认值并打印质心对照;
|
|
475
|
+
D 组构造"有人声段 + 间隙",量人声段旁白/背景响度差。任何一项不达标 exit 1。
|
|
476
|
+
|
|
477
|
+
### 13.4 改音频代码后的自检清单
|
|
478
|
+
|
|
479
|
+
1. `python <REPO>/tools/audio_similarity.py --strict` —— A/B/C 三组达标(在包根执行)。
|
|
480
|
+
2. `python selfcheck.py` —— 43 条全 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。
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
|
|
44
44
|
```bash
|
|
45
45
|
python render.py --stage explain # 哪些阶段过时了、为什么、最小重跑命令
|
|
46
|
-
python render.py --stage explain --json # 机读(给脚本/agent 用)
|
|
47
46
|
python render.py --stage qa --strict-stale # 有过时就失败(CI / 交付前)
|
|
47
|
+
python scripts/stagewatch.py --work work --json # 机读(给脚本/agent 用;render.py 没有 --json)
|
|
48
48
|
```
|
|
49
49
|
|
|
50
50
|
## 4. 和人工签核的关系
|
|
@@ -221,5 +221,5 @@ python webengine.py textqa ./web/index.html --step 0.2
|
|
|
221
221
|
信号:-16.3 LUFS / 真峰 -1.2 dBTP / 锐度比 0.94 / 黑帧 0 / 字幕 86 条 = 剧本 86 句
|
|
222
222
|
时间:单帧闪烁 0 / 画面空窗 0 / 音画同步最大偏差 38 ms
|
|
223
223
|
页面:文字重叠 0 / 出画 0 / 一闪而过 0 / 从没露面 0
|
|
224
|
-
自检:python selfcheck.py -> PASS
|
|
224
|
+
自检:python selfcheck.py -> PASS 43 / FAIL 0 / SKIP 0
|
|
225
225
|
```
|
|
@@ -121,7 +121,7 @@ assets/web/film.js 是 12 秒 / 3 个场景的完整模板:
|
|
|
121
121
|
|---|---|
|
|
122
122
|
| 1920x1080 PNG 截图 + 页面绘制 | 约 70–120 ms/帧(2 workers 时 70–85 ms/帧均摊) |
|
|
123
123
|
| 12 秒 / 360 帧 / 2 workers / chunk 3s | 28.0 s 出片(约 78 ms/帧均摊) |
|
|
124
|
-
| 自检(24
|
|
124
|
+
| 自检(24 项,含真实无头浏览器与跨会话确定性验证) | 约 15 s |
|
|
125
125
|
|
|
126
126
|
调参顺序:先降 workers(1–2 个足够,多了内存涨),再降 chunk_s(块小重渲代价低),
|
|
127
127
|
最后才动 crf/preset(crf 17 / preset slow 是质量档;预览可用 crf 20 / preset veryfast)。
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
#
|
|
3
3
|
# pip install -r requirements.txt # 必需 + 强烈建议(可选项默认注释,按需打开)
|
|
4
4
|
#
|
|
5
|
-
# 分三档,与 SKILL.md §
|
|
5
|
+
# 分三档,与 README「环境前提」(必需 / 可选两段)对应;SKILL.md §10 的脚本表写了每个文件依赖谁。
|
|
6
|
+
# 强烈建议档在这里单独列出(README 把它并进"可选"一句话里,档位以本文件为准)。
|
|
6
7
|
|
|
7
8
|
# ---- 必需:缺任何一条,主体流程跑不起来 ----
|
|
8
9
|
numpy>=1.24
|