@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,23 +1,23 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
"""voicekit.py — 旁白声线:直接给 edge voice id + 语气档 + 响度补偿表。
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
filmkit.tts_batch
|
|
6
|
-
|
|
4
|
+
旁白不能全片一个人、一个语气念完:声线、语速、音高、音量与前后留白都要能逐句给,
|
|
5
|
+
并且随内容变化。只把 script.voice / rate / pitch 三个单值传给 filmkit.tts_batch,
|
|
6
|
+
全片就是同一个人用同一种语气念到底(听感即「旁白声音都差不多」)。
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
zh-CN-YunyangNeural
|
|
13
|
-
|
|
8
|
+
声线只认裸 edge voice id
|
|
9
|
+
------------------------
|
|
10
|
+
声线一律写 edge voice id(zh-CN-YunxiNeural),映射只发生一次、就在设计里。
|
|
11
|
+
多一层 persona 身份映射(写 narrator_male、真正下发给 edge-tts 的却是
|
|
12
|
+
zh-CN-YunyangNeural)时,任何一层没对齐,表现都是「配了却没生效」,
|
|
13
|
+
而且要等听成片才发现。
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
身份是数据,不该由一个正则或一张表决定:没有 persona 表,也没有
|
|
16
|
+
「文本里有引号就偷偷换人」那套规则。
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
每把音色的实测响度补偿**不能丢**:不补补偿,换音色就要人肉重新对电平,
|
|
19
|
+
正是要根除的「改了不生效 / 要人调」。所以补偿独立成一张按 voice id 的数据表
|
|
20
|
+
VOICE_LOUDNESS(没有任何身份含义)。
|
|
21
21
|
|
|
22
22
|
本模块只做「策展 + 计划」:给出每句用哪个音色、什么语气、语速/音高/音量抖多少、
|
|
23
23
|
前后留白多少。真正的合成仍在 filmkit.tts(Lead 负责接线),因此本模块
|
|
@@ -33,24 +33,23 @@ zh-CN-YunyangNeural。任何一层没对齐,表现都是「配了却没生效
|
|
|
33
33
|
声线从哪来(高到低)
|
|
34
34
|
--------------------
|
|
35
35
|
lines[].voice(显式 edge id)
|
|
36
|
-
> script.voice.map / design.audio.voice.map 的 kind
|
|
36
|
+
> script.voice.map / design.audio.voice.map 的 kind 映射
|
|
37
37
|
> style_plan["voice"]["map"][kind](风格包 / designkit.design_to_plan)
|
|
38
38
|
> script.voice.id / design.audio.voice.id / style_plan["voice"]["default"](主声线)
|
|
39
|
-
|
|
39
|
+
没有任何来源就报错(并给出迁移路径)——设计必须明确「谁在说话」。
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
声线策略里的 max_distinct 随之退休 —— 想少换人就少写几个 kind,不需要中间层猜意图。
|
|
41
|
+
声线一旦确定就**不再治理**:map 怎么写就怎么念,没有中间层在段边界偷偷换人、
|
|
42
|
+
也没有按预算限制同一部片出现几把声音。想少换人就少写几个 kind,不需要中间层猜意图。
|
|
44
43
|
|
|
45
44
|
确定性
|
|
46
45
|
------
|
|
47
46
|
所有抖动都来自 sha1(title|id|voice),禁 random / time / hash()(str hash 带盐)。
|
|
48
47
|
同一份 film.json 两次 voice_plan() 完全相等;jitter=0 时与「无抖动」逐字段相等。
|
|
49
48
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
ValueError
|
|
49
|
+
老结构
|
|
50
|
+
------
|
|
51
|
+
顶层 voice 单值与「没有 design 的 legacy 计划」都不受支持:voice_plan 直接抛
|
|
52
|
+
ValueError,消息里给三条可照抄的路径(design.audio.voice / lines[].voice / voice.map)。
|
|
54
53
|
不静默回落是刻意的:静默回落 = 配音变了却没人知道,比报错难查得多。
|
|
55
54
|
|
|
56
55
|
用法
|
|
@@ -58,7 +57,7 @@ ValueError,消息里给三条可照抄的迁移路径(design.audio.voice / l
|
|
|
58
57
|
import voicekit as VK
|
|
59
58
|
plan = VK.voice_plan(script, ST.resolve_script_style(script))
|
|
60
59
|
VK.describe(plan)
|
|
61
|
-
VK.spec_key(dict(plan[0], text=script["lines"][0]["text"])) #
|
|
60
|
+
VK.spec_key(dict(plan[0], text=script["lines"][0]["text"])) # 合成缓存键
|
|
62
61
|
VK.calibrate_voice("zh-CN-YunxiNeural") # 联网实测;离线 measured=False,不抛
|
|
63
62
|
|
|
64
63
|
python voicekit.py --list # 音色响度表 + 语气档
|
|
@@ -85,10 +84,10 @@ import wave
|
|
|
85
84
|
# 一、响度补偿(数据,不是身份):换音色不用手调电平
|
|
86
85
|
# ============================================================================
|
|
87
86
|
#
|
|
88
|
-
#
|
|
89
|
-
#
|
|
87
|
+
# 为什么这张表必须有:
|
|
88
|
+
# 九把音色的实测响度跨度 6.0 dB(-3.0 ~ +3.0)。差别来自音色本身(基频、气声、
|
|
90
89
|
# 端点侧的归一化),跟台词、语气、机器负载都无关,但**换一把音色就会整体偏几 dB**。
|
|
91
|
-
#
|
|
90
|
+
# 不补补偿,每次换音色都要试听、手调 gain —— 那正是「改了不生效 / 要人调」。
|
|
92
91
|
#
|
|
93
92
|
# 口径:gain_db = 目标响度 - 实测 rms_db,夹在 ±3 dB(正 = 该音色偏轻,mix 侧补回来)。
|
|
94
93
|
# - 实测:2026-11-09 在本机用 edge-tts 7.3.1 合成同一句校准文本(CALIB_TEXT,21 个
|
|
@@ -105,7 +104,7 @@ VOICE_LOUDNESS = {
|
|
|
105
104
|
"zh-TW-HsiaoChenNeural": -3.0, # 软糯女声(台湾腔),原始 -3.46,夹到 -3
|
|
106
105
|
"zh-CN-XiaoyiNeural": -0.6, # 活泼女声
|
|
107
106
|
"zh-CN-YunjianNeural": 1.0, # 有力男声
|
|
108
|
-
"zh-CN-YunxiNeural": 1.9, #
|
|
107
|
+
"zh-CN-YunxiNeural": 1.9, # 青年男声(最常用的旁白音色)
|
|
109
108
|
"zh-CN-YunxiaNeural": 0.3, # 少年音
|
|
110
109
|
"zh-CN-liaoning-XiaobeiNeural": 0.0, # 东北女声(本次校准的基准,补偿 0)
|
|
111
110
|
"zh-CN-shaanxi-XiaoniNeural": -0.3, # 陕西女声
|
|
@@ -180,7 +179,7 @@ except Exception: # noqa: BLE001 — stylekit 缺失时
|
|
|
180
179
|
# 三、小工具:参数解析/格式化、汉字计数、抖动
|
|
181
180
|
# ============================================================================
|
|
182
181
|
# 允许小数("+4.5%"):edge-tts 只收整数百分比,所以解析后按 _round_half_away 取整。
|
|
183
|
-
#
|
|
182
|
+
# 正则只认整数时 "+4.5%" 解析失败 → 静默回落 0,而 0 与 "+0%" 在 spec_key 里
|
|
184
183
|
# 是**同一个 TTS 缓存键**,于是「把语速从 0 调到 4.5%」完全没生效,还复用了旧音频。
|
|
185
184
|
# 这是配置写对了却不生效里最难查的一类:没有报错、没有日志、听起来就是「没反应」。
|
|
186
185
|
_PCT_RE = re.compile(r"^\s*([+-]?)(\d+(?:\.\d+)?)\s*%\s*$")
|
|
@@ -283,7 +282,7 @@ def _quantize(v, u):
|
|
|
283
282
|
# 四、文本规则(说什么):全部确定性,命中即返回理由
|
|
284
283
|
# ============================================================================
|
|
285
284
|
# 引语起始符。**不含书名号《》**:正文里出现「《设计模式》」是书名,不是引用某人在说话。
|
|
286
|
-
#
|
|
285
|
+
# 把 《 算进引语,「今天我们聊《设计模式》」这句就会判成 quote 语气 ——
|
|
287
286
|
# 整句降速、留白加长,听感像突然换了频道(而这句根本没有人引语)。
|
|
288
287
|
_QUOTE_OPEN = ('"', "\u201c", "\u300c", "\u300e")
|
|
289
288
|
_QUOTE_WORDS = ("他说", "她说", "有人问", "所谓", "据称")
|
|
@@ -329,7 +328,7 @@ def _rules(text):
|
|
|
329
328
|
def _resolve(script, style_plan):
|
|
330
329
|
"""拿风格计划:显式参数 > script["_style"](render 已解析过)> stylekit 现算 > {}。
|
|
331
330
|
|
|
332
|
-
返回 {}
|
|
331
|
+
返回 {} 不是故障:声线来源是 design.audio.voice,本模块自己就能读;
|
|
333
332
|
stylekit 只负责风格包/节奏这一层,缺了它照样能出 voice 计划。
|
|
334
333
|
"""
|
|
335
334
|
if isinstance(style_plan, dict) and style_plan:
|
|
@@ -346,9 +345,9 @@ def _resolve(script, style_plan):
|
|
|
346
345
|
|
|
347
346
|
|
|
348
347
|
def _design_voice(script):
|
|
349
|
-
"""
|
|
348
|
+
"""读设计里的声线:返回 (主声线, {kind: id}, 策略 {auto, jitter})。
|
|
350
349
|
|
|
351
|
-
为什么直接读 script["design"] 而不只依赖 style_plan
|
|
350
|
+
为什么直接读 script["design"] 而不只依赖 style_plan:单测/验收会直接
|
|
352
351
|
voice_plan(script),此时没有 render.py 解析过的 _style;而 stylekit 对「没有
|
|
353
352
|
design」的剧本已经直接报错,_resolve 只能返回 {}。design.audio.voice 是声线的
|
|
354
353
|
唯一事实来源,能读就读,别要求调用方先跑一遍 render。
|
|
@@ -385,7 +384,7 @@ def _design_voice(script):
|
|
|
385
384
|
|
|
386
385
|
|
|
387
386
|
def _script_voice(script):
|
|
388
|
-
"""
|
|
387
|
+
"""script.voice 写成 dict 时的形状:返回 (主声线, {kind: id})。"""
|
|
389
388
|
vo = script.get("voice")
|
|
390
389
|
if not isinstance(vo, dict):
|
|
391
390
|
return None, {}
|
|
@@ -404,7 +403,7 @@ def _style_voice(sp):
|
|
|
404
403
|
return vmap, default
|
|
405
404
|
|
|
406
405
|
|
|
407
|
-
#
|
|
406
|
+
# 迁移说明只写一份:三处报错(非法声线 / 老结构 / map 值)都指向它,避免各处各写一套。
|
|
408
407
|
_MIGRATE = (
|
|
409
408
|
"声线现在一律写 edge voice id(形如 zh-CN-YunxiNeural)。迁移任选一条:\n"
|
|
410
409
|
" 1) design.json:audio.voice = {\"mode\": \"edge_id\", \"id\": \"zh-CN-YunxiNeural\"};"
|
|
@@ -421,10 +420,10 @@ def _bad_voice(field, value):
|
|
|
421
420
|
|
|
422
421
|
|
|
423
422
|
def _old_structure(script, sp, scalar=None):
|
|
424
|
-
"""
|
|
423
|
+
"""老结构(顶层 voice 单值 / 没有 design 的 legacy 计划)不受支持:直说怎么迁移。"""
|
|
425
424
|
bits = []
|
|
426
425
|
if scalar:
|
|
427
|
-
bits.append("顶层 voice = %r
|
|
426
|
+
bits.append("顶层 voice = %r,单值声线" % scalar)
|
|
428
427
|
if sp.get("legacy"):
|
|
429
428
|
bits.append("风格计划判定为 legacy(film.json 里没有 design/style)")
|
|
430
429
|
return ValueError(
|
|
@@ -504,11 +503,11 @@ def voice_plan(script, style_plan=None):
|
|
|
504
503
|
scalar = script.get("voice")
|
|
505
504
|
has_line_voice = any(isinstance(ln, dict) and ln.get("voice") for ln in lines)
|
|
506
505
|
if isinstance(scalar, str) and scalar.strip():
|
|
507
|
-
#
|
|
506
|
+
# 顶层单值 voice。声线事实来源只有 design.audio.voice(或逐句 voice),
|
|
508
507
|
# 再读它会和 design 打架,而「谁生效」取决于调用顺序 —— 最难查的一类。
|
|
509
508
|
raise _old_structure(script, sp, scalar.strip())
|
|
510
509
|
if not (default or vmap or has_line_voice):
|
|
511
|
-
#
|
|
510
|
+
# 没有任何来源时**不猜**:静默套 kind 缺省会让整片悄悄换声线。
|
|
512
511
|
# 宁可报错一次,让人把 design.audio.voice 补上。
|
|
513
512
|
if isinstance(script.get("design"), dict):
|
|
514
513
|
raise ValueError(
|
|
@@ -537,8 +536,8 @@ def voice_plan(script, style_plan=None):
|
|
|
537
536
|
rules = _rules(ln.get("text")) if use_rules else {
|
|
538
537
|
"tone": "", "rate": 0, "volume": 0, "tail": 0.0, "reasons": []}
|
|
539
538
|
|
|
540
|
-
#
|
|
541
|
-
#
|
|
539
|
+
# 身份只有一个来源,写错就报错,不「悄悄回落到某个缺省声线」:
|
|
540
|
+
# 写错名会静默换人(听感变了但没有任何提示),是本项目最忌讳的坑。
|
|
542
541
|
raw_p = ln.get("persona")
|
|
543
542
|
if raw_p not in (None, ""):
|
|
544
543
|
raise _bad_voice("lines[%d].persona" % i, raw_p)
|
|
@@ -619,7 +618,7 @@ def voice_plan(script, style_plan=None):
|
|
|
619
618
|
|
|
620
619
|
|
|
621
620
|
# ============================================================================
|
|
622
|
-
# 六、缓存键(v2:trim 进 key
|
|
621
|
+
# 六、缓存键(v2:trim 进 key,否则改 trim 会静默复用旧音频)
|
|
623
622
|
# ============================================================================
|
|
624
623
|
def _norm_pct(v, default="+0%"):
|
|
625
624
|
if v in (None, ""):
|
|
@@ -636,7 +635,7 @@ def _norm_hz(v, default="+0Hz"):
|
|
|
636
635
|
def spec_key(spec):
|
|
637
636
|
"""v2 合成缓存键:sha1("v2|text|voice|rate|pitch|volume|trim|trim_db")[:16]。
|
|
638
637
|
|
|
639
|
-
|
|
638
|
+
不复用旧的 tts_key(text|voice|rate|pitch):那套键把 trim 漏在 key 外,
|
|
640
639
|
同一句改 trim/trim_db 会静默复用旧音频。spec 传一行计划 + text 即可:
|
|
641
640
|
spec_key(dict(plan[0], text=line["text"]));trim/trim_db 缺省与 filmkit.tts 一致。
|
|
642
641
|
"""
|
|
@@ -994,7 +993,7 @@ def main(argv=None):
|
|
|
994
993
|
if only:
|
|
995
994
|
unknown = [x for x in only if not is_edge_voice(x)]
|
|
996
995
|
if unknown:
|
|
997
|
-
#
|
|
996
|
+
# 拼错名字会静默变成空集合 -> 打出「所有音色都合成失败(多半没网)」,
|
|
998
997
|
# 把配置错误伪装成网络错误。这里先点名校验,给人话可照抄的可用清单。
|
|
999
998
|
print("[未知音色] %s;可用:%s"
|
|
1000
999
|
% ("、".join(unknown), " / ".join(VOICE_LOUDNESS)), file=sys.stderr)
|