@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,8 +1,8 @@
1
1
  # -*- coding: utf-8 -*-
2
- """sfxkit — 物理建模程序化音效库(v0.3.0)。
2
+ """sfxkit — 物理建模程序化音效库。
3
3
 
4
4
  参考项目:ai-history-film(MIT)的 sfx/ 包。本文件为**单文件重写**,API 按
5
- v0.3.0 协作契约 §5.1 冻结;算法思想(模态合成、泊松/Cox 纹理、镜像声源混响、
5
+ 协作契约 §5.1 冻结;算法思想(模态合成、泊松/Cox 纹理、镜像声源混响、
6
6
  BS.1770 响度、块级动态处理)改编自 ai-history-film(MIT),实现细节针对本项目
7
7
  "确定性、无 scipy 也能跑、长片也能跑"三条约束重写,未整目录拷贝。
8
8
 
@@ -35,7 +35,7 @@ try: # scipy 可选:没装就走 FFT 卷积
35
35
  except Exception: # pragma: no cover - 兜底路径
36
36
  _sig = None
37
37
 
38
- __version__ = "0.3.0"
38
+ __version__ = "1.0.4"
39
39
 
40
40
  SR = 48000
41
41
  LN1000 = 6.907755278982137 # ln(1000):幅度衰减 60 dB 对应的自然对数
@@ -788,10 +788,10 @@ def _resonator_ir(f: float, t60: float, amp: float, n_max: int,
788
788
 
789
789
  为什么不复用通用的 _iir_ir():无 scipy 时 resonator 也要走 FFT 卷积,而金属/
790
790
  玻璃模态的 t60 可达 5~7 s(见 MATERIALS 的 eta 量级),-80 dB 衰减长度约
791
- 1.33·t60·sr ≈ 40 万采样。旧的 _iir_ir(cap=20000) 在 0.42 s 处把 IR 砍成 0:
791
+ 1.33·t60·sr ≈ 40 万采样。固定 cap=20000 的通用 IR 在 0.42 s 处就把 IR 砍成 0:
792
792
  截断点在 -25 dB 上直接跳变,听感是长尾里多一下“咔”,且与 scipy 路径的
793
793
  产物长度/尾音全不同(references/sfx-design.md §12 要求静态滤波与 scipy
794
- 相对误差 ≤ 2e-4,实测旧兜底最差 -8 dB)。这个二阶系统的 IR 有闭式解,
794
+ 相对误差 ≤ 2e-4,实测这类截断最差 -8 dB)。这个二阶系统的 IR 有闭式解,
795
795
  一次向量化算完,比逐样本 Python 递推快两三个数量级。
796
796
  长度取 min(n_max, -80 dB 衰减长度):输出只取前 n_max 个采样,h 超过 n_max
797
797
  的部分不可能影响这些采样,所以这个截断是精确的、不是近似。
@@ -801,7 +801,7 @@ def _resonator_ir(f: float, t60: float, amp: float, n_max: int,
801
801
  return np.zeros(0)
802
802
  a1 = math.exp(-LN1000 / (max(float(t60), 1e-5) * float(sr)))
803
803
  w = 2.0 * np.pi * float(f) / float(sr)
804
- # 借 _ir_len() 的极点半径估计(它已处理不衰减/数值退化),保证与旧路径同口径
804
+ # 借 _ir_len() 的极点半径估计(它已处理不衰减/数值退化),保证与通用 IR 同口径
805
805
  a = np.array([1.0, -2.0 * a1 * math.cos(w), a1 * a1])
806
806
  b = np.array([0.0, float(amp) * a1 * math.sin(w)])
807
807
  L = int(_ir_len(b, a, cap=n_max))
@@ -1028,7 +1028,7 @@ class Body:
1028
1028
  if not events:
1029
1029
  return y
1030
1030
  if _sig is not None:
1031
- # 有 scipy:逐模态 lfilter(保持 v0.7 及以前的产物逐样本不变——用户成片不出新声音)
1031
+ # 有 scipy:逐模态 lfilter(产物逐样本不变——换实现不改用户成片的声音)
1032
1032
  exc = np.zeros(n)
1033
1033
  for i, (f, t60) in enumerate(zip(self.freqs, self.t60s)):
1034
1034
  exc[:] = 0.0
@@ -2064,10 +2064,10 @@ CUE_ALIASES = {
2064
2064
  }
2065
2065
 
2066
2066
  # ============================================================================
2067
- # 调性跟随 / 内容派生 seed(v0.7,references/audio-variation.md §3.1)
2067
+ # 调性跟随 / 内容派生 seed(references/audio-variation.md §3.1)
2068
2068
  # ============================================================================
2069
- # 提示音以前硬编码 D 小调五声:配乐一旦选到 C / Am 等调,提示音就"跑调"。
2070
- # 这里给 in_key() 加可选的 key 参数,key=None 时逐音保持 v0.6 的 D 小调五声
2069
+ # 提示音必须能跟随配乐调性:配乐选到 C / Am 等调时,硬编码 D 小调五声会"跑调"。
2070
+ # 这里给 in_key() 可选的 key 参数,key=None 时逐音保持 D 小调五声
2071
2071
  # (自检/legacy 逐样本兼容),C 路(CueBus.build_audio)再把配乐调性透传下来。
2072
2072
  #
2073
2073
  # 调名表与 scorekit.KEYS_ALL 对齐(4 个小调 + 6 个大调),不另发明一套写法:
@@ -2086,7 +2086,7 @@ def _penta(key="Dm"):
2086
2086
  """把某一调的五声音阶算成 [Hz, ...](从小到大,MIDI 24~107)。
2087
2087
 
2088
2088
  小调用自然小调五声、大调用大调五声;结果按调名缓存(纯 numpy,无随机)。
2089
- key 缺省 / None 仍是 D 小调五声:与 v0.6 的数组逐音一致,legacy 逐样本不变。
2089
+ key 缺省 / None 仍是 D 小调五声:与硬编码表逐音一致,legacy 逐样本不变。
2090
2090
  """
2091
2091
  name = "Dm" if key is None else str(key)
2092
2092
  got = _PENTA_CACHE.get(name)
@@ -2107,7 +2107,7 @@ def _penta(key="Dm"):
2107
2107
  def in_key(freq: float, key: str | None = None) -> float:
2108
2108
  """把任意频率吸附到最近的五声音阶音(与配乐同调)。
2109
2109
 
2110
- key=None -> v0.6 的旧行为:硬编码 D 小调五声(D F G A C),逐样本兼容。
2110
+ key=None -> 硬编码 D 小调五声(D F G A C),逐样本兼容。
2111
2111
  key 给 "Dm"/"Am"/"Cm"/"Em"(自然小调五声)或 "C"/"G"/"D"/"F"/"A"/"Bb"
2112
2112
  (大调五声)-> 吸附到该调;未知调名 raise ValueError,不静默降级(与 _enum 同口径)。
2113
2113
  """
@@ -2119,11 +2119,12 @@ def in_key(freq: float, key: str | None = None) -> float:
2119
2119
 
2120
2120
 
2121
2121
  def content_seed(base: int, style: str, content_fp: str, recipe: str, t: float) -> int:
2122
- """内容派生 seed(v0.7 契约 §3.1):同片同 cue 稳定,换片同一位置也换种子。
2122
+ """内容派生 seed(契约 §3.1):同片同 cue 稳定,换片同一位置也换种子。
2123
2123
 
2124
2124
  公式(与 filmkit.content_seed 逐字一致,两边同参必须同值):
2125
2125
  sha1("v0.7|<base>|<style>|<content_fp>|<recipe>|<t 三位小数>") -> 前 8 位 hex
2126
2126
  -> int(h[:8], 16) % 9000 + 1000(32 位非负 int,落在 1000~9999)。
2127
+ (盐里的 "v0.7|" 是冻结字面量:改它等于给全部音效换种子,也与 filmkit 对不上。)
2127
2128
 
2128
2129
  纯 stdlib(hashlib):同进程/跨进程/跨机器同参数同结果,不受 numpy 随机源影响。
2129
2130
  为什么不用 seed_base + i:那样只跟 cue 的**出现顺序**有关,两部内容完全不同的片子
@@ -2139,9 +2140,9 @@ def content_seed(base: int, style: str, content_fp: str, recipe: str, t: float)
2139
2140
  def _enum(value, allowed, where: str, name: str = "值") -> str:
2140
2141
  """严格校验语义枚举参数:未知值立刻报错并列出可用值,不静默落到默认分支。
2141
2142
 
2142
- 历史上 rain(surface=) 严格报错,wind(env=)/ui_blip(kind=)/text_stream(voice=)
2143
- 却把写错的值悄悄当成默认分支——同一个"看起来像开关"的参数,行为却一个炸一个
2144
- 不吭声。统一成严格报错(本仓库的规矩:可以保守,但不能静默降级)。
2143
+ 同一个"看起来像开关"的参数不能有的一炸一个不吭声:rain(surface=) / wind(env=) /
2144
+ ui_blip(kind=) / text_stream(voice=) 一律严格报错,写错的值绝不悄悄当成默认分支
2145
+ (本仓库的规矩:可以保守,但不能静默降级)。
2145
2146
  """
2146
2147
  v = str(value)
2147
2148
  if v not in allowed:
@@ -2326,7 +2327,7 @@ def recipe(name=None, *, tags=(), master=None):
2326
2327
  def ui_blip(seed=0, pitch=2200.0, kind="tick", dur=0.22, **kw):
2327
2328
  """极短的"嘀":玻璃/金属的敲击,音高吸附五声音阶。
2328
2329
 
2329
- v0.7:key 从 **kw 透传(C 路把配乐调性放进来)——None/缺省 = 旧行为(D 小调五声),
2330
+ key 从 **kw 透传(C 路把配乐调性放进来)——None/缺省 = D 小调五声,
2330
2331
  给 "Dm"/"C"/"Am" 等则吸附到该调,未知调名 raise ValueError(见 in_key)。
2331
2332
  """
2332
2333
  kind = _enum(kind, ("tick", "ping"), "ui_blip", "kind")
@@ -2378,7 +2379,7 @@ def relay_click(seed=0, count=1, gap=0.18, **kw):
2378
2379
  def roulette_tick(seed=0, last=False, **kw):
2379
2380
  """转盘抽签的木质"咔嗒":硬接触的木块,last=True 时更重、更低。
2380
2381
 
2381
- v0.7:给了 key 时把木块基频吸附到该调(提示音跟配乐同调);key=None 走旧公式,
2382
+ 给了 key 时把木块基频吸附到该调(提示音跟配乐同调);key=None 走不吸附的公式,
2382
2383
  随机序列与取样点都不变 -> legacy 逐样本兼容。
2383
2384
  """
2384
2385
  g = rng(seed)
@@ -2398,7 +2399,7 @@ def roulette_tick(seed=0, last=False, **kw):
2398
2399
  def answer_chime(seed=0, note=1174.66, grace=880.0, grace_lead=0.07, **kw):
2399
2400
  """"猜中了"的钟琴:一个装饰音 + 一个主音,都在五声音阶上。
2400
2401
 
2401
- v0.7:key 从 **kw 透传(None = 旧行为 D 小调五声;给调名则整条钟琴跟随配乐调性)。
2402
+ key 从 **kw 透传(None = D 小调五声;给调名则整条钟琴跟随配乐调性)。
2402
2403
  """
2403
2404
  key = kw.get("key")
2404
2405
  x = _bus(1.6, stereo=False)
@@ -2454,7 +2455,7 @@ def typing_at(seed=0, times=(0.0, 0.12, 0.25, 0.4), spaces=(), **kw):
2454
2455
  def text_stream(seed=0, times=(0.0, 0.04, 0.08, 0.12), base=1174.7, voice="tone", **kw):
2455
2456
  """AI 逐字输出:比打字更连贯。voice="tone" 是带音高的五声颗粒;"soft" 是不带音高的沙沙。
2456
2457
 
2457
- v0.7:voice="tone" 的音高跟随 **kw 里的 key(None = 旧行为 D 小调五声)。
2458
+ voice="tone" 的音高跟随 **kw 里的 key(None = D 小调五声)。
2458
2459
  """
2459
2460
  voice = _enum(voice, ("tone", "soft"), "text_stream", "voice")
2460
2461
  ts = _times(times, "text_stream")
@@ -2478,7 +2479,7 @@ def text_stream(seed=0, times=(0.0, 0.04, 0.08, 0.12), base=1174.7, voice="tone"
2478
2479
  def data_chirp(seed=0, dur=0.8, rate=28.0, base=1800.0, **kw):
2479
2480
  """数据流:一串很快的、带音高的短脉冲,频率在五声上随机跳动。
2480
2481
 
2481
- v0.7:音高跟随 **kw 里的 key(None = 旧行为 D 小调五声)。
2482
+ 音高跟随 **kw 里的 key(None = D 小调五声)。
2482
2483
  """
2483
2484
  r = float(rate)
2484
2485
  # rate 是"每秒脉冲数",必须为正:rate=0 会在下面 i / r 抛 ZeroDivisionError(内部
@@ -3105,7 +3106,7 @@ def selftest() -> int:
3105
3106
  b = render(k, seed=11, dur=3.0) if k == "rain" else render(k, seed=11)
3106
3107
  if not np.array_equal(np.asarray(a), np.asarray(b)):
3107
3108
  raise ValueError("%s 同 seed 两次渲染不一致" % k)
3108
- # v0.7 契约 §3.1:key=None 与 key="Dm" 必须逐样本一致(旧路径兼容);
3109
+ # 契约 §3.1:key=None 与 key="Dm" 必须逐样本一致(legacy 路径兼容);
3109
3110
  # key 给别的调必须真的换音(用 F4:Dm 五声里有 F、C 大调五声里没有,一定吸附到 E4)。
3110
3111
  old = np.asarray(render("ui_blip", seed=11))
3111
3112
  dm = np.asarray(render("ui_blip", seed=11, key="Dm"))
@@ -3205,7 +3206,7 @@ def selftest() -> int:
3205
3206
  ("metal_hit", dict(obj="bogus")),
3206
3207
  ("relay_click", dict(gap=-1.0, count=3)),
3207
3208
  ("typing_at", dict(times=[-1.0])),
3208
- ("ui_blip", dict(key="bogus")), # v0.7:未知调名必须响亮报错
3209
+ ("ui_blip", dict(key="bogus")), # 未知调名必须响亮报错
3209
3210
  ("answer_chime", dict(key="bogus")))
3210
3211
  for rname, kw in bad:
3211
3212
  try:
@@ -3289,9 +3290,9 @@ def selftest() -> int:
3289
3290
  return "环境声响度贴合 master(±1.5 LU)"
3290
3291
 
3291
3292
  def t_fallback():
3292
- # v0.8:这条以前只查"输出是有限值"——等于只验了"没崩",兜底路径的幅度
3293
- # 正确性完全没查(references/sfx-design.md §12 承诺静态滤波与 scipy 相对
3294
- # 误差 ≤ 2e-4)。现在按当前环境选参照:
3293
+ # 这条不能只查"输出是有限值"——那等于只验了"没崩",兜底路径的幅度正确性
3294
+ # 完全没查(references/sfx-design.md §12 承诺静态滤波与 scipy 相对误差 ≤ 2e-4)。
3295
+ # 所以按当前环境选参照:
3295
3296
  # 有 scipy:同一函数跑两遍(scipy 路径 vs 兜底路径)逐样本比 —— 直接验"等价";
3296
3297
  # 无 scipy:没有 scipy 路径可比,改用**解析参照**(双二阶的阶跃稳态/频响、
3297
3298
  # 谐振器的闭式冲激响应 amp·r^n·sin(ωn)),这样 CI 的 noscipy 任务
@@ -1,5 +1,5 @@
1
1
  # -*- coding: utf-8 -*-
2
- """stagewatch.py — 阶段新鲜度(stale)守卫(v0.6.0)。
2
+ """stagewatch.py — 阶段新鲜度(stale)守卫。
3
3
 
4
4
  解决的是"改了一句台词,旧产物还在用,但不报错"这一类事故。
5
5
 
@@ -38,12 +38,12 @@ BIG_BYTES = 8 << 20 # > 8 MB 走 size+mtime 快路径
38
38
  # 而那正是最需要它报警的场景(旧 tts 配新剧本 = 配音和字幕全错位)。
39
39
  STAGE_IO = {
40
40
  # voice_plan.json 是 **tts** 阶段写的(render.py 的 st_tts:非 legacy 才写),
41
- # 不是 style 写的——v0.7.2 前它被挂在 style.outputs 下,于是单跑 `--stage style`
41
+ # 不是 style 写的——挂错到 style.outputs 下,单跑 `--stage style`
42
42
  # 之后每次重跑都报「产物 voice_plan.json 不见了」(其实从来没写过),
43
43
  # 开了 --strict-stale 就直接失败,而 --stage style 是全新项目的第一条生产命令。
44
44
  # legacy 剧本本来就不写这个文件:check() 只在「上次记录的是具体指纹、现在没了」
45
45
  # 时才报 output-missing,所以 legacy 下记为 missing 不会被当成永久过时。
46
- # v1.0 提案层:proposal.json / design.json 由 propose.py 写(不经 render.py),
46
+ # 提案层:proposal.json / design.json 由 propose.py 写(不经 render.py),
47
47
  # 但**style 与 frames 都必须读它**——所以挂在 style 的 sources 里当"源头输入",
48
48
  # 并在 frames 的 inputs 里再要一次 design.json(改设计 = 画面必须重渲)。
49
49
  # 为什么放 sources 而不是 inputs:它不是 render.py 的任何 stage 产出的,
@@ -214,7 +214,7 @@ def check(work, stage, sources=None, self_built=False, pending=(), _seen=None):
214
214
 
215
215
  self_built: 这些输入是**本次运行自己刚写出来的**(render.py --stage all 跑在
216
216
  一个刚建的 work/ 上)。此时"manifest 没有本阶段记录"只是因为我们还没跑到它,
217
- 不是旧产物;报 no-manifest 等于让全新目录的首跑刷一屏假警报(v0.7.2 修)。
217
+ 不是旧产物;报 no-manifest 等于让全新目录的首跑刷一屏假警报。
218
218
  """
219
219
  io = STAGE_IO.get(str(stage))
220
220
  if io is None: