@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,8 +1,8 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
|
-
"""sfxkit —
|
|
2
|
+
"""sfxkit — 物理建模程序化音效库。
|
|
3
3
|
|
|
4
4
|
参考项目:ai-history-film(MIT)的 sfx/ 包。本文件为**单文件重写**,API 按
|
|
5
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
2067
|
+
# 调性跟随 / 内容派生 seed(references/audio-variation.md §3.1)
|
|
2068
2068
|
# ============================================================================
|
|
2069
|
-
#
|
|
2070
|
-
# 这里给 in_key()
|
|
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
|
|
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 ->
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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")), #
|
|
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
|
-
#
|
|
3293
|
-
#
|
|
3294
|
-
#
|
|
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
|
|
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
|
|
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
|
-
#
|
|
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
|
|
217
|
+
不是旧产物;报 no-manifest 等于让全新目录的首跑刷一屏假警报。
|
|
218
218
|
"""
|
|
219
219
|
io = STAGE_IO.get(str(stage))
|
|
220
220
|
if io is None:
|