diff --git a/E-progress.md b/E-progress.md index 0e46f5b..edb22b5 100644 --- a/E-progress.md +++ b/E-progress.md @@ -4,6 +4,23 @@ Reviewed against `docs/LANES/LANE_E_ASSETS.md` + `docs/LANES/ROUND3_INSTRUCTIONS.md`. Nothing in other lanes' files was touched.* +## Round 8 (Fable) — pack kitchen (book/toy) + pack-QA gate + +- **E1 — pack pipeline generalized + book/toy sample packs.** `build_stock_pack.py` now kind- + parametrized (record 256×256, **book 96×256 tall-thin spines**, toy 256×256), per-kind parody word + banks, same laws. Book/toy sample packs built + staged (`?localdepot`) + indexes committed to + `pipeline/packs/` so **Lane C wires book/toy shelves now**. Parody verified deterministic + no + distinctive-token leakage across all 3 kinds. +- **E2 — pack-QA gate: DONE.** `validate_pack.py` (wired into `validate_manifest.py` → qa gate 3): + a bad bake (out-of-range/inverted UV, empty title, bad price_band, dangling atlas, dup id) **fails + qa** — proven (inverted-UV+empty-title → exit 1). qa `--strict` GREEN 5/5. +- **Tags pushed** (John-delegated): origin/main = HEAD (1a20501) + v1.0/v1.1/v2.0-alpha/v2.0-beta all + live on origin — verified. +- **Blocker (flagged to Fable):** real ~150–250-item book/toy (+ record) packs need `GODVERSE_DSN` + (dealgod Postgres, up on :5432, read from env). One `--kind X --from-db` command each once set. + Current packs are synthetic placeholders (real index format). No fal spend. +- **On-call** (B rain-streak/puddle, D door-swing tell): none requested. + ## Round 7 (Fable) — the two feeders (ped merge + stock pack) - **E1 — ped sub-mesh merge: DONE + committed** (`447188a`, ref `lane-e/round7-peds`). All 19 rigs diff --git a/docs/LANES/LANE_E_NOTES.md b/docs/LANES/LANE_E_NOTES.md index 443de94..356339a 100644 --- a/docs/LANES/LANE_E_NOTES.md +++ b/docs/LANES/LANE_E_NOTES.md @@ -1,5 +1,25 @@ # LANE E — cross-lane notes +## Round 8 + +**→ Lane C (E1 — book + toy packs staged for you):** `build_stock_pack.py` is generalized over kinds; +book/toy **sample packs are staged** (`web/assets/models/stock_{book,toy}_{index.json,atlas_00.webp}`, +`?localdepot`) + indexes committed to `pipeline/packs/`. Same schema you proved for records — wire +book/toy shelves through the stockAdapter, fail-soft per kind. **Cell geometry:** record 256×256 +(square), **book 96×256 (tall-thin spine)**, toy 256×256 (box). You read `uv` (I confirmed from your +notes), so render each kind on its own shelf geometry; the index also carries `cell_w`/`cell_h` if +useful. `artist` carries author (book) / brand (toy). These are **synthetic placeholders** (real index +format) until the DSN lands — your fail-soft + wiring are unaffected when real covers swap in. + +**E2 — pack-QA gate is live** (`validate_pack.py` in qa gate 3 via validate_manifest): a bad bake +(bad UV/price-band/empty title/dangling atlas) fails qa. If you add index fields, tell me and I'll +extend `REQUIRED`. + +**→ Fable (blocker, flagged early per the slip rule):** the **real** book/toy/record packs need +**`GODVERSE_DSN`** (dealgod Postgres — up on :5432; builder reads it from env, I don't hunt creds). +One command per kind once set (`--kind book --from-db --count 200`). Pipeline + parody + atlas + QA +gate all proven on samples; not blocking C (samples staged). No fal spend. + ## Round 7 **→ Lane D (D1 — the flip gate): merged peds ready to validate.** `web/models/peds/*.glb` (19 rigs) diff --git a/pipeline/AUDIT.md b/pipeline/AUDIT.md index 121c8bd..eda2e47 100644 --- a/pipeline/AUDIT.md +++ b/pipeline/AUDIT.md @@ -305,3 +305,26 @@ and the same source row yields the same parody every build. Verified determinist Provenance of the real pack: source = Discogs metadata + cover images (recordgod), **metadata parody-transformed** (no real titles/artists ship); covers are real release art (read-only, non-economy per decision #2). Small book/toy packs are a follow-up once the record pack lands. + +## Round 8 — E1/E2: book + toy stock packs + pack-QA gate + +**E1 — pipeline generalized** (`build_stock_pack.py` now kind-parametrized: `record` 256×256 sleeves, +`book` **96×256 tall-thin spines**, `toy` 256×256 boxes). Per-kind parody word banks; same laws as +the record pack (build-time extract, deterministic parody-transform of metadata, atlas WebPs + JSON +index to C's schema — `stock__index.json`, C reads `uv` not `cell`). Book/toy **sample packs +built + staged** (`web/assets/models/`, `?localdepot`) so Lane C wires book/toy shelves now; indexes +committed to `pipeline/packs/` as the contract. Parody verified deterministic + no distinctive-token +leakage across all 3 kinds ("Nevermind"/"Nirvana" → "Endless Blue"/"Mullet Milk Crates"; +"The Hobbit" → "Under the Homestead"/"C. Underwood"). + +**E2 — pack-QA gate** (`validate_pack.py`, wired into `validate_manifest.py` → qa gate 3): every +`pipeline/packs/stock_*_index.json` entry must have a valid kind, in-range non-inverted UVs, a known +price band, non-empty title/artist, unique id, and an atlas listed in `atlases[]` — a bad bake fails +qa loudly (proven: inverted-UV + empty-title → exit 1). qa `--strict` GREEN 5/5. + +**Blocker (flagged to Fable/John — real data):** the **real** ~150–250-item book/toy packs (and the +real record pack) need **`GODVERSE_DSN`** (dealgod Postgres; up on :5432, read from env, no cred +hunt). One command each once set: `build_stock_pack.py --kind book --from-db --count 200`. The +current staged packs are synthetic placeholders (real index format); provenance of the real packs = +GODVERSE metadata (OpenLibrary/toygod) **parody-transformed** (no real titles/authors/brands ship) + +real cover art (read-only, non-economy). No fal spend this round. diff --git a/pipeline/build_stock_pack.py b/pipeline/build_stock_pack.py index 2a6debc..0f5457a 100644 --- a/pipeline/build_stock_pack.py +++ b/pipeline/build_stock_pack.py @@ -1,154 +1,169 @@ #!/usr/bin/env python3 -"""PROCITY GODVERSE stock pack — real record sleeves for C's dig (?stock=real), round-7 E2. +"""PROCITY GODVERSE stock packs — real stock for C's shelves + dig (?stock=real). -Build-time only: reads the local recordgod dataset (dealgod Postgres `discogs_release`, the -185k-vinyl set), curates ~200-400 sleeves, **parody-transforms titles/artists deterministically** -(the no-real-trademarks law covers metadata), fetches cover images, bakes atlas sheets + a JSON -index, publishes atlases to the depot. NOTHING queries the dataset at runtime — C reads the built -pack (index + atlases) through the depot / ?localdepot=1. +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. - python3 build_stock_pack.py --from-db --count 300 # real pack (needs GODVERSE_DSN + net for covers) - python3 build_stock_pack.py --sample 24 # synthetic pipeline test (no DB, solid-colour sleeves) - python3 build_stock_pack.py --publish # push built atlases + index 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. -Index schema (proposed to Lane C in LANE_E_NOTES — adjust `INDEX_FIELDS` to their contract): - { "version":1, "atlas_px":2048, "cell":256, "items":[ - {"id","title","artist","price","price_band","atlas","uv":[u0,v0,u1,v1]} ] } + 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 -Parody law: real (title,artist) → deterministic seed → parody title/artist recombined from the -word banks below. The output shares NO tokens with the source, so zero real-trademark leakage, -yet reads as period-plausible AU record-shop stock. Same source row → same parody, every build. +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, struct, hashlib, urllib.request +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") # ?localdepot mirror +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 (period-plausible AU record stock; no real acts) ────────────────── -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", "Powderfinger-ish", "Sunset", "Regional", "Mullet"] -BAND_B = ["Galahs", "Dropbears", "Hills Hoists", "Milk Crates", "Southerlies", "Panelvans", - "Rissoles", "Servos", "Bottlebrush", "Sandgropers", "Cockatoos", "Utes"] -FIRST = ["Bazza", "Shazza", "Kylie", "Darryl", "Noeline", "Trev", "Merv", "Dazza", "Coral", "Kev"] -LAST = ["Studebaker", "Fitzroy", "Nullarbor", "Grong", "Wollemi", "Barcoo", "Tuckerbox", "Yakka"] +# ── 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(*parts): - return int(hashlib.sha256("|".join(str(p) for p in parts).encode()).hexdigest()[:12], 16) +def _seed(*p): + return int(hashlib.sha256("|".join(str(x) for x in p).encode()).hexdigest()[:12], 16) -def parody(title, artist, is_group=None): - """Deterministic parody of a real (title, artist). Shares no source tokens.""" - s = _seed(title or "", artist or "") - ptitle = f"{ADJ[s % len(ADJ)]} {NOUN[(s // 7) % len(NOUN)]}" - if (s // 13) % 5 == 0: +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}" - # solo vs group name, deterministically - if is_group is None: - is_group = (s % 2 == 0) - if is_group: - partist = f"{BAND_A[(s // 5) % len(BAND_A)]} {BAND_B[(s // 11) % len(BAND_B)]}".strip() + 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"{FIRST[(s // 17) % len(FIRST)]} {LAST[(s // 19) % len(LAST)]}" + 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(price): - if price is None: - return "bargain" - if price < 8: - return "bargain" - if price < 25: - return "standard" - if price < 60: - return "collector" - return "grail" +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 ──────────────────────────────────────────────────────────────────────── -def rows_from_db(count): - """Curate `count` sleeves from dealgod's discogs_release (needs GODVERSE_DSN + psycopg2). - Picks rows WITH a cover image + non-empty artist/title, varied by decade for shelf variety.""" - import psycopg2 # noqa: build-time only +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 DB dsn (creds) is required for --from-db. " - "Ask John / read from dealgod's env. (Nothing queries the DB at runtime.)") - con = psycopg2.connect(dsn) - cur = con.cursor() - cur.execute(""" - SELECT release_id, artist, title, year, - (SELECT current_price FROM prices p WHERE p.release_id=dr.release_id - ORDER BY current_price LIMIT 1) AS price, - images - FROM discogs_release dr - WHERE artist <> '' AND title <> '' AND images IS NOT NULL AND images <> '[]' - ORDER BY md5(release_id::text) -- deterministic pseudo-random sample - LIMIT %s""", (count,)) + 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 rid, artist, title, year, price, images in cur.fetchall(): + 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({"src_id": rid, "artist": artist, "title": title, "price": price, "img": url}) + out.append({"artist": artist, "title": title, "price": price, "img": url}) con.close() return out -def rows_sample(n): - """Synthetic rows for a no-DB pipeline test — solid-colour 'sleeves', fake source metadata.""" +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 - rows = [] + cw, ch = CELL[kind]; rows = [] os.makedirs(os.path.join(OUT, "_imgs"), exist_ok=True) for i in range(n): - s = _seed("sample", i) + s = _seed(kind, "sample", i) col = ((s >> 0) & 255, (s >> 8) & 255, (s >> 16) & 255) - im = Image.new("RGB", (256, 256), col) - d = ImageDraw.Draw(im) - d.rectangle([16, 16, 240, 240], outline=(255, 255, 255), width=3) - d.ellipse([88, 88, 168, 168], fill=(20, 20, 20)) # a 'record' centre - p = os.path.join(OUT, "_imgs", f"s{i}.png") - im.save(p) - rows.append({"src_id": f"sample-{i}", "artist": f"Real Artist {i}", - "title": f"Real Album {i}", "price": 5 + (s % 80), "img": p}) + 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(rows, cell=256, per_atlas_side=8): +def build_pack(kind, rows): from PIL import Image - per = per_atlas_side * per_atlas_side - items, atlases = [], [] + 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] - sheet = Image.new("RGB", (cell * per_atlas_side, cell * per_atlas_side), (30, 30, 34)) - aname = f"stock_record_atlas_{ai:02d}.webp" + 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 % per_atlas_side, j // per_atlas_side + gx, gy = j % cols, j // cols try: - cov = _load_img(r["img"]).resize((cell, cell)) + cov = _load_img(r["img"]).resize((cw, ch)) except Exception: continue - sheet.paste(cov, (gx * cell, gy * cell)) - ptitle, partist = parody(r["title"], r["artist"]) - u0, v0 = gx / per_atlas_side, gy / per_atlas_side - item = {"id": f"rec_{len(items):04d}", "title": ptitle, "artist": partist, - "price": int(r["price"] or 5), "price_band": price_band(r["price"]), + 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 + 1 / per_atlas_side, 4), round(v0 + 1 / per_atlas_side, 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, "atlas_px": cell * per_atlas_side, "cell": cell, + 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, "stock_record_index.json"), "w"), indent=1) + json.dump(index, open(os.path.join(OUT, f"stock_{kind}_index.json"), "w"), indent=1) return index @@ -165,26 +180,25 @@ def _load_img(src): def main(): a = sys.argv if "--publish" in a: - # atlases + index publish through the same depot path as GLBs (documented; run when built) + import shutil + os.makedirs(STAGE, exist_ok=True); n = 0 for f in os.listdir(OUT): - if f.endswith(".webp") or f == "stock_record_index.json": - os.makedirs(STAGE, exist_ok=True) - import shutil - shutil.copy(os.path.join(OUT, f), os.path.join(STAGE, f)) - print(f"staged pack → {STAGE} (for ?localdepot). Depot push: adapt publish.py to POST " - f"{OUT}/*.webp + index (same /api/upload path).") + 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: - n = int(a[a.index("--sample") + 1]) - rows = rows_sample(n) + rows = rows_sample(kind, int(a[a.index("--sample") + 1])) elif "--from-db" in a: - count = int(a[a.index("--count") + 1]) if "--count" in a else 300 - rows = rows_from_db(count) + rows = rows_from_db(kind, int(a[a.index("--count") + 1]) if "--count" in a else 250) else: print(__doc__); return - idx = build_pack(rows) - print(f"built pack: {idx['count']} sleeves, {len(idx['atlases'])} atlas(es) → {OUT}") - print(f" sample items: {[(i['title'], i['artist'], i['price_band']) for i in idx['items'][:3]]}") + 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__": diff --git a/pipeline/packs/stock_book_index.json b/pipeline/packs/stock_book_index.json new file mode 100644 index 0000000..40a924b --- /dev/null +++ b/pipeline/packs/stock_book_index.json @@ -0,0 +1,461 @@ +{ + "version": 1, + "kind": "book", + "atlas_px": 2048, + "cell_w": 96, + "cell_h": 256, + "count": 32, + "atlases": [ + "stock_book_atlas_00.webp" + ], + "items": [ + { + "id": "boo_0000", + "title": "A Quiet Reef", + "artist": "K. Underwood", + "price": 18, + "price_band": "collector", + "atlas": "stock_book_atlas_00.webp", + "uv": [ + 0.0, + 0.0, + 0.0625, + 0.125 + ] + }, + { + "id": "boo_0001", + "title": "Beneath the Terminus", + "artist": "K. Ngata", + "price": 17, + "price_band": "collector", + "atlas": "stock_book_atlas_00.webp", + "uv": [ + 0.0625, + 0.0, + 0.125, + 0.125 + ] + }, + { + "id": "boo_0002", + "title": "Return to Homestead", + "artist": "A. Rourke", + "price": 28, + "price_band": "collector", + "atlas": "stock_book_atlas_00.webp", + "uv": [ + 0.125, + 0.0, + 0.1875, + 0.125 + ] + }, + { + "id": "boo_0003", + "title": "The Secret Terminus", + "artist": "P. Rourke", + "price": 38, + "price_band": "collector", + "atlas": "stock_book_atlas_00.webp", + "uv": [ + 0.1875, + 0.0, + 0.25, + 0.125 + ] + }, + { + "id": "boo_0004", + "title": "The Long Lighthouse", + "artist": "C. Fenwick", + "price": 24, + "price_band": "collector", + "atlas": "stock_book_atlas_00.webp", + "uv": [ + 0.25, + 0.0, + 0.3125, + 0.125 + ] + }, + { + "id": "boo_0005", + "title": "The Secret Escarpment", + "artist": "A. Pemberton", + "price": 18, + "price_band": "collector", + "atlas": "stock_book_atlas_00.webp", + "uv": [ + 0.3125, + 0.0, + 0.375, + 0.125 + ] + }, + { + "id": "boo_0006", + "title": "Beneath the Gully", + "artist": "P. Ashcombe", + "price": 16, + "price_band": "collector", + "atlas": "stock_book_atlas_00.webp", + "uv": [ + 0.375, + 0.0, + 0.4375, + 0.125 + ] + }, + { + "id": "boo_0007", + "title": "Under the Paddock", + "artist": "E. Delacroix", + "price": 15, + "price_band": "collector", + "atlas": "stock_book_atlas_00.webp", + "uv": [ + 0.4375, + 0.0, + 0.5, + 0.125 + ] + }, + { + "id": "boo_0008", + "title": "The Secret Escarpment", + "artist": "J. Marchetti", + "price": 31, + "price_band": "collector", + "atlas": "stock_book_atlas_00.webp", + "uv": [ + 0.5, + 0.0, + 0.5625, + 0.125 + ] + }, + { + "id": "boo_0009", + "title": "The Lost Verandah", + "artist": "R. Delacroix", + "price": 38, + "price_band": "collector", + "atlas": "stock_book_atlas_00.webp", + "uv": [ + 0.5625, + 0.0, + 0.625, + 0.125 + ] + }, + { + "id": "boo_0010", + "title": "A Quiet Estuary", + "artist": "E. Rourke", + "price": 17, + "price_band": "collector", + "atlas": "stock_book_atlas_00.webp", + "uv": [ + 0.625, + 0.0, + 0.6875, + 0.125 + ] + }, + { + "id": "boo_0011", + "title": "Shadows of the Foreshore", + "artist": "R. Ngata", + "price": 17, + "price_band": "collector", + "atlas": "stock_book_atlas_00.webp", + "uv": [ + 0.6875, + 0.0, + 0.75, + 0.125 + ] + }, + { + "id": "boo_0012", + "title": "The Last Lighthouse", + "artist": "M. Ashcombe", + "price": 22, + "price_band": "collector", + "atlas": "stock_book_atlas_00.webp", + "uv": [ + 0.75, + 0.0, + 0.8125, + 0.125 + ] + }, + { + "id": "boo_0013", + "title": "The Secret Homestead", + "artist": "C. Rourke", + "price": 49, + "price_band": "grail", + "atlas": "stock_book_atlas_00.webp", + "uv": [ + 0.8125, + 0.0, + 0.875, + 0.125 + ] + }, + { + "id": "boo_0014", + "title": "The Lost Verandah", + "artist": "C. Ngata", + "price": 49, + "price_band": "grail", + "atlas": "stock_book_atlas_00.webp", + "uv": [ + 0.875, + 0.0, + 0.9375, + 0.125 + ] + }, + { + "id": "boo_0015", + "title": "Shadows of the Terminus", + "artist": "D. Rourke", + "price": 16, + "price_band": "collector", + "atlas": "stock_book_atlas_00.webp", + "uv": [ + 0.9375, + 0.0, + 1.0, + 0.125 + ] + }, + { + "id": "boo_0016", + "title": "Whispers of the Gully", + "artist": "A. Rourke", + "price": 6, + "price_band": "standard", + "atlas": "stock_book_atlas_00.webp", + "uv": [ + 0.0, + 0.125, + 0.0625, + 0.25 + ] + }, + { + "id": "boo_0017", + "title": "Beneath the Inlet", + "artist": "C. Blaxland", + "price": 28, + "price_band": "collector", + "atlas": "stock_book_atlas_00.webp", + "uv": [ + 0.0625, + 0.125, + 0.125, + 0.25 + ] + }, + { + "id": "boo_0018", + "title": "A Quiet Siding", + "artist": "M. Underwood", + "price": 51, + "price_band": "grail", + "atlas": "stock_book_atlas_00.webp", + "uv": [ + 0.125, + 0.125, + 0.1875, + 0.25 + ] + }, + { + "id": "boo_0019", + "title": "Whispers of the Homestead", + "artist": "R. Marchetti", + "price": 51, + "price_band": "grail", + "atlas": "stock_book_atlas_00.webp", + "uv": [ + 0.1875, + 0.125, + 0.25, + 0.25 + ] + }, + { + "id": "boo_0020", + "title": "A Quiet Gully", + "artist": "T. Ashcombe", + "price": 54, + "price_band": "grail", + "atlas": "stock_book_atlas_00.webp", + "uv": [ + 0.25, + 0.125, + 0.3125, + 0.25 + ] + }, + { + "id": "boo_0021", + "title": "Under the Reef", + "artist": "D. Marchetti", + "price": 25, + "price_band": "collector", + "atlas": "stock_book_atlas_00.webp", + "uv": [ + 0.3125, + 0.125, + 0.375, + 0.25 + ] + }, + { + "id": "boo_0022", + "title": "Beneath the Estuary", + "artist": "D. Ngata", + "price": 20, + "price_band": "collector", + "atlas": "stock_book_atlas_00.webp", + "uv": [ + 0.375, + 0.125, + 0.4375, + 0.25 + ] + }, + { + "id": "boo_0023", + "title": "Whispers of the Foreshore", + "artist": "P. Blaxland", + "price": 51, + "price_band": "grail", + "atlas": "stock_book_atlas_00.webp", + "uv": [ + 0.4375, + 0.125, + 0.5, + 0.25 + ] + }, + { + "id": "boo_0024", + "title": "The Last Ridge", + "artist": "E. Marchetti", + "price": 16, + "price_band": "collector", + "atlas": "stock_book_atlas_00.webp", + "uv": [ + 0.5, + 0.125, + 0.5625, + 0.25 + ] + }, + { + "id": "boo_0025", + "title": "Whispers of the Jetty", + "artist": "J. Corrigan", + "price": 40, + "price_band": "grail", + "atlas": "stock_book_atlas_00.webp", + "uv": [ + 0.5625, + 0.125, + 0.625, + 0.25 + ] + }, + { + "id": "boo_0026", + "title": "Shadows of the Verandah", + "artist": "J. Voss", + "price": 7, + "price_band": "standard", + "atlas": "stock_book_atlas_00.webp", + "uv": [ + 0.625, + 0.125, + 0.6875, + 0.25 + ] + }, + { + "id": "boo_0027", + "title": "The Lost Jetty", + "artist": "J. Blaxland", + "price": 4, + "price_band": "bargain", + "atlas": "stock_book_atlas_00.webp", + "uv": [ + 0.6875, + 0.125, + 0.75, + 0.25 + ] + }, + { + "id": "boo_0028", + "title": "Whispers of the Escarpment", + "artist": "D. Delacroix", + "price": 29, + "price_band": "collector", + "atlas": "stock_book_atlas_00.webp", + "uv": [ + 0.75, + 0.125, + 0.8125, + 0.25 + ] + }, + { + "id": "boo_0029", + "title": "The Last Jetty", + "artist": "J. Ngata", + "price": 29, + "price_band": "collector", + "atlas": "stock_book_atlas_00.webp", + "uv": [ + 0.8125, + 0.125, + 0.875, + 0.25 + ] + }, + { + "id": "boo_0030", + "title": "The Lost Verandah", + "artist": "D. Ngata", + "price": 32, + "price_band": "collector", + "atlas": "stock_book_atlas_00.webp", + "uv": [ + 0.875, + 0.125, + 0.9375, + 0.25 + ] + }, + { + "id": "boo_0031", + "title": "Beneath the Lighthouse", + "artist": "C. Fenwick", + "price": 35, + "price_band": "collector", + "atlas": "stock_book_atlas_00.webp", + "uv": [ + 0.9375, + 0.125, + 1.0, + 0.25 + ] + } + ] +} \ No newline at end of file diff --git a/pipeline/packs/stock_record_index.json b/pipeline/packs/stock_record_index.json new file mode 100644 index 0000000..fb344a4 --- /dev/null +++ b/pipeline/packs/stock_record_index.json @@ -0,0 +1,461 @@ +{ + "version": 1, + "kind": "record", + "atlas_px": 2048, + "cell_w": 256, + "cell_h": 256, + "count": 32, + "atlases": [ + "stock_record_atlas_00.webp" + ], + "items": [ + { + "id": "rec_0000", + "title": "Paper Suburbs", + "artist": "Lonesome Servos", + "price": 15, + "price_band": "standard", + "atlas": "stock_record_atlas_00.webp", + "uv": [ + 0.0, + 0.0, + 0.125, + 0.125 + ] + }, + { + "id": "rec_0001", + "title": "Silver Suburbs, Vol. 2", + "artist": "Riptide Rissoles", + "price": 6, + "price_band": "bargain", + "atlas": "stock_record_atlas_00.webp", + "uv": [ + 0.125, + 0.0, + 0.25, + 0.125 + ] + }, + { + "id": "rec_0002", + "title": "Velvet Ballroom", + "artist": "Riptide Servos", + "price": 7, + "price_band": "bargain", + "atlas": "stock_record_atlas_00.webp", + "uv": [ + 0.25, + 0.0, + 0.375, + 0.125 + ] + }, + { + "id": "rec_0003", + "title": "Concrete Verandah", + "artist": "The Servos", + "price": 48, + "price_band": "collector", + "atlas": "stock_record_atlas_00.webp", + "uv": [ + 0.375, + 0.0, + 0.5, + 0.125 + ] + }, + { + "id": "rec_0004", + "title": "Broken Highway", + "artist": "Regional Galahs", + "price": 44, + "price_band": "collector", + "atlas": "stock_record_atlas_00.webp", + "uv": [ + 0.5, + 0.0, + 0.625, + 0.125 + ] + }, + { + "id": "rec_0005", + "title": "Neon Jetty", + "artist": "Mullet Servos", + "price": 17, + "price_band": "standard", + "atlas": "stock_record_atlas_00.webp", + "uv": [ + 0.625, + 0.0, + 0.75, + 0.125 + ] + }, + { + "id": "rec_0006", + "title": "Hazy Highway", + "artist": "Big Dropbears", + "price": 50, + "price_band": "collector", + "atlas": "stock_record_atlas_00.webp", + "uv": [ + 0.75, + 0.0, + 0.875, + 0.125 + ] + }, + { + "id": "rec_0007", + "title": "Electric Departure", + "artist": "Lonesome Dropbears", + "price": 30, + "price_band": "collector", + "atlas": "stock_record_atlas_00.webp", + "uv": [ + 0.875, + 0.0, + 1.0, + 0.125 + ] + }, + { + "id": "rec_0008", + "title": "Neon Radio", + "artist": "Regional Servos", + "price": 49, + "price_band": "collector", + "atlas": "stock_record_atlas_00.webp", + "uv": [ + 0.0, + 0.125, + 0.125, + 0.25 + ] + }, + { + "id": "rec_0009", + "title": "Midnight Static", + "artist": "Big Southerlies", + "price": 51, + "price_band": "collector", + "atlas": "stock_record_atlas_00.webp", + "uv": [ + 0.125, + 0.125, + 0.25, + 0.25 + ] + }, + { + "id": "rec_0010", + "title": "Electric Tide", + "artist": "Sunset Hills Hoists", + "price": 3, + "price_band": "bargain", + "atlas": "stock_record_atlas_00.webp", + "uv": [ + 0.25, + 0.125, + 0.375, + 0.25 + ] + }, + { + "id": "rec_0011", + "title": "Hazy Radio", + "artist": "Riptide Southerlies", + "price": 8, + "price_band": "standard", + "atlas": "stock_record_atlas_00.webp", + "uv": [ + 0.375, + 0.125, + 0.5, + 0.25 + ] + }, + { + "id": "rec_0012", + "title": "Broken Verandah, Vol. 1", + "artist": "The Galahs", + "price": 41, + "price_band": "collector", + "atlas": "stock_record_atlas_00.webp", + "uv": [ + 0.5, + 0.125, + 0.625, + 0.25 + ] + }, + { + "id": "rec_0013", + "title": "Midnight Tide", + "artist": "Big Hills Hoists", + "price": 51, + "price_band": "collector", + "atlas": "stock_record_atlas_00.webp", + "uv": [ + 0.625, + 0.125, + 0.75, + 0.25 + ] + }, + { + "id": "rec_0014", + "title": "Silver Highway", + "artist": "Riptide Cockatoos", + "price": 4, + "price_band": "bargain", + "atlas": "stock_record_atlas_00.webp", + "uv": [ + 0.75, + 0.125, + 0.875, + 0.25 + ] + }, + { + "id": "rec_0015", + "title": "Velvet Anthems, Vol. 3", + "artist": "Regional Cockatoos", + "price": 37, + "price_band": "collector", + "atlas": "stock_record_atlas_00.webp", + "uv": [ + 0.875, + 0.125, + 1.0, + 0.25 + ] + }, + { + "id": "rec_0016", + "title": "Paper Milk Bar", + "artist": "Riptide Utes", + "price": 35, + "price_band": "collector", + "atlas": "stock_record_atlas_00.webp", + "uv": [ + 0.0, + 0.25, + 0.125, + 0.375 + ] + }, + { + "id": "rec_0017", + "title": "Silver Suburbs, Vol. 2", + "artist": "Lonesome Hills Hoists", + "price": 16, + "price_band": "standard", + "atlas": "stock_record_atlas_00.webp", + "uv": [ + 0.125, + 0.25, + 0.25, + 0.375 + ] + }, + { + "id": "rec_0018", + "title": "Broken Tide", + "artist": "Lonesome Bottlebrush", + "price": 31, + "price_band": "collector", + "atlas": "stock_record_atlas_00.webp", + "uv": [ + 0.25, + 0.25, + 0.375, + 0.375 + ] + }, + { + "id": "rec_0019", + "title": "Midnight Departure", + "artist": "The Dropbears", + "price": 40, + "price_band": "collector", + "atlas": "stock_record_atlas_00.webp", + "uv": [ + 0.375, + 0.25, + 0.5, + 0.375 + ] + }, + { + "id": "rec_0020", + "title": "Paper Radio, Vol. 3", + "artist": "The Sandgropers", + "price": 22, + "price_band": "standard", + "atlas": "stock_record_atlas_00.webp", + "uv": [ + 0.5, + 0.25, + 0.625, + 0.375 + ] + }, + { + "id": "rec_0021", + "title": "Silver Reunion", + "artist": "The Galahs", + "price": 16, + "price_band": "standard", + "atlas": "stock_record_atlas_00.webp", + "uv": [ + 0.625, + 0.25, + 0.75, + 0.375 + ] + }, + { + "id": "rec_0022", + "title": "Hazy Harvest", + "artist": "Regional Cockatoos", + "price": 37, + "price_band": "collector", + "atlas": "stock_record_atlas_00.webp", + "uv": [ + 0.75, + 0.25, + 0.875, + 0.375 + ] + }, + { + "id": "rec_0023", + "title": "Quiet Departure, Vol. 1", + "artist": "Neon Southerlies", + "price": 4, + "price_band": "bargain", + "atlas": "stock_record_atlas_00.webp", + "uv": [ + 0.875, + 0.25, + 1.0, + 0.375 + ] + }, + { + "id": "rec_0024", + "title": "Midnight Ballroom, Vol. 1", + "artist": "Big Galahs", + "price": 29, + "price_band": "collector", + "atlas": "stock_record_atlas_00.webp", + "uv": [ + 0.0, + 0.375, + 0.125, + 0.5 + ] + }, + { + "id": "rec_0025", + "title": "Electric Jetty", + "artist": "Big Servos", + "price": 46, + "price_band": "collector", + "atlas": "stock_record_atlas_00.webp", + "uv": [ + 0.125, + 0.375, + 0.25, + 0.5 + ] + }, + { + "id": "rec_0026", + "title": "Salt Blue", + "artist": "Lonesome Milk Crates", + "price": 23, + "price_band": "standard", + "atlas": "stock_record_atlas_00.webp", + "uv": [ + 0.25, + 0.375, + 0.375, + 0.5 + ] + }, + { + "id": "rec_0027", + "title": "Midnight Jetty", + "artist": "Big Panelvans", + "price": 46, + "price_band": "collector", + "atlas": "stock_record_atlas_00.webp", + "uv": [ + 0.375, + 0.375, + 0.5, + 0.5 + ] + }, + { + "id": "rec_0028", + "title": "Golden Jetty, Vol. 1", + "artist": "The Servos", + "price": 24, + "price_band": "standard", + "atlas": "stock_record_atlas_00.webp", + "uv": [ + 0.5, + 0.375, + 0.625, + 0.5 + ] + }, + { + "id": "rec_0029", + "title": "Electric Ballroom, Vol. 3", + "artist": "Mullet Bottlebrush", + "price": 4, + "price_band": "bargain", + "atlas": "stock_record_atlas_00.webp", + "uv": [ + 0.625, + 0.375, + 0.75, + 0.5 + ] + }, + { + "id": "rec_0030", + "title": "Restless Suburbs", + "artist": "Sunset Utes", + "price": 44, + "price_band": "collector", + "atlas": "stock_record_atlas_00.webp", + "uv": [ + 0.75, + 0.375, + 0.875, + 0.5 + ] + }, + { + "id": "rec_0031", + "title": "Golden Sessions", + "artist": "Mullet Southerlies", + "price": 3, + "price_band": "bargain", + "atlas": "stock_record_atlas_00.webp", + "uv": [ + 0.875, + 0.375, + 1.0, + 0.5 + ] + } + ] +} \ No newline at end of file diff --git a/pipeline/packs/stock_toy_index.json b/pipeline/packs/stock_toy_index.json new file mode 100644 index 0000000..bf39273 --- /dev/null +++ b/pipeline/packs/stock_toy_index.json @@ -0,0 +1,461 @@ +{ + "version": 1, + "kind": "toy", + "atlas_px": 2048, + "cell_w": 256, + "cell_h": 256, + "count": 32, + "atlases": [ + "stock_toy_atlas_00.webp" + ], + "items": [ + { + "id": "toy_0000", + "title": "Speedy Patrol", + "artist": "Redgum Mfg.", + "price": 14, + "price_band": "standard", + "atlas": "stock_toy_atlas_00.webp", + "uv": [ + 0.0, + 0.0, + 0.125, + 0.125 + ] + }, + { + "id": "toy_0001", + "title": "Rescue Voyagers", + "artist": "Billabong Games", + "price": 45, + "price_band": "collector", + "atlas": "stock_toy_atlas_00.webp", + "uv": [ + 0.125, + 0.0, + 0.25, + 0.125 + ] + }, + { + "id": "toy_0002", + "title": "Jungle Racers", + "artist": "Coral Coast Playsystems", + "price": 55, + "price_band": "grail", + "atlas": "stock_toy_atlas_00.webp", + "uv": [ + 0.25, + 0.0, + 0.375, + 0.125 + ] + }, + { + "id": "toy_0003", + "title": "Turbo Tots", + "artist": "Southern Cross Toys", + "price": 35, + "price_band": "collector", + "atlas": "stock_toy_atlas_00.webp", + "uv": [ + 0.375, + 0.0, + 0.5, + 0.125 + ] + }, + { + "id": "toy_0004", + "title": "Jungle Racers", + "artist": "Kanga Fun Co.", + "price": 28, + "price_band": "collector", + "atlas": "stock_toy_atlas_00.webp", + "uv": [ + 0.5, + 0.0, + 0.625, + 0.125 + ] + }, + { + "id": "toy_0005", + "title": "Aqua Buddies", + "artist": "Kanga Play Lab", + "price": 36, + "price_band": "collector", + "atlas": "stock_toy_atlas_00.webp", + "uv": [ + 0.625, + 0.0, + 0.75, + 0.125 + ] + }, + { + "id": "toy_0006", + "title": "Galaxy Friends", + "artist": "Bushland Toys", + "price": 37, + "price_band": "collector", + "atlas": "stock_toy_atlas_00.webp", + "uv": [ + 0.75, + 0.0, + 0.875, + 0.125 + ] + }, + { + "id": "toy_0007", + "title": "Micro Force", + "artist": "Redgum Playsystems", + "price": 27, + "price_band": "collector", + "atlas": "stock_toy_atlas_00.webp", + "uv": [ + 0.875, + 0.0, + 1.0, + 0.125 + ] + }, + { + "id": "toy_0008", + "title": "Galaxy Friends", + "artist": "Kanga Games", + "price": 9, + "price_band": "standard", + "atlas": "stock_toy_atlas_00.webp", + "uv": [ + 0.0, + 0.125, + 0.125, + 0.25 + ] + }, + { + "id": "toy_0009", + "title": "Speedy Ponies", + "artist": "Kanga Games", + "price": 18, + "price_band": "standard", + "atlas": "stock_toy_atlas_00.webp", + "uv": [ + 0.125, + 0.125, + 0.25, + 0.25 + ] + }, + { + "id": "toy_0010", + "title": "Thunder Patrol", + "artist": "Wattle Play Lab", + "price": 41, + "price_band": "collector", + "atlas": "stock_toy_atlas_00.webp", + "uv": [ + 0.25, + 0.125, + 0.375, + 0.25 + ] + }, + { + "id": "toy_0011", + "title": "Wonder Tots", + "artist": "Reef City Fun Co.", + "price": 28, + "price_band": "collector", + "atlas": "stock_toy_atlas_00.webp", + "uv": [ + 0.375, + 0.125, + 0.5, + 0.25 + ] + }, + { + "id": "toy_0012", + "title": "Thunder Patrol", + "artist": "Boomerang Novelties", + "price": 54, + "price_band": "grail", + "atlas": "stock_toy_atlas_00.webp", + "uv": [ + 0.5, + 0.125, + 0.625, + 0.25 + ] + }, + { + "id": "toy_0013", + "title": "Aqua Crew", + "artist": "Billabong Novelties", + "price": 18, + "price_band": "standard", + "atlas": "stock_toy_atlas_00.webp", + "uv": [ + 0.625, + 0.125, + 0.75, + 0.25 + ] + }, + { + "id": "toy_0014", + "title": "Jungle Pals", + "artist": "Billabong Workshop", + "price": 23, + "price_band": "collector", + "atlas": "stock_toy_atlas_00.webp", + "uv": [ + 0.75, + 0.125, + 0.875, + 0.25 + ] + }, + { + "id": "toy_0015", + "title": "Aqua Squad", + "artist": "Billabong Games", + "price": 19, + "price_band": "standard", + "atlas": "stock_toy_atlas_00.webp", + "uv": [ + 0.875, + 0.125, + 1.0, + 0.25 + ] + }, + { + "id": "toy_0016", + "title": "Magic Buddies", + "artist": "Outback Toys", + "price": 12, + "price_band": "standard", + "atlas": "stock_toy_atlas_00.webp", + "uv": [ + 0.0, + 0.25, + 0.125, + 0.375 + ] + }, + { + "id": "toy_0017", + "title": "Aqua Blasters", + "artist": "Boomerang Games", + "price": 34, + "price_band": "collector", + "atlas": "stock_toy_atlas_00.webp", + "uv": [ + 0.125, + 0.25, + 0.25, + 0.375 + ] + }, + { + "id": "toy_0018", + "title": "Aqua Blasters", + "artist": "Kanga Workshop", + "price": 40, + "price_band": "collector", + "atlas": "stock_toy_atlas_00.webp", + "uv": [ + 0.25, + 0.25, + 0.375, + 0.375 + ] + }, + { + "id": "toy_0019", + "title": "Thunder Rangers", + "artist": "Redgum Toys", + "price": 28, + "price_band": "collector", + "atlas": "stock_toy_atlas_00.webp", + "uv": [ + 0.375, + 0.25, + 0.5, + 0.375 + ] + }, + { + "id": "toy_0020", + "title": "Sparkle Patrol", + "artist": "Boomerang Toys", + "price": 27, + "price_band": "collector", + "atlas": "stock_toy_atlas_00.webp", + "uv": [ + 0.5, + 0.25, + 0.625, + 0.375 + ] + }, + { + "id": "toy_0021", + "title": "Wonder Buddies", + "artist": "Boomerang Mfg.", + "price": 36, + "price_band": "collector", + "atlas": "stock_toy_atlas_00.webp", + "uv": [ + 0.625, + 0.25, + 0.75, + 0.375 + ] + }, + { + "id": "toy_0022", + "title": "Magic Bots", + "artist": "Bushland Toys", + "price": 12, + "price_band": "standard", + "atlas": "stock_toy_atlas_00.webp", + "uv": [ + 0.75, + 0.25, + 0.875, + 0.375 + ] + }, + { + "id": "toy_0023", + "title": "Jungle Voyagers", + "artist": "Bushland Workshop", + "price": 13, + "price_band": "standard", + "atlas": "stock_toy_atlas_00.webp", + "uv": [ + 0.875, + 0.25, + 1.0, + 0.375 + ] + }, + { + "id": "toy_0024", + "title": "Thunder Cruisers", + "artist": "Billabong Workshop", + "price": 18, + "price_band": "standard", + "atlas": "stock_toy_atlas_00.webp", + "uv": [ + 0.0, + 0.375, + 0.125, + 0.5 + ] + }, + { + "id": "toy_0025", + "title": "Magic Bots", + "artist": "Wattle Games", + "price": 39, + "price_band": "collector", + "atlas": "stock_toy_atlas_00.webp", + "uv": [ + 0.125, + 0.375, + 0.25, + 0.5 + ] + }, + { + "id": "toy_0026", + "title": "Wonder Beasts", + "artist": "Billabong Workshop", + "price": 38, + "price_band": "collector", + "atlas": "stock_toy_atlas_00.webp", + "uv": [ + 0.25, + 0.375, + 0.375, + 0.5 + ] + }, + { + "id": "toy_0027", + "title": "Robo Blasters", + "artist": "Redgum Play Lab", + "price": 12, + "price_band": "standard", + "atlas": "stock_toy_atlas_00.webp", + "uv": [ + 0.375, + 0.375, + 0.5, + 0.5 + ] + }, + { + "id": "toy_0028", + "title": "Speedy Voyagers", + "artist": "Reef City Workshop", + "price": 46, + "price_band": "collector", + "atlas": "stock_toy_atlas_00.webp", + "uv": [ + 0.5, + 0.375, + 0.625, + 0.5 + ] + }, + { + "id": "toy_0029", + "title": "Wonder Racers", + "artist": "Southern Cross Mfg.", + "price": 45, + "price_band": "collector", + "atlas": "stock_toy_atlas_00.webp", + "uv": [ + 0.625, + 0.375, + 0.75, + 0.5 + ] + }, + { + "id": "toy_0030", + "title": "Cosmic Racers", + "artist": "Kanga Fun Co.", + "price": 5, + "price_band": "bargain", + "atlas": "stock_toy_atlas_00.webp", + "uv": [ + 0.75, + 0.375, + 0.875, + 0.5 + ] + }, + { + "id": "toy_0031", + "title": "Turbo Beasts", + "artist": "Reef City Games", + "price": 17, + "price_band": "standard", + "atlas": "stock_toy_atlas_00.webp", + "uv": [ + 0.875, + 0.375, + 1.0, + 0.5 + ] + } + ] +} \ No newline at end of file diff --git a/pipeline/validate_manifest.py b/pipeline/validate_manifest.py index 8ed39d2..dc948e0 100644 --- a/pipeline/validate_manifest.py +++ b/pipeline/validate_manifest.py @@ -128,6 +128,14 @@ def main(): if f.startswith("procity_") and f not in record: err(f"manifest GLB not in _published.json (provenance drift): {f}") + # pack-index QA (round-8 E2): a bad stock-pack bake fails the same gate as the manifest + try: + import validate_pack + if validate_pack.main() != 0: + err("stock-pack index validation failed (see pack-QA errors above)") + except Exception as e: + warn(f"pack-QA skipped: {e}") + print(f"manifest v{m.get('version')} — facades {len(facade)}, " f"skins {sum(len(v) if isinstance(v, list) else (len(v) if isinstance(v, dict) else 0) for v in sk.values())} groups, " f"GLBs {n_glb}") diff --git a/pipeline/validate_pack.py b/pipeline/validate_pack.py new file mode 100644 index 0000000..9f94401 --- /dev/null +++ b/pipeline/validate_pack.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +"""Validate GODVERSE stock-pack indexes — round-8 E2 QA gate. + +A bad bake (out-of-range UVs, empty titles, unknown price band, dangling atlas ref) must fail QA +loudly instead of rendering blank/garbled sleeves in the dig. Validates every +`pipeline/packs/stock__index.json` (the committed pack contract; atlases live on the depot). +Called by validate_manifest.py so it runs inside qa.sh gate 3. + + python3 pipeline/validate_pack.py # exit 0 = all packs valid, 1 = a bad index +""" +import json, os, sys, glob + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +PACKS = os.path.join(ROOT, "pipeline", "packs") +KINDS = {"record", "book", "toy"} +BANDS = {"bargain", "standard", "collector", "grail"} +REQUIRED = ("id", "title", "artist", "price", "price_band", "atlas", "uv") + + +def validate_index(path): + errs = [] + try: + idx = json.load(open(path)) + except Exception as e: + return [f"{os.path.basename(path)}: does not parse: {e}"] + name = os.path.basename(path) + if idx.get("kind") not in KINDS: + errs.append(f"{name}: bad kind {idx.get('kind')!r}") + atlases = set(idx.get("atlases", [])) + if not atlases: + errs.append(f"{name}: no atlases listed") + items = idx.get("items", []) + if not items: + errs.append(f"{name}: no items") + ids = set() + for i, it in enumerate(items): + miss = [f for f in REQUIRED if f not in it] + if miss: + errs.append(f"{name}[{i}]: missing {miss}"); continue + if not str(it["title"]).strip() or not str(it["artist"]).strip(): + errs.append(f"{name}[{it['id']}]: empty title/artist") + if it["id"] in ids: + errs.append(f"{name}: duplicate id {it['id']}") + ids.add(it["id"]) + if it["price_band"] not in BANDS: + errs.append(f"{name}[{it['id']}]: bad price_band {it['price_band']!r}") + if not isinstance(it["price"], (int, float)) or it["price"] < 0: + errs.append(f"{name}[{it['id']}]: bad price {it['price']!r}") + if it["atlas"] not in atlases: + errs.append(f"{name}[{it['id']}]: atlas {it['atlas']!r} not in atlases[]") + uv = it["uv"] + if not (isinstance(uv, list) and len(uv) == 4): + errs.append(f"{name}[{it['id']}]: uv must be [u0,v0,u1,v1]"); continue + u0, v0, u1, v1 = uv + if not (0 <= u0 < u1 <= 1 and 0 <= v0 < v1 <= 1): + errs.append(f"{name}[{it['id']}]: uv out of range / inverted: {uv}") + return errs + + +def main(): + idxs = sorted(glob.glob(os.path.join(PACKS, "stock_*_index.json"))) + if not idxs: + print("pack-QA: no committed pack indexes in pipeline/packs/ — nothing to validate") + return 0 + all_errs = [] + for p in idxs: + e = validate_index(p) + all_errs += e + idx = json.load(open(p)) + print(f" {os.path.basename(p):28} kind={idx.get('kind')} items={len(idx.get('items', []))} " + f"{'OK' if not e else 'ERR('+str(len(e))+')'}") + for e in all_errs: + print(f" ERR {e}") + if all_errs: + print(f"pack-QA FAIL — {len(all_errs)} error(s)") + return 1 + print(f"pack-QA OK — {len(idxs)} pack index(es) valid") + return 0 + + +if __name__ == "__main__": + sys.exit(main())