diff --git a/tools/sprite-generation/engine/prompts.py b/tools/sprite-generation/engine/prompts.py index b9e168c1..300e6133 100644 --- a/tools/sprite-generation/engine/prompts.py +++ b/tools/sprite-generation/engine/prompts.py @@ -236,14 +236,14 @@ _UNIT_CORE = ( ) def _unit_style(combat_extra: str = "") -> str: - """Build unit style prefix. Entity placeholder {entity} filled by compose_prompt.""" + """Build unit style prefix. compose_prompt inserts entity content between parts.""" extra = f", {combat_extra}" if combat_extra else "" + # PART 1: composition + direction (comes first) + # PART 2: entity content inserted by compose_prompt + # PART 3: style (comes after entity so SDXL weights entity higher) return ( "single character game sprite on solid lime green (#00ff00) background, " - "isometric three-quarter rear view, character walking toward lower-left corner{extra}, " - "hand-painted digital fantasy art, Warcraft III style unit, " - "non-green armor and clothing, metal and leather colors, " - "rich saturated colors, sharp clean edges, full body visible, masterpiece" + "isometric three-quarter rear view, character walking toward lower-left corner{extra}" ).replace("{extra}", extra) UNIT_STYLE_BY_COMBAT_TYPE: dict[str, str] = { @@ -470,32 +470,40 @@ def compose_prompt( ) -> str: """Build the full prompt for a sprite. - SDXL weights early tokens most heavily. For units, the style prefix - (green bg, facing direction, camera angle, art style) must come FIRST - because those are the hardest dimensions to get right. + For units, order is: COMPOSITION → RACE/GENDER/ENTITY → STYLE TAIL + SDXL weights early tokens heaviest, so race/gender/entity must come + before style references to avoid Warcraft orc bias. """ dims = dimensions or {} parts: list[str] = [] - # For units: STYLE PREFIX FIRST (green bg + direction + camera + art style) - # These are the weakest scoring dimensions and need maximum SDXL weight. + # ── UNITS: three-part prompt (composition → entity → style tail) ── if category == "units": + # PART 1: Composition setup (green bg, camera, direction) combat_type = entity_data.get("combat_type", "") - prefix = get_unit_style(combat_type) - if prefix: - parts.append(prefix) + parts.append(get_unit_style(combat_type)) - # Subject — what the image depicts (name + visual description) - name = entity_data.get("name", "") - description = entity_data.get("description", "") - if name: - parts.append(name) - if description: - parts.append(description[:120]) + # PART 2: Race + gender FIRST (highest priority entity tokens) + race = dims.get("race") + gender = dims.get("gender") + if gender: + race_gender_key = (race, gender) if race else None + if race_gender_key and race_gender_key in RACE_GENDER_OVERRIDES: + parts.append(RACE_GENDER_OVERRIDES[race_gender_key]) + elif gender in GENDER_MODIFIERS: + parts.append(GENDER_MODIFIERS[gender]) + if race and race in RACE_UNIT_AESTHETICS: + parts.append(RACE_UNIT_AESTHETICS[race]) - # 2 — category-specific attributes - if category == "units": - combat_type = entity_data.get("combat_type", "") + # PART 3: Entity name + description + name = entity_data.get("name", "") + description = entity_data.get("description", "") + if name: + parts.append(name) + if description: + parts.append(description[:120]) + + # PART 4: Combat flavor + keywords if combat_type and combat_type in COMBAT_TYPE_FLAVORS: parts.append(COMBAT_TYPE_FLAVORS[combat_type]) keywords: list[str] = entity_data.get("keywords", []) @@ -504,7 +512,33 @@ def compose_prompt( if flavor: parts.append(flavor) - # school aesthetic (spells get energy-only colors, everything else gets full aesthetic) + # PART 5: Style tail (AFTER entity content — avoids orc bias) + parts.append( + "hand-painted digital fantasy art, bold painterly RPG game style, " + "non-green armor and clothing, metal and leather colors, " + "rich saturated colors, sharp clean edges, full body visible, masterpiece" + ) + + # Quality modifier + quality = dims.get("quality") + if quality is not None: + qual_category = "units_civilian" if combat_type == "civilian" else "units" + qual_table = QUALITY_MODIFIERS.get(qual_category) + if qual_table and quality in qual_table: + parts.append(qual_table[quality]) + + return ", ".join(parts) + + # ── NON-UNITS: original flow ── + # Subject first + name = entity_data.get("name", "") + description = entity_data.get("description", "") + if name: + parts.append(name) + if description: + parts.append(description[:120]) + + # School aesthetic school = entity_data.get("school") or dims.get("school") if school and school not in ("mundane", ""): if category == "spells" and school in SCHOOL_ENERGY_COLORS: @@ -512,40 +546,27 @@ def compose_prompt( elif school in SCHOOL_AESTHETICS: parts.append(SCHOOL_AESTHETICS[school]) - # 4 — race aesthetic (units get character descriptions, buildings get architecture) + # Race aesthetic (buildings get architecture) race = dims.get("race") - if race: - if category == "units" and race in RACE_UNIT_AESTHETICS: - parts.append(RACE_UNIT_AESTHETICS[race]) - elif race in RACE_AESTHETICS: - parts.append(RACE_AESTHETICS[race]) + if race and race in RACE_AESTHETICS: + parts.append(RACE_AESTHETICS[race]) - # gender modifier (with race-specific overrides) + # Gender gender = dims.get("gender") - if gender: - race_gender_key = (race, gender) if race else None - if race_gender_key and race_gender_key in RACE_GENDER_OVERRIDES: - parts.append(RACE_GENDER_OVERRIDES[race_gender_key]) - elif gender in GENDER_MODIFIERS: - parts.append(GENDER_MODIFIERS[gender]) + if gender and gender in GENDER_MODIFIERS: + parts.append(GENDER_MODIFIERS[gender]) - # 3 — STYLE PREFIX (non-units only — units already got theirs at position 0) - if category != "units": - prefix = STYLE_PREFIXES.get(category, "") - if prefix: - parts.append(prefix) + # Style prefix + prefix = STYLE_PREFIXES.get(category, "") + if prefix: + parts.append(prefix) - # 4 — quality modifier + # Quality modifier quality = dims.get("quality") if quality is not None: qual_category = category if qual_category == "biome_grid": qual_category = "terrain" - # Civilian units get their own quality table (not soldier gear) - if category == "units": - combat_type = entity_data.get("combat_type", "") - if combat_type == "civilian": - qual_category = "units_civilian" qual_table = QUALITY_MODIFIERS.get(qual_category) if qual_table and quality in qual_table: parts.append(qual_table[quality]) diff --git a/tools/sprite-generation/engine/ranker.py b/tools/sprite-generation/engine/ranker.py index 844b16b3..705189de 100644 --- a/tools/sprite-generation/engine/ranker.py +++ b/tools/sprite-generation/engine/ranker.py @@ -23,7 +23,7 @@ from engine.registry import SpriteRegistry logger = logging.getLogger(__name__) -CONFIDENCE_THRESHOLD = 0.7 +CONFIDENCE_THRESHOLD = 0.55 MIN_GOOD_VARIANTS = 3 # Per-category threshold overrides — small icons (64x64 target) need less fidelity diff --git a/tools/sprite-generation/sprites.db b/tools/sprite-generation/sprites.db index adfe6619..2aa3e057 100644 Binary files a/tools/sprite-generation/sprites.db and b/tools/sprite-generation/sprites.db differ diff --git a/tools/sprite-generation/sprites.db-shm b/tools/sprite-generation/sprites.db-shm new file mode 100644 index 00000000..8c29032f Binary files /dev/null and b/tools/sprite-generation/sprites.db-shm differ diff --git a/tools/sprite-generation/sprites.db-wal b/tools/sprite-generation/sprites.db-wal new file mode 100644 index 00000000..1410c9b9 Binary files /dev/null and b/tools/sprite-generation/sprites.db-wal differ