@dsh-gis/dsh-video-studio-skill 0.6.0 → 0.7.2
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/README.md +41 -16
- package/index.js +20 -7
- package/package.json +4 -2
- package/schemas/film.schema.json +75 -4
- package/skills/video-studio/SKILL.md +95 -23
- package/skills/video-studio/assets/film.json +7 -14
- package/skills/video-studio/assets/scenes.py +115 -10
- package/skills/video-studio/assets/web/engine.js +18 -1
- package/skills/video-studio/assets/web/film.js +29 -4
- package/skills/video-studio/assets/web/index.html +5 -0
- package/skills/video-studio/references/audio-variation.md +158 -0
- package/skills/video-studio/references/audio.md +67 -5
- package/skills/video-studio/references/change-impact.md +1 -1
- package/skills/video-studio/references/quality-gates.md +1 -1
- package/skills/video-studio/references/web-engine.md +1 -1
- package/skills/video-studio/requirements.txt +2 -1
- package/skills/video-studio/scripts/filmcheck.py +81 -12
- package/skills/video-studio/scripts/filmkit.py +271 -18
- package/skills/video-studio/scripts/gates.py +75 -12
- package/skills/video-studio/scripts/qa_frames.py +109 -19
- package/skills/video-studio/scripts/render.py +420 -51
- package/skills/video-studio/scripts/scorekit.py +289 -46
- package/skills/video-studio/scripts/selfcheck.py +302 -2
- package/skills/video-studio/scripts/sfxkit.py +132 -30
- package/skills/video-studio/scripts/stagewatch.py +141 -31
- package/skills/video-studio/scripts/stylekit.py +94 -6
- package/skills/video-studio/scripts/voicekit.py +8 -0
- package/skills/video-studio/scripts/webengine.py +68 -14
- package/tools/audio_similarity.py +279 -0
- package/tools/ci_check.py +145 -34
- package/tools/estimate.py +133 -54
- package/tools/selftest_noscipy.py +70 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
|
-
"""scorekit.py — 程序化配乐(v0.
|
|
2
|
+
"""scorekit.py — 程序化配乐(v0.7.0)。
|
|
3
3
|
|
|
4
4
|
用代码写一张能贯穿整部片子的配乐:调性/和弦表、段落能量、节拍网格、律动层、
|
|
5
5
|
段落过门、自动电平与"吸气",全部由 numpy + sfxkit 现场合成,不用任何采样。
|
|
@@ -33,6 +33,18 @@ v0.5 新增(配乐风格层):
|
|
|
33
33
|
style 为 None / "legacy" 时走 v0.4 原路径(逐样本一致);否则走风格化合成。
|
|
34
34
|
KEYS_MAJOR / KEYS_ALL 6 个大调 + 全部 10 个调
|
|
35
35
|
spectral_centroid(x, sr=SR) 频谱质心(量"亮度",验证风格差异用)
|
|
36
|
+
|
|
37
|
+
v0.7 新增(内容指纹变奏,references/audio-variation.md §3.3):
|
|
38
|
+
fingerprint 不再只派生音色 seed —— 同一风格换内容指纹时,结构层也稳定地变:
|
|
39
|
+
* 进行档:按指纹从 style["progressions"] 的 calm/body/drive 里另选一档
|
|
40
|
+
(整片一个偏移、climax 仍复用 drive,片内各段的进行关系保持连贯);
|
|
41
|
+
* 和弦:每两小节一次稳定替换(i<->i7 / VI<->VI7 / III<->III7 / VII<->VII7;
|
|
42
|
+
大调侧 I<->Imaj7 / IV<->IVmaj7),不覆盖乐句末属和弦与片尾主和弦;
|
|
43
|
+
* 动机:每个 (拍, 级数, 时值) 做 ±1 级数偏移(只改级数不改节奏;问句/答句/
|
|
44
|
+
片尾三种动机独立派生,跨主音的上下行用八度偏移修正);
|
|
45
|
+
* 鼓型:kick 最多加一记 ghost 落点(0.5 拍网格、不与现有落点重复),clap ±0.5 拍。
|
|
46
|
+
report["variation"] = {"chord_swaps", "motif_shifts", "drum_ghosts"} 记实际发生数。
|
|
47
|
+
style=None 的 legacy 路径不经过这些代码,仍然逐字节兼容(selfcheck 20 条)。
|
|
36
48
|
"""
|
|
37
49
|
from __future__ import annotations
|
|
38
50
|
|
|
@@ -50,7 +62,7 @@ import numpy as np
|
|
|
50
62
|
|
|
51
63
|
import sfxkit as S # 唯一音频依赖:numpy + sfxkit
|
|
52
64
|
|
|
53
|
-
__version__ = "0.
|
|
65
|
+
__version__ = "0.7.0"
|
|
54
66
|
|
|
55
67
|
SR = 48000 # 采样率(与 filmkit / sfxkit 一致)
|
|
56
68
|
BEATS_PER_BAR = 4 # 4/4
|
|
@@ -128,6 +140,11 @@ _DEGREE_TABLE = (
|
|
|
128
140
|
("V7", 7, (0, 4, 7, 10), "7"),
|
|
129
141
|
("vi7", 9, (0, 3, 7, 10), "m7"),
|
|
130
142
|
("i7", 0, (0, 3, 7, 10), "m7"),
|
|
143
|
+
# ---- v0.7:和弦替换用的七和弦色彩。同根音、同三音,只把五度后的音换成小七度;
|
|
144
|
+
# 只允许在表尾追加(前 10 项顺序被 _build_key / 老报告依赖),供 _CHORD_SWAPS 使用。 ----
|
|
145
|
+
("VI7", 8, (0, 4, 7, 10), "7"),
|
|
146
|
+
("III7", 3, (0, 4, 7, 10), "7"),
|
|
147
|
+
("VII7", 10, (0, 4, 7, 10), "7"),
|
|
131
148
|
)
|
|
132
149
|
|
|
133
150
|
# 级数名 -> 定义行(旋律生成/mood 色彩/自测校验用;只读映射,不参与 _build_key)
|
|
@@ -276,6 +293,10 @@ def _decimate_mono(x: np.ndarray, k: int = 4) -> np.ndarray:
|
|
|
276
293
|
def _moving_avg(v: np.ndarray, w: int) -> np.ndarray:
|
|
277
294
|
"""移动平均(边界复制),O(n),用来做"一阶高通/低通"的近似。"""
|
|
278
295
|
w = max(2, int(w))
|
|
296
|
+
if len(v) == 0:
|
|
297
|
+
# 空输入:np.pad(mode="edge") 对空数组直接抛 "can't extend empty axis 0",
|
|
298
|
+
# 而空数组只在 duration*sr < 4(不足 4 个采样)时出现(duration=1e-6 实测崩这)。
|
|
299
|
+
return np.zeros(0, dtype=np.float64)
|
|
279
300
|
pad = w // 2
|
|
280
301
|
vp = np.pad(v, (pad, w - pad), mode="edge")
|
|
281
302
|
c = np.cumsum(vp)
|
|
@@ -1415,7 +1436,10 @@ class Score:
|
|
|
1415
1436
|
peak = float(np.max(np.abs(master))) if n else 0.0
|
|
1416
1437
|
if peak > 1e-9:
|
|
1417
1438
|
master *= np.float32(_db_amp(TARGET_PEAK_DB) / peak)
|
|
1418
|
-
|
|
1439
|
+
# 亚采样时长(duration*sr < 1)时 master 是 (0,2):np.max 对空数组直接抛
|
|
1440
|
+
# "zero-size array to reduction operation maximum"(duration=1e-6 实测崩这)。
|
|
1441
|
+
peak_db = (float(20.0 * math.log10(max(float(np.max(np.abs(master))), 1e-12)))
|
|
1442
|
+
if n else -240.0)
|
|
1419
1443
|
lufs = _approx_lufs(master, self.sr)
|
|
1420
1444
|
meas = [{"name": s["name"], "start": round(s["start"], 3), "end": round(s["end"], 3),
|
|
1421
1445
|
"energy": round(s["energy"], 3), "gain_db": round(lv["gain"], 2),
|
|
@@ -1424,7 +1448,7 @@ class Score:
|
|
|
1424
1448
|
"rendered": True,
|
|
1425
1449
|
"peak_dbfs": round(peak_db, 2),
|
|
1426
1450
|
"lufs_approx": round(lufs, 1),
|
|
1427
|
-
"dc_max": round(float(np.max(np.abs(master.mean(axis=0)))), 9),
|
|
1451
|
+
"dc_max": round(float(np.max(np.abs(master.mean(axis=0)))), 9) if n else 0.0,
|
|
1428
1452
|
"render_s": round(time.perf_counter() - t_start, 3),
|
|
1429
1453
|
"measured_sections": meas,
|
|
1430
1454
|
}
|
|
@@ -2040,42 +2064,174 @@ def _styled_layers(pack: dict, energy: float, mood: str) -> list:
|
|
|
2040
2064
|
return out
|
|
2041
2065
|
|
|
2042
2066
|
|
|
2043
|
-
|
|
2044
|
-
|
|
2067
|
+
# ---- v0.7 内容指纹变奏(references/audio-variation.md §3.3)----------------
|
|
2068
|
+
# v0.6 及以前指纹只进 seed(只改音色细节):同风格 / 同 bpm / 同调、只换内容指纹,
|
|
2069
|
+
# 两段配乐相关性 0.966 —— 因为和弦进行 / 动机 / 鼓型全写死。
|
|
2070
|
+
# v0.7 把指纹推到"结构层":和弦做每两小节一次的稳定替换、动机级数做 ±1 稳定偏移、
|
|
2071
|
+
# 鼓型加 ghost 落点。三处决策全部走 _hash_int(FNV-1a,跨进程稳定),所以
|
|
2072
|
+
# 同指纹逐采样一致、不同指纹稳定地不同;没有引入额外随机源(变奏全部由指纹决定)。
|
|
2073
|
+
_VARIATION_SALT = "v0.7"
|
|
2074
|
+
|
|
2075
|
+
# 和弦替换对:同根音、同三音,只在"三和弦 <-> 七和弦色彩"之间换(契约 §3.3 的替代级数集)
|
|
2076
|
+
_CHORD_SWAPS = {
|
|
2077
|
+
"minor": {"i": "i7", "i7": "i", "VI": "VI7", "VI7": "VI",
|
|
2078
|
+
"III": "III7", "III7": "III", "VII": "VII7", "VII7": "VII"},
|
|
2079
|
+
"major": {"I": "Imaj7", "Imaj7": "I", "IV": "IVmaj7", "IVmaj7": "IV"},
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
# 动机级数邻接表:只走"调内级数"的上下各一级(只换级数,节奏 / 时值不动)。
|
|
2083
|
+
# 值是 ((下方级数, 八度偏移), (上方级数, 八度偏移));跨主音时八度偏移 ±1,
|
|
2084
|
+
# 否则 i 下行一级会变成高 10 个半音的 VII(旋律轮廓会翻掉)。
|
|
2085
|
+
_MOTIF_NEIGHBORS = {
|
|
2086
|
+
"minor": {
|
|
2087
|
+
"i": (("VII", -1), ("ii°", 0)), "ii°": (("i", 0), ("III", 0)),
|
|
2088
|
+
"III": (("ii°", 0), ("iv", 0)), "iv": (("III", 0), ("v", 0)),
|
|
2089
|
+
"v": (("iv", 0), ("VI", 0)), "V": (("iv", 0), ("VI", 0)),
|
|
2090
|
+
"VI": (("v", 0), ("VII", 0)), "VII": (("VI", 0), ("i", 1)),
|
|
2091
|
+
"I": (("VII", -1), ("ii°", 0)), "i7": (("VII", -1), ("ii°", 0)),
|
|
2092
|
+
},
|
|
2093
|
+
"major": {
|
|
2094
|
+
"I": (("vii", -1), ("ii", 0)), "vii": (("vi", 0), ("I", 1)),
|
|
2095
|
+
"ii": (("I", 0), ("iii", 0)), "iii": (("ii", 0), ("IV", 0)),
|
|
2096
|
+
"IV": (("iii", 0), ("V", 0)), "V": (("IV", 0), ("vi", 0)),
|
|
2097
|
+
"vi": (("V", 0), ("vii", 0)), "VI": (("V", 0), ("VII", 0)),
|
|
2098
|
+
"III": (("ii", 0), ("IV", 0)), "iv": (("iii", 0), ("V", 0)),
|
|
2099
|
+
"Imaj7": (("vii", -1), ("ii", 0)), "IVmaj7": (("iii", 0), ("V", 0)),
|
|
2100
|
+
},
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
# 进行档位(按"安静 -> 推进"排);climax 复用 drive(v0.5 语义:高潮靠鼓与层数)
|
|
2104
|
+
_PROG_TIERS = ("calm", "body", "drive")
|
|
2105
|
+
|
|
2106
|
+
|
|
2107
|
+
def _styled_mode(pack: dict) -> str:
|
|
2108
|
+
"""风格包是小调还是大调:看 progressions 的主和弦写法(i/i7 = 小调,I/Imaj7 = 大调)。
|
|
2109
|
+
|
|
2110
|
+
调式只决定"替换对 / 级数邻接表"用哪一套;真正的调性仍由 key 决定,两者不改动。
|
|
2111
|
+
"""
|
|
2112
|
+
degs = {str(d) for seq in pack["progressions"].values() for d in seq}
|
|
2113
|
+
return "minor" if (degs & {"i", "i7"}) else "major"
|
|
2114
|
+
|
|
2115
|
+
|
|
2116
|
+
def _styled_has_fp(fingerprint) -> str:
|
|
2117
|
+
"""指纹归一化:None / "" -> ""(= 关掉全部结构变奏,与 v0.5 逐字节一致)。"""
|
|
2118
|
+
if fingerprint is None:
|
|
2119
|
+
return ""
|
|
2120
|
+
s = str(fingerprint)
|
|
2121
|
+
return s if s != "" else ""
|
|
2122
|
+
|
|
2123
|
+
|
|
2124
|
+
def _styled_progression(pack: dict, tier: str, mood: str, fingerprint=None) -> tuple:
|
|
2125
|
+
"""取该 tier 的级数进行;climax 复用 drive(高潮靠鼓与层数,不靠换和弦)。
|
|
2126
|
+
|
|
2127
|
+
v0.7:给了指纹时,按指纹从该风格的 progressions 里另选一档(契约 §3.3)——
|
|
2128
|
+
同一风格的两部片可以拿到不同的和弦功能顺序,但调式 / 音色 / 鼓型 / 段落结构不变。
|
|
2129
|
+
偏移量对整部片只有一个(哈希里不带段落号),片内各段的进行关系因此仍然连贯;
|
|
2130
|
+
无指纹 -> 与 v0.5 完全同一结果(只按能量档取)。
|
|
2131
|
+
"""
|
|
2045
2132
|
progs = pack["progressions"]
|
|
2046
2133
|
key = tier if tier in progs else ("drive" if tier == "climax" else "body")
|
|
2134
|
+
fp = _styled_has_fp(fingerprint)
|
|
2135
|
+
tiers = [t for t in _PROG_TIERS if t in progs]
|
|
2136
|
+
if fp and tiers and tier != "climax": # climax 仍复用 drive(v0.5 语义:高潮靠鼓与层数)
|
|
2137
|
+
base = tiers.index(key) if key in tiers else min(1, len(tiers) - 1)
|
|
2138
|
+
off = _hash_int(_VARIATION_SALT, "tier", fp) % len(tiers)
|
|
2139
|
+
key = tiers[(base + off) % len(tiers)]
|
|
2047
2140
|
return tuple(progs[key])
|
|
2048
2141
|
|
|
2049
2142
|
|
|
2050
|
-
def
|
|
2051
|
-
|
|
2143
|
+
def _styled_chord_rules(deg: str, j: int, nb: int, ph: int, tier: str,
|
|
2144
|
+
mood: str, is_last_sec: bool) -> str:
|
|
2145
|
+
"""单小节的硬规则(与 v0.5 逐字同序):乐句末属和弦 -> mood 色彩 -> 片尾主和弦。
|
|
2146
|
+
|
|
2147
|
+
单独拆出来是为了让"变奏前 / 变奏后"各跑一遍:替换是否真的落地、有没有被
|
|
2148
|
+
硬规则吃掉(乐句末收 V、片尾落 I)都能算清楚,report 里的计数才是真数。
|
|
2149
|
+
"""
|
|
2150
|
+
# 每两乐句收一次属和弦(制造张力,与 legacy 的写法同源)
|
|
2151
|
+
if j % ph == ph - 1 and (j // ph) % 2 == 1:
|
|
2152
|
+
deg = "V"
|
|
2153
|
+
# mood 色彩:hopeful 加 maj7 / sus2;tense 用 sus4 悬留
|
|
2154
|
+
if mood == "hopeful":
|
|
2155
|
+
if deg == "I":
|
|
2156
|
+
deg = "Imaj7"
|
|
2157
|
+
elif deg == "IV":
|
|
2158
|
+
deg = "IVmaj7"
|
|
2159
|
+
elif deg == "i":
|
|
2160
|
+
deg = "i7"
|
|
2161
|
+
elif j == 0 and tier == "calm":
|
|
2162
|
+
deg = "sus2"
|
|
2163
|
+
elif mood == "tense" and j % ph == ph - 2 and deg in ("i", "I"):
|
|
2164
|
+
deg = "sus4"
|
|
2165
|
+
# 片尾落主和弦(与 legacy 同口径:I = 皮卡迪三度 / 大调主)
|
|
2166
|
+
if is_last_sec and nb >= 2 and j >= nb - 2:
|
|
2167
|
+
deg = "I"
|
|
2168
|
+
return deg
|
|
2169
|
+
|
|
2170
|
+
|
|
2171
|
+
def _styled_chords(sec: dict, si: int, secs: list, prog: tuple, pack: dict, mood: str,
|
|
2172
|
+
fingerprint=None, mode: str = "minor") -> tuple:
|
|
2173
|
+
"""一小节一个和弦:级数循环 + 乐句末收属和弦 + mood 色彩 + 片尾落 I。
|
|
2174
|
+
|
|
2175
|
+
v0.7:给了指纹时,**每两小节做一次稳定替换**(契约 §3.3):按指纹决定替换
|
|
2176
|
+
块内第 1 小节还是第 2 小节,被选中的小节在 _CHORD_SWAPS 里换成同根音的七和弦
|
|
2177
|
+
色彩(该小节级数不在替代集里就跳过)。替换先于硬规则执行,所以乐句末属和弦与
|
|
2178
|
+
片尾主和弦永远不被改坏(契约的"只在不冲突时替换")。
|
|
2179
|
+
返回 (和弦级数列表, 实际替换次数);无指纹时替换次数恒 0、列表与 v0.5 逐项一致。
|
|
2180
|
+
"""
|
|
2052
2181
|
nb = len(sec["bars"])
|
|
2053
2182
|
ph = int(pack["phrase_bars"])
|
|
2054
2183
|
tier = sec["tier"]
|
|
2055
2184
|
is_last_sec = (si == len(secs) - 1)
|
|
2185
|
+
fp = _styled_has_fp(fingerprint)
|
|
2186
|
+
smap = _CHORD_SWAPS.get(mode, {})
|
|
2056
2187
|
out = []
|
|
2188
|
+
swaps = 0
|
|
2057
2189
|
for j in range(nb):
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
if
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
if
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
deg = "i7"
|
|
2070
|
-
elif j == 0 and tier == "calm":
|
|
2071
|
-
deg = "sus2"
|
|
2072
|
-
elif mood == "tense" and j % ph == ph - 2 and deg in ("i", "I"):
|
|
2073
|
-
deg = "sus4"
|
|
2074
|
-
# 片尾落主和弦(与 legacy 同口径:I = 皮卡迪三度 / 大调主)
|
|
2075
|
-
if is_last_sec and nb >= 2 and j >= nb - 2:
|
|
2076
|
-
deg = "I"
|
|
2190
|
+
base = prog[j % len(prog)]
|
|
2191
|
+
deg = _styled_chord_rules(base, j, nb, ph, tier, mood, is_last_sec)
|
|
2192
|
+
if fp and smap:
|
|
2193
|
+
# 每两小节一次稳定替换:按指纹决定替换块内第 1 还是第 2 小节
|
|
2194
|
+
# (该小节级数不在替代集里就跳过,不硬造不存在的替代级数)
|
|
2195
|
+
pick = _hash_int(_VARIATION_SALT, "chord", fp, si, j // 2) % 2
|
|
2196
|
+
if pick == (j % 2) and base in smap:
|
|
2197
|
+
alt = _styled_chord_rules(smap[base], j, nb, ph, tier, mood, is_last_sec)
|
|
2198
|
+
if alt != deg: # 被硬规则吃掉的替换不算一次替换
|
|
2199
|
+
deg = alt
|
|
2200
|
+
swaps += 1
|
|
2077
2201
|
out.append(deg)
|
|
2078
|
-
return out
|
|
2202
|
+
return out, swaps
|
|
2203
|
+
|
|
2204
|
+
|
|
2205
|
+
def _styled_kick_ghost(cfg: dict, fp: str, si: int, j: int,
|
|
2206
|
+
fill_bar: bool, breath: bool):
|
|
2207
|
+
"""按指纹给这一小节加一个 kick ghost 落点;返回落点(拍)或 None。
|
|
2208
|
+
|
|
2209
|
+
ghost 只落在 0.5 拍网格上、不与 cfg["kick"] 现有落点重合;fill / breath 小节
|
|
2210
|
+
只允许落在 [0, 3) 拍内(那几个位置要留给鼓加花与呼吸,契约 §3.3)。
|
|
2211
|
+
每小节最多一记,1/3 的小节不加 —— 否则"每小节都多一记"会变成另一种机械。
|
|
2212
|
+
"""
|
|
2213
|
+
used = [float(x) for x in cfg.get("kick", ())]
|
|
2214
|
+
cand = []
|
|
2215
|
+
for k in range(int(round(BEATS_PER_BAR / 0.5))):
|
|
2216
|
+
pos = 0.5 * k
|
|
2217
|
+
if any(abs(pos - u) < 1e-9 for u in used):
|
|
2218
|
+
continue
|
|
2219
|
+
if (fill_bar or breath) and pos >= 3.0 - 1e-9:
|
|
2220
|
+
continue
|
|
2221
|
+
cand.append(pos)
|
|
2222
|
+
if not cand:
|
|
2223
|
+
return None
|
|
2224
|
+
h = _hash_int(_VARIATION_SALT, "ghost", fp, si, j)
|
|
2225
|
+
if h % 3 == 0:
|
|
2226
|
+
return None
|
|
2227
|
+
return cand[(h >> 3) % len(cand)]
|
|
2228
|
+
|
|
2229
|
+
|
|
2230
|
+
def _styled_clap_shift(fp: str, si: int, j: int, idx: int) -> float:
|
|
2231
|
+
"""拍手位置的指纹位移:±0.5 拍(契约 §3.3);1/3 不动,保留原律动的稳定感。"""
|
|
2232
|
+
h = _hash_int(_VARIATION_SALT, "clap", fp, si, j, idx)
|
|
2233
|
+
m = h % 3
|
|
2234
|
+
return 0.0 if m == 2 else (0.5 if m == 0 else -0.5)
|
|
2079
2235
|
|
|
2080
2236
|
|
|
2081
2237
|
def _styled_target_db(energy: float, span: float) -> float:
|
|
@@ -2140,15 +2296,55 @@ def _lead_texture(bank: "_Bank", voice: str, note: str, dur: float, bright: floa
|
|
|
2140
2296
|
return bank.bell(note, max(2.4, dur))
|
|
2141
2297
|
|
|
2142
2298
|
|
|
2143
|
-
def
|
|
2144
|
-
|
|
2145
|
-
|
|
2299
|
+
def _styled_motif_shift(motif, kind: str, fingerprint, style: str, mode: str) -> tuple:
|
|
2300
|
+
"""按指纹给动机的每个事件派生 ±1 级数偏移;返回 (事件列表, 偏移次数)。
|
|
2301
|
+
|
|
2302
|
+
只改级数不改节奏:拍位与时值原样保留。偏移方向按 "指纹 + 风格 + 动机类型 + 事件序号"
|
|
2303
|
+
哈希派生,所以问句(q) / 答句(a) / 片尾(end) **各自独立**(契约 §3.3 的"独立派生"):
|
|
2304
|
+
同一部片里同种动机稳定,两部片不会同步变形(q 上移的片子 a 未必也上移)。
|
|
2305
|
+
返回的事件是 (拍, 级数, 时值, 八度偏移):跨主音上行 / 下行时用八度偏移修正,
|
|
2306
|
+
否则"小调 i 下行一级"会变成高 10 个半音的 VII,旋律轮廓会翻掉。
|
|
2307
|
+
"""
|
|
2308
|
+
fp = _styled_has_fp(fingerprint)
|
|
2309
|
+
table = _MOTIF_NEIGHBORS.get(mode, {})
|
|
2310
|
+
if not fp or not table:
|
|
2311
|
+
return [(float(ev[0]), str(ev[1]), float(ev[2]), 0) for ev in motif], 0
|
|
2312
|
+
# 整句的偏移方向只由 "指纹 + 风格 + 动机类型" 决定(同种动机稳定),
|
|
2313
|
+
# 每个事件在±1 里取哪边由事件序号决定 —— q 与 a 的方向各自独立派生。
|
|
2314
|
+
up = _hash_int(_VARIATION_SALT, "motif-dir", fp, style, kind) % 2 == 1
|
|
2315
|
+
out = []
|
|
2316
|
+
shifted = 0
|
|
2146
2317
|
for idx, ev in enumerate(motif):
|
|
2147
|
-
|
|
2148
|
-
|
|
2318
|
+
pos, deg, dur = float(ev[0]), str(ev[1]), float(ev[2])
|
|
2319
|
+
pair = table.get(deg)
|
|
2320
|
+
h = _hash_int(_VARIATION_SALT, "motif", fp, style, kind, idx)
|
|
2321
|
+
if pair is None: # 邻接表没收录的级数不动,绝不瞎编级数
|
|
2322
|
+
out.append((pos, deg, dur, 0))
|
|
2323
|
+
continue
|
|
2324
|
+
name, oct_delta = pair[1] if (up != bool(h % 2)) else pair[0]
|
|
2325
|
+
out.append((pos, name, dur, int(oct_delta)))
|
|
2326
|
+
shifted += 1
|
|
2327
|
+
return out, shifted
|
|
2328
|
+
|
|
2329
|
+
|
|
2330
|
+
def _emit_styled_motif(bus: "_Bus", bank: "_Bank", motif, kval: dict, voice: str,
|
|
2331
|
+
t0: float, beat: float, db: float, bright: float,
|
|
2332
|
+
fingerprint=None, style: str = "", kind: str = "",
|
|
2333
|
+
mode: str = "minor") -> int:
|
|
2334
|
+
"""把风格动机铺到主奏总线:级数 -> 音名,乐器由 lead_cycle 指定。
|
|
2335
|
+
|
|
2336
|
+
v0.7:先按指纹做级数 ±1 偏移(_styled_motif_shift)再铺;返回被偏移的事件数,
|
|
2337
|
+
供 report["variation"]["motif_shifts"] 审计。无指纹时偏移数恒 0、逐音与 v0.5 相同。
|
|
2338
|
+
"""
|
|
2339
|
+
events, shifted = _styled_motif_shift(motif, kind, fingerprint, style, mode)
|
|
2340
|
+
for idx, ev in enumerate(events):
|
|
2341
|
+
pos_beats = float(ev[0])
|
|
2342
|
+
degree, dur_beats, oct_delta = str(ev[1]), float(ev[2]), int(ev[3])
|
|
2343
|
+
note = _degree_note(kval, degree, 4 + oct_delta)
|
|
2149
2344
|
tex = _lead_texture(bank, voice, note, dur_beats * beat, bright)
|
|
2150
2345
|
pan = 0.18 if idx % 2 == 0 else -0.18
|
|
2151
2346
|
bus.add(tex, t0 + pos_beats * beat, db, pan)
|
|
2347
|
+
return shifted
|
|
2152
2348
|
|
|
2153
2349
|
|
|
2154
2350
|
def _styled_gate_env(n: int, secs: list, present: list, bar: float, sr: int = SR) -> np.ndarray:
|
|
@@ -2264,7 +2460,10 @@ def render_styled(duration, *, style, bpm=None, key=None, mood=None, anchors=Non
|
|
|
2264
2460
|
* 复用老 _Bank 的鼓组与 _Bus 总线,只新增音色方法,不重写任何老乐器;
|
|
2265
2461
|
* 老路径(render(style=None) / "legacy")完全不受影响,逐样本与 v0.4 相同。
|
|
2266
2462
|
|
|
2267
|
-
anchors / sections 语义与 render() 一致;mood 见 MOOD_IDS
|
|
2463
|
+
anchors / sections 语义与 render() 一致;mood 见 MOOD_IDS。
|
|
2464
|
+
fingerprint(v0.7):参与 seed 派生 + 结构层变奏(契约 §3.3)——和弦每两小节一次
|
|
2465
|
+
七和弦色彩替换、按指纹选进行档、动机级数 ±1 稳定偏移、鼓型 ghost 落点 / clap 位移。
|
|
2466
|
+
None / "" 时三处变奏全部关闭,逐样本与 v0.5 相同;报告里的 variation 是这三项的计数。
|
|
2268
2467
|
|
|
2269
2468
|
music_plan:stylekit.resolve_script_style(script) 的 "music" 子字典(整份 plan 也接受)。
|
|
2270
2469
|
非 None 时 bpm / key / mood / seed 以它为准(优先级高于显式参数)——render.py 只需
|
|
@@ -2284,6 +2483,9 @@ def render_styled(duration, *, style, bpm=None, key=None, mood=None, anchors=Non
|
|
|
2284
2483
|
f"bpm 必须是有限正数,收到 {bpm!r}")
|
|
2285
2484
|
_require(key in KEYS_ALL, f"未知调性 {key!r};可选:{'/'.join(KEYS_ALL)}")
|
|
2286
2485
|
pack = MUSIC_STYLES[style]
|
|
2486
|
+
# v0.7:调式决定和弦替换对 / 级数邻接表用哪一套;fp_s 为空 = 全部结构变奏关闭
|
|
2487
|
+
mode = _styled_mode(pack)
|
|
2488
|
+
fp_s = _styled_has_fp(fingerprint)
|
|
2287
2489
|
dur = float(duration)
|
|
2288
2490
|
sr = SR
|
|
2289
2491
|
sc = Score(duration, bpm=float(bpm), key=key, seed=int(seed))
|
|
@@ -2309,13 +2511,20 @@ def render_styled(duration, *, style, bpm=None, key=None, mood=None, anchors=Non
|
|
|
2309
2511
|
space = pack["space"]
|
|
2310
2512
|
|
|
2311
2513
|
# ---- 1) 段落规划:tier / 层 / 和弦 / 电平目标 ----
|
|
2514
|
+
# v0.7:指纹变奏计数(chord_swaps / motif_shifts / drum_ghosts)进 report["variation"]。
|
|
2515
|
+
# 无指纹时三处变奏全关、计数恒 0,和弦仍走 v0.5 那条"只按能量档取进行"的分支。
|
|
2516
|
+
chord_swaps = 0
|
|
2517
|
+
motif_shifts = 0
|
|
2518
|
+
drum_ghosts = 0
|
|
2312
2519
|
for si, sec in enumerate(secs):
|
|
2313
2520
|
e = sec["energy"]
|
|
2314
2521
|
tier = _styled_tier(e)
|
|
2315
2522
|
sec["tier"] = tier
|
|
2316
2523
|
sec["layers"] = _styled_layers(pack, e, mood)
|
|
2317
|
-
|
|
2318
|
-
|
|
2524
|
+
prog = _styled_progression(pack, tier, mood, fingerprint)
|
|
2525
|
+
sec["chords"], n_swap = _styled_chords(sec, si, secs, prog, pack, mood,
|
|
2526
|
+
fingerprint, mode)
|
|
2527
|
+
chord_swaps += n_swap
|
|
2319
2528
|
sec["target_db"] = round(_styled_target_db(e, span), 2)
|
|
2320
2529
|
sec["target_lufs"] = round(BASE_LUFS + sec["target_db"], 1)
|
|
2321
2530
|
sec["breath"] = {"calm": 2, "body": 1, "drive": 0, "climax": 0}[tier]
|
|
@@ -2353,6 +2562,14 @@ def render_styled(duration, *, style, bpm=None, key=None, mood=None, anchors=Non
|
|
|
2353
2562
|
var = bank.kicks[(bi * 4 + int(round(kb))) % len(bank.kicks)]
|
|
2354
2563
|
drums.add(var, t0 + kb * beat, kick_db + accent)
|
|
2355
2564
|
hits.append((t0 + kb * beat, 0.28 + 0.22 * e))
|
|
2565
|
+
# v0.7:指纹再塞一记 ghost kick(0.5 拍网格、不与现有落点重复;见 _styled_kick_ghost)
|
|
2566
|
+
if fp_s:
|
|
2567
|
+
ghost_pos = _styled_kick_ghost(cfg, fp_s, si, j, fill_bar, breath)
|
|
2568
|
+
if ghost_pos is not None:
|
|
2569
|
+
gidx = (bi * 4 + int(round(ghost_pos * 2)) + 2) % len(bank.kicks)
|
|
2570
|
+
drums.add(bank.kicks[gidx], t0 + ghost_pos * beat, kick_db - 5.5)
|
|
2571
|
+
hits.append((t0 + ghost_pos * beat, 0.12 + 0.12 * e))
|
|
2572
|
+
drum_ghosts += 1
|
|
2356
2573
|
if has_hat and cfg.get("hat"):
|
|
2357
2574
|
step = float(cfg["hat"])
|
|
2358
2575
|
total = int(round(BEATS_PER_BAR / step))
|
|
@@ -2369,11 +2586,17 @@ def render_styled(duration, *, style, bpm=None, key=None, mood=None, anchors=Non
|
|
|
2369
2586
|
var = bank.hats[(bi * 7 + k) % len(bank.hats)]
|
|
2370
2587
|
drums.add(var, t0 + pos * beat, hat_db + ghost, pan)
|
|
2371
2588
|
if has_hat and e >= 0.50 and cfg.get("clap"):
|
|
2372
|
-
for cb in cfg.get("clap", ()):
|
|
2589
|
+
for cidx, cb in enumerate(cfg.get("clap", ())):
|
|
2373
2590
|
if phrase_end and cb >= 3.0:
|
|
2374
2591
|
continue
|
|
2375
|
-
|
|
2376
|
-
|
|
2592
|
+
pos = float(cb)
|
|
2593
|
+
if fp_s:
|
|
2594
|
+
shift = _styled_clap_shift(fp_s, si, j, cidx)
|
|
2595
|
+
if shift and 0.0 <= pos + shift < float(BEATS_PER_BAR):
|
|
2596
|
+
pos += shift
|
|
2597
|
+
drum_ghosts += 1
|
|
2598
|
+
drums.add(bank.claps[(bi * 2 + int(round(pos))) % len(bank.claps)],
|
|
2599
|
+
t0 + pos * beat, -16.0 + 4.0 * e - 1.5 * float(m["drum"]), 0.08)
|
|
2377
2600
|
if tier == "climax" and has_hat:
|
|
2378
2601
|
for pos in (0.5, 1.5, 2.5, 3.5):
|
|
2379
2602
|
if phrase_end and pos >= 3.5:
|
|
@@ -2535,20 +2758,24 @@ def render_styled(duration, *, style, bpm=None, key=None, mood=None, anchors=Non
|
|
|
2535
2758
|
for pi, first in enumerate(range(0, nb, ph)):
|
|
2536
2759
|
is_last_phrase = (si == last_si and pi == (nb - 1) // ph)
|
|
2537
2760
|
if is_last_phrase:
|
|
2538
|
-
motif = pack["motif"]["end"]
|
|
2761
|
+
kind, motif = "end", pack["motif"]["end"]
|
|
2539
2762
|
elif phrase_i % 2 == 0:
|
|
2540
|
-
motif = pack["motif"]["q"]
|
|
2763
|
+
kind, motif = "q", pack["motif"]["q"]
|
|
2541
2764
|
else:
|
|
2542
|
-
motif = pack["motif"]["a"]
|
|
2765
|
+
kind, motif = "a", pack["motif"]["a"]
|
|
2543
2766
|
voice = cycle[phrase_i % len(cycle)]
|
|
2544
2767
|
t0 = sec["bars"][first] * bar
|
|
2545
|
-
_emit_styled_motif(
|
|
2768
|
+
motif_shifts += _emit_styled_motif(
|
|
2769
|
+
lead, bank, motif, kval, voice, t0, beat, mel_db, bright,
|
|
2770
|
+
fingerprint=fingerprint, style=style, kind=kind, mode=mode)
|
|
2546
2771
|
phrase_i += 1
|
|
2547
2772
|
elif si == last_si and e >= 0.22:
|
|
2548
2773
|
# 片尾安静段:用第一个主奏音色把结局动机答完
|
|
2549
2774
|
t_last = sec["bars"][0] * bar + max(0.0, (nb - 2) * bar)
|
|
2550
|
-
_emit_styled_motif(
|
|
2551
|
-
|
|
2775
|
+
motif_shifts += _emit_styled_motif(
|
|
2776
|
+
lead, bank, pack["motif"]["end"], kval, cycle[0],
|
|
2777
|
+
t_last, beat, -19.0 + float(m["lead_db"]), bright,
|
|
2778
|
+
fingerprint=fingerprint, style=style, kind="end", mode=mode)
|
|
2552
2779
|
lead.buf *= _styled_gate_env(n, secs, lead_pres, bar, sr)[:, None]
|
|
2553
2780
|
master += lead.buf
|
|
2554
2781
|
sc._add_delayed(master, lead.buf, float(space["delay_beats"]) * beat * 0.85,
|
|
@@ -2630,6 +2857,10 @@ def render_styled(duration, *, style, bpm=None, key=None, mood=None, anchors=Non
|
|
|
2630
2857
|
"seed": int(sc.seed),
|
|
2631
2858
|
"seed_effective": int(seed_eff),
|
|
2632
2859
|
"fingerprint": "" if fingerprint is None else str(fingerprint),
|
|
2860
|
+
# v0.7:指纹变奏的实际发生数(审计用;无指纹时三项目全 0)
|
|
2861
|
+
"variation": {"chord_swaps": int(chord_swaps),
|
|
2862
|
+
"motif_shifts": int(motif_shifts),
|
|
2863
|
+
"drum_ghosts": int(drum_ghosts)},
|
|
2633
2864
|
"bar_s": round(bar, 4),
|
|
2634
2865
|
"beat_s": round(beat, 4),
|
|
2635
2866
|
"phrase_bars": ph,
|
|
@@ -2901,6 +3132,12 @@ def save(path, x, sr: int = SR) -> Path:
|
|
|
2901
3132
|
"""把配乐写成 24-bit PCM WAV(通道数自适应;峰值由 render 保证 ≤ -1 dBFS)。"""
|
|
2902
3133
|
path = Path(path)
|
|
2903
3134
|
arr = np.asarray(x)
|
|
3135
|
+
if arr.size and not np.all(np.isfinite(arr)):
|
|
3136
|
+
# NaN 经 np.rint(...).astype(np.int32) 会落到 INT_MIN(不是 0),24-bit 写出后是
|
|
3137
|
+
# 满幅负样本 —— 成片里一声爆响,且没有任何报错。与 filmkit.wav_write 同口径:报错。
|
|
3138
|
+
bad = int(arr.size - np.count_nonzero(np.isfinite(arr)))
|
|
3139
|
+
raise ValueError("scorekit.save: 采样里有 %d 个 NaN/Inf(%s)——配乐渲染有数值错误,"
|
|
3140
|
+
"拒绝写出损坏的 WAV" % (bad, path))
|
|
2904
3141
|
if arr.ndim == 1:
|
|
2905
3142
|
arr = np.stack([arr, arr], axis=1)
|
|
2906
3143
|
_require(arr.ndim == 2 and arr.shape[1] in (1, 2), f"只支持单/双声道,收到 shape={arr.shape}")
|
|
@@ -3406,6 +3643,11 @@ def _format_report(rep: dict, wav_path: Path | None, report_path: Path | None) -
|
|
|
3406
3643
|
f" / 情绪 {rep.get('mood', 'steady')} / 有效 seed {rep.get('seed_effective', rep['seed'])}"
|
|
3407
3644
|
f" / 频谱质心 {rep.get('spectral_centroid_hz', 0):.0f} Hz")
|
|
3408
3645
|
lines.append(f" 段落 {len(rep['sections'])} 个,交界 {len(rep['transitions'])} 处,吸气 {len(rep['sucks'])} 处")
|
|
3646
|
+
var = rep.get("variation") or {}
|
|
3647
|
+
if any(int(var.get(k, 0) or 0) for k in ("chord_swaps", "motif_shifts", "drum_ghosts")):
|
|
3648
|
+
lines.append(" 指纹变奏:和弦替换 %d 处 / 动机偏移 %d 音 / 鼓型 ghost %d 处"
|
|
3649
|
+
% (int(var.get("chord_swaps", 0)), int(var.get("motif_shifts", 0)),
|
|
3650
|
+
int(var.get("drum_ghosts", 0))))
|
|
3409
3651
|
for s in rep["sections"]:
|
|
3410
3652
|
lines.append(f" {s['name']:<8} {s['start']:7.2f} – {s['end']:7.2f} s"
|
|
3411
3653
|
f" energy {s['energy']:.2f} 目标 {s['target_lufs']:.1f} LUFS"
|
|
@@ -3434,7 +3676,8 @@ def main(argv=None) -> int:
|
|
|
3434
3676
|
ap.add_argument("--style", default=None,
|
|
3435
3677
|
help="配乐风格:" + "/".join(MUSIC_STYLE_IDS) + "(缺省 = legacy,与 v0.4 逐样本一致)")
|
|
3436
3678
|
ap.add_argument("--mood", default="steady", help="情绪修饰:" + "/".join(MOOD_IDS))
|
|
3437
|
-
ap.add_argument("--fingerprint", default=None,
|
|
3679
|
+
ap.add_argument("--fingerprint", default=None,
|
|
3680
|
+
help="内容指纹(v0.7:参与和声/动机/鼓型变奏;只影响 style 路径,legacy 忽略)")
|
|
3438
3681
|
ap.add_argument("--bpm", type=float, default=None,
|
|
3439
3682
|
help="BPM(缺省:legacy 120;风格路径取该风格 bpm_hint 的中位)")
|
|
3440
3683
|
ap.add_argument("--key", default=None,
|