@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.
Files changed (51) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +84 -108
  3. package/THIRD_PARTY_NOTICES.md +2 -2
  4. package/package.json +1 -1
  5. package/schemas/design.schema.json +5 -5
  6. package/schemas/film.schema.json +31 -31
  7. package/skills/video-studio/SKILL.md +168 -268
  8. package/skills/video-studio/assets/film.json +2 -3
  9. package/skills/video-studio/assets/golden_frames.json +11 -11
  10. package/skills/video-studio/assets/scenes.py +92 -49
  11. package/skills/video-studio/references/{style-packs.md → appearance.md} +17 -15
  12. package/skills/video-studio/references/audio-variation.md +73 -96
  13. package/skills/video-studio/references/audio.md +40 -44
  14. package/skills/video-studio/references/change-impact.md +2 -2
  15. package/skills/video-studio/references/{v08-quality.md → delivery-gates.md} +9 -11
  16. package/skills/video-studio/references/frame-qa.md +47 -15
  17. package/skills/video-studio/references/pipeline.md +4 -4
  18. package/skills/video-studio/references/pitfalls.md +22 -3
  19. package/skills/video-studio/references/proposal.md +19 -36
  20. package/skills/video-studio/references/quality-gates.md +11 -11
  21. package/skills/video-studio/references/scene-authoring.md +2 -2
  22. package/skills/video-studio/references/scene-grammar.md +17 -24
  23. package/skills/video-studio/references/sfx-design.md +7 -5
  24. package/skills/video-studio/references/web-engine.md +7 -7
  25. package/skills/video-studio/references/word-timeline.md +7 -7
  26. package/skills/video-studio/requirements.txt +9 -13
  27. package/skills/video-studio/scripts/designkit.py +84 -89
  28. package/skills/video-studio/scripts/filmcheck.py +19 -19
  29. package/skills/video-studio/scripts/filmkit.py +2949 -2948
  30. package/skills/video-studio/scripts/gates.py +4 -4
  31. package/skills/video-studio/scripts/grammar.py +10 -10
  32. package/skills/video-studio/scripts/musicgen.py +3 -4
  33. package/skills/video-studio/scripts/preflight.py +5 -5
  34. package/skills/video-studio/scripts/previewkit.py +11 -12
  35. package/skills/video-studio/scripts/propose.py +11 -11
  36. package/skills/video-studio/scripts/qa_frames.py +34 -35
  37. package/skills/video-studio/scripts/qaasr.py +26 -7
  38. package/skills/video-studio/scripts/qavmaf.py +19 -19
  39. package/skills/video-studio/scripts/render.py +137 -141
  40. package/skills/video-studio/scripts/scorekit.py +21 -23
  41. package/skills/video-studio/scripts/selfcheck.py +208 -135
  42. package/skills/video-studio/scripts/sfxkit.py +27 -26
  43. package/skills/video-studio/scripts/stagewatch.py +4 -4
  44. package/skills/video-studio/scripts/stylekit.py +77 -282
  45. package/skills/video-studio/scripts/voicekit.py +43 -44
  46. package/skills/video-studio/scripts/webengine.py +62 -64
  47. package/tools/audio_similarity.py +12 -12
  48. package/tools/ci_check.py +16 -16
  49. package/tools/estimate.py +24 -25
  50. package/tools/golden_frames.py +17 -18
  51. package/tools/silhouette_qa.py +13 -14
@@ -1,27 +1,27 @@
1
1
  # -*- coding: utf-8 -*-
2
- """webengine.py — 浏览器逐帧渲染引擎(v0.8.0)。
2
+ """webengine.py — 浏览器逐帧渲染引擎。
3
3
 
4
4
  用无头浏览器(优先 Microsoft Edge,失败回落 Playwright Chromium)打开 Canvas 页面,
5
5
  把时间精确跳到「帧号 / FPS」后经 CDP 截 PNG,喂给 ffmpeg 的 image2pipe 编成视频。
