"""accept_pages.py: 渲图验收的 verdict 记录器(与 svg_preview.py / 导出 gate 配套)。 流程:svg_preview.py 渲图并登记 `.build/acceptance.json` → 逐页 read/look_at_image 过目 → 用本脚本把看过且合格的页标 pass(有问题的标 fail 并写原因)→ 全部 pass 后 svg_to_pptx.py 才肯导出。 用法: python accept_pages.py --status # 看各页验收状态 python accept_pages.py --pass P01_cover P02_toc python accept_pages.py --pass-all # 全部标 pass python accept_pages.py --fail P05_kpi --reason "大字压说明文字" 标 pass 的前提(逐页校验,不满足的页拒标并退非零): - 该页渲过图(acceptance.json 有记录、PNG 还在); - 渲图之后 svg_output 源没再改过(sha1 一致)—— 改了就得重跑 finalize + svg_preview 再来,盲改混不过 gate。 """ from __future__ import annotations import argparse import hashlib import sys try: # zcbot: Windows GBK 控制台兼容 sys.stdout.reconfigure(encoding="utf-8", errors="replace") sys.stderr.reconfigure(encoding="utf-8", errors="replace") except Exception: pass from datetime import datetime from pathlib import Path from svg_preview import acceptance_path, load_acceptance, save_acceptance def _sha1(path: Path) -> str: return hashlib.sha1(path.read_bytes()).hexdigest() def _page_problems(project_root: Path, stem: str, entry: dict) -> list[str]: """标 pass 前的逐页前提校验;返回问题列表(空 = 可标)。""" problems = [] png = Path(entry.get("png") or "") if not png.is_absolute(): png = project_root / png if not entry.get("png") or not png.exists(): problems.append("渲染 PNG 不存在 —— 先跑 svg_preview.py") src = project_root / "svg_output" / f"{stem}.svg" if not src.exists(): src = Path(entry.get("source") or "") if str(src) and not src.is_absolute(): src = project_root / src if not src.exists(): problems.append("找不到源 SVG") elif _sha1(src) != entry.get("source_sha1"): problems.append("渲图后源 SVG 又改过 —— 重跑 finalize_svg + svg_preview 再标") return problems def main() -> int: ap = argparse.ArgumentParser(description="标记渲图验收 verdict(配合导出 gate)") ap.add_argument("project_dir", type=Path) g = ap.add_mutually_exclusive_group(required=True) g.add_argument("--status", action="store_true", help="打印各页验收状态") g.add_argument("--pass", dest="mark_pass", nargs="+", metavar="PAGE", help="把指定页(文件名去 .svg)标为 pass") g.add_argument("--pass-all", action="store_true", help="把所有已渲页标为 pass(仍逐页校验 sha/PNG)") g.add_argument("--fail", dest="mark_fail", nargs="+", metavar="PAGE", help="把指定页标为 fail(配 --reason)") ap.add_argument("--reason", default="", help="fail 的原因,写进记录") args = ap.parse_args() root = args.project_dir if not root.is_dir(): print(f"[fatal] 不是目录:{root}") return 1 data = load_acceptance(root) pages: dict = data["pages"] svg_dir = root / "svg_output" all_stems = sorted(p.stem for p in svg_dir.glob("*.svg")) if svg_dir.is_dir() else [] unrendered = [s for s in all_stems if s not in pages] if args.status: if not pages and not all_stems: print(f"[status] 无验收记录也无 svg_output:{acceptance_path(root)}") return 1 counts = {"pass": 0, "fail": 0, "pending": 0} for stem in sorted(pages): e = pages[stem] v = e.get("verdict", "pending") counts[v if v in counts else "pending"] += 1 extra = "" if v == "pass" and _page_problems(root, stem, e): extra = " <- 源已改动/PNG 丢失,导出 gate 会拒(需重渲重标)" reason = f" ({e.get('reason')})" if e.get("reason") else "" print(f" {v:8s} {stem}{reason}{extra}") for stem in unrendered: print(f" {'-':8s} {stem} <- 从未渲过") print(f"[status] pass {counts['pass']} / fail {counts['fail']} / " f"pending {counts['pending']} / 未渲 {len(unrendered)}") return 0 now = datetime.now().isoformat(timespec="seconds") if args.mark_fail: missing = [s for s in args.mark_fail if s not in pages] if missing: print(f"[fatal] 无渲染记录的页:{', '.join(missing)}(先 svg_preview)") return 1 for stem in args.mark_fail: pages[stem].update(verdict="fail", verdict_at=now, reason=args.reason or "unspecified") print(f" [fail] {stem}" + (f":{args.reason}" if args.reason else "")) save_acceptance(root, data) return 0 targets = sorted(pages) if args.pass_all else args.mark_pass missing = [s for s in targets if s not in pages] if missing: print(f"[fatal] 无渲染记录的页:{', '.join(missing)}(先 svg_preview," "页名 = SVG 文件名去 .svg)") return 1 refused = 0 for stem in targets: problems = _page_problems(root, stem, pages[stem]) if problems: refused += 1 print(f" [refused] {stem}:{'; '.join(problems)}") continue pages[stem].update(verdict="pass", verdict_at=now) pages[stem].pop("reason", None) print(f" [pass] {stem}") save_acceptance(root, data) if unrendered and args.pass_all: print(f" [warn] {len(unrendered)} 页从未渲过、不在记录里:" f"{', '.join(unrendered[:8])}{' …' if len(unrendered) > 8 else ''}") if refused: print(f"[accept_pages] {refused} 页被拒 —— 重跑 finalize_svg + svg_preview " "后再标。") return 1 return 0 if __name__ == "__main__": raise SystemExit(main())