feat(gamedev): comfyui_generate_styled_asset_oneshot — aplica estilo a un asset con auto-post + amplía catálogo a 6 estilos
Pipeline one-shot que aplica un style preset curado a un asset en una llamada (kind, subject, style_preset) y auto-ejecuta el post-proceso que el estilo declara: los estilos pixelart (gameboy, pixel-art-retro) salen ya pixelizados del pipeline, cerrando el hueco #1 del sistema de style presets (report 0190) donde el caller tenía que llamar comfyui_pixelize_image a mano. Reutiliza el dispatch _SUPPORTED (kind->builder) de comfyui_generate_asset_pack_oneshot en vez de redefinir el mapa. Parte pura aislada en styled_asset_build_only para validar kind/estilo desconocido sin tocar la GPU. Export a Godot consciente del post (pixelart si hubo pixelize, para fijar el filtro Nearest). Catálogo de estilos ampliado de 3 a 6: cyberpunk-neon (prompt puro SD1.5), low-poly-flat (prompt puro SD1.5), cartoon-cel-shaded (LoRA anime_style_box_sd15 0.7). Verificación: 11 tests offline del pipeline + suite de presets verde (27 passed). Prueba real en GPU: mismo "treasure chest" en cyberpunk-neon, low-poly-flat y gameboy one-shot; gameboy pasa de 17374 colores (crudo) a 4 (paleta Game Boy) auto-pixelizado directo del pipeline. Detalle en reports/0191. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -150,7 +150,7 @@ firmas. Extensible: añadir un estilo = una entrada en `_PRESETS`.
|
|||||||
|
|
||||||
| ID | Firma corta | Qué hace |
|
| 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_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. **6 estilos**: **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), **cyberpunk-neon** (prompt puro SD1.5, glow magenta/cyan, sin post), **low-poly-flat** (prompt puro SD1.5, facetas/flat shading PS1, sin post, transparent), **cartoon-cel-shaded** (LoRA `anime_style_box_sd15` 0.7 + prompt cel-shaded, sin post, transparent). Extensible: añadir un estilo = una entrada en `_PRESETS`. |
|
||||||
| `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). |
|
| `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):**
|
**Ejemplo canónico (mismo subject, look del juego entero):**
|
||||||
@@ -181,9 +181,11 @@ if ap["post"].get("pixelize"): # gameboy/pixel-re
|
|||||||
Validado e2e en GPU con el MISMO `knight character` en los 3 estilos (`reports/0190`):
|
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
|
gameboy 4 colores verde (`prompt_id 0657e3e3`), ghibli 78 552 colores acuarela
|
||||||
(`42f2f492`), pixel-art-retro SDXL 768 16 colores (`84b08581`) — tres looks
|
(`42f2f492`), pixel-art-retro SDXL 768 16 colores (`84b08581`) — tres looks
|
||||||
visiblemente distintos y coherentes. **Gotcha**: el `post` no se aplica solo (el caller
|
visiblemente distintos y coherentes. **Gotcha**: en el flujo manual de arriba el `post` no
|
||||||
llama `comfyui_pixelize_image`); el LoRA y el checkpoint deben casar de base (pixel-art-xl
|
se aplica solo (el caller llama `comfyui_pixelize_image`) — para evitarlo usa el pipeline
|
||||||
es SDXL → exige juggernaut); OOM en 8 GB → bajar `size`, NO matar procesos.
|
one-shot `comfyui_generate_styled_asset_oneshot` (abajo), que auto-aplica el post. 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)
|
||||||
|
|
||||||
@@ -191,6 +193,7 @@ es SDXL → exige juggernaut); OOM en 8 GB → bajar `size`, NO matar procesos.
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `comfyui_generate_asset_pack_oneshot_py_pipelines` | `(pack, *, checkpoint="dreamshaper_8…", style="", lora=None, base_seed=0, size=None, server="127.0.0.1:8188", export_godot=None, out_dir=None, …) -> dict` | **Set COHERENTE de assets 2D de un mismo juego de un solo tiro**: `pack=[{"kind","subject"}, …]` → despacha cada `kind` a su builder atómico (26 kinds: item_icon, enemy_creature, prop_object, seamless_tile, ui_hud, particle_texture, …) compartiendo el MISMO `checkpoint`/`lora` + `style` común inyectado al `subject` + `seed = base_seed + i`, encola (`submit`) + espera (`wait`) + descarga (`fetch`) cada uno, y (si `export_godot`) los exporta a Godot. Promoción a pipeline del patrón "N builders con el mismo estilo" (issue 0087). Fail-fast si `kind` desconocido; un OOM aislado no aborta el resto. Probado e2e en GPU SD1.5 512: `magic sword`(item_icon, seed 42) + `goblin warrior`(enemy_creature, seed 43), `style="dark fantasy, hand-painted"` → 2/2 PNG 512×512 RGBA coherentes (`prompt_id f7cfda43` + `11d1d031`, `reports/0179`). Impuro: HTTP + disco + (export) subprocess. |
|
| `comfyui_generate_asset_pack_oneshot_py_pipelines` | `(pack, *, checkpoint="dreamshaper_8…", style="", lora=None, base_seed=0, size=None, server="127.0.0.1:8188", export_godot=None, out_dir=None, …) -> dict` | **Set COHERENTE de assets 2D de un mismo juego de un solo tiro**: `pack=[{"kind","subject"}, …]` → despacha cada `kind` a su builder atómico (26 kinds: item_icon, enemy_creature, prop_object, seamless_tile, ui_hud, particle_texture, …) compartiendo el MISMO `checkpoint`/`lora` + `style` común inyectado al `subject` + `seed = base_seed + i`, encola (`submit`) + espera (`wait`) + descarga (`fetch`) cada uno, y (si `export_godot`) los exporta a Godot. Promoción a pipeline del patrón "N builders con el mismo estilo" (issue 0087). Fail-fast si `kind` desconocido; un OOM aislado no aborta el resto. Probado e2e en GPU SD1.5 512: `magic sword`(item_icon, seed 42) + `goblin warrior`(enemy_creature, seed 43), `style="dark fantasy, hand-painted"` → 2/2 PNG 512×512 RGBA coherentes (`prompt_id f7cfda43` + `11d1d031`, `reports/0179`). Impuro: HTTP + disco + (export) subprocess. |
|
||||||
| `comfyui_generate_character_set_oneshot_py_pipelines` | `(character, *, style="game character, full body, clean background", checkpoint="dreamshaper_8…", base_kind="enemy_creature", directions=8, make_directional=True, make_3d=True, directional_model="sv3d", elevation=15.0, seed=0, size=512, directional_size=None, flatten_color=(255,255,255), variant_3d="mini", lora=None, server="127.0.0.1:8188", export_godot=None, out_dir=None, free_vram=True, …) -> dict` | **Set COMPLETO y COHERENTE de UN personaje de un solo tiro** (culminación cross-frontera del grupo): genera del MISMO personaje (1) imagen **base 2D** recortada a alpha, (2) **sprite direccional N-way** (vistas 3D consistentes SV3D/Zero123) y (3) **malla 3D `.glb`** (Hunyuan3D-2). La CLAVE es la coherencia: el direccional y el 3D parten de la **MISMA base 2D aplanada** (`base_flat`), no de tres generaciones independientes → mismo personaje en las tres representaciones, no tres personajes distintos. Compone un builder de personaje (`enemy_creature`/`portrait_avatar`/`topdown_sprite`, elegido por introspección) + `comfyui_flatten_alpha_on_color` (aplana la base recortada sobre blanco — los modelos 3D y `LoadImage` hacen `convert("RGB")` y tiran el alpha) + `comfyui_image_to_3d_oneshot` + `comfyui_build_directional_sprite_workflow` + `submit`/`wait`/`fetch` + `comfyui_export_asset_to_godot`. **Secuencial liberando VRAM** (`POST /free`) entre los pasos pesados, el 3D ANTES del direccional (SV3D es el de mayor pico, ~7.1 GB), para caber en 8 GB. Un fallo aislado (p.ej. OOM en el 3D) NO aborta el resto: deja el set PARCIAL. Promoción a pipeline (issue 0087) de la secuencia que hoy exige 4 llamadas a mano. Probado e2e en GPU — ver `reports/0188`. Impuro: HTTP + disco + (export) subprocess. |
|
| `comfyui_generate_character_set_oneshot_py_pipelines` | `(character, *, style="game character, full body, clean background", checkpoint="dreamshaper_8…", base_kind="enemy_creature", directions=8, make_directional=True, make_3d=True, directional_model="sv3d", elevation=15.0, seed=0, size=512, directional_size=None, flatten_color=(255,255,255), variant_3d="mini", lora=None, server="127.0.0.1:8188", export_godot=None, out_dir=None, free_vram=True, …) -> dict` | **Set COMPLETO y COHERENTE de UN personaje de un solo tiro** (culminación cross-frontera del grupo): genera del MISMO personaje (1) imagen **base 2D** recortada a alpha, (2) **sprite direccional N-way** (vistas 3D consistentes SV3D/Zero123) y (3) **malla 3D `.glb`** (Hunyuan3D-2). La CLAVE es la coherencia: el direccional y el 3D parten de la **MISMA base 2D aplanada** (`base_flat`), no de tres generaciones independientes → mismo personaje en las tres representaciones, no tres personajes distintos. Compone un builder de personaje (`enemy_creature`/`portrait_avatar`/`topdown_sprite`, elegido por introspección) + `comfyui_flatten_alpha_on_color` (aplana la base recortada sobre blanco — los modelos 3D y `LoadImage` hacen `convert("RGB")` y tiran el alpha) + `comfyui_image_to_3d_oneshot` + `comfyui_build_directional_sprite_workflow` + `submit`/`wait`/`fetch` + `comfyui_export_asset_to_godot`. **Secuencial liberando VRAM** (`POST /free`) entre los pasos pesados, el 3D ANTES del direccional (SV3D es el de mayor pico, ~7.1 GB), para caber en 8 GB. Un fallo aislado (p.ej. OOM en el 3D) NO aborta el resto: deja el set PARCIAL. Promoción a pipeline (issue 0087) de la secuencia que hoy exige 4 llamadas a mano. Probado e2e en GPU — ver `reports/0188`. Impuro: HTTP + disco + (export) subprocess. |
|
||||||
|
| `comfyui_generate_styled_asset_oneshot_py_pipelines` | `(kind, subject, style_preset, *, seed=0, server="127.0.0.1:8188", out_dir=None, export_godot=None, style_override=None, negative_extra=None, free_vram=False, **builder_extra) -> dict` | **Aplica un ESTILO curado a UN asset de un solo tiro, con AUTO-POST**: `comfyui_get_gamedev_style_preset(style_preset)` → `comfyui_apply_style_preset` → despacha `kind` a su builder (REUTILIZA el dispatch `_SUPPORTED` del pack, mismos 26 kinds) → `submit`/`wait`/`fetch` → **auto-aplica el `post` del preset** (`comfyui_pixelize_image` si el estilo lo pide) → export opcional a Godot (como `pixelart` si hubo pixelize → fija el filtro Nearest). Cierra el hueco #1 de los style presets (report 0190): los estilos pixelart (gameboy, pixel-art-retro) salen ya pixelizados del pipeline, **sin llamar a `comfyui_pixelize_image` a mano**. Devuelve `path` (FINAL post-procesado) y `raw_path` (crudo); `path==raw_path` si el estilo no pide post. Kind/estilo desconocido → `ok=False` sin tocar la GPU (validación pura; parte pura aislada en `styled_asset_build_only`). Probado e2e en GPU: mismo `treasure chest`(prop_object) en cyberpunk-neon (`prompt_id 02473baa`), low-poly-flat (`7a186053`) y gameboy (`46b396e2`, crudo 17374 colores → final **4 colores** Game Boy, auto-pixelizado) — ver `reports/0191`. Impuro: HTTP + disco + (export) subprocess. |
|
||||||
|
|
||||||
## Ejemplo end-to-end con builder (Fase 1 GPU → Fase 2 CPU → Godot)
|
## Ejemplo end-to-end con builder (Fase 1 GPU → Fase 2 CPU → Godot)
|
||||||
|
|
||||||
|
|||||||
@@ -134,6 +134,79 @@ _PRESETS: dict[str, dict] = {
|
|||||||
"8GB -> bajar size a 512 (NO matar procesos). El LoRA da el estilo; el post el grid."
|
"8GB -> bajar size a 512 (NO matar procesos). El LoRA da el estilo; el post el grid."
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
# Cyberpunk neón: ciudad nocturna, luces de neón, glow, alto contraste, atmósfera
|
||||||
|
# Blade Runner. No hay un LoRA cyberpunk gratis instalado en el servidor; el look se
|
||||||
|
# logra con PROMPT puro sobre SD1.5 (dreamshaper_8 lo cubre bien) — es un estilo muy
|
||||||
|
# dirigible por prompt. Sin post (es ilustración con gradientes/glow, NO pixelart;
|
||||||
|
# pixelizar mataría el brillo). transparent=False para conservar el fondo neón.
|
||||||
|
"cyberpunk-neon": {
|
||||||
|
"name": "cyberpunk-neon",
|
||||||
|
"subject_prefix": "",
|
||||||
|
"subject_suffix": ", neon-lit, glowing edges, high contrast, futuristic",
|
||||||
|
"style": "cyberpunk neon art, blade runner aesthetic, glowing neon lights, dark city night, vibrant magenta and cyan glow, rim lighting, reflective surfaces, atmospheric haze, detailed digital painting",
|
||||||
|
"negative": "daylight, pastel, flat lighting, washed out, low contrast, pixel art, lowres, blurry, deformed, text, watermark, signature",
|
||||||
|
"checkpoint": "dreamshaper_8.safetensors",
|
||||||
|
"lora": None,
|
||||||
|
"lora_strength": 1.0,
|
||||||
|
"size": 512,
|
||||||
|
"transparent": False,
|
||||||
|
"post": {},
|
||||||
|
"notes": (
|
||||||
|
"Sin LoRA: no hay un LoRA cyberpunk gratis instalado y no se descargo ninguno "
|
||||||
|
"gated/de pago. El neón lo da el prompt (glow magenta/cyan, rim lighting, ciudad "
|
||||||
|
"nocturna) sobre dreamshaper_8 (SD1.5), que rinde bien en este registro. Sin post: "
|
||||||
|
"el brillo y los gradientes son la identidad del estilo; pixelizar los destruiria. "
|
||||||
|
"transparent=False para conservar el ambiente neón del fondo."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
# Low-poly flat: estética PS1/PSX y arte 3D minimalista — facetas geométricas, flat
|
||||||
|
# shading sin gradientes suaves, pocos polígonos, colores planos. PROMPT puro sobre
|
||||||
|
# SD1.5: el LoRA 3d_render_redmond empuja a render fotorrealista (lo contrario de
|
||||||
|
# low-poly), asi que se evita a propósito. Sin post (es flat shading limpio, no grid).
|
||||||
|
"low-poly-flat": {
|
||||||
|
"name": "low-poly-flat",
|
||||||
|
"subject_prefix": "",
|
||||||
|
"subject_suffix": ", low poly, faceted, flat shaded, geometric",
|
||||||
|
"style": "low poly 3d art, flat shading, faceted geometry, minimal polygons, PS1 PSX aesthetic, clean solid colors, isometric game asset, no gradients, crisp facets",
|
||||||
|
"negative": "photorealistic, smooth shading, soft gradient, high detail, realistic texture, blurry, noisy, pixel art, deformed, text, watermark, signature",
|
||||||
|
"checkpoint": "dreamshaper_8.safetensors",
|
||||||
|
"lora": None,
|
||||||
|
"lora_strength": 1.0,
|
||||||
|
"size": 512,
|
||||||
|
"transparent": True,
|
||||||
|
"post": {},
|
||||||
|
"notes": (
|
||||||
|
"Sin LoRA a propósito: 3d_render_redmond_sd15 (instalado) empuja a render "
|
||||||
|
"fotorrealista, lo OPUESTO a low-poly. El look faceteado/flat lo da el prompt "
|
||||||
|
"(low poly, faceted, flat shading, PS1) sobre dreamshaper_8 (SD1.5). Sin post: el "
|
||||||
|
"flat shading es limpio de por sí, no necesita pixelize. transparent=True porque un "
|
||||||
|
"asset low-poly suele ir recortado sobre el juego (silueta sólida bien definida)."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
# Cartoon cel-shaded: dibujos animados / anime con sombreado plano por celdas, líneas
|
||||||
|
# negras gruesas, colores saturados y planos (look toon/Borderlands/Zelda Wind Waker).
|
||||||
|
# Usa anime_style_box_sd15 (LoRA gratis ya instalado) a fuerza media + prompt cel-shaded.
|
||||||
|
# Sin post (es ilustración vectorial limpia, no pixelart).
|
||||||
|
"cartoon-cel-shaded": {
|
||||||
|
"name": "cartoon-cel-shaded",
|
||||||
|
"subject_prefix": "",
|
||||||
|
"subject_suffix": ", cel shaded, bold outlines, flat colors, cartoon",
|
||||||
|
"style": "cartoon cel-shaded art, bold black outlines, flat color fills, hard cel shadows, vibrant saturated colors, clean vector look, anime toon shading, comic style",
|
||||||
|
"negative": "photorealistic, soft shading, gradient, realistic texture, painterly, blurry, noisy, pixel art, lowres, grainy, deformed, text, watermark, signature",
|
||||||
|
"checkpoint": "dreamshaper_8.safetensors",
|
||||||
|
"lora": "anime_style_box_sd15.safetensors",
|
||||||
|
"lora_strength": 0.7,
|
||||||
|
"size": 512,
|
||||||
|
"transparent": True,
|
||||||
|
"post": {},
|
||||||
|
"notes": (
|
||||||
|
"anime_style_box_sd15.safetensors (gratis, ya instalado en /mnt/2tb) a strength 0.7 "
|
||||||
|
"empuja el toon/anime; el prompt sella el cel-shading (outlines negros gruesos, "
|
||||||
|
"sombras duras por celdas, colores planos saturados). SD1.5 (dreamshaper_8). Sin "
|
||||||
|
"post: el look vectorial limpio no necesita pixelize. transparent=True para recortar "
|
||||||
|
"la silueta del personaje/objeto cartoon sobre el juego."
|
||||||
|
),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -141,7 +214,8 @@ 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.
|
"""Devuelve la receta de un style preset gamedev, o el catalogo si name es None.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
name: identificador del estilo ("gameboy", "ghibli", "pixel-art-retro"). Si es
|
name: identificador del estilo ("gameboy", "ghibli", "pixel-art-retro",
|
||||||
|
"cyberpunk-neon", "low-poly-flat", "cartoon-cel-shaded"). Si es
|
||||||
None (o cadena vacia), devuelve el catalogo de nombres disponibles en vez de
|
None (o cadena vacia), devuelve el catalogo de nombres disponibles en vez de
|
||||||
una receta concreta (discovery). Insensible a mayusculas y a '_' vs '-'.
|
una receta concreta (discovery). Insensible a mayusculas y a '_' vs '-'.
|
||||||
|
|
||||||
|
|||||||
@@ -65,8 +65,12 @@ def test_golden_ghibli_degrades_to_watercolor_lora():
|
|||||||
|
|
||||||
def test_edge_catalog_when_none():
|
def test_edge_catalog_when_none():
|
||||||
cat = comfyui_get_gamedev_style_preset(None)
|
cat = comfyui_get_gamedev_style_preset(None)
|
||||||
assert set(cat["names"]) == {"gameboy", "ghibli", "pixel-art-retro"}
|
# Los 3 originales + los 3 ampliados (2026-06-27); el catalogo crece, asi que se
|
||||||
assert cat["count"] == 3
|
# comprueba inclusion y conteo minimo, no igualdad exacta (evita romper al ampliar).
|
||||||
|
assert {"gameboy", "ghibli", "pixel-art-retro"} <= set(cat["names"])
|
||||||
|
assert {"cyberpunk-neon", "low-poly-flat", "cartoon-cel-shaded"} <= set(cat["names"])
|
||||||
|
assert cat["count"] >= 6
|
||||||
|
assert cat["count"] == len(cat["names"])
|
||||||
# Cadena vacia tambien devuelve catalogo (discovery).
|
# Cadena vacia tambien devuelve catalogo (discovery).
|
||||||
assert comfyui_get_gamedev_style_preset("") == cat
|
assert comfyui_get_gamedev_style_preset("") == cat
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
---
|
||||||
|
name: comfyui_generate_styled_asset_oneshot
|
||||||
|
kind: pipeline
|
||||||
|
lang: py
|
||||||
|
domain: pipelines
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: impure
|
||||||
|
signature: "def comfyui_generate_styled_asset_oneshot(kind: str, subject: str, style_preset: str, *, seed: int = 0, server: str = \"127.0.0.1:8188\", out_dir: str | None = None, export_godot: str | None = None, style_override: str | None = None, negative_extra: str | None = None, wait_timeout: float = 600.0, free_vram: bool = False, godot_bin: str | None = None, **builder_extra) -> dict"
|
||||||
|
description: "One-shot del grupo gamedev-2d que aplica un ESTILO curado a un asset en UNA llamada y AUTO-APLICA su post-proceso. Cierra el ultimo hueco manual del sistema de style presets (report 0190): antes habia que encadenar a mano get_gamedev_style_preset -> apply_style_preset -> despachar al builder del kind -> generar -> y llamar a mano comfyui_pixelize_image. Aqui se hace de un tiro y, para los estilos que declaran post (gameboy, pixel-art-retro), el PNG sale YA pixelizado sin paso manual. Flujo: comfyui_get_gamedev_style_preset(style_preset) -> comfyui_apply_style_preset(preset, subject) -> despacho kind->builder REUTILIZANDO el dispatch _SUPPORTED de comfyui_generate_asset_pack_oneshot -> submit/wait/fetch -> auto-post (comfyui_pixelize_image si el preset lo pide) -> export opcional a Godot (como pixelart si hubo pixelize, para fijar el filtro Nearest). Doctrina issue 0087: el registry crece promoviendo a un pipeline one-shot la composicion repetida 'elegir estilo -> aplicarlo a un kind -> generar -> post-procesar', no inflando el helper de presets ni los builders. Un kind/estilo desconocido devuelve ok=False con error claro SIN tocar la GPU. Devuelve {ok, kind, subject, style_preset, seed, prompt_id, raw_path, path, post, post_applied, size, transparent, godot_kind, exported, coherence_note, error}. Impuro: HTTP a ComfyUI + disco + (export) subprocess."
|
||||||
|
tags: [gamedev-2d, pipelines, comfyui, ml]
|
||||||
|
uses_functions: [comfyui_get_gamedev_style_preset_py_ml, comfyui_apply_style_preset_py_ml, comfyui_pixelize_image_py_ml, comfyui_submit_workflow_py_ml, comfyui_wait_result_py_ml, comfyui_fetch_output_image_py_ml, comfyui_generate_asset_pack_oneshot_py_pipelines, comfyui_export_asset_to_godot_py_pipelines]
|
||||||
|
uses_types: []
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: "error_py_core"
|
||||||
|
imports: [comfyui_get_gamedev_style_preset_py_ml, comfyui_apply_style_preset_py_ml, comfyui_pixelize_image_py_ml, comfyui_submit_workflow_py_ml, comfyui_wait_result_py_ml, comfyui_fetch_output_image_py_ml, comfyui_generate_asset_pack_oneshot_py_pipelines, comfyui_export_asset_to_godot_py_pipelines]
|
||||||
|
tested: true
|
||||||
|
test_file_path: "python/functions/pipelines/comfyui_generate_styled_asset_oneshot_test.py"
|
||||||
|
tests: [test_unknown_kind_fails_without_network, test_unknown_style_fails_without_network, test_empty_subject_fails_without_network, test_build_only_preset_drives_workflow, test_build_only_lora_preset_injects_lora, test_catalog_has_at_least_5_styles, test_every_style_builds_a_valid_workflow, test_post_auto_applied_for_gameboy, test_no_post_keeps_raw_for_cyberpunk, test_post_failure_keeps_raw_and_flags_error, test_export_godot_uses_pixelart_when_post]
|
||||||
|
file_path: "python/functions/pipelines/comfyui_generate_styled_asset_oneshot.py"
|
||||||
|
params:
|
||||||
|
- name: kind
|
||||||
|
desc: "tipo de asset a generar; uno de supported_kinds() del grupo gamedev-2d (item_icon, enemy_creature, prop_object, seamless_tile, ui_hud, particle_texture, ... 26 kinds). Un kind desconocido devuelve ok=False con error claro SIN tocar la GPU."
|
||||||
|
- name: subject
|
||||||
|
desc: "que dibujar ('knight character', 'health potion', 'stone wall tile'). Se combina con el prefijo/sufijo del estilo. Vacio -> error sin red. Es el primer posicional."
|
||||||
|
- name: style_preset
|
||||||
|
desc: "nombre del estilo curado a aplicar, del catalogo de comfyui_get_gamedev_style_preset (gameboy, ghibli, pixel-art-retro, cyberpunk-neon, low-poly-flat, cartoon-cel-shaded). Insensible a mayusculas y a '_' vs '-'. Un estilo desconocido devuelve ok=False con error claro (+ lista de disponibles) SIN tocar la GPU."
|
||||||
|
- name: seed
|
||||||
|
desc: "semilla del KSampler para reproducibilidad. keyword-only."
|
||||||
|
- name: server
|
||||||
|
desc: "host:port del servidor ComfyUI; se acepta con o sin esquema (http://). keyword-only."
|
||||||
|
- name: out_dir
|
||||||
|
desc: "directorio local donde descargar el PNG crudo y el post-procesado; None = un dir temporal por (estilo, seed) en tempdir. keyword-only."
|
||||||
|
- name: export_godot
|
||||||
|
desc: "ruta de un proyecto Godot 4; si se da, el asset FINAL (ya post-procesado) se exporta a res://assets/...; si hubo pixelize se exporta como 'pixelart' (fija el filtro Nearest global). None = no exportar. keyword-only."
|
||||||
|
- name: style_override
|
||||||
|
desc: "sustituye el descriptor 'style' del preset (override puntual); None usa el del preset. keyword-only."
|
||||||
|
- name: negative_extra
|
||||||
|
desc: "negativo extra del caller; se MERGEA con el negativo del estilo (no lo reemplaza). keyword-only."
|
||||||
|
- name: wait_timeout
|
||||||
|
desc: "segundos maximos esperando el trabajo en ComfyUI. keyword-only."
|
||||||
|
- name: free_vram
|
||||||
|
desc: "si True hace POST /free ANTES de generar para liberar VRAM de tareas previas (util en 8 GB con modelos ya cargados). keyword-only."
|
||||||
|
- name: godot_bin
|
||||||
|
desc: "binario de Godot para el reimport headless; None autodetecta. keyword-only."
|
||||||
|
- name: builder_extra
|
||||||
|
desc: "kwargs extra reenviados al builder del kind: posicionales requeridos por algunos builders (p.ej. expression='angry' en emote) o passthrough que el builder admita (steps, cfg, width, height, ...). Sobreescriben la coherencia del estilo si se pasan."
|
||||||
|
output: "dict con ok (bool, True si el asset se genero y, si el estilo pedia post, el post se aplico), kind/subject/style_preset/seed (eco; subject es el combinado con el estilo), prompt_id (str, id del trabajo en ComfyUI), raw_path (PNG descargado SIN post), path (PNG FINAL entregable: post-procesado si el estilo lo pedia, si no igual a raw_path), post (spec de post del estilo, {} si no pedia), post_applied ({kind, out_path, n_colors_final, size} en exito | {kind, ok:False, error} si fallo | None si el estilo no pedia post), size (int) y transparent (bool) recomendados por el estilo, godot_kind (bucket Godot usado), exported (resultado de export a Godot o None), coherence_note (str), error (str). Un fallo de post o export se anota en error pero el crudo queda en raw_path."
|
||||||
|
---
|
||||||
|
|
||||||
|
Pipeline que aplica un estilo CURADO (gameboy, ghibli, pixel-art-retro, cyberpunk-neon,
|
||||||
|
low-poly-flat, cartoon-cel-shaded, ...) a un asset de juego de un solo tiro y, lo
|
||||||
|
importante, **auto-ejecuta el post-proceso que el estilo declara**: los estilos pixelart
|
||||||
|
(gameboy, pixel-art-retro) salen ya pixelizados del pipeline, sin tener que llamar a
|
||||||
|
`comfyui_pixelize_image` a mano. Es la pieza que cerraba el sistema de style presets del
|
||||||
|
grupo `gamedev-2d` (report 0190).
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```python
|
||||||
|
import sys, os
|
||||||
|
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
|
||||||
|
from pipelines.comfyui_generate_styled_asset_oneshot import comfyui_generate_styled_asset_oneshot
|
||||||
|
|
||||||
|
# Estilo CON post: el icono sale YA pixelizado (paleta Game Boy de 4 tonos), sin paso manual
|
||||||
|
res = comfyui_generate_styled_asset_oneshot(
|
||||||
|
"item_icon", "health potion", "gameboy", seed=7, out_dir="/tmp/styled_gameboy",
|
||||||
|
)
|
||||||
|
# res["raw_path"] -> PNG crudo de la difusion
|
||||||
|
# res["path"] -> PNG FINAL ya pixelizado (auto-post) — distinto del crudo
|
||||||
|
# res["post_applied"] -> {"kind":"pixelize","n_colors_final":4, ...}
|
||||||
|
|
||||||
|
# Estilo SIN post: el mismo subject en estilo neón (path == raw_path, no se pixeliza)
|
||||||
|
res = comfyui_generate_styled_asset_oneshot(
|
||||||
|
"enemy_creature", "street samurai", "cyberpunk-neon", seed=5, out_dir="/tmp/styled_cyber",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Con export directo a un proyecto Godot 4 (pixelart -> Nearest filter global automatico)
|
||||||
|
res = comfyui_generate_styled_asset_oneshot(
|
||||||
|
"prop_object", "treasure chest", "pixel-art-retro", seed=3,
|
||||||
|
export_godot=os.path.expanduser("~/gamedev/projects/dungeon"),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Kind que exige un posicional extra (emote.expression) -> pasarlo como kwarg
|
||||||
|
res = comfyui_generate_styled_asset_oneshot(
|
||||||
|
"emote", "hero", "cartoon-cel-shaded", expression="angry", seed=1,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Lanzable tambien por `fn run` (despacha como pipeline Python; corre el `__main__` de demo):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./fn run comfyui_generate_styled_asset_oneshot # demo: item_icon "health potion" estilo gameboy
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cuando usarla
|
||||||
|
|
||||||
|
Usala cuando quieras UN asset concreto con un look consistente de un catalogo de estilos
|
||||||
|
ya curados, sin acordarte del checkpoint/LoRA/negativo correctos ni del post-proceso que
|
||||||
|
ese estilo necesita. Es el reemplazo one-shot de "buscar el preset -> aplicarlo al builder
|
||||||
|
-> generar -> pixelizar a mano": una sola llamada con `(kind, subject, style_preset)`. Para
|
||||||
|
un SET de assets variados que comparten un estilo libre (no de catalogo), usa
|
||||||
|
`comfyui_generate_asset_pack_oneshot`. Para las tres representaciones (2D + direccional +
|
||||||
|
3D) de UN personaje, usa `comfyui_generate_character_set_oneshot`. Para añadir un estilo
|
||||||
|
nuevo al catalogo, edita `_PRESETS` en `comfyui_get_gamedev_style_preset` (datos puros) —
|
||||||
|
este pipeline lo recoge sin tocarse.
|
||||||
|
|
||||||
|
## Gotchas
|
||||||
|
|
||||||
|
- **Auto-post = la razon de existir.** Si el estilo declara `post` (gameboy, pixel-art-retro
|
||||||
|
traen `{"pixelize": {...}}`), el pipeline lo ejecuta sobre el PNG descargado y devuelve la
|
||||||
|
ruta post-procesada en `path` (el crudo queda en `raw_path`). Para estilos sin post
|
||||||
|
(ghibli, cyberpunk-neon, low-poly-flat, cartoon-cel-shaded) `path == raw_path`.
|
||||||
|
- **post + transparent son incompatibles (el post gana).** `comfyui_pixelize_image` hace
|
||||||
|
`convert("RGB")`, que descarta el alpha. Por eso los presets con post traen
|
||||||
|
`transparent=False` (asset opaco). No pidas un estilo con post sobre un kind donde
|
||||||
|
necesites alpha: el pixelize aplanaria el recorte. Los estilos sin post respetan el
|
||||||
|
`transparent` que recomienden (cyberpunk-neon False, cartoon-cel-shaded/low-poly-flat True).
|
||||||
|
- **El post elige el bucket Godot.** Si hubo pixelize, el export va como `pixelart`
|
||||||
|
(asegura `default_texture_filter=0` Nearest en project.godot); si no, usa el bucket
|
||||||
|
natural del builder (sprite/vfx/tileset).
|
||||||
|
- **Reutiliza el dispatch del pack.** El mapa `kind -> (godot_kind, builder)` NO se redefine
|
||||||
|
aqui: se importa `_SUPPORTED` de `comfyui_generate_asset_pack_oneshot`, asi que los 26
|
||||||
|
kinds soportados son exactamente los del pack (una sola fuente de verdad).
|
||||||
|
- **Un kind o estilo desconocido NO toca la GPU.** La resolucion del preset y la
|
||||||
|
construccion del workflow (pasos 1-3) son puras y se hacen antes de encolar; un kind/estilo
|
||||||
|
invalido devuelve `ok=False` con error claro y `prompt_id == ""`. La parte pura es
|
||||||
|
invocable sola con `styled_asset_build_only(...)` para inspeccionar el grafo y el post sin
|
||||||
|
generar.
|
||||||
|
- **VRAM / OOM (RTX 3070 8 GB).** Los presets nuevos usan SD1.5 (dreamshaper_8) a 512 y caben
|
||||||
|
holgados; pixel-art-retro usa SDXL (juggernaut_xl_v11) a 768 y pica mas. Con modelos ya
|
||||||
|
cargados de otra tarea, pasa `free_vram=True` (hace `POST /free` antes de generar) o limpia
|
||||||
|
tu mismo (`POST http://127.0.0.1:8188/free {"unload_models":true,"free_memory":true}`). Si
|
||||||
|
hay OOM, baja la resolucion del preset o el `size` del builder via `builder_extra`; NO se
|
||||||
|
matan procesos.
|
||||||
|
- **Fallo aislado de post/export no borra el crudo.** Si el pixelize o el export fallan, el
|
||||||
|
PNG crudo sigue en `raw_path` y el error se anota; `ok=False` solo si el post pedido fallo.
|
||||||
|
- **`server`** se normaliza (acepta con o sin `http://`); internamente es `host:port`.
|
||||||
|
|
||||||
|
## Capability growth log
|
||||||
|
|
||||||
|
v1.0.0 (2026-06-27) — version inicial. Promueve a un pipeline one-shot la secuencia
|
||||||
|
"elegir un style preset curado -> aplicarlo a un kind de asset -> generar -> auto-aplicar
|
||||||
|
su post-proceso" (issue 0087). Cierra el hueco #1 del sistema de style presets (report
|
||||||
|
0190): el post-proceso (pixelize) ya no se llama a mano — los estilos pixelart salen
|
||||||
|
pixelizados directos del pipeline. Reutiliza el dispatch `_SUPPORTED` del pack one-shot
|
||||||
|
para no duplicar el mapa kind->builder.
|
||||||
@@ -0,0 +1,412 @@
|
|||||||
|
"""comfyui_generate_styled_asset_oneshot — aplica un ESTILO a un asset de un solo tiro.
|
||||||
|
|
||||||
|
One-shot del grupo `gamedev-2d` que cierra el último hueco manual del sistema de
|
||||||
|
*style presets* (report 0190): antes, aplicar un estilo curado (gameboy / ghibli /
|
||||||
|
pixel-art-retro / ...) a un asset exigía encadenar A MANO cuatro pasos —
|
||||||
|
`comfyui_get_gamedev_style_preset` → `comfyui_apply_style_preset` → despachar al
|
||||||
|
builder del `kind` y generar → y, lo que más se olvidaba, llamar a mano al
|
||||||
|
post-proceso (`comfyui_pixelize_image`) que el preset declaraba. Este pipeline lo
|
||||||
|
promueve a UNA sola llamada y, sobre todo, **auto-aplica el post-proceso del preset**:
|
||||||
|
si el estilo pide pixelizar (gameboy, pixel-art-retro), el PNG sale ya pixelizado del
|
||||||
|
pipeline, sin paso manual. Es la doctrina del issue 0087 aplicada al estilo: el
|
||||||
|
registry no crece inflando builders ni el helper de presets, crece promoviendo a un
|
||||||
|
pipeline one-shot la composición repetida "elegir estilo → aplicarlo a un kind →
|
||||||
|
generar → post-procesar".
|
||||||
|
|
||||||
|
Flujo:
|
||||||
|
|
||||||
|
1. comfyui_get_gamedev_style_preset(style_preset) -> receta de estilo (datos puros)
|
||||||
|
2. comfyui_apply_style_preset(preset, subject) -> subject + builder_kwargs + size
|
||||||
|
+ transparent + spec de post
|
||||||
|
3. despacho kind -> builder atómico del registry (REUTILIZA el dispatch _SUPPORTED
|
||||||
|
de comfyui_generate_asset_pack_oneshot, no se redefine el mapa kind->builder)
|
||||||
|
4. comfyui_submit_workflow + comfyui_wait_result + comfyui_fetch_output_image
|
||||||
|
5. AUTO-POST: si el preset declara post (p.ej. {"pixelize": {...}}), se aplica al
|
||||||
|
PNG descargado con comfyui_pixelize_image -> la ruta FINAL ya es la post-procesada
|
||||||
|
6. export opcional a un proyecto Godot (comfyui_export_asset_to_godot); si hubo
|
||||||
|
pixelize, el asset se exporta como `pixelart` (filtro Nearest global en Godot)
|
||||||
|
|
||||||
|
A diferencia de `comfyui_generate_asset_pack_oneshot` (que comparte UN estilo libre
|
||||||
|
concatenado al subject de N assets variados) y de `comfyui_generate_character_set_oneshot`
|
||||||
|
(que produce las tres representaciones de UN personaje), este pipeline aplica UN preset
|
||||||
|
de estilo CURADO (con su checkpoint/LoRA/negativo/post coherentes) a UN asset concreto.
|
||||||
|
Reutiliza el mismo catálogo de `kind`s del pack, así que sirve para cualquiera de los 26
|
||||||
|
builders de sujeto del grupo.
|
||||||
|
|
||||||
|
Pipeline impuro: red (HTTP a ComfyUI) + escritura en disco + (export) subprocess.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import inspect
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import urllib.error
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
_FUNCTIONS_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
if _FUNCTIONS_ROOT not in sys.path:
|
||||||
|
sys.path.insert(0, _FUNCTIONS_ROOT)
|
||||||
|
|
||||||
|
# --- presets de estilo (puros) ---
|
||||||
|
from ml.comfyui_apply_style_preset import comfyui_apply_style_preset
|
||||||
|
from ml.comfyui_get_gamedev_style_preset import comfyui_get_gamedev_style_preset
|
||||||
|
|
||||||
|
# --- post-proceso CPU (impuro: disco) ---
|
||||||
|
from ml.comfyui_pixelize_image import comfyui_pixelize_image
|
||||||
|
|
||||||
|
# --- transporte ComfyUI (impuro) ---
|
||||||
|
from ml.comfyui_fetch_output_image import comfyui_fetch_output_image
|
||||||
|
from ml.comfyui_submit_workflow import comfyui_submit_workflow
|
||||||
|
from ml.comfyui_wait_result import comfyui_wait_result
|
||||||
|
|
||||||
|
# --- export opcional a Godot ---
|
||||||
|
from pipelines.comfyui_export_asset_to_godot import comfyui_export_asset_to_godot
|
||||||
|
|
||||||
|
# REUTILIZA el dispatch kind -> (godot_kind, builder) del pack one-shot en vez de
|
||||||
|
# redefinir el mapa: una sola fuente de verdad de los `kind`s soportados y de a qué
|
||||||
|
# builder atómico va cada uno. _CKPT_PARAMS y _first_image también se comparten.
|
||||||
|
from pipelines.comfyui_generate_asset_pack_oneshot import (
|
||||||
|
_CKPT_PARAMS,
|
||||||
|
_SUPPORTED,
|
||||||
|
_first_image,
|
||||||
|
supported_kinds,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _free_vram(server: str) -> bool:
|
||||||
|
"""Pide a ComfyUI que descargue modelos y libere VRAM (POST /free).
|
||||||
|
|
||||||
|
Best-effort: en una RTX 3070 de 8 GB con modelos ya cargados de otra tarea, liberar
|
||||||
|
antes de generar evita el OOM. No lanza: si el endpoint falla, devuelve False y el
|
||||||
|
caller sigue.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
body = json.dumps({"unload_models": True, "free_memory": True}).encode()
|
||||||
|
req = urllib.request.Request(
|
||||||
|
f"http://{server}/free", data=body,
|
||||||
|
headers={"Content-Type": "application/json"}, method="POST",
|
||||||
|
)
|
||||||
|
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||||
|
resp.read()
|
||||||
|
return True
|
||||||
|
except (urllib.error.URLError, OSError):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _build_styled_workflow(
|
||||||
|
kind: str,
|
||||||
|
applied: dict,
|
||||||
|
*,
|
||||||
|
seed: int,
|
||||||
|
builder_extra: dict,
|
||||||
|
) -> tuple[dict, str]:
|
||||||
|
"""Construye el workflow de UN asset con el estilo del preset ya aplicado.
|
||||||
|
|
||||||
|
PURO (no toca la red): localiza el builder del `kind` en el dispatch compartido
|
||||||
|
`_SUPPORTED`, introspecciona su firma y le pasa SOLO los kwargs que admita, tomados
|
||||||
|
del resultado de `comfyui_apply_style_preset` (`applied`): el `subject` combinado, el
|
||||||
|
`style`/`checkpoint`/`lora`/`lora_strength`/`negative` del estilo, y la `size`/
|
||||||
|
`transparent` recomendadas. A diferencia del pack (que concatena un `style` libre al
|
||||||
|
subject), aquí el `style` del preset SUSTITUYE el `style` categórico del builder, que
|
||||||
|
es lo que da el look del estilo curado.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
kind: tipo de asset (uno de `supported_kinds()`).
|
||||||
|
applied: dict de `comfyui_apply_style_preset` ({subject, builder_kwargs, size,
|
||||||
|
transparent, post, name}).
|
||||||
|
seed: semilla del KSampler. keyword-only.
|
||||||
|
builder_extra: kwargs extra del caller para posicionales requeridos por algunos
|
||||||
|
builders (p.ej. `expression` en `emote`) o passthrough que el builder admita.
|
||||||
|
keyword-only.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(workflow, godot_kind): el dict API-format y el bucket Godot del builder
|
||||||
|
(sprite/vfx/tileset) para el export.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: si el `kind` no está soportado o falta un posicional requerido del
|
||||||
|
builder (p.ej. `expression` en `emote`).
|
||||||
|
"""
|
||||||
|
if kind not in _SUPPORTED:
|
||||||
|
raise ValueError(
|
||||||
|
f"kind {kind!r} no soportado. Soportados: {', '.join(supported_kinds())}"
|
||||||
|
)
|
||||||
|
godot_kind, fn = _SUPPORTED[kind]
|
||||||
|
params = inspect.signature(fn).parameters
|
||||||
|
bk = applied["builder_kwargs"] # {style, checkpoint, lora, lora_strength, negative}
|
||||||
|
|
||||||
|
kw: dict = {}
|
||||||
|
# checkpoint -> el nombre de parámetro que use el builder (checkpoint/ckpt_name/ckpt).
|
||||||
|
for cand in _CKPT_PARAMS:
|
||||||
|
if cand in params:
|
||||||
|
kw[cand] = bk["checkpoint"]
|
||||||
|
break
|
||||||
|
if "style" in params:
|
||||||
|
kw["style"] = bk["style"]
|
||||||
|
if "negative" in params:
|
||||||
|
kw["negative"] = bk["negative"]
|
||||||
|
if bk.get("lora") and "lora" in params:
|
||||||
|
kw["lora"] = bk["lora"]
|
||||||
|
if "lora_strength" in params:
|
||||||
|
kw["lora_strength"] = bk["lora_strength"]
|
||||||
|
if "size" in params and applied.get("size") is not None:
|
||||||
|
kw["size"] = applied["size"]
|
||||||
|
if "transparent" in params:
|
||||||
|
kw["transparent"] = applied["transparent"]
|
||||||
|
if "seed" in params:
|
||||||
|
kw["seed"] = seed
|
||||||
|
|
||||||
|
# Posicionales requeridos más allá del primero (= subject). Ej: emote.expression.
|
||||||
|
pos_extra = [
|
||||||
|
p.name
|
||||||
|
for p in params.values()
|
||||||
|
if p.default is inspect.Parameter.empty
|
||||||
|
and p.kind in (inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD)
|
||||||
|
][1:]
|
||||||
|
pos_args = [applied["subject"]]
|
||||||
|
for name in pos_extra:
|
||||||
|
if name not in builder_extra:
|
||||||
|
raise ValueError(
|
||||||
|
f"kind {kind!r} requiere el campo {name!r}; pásalo como kwarg "
|
||||||
|
f"(p.ej. {name}=...)"
|
||||||
|
)
|
||||||
|
pos_args.append(builder_extra[name])
|
||||||
|
|
||||||
|
# Passthrough: cualquier otro kwarg extra del caller que el builder admita
|
||||||
|
# (steps, cfg, rembg_model, width, height, glow, ...). Sobreescribe la coherencia.
|
||||||
|
used = set(pos_extra)
|
||||||
|
for key, val in builder_extra.items():
|
||||||
|
if key in used:
|
||||||
|
continue
|
||||||
|
if key in params:
|
||||||
|
kw[key] = val
|
||||||
|
|
||||||
|
return fn(*pos_args, **kw), godot_kind
|
||||||
|
|
||||||
|
|
||||||
|
def styled_asset_build_only(
|
||||||
|
kind: str,
|
||||||
|
subject: str,
|
||||||
|
style_preset: str,
|
||||||
|
*,
|
||||||
|
seed: int = 0,
|
||||||
|
style_override: str | None = None,
|
||||||
|
negative_extra: str | None = None,
|
||||||
|
**builder_extra,
|
||||||
|
) -> dict:
|
||||||
|
"""Parte PURA del pipeline: resuelve el preset y construye el workflow, sin tocar la red.
|
||||||
|
|
||||||
|
Útil para inspeccionar/testear el grafo y el post-proceso que el pipeline va a aplicar
|
||||||
|
sin generar nada. Es lo que el pipeline hace en sus pasos 1-3.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict {kind, godot_kind, applied, workflow} donde `applied` es el resultado de
|
||||||
|
`comfyui_apply_style_preset` (incluye `post`, la spec de post-proceso) y `workflow`
|
||||||
|
es el dict API-format listo para `comfyui_submit_workflow`.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: si el `kind` o el `style_preset` no existen, o si subject está vacío,
|
||||||
|
o si falta un posicional requerido por el builder.
|
||||||
|
"""
|
||||||
|
preset = comfyui_get_gamedev_style_preset(style_preset) # ValueError si no existe
|
||||||
|
applied = comfyui_apply_style_preset(
|
||||||
|
preset, subject, style=style_override, negative=negative_extra
|
||||||
|
)
|
||||||
|
workflow, godot_kind = _build_styled_workflow(
|
||||||
|
kind, applied, seed=seed, builder_extra=builder_extra
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"kind": kind, "godot_kind": godot_kind, "applied": applied, "workflow": workflow,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def comfyui_generate_styled_asset_oneshot(
|
||||||
|
kind: str,
|
||||||
|
subject: str,
|
||||||
|
style_preset: str,
|
||||||
|
*,
|
||||||
|
seed: int = 0,
|
||||||
|
server: str = "127.0.0.1:8188",
|
||||||
|
out_dir: str | None = None,
|
||||||
|
export_godot: str | None = None,
|
||||||
|
style_override: str | None = None,
|
||||||
|
negative_extra: str | None = None,
|
||||||
|
wait_timeout: float = 600.0,
|
||||||
|
free_vram: bool = False,
|
||||||
|
godot_bin: str | None = None,
|
||||||
|
**builder_extra,
|
||||||
|
) -> dict:
|
||||||
|
"""Genera UN asset aplicándole un estilo CURADO de un solo tiro, con auto-post-proceso.
|
||||||
|
|
||||||
|
Toma un `kind` de asset (uno de `supported_kinds()`), un `subject` (qué dibujar) y el
|
||||||
|
nombre de un `style_preset` del catálogo de `comfyui_get_gamedev_style_preset`
|
||||||
|
("gameboy", "ghibli", "pixel-art-retro", "cyberpunk-neon", "low-poly-flat",
|
||||||
|
"cartoon-cel-shaded", ...), y produce el PNG final ya con el estilo aplicado Y su
|
||||||
|
post-proceso ejecutado. Para los estilos que declaran post (gameboy, pixel-art-retro)
|
||||||
|
el resultado sale ya pixelizado del pipeline — sin llamar a `comfyui_pixelize_image` a
|
||||||
|
mano, que es el hueco que este one-shot cierra.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
kind: tipo de asset; uno de `supported_kinds()` (item_icon, enemy_creature,
|
||||||
|
prop_object, seamless_tile, ui_hud, ...). Un kind desconocido devuelve
|
||||||
|
ok=False con error claro SIN tocar la GPU.
|
||||||
|
subject: qué dibujar ("knight character", "health potion", "stone wall tile").
|
||||||
|
Se combina con el prefijo/sufijo del estilo. Vacío -> error sin red.
|
||||||
|
style_preset: nombre del estilo curado a aplicar. Insensible a mayúsculas y a
|
||||||
|
'_' vs '-'. Un estilo desconocido devuelve ok=False con error claro (y la
|
||||||
|
lista de estilos disponibles) SIN tocar la GPU.
|
||||||
|
seed: semilla del KSampler para reproducibilidad. keyword-only.
|
||||||
|
server: host:port del ComfyUI; se acepta con o sin esquema (http://). keyword-only.
|
||||||
|
out_dir: directorio local donde descargar el PNG (crudo y post-procesado); None =
|
||||||
|
un dir temporal por (estilo, seed). keyword-only.
|
||||||
|
export_godot: ruta de un proyecto Godot 4; si se da, el asset FINAL (ya
|
||||||
|
post-procesado) se exporta a res://assets/...; si hubo pixelize se exporta como
|
||||||
|
`pixelart` (fija el filtro Nearest global). None = no exportar. keyword-only.
|
||||||
|
style_override: sustituye el `style` del preset (override puntual); None usa el del
|
||||||
|
preset. keyword-only.
|
||||||
|
negative_extra: negativo extra del caller; se MERGEA con el del estilo (no lo
|
||||||
|
reemplaza). keyword-only.
|
||||||
|
wait_timeout: segundos máximos esperando el trabajo en ComfyUI. keyword-only.
|
||||||
|
free_vram: si True hace POST /free ANTES de generar para liberar VRAM de tareas
|
||||||
|
previas (útil en 8 GB con modelos ya cargados). keyword-only.
|
||||||
|
godot_bin: binario de Godot para el reimport headless; None autodetecta. keyword-only.
|
||||||
|
**builder_extra: kwargs extra para el builder del `kind` (posicionales requeridos
|
||||||
|
como `expression` en `emote`, o passthrough como `steps`/`cfg`/`width`/`height`).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict con:
|
||||||
|
- ok (bool): True si el asset se generó (y, si el estilo pedía post, el post se
|
||||||
|
aplicó). Un fallo de post o de export se anota en `error` pero el crudo queda
|
||||||
|
en `raw_path`.
|
||||||
|
- kind, subject, style_preset, seed (eco de la entrada; `subject` es el combinado).
|
||||||
|
- prompt_id (str): id del trabajo en ComfyUI.
|
||||||
|
- raw_path (str): PNG descargado SIN post-procesar.
|
||||||
|
- path (str): PNG FINAL entregable (post-procesado si el estilo lo pedía; si no,
|
||||||
|
igual a raw_path).
|
||||||
|
- post (dict): la spec de post del estilo ({} si no pedía).
|
||||||
|
- post_applied (dict|None): resultado del post-proceso si se aplicó
|
||||||
|
({kind, out_path, n_colors_final, size} en éxito; {kind, ok:False, error} si
|
||||||
|
falló); None si el estilo no pedía post.
|
||||||
|
- size (int), transparent (bool): los que el estilo recomendó.
|
||||||
|
- godot_kind (str): bucket Godot usado (sprite/vfx/tileset, o pixelart si hubo post).
|
||||||
|
- exported (dict|None): resultado de export a Godot, o None.
|
||||||
|
- coherence_note (str), error (str).
|
||||||
|
"""
|
||||||
|
server = server.replace("http://", "").replace("https://", "").strip("/")
|
||||||
|
out: dict = {
|
||||||
|
"ok": False, "kind": kind, "subject": subject, "style_preset": style_preset,
|
||||||
|
"seed": seed, "prompt_id": "", "raw_path": "", "path": "", "post": {},
|
||||||
|
"post_applied": None, "size": None, "transparent": None, "godot_kind": "",
|
||||||
|
"exported": None, "coherence_note": "", "error": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 1-3. resolver estilo + construir workflow (PURO; valida kind/estilo sin red) ---
|
||||||
|
try:
|
||||||
|
built = styled_asset_build_only(
|
||||||
|
kind, subject, style_preset, seed=seed,
|
||||||
|
style_override=style_override, negative_extra=negative_extra,
|
||||||
|
**builder_extra,
|
||||||
|
)
|
||||||
|
except ValueError as exc:
|
||||||
|
out["error"] = str(exc)
|
||||||
|
return out
|
||||||
|
|
||||||
|
applied = built["applied"]
|
||||||
|
workflow = built["workflow"]
|
||||||
|
out["subject"] = applied["subject"]
|
||||||
|
out["size"] = applied.get("size")
|
||||||
|
out["transparent"] = applied.get("transparent")
|
||||||
|
out["post"] = applied.get("post", {})
|
||||||
|
out["godot_kind"] = built["godot_kind"]
|
||||||
|
out["coherence_note"] = (
|
||||||
|
f"Estilo {applied['name']!r} aplicado a un {kind!r}: "
|
||||||
|
f"checkpoint={applied['builder_kwargs']['checkpoint']!r}, "
|
||||||
|
f"lora={applied['builder_kwargs']['lora']!r}, "
|
||||||
|
f"post={'sí' if out['post'] else 'no'} (auto-aplicado por el pipeline)."
|
||||||
|
)
|
||||||
|
|
||||||
|
asset_dir = out_dir or os.path.join(
|
||||||
|
tempfile.gettempdir(), f"styled_asset_{applied['name']}_seed{seed}"
|
||||||
|
)
|
||||||
|
os.makedirs(asset_dir, exist_ok=True)
|
||||||
|
|
||||||
|
if free_vram:
|
||||||
|
_free_vram(server)
|
||||||
|
|
||||||
|
# --- 4. encolar + esperar + descargar ---
|
||||||
|
try:
|
||||||
|
sub = comfyui_submit_workflow(workflow, server=server)
|
||||||
|
out["prompt_id"] = sub["prompt_id"]
|
||||||
|
except (RuntimeError, KeyError) as exc:
|
||||||
|
out["error"] = f"submit falló: {exc}"
|
||||||
|
return out
|
||||||
|
|
||||||
|
try:
|
||||||
|
outputs = comfyui_wait_result(out["prompt_id"], server=server, timeout=wait_timeout)
|
||||||
|
except (TimeoutError, RuntimeError) as exc:
|
||||||
|
out["error"] = f"wait falló: {exc}"
|
||||||
|
return out
|
||||||
|
|
||||||
|
img = _first_image(outputs)
|
||||||
|
if img is None:
|
||||||
|
out["error"] = f"el workflow no produjo imágenes (outputs={list(outputs)})"
|
||||||
|
return out
|
||||||
|
|
||||||
|
fetched = comfyui_fetch_output_image(
|
||||||
|
img["filename"], subfolder=img.get("subfolder", ""),
|
||||||
|
type_=img.get("type", "output"), server=server, dest_dir=asset_dir,
|
||||||
|
)
|
||||||
|
if not fetched.get("ok"):
|
||||||
|
out["error"] = f"fetch falló: {fetched.get('error')}"
|
||||||
|
return out
|
||||||
|
out["raw_path"] = fetched["path"]
|
||||||
|
out["path"] = fetched["path"] # por defecto el crudo es el final (estilos sin post)
|
||||||
|
|
||||||
|
# --- 5. AUTO-POST: aplica el post-proceso que el estilo declaró ---
|
||||||
|
# Hoy el único post soportado es `pixelize`; el dispatch es extensible (un estilo
|
||||||
|
# nuevo con otro post añade su rama aquí). Si el estilo no pide post, raw == final.
|
||||||
|
post_ok = True
|
||||||
|
pixelize_spec = out["post"].get("pixelize") if isinstance(out["post"], dict) else None
|
||||||
|
if pixelize_spec:
|
||||||
|
base, ext = os.path.splitext(os.path.basename(out["raw_path"]))
|
||||||
|
dst = os.path.join(asset_dir, f"{base}_{applied['name']}{ext or '.png'}")
|
||||||
|
pr = comfyui_pixelize_image(out["raw_path"], dst, **pixelize_spec)
|
||||||
|
if pr.get("ok"):
|
||||||
|
out["path"] = pr["out_path"]
|
||||||
|
out["post_applied"] = {
|
||||||
|
"kind": "pixelize", "out_path": pr["out_path"],
|
||||||
|
"n_colors_final": pr["n_colors_final"], "size": pr["size"],
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
post_ok = False
|
||||||
|
out["post_applied"] = {"kind": "pixelize", "ok": False, "error": pr.get("error")}
|
||||||
|
out["error"] = f"post pixelize falló (asset crudo OK en raw_path): {pr.get('error')}"
|
||||||
|
|
||||||
|
# --- 6. export opcional a Godot (asset FINAL ya post-procesado) ---
|
||||||
|
if export_godot and out["path"]:
|
||||||
|
# Si se pixelizó, exportar como `pixelart` asegura el filtro Nearest global en Godot.
|
||||||
|
godot_kind = "pixelart" if out["post_applied"] and out["post_applied"].get("out_path") \
|
||||||
|
else out["godot_kind"]
|
||||||
|
exp = comfyui_export_asset_to_godot(
|
||||||
|
out["path"], godot_kind, export_godot, reimport=True, godot_bin=godot_bin,
|
||||||
|
)
|
||||||
|
out["exported"] = exp
|
||||||
|
if not exp.get("ok"):
|
||||||
|
out["error"] = (out["error"] + "; " if out["error"] else "") + \
|
||||||
|
f"export falló (asset generado igual): {exp.get('error')}"
|
||||||
|
|
||||||
|
out["ok"] = bool(out["path"]) and post_ok
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
# Alias con el nombre completo del ID para descubrimiento por convención.
|
||||||
|
generate_styled_asset_oneshot = comfyui_generate_styled_asset_oneshot
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
res = comfyui_generate_styled_asset_oneshot(
|
||||||
|
"item_icon", "health potion", "gameboy", seed=7, out_dir="/tmp/styled_asset_demo",
|
||||||
|
)
|
||||||
|
print(json.dumps(res, indent=2, ensure_ascii=False))
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
"""Tests de comfyui_generate_styled_asset_oneshot (offline; sin ComfyUI ni GPU).
|
||||||
|
|
||||||
|
Cubre el contrato del pipeline sin tocar la red ni la GPU:
|
||||||
|
- Error: kind desconocido / estilo desconocido / subject vacío -> ok=False SIN red.
|
||||||
|
- Golden build: el preset elige checkpoint/lora/negativo y el workflow los lleva; el
|
||||||
|
`style` del preset SUSTITUYE el del builder; transparent/size del preset se aplican.
|
||||||
|
- Golden auto-post: un estilo con post (gameboy) auto-pixeliza el PNG -> `path` final
|
||||||
|
!= `raw_path` y sale ya pixelizado, sin paso manual.
|
||||||
|
- Edge: un estilo SIN post (cyberpunk-neon) deja `path` == `raw_path` (no toca el crudo).
|
||||||
|
- Edge: catálogo ampliado a >=5 estilos; cada estilo nuevo construye un workflow válido.
|
||||||
|
- Error: un fallo de post deja el crudo en raw_path y ok=False con error claro.
|
||||||
|
- Edge: export a Godot usa el asset FINAL y lo manda como `pixelart` si hubo pixelize.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
import pipelines.comfyui_generate_styled_asset_oneshot as smod # noqa: E402
|
||||||
|
from pipelines.comfyui_generate_styled_asset_oneshot import ( # noqa: E402
|
||||||
|
comfyui_generate_styled_asset_oneshot,
|
||||||
|
styled_asset_build_only,
|
||||||
|
)
|
||||||
|
from ml.comfyui_get_gamedev_style_preset import comfyui_get_gamedev_style_preset # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
# --- Error paths que NO tocan la red ---
|
||||||
|
|
||||||
|
def test_unknown_kind_fails_without_network():
|
||||||
|
res = comfyui_generate_styled_asset_oneshot("does_not_exist", "knight", "gameboy")
|
||||||
|
assert res["ok"] is False
|
||||||
|
assert "no soportado" in res["error"]
|
||||||
|
assert res["prompt_id"] == "" # no llegó a encolar
|
||||||
|
|
||||||
|
|
||||||
|
def test_unknown_style_fails_without_network():
|
||||||
|
res = comfyui_generate_styled_asset_oneshot("item_icon", "potion", "does_not_exist")
|
||||||
|
assert res["ok"] is False
|
||||||
|
assert "desconocido" in res["error"]
|
||||||
|
assert res["prompt_id"] == ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_empty_subject_fails_without_network():
|
||||||
|
res = comfyui_generate_styled_asset_oneshot("item_icon", " ", "gameboy")
|
||||||
|
assert res["ok"] is False
|
||||||
|
assert res["prompt_id"] == ""
|
||||||
|
|
||||||
|
|
||||||
|
# --- Golden build puro: el preset dirige el workflow ---
|
||||||
|
|
||||||
|
def test_build_only_preset_drives_workflow():
|
||||||
|
built = styled_asset_build_only("item_icon", "knight character", "gameboy", seed=7)
|
||||||
|
blob = json.dumps(built["workflow"])
|
||||||
|
# checkpoint del preset gameboy (SD1.5)
|
||||||
|
assert "dreamshaper_8.safetensors" in blob
|
||||||
|
# el subject combinó el sufijo del estilo gameboy
|
||||||
|
assert "8-bit" in built["applied"]["subject"]
|
||||||
|
# el style del preset SUSTITUYE el style categórico del builder
|
||||||
|
assert "Game Boy" in blob
|
||||||
|
# gameboy declara post pixelize (lo que el pipeline auto-aplicará)
|
||||||
|
assert built["applied"]["post"].get("pixelize")
|
||||||
|
assert built["godot_kind"] == "sprite"
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_only_lora_preset_injects_lora():
|
||||||
|
# cartoon-cel-shaded usa anime_style_box_sd15 -> el workflow debe inyectar el LoRA
|
||||||
|
built = styled_asset_build_only("enemy_creature", "goblin", "cartoon-cel-shaded", seed=1)
|
||||||
|
blob = json.dumps(built["workflow"])
|
||||||
|
assert "anime_style_box_sd15.safetensors" in blob
|
||||||
|
assert any(n.get("class_type") == "LoraLoader" for n in built["workflow"].values())
|
||||||
|
|
||||||
|
|
||||||
|
# --- Catálogo ampliado: >=5 estilos, cada uno construible ---
|
||||||
|
|
||||||
|
def test_catalog_has_at_least_5_styles():
|
||||||
|
cat = comfyui_get_gamedev_style_preset()
|
||||||
|
assert cat["count"] >= 5
|
||||||
|
for nuevo in ("cyberpunk-neon", "low-poly-flat", "cartoon-cel-shaded"):
|
||||||
|
assert nuevo in cat["names"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_every_style_builds_a_valid_workflow():
|
||||||
|
cat = comfyui_get_gamedev_style_preset()
|
||||||
|
for style in cat["names"]:
|
||||||
|
built = styled_asset_build_only("item_icon", "magic ring", style, seed=3)
|
||||||
|
wf = built["workflow"]
|
||||||
|
classes = {n.get("class_type") for n in wf.values()}
|
||||||
|
assert "KSampler" in classes and "SaveImage" in classes, style
|
||||||
|
|
||||||
|
|
||||||
|
# --- Mock de transporte: submit/wait/fetch sin red ---
|
||||||
|
|
||||||
|
def _mock_transport(monkeypatch, *, raw_name="gen.png"):
|
||||||
|
def fake_submit(workflow, server="127.0.0.1:8188", **kw):
|
||||||
|
assert "://" not in server # server normalizado
|
||||||
|
return {"prompt_id": "pid-1", "client_id": "c"}
|
||||||
|
|
||||||
|
def fake_wait(prompt_id, server="127.0.0.1:8188", timeout=600.0, **kw):
|
||||||
|
return {"9": {"images": [{"filename": raw_name, "subfolder": "", "type": "output"}]}}
|
||||||
|
|
||||||
|
def fake_fetch(filename, *, subfolder="", type_="output",
|
||||||
|
server="127.0.0.1:8188", dest_dir=".", timeout=60.0):
|
||||||
|
os.makedirs(dest_dir, exist_ok=True)
|
||||||
|
path = os.path.join(dest_dir, filename)
|
||||||
|
# PNG real mínimo (8x8 RGB) para que el pixelize de verdad pueda abrirlo.
|
||||||
|
from PIL import Image
|
||||||
|
Image.new("RGB", (64, 64), (123, 80, 200)).save(path)
|
||||||
|
return {"ok": True, "path": path, "size_bytes": 100, "error": ""}
|
||||||
|
|
||||||
|
monkeypatch.setattr(smod, "comfyui_submit_workflow", fake_submit)
|
||||||
|
monkeypatch.setattr(smod, "comfyui_wait_result", fake_wait)
|
||||||
|
monkeypatch.setattr(smod, "comfyui_fetch_output_image", fake_fetch)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Golden auto-post: gameboy pixeliza automáticamente ---
|
||||||
|
|
||||||
|
def test_post_auto_applied_for_gameboy(monkeypatch, tmp_path):
|
||||||
|
_mock_transport(monkeypatch)
|
||||||
|
res = comfyui_generate_styled_asset_oneshot(
|
||||||
|
"item_icon", "health potion", "gameboy", seed=7,
|
||||||
|
server="http://127.0.0.1:8188", out_dir=str(tmp_path),
|
||||||
|
)
|
||||||
|
assert res["ok"] is True, res["error"]
|
||||||
|
assert res["raw_path"] and os.path.isfile(res["raw_path"])
|
||||||
|
# el path FINAL es el post-procesado, distinto del crudo
|
||||||
|
assert res["path"] != res["raw_path"]
|
||||||
|
assert os.path.isfile(res["path"])
|
||||||
|
assert res["post_applied"]["kind"] == "pixelize"
|
||||||
|
# paleta game-boy -> a lo sumo 4 colores en el resultado
|
||||||
|
assert 0 < res["post_applied"]["n_colors_final"] <= 4
|
||||||
|
|
||||||
|
|
||||||
|
# --- Edge: estilo SIN post no toca el crudo ---
|
||||||
|
|
||||||
|
def test_no_post_keeps_raw_for_cyberpunk(monkeypatch, tmp_path):
|
||||||
|
_mock_transport(monkeypatch)
|
||||||
|
res = comfyui_generate_styled_asset_oneshot(
|
||||||
|
"enemy_creature", "street samurai", "cyberpunk-neon", seed=5,
|
||||||
|
out_dir=str(tmp_path),
|
||||||
|
)
|
||||||
|
assert res["ok"] is True, res["error"]
|
||||||
|
assert res["path"] == res["raw_path"] # sin post -> final == crudo
|
||||||
|
assert res["post_applied"] is None
|
||||||
|
assert res["post"] == {}
|
||||||
|
|
||||||
|
|
||||||
|
# --- Error: fallo de post deja el crudo y ok=False ---
|
||||||
|
|
||||||
|
def test_post_failure_keeps_raw_and_flags_error(monkeypatch, tmp_path):
|
||||||
|
_mock_transport(monkeypatch)
|
||||||
|
|
||||||
|
def fake_pixelize(src, dst, **kw):
|
||||||
|
return {"ok": False, "out_path": "", "error": "simulated PIL failure"}
|
||||||
|
|
||||||
|
monkeypatch.setattr(smod, "comfyui_pixelize_image", fake_pixelize)
|
||||||
|
res = comfyui_generate_styled_asset_oneshot(
|
||||||
|
"item_icon", "sword", "gameboy", seed=1, out_dir=str(tmp_path),
|
||||||
|
)
|
||||||
|
assert res["ok"] is False
|
||||||
|
assert res["raw_path"] and os.path.isfile(res["raw_path"]) # crudo preservado
|
||||||
|
assert "post pixelize falló" in res["error"]
|
||||||
|
assert res["post_applied"]["ok"] is False
|
||||||
|
|
||||||
|
|
||||||
|
# --- Edge: export a Godot usa el asset final como pixelart si hubo post ---
|
||||||
|
|
||||||
|
def test_export_godot_uses_pixelart_when_post(monkeypatch, tmp_path):
|
||||||
|
_mock_transport(monkeypatch)
|
||||||
|
captured = {}
|
||||||
|
|
||||||
|
def fake_export(asset_path, kind, godot_project, *, reimport=True,
|
||||||
|
name=None, godot_bin=None):
|
||||||
|
captured["kind"] = kind
|
||||||
|
captured["asset"] = asset_path
|
||||||
|
return {"ok": True, "kind": kind}
|
||||||
|
|
||||||
|
monkeypatch.setattr(smod, "comfyui_export_asset_to_godot", fake_export)
|
||||||
|
res = comfyui_generate_styled_asset_oneshot(
|
||||||
|
"item_icon", "potion", "gameboy", seed=2, out_dir=str(tmp_path),
|
||||||
|
export_godot=str(tmp_path / "godot_proj"),
|
||||||
|
)
|
||||||
|
assert res["ok"] is True, res["error"]
|
||||||
|
assert captured["kind"] == "pixelart" # post -> Nearest filter en Godot
|
||||||
|
assert captured["asset"] == res["path"] # exporta el FINAL post-procesado
|
||||||
|
assert res["exported"]["ok"] is True
|
||||||
Reference in New Issue
Block a user