Files
vt1200/bad-apple/pipeline/build.py
T

106 lines
3.6 KiB
Python

"""CLI: video -> vector frames -> tile deltas -> movie.bin + manifest."""
from __future__ import annotations
import argparse
import json
import os
import numpy as np
from .cost import CostConstants, estimate_record_us, select_records
from .frames import extract_binary
from .movie import MovieHeader, write_movie
from .preview import write_previews
from .vectorize import vectorize_frame
HERE = os.path.dirname(os.path.abspath(__file__))
def _stats(values) -> dict:
if not values:
return {"mean": 0.0, "p50": 0, "p95": 0, "max": 0}
arr = np.asarray(values, dtype=float)
return {
"mean": float(arr.mean()),
"p50": int(np.percentile(arr, 50)),
"p95": int(np.percentile(arr, 95)),
"max": int(arr.max()),
}
def build_movie(video: str, out_dir: str, params: dict | None = None) -> dict:
with open(os.path.join(HERE, "params.json"), encoding="utf-8") as handle:
p = json.load(handle)
if params:
p.update(params)
frames = []
for index, frame in enumerate(extract_binary(
video, p["start"], p["duration"], p["fps"],
p["width"], p["height"], p)):
frames.append(vectorize_frame(frame, p))
if (index + 1) % 60 == 0:
print(" vectorised %d frames" % (index + 1))
if not frames:
raise RuntimeError("no frames extracted from %s" % video)
constants = CostConstants()
records = select_records(frames, constants, budget_us=13900.0, max_skip=2)
height, width = frames[0].shape
header = MovieHeader(
width=width, height=height, scale=int(p.get("scale", 2)),
tile=int(p.get("tile", 32)), tiles_x=width // int(p.get("tile", 32)),
tiles_y=height // int(p.get("tile", 32)), frame_count=len(records),
source_fps=int(p.get("fps", 60)), display_hz=72)
os.makedirs(out_dir, exist_ok=True)
data_bytes = write_movie(os.path.join(out_dir, "movie.bin"), records, header)
dirty = [len(record.tiles) for record in records]
rects = [sum(len(tile.rects) for tile in record.tiles) for record in records]
costs = [estimate_record_us(record, constants) for record in records]
manifest = {
"video": video,
"data_bytes": data_bytes,
"frame_count": len(records),
"source_frames": len(frames),
"dropped": len(frames) - len(records),
"width": width,
"height": height,
"scale": header.scale,
"tile": header.tile,
"source_fps": header.source_fps,
"display_hz": header.display_hz,
"dirty_tiles": _stats(dirty),
"rects": _stats(rects),
"cost_us": _stats(costs),
}
with open(os.path.join(out_dir, "manifest.json"), "w",
encoding="utf-8") as handle:
json.dump(manifest, handle, indent=2, sort_keys=True)
write_previews(os.path.join(out_dir, "preview"), frames,
[0, 60, 300, 900, 1799])
return manifest
def main(argv=None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--video", required=True)
parser.add_argument("--out", default="data")
parser.add_argument("--duration", type=float, default=None)
parser.add_argument("--start", type=float, default=None)
args = parser.parse_args(argv)
overrides = {}
if args.duration is not None:
overrides["duration"] = args.duration
if args.start is not None:
overrides["start"] = args.start
manifest = build_movie(args.video, args.out, overrides)
print(json.dumps(manifest, indent=2, sort_keys=True))
return 0
if __name__ == "__main__":
raise SystemExit(main())