Fast_deploy / ComfyUI-MiniMaxH3-Context-Loop /ComfyUI-MAINodes /tools /compare_deck /make_manifest.py
Download ComfyUI-MiniMaxH3-Context-Loop/ComfyUI-MAINodes/tools/compare_deck/make_manifest.py from dzmdd/Fast_deploy: direct link, hf CLI and curl.
- Browser
- Download file 7.91 kB
-
https://huggingface.co/dzmdd/Fast_deploy/resolve/main/ComfyUI-MiniMaxH3-Context-Loop/ComfyUI-MAINodes/tools/compare_deck/make_manifest.py
- Command line
-
hf download hf://dzmdd/Fast_deploy/ComfyUI-MiniMaxH3-Context-Loop/ComfyUI-MAINodes/tools/compare_deck/make_manifest.py
-
curl -L -o make_manifest.py https://huggingface.co/dzmdd/Fast_deploy/resolve/main/ComfyUI-MiniMaxH3-Context-Loop/ComfyUI-MAINodes/tools/compare_deck/make_manifest.py
7.91 kB
| #!/usr/bin/env python3 | |
| """Starter manifest for build_compare_page.py, from a directory of mp4s. | |
| Point it at a folder of clips and it writes the boring half of a manifest: | |
| one row per clip, arm from the filename stem, src (and poster) paths, plus | |
| fps, duration and aspect probed with ffprobe. Metrics merge in by stem from | |
| either a directory of one-JSON-per-clip or a single CSV. | |
| It does NOT invent groups or descriptions. Both come out as empty strings | |
| for a human to fill, because the grouping is the argument the deck is | |
| making and a guessed one is worse than a blank. | |
| python3 make_manifest.py CLIPS_DIR manifest.json \\ | |
| --title "de-rope block sweep" \\ | |
| --posters posters/ --metrics metrics.csv \\ | |
| --src-prefix media/derope --poster-prefix posters | |
| Paths in the manifest are `<prefix>/<filename>`. The built page resolves | |
| them relative to the HTML file, so pass the prefix as the page will see it | |
| (default: the CLIPS_DIR / --posters values exactly as typed). | |
| """ | |
| import argparse, csv, json, os, subprocess, sys | |
| def probe(path): | |
| """(fps, duration_s, width, height) for one file, via ffprobe.""" | |
| cmd = ["ffprobe", "-v", "error", "-select_streams", "v:0", | |
| "-show_entries", "stream=r_frame_rate,width,height", | |
| "-show_entries", "format=duration", "-of", "json", path] | |
| try: | |
| raw = subprocess.run(cmd, capture_output=True, text=True, | |
| check=True).stdout | |
| except FileNotFoundError: | |
| sys.exit("ffprobe not found on PATH; it is required") | |
| except subprocess.CalledProcessError as e: | |
| sys.exit("ffprobe failed on %s: %s" % (path, e.stderr.strip())) | |
| d = json.loads(raw) | |
| if not d.get("streams"): | |
| sys.exit("no video stream in %s" % path) | |
| st = d["streams"][0] | |
| num, _, den = st.get("r_frame_rate", "0/1").partition("/") | |
| fps = float(num) / float(den or 1) if float(den or 1) else 0.0 | |
| dur = float(d.get("format", {}).get("duration") or 0.0) | |
| return fps, dur, int(st.get("width") or 0), int(st.get("height") or 0) | |
| def load_metrics(path, key_col): | |
| """{stem: {metric: value}} from a CSV file or a directory of JSONs.""" | |
| out = {} | |
| if os.path.isdir(path): | |
| for f in sorted(os.listdir(path)): | |
| if not f.endswith(".json"): | |
| continue | |
| with open(os.path.join(path, f), encoding="utf-8") as fh: | |
| d = json.load(fh) | |
| if not isinstance(d, dict): | |
| sys.exit("%s is not a flat JSON object" % os.path.join(path, f)) | |
| out[os.path.splitext(f)[0]] = d | |
| return out | |
| with open(path, newline="", encoding="utf-8") as fh: | |
| rd = csv.DictReader(fh) | |
| if rd.fieldnames is None: | |
| sys.exit("%s has no header row" % path) | |
| kc = key_col or rd.fieldnames[0] | |
| if kc not in rd.fieldnames: | |
| sys.exit("column %r not in %s (has %s)" | |
| % (kc, path, ", ".join(rd.fieldnames))) | |
| for r in rd: | |
| stem = os.path.splitext(str(r[kc]).strip())[0] | |
| out[stem] = {k: v for k, v in r.items() if k != kc} | |
| return out | |
| def coerce(v): | |
| """CSV and JSON both arrive stringly; numbers sort and bar, strings do not.""" | |
| if isinstance(v, (int, float)) or v is None: | |
| return v | |
| s = str(v).strip() | |
| if s == "": | |
| return None | |
| try: | |
| return int(s) if s.lstrip("+-").isdigit() else float(s) | |
| except ValueError: | |
| return s | |
| def main(): | |
| p = argparse.ArgumentParser( | |
| description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) | |
| p.add_argument("clips_dir", help="directory of clips (probed, not copied)") | |
| p.add_argument("out_json", help="manifest to write") | |
| p.add_argument("--glob-ext", default=".mp4", | |
| help="clip extension to pick up (default .mp4)") | |
| p.add_argument("--posters", metavar="DIR", | |
| help="directory of posters; matched by stem, .jpg then .png") | |
| p.add_argument("--metrics", metavar="PATH", | |
| help="a CSV file, or a directory of one flat JSON per clip; " | |
| "merged onto rows by filename stem") | |
| p.add_argument("--metrics-key", metavar="COL", | |
| help="CSV column holding the clip stem (default: first column)") | |
| p.add_argument("--src-prefix", metavar="P", | |
| help="path prefix for src in the page (default: CLIPS_DIR)") | |
| p.add_argument("--poster-prefix", metavar="P", | |
| help="path prefix for poster in the page (default: --posters)") | |
| p.add_argument("--title", default="compare deck", help="page title") | |
| p.add_argument("--h1", help="page heading (default: --title)") | |
| p.add_argument("--lede-html", default="", | |
| help="one paragraph of intro HTML, verbatim") | |
| p.add_argument("--fps", type=float, | |
| help="force the page clock instead of using the probed fps") | |
| p.add_argument("--aspect", | |
| help='force aspect, e.g. "16/9" (default: probed from the ' | |
| "first clip)") | |
| p.add_argument("--force", action="store_true", | |
| help="overwrite out_json if it exists") | |
| a = p.parse_args() | |
| if os.path.exists(a.out_json) and not a.force: | |
| sys.exit("%s exists; pass --force to overwrite" % a.out_json) | |
| if not os.path.isdir(a.clips_dir): | |
| sys.exit("%s is not a directory" % a.clips_dir) | |
| names = sorted(f for f in os.listdir(a.clips_dir) if f.endswith(a.glob_ext)) | |
| if not names: | |
| sys.exit("no %s files in %s" % (a.glob_ext, a.clips_dir)) | |
| metrics = load_metrics(a.metrics, a.metrics_key) if a.metrics else {} | |
| src_prefix = a.clips_dir if a.src_prefix is None else a.src_prefix | |
| poster_prefix = a.posters if a.poster_prefix is None else a.poster_prefix | |
| rows, fps_seen, aspect, mkeys = [], [], None, [] | |
| for name in names: | |
| stem = os.path.splitext(name)[0] | |
| fps, dur, w, h = probe(os.path.join(a.clips_dir, name)) | |
| fps_seen.append(fps) | |
| if aspect is None and w and h: | |
| aspect = "%d/%d" % (w, h) | |
| row = {"arm": stem, "group": "", "desc": "", | |
| "src": os.path.join(src_prefix, name).replace(os.sep, "/"), | |
| "dur_s": round(dur, 3)} | |
| if a.posters: | |
| for ext in (".jpg", ".png"): | |
| if os.path.exists(os.path.join(a.posters, stem + ext)): | |
| row["poster"] = os.path.join( | |
| poster_prefix, stem + ext).replace(os.sep, "/") | |
| break | |
| else: | |
| print("no poster for %s" % stem, file=sys.stderr) | |
| for k, v in (metrics.get(stem) or {}).items(): | |
| row[k] = coerce(v) | |
| if k not in mkeys: | |
| mkeys.append(k) | |
| if metrics and stem not in metrics: | |
| print("no metrics row for %s" % stem, file=sys.stderr) | |
| rows.append(row) | |
| uniq = sorted(set(round(f, 4) for f in fps_seen if f)) | |
| if a.fps is None and len(uniq) > 1: | |
| sys.exit("clips disagree on fps (%s); pass --fps to pick one" | |
| % ", ".join(str(u) for u in uniq)) | |
| fps = a.fps if a.fps is not None else (uniq[0] if uniq else 24) | |
| cols = [["dur_s", "duration (s)", 2, 1]] | |
| cols += [[k, k, 2, 1] for k in mkeys] | |
| man = {"title": a.title, "h1": a.h1 or a.title, "lede_html": a.lede_html, | |
| "aspect": a.aspect or aspect or "16/9", | |
| "fps": int(fps) if float(fps).is_integer() else fps, | |
| "media": "", "cols": cols, "groups": [], "rows": rows} | |
| with open(a.out_json, "w", encoding="utf-8") as fh: | |
| json.dump(man, fh, indent=1) | |
| fh.write("\n") | |
| print("wrote %s (%d rows, fps %s, aspect %s, %d metric cols); " | |
| "fill in group and desc before building" | |
| % (a.out_json, len(rows), man["fps"], man["aspect"], len(mkeys))) | |
| if __name__ == "__main__": | |
| main() | |