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:
@@ -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
|
||||
Reference in New Issue
Block a user