@dsh-gis/dsh-video-studio-skill 0.9.1 → 1.0.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/CHANGELOG.md +424 -0
- package/README.md +287 -327
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/package.json +9 -10
- package/schemas/design.schema.json +722 -0
- package/schemas/film.schema.json +4 -15
- package/schemas/proposal.schema.json +133 -0
- package/skills/video-studio/SKILL.md +1316 -1123
- package/skills/video-studio/assets/design_fixture.json +141 -0
- package/skills/video-studio/assets/film.json +1 -2
- package/skills/video-studio/assets/golden_frames.json +11 -8
- package/skills/video-studio/assets/scenes.py +67 -22
- package/skills/video-studio/assets/voice_loudness.json +16 -0
- package/skills/video-studio/references/audio-variation.md +15 -11
- package/skills/video-studio/references/audio.md +36 -34
- package/skills/video-studio/references/change-impact.md +3 -3
- package/skills/video-studio/references/frame-qa.md +2 -2
- package/skills/video-studio/references/proposal.md +177 -0
- package/skills/video-studio/references/quality-gates.md +1 -1
- package/skills/video-studio/references/scene-grammar.md +28 -23
- package/skills/video-studio/references/sfx-design.md +1 -1
- package/skills/video-studio/references/style-packs.md +95 -257
- package/skills/video-studio/references/v08-quality.md +8 -8
- package/skills/video-studio/references/web-engine.md +1 -1
- package/skills/video-studio/references/word-timeline.md +2 -2
- package/skills/video-studio/scripts/designkit.py +2120 -0
- package/skills/video-studio/scripts/filmcheck.py +7 -33
- package/skills/video-studio/scripts/filmkit.py +153 -78
- package/skills/video-studio/scripts/grammar.py +67 -7
- package/skills/video-studio/scripts/musicgen.py +2043 -0
- package/skills/video-studio/scripts/preflight.py +10 -3
- package/skills/video-studio/scripts/previewkit.py +849 -0
- package/skills/video-studio/scripts/propose.py +825 -0
- package/skills/video-studio/scripts/qa_frames.py +45 -10
- package/skills/video-studio/scripts/qavmaf.py +77 -11
- package/skills/video-studio/scripts/render.py +472 -154
- package/skills/video-studio/scripts/scorekit.py +50 -1748
- package/skills/video-studio/scripts/selfcheck.py +658 -131
- package/skills/video-studio/scripts/stagewatch.py +21 -5
- package/skills/video-studio/scripts/stylekit.py +265 -741
- package/skills/video-studio/scripts/voicekit.py +502 -380
- package/skills/video-studio/scripts/webengine.py +15 -14
- package/tools/audio_similarity.py +57 -30
- package/tools/ci_check.py +7 -1
- package/tools/estimate.py +14 -8
- package/tools/golden_frames.py +56 -11
- package/tools/silhouette_qa.py +521 -168
- package/docs/demo-pil-sheet.png +0 -0
- package/docs/styles/demo-results.json +0 -76
- package/docs/styles/sheet-8-styles.png +0 -0
- package/docs/styles/sheet-ai.png +0 -0
- package/docs/styles/sheet-before-after.png +0 -0
- package/docs/styles/sheet-craft.png +0 -0
- package/docs/styles/sheet-night.png +0 -0
- package/skills/video-studio/assets/score_demo.json +0 -307
|
@@ -81,6 +81,10 @@ FLICKER_ISO = 1.5 # 孤立性比值:当前帧异常度 >= 1.5 x 前后
|
|
|
81
81
|
# "回到原位"闸门:|F[t+1]-F[t-1]| 必须小于 1.6 x 中间帧偏离量。
|
|
82
82
|
# 单帧闪烁 ≈ 0(前后几乎相同);加速运动 ≈ 2 以上(前后差更多)→ 排除。
|
|
83
83
|
FLICKER_RETURN_RATIO = 1.6
|
|
84
|
+
# 符号一致度下限:块内像素"同向"变化的比例。真闪白/闪黑整块同向(0.75–1.0);
|
|
85
|
+
# 文字入场 / 母题动画 / 镜头缓推是有增有减的纹理变化(实测 0.42)。
|
|
86
|
+
# 低于此值不判 error(降级 warning 供人复核)——v1.0.2 修掉零拷贝首片的 G9 误杀。
|
|
87
|
+
FLICKER_AGREE_MIN = 0.60
|
|
84
88
|
# 与已登记视觉事件(闪白/震屏/冲击环)重合多久内视为"设计好的重击",不计 error
|
|
85
89
|
FLICKER_EXPLAIN_WIN = 0.2
|
|
86
90
|
# 转化为"可解释设计事件"的 event.kind(小写匹配;cues.json §4.2 的 events 由页面导出)
|
|
@@ -366,11 +370,16 @@ def sample_frames(path, w=160, h=90, from_s=0.0, to_s=None) -> np.ndarray:
|
|
|
366
370
|
return np.stack(frames, axis=0)
|
|
367
371
|
|
|
368
372
|
|
|
369
|
-
def _block_max(diff, block) -> tuple
|
|
370
|
-
"""把 (h,w) 差异图按 block×block 分块取均值最大,返回 (最大均值, bbox,
|
|
373
|
+
def _block_max(diff, block, signed=None) -> tuple:
|
|
374
|
+
"""把 (h,w) 差异图按 block×block 分块取均值最大,返回 (最大均值, bbox, 该块中值, 符号一致度)。
|
|
371
375
|
|
|
372
376
|
中值是复核线索:整块一起变亮(真单帧事件)时中值也高;只有小块元素在动时
|
|
373
377
|
均值被少数像素拉高、中值仍接近 0(见 references/frame-qa.md 的 16.933 s 案例)。
|
|
378
|
+
|
|
379
|
+
v1.0.2 加符号一致度(signed 传带符号的二阶差分时才算):整块**同向**变亮/变暗
|
|
380
|
+
= 真闪白/闪黑;文字逐字入场、母题动画、镜头缓推产生的只是**有增有减**的纹理变化,
|
|
381
|
+
符号一致度低。实测(v1.0.2 零拷贝首片):t=5.867s 被误报的块 score 51.5 / median 44.5,
|
|
382
|
+
但符号一致度只有 0.42;同一位置人为注入 +50 灰闪白后一致度 0.75。
|
|
374
383
|
"""
|
|
375
384
|
h, w = diff.shape
|
|
376
385
|
b = max(1, int(block))
|
|
@@ -387,7 +396,14 @@ def _block_max(diff, block) -> tuple[float, tuple[int, int, int, int], float]:
|
|
|
387
396
|
bb = (x0, y0, x1, y1)
|
|
388
397
|
blk = diff[y0:y1, x0:x1]
|
|
389
398
|
med = float(np.median(blk)) if blk.size else 0.0
|
|
390
|
-
|
|
399
|
+
agree = None
|
|
400
|
+
if isinstance(signed, np.ndarray) and signed.shape == diff.shape:
|
|
401
|
+
sblk = signed[y0:y1, x0:x1].astype(np.float64)
|
|
402
|
+
if sblk.size:
|
|
403
|
+
m = float(sblk.mean())
|
|
404
|
+
# 与"块内平均方向"同向的像素占比:整块同向变亮 ≈ 1.0,纹理/运动互有增减 ≈ 0.5
|
|
405
|
+
agree = float(np.mean(np.sign(sblk) == np.sign(m))) if m != 0 else 0.0
|
|
406
|
+
return float(means[r, c]), bb, med, agree
|
|
391
407
|
|
|
392
408
|
|
|
393
409
|
def _block_peak(diff, block) -> float:
|
|
@@ -434,10 +450,11 @@ def _scan_full(path, w=SAMPLE_W, h=SAMPLE_H, block=FLICKER_BLOCK, fps=None):
|
|
|
434
450
|
if len(history) > lag:
|
|
435
451
|
history.pop(0)
|
|
436
452
|
if prev2 is not None:
|
|
437
|
-
|
|
438
|
-
|
|
453
|
+
signed = (prev1 - (prev2 + fr) * 0.5) * 255.0
|
|
454
|
+
diff = np.abs(signed)
|
|
455
|
+
s, bb, med, agree = _block_max(diff, block, signed=signed)
|
|
439
456
|
scores.append(float(s))
|
|
440
|
-
boxes.append((bb, med))
|
|
457
|
+
boxes.append((bb, med, agree))
|
|
441
458
|
# 同一个块上算"前后两帧彼此差多少",用于区分闪烁与加速运动
|
|
442
459
|
if bb:
|
|
443
460
|
x0, y0, x1, y1 = bb
|
|
@@ -502,7 +519,9 @@ def _flicker_from_scores(scores, boxes, fps, thresh, returns=None, suppressed=No
|
|
|
502
519
|
continue
|
|
503
520
|
if s < FLICKER_ISO * max(p, q):
|
|
504
521
|
continue
|
|
505
|
-
|
|
522
|
+
box = boxes[t - 1] if 0 <= t - 1 < len(boxes) else ((0, 0, 0, 0), 0.0, None)
|
|
523
|
+
bb, med = box[0], box[1]
|
|
524
|
+
agree = box[2] if len(box) > 2 else None
|
|
506
525
|
r = None
|
|
507
526
|
if returns is not None and t < len(returns):
|
|
508
527
|
r = float(returns[t])
|
|
@@ -522,6 +541,7 @@ def _flicker_from_scores(scores, boxes, fps, thresh, returns=None, suppressed=No
|
|
|
522
541
|
"bbox": [int(v) for v in bb],
|
|
523
542
|
"ratio": round(s / max(p, q, 1e-9), 2),
|
|
524
543
|
"median": round(float(med), 2),
|
|
544
|
+
**({"agree": round(float(agree), 2)} if agree is not None else {}),
|
|
525
545
|
**({"return": round(r, 2)} if r is not None and math.isfinite(r) else {})})
|
|
526
546
|
return out
|
|
527
547
|
|
|
@@ -1079,13 +1099,28 @@ def report(film, *, cues_json=None, expect_duration=None, out_json=None, out_png
|
|
|
1079
1099
|
fl["expected"] = any(abs(fl["t"] - te) <= FLICKER_EXPLAIN_WIN for te in explained_times)
|
|
1080
1100
|
|
|
1081
1101
|
errors = []
|
|
1102
|
+
textured = []
|
|
1082
1103
|
for fl in flicker:
|
|
1083
1104
|
if fl.get("expected"):
|
|
1084
1105
|
continue
|
|
1085
1106
|
bb = fl.get("bbox") or [0, 0, 0, 0]
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1107
|
+
msg = ("t=%.3fs score=%.1f(孤立比值 %.2f,块中值 %.2f,异常块 %d,%d–%d,%d,阈值 %.1f)"
|
|
1108
|
+
% (fl["t"], fl["score"], fl.get("ratio", 0.0), fl.get("median", 0.0),
|
|
1109
|
+
bb[0], bb[1], bb[2], bb[3], FLICKER_THRESH))
|
|
1110
|
+
# v1.0.2:符号一致度 = 块内像素是否**同向**变亮/变暗。
|
|
1111
|
+
# 真闪白/闪黑整块同向(实测 0.75–1.0);文字逐字入场、母题动画、镜头缓推
|
|
1112
|
+
# 是"有增有减"的纹理变化(实测 0.42)。后者以前会被判成闪烁,让新用户首片
|
|
1113
|
+
# 就 fail 在 G9(零拷贝自测实测:t=5.867s 标题逐字入场被误判)。
|
|
1114
|
+
# 处理:低于阈值不静默放过,降级为 warning 并保留全部数值,交给人工复核。
|
|
1115
|
+
fl["sign_agree"] = fl.get("agree")
|
|
1116
|
+
if fl.get("agree") is not None and float(fl["agree"]) < FLICKER_AGREE_MIN:
|
|
1117
|
+
textured.append(msg + (",符号一致度 %.2f < %.2f → 判为纹理/动画扰动"
|
|
1118
|
+
% (float(fl["agree"]), FLICKER_AGREE_MIN)))
|
|
1119
|
+
continue
|
|
1120
|
+
errors.append("单帧闪烁 " + msg)
|
|
1121
|
+
for msg in textured:
|
|
1122
|
+
warnings.append("疑似非闪烁(块内像素有增有减,像是文字/母题动画):" + msg
|
|
1123
|
+
+ ";若画面确有单帧闪白,调低 FLICKER_AGREE_MIN 或登记设计事件")
|
|
1089
1124
|
explained = sum(1 for fl in flicker if fl.get("expected"))
|
|
1090
1125
|
if explained:
|
|
1091
1126
|
warnings.append("已排除 %d 处与已登记视觉事件(闪白/震屏/冲击环)重合的单帧闪烁;"
|
|
@@ -458,7 +458,27 @@ def _looks_legacy(script) -> bool:
|
|
|
458
458
|
|
|
459
459
|
|
|
460
460
|
def _style_plan(script) -> dict:
|
|
461
|
-
"""
|
|
461
|
+
"""解析剧本风格计划:**design 优先**,其次 script["_style"],最后 stylekit。
|
|
462
|
+
|
|
463
|
+
为什么必须先看 design:v1.0 之后"这一版长什么样"由 work/design.json 决定,
|
|
464
|
+
而 qavmaf 是**独立进程**——它拿到的 script 里没有 render 塞进去的 _style。
|
|
465
|
+
早先这里直接调 stylekit,于是 design 路径的片子会被解析成 tech-deep(默认风格),
|
|
466
|
+
参考帧用一套主题、成片用另一套:实测 VMAF 1.50、同源抽查 MAE=0.0384。
|
|
467
|
+
这不是"画质劣化",是**参考帧根本不是同一部片**。
|
|
468
|
+
"""
|
|
469
|
+
# ① design(与 render._plan_from_design 同一口径)
|
|
470
|
+
design = script.get("design") if isinstance(script, dict) else None
|
|
471
|
+
if isinstance(design, dict) and design:
|
|
472
|
+
import designkit as DK
|
|
473
|
+
bad = DK.validate_design(design)
|
|
474
|
+
if bad:
|
|
475
|
+
raise LookupError("design.json 没过校验,参考帧无法与成片同源:%s" % ";".join(bad[:3]))
|
|
476
|
+
return DK.design_to_plan(design)
|
|
477
|
+
# ② 调用方已经解析过(extract_frames 会塞进来)
|
|
478
|
+
cached = script.get("_style") if isinstance(script, dict) else None
|
|
479
|
+
if isinstance(cached, dict) and cached:
|
|
480
|
+
return cached
|
|
481
|
+
# ③ 老路径:stylekit
|
|
462
482
|
try:
|
|
463
483
|
import stylekit as ST
|
|
464
484
|
except Exception as e: # noqa: BLE001
|
|
@@ -563,10 +583,17 @@ def _prepare_script(script, tl):
|
|
|
563
583
|
"""
|
|
564
584
|
sc = dict(script) # 浅拷贝:不改调用方的对象
|
|
565
585
|
sc["_total"] = float(_f(tl.get("total"), 0.0))
|
|
566
|
-
|
|
567
|
-
|
|
586
|
+
# design 必须**优先于**缓存的 _style:调用方(render.st_qa)可能已经用一个
|
|
587
|
+
# 错误的 _style 污染过 script —— 它自己解析时没传 work、读不到 design.json,
|
|
588
|
+
# 于是 _style 是 stylekit 的 auto 结果。看到 _style 就直接用,等于沿用那个错误。
|
|
589
|
+
if isinstance(sc.get("design"), dict) and sc["design"]:
|
|
568
590
|
plan = _style_plan(sc)
|
|
569
591
|
sc["_style"] = plan
|
|
592
|
+
else:
|
|
593
|
+
plan = sc.get("_style")
|
|
594
|
+
if not isinstance(plan, dict):
|
|
595
|
+
plan = _style_plan(sc)
|
|
596
|
+
sc["_style"] = plan
|
|
570
597
|
legacy = bool((plan or {}).get("legacy"))
|
|
571
598
|
if legacy:
|
|
572
599
|
return sc, None
|
|
@@ -577,22 +604,48 @@ def _prepare_script(script, tl):
|
|
|
577
604
|
return sc, theme
|
|
578
605
|
|
|
579
606
|
|
|
580
|
-
def
|
|
581
|
-
"""
|
|
607
|
+
def _sub_kw_for(script) -> dict:
|
|
608
|
+
"""从已解析的风格计划里取字幕样式参数(v1.0.2,与 render.py:_sub_kw 同口径)。
|
|
609
|
+
|
|
610
|
+
⚠ 为什么参考帧必须传这些参数(实测的**误杀好片**缺陷):
|
|
611
|
+
render.py 的 frames / stills 阶段会传 design 的 visual.subtitle(size/align/
|
|
612
|
+
plate_alpha/radius/max_lines/family/y);参考帧不传就永远是 filmkit 的模块
|
|
613
|
+
默认值(size=46 等)。同一部片(设计 size=52 / radius=14 / plate_alpha=180)
|
|
614
|
+
实测 VMAF 91.33 —— 被阈值 92 判 fail,而两条路径用同一份参数时 ≥96。
|
|
615
|
+
门禁误杀好片比漏查更有害,所以这里必须与主渲染路径吃同一份参数。
|
|
616
|
+
|
|
617
|
+
实现在 filmkit.subtitle_kwargs(**不复制第二份**:两份迟早漂移);
|
|
618
|
+
老版 filmkit 没有这个函数时退回空 dict(= 旧行为,跨版本兼容)。
|
|
619
|
+
"""
|
|
620
|
+
plan = script.get("_style") if isinstance(script, dict) else None
|
|
621
|
+
fn = getattr(_K, "subtitle_kwargs", None)
|
|
622
|
+
if fn is None: # 老 filmkit:还没有字幕样式参数
|
|
623
|
+
return {}
|
|
624
|
+
try:
|
|
625
|
+
kw = fn(plan)
|
|
626
|
+
except Exception: # noqa: BLE001 —— 样式解析失败不该挡住整片门禁
|
|
627
|
+
return {}
|
|
628
|
+
return kw if isinstance(kw, dict) else {}
|
|
629
|
+
|
|
582
630
|
|
|
583
|
-
|
|
631
|
+
def _draw_subtitle_like_render(frame, text, theme, words, kw=None):
|
|
632
|
+
"""烧字幕,与 render.py 同口径(v0.8 带 words 词边界折行,v1.0.2 带设计字幕样式)。
|
|
633
|
+
|
|
634
|
+
⚠ 参考帧不传 words 也是**会误杀好片**的缺陷(实测):
|
|
584
635
|
render.py 的 frames / stills 阶段调 draw_subtitle 时总会传 words,
|
|
585
636
|
字幕会按词边界折行("…整整 / 一句旁白");参考帧不传就退回字符折行
|
|
586
637
|
("…整整一 / 句旁白")。38 字字幕只差一个断点、约 2% 像素不同,
|
|
587
638
|
但近静态画面的 VMAF 从 99.95 掉到 8.55 —— 门禁会把同源的好片判 fail。
|
|
588
|
-
|
|
639
|
+
kw 是 design 的字幕样式(见 _sub_kw_for);老版 filmkit 的 draw_subtitle
|
|
640
|
+
没有 words 形参,TypeError 时退回旧调用(保留)。
|
|
589
641
|
"""
|
|
642
|
+
kw = kw or {}
|
|
590
643
|
if not words:
|
|
591
|
-
return _K.draw_subtitle(frame, text, theme=theme)
|
|
644
|
+
return _K.draw_subtitle(frame, text, theme=theme, **kw)
|
|
592
645
|
try:
|
|
593
|
-
return _K.draw_subtitle(frame, text, theme=theme, words=words)
|
|
646
|
+
return _K.draw_subtitle(frame, text, theme=theme, words=words, **kw)
|
|
594
647
|
except TypeError: # 老 filmkit:没有 words 形参(保持旧行为)
|
|
595
|
-
return _K.draw_subtitle(frame, text, theme=theme)
|
|
648
|
+
return _K.draw_subtitle(frame, text, theme=theme, **kw)
|
|
596
649
|
|
|
597
650
|
|
|
598
651
|
def _draw_ref_frame(scenes, scenes_mod, script, theme, fi, fps, total):
|
|
@@ -625,7 +678,9 @@ def _draw_ref_frame(scenes, scenes_mod, script, theme, fi, fps, total):
|
|
|
625
678
|
words = fn(scenes, t, hold) if fn is not None else None
|
|
626
679
|
except Exception: # noqa: BLE001 —— 词表异常不该让参考帧渲不出来
|
|
627
680
|
words = None
|
|
628
|
-
|
|
681
|
+
# 字幕样式也必须与主渲染路径同源(v1.0.2;见 _sub_kw_for 的实测说明)
|
|
682
|
+
frame = _draw_subtitle_like_render(frame, text, theme, words,
|
|
683
|
+
kw=_sub_kw_for(script))
|
|
629
684
|
fade = 1.0
|
|
630
685
|
if t < 0.5:
|
|
631
686
|
fade *= _K.ease_out(t / 0.5)
|
|
@@ -1229,6 +1284,17 @@ def draw_frame(scene, t_local, dur, t_global, script, frame):
|
|
|
1229
1284
|
fp.write(scenes_src)
|
|
1230
1285
|
script = {"title": "qavmaf selftest", "lines": [
|
|
1231
1286
|
{"id": "l01", "text": "一", "sub": "自检"}]}
|
|
1287
|
+
# v1.0.1 起没有内置风格表:合成剧本也必须自带一份**现场生成**的设计,
|
|
1288
|
+
# 否则 _style_plan() 解析不出主题("不写 style 就走老外观"的兼容路径已移除),
|
|
1289
|
+
# 参考帧无法渲染。这里用 designkit 现场采样(seed 固定 = 可复现)、
|
|
1290
|
+
# design_to_plan 与 render 同一口径,**不引入任何写死主题/模板**。
|
|
1291
|
+
try:
|
|
1292
|
+
import designkit as _DK
|
|
1293
|
+
_design = _DK.sample_designs({"topic": "qavmaf selftest"}, n=1, seed=1)[0]
|
|
1294
|
+
script["_style"] = _DK.design_to_plan(_design)
|
|
1295
|
+
except Exception as e: # noqa: BLE001 —— 缺 designkit 也要给出可读的 FAIL
|
|
1296
|
+
print("[FAIL] 自检需要 designkit.py 现场生成设计(v1.0.1 无内置风格):%s" % e)
|
|
1297
|
+
return 1
|
|
1232
1298
|
scenes = [dict(script["lines"][0], start=0.0, end=n / float(fps),
|
|
1233
1299
|
dur=n / float(fps), voice_start=0.0, voice_end=n / float(fps))]
|
|
1234
1300
|
tl = {"total": n / float(fps), "fps": fps, "frames": n, "scenes": scenes,
|