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>
221 lines
9.1 KiB
Python
221 lines
9.1 KiB
Python
"""comfyui_generate_mixed_oneshot — mezcla de capacidades + subject -> PNG juzgado.
|
|
|
|
One-shot del "mixer" del grupo `comfyui-skill`: parte de un workflow base (una
|
|
skill guardada, el builder `txt2img`, o un dict ya construido), le aplica el
|
|
conjunto de capacidades elegido con `comfyui_compose_capabilities` (LoRAs +
|
|
ControlNet + IPAdapter + hires + FaceDetailer, cada una activable), encola,
|
|
espera, descarga el PNG y (si `judge=True`) lo puntua con el panel
|
|
`comfyui-judge`. Promueve a una sola llamada la secuencia repetida
|
|
base -> compose -> submit -> wait -> fetch -> judge (issue 0087).
|
|
|
|
Compone funciones del registry:
|
|
|
|
comfyui_build_txt2img_workflow_py_ml (base 'txt2img')
|
|
comfyui_load_skill_py_ml (base = slug de skill)
|
|
comfyui_build_skill_workflow_py_ml (receta + subject -> workflow, base = skill)
|
|
comfyui_compose_capabilities_py_ml (mezcla de capacidades, PURA)
|
|
comfyui_submit_workflow_py_ml (POST /prompt)
|
|
comfyui_wait_result_py_ml (poll /history)
|
|
comfyui_fetch_output_image_py_ml (GET /view -> disco)
|
|
comfyui_judge_image_py_ml (panel multi-juez)
|
|
|
|
Pipeline impuro: red (HTTP) + escritura en disco + (si juzga) API Anthropic.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import sys
|
|
|
|
_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)
|
|
|
|
from ml.comfyui_build_txt2img_workflow import comfyui_build_txt2img_workflow
|
|
from ml.comfyui_compose_capabilities import comfyui_compose_capabilities
|
|
from ml.comfyui_fetch_output_image import comfyui_fetch_output_image
|
|
from ml.comfyui_judge_image import comfyui_judge_image
|
|
from ml.comfyui_submit_workflow import comfyui_submit_workflow
|
|
from ml.comfyui_wait_result import comfyui_wait_result
|
|
|
|
|
|
def _resolve_skill_prompt(recipe: dict, subject: str) -> str:
|
|
"""Prompt positivo resuelto de una receta de skill (para el juez de fidelidad)."""
|
|
scaffold = recipe.get("prompt_scaffold") or {}
|
|
positive = str(scaffold.get("positive", "") or "")
|
|
if "{subject}" in positive:
|
|
positive = positive.replace("{subject}", subject)
|
|
elif not positive:
|
|
positive = subject
|
|
else:
|
|
positive = f"{subject}, {positive}"
|
|
triggers = scaffold.get("trigger_words") or []
|
|
if triggers:
|
|
positive = ", ".join(list(triggers) + [positive]) if positive else ", ".join(triggers)
|
|
return positive
|
|
|
|
|
|
def _resolve_base(base, subject, *, checkpoint, negative, seed, library_dir):
|
|
"""Devuelve (workflow_base, prompt_resolved). Despacha por tipo de `base`.
|
|
|
|
- dict -> se usa tal cual; prompt_resolved = subject.
|
|
- 'txt2img' -> comfyui_build_txt2img_workflow(checkpoint, subject, negative).
|
|
- otro str -> slug de skill: load_skill + build_skill_workflow.
|
|
"""
|
|
if isinstance(base, dict):
|
|
return base, subject
|
|
|
|
if base == "txt2img":
|
|
if not checkpoint:
|
|
raise ValueError(
|
|
"comfyui_generate_mixed_oneshot: base='txt2img' requiere checkpoint."
|
|
)
|
|
wf = comfyui_build_txt2img_workflow(checkpoint, subject, negative, seed=seed)
|
|
return wf, subject
|
|
|
|
# Cualquier otro str se trata como slug de skill.
|
|
from ml.comfyui_build_skill_workflow import build_skill_workflow
|
|
from ml.comfyui_load_skill import comfyui_load_skill
|
|
|
|
loaded = comfyui_load_skill(base, library_dir=library_dir)
|
|
if not loaded.get("ok"):
|
|
raise ValueError(f"load_skill('{base}') fallo: {loaded.get('error')}")
|
|
recipe = loaded["recipe"]
|
|
wf = build_skill_workflow(recipe, subject, seed=seed)
|
|
return wf, _resolve_skill_prompt(recipe, subject)
|
|
|
|
|
|
def comfyui_generate_mixed_oneshot(
|
|
base,
|
|
subject: str,
|
|
*,
|
|
capabilities: dict | None = None,
|
|
server: str = "127.0.0.1:8188",
|
|
dest: str | None = None,
|
|
seed: int = 0,
|
|
judge: bool = True,
|
|
checkpoint: str | None = None,
|
|
negative: str = "",
|
|
library_dir: str | None = None,
|
|
wait_timeout: float = 600.0,
|
|
) -> dict:
|
|
"""Genera (y opcionalmente juzga) una imagen mezclando capacidades, end-to-end.
|
|
|
|
Args:
|
|
base: workflow base. dict (workflow API format ya construido), la cadena
|
|
'txt2img' (construye uno con `checkpoint`+`subject`), o un slug de
|
|
skill guardada (carga su receta y la compila con `subject`).
|
|
subject: sujeto/prompt principal. En 'txt2img' es el prompt positivo; en
|
|
una skill sustituye `{subject}` en el scaffold.
|
|
capabilities: dict con las capacidades a mezclar, tal cual las acepta
|
|
comfyui_compose_capabilities: {loras, controlnet, ipadapter, hires,
|
|
facedetailer}. Las ausentes/None quedan desactivadas. None = sin
|
|
mezcla (solo el base). keyword-only.
|
|
server: host:port del servidor ComfyUI (sin esquema). keyword-only.
|
|
dest: directorio local donde guardar el PNG (None = cwd). keyword-only.
|
|
seed: semilla de generacion. keyword-only.
|
|
judge: si True, puntua el PNG con el panel comfyui-judge. keyword-only.
|
|
checkpoint: checkpoint para base='txt2img' (obligatorio en ese caso).
|
|
keyword-only.
|
|
negative: prompt negativo para base='txt2img'. keyword-only.
|
|
library_dir: raiz de la libreria de skills (base = slug). keyword-only.
|
|
wait_timeout: segundos maximos esperando al servidor. keyword-only.
|
|
|
|
Returns:
|
|
dict {ok, base, prompt_id, image_path, prompt_resolved, capabilities_active,
|
|
judge, error}. capabilities_active = lista de las capacidades activadas
|
|
(evidencia de la mezcla). judge = {verdict, score, votes} o None (si
|
|
judge=False o el panel falla). Si falla un paso, ok=False y error explica
|
|
cual.
|
|
"""
|
|
base_label = base if isinstance(base, str) else "dict"
|
|
caps = capabilities or {}
|
|
caps_active = [k for k, v in caps.items() if v is not None]
|
|
out = {"ok": False, "base": base_label, "prompt_id": "", "image_path": "",
|
|
"prompt_resolved": "", "capabilities_active": caps_active,
|
|
"judge": None, "error": ""}
|
|
|
|
# 1. Resolver el workflow base (skill / txt2img / dict).
|
|
try:
|
|
base_wf, prompt_resolved = _resolve_base(
|
|
base, subject, checkpoint=checkpoint, negative=negative,
|
|
seed=seed, library_dir=library_dir,
|
|
)
|
|
except (ValueError, KeyError) as exc:
|
|
return {**out, "error": f"resolver base fallo: {exc}"}
|
|
out["prompt_resolved"] = prompt_resolved
|
|
|
|
# 2. Mezclar las capacidades (funcion pura del registry).
|
|
try:
|
|
workflow = comfyui_compose_capabilities(base_wf, **caps)
|
|
except (ValueError, TypeError) as exc:
|
|
return {**out, "error": f"compose_capabilities fallo: {exc}"}
|
|
|
|
# 3. Encolar.
|
|
try:
|
|
sub = comfyui_submit_workflow(workflow, server=server)
|
|
prompt_id = sub["prompt_id"]
|
|
except (RuntimeError, KeyError) as exc:
|
|
return {**out, "error": f"submit fallo: {exc}"}
|
|
out["prompt_id"] = prompt_id
|
|
|
|
# 4. Esperar a que termine.
|
|
try:
|
|
outputs = comfyui_wait_result(prompt_id, server=server, timeout=wait_timeout)
|
|
except (TimeoutError, RuntimeError) as exc:
|
|
return {**out, "error": f"wait fallo: {exc}"}
|
|
|
|
# 5. Localizar el primer PNG en los outputs.
|
|
img = None
|
|
for node_out in outputs.values():
|
|
images = node_out.get("images") if isinstance(node_out, dict) else None
|
|
if images:
|
|
img = images[0]
|
|
break
|
|
if img is None:
|
|
return {**out, "error": f"el workflow no produjo imagenes (outputs={list(outputs)})"}
|
|
|
|
# 6. Descargar la imagen a disco.
|
|
fetched = comfyui_fetch_output_image(
|
|
img["filename"], subfolder=img.get("subfolder", ""),
|
|
type_=img.get("type", "output"), server=server, dest_dir=dest or ".",
|
|
)
|
|
if not fetched.get("ok"):
|
|
return {**out, "error": f"fetch de imagen fallo: {fetched.get('error')}"}
|
|
out["image_path"] = fetched["path"]
|
|
out["ok"] = True
|
|
|
|
if not judge:
|
|
return out
|
|
|
|
# 7. Juzgar el resultado con el panel multi-juez.
|
|
verdict = comfyui_judge_image(out["image_path"], prompt_resolved, server=server)
|
|
if not verdict.get("ok"):
|
|
out["error"] = f"juez fallo (imagen generada igualmente): {verdict.get('error')}"
|
|
return out
|
|
out["judge"] = {"verdict": verdict["verdict"], "score": verdict["score"],
|
|
"votes": verdict["votes"]}
|
|
return out
|
|
|
|
|
|
# Alias con el nombre completo del ID para descubrimiento por convencion.
|
|
generate_mixed_oneshot = comfyui_generate_mixed_oneshot
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import json
|
|
|
|
res = comfyui_generate_mixed_oneshot(
|
|
"txt2img",
|
|
"a heroic knight in 3d render style, dramatic lighting",
|
|
checkpoint="dreamshaper_8.safetensors",
|
|
capabilities={
|
|
"loras": [
|
|
{"name": "SD15_3d_render_redmond.safetensors", "strength_model": 0.9},
|
|
{"name": "SD15_detail_tweaker.safetensors", "strength_model": 0.5},
|
|
],
|
|
"facedetailer": {"denoise": 0.45},
|
|
},
|
|
dest="/tmp/comfy_mixed", seed=42, judge=True,
|
|
)
|
|
print(json.dumps(res, indent=2, ensure_ascii=False))
|