@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.
Files changed (39) hide show
  1. package/CHANGELOG.md +951 -0
  2. package/README.md +51 -8
  3. package/THIRD_PARTY_NOTICES.md +1 -0
  4. package/index.js +4 -1
  5. package/package.json +5 -3
  6. package/schemas/film.schema.json +162 -2
  7. package/skills/video-studio/SKILL.md +211 -11
  8. package/skills/video-studio/assets/golden_frames.json +65 -0
  9. package/skills/video-studio/assets/web/engine.js +63 -2
  10. package/skills/video-studio/assets/web/film.js +224 -101
  11. package/skills/video-studio/assets/web/index.html +4 -2
  12. package/skills/video-studio/assets/web/qa_text.js +36 -8
  13. package/skills/video-studio/references/audio-variation.md +4 -3
  14. package/skills/video-studio/references/audio.md +2 -2
  15. package/skills/video-studio/references/change-impact.md +1 -1
  16. package/skills/video-studio/references/quality-gates.md +25 -3
  17. package/skills/video-studio/references/style-packs.md +7 -3
  18. package/skills/video-studio/references/v08-quality.md +256 -0
  19. package/skills/video-studio/references/web-engine.md +19 -2
  20. package/skills/video-studio/references/word-timeline.md +147 -0
  21. package/skills/video-studio/requirements.txt +14 -0
  22. package/skills/video-studio/scripts/filmcheck.py +121 -0
  23. package/skills/video-studio/scripts/filmkit.py +521 -38
  24. package/skills/video-studio/scripts/gates.py +88 -4
  25. package/skills/video-studio/scripts/preflight.py +774 -0
  26. package/skills/video-studio/scripts/qa_frames.py +49 -8
  27. package/skills/video-studio/scripts/qaasr.py +1563 -0
  28. package/skills/video-studio/scripts/qavmaf.py +1442 -0
  29. package/skills/video-studio/scripts/render.py +719 -32
  30. package/skills/video-studio/scripts/scorekit.py +16 -30
  31. package/skills/video-studio/scripts/selfcheck.py +274 -0
  32. package/skills/video-studio/scripts/sfxkit.py +130 -14
  33. package/skills/video-studio/scripts/stagewatch.py +19 -6
  34. package/skills/video-studio/scripts/stylekit.py +43 -12
  35. package/skills/video-studio/scripts/voicekit.py +15 -7
  36. package/skills/video-studio/scripts/webengine.py +1119 -72
  37. package/tools/ci_check.py +171 -31
  38. package/tools/estimate.py +130 -19
  39. package/tools/golden_frames.py +664 -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
- st = x.get("status", SKIP)
335
- counts[st] = counts.get(st, 0) + 1
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))