Files
fn_registry/python/functions/ml/comfyui_apply_style_preset.py
T
egutierrez 0eefb7cfcd feat(gamedev): sistema de style presets reutilizable (gameboy/ghibli/pixel-art-retro)
Calidad por ESTILO en vez de por tipo: un dev fija el look del juego una vez
y todos los assets salen coherentes. Diseño (A) datos puros + helper, no
pipeline monolítico (issue 0087, crecer por composición).

- comfyui_get_gamedev_style_preset(name=None): recetas curadas o catálogo.
  gameboy (sin LoRA, post pixelize paleta game-boy 4 tonos), ghibli (degrada
  a watercolor_style_sd15 gratis instalado, sin LoRA Ghibli gated), pixel-art-retro
  (reutiliza pixel-art-xl SDXL + juggernaut + post pixelize 16 colores). Extensible.
- comfyui_apply_style_preset(preset, subject): traduce a kwargs **spread-ables
  para cualquier builder de sujeto + size/transparent/post. Pura, no muta.
- 16 tests offline verdes. Validado e2e GPU: mismo 'knight character' en 3
  estilos visiblemente distintos (4 vs 78552 vs 16 colores).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 12:36:18 +02:00

136 lines
5.2 KiB
Python

"""comfyui_apply_style_preset — traduce un style preset gamedev a los kwargs de un builder.
Toma una receta de estilo (de `comfyui_get_gamedev_style_preset`) y un `subject` del usuario
y produce, de forma PURA, lo que un builder de sujeto del grupo `gamedev-2d` necesita:
- el `subject` combinado con el prefijo/sufijo del estilo,
- los kwargs comunes a todos los builders de sujeto (`style`, `checkpoint`, `lora`,
`lora_strength`, `negative`) listos para hacer `**spread`,
- la resolucion y el recorte recomendados (`size`, `transparent`),
- y la spec de post-proceso (`post`, p.ej. pixelize) que el caller aplica al PNG resultante.
Asi el mismo estilo se aplica a CUALQUIER builder de sujeto (item_icon, enemy_creature,
prop_object, structure, ...) sin acoplar este helper a sus firmas, y el preset elige el
checkpoint/lora coherentes ANTES de construir el grafo.
Patron de uso:
preset = comfyui_get_gamedev_style_preset("gameboy")
ap = comfyui_apply_style_preset(preset, "knight character")
wf = comfyui_build_enemy_creature_workflow(
ap["subject"], size=ap["size"], transparent=ap["transparent"], **ap["builder_kwargs"]
)
# tras submit/wait/fetch, si ap["post"].get("pixelize"):
# comfyui_pixelize_image(raw_png, dst_png, **ap["post"]["pixelize"])
Funcion pura: sin red, sin I/O. No muta el preset de entrada (copia lo que devuelve).
"""
from __future__ import annotations
import copy
# Claves obligatorias de una receta valida (las que produce comfyui_get_gamedev_style_preset).
_REQUIRED = (
"name",
"subject_prefix",
"subject_suffix",
"style",
"negative",
"checkpoint",
"lora",
"lora_strength",
"size",
"transparent",
"post",
)
def _merge_negative(a: str, b: str) -> str:
"""Une dos negativos por comas sin duplicar terminos ni dejar comas sueltas."""
seen: list[str] = []
for chunk in (a or "", b or ""):
for term in chunk.split(","):
t = term.strip()
if t and t.lower() not in {s.lower() for s in seen}:
seen.append(t)
return ", ".join(seen)
def comfyui_apply_style_preset(
preset: dict,
subject: str,
*,
style: str | None = None,
negative: str | None = None,
) -> dict:
"""Aplica un style preset a un subject y devuelve los kwargs listos para un builder.
Args:
preset: receta de estilo (dict de comfyui_get_gamedev_style_preset). Debe traer
los campos del preset; se valida que esten presentes. No se muta.
subject: lo que el usuario quiere generar (ej. "knight character", "health potion").
Se combina con el prefijo/sufijo del estilo. No puede estar vacio.
style: si se pasa, sustituye al `style` del preset (override puntual). None usa el
del preset. keyword-only.
negative: negativo extra del caller; se MERGEA con el negativo del estilo (no lo
reemplaza). None = solo el del estilo. keyword-only.
Returns:
dict con:
- "name" (str): nombre del estilo aplicado.
- "subject" (str): subject combinado con prefijo/sufijo del estilo.
- "builder_kwargs" (dict): {style, checkpoint, lora, lora_strength, negative} —
los kwargs comunes a los builders de sujeto, para hacer **spread.
- "size" (int): resolucion recomendada por el estilo.
- "transparent" (bool): recorte a alpha recomendado por el estilo.
- "post" (dict): post-proceso CPU a aplicar al PNG ({"pixelize": {...}} o {}).
Raises:
ValueError: si subject esta vacio o el preset no trae los campos requeridos.
"""
if not subject or not subject.strip():
raise ValueError("comfyui_apply_style_preset: 'subject' no puede estar vacio")
if not isinstance(preset, dict):
raise ValueError("comfyui_apply_style_preset: 'preset' debe ser un dict")
missing = [k for k in _REQUIRED if k not in preset]
if missing:
raise ValueError(
f"comfyui_apply_style_preset: preset incompleto, faltan campos {missing}. "
"Usa comfyui_get_gamedev_style_preset para obtener una receta valida."
)
subject_full = (
f"{preset['subject_prefix']}{subject.strip()}{preset['subject_suffix']}"
).strip().strip(",").strip()
style_final = style if style is not None else preset["style"]
neg_final = _merge_negative(preset["negative"], negative or "")
return {
"name": preset["name"],
"subject": subject_full,
"builder_kwargs": {
"style": style_final,
"checkpoint": preset["checkpoint"],
"lora": preset["lora"],
"lora_strength": preset["lora_strength"],
"negative": neg_final,
},
"size": preset["size"],
"transparent": preset["transparent"],
"post": copy.deepcopy(preset["post"]),
}
if __name__ == "__main__":
import json
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from ml.comfyui_get_gamedev_style_preset import comfyui_get_gamedev_style_preset
p = comfyui_get_gamedev_style_preset("pixel-art-retro")
ap = comfyui_apply_style_preset(p, "knight character")
print(json.dumps(ap, indent=2, ensure_ascii=False))