Spaces:
Sleeping
Sleeping
| """Phase 1 (SPEC §12) — local end-to-end deck: art + frame + composite. | |
| Loads a deck JSON (from Phase 0), generates the 22 central arts via the open | |
| ≤32B image endpoint, composites each with the reusable frame + correct | |
| name/numeral, writes 22 finished card PNGs, and a contact sheet to eyeball. | |
| Usage: | |
| set -a; source ~/tokens; set +a | |
| python -m scripts.phase1_deck phase0_out/lord_of_the_rings.json | |
| python -m scripts.phase1_deck phase0_out/lord_of_the_rings.json --limit 6 | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import os | |
| import time | |
| from PIL import Image | |
| from arcana.archetypes import ROMAN_BY_NUMBER | |
| from arcana.compositor import CARD_H, CARD_W, compose_card | |
| from arcana.imagegen import get_imagegen | |
| ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | |
| BASE_SEED = 770077 # fixed seed family → deck cohesion (§7) | |
| def contact_sheet(cards: list[Image.Image], cols: int = 6) -> Image.Image: | |
| if not cards: | |
| return Image.new("RGB", (CARD_W, CARD_H), (10, 8, 16)) | |
| tw, th = CARD_W // 3, CARD_H // 3 | |
| rows = (len(cards) + cols - 1) // cols | |
| sheet = Image.new("RGB", (cols * (tw + 10) + 10, rows * (th + 10) + 10), (10, 8, 16)) | |
| for i, c in enumerate(cards): | |
| thumb = c.resize((tw, th), Image.LANCZOS) | |
| x = 10 + (i % cols) * (tw + 10) | |
| y = 10 + (i // cols) * (th + 10) | |
| sheet.paste(thumb, (x, y)) | |
| return sheet | |
| def main() -> int: | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("deck_json") | |
| ap.add_argument("--limit", type=int, default=0, help="only first N cards (quick test)") | |
| args = ap.parse_args() | |
| deck = json.load(open(args.deck_json)) | |
| theme = deck["theme"] | |
| style = deck.get("style_suffix") | |
| cards = deck["cards"] | |
| if args.limit: | |
| cards = cards[:args.limit] | |
| slug = "".join(ch if ch.isalnum() else "_" for ch in theme.lower())[:40] | |
| out_dir = os.path.join(ROOT, "cards", slug) | |
| os.makedirs(out_dir, exist_ok=True) | |
| print(f"THEME: {theme}\n style: {style}\n out: {out_dir}") | |
| ig = get_imagegen(deck_style=style) | |
| composed = [] | |
| for c in cards: | |
| n = c["arcana_number"] | |
| t0 = time.time() | |
| try: | |
| art = ig.generate(c["art_prompt"], seed=BASE_SEED + n) | |
| except Exception as e: | |
| print(f" ✗ {n:>2} {c['concept']}: art failed {type(e).__name__}: {str(e)[:90]}") | |
| continue | |
| card = compose_card(art, c["concept"], ROMAN_BY_NUMBER[n]) | |
| path = os.path.join(out_dir, f"{n:02d}_{''.join(ch if ch.isalnum() else '_' for ch in c['concept'].lower())[:24]}.png") | |
| card.save(path) | |
| c["art_path"] = os.path.relpath(path, ROOT) | |
| composed.append(card) | |
| print(f" ✓ {n:>2} {c['arcana_name']:<18} → {c['concept']:<28} {time.time()-t0:.1f}s") | |
| sheet = contact_sheet(composed) | |
| sheet_path = os.path.join(ROOT, "cards", f"{slug}_contact.png") | |
| sheet.save(sheet_path) | |
| with open(os.path.join(out_dir, "deck.json"), "w") as f: | |
| json.dump(deck, f, indent=2, ensure_ascii=False) | |
| print(f"\n contact sheet → {sheet_path} ({len(composed)} cards)") | |
| return 0 | |
| if __name__ == "__main__": | |
| raise SystemExit(main()) | |