File size: 3,214 Bytes
b04d6d2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
"""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())