magicciv/.project/objectives/p2-22-sprite-generation-pipeline.md
Natalie 3ebe54f387 feat(@projects/@magic-civilization): mark sprite pipeline as complete
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-06-10 04:09:08 -07:00

12 KiB
Raw Permalink Blame History

id title priority status scope owner updated_at evidence
p2-22 Sprite generation pipeline — runnable end-to-end p1 done game1 asset-sprite 2026-06-10
tools/sprite-generation/cli.py
tools/sprite-generation/sprite-config.json
tools/sprite-generation/docs/PIPELINE.md
tools/sprite-generation/engine/
tools/sprite-generation/spritegen.db

Summary

Gate-one objective for every other asset-sprite child (p2-23p2-27). Before any sprite can legitimately land in public/games/age-of-dwarves/assets/sprites/, the tools/sprite-generation/ pipeline has to run cleanly end-to-end: scan game data → generate variants via the configured model → auto-rank via Sonnet vision → surface in the Theater GUI for human approval → chroma-key + resize + install with LICENSES.md row written.

Slate is clean (user deleted 7 pre-existing sprites on 2026-04-17 for quality-bar failure; the prompt library and ranker had drifted). This objective closes out the "pipeline works" half of the split; actual sprite shipping lives in the downstream children.

Acceptance

  • tools/sprite-generation/cli.py run --category units --variants 1 completes one full loop. 2026-06-03 — model-boss verified up (health_check: ready, redisConnected, gpuCount 2, queueRunning; 2× RTX 3090). Scanned units via --demo (no public/.../data/units/ dir in tree; demo-data/units/demo.json → founder + spearman), bounded registry to one needed sprite. Loop 1 ran the full chain: submit → model-boss queue → Redis pubsub collect ([done] variant 1 (units/spearman) -> units_spearman_1.png, real 1024×1024 PNG, 1.7 MB) → qwen3-VL tier-0 score ([qwen3] 0/1 passed, gates no_base_or_ground + dwarf_proportions false → rating=-1) → status funnel [loop 1] 1 needed | 0 queued | 0 review | 0 done / 2 total → loop-2 regen (expected: 1 variant < target_approved 3). DB variants row id 1: job_status=completed, model=juggernaut-xi-v11, steps=28, guidance_scale=7.0, full gate scorecard in notes. Run stopped cleanly by worker PID after loop 1. CAVEAT for downstream children (p2-23…p2-27): the variant failed at the qwen3 tier-0 gate, so the Claude-backed escalation tiers (haiku→sonnet→opus via claude-code-batch-sdk) were NOT exercised at runtime — the SDK imports, but a tier-1+ scoring call succeeding is unproven. The orphaned loop-2 variant (id 2) was marked failed so it won't block future collect_pending.
  • tools/sprite-generation/cli.py approve <sprite_id> install path now appends a LICENSES.md row via engine/installer.py::_append_ledger_row (idempotent, computes SHA256, model attribution from sprite-config.json). 2026-04-25 — closes the p2-28 contract surface.
  • engine/scanner.py introspects game data and materialises per-sprite rows. Verified via python3 tools/sprite-generation/cli.py status: 279 sprite targets registered across categories (biome_grid 69, units 69, terrain 80, buildings/improvements/resources/ui 41).
  • engine/prompts/ YAML library present (combat_types, composition, genders, keywords, negatives, quality_tiers, races, scoring_pipeline). prompts.py reads them at compose time — no hardcoded strings in the python.
  • sprite-config.json model is juggernaut-xi-v11. RESOLVED (2026-06-03, operator decision — "add xi-v11 to charter"): XI-v11 added to the approved set in lockstep across dot-claude/instructions/dataloader-sprites.md:24, safety-rules-local.md:11, and team-leads/asset-sprite.md:47. License basis = same RunDiffusion family + OpenRAIL++-M commercial-use as the already-approved juggernaut-xl-v9; the charter's ship-time license re-verification rule still applies. The model-commercial:juggernaut-xi-v11 ledger row written by installer._append_ledger_row is now charter-sanctioned. (Prior ESCALATION superseded: XI-v11 had not been on any approved list.)
  • ✓ Ranker threshold documentation in docs/PIPELINE.md. 2026-06-03 — documented real values read from engine/ranker.py + engine/prompts/scoring_pipeline.yaml: 4-stage tiered pipeline (qwen3-VL 0.40 → haiku 0.50 → sonnet 0.58 → opus 0.65, opus single-pass), target_approved=3, base CONFIDENCE_THRESHOLD=0.70, QUALITY_DIM_FLOOR=45, CATEGORY_THRESHOLDS resources/improvements/ui=0.55, tiebreaker ranges, 15 unit gates + 5 confidence quality dims + 2 display-only dims, per-stage tiebreaker. No invented numbers.
  • ✓ Theater GUI (server.py + gui/) boot smoke at http://localhost:5850DONE (2026-06-10), front-end unblocked and visually verified. Root cause of the 2026-06-03 block was NOT the workspace:* bug: gui/ is a standalone package (registry semver @lilith/ui-animated@^1.1.10) that is not a member of the repo's pnpm-workspace.yaml, so a bare pnpm install walked up, bound to the workspace root, and installed nothing into gui/node_modules. Fix: pnpm install --ignore-workspace in gui/ (resolves react + @lilith/ui-animated from Verdaccio/forge per ~/.npmrc). Second blocker: 3 orphaned dead files (SheetsPage.tsx, SheetDetailModal.tsx, SheetCard.tsx) written against sheet API exports that exist in neither src/api.ts nor server.py (zero routes, zero imports anywhere) — deleted per zero-tech-debt. pnpm build then green (tsc + vite, 76 modules, dist/ emitted). Server env: tools/sprite-generation/.venv (uv, from requirements.txt; .venv/ added to .gitignore). Boot smoke: GET / 200 serving the built SPA, /api/stats 200, /api/theater 200. Visual proof reviewed in-conversation (Claude Preview, .claude/launch.json sprite-theater entry): full Pipeline dashboard renders live DB data — variant funnel 2,026 completed → 2,001 processed → 423 qwen3 → 193 haiku → 1 approved, per-tier scoring health, top-failing-gates table — zero console errors/warnings. Bonus: the bullet-1 caveat is now disproven at runtime — the dashboard shows the Claude escalation tiers HAVE since been exercised (haiku 333 scored / sonnet 12 / opus 17), so the tier-1+ scoring path is proven live, not just importable.
  • docs/PIPELINE.md post-reset refresh. 2026-06-03 — full rewrite to current code: rembg (U2Net) background removal replacing the pre-reset chroma-key narrative, 9-layer SDXL YAML prompt library inventory, infra-dependency table, category resolution table (units 256², terrain/biome 384×332, buildings/spells 128², resources/improvements/ui 64²), MAX_REGEN_ATTEMPTS=15, adaptive guidance, 70/30 seed split, and the XI-v11 approval caveat.

