@dsh-gis/dsh-video-studio-skill 0.9.1 → 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 (63) hide show
  1. package/CHANGELOG.md +473 -0
  2. package/README.md +263 -327
  3. package/THIRD_PARTY_NOTICES.md +2 -2
  4. package/package.json +9 -10
  5. package/schemas/design.schema.json +722 -0
  6. package/schemas/film.schema.json +31 -42
  7. package/schemas/proposal.schema.json +133 -0
  8. package/skills/video-studio/SKILL.md +1216 -1123
  9. package/skills/video-studio/assets/design_fixture.json +141 -0
  10. package/skills/video-studio/assets/film.json +2 -4
  11. package/skills/video-studio/assets/golden_frames.json +13 -10
  12. package/skills/video-studio/assets/scenes.py +153 -65
  13. package/skills/video-studio/assets/voice_loudness.json +16 -0
  14. package/skills/video-studio/references/appearance.md +123 -0
  15. package/skills/video-studio/references/audio-variation.md +74 -93
  16. package/skills/video-studio/references/audio.md +64 -66
  17. package/skills/video-studio/references/change-impact.md +5 -5
  18. package/skills/video-studio/references/{v08-quality.md → delivery-gates.md} +16 -18
  19. package/skills/video-studio/references/frame-qa.md +48 -16
  20. package/skills/video-studio/references/pipeline.md +4 -4
  21. package/skills/video-studio/references/pitfalls.md +22 -3
  22. package/skills/video-studio/references/proposal.md +160 -0
  23. package/skills/video-studio/references/quality-gates.md +11 -11
  24. package/skills/video-studio/references/scene-authoring.md +2 -2
  25. package/skills/video-studio/references/scene-grammar.md +40 -42
  26. package/skills/video-studio/references/sfx-design.md +8 -6
  27. package/skills/video-studio/references/web-engine.md +8 -8
  28. package/skills/video-studio/references/word-timeline.md +9 -9
  29. package/skills/video-studio/requirements.txt +9 -13
  30. package/skills/video-studio/scripts/designkit.py +2115 -0
  31. package/skills/video-studio/scripts/filmcheck.py +22 -48
  32. package/skills/video-studio/scripts/filmkit.py +2980 -2904
  33. package/skills/video-studio/scripts/gates.py +4 -4
  34. package/skills/video-studio/scripts/grammar.py +73 -13
  35. package/skills/video-studio/scripts/musicgen.py +2042 -0
  36. package/skills/video-studio/scripts/preflight.py +14 -7
  37. package/skills/video-studio/scripts/previewkit.py +848 -0
  38. package/skills/video-studio/scripts/propose.py +825 -0
  39. package/skills/video-studio/scripts/qa_frames.py +73 -39
  40. package/skills/video-studio/scripts/qaasr.py +26 -7
  41. package/skills/video-studio/scripts/qavmaf.py +89 -23
  42. package/skills/video-studio/scripts/render.py +545 -231
  43. package/skills/video-studio/scripts/scorekit.py +55 -1755
  44. package/skills/video-studio/scripts/selfcheck.py +815 -215
  45. package/skills/video-studio/scripts/sfxkit.py +27 -26
  46. package/skills/video-studio/scripts/stagewatch.py +24 -8
  47. package/skills/video-studio/scripts/stylekit.py +207 -888
  48. package/skills/video-studio/scripts/voicekit.py +508 -387
  49. package/skills/video-studio/scripts/webengine.py +71 -72
  50. package/tools/audio_similarity.py +64 -37
  51. package/tools/ci_check.py +20 -14
  52. package/tools/estimate.py +34 -29
  53. package/tools/golden_frames.py +72 -28
  54. package/tools/silhouette_qa.py +520 -168
  55. package/docs/demo-pil-sheet.png +0 -0
  56. package/docs/styles/demo-results.json +0 -76
  57. package/docs/styles/sheet-8-styles.png +0 -0
  58. package/docs/styles/sheet-ai.png +0 -0
  59. package/docs/styles/sheet-before-after.png +0 -0
  60. package/docs/styles/sheet-craft.png +0 -0
  61. package/docs/styles/sheet-night.png +0 -0
  62. package/skills/video-studio/assets/score_demo.json +0 -307
  63. package/skills/video-studio/references/style-packs.md +0 -283
