---
name: comfyui_build_asset_variant_workflow
kind: function
lang: py
domain: ml
purity: pure
version: 1.0.0
signature: "def comfyui_build_asset_variant_workflow(input_image: str, variant: str, *, checkpoint: str = \"IMG_dreamshaper_8.safetensors\", denoise: float = 0.5, style: str = \"game asset\", size: int | None = 512, seed: int = 0, lora: str | None = None, lora_strength: float = 1.0, upscale_method: str = \"lanczos\", crop: str = \"disabled\", negative: str | None = None, steps: int = 28, cfg: float = 7.0, sampler_name: str = \"dpmpp_2m\", scheduler: str = \"karras\", filename_prefix: str = \"asset_variant\") -> dict"
description: "Construye el dict (API format) del workflow de una VARIANTE img2img de un asset 2D ya generado: parte de una IMAGEN existente (un sprite de enemigo, un icono...) y produce una version coherente que cambia material/paleta/tier/estado (ice element, fire element, battle-damaged, golden tier 2, corrupted) manteniendo la composicion, la pose y la silueta del original. A diferencia de los builders gamedev hermanos (enemy_creature, item_icon...), que parten de TEXTO (txt2img desde ruido), este parte de una imagen via img2img con denoise MEDIO (~0.45-0.6): el KSampler arranca del latente de la imagen base, no de ruido. Normaliza el tamano con un ImageScale opcional (size) o preserva las dimensiones del original (size=None). Compone comfyui_build_img2img_workflow + comfyui_inject_lora (estilo opcional). Pura, sin red ni I/O. class_types verificados contra /object_info (8GB lowvram)."
tags: [comfyui, ml, gamedev-2d, img2img, variant, asset-transform, stable-diffusion, workflow]
uses_functions: [comfyui_build_img2img_workflow_py_ml, comfyui_inject_lora_py_ml]
uses_types: []
returns: []
returns_optional: false
error_type: ""
params:
  - name: input_image
    desc: "Nombre del archivo de la imagen base dentro de la carpeta input/ del servidor ComfyUI (un asset YA generado). Lo carga el nodo LoadImage. Subelo antes con POST /upload/image o copialo a ~/ComfyUI/input/. No puede estar vacio."
  - name: variant
    desc: "Descripcion de la variante a producir (ej. 'ice element, frozen', 'fire element, molten', 'battle-damaged, cracked', 'golden tier 2', 'corrupted shadow'). Reescribe material/paleta/estado del asset manteniendo su composicion. No describe el sujeto desde cero: transforma el que ya existe en input_image. No puede estar vacio."
  - name: checkpoint
    desc: "Checkpoint del servidor. 'IMG_dreamshaper_8.safetensors' (SD1.5, holgado en 8GB lowvram) por defecto. keyword-only."
  - name: denoise
    desc: "Fuerza de denoising del KSampler (cuanto se aparta del original). ~0.3 apenas cambia; 0.45-0.6 (recomendado) cambia material/paleta conservando silueta/pose; ~0.8 se aleja y empieza a ser casi txt2img. Se clampa a [0.0, 1.0]. keyword-only."
  - name: style
    desc: "Descriptor de estilo que mantiene coherentes las variantes de un set (ej. 'game asset', 'dark fantasy creature', 'pixel art'). Mismo style + checkpoint + (lora) en todas las variantes del mismo asset. keyword-only."
  - name: size
    desc: "Lado en px al que se NORMALIZA la imagen base antes de encodearla (inserta un ImageScale a size x size). None = no escala; la variante hereda las dimensiones EXACTAS del original (preserva proporcion sin deformar). 512 por defecto (SD1.5). keyword-only."
  - name: seed
    desc: "Semilla del KSampler. keyword-only."
  - name: lora
    desc: "LoRA de estilo opcional en models/loras (ej. 'SD15_dark_fantasy.safetensors'). None = sin LoRA. keyword-only."
  - name: lora_strength
    desc: "Fuerza del LoRA sobre model y clip. Se clampa a [0.0, 2.0]. keyword-only."
  - name: upscale_method
    desc: "Metodo del ImageScale ('lanczos', 'bilinear', 'bicubic', 'area', 'nearest-exact'). Solo se usa si size no es None. keyword-only."
  - name: crop
    desc: "Modo de recorte del ImageScale ('disabled' conserva todo el contenido, 'center' recorta al centro para encajar el ratio). Solo si size no es None. keyword-only."
  - name: negative
    desc: "Prompt negativo. None usa el negativo por defecto pensado para variantes (conservar pose/composicion, una figura, fondo limpio). keyword-only."
  - name: steps
    desc: "Pasos de sampling del KSampler. keyword-only."
  - name: cfg
    desc: "Classifier-free guidance scale. keyword-only."
  - name: sampler_name
    desc: "Nombre del sampler (ej. 'dpmpp_2m', 'euler'). keyword-only."
  - name: scheduler
    desc: "Scheduler del sampler (ej. 'karras', 'normal'). keyword-only."
  - name: filename_prefix
    desc: "Prefijo del archivo de salida en SaveImage. keyword-only."
