magicciv/tools/sprite-generation/engine/pipeline.py

167 lines
5.9 KiB
Python

"""End-to-end approval pipeline: approve → process → install → manifest.
Single entry point for shipping a sprite variant from raw generation
through to game-ready installed asset with manifest entry.
"""
from __future__ import annotations
import sqlite3
from pathlib import Path
from engine.processor import SpriteProcessor
from engine.registry import SpriteRegistry
class SpritePipeline:
"""Orchestrates the approve → process → install → manifest flow."""
def __init__(
self,
registry: SpriteRegistry,
raw_dir: Path,
variants_dir: Path,
assets_dir: Path,
game_db_path: Path,
) -> None:
self.registry = registry
self.raw_dir = raw_dir
self.variants_dir = variants_dir
self.assets_dir = assets_dir
self.game_db_path = game_db_path
self.processor = SpriteProcessor()
self._ensure_game_db()
def _ensure_game_db(self) -> None:
"""Create the game-side sprites.db manifest if it doesn't exist."""
self.game_db_path.parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(str(self.game_db_path))
conn.execute("""
CREATE TABLE IF NOT EXISTS sprites (
entity_type TEXT NOT NULL,
entity_id TEXT NOT NULL,
quality INTEGER,
variant TEXT,
path TEXT NOT NULL,
width INTEGER NOT NULL,
height INTEGER NOT NULL,
installed_at TEXT NOT NULL,
PRIMARY KEY (entity_type, entity_id, quality, variant)
)
""")
conn.commit()
conn.close()
def approve_and_install(
self,
variant_id: int,
alt_name: str | None = None,
) -> Path | None:
"""Full pipeline for a single variant.
1. Mark variant as approved in pipeline sprites.db
2. Process raw image (background removal + resize)
3. Copy processed image to game assets directory
4. Write entry to game-side sprites.db manifest
Args:
variant_id: The variant to approve and install.
alt_name: If set, install as an alternate (e.g. "alt" → founder_alt.png).
If None, installs as the primary sprite.
Returns:
Path to installed asset, or None on failure.
"""
# 1. Get variant and sprite data
variant = self.registry.conn.execute(
"SELECT * FROM variants WHERE id=?", (variant_id,)
).fetchone()
if not variant:
print(f"Variant #{variant_id} not found")
return None
sprite_id = variant["sprite_id"]
sprite = self.registry.get_sprite(sprite_id)
if not sprite:
print(f"Sprite '{sprite_id}' not found")
return None
raw_path = Path(variant["raw_path"])
if not raw_path.exists():
print(f"Raw image not found: {raw_path}")
return None
category = sprite["category"]
# 2. Approve in pipeline DB
self.registry.approve_variant(variant_id)
print(f" [1/4] Approved #{variant_id} for {sprite_id}")
# 3. Process (background removal + resize)
safe_name = sprite_id.replace("/", "_")
processed_filename = f"{safe_name}_{variant_id}.png"
processed_path = self.variants_dir / processed_filename
self.variants_dir.mkdir(parents=True, exist_ok=True)
if not self.processor.process(raw_path, category, processed_path):
print(f" Processing failed for #{variant_id}")
return None
# Update processed_path in pipeline DB
self.registry.update_variant_status(
variant_id, variant["job_status"],
processed_path=str(processed_path),
)
print(f" [2/4] Processed → {processed_path.name}")
# 4. Install to game assets
# Use the sprite's registered install_path (follows {id}_{race}_{gender} convention)
# or fall back to entity_id-based path
install_path = sprite.get("install_path")
if install_path:
asset_rel_path = install_path
else:
entity_id = sprite["entity_id"]
asset_rel_path = f"sprites/{category}/{entity_id}.png"
if alt_name:
# Insert alt name before extension: spearmen_dwarves_m.png → spearmen_dwarves_m_front.png
base, ext = asset_rel_path.rsplit(".", 1)
asset_rel_path = f"{base}_{alt_name}.{ext}"
asset_full_path = self.assets_dir / asset_rel_path
asset_full_path.parent.mkdir(parents=True, exist_ok=True)
import shutil
shutil.copy2(processed_path, asset_full_path)
# Mark installed in pipeline DB
self.registry.mark_installed(sprite_id)
print(f" [3/4] Installed → {asset_rel_path}")
# 5. Write to game manifest DB
from PIL import Image
img = Image.open(asset_full_path)
width, height = img.size
from datetime import datetime, timezone
now = datetime.now(timezone.utc).isoformat()
conn = sqlite3.connect(str(self.game_db_path))
conn.execute(
"""INSERT OR REPLACE INTO sprites
(entity_type, entity_id, quality, variant, path, width, height, installed_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
(category, entity_id, 0, alt_name or "default", asset_rel_path, width, height, now),
)
conn.commit()
conn.close()
print(f" [4/4] Manifest → {self.game_db_path.name} ({category}/{entity_id})")
return asset_full_path
def list_installed(self) -> list[dict]:
"""List all entries in the game manifest DB."""
conn = sqlite3.connect(str(self.game_db_path))
conn.row_factory = sqlite3.Row
rows = conn.execute("SELECT * FROM sprites ORDER BY entity_type, entity_id").fetchall()
conn.close()
return [dict(r) for r in rows]