6
6
  分块并行 + 断点续跑 + 显式缺口检测 + 崩溃重开;同一帧号渲染出的画面完全一致。
7
7
 
8
- v0.8.0:**跟随风格包**。公开入口都多了一组可选参数(style / width / height / fps):
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 优先读它,取不到就退回 v0.7 深蓝橙常量。
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__,渲染像素与 v0.7 逐字节一致(自检里有「无 style 页面无 __STYLE__」断言,
18
- 外部还有拿 git HEAD 老版本渲同一帧比 SHA-256 的证据)。
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
- 代码按本项目 v0.3.0 契约重写;拼接复用 filmkit.concat_segments(绝对路径,v0.2 修过的坑)。
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 magazine | --style work/style.json | --style '{"theme":{...}}'
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.8.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
- # 确定性模式(v0.4.0):关掉 GPU 光栅,2D canvas 全部走软件光栅。
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
- # ---------------------------------------------------------------- 风格注入(v0.8.0 契约)
135
+ # ---------------------------------------------------------------- 风格注入(契约)
136
136
  # style 的两种合法写法:
137
137
  # ① stylekit 的 style plan(dict,至少含 "theme" 字典;render.py --stage style 落的 work/style.json 就是它)
138
- # ② 裸主题字典(stylekit.THEMES[...] / stylekit.theme_of(id),扁平字段,颜色是 (r,g,b) 元组)
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
- # 主题里必须齐全的色板键(stylekit 的 8 套主题全都有;少一个就说明传错东西了)
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
- "可用写法:stylekit 的 plan(work/style.json)或 stylekit.THEMES[...]。收到键:%s"
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
- v0.5 以前直接 float(await page.evaluate("window.DURATION")):页面忘了导出 DURATION
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 历史口径是 30)。
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
- v0.4.0 起多了两道**可操作**的契约检查(以前缺 window.ready / renderFrame 时只会
500
- 抛 Playwright 的裸超时异常,看不出是页面脚本挂了还是没调 FK.boot()):
499
+ 两道**可操作**的契约检查(否则缺 window.ready / renderFrame 时只会抛 Playwright 的
500
+ 裸超时异常,看不出是页面脚本挂了还是没调 FK.boot()):
501
501
  1. 等到 window.ready === true 超时 → 报错里带上页面异常/控制台错误和契约快照;
502
- 2. window.renderFrame 不是函数 → 直接报错(不再逐帧重试 5 次才发现)。
502
+ 2. window.renderFrame 不是函数 → 直接报错(不要靠逐帧重试 5 次才发现)。
503
503
  确定性:deterministic=True(或环境变量 WEBENGINE_DETERMINISTIC=1)走软件光栅,
504
504
  跨浏览器会话 byte 级可复现;缺省走 GPU,快约 22%。
505
505
 
