chore(sprite-generation): 🔧 Update sprite generation CLI/server logic and database configuration

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Claude Code 2026-03-26 11:38:32 -07:00
parent 516994ba35
commit c07d07a2ab
5 changed files with 178 additions and 4 deletions

View file

@ -26,9 +26,9 @@ RAW_DIR = TOOL_DIR / "raw"
VARIANTS_DIR = TOOL_DIR / "variants"
HEX_MASK_PATH = TOOL_DIR / "hex_mask.png"
LOCAL_DATA = PROJECT / "games" / "age-of-four" / "data"
LOCAL_DATA = PROJECT / "games" / "age-of-dwarves" / "data"
DEMO_DATA = TOOL_DIR / "demo-data"
ASSETS_DIR = PROJECT / "games" / "age-of-four" / "assets"
ASSETS_DIR = PROJECT / "games" / "age-of-dwarves" / "assets"
def _data_dir(args: argparse.Namespace) -> Path:
@ -129,10 +129,13 @@ def cmd_generate(args: argparse.Namespace) -> None:
print(f" ... and {len(sprite_ids) - 50} more")
return
pose_ref = Path(args.pose_ref) if args.pose_ref else None
completed = asyncio.run(gen.generate_batch(
sprite_ids=sprite_ids,
variants_per=args.variants,
priority=args.priority,
pose_reference=pose_ref,
img2img_strength=args.strength,
))
print(f"\nGenerated {completed} images. Run 'rank' to score them.")
@ -169,6 +172,24 @@ def cmd_install(args: argparse.Namespace) -> None:
print(f"\n{'Would install' if args.dry_run else 'Installed'}: {count} sprites")
def cmd_approve(args: argparse.Namespace) -> None:
from engine.pipeline import SpritePipeline
reg = _registry()
pipeline = SpritePipeline(
registry=reg,
raw_dir=RAW_DIR,
variants_dir=VARIANTS_DIR,
assets_dir=ASSETS_DIR,
game_db_path=LOCAL_DATA / "sprites.db",
)
result = pipeline.approve_and_install(args.variant, alt_name=args.alt)
if result:
print(f"\nShipped: {result}")
else:
print("\nFailed to install.")
def cmd_reset(args: argparse.Namespace) -> None:
reg = _registry()
if not args.sprite:
@ -195,6 +216,136 @@ def cmd_review(args: argparse.Namespace) -> None:
print("Install dependencies: pip install fastapi uvicorn")
def cmd_run(args: argparse.Namespace) -> None:
"""Orchestrate the full generate → rank → regen loop continuously.
Runs until all sprites are in review (ranked) or installed.
The human reviews and approves via the GUI at localhost:5850.
"""
import asyncio
import time
from engine.generator import SpriteGenerator
from engine.ranker import SpriteRanker
reg = _registry()
config = _load_config()
gen = SpriteGenerator(config=config, registry=reg, raw_dir=RAW_DIR)
ranker = SpriteRanker(registry=reg, raw_dir=RAW_DIR)
pose_ref = Path(args.pose_ref) if args.pose_ref else None
batch_size = args.batch or 4
variants_per = args.variants
# Start GUI server in background thread
import threading
def _serve():
from server import create_app
import uvicorn
app = create_app(registry=reg, raw_dir=RAW_DIR, variants_dir=VARIANTS_DIR)
uvicorn.run(app, host="0.0.0.0", port=args.port, log_level="warning")
server_thread = threading.Thread(target=_serve, daemon=True)
server_thread.start()
print(f"Review GUI: http://localhost:{args.port}/?spriteTheater=true")
loop_count = 0
while True:
loop_count += 1
# --- Phase 1: Generate ---
needed = reg.get_sprites(
category=args.category,
status="needed",
limit=batch_size,
)
if needed:
sprite_ids = [s["id"] for s in needed]
print(f"\n[loop {loop_count}] Generating {len(sprite_ids)} sprites × {variants_per} variants...")
completed = asyncio.run(gen.generate_batch(
sprite_ids=sprite_ids,
variants_per=variants_per,
priority="high",
pose_reference=pose_ref,
img2img_strength=args.strength,
))
print(f" Generated {completed} images")
# --- Phase 2: Rank unranked review sprites ---
unranked = reg.conn.execute("""
SELECT DISTINCT s.id FROM sprites s
WHERE s.status = 'review'
AND NOT EXISTS (
SELECT 1 FROM variants v
WHERE v.sprite_id = s.id
AND v.job_status = 'completed'
AND v.notes IS NOT NULL
)
LIMIT ?
""", (batch_size,)).fetchall()
if unranked:
print(f"\n[loop {loop_count}] Ranking {len(unranked)} unranked sprites...")
for row in unranked:
sprite_id = row["id"]
result = asyncio.run(ranker.rank_and_filter(sprite_id))
good = result["good_count"]
total = len(result["ranked"])
if result["needs_regen"]:
reg.update_sprite_status(sprite_id, "needed")
print(f" {sprite_id}: {good}/{total} good — needs regen, reset to needed")
else:
print(f" {sprite_id}: {good}/{total} good — ready for review")
# --- Phase 3: Re-rank previously ranked but deficient sprites ---
# (sprites in review that HAVE been ranked but didn't pass)
deficient = reg.conn.execute("""
SELECT DISTINCT s.id FROM sprites s
WHERE s.status = 'review'
AND EXISTS (
SELECT 1 FROM variants v
WHERE v.sprite_id = s.id
AND v.job_status = 'completed'
AND v.notes IS NOT NULL
)
LIMIT ?
""", (batch_size,)).fetchall()
# Only re-check deficient if nothing else to do
if not needed and not unranked and deficient:
print(f"\n[loop {loop_count}] Re-checking {len(deficient)} ranked sprites...")
for row in deficient:
sprite_id = row["id"]
result = asyncio.run(ranker.rank_and_filter(sprite_id))
if result["needs_regen"]:
reg.update_sprite_status(sprite_id, "needed")
print(f" {sprite_id}: still deficient, reset to needed")
# --- Status ---
stats = reg.get_stats()
total_row = stats["total"]
needed_count = total_row.get("needed", 0)
review_count = total_row.get("review", 0)
approved_count = total_row.get("approved", 0)
installed_count = total_row.get("installed", 0)
total_count = sum(total_row.values())
done_count = approved_count + installed_count
print(f"\n[loop {loop_count}] Status: {needed_count} needed, {review_count} review, {done_count} done / {total_count} total")
if needed_count == 0 and review_count == 0:
print("\nAll sprites processed. Waiting for new work...")
# --- Pause between loops ---
pause = 10 if needed else 30
print(f" Next loop in {pause}s... (Ctrl+C to stop)")
try:
time.sleep(pause)
except KeyboardInterrupt:
print("\nStopping pipeline.")
break
def cmd_export(args: argparse.Namespace) -> None:
import csv
import io
@ -223,7 +374,17 @@ def main() -> None:
parser.add_argument("--demo", action="store_true", help="Use minimal demo data (1 per domain)")
sub = parser.add_subparsers(dest="command")
# start (default) — launch GUI server
# run — full pipeline orchestrator
p = sub.add_parser("run", help="Run full pipeline: generate → rank → regen loop + GUI")
p.add_argument("--port", type=int, default=5850, help="GUI server port (default: 5850)")
p.add_argument("--category", type=str, help="Only process one category")
p.add_argument("--variants", type=int, default=4, help="Variants per sprite (default: 4)")
p.add_argument("--batch", type=int, default=4, help="Sprites per batch (default: 4)")
p.add_argument("--pose-ref", type=str, help="Pose reference image for img2img")
p.add_argument("--strength", type=float, default=0.6, help="img2img strength (default: 0.6)")
p.set_defaults(func=cmd_run)
# start — launch GUI server only
p = sub.add_parser("start", help="Launch review GUI server")
p.add_argument("--port", type=int, default=5850, help="Server port (default: 5850)")
p.set_defaults(func=cmd_review)
@ -244,6 +405,8 @@ def main() -> None:
p.add_argument("--priority", choices=["low", "normal", "high"], default="normal")
p.add_argument("--max", type=int, help="Max sprites to generate")
p.add_argument("--dry-run", action="store_true", help="Show what would be generated")
p.add_argument("--pose-ref", type=str, help="Path to pose reference image for img2img (consistent facing)")
p.add_argument("--strength", type=float, default=0.75, help="img2img denoising strength (0=copy, 1=ignore ref)")
p.set_defaults(func=cmd_generate)
# rank
@ -258,6 +421,12 @@ def main() -> None:
p.add_argument("--dry-run", action="store_true")
p.set_defaults(func=cmd_install)
# approve
p = sub.add_parser("approve", help="Approve variant → process → install → manifest")
p.add_argument("variant", type=int, help="Variant ID to approve (e.g. 129)")
p.add_argument("--alt", type=str, help="Install as alternate (e.g. --alt front)")
p.set_defaults(func=cmd_approve)
# reset
p = sub.add_parser("reset", help="Reset sprite back to needed status")
p.add_argument("--sprite", type=str, required=True, help="Sprite ID to reset")

View file

@ -130,6 +130,11 @@ def create_app(
registry.update_sprite_status(sprite_id, "skip")
return {"status": "skip"}
@app.post("/api/variants/{variant_id:int}/reject")
def reject_variant(variant_id: int) -> dict:
registry.reject_variant(variant_id)
return {"status": "rejected", "variant_id": variant_id}
@app.post("/api/sprites/{sprite_id:path}/regenerate")
def regenerate_sprite(sprite_id: str, body: RegenerateRequest) -> dict:
sprite = registry.get_sprite(sprite_id)
@ -182,7 +187,7 @@ def create_app(
@app.get("/api/variants/recent")
def get_recent_variants(
limit: Annotated[int, Query(ge=1, le=100)] = 30,
limit: Annotated[int, Query(ge=1, le=5000)] = 30,
) -> list[dict]:
return registry.get_recent_variants(limit=limit)

Binary file not shown.