@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
|
@@ -113,10 +113,13 @@ class GateLedger:
|
|
|
113
113
|
"""一次运行内的门禁账本(多 stage 共用;--stage all 时是同一个对象)。"""
|
|
114
114
|
|
|
115
115
|
def __init__(self, work, script=None, scenes_path="", script_path="", engine="pil",
|
|
116
|
-
web_entry=""):
|
|
116
|
+
web_entry="", run_id=""):
|
|
117
117
|
self.work = str(work or "work")
|
|
118
118
|
self.items = []
|
|
119
119
|
self.t0 = time.time()
|
|
120
|
+
# v0.8:run_id 是"这一版"的标签(--run-id)。没给就按时间戳生成,
|
|
121
|
+
# 保证每次运行在 work/runs/<runId>/ 下都有独立记录,而不是互相覆盖。
|
|
122
|
+
self.run_id = str(run_id or "").strip() or time.strftime("r%Y%m%d-%H%M%S")
|
|
120
123
|
self.script = script or {}
|
|
121
124
|
self.scenes_path = str(scenes_path or "")
|
|
122
125
|
self.script_path = str(script_path or "")
|
|
@@ -181,13 +184,30 @@ class GateLedger:
|
|
|
181
184
|
# ---- 落盘 ----
|
|
182
185
|
def to_dict(self):
|
|
183
186
|
d = dict(schema=SCHEMA, at=self.t0, contentHash=self.content_hash,
|
|
187
|
+
runId=self.run_id,
|
|
184
188
|
engine=self.engine, counts=self.counts(),
|
|
185
|
-
items=list(self.items), bypassed=list(self.bypassed)
|
|
189
|
+
items=list(self.items), bypassed=list(self.bypassed),
|
|
190
|
+
inputs=self.input_fingerprints())
|
|
186
191
|
if self.engine == "web":
|
|
187
192
|
# 审计用:让用户/CI 能看出这个指纹到底盖了哪些页面脚本(不进 hash,绝对路径不外泄)
|
|
188
193
|
d["webAssets"] = list(self.web_assets)
|
|
189
194
|
return d
|
|
190
195
|
|
|
196
|
+
def input_fingerprints(self):
|
|
197
|
+
"""输入指纹快照(v0.8):剧本 / scenes / 词级 / 本脚本。
|
|
198
|
+
|
|
199
|
+
为什么要单独记:contentHash 是"一个 16 位 hex",能判"变了没",但**说不出
|
|
200
|
+
变了哪个**。批量出片时最常见的问题是"这条用的还是上一版的词级时间戳",
|
|
201
|
+
有个逐项指纹才能在事后对账(也让 QT 报告能标出"本次跑的是哪一版输入")。
|
|
202
|
+
"""
|
|
203
|
+
return {
|
|
204
|
+
"script": _sha1_file(self.script_path) if self.script_path else "missing",
|
|
205
|
+
"scenes": _sha1_file(self.scenes_path) if self.scenes_path else "missing",
|
|
206
|
+
"words": _sha1_file(os.path.join(self.work, "words.json")),
|
|
207
|
+
"gates_py": _sha1_file(os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
|
208
|
+
"filmkit.py")),
|
|
209
|
+
}
|
|
210
|
+
|
|
191
211
|
def write_json(self):
|
|
192
212
|
"""work/gates.json —— 给程序读(QC-REPORT.md 只是它的渲染层)。"""
|
|
193
213
|
os.makedirs(self.work, exist_ok=True)
|
|
@@ -198,6 +218,37 @@ class GateLedger:
|
|
|
198
218
|
return p
|
|
199
219
|
|
|
200
220
|
|
|
221
|
+
def archive_run(work, ledger, artifacts=None, report_path=None):
|
|
222
|
+
"""把"这一版"的账本快照归档到 work/runs/<runId>/(v0.8)。
|
|
223
|
+
|
|
224
|
+
只归档**小文件**(gates.json / QC-REPORT.md / run.json)。视频/音频/分块体积大,
|
|
225
|
+
就地保留即可——这里要解决的是"跑了几版之后,谁都说不清上一版的门禁结论是什么",
|
|
226
|
+
不是备份媒体。返回 run 目录。
|
|
227
|
+
"""
|
|
228
|
+
d = os.path.join(str(work), "runs", str(ledger.run_id))
|
|
229
|
+
try:
|
|
230
|
+
os.makedirs(d, exist_ok=True)
|
|
231
|
+
except OSError as e: # noqa: BLE001
|
|
232
|
+
return "", "建目录失败:%s" % e
|
|
233
|
+
info = dict(runId=ledger.run_id, at=ledger.t0,
|
|
234
|
+
atText=time.strftime("%Y-%m-%d %H:%M:%S"),
|
|
235
|
+
contentHash=ledger.content_hash, counts=ledger.counts(),
|
|
236
|
+
inputs=ledger.input_fingerprints(),
|
|
237
|
+
artifacts=[str(x) for x in (artifacts or [])])
|
|
238
|
+
_atomic_json(os.path.join(d, "run.json"), info)
|
|
239
|
+
for src, name in ((os.path.join(str(work), "gates.json"), "gates.json"),
|
|
240
|
+
(str(report_path or ""), "QC-REPORT.md")):
|
|
241
|
+
try:
|
|
242
|
+
if src and os.path.exists(src):
|
|
243
|
+
with open(src, "rb") as fh:
|
|
244
|
+
data = fh.read()
|
|
245
|
+
with open(os.path.join(d, name), "wb") as fh:
|
|
246
|
+
fh.write(data)
|
|
247
|
+
except OSError:
|
|
248
|
+
pass
|
|
249
|
+
return d, ""
|
|
250
|
+
|
|
251
|
+
|
|
201
252
|
# ---------------------------------------------------------------- 签核账本
|
|
202
253
|
def signoff_path(work):
|
|
203
254
|
return os.path.join(str(work), "signoff.json")
|
|
@@ -330,9 +381,22 @@ def render_report(work, out=None):
|
|
|
330
381
|
ch = str(g.get("contentHash") or "")
|
|
331
382
|
items = list(g.get("items") or [])
|
|
332
383
|
counts = {OK: 0, FAIL: 0, SKIP: 0}
|
|
384
|
+
# 坏条目必须先归到"不通过"里,不能让它从三种计数里漏出去:
|
|
385
|
+
# 以前 st 不是 pass/fail/skipped 时 counts 只加了个 count[x],而结论分支只看
|
|
386
|
+
# counts[FAIL] / counts[SKIP] —— 于是 "status": "warn"(或大小写写错、null)
|
|
387
|
+
# 的条目在矩阵里照样列出来,结论却写"所有门禁本次都执行且通过"。
|
|
388
|
+
# 这正是本模块开头要防的"看起来干净、其实没在查",所以未知状态一律按 fail 计。
|
|
389
|
+
bad_items = 0
|
|
333
390
|
for x in items:
|
|
334
|
-
|
|
335
|
-
|
|
391
|
+
if not isinstance(x, dict):
|
|
392
|
+
bad_items += 1
|
|
393
|
+
continue
|
|
394
|
+
st = x.get("status")
|
|
395
|
+
if st not in (OK, FAIL, SKIP):
|
|
396
|
+
bad_items += 1
|
|
397
|
+
counts[FAIL] += 1
|
|
398
|
+
continue
|
|
399
|
+
counts[st] += 1
|
|
336
400
|
state, stale = signoff_state(work, ch)
|
|
337
401
|
signed_n = sum(1 for v in state.values() if v["status"] == OK)
|
|
338
402
|
|
|
@@ -346,11 +410,20 @@ def render_report(work, out=None):
|
|
|
346
410
|
L.append("| 项 | 值 |")
|
|
347
411
|
L.append("|---|---|")
|
|
348
412
|
L.append("| 内容指纹 | `%s` |" % (ch or "未生成"))
|
|
413
|
+
L.append("| 运行编号 | `%s` |" % (g.get("runId") or "-"))
|
|
349
414
|
L.append("| 引擎 | `%s` |" % (g.get("engine") or "-"))
|
|
415
|
+
_inp = g.get("inputs") or {}
|
|
416
|
+
if _inp:
|
|
417
|
+
L.append("| 输入指纹 | script `%s` · scenes `%s` · words `%s` |"
|
|
418
|
+
% (_inp.get("script", "-"), _inp.get("scenes", "-"),
|
|
419
|
+
_inp.get("words", "-")))
|
|
350
420
|
L.append("| 生成时间 | %s |"
|
|
351
421
|
% time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(g.get("at") or time.time())))
|
|
352
422
|
L.append("| 门禁合计 | pass %d · fail %d · skipped %d |"
|
|
353
423
|
% (counts.get(OK, 0), counts.get(FAIL, 0), counts.get(SKIP, 0)))
|
|
424
|
+
if bad_items:
|
|
425
|
+
L.append("| 账本损坏条目 | %d 条(status 不是 pass/fail/skipped)——按 fail 计 |"
|
|
426
|
+
% bad_items)
|
|
354
427
|
L.append("| 人工签核 | %d/%d 有效 |" % (signed_n, len(GATE_ORDER)))
|
|
355
428
|
L.append("")
|
|
356
429
|
L.append("## 一、门禁矩阵")
|
|
@@ -359,6 +432,12 @@ def render_report(work, out=None):
|
|
|
359
432
|
L.append("| 门禁 | 判据 | 结果 | 实测 / 说明 |")
|
|
360
433
|
L.append("|---|---|---|---|")
|
|
361
434
|
for x in items:
|
|
435
|
+
if not isinstance(x, dict):
|
|
436
|
+
# 坏条目不能让整份报告炸掉(.get 会 AttributeError,_finish_gates 只
|
|
437
|
+
# 打一行 WARN,交付目录里就没有 QC-REPORT.md 了):照实列成一行。
|
|
438
|
+
L.append("| `?` | (账本条目损坏) | **fail** | %r |"
|
|
439
|
+
% (str(x)[:60].replace("|", "\\|"),))
|
|
440
|
+
continue
|
|
362
441
|
L.append("| `%s` | %s | **%s** | %s |"
|
|
363
442
|
% (x.get("id", "?"), x.get("title", ""), x.get("status", "?"),
|
|
364
443
|
(x.get("detail") or "-").replace("|", "\\|")))
|
|
@@ -421,6 +500,11 @@ def render_report(work, out=None):
|
|
|
421
500
|
"产出真实门禁再交付。" % why)
|
|
422
501
|
elif counts.get(FAIL):
|
|
423
502
|
L.append("- **未通过**:fail %d 项,按上表逐条修,别宣称完成。" % counts.get(FAIL))
|
|
503
|
+
elif bad_items:
|
|
504
|
+
# 条目损坏 / 状态值不认识:既不能算"通过",也不该说成"查了没过"。
|
|
505
|
+
L.append("- **未判定**:账本里有 %d 条损坏的记录(不是合法 dict 或 status 不是 "
|
|
506
|
+
"pass/fail/skipped),无法判断这些门禁到底查没查、过没过。"
|
|
507
|
+
"**这不等于通过**——重跑 --stage qa 重新产出账本。" % bad_items)
|
|
424
508
|
elif counts.get(SKIP):
|
|
425
509
|
L.append("- **有条件通过**:skipped %d 项(明确没执行),交付时必须逐项说明原因。"
|
|
426
510
|
% counts.get(SKIP))
|