@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
@@ -43,7 +43,7 @@ HERE = Path(__file__).resolve().parent
43
43
 
44
44
  KINDS = ("hook", "body", "quote", "outro", "emphasis", "calm") # 也是 voicekit 的语气 id
45
45
 
46
- # 视觉场景类型(v0.9):决定"这段画面长成什么构图",与叙事 kind **正交**——
46
+ # 视觉场景类型:决定"这段画面长成什么构图",与叙事 kind **正交**——
47
47
  # kind 管声线/语气/配乐能量(voicekit / scorekit 读它);
48
48
  # type 只管构图(assets/scenes.py 的 _DRAWERS 读它)。
49
49
  # 所以**不新增 kind 值**:hero/stat/... 不是语气,硬塞进 KINDS 会撞 voicekit 的 tone id
@@ -65,8 +65,8 @@ ID_RE = re.compile(r"^[A-Za-z0-9_.\-]+$")
65
65
  CPS = 4.2 # 每秒约 4.2 字(voicekit 实测中位口径)
66
66
  # 语速区间而不是单一 4.2:voicekit 校准实测 3.84–4.61 cps(9 个 persona,固定校准句),
67
67
  # 而 stylekit 的声线策略允许的 cps_band 是 3.6–5.6(rate 覆盖 ±15% 后)。
68
- # 只用中位数会误报:模板片实测 5.2 字/秒的句子被按 4.2 估 -> 全片多算 5.2 s ->
69
- # "剧本与 timeline 差 5.23 s"的假 WARN(v0.7.2 修,模板片复现)。
68
+ # 只用中位数会误报:夹具片实测 5.2 字/秒的句子被按 4.2 估 -> 全片多算 5.2 s ->
69
+ # "剧本与 timeline 差 5.23 s"的假 WARN(改按声线带宽判,夹具片可复现)。
70
70
  CPS_LO, CPS_HI = 3.6, 5.6
71
71
  DEFAULT_LEAD, DEFAULT_TAIL, DEFAULT_MIN_DUR = 0.62, 0.92, 3.0
72
72
  TIMELINE_TOL = 0.5 # 估算与 timeline 差超过这个秒数就 WARN
@@ -82,7 +82,7 @@ KEY_ALIASES = {
82
82
  "cue": "cues", "sfx_cues": "cues", "sound": "sfx", "bgm": "music",
83
83
  }
84
84
 
85
- # 不是 film.json 的其他产物(例如 assets/score_demo.json 是 scorekit 报告)——
85
+ # 不是 film.json 的其他产物(例如配乐引擎落盘的 score.json 报告)——
86
86
  # 交给本脚本前先认出来,直接 SKIP 而不是按剧本报"缺 title"的错误。
87
87
  OTHER_ARTIFACT_KEYS = ("generator", "meas", "reproduce", "sections", "beats",
88
88
  "transitions", "sr", "duration", "score")
@@ -211,7 +211,7 @@ LINE_ITEM_SPEC = {
211
211
  },
212
212
  }
213
213
 
214
- # ---- v0.9 视觉场景字段的嵌套结构(写法照抄 LINE_ITEM_SPEC)----
214
+ # ---- 视觉场景字段的嵌套结构(写法照抄 LINE_ITEM_SPEC)----
215
215
  # 这些对象的键由 scenes.py 的 10 个绘制器读:拼错就是"写了不生效",所以**不**用
216
216
  # open=True(那是留给 sfx 配方参数那种真·透传的)。每层列全 props,才能把拼错报成
217
217
  # unknown_key(WARN + "是不是想写 X"),把类型写错报成 ERROR。