@@ -1,16 +1,15 @@
1
1
  # -*- coding: utf-8 -*-
2
- """golden_frames.py — 金帧像素回归(v0.8.0)。
2
+ """golden_frames.py — 金帧像素回归。
3
3
 
4
4
  解决什么问题
5
5
  ------------
6
- v0.5 起 scenes.py 的注释里就写着「基线三帧 sha256 由 selfcheck 比对」,但一直没人真的
7
- 实现:改一行绘制代码、换一套主题、动字幕排版,肉眼看不出来、qa_frames 的统计指标也
8
- 未必报——只有"同一帧的像素哈希"能当场抓住。本工具就是那条基线。
6
+ 改一行绘制代码、换一套设计、动字幕排版,肉眼看不出来、qa_frames 的统计指标也未必报——
7
+ 只有"同一帧的像素哈希"能当场抓住。本工具就是那条基线。
9
8
 
10
9
  做什么
11
10
  ------
12
11
  渲染指定帧号 -> 每帧算 SHA-256 -> 与 skills/video-studio/assets/golden_frames.json 比对。
13
- * --write 生成/更新基线(模板有意改动后重录,并把 JSON 一起提交)
12
+ * --write 生成/更新基线(绘制代码有意改动后重录,并把 JSON 一起提交)
14
13
  * --check 校验(默认):同环境像素变了 = fail,并列出哪几帧变了
15
14
 
16
15
  三档语义("没查"绝不能写成"通过")
@@ -30,7 +29,7 @@ sha1),指纹不同直接 skipped(跨机不误报);指纹相同还变
30
29
  基线怎么来的(确定性)
31
30
  ----------------------
32
31
  不联网、不依赖 TTS 产物:用固定的假语音时长(FAKE_VOICE_DUR=2.0s)排一条合成时间轴,
33
- 取模板片的 3 个时刻(hook / body / outro)作为默认帧号。这样同一份 film.json + scenes.py
32
+ 取夹具片的 3 个时刻(hook / body / outro)作为默认帧号。这样同一份 film.json + scenes.py
34
33
  在任何机器上都能立刻重算基线;真实旁白时长只影响"哪一帧是哪一句",不影响"这一帧画成
35
34
  什么样",后者才是本工具要守的东西。帧号与场景 id、段内时刻都记进基线,便于人工复核。
36
35
 
