@dsh-gis/dsh-video-studio-skill 0.7.2 → 0.8.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 +951 -0
- package/README.md +51 -8
- package/THIRD_PARTY_NOTICES.md +1 -0
- package/index.js +4 -1
- package/package.json +5 -3
- package/schemas/film.schema.json +162 -2
- package/skills/video-studio/SKILL.md +211 -11
- package/skills/video-studio/assets/golden_frames.json +65 -0
- 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/style-packs.md +7 -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 +121 -0
- package/skills/video-studio/scripts/filmkit.py +521 -38
- package/skills/video-studio/scripts/gates.py +88 -4
- 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 +719 -32
- package/skills/video-studio/scripts/scorekit.py +16 -30
- package/skills/video-studio/scripts/selfcheck.py +274 -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 +43 -12
- package/skills/video-studio/scripts/voicekit.py +15 -7
- package/skills/video-studio/scripts/webengine.py +1119 -72
- package/tools/ci_check.py +171 -31
- package/tools/estimate.py +130 -19
- package/tools/golden_frames.py +664 -0
|
@@ -315,6 +315,46 @@ SCRIPT_SPEC = {
|
|
|
315
315
|
"web": {"t": "dict", "props": {
|
|
316
316
|
"entry": {"t": "str"},
|
|
317
317
|
"chunk_s": {"t": "num", "min": 0.0}}},
|
|
318
|
+
# ---- v0.8 ----
|
|
319
|
+
# 词级时间戳开关(字幕按词折行 / cue 词锚点 / G10 门禁都依赖它)
|
|
320
|
+
"words": {"t": "bool"},
|
|
321
|
+
# TTS 后端选择(v0.8 接口化;默认 edge,其它后端需先 register_tts_backend)
|
|
322
|
+
"tts": {"t": "dict", "props": {
|
|
323
|
+
"backend": {"t": "str"},
|
|
324
|
+
"voice": {"t": ("str", "dict")}}},
|
|
325
|
+
# 画幅与帧率:只允许 16:9 横屏 + 下采样输出(见 filmkit.video_plan 的取舍说明)
|
|
326
|
+
"video": {"t": "dict", "props": {
|
|
327
|
+
"width": {"t": "int", "min": 2},
|
|
328
|
+
"height": {"t": "int", "min": 2},
|
|
329
|
+
"out_width": {"t": "int", "min": 2},
|
|
330
|
+
"out_height": {"t": "int", "min": 2},
|
|
331
|
+
"fps": {"t": "int", "min": 1, "max": 120}}},
|
|
332
|
+
# 质检门禁参数(全片 VMAF/SSIM 阻断 + ASR 回读)
|
|
333
|
+
"qa": {"t": "dict", "props": {
|
|
334
|
+
"vmaf": {"t": "dict", "props": {
|
|
335
|
+
# enabled / require_vmaf 是 v0.8 起草时留下的**惰性键**:schema 与这里都认,
|
|
336
|
+
# 但 render.py / qavmaf.py 全仓没有读取点——写了它门禁照跑,用户以为自己关掉了。
|
|
337
|
+
# 真正生效的是 mode=off(或命令行 --vmaf off)。这里标 inert 让 filmcheck 报 WARN,
|
|
338
|
+
# 因为本项目的立场是"写对了却不生效"比"报未知键"更坏。
|
|
339
|
+
"enabled": {"t": "bool", "inert": "写它不生效;要跳过 G11 请写 qa.vmaf.mode=\"off\"(或 --vmaf off)"},
|
|
340
|
+
"mode": {"t": "str", "enum": ("full", "sample", "off")},
|
|
341
|
+
"pass": {"t": "num", "min": 0.0},
|
|
342
|
+
"warn": {"t": "num", "min": 0.0},
|
|
343
|
+
# v0.8.1:键名对齐 schemas/film.schema.json 与 qavmaf.report(sample_step=...)。
|
|
344
|
+
# 这里以前写的是 sample_s —— 全仓没有任何消费者(render 不读、qavmaf 只认
|
|
345
|
+
# sample_step),于是"写对了却完全没生效",而 schema 里合法写完还会被本脚本
|
|
346
|
+
# 报成未知键。别再兼容 sample_s:它从来没生效过,留着只会让人以为能用。
|
|
347
|
+
"sample_step": {"t": "int", "min": 1}}},
|
|
348
|
+
"ssim": {"t": "dict", "props": {
|
|
349
|
+
"enabled": {"t": "bool", "inert": "写它不生效;SSIM 本来就只是诊断项(不单独阻断)"},
|
|
350
|
+
"pass": {"t": "num", "min": 0.0, "max": 1.0},
|
|
351
|
+
"warn": {"t": "num", "min": 0.0, "max": 1.0}}},
|
|
352
|
+
"asr": {"t": "dict", "props": {
|
|
353
|
+
"enabled": {"t": "bool"},
|
|
354
|
+
"model": {"t": "str"},
|
|
355
|
+
"recall_pass": {"t": "num", "min": 0.0, "max": 1.0},
|
|
356
|
+
"recall_warn": {"t": "num", "min": 0.0, "max": 1.0}}},
|
|
357
|
+
"require_vmaf": {"t": "bool", "inert": "写它不生效;要强制 G11 别记 skipped 请用 --strict-gates"}}},
|
|
318
358
|
},
|
|
319
359
|
}
|
|
320
360
|
|
|
@@ -350,6 +390,12 @@ def _check(val, rule, path, rep, ctx):
|
|
|
350
390
|
return
|
|
351
391
|
|
|
352
392
|
enum = rule.get("enum")
|
|
393
|
+
# 惰性键:schema 与 filmcheck 都认它,但代码里没有任何读取点。
|
|
394
|
+
# 报 WARN 而不是 ERROR —— 老剧本里写了它不该变成硬失败;但也绝不能静默通过,
|
|
395
|
+
# 否则"写对了却不生效"会一路带到交付(本项目历史上最贵的一类缺陷)。
|
|
396
|
+
if rule.get("inert"):
|
|
397
|
+
rep.warn(path, "这个键**当前不生效**:%s" % rule["inert"], "inert_key")
|
|
398
|
+
|
|
353
399
|
if rule.get("enum_dynamic") == "style":
|
|
354
400
|
styles = ctx.get("styles")
|
|
355
401
|
if styles is None:
|
|
@@ -453,6 +499,13 @@ def _cue_ref(at):
|
|
|
453
499
|
float(off)
|
|
454
500
|
except ValueError:
|
|
455
501
|
return "bad", "偏移 %r 不是数字(写 l03+0.35 或 l03-0.2)" % off
|
|
502
|
+
# v0.8 词锚点:<id>:w<N>[+/-秒]。把 ":wN" 剥掉再做 id 存在性检查,
|
|
503
|
+
# 否则合法写法会被报成"场景 id 不存在"(假失败比不校验更糟)。
|
|
504
|
+
if ":w" in sid:
|
|
505
|
+
head, _sep, tail = sid.partition(":w")
|
|
506
|
+
if not tail.isdigit() or int(tail) < 1:
|
|
507
|
+
return "bad", "词锚点要写成 l03:w2(词序号从 1 开始),收到 %r" % sid
|
|
508
|
+
return "id", head
|
|
456
509
|
return "id", sid
|
|
457
510
|
|
|
458
511
|
|
|
@@ -485,6 +538,27 @@ def check_cross(script, script_path, rep, ctx):
|
|
|
485
538
|
ids.append(lid)
|
|
486
539
|
idset = set(ids)
|
|
487
540
|
|
|
541
|
+
# ---- lines[].persona / lines[].tone 拼错会静默回落:枚举从 voicekit 单向读取 ----
|
|
542
|
+
personas, tones = _load_personas()
|
|
543
|
+
if personas:
|
|
544
|
+
for i, ln in enumerate(lines):
|
|
545
|
+
raw = ln.get("persona")
|
|
546
|
+
if raw in (None, ""):
|
|
547
|
+
continue
|
|
548
|
+
name = str(raw).strip()
|
|
549
|
+
# 原始 edge-tts voice id(zh-CN-YunxiNeural)也允许写在 persona 位
|
|
550
|
+
if name in personas or ("-" in name and name.endswith("Neural")):
|
|
551
|
+
continue
|
|
552
|
+
near = _suggest(name, personas)
|
|
553
|
+
rep.warn("lines[%d].persona" % i,
|
|
554
|
+
"未知 persona %r —— voicekit 会**静默回落**到 kind 默认声线"
|
|
555
|
+
"(成片换人,且不会有任何报错)%s。可用:%s"
|
|
556
|
+
% (name, (";是不是想写 %r?" % near) if near else "",
|
|
557
|
+
" / ".join(personas[:9])), "unknown_persona")
|
|
558
|
+
# tone 不在这里查:LINE_SPEC 里已经有 enum(KINDS 与 voicekit.TONE_IDS 是同一组 6 个),
|
|
559
|
+
# 那是 ERROR 而不是 WARN。这里只补 persona —— 它没有静态枚举,拼错会静默换声线。
|
|
560
|
+
_ = tones
|
|
561
|
+
|
|
488
562
|
# ---- lines[].tail 当前不生效(保留字段):写了会静默无效,必须说出来 ----
|
|
489
563
|
for i, ln in enumerate(lines):
|
|
490
564
|
if _f(ln.get("tail", 0.0)) > 0:
|
|
@@ -497,6 +571,32 @@ def check_cross(script, script_path, rep, ctx):
|
|
|
497
571
|
"tail_map)。例如 %s: 1.4" % lid),
|
|
498
572
|
"inert_line_tail")
|
|
499
573
|
|
|
574
|
+
# ---- v0.8 video 块:只支持 16:9 横屏 + 下采样输出 ----
|
|
575
|
+
# **不要在这里重写一遍规则**:允许列表 / 宽高比 / 上采样拒绝的权威实现在
|
|
576
|
+
# filmkit.video_plan()。以前这里是第二套手写判据,于是"1600x900"能过 filmcheck
|
|
577
|
+
# (0 error 0 warning)却在 render 的 st_layout 被 video_plan 拒片——校验器放行、
|
|
578
|
+
# 渲染器拒收,用户只会觉得门禁在骗人。这里改成直接调用权威实现,只做错误翻译。
|
|
579
|
+
vv = script.get("video") if isinstance(script.get("video"), dict) else None
|
|
580
|
+
if vv:
|
|
581
|
+
try:
|
|
582
|
+
import filmkit as _K
|
|
583
|
+
except Exception: # noqa: BLE001
|
|
584
|
+
_K = None
|
|
585
|
+
if _K is not None:
|
|
586
|
+
try:
|
|
587
|
+
_K.video_plan(script)
|
|
588
|
+
except ValueError as e:
|
|
589
|
+
rep.err("video", "%s(filmkit.video_plan 会在渲染时拒片,这里提前拦)" % e,
|
|
590
|
+
"bad_video")
|
|
591
|
+
except Exception as e: # noqa: BLE001
|
|
592
|
+
rep.err("video", "画幅解析失败:%s" % e, "bad_video")
|
|
593
|
+
else:
|
|
594
|
+
# filmkit 不可用(拷出来只带了 filmcheck)时退回最小校验:只查 fps 枚举,
|
|
595
|
+
# 并明说"完整校验需要 filmkit.py",避免让人以为已经全查过。
|
|
596
|
+
rep.warn("video", "找不到 filmkit.py:只做了 fps 枚举校验,"
|
|
597
|
+
"画幅/上采样规则未校验(完整校验请把 filmkit.py 放在同目录)",
|
|
598
|
+
"video_partial")
|
|
599
|
+
|
|
500
600
|
# ---- sfx.cues[].at 引用的场景 id 必须在 lines 里 ----
|
|
501
601
|
sfx = script.get("sfx") if isinstance(script.get("sfx"), dict) else {}
|
|
502
602
|
cues = sfx.get("cues") if isinstance(sfx.get("cues"), list) else []
|
|
@@ -629,6 +729,27 @@ def _load_styles():
|
|
|
629
729
|
return None
|
|
630
730
|
|
|
631
731
|
|
|
732
|
+
def _load_personas():
|
|
733
|
+
"""从 voicekit 单向读取 persona 名与 tone id;拿不到返回 (None, None) 降级成不查。
|
|
734
|
+
|
|
735
|
+
为什么要查:voicekit 对写错的 persona/tone 是**静默回落**(reason 里留
|
|
736
|
+
unknown_persona:xxx,但不报错、不告警),于是"拼错一个声线名"的后果是整片
|
|
737
|
+
静默换成 kind 默认声线——听感变了,却没有任何一处提示。schema 层只校验 t=str,
|
|
738
|
+
拦不住;这里补上(规则仍然只有一份:枚举从 voicekit 读,不复制)。
|
|
739
|
+
"""
|
|
740
|
+
try:
|
|
741
|
+
if str(HERE) not in sys.path:
|
|
742
|
+
sys.path.insert(0, str(HERE))
|
|
743
|
+
import voicekit # noqa: F401 (只读枚举,不复制)
|
|
744
|
+
personas = list(getattr(voicekit, "PERSONA_IDS", ()) or
|
|
745
|
+
getattr(voicekit, "VOICE_PERSONAS", {}).keys())
|
|
746
|
+
tones = list(getattr(voicekit, "TONE_IDS", ()) or
|
|
747
|
+
getattr(voicekit, "VOICE_TONES", {}).keys())
|
|
748
|
+
return (personas or None), (tones or None)
|
|
749
|
+
except Exception: # noqa: BLE001
|
|
750
|
+
return None, None
|
|
751
|
+
|
|
752
|
+
|
|
632
753
|
def _classify(data):
|
|
633
754
|
"""film = 像剧本;artifact = 其他产物(配乐报告等),跳过;unknown = 判不了。"""
|
|
634
755
|
if not isinstance(data, dict):
|