@dsh-gis/dsh-video-studio-skill 0.7.2 → 0.8.1
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 +951 -0
- package/README.md +51 -8
- package/THIRD_PARTY_NOTICES.md +1 -0
- package/index.js +4 -1
- package/package.json +5 -3
- package/schemas/film.schema.json +162 -2
- package/skills/video-studio/SKILL.md +211 -11
- package/skills/video-studio/assets/golden_frames.json +65 -0
- package/skills/video-studio/assets/web/engine.js +63 -2
- package/skills/video-studio/assets/web/film.js +224 -101
- package/skills/video-studio/assets/web/index.html +4 -2
- package/skills/video-studio/assets/web/qa_text.js +36 -8
- package/skills/video-studio/references/audio-variation.md +4 -3
- package/skills/video-studio/references/audio.md +2 -2
- package/skills/video-studio/references/change-impact.md +1 -1
- package/skills/video-studio/references/quality-gates.md +25 -3
- package/skills/video-studio/references/style-packs.md +7 -3
- package/skills/video-studio/references/v08-quality.md +256 -0
- package/skills/video-studio/references/web-engine.md +19 -2
- package/skills/video-studio/references/word-timeline.md +147 -0
- package/skills/video-studio/requirements.txt +14 -0
- package/skills/video-studio/scripts/filmcheck.py +121 -0
- package/skills/video-studio/scripts/filmkit.py +521 -38
- package/skills/video-studio/scripts/gates.py +88 -4
- package/skills/video-studio/scripts/preflight.py +774 -0
- package/skills/video-studio/scripts/qa_frames.py +49 -8
- package/skills/video-studio/scripts/qaasr.py +1563 -0
- package/skills/video-studio/scripts/qavmaf.py +1442 -0
- package/skills/video-studio/scripts/render.py +719 -32
- package/skills/video-studio/scripts/scorekit.py +16 -30
- package/skills/video-studio/scripts/selfcheck.py +274 -0
- package/skills/video-studio/scripts/sfxkit.py +130 -14
- package/skills/video-studio/scripts/stagewatch.py +19 -6
- package/skills/video-studio/scripts/stylekit.py +43 -12
- package/skills/video-studio/scripts/voicekit.py +15 -7
- package/skills/video-studio/scripts/webengine.py +1119 -72
- package/tools/ci_check.py +171 -31
- package/tools/estimate.py +130 -19
- package/tools/golden_frames.py +664 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* film.js — 场景模板(v0.
|
|
1
|
+
/* film.js — 场景模板(v0.8.0)
|
|
2
2
|
*
|
|
3
3
|
* 3 个场景 / 默认 12 秒(global.DURATION 由场景表推出,见下方 resolveDuration:
|
|
4
4
|
* 加一幕、改一幕时长都不用手工同步常量;要锁死就写 window.WINDOW_DURATION)。
|
|
@@ -9,6 +9,19 @@
|
|
|
9
9
|
* ① 逐字入场(0–4s) ② 数字滚动(4–8s) ③ 粒子 / 发光(8–12s)
|
|
10
10
|
* ④ HUD 标注(贯穿全片) ⑤ cue / event 登记(由 window.getCues()/getEvents() 导出)
|
|
11
11
|
*
|
|
12
|
+
* v0.8.0 风格跟随(契约)
|
|
13
|
+
* ----------------------
|
|
14
|
+
* 色板 / 版式 / 材质强度 / HUD 装帧优先读 window.__STYLE__(webengine.open_page(style=...)
|
|
15
|
+
* 在页面加载前注入);取不到就用本文件里的 v0.7 常量。
|
|
16
|
+
* - 色板:theme.{bg,bg2,surface,ink,dim,accent,accent2,rule} 决定底色 / 正文 / 强调色;
|
|
17
|
+
* - 版式:layout.default ∈ center-stack / left-stack / split-right 决定标题块的锚点与对齐,
|
|
18
|
+
* hud ∈ chip-left / corner-line / none 决定 HUD 装帧;
|
|
19
|
+
* - 材质:material.vignette / texture_alpha 决定暗角与网格强度;
|
|
20
|
+
* - 动效:motion.entrance_dy 决定标题入场位移;motion.transition=wash 时转场用底色而不是黑;
|
|
21
|
+
* - 画布:canvas.{w,h,fps} 已在 engine.js 里生效;本文件用 X()/Y()/S() 把 1920x1080 设计坐标
|
|
22
|
+
* 等比缩放到实际画布(1280x720 等 16:9 尺寸),1920x1080 时三者原样返回。
|
|
23
|
+
* **没有 __STYLE__ 时**,上面每条取到的都是 v0.7 原值,渲染像素与旧版逐字节一致(自检有证据)。
|
|
24
|
+
*
|
|
12
25
|
* 纪律:每帧都是「帧号 → 画面」的纯函数——不读上一帧状态、不用 Math.random / Date.now。
|
|
13
26
|
* 画面里的所有文字都走 FK.g.text(),qaText 才能看到它们的包围盒。
|
|
14
27
|
* 改编自 ai-history-film(MIT)video/web2 的场景组织方式(逐字入场 / 计数器 / HUD 的写法)。
|
|
@@ -20,6 +33,101 @@
|
|
|
20
33
|
var W = F.W, H = F.H, g = F.g, E = F.E;
|
|
21
34
|
var seg = F.seg, lerp = F.lerp, clamp = F.clamp;
|
|
22
35
|
|
|
36
|
+
/* ================================================================ 风格包 → 色板 / 版式 */
|
|
37
|
+
var ST = (global.__STYLE__ && typeof global.__STYLE__ === 'object') ? global.__STYLE__ : null;
|
|
38
|
+
var TH = (ST && ST.theme && typeof ST.theme === 'object') ? ST.theme : null;
|
|
39
|
+
var MOTION = (ST && ST.motion && typeof ST.motion === 'object') ? ST.motion : null;
|
|
40
|
+
var MATERIAL = (ST && ST.material && typeof ST.material === 'object') ? ST.material : null;
|
|
41
|
+
|
|
42
|
+
/* 设计坐标 → 实际画布:1920x1080 时 KX = KY = KS = 1,三个函数原样返回(不做任何取整),
|
|
43
|
+
* 所以旧页面(无 style)尺寸路径一个数字都不变。 */
|
|
44
|
+
var KX = W / 1920, KY = H / 1080, KS = Math.min(KX, KY);
|
|
45
|
+
function X(v) { return KX === 1 ? v : Math.round(v * KX); }
|
|
46
|
+
function Y(v) { return KY === 1 ? v : Math.round(v * KY); }
|
|
47
|
+
function S(v) { return KS === 1 ? v : Math.round(v * KS); }
|
|
48
|
+
|
|
49
|
+
/* 颜色归一:接受 "#rrggbb" / [r,g,b];解析不了返回 null(由调用点的兜底值接手)。 */
|
|
50
|
+
function toRgb(c) {
|
|
51
|
+
if (Array.isArray(c) && c.length >= 3) {
|
|
52
|
+
var a = [Number(c[0]) | 0, Number(c[1]) | 0, Number(c[2]) | 0];
|
|
53
|
+
if (a[0] >= 0 && a[0] <= 255 && a[1] >= 0 && a[1] <= 255 && a[2] >= 0 && a[2] <= 255) return a;
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
if (typeof c === 'string') {
|
|
57
|
+
var s = c.trim();
|
|
58
|
+
var m = /^#([0-9a-fA-F]{6})$/.exec(s);
|
|
59
|
+
if (m) { var n = parseInt(m[1], 16); return [(n >> 16) & 255, (n >> 8) & 255, n & 255]; }
|
|
60
|
+
var m2 = /^rgba?\(([^)]+)\)$/.exec(s); // 也认 rgb()/rgba() 字符串
|
|
61
|
+
if (m2) {
|
|
62
|
+
var parts = m2[1].split(',');
|
|
63
|
+
if (parts.length >= 3) {
|
|
64
|
+
return [parseInt(parts[0], 10) | 0, parseInt(parts[1], 10) | 0, parseInt(parts[2], 10) | 0];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
function P(key, fb) { var a = TH ? toRgb(TH[key]) : null; return a || fb; }
|
|
71
|
+
function rgbStr(a) { return 'rgb(' + a[0] + ',' + a[1] + ',' + a[2] + ')'; }
|
|
72
|
+
function A(c, alpha) {
|
|
73
|
+
var a = toRgb(c) || [255, 255, 255];
|
|
74
|
+
return 'rgba(' + a[0] + ',' + a[1] + ',' + a[2] + ',' + alpha + ')';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* HUD 小字的透明度:深色主题按 v0.7 的 alpha;浅色主题上"淡白字"会消失,统一乘一个系数。
|
|
78
|
+
* 没有 __STYLE__ 时系数 = 1,A() 收到的 alpha 与旧版字面量完全一致。 */
|
|
79
|
+
var HUD_BOOST = (ST && ST.dark === false) ? 1.6 : 1.0;
|
|
80
|
+
function HA(alpha) { return A(C.ink, Math.min(1, alpha * HUD_BOOST)); }
|
|
81
|
+
|
|
82
|
+
/* 底部色板:没有 __STYLE__ 时每一项都是 v0.7 常量(包括字符串写法,便于逐字节对比)。 */
|
|
83
|
+
var C = {
|
|
84
|
+
ink: TH ? rgbStr(P('ink', [234, 242, 255])) : '#eaf2ff',
|
|
85
|
+
dim: TH ? rgbStr(P('dim', [142, 164, 192])) : '#8ea4c0',
|
|
86
|
+
bright: TH ? rgbStr(P('ink', [246, 249, 255])) : '#f6f9ff',
|
|
87
|
+
accent: P('accent', [255, 140, 66]),
|
|
88
|
+
cyan: P('accent2', [90, 210, 255]),
|
|
89
|
+
warm: P('accent2', [255, 196, 120]),
|
|
90
|
+
gridAlpha: (MATERIAL && MATERIAL.texture_alpha != null) ? Number(MATERIAL.texture_alpha) : 0.045,
|
|
91
|
+
vig: (MATERIAL && MATERIAL.vignette != null) ? Number(MATERIAL.vignette) : 0.5,
|
|
92
|
+
/* 白闪:深色主题保持纯白;浅色主题用 ink(白闪在浅底上会"消失",等于闪了一下没有) */
|
|
93
|
+
flash: (ST && ST.dark === false && TH) ? rgbStr(P('ink', [255, 255, 255])) : '#ffffff',
|
|
94
|
+
/* 转场收尾:transition=wash 用底色淡入,其余(含无 style)仍是黑场 */
|
|
95
|
+
wipe: (MOTION && MOTION.transition === 'wash' && TH) ? rgbStr(P('bg', [11, 14, 20])) : '#000000'
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
var BG_STOPS = TH
|
|
99
|
+
? [[0, P('bg', [11, 14, 20])], [0.55, P('bg2', [14, 22, 36])], [1, P('bg', [10, 13, 19])]]
|
|
100
|
+
: [[0, '#0b0e14'], [0.55, '#0e1624'], [1, '#0a0d13']];
|
|
101
|
+
|
|
102
|
+
/* 版式:没有 __STYLE__ 时 HUD = 'default'(v0.7 原样)、LAYOUT = 'center-stack'。 */
|
|
103
|
+
var LAYOUT = (ST && ST.layout && typeof ST.layout.default === 'string') ? ST.layout.default : 'center-stack';
|
|
104
|
+
var HUD = (ST && typeof ST.hud === 'string') ? ST.hud : 'default';
|
|
105
|
+
var ANCHOR = 'center'; // center-stack 居中;left-stack 左;split-right 右
|
|
106
|
+
if (ST) {
|
|
107
|
+
if (LAYOUT === 'left-stack') ANCHOR = 'left';
|
|
108
|
+
else if (LAYOUT === 'split-right') ANCHOR = 'right';
|
|
109
|
+
}
|
|
110
|
+
var MARGIN = X(168);
|
|
111
|
+
/* 标题入场位移:无 style 时是 v0.7 的 90px;有 style 时按 motion.entrance_dy 放大 4 倍
|
|
112
|
+
* (stylekit 里的单位是 PIL 排版像素,网页坐标要乘 4 才接近原来 90px 的观感)。 */
|
|
113
|
+
var RISE = (MOTION && MOTION.entrance_dy) ? S(Number(MOTION.entrance_dy) * 4) : Y(90);
|
|
114
|
+
|
|
115
|
+
function anchorX(width) { // 逐字排版用的左起 x
|
|
116
|
+
if (ANCHOR === 'left') return MARGIN;
|
|
117
|
+
if (ANCHOR === 'right') return W - MARGIN - width;
|
|
118
|
+
return (W - width) / 2;
|
|
119
|
+
}
|
|
120
|
+
function place() { // 居中/左/右对齐的单行文字锚点
|
|
121
|
+
if (ANCHOR === 'left') return { x: MARGIN, align: 'left' };
|
|
122
|
+
if (ANCHOR === 'right') return { x: W - MARGIN, align: 'right' };
|
|
123
|
+
return { x: W / 2, align: 'center' };
|
|
124
|
+
}
|
|
125
|
+
function blockX(blockWidth) { // 进度条 / 强调线这类"块"的左边界
|
|
126
|
+
if (ANCHOR === 'left') return MARGIN;
|
|
127
|
+
if (ANCHOR === 'right') return W - MARGIN - blockWidth;
|
|
128
|
+
return (W - blockWidth) / 2;
|
|
129
|
+
}
|
|
130
|
+
|
|
23
131
|
/* 成片秒数(webengine.py / render.py 会读它做分块与校验)。
|
|
24
132
|
* 默认由场景表推出 = 最后一个场景的 end(加/删/改场景后自动跟着变,不会忘记同步);
|
|
25
133
|
* 要显式锁死(例如页面里还有场景以外的动画),在 index.html 里先写
|
|
@@ -36,125 +144,120 @@
|
|
|
36
144
|
return F.timelineEnd();
|
|
37
145
|
}
|
|
38
146
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
dim: '#8ea4c0',
|
|
42
|
-
accent: [255, 140, 66],
|
|
43
|
-
cyan: [90, 210, 255],
|
|
44
|
-
warm: [255, 196, 120]
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
/* qaText 的「从没露面」检查要先声明 key:必须与 g.text({key}) 对齐 */
|
|
147
|
+
/* qaText 的「从没露面」检查要先声明 key:必须与 g.text({key}) 对齐。
|
|
148
|
+
* hud='none' 的装帧不画品牌/章节文字,就别声明它们(否则 never 会误报)。 */
|
|
48
149
|
F.watch('open.title', '标题:逐帧确定性');
|
|
49
150
|
F.watch('open.sub', '副标题:Canvas 2D 极简时间轴');
|
|
50
151
|
F.watch('data.num', '数字滚动:0 → 1920');
|
|
51
152
|
F.watch('data.cap', '说明:同一帧号 → 同一张图');
|
|
52
153
|
F.watch('fx.title', '标题:确定性渲染');
|
|
53
154
|
F.watch('fx.hud', 'HUD:粒子数 / 帧率标注');
|
|
54
|
-
F.watch('hud.ch', 'HUD:章节名');
|
|
155
|
+
if (HUD !== 'none') F.watch('hud.ch', 'HUD:章节名');
|
|
55
156
|
F.watch('hud.tc', 'HUD:时间码');
|
|
56
|
-
F.watch('hud.brand', 'HUD:品牌名');
|
|
157
|
+
if (HUD !== 'none') F.watch('hud.brand', 'HUD:品牌名');
|
|
57
158
|
|
|
58
159
|
/* ================================================================ 背景层(最先注册 → 最先画) */
|
|
59
160
|
F.always(function (t) {
|
|
60
|
-
g.bgGradient(
|
|
61
|
-
g.grid({ t: t, step: 120, alpha:
|
|
62
|
-
g.vignette(
|
|
161
|
+
g.bgGradient(BG_STOPS);
|
|
162
|
+
g.grid({ t: t, step: X(120), alpha: C.gridAlpha, color: C.cyan, drift: S(6) });
|
|
163
|
+
g.vignette(C.vig);
|
|
63
164
|
});
|
|
64
165
|
|
|
65
166
|
/* ================================================================ 场景 1:逐字入场(0–4s) */
|
|
66
|
-
F.scene('open', 0, 4.0, function (
|
|
167
|
+
F.scene('open', 0, 4.0, function (S1) {
|
|
67
168
|
// ⚠ 命名约定(见 references/frame-qa.md §2.5):
|
|
68
169
|
// qa_frames 的音画同步**先按 kind == cue 的配方名配对**。
|
|
69
170
|
// 所以每个"该响一下"的画面事件,kind 要写成**同一个配方名**(ui_blip / whoosh / impact …);
|
|
70
171
|
// 只有纯装饰性的视觉事件(交界黑场)才用 flash 这类通用名。
|
|
71
172
|
// 底下每条 S.cue 都配了一条同名 S.event —— 这样同步检查才真的在查,而不是静默全过。
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
173
|
+
S1.cue(0.10, 'ui_blip', { pitch: 2200, gain: -4 });
|
|
174
|
+
S1.event(0.10, 'ui_blip', { a: 0.5 });
|
|
175
|
+
S1.cue(0.34, 'ui_blip', { pitch: 2600, gain: -6 });
|
|
176
|
+
S1.event(0.34, 'ui_blip', { a: 0.5 });
|
|
177
|
+
S1.cue(1.05, 'whoosh', { dur: 0.55, size: 0.6, gain: -10 });
|
|
178
|
+
S1.event(1.05, 'whoosh', { size: 0.6 }); // 强调线"画出来"的同时一声掠过
|
|
179
|
+
S1.event(3.70, 'flash', { a: 0.18 }); // 4s 交界黑场起坡(纯视觉,无对应音效)
|
|
79
180
|
|
|
80
181
|
var TITLE = '逐帧确定性';
|
|
81
|
-
var SIZE = 150;
|
|
82
|
-
var TRACK = 8;
|
|
182
|
+
var SIZE = S(150);
|
|
183
|
+
var TRACK = S(8);
|
|
83
184
|
|
|
84
|
-
|
|
85
|
-
// ① 逐字入场:每个字从下方
|
|
185
|
+
S1.update(function (t) {
|
|
186
|
+
// ① 逐字入场:每个字从下方 RISE 升起 + 淡入,错峰 0.075s(RISE 跟随 style 的 motion.entrance_dy)
|
|
86
187
|
var lay = g.layoutChars(TITLE, SIZE, '700', null, TRACK);
|
|
87
|
-
var x = (
|
|
88
|
-
var y = 470;
|
|
188
|
+
var x = anchorX(lay.width);
|
|
189
|
+
var y = Y(470);
|
|
89
190
|
for (var i = 0; i < lay.chars.length; i++) {
|
|
90
191
|
var p = E.out5(seg(t, 0.16 + i * 0.075, 0.72 + i * 0.075));
|
|
91
192
|
var a = seg(t, 0.16 + i * 0.075, 0.44 + i * 0.075);
|
|
92
|
-
g.text(lay.chars[i].ch, x + lay.chars[i].x, y + (1 - p) *
|
|
193
|
+
g.text(lay.chars[i].ch, x + lay.chars[i].x, y + (1 - p) * RISE, {
|
|
93
194
|
size: SIZE, weight: '700', color: C.ink, alpha: a,
|
|
94
195
|
key: 'open.title.' + i, group: 'open.title'
|
|
95
196
|
});
|
|
96
197
|
}
|
|
97
|
-
//
|
|
98
|
-
var lw = E.out3(seg(t, 1.15, 1.9)) * 360;
|
|
99
|
-
g.rect(
|
|
198
|
+
// 标题下方一条「画出来」的强调线(跟随版式锚点)
|
|
199
|
+
var lw = E.out3(seg(t, 1.15, 1.9)) * X(360);
|
|
200
|
+
g.rect(blockX(lw), y + Y(118), lw, S(4), A(C.accent, 0.9));
|
|
100
201
|
// 副标题淡入 + 上浮
|
|
101
202
|
var a2 = E.out3(seg(t, 1.35, 2.05));
|
|
102
|
-
|
|
103
|
-
|
|
203
|
+
var ps = place();
|
|
204
|
+
g.text('Canvas 2D · 极简时间轴 · 无 GSAP / three.js', ps.x, Y(646) + (1 - a2) * Y(26), {
|
|
205
|
+
size: S(40), weight: '400', color: C.dim, align: ps.align, alpha: a2,
|
|
104
206
|
key: 'open.sub', group: 'open.sub'
|
|
105
207
|
});
|
|
106
208
|
// 两侧装饰竖线("长出来")
|
|
107
209
|
var dw = E.out3(seg(t, 1.6, 2.2));
|
|
108
210
|
if (dw > 0.001) {
|
|
109
|
-
g.rect(196, 430 + (1 - dw) * 60, 2, 120 * dw,
|
|
110
|
-
g.rect(W - 198, 430 + (1 - dw) * 60, 2, 120 * dw,
|
|
211
|
+
g.rect(X(196), Y(430) + (1 - dw) * Y(60), S(2), Y(120) * dw, A(C.cyan, 0.55));
|
|
212
|
+
g.rect(W - X(198), Y(430) + (1 - dw) * Y(60), S(2), Y(120) * dw, A(C.cyan, 0.55));
|
|
111
213
|
}
|
|
112
214
|
});
|
|
113
215
|
});
|
|
114
216
|
|
|
115
217
|
/* ================================================================ 场景 2:数字滚动(4–8s) */
|
|
116
|
-
F.scene('data', 4.0, 4.0, function (
|
|
218
|
+
F.scene('data', 4.0, 4.0, function (S2) {
|
|
117
219
|
// 同上:cue 与 event 成对出现、同名,同步检查才有东西可配
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
220
|
+
S2.cue(0.10, 'data_chirp', { gain: -4 });
|
|
221
|
+
S2.event(0.10, 'data_chirp', {});
|
|
222
|
+
S2.cue(1.85, 'ui_click', { gain: -5 });
|
|
223
|
+
S2.event(1.85, 'ui_click', {}); // 数字滚完落定的一声"咔"
|
|
224
|
+
S2.cue(2.12, 'relay_click', { gain: -12 });
|
|
225
|
+
S2.event(2.12, 'relay_click', {});
|
|
226
|
+
|
|
227
|
+
S2.update(function (t) {
|
|
126
228
|
// ② 数字滚动:0 → 1920,走 out5 缓动(声音 chirp 与它同一条时间线)
|
|
127
229
|
var p = E.out5(seg(t, 0.25, 1.85));
|
|
128
230
|
var num = Math.round(lerp(0, 1920, p));
|
|
129
231
|
var s = String(num);
|
|
130
232
|
while (s.length < 4) s = '0' + s;
|
|
131
|
-
|
|
132
|
-
|
|
233
|
+
var pn = place();
|
|
234
|
+
g.text(s, pn.x, Y(470), {
|
|
235
|
+
size: S(236), weight: '700', align: pn.align, color: C.bright,
|
|
133
236
|
key: 'data.num', group: 'data.num'
|
|
134
237
|
});
|
|
135
|
-
g.text('FRAME-STABLE CUTS',
|
|
136
|
-
size: 26, weight: '500', align:
|
|
238
|
+
g.text('FRAME-STABLE CUTS', pn.x, Y(606), {
|
|
239
|
+
size: S(26), weight: '500', align: pn.align, color: A(C.cyan, 0.85),
|
|
137
240
|
alpha: E.out3(seg(t, 0.9, 1.4)), key: 'data.kicker', group: 'data.kicker'
|
|
138
241
|
});
|
|
139
242
|
// 说明文字
|
|
140
243
|
var a = E.out3(seg(t, 1.05, 1.75));
|
|
141
|
-
g.text('同一帧号 → 同一张图',
|
|
142
|
-
size: 44, weight: '400', align:
|
|
244
|
+
g.text('同一帧号 → 同一张图', pn.x, Y(700) + (1 - a) * Y(20), {
|
|
245
|
+
size: S(44), weight: '400', align: pn.align, color: C.dim, alpha: a,
|
|
143
246
|
key: 'data.cap', group: 'data.cap'
|
|
144
247
|
});
|
|
145
248
|
// 进度条(数字滚动的可视化)
|
|
146
|
-
var bw = 720, bx = (
|
|
147
|
-
g.rect(bx, by, bw, 3,
|
|
148
|
-
g.rect(bx, by, bw * p, 3,
|
|
249
|
+
var bw = X(720), bx = blockX(bw), by = Y(792);
|
|
250
|
+
g.rect(bx, by, bw, S(3), A(C.ink, Math.min(0.5, 0.12 * HUD_BOOST)));
|
|
251
|
+
g.rect(bx, by, bw * p, S(3), A(C.accent, 0.95));
|
|
149
252
|
// ④ HUD 标注:右上角小徽标
|
|
150
253
|
var ha = E.out3(seg(t, 0.5, 1.0));
|
|
151
254
|
if (ha > 0.001) {
|
|
152
|
-
g.text('◆ DATA LAYER', W - 64, 206, {
|
|
153
|
-
size: 22, weight: '600', align: 'right', color:
|
|
255
|
+
g.text('◆ DATA LAYER', W - X(64), Y(206), {
|
|
256
|
+
size: S(22), weight: '600', align: 'right', color: A(C.accent, 0.9),
|
|
154
257
|
alpha: ha, key: 'data.hud', group: 'data.hud'
|
|
155
258
|
});
|
|
156
|
-
g.text('counter → 1920', W - 64, 238, {
|
|
157
|
-
size: 18, weight: '400', align: 'right', color:
|
|
259
|
+
g.text('counter → 1920', W - X(64), Y(238), {
|
|
260
|
+
size: S(18), weight: '400', align: 'right', color: HA(0.45),
|
|
158
261
|
alpha: ha, key: 'data.hud2', group: 'data.hud'
|
|
159
262
|
});
|
|
160
263
|
}
|
|
@@ -162,50 +265,51 @@
|
|
|
162
265
|
});
|
|
163
266
|
|
|
164
267
|
/* ================================================================ 场景 3:粒子 / 发光(8–12s) */
|
|
165
|
-
F.scene('fx', 8.0, 4.0, function (
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
268
|
+
F.scene('fx', 8.0, 4.0, function (S3) {
|
|
269
|
+
S3.event(-0.30, 'flash', { a: 0.18 }); // 8s 交界黑场起坡(本幕负偏移 = 上一幕末帧)
|
|
270
|
+
S3.cue(0.02, 'reverse_swell', { gain: -4, dur: 1.2 });
|
|
271
|
+
S3.event(0.02, 'reverse_swell', {}); // 吸气 = 画面从黑里亮起来
|
|
272
|
+
S3.cue(0.62, 'impact', { size: 1.15, gain: -1 });
|
|
273
|
+
S3.event(0.62, 'impact', { a: 0.8 }); // 重击:白闪 + 冲击环同一时刻
|
|
274
|
+
S3.event(0.62, 'ring', { r: 620 }); // 纯装饰(无对应音效)
|
|
275
|
+
S3.cue(1.55, 'ui_blip', { pitch: 1800, gain: -8 });
|
|
276
|
+
S3.event(1.55, 'ui_blip', { a: 0.5 });
|
|
277
|
+
|
|
278
|
+
S3.update(function (t) {
|
|
176
279
|
// ③ 粒子 / 发光:位置全部由 hash(seed,i,*) 决定,同一 t 永远同一张图
|
|
177
|
-
g.particles({ n: 150, seed: 20261107, t: t, color: C.accent, alpha: 0.5, speed: 1.0, rmin: 2, rmax: 6, wobble: 30 });
|
|
280
|
+
g.particles({ n: 150, seed: 20261107, t: t, color: C.accent, alpha: 0.5, speed: 1.0, rmin: S(2), rmax: S(6), wobble: Y(30) });
|
|
178
281
|
// 冲击环:impact 时刻从中心扩散
|
|
179
282
|
var rp = E.out5(seg(t, 0.62, 2.3));
|
|
180
283
|
if (rp > 0.001 && rp < 0.999) {
|
|
181
|
-
g.ring(W / 2, 520, lerp(40, 640, rp),
|
|
284
|
+
g.ring(W / 2, Y(520), lerp(S(40), S(640), rp), A(C.warm, (1 - rp) * 0.55), S(3));
|
|
182
285
|
}
|
|
183
286
|
// 中心发光:正弦呼吸,相位只由绝对时间 T 决定
|
|
184
287
|
var T = 8.0 + t;
|
|
185
|
-
g.glow(W / 2, 520, 250, C.accent, 0.18 + 0.07 * Math.sin(T * 2.0));
|
|
186
|
-
g.glow(W / 2, 520, 130, C.warm, 0.10 + 0.05 * Math.sin(T * 2.6 + 1.2));
|
|
288
|
+
g.glow(W / 2, Y(520), S(250), C.accent, 0.18 + 0.07 * Math.sin(T * 2.0));
|
|
289
|
+
g.glow(W / 2, Y(520), S(130), C.warm, 0.10 + 0.05 * Math.sin(T * 2.6 + 1.2));
|
|
187
290
|
// 标题逐字入场(复用同一套缓动)
|
|
188
|
-
var lay = g.layoutChars('确定性渲染', 132, '700', null, 10);
|
|
189
|
-
var x = (
|
|
190
|
-
var y = 520;
|
|
291
|
+
var lay = g.layoutChars('确定性渲染', S(132), '700', null, S(10));
|
|
292
|
+
var x = anchorX(lay.width);
|
|
293
|
+
var y = Y(520);
|
|
191
294
|
for (var i = 0; i < lay.chars.length; i++) {
|
|
192
295
|
var p = E.out5(seg(t, 1.05 + i * 0.07, 1.55 + i * 0.07));
|
|
193
|
-
g.text(lay.chars[i].ch, x + lay.chars[i].x, y + (1 - p) * 60, {
|
|
194
|
-
size: 132, weight: '700', color:
|
|
296
|
+
g.text(lay.chars[i].ch, x + lay.chars[i].x, y + (1 - p) * Y(60), {
|
|
297
|
+
size: S(132), weight: '700', color: C.flash, alpha: p,
|
|
195
298
|
key: 'fx.title.' + i, group: 'fx.title'
|
|
196
299
|
});
|
|
197
300
|
}
|
|
198
301
|
// HUD 标注
|
|
199
|
-
|
|
200
|
-
|
|
302
|
+
var pf = place();
|
|
303
|
+
g.text('PARTICLES 150 · 30 FPS · 同一帧同一图', pf.x, Y(660), {
|
|
304
|
+
size: S(28), weight: '400', align: pf.align, color: HA(0.66),
|
|
201
305
|
alpha: E.out3(seg(t, 1.3, 1.9)), key: 'fx.hud', group: 'fx.hud'
|
|
202
306
|
});
|
|
203
307
|
// 白闪:impact 事件的可视化(event 已登记同一条时间;只从 impact 时刻开始衰减)
|
|
204
308
|
var fp = seg(t, 0.62, 0.92);
|
|
205
|
-
if (t >= 0.62 && fp < 0.999) g.flash(0.55 * (1 - fp),
|
|
206
|
-
//
|
|
309
|
+
if (t >= 0.62 && fp < 0.999) g.flash(0.55 * (1 - fp), C.flash);
|
|
310
|
+
// 片尾淡出到黑(wash 主题淡到自己的底色)
|
|
207
311
|
var fo = E.in2(seg(t, 3.35, 4.0));
|
|
208
|
-
if (fo > 0.001) g.flash(fo,
|
|
312
|
+
if (fo > 0.001) g.flash(fo, C.wipe);
|
|
209
313
|
});
|
|
210
314
|
});
|
|
211
315
|
|
|
@@ -217,30 +321,49 @@
|
|
|
217
321
|
/* ================================================================ HUD 层(最后注册 → 最后画,压在所有场景之上) */
|
|
218
322
|
F.always(function (t) {
|
|
219
323
|
var dur = Number(global.DURATION) || 12.0;
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
324
|
+
// HUD 装帧(只在注入 style 时启用;default = v0.7 原样)
|
|
325
|
+
if (HUD === 'chip-left') {
|
|
326
|
+
g.rrect(X(48), Y(36), X(300), Y(84), S(12), A(P('surface', [26, 42, 64]), 0.55));
|
|
327
|
+
} else if (HUD === 'corner-line') {
|
|
328
|
+
var cl = A(P('rule', [255, 255, 255]), 0.28);
|
|
329
|
+
var m = X(40), ln = X(96), th = S(1);
|
|
330
|
+
g.rect(m, m, ln, th, cl);
|
|
331
|
+
g.rect(m, m, th, ln, cl);
|
|
332
|
+
g.rect(W - m - ln, m, ln, th, cl);
|
|
333
|
+
g.rect(W - m - th, m, th, ln, cl);
|
|
334
|
+
g.rect(m, H - m - th, ln, th, cl);
|
|
335
|
+
g.rect(m, H - m - th, th, ln, cl);
|
|
336
|
+
g.rect(W - m - ln, H - m - th, ln, th, cl);
|
|
337
|
+
g.rect(W - m - th, H - m - th, th, ln, cl);
|
|
338
|
+
}
|
|
339
|
+
if (HUD !== 'none') {
|
|
340
|
+
g.text('DSH VIDEO STUDIO', X(64), Y(60), {
|
|
341
|
+
size: S(24), weight: '500', color: HA(0.55), key: 'hud.brand'
|
|
342
|
+
});
|
|
343
|
+
var chap = t < 4 ? '01 · 逐字入场' : (t < 8 ? '02 · 数字滚动' : '03 · 粒子 / 发光');
|
|
344
|
+
g.text(chap, X(64), Y(96), {
|
|
345
|
+
size: S(22), weight: '500', color: A(C.cyan, 0.8), key: 'hud.ch', group: 'hud.ch'
|
|
346
|
+
});
|
|
347
|
+
}
|
|
227
348
|
// 时间码每帧都在变 → 也保证「不同帧渲染结果不同」
|
|
228
349
|
var tc = 'T ' + (t < 10 ? '0' : '') + t.toFixed(2) + ' / ' + dur.toFixed(2);
|
|
229
|
-
g.text(tc, W - 64, 60, {
|
|
230
|
-
size: 22, weight: '500', align: 'right', color:
|
|
350
|
+
g.text(tc, W - X(64), Y(60), {
|
|
351
|
+
size: S(22), weight: '500', align: 'right', color: HA(0.45),
|
|
231
352
|
key: 'hud.tc', group: 'hud.tc'
|
|
232
353
|
});
|
|
233
|
-
//
|
|
234
|
-
g.rect(64, 122, W - 128, 1,
|
|
235
|
-
g.rect(64, 122, (W - 128) * clamp(t / dur, 0, 1), 2,
|
|
236
|
-
//
|
|
237
|
-
|
|
238
|
-
|
|
354
|
+
// 顶部进度线(HUD 装帧为 none 时也保留:它是"逐帧在动"的最直观证据)
|
|
355
|
+
g.rect(X(64), Y(122), W - X(128), S(1), A(C.cyan, 0.16));
|
|
356
|
+
g.rect(X(64), Y(122), (W - X(128)) * clamp(t / dur, 0, 1), S(2), A(C.accent, 0.85));
|
|
357
|
+
// 底部品牌行(有 style 时写风格名,便于目检"这条片用的是哪套画风")
|
|
358
|
+
var foot = ST ? ('v0.8.0 · ' + (ST.style_name || ST.style || ST.theme_label || 'style') + ' · Canvas2D 逐帧渲染')
|
|
359
|
+
: 'v0.3.0 · Canvas2D 逐帧渲染 · 不使用 GSAP / three.js';
|
|
360
|
+
g.text(foot, X(64), H - Y(52), {
|
|
361
|
+
size: S(20), weight: '400', color: HA(0.3), key: 'hud.foot'
|
|
239
362
|
});
|
|
240
|
-
// 场景交界黑场「呼吸」(4s / 8s
|
|
363
|
+
// 场景交界黑场「呼吸」(4s / 8s),避免硬切(wash 主题淡到自己的底色)
|
|
241
364
|
var d = Math.min(Math.abs(t - 4.0), Math.abs(t - 8.0));
|
|
242
365
|
var dip = clamp(1 - d / 0.3);
|
|
243
|
-
if (dip > 0.001) g.flash(0.5 * E.out2(dip),
|
|
366
|
+
if (dip > 0.001) g.flash(0.5 * E.out2(dip), C.wipe);
|
|
244
367
|
// ↑ 这条在 dip 的"起坡帧"会有一个可见的亮度跳变(clamp 从 0 开始斜升)。
|
|
245
368
|
// 对观众来说它是转场的一部分,但对逐帧质检它长得像单帧闪烁:
|
|
246
369
|
// 所以把起坡时刻登记成 flash 事件,qa_frames 会把它从 flicker 里排除(见 FLICKER_EXPLAIN_WIN)。
|
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=1920, height=1080">
|
|
6
|
-
<title>DSH Video Studio · Canvas2D 逐帧渲染模板(v0.
|
|
6
|
+
<title>DSH Video Studio · Canvas2D 逐帧渲染模板(v0.8.0)</title>
|
|
7
7
|
<!-- 本地样式与脚本:不引 CDN、不连任何网络 -->
|
|
8
8
|
<link rel="stylesheet" href="style.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
|
-
<!-- 1920x1080
|
|
11
|
+
<!-- 画布默认 1920x1080,必须与 webengine.py 的 viewport 一致,截图才是 1:1 像素。
|
|
12
|
+
v0.8.0:webengine.open_page(style=...) 注入 window.__STYLE__.canvas 时,engine.js 会把
|
|
13
|
+
canvas 宽高与 CSS 尺寸改成注入值(仍只允许 16:9 横屏);没有 __STYLE__ 时就是下面这组。 -->
|
|
12
14
|
<canvas id="stage" width="1920" height="1080"></canvas>
|
|
13
15
|
|
|
14
16
|
<script src="engine.js"></script>
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
/* qa_text.js — 浏览器端文字质检(v0.
|
|
1
|
+
/* qa_text.js — 浏览器端文字质检(v0.8.0)
|
|
2
2
|
*
|
|
3
3
|
* 逐帧重渲染画布,读回 engine.js 登记的文字包围盒,找出四类问题:
|
|
4
4
|
* ① overlaps 文字互相压住(同一 group 内的逐字排版不算)
|
|
5
|
-
* ② offscreen
|
|
6
|
-
* ③ brief 一闪而过(可见时长 < 0.6s
|
|
5
|
+
* ② offscreen 出画(包围盒越过画布边界 6px;边界取 FK.W/FK.H,v0.8 起画布可非 1080p)
|
|
6
|
+
* ③ brief 一闪而过(可见时长 < 0.6s;按采样点数×步长算,不是首末跨度)
|
|
7
7
|
* ④ never 从没露面(需页面用 FK.watch(key, 标签) 声明)
|
|
8
8
|
*
|
|
9
|
+
* 深审修复:参数 fail-closed —— step 非正 / >= 0.6s(brief 阈值)或 t1<t0 直接抛错。
|
|
10
|
+
* 以前这三种情况会返回空结果,调用方(CLI textqa)把它当"零缺陷"exit 0:门禁看着是绿的,
|
|
11
|
+
* 其实一行都没查。
|
|
12
|
+
*
|
|
9
13
|
* 判据改编自 ai-history-film(MIT)video/qa_text.js:
|
|
10
14
|
* 重叠面积比 0.08、出画容差 6px、短命阈值 0.6s。
|
|
11
15
|
*/
|
|
@@ -19,16 +23,35 @@
|
|
|
19
23
|
}
|
|
20
24
|
function nameOf(q) { return (q.group || q.key) + '「' + short(q.text) + '」'; }
|
|
21
25
|
|
|
26
|
+
/* 「一闪而过」判据:可见时长 < 0.6 s(与 webengine.QA_BRIEF_S / 文档同口径)。
|
|
27
|
+
* 采样步长必须小于它,否则每段文字最多被采到 1 个点,该判据永远不可能触发 —— 那时
|
|
28
|
+
* 返回一个"零缺陷"的结果等于骗人,所以这里直接抛错(Python 侧也有一道同样的校验,
|
|
29
|
+
* 为的是 CLI 能给 exit 2 + 可读报错)。 */
|
|
30
|
+
var BRIEF_S = 0.6;
|
|
31
|
+
|
|
22
32
|
async function qaText(t0, t1, step) {
|
|
23
33
|
var F = global.FK;
|
|
24
34
|
if (!F) throw new Error('qa_text.js: 找不到 window.FK(engine.js 未加载?)');
|
|
25
|
-
var start =
|
|
35
|
+
var start = (t0 == null) ? 0 : Number(t0);
|
|
26
36
|
var dur = Number(global.DURATION) || 0;
|
|
27
37
|
var end = (t1 == null || !isFinite(Number(t1))) ? dur : Number(t1);
|
|
28
38
|
var fps = Number(global.FPS) || 30;
|
|
29
|
-
var
|
|
30
|
-
|
|
31
|
-
|
|
39
|
+
var stepN = (step == null) ? 0.2 : Number(step);
|
|
40
|
+
if (!isFinite(start) || !isFinite(end)) {
|
|
41
|
+
throw new Error('qaText: 时间范围必须是有限秒数(t0=' + t0 + ' t1=' + t1 + ')');
|
|
42
|
+
}
|
|
43
|
+
if (!isFinite(stepN) || stepN <= 0) {
|
|
44
|
+
throw new Error('qaText: step 必须是正数秒(收到 ' + step + ')');
|
|
45
|
+
}
|
|
46
|
+
if (stepN >= BRIEF_S) {
|
|
47
|
+
throw new Error('qaText: step=' + stepN + 's 必须小于 ' + BRIEF_S
|
|
48
|
+
+ 's(一闪而过阈值);步长 >= 阈值时每段文字最多被采到 1 个点,该判据永远不可能触发');
|
|
49
|
+
}
|
|
50
|
+
if (end < start) {
|
|
51
|
+
throw new Error('qaText: t1=' + end + ' < t0=' + start
|
|
52
|
+
+ '(空扫描不是"没有缺陷",不能当通过)');
|
|
53
|
+
}
|
|
54
|
+
var dt = Math.max(stepN, 1 / fps);
|
|
32
55
|
F.qaReset();
|
|
33
56
|
var sampled = 0; // 实际扫描的采样帧数(=== frames 字段,与文档一致)
|
|
34
57
|
|
|
@@ -84,7 +107,12 @@
|
|
|
84
107
|
|
|
85
108
|
var brief = [];
|
|
86
109
|
life.forEach(function (L, key) {
|
|
87
|
-
|
|
110
|
+
/* 可见时长 = 被采到的点数 × 步长。不能再用 last - first + dt(首末跨度):
|
|
111
|
+
* 同一个 key 如果在两段互不相连的时间各闪 0.1 s(比如 t=0 与 t=5.9 各一次),
|
|
112
|
+
* 跨度会把 0.2 s 的闪烁算成 6.1 s 的可见时长而漏报。连续出现时两种算法等价
|
|
113
|
+
* (点数 = 跨度/dt + 1),所以换成 n*dt 只影响"分散出现"这一类,判据更严格也
|
|
114
|
+
* 更符合文档写的"可见时长"。 */
|
|
115
|
+
var d = L.n * dt;
|
|
88
116
|
if (d < 0.6) brief.push({ at: r2(L.first), dur: r2(d), el: (L.group || key) + '「' + short(L.text) + '」' });
|
|
89
117
|
});
|
|
90
118
|
|
|
@@ -71,7 +71,8 @@ def build_audio(self, total=None, sr=SR, base_db=None, default_gain_db=0.0,
|
|
|
71
71
|
- **必须保证**:同 `fingerprint` 逐采样一致;不同 fingerprint 的相关性显著下降(§5 阈值)。
|
|
72
72
|
- 新增报告字段:`variation`(dict:`{"chord_swaps": n, "motif_shifts": n, "drum_ghosts": n}`),
|
|
73
73
|
写进 work/score.json,方便审计。
|
|
74
|
-
- legacy 路径(style=None
|
|
74
|
+
- legacy 路径(style=None)**逐字节不变**(`scorekit.py --selftest` 的第 20 条会验;
|
|
75
|
+
别和 `selfcheck.py` 的编号搞混——那边第 21 条只查 scorekit 同 seed 确定性)。
|
|
75
76
|
|
|
76
77
|
### 3.4 assets/scenes.py 的 cues()(B 路)
|
|
77
78
|
|
|
@@ -128,11 +129,11 @@ def build_audio(self, total=None, sr=SR, base_db=None, default_gain_db=0.0,
|
|
|
128
129
|
> 下面 1–4 的路径都按**仓库根**写(`python skills/video-studio/scripts/…`);如果你已经 `cd` 进技能目录,
|
|
129
130
|
> 去掉 `skills/video-studio/` 前缀即可。`<REPO>` = 仓库根 / 装好后的包根。
|
|
130
131
|
|
|
131
|
-
1. `python skills/video-studio/scripts/selfcheck.py` 全 PASS(现在 43
|
|
132
|
+
1. `python skills/video-studio/scripts/selfcheck.py` 全 PASS(现在 51 条;v0.7.2 时的 43 条是旧的)。
|
|
132
133
|
2. `python skills/video-studio/scripts/scorekit.py --selftest` 全 PASS。
|
|
133
134
|
3. `python skills/video-studio/scripts/sfxkit.py --selftest` 全 PASS。
|
|
134
135
|
4. `python <REPO>/tools/ci_check.py` 全 PASS(在包根执行)。
|
|
135
|
-
5. legacy
|
|
136
|
+
5. legacy 逐样本不变:`scorekit.py --selftest` 第 20 条 + `selfcheck.py` 第 26(风格 legacy)/33(音效 legacy)条,**这些断言不要改**。
|
|
136
137
|
|
|
137
138
|
## 6. 协作规则
|
|
138
139
|
|
|
@@ -439,7 +439,7 @@ v0.5 的 style pack 决定的是**风格**(8 套),不是**这一部的具
|
|
|
439
439
|
|
|
440
440
|
| 层 | 行为 | 开关 |
|
|
441
441
|
|---|---|---|
|
|
442
|
-
| 配乐和声/动机/鼓型 | 内容指纹做稳定局部替换(每两小节、±1 级数、ghost 击) | `style: auto
|
|
442
|
+
| 配乐和声/动机/鼓型 | 内容指纹做稳定局部替换(每两小节、±1 级数、ghost 击) | `style: auto`(默认);`seed_from_content: false` **只关 seed 派生**(音符细节不再随内容变),结构变奏照旧——`render.py` 只要不是 legacy 就把 `fingerprint` 传给 scorekit(`st_score` 里 `fp_hex` 那段)。想连结构一起回到旧行为只有不写 `style`(legacy) |
|
|
443
443
|
| 音效 seed | `content_seed(base, style, fp, type, t)`,随内容变 | `sfx.content_seed`(默认 true;false = 旧 seed_base+i) |
|
|
444
444
|
| 提示音调性 | `in_key(freq, key)` 跟随配乐调性 | `CueBus.build_audio(key=...)`,render.py 自动传 |
|
|
445
445
|
| 段落边界 | [边界-2.2s, 边界] 内的 whoosh/riser 自动 -3 dB | `sfx.duck_transitions`(默认 true) |
|
|
@@ -477,6 +477,6 @@ D 组构造"有人声段 + 间隙",量人声段旁白/背景响度差。任何
|
|
|
477
477
|
### 13.4 改音频代码后的自检清单
|
|
478
478
|
|
|
479
479
|
1. `python <REPO>/tools/audio_similarity.py --strict` —— A/B/C 三组达标(在包根执行)。
|
|
480
|
-
2. `python selfcheck.py` ——
|
|
480
|
+
2. `python selfcheck.py` —— 51 条全 PASS(第 33 条盯 legacy 逐样本兼容,第 35–38 条盯旁白优先,第 39 条盯音频边界)。
|
|
481
481
|
3. `python scorekit.py --selftest` / `python sfxkit.py --selftest` —— 各自全 PASS。
|
|
482
482
|
4. 改 `assets/scenes.py` 的 `cues()` 后,跑一次 `--stage cues` 看类型分布是否覆盖 hook/body/quote/outro。
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
|---|---|---|
|
|
38
38
|
| 改了 `text` 只重跑 `tts` | 新语音配旧时间轴 → 字幕漂移、音效错位 | 跑 `--stage explain`,按它给的最小重跑命令 |
|
|
39
39
|
| 改了 `style` 只重跑 `frames` | 画风换了但配乐/声线还是旧的(或反之) | `style` 是所有下游的上游,通常整链重跑 |
|
|
40
|
-
| 手工删 `work/_plan.json` 绕过指纹 | 拼接出的片子画面和旁白对不上,且**不报错** | 删 `work/segments` 整目录重渲 |
|
|
40
|
+
| 手工删 `work/segments/_plan.json` 绕过指纹 | 拼接出的片子画面和旁白对不上,且**不报错** | 删 `work/segments` 整目录重渲 |
|
|
41
41
|
|
|
42
42
|
## 3. 一条命令代替这张表
|
|
43
43
|
|