from __future__ import annotations import json from pathlib import Path from PIL import Image, ImageDraw, ImageFont ROOT = Path(__file__).resolve().parents[1] REVIEW_DIR = ROOT / "artifacts" / "music-template-extraction" / "review" OUTPUT_DIR = ROOT / "artifacts" / "music-template-extraction" / "contact-sheets-local" def load_font(size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont: for path in ( Path(r"C:\Windows\Fonts\msyh.ttc"), Path(r"C:\Windows\Fonts\simhei.ttf"), ): if path.exists(): return ImageFont.truetype(str(path), size) return ImageFont.load_default() def crop_text(text: str, length: int = 38) -> str: return text if len(text) <= length else f"{text[:length]}…" def main() -> None: manifest = json.loads((REVIEW_DIR / "manifest.json").read_text(encoding="utf-8")) items = manifest["items"] OUTPUT_DIR.mkdir(parents=True, exist_ok=True) title_font = load_font(24) meta_font = load_font(17) for scene in dict.fromkeys(item["scene"] for item in items): scene_items = [item for item in items if item["scene"] == scene] width = 1820 row_height = 245 canvas = Image.new("RGB", (width, 52 + row_height * len(scene_items)), "#f5f7fa") draw = ImageDraw.Draw(canvas) draw.text((18, 12), f"{scene} · {len(scene_items)} candidates", fill="#172033", font=title_font) for row, item in enumerate(scene_items): top = 52 + row * row_height draw.rectangle((8, top + 4, width - 8, top + row_height - 4), fill="white", outline="#d8dee8") label = f"{item['id']} {crop_text(item['name'])} · {item['slides']}页 · {item['score']}分" draw.text((20, top + 14), label, fill="#172033", font=meta_font) for column, filename in enumerate(item["samples"]): image_path = REVIEW_DIR / item["id"] / filename with Image.open(image_path) as sample: sample = sample.convert("RGB") sample.thumbnail((342, 192)) x = 20 + column * 358 y = top + 45 canvas.paste(sample, (x, y)) draw.rectangle((x, y, x + sample.width, y + sample.height), outline="#c9d1dc") canvas.save(OUTPUT_DIR / f"{scene}.jpg", quality=91, optimize=True) if __name__ == "__main__": main()