PROCITY/pipeline/build_stock_pack.py
m3ultra a667283cf3 Lane E round 8: REAL stock packs (record 350 / book 311 / toy 273) live on depot
John unblocked GODVERSE DB access (m1ultra native-Postgres clones, read-only SELECT,
peer auth). All 3 packs rebuilt from real data:
- record 350: discogs_full.inventory (real store stock) join release/release_image,
  real Discogs cover art.
- book 311 + toy 273: dealgod.products by category_path, real product photos + prices.
Metadata parody-transformed (deterministic, no source-token leak -> no real trademarks
in pack text); cover images are real release/product art (read-only, non-economy,
decision #2); book prices seeded (source is info-only, John's call). 15 atlases
published to the depot; indexes committed (the C contract) + staged for ?localdepot;
all pass the pack-QA gate. build_stock_pack.py gained --from-tsv (extract-on-DB-box ->
build-here), portrait 176x256 book cell, seeded-price fallback. qa --strict GREEN 5/5.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 04:09:14 +10:00

240 lines
13 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": (176, 256), "toy": (256, 256)} # book = portrait cover (2:3-ish)
GRID = {"record": 8, "book": 10, "toy": 8} # columns per atlas row
def seeded_price(kind, title, artist):
"""Some sources carry no price (books are info-only) — mint a deterministic, kind-plausible
price so the buy loop + price bands work. Same item → same price every build."""
s = _seed("price", kind, title or "", artist or "")
lo, hi = {"book": (3, 40), "toy": (4, 35), "record": (8, 60)}.get(kind, (5, 40))
return lo + s % (hi - lo + 1)
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_from_tsv(path):
"""Rows from a `artist\\ttitle\\tprice\\tcover_url` TSV extracted from GODVERSE (discogs_full
inventory JOIN release JOIN release_image — John's real store stock + saved Discogs covers).
Covers are fetched from the URL at build time; metadata is parody-transformed downstream."""
out = []
for line in open(path, encoding="utf-8"):
parts = line.rstrip("\n").split("\t")
if len(parts) < 4 or not parts[3]: # need a cover URL
continue
col0, col1, price, cover = parts[0], parts[1], parts[2], parts[3]
# record source is (artist, title); single-field sources (books/toys from products) put the
# title in col0 and leave col1 empty — carry the real title through so parody seeds off it.
if col1:
artist, title = col0, col1
elif col0:
artist, title = "", col0
else:
continue
out.append({"artist": artist, "title": title,
"price": int(price) if str(price).lstrip("-").isdigit() else None, "img": cover})
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
price = r["price"] if r.get("price") else seeded_price(kind, r["title"], r["artist"])
item = {"id": f"{kind[:3]}_{len(items):04d}", "title": ptitle, "artist": partist,
"price": int(price), "price_band": price_band(kind, 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 "--from-tsv" in a:
rows = rows_from_tsv(a[a.index("--from-tsv") + 1])
elif "--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()