@@ -51,7 +50,7 @@ CLI
51
50
  """
52
51
  from __future__ import annotations
53
52
 
54
- __version__ = "0.8.0"
53
+ __version__ = "1.0.4"
55
54
 
56
55
  import argparse
57
56
  import hashlib
@@ -69,6 +68,11 @@ SCRIPTS = os.path.join(SKILL, "scripts")
69
68
  ASSETS = os.path.join(SKILL, "assets")
70
69
 
71
70
  DEFAULT_SCRIPT = os.path.join(ASSETS, "film.json")
71
+ # v1.0 / D3:金帧基线绑"固定 fixture 设计"而不是内置风格。
72
+ # 为什么:金帧要守的是"同一份设计 + 同一份场景代码,像素不许漂",
73
+ # 不是"某套内置风格长什么样"——后者会随风格表下沉到 presets 而失去意义。
74
+ # 没有这个文件时回落到 stylekit 老口径(它现在只会明确报错),不阻塞诊断。
75
+ DEFAULT_DESIGN = os.path.join(ASSETS, "design_fixture.json")
72
76
  DEFAULT_SCENES = os.path.join(ASSETS, "scenes.py")
73
77
  DEFAULT_GOLDEN = os.path.join(ASSETS, "golden_frames.json")
74
78
 
@@ -79,7 +83,7 @@ FAKE_VOICE_DUR = 2.0
79
83
  HOOK_LOCAL = 1.2
80
84
  BODY_LOCAL = 1.6
81
85
  OUTRO_LOCAL = 1.5
82
- # 参与字体指纹的字重(模板的绘制函数只用到 bold / reg / light)
86
+ # 参与字体指纹的字重(内置绘制器只用到 bold / reg / light)
83
87
  FONT_WEIGHTS = ("bold", "reg", "light")
84
88
 
85
89
  RESULT_KEYS = ("status", "frames", "font_fp")
@@ -186,7 +190,7 @@ def build_timeline(K, script):
186
190
  与 render.st_layout 的差别只在 dur 的来源:这里每句固定 FAKE_VOICE_DUR 秒,
187
191
  其余(lead/tail/min_dur/hold/bar)照常走 filmkit.build_timeline,结果可复现。
188
192
 
189
- 但 **fps 必须跟 render.py 一样取自 filmkit.video_plan**(v0.8 起 fps 可配置):
193
+ 但 **fps 必须跟 render.py 一样取自 filmkit.video_plan**(fps 可按片配置):
190
194
  build_timeline 的默认 fps=30 会让 24/25/50/60 fps 的片子按 30 fps 编号,
191
195
  于是基线里的 frame 号既不是成片的真实帧号,t=frame/fps 也不是真实时刻——
192
196
  那份基线守的是"另一部片子"的画面,回放/比对都错位(且不会报错)。
@@ -216,11 +220,39 @@ def build_timeline(K, script):
216
220
  return tl
217
221
 
218
222
 
219
- def resolve_plan(ST, script):
220
- """解析风格计划并挂到 script["_style"](与 render.resolve_style 同口径,含 theme_overrides)。
223
+ def _design_plan(design_path):
224
+ """读 fixture 设计并转成计划;读不到返回 None(回落到 stylekit 老口径)。"""
225
+ if not design_path or not os.path.exists(str(design_path)):
226
+ return None
227
+ try:
228
+ with open(design_path, encoding="utf-8") as fh:
229
+ design = json.load(fh)
230
+ except (OSError, json.JSONDecodeError) as e:
231
+ raise RuntimeError("fixture 设计读不出来(%s):%s" % (design_path, e)) from e
232
+ sys.path.insert(0, SCRIPTS)
233
+ import designkit as DK
234
+ bad = DK.validate_design(design)
235
+ if bad:
236
+ raise RuntimeError("fixture 设计没过校验(%s):%s" % (design_path, ";".join(bad)))
237
+ return DK.design_to_plan(design)
238
+
239
+
240
+ def resolve_plan(ST, script, design_path=None):
241
+ """解析计划并挂到 script["_style"]。
242
+
243
+ v1.0 / D3 起优先用 **fixture 设计**(design_path,默认 assets/design_fixture.json):
244
+ 金帧守的是"固定设计 + 固定场景代码 -> 像素不漂",与"哪套内置风格"无关。
245
+ 没有 fixture 时回落到 stylekit 老口径,不阻塞老项目。
221
246
 
222
247
  主题体检失败在这里就抛错(不要等渲到第 3 帧才炸)。
223
248
  """
249
+ plan = _design_plan(design_path)
250
+ if plan is not None:
251
+ script["_style"] = plan
252
+ bad = ST.theme_lint(plan.get("theme") or {})
253
+ if bad:
254
+ raise RuntimeError("fixture 设计主题体检失败:%s" % ";".join(bad))
255
+ return plan
224
256
  plan = ST.resolve_script_style(script)
225
257
  bad = ST.theme_lint(plan.get("theme") or ST.THEMES[plan["theme_id"]])