@@ -282,7 +282,7 @@ LINE_SPEC = {
282
282
  "persona": {"t": "str"},
283
283
  "tone": {"t": "str", "enum": KINDS},
284
284
  "voice": {"t": "str"},
285
- # ---- v0.9 视觉场景字段(7 个全可选)----
285
+ # ---- 视觉场景字段(7 个全可选)----
286
286
  # type 缺省由 grammar.py 按内容推导;其余字段是给绘制器的原料,不写就不画。
287
287
  # 注意这些字段与 kind **正交**,不是新 kind:kind 仍只有上面 6 个(voicekit 的语气 id)。
288
288
  "type": {"t": "str", "enum": SCENE_TYPES},
@@ -322,7 +322,11 @@ SCRIPT_SPEC = {
322
322
  "props": {
323
323
  "title": {"t": "str", "nonempty_warn": True},
324
324
  "end_text": {"t": "str"},
325
- "style": {"t": "str", "enum_dynamic": "style"},
325
+ # style is deprecated: appearance comes from work/design.json.
326
+ # It used to use enum_dynamic -> _load_styles(); STYLE_IDS is empty, so it emitted
327
+ # a misleading WARN saying stylekit.py was missing (the file was there).
328
+ # Now it is an inert key: accepted (old scripts must not hard-fail) but reported.
329
+ "style": {"t": "str", "inert": "已废弃:外观由 work/design.json 决定(python propose.py --brief ... 再 --confirm),这里写任何值都不生效"},
326
330
  "engine": {"t": "str", "enum": ENGINES},
327
331
  "voice": {"t": ("str", "dict"), "open": "dict"},
328
332
  "rate": {"t": "str", "pattern": RATE_RE, "example": "+6%"},
@@ -365,10 +369,10 @@ SCRIPT_SPEC = {
365
369
  "seed": {"t": "int", "min": 0},
366
370
  "seed_from_content": {"t": "bool"},
367
371
  "gain_db": {"t": "num"}, "duck_db": {"t": "num"},
368
- # v0.7.1 旁白优先:mix_film 读这四个(背景在人声处额外让位多少 / 阈值)
372
+ # 旁白优先:mix_film 读这四个(背景在人声处额外让位多少 / 阈值)
369
373
  "voice_duck_db": {"t": "num"}, "voice_duck_sfx_db": {"t": "num"},
370
374
  "voice_thresh_db": {"t": "num"}, "voice_floor_db": {"t": "num"},
371
- # v0.7.1 SNR 兜底:混音后实测人声/背景差,不足就整体压背景
375
+ # SNR 兜底:混音后实测人声/背景差,不足就整体压背景
372
376
  "guard_snr_db": {"t": "num"}, "guard_cap_db": {"t": "num"},
373
377
  "seg": {"t": "num", "min": 0.0}, "amp": {"t": "num", "min": 0.0},
374
378
  "gain": {"t": "num", "min": 0.0}, "xfade": {"t": "num", "min": 0.0},
@@ -381,7 +385,7 @@ SCRIPT_SPEC = {
381
385
  # render.py 的 st_sfx 把它透传给 CueBus.build_audio():按配方名覆盖基础电平。
382
386
  # 以前 schema/filmcheck 都没写,用户写了会被当未知键警告(其实真的会生效)。
383
387
  "base_db": {"t": "dict", "open": True},
384
- # v0.7 音频差异化:content_seed 由 CueBus 读(位置派生 vs 内容派生);
388
+ # 音频差异化:content_seed 由 CueBus 读(位置派生 vs 内容派生);
385
389
  # duck_transitions 由 render.st_sfx 读(段落边界前的转场音效自动让位)。
386
390
  "content_seed": {"t": "bool"},
387
391
  "duck_transitions": {"t": "bool"},
@@ -389,10 +393,10 @@ SCRIPT_SPEC = {
389
393
  "web": {"t": "dict", "props": {
390
394
  "entry": {"t": "str"},
391
395
  "chunk_s": {"t": "num", "min": 0.0}}},
392
- # ---- v0.8 ----
396
+ # ---- 词级时间戳 / TTS 后端 ----
393
397
  # 词级时间戳开关(字幕按词折行 / cue 词锚点 / G10 门禁都依赖它)
394
398
  "words": {"t": "bool"},
395
- # TTS 后端选择(v0.8 接口化;默认 edge,其它后端需先 register_tts_backend)
399
+ # TTS 后端选择(接口化;默认 edge,其它后端需先 register_tts_backend)
396
400
  "tts": {"t": "dict", "props": {
397
401
  "backend": {"t": "str"},
398
402
  "voice": {"t": ("str", "dict")}}},
@@ -406,7 +410,7 @@ SCRIPT_SPEC = {
406
410
  # 质检门禁参数(全片 VMAF/SSIM 阻断 + ASR 回读)
407
411
  "qa": {"t": "dict", "props": {
408
412
  "vmaf": {"t": "dict", "props": {
409
- # enabled / require_vmaf 是 v0.8 起草时留下的**惰性键**:schema 与这里都认,
413
+ # enabled / require_vmaf 是起草时留下的**惰性键**:schema 与这里都认,
410
414
  # 但 render.py / qavmaf.py 全仓没有读取点——写了它门禁照跑,用户以为自己关掉了。
411
415
  # 真正生效的是 mode=off(或命令行 --vmaf off)。这里标 inert 让 filmcheck 报 WARN,
412
416
  # 因为本项目的立场是"写对了却不生效"比"报未知键"更坏。
@@ -414,7 +418,7 @@ SCRIPT_SPEC = {
414
418
  "mode": {"t": "str", "enum": ("full", "sample", "off")},
415
419
  "pass": {"t": "num", "min": 0.0},
416
420
  "warn": {"t": "num", "min": 0.0},
417
- # v0.8.1:键名对齐 schemas/film.schema.json 与 qavmaf.report(sample_step=...)。
421
+ # 键名对齐 schemas/film.schema.json 与 qavmaf.report(sample_step=...)。
418
422
  # 这里以前写的是 sample_s —— 全仓没有任何消费者(render 不读、qavmaf 只认
419
423
  # sample_step),于是"写对了却完全没生效",而 schema 里合法写完还会被本脚本
420
424
  # 报成未知键。别再兼容 sample_s:它从来没生效过,留着只会让人以为能用。
@@ -470,22 +474,6 @@ def _check(val, rule, path, rep, ctx):
470
474
  if rule.get("inert"):
471
475
  rep.warn(path, "这个键**当前不生效**:%s" % rule["inert"], "inert_key")
472
476
 
473
- if rule.get("enum_dynamic") == "style":
474
- styles = ctx.get("styles")
475
- if styles is None:
476
- if not ctx.get("style_warned"):
477
- ctx["style_warned"] = True
478
- rep.warn(path, "stylekit.STYLES 不可用(找不到 %s/stylekit.py),"
479
- "跳过 style 枚举校验——只提示,不判错" % str(HERE), "stylekit_missing")
480
- else:
481
- enum = ["auto"] + list(styles)
482
- if enum and isinstance(val, str) and val not in enum:
483
- near = _suggest(val, [e for e in enum if isinstance(e, str)])
484
- hint = (";是不是想写 %r?" % near) if near else ""
485
- rep.err(path, "枚举错误:%r 不是可用值(可用:%s)%s"
486
- % (val, " / ".join(str(e) for e in enum), hint), "bad_enum")
487
- return
488
-
489
477
  if isinstance(val, (int, float)) and not isinstance(val, bool):
490
478
  if rule.get("positive") and val <= 0:
491
479
  rep.err(path, "必须大于 0(收到 %r;bpm 这类字段也可以写 \"auto\")" % val, "bad_range")
@@ -573,7 +561,7 @@ def _cue_ref(at):
573
561
  float(off)
574
562
  except ValueError:
575
563
  return "bad", "偏移 %r 不是数字(写 l03+0.35 或 l03-0.2)" % off
576
- # v0.8 词锚点:<id>:w<N>[+/-秒]。把 ":wN" 剥掉再做 id 存在性检查,
564
+ # 词锚点:<id>:w<N>[+/-秒]。把 ":wN" 剥掉再做 id 存在性检查,
577
565
  # 否则合法写法会被报成"场景 id 不存在"(假失败比不校验更糟)。
578
566
  if ":w" in sid:
579
567
  head, _sep, tail = sid.partition(":w")
@@ -645,7 +633,7 @@ def check_cross(script, script_path, rep, ctx):
645
633
  "tail_map)。例如 %s: 1.4" % lid),
646
634
  "inert_line_tail")
647
635
 
648
- # ---- v0.9 场景语法:绘制器的容量上限(超了会被静默截断) ----
636
+ # ---- 场景语法:绘制器的容量上限(超了会被静默截断) ----
649
637
  # 这是本项目最忌讳的一类坑:作者写对了、跑得通、但画面上少了几条,而没有任何提示。
650
638
  # 容量由 assets/scenes.py 的实现决定(stats 取前 3 条、series 前 6 条、steps 前 5 条、
651
639
  # sides 每侧前 3 条),改那边要同步改这里。
@@ -668,7 +656,7 @@ def check_cross(script, script_path, rep, ctx):
668
656
  "sides.%s 有 %d 条,但左右对比构图每侧只画前 3 条"
669
657
  % (side, len(its)), "scene_capacity")
670
658
 
671
- # ---- v0.8 video 块:只支持 16:9 横屏 + 下采样输出 ----
659
+ # ---- video 块:只支持 16:9 横屏 + 下采样输出 ----
672
660
  # **不要在这里重写一遍规则**:允许列表 / 宽高比 / 上采样拒绝的权威实现在
673
661
  # filmkit.video_plan()。以前这里是第二套手写判据,于是"1600x900"能过 filmcheck
674
662
  # (0 error 0 warning)却在 render 的 st_layout 被 video_plan 拒片——校验器放行、
@@ -813,19 +801,6 @@ def _fail(msg, as_json, code=2):
813
801
  raise SystemExit(code)
814
802
 
815
803
 
816
- def _load_styles():
817
- """从 stylekit.STYLES 单向读取风格 id;拿不到返回 None(调用方降级成只 WARN)。"""
818
- try:
819
- if str(HERE) not in sys.path:
820
- sys.path.insert(0, str(HERE))
821
- import stylekit # noqa: F401 (只读枚举,不复制)
822
- # 优先用公开的 STYLE_IDS(8 套),没有就退回 STYLES 的键(含内部 legacy)
823
- ids = list(getattr(stylekit, "STYLE_IDS", ()) or getattr(stylekit, "STYLES", {}).keys())
824
- return ids or None
825
- except Exception: # noqa: BLE001
826
- return None
827
-
828
-
829
804
  def _load_personas():
830
805
  """从 voicekit 单向读取 persona 名与 tone id;拿不到返回 (None, None) 降级成不查。
831
806
 
@@ -891,7 +866,7 @@ def main(argv=None):
891
866
 
892
867
  kind = _classify(data)
893
868
  rep = Report()
894
- ctx = {"styles": _load_styles(), "style_warned": False, "timeline": None}
869
+ ctx = {"timeline": None}
895
870
  skipped = False
896
871
  if kind == "artifact":
897
872
  skipped = True
@@ -911,7 +886,6 @@ def main(argv=None):
911
886
  "errors": rep.errors,
912
887
  "warnings": rep.warnings,
913
888
  "counts": {"errors": len(rep.errors), "warnings": len(rep.warnings)},
914
- "style_ids": ctx.get("styles"),
915
889
  "timeline": ctx.get("timeline"),
916
890
  }
917
891