@dsh-gis/dsh-video-studio-skill 1.0.2 → 1.0.4
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 +49 -0
- package/README.md +84 -108
- package/THIRD_PARTY_NOTICES.md +2 -2
- package/package.json +1 -1
- package/schemas/design.schema.json +5 -5
- package/schemas/film.schema.json +31 -31
- package/skills/video-studio/SKILL.md +168 -268
- package/skills/video-studio/assets/film.json +2 -3
- package/skills/video-studio/assets/golden_frames.json +11 -11
- package/skills/video-studio/assets/scenes.py +92 -49
- package/skills/video-studio/references/{style-packs.md → appearance.md} +17 -15
- package/skills/video-studio/references/audio-variation.md +73 -96
- package/skills/video-studio/references/audio.md +40 -44
- package/skills/video-studio/references/change-impact.md +2 -2
- package/skills/video-studio/references/{v08-quality.md → delivery-gates.md} +9 -11
- package/skills/video-studio/references/frame-qa.md +47 -15
- package/skills/video-studio/references/pipeline.md +4 -4
- package/skills/video-studio/references/pitfalls.md +22 -3
- package/skills/video-studio/references/proposal.md +19 -36
- package/skills/video-studio/references/quality-gates.md +11 -11
- package/skills/video-studio/references/scene-authoring.md +2 -2
- package/skills/video-studio/references/scene-grammar.md +17 -24
- package/skills/video-studio/references/sfx-design.md +7 -5
- package/skills/video-studio/references/web-engine.md +7 -7
- package/skills/video-studio/references/word-timeline.md +7 -7
- package/skills/video-studio/requirements.txt +9 -13
- package/skills/video-studio/scripts/designkit.py +84 -89
- package/skills/video-studio/scripts/filmcheck.py +19 -19
- package/skills/video-studio/scripts/filmkit.py +2949 -2948
- package/skills/video-studio/scripts/gates.py +4 -4
- package/skills/video-studio/scripts/grammar.py +10 -10
- package/skills/video-studio/scripts/musicgen.py +3 -4
- package/skills/video-studio/scripts/preflight.py +5 -5
- package/skills/video-studio/scripts/previewkit.py +11 -12
- package/skills/video-studio/scripts/propose.py +11 -11
- package/skills/video-studio/scripts/qa_frames.py +34 -35
- package/skills/video-studio/scripts/qaasr.py +26 -7
- package/skills/video-studio/scripts/qavmaf.py +19 -19
- package/skills/video-studio/scripts/render.py +137 -141
- package/skills/video-studio/scripts/scorekit.py +21 -23
- package/skills/video-studio/scripts/selfcheck.py +208 -135
- package/skills/video-studio/scripts/sfxkit.py +27 -26
- package/skills/video-studio/scripts/stagewatch.py +4 -4
- package/skills/video-studio/scripts/stylekit.py +77 -282
- package/skills/video-studio/scripts/voicekit.py +43 -44
- package/skills/video-studio/scripts/webengine.py +62 -64
- package/tools/audio_similarity.py +12 -12
- package/tools/ci_check.py +16 -16
- package/tools/estimate.py +24 -25
- package/tools/golden_frames.py +17 -18
- package/tools/silhouette_qa.py +13 -14
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
|
-
"""webengine.py —
|
|
2
|
+
"""webengine.py — 浏览器逐帧渲染引擎。
|
|
3
3
|
|
|
4
4
|
用无头浏览器(优先 Microsoft Edge,失败回落 Playwright Chromium)打开 Canvas 页面,
|
|
5
5
|
把时间精确跳到「帧号 / FPS」后经 CDP 截 PNG,喂给 ffmpeg 的 image2pipe 编成视频。
|
|
6
6
|
分块并行 + 断点续跑 + 显式缺口检测 + 崩溃重开;同一帧号渲染出的画面完全一致。
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
**跟随风格包**。公开入口都有一组可选参数(style / width / height / fps):
|
|
9
9
|
style stylekit 的 style plan(dict,含 "theme")或裸主题 dict;None = 不注入
|
|
10
10
|
width/height 画布尺寸,只接受 16:9 横屏偶数尺寸(竖屏 / 4:3 / 奇数直接 raise)
|
|
11
11
|
fps 写进注入 payload 的帧率;None = 不约束(页面自己的 window.FPS 为准)
|
|
12
12
|
注入方式:add_init_script 在**页面加载前**写 window.__STYLE__(主题色板 / 版式 / 字体栈 /
|
|
13
|
-
画布 w·h·fps)。assets/web/engine.js 与 film.js
|
|
13
|
+
画布 w·h·fps)。assets/web/engine.js 与 film.js 优先读它,取不到就退回深蓝橙常量。
|
|
14
14
|
|
|
15
15
|
三条硬约束(改这块必须先读):
|
|
16
16
|
1. **style=None 时一个字节都不注入**:open_page 连 add_init_script 都不调,页面里不存在
|
|
17
|
-
window.__STYLE__
|
|
18
|
-
|
|
17
|
+
window.__STYLE__,渲染像素与默认外观逐字节一致(自检里有「无 style 页面无 __STYLE__」断言,
|
|
18
|
+
外部还有逐帧 SHA-256 对照)。
|
|
19
19
|
2. 注入内容只来自入参:不读环境变量/时间/随机数,JSON 用 sort_keys 序列化,同输入同输出。
|
|
20
20
|
3. 尺寸与帧率 fail-closed:不符合 16:9 横屏偶数就 raise,绝不静默缩放或改帧率
|
|
21
21
|
(静默改帧率会让分块帧号整体错位,成片音画不同步且门禁看不出来)。
|
|
22
22
|
|
|
23
23
|
算法改编自 ai-history-film(MIT)video/render.py(逐帧 seek / 分块并行 / 崩溃重开 / 断点续跑),
|
|
24
|
-
|
|
24
|
+
代码按本项目契约重写;拼接复用 filmkit.concat_segments(必须传绝对路径,别改成相对路径)。
|
|
25
25
|
|
|
26
26
|
设计要点:
|
|
27
27
|
1. 逐帧渲染是无状态的:崩溃后重开浏览器从同一帧继续即可,不需要回滚。
|
|
@@ -38,13 +38,13 @@ CLI:
|
|
|
38
38
|
python webengine.py full <html> --out x.mp4 [--workers 2] [--resume]
|
|
39
39
|
python webengine.py textqa <html> [--from 0 --to 12 --step 0.2]
|
|
40
40
|
python webengine.py selftest # 等价于 --selftest
|
|
41
|
-
# 以上命令都可加风格参数(缺省 =
|
|
42
|
-
# --style
|
|
41
|
+
# 以上命令都可加风格参数(缺省 = 不注入):
|
|
42
|
+
# --style work/style.json | --style '{"theme":{...}}'
|
|
43
43
|
# --width 1920 --height 1080 [--fps 30]
|
|
44
44
|
"""
|
|
45
45
|
from __future__ import annotations
|
|
46
46
|
|
|
47
|
-
__version__ = "0.
|
|
47
|
+
__version__ = "1.0.4"
|
|
48
48
|
|
|
49
49
|
import argparse
|
|
50
50
|
import asyncio
|
|
@@ -87,7 +87,7 @@ BROWSER_ARGS = [
|
|
|
87
87
|
"--disable-renderer-backgrounding",
|
|
88
88
|
"--disable-backgrounding-occluded-windows",
|
|
89
89
|
]
|
|
90
|
-
#
|
|
90
|
+
# 确定性模式:关掉 GPU 光栅,2D canvas 全部走软件光栅。
|
|
91
91
|
# 为什么需要:默认参数下**同一页面会话内**截图 byte 完全一致,但**跨浏览器会话**
|
|
92
92
|
# (崩溃重开 / 多 worker / 续跑重渲)不完全一致——GPU 抗锯齿光栅化会有极小的非确定性。
|
|
93
93
|
# 实测(assets/web 模板,f=179/180,8 个独立会话):
|
|
@@ -132,10 +132,10 @@ _K = _load_filmkit()
|
|
|
132
132
|
FFMPEG = _K.FFMPEG
|
|
133
133
|
|
|
134
134
|
|
|
135
|
-
# ----------------------------------------------------------------
|
|
135
|
+
# ---------------------------------------------------------------- 风格注入(契约)
|
|
136
136
|
# style 的两种合法写法:
|
|
137
137
|
# ① stylekit 的 style plan(dict,至少含 "theme" 字典;render.py --stage style 落的 work/style.json 就是它)
|
|
138
|
-
# ②
|
|
138
|
+
# ② 裸主题字典(扁平字段,颜色是 (r,g,b) 元组;设计的 theme 就是这种形状)
|
|
139
139
|
# 归一化后是**纯数据** payload,经 add_init_script 写成 window.__STYLE__。
|
|
140
140
|
# 这里不做任何"猜":缺键 / 颜色非法 / 字体族未知一律 raise,不做静默回落(回落等于换个画风还不告诉人)。
|
|
141
141
|
STYLE_VERSION = 1
|
|
@@ -149,7 +149,7 @@ FONT_STACKS = {
|
|
|
149
149
|
"round": 'DengXian, "Microsoft YaHei", sans-serif',
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
# 主题里必须齐全的色板键(
|
|
152
|
+
# 主题里必须齐全的色板键(designkit 构造的主题都有;少一个就说明传错东西了)
|
|
153
153
|
THEME_COLOR_KEYS = ("bg", "bg2", "surface", "ink", "muted", "dim",
|
|
154
154
|
"accent", "accent2", "good", "bad", "rule")
|
|
155
155
|
|
|
@@ -243,7 +243,7 @@ def _style_theme_dict(style):
|
|
|
243
243
|
"legacy": None, "layout_default": None, "chapter_layout": None}
|
|
244
244
|
raise ValueError(
|
|
245
245
|
"style 既不是 style plan(没有 \"theme\" 字典)也不是裸主题 dict(没有 bg / ink)。"
|
|
246
|
-
"
|
|
246
|
+
"可用写法:已解析的计划(work/style.json)或裸主题 dict。收到键:%s"
|
|
247
247
|
% (sorted(style)[:12],))
|
|
248
248
|
|
|
249
249
|
|
|
@@ -467,10 +467,10 @@ def _format_contract(contract: dict) -> str:
|
|
|
467
467
|
async def page_number(page, key: str, *, default=None) -> float:
|
|
468
468
|
"""读页面上的数值契约(window.DURATION / window.FPS),缺了或不是正数就给可操作报错。
|
|
469
469
|
|
|
470
|
-
|
|
470
|
+
直接 float(await page.evaluate("window.DURATION")) 不够:页面忘了导出 DURATION
|
|
471
471
|
时得到的是 None,报错只剩一句 "float() argument must be ... 'NoneType'",看不出
|
|
472
472
|
是页面契约缺字段(实测 page_meta / export_cues 都会这样)。
|
|
473
|
-
default 传 None 表示必填(DURATION);传数字表示缺省值(FPS
|
|
473
|
+
default 传 None 表示必填(DURATION);传数字表示缺省值(FPS 缺省口径是 30)。
|
|
474
474
|
"""
|
|
475
475
|
raw = await page.evaluate("k => window[k]", key)
|
|
476
476
|
if raw is None:
|
|
@@ -496,15 +496,15 @@ async def open_page(pw, url, width=W, height=H, log=False, deterministic=None,
|
|
|
496
496
|
浏览器优先 Microsoft Edge(channel="msedge"),启动失败回落 Playwright 自带的 Chromium;
|
|
497
497
|
视口按 width x height(devicePixelRatio=1),截图与画布 1:1 像素。
|
|
498
498
|
|
|
499
|
-
|
|
500
|
-
|
|
499
|
+
两道**可操作**的契约检查(否则缺 window.ready / renderFrame 时只会抛 Playwright 的
|
|
500
|
+
裸超时异常,看不出是页面脚本挂了还是没调 FK.boot()):
|
|
501
501
|
1. 等到 window.ready === true 超时 → 报错里带上页面异常/控制台错误和契约快照;
|
|
502
|
-
2. window.renderFrame 不是函数 →
|
|
502
|
+
2. window.renderFrame 不是函数 → 直接报错(不要靠逐帧重试 5 次才发现)。
|
|
503
503
|
确定性:deterministic=True(或环境变量 WEBENGINE_DETERMINISTIC=1)走软件光栅,
|
|
504
504
|
跨浏览器会话 byte 级可复现;缺省走 GPU,快约 22%。
|
|
505
505
|
|
|
506
|
-
|
|
507
|
-
style=None 时**不调用 add_init_script**,页面里不存在 __STYLE__
|
|
506
|
+
风格注入:style 非 None 时在 page.goto **之前** add_init_script 注入 window.__STYLE__;
|
|
507
|
+
style=None 时**不调用 add_init_script**,页面里不存在 __STYLE__(默认外观逐字节不变)。
|
|
508
508
|
width/height 一律先过 _validate_canvas(16:9 横屏偶数,否则 raise);fps 只用于写进
|
|
509
509
|
payload(None = 不让 payload 约束帧率,页面 window.FPS 才是真值)。
|
|
510
510
|
"""
|
|
@@ -603,7 +603,7 @@ async def open_page(pw, url, width=W, height=H, log=False, deterministic=None,
|
|
|
603
603
|
if not got.get("hasStyle") or not got.get("applied"):
|
|
604
604
|
raise RuntimeError(
|
|
605
605
|
"页面 %s 的 engine.js 不认 window.__STYLE__(FK.style 为空;注入后画风不会变)。"
|
|
606
|
-
"
|
|
606
|
+
"风格注入要新版 engine.js 才认;把新版 assets/web/ 一起拷过去,"
|
|
607
607
|
"或确认页面脚本顺序是 engine.js → film.js → qa_text.js。" % url)
|
|
608
608
|
if got.get("w") != want_w or got.get("h") != want_h:
|
|
609
609
|
raise RuntimeError(
|
|
@@ -617,7 +617,7 @@ async def open_page(pw, url, width=W, height=H, log=False, deterministic=None,
|
|
|
617
617
|
"帧率必须来自同一份 timeline,不能靠 style 猜。"
|
|
618
618
|
% (page_fps, payload["canvas"]["fps"]))
|
|
619
619
|
# 截图 = 视口(device_scale_factor=1),所以页面画布的可视区域必须正好铺满视口。
|
|
620
|
-
#
|
|
620
|
+
# 不查这一条的话,style=None + --width 1280 --height 720 会"成功"输出 1280x720:
|
|
621
621
|
# 页面画布仍是 1920x1080(没有 __STYLE__ 就没人改它),截出来的是原画左上角一块
|
|
622
622
|
# ——实测与 1920 帧左上角的 1280x720 裁切逐像素完全相同(422 KB PNG,0 像素差异),
|
|
623
623
|
# 标题 / HUD / 时间码全被裁掉,而 CLI 照旧 exit 0。画布比视口小则是四边留黑边,
|
|
@@ -648,7 +648,7 @@ async def open_page(pw, url, width=W, height=H, log=False, deterministic=None,
|
|
|
648
648
|
% (left, top, cw, ch, int(width), int(height)))
|
|
649
649
|
except BaseException:
|
|
650
650
|
# 用 BaseException 而不是 Exception:worker 被取消(task.cancel)抛的是
|
|
651
|
-
# CancelledError,它不继承 Exception ——
|
|
651
|
+
# CancelledError,它不继承 Exception —— 用 Exception 会把刚启动的浏览器丢下不管。
|
|
652
652
|
try:
|
|
653
653
|
await browser.close()
|
|
654
654
|
except Exception:
|
|
@@ -691,7 +691,7 @@ async def _page_meta_async(html_path, deterministic=None, style=None, width=W, h
|
|
|
691
691
|
def page_meta(html_path, *, style=None, width=W, height=H, fps=None):
|
|
692
692
|
"""打开一次页面,读回 (DURATION, FPS)。
|
|
693
693
|
|
|
694
|
-
|
|
694
|
+
style 非 None 时按注入后的页面读(注入可能改画布尺寸,帧率仍以页面为准,
|
|
695
695
|
除非调用方显式传 fps 并在 payload 里断言)。
|
|
696
696
|
"""
|
|
697
697
|
_setup_stdout()
|
|
@@ -703,7 +703,7 @@ def snap(html_path, times, out_dir, tag="", deterministic=None, *, style=None,
|
|
|
703
703
|
width=W, height=H, fps=None) -> list:
|
|
704
704
|
"""截取指定时刻的静帧(PNG);多帧时额外拼一张联系表。返回帧 PNG 路径列表。
|
|
705
705
|
|
|
706
|
-
|
|
706
|
+
style/width/height/fps 透传给 open_page(缺省 = 1920x1080、不注入)。
|
|
707
707
|
"""
|
|
708
708
|
_setup_stdout()
|
|
709
709
|
tlist = [float(t) for t in times]
|
|
@@ -806,7 +806,7 @@ def _norm_events(raw) -> list:
|
|
|
806
806
|
def export_cues(html_path, deterministic=None, *, style=None, width=W, height=H, fps=None) -> dict:
|
|
807
807
|
"""导出页面登记的 cue / 视觉事件,返回可直接写 work/cues.json 的字典(契约 §4.2)。
|
|
808
808
|
|
|
809
|
-
|
|
809
|
+
width/height 写进返回头(打完 style 注入后画布可能不是 1080p),其余照旧。
|
|
810
810
|
"""
|
|
811
811
|
_setup_stdout()
|
|
812
812
|
return asyncio.run(_export_cues_async(html_path, deterministic,
|
|
@@ -823,7 +823,7 @@ async def _export_cues_async(html_path, deterministic=None, *, style=None,
|
|
|
823
823
|
try:
|
|
824
824
|
duration = await page_number(page, "DURATION")
|
|
825
825
|
fps = await page_number(page, "FPS", default=FPS)
|
|
826
|
-
# 页面没导出 getCues/getEvents
|
|
826
|
+
# 页面没导出 getCues/getEvents 时,`? : []` 兜底会静默返回空数组:cues.json 里
|
|
827
827
|
# 0 条提示点,配乐/音效阶段照样跑完,只是整片悄悄变哑、音画同步也没有可配的事件
|
|
828
828
|
# ——「静默丢一整个约定」比报错难查得多。engine.js 本身就导出这两个函数(没有提示
|
|
829
829
|
# 点时返回空数组也算导出),所以 fail-closed 不会误伤正常页面。
|
|
@@ -882,7 +882,7 @@ def _chunk_dir(out_mp4, from_s: float, to_s: float, token: str = "") -> Path:
|
|
|
882
882
|
"""块目录:<stem>_<from>-<to>[_<token>]。
|
|
883
883
|
|
|
884
884
|
token 只在"画布尺寸不是 1920x1080 或注入了 style"时非空——不同画风/尺寸的块不能混用,
|
|
885
|
-
否则断点续跑会把旧画风的块拼进新片(花片)。token
|
|
885
|
+
否则断点续跑会把旧画风的块拼进新片(花片)。token 为空时目录名保持不变,续跑才能命中。
|
|
886
886
|
"""
|
|
887
887
|
out = Path(out_mp4)
|
|
888
888
|
name = "%s_%g-%g" % (out.stem, float(from_s), float(to_s))
|
|
@@ -892,7 +892,7 @@ def _chunk_dir(out_mp4, from_s: float, to_s: float, token: str = "") -> Path:
|
|
|
892
892
|
|
|
893
893
|
|
|
894
894
|
def _style_token(payload, width: int, height: int) -> str:
|
|
895
|
-
"""分块目录的风格后缀;默认尺寸 + 无 style
|
|
895
|
+
"""分块目录的风格后缀;默认尺寸 + 无 style 时返回空串(保持既有目录名,续跑才能命中)。"""
|
|
896
896
|
if payload is None and (int(width), int(height)) == (W, H):
|
|
897
897
|
return ""
|
|
898
898
|
parts = ["w%dh%d" % (int(width), int(height))]
|
|
@@ -941,8 +941,8 @@ def _probe_duration(path):
|
|
|
941
941
|
def _check_chunk_durations(chunks, fps: float) -> None:
|
|
942
942
|
"""块时长校验:块损坏 / 旧参数残留都会在拼接前被拦下。
|
|
943
943
|
|
|
944
|
-
|
|
945
|
-
|
|
944
|
+
探测不出时长的块(空文件 / 非视频 / ffprobe 失败)不许 continue 静默放过——那要等到
|
|
945
|
+
concat 才发现;一并算异常:"读不出时长"本身就说明块是坏的。
|
|
946
946
|
"""
|
|
947
947
|
bad = []
|
|
948
948
|
for f0, f1, path in chunks:
|
|
@@ -1098,7 +1098,7 @@ def render_full(html_path, out_mp4, duration, *, workers=2, chunk_s=30.0,
|
|
|
1098
1098
|
duration 传 None 时读页面 window.DURATION;传数字时与页面对不上会直接 raise。
|
|
1099
1099
|
deterministic=True(或 WEBENGINE_DETERMINISTIC=1)→ 软件光栅,跨会话 byte 可复现。
|
|
1100
1100
|
|
|
1101
|
-
|
|
1101
|
+
style 非 None 时整片跟随风格包(每个 worker 都在页面加载前注入同一份
|
|
1102
1102
|
__STYLE__),块目录名会带尺寸 + style 指纹后缀,**换风格不会复用旧块**;
|
|
1103
1103
|
width/height 只收 16:9 横屏偶数(_validate_canvas),fps 非 None 时要求页面帧率一致。
|
|
1104
1104
|
"""
|
|
@@ -1121,7 +1121,7 @@ async def _render_full_async(html_path, out_mp4, duration, *, workers=2, chunk_s
|
|
|
1121
1121
|
width, height = _validate_canvas(width, height)
|
|
1122
1122
|
payload = normalize_style(style, width=width, height=height, fps=fps) if style is not None else None
|
|
1123
1123
|
|
|
1124
|
-
# chunk_s <= 0
|
|
1124
|
+
# chunk_s <= 0 会被后面的 max(1, ...) 静默夹成「1 帧/块」:360 帧的 12 s 片会拆出
|
|
1125
1125
|
# 360 个块,每块单独起一个 ffmpeg 进程再拼接,慢到没法用,而且日志里只看得出"块很多",
|
|
1126
1126
|
# 看不出是自己传错了参数。放在开浏览器之前校验,参数错了不用白等一次页面加载。
|
|
1127
1127
|
chunk_s = float(chunk_s)
|
|
@@ -1223,10 +1223,10 @@ QA_BRIEF_S = 0.6
|
|
|
1223
1223
|
|
|
1224
1224
|
|
|
1225
1225
|
def _check_qa_window(t0, t1, step):
|
|
1226
|
-
"""文字质检扫描参数校验;非法 raise ValueError
|
|
1226
|
+
"""文字质检扫描参数校验;非法 raise ValueError(否则非法参数会被静默吞成空结果 + exit 0)。
|
|
1227
1227
|
|
|
1228
|
-
|
|
1229
|
-
|
|
1228
|
+
qa_text.js 用 `if (end < start) return empty` 与 `dt = max(step, 1/fps)` 兜底,于是
|
|
1229
|
+
--from 2 --to 1 会打印"重叠 0、出画 0…"并 exit 0;--step 1 因为采不到 0.5 s 的闪
|
|
1230
1230
|
字而永远报 brief=0。空扫描 / 扫不到的扫描都不能算"没有缺陷"。
|
|
1231
1231
|
"""
|
|
1232
1232
|
try:
|
|
@@ -1260,7 +1260,7 @@ _QA_LIST_KEYS = ("overlaps", "offscreen", "brief", "never")
|
|
|
1260
1260
|
def _check_qa_result(result):
|
|
1261
1261
|
"""校验页面 qaText 的返回值形状;形状不对 raise RuntimeError(不能让缺字段=全 0=PASS)。
|
|
1262
1262
|
|
|
1263
|
-
|
|
1263
|
+
只判 `isinstance(result, dict)` 不够:页面 `qaText=()=>({})` 时 CLI 会把四类缺陷
|
|
1264
1264
|
都打印成 0 并 exit 0——**假通过**。契约要求四个数组 + frames(实际采样点数),
|
|
1265
1265
|
这里连 frames<=0 也拦(扫描 0 帧等于没查)。
|
|
1266
1266
|
"""
|
|
@@ -1287,11 +1287,11 @@ def qa_text(html_path, t0=0.0, t1=None, step=0.2, deterministic=None, *,
|
|
|
1287
1287
|
style=None, width=W, height=H, fps=None) -> dict:
|
|
1288
1288
|
"""浏览器端文字质检:重叠 / 出画 / 一闪而过 / 从没露面(渲染期间会临时改页面状态)。
|
|
1289
1289
|
|
|
1290
|
-
|
|
1290
|
+
跟着 style 一起检查——换了浅色主题/非 1080p 画布后,出画与重叠的判据必须用
|
|
1291
1291
|
同一份画布尺寸(qa_text.js 读 FK.W / FK.H),否则"检查通过"只是没在检查新尺寸。
|
|
1292
1292
|
|
|
1293
|
-
|
|
1294
|
-
|
|
1293
|
+
参数与返回值 fail-closed(空扫描 / 扫不到的扫描 / 页面返回 {} 如果都 exit 0,
|
|
1294
|
+
门禁看着是绿的,其实什么都没查)。见 _check_qa_window / _check_qa_result。
|
|
1295
1295
|
"""
|
|
1296
1296
|
_setup_stdout()
|
|
1297
1297
|
t0, t1, step = _check_qa_window(t0, t1, step)
|
|
@@ -1319,14 +1319,14 @@ async def _qa_text_async(html_path, t0, t1, step, deterministic=None, *,
|
|
|
1319
1319
|
return _check_qa_result(result)
|
|
1320
1320
|
|
|
1321
1321
|
|
|
1322
|
-
# ---------------------------------------------------------------- 指定帧号截 PNG
|
|
1322
|
+
# ---------------------------------------------------------------- 指定帧号截 PNG
|
|
1323
1323
|
def render_frames(html_path, frames, out_dir=None, *, tag="", style=None, width=W, height=H,
|
|
1324
1324
|
fps=None, deterministic=None) -> list:
|
|
1325
1325
|
"""把**指定帧号**渲成 PNG,返回 [{"f","path","sha256","bytes","fps"}](不编码视频)。
|
|
1326
1326
|
|
|
1327
|
-
|
|
1327
|
+
这是给 render.py / 自检 / 人工验收用的最小确定性入口:
|
|
1328
1328
|
帧号 → 时间 = f / FPS(与 render_full 同一口径),不存在"按播放时间截屏"。
|
|
1329
|
-
style=None 时不注入 __STYLE__
|
|
1329
|
+
style=None 时不注入 __STYLE__(像素与默认外观一致);传 style 时整页跟随风格包。
|
|
1330
1330
|
out_dir 缺省 ./frames;文件名 <tag>f<帧号:05d>.png。
|
|
1331
1331
|
"""
|
|
1332
1332
|
_setup_stdout()
|
|
@@ -1466,7 +1466,7 @@ async def _selftest_probe() -> dict:
|
|
|
1466
1466
|
return out
|
|
1467
1467
|
|
|
1468
1468
|
|
|
1469
|
-
# ----------------------------------------------------------------
|
|
1469
|
+
# ---------------------------------------------------------------- 自检辅助
|
|
1470
1470
|
_NONDET_JS = re.compile(r"(Math\.random|Date\.now|performance\.now|new\s+Date\s*\()")
|
|
1471
1471
|
|
|
1472
1472
|
|
|
@@ -1513,7 +1513,7 @@ async def _open_page_expect_error(html_path, ready_timeout=3000) -> str:
|
|
|
1513
1513
|
raise RuntimeError("坏页面居然没有报错:%s" % html_path)
|
|
1514
1514
|
|
|
1515
1515
|
|
|
1516
|
-
# ----------------------------------------------------------------
|
|
1516
|
+
# ---------------------------------------------------------------- 自检:风格注入
|
|
1517
1517
|
# 自检用的主题故意选"浅色 + 衬线 + 右对齐 + 四角线框":与模板默认的深蓝橙 + 居中 + 无框
|
|
1518
1518
|
# 在每一次可观测维度上都不同,任何一处没跟着 __STYLE__ 走都会暴露成"两张图一样"。
|
|
1519
1519
|
_SELFTEST_STYLE = {
|
|
@@ -1605,7 +1605,7 @@ def selftest() -> int:
|
|
|
1605
1605
|
print(("[PASS] " if ok else "[FAIL] ") + title + ((" —— " + detail) if detail else ""), flush=True)
|
|
1606
1606
|
|
|
1607
1607
|
print("=" * 78)
|
|
1608
|
-
print("webengine.py --selftest
|
|
1608
|
+
print("webengine.py --selftest:无头浏览器逐帧渲染 / 确定性 / cue / 文字质检 / 风格注入")
|
|
1609
1609
|
print("=" * 78)
|
|
1610
1610
|
|
|
1611
1611
|
names = ("index.html", "engine.js", "film.js", "style.css", "qa_text.js")
|
|
@@ -1792,7 +1792,7 @@ def selftest() -> int:
|
|
|
1792
1792
|
check("不同帧渲染结果不同", bool(probe.get("diff_frames")),
|
|
1793
1793
|
"f=45 与 f=46 的 PNG 不同" if probe.get("diff_frames") else (perr or "相邻帧截图相同(时间码没变?)"))
|
|
1794
1794
|
|
|
1795
|
-
# ----
|
|
1795
|
+
# ---- 画布尺寸 / 风格注入 ----
|
|
1796
1796
|
good_sizes, bad_sizes, portrait_msg = [], [], ""
|
|
1797
1797
|
for w_, h_ in ((1920, 1080), (1280, 720), (3840, 2160)):
|
|
1798
1798
|
try:
|
|
@@ -1820,7 +1820,7 @@ def selftest() -> int:
|
|
|
1820
1820
|
";".join(bad_sizes) or ("合法:%s;竖屏报错点名竖屏" % "、".join(good_sizes)))
|
|
1821
1821
|
|
|
1822
1822
|
# ---- 深审:确定性开关三态 / 文字质检参数与返回值 fail-closed ----
|
|
1823
|
-
# 这三条都是纯函数校验(不开浏览器),专门守"门禁看着干净、其实没在查"
|
|
1823
|
+
# 这三条都是纯函数校验(不开浏览器),专门守"门禁看着干净、其实没在查"的三条路径:
|
|
1824
1824
|
# · CLI --deterministic 用 store_true 时默认 False 会压掉 WEBENGINE_DETERMINISTIC;
|
|
1825
1825
|
# · textqa 空扫描(t1<t0)/ 步长 >= brief 阈值时返回全 0 并 exit 0;
|
|
1826
1826
|
# · 页面 qaText 返回 {} / frames=0 时同样被当成"零缺陷"。
|
|
@@ -2060,7 +2060,7 @@ def selftest() -> int:
|
|
|
2060
2060
|
% (len(bad_qa.get("overlaps") or []), len(bad_qa.get("offscreen") or []),
|
|
2061
2061
|
len(bad_qa.get("brief") or []), len(bad_qa.get("never") or []))))
|
|
2062
2062
|
|
|
2063
|
-
# 坏页面 3b:CLI textqa 遇到这四类必须 exit 1
|
|
2063
|
+
# 坏页面 3b:CLI textqa 遇到这四类必须 exit 1(只把重叠/出画算失败就漏了后两类)
|
|
2064
2064
|
cli_bad = None
|
|
2065
2065
|
try:
|
|
2066
2066
|
cp = subprocess.run(
|
|
@@ -2162,13 +2162,11 @@ def _parse_frames(text: str) -> list:
|
|
|
2162
2162
|
def _resolve_style_spec(text):
|
|
2163
2163
|
"""解析 --style 的**两种**写法:内联 JSON / JSON 文件路径。
|
|
2164
2164
|
|
|
2165
|
-
返回 normalize_style 能吃的 dict;空字符串返回 None(=
|
|
2165
|
+
返回 normalize_style 能吃的 dict;空字符串返回 None(= 不注入)。
|
|
2166
2166
|
解析失败一律 SystemExit(CLI 场景,带可操作提示),不做静默回落。
|
|
2167
2167
|
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
而那两个函数现在只会抛 KeyError —— 与其让它抛一个"没有风格 X"的错,
|
|
2171
|
-
不如直接把写法和替代方案说清楚。
|
|
2168
|
+
不接受"风格 id"这种写法:没有内置外观表,`--style magazine` 没有对象可指。
|
|
2169
|
+
与其抛一个"没有风格 X"的错,不如直接把两种合法写法和替代方案说清楚。
|
|
2172
2170
|
"""
|
|
2173
2171
|
s = str(text or "").strip()
|
|
2174
2172
|
if not s:
|
|
@@ -2186,7 +2184,7 @@ def _resolve_style_spec(text):
|
|
|
2186
2184
|
raise SystemExit("[错误] --style 的 JSON 文件 %s 解析失败:%s" % (p, exc))
|
|
2187
2185
|
raise SystemExit(
|
|
2188
2186
|
"[错误] --style %r 不是 JSON、也不是存在的文件——而**按内置风格 id 取外观这条路已经没有了**。\n"
|
|
2189
|
-
"
|
|
2187
|
+
" 本技能不提供内置风格/主题表。现在注入外观只有两种写法:\n"
|
|
2190
2188
|
" ① --style '{\"theme\": {...}, \"canvas\": {...}}' # 内联 JSON\n"
|
|
2191
2189
|
" ② --style work/style.json # 由 render.py --stage style 落盘\n"
|
|
2192
2190
|
" 想生成一套新外观:python propose.py --brief \"题材\" --script film.json\n"
|
|
@@ -2194,10 +2192,10 @@ def _resolve_style_spec(text):
|
|
|
2194
2192
|
|
|
2195
2193
|
|
|
2196
2194
|
def _add_style_args(p, *, fps=True):
|
|
2197
|
-
"""给子命令加风格参数(缺省 =
|
|
2195
|
+
"""给子命令加风格参数(缺省 = 不注入、1920x1080)。"""
|
|
2198
2196
|
p.add_argument("--style", default="",
|
|
2199
2197
|
help="外观载荷:work/style.json / 内联 JSON;缺省不注入"
|
|
2200
|
-
"
|
|
2198
|
+
"(不接受风格 id:内置风格表已删除)")
|
|
2201
2199
|
p.add_argument("--width", type=int, default=W, help="画布宽(只收 16:9 横屏偶数,默认 %d)" % W)
|
|
2202
2200
|
p.add_argument("--height", type=int, default=H, help="画布高(默认 %d)" % H)
|
|
2203
2201
|
if fps:
|
|
@@ -2208,7 +2206,7 @@ def _add_style_args(p, *, fps=True):
|
|
|
2208
2206
|
def _build_parser() -> argparse.ArgumentParser:
|
|
2209
2207
|
ap = argparse.ArgumentParser(
|
|
2210
2208
|
prog="webengine.py",
|
|
2211
|
-
description="
|
|
2209
|
+
description="浏览器逐帧渲染引擎:Canvas 页面 → CDP 截图 → ffmpeg → mp4(支持风格包注入)",
|
|
2212
2210
|
)
|
|
2213
2211
|
ap.add_argument("--selftest", action="store_true", help="跑自检(不联网,≤60 秒)")
|
|
2214
2212
|
sub = ap.add_subparsers(dest="cmd")
|
|
@@ -2288,7 +2286,7 @@ def main(argv=None) -> int:
|
|
|
2288
2286
|
return 2
|
|
2289
2287
|
|
|
2290
2288
|
# --style 统一在分支之前解析:内联 JSON / JSON 文件 / 风格 id 三种写法,解析失败算用法
|
|
2291
|
-
# 错误(exit 2
|
|
2289
|
+
# 错误(exit 2)。每个分支各自解析时,坏 id 会抛 SystemExit 变成 exit 1,脚本里分不清
|
|
2292
2290
|
# "参数写错"和"渲染失败"。
|
|
2293
2291
|
try:
|
|
2294
2292
|
style_spec = _resolve_style_spec(getattr(args, "style", ""))
|
|
@@ -2305,7 +2303,7 @@ def main(argv=None) -> int:
|
|
|
2305
2303
|
% (args.times,), flush=True)
|
|
2306
2304
|
return 2
|
|
2307
2305
|
if args.every is not None and float(args.every) <= 0:
|
|
2308
|
-
#
|
|
2306
|
+
# --every 0 会被当成"没写"、--every -1 则是死循环:先 page_meta 开一次浏览器,
|
|
2309
2307
|
# 之后 while k <= end 永远成立,times 一路追加到内存爆掉(实测 40 s 都出不来)。
|
|
2310
2308
|
print("[错误] --every 必须是正数秒(当前 %r);不想等间隔采样就别传 --every" % (args.every,),
|
|
2311
2309
|
flush=True)
|
|
@@ -2361,7 +2359,7 @@ def main(argv=None) -> int:
|
|
|
2361
2359
|
return 2
|
|
2362
2360
|
except RuntimeError as exc:
|
|
2363
2361
|
# 页面契约 / 浏览器起不来这类错误(缺 getCues、ready 超时、缺 playwright…):
|
|
2364
|
-
#
|
|
2362
|
+
# 直接抛 traceback 会变成 exit 1,且报错淹没在栈里。这是"要改页面/环境"的用法
|
|
2365
2363
|
# 错误,和 --style 写坏一样给 exit 2 + 一行可读原因。
|
|
2366
2364
|
print("[错误] %s" % exc, flush=True)
|
|
2367
2365
|
return 2
|
|
@@ -2398,7 +2396,7 @@ def main(argv=None) -> int:
|
|
|
2398
2396
|
return 2
|
|
2399
2397
|
except RuntimeError as exc:
|
|
2400
2398
|
# 同 cues:页面 qaText 返回值形状不对 / 契约缺失 / 浏览器起不来都属于"要改页面
|
|
2401
|
-
# 或环境",给 exit 2 +
|
|
2399
|
+
# 或环境",给 exit 2 + 可读原因;别让一个 traceback 决定门禁红绿。
|
|
2402
2400
|
print("[错误] %s" % exc, flush=True)
|
|
2403
2401
|
return 2
|
|
2404
2402
|
print("[textqa] 重叠 %d、出画 %d、一闪而过 %d、从没露面 %d"
|
|
@@ -2413,7 +2411,7 @@ def main(argv=None) -> int:
|
|
|
2413
2411
|
if args.out:
|
|
2414
2412
|
print("[textqa] 详情 → %s" % _json_dump(result, args.out), flush=True)
|
|
2415
2413
|
# 四类缺陷都算没通过:重叠/出画是布局错,一闪而过/从没露面同样会让成片看起来"漏字"。
|
|
2416
|
-
#
|
|
2414
|
+
# 只看前两类的话,后两类会被打印出来却 exit 0 —— 门禁形同建议。
|
|
2417
2415
|
bad = (bool(result.get("overlaps")) or bool(result.get("offscreen"))
|
|
2418
2416
|
or bool(result.get("brief")) or bool(result.get("never")))
|
|
2419
2417
|
return 1 if bad else 0
|
|
@@ -9,15 +9,15 @@
|
|
|
9
9
|
|
|
10
10
|
四组对照(阈值来自 skills/video-studio/references/audio-variation.md §5):
|
|
11
11
|
|
|
12
|
-
A 跨片配乐(
|
|
12
|
+
A 跨片配乐(musicgen 参数化编排):
|
|
13
13
|
同参数/同 bpm/同调、只换内容指纹 -> 相关性(目标 <= 0.85);
|
|
14
14
|
换编排参数(mood + 音色) -> 相关性(目标 <= 0.52)
|
|
15
|
-
B
|
|
15
|
+
B 跨片音效:同 cue 序列,legacy(不传 style/content_fp)必须逐样本相同;
|
|
16
16
|
不同 content_fp 必须不同(否则"跨片音效都一样"没被修掉)
|
|
17
|
-
C
|
|
18
|
-
D
|
|
17
|
+
C 片内分离:duck 默认深度(目标 >= 3.5 dB)与配乐/音效的频谱质心对照
|
|
18
|
+
D 旁白优先:人声段 旁白/背景 响度差(目标 >= 12 dB)+ 侧链让位 >= 4 dB
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
两种用法:不带 --strict 是诊断(打印对照数值),--strict 是验收(逐条对阈值判定,失败 exit 1)。
|
|
21
21
|
"""
|
|
22
22
|
from __future__ import annotations
|
|
23
23
|
|
|
@@ -35,9 +35,9 @@ sys.path.insert(0, str(SCRIPTS))
|
|
|
35
35
|
|
|
36
36
|
# 契约 §5 的阈值(改契约时两边一起改)
|
|
37
37
|
THRESH_A_SAME_STYLE_MAX = 0.85 # 同参数、只换内容指纹的相关性上限
|
|
38
|
-
THRESH_A_CROSS_STYLE_MAX = 0.52 #
|
|
39
|
-
#
|
|
40
|
-
#
|
|
38
|
+
THRESH_A_CROSS_STYLE_MAX = 0.52 # 换编排参数的相关性上限(改造前实测 0.534,抬到 0.53 门禁就没意义了)
|
|
39
|
+
# 配乐由 musicgen 参数化编排,这两条阈值是「跨片不能太像」的验收线:
|
|
40
|
+
# 同参数换指纹要 <=0.85,换编排参数要 <=0.52。
|
|
41
41
|
# 生成链是确定性的,换平台/换 numpy 若把实测值推过线,先查 musicgen 的参数有没有
|
|
42
42
|
# 变成死旋钮,别第一反应去抬阈值。
|
|
43
43
|
THRESH_C_DUCK_MIN = 3.5 # duck 默认深度下限(dB)
|
|
@@ -104,7 +104,7 @@ def _utf8_stdio():
|
|
|
104
104
|
|
|
105
105
|
|
|
106
106
|
def _music(dur, profile, fp):
|
|
107
|
-
"""
|
|
107
|
+
"""用参数化编排器现场生成一条配乐。
|
|
108
108
|
|
|
109
109
|
profile 是「一套编排参数」,对应旧版的一个 style id —— 换成 mood + 音色 + 和弦进行:
|
|
110
110
|
* "steady" -> 稳重:pad=saw / bass=sine / arp=pluck / lead=bell
|
|
@@ -192,7 +192,7 @@ def _duck_default():
|
|
|
192
192
|
|
|
193
193
|
def main(argv=None):
|
|
194
194
|
_utf8_stdio()
|
|
195
|
-
ap = argparse.ArgumentParser(description="
|
|
195
|
+
ap = argparse.ArgumentParser(description="配乐/音效相似度诊断与验收")
|
|
196
196
|
ap.add_argument("--out", default="", help="把对照样本写成 wav 到这个目录")
|
|
197
197
|
ap.add_argument("--dur", type=float, default=20.0, help="配乐对照时长(秒)")
|
|
198
198
|
ap.add_argument("--strict", action="store_true", help="按契约 §5 阈值判定,失败 exit 1")
|
|
@@ -238,7 +238,7 @@ def main(argv=None):
|
|
|
238
238
|
legacy_same = bool(np.array_equal(b_legacy1, b_legacy2))
|
|
239
239
|
cross_diff = not bool(np.array_equal(b_new_a, b_new_b))
|
|
240
240
|
repeat_same = bool(np.array_equal(b_new_a, b_new_a2))
|
|
241
|
-
print(" legacy(不传 style/fp)两次相同 : %s (必须 True
|
|
241
|
+
print(" legacy(不传 style/fp)两次相同 : %s (必须 True,逐样本兼容)" % legacy_same)
|
|
242
242
|
print(" 不同内容指纹 -> 音效轨不同 : %s (必须 True)" % cross_diff)
|
|
243
243
|
print(" 同内容指纹两次相同 : %s (必须 True,可复现)" % repeat_same)
|
|
244
244
|
if args.strict:
|
|
@@ -264,7 +264,7 @@ def main(argv=None):
|
|
|
264
264
|
|
|
265
265
|
print()
|
|
266
266
|
print("=" * 72)
|
|
267
|
-
print("D.
|
|
267
|
+
print("D. 旁白优先:人声与背景的响度差")
|
|
268
268
|
print("=" * 72)
|
|
269
269
|
snr_db, base_db, _duck_with_guard = _voice_priority_snr()
|
|
270
270
|
# 侧链让位单独量一次(guard_snr_db=0):兜底的整体补压也是"背景变低",
|
package/tools/ci_check.py
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
跑法:python tools/ci_check.py [-v]
|
|
5
5
|
|
|
6
6
|
它检查的是**这个仓库作为"可分发的技能插件"的完整性**,而不是视频能不能出片
|
|
7
|
-
(出片要靠 selfcheck.py + 真跑一遍 render.py,见 SKILL.md §
|
|
7
|
+
(出片要靠 selfcheck.py + 真跑一遍 render.py,见 SKILL.md §13):
|
|
8
8
|
|
|
9
9
|
1 语法:scripts/ + assets/ + tools/ 下的 .py 全部能编译(SyntaxWarning 也算失败)
|
|
10
10
|
2 JS 语法:根目录与 assets/web/ 的 .js 通过 node --check
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
10 CI workflow:step 名的裸冒号(会让整份 workflow 静默不跑),
|
|
22
22
|
以及 run 里 python <路径>.py 指向的文件必须存在
|
|
23
23
|
11 体积:单个文件不超过 400 KB(技能要能塞进上下文与包里)
|
|
24
|
-
12 film.json 契约:schemas/film.schema.json + scripts/filmcheck.py
|
|
24
|
+
12 film.json 契约:schemas/film.schema.json + scripts/filmcheck.py 校验夹具(规则只留一份)
|
|
25
25
|
|
|
26
26
|
三档结论(与技能自己的门禁账本同口径):PASS = 这次真的查了且通过;FAIL = 查了没过;
|
|
27
27
|
SKIP = **这次没执行**(比如本地没装 node / npm),**不等于通过**。
|
|
@@ -49,22 +49,22 @@ REFS = SKILL / "references"
|
|
|
49
49
|
|
|
50
50
|
# 技能允许的三方依赖(与 README「环境前提」一一对应;加东西必须先改文档)
|
|
51
51
|
ALLOWED_THIRD_PARTY = {"numpy", "PIL", "cv2", "scipy", "edge_tts", "playwright",
|
|
52
|
-
#
|
|
52
|
+
# ASR 回读门禁(可选,缺了 qaasr 自动降级到 ffmpeg whisper / skipped)
|
|
53
53
|
"faster_whisper"}
|
|
54
54
|
# 同目录同级模块(拷走整个目录后仍能互相 import)
|
|
55
55
|
LOCAL_MODULES = {"filmkit", "sfxkit", "scorekit", "webengine", "qa_frames", "render", "selfcheck",
|
|
56
56
|
"stylekit", "voicekit", "scenes", "scenes_demo",
|
|
57
|
-
#
|
|
57
|
+
# 门禁账本 / 交付报告 / 签核、阶段新鲜度守卫、film.json 契约校验器
|
|
58
58
|
"gates", "stagewatch", "filmcheck",
|
|
59
|
-
#
|
|
59
|
+
# 渲染前资源闸门、全片 VMAF/SSIM 门禁、ASR 回读门禁
|
|
60
60
|
"preflight", "qavmaf", "qaasr",
|
|
61
|
-
#
|
|
61
|
+
# 场景语法(内容 -> 构图选型)。纯标准库,可被 scenes.py 反向 import
|
|
62
62
|
"grammar",
|
|
63
|
-
#
|
|
63
|
+
# 设计 DSL / 离线预览 / 提案器
|
|
64
64
|
"designkit", "previewkit", "propose",
|
|
65
|
-
#
|
|
65
|
+
# 没有 presetkit(随预设目录一起移除);这里不能再列它,
|
|
66
66
|
# 否则 ci_check 会去 import 一个不存在的模块
|
|
67
|
-
#
|
|
67
|
+
# 参数化配乐编排器(不提供曲风表,按参数现场编排)
|
|
68
68
|
"musicgen"}
|
|
69
69
|
MAX_FILE_BYTES = 400 * 1024
|
|
70
70
|
# 体积检查忽略的目录后缀:.team 是本地调研/自测工作区(gitignore),不随包发布;
|
|
@@ -176,7 +176,7 @@ def t_json():
|
|
|
176
176
|
assert not bad_badges, ("README 徽章版本 %s 与 package.json version %s 不一致"
|
|
177
177
|
% ("/".join(bad_badges), ver))
|
|
178
178
|
# CHANGELOG 的第一条标题也必须是当前版本:以前只查 README 徽章,于是"发了新版本但
|
|
179
|
-
# CHANGELOG 顶部还是上一版"
|
|
179
|
+
# CHANGELOG 顶部还是上一版"没人发现(真发生过)。
|
|
180
180
|
changelog = ROOT / "CHANGELOG.md"
|
|
181
181
|
assert changelog.is_file(), "缺 CHANGELOG.md"
|
|
182
182
|
m = re.search(r"^##\s+(\d+\.\d+\.\d+)", changelog.read_text(encoding="utf-8"), re.M)
|
|
@@ -260,8 +260,8 @@ def t_pack():
|
|
|
260
260
|
missing = [m for m in must if m not in files]
|
|
261
261
|
assert not missing, "包内缺文件:%s" % ", ".join(missing)
|
|
262
262
|
# 目录部分改成**派生式**:skills/ schemas/ locale/ tools/ docs/ 下磁盘上有的(除缓存/字节码)
|
|
263
|
-
#
|
|
264
|
-
#
|
|
263
|
+
# 必须全部进包。手写 must 列表对新增文件天然免疫:gates.py / stagewatch.py /
|
|
264
|
+
# preflight.py / qavmaf.py / qaasr.py / tools/golden_frames.py 都曾被漏在列表外——
|
|
265
265
|
# "必需文件必须在包里"这条检查对**新增文件天然免疫**,等于只保护了第一版写下的那些路径。
|
|
266
266
|
shipped_dirs = ("skills", "schemas", "locale", "tools", "docs")
|
|
267
267
|
on_disk = []
|
|
@@ -334,7 +334,7 @@ def t_doc_refs():
|
|
|
334
334
|
# 第三种:带占位符的路径(<SKILL_DIR>/... 与 <REPO>/...)。ci_check 之前只认 scripts/|assets/|references/
|
|
335
335
|
# 前缀,SKILL.md 里 <SKILL_DIR>/../../../tools/estimate.py 这种写错两级相对层级的命令一直没人查——
|
|
336
336
|
# 实测它指向仓库上一级,是条**跑不通的命令**。语义:<SKILL_DIR> = skills/video-studio/,
|
|
337
|
-
# <REPO> = 包根(仓库根)。注意**不能**把它们当成"当前工作目录"
|
|
337
|
+
# <REPO> = 包根(仓库根)。注意**不能**把它们当成"当前工作目录":把技能解到 myfilm/ 下面后,
|
|
338
338
|
# 那里的 skills 与 tools 都不存在,但文档里的占位符指的是包内位置,仍然应当成立。
|
|
339
339
|
ph_bad = []
|
|
340
340
|
ph_re = re.compile(r"(<SKILL_DIR>|<REPO>)/([A-Za-z0-9_.\-/{}*]+)")
|
|
@@ -447,7 +447,7 @@ def t_py39():
|
|
|
447
447
|
def t_md_fences():
|
|
448
448
|
"""每篇 .md 的代码围栏必须成对(按围栏状态机判,不是数出现次数)。
|
|
449
449
|
|
|
450
|
-
|
|
450
|
+
早期这里写的是 startswith("\\`\\`\\`")——那是一个无效转义,Python 把反斜杠原样保留,
|
|
451
451
|
于是永远匹配不到真实的 ``` 行,检查恒过("看着在查,其实查不出",正是本仓库最怕的形态)。
|
|
452
452
|
现在改成状态机:开围栏记下字符与长度,闭合围栏必须同字符、长度不小于开口、且不带信息串。
|
|
453
453
|
"""
|
|
@@ -538,9 +538,9 @@ def t_film_contract():
|
|
|
538
538
|
cwd=str(scripts), capture_output=True, text=True,
|
|
539
539
|
encoding="utf-8", errors="replace", timeout=120)
|
|
540
540
|
out = (p.stdout or "") + (p.stderr or "")
|
|
541
|
-
assert p.returncode == 0, ("
|
|
541
|
+
assert p.returncode == 0, ("夹具 film.json 没过契约校验(exit=%d):\n%s"
|
|
542
542
|
% (p.returncode, out.strip()[-600:]))
|
|
543
|
-
return "
|
|
543
|
+
return "夹具 film.json 通过契约校验;schema %d 字节" % len(schema.read_text(encoding="utf-8"))
|
|
544
544
|
|
|
545
545
|
def main(argv=None) -> int:
|
|
546
546
|
ap = argparse.ArgumentParser(description="仓库级体检(不需要 ffmpeg / 字体 / 网络)")
|