226
258
  if bad:
@@ -266,7 +298,7 @@ def env_fingerprint():
266
298
  口径细节:**Python 只按 major.minor 参与指纹**,补丁号只记录不参与比较。
267
299
  理由是真正决定抗锯齿像素的是 Pillow(FreeType)与 numpy,不是 CPython 的补丁版本;
268
300
  而按补丁号比较会让"作者机器 3.12.16 / 用户机器 3.12.14"这种正常差异一律 skipped——
269
- 实测踩过:selfcheck 第 51 条因此显示"0 帧像素一致"却记 PASS,等于这条门禁从没真的比过。
301
+ 实测踩过:金帧回归因此显示"0 帧像素一致"却记 PASS,等于这条门禁从没真的比过。
270
302
  真要跨 Pillow/numpy 版本比,仍然会 skipped,那才是像素不可比的情形。
271
303
  """
272
304
  import numpy
@@ -281,7 +313,7 @@ def env_fingerprint():
281
313
  def _env_fp_from(env):
282
314
  """按"参与比较的口径"给 env 字典算指纹(Python 只取 major.minor)。
283
315
 
284
- 单独抽出来是为了**兼容老基线**:v0.8.0 的 golden_frames.json 里记的是完整补丁号
316
+ 单独抽出来是为了**兼容老基线**:早期 golden_frames.json 里记的是完整补丁号
285
317
  (如 3.12.16),如果直接拿新口径算出的指纹去比,老基线会永远 skipped——那等于把
286
318
  "补丁号差异"永久固化成"这条门禁不用跑"。这里对**存的 env** 和**当前 env** 走同一个
287
319
  归一化函数,所以 3.12.14 与 3.12.16 视为同一环境,而 Pillow/numpy 的差异照旧拦住。
@@ -384,7 +416,7 @@ def default_frame_specs(tl):
384
416
 
385
417
 
386
418
  # ---------------------------------------------------------------- 基线读写
387
- def _golden_payload(script_path, scenes_path, K, plan, tl, rendered):
419
+ def _golden_payload(script_path, scenes_path, K, plan, tl, rendered, design_path=None):
388
420
  font_fp, fonts = font_fingerprint(K, plan)
389
421
  env_fp, env = env_fingerprint()
390
422
  return {
@@ -392,12 +424,13 @@ def _golden_payload(script_path, scenes_path, K, plan, tl, rendered):
392
424
  "generator": "golden_frames.py v%s" % __version__,
393
425
  "comment": "金帧基线:字体指纹+环境指纹一致时,每一帧像素 sha256 必须相同;"
394
426
  "换机/字体/Pillow 版本不同判 skipped(不误报),同环境像素变了才是 fail。"
395
- "改动模板(film.json / scenes.py / stylekit 主题)后请 --write 重录并提交本文件。",
427
+ "改动夹具(film.json / scenes.py / 设计)后请 --write 重录并提交本文件。",
396
428
  "canvas": [int(K.W), int(K.H)],
397
- # 记时间轴自己的 fps(不是写死的 K.FPS):v0.8 起 film.json 可以设 24/25/50/60
429
+ # 记时间轴自己的 fps(不是写死的 K.FPS):film.json 可以设 24/25/50/60
398
430
  "fps": int(tl["fps"]),
399
431
  "script": _rel(script_path),
400
432
  "scenes": _rel(scenes_path),
433
+ "design": _rel(design_path) if design_path else "",
401
434
  "font_fp": font_fp,
402
435
  "fonts": fonts,
403
436
  "env_fp": env_fp,
@@ -407,6 +440,9 @@ def _golden_payload(script_path, scenes_path, K, plan, tl, rendered):
407
440
  "scenes_sha1": _sha1_file(scenes_path),
408
441
  "filmkit_sha1": _sha1_file(os.path.join(SCRIPTS, "filmkit.py")),
409
442
  "stylekit_sha1": _sha1_file(os.path.join(SCRIPTS, "stylekit.py")),
443
+ # 设计 fixture 进指纹:改了基准设计却不重录基线 = "改了但基线不报警"
444
+ "design_sha1": _sha1_file(design_path) if design_path else "",
445
+ "designkit_sha1": _sha1_file(os.path.join(SCRIPTS, "designkit.py")),
410
446
  "golden_tool_sha1": _sha1_file(os.path.abspath(__file__)),
411
447
  },
