feat(gamedev): comfyui_build_normal_map_workflow — normal/depth map de sprite para 2.5D
Builder puro (dict API format) que genera el normal map (o depth/height) de un sprite existente para iluminacion dinamica 2.5D (Godot normal_map, Unity sprite normal). Pipeline LoadImage -> preprocesador controlnet_aux -> SaveImage, ~0 VRAM. method selecciona el nodo (verificados contra /object_info): - normal (default): BAE-NormalMapPreprocessor, normal canonico azul/violeta usable directo en motor. - normal_midas: MiDaS, unico con control de intensidad (strength -> a). - normal_dsine: DSINE. depth: DepthAnythingV2 (height en gris). Nodos de normal NATIVOS, sin necesidad de depth->Sobel post (gap innecesario). 11 tests offline verdes. Probado e2e en GPU (8GB): normal map de un icono de pocion, prompt_id d47f9943, tono azul/violeta verificado. Report 0150. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -38,6 +38,7 @@ VFX (ver `reports/0143`).
|
|||||||
| `comfyui_build_item_icon_workflow_py_ml` | `(item, *, style="game icon, clean, centered", checkpoint="dreamshaper_8…", size=512, transparent=True, lora=None, …) -> dict` | UN icono de item de inventario (espada/poción/anillo/libro/escudo): txt2img cuadrado + prompt scaffold de icono + LoRA estilo opcional + Rembg (alpha). Set coherente = mismo style/checkpoint/lora por item. SD1.5. |
|
| `comfyui_build_item_icon_workflow_py_ml` | `(item, *, style="game icon, clean, centered", checkpoint="dreamshaper_8…", size=512, transparent=True, lora=None, …) -> dict` | UN icono de item de inventario (espada/poción/anillo/libro/escudo): txt2img cuadrado + prompt scaffold de icono + LoRA estilo opcional + Rembg (alpha). Set coherente = mismo style/checkpoint/lora por item. SD1.5. |
|
||||||
| `comfyui_build_portrait_avatar_workflow_py_ml` | `(character, *, style="character portrait", ref_face=None, checkpoint="dreamshaper_8…", size=512, facedetailer=True, lora=None, …) -> dict` | UN retrato/avatar de personaje (busto centrado, cara al espectador, fondo simple): txt2img + prompt scaffold de retrato + FaceDetailer (cara nítida) + LoRA estilo opcional; `ref_face` → IPAdapter-FaceID para rostro consistente entre retratos. Diálogo/perfil/selección. SD1.5. |
|
| `comfyui_build_portrait_avatar_workflow_py_ml` | `(character, *, style="character portrait", ref_face=None, checkpoint="dreamshaper_8…", size=512, facedetailer=True, lora=None, …) -> dict` | UN retrato/avatar de personaje (busto centrado, cara al espectador, fondo simple): txt2img + prompt scaffold de retrato + FaceDetailer (cara nítida) + LoRA estilo opcional; `ref_face` → IPAdapter-FaceID para rostro consistente entre retratos. Diálogo/perfil/selección. SD1.5. |
|
||||||
| `comfyui_build_parallax_background_workflow_py_ml` | `(scene, *, style="game background, side-scroller…", layers=3, checkpoint="dreamshaper_8…", depth_node="DepthAnythingV2Preprocessor", width=1024, height=512, …) -> dict` | Fondo en capas para parallax 2.5D: genera el fondo apaisado (txt2img) + su depth map (`DepthAnythingV2Preprocessor` sobre el VAEDecode), dos SaveImage. El split en N bandas por profundidad es post (GAP: `split_parallax_layers`, aún no creada). Probado e2e en GPU (`reports/0149`). SD1.5. |
|
| `comfyui_build_parallax_background_workflow_py_ml` | `(scene, *, style="game background, side-scroller…", layers=3, checkpoint="dreamshaper_8…", depth_node="DepthAnythingV2Preprocessor", width=1024, height=512, …) -> dict` | Fondo en capas para parallax 2.5D: genera el fondo apaisado (txt2img) + su depth map (`DepthAnythingV2Preprocessor` sobre el VAEDecode), dos SaveImage. El split en N bandas por profundidad es post (GAP: `split_parallax_layers`, aún no creada). Probado e2e en GPU (`reports/0149`). SD1.5. |
|
||||||
|
| `comfyui_build_normal_map_workflow_py_ml` | `(image, *, method="normal", strength=1.0, resolution=512, bg_threshold=0.1, filename_prefix="normal_map") -> dict` | Normal/depth map de un sprite existente para iluminación dinámica 2.5D (Godot CanvasItem `normal_map`, Unity sprite normal). `LoadImage → preprocesador controlnet_aux → SaveImage`. `method`: `normal` (default, `BAE-NormalMapPreprocessor`, normal canónico **azul/violeta** usable directo en motor), `normal_midas` (MiDaS, único con `strength`→`a`, paleta no canónica), `normal_dsine` (DSINE), `depth` (`DepthAnythingV2`, height en gris). `image` debe estar en `input/` de ComfyUI. Coste VRAM ≈0. Probado e2e en GPU (`reports/0150`). |
|
||||||
|
|
||||||
## Funciones de post-proceso y puente (`gamedev`, CPU)
|
## Funciones de post-proceso y puente (`gamedev`, CPU)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
---
|
||||||
|
name: comfyui_build_normal_map_workflow
|
||||||
|
kind: function
|
||||||
|
lang: py
|
||||||
|
domain: ml
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: pure
|
||||||
|
signature: "def comfyui_build_normal_map_workflow(image: str, *, method: str = \"normal\", strength: float = 1.0, resolution: int = 512, bg_threshold: float = 0.1, filename_prefix: str = \"normal_map\") -> dict # method='normal' -> BAE (canonico)"
|
||||||
|
description: "Construye el dict (API format) de un workflow ComfyUI que genera el NORMAL MAP (o DEPTH/HEIGHT) de un sprite para iluminacion dinamica 2.5D (Godot CanvasItem normal_map, Unity sprite normal). Pipeline LoadImage -> preprocesador controlnet_aux -> SaveImage. method elige el nodo: 'normal' (BAE, default, normal canonico azul/violeta), 'normal_midas' (MiDaS, con strength), 'normal_dsine' (DSINE), 'depth' (DepthAnythingV2 height). Nodos de normal NATIVOS, sin necesidad de depth->Sobel post. Pura, sin red ni I/O. class_types verificados contra /object_info."
|
||||||
|
tags: [comfyui, ml, gamedev, gamedev-2d, normal-map, depth-map, lighting, sprite, workflow, controlnet-aux]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: []
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: ""
|
||||||
|
imports: []
|
||||||
|
params:
|
||||||
|
- name: image
|
||||||
|
desc: "Nombre del archivo de entrada dentro del directorio input/ de ComfyUI (lo que consume LoadImage). No vacio. Si el sprite vive en output/, copialo a input/ antes de enviar (I/O del runner)."
|
||||||
|
- name: method
|
||||||
|
desc: "Preprocesador: 'normal' (default, BAE-NormalMapPreprocessor, normal canonico azul/violeta usable en motor), 'normal_midas' (MiDaS, con strength, paleta no canonica), 'normal_dsine' (DSINE), 'depth' (DepthAnythingV2, height map en gris). keyword-only."
|
||||||
|
- name: strength
|
||||||
|
desc: "Intensidad del relieve. Solo efectivo con method='normal_midas' (MiDaS): a = clamp(strength*2pi, 0, 5pi); strength=1.0 -> a=2pi. Debe ser >= 0. Para los demas metodos se ignora. keyword-only."
|
||||||
|
- name: resolution
|
||||||
|
desc: "Lado en px al que el preprocesador re-escala antes de inferir (64..16384). Conviene matchear el lado del sprite. keyword-only."
|
||||||
|
- name: bg_threshold
|
||||||
|
desc: "Umbral de fondo de MiDaS (0..1). Solo aplica a method='normal_midas'. keyword-only."
|
||||||
|
- name: filename_prefix
|
||||||
|
desc: "Prefijo del PNG resultante en output/. keyword-only."
|
||||||
|
output: "dict en API format con 3 nodos (LoadImage -> preprocesador normal/depth -> SaveImage), listo para comfyui_submit_workflow."
|
||||||
|
tested: true
|
||||||
|
tests: ["golden: 3 nodos LoadImage->BAE-NormalMapPreprocessor->SaveImage, image reflejado, sin a", "edge image reflejado en LoadImage", "edge strength escala a linealmente y satura en 5pi (method=normal_midas), default BAE sin a", "edge method='depth' selecciona DepthAnythingV2Preprocessor sin a", "edge method midas/dsine seleccionan su nodo", "edge resolution/filename_prefix reflejados", "error image vacio / method invalido / strength negativo / resolution fuera de rango -> ValueError", "determinismo"]
|
||||||
|
test_file_path: "python/functions/ml/comfyui_build_normal_map_workflow_test.py"
|
||||||
|
file_path: "python/functions/ml/comfyui_build_normal_map_workflow.py"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```python
|
||||||
|
import sys, os
|
||||||
|
sys.path.insert(0, os.path.join("python", "functions"))
|
||||||
|
from ml.comfyui_build_normal_map_workflow import comfyui_build_normal_map_workflow
|
||||||
|
|
||||||
|
# Normal map CANONICO azul/violeta (BAE) de un sprite ya copiado a input/ de ComfyUI.
|
||||||
|
wf = comfyui_build_normal_map_workflow(
|
||||||
|
"item_icon_potion_00001_.png",
|
||||||
|
method="normal", # BAE: normal tangent-space usable directo en Godot/Unity
|
||||||
|
resolution=512,
|
||||||
|
filename_prefix="potion_normal",
|
||||||
|
)
|
||||||
|
# wf -> comfyui_submit_workflow(wf) ; luego esperar y leer el PNG de output/.
|
||||||
|
|
||||||
|
# Variante con control de intensidad de relieve (MiDaS, paleta no canonica):
|
||||||
|
wf_midas = comfyui_build_normal_map_workflow("sprite.png", method="normal_midas", strength=1.5)
|
||||||
|
|
||||||
|
# Variante depth/height (escala de grises) para parallax/height:
|
||||||
|
wf_depth = comfyui_build_normal_map_workflow("bg_layer.png", method="depth")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cuando usarla
|
||||||
|
|
||||||
|
- Cuando un sprite/imagen 2D debe reaccionar a **luces dinamicas** del motor (2.5D
|
||||||
|
lighting): generas su normal map y lo enchufas en `Godot Sprite2D.material` (normal_map)
|
||||||
|
o como secondary texture "Normal map" del sprite en Unity.
|
||||||
|
- Cuando necesitas un **height/depth map** de un fondo o tile para parallax o
|
||||||
|
desplazamiento (usa `method="depth"`).
|
||||||
|
- Builder hermano de los `comfyui_build_*_workflow` del grupo `gamedev-2d`: produce el
|
||||||
|
dict y delega el envio a `comfyui_submit_workflow`.
|
||||||
|
|
||||||
|
## Gotchas
|
||||||
|
|
||||||
|
- **`image` vive en input/, no en output/.** LoadImage solo ve el directorio `input/` de
|
||||||
|
ComfyUI. Para usar un sprite generado (que esta en `output/`), copialo a `input/` antes
|
||||||
|
de enviar el workflow. Esta funcion es pura: no copia nada.
|
||||||
|
- **`strength` solo afecta a `method="normal_midas"` (MiDaS).** El default BAE, DSINE y
|
||||||
|
DepthAnything no exponen intensidad — con esos metodos `strength` se ignora (documentado,
|
||||||
|
no es bug).
|
||||||
|
- **MiDaS (`normal_midas`) NO da el azul/violeta canonico**: su paleta domina verde/rosa
|
||||||
|
(otra convencion de color). Para un normal map usable directo en motor usa el default
|
||||||
|
`method="normal"` (BAE), que da el azul/violeta tangent-space (~(128,128,255) de fondo).
|
||||||
|
Usa MiDaS solo cuando necesites variar el relieve via `strength`.
|
||||||
|
- **`method="depth"` NO produce un normal map**, sino un depth/height en escala de grises.
|
||||||
|
- **Primera ejecucion descarga pesos** del preprocesador (MiDaS/BAE/DSINE/DepthAnything)
|
||||||
|
via controlnet_aux si no estan en cache. Requiere red la 1a vez; despues es local.
|
||||||
|
- **No se necesita depth->Sobel->normal post.** Hay nodos de normal NATIVOS en el server,
|
||||||
|
asi que una `comfyui_depth_to_normal` auxiliar es innecesaria hoy (solo se justificaria
|
||||||
|
para derivar el normal de un depth de mas calidad — gap anotado, no creado).
|
||||||
|
- Coste VRAM ~0 (no hay difusion): apto para el server 8GB lowvram.
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
"""Construye un workflow ComfyUI de NORMAL MAP (o DEPTH/HEIGHT) de un sprite, en API format.
|
||||||
|
|
||||||
|
Para iluminacion dinamica 2.5D: un sprite plano reacciona a luces del motor si se le
|
||||||
|
acompana de su mapa de normales (Godot CanvasItem `normal_map`, Unity Sprite secondary
|
||||||
|
texture "Normal map"). El normal map se ve azul/violeta (XY en R/G, Z en B) — el tono
|
||||||
|
plano dominante es ~(128,128,255).
|
||||||
|
|
||||||
|
Pipeline minimo (sin difusion, ~0 VRAM extra):
|
||||||
|
|
||||||
|
LoadImage ─IMAGE─► <preprocesador normal/depth> ─IMAGE─► SaveImage
|
||||||
|
|
||||||
|
El preprocesador lo elige `method`, y todos son nodos del pack controlnet_aux
|
||||||
|
(ya instalado). class_types verificados EXACTOS contra /object_info del servidor:
|
||||||
|
|
||||||
|
- method="normal" -> 'BAE-NormalMapPreprocessor' inputs: image, resolution
|
||||||
|
DEFAULT. Produce el normal map CANONICO azul/violeta (fondo plano ~(128,128,255),
|
||||||
|
convencion tangent-space que Godot/Unity consumen directamente). Sin parametro de
|
||||||
|
intensidad -> `strength` se ignora. Es el resultado usable out-of-the-box.
|
||||||
|
- method="normal_midas" -> 'MiDaS-NormalMapPreprocessor'
|
||||||
|
inputs: image(IMAGE), a(FLOAT, default 2*pi), bg_threshold(FLOAT), resolution(INT)
|
||||||
|
UNICO normal-preprocessor con control de intensidad nativo (`a`), por eso es el que
|
||||||
|
mapea `strength` (a = clamp(strength*2*pi, 0, 5*pi)). OJO: su paleta de color NO es
|
||||||
|
el azul/violeta canonico (domina verde/rosa); usalo solo si necesitas variar el
|
||||||
|
relieve via strength, no como normal map directo de motor.
|
||||||
|
- method="normal_dsine" -> 'DSINE-NormalMapPreprocessor' inputs: image, fov, iterations, resolution
|
||||||
|
Normal alternativo (DSINE); `strength` se ignora (usa fov/iterations por defecto).
|
||||||
|
- method="depth" -> 'DepthAnythingV2Preprocessor' inputs: image, ckpt_name, resolution
|
||||||
|
Produce un DEPTH/HEIGHT map (escala de grises), NO un normal. Util para
|
||||||
|
parallax/height; `strength` se ignora (DepthAnything no tiene intensidad ajustable).
|
||||||
|
|
||||||
|
Nota de diseno: el default es "normal" (BAE, no "depth") porque el objetivo de la funcion
|
||||||
|
es producir el normal map azul/violeta usable directamente. Hay nodos de normal NATIVOS en
|
||||||
|
el servidor, asi que NO se necesita el camino depth -> Sobel -> normal como post (esa
|
||||||
|
funcion auxiliar `comfyui_depth_to_normal` queda como posible futura solo si se quisiera
|
||||||
|
derivar el normal de un depth de mayor calidad; hoy es innecesaria).
|
||||||
|
|
||||||
|
`image` es el nombre del archivo dentro del directorio `input/` de ComfyUI (lo que espera
|
||||||
|
LoadImage). Para usar un sprite que ya esta en `output/`, hay que copiarlo a `input/`
|
||||||
|
antes de enviar el workflow (eso es I/O del runner, no de esta funcion pura).
|
||||||
|
|
||||||
|
Funcion pura: sin red, sin I/O, sin mutar entradas. Devuelve un dict nuevo.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
_TWO_PI = 6.283185307179586
|
||||||
|
_MAX_A = 15.707963267948966 # 5*pi, el max que admite MiDaS-NormalMapPreprocessor.a
|
||||||
|
|
||||||
|
# method -> class_type EXACTO del nodo preprocesador (verificado contra /object_info).
|
||||||
|
_METHODS = {
|
||||||
|
"normal": "BAE-NormalMapPreprocessor",
|
||||||
|
"normal_midas": "MiDaS-NormalMapPreprocessor",
|
||||||
|
"normal_dsine": "DSINE-NormalMapPreprocessor",
|
||||||
|
"depth": "DepthAnythingV2Preprocessor",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def comfyui_build_normal_map_workflow(
|
||||||
|
image: str,
|
||||||
|
*,
|
||||||
|
method: str = "normal",
|
||||||
|
strength: float = 1.0,
|
||||||
|
resolution: int = 512,
|
||||||
|
bg_threshold: float = 0.1,
|
||||||
|
filename_prefix: str = "normal_map",
|
||||||
|
) -> dict:
|
||||||
|
"""Construye el dict (API format) de un workflow de normal/depth map de un sprite.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
image: nombre del archivo de entrada dentro del directorio `input/` de ComfyUI
|
||||||
|
(lo que consume LoadImage). No puede estar vacio. Si el sprite vive en
|
||||||
|
`output/`, copialo antes a `input/` (I/O del runner).
|
||||||
|
method: preprocesador a usar. keyword-only.
|
||||||
|
'normal' (default, BAE, normal map canonico azul/violeta usable en motor),
|
||||||
|
'normal_midas' (MiDaS, con `strength`, paleta no canonica), 'normal_dsine'
|
||||||
|
(DSINE), 'depth' (DepthAnythingV2, height map en gris).
|
||||||
|
strength: intensidad del relieve del normal map. Solo tiene efecto con
|
||||||
|
method='normal_midas' (MiDaS): se mapea a `a` = clamp(strength*2*pi, 0, 5*pi);
|
||||||
|
strength=1.0 -> a=2*pi (relieve estandar). Debe ser >= 0. Para los demas
|
||||||
|
metodos se ignora (el nodo no expone intensidad). keyword-only.
|
||||||
|
resolution: lado en px al que el preprocesador re-escala antes de inferir
|
||||||
|
(64..16384). Conviene matchear el lado del sprite. keyword-only.
|
||||||
|
bg_threshold: umbral de fondo de MiDaS (0..1). Solo aplica a method='normal_midas'.
|
||||||
|
keyword-only.
|
||||||
|
filename_prefix: prefijo del PNG resultante en output/. keyword-only.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict en API format con 3 nodos (LoadImage -> preprocesador -> SaveImage), listo
|
||||||
|
para comfyui_submit_workflow.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: si image esta vacio, method no es valido, strength < 0, o
|
||||||
|
resolution esta fuera de 64..16384.
|
||||||
|
"""
|
||||||
|
if not image or not image.strip():
|
||||||
|
raise ValueError("comfyui_build_normal_map_workflow: 'image' no puede estar vacio")
|
||||||
|
if method not in _METHODS:
|
||||||
|
raise ValueError(
|
||||||
|
f"comfyui_build_normal_map_workflow: method debe ser uno de "
|
||||||
|
f"{tuple(_METHODS)}, no {method!r}"
|
||||||
|
)
|
||||||
|
if strength < 0:
|
||||||
|
raise ValueError(
|
||||||
|
f"comfyui_build_normal_map_workflow: strength debe ser >= 0, no {strength!r}"
|
||||||
|
)
|
||||||
|
if not (64 <= resolution <= 16384):
|
||||||
|
raise ValueError(
|
||||||
|
f"comfyui_build_normal_map_workflow: resolution debe estar en 64..16384, "
|
||||||
|
f"no {resolution!r}"
|
||||||
|
)
|
||||||
|
|
||||||
|
class_type = _METHODS[method]
|
||||||
|
|
||||||
|
preproc_inputs = {"image": ["1", 0], "resolution": resolution}
|
||||||
|
if method == "normal_midas":
|
||||||
|
# MiDaS expone `a` (intensidad) y `bg_threshold`; ahi se refleja `strength`.
|
||||||
|
preproc_inputs["a"] = min(max(strength * _TWO_PI, 0.0), _MAX_A)
|
||||||
|
preproc_inputs["bg_threshold"] = bg_threshold
|
||||||
|
|
||||||
|
return {
|
||||||
|
"1": {"class_type": "LoadImage", "inputs": {"image": image}},
|
||||||
|
"2": {"class_type": class_type, "inputs": preproc_inputs},
|
||||||
|
"3": {
|
||||||
|
"class_type": "SaveImage",
|
||||||
|
"inputs": {"images": ["2", 0], "filename_prefix": filename_prefix},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import json
|
||||||
|
|
||||||
|
wf = comfyui_build_normal_map_workflow(
|
||||||
|
"item_icon_potion_00001_.png", method="normal"
|
||||||
|
)
|
||||||
|
print(json.dumps(wf, indent=2))
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
"""Tests offline de comfyui_build_normal_map_workflow (estructura del dict, sin GPU)."""
|
||||||
|
|
||||||
|
import math
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
from ml.comfyui_build_normal_map_workflow import ( # noqa: E402
|
||||||
|
comfyui_build_normal_map_workflow,
|
||||||
|
)
|
||||||
|
|
||||||
|
_TWO_PI = 6.283185307179586
|
||||||
|
|
||||||
|
|
||||||
|
def _by_class(wf, cls):
|
||||||
|
return [n for n in wf.values() if n["class_type"] == cls]
|
||||||
|
|
||||||
|
|
||||||
|
def test_golden_normal_default():
|
||||||
|
wf = comfyui_build_normal_map_workflow("sprite.png")
|
||||||
|
# 3 nodos: LoadImage -> BAE-NormalMapPreprocessor (canonico) -> SaveImage.
|
||||||
|
assert len(wf) == 3
|
||||||
|
load = _by_class(wf, "LoadImage")
|
||||||
|
assert len(load) == 1
|
||||||
|
assert load[0]["inputs"]["image"] == "sprite.png"
|
||||||
|
pre = _by_class(wf, "BAE-NormalMapPreprocessor")
|
||||||
|
assert len(pre) == 1
|
||||||
|
# El preprocesador consume el IMAGE del LoadImage.
|
||||||
|
assert pre[0]["inputs"]["image"] == ["1", 0]
|
||||||
|
assert pre[0]["inputs"]["resolution"] == 512
|
||||||
|
# BAE no expone intensidad: no hay `a`.
|
||||||
|
assert "a" not in pre[0]["inputs"]
|
||||||
|
save = _by_class(wf, "SaveImage")
|
||||||
|
assert len(save) == 1
|
||||||
|
assert save[0]["inputs"]["images"] == ["2", 0]
|
||||||
|
assert save[0]["inputs"]["filename_prefix"] == "normal_map"
|
||||||
|
|
||||||
|
|
||||||
|
def test_edge_image_reflected():
|
||||||
|
wf = comfyui_build_normal_map_workflow("portrait_knight_woman_00001_.png")
|
||||||
|
assert _by_class(wf, "LoadImage")[0]["inputs"]["image"] == "portrait_knight_woman_00001_.png"
|
||||||
|
|
||||||
|
|
||||||
|
def test_edge_strength_reflected_in_a():
|
||||||
|
# strength escala `a` linealmente (a = strength * 2*pi) con method='normal_midas'.
|
||||||
|
wf = comfyui_build_normal_map_workflow("s.png", method="normal_midas", strength=0.5)
|
||||||
|
a = _by_class(wf, "MiDaS-NormalMapPreprocessor")[0]["inputs"]["a"]
|
||||||
|
assert math.isclose(a, 0.5 * _TWO_PI)
|
||||||
|
# strength alto satura en 5*pi (max del nodo).
|
||||||
|
wf2 = comfyui_build_normal_map_workflow("s.png", method="normal_midas", strength=10.0)
|
||||||
|
a2 = _by_class(wf2, "MiDaS-NormalMapPreprocessor")[0]["inputs"]["a"]
|
||||||
|
assert math.isclose(a2, 15.707963267948966)
|
||||||
|
# El default (BAE) no usa strength: sin `a`.
|
||||||
|
wf3 = comfyui_build_normal_map_workflow("s.png", strength=3.0)
|
||||||
|
assert "a" not in _by_class(wf3, "BAE-NormalMapPreprocessor")[0]["inputs"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_edge_method_depth_selects_depthanything():
|
||||||
|
wf = comfyui_build_normal_map_workflow("s.png", method="depth")
|
||||||
|
assert len(_by_class(wf, "DepthAnythingV2Preprocessor")) == 1
|
||||||
|
assert len(_by_class(wf, "MiDaS-NormalMapPreprocessor")) == 0
|
||||||
|
# depth no usa `a`/bg_threshold (no expone intensidad).
|
||||||
|
pre = _by_class(wf, "DepthAnythingV2Preprocessor")[0]
|
||||||
|
assert "a" not in pre["inputs"]
|
||||||
|
assert pre["inputs"]["resolution"] == 512
|
||||||
|
|
||||||
|
|
||||||
|
def test_edge_method_midas_and_dsine():
|
||||||
|
wf_midas = comfyui_build_normal_map_workflow("s.png", method="normal_midas")
|
||||||
|
midas = _by_class(wf_midas, "MiDaS-NormalMapPreprocessor")
|
||||||
|
assert len(midas) == 1
|
||||||
|
assert "a" in midas[0]["inputs"] and "bg_threshold" in midas[0]["inputs"]
|
||||||
|
wf_ds = comfyui_build_normal_map_workflow("s.png", method="normal_dsine")
|
||||||
|
assert len(_by_class(wf_ds, "DSINE-NormalMapPreprocessor")) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_edge_resolution_and_prefix():
|
||||||
|
wf = comfyui_build_normal_map_workflow(
|
||||||
|
"s.png", resolution=1024, filename_prefix="potion_normal"
|
||||||
|
)
|
||||||
|
assert _by_class(wf, "BAE-NormalMapPreprocessor")[0]["inputs"]["resolution"] == 1024
|
||||||
|
assert _by_class(wf, "SaveImage")[0]["inputs"]["filename_prefix"] == "potion_normal"
|
||||||
|
|
||||||
|
|
||||||
|
def test_error_empty_image():
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
comfyui_build_normal_map_workflow("")
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
comfyui_build_normal_map_workflow(" ")
|
||||||
|
|
||||||
|
|
||||||
|
def test_error_bad_method():
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
comfyui_build_normal_map_workflow("s.png", method="bogus")
|
||||||
|
|
||||||
|
|
||||||
|
def test_error_negative_strength():
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
comfyui_build_normal_map_workflow("s.png", strength=-1.0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_error_resolution_out_of_range():
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
comfyui_build_normal_map_workflow("s.png", resolution=32)
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
comfyui_build_normal_map_workflow("s.png", resolution=20000)
|
||||||
|
|
||||||
|
|
||||||
|
def test_determinism():
|
||||||
|
a = comfyui_build_normal_map_workflow("s.png", strength=0.7, resolution=768)
|
||||||
|
b = comfyui_build_normal_map_workflow("s.png", strength=0.7, resolution=768)
|
||||||
|
assert a == b
|
||||||
Reference in New Issue
Block a user