output: "dict en API format listo para comfyui_submit_workflow: img2img base (parte de input_image) con prompt de variante + ImageScale opcional (normaliza a size) + LoRA opcional. Nodos: CheckpointLoaderSimple '4', LoadImage '10', VAEEncode '11', CLIPTextEncode '6'/'7', KSampler '3' (denoise medio), VAEDecode '8', SaveImage '9', + ImageScale y LoraLoader si aplican."
tested: true
tests: ["estructura img2img (LoadImage+VAEEncode, sin EmptyLatentImage)", "input_image/prompt reflejados en LoadImage y CLIPTextEncode positivo", "size por defecto inserta ImageScale a 512; size=None lo omite", "denoise se clampa a [0,1]", "filename_prefix/seed/lora opcional reflejados", "input_image o variant vacios -> ValueError", "determinismo: misma entrada -> mismo dict"]
test_file_path: "python/functions/ml/tests/test_comfyui_build_asset_variant_workflow.py"
file_path: python/functions/ml/comfyui_build_asset_variant_workflow.py
---

Construye el dict (API format) del workflow de una **variante de un asset 2D que ya
existe** (img2img). Builder gamedev hermano de `comfyui_build_enemy_creature_workflow`
e `comfyui_build_item_icon_workflow`, pero con un eje distinto: en vez de generar un
TIPO de asset desde texto, **transforma** una imagen concreta (un sprite ya generado)
en una variante coherente — la version "de hielo", "de fuego", "dañada" o "tier 2
dorada" — conservando silueta, pose y composición del original.

## Ejemplo

```python
import sys, os
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
from ml.comfyui_build_asset_variant_workflow import comfyui_build_asset_variant_workflow

# Variante "de hielo" de un sprite de goblin ya generado (subido al input/ del server)
wf = comfyui_build_asset_variant_workflow(
    "enemy_creature_00001_.png",          # asset existente en el input/ de ComfyUI
    "ice element, frozen",                # la variante a producir
    style="dark fantasy creature, game asset",
    denoise=0.5,                          # medio: cambia material/paleta, conserva silueta
    seed=7,
)
# wf parte de una imagen (img2img), NO de ruido:
#   "VAEEncode" in {n["class_type"] for n in wf.values()}      # True
#   "EmptyLatentImage" not in {n["class_type"] for n in wf.values()}  # True (no es txt2img)
#   wf["10"]["inputs"]["image"]    == "enemy_creature_00001_.png"
#   wf["3"]["inputs"]["denoise"]   == 0.5
#   "ice element, frozen" in wf["6"]["inputs"]["text"]
```

El bloque se lanza con el python del venv (`python/.venv/bin/python3`). Nota: `./fn
run` directo no aplica a este builder porque su firma usa `*` (keyword-only) y el
generador de runner de `fn run` no lo soporta — igual que `comfyui_build_img2img_workflow`.
Usa el import de arriba o un heredoc.

Set de variantes del MISMO asset (mismo `input_image`/`style`/`seed`, distinto `variant`):

```python
for v in ["ice element, frozen", "fire element, molten", "battle-damaged, cracked", "golden tier 2"]:
    wf = comfyui_build_asset_variant_workflow("enemy_creature_00001_.png", v,
                                              style="dark fantasy creature, game asset",
                                              denoise=0.5, seed=7)
    # enviar con comfyui_submit_workflow -> familia coherente de variantes
```

Para enviar a la GPU: subir la base con `POST /upload/image`, luego
`comfyui_submit_workflow(wf)` + `comfyui_wait_result(prompt_id)` +
`comfyui_fetch_output_image(filename)`.

## Cuando usarla

Cuando ya tienes un asset 2D generado y quieres **derivar variantes coherentes** de
él (elemento/material/tier/estado) sin redibujar desde cero: el sprite de hielo del
mismo enemigo, la armadura dorada del mismo personaje, la versión dañada del mismo
prop. Es img2img con denoise medio que conserva la composición original. Para generar
un asset NUEVO desde texto usa los builders txt2img hermanos
(`comfyui_build_enemy_creature_workflow`, `comfyui_build_item_icon_workflow`...); para
ampliar/refinar resolución usa `comfyui_build_upscale_workflow`; para img2img genérico
sin scaffolding de variante usa `comfyui_build_img2img_workflow` directo.

## Gotchas

- Es **img2img**, no txt2img: SIEMPRE parte de una imagen (`input_image`), no de ruido
  en blanco. Esa imagen debe existir en la carpeta `input/` del servidor ComfyUI
  (subir con `POST /upload/image` o copiar a `~/ComfyUI/input/`). Es pura: NO valida
  que exista; si no está, ComfyUI rechaza el workflow con HTTP 400 al enviarlo. Valida
  antes con `comfyui_validate_workflow`.
- `denoise` es la palanca clave: cerca de 0.0 apenas cambia (variante invisible);
  0.45-0.6 es el rango útil (cambia material/paleta manteniendo silueta); cerca de 0.8
  se aleja del original y deriva la pose/composición (deja de ser variante coherente y
  se acerca a un txt2img). Default 0.5.
- `size` reescala la imagen base a `size x size` con un ImageScale ANTES de encodear.
  Con `size=512` y un asset cuadrado 512 es no-op de tamaño; con un asset NO cuadrado y
  `crop="disabled"` el ImageScale fuerza el ratio cuadrado y puede deformar — pasa
  `size=None` para preservar las dimensiones/proporción exactas del original, o
  `crop="center"` para recortar al centro en vez de deformar.
- El prompt refuerza "same composition, same pose, same silhouette" además del denoise
  medio; aun así, denoise alto o un `variant` que implique cambio de forma (ej. "giant
  version") puede alterar la silueta. Para variantes solo de paleta/material, mantén
  denoise ≤0.55.
- Asume checkpoint con VAE embebido (VAEEncode/VAEDecode usan el VAE del checkpoint).
  Para un VAE externo hay que reconectar esas entradas a mano.
- 8GB lowvram: SD1.5 a 512² va holgado. Si OOM, baja `size` (384) o `denoise`; NO subas
  a SDXL en 8GB para esto.
