@dsh-gis/dsh-video-studio-skill 0.7.2 → 0.9.1
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 +1109 -0
- package/README.md +76 -10
- package/THIRD_PARTY_NOTICES.md +1 -0
- package/docs/styles/sheet-8-styles.png +0 -0
- package/docs/styles/sheet-before-after.png +0 -0
- package/index.js +4 -1
- package/package.json +7 -3
- package/schemas/film.schema.json +329 -2
- package/skills/video-studio/SKILL.md +270 -13
- package/skills/video-studio/assets/golden_frames.json +65 -0
- package/skills/video-studio/assets/scenes.py +2162 -1190
- package/skills/video-studio/assets/web/engine.js +63 -2
- package/skills/video-studio/assets/web/film.js +224 -101
- package/skills/video-studio/assets/web/index.html +4 -2
- package/skills/video-studio/assets/web/qa_text.js +36 -8
- package/skills/video-studio/references/audio-variation.md +4 -3
- package/skills/video-studio/references/audio.md +2 -2
- package/skills/video-studio/references/change-impact.md +1 -1
- package/skills/video-studio/references/quality-gates.md +25 -3
- package/skills/video-studio/references/scene-grammar.md +306 -0
- package/skills/video-studio/references/style-packs.md +108 -3
- package/skills/video-studio/references/v08-quality.md +256 -0
- package/skills/video-studio/references/web-engine.md +19 -2
- package/skills/video-studio/references/word-timeline.md +147 -0
- package/skills/video-studio/requirements.txt +14 -0
- package/skills/video-studio/scripts/filmcheck.py +218 -0
- package/skills/video-studio/scripts/filmkit.py +521 -38
- package/skills/video-studio/scripts/gates.py +88 -4
- package/skills/video-studio/scripts/grammar.py +559 -0
- package/skills/video-studio/scripts/preflight.py +774 -0
- package/skills/video-studio/scripts/qa_frames.py +49 -8
- package/skills/video-studio/scripts/qaasr.py +1563 -0
- package/skills/video-studio/scripts/qavmaf.py +1442 -0
- package/skills/video-studio/scripts/render.py +764 -32
- package/skills/video-studio/scripts/scorekit.py +16 -30
- package/skills/video-studio/scripts/selfcheck.py +483 -0
- package/skills/video-studio/scripts/sfxkit.py +130 -14
- package/skills/video-studio/scripts/stagewatch.py +19 -6
- package/skills/video-studio/scripts/stylekit.py +242 -12
- package/skills/video-studio/scripts/voicekit.py +15 -7
- package/skills/video-studio/scripts/webengine.py +1119 -72
- package/tools/ci_check.py +173 -31
- package/tools/estimate.py +130 -19
- package/tools/golden_frames.py +664 -0
- package/tools/silhouette_qa.py +910 -0
|
@@ -42,6 +42,17 @@ HERE = Path(__file__).resolve().parent
|
|
|
42
42
|
# ============================================================================
|
|
43
43
|
|
|
44
44
|
KINDS = ("hook", "body", "quote", "outro", "emphasis", "calm") # 也是 voicekit 的语气 id
|
|
45
|
+
|
|
46
|
+
# 视觉场景类型(v0.9):决定"这段画面长成什么构图",与叙事 kind **正交**——
|
|
47
|
+
# kind 管声线/语气/配乐能量(voicekit / scorekit 读它);
|
|
48
|
+
# type 只管构图(assets/scenes.py 的 _DRAWERS 读它)。
|
|
49
|
+
# 所以**不新增 kind 值**:hero/stat/... 不是语气,硬塞进 KINDS 会撞 voicekit 的 tone id
|
|
50
|
+
# (TONE_IDS 与 KINDS 是同一组 6 个)。缺省(不写 lines[].type)= 由 grammar.py 按内容推导。
|
|
51
|
+
SCENE_TYPES = ("hero", "statement", "list", "stat", "compare", "process",
|
|
52
|
+
"quote", "chapter", "chart", "outro")
|
|
53
|
+
# 与 assets/scenes.py 的 _DRAWERS 键对齐,改一边必须改另一边。
|
|
54
|
+
# (grammar.SCENE_TYPES 是同一组 10 个值,但那份顺序用于"平手取位";这里只做成员校验,
|
|
55
|
+
# 本元组的顺序只影响报错信息里的枚举列举,不影响判定。三处不要再各写一份新名单。)
|
|
45
56
|
PRESETS = ("ultrafast", "superfast", "veryfast", "faster", "fast",
|
|
46
57
|
"medium", "slow", "slower", "veryslow")
|
|
47
58
|
MUSIC_MODES = ("score", "bed", "file", "off")
|
|
@@ -200,6 +211,59 @@ LINE_ITEM_SPEC = {
|
|
|
200
211
|
},
|
|
201
212
|
}
|
|
202
213
|
|
|
214
|
+
# ---- v0.9 视觉场景字段的嵌套结构(写法照抄 LINE_ITEM_SPEC)----
|
|
215
|
+
# 这些对象的键由 scenes.py 的 10 个绘制器读:拼错就是"写了不生效",所以**不**用
|
|
216
|
+
# open=True(那是留给 sfx 配方参数那种真·透传的)。每层列全 props,才能把拼错报成
|
|
217
|
+
# unknown_key(WARN + "是不是想写 X"),把类型写错报成 ERROR。
|
|
218
|
+
# required 与 schema 表里的问号标记一致:必填 = 绘制器没有它就没法画。
|
|
219
|
+
LINE_STAT_SPEC = { # type=stat 的数字卡
|
|
220
|
+
"t": "dict", "required": ["value"],
|
|
221
|
+
"props": {
|
|
222
|
+
# 数字或字符串("92"、">3x"、"1/2" 这类写法只能当字符串)
|
|
223
|
+
"value": {"t": ("num", "str"), "nonempty": True},
|
|
224
|
+
"unit": {"t": "str"},
|
|
225
|
+
"label": {"t": "str"},
|
|
226
|
+
"note": {"t": "str"},
|
|
227
|
+
},
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
LINE_SERIES_SPEC = { # type=chart 的数据条
|
|
231
|
+
"t": "dict", "required": ["label", "value"],
|
|
232
|
+
"props": {
|
|
233
|
+
"label": {"t": "str", "nonempty": True},
|
|
234
|
+
"value": {"t": ("num", "str"), "nonempty": True},
|
|
235
|
+
"unit": {"t": "str"},
|
|
236
|
+
},
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
LINE_STEP_SPEC = { # type=process 的流程节点
|
|
240
|
+
"t": "dict", "required": ["label"],
|
|
241
|
+
"props": {
|
|
242
|
+
"label": {"t": "str", "nonempty": True},
|
|
243
|
+
"note": {"t": "str"},
|
|
244
|
+
},
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
# 对比卡的一侧:items 每项可以写字符串,也可以写 {text}(短条目直接写字符串更省事,两种都认)
|
|
248
|
+
LINE_SIDE_SPEC = {
|
|
249
|
+
"t": "dict",
|
|
250
|
+
"props": {
|
|
251
|
+
"head": {"t": "str"},
|
|
252
|
+
"items": {"t": "list", "item": {
|
|
253
|
+
"t": ("str", "dict"), "required": ["text"],
|
|
254
|
+
"props": {"text": {"t": "str", "nonempty": True}},
|
|
255
|
+
}},
|
|
256
|
+
},
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
LINE_SIDES_SPEC = { # type=compare 的左右对比(left/right 都可省略)
|
|
260
|
+
"t": "dict",
|
|
261
|
+
"props": {
|
|
262
|
+
"left": LINE_SIDE_SPEC,
|
|
263
|
+
"right": LINE_SIDE_SPEC,
|
|
264
|
+
},
|
|
265
|
+
}
|
|
266
|
+
|
|
203
267
|
LINE_SPEC = {
|
|
204
268
|
"t": "dict", "required": ["text"],
|
|
205
269
|
"props": {
|
|
@@ -218,6 +282,16 @@ LINE_SPEC = {
|
|
|
218
282
|
"persona": {"t": "str"},
|
|
219
283
|
"tone": {"t": "str", "enum": KINDS},
|
|
220
284
|
"voice": {"t": "str"},
|
|
285
|
+
# ---- v0.9 视觉场景字段(7 个全可选)----
|
|
286
|
+
# type 缺省由 grammar.py 按内容推导;其余字段是给绘制器的原料,不写就不画。
|
|
287
|
+
# 注意这些字段与 kind **正交**,不是新 kind:kind 仍只有上面 6 个(voicekit 的语气 id)。
|
|
288
|
+
"type": {"t": "str", "enum": SCENE_TYPES},
|
|
289
|
+
"quote": {"t": "str"},
|
|
290
|
+
"by": {"t": "str"},
|
|
291
|
+
"stats": {"t": "list", "item": LINE_STAT_SPEC},
|
|
292
|
+
"series": {"t": "list", "item": LINE_SERIES_SPEC},
|
|
293
|
+
"steps": {"t": "list", "item": LINE_STEP_SPEC},
|
|
294
|
+
"sides": LINE_SIDES_SPEC,
|
|
221
295
|
},
|
|
222
296
|
}
|
|
223
297
|
|
|
@@ -315,6 +389,46 @@ SCRIPT_SPEC = {
|
|
|
315
389
|
"web": {"t": "dict", "props": {
|
|
316
390
|
"entry": {"t": "str"},
|
|
317
391
|
"chunk_s": {"t": "num", "min": 0.0}}},
|
|
392
|
+
# ---- v0.8 ----
|
|
393
|
+
# 词级时间戳开关(字幕按词折行 / cue 词锚点 / G10 门禁都依赖它)
|
|
394
|
+
"words": {"t": "bool"},
|
|
395
|
+
# TTS 后端选择(v0.8 接口化;默认 edge,其它后端需先 register_tts_backend)
|
|
396
|
+
"tts": {"t": "dict", "props": {
|
|
397
|
+
"backend": {"t": "str"},
|
|
398
|
+
"voice": {"t": ("str", "dict")}}},
|
|
399
|
+
# 画幅与帧率:只允许 16:9 横屏 + 下采样输出(见 filmkit.video_plan 的取舍说明)
|
|
400
|
+
"video": {"t": "dict", "props": {
|
|
401
|
+
"width": {"t": "int", "min": 2},
|
|
402
|
+
"height": {"t": "int", "min": 2},
|
|
403
|
+
"out_width": {"t": "int", "min": 2},
|
|
404
|
+
"out_height": {"t": "int", "min": 2},
|
|
405
|
+
"fps": {"t": "int", "min": 1, "max": 120}}},
|
|
406
|
+
# 质检门禁参数(全片 VMAF/SSIM 阻断 + ASR 回读)
|
|
407
|
+
"qa": {"t": "dict", "props": {
|
|
408
|
+
"vmaf": {"t": "dict", "props": {
|
|
409
|
+
# enabled / require_vmaf 是 v0.8 起草时留下的**惰性键**:schema 与这里都认,
|
|
410
|
+
# 但 render.py / qavmaf.py 全仓没有读取点——写了它门禁照跑,用户以为自己关掉了。
|
|
411
|
+
# 真正生效的是 mode=off(或命令行 --vmaf off)。这里标 inert 让 filmcheck 报 WARN,
|
|
412
|
+
# 因为本项目的立场是"写对了却不生效"比"报未知键"更坏。
|
|
413
|
+
"enabled": {"t": "bool", "inert": "写它不生效;要跳过 G11 请写 qa.vmaf.mode=\"off\"(或 --vmaf off)"},
|
|
414
|
+
"mode": {"t": "str", "enum": ("full", "sample", "off")},
|
|
415
|
+
"pass": {"t": "num", "min": 0.0},
|
|
416
|
+
"warn": {"t": "num", "min": 0.0},
|
|
417
|
+
# v0.8.1:键名对齐 schemas/film.schema.json 与 qavmaf.report(sample_step=...)。
|
|
418
|
+
# 这里以前写的是 sample_s —— 全仓没有任何消费者(render 不读、qavmaf 只认
|
|
419
|
+
# sample_step),于是"写对了却完全没生效",而 schema 里合法写完还会被本脚本
|
|
420
|
+
# 报成未知键。别再兼容 sample_s:它从来没生效过,留着只会让人以为能用。
|
|
421
|
+
"sample_step": {"t": "int", "min": 1}}},
|
|
422
|
+
"ssim": {"t": "dict", "props": {
|
|
423
|
+
"enabled": {"t": "bool", "inert": "写它不生效;SSIM 本来就只是诊断项(不单独阻断)"},
|
|
424
|
+
"pass": {"t": "num", "min": 0.0, "max": 1.0},
|
|
425
|
+
"warn": {"t": "num", "min": 0.0, "max": 1.0}}},
|
|
426
|
+
"asr": {"t": "dict", "props": {
|
|
427
|
+
"enabled": {"t": "bool"},
|
|
428
|
+
"model": {"t": "str"},
|
|
429
|
+
"recall_pass": {"t": "num", "min": 0.0, "max": 1.0},
|
|
430
|
+
"recall_warn": {"t": "num", "min": 0.0, "max": 1.0}}},
|
|
431
|
+
"require_vmaf": {"t": "bool", "inert": "写它不生效;要强制 G11 别记 skipped 请用 --strict-gates"}}},
|
|
318
432
|
},
|
|
319
433
|
}
|
|
320
434
|
|
|
@@ -350,6 +464,12 @@ def _check(val, rule, path, rep, ctx):
|
|
|
350
464
|
return
|
|
351
465
|
|
|
352
466
|
enum = rule.get("enum")
|
|
467
|
+
# 惰性键:schema 与 filmcheck 都认它,但代码里没有任何读取点。
|
|
468
|
+
# 报 WARN 而不是 ERROR —— 老剧本里写了它不该变成硬失败;但也绝不能静默通过,
|
|
469
|
+
# 否则"写对了却不生效"会一路带到交付(本项目历史上最贵的一类缺陷)。
|
|
470
|
+
if rule.get("inert"):
|
|
471
|
+
rep.warn(path, "这个键**当前不生效**:%s" % rule["inert"], "inert_key")
|
|
472
|
+
|
|
353
473
|
if rule.get("enum_dynamic") == "style":
|
|
354
474
|
styles = ctx.get("styles")
|
|
355
475
|
if styles is None:
|
|
@@ -453,6 +573,13 @@ def _cue_ref(at):
|
|
|
453
573
|
float(off)
|
|
454
574
|
except ValueError:
|
|
455
575
|
return "bad", "偏移 %r 不是数字(写 l03+0.35 或 l03-0.2)" % off
|
|
576
|
+
# v0.8 词锚点:<id>:w<N>[+/-秒]。把 ":wN" 剥掉再做 id 存在性检查,
|
|
577
|
+
# 否则合法写法会被报成"场景 id 不存在"(假失败比不校验更糟)。
|
|
578
|
+
if ":w" in sid:
|
|
579
|
+
head, _sep, tail = sid.partition(":w")
|
|
580
|
+
if not tail.isdigit() or int(tail) < 1:
|
|
581
|
+
return "bad", "词锚点要写成 l03:w2(词序号从 1 开始),收到 %r" % sid
|
|
582
|
+
return "id", head
|
|
456
583
|
return "id", sid
|
|
457
584
|
|
|
458
585
|
|
|
@@ -485,6 +612,27 @@ def check_cross(script, script_path, rep, ctx):
|
|
|
485
612
|
ids.append(lid)
|
|
486
613
|
idset = set(ids)
|
|
487
614
|
|
|
615
|
+
# ---- lines[].persona / lines[].tone 拼错会静默回落:枚举从 voicekit 单向读取 ----
|
|
616
|
+
personas, tones = _load_personas()
|
|
617
|
+
if personas:
|
|
618
|
+
for i, ln in enumerate(lines):
|
|
619
|
+
raw = ln.get("persona")
|
|
620
|
+
if raw in (None, ""):
|
|
621
|
+
continue
|
|
622
|
+
name = str(raw).strip()
|
|
623
|
+
# 原始 edge-tts voice id(zh-CN-YunxiNeural)也允许写在 persona 位
|
|
624
|
+
if name in personas or ("-" in name and name.endswith("Neural")):
|
|
625
|
+
continue
|
|
626
|
+
near = _suggest(name, personas)
|
|
627
|
+
rep.warn("lines[%d].persona" % i,
|
|
628
|
+
"未知 persona %r —— voicekit 会**静默回落**到 kind 默认声线"
|
|
629
|
+
"(成片换人,且不会有任何报错)%s。可用:%s"
|
|
630
|
+
% (name, (";是不是想写 %r?" % near) if near else "",
|
|
631
|
+
" / ".join(personas[:9])), "unknown_persona")
|
|
632
|
+
# tone 不在这里查:LINE_SPEC 里已经有 enum(KINDS 与 voicekit.TONE_IDS 是同一组 6 个),
|
|
633
|
+
# 那是 ERROR 而不是 WARN。这里只补 persona —— 它没有静态枚举,拼错会静默换声线。
|
|
634
|
+
_ = tones
|
|
635
|
+
|
|
488
636
|
# ---- lines[].tail 当前不生效(保留字段):写了会静默无效,必须说出来 ----
|
|
489
637
|
for i, ln in enumerate(lines):
|
|
490
638
|
if _f(ln.get("tail", 0.0)) > 0:
|
|
@@ -497,6 +645,55 @@ def check_cross(script, script_path, rep, ctx):
|
|
|
497
645
|
"tail_map)。例如 %s: 1.4" % lid),
|
|
498
646
|
"inert_line_tail")
|
|
499
647
|
|
|
648
|
+
# ---- v0.9 场景语法:绘制器的容量上限(超了会被静默截断) ----
|
|
649
|
+
# 这是本项目最忌讳的一类坑:作者写对了、跑得通、但画面上少了几条,而没有任何提示。
|
|
650
|
+
# 容量由 assets/scenes.py 的实现决定(stats 取前 3 条、series 前 6 条、steps 前 5 条、
|
|
651
|
+
# sides 每侧前 3 条),改那边要同步改这里。
|
|
652
|
+
_CAP = (("stats", 3), ("series", 6), ("steps", 5))
|
|
653
|
+
for i, ln in enumerate(lines):
|
|
654
|
+
for fld, cap in _CAP:
|
|
655
|
+
v = ln.get(fld)
|
|
656
|
+
if isinstance(v, list) and len(v) > cap:
|
|
657
|
+
rep.warn("lines[%d].%s" % (i, fld),
|
|
658
|
+
"%s 有 %d 条,但 %s 构图只画前 %d 条(其余不会出现在画面上)。"
|
|
659
|
+
"要全画请拆成多段,或减到 %d 条以内"
|
|
660
|
+
% (fld, len(v), fld, cap, cap), "scene_capacity")
|
|
661
|
+
sides = ln.get("sides")
|
|
662
|
+
if isinstance(sides, dict):
|
|
663
|
+
for side in ("left", "right"):
|
|
664
|
+
blk = sides.get(side)
|
|
665
|
+
its = blk.get("items") if isinstance(blk, dict) else None
|
|
666
|
+
if isinstance(its, list) and len(its) > 3:
|
|
667
|
+
rep.warn("lines[%d].sides.%s.items" % (i, side),
|
|
668
|
+
"sides.%s 有 %d 条,但左右对比构图每侧只画前 3 条"
|
|
669
|
+
% (side, len(its)), "scene_capacity")
|
|
670
|
+
|
|
671
|
+
# ---- v0.8 video 块:只支持 16:9 横屏 + 下采样输出 ----
|
|
672
|
+
# **不要在这里重写一遍规则**:允许列表 / 宽高比 / 上采样拒绝的权威实现在
|
|
673
|
+
# filmkit.video_plan()。以前这里是第二套手写判据,于是"1600x900"能过 filmcheck
|
|
674
|
+
# (0 error 0 warning)却在 render 的 st_layout 被 video_plan 拒片——校验器放行、
|
|
675
|
+
# 渲染器拒收,用户只会觉得门禁在骗人。这里改成直接调用权威实现,只做错误翻译。
|
|
676
|
+
vv = script.get("video") if isinstance(script.get("video"), dict) else None
|
|
677
|
+
if vv:
|
|
678
|
+
try:
|
|
679
|
+
import filmkit as _K
|
|
680
|
+
except Exception: # noqa: BLE001
|
|
681
|
+
_K = None
|
|
682
|
+
if _K is not None:
|
|
683
|
+
try:
|
|
684
|
+
_K.video_plan(script)
|
|
685
|
+
except ValueError as e:
|
|
686
|
+
rep.err("video", "%s(filmkit.video_plan 会在渲染时拒片,这里提前拦)" % e,
|
|
687
|
+
"bad_video")
|
|
688
|
+
except Exception as e: # noqa: BLE001
|
|
689
|
+
rep.err("video", "画幅解析失败:%s" % e, "bad_video")
|
|
690
|
+
else:
|
|
691
|
+
# filmkit 不可用(拷出来只带了 filmcheck)时退回最小校验:只查 fps 枚举,
|
|
692
|
+
# 并明说"完整校验需要 filmkit.py",避免让人以为已经全查过。
|
|
693
|
+
rep.warn("video", "找不到 filmkit.py:只做了 fps 枚举校验,"
|
|
694
|
+
"画幅/上采样规则未校验(完整校验请把 filmkit.py 放在同目录)",
|
|
695
|
+
"video_partial")
|
|
696
|
+
|
|
500
697
|
# ---- sfx.cues[].at 引用的场景 id 必须在 lines 里 ----
|
|
501
698
|
sfx = script.get("sfx") if isinstance(script.get("sfx"), dict) else {}
|
|
502
699
|
cues = sfx.get("cues") if isinstance(sfx.get("cues"), list) else []
|
|
@@ -629,6 +826,27 @@ def _load_styles():
|
|
|
629
826
|
return None
|
|
630
827
|
|
|
631
828
|
|
|
829
|
+
def _load_personas():
|
|
830
|
+
"""从 voicekit 单向读取 persona 名与 tone id;拿不到返回 (None, None) 降级成不查。
|
|
831
|
+
|
|
832
|
+
为什么要查:voicekit 对写错的 persona/tone 是**静默回落**(reason 里留
|
|
833
|
+
unknown_persona:xxx,但不报错、不告警),于是"拼错一个声线名"的后果是整片
|
|
834
|
+
静默换成 kind 默认声线——听感变了,却没有任何一处提示。schema 层只校验 t=str,
|
|
835
|
+
拦不住;这里补上(规则仍然只有一份:枚举从 voicekit 读,不复制)。
|
|
836
|
+
"""
|
|
837
|
+
try:
|
|
838
|
+
if str(HERE) not in sys.path:
|
|
839
|
+
sys.path.insert(0, str(HERE))
|
|
840
|
+
import voicekit # noqa: F401 (只读枚举,不复制)
|
|
841
|
+
personas = list(getattr(voicekit, "PERSONA_IDS", ()) or
|
|
842
|
+
getattr(voicekit, "VOICE_PERSONAS", {}).keys())
|
|
843
|
+
tones = list(getattr(voicekit, "TONE_IDS", ()) or
|
|
844
|
+
getattr(voicekit, "VOICE_TONES", {}).keys())
|
|
845
|
+
return (personas or None), (tones or None)
|
|
846
|
+
except Exception: # noqa: BLE001
|
|
847
|
+
return None, None
|
|
848
|
+
|
|
849
|
+
|
|
632
850
|
def _classify(data):
|
|
633
851
|
"""film = 像剧本;artifact = 其他产物(配乐报告等),跳过;unknown = 判不了。"""
|
|
634
852
|
if not isinstance(data, dict):
|