412
448
  "frames": [dict(sp, sha256=sha, scene=scene_id, frame=int(f))
@@ -414,13 +450,13 @@ def _golden_payload(script_path, scenes_path, K, plan, tl, rendered):
414
450
  }
415
451
 
416
452
 
417
- def write_golden(script_path, scenes_path, golden_path, frames=None):
453
+ def write_golden(script_path, scenes_path, golden_path, frames=None, design_path=None):
418
454
  """--write:渲染金帧并写基线。返回 result(status=pass,written=True)。"""
419
455
  K, ST = _modules()
420
456
  script = _load_script(script_path)
421
457
  scenes_mod = _load_scenes(scenes_path)
422
458
  tl = build_timeline(K, script)
423
- plan = resolve_plan(ST, script)
459
+ plan = resolve_plan(ST, script, design_path=design_path)
424
460
  if frames:
425
461
  specs = [dict(frame=int(f), scene=str(_scene_at(tl, int(f) / float(tl["fps"]))[0]
426
462
  .get("id") or ""),
@@ -432,7 +468,8 @@ def write_golden(script_path, scenes_path, golden_path, frames=None):
432
468
  specs = default_frame_specs(tl)
433
469
  rendered = render_frames(script, scenes_mod, plan, tl, [sp["frame"] for sp in specs])
434
470
  plan = dict(plan, specs=specs)
435
- data = _golden_payload(script_path, scenes_path, K, plan, tl, rendered)
471
+ data = _golden_payload(script_path, scenes_path, K, plan, tl, rendered,
472
+ design_path=design_path)
436
473
  golden_path = os.path.abspath(str(golden_path))
437
474
  d = os.path.dirname(golden_path)
438
475
  if d:
@@ -475,7 +512,7 @@ def _fmt_fp_pair(actual, expect):
475
512
  return "%s… vs %s…" % (str(actual)[:12], str(expect)[:12])
476
513
 
477
514
 
478
- def check_golden(script_path, scenes_path, golden_path):
515
+ def check_golden(script_path, scenes_path, golden_path, design_path=None):
479
516
  """--check:渲染基线里的帧号并与基线比对(默认命令)。"""
480
517
  golden, why = _load_golden(golden_path)
481
518
  if golden is None:
@@ -498,7 +535,7 @@ def check_golden(script_path, scenes_path, golden_path):
498
535
  script = _load_script(script_path)
499
536
  scenes_mod = _load_scenes(scenes_path)
500
537
  tl = build_timeline(K, script)
501
- plan = resolve_plan(ST, script)
538
+ plan = resolve_plan(ST, script, design_path=design_path)
502
539
  font_fp, fonts = font_fingerprint(K, plan)
503
540
  env_fp, env = env_fingerprint()
504
541
  # 先做"能不能比"的判定,再渲(省得换机时白渲三帧)
@@ -510,7 +547,7 @@ def check_golden(script_path, scenes_path, golden_path):
510
547
  reason="skipped: font differs(字体指纹 %s)——换机/字体版本不同,"
511
548
  "像素比对不成立;确认画面后在当前机器 --write 重录"
512
549
  % _fmt_fp_pair(font_fp, golden.get("font_fp")))
513
- # 比指纹时对**存的 env** 走同一个归一化(见 _env_fp_from):这样 v0.8.0 那些
550
+ # 比指纹时对**存的 env** 走同一个归一化(见 _env_fp_from):这样早期那些
514
551
  # 记着完整补丁号的老基线不会因为 3.12.14 vs 3.12.16 永久 skipped。
515
552
  golden_env_fp = _env_fp_from(golden.get("env") or {})
516
553
  if golden_env_fp != env_fp:
@@ -551,7 +588,9 @@ def check_golden(script_path, scenes_path, golden_path):
551
588
  cur_inputs = dict(script_sha1=_sha1_file(script_path),
552
589
  scenes_sha1=_sha1_file(scenes_path),
553
590
  filmkit_sha1=_sha1_file(os.path.join(SCRIPTS, "filmkit.py")),
554
- stylekit_sha1=_sha1_file(os.path.join(SCRIPTS, "stylekit.py")))
591
+ stylekit_sha1=_sha1_file(os.path.join(SCRIPTS, "stylekit.py")),
592
+ design_sha1=(_sha1_file(design_path) if design_path else ""),
593
+ designkit_sha1=_sha1_file(os.path.join(SCRIPTS, "designkit.py")))
555
594
  stale = [k for k, v in cur_inputs.items()
556
595
  if inputs.get(k) and inputs.get(k) != v]
