Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e49841a60a |
@@ -0,0 +1,99 @@
|
||||
---
|
||||
name: comfyui_build_audio_workflow
|
||||
kind: function
|
||||
lang: py
|
||||
domain: ml
|
||||
version: "1.0.0"
|
||||
purity: pure
|
||||
signature: "def comfyui_build_audio_workflow(ckpt_name: str, prompt: str, *, lyrics: str = \"\", seconds: float = 10.0, seed: int = 0, steps: int = 50, cfg: float = 5.0, sampler_name: str = \"euler\", scheduler: str = \"simple\", shift: float = 5.0, lyrics_strength: float = 1.0, filename_prefix: str = \"audio/comfy_audio\") -> dict"
|
||||
description: "Construye el dict de un workflow ComfyUI texto->audio (ACE-Step) en API format. Cadena con nodos de audio NATIVOS de ComfyUI 0.26.0: CheckpointLoaderSimple(AUDIO_ace_step_v1_3.5b.safetensors -> MODEL, CLIP, VAE) -> TextEncodeAceStepAudio(tags=prompt, lyrics) como positive + ConditioningZeroOut como negative + EmptyAceStepLatentAudio(seconds) -> ModelSamplingSD3(shift) -> KSampler -> VAEDecodeAudio -> SaveAudio(.flac). ACE-Step es abierto (Apache 2.0). Genera musica y SFX por texto; lyrics opcional para voz cantada. Pura, sin red ni I/O. Hermana de audio de comfyui_build_txt2img_workflow."
|
||||
tags: [comfyui, audio, ace-step, sfx, music, ml, workflow]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: ""
|
||||
imports: []
|
||||
params:
|
||||
- name: ckpt_name
|
||||
desc: "Nombre del checkpoint ACE-Step tal como lo ve el servidor ComfyUI (ej. 'AUDIO_ace_step_v1_3.5b.safetensors', todo-en-uno: DiT + text encoder + VAE de audio). Debe estar entre los que devuelve comfyui_object_info en CheckpointLoaderSimple."
|
||||
- name: prompt
|
||||
desc: "Descripcion del sonido o estilo musical. Va al campo 'tags' de TextEncodeAceStepAudio. Ej. '8-bit coin pickup sound, retro game' o 'lofi hip hop, mellow piano, 90 bpm'."
|
||||
- name: lyrics
|
||||
desc: "Letra cantada para musica con voz. Vacio '' para SFX o musica instrumental. keyword-only."
|
||||
- name: seconds
|
||||
desc: "Duracion del audio en segundos (min 1.0). Controla el tamano del latente via EmptyAceStepLatentAudio. keyword-only."
|
||||
- name: seed
|
||||
desc: "Semilla del KSampler. 0 es determinista; cambiar para variar el resultado. keyword-only."
|
||||
- name: steps
|
||||
desc: "Pasos de sampling del KSampler. 50 recomendado para ACE-Step. keyword-only."
|
||||
- name: cfg
|
||||
desc: "Classifier-free guidance scale. 5.0 recomendado para ACE-Step. keyword-only."
|
||||
- name: sampler_name
|
||||
desc: "Algoritmo del KSampler. Por defecto 'euler'. keyword-only."
|
||||
- name: scheduler
|
||||
desc: "Scheduler del KSampler. Por defecto 'simple'. keyword-only."
|
||||
- name: shift
|
||||
desc: "Shift del ModelSamplingSD3 aplicado al MODEL antes del sampling. 5.0 recomendado para ACE-Step; mejora la coherencia temporal. keyword-only."
|
||||
- name: lyrics_strength
|
||||
desc: "Fuerza del condicionamiento de la letra (1.0 por defecto; sin efecto practico cuando lyrics esta vacio). keyword-only."
|
||||
- name: filename_prefix
|
||||
desc: "Prefijo del .flac generado por SaveAudio en output/ del servidor. keyword-only."
|
||||
output: "dict en API format listo para comfyui_submit_workflow. node_ids string; cada valor con class_type + inputs. Devuelve 8 nodos: CheckpointLoaderSimple, TextEncodeAceStepAudio, ConditioningZeroOut, EmptyAceStepLatentAudio, ModelSamplingSD3, KSampler, VAEDecodeAudio y SaveAudio. El denoise del KSampler se fija a 1.0 (genera desde el latente vacio, no es audio2audio)."
|
||||
tested: true
|
||||
tests: ["estructura: 8 nodos ACE-Step presentes + ckpt en CheckpointLoaderSimple + prompt en TextEncodeAceStepAudio.tags", "cableado: clip [4,1], positive [6,0], negative via ConditioningZeroOut [10,0], model post ModelSamplingSD3 [11,0], vae [4,2], denoise 1.0", "params reflejados (lyrics/seconds/seed/steps/cfg/sampler_name/scheduler/shift/lyrics_strength/filename_prefix)", "edge: seconds y seed variables se reflejan en EmptyAceStepLatentAudio y KSampler", "determinismo: misma entrada -> mismo dict (builder puro)"]
|
||||
test_file_path: "python/functions/ml/tests/test_comfyui_build_audio_workflow.py"
|
||||
file_path: "python/functions/ml/comfyui_build_audio_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_audio_workflow import comfyui_build_audio_workflow
|
||||
|
||||
wf = comfyui_build_audio_workflow(
|
||||
ckpt_name="AUDIO_ace_step_v1_3.5b.safetensors",
|
||||
prompt="8-bit coin pickup sound, retro game, short",
|
||||
seconds=4.0, seed=42,
|
||||
)
|
||||
# wf["6"]["class_type"] == "TextEncodeAceStepAudio"
|
||||
# wf["9"]["class_type"] == "SaveAudio"
|
||||
# -> comfyui_submit_workflow(wf, server="127.0.0.1:8188") para encolar (necesita GPU)
|
||||
# -> comfyui_wait_result(prompt_id) -> comfyui_fetch_output_audio(prompt_id, dest=...)
|
||||
```
|
||||
|
||||
O lanzable directo con: `./fn run comfyui_build_audio_workflow` (imprime el JSON del workflow ACE-Step de ejemplo).
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Antes de enviar una generacion de audio (musica o SFX por texto) a ComfyUI:
|
||||
construye aqui el dict del workflow ACE-Step y pasalo a `comfyui_submit_workflow`.
|
||||
Usala cuando quieres un sonido o pieza musical descrita en lenguaje natural
|
||||
(`prompt`), opcionalmente con letra cantada (`lyrics`). Baja el resultado con
|
||||
`comfyui_fetch_output_audio`. Verifica el workflow contra el servidor con
|
||||
`comfyui_validate_workflow` antes de encolar.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Es API format (nodos numerados), NO el formato de la UI de ComfyUI. Es lo que
|
||||
acepta POST /prompt.
|
||||
- El checkpoint ACE-Step debe existir y ser visible para el servidor (carpeta de
|
||||
checkpoints o extra_model_paths) o ComfyUI rechaza el workflow con HTTP 400 al
|
||||
enviarlo. Esta funcion es pura y no valida contra el servidor.
|
||||
- Stable Audio Open 1.0 (la otra via nativa, mas ligera) esta GATED en HuggingFace
|
||||
(resolve da HTTP 403 sin aceptar la licencia): por eso el modelo por defecto es
|
||||
ACE-Step, que es abierto (Apache 2.0) y no gated.
|
||||
- VRAM 8GB: `ace_step_v1_3.5b.safetensors` pesa ~7.7GB. Arrancar ComfyUI con
|
||||
`--lowvram` para que streamee bloques a CPU; aun asi va justo. Antes de generar
|
||||
audio, liberar VRAM de SD/Flux con POST /free {"unload_models":true,
|
||||
"free_memory":true}. Si da OOM, bajar `seconds`. El builder es puro: no toca la
|
||||
GPU, solo arma el dict (un OOM ocurre en el submit posterior, no aqui).
|
||||
- ACE-Step es modelo de MUSICA: para SFX cortos funciona pero el resultado tiende
|
||||
a sonar "musical". `seconds` minimo 1.0. Para SFX muy cortos usar 2-4 s.
|
||||
- SaveAudio guarda `.flac` por defecto (clave "audio" en outputs[node]). Para bajar
|
||||
el archivo usa `comfyui_fetch_output_audio` (no `comfyui_fetch_output_video`, que
|
||||
solo busca extensiones de video).
|
||||
- `lyrics` vacio = instrumental/SFX. Con letra, ACE-Step canta; `lyrics_strength`
|
||||
ajusta cuanto se ciñe a ella.
|
||||
@@ -0,0 +1,126 @@
|
||||
"""Construye un workflow ComfyUI de texto->audio (ACE-Step) en "API format".
|
||||
|
||||
API format: cada clave es un node_id (string); cada nodo tiene class_type +
|
||||
inputs. Las conexiones entre nodos son listas [node_id, output_index]. Este es
|
||||
el formato que acepta POST /prompt, distinto del formato de la UI (graph con
|
||||
links explicitos).
|
||||
|
||||
El grafo usa los nodos de audio NATIVOS de ComfyUI 0.26.0 para el modelo
|
||||
ACE-Step (abierto, Apache 2.0): CheckpointLoaderSimple ->
|
||||
TextEncodeAceStepAudio (tags + lyrics) -> EmptyAceStepLatentAudio ->
|
||||
ModelSamplingSD3 -> KSampler -> VAEDecodeAudio -> SaveAudio. El negative se
|
||||
construye con ConditioningZeroOut sobre el positive (patron oficial de ACE-Step).
|
||||
|
||||
Funcion pura: sin red, sin I/O. Determinista para los mismos argumentos.
|
||||
"""
|
||||
|
||||
|
||||
def comfyui_build_audio_workflow(
|
||||
ckpt_name: str,
|
||||
prompt: str,
|
||||
*,
|
||||
lyrics: str = "",
|
||||
seconds: float = 10.0,
|
||||
seed: int = 0,
|
||||
steps: int = 50,
|
||||
cfg: float = 5.0,
|
||||
sampler_name: str = "euler",
|
||||
scheduler: str = "simple",
|
||||
shift: float = 5.0,
|
||||
lyrics_strength: float = 1.0,
|
||||
filename_prefix: str = "audio/comfy_audio",
|
||||
) -> dict:
|
||||
"""Construye el dict del workflow texto->audio para ACE-Step.
|
||||
|
||||
Cadena de nodos: CheckpointLoaderSimple -> TextEncodeAceStepAudio (positivo)
|
||||
+ ConditioningZeroOut (negativo) + EmptyAceStepLatentAudio -> ModelSamplingSD3
|
||||
-> KSampler -> VAEDecodeAudio -> SaveAudio. SaveAudio escribe un .flac en la
|
||||
carpeta output/<filename_prefix> del servidor ComfyUI.
|
||||
|
||||
Args:
|
||||
ckpt_name: nombre del checkpoint ACE-Step tal como lo ve el servidor
|
||||
(ej. "AUDIO_ace_step_v1_3.5b.safetensors"). Debe estar entre los que
|
||||
devuelve comfyui_object_info en CheckpointLoaderSimple.
|
||||
prompt: descripcion del sonido o estilo musical (va al campo "tags" de
|
||||
TextEncodeAceStepAudio). Ej. "8-bit coin pickup sound, retro game".
|
||||
lyrics: letra cantada para musica con voz. Vacio "" para SFX o musica
|
||||
instrumental.
|
||||
seconds: duracion del audio en segundos (min 1.0). Controla el tamano
|
||||
del latente via EmptyAceStepLatentAudio.
|
||||
seed: semilla del KSampler (cambia para variar el resultado).
|
||||
steps: pasos de sampling del KSampler (50 recomendado para ACE-Step).
|
||||
cfg: classifier-free guidance scale (5.0 recomendado para ACE-Step).
|
||||
sampler_name: nombre del sampler (ej. "euler").
|
||||
scheduler: scheduler del sampler (ej. "simple").
|
||||
shift: shift del ModelSamplingSD3 aplicado al MODEL antes del sampling
|
||||
(5.0 recomendado para ACE-Step). Mejora la coherencia temporal.
|
||||
lyrics_strength: fuerza del condicionamiento de la letra (1.0 por
|
||||
defecto; sin efecto practico cuando lyrics esta vacio).
|
||||
filename_prefix: prefijo del .flac generado por SaveAudio en output/.
|
||||
|
||||
Returns:
|
||||
dict en API format listo para comfyui_submit_workflow. Las claves son
|
||||
node_ids ("3".."11") y cada valor tiene class_type + inputs.
|
||||
"""
|
||||
return {
|
||||
"4": {
|
||||
"class_type": "CheckpointLoaderSimple",
|
||||
"inputs": {"ckpt_name": ckpt_name},
|
||||
},
|
||||
"6": {
|
||||
"class_type": "TextEncodeAceStepAudio",
|
||||
"inputs": {
|
||||
"clip": ["4", 1],
|
||||
"tags": prompt,
|
||||
"lyrics": lyrics,
|
||||
"lyrics_strength": lyrics_strength,
|
||||
},
|
||||
},
|
||||
"10": {
|
||||
"class_type": "ConditioningZeroOut",
|
||||
"inputs": {"conditioning": ["6", 0]},
|
||||
},
|
||||
"5": {
|
||||
"class_type": "EmptyAceStepLatentAudio",
|
||||
"inputs": {"seconds": seconds, "batch_size": 1},
|
||||
},
|
||||
"11": {
|
||||
"class_type": "ModelSamplingSD3",
|
||||
"inputs": {"model": ["4", 0], "shift": shift},
|
||||
},
|
||||
"3": {
|
||||
"class_type": "KSampler",
|
||||
"inputs": {
|
||||
"seed": seed,
|
||||
"steps": steps,
|
||||
"cfg": cfg,
|
||||
"sampler_name": sampler_name,
|
||||
"scheduler": scheduler,
|
||||
"denoise": 1.0,
|
||||
"model": ["11", 0],
|
||||
"positive": ["6", 0],
|
||||
"negative": ["10", 0],
|
||||
"latent_image": ["5", 0],
|
||||
},
|
||||
},
|
||||
"8": {
|
||||
"class_type": "VAEDecodeAudio",
|
||||
"inputs": {"samples": ["3", 0], "vae": ["4", 2]},
|
||||
},
|
||||
"9": {
|
||||
"class_type": "SaveAudio",
|
||||
"inputs": {"filename_prefix": filename_prefix, "audio": ["8", 0]},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import json
|
||||
|
||||
wf = comfyui_build_audio_workflow(
|
||||
ckpt_name="AUDIO_ace_step_v1_3.5b.safetensors",
|
||||
prompt="8-bit coin pickup sound, retro game, short",
|
||||
seconds=4.0,
|
||||
seed=42,
|
||||
)
|
||||
print(json.dumps(wf, indent=2))
|
||||
@@ -1,79 +0,0 @@
|
||||
---
|
||||
name: comfyui_extract_template
|
||||
kind: function
|
||||
lang: py
|
||||
domain: ml
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def comfyui_extract_template(name: str, comfyui_python: str | None = None, to_api: bool = False, server: str = \"127.0.0.1:8188\") -> dict"
|
||||
description: "Extrae el grafo de nodos de un workflow template oficial de ComfyUI por su template_id. Devuelve el grafo completo (formato UI: nodes/links), la lista de class_types que usa (aplanando subgrafos y descartando UUID de instancia), el formato, el bundle y los assets en disco. Opcionalmente (to_api=True) convierte el grafo UI a API format reutilizando comfyui_import_workflow_json (requiere un servidor ComfyUI vivo). Nombre inexistente -> error legible con sugerencias, sin traceback. Localiza el interprete de ComfyUI y usa su API oficial via subprocess. Impura: lee disco (+ red opcional si to_api)."
|
||||
tags: [comfyui, ml, templates, workflow, extract]
|
||||
uses_functions: ["comfyui_import_workflow_json_py_ml"]
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
params:
|
||||
- name: name
|
||||
desc: "template_id exacto del template (p.ej. 'sdxl_simple_example', 'image_sdxl'). Usa comfyui_list_templates para ver los nombres disponibles."
|
||||
- name: comfyui_python
|
||||
desc: "Ruta al interprete python de ComfyUI con el paquete comfyui-workflow-templates. None autodetecta (env COMFYUI_PYTHON, ~/ComfyUI/.venv/bin/python)."
|
||||
- name: to_api
|
||||
desc: "True intenta convertir el grafo UI a API format via comfyui_import_workflow_json (requiere servidor ComfyUI vivo en `server`). Si falla, el grafo UI se devuelve igualmente y el motivo va en api_error."
|
||||
- name: server
|
||||
desc: "host:port del servidor ComfyUI usado para la conversion to_api (default '127.0.0.1:8188')."
|
||||
output: "dict {ok, name, format, class_types, has_subgraphs, n_nodes, graph, api_workflow, api_error, bundle, version, assets, error}. graph = dict del template (formato UI o API). class_types = lista ordenada de tipos de nodo reales. api_workflow = dict API si to_api tuvo exito, si no {}. Nunca lanza: nombre inexistente -> ok=False con error + sugerencias."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "python/functions/ml/comfyui_extract_template.py"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
# Lanzable directo (grafo slim + class_types de un template concreto):
|
||||
python/.venv/bin/python3 python/functions/ml/comfyui_extract_template.py sdxl_simple_example
|
||||
|
||||
# Con conversion a API format (necesita ComfyUI corriendo en 127.0.0.1:8188):
|
||||
python/.venv/bin/python3 python/functions/ml/comfyui_extract_template.py sdxl_simple_example --to-api
|
||||
```
|
||||
|
||||
```python
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
|
||||
from ml.comfyui_extract_template import comfyui_extract_template
|
||||
|
||||
res = comfyui_extract_template("sdxl_simple_example")
|
||||
print(res["format"], res["n_nodes"], "nodos") # ui_graph 25 nodos
|
||||
print(res["class_types"]) # ['CheckpointLoaderSimple', 'KSamplerAdvanced', ...]
|
||||
graph = res["graph"] # dict cargable en la UI tal cual
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando quieras reutilizar la estructura de nodos de un template oficial: cargar su
|
||||
grafo en tu UI, usarlo de base para un workflow propio, o saber exactamente que
|
||||
class_types encadena. Segundo paso del flujo listar (`comfyui_list_templates`) ->
|
||||
extraer. Para encolar el resultado en `/prompt` usa `to_api=True` (o pasa el grafo por
|
||||
`comfyui_import_workflow_json`).
|
||||
|
||||
## Gotchas
|
||||
|
||||
- El grafo viene en **formato UI** (nodes/links con posiciones), no en API format. La
|
||||
UI de ComfyUI lo entiende tal cual (cargalo o copia el dict); para `/prompt` hay que
|
||||
convertirlo a API format con `to_api=True`.
|
||||
- `to_api=True` reutiliza `comfyui_import_workflow_json`, que necesita un **servidor
|
||||
ComfyUI vivo** para mapear los widgets a sus claves de input. Sin servidor, la
|
||||
extraccion del grafo UI sigue funcionando (ok=True) y el motivo del fallo de
|
||||
conversion va en `api_error` (no rompe). KISS: no se fuerza la conversion.
|
||||
- Templates **subgraphed** (con `definitions.subgraphs`, `has_subgraphs=True`): la
|
||||
conversion a API NO expande el subgraph (limitacion de la normalizacion UI->API
|
||||
estandar), asi que `api_workflow` puede quedar con solo los nodos top-level. Para
|
||||
esos, cargar el grafo UI en la UI es lo fiable. `class_types` sí incluye los nodos
|
||||
reales de dentro del subgraph.
|
||||
- Nombre inexistente -> `ok=False` con `error` legible y sugerencias por substring (o
|
||||
difflib). No lanza traceback.
|
||||
- El paquete vive en el venv de ComfyUI; si no se encuentra el interprete o el paquete,
|
||||
`ok=False` indicando `pip install comfyui-workflow-templates`.
|
||||
@@ -1,302 +0,0 @@
|
||||
"""Extrae el grafo de nodos de un workflow template oficial de ComfyUI por su nombre.
|
||||
|
||||
Funcion impura: lee disco (el .json del template instalado) ejecutando la API oficial
|
||||
del paquete comfyui-workflow-templates dentro del interprete de ComfyUI.
|
||||
|
||||
Dado el nombre de un template (su template_id, p.ej. "image_sdxl" o
|
||||
"api_bfl_flux2_max_sofa_swap"), devuelve:
|
||||
- graph: el dict completo del .json (formato UI: nodes/links con posiciones).
|
||||
- class_types: la lista de tipos de nodo (class_type) que usa, aplanando los
|
||||
subgrafos de `definitions` si los hay.
|
||||
- format: "ui_graph" (lo normal en los templates) o "api".
|
||||
- assets: rutas en disco de los ficheros del template (json + previews .webp).
|
||||
|
||||
Opcionalmente (to_api=True) intenta convertir el grafo UI a API format reutilizando
|
||||
comfyui_import_workflow_json del registry. Esa conversion necesita un servidor ComfyUI
|
||||
vivo para mapear los widgets a sus claves de input; si no lo hay, se devuelve el grafo
|
||||
UI + class_types igualmente y se reporta el motivo en api_error (KISS: no se fuerza la
|
||||
conversion de grafos complejos).
|
||||
|
||||
El paquete vive en el venv de ComfyUI (no en el del registry), por eso esta funcion no
|
||||
lo importa: localiza el interprete de ComfyUI y le pasa un script que usa la API oficial.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
_THIS_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
if _THIS_DIR not in sys.path:
|
||||
sys.path.insert(0, _THIS_DIR)
|
||||
|
||||
|
||||
# Script que corre DENTRO del python de ComfyUI. Resuelve un template por id, vuelca su
|
||||
# grafo + metadata como JSON. Si no existe, devuelve sugerencias cercanas.
|
||||
_EXTRACT_SCRIPT = r"""
|
||||
import json, sys, difflib, re
|
||||
try:
|
||||
import comfyui_workflow_templates_core as core
|
||||
except Exception as exc:
|
||||
print(json.dumps({"__err__": "import", "msg": str(exc)}))
|
||||
sys.exit(0)
|
||||
|
||||
_UUID_RE = re.compile(r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$")
|
||||
|
||||
TID = json.loads({tid_json!r})
|
||||
m = core.load_manifest()
|
||||
if TID not in m.templates:
|
||||
near = [k for k in m.templates if TID.lower() in k.lower()][:8]
|
||||
if not near:
|
||||
near = difflib.get_close_matches(TID, list(m.templates.keys()), n=8, cutoff=0.6)
|
||||
print(json.dumps({"__err__": "not_found", "suggestions": near}))
|
||||
sys.exit(0)
|
||||
|
||||
entry = m.templates[TID]
|
||||
json_asset = next((a.filename for a in entry.assets if a.filename.endswith(".json")), None)
|
||||
if not json_asset:
|
||||
print(json.dumps({"__err__": "no_json"}))
|
||||
sys.exit(0)
|
||||
|
||||
path = core.get_asset_path(TID, json_asset)
|
||||
with open(path, encoding="utf-8") as fh:
|
||||
graph = json.load(fh)
|
||||
|
||||
# Detecta formato y extrae class_types.
|
||||
fmt = "unknown"
|
||||
class_types = set()
|
||||
has_subgraphs = False
|
||||
if isinstance(graph, dict) and isinstance(graph.get("nodes"), list):
|
||||
fmt = "ui_graph"
|
||||
for n in graph["nodes"]:
|
||||
t = n.get("type") if isinstance(n, dict) else None
|
||||
if t and not _UUID_RE.match(str(t)):
|
||||
class_types.add(t)
|
||||
defs = graph.get("definitions")
|
||||
if isinstance(defs, dict) and isinstance(defs.get("subgraphs"), list):
|
||||
for sg in defs["subgraphs"]:
|
||||
for n in (sg.get("nodes") or []) if isinstance(sg, dict) else []:
|
||||
if isinstance(n, dict) and n.get("type"):
|
||||
has_subgraphs = True
|
||||
if not _UUID_RE.match(str(n["type"])):
|
||||
class_types.add(n["type"])
|
||||
elif isinstance(graph, dict):
|
||||
fmt = "api"
|
||||
for v in graph.values():
|
||||
if isinstance(v, dict) and v.get("class_type"):
|
||||
class_types.add(v["class_type"])
|
||||
|
||||
print(json.dumps({
|
||||
"graph": graph,
|
||||
"class_types": sorted(class_types),
|
||||
"format": fmt,
|
||||
"has_subgraphs": has_subgraphs,
|
||||
"bundle": entry.bundle,
|
||||
"version": entry.version,
|
||||
"assets": core.resolve_all_assets(TID),
|
||||
"json_path": path,
|
||||
}))
|
||||
"""
|
||||
|
||||
|
||||
def _find_comfyui_python(explicit: str | None) -> str | None:
|
||||
"""Localiza un interprete de ComfyUI con el paquete instalado (ver list_templates)."""
|
||||
candidates = []
|
||||
if explicit:
|
||||
candidates.append(os.path.expanduser(explicit))
|
||||
env = os.environ.get("COMFYUI_PYTHON")
|
||||
if env:
|
||||
candidates.append(os.path.expanduser(env))
|
||||
candidates += [
|
||||
os.path.expanduser("~/ComfyUI/.venv/bin/python"),
|
||||
os.path.expanduser("~/ComfyUI/venv/bin/python"),
|
||||
os.path.expanduser("~/comfyui/.venv/bin/python"),
|
||||
sys.executable,
|
||||
]
|
||||
for c in candidates:
|
||||
if c and os.path.isfile(c):
|
||||
return c
|
||||
return None
|
||||
|
||||
|
||||
def comfyui_extract_template(
|
||||
name: str,
|
||||
comfyui_python: str | None = None,
|
||||
to_api: bool = False,
|
||||
server: str = "127.0.0.1:8188",
|
||||
) -> dict:
|
||||
"""Extrae el grafo y los class_types de un template oficial de ComfyUI por nombre.
|
||||
|
||||
Args:
|
||||
name: template_id exacto del template (p.ej. "image_sdxl"). Usa
|
||||
comfyui_list_templates para ver los nombres disponibles.
|
||||
comfyui_python: ruta al interprete python de ComfyUI con el paquete
|
||||
comfyui-workflow-templates. Si None, se autodetecta.
|
||||
to_api: si True, intenta convertir el grafo UI a API format reutilizando
|
||||
comfyui_import_workflow_json (requiere un servidor ComfyUI vivo en
|
||||
`server`). Si la conversion falla, se devuelve el grafo UI igualmente y
|
||||
el motivo va en api_error.
|
||||
server: host:port del servidor ComfyUI para la conversion to_api.
|
||||
|
||||
Returns:
|
||||
dict {ok, name, format, class_types, has_subgraphs, n_nodes, graph,
|
||||
api_workflow, api_error, bundle, version, assets, error}:
|
||||
- graph: el dict del template en formato UI (o API si ya lo estaba).
|
||||
- class_types: lista ordenada de tipos de nodo del grafo (incluye los de
|
||||
subgrafos de `definitions`).
|
||||
- api_workflow: dict en API format si to_api tuvo exito, si no {}.
|
||||
Nunca lanza. Nombre inexistente -> ok=False con error legible + sugerencias.
|
||||
"""
|
||||
py = _find_comfyui_python(comfyui_python)
|
||||
base = {
|
||||
"ok": False,
|
||||
"name": name,
|
||||
"format": "",
|
||||
"class_types": [],
|
||||
"has_subgraphs": False,
|
||||
"n_nodes": 0,
|
||||
"graph": {},
|
||||
"api_workflow": {},
|
||||
"api_error": "",
|
||||
"bundle": "",
|
||||
"version": "",
|
||||
"assets": [],
|
||||
"error": "",
|
||||
}
|
||||
if not py:
|
||||
base["error"] = (
|
||||
"no se encontro un interprete de ComfyUI. Pasa comfyui_python=... o "
|
||||
"define COMFYUI_PYTHON. Instala el paquete con: "
|
||||
"pip install comfyui-workflow-templates"
|
||||
)
|
||||
return base
|
||||
|
||||
script = _EXTRACT_SCRIPT.replace("{tid_json!r}", repr(json.dumps(name)))
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
[py, "-c", script],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=60,
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
base["error"] = f"fallo al ejecutar el interprete de ComfyUI ({py}): {exc}"
|
||||
return base
|
||||
|
||||
if proc.returncode != 0:
|
||||
base["error"] = f"el interprete de ComfyUI fallo: {proc.stderr.strip()[:500]}"
|
||||
return base
|
||||
|
||||
try:
|
||||
data = json.loads(proc.stdout.strip().splitlines()[-1])
|
||||
except Exception as exc: # noqa: BLE001
|
||||
base["error"] = f"salida no parseable del interprete de ComfyUI: {exc}"
|
||||
return base
|
||||
|
||||
err = data.get("__err__")
|
||||
if err == "import":
|
||||
base["error"] = (
|
||||
f"el paquete comfyui-workflow-templates no esta instalado en {py} "
|
||||
f"({data.get('msg', '')}). Instalalo con: "
|
||||
"pip install comfyui-workflow-templates"
|
||||
)
|
||||
return base
|
||||
if err == "not_found":
|
||||
sug = data.get("suggestions", [])
|
||||
hint = f" ¿Quizas: {', '.join(sug)}?" if sug else ""
|
||||
base["error"] = f"template '{name}' no existe en el paquete.{hint}"
|
||||
return base
|
||||
if err == "no_json":
|
||||
base["error"] = f"el template '{name}' no tiene asset .json."
|
||||
return base
|
||||
|
||||
graph = data.get("graph", {})
|
||||
fmt = data.get("format", "")
|
||||
nodes = graph.get("nodes") if isinstance(graph, dict) else None
|
||||
n_nodes = len(nodes) if isinstance(nodes, list) else (
|
||||
len(graph) if fmt == "api" and isinstance(graph, dict) else 0
|
||||
)
|
||||
|
||||
out = {
|
||||
"ok": True,
|
||||
"name": name,
|
||||
"format": fmt,
|
||||
"class_types": data.get("class_types", []),
|
||||
"has_subgraphs": data.get("has_subgraphs", False),
|
||||
"n_nodes": n_nodes,
|
||||
"graph": graph,
|
||||
"api_workflow": {},
|
||||
"api_error": "",
|
||||
"bundle": data.get("bundle", ""),
|
||||
"version": data.get("version", ""),
|
||||
"assets": data.get("assets", []),
|
||||
"error": "",
|
||||
}
|
||||
|
||||
if to_api:
|
||||
if fmt == "api":
|
||||
out["api_workflow"] = graph
|
||||
else:
|
||||
out["api_workflow"], out["api_error"] = _convert_to_api(graph, server)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def _convert_to_api(graph: dict, server: str) -> tuple[dict, str]:
|
||||
"""Convierte un grafo UI a API format via comfyui_import_workflow_json del registry.
|
||||
|
||||
Requiere un servidor ComfyUI vivo para mapear widgets. Devuelve (workflow, "")
|
||||
si tuvo exito o ({}, motivo) si fallo. No lanza.
|
||||
"""
|
||||
try:
|
||||
from comfyui_import_workflow_json import comfyui_import_workflow_json
|
||||
except Exception as exc: # noqa: BLE001
|
||||
return {}, f"no se pudo importar comfyui_import_workflow_json: {exc}"
|
||||
|
||||
tmp = None
|
||||
try:
|
||||
with tempfile.NamedTemporaryFile(
|
||||
"w", suffix=".json", delete=False, encoding="utf-8"
|
||||
) as fh:
|
||||
json.dump(graph, fh)
|
||||
tmp = fh.name
|
||||
res = comfyui_import_workflow_json(tmp, server=server)
|
||||
if res.get("ok"):
|
||||
return res.get("workflow", {}), ""
|
||||
return {}, (
|
||||
res.get("error", "conversion fallida")
|
||||
+ f" (requiere un servidor ComfyUI vivo en {server})"
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
return {}, f"conversion to_api fallida: {exc}"
|
||||
finally:
|
||||
if tmp and os.path.exists(tmp):
|
||||
try:
|
||||
os.unlink(tmp)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
ap = argparse.ArgumentParser(description="Extrae el grafo de un template ComfyUI")
|
||||
ap.add_argument("name", help="template_id (ver comfyui_list_templates)")
|
||||
ap.add_argument("--comfyui-python", default=None)
|
||||
ap.add_argument("--to-api", action="store_true")
|
||||
ap.add_argument("--server", default="127.0.0.1:8188")
|
||||
ap.add_argument("--full", action="store_true", help="incluye el grafo entero")
|
||||
args = ap.parse_args()
|
||||
|
||||
res = comfyui_extract_template(
|
||||
args.name,
|
||||
args.comfyui_python,
|
||||
to_api=args.to_api,
|
||||
server=args.server,
|
||||
)
|
||||
if args.full or not res["ok"]:
|
||||
print(json.dumps(res, indent=2, ensure_ascii=False))
|
||||
else:
|
||||
slim = {k: v for k, v in res.items() if k != "graph"}
|
||||
slim["graph_keys"] = list(res["graph"].keys()) if isinstance(res["graph"], dict) else []
|
||||
print(json.dumps(slim, indent=2, ensure_ascii=False))
|
||||
@@ -0,0 +1,85 @@
|
||||
---
|
||||
name: comfyui_fetch_output_audio
|
||||
kind: function
|
||||
lang: py
|
||||
domain: ml
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def comfyui_fetch_output_audio(prompt_id: str, *, server: str = \"127.0.0.1:8188\", dest: str | None = None, outputs: dict | None = None, timeout: float = 120.0) -> dict"
|
||||
description: "Localiza y descarga el output de audio de un workflow ComfyUI a disco local. Hermana de comfyui_fetch_output_video / _image / _mesh pero para los nodos de audio (SaveAudio, SaveAudioMP3, SaveAudioOpus, SaveAudioAdvanced): esos exponen su salida en GET /history bajo la clave 'audio' con items {filename, subfolder, type}. Localiza el primer .flac/.wav/.mp3/.opus/.ogg/.m4a, lo baja via GET /view y opcionalmente lo escribe en dest. Acepta outputs= ya obtenido de comfyui_wait_result para evitar re-consultar /history. Impura: HTTP GET + escritura en disco, solo stdlib."
|
||||
tags: [comfyui, audio, fetch, ace-step, ml, download, workflow]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: error_go_core
|
||||
imports: []
|
||||
params:
|
||||
- name: prompt_id
|
||||
desc: "id devuelto por comfyui_submit_workflow, de un workflow cuyo nodo de audio (SaveAudio/SaveAudioMP3/...) ya termino (usa comfyui_wait_result antes si dudas). Se ignora si se pasa outputs."
|
||||
- name: server
|
||||
desc: "host:port del servidor ComfyUI sin esquema. keyword-only."
|
||||
- name: dest
|
||||
desc: "Ruta destino. Si None, escribe el basename del audio en el cwd. Si es un directorio existente (o termina en separador), escribe el basename dentro. Si es una ruta de archivo, escribe ahi. keyword-only."
|
||||
- name: outputs
|
||||
desc: "dict de outputs ya obtenido (el que devuelve comfyui_wait_result). Si se pasa, se busca el audio ahi y NO se consulta /history (evita una peticion de red extra). keyword-only."
|
||||
- name: timeout
|
||||
desc: "Timeout de cada peticion HTTP en segundos. keyword-only."
|
||||
output: "dict {ok, path, format, bytes, error}. path = ruta local del archivo de audio guardado, format = extension sin punto (ej. 'flac' o 'mp3'), bytes = bytes descargados. Si falla, ok=False y error explica (sin audio en los outputs, HTTP, conexion o escritura)."
|
||||
tested: true
|
||||
tests:
|
||||
- "test_is_audio_item_por_extension"
|
||||
- "test_find_saveaudio_flac_bajo_audio"
|
||||
- "test_find_saveaudiomp3_bajo_audio"
|
||||
- "test_find_prioriza_clave_audio"
|
||||
- "test_find_sin_audio_devuelve_none"
|
||||
test_file_path: "python/functions/ml/comfyui_fetch_output_audio_test.py"
|
||||
file_path: "python/functions/ml/comfyui_fetch_output_audio.py"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```python
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
|
||||
from ml.comfyui_fetch_output_audio import comfyui_fetch_output_audio
|
||||
|
||||
# Tras comfyui_submit_workflow + comfyui_wait_result de un workflow de audio
|
||||
# (ACE-Step, Stable Audio), baja el .flac/.mp3 al disco.
|
||||
res = comfyui_fetch_output_audio("8a278988-8a94-4225-add3-88a406f7101c", dest="/tmp/audios")
|
||||
# res == {"ok": True, "path": "/tmp/audios/comfy_audio_00001_.flac",
|
||||
# "format": "flac", "bytes": 882000, "error": ""}
|
||||
|
||||
# Si ya tienes los outputs de comfyui_wait_result, pasalos y evita re-consultar /history:
|
||||
outputs = {"9": {"audio": [{"filename": "comfy_audio_00001_.flac", "subfolder": "audio", "type": "output"}]}}
|
||||
res2 = comfyui_fetch_output_audio("ignored", dest="/tmp/audios", outputs=outputs)
|
||||
```
|
||||
|
||||
Lánzalo con el python del venv (import de arriba o heredoc). Nota: `./fn run` directo no aplica porque la firma usa `*` (keyword-only), no soportado por el generador de runner de `fn run`.
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Después de generar audio con ComfyUI (música o SFX por texto con ACE-Step, o Stable
|
||||
Audio), cuando necesites el archivo `.flac`/`.wav`/`.mp3`/`.opus` real en disco (no
|
||||
solo su nombre): para reproducirlo, subirlo a un vault, o usarlo como asset de un
|
||||
juego. Es la hermana de `comfyui_fetch_output_video` (vídeo/animación),
|
||||
`comfyui_fetch_output_image` (imágenes) y `comfyui_fetch_output_mesh` (mallas 3D).
|
||||
El builder hermano es `comfyui_build_audio_workflow`.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Impura: hace HTTP GET a /history y /view y escribe en disco. Requiere el server
|
||||
vivo y que el prompt YA haya terminado (usa `comfyui_wait_result` antes, o pásale
|
||||
`outputs=`).
|
||||
- Los nodos SaveAudio* exponen el archivo bajo la clave `"audio"` de los outputs
|
||||
(no `"images"` como los de imagen/vídeo). Por eso `comfyui_fetch_output_video` NO
|
||||
sirve para audio: busca extensiones de vídeo y claves gifs/videos/images.
|
||||
- SaveAudio guarda `.flac` por defecto; SaveAudioMP3 `.mp3`, SaveAudioOpus `.opus`.
|
||||
La función cubre todas por extensión.
|
||||
- Toma el PRIMER archivo de audio que encuentra. Si un workflow exporta varios,
|
||||
baja solo uno; para los demás llama otra vez o usa GET /view con el filename concreto.
|
||||
- El history se purga al reiniciar el server: si el prompt ya no está, devuelve
|
||||
`ok=False`. Pasar `outputs=` evita esa consulta y el problema.
|
||||
- `dest` se interpreta: None -> cwd; directorio EXISTENTE -> dentro; ruta de archivo
|
||||
-> esa ruta. Un directorio que aún no existe se trata como ruta de archivo: créalo
|
||||
antes (o termina la ruta en separador).
|
||||
@@ -0,0 +1,162 @@
|
||||
"""Localiza y descarga el output de audio de un workflow ComfyUI a disco.
|
||||
|
||||
Hermana de comfyui_fetch_output_video / comfyui_fetch_output_image / _mesh, pero
|
||||
para los nodos de audio (SaveAudio, SaveAudioMP3, SaveAudioOpus, SaveAudioAdvanced).
|
||||
Esos nodos exponen su salida en GET /history/{prompt_id} bajo la clave "audio"
|
||||
como lista de items {filename, subfolder, type}. Esta funcion localiza el primer
|
||||
archivo con extension de audio (.flac/.wav/.mp3/.opus/.ogg/.m4a), lo baja via
|
||||
GET /view a disco y, opcionalmente, lo escribe en `dest`.
|
||||
|
||||
Impura: red (HTTP GET a /history y /view) + escritura en disco. Solo stdlib.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
# Extensiones de audio que producen los nodos SaveAudio* de ComfyUI.
|
||||
_AUDIO_EXTS = (".flac", ".wav", ".mp3", ".opus", ".ogg", ".m4a")
|
||||
# Claves de output preferentes para audio (se inspeccionan primero).
|
||||
_AUDIO_KEYS = ("audio", "audios")
|
||||
|
||||
|
||||
def _is_audio_item(item: dict) -> bool:
|
||||
"""True si el item de output apunta a un archivo de audio (por extension)."""
|
||||
fn = (item.get("filename") or "").lower()
|
||||
return fn.endswith(_AUDIO_EXTS)
|
||||
|
||||
|
||||
def _find_audio_output(outputs: dict) -> dict | None:
|
||||
"""Busca en los outputs de /history el primer archivo de audio.
|
||||
|
||||
Hace dos pasadas: primero en la clave preferente "audio" (la que usan los
|
||||
nodos SaveAudio*), luego en cualquier clave por si un nodo lo expone bajo
|
||||
otro nombre. Devuelve {filename, subfolder, type} o None.
|
||||
"""
|
||||
for prefer in (True, False):
|
||||
for node_out in outputs.values():
|
||||
if not isinstance(node_out, dict):
|
||||
continue
|
||||
for key, items in node_out.items():
|
||||
if prefer and key not in _AUDIO_KEYS:
|
||||
continue
|
||||
if not isinstance(items, list):
|
||||
continue
|
||||
for item in items:
|
||||
if isinstance(item, dict) and _is_audio_item(item):
|
||||
return {
|
||||
"filename": item.get("filename", ""),
|
||||
"subfolder": item.get("subfolder", ""),
|
||||
"type": item.get("type", "output"),
|
||||
}
|
||||
return None
|
||||
|
||||
|
||||
def _resolve_dest(dest: str | None, filename: str) -> str:
|
||||
"""Resuelve la ruta local destino a partir de `dest` y el basename remoto."""
|
||||
base = os.path.basename(filename)
|
||||
if dest is None:
|
||||
return os.path.join(os.getcwd(), base)
|
||||
expanded = os.path.expanduser(dest)
|
||||
if os.path.isdir(expanded) or expanded.endswith(os.sep):
|
||||
return os.path.join(expanded, base)
|
||||
return expanded
|
||||
|
||||
|
||||
def comfyui_fetch_output_audio(
|
||||
prompt_id: str,
|
||||
*,
|
||||
server: str = "127.0.0.1:8188",
|
||||
dest: str | None = None,
|
||||
outputs: dict | None = None,
|
||||
timeout: float = 120.0,
|
||||
) -> dict:
|
||||
"""Descarga el audio de un prompt ComfyUI ya ejecutado a disco local.
|
||||
|
||||
Args:
|
||||
prompt_id: id devuelto por comfyui_submit_workflow, de un workflow cuyo
|
||||
nodo de audio (SaveAudio/SaveAudioMP3/...) ya termino (usa
|
||||
comfyui_wait_result antes si dudas). Se ignora si se pasa `outputs`.
|
||||
server: host:port del servidor ComfyUI (sin esquema). keyword-only.
|
||||
dest: ruta destino. Si None, escribe el basename del audio en el cwd.
|
||||
Si es un directorio (o termina en separador), escribe el basename
|
||||
dentro. Si es una ruta de archivo, escribe ahi. keyword-only.
|
||||
outputs: dict de outputs ya obtenido (el que devuelve comfyui_wait_result).
|
||||
Si se pasa, se busca el audio ahi y NO se consulta /history (evita una
|
||||
peticion de red extra justo despues de esperar). keyword-only.
|
||||
timeout: timeout de cada peticion HTTP en segundos. keyword-only.
|
||||
|
||||
Returns:
|
||||
dict {ok, path, format, bytes, error}. path = ruta local del archivo de
|
||||
audio guardado; format = extension sin punto (ej. "flac" o "mp3"); bytes =
|
||||
tamano descargado. Si falla, ok=False y error explica (sin audio en los
|
||||
outputs, HTTP, conexion o escritura).
|
||||
"""
|
||||
# 1. Obtener los outputs: del parametro (sin red) o consultando /history.
|
||||
if outputs is None:
|
||||
hist_url = f"http://{server}/history/{prompt_id}"
|
||||
try:
|
||||
with urllib.request.urlopen(hist_url, timeout=timeout) as resp:
|
||||
hist = json.loads(resp.read())
|
||||
except urllib.error.HTTPError as exc:
|
||||
body = exc.read().decode(errors="replace")[:200]
|
||||
return {"ok": False, "path": "", "format": "", "bytes": 0,
|
||||
"error": f"HTTP {exc.code} en {hist_url}: {body}"}
|
||||
except urllib.error.URLError as exc:
|
||||
return {"ok": False, "path": "", "format": "", "bytes": 0,
|
||||
"error": f"no se pudo conectar a {hist_url}: {exc.reason}"}
|
||||
except json.JSONDecodeError as exc:
|
||||
return {"ok": False, "path": "", "format": "", "bytes": 0,
|
||||
"error": f"respuesta no es JSON valido desde {hist_url}: {exc}"}
|
||||
entry = hist.get(prompt_id)
|
||||
if not entry:
|
||||
return {"ok": False, "path": "", "format": "", "bytes": 0,
|
||||
"error": f"prompt_id {prompt_id} no esta en /history (¿no termino o se purgo?)"}
|
||||
outputs = entry.get("outputs", {})
|
||||
|
||||
audio = _find_audio_output(outputs or {})
|
||||
if audio is None:
|
||||
return {"ok": False, "path": "", "format": "", "bytes": 0,
|
||||
"error": f"sin archivo de audio en los outputs de {prompt_id}"}
|
||||
|
||||
# 2. Descargar el archivo via GET /view.
|
||||
qs = urllib.parse.urlencode({
|
||||
"filename": audio["filename"],
|
||||
"subfolder": audio["subfolder"],
|
||||
"type": audio["type"],
|
||||
})
|
||||
view_url = f"http://{server}/view?{qs}"
|
||||
try:
|
||||
with urllib.request.urlopen(view_url, timeout=timeout) as resp:
|
||||
blob = resp.read()
|
||||
except urllib.error.HTTPError as exc:
|
||||
body = exc.read().decode(errors="replace")[:200]
|
||||
return {"ok": False, "path": "", "format": "", "bytes": 0,
|
||||
"error": f"HTTP {exc.code} en {view_url}: {body}"}
|
||||
except urllib.error.URLError as exc:
|
||||
return {"ok": False, "path": "", "format": "", "bytes": 0,
|
||||
"error": f"no se pudo conectar a {view_url}: {exc.reason}"}
|
||||
|
||||
# 3. Escribir a disco.
|
||||
out_path = _resolve_dest(dest, audio["filename"])
|
||||
try:
|
||||
parent = os.path.dirname(out_path)
|
||||
if parent:
|
||||
os.makedirs(parent, exist_ok=True)
|
||||
with open(out_path, "wb") as f:
|
||||
f.write(blob)
|
||||
except OSError as exc:
|
||||
return {"ok": False, "path": "", "format": "", "bytes": 0,
|
||||
"error": f"no se pudo escribir en {out_path!r}: {exc}"}
|
||||
|
||||
fmt = os.path.splitext(audio["filename"])[1].lstrip(".").lower()
|
||||
return {"ok": True, "path": out_path, "format": fmt, "bytes": len(blob), "error": ""}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
pid = sys.argv[1] if len(sys.argv) > 1 else "00000000-0000-0000-0000-000000000000"
|
||||
res = comfyui_fetch_output_audio(pid, dest="/tmp/comfy_audio")
|
||||
print(json.dumps(res, indent=2))
|
||||
@@ -0,0 +1,50 @@
|
||||
"""Tests de localizacion de output para comfyui_fetch_output_audio.
|
||||
|
||||
Solo cubren la logica pura de busqueda (_is_audio_item / _find_audio_output): no
|
||||
tocan red ni disco. La descarga real via HTTP se prueba en el flujo e2e con el
|
||||
servidor ComfyUI vivo.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
|
||||
from comfyui_fetch_output_audio import _find_audio_output, _is_audio_item
|
||||
|
||||
|
||||
def test_is_audio_item_por_extension():
|
||||
assert _is_audio_item({"filename": "comfy_audio_00001_.flac"})
|
||||
assert _is_audio_item({"filename": "x.mp3"})
|
||||
assert _is_audio_item({"filename": "x.WAV"})
|
||||
assert not _is_audio_item({"filename": "x.png"})
|
||||
assert not _is_audio_item({"filename": ""})
|
||||
|
||||
|
||||
def test_find_saveaudio_flac_bajo_audio():
|
||||
outputs = {
|
||||
"9": {"audio": [{"filename": "comfy_audio_00001_.flac",
|
||||
"subfolder": "audio", "type": "output"}]}
|
||||
}
|
||||
got = _find_audio_output(outputs)
|
||||
assert got == {"filename": "comfy_audio_00001_.flac",
|
||||
"subfolder": "audio", "type": "output"}
|
||||
|
||||
|
||||
def test_find_saveaudiomp3_bajo_audio():
|
||||
outputs = {"12": {"audio": [{"filename": "track.mp3", "subfolder": "", "type": "output"}]}}
|
||||
assert _find_audio_output(outputs)["filename"] == "track.mp3"
|
||||
|
||||
|
||||
def test_find_prioriza_clave_audio():
|
||||
# Un nodo deja un png bajo "images" y otro un flac bajo "audio": gana el audio.
|
||||
outputs = {
|
||||
"9": {"images": [{"filename": "preview.png", "subfolder": "", "type": "output"}]},
|
||||
"10": {"audio": [{"filename": "out.flac", "subfolder": "", "type": "output"}]},
|
||||
}
|
||||
assert _find_audio_output(outputs)["filename"] == "out.flac"
|
||||
|
||||
|
||||
def test_find_sin_audio_devuelve_none():
|
||||
outputs = {"9": {"images": [{"filename": "preview.png", "subfolder": "", "type": "output"}]}}
|
||||
assert _find_audio_output(outputs) is None
|
||||
assert _find_audio_output({}) is None
|
||||
@@ -1,82 +0,0 @@
|
||||
---
|
||||
name: comfyui_list_templates
|
||||
kind: function
|
||||
lang: py
|
||||
domain: ml
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def comfyui_list_templates(comfyui_python: str | None = None, bundle: str | None = None, name_filter: str | None = None, with_nodes: bool = True, workflows_only: bool = True, limit: int = 0) -> dict"
|
||||
description: "Lista los workflow templates oficiales del paquete pip comfyui-workflow-templates (los del menu 'Browse Templates' del frontend de ComfyUI). Devuelve nombre, bundle/categoria, path en disco, n_nodes y node_types (class_types reales, aplanando subgrafos y descartando los UUID de instancia). Localiza el interprete de ComfyUI y usa su API oficial via subprocess (el paquete vive en el venv de ComfyUI, no en el del registry). Impura: lee disco. Filtra entradas no-workflow (index*/localizacion) por defecto."
|
||||
tags: [comfyui, ml, templates, workflow, discovery]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
params:
|
||||
- name: comfyui_python
|
||||
desc: "Ruta al interprete python de ComfyUI con el paquete comfyui-workflow-templates instalado. None autodetecta (env COMFYUI_PYTHON, ~/ComfyUI/.venv/bin/python, ~/ComfyUI/venv/bin/python)."
|
||||
- name: bundle
|
||||
desc: "Filtra por bundle exacto: 'media-api', 'media-image', 'media-video' o 'media-other'. None = todos."
|
||||
- name: name_filter
|
||||
desc: "Subcadena (case-insensitive) que debe contener el nombre del template. None = sin filtro."
|
||||
- name: with_nodes
|
||||
desc: "True (default) incluye node_types en cada registro; False los omite (registros mas ligeros)."
|
||||
- name: workflows_only
|
||||
desc: "True (default) excluye entradas que no son grafos de workflow (ficheros index*/localizacion del paquete)."
|
||||
- name: limit
|
||||
desc: "Si > 0, trunca a los primeros N templates tras filtrar y ordenar por nombre."
|
||||
output: "dict {ok: bool, count: int, package_version: str, templates: list, error: str}. Cada template: {name, category, bundle, version, path, n_nodes, node_types, is_workflow}. Nunca lanza: paquete ausente o interprete no hallado -> ok=False con error legible que indica como instalar (pip install comfyui-workflow-templates)."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "python/functions/ml/comfyui_list_templates.py"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```bash
|
||||
# Lanzable directo (muestra version del paquete + 15 primeros con sus node_types):
|
||||
./fn run comfyui_list_templates
|
||||
|
||||
# Filtrado por bundle de imagen, sin abrir node_types, primeros 20:
|
||||
python/.venv/bin/python3 python/functions/ml/comfyui_list_templates.py \
|
||||
--bundle media-image --no-nodes --limit 20
|
||||
```
|
||||
|
||||
```python
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
|
||||
from ml.comfyui_list_templates import comfyui_list_templates
|
||||
|
||||
res = comfyui_list_templates(name_filter="sdxl")
|
||||
print(res["count"], "templates SDXL") # p.ej. 4
|
||||
for t in res["templates"]:
|
||||
print(t["name"], t["n_nodes"], t["node_types"][:3])
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Para descubrir que workflow templates oficiales trae ComfyUI sin abrir la UI:
|
||||
explorar el catalogo, filtrar por bundle/nombre, o saber que `node_types` usa cada
|
||||
template antes de extraerlo con `comfyui_extract_template`. Primer paso del flujo
|
||||
listar -> extraer -> (cargar en UI / convertir a API).
|
||||
|
||||
## Gotchas
|
||||
|
||||
- El paquete `comfyui-workflow-templates` vive en el venv de ComfyUI, NO en el del
|
||||
registry. La funcion no lo importa: localiza el python de ComfyUI y corre su API
|
||||
oficial en un subprocess. Si no encuentra ese interprete (o el paquete no esta
|
||||
instalado) devuelve `ok=False` con un error que dice como instalarlo. No lanza.
|
||||
- Desde la 0.10.x el paquete es multi-bundle y ya NO expone una carpeta `templates/`
|
||||
unica (la API antigua `get_templates_path()` lanza a proposito). Por eso se usa
|
||||
`comfyui_workflow_templates_core` (`load_manifest`/`get_asset_path`).
|
||||
- `node_types` aplana los subgrafos de `definitions` y descarta los `type` que son
|
||||
UUID (instancias de subgraph), para mostrar class_types reales (KSampler, CLIPLoader,
|
||||
…) en vez de identificadores opacos. `n_nodes` cuenta solo los nodos top-level.
|
||||
- `workflows_only=True` (default) excluye ~16 entradas `index*` que son metadata de
|
||||
localizacion del frontend, no grafos. Pasa `workflows_only=False` (o `--all` en CLI)
|
||||
para verlas.
|
||||
- Impura: abre cada `.json` en disco (≈451 ficheros pequeños, ~0.2s). No toca red ni
|
||||
arranca GPU.
|
||||
@@ -1,284 +0,0 @@
|
||||
"""Lista los workflow templates oficiales que trae el paquete comfyui-workflow-templates.
|
||||
|
||||
Funcion impura: lee disco (los .json de los templates instalados) ejecutando la
|
||||
API oficial del paquete dentro del interprete de ComfyUI.
|
||||
|
||||
ComfyUI 0.26+ distribuye los templates oficiales (los del menu "Browse Templates"
|
||||
del frontend) en el paquete pip `comfyui-workflow-templates`, que desde la 0.10.x es
|
||||
un meta-paquete multi-bundle: ya NO expone una carpeta `templates/` unica, sino una
|
||||
API en `comfyui_workflow_templates_core` (`load_manifest`, `iter_templates`,
|
||||
`get_asset_path`). Cada template es un grafo de nodos en formato UI (nodes/links con
|
||||
posiciones), agrupado en uno de cuatro bundles: media-api, media-image, media-video,
|
||||
media-other.
|
||||
|
||||
Como el paquete vive en el venv de ComfyUI (no en el del registry), esta funcion no
|
||||
lo importa directamente: localiza el interprete de ComfyUI y le pasa un script que usa
|
||||
la API oficial y vuelca el catalogo como JSON. Asi es robusta ante cambios de la
|
||||
estructura interna del paquete.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
# Script que corre DENTRO del python de ComfyUI. Usa la API oficial del paquete y
|
||||
# vuelca el catalogo (metadata + node_types por template) como una linea JSON.
|
||||
_DUMP_SCRIPT = r"""
|
||||
import json, sys, re
|
||||
try:
|
||||
import comfyui_workflow_templates_core as core
|
||||
except Exception as exc:
|
||||
print(json.dumps({"__err__": "import", "msg": str(exc)}))
|
||||
sys.exit(0)
|
||||
|
||||
_UUID_RE = re.compile(r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$")
|
||||
|
||||
def _collect_types(graph):
|
||||
# Recoge class_types reales: aplana los subgrafos de definitions y descarta los
|
||||
# type que son UUID (instancias de subgraph, cuyo contenido real ya se incluye).
|
||||
types = set()
|
||||
if isinstance(graph, dict) and isinstance(graph.get("nodes"), list):
|
||||
for n in graph["nodes"]:
|
||||
if isinstance(n, dict) and n.get("type") and not _UUID_RE.match(str(n["type"])):
|
||||
types.add(n["type"])
|
||||
defs = graph.get("definitions")
|
||||
if isinstance(defs, dict) and isinstance(defs.get("subgraphs"), list):
|
||||
for sg in defs["subgraphs"]:
|
||||
for n in (sg.get("nodes") or []) if isinstance(sg, dict) else []:
|
||||
if isinstance(n, dict) and n.get("type") and not _UUID_RE.match(str(n["type"])):
|
||||
types.add(n["type"])
|
||||
return len(graph["nodes"]), sorted(types)
|
||||
if isinstance(graph, dict): # API format
|
||||
for v in graph.values():
|
||||
if isinstance(v, dict) and v.get("class_type"):
|
||||
types.add(v["class_type"])
|
||||
if types:
|
||||
return len(graph), sorted(types)
|
||||
return 0, []
|
||||
|
||||
WITH_NODES = {with_nodes}
|
||||
m = core.load_manifest()
|
||||
try:
|
||||
import importlib.metadata as _md
|
||||
pkg_version = _md.version("comfyui-workflow-templates")
|
||||
except Exception:
|
||||
pkg_version = ""
|
||||
|
||||
out = []
|
||||
for tid, entry in m.templates.items():
|
||||
json_asset = next(
|
||||
(a.filename for a in entry.assets if a.filename.endswith(".json")), None
|
||||
)
|
||||
path = core.get_asset_path(tid, json_asset) if json_asset else ""
|
||||
rec = {
|
||||
"name": tid,
|
||||
"bundle": entry.bundle,
|
||||
"category": entry.bundle,
|
||||
"version": entry.version,
|
||||
"path": path,
|
||||
"n_nodes": 0,
|
||||
"node_types": [],
|
||||
}
|
||||
rec["is_workflow"] = False
|
||||
if path:
|
||||
try:
|
||||
with open(path, encoding="utf-8") as fh:
|
||||
graph = json.load(fh)
|
||||
n_nodes, node_types = _collect_types(graph)
|
||||
is_api = isinstance(graph, dict) and any(
|
||||
isinstance(v, dict) and v.get("class_type") for v in graph.values()
|
||||
)
|
||||
rec["is_workflow"] = bool(
|
||||
(isinstance(graph, dict) and isinstance(graph.get("nodes"), list) and graph["nodes"])
|
||||
or is_api
|
||||
)
|
||||
rec["n_nodes"] = n_nodes
|
||||
if WITH_NODES:
|
||||
rec["node_types"] = node_types
|
||||
except Exception:
|
||||
pass
|
||||
out.append(rec)
|
||||
|
||||
print(json.dumps({"package_version": pkg_version, "templates": out}))
|
||||
"""
|
||||
|
||||
|
||||
def _find_comfyui_python(explicit: str | None) -> str | None:
|
||||
"""Devuelve la ruta a un interprete de ComfyUI que tenga el paquete instalado.
|
||||
|
||||
Orden de busqueda: argumento explicito -> env COMFYUI_PYTHON -> candidatos
|
||||
habituales (~/ComfyUI/.venv, ~/ComfyUI/venv) -> el python actual. Devuelve None
|
||||
si ninguno existe en disco.
|
||||
"""
|
||||
candidates = []
|
||||
if explicit:
|
||||
candidates.append(os.path.expanduser(explicit))
|
||||
env = os.environ.get("COMFYUI_PYTHON")
|
||||
if env:
|
||||
candidates.append(os.path.expanduser(env))
|
||||
candidates += [
|
||||
os.path.expanduser("~/ComfyUI/.venv/bin/python"),
|
||||
os.path.expanduser("~/ComfyUI/venv/bin/python"),
|
||||
os.path.expanduser("~/comfyui/.venv/bin/python"),
|
||||
sys.executable,
|
||||
]
|
||||
for c in candidates:
|
||||
if c and os.path.isfile(c):
|
||||
return c
|
||||
return None
|
||||
|
||||
|
||||
def comfyui_list_templates(
|
||||
comfyui_python: str | None = None,
|
||||
bundle: str | None = None,
|
||||
name_filter: str | None = None,
|
||||
with_nodes: bool = True,
|
||||
workflows_only: bool = True,
|
||||
limit: int = 0,
|
||||
) -> dict:
|
||||
"""Lista los templates oficiales de ComfyUI con su grafo de nodos.
|
||||
|
||||
Args:
|
||||
comfyui_python: ruta al interprete python de ComfyUI que tiene instalado
|
||||
el paquete comfyui-workflow-templates. Si None, se autodetecta (env
|
||||
COMFYUI_PYTHON o ~/ComfyUI/.venv/bin/python).
|
||||
bundle: si se da, filtra por bundle exacto ("media-api", "media-image",
|
||||
"media-video", "media-other").
|
||||
name_filter: si se da, filtra a templates cuyo nombre contenga esta
|
||||
subcadena (case-insensitive).
|
||||
with_nodes: si True (default) incluye node_types en cada registro. Si
|
||||
False los omite (registros mas ligeros).
|
||||
workflows_only: si True (default) excluye entradas que no son grafos de
|
||||
workflow (ficheros index*/localizacion del paquete).
|
||||
limit: si > 0, trunca la lista a los primeros N tras filtrar.
|
||||
|
||||
Returns:
|
||||
dict {ok, count, package_version, templates, error}:
|
||||
- templates: lista de {name, category, bundle, version, path, n_nodes,
|
||||
node_types} ordenada por name.
|
||||
- count: numero de templates devueltos (tras filtros y limit).
|
||||
Nunca lanza: cualquier fallo (paquete ausente, interprete no hallado)
|
||||
devuelve ok=False con un error legible.
|
||||
"""
|
||||
py = _find_comfyui_python(comfyui_python)
|
||||
if not py:
|
||||
return {
|
||||
"ok": False,
|
||||
"count": 0,
|
||||
"package_version": "",
|
||||
"templates": [],
|
||||
"error": (
|
||||
"no se encontro un interprete de ComfyUI. Pasa comfyui_python=... "
|
||||
"o define COMFYUI_PYTHON. El paquete se instala con: "
|
||||
"pip install comfyui-workflow-templates"
|
||||
),
|
||||
}
|
||||
|
||||
script = _DUMP_SCRIPT.replace("{with_nodes}", "True" if with_nodes else "False")
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
[py, "-c", script],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=120,
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
return {
|
||||
"ok": False,
|
||||
"count": 0,
|
||||
"package_version": "",
|
||||
"templates": [],
|
||||
"error": f"fallo al ejecutar el interprete de ComfyUI ({py}): {exc}",
|
||||
}
|
||||
|
||||
if proc.returncode != 0:
|
||||
return {
|
||||
"ok": False,
|
||||
"count": 0,
|
||||
"package_version": "",
|
||||
"templates": [],
|
||||
"error": f"el interprete de ComfyUI fallo: {proc.stderr.strip()[:500]}",
|
||||
}
|
||||
|
||||
try:
|
||||
data = json.loads(proc.stdout.strip().splitlines()[-1])
|
||||
except Exception as exc: # noqa: BLE001
|
||||
return {
|
||||
"ok": False,
|
||||
"count": 0,
|
||||
"package_version": "",
|
||||
"templates": [],
|
||||
"error": f"salida no parseable del interprete de ComfyUI: {exc}",
|
||||
}
|
||||
|
||||
if data.get("__err__") == "import":
|
||||
return {
|
||||
"ok": False,
|
||||
"count": 0,
|
||||
"package_version": "",
|
||||
"templates": [],
|
||||
"error": (
|
||||
"el paquete comfyui-workflow-templates no esta instalado en "
|
||||
f"{py} ({data.get('msg', '')}). Instalalo con: "
|
||||
"pip install comfyui-workflow-templates"
|
||||
),
|
||||
}
|
||||
|
||||
templates = data.get("templates", [])
|
||||
if workflows_only:
|
||||
templates = [t for t in templates if t.get("is_workflow")]
|
||||
if bundle:
|
||||
templates = [t for t in templates if t.get("bundle") == bundle]
|
||||
if name_filter:
|
||||
nf = name_filter.lower()
|
||||
templates = [t for t in templates if nf in t.get("name", "").lower()]
|
||||
templates.sort(key=lambda t: t.get("name", ""))
|
||||
if limit and limit > 0:
|
||||
templates = templates[:limit]
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"count": len(templates),
|
||||
"package_version": data.get("package_version", ""),
|
||||
"templates": templates,
|
||||
"error": "",
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
ap = argparse.ArgumentParser(description="Lista templates oficiales de ComfyUI")
|
||||
ap.add_argument("--comfyui-python", default=None)
|
||||
ap.add_argument("--bundle", default=None)
|
||||
ap.add_argument("--name-filter", default=None)
|
||||
ap.add_argument("--no-nodes", action="store_true", help="omite node_types")
|
||||
ap.add_argument("--all", action="store_true", help="incluye entradas no-workflow (index*)")
|
||||
ap.add_argument("--limit", type=int, default=0)
|
||||
ap.add_argument("--full", action="store_true", help="dump completo (todos los node_types)")
|
||||
args = ap.parse_args()
|
||||
|
||||
res = comfyui_list_templates(
|
||||
args.comfyui_python,
|
||||
bundle=args.bundle,
|
||||
name_filter=args.name_filter,
|
||||
with_nodes=not args.no_nodes,
|
||||
workflows_only=not args.all,
|
||||
limit=args.limit,
|
||||
)
|
||||
if args.full or not res["ok"]:
|
||||
print(json.dumps(res, indent=2, ensure_ascii=False))
|
||||
else:
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"ok": res["ok"],
|
||||
"count": res["count"],
|
||||
"package_version": res["package_version"],
|
||||
"sample": res["templates"][:15],
|
||||
},
|
||||
indent=2,
|
||||
ensure_ascii=False,
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,90 @@
|
||||
"""Tests de estructura para comfyui_build_audio_workflow (funcion pura, ACE-Step)."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
|
||||
from ml.comfyui_build_audio_workflow import comfyui_build_audio_workflow
|
||||
from _comfyui_wf_assert import assert_api_format, class_types, node_by_ct
|
||||
|
||||
|
||||
def test_estructura_y_nodos_acestep():
|
||||
wf = comfyui_build_audio_workflow(
|
||||
"AUDIO_ace_step_v1_3.5b.safetensors", "retro coin sfx"
|
||||
)
|
||||
assert_api_format(wf)
|
||||
cts = class_types(wf)
|
||||
for ct in (
|
||||
"CheckpointLoaderSimple",
|
||||
"TextEncodeAceStepAudio",
|
||||
"ConditioningZeroOut",
|
||||
"EmptyAceStepLatentAudio",
|
||||
"ModelSamplingSD3",
|
||||
"KSampler",
|
||||
"VAEDecodeAudio",
|
||||
"SaveAudio",
|
||||
):
|
||||
assert ct in cts, f"falta nodo {ct}"
|
||||
assert len(wf) == 8
|
||||
|
||||
|
||||
def test_ckpt_y_prompt_reflejados():
|
||||
wf = comfyui_build_audio_workflow("AUDIO_x.safetensors", "magic spell whoosh")
|
||||
assert node_by_ct(wf, "CheckpointLoaderSimple")["inputs"]["ckpt_name"] == "AUDIO_x.safetensors"
|
||||
enc = node_by_ct(wf, "TextEncodeAceStepAudio")
|
||||
assert enc["inputs"]["tags"] == "magic spell whoosh"
|
||||
assert enc["inputs"]["lyrics"] == ""
|
||||
|
||||
|
||||
def test_cableado_ksampler():
|
||||
wf = comfyui_build_audio_workflow("AUDIO_x.safetensors", "p")
|
||||
ks = node_by_ct(wf, "KSampler")["inputs"]
|
||||
# model viene de ModelSamplingSD3 ("11"), no del checkpoint directo
|
||||
assert ks["model"] == ["11", 0]
|
||||
assert ks["positive"] == ["6", 0]
|
||||
# negative pasa por ConditioningZeroOut ("10")
|
||||
assert ks["negative"] == ["10", 0]
|
||||
assert ks["latent_image"] == ["5", 0]
|
||||
assert ks["denoise"] == 1.0
|
||||
# ModelSamplingSD3 toma el MODEL del checkpoint
|
||||
assert node_by_ct(wf, "ModelSamplingSD3")["inputs"]["model"] == ["4", 0]
|
||||
# VAEDecodeAudio usa el VAE del checkpoint
|
||||
assert node_by_ct(wf, "VAEDecodeAudio")["inputs"]["vae"] == ["4", 2]
|
||||
# ConditioningZeroOut deriva del positive
|
||||
assert node_by_ct(wf, "ConditioningZeroOut")["inputs"]["conditioning"] == ["6", 0]
|
||||
|
||||
|
||||
def test_edge_seconds_y_seed_variables():
|
||||
wf_a = comfyui_build_audio_workflow("c", "p", seconds=4.0, seed=42)
|
||||
wf_b = comfyui_build_audio_workflow("c", "p", seconds=8.0, seed=99)
|
||||
assert node_by_ct(wf_a, "EmptyAceStepLatentAudio")["inputs"]["seconds"] == 4.0
|
||||
assert node_by_ct(wf_b, "EmptyAceStepLatentAudio")["inputs"]["seconds"] == 8.0
|
||||
assert node_by_ct(wf_a, "KSampler")["inputs"]["seed"] == 42
|
||||
assert node_by_ct(wf_b, "KSampler")["inputs"]["seed"] == 99
|
||||
|
||||
|
||||
def test_params_reflejados():
|
||||
wf = comfyui_build_audio_workflow(
|
||||
"c", "p",
|
||||
lyrics="la la la", steps=30, cfg=4.0, sampler_name="dpmpp_2m",
|
||||
scheduler="karras", shift=3.5, lyrics_strength=0.7,
|
||||
filename_prefix="audio/mio",
|
||||
)
|
||||
enc = node_by_ct(wf, "TextEncodeAceStepAudio")["inputs"]
|
||||
assert enc["lyrics"] == "la la la"
|
||||
assert enc["lyrics_strength"] == 0.7
|
||||
ks = node_by_ct(wf, "KSampler")["inputs"]
|
||||
assert ks["steps"] == 30
|
||||
assert ks["cfg"] == 4.0
|
||||
assert ks["sampler_name"] == "dpmpp_2m"
|
||||
assert ks["scheduler"] == "karras"
|
||||
assert node_by_ct(wf, "ModelSamplingSD3")["inputs"]["shift"] == 3.5
|
||||
assert node_by_ct(wf, "SaveAudio")["inputs"]["filename_prefix"] == "audio/mio"
|
||||
|
||||
|
||||
def test_determinismo():
|
||||
a = comfyui_build_audio_workflow("c", "p", seconds=5.0, seed=7)
|
||||
b = comfyui_build_audio_workflow("c", "p", seconds=5.0, seed=7)
|
||||
assert a == b
|
||||
Reference in New Issue
Block a user