feat(gamedev): comfyui_build_emote_workflow — emotes/expresiones de personaje
Builder puro (dict API format) del grupo gamedev: genera el workflow de UN emote/expresion facial del mismo personaje (alegre, triste, enfadado, sorprendido, neutral) para sistema de dialogo, retratos reactivos o emotes de chat. La clave es la consistencia del personaje entre expresiones: ref_face encadena IPAdapter-FaceID para que varie solo la expresion y el rostro sea el mismo; facedetailer regenera la cara conservando la expresion. Compone comfyui_build_ipadapter_workflow / comfyui_build_txt2img_workflow + comfyui_inject_lora + comfyui_build_facedetailer_workflow. Hermano de comfyui_build_portrait_avatar/sprite_sheet_workflow. 12 tests offline verdes (golden + edge + error) y 1 generacion real verificada en GPU (8GB lowvram): la expresion happy/smiling se lee claramente. Fila en docs/capabilities/gamedev-2d.md. Evidencia en reports/0151. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
---
|
||||
name: comfyui_build_emote_workflow
|
||||
kind: function
|
||||
lang: py
|
||||
domain: ml
|
||||
version: "1.0.0"
|
||||
purity: pure
|
||||
signature: "def comfyui_build_emote_workflow(character: str, expression: str, *, ref_face: str | None = None, style: str = \"character portrait\", checkpoint: str = \"dreamshaper_8.safetensors\", size: int = 512, facedetailer: bool = True, seed: int = 0, lora: str | None = None, lora_strength: float = 1.0, weight: float = 0.85, negative: str | None = None, steps: int = 28, cfg: float = 7.0, sampler_name: str = \"dpmpp_2m\", scheduler: str = \"karras\", fd_denoise: float = 0.45, bbox_model: str = \"face_yolov8m.pt\", filename_prefix: str = \"emote\") -> dict"
|
||||
description: "Construye el dict (API format) del workflow de UN emote/expresion facial de personaje 2D (alegre, triste, enfadado, sorprendido, neutral...) para sistema de dialogo, retratos reactivos o emotes de chat. La clave es la consistencia del personaje entre expresiones: con ref_face encadena IPAdapter-FaceID para que el rostro sea el mismo y varie SOLO la expresion; con facedetailer regenera la cara con detalle conservando la expresion (Impact-Pack). Compone comfyui_build_ipadapter_workflow / comfyui_build_txt2img_workflow + comfyui_inject_lora + comfyui_build_facedetailer_workflow. Hermano de comfyui_build_portrait_avatar/sprite_sheet_workflow. Pura, sin red ni I/O. class_types verificados contra /object_info (8GB lowvram)."
|
||||
tags: [comfyui, ml, gamedev, gamedev-2d, emote, expression, character, dialogue, faceid, ipadapter, facedetailer, workflow]
|
||||
uses_functions: [comfyui_build_txt2img_workflow_py_ml, comfyui_build_ipadapter_workflow_py_ml, comfyui_inject_lora_py_ml, comfyui_build_facedetailer_workflow_py_ml]
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: ""
|
||||
imports: []
|
||||
params:
|
||||
- name: character
|
||||
desc: "Descripcion del personaje (ej. 'a knight woman with red hair', 'an old wizard with a long white beard'). Se inserta en un prompt scaffold de emote. No puede estar vacio. Manten el MISMO character en todas las expresiones de un set para coherencia de personaje."
|
||||
- name: expression
|
||||
desc: "Expresion facial a representar (ej. 'happy, smiling', 'sad', 'angry', 'surprised', 'neutral'). Es lo unico que deberia cambiar entre los emotes de un mismo personaje. No puede estar vacio."
|
||||
- name: ref_face
|
||||
desc: "Nombre de una imagen de rostro de referencia del personaje en input/ del servidor. Si se pasa, encadena IPAdapter-FaceID para que todos los emotes tengan el MISMO rostro (identidad consistente entre expresiones). None = identidad solo por prompt + seed. keyword-only."
|
||||
- name: style
|
||||
desc: "Descriptor de estilo que mantiene consistentes los emotes de un set (ej. 'character portrait', 'anime portrait', 'realistic RPG portrait'). Pasa el MISMO style + checkpoint + lora a todas las expresiones para coherencia visual. keyword-only."
|
||||
- name: checkpoint
|
||||
desc: "Checkpoint del servidor. 'dreamshaper_8.safetensors' (SD1.5, holgado en 8GB lowvram) por defecto. FaceID solo instalado para SD1.5: con checkpoint SDXL deja ref_face=None. keyword-only."
|
||||
- name: size
|
||||
desc: "Lado del cuadrado en px (width = height = size). 512 SD1.5 por defecto, encuadre tipico de retrato/emote. keyword-only."
|
||||
- name: facedetailer
|
||||
desc: "Si True encadena FaceDetailer (Impact-Pack) para regenerar la cara con detalle (donde se lee la expresion) y descarta el SaveImage base (el unico PNG guardado es el refinado). False deja la imagen tal cual sale del VAEDecode. keyword-only."
|
||||
- name: seed
|
||||
desc: "Semilla del KSampler (y del sampler del FaceDetailer). Misma seed + mismo character/ref_face -> mismo personaje; variar solo expression mantiene la identidad y cambia la cara. keyword-only."
|
||||
- name: lora
|
||||
desc: "LoRA de estilo opcional en models/loras (ej. 'anime_lineart_sd15.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: weight
|
||||
desc: "Peso del IPAdapter-FaceID (solo si ref_face). 0.85 = parecido alto; baja para mas libertad expresiva, sube para mas parecido. keyword-only."
|
||||
- name: negative
|
||||
desc: "Prompt negativo. None usa el negativo por defecto pensado para emotes (una cara bien formada, sin texto/recorte; NO filtra ninguna expresion). keyword-only."
|
||||
- name: steps
|
||||
desc: "Pasos del KSampler. keyword-only."
|
||||
- name: cfg
|
||||
desc: "CFG del KSampler (y del sampler del FaceDetailer). keyword-only."
|
||||
- name: sampler_name
|
||||
desc: "Sampler del KSampler. keyword-only."
|
||||
- name: scheduler
|
||||
desc: "Scheduler del KSampler. keyword-only."
|
||||
- name: fd_denoise
|
||||
desc: "Fuerza de re-difusion de la cara en el FaceDetailer (0.45: refina sin perder la identidad de FaceID ni la expresion). Solo si facedetailer=True. keyword-only."
|
||||
- name: bbox_model
|
||||
desc: "Modelo de deteccion de caras de Ultralytics para el FaceDetailer ('face_yolov8m.pt'). Solo si facedetailer=True. keyword-only."
|
||||
- name: filename_prefix
|
||||
desc: "Prefijo del PNG en output/. keyword-only."
|
||||
output: "dict en API format listo para comfyui_submit_workflow: base txt2img (o IPAdapter-FaceID si ref_face) con prompt scaffold de emote ('portrait of {character}, {expression} expression, emote, {style}, expressive face, ..., clean background') + LoRA de estilo opcional + FaceDetailer (si facedetailer, con SaveImage base descartado y la expresion conservada en el prompt del detailer). UN emote; un set coherente -> llamar por expression con mismo character/style/checkpoint/(lora) y, para el mismo personaje, el mismo ref_face/seed."
|
||||
tested: true
|
||||
tests: ["golden facedetailer: clases CheckpointLoaderSimple/KSampler/VAEDecode/FaceDetailer/UltralyticsDetectorProvider; 'portrait of'+'{expression} expression'+'emote'+'clean background' en prompt; un solo SaveImage = fd_save <- FaceDetailer; FaceDetailer.image <- VAEDecode; sin IPAdapterFaceID si no ref_face", "golden detailer conserva expresion: >=2 CLIPTextEncode con la expresion (base + fd_pos), fd_pos enfoca 'detailed face'", "edge facedetailer=False: sin FaceDetailer, SaveImage base <- VAEDecode", "edge ref_face: IPAdapterUnifiedLoaderFaceID/IPAdapterFaceID presentes, LoadImage con ref_face, weight reflejado, KSampler.model <- rama FaceID (consistencia)", "edge expression en prompt", "edge style en prompt", "edge size: width==height==768 (cuadrado)", "edge lora: LoraLoader con strength", "edge seed reflejado en KSampler", "error character vacio -> ValueError", "error expression vacio -> ValueError", "determinismo"]
|
||||
test_file_path: "python/functions/ml/comfyui_build_emote_workflow_test.py"
|
||||
file_path: "python/functions/ml/comfyui_build_emote_workflow.py"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```python
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
|
||||
from ml.comfyui_build_emote_workflow import comfyui_build_emote_workflow
|
||||
|
||||
# UN emote con cara nitida (FaceDetailer), identidad por prompt + seed.
|
||||
wf = comfyui_build_emote_workflow(
|
||||
"a knight woman with red hair",
|
||||
"happy, smiling",
|
||||
style="realistic RPG portrait",
|
||||
facedetailer=True,
|
||||
seed=7,
|
||||
)
|
||||
# -> comfyui_submit_workflow(wf) -> comfyui_wait_result -> comfyui_fetch_output_image
|
||||
|
||||
# Set de emotes del MISMO personaje: sube una cara de referencia a input/ del
|
||||
# servidor y pasala como ref_face (IPAdapter-FaceID). Mismo character/ref_face/seed,
|
||||
# varia SOLO expression -> mismo rostro, distinta expresion.
|
||||
# for expr in ["happy, smiling", "sad", "angry", "surprised", "neutral"]:
|
||||
# wf = comfyui_build_emote_workflow(
|
||||
# "a knight woman with red hair", expr,
|
||||
# ref_face="hero_face.png", style="realistic RPG portrait", seed=7)
|
||||
# # submit + wait + fetch cada uno -> luego comfyui_build_grid para una hoja.
|
||||
```
|
||||
|
||||
O lanzable directo con: `./fn run comfyui_build_emote_workflow` (imprime nodos + class_types del ejemplo).
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando necesites el set de expresiones/emotes de un personaje para un sistema de
|
||||
dialogo, retratos reactivos (la cara del NPC cambia segun el tono de la
|
||||
conversacion) o emotes de chat: alegre, triste, enfadado, sorprendido, neutral...
|
||||
Es UNA expresion por llamada. Para que sea el MISMO personaje en todas, sube una
|
||||
foto de su cara a `input/` y pasala como `ref_face` -> IPAdapter-FaceID fija el
|
||||
rostro y solo cambia la expresion entre llamadas. Mantén `character` + `style` +
|
||||
`checkpoint` + (`lora`) + `seed` constantes y varia solo `expression`. Un set
|
||||
completo se monta en una hoja de emotes con `comfyui_build_grid` sobre los PNG.
|
||||
Para un busto generico (sin expresion concreta) usa el hermano
|
||||
`comfyui_build_portrait_avatar_workflow`.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **La expresion es lo unico que debe variar**: para coherencia de personaje en
|
||||
un set, cambia SOLO `expression` y deja `character`/`style`/`checkpoint`/`lora`/
|
||||
`seed`/`ref_face` igual. Cambiar otros parametros rompe la identidad entre
|
||||
emotes.
|
||||
- **FaceID solo SD1.5**: `ref_face` requiere los modelos IPAdapter-FaceID, que en
|
||||
este servidor solo estan para SD1.5 (dreamshaper_8). Con un checkpoint SDXL deja
|
||||
`ref_face=None` (identidad por prompt + seed) o usa un checkpoint SD1.5.
|
||||
- **El detailer conserva la expresion**: el prompt del FaceDetailer incluye
|
||||
`{expression} expression` para no perder el emote al refinar la cara. Reutiliza
|
||||
el `CheckpointLoaderSimple` crudo (no la rama IPAdapter); por eso `fd_denoise`
|
||||
es bajo (0.45): refina ojos/boca/cejas preservando la identidad de FaceID. Si la
|
||||
expresion se diluye al refinar, sube `fd_denoise` ligeramente; si se aleja el
|
||||
parecido, bajalo (0.3-0.4) o sube `weight`.
|
||||
- **SaveImage unico con facedetailer**: cuando `facedetailer=True` se descarta el
|
||||
SaveImage base y solo se guarda el PNG del detailer (`fd_save`) — no hay doble
|
||||
guardado. Con `facedetailer=False` el SaveImage base toma del VAEDecode.
|
||||
- **Requiere custom nodes instalados**: IPAdapter_plus (cubiq) para FaceID e
|
||||
Impact-Pack para FaceDetailer/UltralyticsDetectorProvider. Si un nodo falta, el
|
||||
submit fallara; consulta `/object_info` del servidor para confirmar.
|
||||
- **El negativo por defecto no filtra expresiones**: 'neutral', 'sad', etc. son
|
||||
expresiones validas, por eso el negativo solo cubre defectos de cara/anatomia,
|
||||
no estados de animo. Si pasas un `negative` propio, no incluyas terminos que
|
||||
contradigan la `expression` pedida.
|
||||
- **8GB lowvram**: `size=512` con dreamshaper_8 va holgado; SDXL pide mas VRAM y
|
||||
resolucion mayor. Si hay OOM, baja `size` o desactiva `facedetailer`.
|
||||
- Es una funcion **pura**: solo arma el dict. La generacion real (GPU) la hacen
|
||||
`comfyui_submit_workflow` + `comfyui_wait_result` + `comfyui_fetch_output_image`.
|
||||
@@ -0,0 +1,272 @@
|
||||
"""Construye el workflow ComfyUI de UN emote/expresion facial de personaje (API format).
|
||||
|
||||
Emotes/expresiones del MISMO personaje (alegre, triste, enfadado, sorprendido,
|
||||
neutral...) para un sistema de dialogo, retratos reactivos o emotes de chat. La
|
||||
clave es la consistencia del personaje entre expresiones: el rostro debe ser el
|
||||
mismo y variar SOLO la expresion. Es el builder hermano de
|
||||
comfyui_build_portrait_avatar_workflow / comfyui_build_sprite_sheet_workflow:
|
||||
mismo patron (PURO, dict API format) que compone funciones existentes del
|
||||
registry, no reescribe el grafo.
|
||||
|
||||
Cableado segun los argumentos:
|
||||
|
||||
[IPAdapter-FaceID si ref_face] -> mismo rostro entre emotes
|
||||
CheckpointLoaderSimple -> [LoraLoader si lora] -> KSampler
|
||||
-> CLIPTextEncode (scaffold de emote) ...
|
||||
-> VAEDecode -> SaveImage
|
||||
-> [FaceDetailer si facedetailer] -> SaveImage final
|
||||
|
||||
Compone:
|
||||
- comfyui_build_ipadapter_workflow (mode='faceid') -> identidad de rostro
|
||||
consistente desde una imagen de referencia del personaje (o
|
||||
comfyui_build_txt2img_workflow si no hay ref_face)
|
||||
- comfyui_inject_lora -> LoRA de estilo opcional
|
||||
- comfyui_build_facedetailer_workflow (modo dict) -> cara nitida (regenera la
|
||||
cara detectada con un sampler de difusion, el 'pain #1' de los retratos)
|
||||
|
||||
Por que IPAdapter-FaceID y no solo prompt: para un set de emotes coherente el
|
||||
MISMO personaje tiene que aparecer en todas las expresiones; FaceID extrae el
|
||||
embedding de la cara de `ref_face` y lo impone, de modo que cambia la expresion
|
||||
pero no la identidad (report 0137 / report 0148). Sin `ref_face`, la identidad la
|
||||
dan solo el prompt + la seed (estable entre llamadas con la misma seed y el mismo
|
||||
character, pero no anclada a una cara concreta de referencia).
|
||||
|
||||
Por que FaceDetailer al final: el primer render pierde detalle en la cara a 512px
|
||||
(ojos, boca, cejas) y la expresion es precisamente donde se lee el emote;
|
||||
FaceDetailer detecta la cara (YOLO) y la regenera ampliada conservando la
|
||||
expresion. Cuando se activa, se descarta el SaveImage base para que el unico PNG
|
||||
guardado sea el de la cara ya refinada.
|
||||
|
||||
Una expresion por llamada. Un set de emotes del mismo personaje se obtiene
|
||||
llamando N veces (mismo character/ref_face/seed/style/checkpoint, cambiando solo
|
||||
`expression`) y montando los PNG resultantes con comfyui_build_grid en una hoja.
|
||||
|
||||
class_types/inputs verificados contra /object_info del servidor (8GB lowvram) a
|
||||
traves de los builders que compone (IPAdapterUnifiedLoaderFaceID/IPAdapterFaceID,
|
||||
LoraLoader, UltralyticsDetectorProvider/FaceDetailer de Impact-Pack).
|
||||
|
||||
Funcion pura: sin red, sin I/O. No muta dicts de entrada (los builders/inyectores
|
||||
que compone trabajan sobre copias). Determinista para los mismos argumentos.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
# Negativo por defecto pensado para emotes: una sola cara bien formada, sin
|
||||
# texto/marcas ni recortes que ensucien el busto centrado. No filtra ninguna
|
||||
# expresion concreta (neutral, triste, etc. son expresiones validas).
|
||||
_EMOTE_NEGATIVE = (
|
||||
"blurry, lowres, deformed face, disfigured, bad anatomy, extra limbs, "
|
||||
"extra fingers, mutated hands, ugly, text, watermark, signature, "
|
||||
"cropped, out of frame, multiple faces, two heads, jpeg artifacts"
|
||||
)
|
||||
|
||||
|
||||
def comfyui_build_emote_workflow(
|
||||
character: str,
|
||||
expression: str,
|
||||
*,
|
||||
ref_face: str | None = None,
|
||||
style: str = "character portrait",
|
||||
checkpoint: str = "dreamshaper_8.safetensors",
|
||||
size: int = 512,
|
||||
facedetailer: bool = True,
|
||||
seed: int = 0,
|
||||
lora: str | None = None,
|
||||
lora_strength: float = 1.0,
|
||||
weight: float = 0.85,
|
||||
negative: str | None = None,
|
||||
steps: int = 28,
|
||||
cfg: float = 7.0,
|
||||
sampler_name: str = "dpmpp_2m",
|
||||
scheduler: str = "karras",
|
||||
fd_denoise: float = 0.45,
|
||||
bbox_model: str = "face_yolov8m.pt",
|
||||
filename_prefix: str = "emote",
|
||||
) -> dict:
|
||||
"""Construye el dict (API format) del workflow de un emote/expresion de personaje.
|
||||
|
||||
Args:
|
||||
character: descripcion del personaje (ej. "a knight woman with red hair",
|
||||
"an old wizard with a long white beard"). Se inserta en un prompt
|
||||
scaffold de emote. No puede estar vacio. Manten el MISMO character en
|
||||
todas las expresiones de un set para coherencia de personaje.
|
||||
expression: expresion facial a representar (ej. "happy, smiling", "sad",
|
||||
"angry", "surprised", "neutral"). Es lo unico que deberia cambiar
|
||||
entre los emotes de un mismo personaje. No puede estar vacio.
|
||||
ref_face: nombre de una imagen de rostro de referencia del personaje en el
|
||||
directorio input/ del servidor ComfyUI. Si se pasa, encadena
|
||||
IPAdapter-FaceID para que todos los emotes tengan el MISMO rostro
|
||||
(identidad consistente entre expresiones). None = identidad solo por
|
||||
prompt + seed. keyword-only.
|
||||
style: descriptor de estilo que mantiene consistentes los emotes de un set
|
||||
(ej. "character portrait", "anime portrait", "realistic RPG
|
||||
portrait"). Pasa el MISMO style + checkpoint + (lora) a todas las
|
||||
expresiones para coherencia visual. keyword-only.
|
||||
checkpoint: checkpoint del servidor. 'dreamshaper_8.safetensors' (SD1.5,
|
||||
holgado en 8GB lowvram) por defecto. FaceID solo esta instalado para
|
||||
SD1.5: si usas un checkpoint SDXL, deja ref_face=None. keyword-only.
|
||||
size: lado del cuadrado en px (width = height = size). 512 SD1.5 por
|
||||
defecto, encuadre tipico de retrato/emote. keyword-only.
|
||||
facedetailer: si True encadena FaceDetailer (Impact-Pack) para regenerar la
|
||||
cara con detalle (donde se lee la expresion) y descarta el SaveImage
|
||||
base (el unico PNG guardado es el refinado). False deja la imagen tal
|
||||
cual sale del VAEDecode. keyword-only.
|
||||
seed: semilla del KSampler (y del sampler del FaceDetailer). Misma seed +
|
||||
mismo character/ref_face -> mismo personaje; variar solo expression
|
||||
mantiene la identidad y cambia la cara. keyword-only.
|
||||
lora: LoRA de estilo opcional en models/loras (ej.
|
||||
'anime_lineart_sd15.safetensors'). None = sin LoRA. keyword-only.
|
||||
lora_strength: fuerza del LoRA sobre model y clip. Se clampa a [0.0, 2.0].
|
||||
keyword-only.
|
||||
weight: peso del IPAdapter-FaceID (solo si ref_face). 0.85 = parecido alto;
|
||||
baja para mas libertad expresiva, sube para mas parecido. keyword-only.
|
||||
negative: prompt negativo. None usa el negativo por defecto pensado para
|
||||
emotes (una cara, bien formada, sin texto/recorte; no filtra ninguna
|
||||
expresion). keyword-only.
|
||||
steps: pasos del KSampler. keyword-only.
|
||||
cfg: CFG del KSampler (y del sampler del FaceDetailer). keyword-only.
|
||||
sampler_name: sampler del KSampler. keyword-only.
|
||||
scheduler: scheduler del KSampler. keyword-only.
|
||||
fd_denoise: fuerza de re-difusion de la cara en el FaceDetailer (0.45 por
|
||||
defecto: refina sin perder la identidad de FaceID ni la expresion).
|
||||
Solo se usa si facedetailer=True. keyword-only.
|
||||
bbox_model: modelo de deteccion de caras de Ultralytics para el
|
||||
FaceDetailer ('face_yolov8m.pt'). Solo si facedetailer=True.
|
||||
keyword-only.
|
||||
filename_prefix: prefijo del PNG generado en output/. keyword-only.
|
||||
|
||||
Returns:
|
||||
dict en API format listo para comfyui_submit_workflow: base txt2img (o
|
||||
IPAdapter-FaceID si ref_face) con prompt scaffold de emote + LoRA de estilo
|
||||
opcional + FaceDetailer (si facedetailer). Es UN emote; un set coherente
|
||||
-> llamar por expression con el mismo character/style/checkpoint/(lora) y,
|
||||
para el mismo personaje, el mismo ref_face/seed.
|
||||
|
||||
Raises:
|
||||
ValueError: si character o expression estan vacios, o si la base/inyectores
|
||||
no encuentran los nodos donde enganchar (propagado por los builders que
|
||||
compone).
|
||||
"""
|
||||
from ml.comfyui_build_txt2img_workflow import comfyui_build_txt2img_workflow
|
||||
|
||||
if not character or not character.strip():
|
||||
raise ValueError(
|
||||
"comfyui_build_emote_workflow: 'character' no puede estar vacio"
|
||||
)
|
||||
if not expression or not expression.strip():
|
||||
raise ValueError(
|
||||
"comfyui_build_emote_workflow: 'expression' no puede estar vacio"
|
||||
)
|
||||
|
||||
character = character.strip()
|
||||
expression = expression.strip()
|
||||
lora_strength = max(0.0, min(2.0, float(lora_strength)))
|
||||
neg = _EMOTE_NEGATIVE if negative is None else negative
|
||||
|
||||
# Prompt scaffold de emote: retrato del personaje con la expresion marcada,
|
||||
# encuadre de emote (cara expresiva mirando al espectador) y fondo limpio para
|
||||
# recorte/uso en UI de dialogo.
|
||||
positive = (
|
||||
f"portrait of {character}, {expression} expression, emote, "
|
||||
f"{style}, expressive face, head and shoulders, looking at viewer, "
|
||||
"clean background, high detail"
|
||||
)
|
||||
|
||||
if ref_face:
|
||||
from ml.comfyui_build_ipadapter_workflow import comfyui_build_ipadapter_workflow
|
||||
|
||||
wf = comfyui_build_ipadapter_workflow(
|
||||
positive,
|
||||
ref_face,
|
||||
base_checkpoint=checkpoint,
|
||||
mode="faceid",
|
||||
weight=weight,
|
||||
negative=neg,
|
||||
steps=steps,
|
||||
cfg=cfg,
|
||||
width=size,
|
||||
height=size,
|
||||
seed=seed,
|
||||
sampler_name=sampler_name,
|
||||
scheduler=scheduler,
|
||||
filename_prefix=filename_prefix,
|
||||
)
|
||||
else:
|
||||
wf = comfyui_build_txt2img_workflow(
|
||||
checkpoint,
|
||||
positive,
|
||||
neg,
|
||||
steps=steps,
|
||||
cfg=cfg,
|
||||
width=size,
|
||||
height=size,
|
||||
seed=seed,
|
||||
sampler_name=sampler_name,
|
||||
scheduler=scheduler,
|
||||
filename_prefix=filename_prefix,
|
||||
)
|
||||
|
||||
if lora:
|
||||
from ml.comfyui_inject_lora import comfyui_inject_lora
|
||||
|
||||
wf = comfyui_inject_lora(
|
||||
wf, lora, strength_model=lora_strength, strength_clip=lora_strength
|
||||
)
|
||||
|
||||
if facedetailer:
|
||||
from ml.comfyui_build_facedetailer_workflow import (
|
||||
comfyui_build_facedetailer_workflow,
|
||||
)
|
||||
|
||||
# Descarta el SaveImage base: el FaceDetailer toma la imagen del VAEDecode y
|
||||
# anhade su propio SaveImage (fd_save). Asi el unico PNG guardado es el de la
|
||||
# cara ya refinada (sin doble guardado). dict-comprehension = copia, no muta.
|
||||
wf = {k: v for k, v in wf.items() if v.get("class_type") != "SaveImage"}
|
||||
|
||||
# Prompt del detailer enfocado en la cara CONSERVANDO la expresion: la
|
||||
# expresion es lo que distingue un emote, no debe perderse al refinar.
|
||||
fd_positive = (
|
||||
f"{character}, {expression} expression, {style}, "
|
||||
"detailed face, sharp eyes, expressive"
|
||||
)
|
||||
wf = comfyui_build_facedetailer_workflow(
|
||||
wf,
|
||||
checkpoint,
|
||||
fd_positive,
|
||||
neg,
|
||||
bbox_model=bbox_model,
|
||||
denoise=fd_denoise,
|
||||
steps=steps,
|
||||
cfg=cfg,
|
||||
seed=seed,
|
||||
sampler_name=sampler_name,
|
||||
scheduler=scheduler,
|
||||
filename_prefix=filename_prefix,
|
||||
)
|
||||
|
||||
return wf
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import json
|
||||
|
||||
wf = comfyui_build_emote_workflow(
|
||||
"a knight woman with red hair",
|
||||
"happy, smiling",
|
||||
style="realistic RPG portrait",
|
||||
facedetailer=True,
|
||||
seed=7,
|
||||
)
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"nodes": list(wf),
|
||||
"classes": sorted({n["class_type"] for n in wf.values()}),
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,188 @@
|
||||
"""Tests offline de comfyui_build_emote_workflow (estructura del dict, sin GPU)."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
from ml.comfyui_build_emote_workflow import ( # noqa: E402
|
||||
comfyui_build_emote_workflow,
|
||||
)
|
||||
|
||||
|
||||
def _classes(wf):
|
||||
return sorted({n["class_type"] for n in wf.values()})
|
||||
|
||||
|
||||
def _by_class(wf, cls):
|
||||
return [n for n in wf.values() if n["class_type"] == cls]
|
||||
|
||||
|
||||
def _positive_node(wf, needle):
|
||||
return next(
|
||||
n
|
||||
for n in wf.values()
|
||||
if n["class_type"] == "CLIPTextEncode" and needle in n["inputs"]["text"]
|
||||
)
|
||||
|
||||
|
||||
def test_golden_recipe_facedetailer():
|
||||
wf = comfyui_build_emote_workflow(
|
||||
"a knight woman with red hair", "happy, smiling", seed=7
|
||||
)
|
||||
cls = _classes(wf)
|
||||
# Cadena base txt2img + FaceDetailer (default facedetailer=True).
|
||||
assert "CheckpointLoaderSimple" in cls
|
||||
assert "KSampler" in cls
|
||||
assert "VAEDecode" in cls
|
||||
assert "FaceDetailer" in cls
|
||||
assert "UltralyticsDetectorProvider" in cls
|
||||
# Sin ref_face -> no hay rama IPAdapter-FaceID.
|
||||
assert "IPAdapterFaceID" not in cls
|
||||
# El personaje y la expresion aparecen en el prompt con el scaffold de emote.
|
||||
pos = _positive_node(wf, "a knight woman with red hair")
|
||||
assert "portrait of" in pos["inputs"]["text"]
|
||||
assert "happy, smiling expression" in pos["inputs"]["text"]
|
||||
assert "emote" in pos["inputs"]["text"]
|
||||
assert "clean background" in pos["inputs"]["text"]
|
||||
# FaceDetailer activado -> el unico SaveImage es el del detailer (fd_save).
|
||||
saves = _by_class(wf, "SaveImage")
|
||||
assert len(saves) == 1
|
||||
fd_face_id = next(nid for nid, n in wf.items() if n["class_type"] == "FaceDetailer")
|
||||
assert saves[0]["inputs"]["images"] == [fd_face_id, 0]
|
||||
# El FaceDetailer toma la imagen del VAEDecode del base.
|
||||
vd_id = next(nid for nid, n in wf.items() if n["class_type"] == "VAEDecode")
|
||||
fd = _by_class(wf, "FaceDetailer")[0]
|
||||
assert fd["inputs"]["image"] == [vd_id, 0]
|
||||
|
||||
|
||||
def test_golden_facedetailer_keeps_expression():
|
||||
wf = comfyui_build_emote_workflow(
|
||||
"a knight woman with red hair", "happy, smiling", seed=7
|
||||
)
|
||||
# Hay al menos dos nodos positivos que mencionan la expresion: base + detailer.
|
||||
matches = [
|
||||
n
|
||||
for n in wf.values()
|
||||
if n["class_type"] == "CLIPTextEncode"
|
||||
and "happy, smiling expression" in n["inputs"]["text"]
|
||||
]
|
||||
assert len(matches) >= 2
|
||||
# El CLIPTextEncode del detailer (fd_pos) conserva la expresion y enfoca la cara.
|
||||
fd_pos = _positive_node(wf, "detailed face")
|
||||
assert "happy, smiling expression" in fd_pos["inputs"]["text"]
|
||||
|
||||
|
||||
def test_edge_no_facedetailer_keeps_base_save():
|
||||
wf = comfyui_build_emote_workflow("an old wizard", "angry", facedetailer=False)
|
||||
cls = _classes(wf)
|
||||
assert "FaceDetailer" not in cls
|
||||
# SaveImage base presente y toma del VAEDecode.
|
||||
saves = _by_class(wf, "SaveImage")
|
||||
assert len(saves) == 1
|
||||
vd_id = next(nid for nid, n in wf.items() if n["class_type"] == "VAEDecode")
|
||||
assert saves[0]["inputs"]["images"] == [vd_id, 0]
|
||||
|
||||
|
||||
def test_edge_ref_face_activates_faceid():
|
||||
wf = comfyui_build_emote_workflow(
|
||||
"a knight woman with red hair",
|
||||
"surprised",
|
||||
ref_face="hero_face.png",
|
||||
weight=0.9,
|
||||
facedetailer=False,
|
||||
)
|
||||
cls = _classes(wf)
|
||||
assert "IPAdapterUnifiedLoaderFaceID" in cls
|
||||
assert "IPAdapterFaceID" in cls
|
||||
# La imagen de referencia del personaje se carga con LoadImage.
|
||||
loads = _by_class(wf, "LoadImage")
|
||||
assert any(n["inputs"]["image"] == "hero_face.png" for n in loads)
|
||||
# weight reflejado en el nodo FaceID.
|
||||
faceid = _by_class(wf, "IPAdapterFaceID")[0]
|
||||
assert faceid["inputs"]["weight"] == 0.9
|
||||
# El KSampler consume el MODEL condicionado por la rama IPAdapter (consistencia).
|
||||
ks = _by_class(wf, "KSampler")[0]
|
||||
apply_id = next(nid for nid, n in wf.items() if n["class_type"] == "IPAdapterFaceID")
|
||||
assert ks["inputs"]["model"] == [apply_id, 0]
|
||||
|
||||
|
||||
def test_edge_expression_reflected_in_prompt():
|
||||
wf = comfyui_build_emote_workflow(
|
||||
"a cheerful bard", "sad, crying", facedetailer=False
|
||||
)
|
||||
pos = _positive_node(wf, "a cheerful bard")
|
||||
assert "sad, crying expression" in pos["inputs"]["text"]
|
||||
|
||||
|
||||
def test_edge_style_reflected_in_prompt():
|
||||
wf = comfyui_build_emote_workflow(
|
||||
"a cheerful bard",
|
||||
"neutral",
|
||||
style="anime portrait, cel shaded",
|
||||
facedetailer=False,
|
||||
)
|
||||
pos = _positive_node(wf, "a cheerful bard")
|
||||
assert "anime portrait, cel shaded" in pos["inputs"]["text"]
|
||||
|
||||
|
||||
def test_edge_size_reflected_square():
|
||||
wf = comfyui_build_emote_workflow(
|
||||
"a stoic paladin", "angry", size=768, facedetailer=False
|
||||
)
|
||||
latent = _by_class(wf, "EmptyLatentImage")[0]["inputs"]
|
||||
assert latent["width"] == 768
|
||||
assert latent["height"] == 768 # cuadrado (retrato/emote)
|
||||
|
||||
|
||||
def test_edge_lora_reflected():
|
||||
wf = comfyui_build_emote_workflow(
|
||||
"a dark sorceress",
|
||||
"smug",
|
||||
lora="anime_lineart_sd15.safetensors",
|
||||
lora_strength=0.8,
|
||||
facedetailer=False,
|
||||
)
|
||||
loras = _by_class(wf, "LoraLoader")
|
||||
assert len(loras) == 1
|
||||
assert loras[0]["inputs"]["lora_name"] == "anime_lineart_sd15.safetensors"
|
||||
assert loras[0]["inputs"]["strength_model"] == 0.8
|
||||
|
||||
|
||||
def test_edge_seed_reflected():
|
||||
wf = comfyui_build_emote_workflow(
|
||||
"a grizzled mercenary", "neutral", seed=123, facedetailer=False
|
||||
)
|
||||
ks = _by_class(wf, "KSampler")[0]
|
||||
assert ks["inputs"]["seed"] == 123
|
||||
|
||||
|
||||
def test_error_empty_character():
|
||||
try:
|
||||
comfyui_build_emote_workflow(" ", "happy")
|
||||
assert False
|
||||
except ValueError as e:
|
||||
assert "character" in str(e)
|
||||
|
||||
|
||||
def test_error_empty_expression():
|
||||
try:
|
||||
comfyui_build_emote_workflow("a knight", " ")
|
||||
assert False
|
||||
except ValueError as e:
|
||||
assert "expression" in str(e)
|
||||
|
||||
|
||||
def test_determinism():
|
||||
a = comfyui_build_emote_workflow(
|
||||
"a knight woman with red hair",
|
||||
"happy, smiling",
|
||||
lora="anime_lineart_sd15.safetensors",
|
||||
seed=7,
|
||||
)
|
||||
b = comfyui_build_emote_workflow(
|
||||
"a knight woman with red hair",
|
||||
"happy, smiling",
|
||||
lora="anime_lineart_sd15.safetensors",
|
||||
seed=7,
|
||||
)
|
||||
assert a == b
|
||||
Reference in New Issue
Block a user