Spaces:
Sleeping
Sleeping
Download skill/scripts/validate_release.py from cpuai/ahp-development-workstation: direct link, hf CLI and curl.
- Browser
- Download file 6.72 kB
-
https://huggingface.co/spaces/cpuai/ahp-development-workstation/resolve/main/skill/scripts/validate_release.py
- Command line
-
hf download hf://spaces/cpuai/ahp-development-workstation/skill/scripts/validate_release.py
-
curl -L -o validate_release.py https://huggingface.co/spaces/cpuai/ahp-development-workstation/resolve/main/skill/scripts/validate_release.py
6.72 kB
| #!/usr/bin/env python3 | |
| # -*- coding: utf-8 -*- | |
| """用 ffprobe 检查成品,生成报告并创建独立、无密钥的源码包和完整发布包。""" | |
| import argparse | |
| import hashlib | |
| import json | |
| import re | |
| import subprocess | |
| import zipfile | |
| from pathlib import Path | |
| ROOT = Path(__file__).resolve().parents[1] | |
| STORY = json.loads((ROOT / "story.json").read_text(encoding="utf-8")) | |
| TIMING = json.loads((ROOT / "timing.json").read_text(encoding="utf-8")) | |
| OUT = ROOT / "output" | |
| VIDEO = OUT / f"{STORY['slug']}.mp4" | |
| def parse_probe(path: Path) -> dict: | |
| stdout = subprocess.check_output(["ffprobe", "-v", "error", "-show_format", "-show_streams", | |
| "-of", "json", str(path)], text=True) | |
| return json.loads(stdout) | |
| def inspect() -> dict: | |
| if not VIDEO.is_file() or VIDEO.stat().st_size < 100_000: | |
| raise FileNotFoundError(f"真正渲染的视频不存在或不完整: {VIDEO}") | |
| data = parse_probe(VIDEO) | |
| video = next((s for s in data["streams"] if s["codec_type"] == "video"), None) | |
| audio = next((s for s in data["streams"] if s["codec_type"] == "audio"), None) | |
| duration = float(data["format"]["duration"]) | |
| width = int(STORY["video"]["width"]) | |
| height = int(STORY["video"]["height"]) | |
| fps = int(STORY["video"]["fps"]) | |
| actual_fps = (video.get("avg_frame_rate") or video.get("r_frame_rate") or "0/1") if video else "0/1" | |
| values = actual_fps.split("/") | |
| actual_rate = float(values[0]) / max(float(values[1]), .0001) | |
| caption_file = OUT / f"{STORY['slug']}.srt" | |
| snapshots = list((OUT / "snapshots").glob("*.png")) | |
| snapshots += list((OUT / "snapshots-fixed").glob("*.png")) | |
| technical = bool( | |
| video and audio and video.get("codec_name") == "h264" and | |
| audio.get("codec_name") == "aac" and int(video["width"]) == width and | |
| int(video["height"]) == height and abs(actual_rate - fps) < .1 and | |
| abs(duration - float(TIMING["duration"])) < .12 and | |
| caption_file.is_file() and caption_file.stat().st_size > 10 | |
| ) | |
| from narrate import validate_story, clean | |
| subtitles = json.loads((ROOT/'captions.json').read_text(encoding='utf-8')) | |
| expected = clean(''.join(line['speech'] for group in validate_story(STORY) for line in group)) | |
| emitted = clean(''.join(c.get('text','') for c in subtitles)) | |
| exact_subtitles = bool(expected == emitted and len(subtitles)>0 and | |
| all(c.get('precise_tts_boundaries') is True for c in subtitles) and | |
| TIMING.get('verbatim_captions') is True and not TIMING.get('warnings')) | |
| if not exact_subtitles: | |
| raise ValueError('配音原稿与字幕文字/词边界不一致,拒绝打包。请重新生成真实配音字幕。') | |
| has_snapshots = len(snapshots) >= 3 | |
| has_real_voice = not TIMING.get("mock", False) | |
| h = hashlib.sha256() | |
| with VIDEO.open("rb") as f: | |
| for chunk in iter(lambda: f.read(2 * 1024 * 1024), b""): | |
| h.update(chunk) | |
| result = { | |
| "file": str(VIDEO), "bytes": VIDEO.stat().st_size, "sha256": h.hexdigest(), | |
| "duration": round(duration, 6), "width": video.get("width") if video else None, | |
| "height": video.get("height") if video else None, "fps": actual_fps, | |
| "video_codec": video.get("codec_name") if video else None, | |
| "audio_codec": audio.get("codec_name") if audio else None, | |
| "audio_rate": audio.get("sample_rate") if audio else None, | |
| "snapshot_count": len(snapshots), "tts_mock": TIMING.get("mock", False), | |
| "subtitle_verbatim_pass": exact_subtitles, | |
| "technical_pass": technical, | |
| "has_story_snapshots": has_snapshots, | |
| "manual_science_review_required": True, | |
| "qa_pass": bool(technical and has_snapshots and has_real_voice), | |
| "manual_review": "required: watch video and listen before classroom use", | |
| "warnings": TIMING.get("warnings", []) | |
| } | |
| OUT.mkdir(exist_ok=True) | |
| (OUT / "validation.json").write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8") | |
| return result | |
| def eligible_files(full: bool): | |
| allowed = ["README.md", "requirements.txt", "story.json", "timing.json", "captions.json", "meta.json", | |
| "package.json", "index.html"] | |
| found = [ROOT / n for n in allowed if (ROOT / n).is_file()] | |
| for dirname in ("scripts", "assets"): | |
| found.extend(p for p in (ROOT / dirname).rglob("*") if p.is_file() and "__pycache__" not in p.parts) | |
| found.extend([OUT / f"{STORY['slug']}.srt", OUT / "validation.json"]) | |
| if full: | |
| found.append(VIDEO) | |
| for sn in ("snapshots", "snapshots-fixed"): | |
| found.extend(p for p in (OUT / sn).glob("*.png") if p.is_file()) | |
| for p in found: | |
| if (p.is_file() and not p.name.startswith(".env") and p.suffix.lower() not in | |
| (".pyc", ".pem", ".key", ".pfx", ".ttf", ".otf", ".woff", ".woff2") and ".git" not in p.parts): | |
| yield p | |
| def pack_release() -> list[dict]: | |
| packages = [] | |
| for full in (False, True): | |
| target = OUT / f"{STORY['slug']}-{'complete' if full else 'source'}.zip" | |
| members = list(eligible_files(full)) | |
| with zipfile.ZipFile(target, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=6) as z: | |
| for p in members: | |
| z.write(p, (ROOT.name + "/" + p.relative_to(ROOT).as_posix())) | |
| with zipfile.ZipFile(target) as z: | |
| if z.testzip() is not None: | |
| raise ValueError(f"损坏的 ZIP: {target}") | |
| packages.append({"file": str(target), "size_bytes": target.stat().st_size, | |
| "entries": len(members)}) | |
| return packages | |
| def main() -> None: | |
| parser = argparse.ArgumentParser(description="检查成片、生成 validation.json、打包") | |
| parser.add_argument("--inspect-only", action="store_true", help="只检查和输出报告,不打包") | |
| opts = parser.parse_args() | |
| result = inspect() | |
| print(json.dumps(result, ensure_ascii=False, indent=2)) | |
| if not result["qa_pass"]: | |
| issues = [] | |
| if not result["technical_pass"]: | |
| issues.append("ffprobe 媒体规格/时长/字幕文件错误") | |
| if not result["has_story_snapshots"]: | |
| issues.append("缺少至少3张视频关键帧") | |
| if result["tts_mock"]: | |
| issues.append("仍使用模拟配音,禁止发布") | |
| raise SystemExit("不能发布:" + ";".join(issues)) | |
| if not opts.inspect_only: | |
| print(json.dumps({"packages": pack_release()}, ensure_ascii=False, indent=2)) | |
| print("技术检查通过;仍需人工完整观看试听后才能作为课堂正式成品。") | |
| if __name__ == "__main__": | |
| main() | |