506
- v0.8.0 风格:style 非 None 时在 page.goto **之前** add_init_script 注入 window.__STYLE__;
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
- "风格注入要 engine.js v0.8.0 起才认;把新版 assets/web/ 一起拷过去,"
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
- # 以前不查这一条,于是 style=None + --width 1280 --height 720 会"成功"输出 1280x720:
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
- v0.8.0:style 非 None 时按注入后的页面读(注入可能改画布尺寸,帧率仍以页面为准,
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
- v0.8.0:style/width/height/fps 透传给 open_page(缺省 = 旧行为:1920x1080、不注入)。
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
- v0.8.0:width/height 写进返回头(打完 style 注入后画布可能不是 1080p),其余照旧。
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 时,以前 `? : []` 会静默返回空数组:cues.json 里
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 为空时目录名与 v0.7 完全一致。
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 时返回空串(保持 v0.7 的目录名)。"""
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
- v0.4.0:探测不出时长的块(空文件 / 非视频 / ffprobe 失败)以前是 continue 静默放过,
945
- 结果 concat 时才发现;现在一并算异常——"读不出时长"本身就说明块是坏的。
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
- v0.8.0:style 非 None 时整片跟随风格包(每个 worker 都在页面加载前注入同一份
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 以前会被后面的 max(1, ...) 静默夹成「1 帧/块」:360 帧的 12 s 片会拆出
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(以前非法参数会被静默吞成空结果 + exit 0)。
1226
+ """文字质检扫描参数校验;非法 raise ValueError(否则非法参数会被静默吞成空结果 + exit 0)。
1227
1227
 
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 的闪
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
- 历史坑:以前只判 `isinstance(result, dict)`,页面 `qaText=()=>({})` 时 CLI 把四类缺陷
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
- v0.8.0:跟着 style 一起检查——换了浅色主题/非 1080p 画布后,出画与重叠的判据必须用
1290
+ 跟着 style 一起检查——换了浅色主题/非 1080p 画布后,出画与重叠的判据必须用
1291
1291
  同一份画布尺寸(qa_text.js 读 FK.W / FK.H),否则"检查通过"只是没在检查新尺寸。
1292
1292
 
1293
- 深审修复:参数与返回值 fail-closed(之前空扫描 / 扫不到的扫描 / 页面返回 {} 都是
1294
- exit 0,门禁看着是绿的,其实什么都没查)。见 _check_qa_window / _check_qa_result。
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(v0.8.0)
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
- 这是 v0.8.0 给 render.py / 自检 / 人工验收用的最小确定性入口:
1327
+ 这是给 render.py / 自检 / 人工验收用的最小确定性入口:
1328
1328
  帧号 → 时间 = f / FPS(与 render_full 同一口径),不存在"按播放时间截屏"。
1329
- style=None 时不注入 __STYLE__(像素与 v0.7 一致);传 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
- # ---------------------------------------------------------------- 自检辅助(v0.4.0)
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
- # ---------------------------------------------------------------- 自检:风格注入(v0.8.0)
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(v0.8.0):无头浏览器逐帧渲染 / 确定性 / cue / 文字质检 / 风格注入")
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
- # ---- v0.8.0:画布尺寸 / 风格注入 ----
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
- v1.0.1:**不再接受"stylekit 风格 id"这种写法**。内置风格表已删除,
2169
- `--style magazine` 这种用法没有对象了。早先这里会调 ST.get()/ST.theme_of(),
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
- " v1.0.1 删除了全部内置风格/主题表(本技能不提供模板)。现在注入外观只有两种写法:\n"
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
- """给子命令加风格参数(缺省 = v0.7 旧行为:不注入、1920x1080)。"""
2195
+ """给子命令加风格参数(缺省 = 不注入、1920x1080)。"""
2198
2196
  p.add_argument("--style", default="",
2199
2197
  help="外观载荷:work/style.json / 内联 JSON;缺省不注入"
2200
- "(v1.0.1 起不接受风格 id:内置风格表已删除)")
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="浏览器逐帧渲染引擎(v0.8.0):Canvas 页面 → CDP 截图 → ffmpeg → mp4(支持风格包注入)",
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)。以前每个分支各自解析,坏 id 会抛 SystemExit 变成 exit 1,脚本里分不清
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
- # 以前 --every 0 被当成"没写"、--every -1 则是死循环:先 page_meta 开一次浏览器,
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
- # 以前直接抛 traceback(exit 1,且报错淹没在栈里)。这是"要改页面/环境"的用法
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 + 可读原因;不再让一个 traceback 决定门禁红绿。
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
- # v0.4.0 之前只看前两类,后两类会被打印出来却 exit 0 —— 门禁形同建议。
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 跨片配乐(v1.0.1 起用 musicgen 参数化编排):
12
+ A 跨片配乐(musicgen 参数化编排):
13
13
  同参数/同 bpm/同调、只换内容指纹 -> 相关性(目标 <= 0.85);
14
14
  换编排参数(mood + 音色) -> 相关性(目标 <= 0.52)
15
- B 跨片音效(v0.7):同 cue 序列,legacy(不传 style/content_fp)必须逐样本相同;
15
+ B 跨片音效:同 cue 序列,legacy(不传 style/content_fp)必须逐样本相同;
16
16
  不同 content_fp 必须不同(否则"跨片音效都一样"没被修掉)
