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>
This commit is contained in:
2026-06-27 12:36:18 +02:00
parent 9f0d2e2338
commit 0eefb7cfcd
7 changed files with 713 additions and 0 deletions
+48
View File
@@ -137,6 +137,54 @@ del dibujo del dev, no inventar un tipo nuevo desde texto.
| `godot_map_asset_dir_py_core` | `(kind) -> str` | Mapea `kind` -> subcarpeta de `res://assets/`. Pura. | | `godot_map_asset_dir_py_core` | `(kind) -> str` | Mapea `kind` -> subcarpeta de `res://assets/`. Pura. |
| `godot_clean_asset_name_py_core` | `(filename, *, override=None) -> str` | Normaliza el nombre `<prefijo>_NNNNN_.<ext>` a snake_case seguro para `res://`. Pura. | | `godot_clean_asset_name_py_core` | `(filename, *, override=None) -> str` | Normaliza el nombre `<prefijo>_NNNNN_.<ext>` a snake_case seguro para `res://`. Pura. |
## Estilos (style presets) — calidad por ESTILO reutilizable
Un *style preset* es la receta curada de un look visual que se aplica a **TODOS** los
assets de un juego de una vez ("todo en Game Boy", "estilo Ghibli", "pixel-art retro").
En vez de repetir a mano `style`/`checkpoint`/`lora`/`negative` + post-proceso en cada
builder, el preset los empaqueta como DATOS puros y el helper los traduce a los kwargs de
cualquier builder de sujeto (item_icon, enemy_creature, prop_object, …) o del pipeline
`comfyui_generate_asset_pack_oneshot`. Diseño (issue 0087): función pura de presets +
helper de aplicación (NO un pipeline monolítico) — máxima composabilidad, sin acoplar
firmas. Extensible: añadir un estilo = una entrada en `_PRESETS`.
| ID | Firma corta | Qué hace |
|---|---|---|
| `comfyui_get_gamedev_style_preset_py_ml` | `(name=None) -> dict` | Devuelve la receta de un STYLE PRESET curado o el catálogo si `name=None`. Receta = `{subject_prefix, subject_suffix, style, negative, checkpoint, lora, lora_strength, size, transparent, post, notes}`. Pura, copias profundas. Estilos iniciales: **gameboy** (sin LoRA → prompt + post `pixelize` paleta `game-boy` 4 tonos verde), **ghibli** (degrada a `watercolor_style_sd15` gratis instalado + prompt; no hay LoRA Ghibli dedicado ni se descargó nada gated), **pixel-art-retro** (reutiliza `pixel-art-xl` SDXL ya instalado → checkpoint `juggernaut_xl_v11` + size 768 + post `pixelize` 16 colores). |
| `comfyui_apply_style_preset_py_ml` | `(preset, subject, *, style=None, negative=None) -> dict` | Traduce un preset + un `subject` a `{name, subject (con prefijo/sufijo), builder_kwargs={style,checkpoint,lora,lora_strength,negative}, size, transparent, post}`. Los `builder_kwargs` hacen `**spread` directo en cualquier builder de sujeto; `size`/`transparent` van aparte (recomendaciones); el caller aplica `post["pixelize"]` al PNG si existe. Pura, no muta el preset; `negative` se mergea (no reemplaza). |
**Ejemplo canónico (mismo subject, look del juego entero):**
```python
import sys, os
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
from ml.comfyui_get_gamedev_style_preset import comfyui_get_gamedev_style_preset
from ml.comfyui_apply_style_preset import comfyui_apply_style_preset
from ml.comfyui_build_enemy_creature_workflow import comfyui_build_enemy_creature_workflow
from ml.comfyui_submit_workflow import comfyui_submit_workflow
from ml.comfyui_wait_result import comfyui_wait_result
from ml.comfyui_fetch_output_image import comfyui_fetch_output_image
from ml.comfyui_pixelize_image import comfyui_pixelize_image
preset = comfyui_get_gamedev_style_preset("gameboy") # o "ghibli" / "pixel-art-retro"
ap = comfyui_apply_style_preset(preset, "knight character")
wf = comfyui_build_enemy_creature_workflow(ap["subject"], size=ap["size"],
transparent=ap["transparent"], seed=7, **ap["builder_kwargs"])
pid = comfyui_submit_workflow(wf)["prompt_id"]
outs = comfyui_wait_result(pid, timeout=500)
fn = next(i["filename"] for o in outs.values() for i in o.get("images", []))
raw = comfyui_fetch_output_image(fn, dest_dir="/tmp")["path"]
if ap["post"].get("pixelize"): # gameboy/pixel-retro sellan el grid/paleta
comfyui_pixelize_image(raw, "/tmp/knight.png", **ap["post"]["pixelize"])
```
Validado e2e en GPU con el MISMO `knight character` en los 3 estilos (`reports/0190`):
gameboy 4 colores verde (`prompt_id 0657e3e3`), ghibli 78 552 colores acuarela
(`42f2f492`), pixel-art-retro SDXL 768 16 colores (`84b08581`) — tres looks
visiblemente distintos y coherentes. **Gotcha**: el `post` no se aplica solo (el caller
llama `comfyui_pixelize_image`); el LoRA y el checkpoint deben casar de base (pixel-art-xl
es SDXL → exige juggernaut); OOM en 8 GB → bajar `size`, NO matar procesos.
## Pipelines one-shot (`gamedev-2d`, impuros) ## Pipelines one-shot (`gamedev-2d`, impuros)
| ID | Firma corta | Qué hace | | ID | Firma corta | Qué hace |
@@ -0,0 +1,83 @@
---
name: comfyui_apply_style_preset
kind: function
lang: py
domain: ml
version: "1.0.0"
purity: pure
signature: "def comfyui_apply_style_preset(preset: dict, subject: str, *, style: str | None = None, negative: str | None = None) -> dict"
description: "Traduce un STYLE PRESET gamedev (de comfyui_get_gamedev_style_preset) + un subject del usuario a lo que necesita un builder de sujeto del grupo gamedev-2d: el subject combinado con el prefijo/sufijo del estilo, los kwargs comunes (style, checkpoint, lora, lora_strength, negative) listos para **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. Asi el mismo estilo se aplica a CUALQUIER builder (item_icon, enemy_creature, prop_object, ...) y al pipeline comfyui_generate_asset_pack_oneshot sin acoplar firmas. Pura, sin red ni I/O; no muta el preset."
tags: [comfyui, ml, gamedev-2d, style, preset, theme]
uses_functions: [comfyui_get_gamedev_style_preset_py_ml]
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: []
params:
- name: preset
desc: "Receta de estilo (dict de comfyui_get_gamedev_style_preset). Debe traer los campos del preset (se valida que esten). No se muta."
- name: subject
desc: "Lo que el usuario quiere generar (ej. 'knight character', 'health potion'). Se combina con el prefijo/sufijo del estilo. No puede estar vacio."
- name: style
desc: "Override puntual: si se pasa, sustituye al style del preset. None usa el del preset. keyword-only."
- name: negative
desc: "Negativo extra del caller; se MERGEA (sin duplicar) con el negativo del estilo, no lo reemplaza. None = solo el del estilo. keyword-only."
output: "dict con: name (estilo aplicado), subject (combinado con prefijo/sufijo), builder_kwargs ({style, checkpoint, lora, lora_strength, negative} para **spread en el builder), size (resolucion recomendada), transparent (recorte recomendado), post (post-proceso CPU: {'pixelize': {...}} o {})."
tested: true
tests: ["golden gameboy: subject combina suffix (8-bit), builder_kwargs con las 5 claves comunes, checkpoint dreamshaper, lora None, post pixelize paleta game-boy", "golden contrato: los builder_kwargs hacen **spread en comfyui_build_item_icon_workflow sin TypeError y el LoRA del preset aparece en el grafo", "edge style override sustituye el del preset", "edge negative se mergea con el del estilo (no se pierde photorealistic) y deduplica", "edge no muta el preset de entrada", "error subject vacio -> ValueError", "error preset incompleto -> ValueError"]
test_file_path: "python/functions/ml/comfyui_apply_style_preset_test.py"
file_path: "python/functions/ml/comfyui_apply_style_preset.py"
---
## Ejemplo
```python
import sys, os
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
from ml.comfyui_get_gamedev_style_preset import comfyui_get_gamedev_style_preset
from ml.comfyui_apply_style_preset import comfyui_apply_style_preset
from ml.comfyui_build_enemy_creature_workflow import comfyui_build_enemy_creature_workflow
# 1. Elegir estilo + aplicarlo a un subject
preset = comfyui_get_gamedev_style_preset("gameboy")
ap = comfyui_apply_style_preset(preset, "knight character")
# 2. Construir el workflow con cualquier builder de sujeto (kwargs por **spread)
wf = comfyui_build_enemy_creature_workflow(
ap["subject"], size=ap["size"], transparent=ap["transparent"], **ap["builder_kwargs"]
)
# 3. Generar (submit/wait/fetch) y, si el estilo lo pide, post-proceso:
# if ap["post"].get("pixelize"):
# comfyui_pixelize_image(raw_png, dst_png, **ap["post"]["pixelize"])
```
O lanzable directo con: `./fn run comfyui_apply_style_preset` (aplica pixel-art-retro a "knight character").
## Cuando usarla
Justo despues de elegir un estilo con `comfyui_get_gamedev_style_preset`, para convertir
esa receta en los argumentos exactos de un builder. Es el puente entre "que estilo quiero"
y "como lo paso a item_icon/enemy_creature/prop_object/...". El mismo `ap` sirve para
generar N assets distintos en el MISMO estilo (varia solo el `subject`). Para overrides
puntuales sin tocar el preset, usa `style=`/`negative=`.
## Gotchas
- Devuelve `builder_kwargs` con EXACTAMENTE las 5 claves comunes a los builders de SUJETO
(`style`, `checkpoint`, `lora`, `lora_strength`, `negative`). Builders que NO las acepten
todas (p.ej. `seamless_tile`, `parallax_background` no tienen `transparent`/`lora` igual)
exigen filtrar las claves; este helper esta pensado para los builders de sujeto cuadrado.
- `size` y `transparent` van FUERA de `builder_kwargs` (son recomendaciones del estilo): el
caller los pasa explicitos o decide otros. `transparent=False` en los presets de demo es
para que el look (paleta/pintura) cubra todo el frame; para un sprite con alpha pon
`transparent=True` (el recorte es ortogonal al estilo).
- El `post` NO se aplica solo: el caller debe llamar `comfyui_pixelize_image(raw, dst,
**ap["post"]["pixelize"])` tras descargar el PNG si `ap["post"].get("pixelize")`. Sin eso,
estilos como gameboy/pixel-art-retro no sellan su grid/paleta.
- Es **pura**: no llama a ningun builder ni toca la GPU; solo arma kwargs. No muta el
`preset` de entrada (lo que devuelve es independiente).
## Capability growth log
(v1.0.0 — sin cambios todavia.)
@@ -0,0 +1,135 @@
"""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))
@@ -0,0 +1,92 @@
"""Tests offline de comfyui_apply_style_preset (traduccion preset -> kwargs, sin GPU)."""
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from ml.comfyui_apply_style_preset import comfyui_apply_style_preset # noqa: E402
from ml.comfyui_get_gamedev_style_preset import ( # noqa: E402
comfyui_get_gamedev_style_preset,
)
def test_golden_apply_gameboy_to_subject():
p = comfyui_get_gamedev_style_preset("gameboy")
ap = comfyui_apply_style_preset(p, "knight character")
# El subject combina prefijo/sufijo del estilo.
assert "knight character" in ap["subject"]
assert "8-bit" in ap["subject"] # del subject_suffix del gameboy
# builder_kwargs trae las claves comunes a los builders de sujeto, listas para **spread.
bk = ap["builder_kwargs"]
assert set(bk) == {"style", "checkpoint", "lora", "lora_strength", "negative"}
assert bk["checkpoint"] == "dreamshaper_8.safetensors"
assert bk["lora"] is None
assert "Game Boy" in bk["style"]
# Recomendaciones y post propagados.
assert ap["transparent"] is False
assert ap["post"]["pixelize"]["palette"] == "game-boy"
def test_golden_kwargs_spreadable_into_builder():
# Los builder_kwargs son exactamente los que aceptan los builders de sujeto:
# se hace **spread sin TypeError (verifica el contrato con item_icon).
from ml.comfyui_build_item_icon_workflow import comfyui_build_item_icon_workflow
p = comfyui_get_gamedev_style_preset("ghibli")
ap = comfyui_apply_style_preset(p, "magic potion")
wf = comfyui_build_item_icon_workflow(
ap["subject"], size=ap["size"], transparent=ap["transparent"], **ap["builder_kwargs"]
)
cls = sorted({n["class_type"] for n in wf.values()})
assert "KSampler" in cls
# El LoRA watercolor del preset aparece en el grafo.
loras = [n for n in wf.values() if n["class_type"] == "LoraLoader"]
assert loras and loras[0]["inputs"]["lora_name"] == "watercolor_style_sd15.safetensors"
def test_edge_style_override():
p = comfyui_get_gamedev_style_preset("gameboy")
ap = comfyui_apply_style_preset(p, "tree", style="custom override style")
assert ap["builder_kwargs"]["style"] == "custom override style"
def test_edge_negative_merged_not_replaced():
p = comfyui_get_gamedev_style_preset("gameboy")
ap = comfyui_apply_style_preset(p, "tree", negative="extra unwanted thing")
neg = ap["builder_kwargs"]["negative"]
assert "extra unwanted thing" in neg
assert "photorealistic" in neg # del negativo del estilo, no se pierde
def test_edge_negative_dedup():
p = comfyui_get_gamedev_style_preset("gameboy")
# "photo" ya esta en el negativo del estilo; no debe duplicarse.
ap = comfyui_apply_style_preset(p, "tree", negative="photo")
assert ap["builder_kwargs"]["negative"].lower().count("photo,") + \
ap["builder_kwargs"]["negative"].lower().endswith("photo") <= 2
def test_edge_does_not_mutate_preset():
p = comfyui_get_gamedev_style_preset("pixel-art-retro")
before = dict(p)
ap = comfyui_apply_style_preset(p, "knight")
ap["post"]["pixelize"]["colors"] = 999 # mutar el resultado
assert p == before # el preset original intacto
assert p["post"]["pixelize"]["colors"] == 16
def test_error_empty_subject():
p = comfyui_get_gamedev_style_preset("gameboy")
try:
comfyui_apply_style_preset(p, " ")
assert False, "deberia lanzar ValueError"
except ValueError as e:
assert "subject" in str(e)
def test_error_incomplete_preset():
try:
comfyui_apply_style_preset({"name": "broken"}, "knight")
assert False, "deberia lanzar ValueError"
except ValueError as e:
assert "incompleto" in str(e) or "faltan" in str(e)
@@ -0,0 +1,84 @@
---
name: comfyui_get_gamedev_style_preset
kind: function
lang: py
domain: ml
version: "1.0.0"
purity: pure
signature: "def comfyui_get_gamedev_style_preset(name: str | None = None) -> dict"
description: "Devuelve la receta de un STYLE PRESET gamedev curado y reutilizable (gameboy, ghibli, pixel-art-retro) o el catalogo de nombres si name es None. Un preset empaqueta como DATOS puros el look de un juego entero (subject_prefix/suffix, style, negative, checkpoint recomendado, lora + strength, size, transparent, post-proceso pixelize) para que cualquier builder/pipeline del grupo gamedev-2d lo aplique via comfyui_apply_style_preset y todos los assets salgan coherentes en ese estilo. Pura, sin red ni I/O; devuelve copias profundas. Extensible: anadir un estilo = una entrada en _PRESETS."
tags: [comfyui, ml, gamedev-2d, style, preset, theme, lora]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: []
params:
- name: name
desc: "Identificador del estilo: 'gameboy', 'ghibli' o 'pixel-art-retro'. Insensible a mayusculas y a '_' vs '-'. Si es None o cadena vacia devuelve el catalogo {names, count} en vez de una receta (discovery)."
output: "Si name es None/'': dict {names: list[str], count: int} con el catalogo. Si name es valido: copia profunda de la receta del estilo con campos {name, subject_prefix, subject_suffix, style, negative, checkpoint, lora, lora_strength, size, transparent, post, notes}. Es una COPIA — mutarla no afecta al registro interno."
tested: true
tests: ["golden: los 3 presets (gameboy/ghibli/pixel-art-retro) devuelven recetas con todos los campos requeridos, checkpoint .safetensors, size>0", "golden gameboy: lora None + post pixelize paleta game-boy 4 colores", "golden pixel-art-retro: lora pixel-art-xl SDXL + checkpoint juggernaut + size>=768 + post pixelize", "golden ghibli: degrada a watercolor_style_sd15 + sin post pixelize", "edge name None/'' -> catalogo de 3 nombres", "edge insensible a mayusculas y '_'/'-'", "edge devuelve copia profunda (mutar no afecta), error name desconocido -> ValueError listando disponibles"]
test_file_path: "python/functions/ml/comfyui_get_gamedev_style_preset_test.py"
file_path: "python/functions/ml/comfyui_get_gamedev_style_preset.py"
---
## Ejemplo
```python
import sys, os
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
from ml.comfyui_get_gamedev_style_preset import comfyui_get_gamedev_style_preset
# Discovery: que estilos hay
print(comfyui_get_gamedev_style_preset()) # {'names': ['gameboy', 'ghibli', 'pixel-art-retro'], 'count': 3}
# Receta de un estilo concreto (copia segura de mutar)
preset = comfyui_get_gamedev_style_preset("gameboy")
print(preset["checkpoint"], preset["lora"], preset["post"]["pixelize"]["palette"])
# dreamshaper_8.safetensors None game-boy
# Aplicarlo a un asset con el helper hermano (ver comfyui_apply_style_preset):
# 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"])
```
O lanzable directo con: `./fn run comfyui_get_gamedev_style_preset` (imprime catalogo + receta gameboy).
## Cuando usarla
Cuando quieras que TODOS los assets de un juego salgan en un MISMO look coherente sin
repetir a mano el `style`/`checkpoint`/`lora`/`negative` ni el post-proceso en cada
builder. Llama una vez al principio para fijar el estilo del proyecto ("todo en Game Boy",
"estilo Ghibli", "pixel-art retro 16-bit"), pasa la receta a `comfyui_apply_style_preset`
y de ahi a cualquier builder de sujeto (`item_icon`, `enemy_creature`, `prop_object`,
`structure`, ...) o al pipeline `comfyui_generate_asset_pack_oneshot`. Usa `name=None`
para descubrir los estilos disponibles.
## Gotchas
- Es una funcion **pura**: solo devuelve datos (la receta). La generacion real (GPU) y el
post-proceso (pixelize) los hacen los builders + `comfyui_apply_style_preset` +
`comfyui_submit_workflow`/`comfyui_wait_result`/`comfyui_fetch_output_image` +
`comfyui_pixelize_image`.
- **El checkpoint y el LoRA deben casar de base**: `pixel-art-retro` usa el LoRA SDXL
`pixel-art-xl` -> exige `checkpoint` SDXL (`juggernaut_xl_v11`) y `size` 768. Aplicar un
LoRA SDXL sobre un checkpoint SD1.5 da basura. Si anades un estilo con LoRA nuevo,
descargalo a `models/loras` y verifica su base antes.
- **ghibli no usa un LoRA Ghibli dedicado**: no hay ninguno instalado y no se descargo
ninguno gated/de pago. Degrada a `watercolor_style_sd15` (gratis, ya instalado) +
prompt Ghibli para el look pintado/acuarela. Un LoRA Ghibli especifico de Civitai
mejoraria el parecido facial — pendiente humano.
- **gameboy se resuelve por POST, no por LoRA**: sin LoRA; el look DMG de 4 tonos verde lo
da `comfyui_pixelize_image` con la paleta builtin `game-boy` + dithering. El caller DEBE
aplicar el post (`preset["post"]["pixelize"]`) o solo vera un sprite monocromo-ish sin
la paleta sellada.
- **Modelos verificados en el servidor** (8GB lowvram, modelos en `/mnt/2tb`): si cambias
de PC valida que `dreamshaper_8`, `juggernaut_xl_v11`, `pixel-art-xl` y
`watercolor_style_sd15` existan (`GET /object_info/CheckpointLoaderSimple` y `/LoraLoader`).
## Capability growth log
(v1.0.0 — sin cambios todavia.)
@@ -0,0 +1,174 @@
"""comfyui_get_gamedev_style_preset — recetas de ESTILO curadas y reutilizables para gamedev.
Un *style preset* es la receta de un look visual coherente que un dev quiere aplicar
a TODOS los assets de su juego de una vez ("todo en estilo Game Boy", "estilo Ghibli",
"pixel-art retro 16-bit"). En vez de repetir a mano el mismo `style`/`checkpoint`/`lora`/
`negative` y el mismo post-proceso en cada builder gamedev, el preset los empaqueta como
DATOS puros que cualquier builder o pipeline del grupo `gamedev-2d` consume vía su helper
hermano `comfyui_apply_style_preset`.
Diseno (issue 0087, crecer por composicion no por inflado): se eligio (A) una funcion pura
de presets + un helper de aplicacion, NO (B) un pipeline monolitico, porque:
- Los presets son datos puros: reutilizables por CUALQUIER builder de sujeto
(item_icon, enemy_creature, prop_object, ...) y por el pipeline ya existente
`comfyui_generate_asset_pack_oneshot` (que comparte checkpoint/style/lora) sin acoplar
sus firmas heterogeneas.
- El helper `comfyui_apply_style_preset` traduce el preset a los kwargs comunes de los
builders + indica el post-proceso. Asi el preset se aplica al builder ANTES de construir
el grafo (eligiendo checkpoint/lora coherentes), no parcheando el dict despues.
Cada preset es un dict con estos campos:
{
"name": str, # identificador del estilo
"subject_prefix": str, # texto que precede al subject del usuario
"subject_suffix": str, # texto que sigue al subject del usuario
"style": str, # descriptor de estilo -> kwarg `style` del builder
"negative": str, # negativo extra del estilo -> se mergea con el del builder
"checkpoint": str, # checkpoint recomendado (SD1.5 vs SDXL segun el LoRA)
"lora": str | None, # LoRA del estilo (None = solo prompt + post)
"lora_strength": float, # fuerza del LoRA
"size": int, # resolucion recomendada (SDXL pide mas)
"transparent": bool, # recorte a alpha recomendado para este look
"post": dict, # post-proceso CPU; {"pixelize": {...}} o {} si no aplica
"notes": str, # como se logro el look y caveats
}
Funcion pura: sin red, sin I/O, sin estado. Devuelve copias profundas para que el caller
no mute el registro interno. Extensible: anadir un estilo = una entrada en `_PRESETS`.
"""
from __future__ import annotations
import copy
# Modelos verificados presentes en el servidor (8GB lowvram, modelos en /mnt/2tb):
# checkpoints: dreamshaper_8 (SD1.5), juggernaut_xl_v11 (SDXL)
# loras: pixel-art-xl (SDXL), watercolor_style_sd15 (SD1.5), ...
# Si se anade un estilo con un LoRA nuevo, descargar antes a models/loras y verificar
# que el `checkpoint` casa con su base (un LoRA SDXL exige checkpoint SDXL).
_PRESETS: dict[str, dict] = {
# Game Boy DMG: paleta de 4 tonos verde + dithering 1-bit + lowres. Sin LoRA:
# el prompt empuja a monocromo simple y el post `pixelize` con la paleta builtin
# "game-boy" sella el look icónico de 4 tonos. transparent=False para que el verde
# cubra todo el frame (recorte a alpha es ortogonal y se hace despues si se quiere).
"gameboy": {
"name": "gameboy",
"subject_prefix": "",
"subject_suffix": ", 8-bit, simple shapes, limited palette, retro handheld",
"style": "Game Boy DMG game art, monochrome green, retro handheld sprite, low detail, flat shading",
"negative": "color, colorful, vibrant, photorealistic, photo, 3d render, smooth gradient, high detail, realistic lighting",
"checkpoint": "dreamshaper_8.safetensors",
"lora": None,
"lora_strength": 1.0,
"size": 512,
"transparent": False,
"post": {
"pixelize": {
"downscale": 6,
"colors": 4,
"palette": "game-boy",
"dither": True,
"upscale_back": True,
}
},
"notes": (
"Sin LoRA: el look DMG lo da el post-proceso pixelize con la paleta builtin "
"'game-boy' (4 tonos verde 0f380f/306230/8bac0f/9bbc0f) + dithering. El prompt "
"solo empuja a formas simples y monocromo. transparent=False para que la paleta "
"verde cubra todo el frame."
),
},
# Studio Ghibli: anime pintado a mano, colores suaves, fondos acuarela. No hay un LoRA
# Ghibli dedicado instalado (no se descargo ninguno gated/de pago); el look pintado se
# logra con watercolor_style_sd15 (LoRA gratis ya instalado) a fuerza media + prompt
# Ghibli. Sin post-proceso (es ilustracion pintada, no pixelart).
"ghibli": {
"name": "ghibli",
"subject_prefix": "",
"subject_suffix": ", anime illustration, soft lighting, painterly",
"style": "Studio Ghibli style, hand-painted anime, soft colors, watercolor background, whimsical, gentle, detailed illustration",
"negative": "photo, photorealistic, 3d render, harsh shadows, pixel art, lowres, deformed, text, watermark, signature",
"checkpoint": "dreamshaper_8.safetensors",
"lora": "watercolor_style_sd15.safetensors",
"lora_strength": 0.7,
"size": 512,
"transparent": False,
"post": {},
"notes": (
"No hay LoRA Ghibli dedicado instalado y no se descargo ninguno gated/de pago "
"(error-path: degradar a LoRA gratis + prompt). watercolor_style_sd15 (gratis, ya "
"instalado) a strength 0.7 + prompt Ghibli da el look pintado/acuarela. Un LoRA "
"Ghibli especifico de Civitai mejoraria el parecido facial — pendiente humano si "
"se quiere. transparent=False para conservar el fondo acuarela."
),
},
# Pixel-art retro 16-bit (SNES/Genesis): reutiliza el LoRA pixel-art-xl ya instalado
# (es SDXL -> exige checkpoint SDXL juggernaut_xl_v11 y resolucion mayor). El post
# pixelize a 16 colores sella los pixeles duros (el LoRA da el estilo, el post el grid).
"pixel-art-retro": {
"name": "pixel-art-retro",
"subject_prefix": "",
"subject_suffix": ", pixel art, game sprite, crisp pixels",
"style": "16-bit pixel art, SNES JRPG sprite, retro game, limited palette, clean outline, flat colors",
"negative": "blurry, smooth, photorealistic, 3d render, realistic, antialiasing, soft, gradient, noise",
"checkpoint": "juggernaut_xl_v11.safetensors",
"lora": "pixel-art-xl.safetensors",
"lora_strength": 1.0,
"size": 768,
"transparent": False,
"post": {
"pixelize": {
"downscale": 8,
"colors": 16,
"palette": None,
"dither": False,
"upscale_back": True,
}
},
"notes": (
"Reutiliza pixel-art-xl.safetensors (SDXL, ya instalado) -> requiere checkpoint "
"SDXL juggernaut_xl_v11 y size 768 (a 512 SDXL+pixel-art-xl pierde calidad). El "
"post pixelize a 16 colores (paleta auto MEDIANCUT) da el grid duro 16-bit. OOM en "
"8GB -> bajar size a 512 (NO matar procesos). El LoRA da el estilo; el post el grid."
),
},
}
def comfyui_get_gamedev_style_preset(name: str | None = None) -> dict:
"""Devuelve la receta de un style preset gamedev, o el catalogo si name es None.
Args:
name: identificador del estilo ("gameboy", "ghibli", "pixel-art-retro"). Si es
None (o cadena vacia), devuelve el catalogo de nombres disponibles en vez de
una receta concreta (discovery). Insensible a mayusculas y a '_' vs '-'.
Returns:
- Si name es None/"": dict {"names": list[str], "count": int} con el catalogo.
- Si name es un estilo valido: copia profunda de su receta (ver el modulo para los
campos). Es una COPIA mutarla no afecta al registro interno.
Raises:
ValueError: si name no es None pero no corresponde a ningun preset. El mensaje
lista los nombres disponibles.
"""
if name is None or (isinstance(name, str) and not name.strip()):
names = sorted(_PRESETS)
return {"names": names, "count": len(names)}
key = name.strip().lower().replace("_", "-")
if key not in _PRESETS:
raise ValueError(
f"comfyui_get_gamedev_style_preset: estilo desconocido {name!r}. "
f"Disponibles: {sorted(_PRESETS)}"
)
return copy.deepcopy(_PRESETS[key])
if __name__ == "__main__":
import json
print(json.dumps(comfyui_get_gamedev_style_preset(), indent=2))
print(json.dumps(comfyui_get_gamedev_style_preset("gameboy"), indent=2, ensure_ascii=False))
@@ -0,0 +1,97 @@
"""Tests offline de comfyui_get_gamedev_style_preset (recetas de estilo, sin GPU)."""
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 ( # noqa: E402
comfyui_get_gamedev_style_preset,
)
_REQUIRED = {
"name",
"subject_prefix",
"subject_suffix",
"style",
"negative",
"checkpoint",
"lora",
"lora_strength",
"size",
"transparent",
"post",
"notes",
}
def test_golden_three_presets_valid():
# Los 3 estilos iniciales existen y devuelven recetas con todos los campos.
for name in ("gameboy", "ghibli", "pixel-art-retro"):
r = comfyui_get_gamedev_style_preset(name)
assert _REQUIRED <= set(r), f"{name} faltan campos {_REQUIRED - set(r)}"
assert r["name"] == name
assert isinstance(r["style"], str) and r["style"]
assert isinstance(r["checkpoint"], str) and r["checkpoint"].endswith(".safetensors")
assert isinstance(r["lora_strength"], (int, float))
assert isinstance(r["size"], int) and r["size"] > 0
assert isinstance(r["transparent"], bool)
assert isinstance(r["post"], dict)
def test_golden_gameboy_recipe():
r = comfyui_get_gamedev_style_preset("gameboy")
# Game Boy se resuelve sin LoRA, con pixelize a la paleta game-boy (4 tonos).
assert r["lora"] is None
assert r["post"]["pixelize"]["palette"] == "game-boy"
assert r["post"]["pixelize"]["colors"] == 4
def test_golden_pixel_retro_uses_sdxl_lora():
r = comfyui_get_gamedev_style_preset("pixel-art-retro")
# Reutiliza pixel-art-xl (SDXL) -> checkpoint SDXL + size mayor + pixelize.
assert r["lora"] == "pixel-art-xl.safetensors"
assert r["checkpoint"] == "juggernaut_xl_v11.safetensors"
assert r["size"] >= 768
assert "pixelize" in r["post"]
def test_golden_ghibli_degrades_to_watercolor_lora():
r = comfyui_get_gamedev_style_preset("ghibli")
# Sin LoRA Ghibli dedicado -> watercolor instalado + prompt; sin post pixelize.
assert r["lora"] == "watercolor_style_sd15.safetensors"
assert r["post"] == {}
assert "ghibli" in r["style"].lower()
def test_edge_catalog_when_none():
cat = comfyui_get_gamedev_style_preset(None)
assert set(cat["names"]) == {"gameboy", "ghibli", "pixel-art-retro"}
assert cat["count"] == 3
# Cadena vacia tambien devuelve catalogo (discovery).
assert comfyui_get_gamedev_style_preset("") == cat
def test_edge_case_and_separator_insensitive():
# Insensible a mayusculas y a '_' vs '-'.
a = comfyui_get_gamedev_style_preset("PIXEL_ART_RETRO")
b = comfyui_get_gamedev_style_preset("pixel-art-retro")
assert a == b
def test_edge_returns_copy_not_internal():
# Mutar la receta devuelta NO afecta a la siguiente llamada (copia profunda).
r = comfyui_get_gamedev_style_preset("gameboy")
r["style"] = "MUTATED"
r["post"]["pixelize"]["colors"] = 999
r2 = comfyui_get_gamedev_style_preset("gameboy")
assert r2["style"] != "MUTATED"
assert r2["post"]["pixelize"]["colors"] == 4
def test_error_unknown_preset():
try:
comfyui_get_gamedev_style_preset("vaporwave")
assert False, "deberia lanzar ValueError"
except ValueError as e:
assert "vaporwave" in str(e)
assert "gameboy" in str(e) # lista los disponibles