557
596
  if changed:
@@ -607,12 +646,15 @@ def main(argv=None):
607
646
  g = ap.add_mutually_exclusive_group()
608
647
  g.add_argument("--check", action="store_true", help="校验(默认)")
609
648
  g.add_argument("--write", action="store_true", help="生成/更新基线")
610
- ap.add_argument("--script", default=DEFAULT_SCRIPT, help="film.json(默认模板片)")
611
- ap.add_argument("--scenes", default=DEFAULT_SCENES, help="scenes.py(默认模板场景)")
649
+ ap.add_argument("--script", default=DEFAULT_SCRIPT, help="film.json(默认夹具片)")
650
+ ap.add_argument("--design", default=DEFAULT_DESIGN,
651
+ help="固定 fixture 设计(默认 assets/design_fixture.json);"
652
+ "传空串回落到老的 stylekit 口径")
653
+ ap.add_argument("--scenes", default=DEFAULT_SCENES, help="scenes.py(默认内置场景)")
612
654
  ap.add_argument("--golden", default=DEFAULT_GOLDEN,
613
655
  help="基线 JSON(默认 skills/video-studio/assets/golden_frames.json)")
614
656
  ap.add_argument("--frames", default="",
615
- help="--write 时指定帧号(逗号分隔);缺省取模板片 3 个时刻")
657
+ help="--write 时指定帧号(逗号分隔);缺省取夹具片 3 个时刻")
616
658
  ap.add_argument("--json", action="store_true", help="stdout 只输出 JSON(机读)")
617
659
  ap.add_argument("--strict", action="store_true",
618
660
  help="skipped 用退出码 2 表示(默认 skipped 退出码 0,但绝不写 pass)")
@@ -627,12 +669,14 @@ def main(argv=None):
627
669
  raise SystemExit("--frames 必须是逗号分隔的整数帧号,收到 %r" % a.frames)
628
670
  if not frames:
629
671
  raise SystemExit("--frames 里没有有效帧号")
630
- res = write_golden(a.script, a.scenes, a.golden, frames)
672
+ res = write_golden(a.script, a.scenes, a.golden, frames,
673
+ design_path=(a.design or None))
631
674
  else:
632
675
  if a.frames:
633
676
  raise SystemExit("--frames 只用于 --write;--check 的帧号由基线决定"
634
677
  "(要换帧号请 --write --frames ... 重录)")
635
- res = check_golden(a.script, a.scenes, a.golden)
678
+ res = check_golden(a.script, a.scenes, a.golden,
679
+ design_path=(a.design or None))
636
680
  except SystemExit:
637
681
  raise
638
682
  except Exception as e: # noqa: BLE001