10dbc510b7
Mueve el indicador de arquitectura del SUFIJO al PREFIJO del nombre de cada LoRA para que el dropdown del LoraLoader muestre de inmediato que LoRA casa con que checkpoint (evita el shape mismatch SD1.5 vs SDXL que crashea ComfyUI). - 20 LoRAs renombradas en disco (15 SD15/SDXL en /mnt/2tb, 5 FLUX en ~/ComfyUI), mapa de reversion en ~/ComfyUI/models/loras/_rename_map.json. - Refs actualizadas en builders gamedev-2d, style presets, pipelines, tests y docs/capabilities. Defaults hardcodeados (pixel-art, lcm-lora, etc.) apuntan a los nombres con prefijo. - Ejemplos genericos en docstrings normalizados a la convencion de prefijo. - comfyui_replicate_civitai_oneshot::_norm ignora el token de arquitectura al comparar, robusto al reordenado (sufijo civitai vs prefijo instalado). Refs a repos HuggingFace (nerijs/pixel-art-xl) y checkpoints (juggernaut_xl_v11) preservados. Verificado: dropdown LoraLoader con prefijos + generacion real pixel-art OK + tests comfyui verdes (481 ml + 26 pipelines). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
204 lines
8.3 KiB
Python
204 lines
8.3 KiB
Python
"""comfyui_compose_capabilities — mezclador de capacidades sobre un workflow base.
|
|
|
|
Toma un workflow ComfyUI en API format (la base: salida de
|
|
comfyui_build_skill_workflow o comfyui_build_txt2img_workflow) y aplica EN ORDEN
|
|
las capacidades que se activen, componiendo los inyectores/builders ENCADENABLES
|
|
del registry. Cada capacidad es un argumento keyword opcional: None (default) =
|
|
desactivada. Asi el mismo dict base se mezcla a la carta y se puede ir mejorando
|
|
(activar/desactivar una capacidad cambia el grafo resultante).
|
|
|
|
Orden de aplicacion (de mas cerca del checkpoint a la salida):
|
|
|
|
1. loras -> comfyui_inject_multi_lora (cadena MODEL/CLIP)
|
|
2. controlnet -> comfyui_inject_controlnet (re-condiciona KSampler.positive)
|
|
3. ipadapter -> comfyui_inject_ipadapter (re-condiciona KSampler.model, tras loras)
|
|
4. facedetailer -> comfyui_build_facedetailer_workflow (regenera caras del VAEDecode)
|
|
5. hires -> comfyui_inject_hires_fix (UltimateSDUpscale tras el VAEDecode)
|
|
|
|
Cada capacidad es independiente: se puede activar cualquier subconjunto. Sin
|
|
ninguna activada devuelve una copia del base intacta.
|
|
|
|
Funcion PURA: sin red, sin I/O. No muta el dict de entrada (copia profunda). Solo
|
|
compone funciones puras del registry.
|
|
|
|
Limitacion conocida (piezas actuales): hires y facedetailer NO encadenan entre
|
|
si. Ambos toman su imagen del VAEDecode original del render; combinarlos deja a
|
|
uno de los dos sin efecto sobre la salida final. Usa uno U otro por workflow, o
|
|
encadenalos manualmente fuera del mixer. Ver el .md (## Gotchas).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import copy
|
|
import os
|
|
import sys
|
|
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
|
|
|
from ml.comfyui_build_facedetailer_workflow import comfyui_build_facedetailer_workflow # noqa: E402
|
|
from ml.comfyui_inject_controlnet import comfyui_inject_controlnet # noqa: E402
|
|
from ml.comfyui_inject_hires_fix import comfyui_inject_hires_fix # noqa: E402
|
|
from ml.comfyui_inject_ipadapter import comfyui_inject_ipadapter # noqa: E402
|
|
from ml.comfyui_inject_multi_lora import comfyui_inject_multi_lora # noqa: E402
|
|
|
|
|
|
def _is_link(v) -> bool:
|
|
"""True si v es una conexion ComfyUI [node_id(str), output_index(int)]."""
|
|
return (
|
|
isinstance(v, list)
|
|
and len(v) == 2
|
|
and isinstance(v[0], str)
|
|
and isinstance(v[1], int)
|
|
)
|
|
|
|
|
|
def _detect_checkpoint(wf: dict) -> str:
|
|
"""Nombre del checkpoint del primer CheckpointLoaderSimple, o '' si no hay."""
|
|
for node in wf.values():
|
|
if node.get("class_type") == "CheckpointLoaderSimple":
|
|
return str(node.get("inputs", {}).get("ckpt_name", "")) or ""
|
|
return ""
|
|
|
|
|
|
def _detect_prompts(wf: dict) -> tuple[str, str]:
|
|
"""Texto (positivo, negativo) de los dos primeros CLIPTextEncode del workflow.
|
|
|
|
En los builders del registry el positivo se inserta antes que el negativo, asi
|
|
que el primer CLIPTextEncode es el positivo y el segundo el negativo.
|
|
"""
|
|
texts = [
|
|
str(n.get("inputs", {}).get("text", ""))
|
|
for n in wf.values()
|
|
if n.get("class_type") == "CLIPTextEncode"
|
|
]
|
|
positive = texts[0] if texts else ""
|
|
negative = texts[1] if len(texts) > 1 else ""
|
|
return positive, negative
|
|
|
|
|
|
def _prune_redundant_saveimages(wf: dict, keep_source_class: str) -> None:
|
|
"""Deja un unico SaveImage: el alimentado por un nodo `keep_source_class`.
|
|
|
|
Tras encadenar facedetailer queda el SaveImage del render base (que ya no es
|
|
la salida final) ademas del SaveImage del detailer. Se borra el primero para
|
|
que el workflow tenga una sola imagen de salida (la procesada). Muta `wf` in
|
|
situ (el caller ya trabaja sobre una copia). No-op si hay <=1 SaveImage o si
|
|
no se encuentra el SaveImage alimentado por `keep_source_class`.
|
|
"""
|
|
saves = [
|
|
(nid, n) for nid, n in wf.items() if n.get("class_type") == "SaveImage"
|
|
]
|
|
if len(saves) <= 1:
|
|
return
|
|
keep = None
|
|
for nid, node in saves:
|
|
src = node.get("inputs", {}).get("images")
|
|
if _is_link(src) and wf.get(src[0], {}).get("class_type") == keep_source_class:
|
|
keep = nid
|
|
break
|
|
if keep is None:
|
|
return
|
|
for nid, _ in saves:
|
|
if nid != keep:
|
|
del wf[nid]
|
|
|
|
|
|
def comfyui_compose_capabilities(
|
|
base_workflow: dict,
|
|
*,
|
|
loras: list[dict] | None = None,
|
|
controlnet: dict | None = None,
|
|
ipadapter: dict | None = None,
|
|
hires: dict | None = None,
|
|
facedetailer: dict | None = None,
|
|
) -> dict:
|
|
"""Aplica en orden las capacidades activadas sobre un workflow base.
|
|
|
|
Args:
|
|
base_workflow: dict en API format (salida de
|
|
comfyui_build_skill_workflow o comfyui_build_txt2img_workflow). No se
|
|
muta; se devuelve una copia.
|
|
loras: lista de dicts {name, strength_model?, strength_clip?} para
|
|
comfyui_inject_multi_lora. None o lista vacia = sin LoRAs. keyword-only.
|
|
controlnet: dict para comfyui_inject_controlnet. Claves: control_image
|
|
(str, obligatoria), cn_name (str, obligatoria), strength (float),
|
|
positive_node (str). None = sin ControlNet. keyword-only.
|
|
ipadapter: dict para comfyui_inject_ipadapter. Claves: ref_image (str,
|
|
obligatoria), mode ('style'|'faceid'), weight (float) y demas
|
|
keyword-only del inyector. None = sin IPAdapter. keyword-only.
|
|
hires: dict de kwargs para comfyui_inject_hires_fix (upscale_by, denoise,
|
|
steps, cfg, seed, upscale_model, ...). {} = hires con defaults. None =
|
|
sin hires. keyword-only.
|
|
facedetailer: dict de overrides para comfyui_build_facedetailer_workflow.
|
|
Claves opcionales: ckpt_name (str; si falta se detecta del workflow),
|
|
positive / negative (str; si faltan se detectan de los CLIPTextEncode),
|
|
y demas params del builder (denoise, steps, cfg, seed, bbox_model, ...).
|
|
{} = facedetailer con detect + defaults. None = sin facedetailer.
|
|
keyword-only.
|
|
|
|
Returns:
|
|
copia del base con las capacidades activadas encadenadas en orden. Si no
|
|
se activa ninguna, una copia del base intacta.
|
|
|
|
Raises:
|
|
ValueError: si una capacidad activada es incompatible (p.ej. controlnet
|
|
sin control_image, ipadapter sin ref_image): se propaga el ValueError
|
|
del inyector correspondiente con el contexto del fallo.
|
|
"""
|
|
wf = copy.deepcopy(base_workflow)
|
|
|
|
if loras:
|
|
wf = comfyui_inject_multi_lora(wf, loras)
|
|
|
|
if controlnet is not None:
|
|
cn = dict(controlnet)
|
|
control_image = cn.pop("control_image", "")
|
|
cn_name = cn.pop("cn_name", "")
|
|
wf = comfyui_inject_controlnet(wf, control_image, cn_name, **cn)
|
|
|
|
if ipadapter is not None:
|
|
ip = dict(ipadapter)
|
|
ref_image = ip.pop("ref_image", "")
|
|
wf = comfyui_inject_ipadapter(wf, ref_image, **ip)
|
|
|
|
if facedetailer is not None:
|
|
fd = dict(facedetailer)
|
|
ckpt_name = fd.pop("ckpt_name", None) or _detect_checkpoint(wf)
|
|
det_pos, det_neg = _detect_prompts(wf)
|
|
positive = fd.pop("positive", None)
|
|
if positive is None:
|
|
positive = det_pos
|
|
negative = fd.pop("negative", None)
|
|
if negative is None:
|
|
negative = det_neg
|
|
wf = comfyui_build_facedetailer_workflow(wf, ckpt_name, positive, negative, **fd)
|
|
# facedetailer anade su propio SaveImage; el del render base ya no es la
|
|
# salida final -> dejar solo el del detailer.
|
|
_prune_redundant_saveimages(wf, "FaceDetailer")
|
|
|
|
if hires is not None:
|
|
h = dict(hires) if isinstance(hires, dict) else {}
|
|
wf = comfyui_inject_hires_fix(wf, **h)
|
|
|
|
return wf
|
|
|
|
|
|
# Alias con el nombre completo del ID para descubrimiento por convencion.
|
|
compose_capabilities = comfyui_compose_capabilities
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import json
|
|
|
|
from ml.comfyui_build_txt2img_workflow import comfyui_build_txt2img_workflow
|
|
|
|
base = comfyui_build_txt2img_workflow("dreamshaper_8.safetensors", "a hero, 3d render")
|
|
mixed = comfyui_compose_capabilities(
|
|
base,
|
|
loras=[
|
|
{"name": "SD15_3d_render_redmond.safetensors", "strength_model": 0.9},
|
|
{"name": "SD15_detail_tweaker.safetensors", "strength_model": 0.5},
|
|
],
|
|
facedetailer={"denoise": 0.45},
|
|
)
|
|
print(json.dumps({"base_nodes": list(base), "mixed_nodes": list(mixed)}, indent=2))
|