"""Render a Studio JSON bundle into PNG slides and optionally MP4.""" import argparse import json import subprocess from pathlib import Path from PIL import Image, ImageDraw, ImageFont def validate(bundle): if bundle.get("version") != 1 or not isinstance(bundle.get("slides"), list) or not bundle["slides"]: raise ValueError("Expected a version 1 Studio bundle with slides") for slide in bundle["slides"]: if not all(isinstance(slide.get(k), str) for k in ("heading", "body", "color")): raise ValueError("Invalid slide") return bundle["slides"] def wrap(draw, text, font, width): lines = [] for paragraph in text.split("\n"): line = "" for char in paragraph: if line and draw.textlength(line + char, font=font) > width: lines.append(line) line = "" line += char lines.append(line) return lines def render(bundle, output, font_path): slides = validate(bundle) output.mkdir(parents=True, exist_ok=True) heading = ImageFont.truetype(str(font_path), 76) body = ImageFont.truetype(str(font_path), 38) for index, slide in enumerate(slides, 1): image = Image.new("RGB", (1080, 1440), slide["color"]) draw = ImageDraw.Draw(image) draw.text((80, 80), f"PROMPT / {index:02}", font=body, fill="#353535") y = 330 for text, font, gap in [(slide["heading"], heading, 108), (slide["body"], body, 65)]: for line in wrap(draw, text, font, 920): if y + gap > 1360: raise ValueError(f"Slide {index} overflows; split the text into more slides") draw.text((80, y), line, font=font, fill="#242424") y += gap y += 70 image.save(output / f"slide-{index:03}.png") def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("bundle", type=Path) parser.add_argument("--output", type=Path, required=True) parser.add_argument("--font", type=Path, required=True, help="CJK font TTF/OTF/TTC") parser.add_argument("--video", action="store_true") args = parser.parse_args() bundle = json.loads(args.bundle.read_text()) render(bundle, args.output, args.font) if args.video: # Use an explicit temporary input sequence so old PNGs cannot leak into a video. import tempfile import shutil with tempfile.TemporaryDirectory() as staging: for index in range(1, len(bundle["slides"]) + 1): shutil.copy(args.output / f"slide-{index:03}.png", Path(staging) / f"slide-{index:03}.png") subprocess.run(["ffmpeg", "-y", "-framerate", "1/4", "-i", str(Path(staging) / "slide-%03d.png"), "-vf", "fps=30,format=yuv420p", "-c:v", "libx264", "-movflags", "+faststart", str(args.output / "video.mp4")], check=True) if __name__ == "__main__": main()