#!/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__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()