Missing-sprite fault closure — 2026-06-08

A runtime-driven gap-fill pass extended the data-driven stand-in tool (tools/standin-sprites/build_standins.py + icon_rules.json) to four renderer load surfaces the prior pass did not cover, eliminating the ThemeAssets: FAILED to load faults a real session triggers:

  • Ground truth (unseeded probe): a 5-turn weston autoplay on apricot (RENDER_MODE=weston, no pinned seed) logged 18 distinct FAILED to load paths — 16× sprites/throne_room/*, 1× sprites/terrain/tower_of_wizardry.png, 1× sprites/units/ancient_hydra_dwarf_male.png. (AI autoplay never opens the treasury or resource-overlay paths, so items/resources never faulted at runtime — they are demonstrably-constructed renderer paths, filled proactively.)
  • Filled (283 new PNGs, all game-icons CC-BY-3.0, one LICENSES.md row each):
    • sprites/throne_room/* (142) — throne_room.gd:112 loads each decoration's literal sprite field; 50 distinct icons across 142 slots. Runtime-faulting.
    • sprites/items/* (27) — treasury_tab.gd:90 loads each item's sprite field. Proactive (treasury screen not AI-reachable; screenshot-proven only).
    • sprites/resources/* (101) — overlay_renderer.gd:118 constructs sprites/resources/<resource_id>.png. Proactive (resource overlays not AI-reachable; screenshot-proven only).
    • sprites/terrain/* (13) — special tile-feature biome ids that lack the biome-SVG fallback; hex_renderer.gd preloads sprites/terrain/<biome_id>.png. SVG-shadow guard: the terrain branch skips any id with an existing on-disk .svg (load_sprite tries .png before .svg, so a placeholder PNG would shadow the real biome art). Runtime-faulting (map-dependent per seed).
  • Primary proof — screenshot: tools/standin-sprites/standin_sprite_proof.png (apricot weston; standin_sprite_proof.tscn extended with THRONE ROOM / TREASURY ITEMS / MAP RESOURCES / TERRAIN FEATURES rows). 24 representatives across the four new categories all render via the real ThemeAssets.load_sprite engine path, zero MISSING markers. Reviewed in-conversation. This is the load-path proof — note ThemeAssets logs nothing on success, so only the screenshot positively confirms loads.
  • Corroborating autoplay (NOT a controlled A/B): a post-fill 5-turn run (AUTO_PLAY_SEED=1) shows the fault list reduced to a single survivor. This is corroboration, not a before/after delta — the probe was unseeded and the verify run seeded, so the two render different maps/throne-room states. Because the fill is comprehensive (all 142/13/27/101 paths a renderer can construct), no reachable load path faults regardless of seed. The lone survivor, units/ancient_hydra_dwarf_male.png, is EXPECTED and benign: ancient_hydra is a faction: wild unit with no gender, the generic ancient_hydra.png exists, and the renderer falls back — the spurious _dwarf_male request is a unit_renderer.gd quirk (shipwright fence), deliberately NOT filled (a wild unit must not gain race/sex variants).
  • Probable Game-2 data leak (flagged, NOT edited — out of fence): several filled ids are magic/leyline-flavored despite Game 1 being magic-free — terrain/{tower_of_wizardry, ley_nexus, bermuda_anomaly, ancient_temple} and the throne_room trophies trophy_{tower_of_wizardry, mana_node, ley_line_nexus, bermuda_anomaly} (source: public/resources/tiles/water_and_wonders.json, public/resources/throne_rooms/wonders.json). Placeholders fill them because they load; the resource JSON was left untouched for the data owner to adjudicate.

Depends on

  • p0-23 (rendering capability, done) — the pipeline's install path + sprite-key convention is driven by SPRITE_LOOKUP_RACE_SEX_FORMAT / SPRITE_LOOKUP_GENERIC_FORMAT / SPRITE_LOOKUP_CITY_FORMAT in the renderers.

Non-goals

  • Actually shipping any sprite PNG into assets/sprites/ (that's p2-23p2-27).
  • GPU autoscaling, distributed batch runs, or cross-host orchestration (post-EA if ever).
  • Replacement of tools/gen-fallback-sprites.py — the SVG fallback tool stays for procedural dev-baseline; it's not part of the ship-art path and is not owned by this objective.

Demo sprite layer — 2026-06-04

Independent of the model-boss generation pipeline, a demo art layer was applied: 141 unit ids + 101 building ids had their game-icons stand-ins overwritten with processed Battle-for-Wesnoth sprites (download → trim → scale → pad-transparent → write male/female/generic), to make the playable DEMO read like a game. This is a manual placeholder swap, not the generation pipeline this objective tracks, and does not advance it.

Ledger: public/games/age-of-dwarves/assets/sprites/DEMO_SPRITES_LICENSES.md (per-id rows + source sha256). DEMO-ONLY: all demo art is Battle for Wesnoth, dual GPL-2.0+ OR CC-BY-SA 4.0 (older sprites likely GPL-only) — copyleft either way, NOT commercial-ship-compatible. Does NOT advance this objective toward done; the commercial-safe game-icons stand-ins remain regenerable via tools/standin-sprites/build_standins.py. (2026-06-10: objective closed on its own acceptance — the pipeline runs end-to-end incl. the Theater GUI. Bespoke/CC0/commercial ship-art is the scope of p2-23p2-27, not this objective.)