← back to Wallco Ai

scripts/build-cactus-murals.py

110 lines

#!/usr/bin/env python3
# Build 10 print-ready 150dpi cactus murals from the existing seamless tiles.
# Output: ~/Projects/wallco-ai/data/cactus-murals/<colorway>_8ft_8ft.png
# Format: 96"×96" (8ft × 8ft) at 150dpi = 14400×14400 px PNG, dpi metadata embedded.
#
# Steve's brief: "create cactus designs in figma. 10 designs for me to use
# for real 150dpi murals."
#
# Strategy: SDXL gave us 1024×1024 seamless tiles. Tile them to fill the mural
# size. For a 96"×96" mural at 150dpi we need 14400×14400 px. The 1024px tile
# fits ~14× into that — we render 14×14 = 196 instances per mural.
#
# Figma import (manual, after this script runs):
#   1. Open Figma, create a new file or open an existing project.
#   2. Drag each .png from data/cactus-murals/ into the canvas.
#   3. Figma will create a frame at the image's natural pixel size.
#   4. Right-click → Frame settings → set DPI to 150 if needed (most
#      printers will read the embedded dpi metadata automatically).
#   5. For each frame, set the unit display to inches: View → Settings →
#      Show as → Inches. The 14400×14400 px frame will read as 96"×96".

import os
import sys
import psycopg2
import psycopg2.extras
from pathlib import Path
from PIL import Image

# Mural dimensions (inches × dpi) and total px
MURAL_IN_WIDTH  = 96   # 8 feet
MURAL_IN_HEIGHT = 96   # 8 feet
DPI = 150
PX_W = MURAL_IN_WIDTH  * DPI   # 14400
PX_H = MURAL_IN_HEIGHT * DPI   # 14400

OUT_DIR = Path.home() / "Projects" / "wallco-ai" / "data" / "cactus-murals"
OUT_DIR.mkdir(parents=True, exist_ok=True)

# Exact 10 colorways Steve approved (Omni Tucson palette)
COLORWAYS = [
    'Catalina Dusk',
    'Adobe Sundown',
    'Tortilla Bone',
    'Mesquite Charcoal',
    'Saguaro Patina',
    'Cochineal Oxblood',
    'Caliche Limestone',
    'Indigo Atelier',
    'Sonoran Espresso',
    'Prickly Pear Plum',
]

def pick_one_per_colorway(conn):
    """Return list of (id, local_path, colorway) — one best row per colorway."""
    cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
    chosen = []
    for cw in COLORWAYS:
        cur.execute("""
            SELECT id, local_path
            FROM spoon_all_designs
            WHERE category='cactus' AND is_published=TRUE
              AND prompt ILIKE %s
              AND local_path IS NOT NULL
            ORDER BY id
            LIMIT 1
        """, (f"%{cw}%",))
        row = cur.fetchone()
        if row and row['local_path'] and Path(row['local_path']).exists():
            chosen.append((row['id'], row['local_path'], cw))
            print(f"  {cw:24} → id={row['id']:5} {row['local_path']}")
        else:
            print(f"  {cw:24} → MISSING (no published row with on-disk file)")
    return chosen

def tile_to_mural(src_path: str, dest_path: Path, cw_name: str):
    """Load src, tile it to PX_W × PX_H, save with 150dpi metadata."""
    src = Image.open(src_path).convert('RGB')
    sw, sh = src.size
    print(f"    tiling {sw}×{sh} → {PX_W}×{PX_H} ({cw_name})")
    out = Image.new('RGB', (PX_W, PX_H))
    # Paste tiles edge-to-edge.
    for y in range(0, PX_H, sh):
        for x in range(0, PX_W, sw):
            out.paste(src, (x, y))
    out.save(dest_path, format='PNG', dpi=(DPI, DPI), optimize=True)
    print(f"    saved {dest_path} ({dest_path.stat().st_size // (1024*1024)} MB)")

def main():
    dsn = "dbname=dw_unified"
    print(f"Connecting {dsn}")
    conn = psycopg2.connect(dsn)
    print(f"Output dir: {OUT_DIR}")
    print(f"Mural: {MURAL_IN_WIDTH}\" × {MURAL_IN_HEIGHT}\" @ {DPI}dpi = {PX_W}×{PX_H} px\n")
    chosen = pick_one_per_colorway(conn)
    print(f"\n→ Building {len(chosen)} murals (expected {len(COLORWAYS)})\n")
    for did, src_path, cw_name in chosen:
        slug = cw_name.lower().replace(' ', '-')
        dest = OUT_DIR / f"cactus_{slug}_8ft-x-8ft_150dpi_id{did}.png"
        if dest.exists() and dest.stat().st_size > 1_000_000:
            print(f"  SKIP exists {dest.name}")
            continue
        tile_to_mural(src_path, dest, cw_name)
    print("\nDONE. Drag the PNGs from")
    print(f"  {OUT_DIR}")
    print("into a Figma file. Each will be a 14400×14400 px frame at 150dpi.")
    print("Switch Figma units to inches (View → Settings) to see them as 96\"×96\".")

if __name__ == "__main__":
    main()