PROCITY/pipeline/build_stock_pack.py
m3ultra 081755bb71 Lane E round 8 (E1+E2): book/toy stock packs + pack-QA gate
E1: build_stock_pack.py generalized over kinds — record (256x256 sleeves), book
(96x256 tall-thin spines), toy (256x256 boxes), per-kind parody word banks, same
build-time/parody/atlas/index laws. Book+toy sample packs built + staged (?localdepot)
+ indexes committed to pipeline/packs/ so Lane C wires book/toy shelves now. Parody
deterministic + no distinctive-token leak across all 3 kinds.
E2: validate_pack.py QA gate (wired into validate_manifest -> qa gate 3) — bad UV /
empty title / bad price_band / dangling atlas / dup id fails qa (proven). qa strict
GREEN 5/5.
Real 150-250-item packs blocked on GODVERSE_DSN (flagged to Fable); samples are
placeholders in the real index format. No fal spend.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 02:27:50 +10:00

206 lines
11 KiB
Python

#!/usr/bin/env python3
"""PROCITY GODVERSE stock packs — real stock for C's shelves + dig (?stock=real).
Build-time only (nothing queries the datasets at runtime): read a local GODVERSE dataset
(dealgod Postgres), curate items, **parody-transform metadata deterministically** (no-real-
trademarks law covers titles/artists/authors/brands), fetch cover images, bake atlas WebPs + a
JSON index to C's schema (`stock_<kind>_index.json`), stage for ?localdepot / publish to the depot.
Generalized over KINDS (round-8 E1): `record` (square sleeves), `book` (TALL-thin spines),
`toy` (square boxes). C reads `uv`, not `cell`, so each kind just bakes its native cell aspect;
the index carries cell_w/cell_h informationally.
python3 build_stock_pack.py --kind record --from-db --count 300 # real (needs GODVERSE_DSN)
python3 build_stock_pack.py --kind book --sample 32 # synthetic pipeline test
python3 build_stock_pack.py --kind toy --sample 32
python3 build_stock_pack.py --publish # stage all built packs for ?localdepot
Index: { version, kind, atlas_px, cell_w, cell_h, atlases[], count,
items:[ {id,title,artist,price,price_band,atlas,uv:[u0,v0,u1,v1]} ] }
(For book/toy, `artist` carries author/brand — C's field is source-agnostic per LANE_C_NOTES.)
"""
import json, os, sys, hashlib, urllib.request
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
OUT = os.path.join(ROOT, "pipeline", "_stockpack")
STAGE = os.path.join(ROOT, "web", "assets", "models")
os.makedirs(OUT, exist_ok=True)
INDEX_FIELDS = ("id", "title", "artist", "price", "price_band", "atlas", "uv")
# ── parody word banks per kind (period-AU-plausible; no real acts/authors/brands) ──────────
BANKS = {
"record": dict(
adj=["Midnight", "Electric", "Velvet", "Golden", "Crimson", "Neon", "Sunburnt", "Hazy",
"Concrete", "Paper", "Silver", "Restless", "Quiet", "Broken", "Endless", "Salt"],
noun=["Sessions", "Highway", "Ballroom", "Suburbs", "Tide", "Radio", "Static", "Harvest",
"Verandah", "Jetty", "Milk Bar", "Cassette", "Reunion", "Departure", "Anthems", "Blue"],
band_a=["The", "Big", "Lonesome", "Riptide", "Sunset", "Regional", "Mullet", "Neon"],
band_b=["Galahs", "Dropbears", "Hills Hoists", "Milk Crates", "Southerlies", "Panelvans",
"Rissoles", "Servos", "Bottlebrush", "Sandgropers", "Cockatoos", "Utes"]),
"book": dict(
adj=["The Lost", "A Quiet", "The Last", "Beneath the", "The Long", "Shadows of the",
"The Secret", "Return to", "The Hollow", "Whispers of the", "The Broken", "Under the"],
noun=["Verandah", "Paddock", "Lighthouse", "Estuary", "Homestead", "Reef", "Gully",
"Terminus", "Foreshore", "Escarpment", "Siding", "Inlet", "Ridge", "Jetty"],
band_a=["J.", "M.", "R.", "E.", "P.", "K.", "D.", "A.", "T.", "C."],
band_b=["Halloran", "Fenwick", "Marchetti", "Delacroix", "Ngata", "Ashcombe", "Voss",
"Pemberton", "Corrigan", "Blaxland", "Underwood", "Rourke"]),
"toy": dict(
adj=["Turbo", "Mega", "Cosmic", "Wonder", "Rescue", "Galaxy", "Jungle", "Speedy",
"Sparkle", "Thunder", "Micro", "Super", "Dino", "Robo", "Magic", "Aqua"],
noun=["Racers", "Rangers", "Buddies", "Cruisers", "Squad", "Friends", "Blasters", "Ponies",
"Bots", "Beasts", "Patrol", "Tots", "Voyagers", "Crew", "Pals", "Force"],
band_a=["Bushland", "Coral Coast", "Redgum", "Southern Cross", "Boomerang", "Kanga",
"Wattle", "Billabong", "Outback", "Reef City"],
band_b=["Toys", "Playsystems", "Games", "Fun Co.", "Novelties", "Workshop", "Mfg.", "Play Lab"]),
}
CELL = {"record": (256, 256), "book": (96, 256), "toy": (256, 256)} # book = tall-thin spine
GRID = {"record": 8, "book": 16, "toy": 8} # columns per atlas row
def _seed(*p):
return int(hashlib.sha256("|".join(str(x) for x in p).encode()).hexdigest()[:12], 16)
def parody(kind, title, artist):
"""Deterministic parody; output shares no source token → zero real-trademark leakage."""
b = BANKS[kind]; s = _seed(kind, title or "", artist or "")
ptitle = f"{b['adj'][s % len(b['adj'])]} {b['noun'][(s // 7) % len(b['noun'])]}"
if kind == "record" and (s // 13) % 5 == 0:
ptitle += f", Vol. {1 + (s // 3) % 3}"
if kind == "book": # "The Lost Verandah" reads as a title already
partist = f"{b['band_a'][(s // 17) % len(b['band_a'])]} {b['band_b'][(s // 19) % len(b['band_b'])]}"
elif (s % 2 == 0) or kind == "toy":
partist = f"{b['band_a'][(s // 5) % len(b['band_a'])]} {b['band_b'][(s // 11) % len(b['band_b'])]}".strip()
else:
partist = f"{b['band_a'][(s // 17) % len(b['band_a'])]} {b['band_b'][(s // 19) % len(b['band_b'])]}"
return ptitle, partist
def price_band(kind, price):
p = price if price is not None else 4
if kind == "book":
return "bargain" if p < 5 else "standard" if p < 15 else "collector" if p < 40 else "grail"
if kind == "toy":
return "bargain" if p < 6 else "standard" if p < 20 else "collector" if p < 50 else "grail"
return "bargain" if p < 8 else "standard" if p < 25 else "collector" if p < 60 else "grail"
# ── data sources ────────────────────────────────────────────────────────────────────────
DB_QUERY = { # (table, price-source) per kind; images column holds cover URLs (discogs_parse pattern)
"record": "discogs_release",
"book": "book_edition", # bookgod / OpenLibrary editions (title, author, cover)
"toy": "toy_listing", # toygod
}
def rows_from_db(kind, count):
import psycopg2
dsn = os.environ.get("GODVERSE_DSN") or os.environ.get("DB_DSN")
if not dsn:
raise SystemExit("GODVERSE_DSN not set — the dealgod Postgres dsn is required for --from-db "
"(nothing queries it at runtime). Ask John. Postgres is up on :5432.")
con = psycopg2.connect(dsn); cur = con.cursor()
tbl = DB_QUERY[kind]
cur.execute(f"""SELECT id, artist, title, price, images FROM {tbl}
WHERE artist<>'' AND title<>'' AND images IS NOT NULL AND images<>'[]'
ORDER BY md5(id::text) LIMIT %s""", (count,))
out = []
for _id, artist, title, price, images in cur.fetchall():
imgs = images if isinstance(images, list) else json.loads(images or "[]")
url = imgs[0].get("uri") if imgs and isinstance(imgs[0], dict) else (imgs[0] if imgs else None)
if url:
out.append({"artist": artist, "title": title, "price": price, "img": url})
con.close()
return out
def rows_sample(kind, n):
"""Synthetic rows (solid-colour cells at the kind's aspect) — proves the pipeline with no DB."""
from PIL import Image, ImageDraw
cw, ch = CELL[kind]; rows = []
os.makedirs(os.path.join(OUT, "_imgs"), exist_ok=True)
for i in range(n):
s = _seed(kind, "sample", i)
col = ((s >> 0) & 255, (s >> 8) & 255, (s >> 16) & 255)
im = Image.new("RGB", (cw, ch), col); d = ImageDraw.Draw(im)
d.rectangle([2, 2, cw - 3, ch - 3], outline=(255, 255, 255), width=2)
if kind == "record":
d.ellipse([cw * .34, ch * .34, cw * .66, ch * .66], fill=(20, 20, 20))
elif kind == "book":
d.line([(cw // 2, 8), (cw // 2, ch - 8)], fill=(255, 255, 255), width=1) # spine crease
p = os.path.join(OUT, "_imgs", f"{kind}{i}.png"); im.save(p)
rows.append({"artist": f"Src {kind} {i}", "title": f"Src Title {i}",
"price": 3 + (s % 55), "img": p})
return rows
# ── atlas builder ───────────────────────────────────────────────────────────────────────
def build_pack(kind, rows):
from PIL import Image
cw, ch = CELL[kind]; cols = GRID[kind]; rowspp = max(1, 2048 // ch)
per = cols * rowspp; items, atlases = [], []
for ai in range((len(rows) + per - 1) // per):
chunk = rows[ai * per:(ai + 1) * per]
W, H = cols * cw, rowspp * ch
sheet = Image.new("RGB", (W, H), (28, 28, 32))
aname = f"stock_{kind}_atlas_{ai:02d}.webp"
for j, r in enumerate(chunk):
gx, gy = j % cols, j // cols
try:
cov = _load_img(r["img"]).resize((cw, ch))
except Exception:
continue
sheet.paste(cov, (gx * cw, gy * ch))
ptitle, partist = parody(kind, r["title"], r["artist"])
u0, v0 = gx * cw / W, gy * ch / H
item = {"id": f"{kind[:3]}_{len(items):04d}", "title": ptitle, "artist": partist,
"price": int(r["price"] or 4), "price_band": price_band(kind, r["price"]),
"atlas": aname, "uv": [round(u0, 4), round(v0, 4),
round(u0 + cw / W, 4), round(v0 + ch / H, 4)]}
items.append({k: item[k] for k in INDEX_FIELDS})
sheet.save(os.path.join(OUT, aname), "WEBP", quality=88)
atlases.append(aname)
index = {"version": 1, "kind": kind, "atlas_px": 2048, "cell_w": cw, "cell_h": ch,
"count": len(items), "atlases": atlases, "items": items}
json.dump(index, open(os.path.join(OUT, f"stock_{kind}_index.json"), "w"), indent=1)
return index
def _load_img(src):
from PIL import Image
import io
if src.startswith("http"):
req = urllib.request.Request(src, headers={"User-Agent": "procity-stockpack/1"})
with urllib.request.urlopen(req, timeout=20) as r:
return Image.open(io.BytesIO(r.read())).convert("RGB")
return Image.open(src).convert("RGB")
def main():
a = sys.argv
if "--publish" in a:
import shutil
os.makedirs(STAGE, exist_ok=True); n = 0
for f in os.listdir(OUT):
if f.startswith("stock_") and (f.endswith(".webp") or f.endswith("_index.json")):
shutil.copy(os.path.join(OUT, f), os.path.join(STAGE, f)); n += 1
print(f"staged {n} pack file(s) → {STAGE} (?localdepot). Depot: publish.py-style POST of the "
f".webp atlases + *_index.json via /api/upload.")
return
kind = a[a.index("--kind") + 1] if "--kind" in a else "record"
if "--sample" in a:
rows = rows_sample(kind, int(a[a.index("--sample") + 1]))
elif "--from-db" in a:
rows = rows_from_db(kind, int(a[a.index("--count") + 1]) if "--count" in a else 250)
else:
print(__doc__); return
idx = build_pack(kind, rows)
print(f"built {kind} pack: {idx['count']} items, {len(idx['atlases'])} atlas(es), "
f"cell {idx['cell_w']}x{idx['cell_h']}{OUT}")
print(f" sample: {[(i['title'], i['artist'], i['price_band']) for i in idx['items'][:3]]}")
if __name__ == "__main__":
main()