feat(sprite-generation): ✨ Add AI-powered sprite generation with new prompt strategies and registry integration
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
c08ad31b8a
commit
005c856053
3 changed files with 73 additions and 59 deletions
|
|
@ -116,17 +116,14 @@ class SpriteGenerator:
|
|||
priority: str = "normal",
|
||||
dimension_id: int | None = None,
|
||||
) -> int | None:
|
||||
"""Submit a single generation job.
|
||||
"""Queue a single generation job via model-boss async jobs API.
|
||||
|
||||
Returns variant_id or None on failure.
|
||||
Submits to the queue and returns immediately. Does NOT wait for
|
||||
the image — that's handled by poll_pending().
|
||||
Returns variant_id or None on submission failure.
|
||||
"""
|
||||
full_prompt = ", ".join(p for p in [prompt, prompt_modifier] if p)
|
||||
|
||||
metadata = {
|
||||
"project": "magic-civilization",
|
||||
"sprite_id": sprite_id,
|
||||
}
|
||||
|
||||
job_id = self._submit_job(
|
||||
prompt=full_prompt,
|
||||
negative=negative,
|
||||
|
|
@ -134,7 +131,6 @@ class SpriteGenerator:
|
|||
height=height,
|
||||
seed=seed,
|
||||
priority=priority,
|
||||
metadata=metadata,
|
||||
)
|
||||
if job_id is None:
|
||||
return None
|
||||
|
|
@ -146,8 +142,6 @@ class SpriteGenerator:
|
|||
prompt_modifier=prompt_modifier,
|
||||
job_id=job_id,
|
||||
)
|
||||
|
||||
metadata["variant_id"] = variant_id
|
||||
return variant_id
|
||||
|
||||
def poll_pending(self, max_polls: int = 1000) -> dict:
|
||||
|
|
@ -250,9 +244,8 @@ class SpriteGenerator:
|
|||
height: int,
|
||||
seed: int,
|
||||
priority: str,
|
||||
metadata: dict,
|
||||
) -> str | None:
|
||||
"""Submit a single job to model-boss API. Returns job_id or None."""
|
||||
"""Submit a job to model-boss async queue. Returns job_id or None."""
|
||||
body = {
|
||||
"model": self.model,
|
||||
"prompt": prompt,
|
||||
|
|
@ -263,7 +256,7 @@ class SpriteGenerator:
|
|||
"guidanceScale": self.defaults.get("guidance_scale", 7.5),
|
||||
"seed": seed,
|
||||
"xPriority": priority,
|
||||
"xClientId": f"sprite-gen:{metadata.get('sprite_id', 'unknown')}",
|
||||
"xClientId": "sprite-generator",
|
||||
}
|
||||
data = json.dumps(body).encode()
|
||||
req = Request(
|
||||
|
|
|
|||
|
|
@ -28,36 +28,38 @@ STYLE_PREFIXES: dict[str, str] = {
|
|||
"masterpiece, best quality, game asset"
|
||||
),
|
||||
"units": (
|
||||
"single fantasy character, 3/4 top-down isometric view, "
|
||||
"full body visible, centered on transparent background, "
|
||||
"painted illustration style like Master of Magic or Heroes of Might and Magic unit portraits, "
|
||||
"one character only, no background scenery, no terrain, no other characters, "
|
||||
"clean silhouette suitable for a strategy game unit token, "
|
||||
"masterpiece, best quality, game sprite"
|
||||
"isometric game character sprite viewed from above at 45-degree angle, "
|
||||
"NOT front-facing, NOT side view, seen from elevated camera looking down, "
|
||||
"single character standing on flat grey background, "
|
||||
"full body visible head to toe, small figure centered in frame, "
|
||||
"painted digital art style, strategy game unit sprite like Age of Wonders or Warcraft III, "
|
||||
"one character only, clean edges, no scenery, no ground texture, "
|
||||
"masterpiece, best quality, game sprite on neutral background"
|
||||
),
|
||||
"buildings": (
|
||||
"single medieval fantasy building, 3/4 isometric view from above, "
|
||||
"one complete structure centered, painted illustration style, "
|
||||
"like a Civilization V city building icon or Age of Wonders building, "
|
||||
"clean edges, no terrain, no people, no other buildings, "
|
||||
"warm lighting, detailed architecture, suitable for a strategy game building icon, "
|
||||
"masterpiece, best quality, game asset"
|
||||
"isometric building viewed from above at 45-degree angle, "
|
||||
"you can see the ROOF and TWO WALLS of the building, "
|
||||
"NOT front-facing, NOT a facade, camera is elevated looking down, "
|
||||
"single small building on flat ground, painted digital art, "
|
||||
"strategy game building like Age of Empires or Civilization V, "
|
||||
"one structure only, clean edges, no other buildings, no characters, "
|
||||
"masterpiece, best quality, game building sprite"
|
||||
),
|
||||
"resources": (
|
||||
"single natural resource deposit or feature, top-down overhead view, "
|
||||
"small icon-sized game sprite for a hex-grid strategy game, "
|
||||
"painted fantasy art style, one distinct feature centered, "
|
||||
"like a Civilization V resource icon on the map, "
|
||||
"clean simple composition, no text, no UI elements, "
|
||||
"masterpiece, best quality, game tile overlay"
|
||||
"single natural resource object centered on transparent background, "
|
||||
"painted fantasy game icon style, like a Civilization V map resource marker, "
|
||||
"ONE distinct recognizable object, NOT a texture, NOT a pattern, "
|
||||
"clean simple icon composition with clear silhouette, "
|
||||
"small game sprite overlay for a hex tile, "
|
||||
"masterpiece, best quality, game resource icon"
|
||||
),
|
||||
"improvements": (
|
||||
"single tile improvement, top-down overhead view, "
|
||||
"small man-made structure or modification on natural ground, "
|
||||
"painted fantasy art style for a hex-grid strategy game, "
|
||||
"like a Civilization V tile improvement (farm, mine, quarry), "
|
||||
"clean simple composition, one feature centered, "
|
||||
"masterpiece, best quality, game tile overlay"
|
||||
"single tile improvement viewed from above, isometric perspective, "
|
||||
"small cultivated area or construction on natural ground, "
|
||||
"painted fantasy game art, strategy game tile overlay, "
|
||||
"like a Civilization V tile improvement, simple clean composition, "
|
||||
"ONE feature centered, recognizable at small scale, "
|
||||
"masterpiece, best quality, game tile improvement"
|
||||
),
|
||||
"spells": (
|
||||
"magical spell effect, dramatic magical energy, "
|
||||
|
|
@ -345,8 +347,8 @@ _GENERATION_SIZES: dict[str, tuple[int, int]] = {
|
|||
"edges": (832, 512),
|
||||
"units": (512, 512),
|
||||
"buildings": (512, 512),
|
||||
"resources": (256, 256),
|
||||
"improvements": (256, 256),
|
||||
"resources": (512, 512),
|
||||
"improvements": (512, 512),
|
||||
"spells": (512, 512),
|
||||
"ui": (256, 256),
|
||||
}
|
||||
|
|
@ -388,44 +390,37 @@ def compose_prompt(
|
|||
entity_data: dict,
|
||||
dimensions: dict | None = None,
|
||||
) -> str:
|
||||
"""Build the full prompt for a sprite from category, entity data, and optional dimensions.
|
||||
"""Build the full prompt for a sprite.
|
||||
|
||||
Composition order:
|
||||
1. Style prefix for category
|
||||
2. Entity-specific description (name + description + combat type + keywords for units)
|
||||
3. School aesthetic (if applicable)
|
||||
4. Race aesthetic (if dimension specified)
|
||||
5. Gender modifier (if dimension specified)
|
||||
6. Quality modifier (if dimension specified)
|
||||
SDXL weights early tokens most heavily, so composition order is:
|
||||
1. SUBJECT FIRST — what the image depicts (name + visual description)
|
||||
2. Category-specific attributes (combat type, school, race, keywords)
|
||||
3. Style/perspective constraints (the style prefix)
|
||||
4. Quality modifier
|
||||
"""
|
||||
dims = dimensions or {}
|
||||
parts: list[str] = []
|
||||
|
||||
# 1 — style prefix
|
||||
prefix = STYLE_PREFIXES.get(category, "")
|
||||
if prefix:
|
||||
parts.append(prefix)
|
||||
|
||||
# 2 — entity-specific description
|
||||
# 1 — SUBJECT FIRST (most important tokens for SDXL)
|
||||
name = entity_data.get("name", "")
|
||||
description = entity_data.get("description", "")
|
||||
if name:
|
||||
parts.append(name)
|
||||
if description:
|
||||
parts.append(description)
|
||||
parts.append(description[:120])
|
||||
|
||||
# 2 — category-specific attributes
|
||||
if category == "units":
|
||||
combat_type = entity_data.get("combat_type", "")
|
||||
if combat_type and combat_type in COMBAT_TYPE_FLAVORS:
|
||||
parts.append(COMBAT_TYPE_FLAVORS[combat_type])
|
||||
|
||||
keywords: list[str] = entity_data.get("keywords", [])
|
||||
for kw in keywords:
|
||||
flavor = KEYWORD_FLAVORS.get(kw)
|
||||
if flavor:
|
||||
parts.append(flavor)
|
||||
|
||||
# 3 — school aesthetic
|
||||
# school aesthetic
|
||||
school = entity_data.get("school") or dims.get("school")
|
||||
if school and school in SCHOOL_AESTHETICS:
|
||||
parts.append(SCHOOL_AESTHETICS[school])
|
||||
|
|
@ -435,16 +430,20 @@ def compose_prompt(
|
|||
if race and race in RACE_AESTHETICS:
|
||||
parts.append(RACE_AESTHETICS[race])
|
||||
|
||||
# 5 — gender modifier
|
||||
# gender modifier
|
||||
gender = dims.get("gender")
|
||||
if gender and gender in GENDER_MODIFIERS:
|
||||
parts.append(GENDER_MODIFIERS[gender])
|
||||
|
||||
# 6 — quality modifier
|
||||
# 3 — STYLE PREFIX (perspective + art style constraints come after subject)
|
||||
prefix = STYLE_PREFIXES.get(category, "")
|
||||
if prefix:
|
||||
parts.append(prefix)
|
||||
|
||||
# 4 — quality modifier
|
||||
quality = dims.get("quality")
|
||||
if quality is not None:
|
||||
qual_category = category
|
||||
# biome_grid shares terrain quality modifiers
|
||||
if qual_category == "biome_grid":
|
||||
qual_category = "terrain"
|
||||
qual_table = QUALITY_MODIFIERS.get(qual_category)
|
||||
|
|
|
|||
|
|
@ -426,6 +426,28 @@ class SpriteRegistry:
|
|||
).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
def get_recent_variants(self, limit: int = 30, since: str | None = None) -> list[dict]:
|
||||
"""Recently completed variants with sprite metadata for the stream ticker."""
|
||||
clauses = ["v.job_status = 'completed'", "v.raw_path IS NOT NULL"]
|
||||
params: list[str | int] = []
|
||||
if since:
|
||||
clauses.append("v.created_at > ?")
|
||||
params.append(since)
|
||||
where = " WHERE " + " AND ".join(clauses)
|
||||
params.append(limit)
|
||||
rows = self.conn.execute(
|
||||
f"""SELECT v.id as variant_id, v.sprite_id, s.category, s.entity_id,
|
||||
v.raw_path, v.processed_path, v.seed, v.created_at,
|
||||
v.rating, v.notes, v.is_approved
|
||||
FROM variants v
|
||||
JOIN sprites s ON v.sprite_id = s.id
|
||||
{where}
|
||||
ORDER BY v.created_at DESC
|
||||
LIMIT ?""",
|
||||
params,
|
||||
).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
# -- stats -----------------------------------------------------------------
|
||||
|
||||
def get_stats(self) -> dict:
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue