← 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()