17
- C 片内分离(v0.7):duck 默认深度(目标 >= 3.5 dB)与配乐/音效的频谱质心对照
18
- D 旁白优先(v0.7.1):人声段 旁白/背景 响度差(目标 >= 12 dB)+ 侧链让位 >= 4 dB
17
+ C 片内分离:duck 默认深度(目标 >= 3.5 dB)与配乐/音效的频谱质心对照
18
+ D 旁白优先:人声段 旁白/背景 响度差(目标 >= 12 dB)+ 侧链让位 >= 4 dB
19
19
 
20
- 本脚本在改造前是"诊断工具",改造后是"验收工具":--strict 会逐条对阈值判定。
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 # 换编排参数的相关性上限(v0.7 改造前的 0.534)
39
- # v1.0.1:配乐改由 musicgen 参数化编排(scorekit 的 9 套风格表已删除),这两条阈值
40
- # 仍是「跨片不能太像」的验收线:同参数换指纹要 <=0.85,换编排参数要 <=0.52。
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
- """用参数化编排器现场生成一条配乐(v1.0.1:scorekit 的 9 套风格表已删除)。
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="配乐/音效相似度诊断(v0.7 验收)")
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,v0.4 兼容)" % legacy_same)
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. 旁白优先:人声与背景的响度差(v0.7.1)")
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 §14):
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
- # v0.8:ASR 回读门禁(可选,缺了 qaasr 自动降级到 ffmpeg whisper / skipped)
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
- # v0.6.0:门禁账本 / 交付报告 / 签核、阶段新鲜度守卫、film.json 契约校验器
57
+ # 门禁账本 / 交付报告 / 签核、阶段新鲜度守卫、film.json 契约校验器
58
58
  "gates", "stagewatch", "filmcheck",
59
- # v0.8.0:渲染前资源闸门、全片 VMAF/SSIM 门禁、ASR 回读门禁
59
+ # 渲染前资源闸门、全片 VMAF/SSIM 门禁、ASR 回读门禁
60
60
  "preflight", "qavmaf", "qaasr",
61
- # v0.9.0:场景语法(内容 -> 构图选型)。纯标准库,可被 scenes.py 反向 import
61
+ # 场景语法(内容 -> 构图选型)。纯标准库,可被 scenes.py 反向 import
62
62
  "grammar",
63
- # v1.0:设计 DSL / 离线预览 / 提案器(D3 改造的第一期)
63
+ # 设计 DSL / 离线预览 / 提案器
64
64
  "designkit", "previewkit", "propose",
65
- # v1.0.1:presetkit 已删除(连同 presets/ 一起);这里不能再列它,
65
+ # 没有 presetkit(随预设目录一起移除);这里不能再列它,
66
66
  # 否则 ci_check 会去 import 一个不存在的模块
67
- # v1.0 / P2:参数化配乐编排器(替代 9 套 MUSIC_STYLES 表)
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 顶部还是上一版"没人发现(历史真发生过,见 v0.7.2 的修复记录)。
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
- # 必须全部进包。以前这里是手写 must 列表,于是 v0.6 新增的 gates.py / stagewatch.py、
264
- # v0.8 新增的 preflight.py / qavmaf.py / qaasr.py / tools/golden_frames.py 都不在列表里——
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> = 包根(仓库根)。注意**不能**把它们当成"当前工作目录":quickstart 把技能拷进 myfilm/ 后,
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
- v0.7.1 之前这里写的是 startswith("\\`\\`\\`")——那是一个无效转义,Python 把反斜杠原样保留,
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, ("模板 film.json 没过契约校验(exit=%d):\n%s"
541
+ assert p.returncode == 0, ("夹具 film.json 没过契约校验(exit=%d):\n%s"
542
542
  % (p.returncode, out.strip()[-600:]))
543
- return "模板 film.json 通过契约校验;schema %d 字节" % len(schema.read_text(encoding="utf-8"))
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 / 字体 / 网络)")