Restructure into Wiki, Studio and Python monorepo with scoped CI
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user