Instala este archivo en el folder del nodo. Úsalo como contexto de referencia experta para cualquier trabajo con AnyFast, fal.ai, Replicate, NVIDIA NIM u otros proveedores de generación de video/imagen via API asíncrona en ComfyUI.
Todos los proveedores (AnyFast, fal.ai, Replicate) siguen el mismo patrón de tres pasos:
SUBMIT → POLL → RETRIEVE
POST GET loop GET/extract URL
r = requests.post(endpoint, json=payload, headers=auth_headers, timeout=(30, 600))
# timeout=(connect, read): el connect debe ser corto, el read largo para payloads grandes
task_id = r.json()["id"] # o "request_id", "prediction_id" según proveedorRegla crítica: nunca usar un timeout plano para submits con imágenes en base64 — el payload puede ser grande. Usar timeout=(connect_secs, read_secs).
time.sleep(initial_wait) # esperar antes del primer poll (3-5s típico)
while time.time() < deadline:
r = requests.get(f"{base_url}/{task_id}", headers=auth_headers, timeout=30)
r.raise_for_status()
status = extract_status(r.json())
if status in DONE_STATES:
return extract_url(r.json())
if status in FAIL_STATES:
raise RuntimeError(extract_error(r.json()))
time.sleep(interval) # 5s típico
raise TimeoutError(...)Regla: los estados de éxito y fallo varían por proveedor — ver sección 2. Siempre lowercase antes de comparar.
Las APIs cambian schemas sin avisar. Usar un walker que recorra el JSON en BFS:
def walk_dicts(root, max_depth=6):
queue = [(root, 0)]
while queue:
current, depth = queue.pop(0)
yield current
if depth >= max_depth:
continue
for v in current.values():
if isinstance(v, dict):
queue.append((v, depth + 1))
elif isinstance(v, list):
for item in v:
if isinstance(item, dict):
queue.append((item, depth + 1))Buscar video_url, url, result_url en cualquier dict del árbol. Esto resiste cambios de schema.
Base URL: https://www.anyfast.ai
Auth header: Authorization: Bearer {api_key}
Content-Type: application/json
Endpoint: POST /v1/video/generations
{
"model": "seedance", // o "seedance-fast" / "seedance-2.0-ultra"
"content": [
{"type": "text", "text": "..."},
{"type": "image_url", "image_url": {"url": "asset://id-or-data-uri"}, "role": "first_frame"}
],
"resolution": "720p", // seedance/fast: 480p|720p|1080p ultra: 720p|1080p|2k
"ratio": "adaptive", // 16:9|9:16|4:3|3:4|1:1|21:9|adaptive
"duration": 5, // 4-15 segundos
"generate_audio": true,
"watermark": false
}Roles en content:
first_frame— primera imagen del video (image_url)last_frame— última imagen del video (image_url)reference_image— referencia de estilo/contenido, hasta 9 (image_url); requiere@image1…@imageNen promptreference_video— video de referencia, hasta 3 (video_url); requiere@video1en promptreference_audio— audio de referencia, hasta 3 (audio_url); requiere@audio1en prompt
Límites por tipo:
- Imágenes: máx 30 MB por imagen, máx 9 por request
- Video: máx 50 MB, duración 2-15s, máx 3 por request
- Audio: máx 15 MB, duración 2-15s, máx 3 por request; debe acompañar al menos una imagen o video
URL formats para imágenes:
asset://asset-id— asset ya subido (lowercase, confirmado por docs del endpoint de generación)data:image/png;base64,...— base64 data URIhttps://...— URL pública
Submit response:
{"id": "cgt-xxx", "task_id": "cgt-xxx", "object": "video", "model": "seedance", "status": "", "progress": 0, "created_at": 1234}Polling: GET /v1/video/generations/{id}
Poll response structure:
{
"code": "success",
"message": "...",
"data": {
"task_id": "...",
"status": "QUEUING|PROCESSING|SUCCESS|FAILED",
"fail_reason": "mensaje de error cuando FAILED",
"progress": "75%",
"data": {
"content": {
"video_url": "https://... (24h validity)"
}
}
}
}Estados de poll: QUEUING → PROCESSING → SUCCESS | FAILED (siempre uppercase — normalizar a lowercase al comparar)
Gotcha importante: el video URL está en body.data.data.content.video_url (anidado 4 niveles). Usar BFS walker.
El flujo de assets es obligatorio para images como first_frame en generación. Los assets necesitan alcanzar estado Active antes de poder usarse.
Flujo completo:
CreateAssetGroup → CreateAsset → ListAssets (poll hasta Active) → usar en generación
POST /volc/asset/CreateAssetGroup
{"model": "volc-asset", "Name": "nombre-grupo"}Response: {"Id": "group-xxx"}
- El campo se llama
Id(capital I, minúscula d) - NO tiene campo GroupType — los grupos se crean sin tipo
POST /volc/asset/CreateAsset
JSON (imágenes — preferido):
{
"model": "volc-asset",
"GroupId": "group-xxx", // REQUERIDO
"Name": "nombre",
"AssetType": "Image", // REQUERIDO: Image | Video | Audio
"URL": "data:image/png;base64,..." // URL pública, data URI, o base64 raw
}Multipart (video/audio):
model=volc-asset-video (o volc-asset-audio)
GroupId=group-xxx REQUERIDO
Name=nombre
AssetType=Video REQUERIDO (default sería "Image" si se omite — bug silencioso)
file=<bytes>
Modelos por tipo (billing):
- Image →
volc-asset - Video →
volc-asset-video - Audio →
volc-asset-audio
Response: {"Id": "asset-xxx"} — el campo se llama Id
Gotcha: GroupId es REQUERIDO en JSON (la doc lo marca explícitamente). Sin él → 400.
POST /volc/asset/ListAssets
{
"model": "volc-asset",
"Filter": {
"GroupIds": ["group-xxx"]
// NO incluir GroupType — los grupos no tienen tipo asignado
},
"PageNumber": 1,
"PageSize": 100
}Response items: {"Id": "asset-xxx", "Status": "Active", "AssetType": "Image", ...}
Gotcha crítico: filtrar por GroupType (ej: "AIGC") retorna Items: [] porque los grupos se crean sin tipo. Omitir siempre ese filtro.
Poll logic:
while time.time() < deadline:
items = list_assets(group_id)
for item in items:
if item["Id"] == asset_id and item["Status"] == "Active":
return # listo para usar
time.sleep(5)
raise RuntimeError("Asset no alcanzó Active en tiempo")Timeout recomendado: 300s (5 min). El processing puede tomar tiempo variable.
| Síntoma | Causa probable | Fix |
|---|---|---|
"The specified asset X is not found" en generación |
Asset no está Active todavía | Hacer poll ListAssets hasta Status=Active |
ListAssets siempre retorna Items: [] |
Filtro GroupType incorrecto |
Remover GroupType del filter |
CreateAsset 400 group not found |
Grupo recién creado no propagado | Retry con delay (4s×3) |
CreateAsset falla con video/audio |
AssetType no enviado, defaultea a Image | Siempre enviar AssetType explícito |
| Error de generación sin mensaje útil | fail_reason no extraído |
Buscar en body.data.fail_reason |
Base URL submit: https://queue.fal.run
Base URL result: https://queue.fal.run (mismo dominio)
Auth header: Authorization: Key {api_key} (no Bearer — es Key)
Submit:
POST https://queue.fal.run/{app_id}
Authorization: Key {key}
Content-Type: application/json
{payload}
Response:
{"request_id": "xxx", "response_url": "...", "status_url": "...", "cancel_url": "...", "queue_position": 0}Poll status:
GET https://queue.fal.run/fal-ai/queue/requests/{request_id}/status
Estados: IN_QUEUE → IN_PROGRESS → COMPLETED
Gotcha: en fal.ai los estados son UPPERCASE con underscore. Diferente a Replicate y AnyFast.
Retrieve result:
GET https://queue.fal.run/fal-ai/queue/requests/{request_id}
El video URL está en r.json()["video"]["url"] para modelos de video (estructura model-specific).
App ID pattern: bytedance/seedance-2.0/{variant} donde variant:
text-to-video— T2Vimage-to-video— I2V (first_frame)fast/text-to-video— fast T2Vfast/image-to-video— fast I2Vreference-to-video— con referencias (imagen + video + audio)
Campos clave para imagen:
image_url→ first frame (string URL o data URI)end_image_url→ last frameimage_urls→ array para reference imagesvideo_urls→ array para reference videosaudio_urls→ array para reference audios
Límite: max 720p. No soporta 1080p ni 2k.
| Síntoma | Causa | Fix |
|---|---|---|
| Auth error | Bearer en vez de Key |
Usar Authorization: Key {api_key} |
| Modelo no encontrado | app_id incorrecto | Verificar variante exacta en fal.ai dashboard |
| Timeout poll | Request en cola larga | Aumentar deadline, el queue puede ser lento |
Base URL: https://api.replicate.com
Auth header: Authorization: Bearer {api_key}
Submit:
POST https://api.replicate.com/v1/predictions
Content-Type: application/json
{
"version": "owner/model:version-sha",
"input": {...}
}
Sync mode (rápido, <60s):
Prefer: wait=60
Poll:
GET https://api.replicate.com/v1/predictions/{prediction_id}
Estados: starting → processing → succeeded | failed | canceled
Response:
{
"id": "...",
"status": "succeeded",
"output": ["https://replicate.delivery/..."], // array de URLs
"error": null,
"logs": "...",
"metrics": {"predict_time": 1.23}
}Gotcha: output es siempre un array, incluso para un solo video/imagen.
Rate limits: 600 req/min submissions, 3000 req/min polling.
| Síntoma | Causa | Fix |
|---|---|---|
starting para siempre |
Cold start del worker | Esperar — puede tomar 30-90s el primer run |
output es null aunque succeeded |
Modelo devuelve output incremental | Hacer otro GET al terminar |
| 429 en poll | Polling demasiado agresivo | Interval mínimo 5s |
Descripción: NVIDIA Inference Microservices — plataforma de inferencia de modelos de IA optimizada con TensorRT. API compatible con OpenAI para LLMs; API propia para modelos de imagen/video. Accesible via NVIDIA AI API (hosted) o como contenedores NIM self-hosted. Tier gratuito disponible via NVIDIA Developer Program.
API Keys: https://build.nvidia.com → "Get API Key"
Base URL: https://integrate.api.nvidia.com/v1
Auth header: Authorization: Bearer nvapi-{key}
Formato de request: idéntico a OpenAI Chat Completions
import requests
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}
payload = {
"model": "deepseek-ai/deepseek-r1", # o cualquier model_id de build.nvidia.com
"messages": [
{"role": "user", "content": "Explica qué es un nodo ComfyUI"}
],
"max_tokens": 1024,
"temperature": 0.6,
"stream": False,
}
r = requests.post(
"https://integrate.api.nvidia.com/v1/chat/completions",
headers=headers,
json=payload,
timeout=(10, 120),
)
text = r.json()["choices"][0]["message"]["content"]Modelos disponibles (selección):
| Categoría | Model ID |
|---|---|
| Reasoning | deepseek-ai/deepseek-r1 |
| Coding | deepseek-ai/deepseek-r1-0528 |
| General | meta/llama-3.1-405b-instruct |
| General | mistralai/mistral-large-2-instruct |
| General | qwen/qwen2.5-72b-instruct |
| Multimodal | google/gemma-3-27b-it |
| Embeddings | nvidia/nv-embedqa-e5-v5 |
Lista completa en: https://build.nvidia.com/models
Streaming:
payload["stream"] = True
with requests.post(url, headers=headers, json=payload, stream=True, timeout=(10, 120)) as r:
for line in r.iter_lines():
if line.startswith(b"data: ") and line != b"data: [DONE]":
chunk = json.loads(line[6:])
delta = chunk["choices"][0]["delta"].get("content", "")
print(delta, end="", flush=True)DeepSeek Reasoning — activar thinking:
payload["chat_template_kwargs"] = {
"enable_thinking": True,
"thinking": True,
}
# La respuesta incluye <think>...</think> antes de la respuesta finalCon SDK oficial de OpenAI (también compatible):
from openai import OpenAI
client = OpenAI(
base_url="https://integrate.api.nvidia.com/v1",
api_key=api_key,
)
response = client.chat.completions.create(
model="deepseek-ai/deepseek-r1",
messages=[{"role": "user", "content": "..."}],
max_tokens=1024,
)Endpoint: POST https://ai.api.nvidia.com/v1/genai/stabilityai/stable-video-diffusion
Auth header: Authorization: Bearer nvapi-{key}
Formato: JSON
import base64, requests
with open("image.png", "rb") as f:
image_b64 = base64.b64encode(f.read()).decode()
payload = {
"image": image_b64, # PNG/JPEG en base64, máx ~200 KB
"seed": 0,
"cfg_scale": 1.8, # adherencia al prompt/imagen (1.0-5.0)
"motion_bucket_id": 127, # solo 127 soportado actualmente
}
r = requests.post(
"https://ai.api.nvidia.com/v1/genai/stabilityai/stable-video-diffusion",
headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
json=payload,
timeout=(15, 300),
)
video_b64 = r.json()["video"] # base64-encoded mp4
video_bytes = base64.b64decode(video_b64)
with open("output.mp4", "wb") as f:
f.write(video_bytes)Parámetros:
| Campo | Tipo | Descripción |
|---|---|---|
image |
string | Imagen de entrada en base64. Máx ~200 KB. PNG o JPEG. |
seed |
int | Semilla para reproducibilidad (0 = aleatorio) |
cfg_scale |
float | Adherencia a la imagen. Rango 1.0–5.0. Default 1.8 |
motion_bucket_id |
int | Intensidad de movimiento. Actualmente solo 127 soportado |
Response:
{"video": "<base64-encoded mp4>"}Gotcha: la imagen debe ser pequeña (≤200 KB aprox). Imágenes más grandes se rechazan. Reducir con PIL antes de enviar.
Preparar imagen para SVD:
from PIL import Image
import io, base64
def prepare_image_for_nim_svd(pil_img, max_kb=180):
pil_img = pil_img.convert("RGB")
# Reducir si es necesario
quality = 90
while True:
buf = io.BytesIO()
pil_img.save(buf, format="JPEG", quality=quality)
if buf.tell() <= max_kb * 1024 or quality <= 40:
break
quality -= 10
return base64.b64encode(buf.getvalue()).decode()Descripción: modelos world foundation de NVIDIA para generación de video a partir de texto o video. Actualmente disponibles como contenedores NIM self-hosted o en early access via NVIDIA AI API.
Modelos relevantes:
| Modelo | Tarea | Descripción |
|---|---|---|
Cosmos-Predict1-7B-Text2World |
T2V | Genera video a partir de texto |
Cosmos-Predict1-7B-Video2World |
V2V | Genera video continuación a partir de video |
Cosmos-Transfer2.5-2B |
V2V stylized | Transfer de estilo/contenido en video |
Endpoint (self-hosted NIM):
POST http://localhost:8000/v1/chat/completions
(sigue formato OpenAI con model = nombre del Cosmos model)
Via API hosted (when available):
POST https://integrate.api.nvidia.com/v1/chat/completions
model: nvidia/cosmos-predict1-7b-text2world
Nota: el acceso a Cosmos via API hosted puede requerir solicitud de acceso o puede no estar disponible en el tier gratuito. Verificar en https://build.nvidia.com/nvidia/cosmos-predict1-7b-text2world.
| Síntoma | Causa | Fix |
|---|---|---|
| 401 Unauthorized | Key mal formada | Key debe ser nvapi-{...} — obtener de build.nvidia.com |
| Imagen rechazada en SVD | Imagen >200 KB | Reducir calidad/resolución antes de base64 |
| 404 modelo no encontrado | Model ID incorrecto | Verificar en build.nvidia.com/models el ID exacto |
| Thinking no activo en DeepSeek | chat_template_kwargs ausente |
Agregar {"enable_thinking": true, "thinking": true} |
| 429 Rate limit | Free tier tiene límites RPM | Implementar retry con backoff |
| Cosmos: timeout | Generación de video world es muy lenta | Usar timeout largo (300-600s) |
| Aspecto | NVIDIA NIM (LLM) | NVIDIA NIM (SVD) | AnyFast | fal.ai |
|---|---|---|---|---|
| Auth | Bearer nvapi-{key} |
Bearer nvapi-{key} |
Bearer {key} |
Key {key} |
| API format | OpenAI-compatible | JSON propio | JSON propio | JSON propio |
| Response | Síncrono | Síncrono (lento) | Asíncrono poll | Asíncrono poll |
| Output | JSON choices[0] |
{"video": base64} |
URL de descarga | URL de descarga |
| Tier gratuito | Sí (limitado) | Sí (limitado) | No | No |
| Modelos | 100+ LLMs | SVD, Cosmos | Seedance 2.0 | Seedance 2.0, FLUX, etc |
Descripción: API de edición de imágenes de OpenAI usando gpt-image-1. Permite inpainting guiado por prompt con máscara RGBA. Distinto a DALL-E 2 — usa comprensión semántica del prompt, no difusión pixel-level.
Base URL: https://api.openai.com/v1/images/edits
Auth header: Authorization: Bearer {openai_api_key}
Formato: multipart/form-data — NO JSON
Requisito: la organización OpenAI debe estar verificada (ID de gobierno en platform.openai.com/settings/organization/general). Sin verificación → HTTP 403.
import requests, base64, io
from PIL import Image
def gpt_inpaint(image_pil, mask_pil, prompt, api_key, quality="medium", size="1024x1024"):
# image_pil: PIL RGB — el image original o recortado
# mask_pil: PIL L (grayscale) — blanco=editar, negro=preservar (convención ComfyUI)
# Convertir imagen a PNG bytes
img_buf = io.BytesIO()
image_pil.convert("RGB").save(img_buf, format="PNG")
img_buf.seek(0)
# Convertir mask: ComfyUI blanco=editar → GPT transparente=editar
mask_rgba = Image.new("RGBA", image_pil.size, (255, 255, 255, 255))
mask_arr = mask_pil.convert("L")
# Donde mask es blanco (editar) → alpha=0 (transparente para GPT)
# Donde mask es negro (preservar) → alpha=255 (opaco para GPT)
import numpy as np
arr = np.array(mask_arr)
alpha = np.where(arr > 127, 0, 255).astype(np.uint8)
rgba_arr = np.zeros((*arr.shape, 4), dtype=np.uint8)
rgba_arr[:, :, :3] = 255
rgba_arr[:, :, 3] = alpha
mask_rgba = Image.fromarray(rgba_arr, "RGBA")
mask_buf = io.BytesIO()
mask_rgba.save(mask_buf, format="PNG")
mask_buf.seek(0)
r = requests.post(
"https://api.openai.com/v1/images/edits",
headers={"Authorization": f"Bearer {api_key}"},
data={
"model": "gpt-image-1",
"prompt": prompt,
"quality": quality, # low | medium | high
"size": size, # 1024x1024 | 1536x1024 | 1024x1536
},
files={
"image[]": ("image.png", img_buf, "image/png"),
"mask": ("mask.png", mask_buf, "image/png"),
},
timeout=(15, 120),
)
r.raise_for_status()
b64 = r.json()["data"][0]["b64_json"]
return Image.open(io.BytesIO(base64.b64decode(b64))).convert("RGB")| Parámetro | Tipo | Valores | Descripción |
|---|---|---|---|
model |
string | "gpt-image-1" |
Requerido |
prompt |
string | — | Descripción COMPLETA de cómo debe verse la imagen final (no solo el área editada) |
image[] |
file | PNG, máx 4 MB | Imagen base |
mask |
file | PNG RGBA, mismas dimensiones | Transparente=editar, opaco=preservar |
quality |
string | low / medium / high |
Default: medium |
size |
string | 1024x1024 / 1536x1024 / 1024x1536 |
Solo estos tres tamaños permitidos |
n |
int | 1–4 | Número de variantes a generar |
output_format |
string | png / webp / jpeg |
Default: png |
ComfyUI MASK: blanco (1.0) = área enmascarada = área a editar
GPT mask PNG: transparente (alpha=0) = área a editar
opaco (alpha=255) = área a preservar
→ Siempre invertir: donde ComfyUI=1 → GPT alpha=0
gpt-image-1 siempre retorna base64 — el parámetro response_format no aplica:
b64 = response.json()["data"][0]["b64_json"]
image_bytes = base64.b64decode(b64)Los únicos tres tamaños de output son 1024x1024, 1536x1024, 1024x1536. Para seleccionar automáticamente según el aspect ratio de la imagen de entrada:
GPT_SIZES = [
(1024, 1024, "1024x1024"),
(1536, 1024, "1536x1024"),
(1024, 1536, "1024x1536"),
]
def pick_gpt_size(img_w, img_h):
ar = img_w / img_h
best = min(GPT_SIZES, key=lambda s: abs(s[0]/s[1] - ar))
return best[2] # "WxH" string| Quality | 1024×1024 | 1536×1024 | 1024×1536 |
|---|---|---|---|
| low | ~$0.011 | ~$0.016 | ~$0.016 |
| medium | ~$0.042 | ~$0.063 | ~$0.063 |
| high | ~$0.167 | ~$0.250 | ~$0.250 |
- Soft masking: gpt-image-1 usa la máscara como guía semántica — puede regenerar ligeramente fuera del área enmascarada. No es pixel-level inpainting clásico.
- Prompt = imagen completa: el prompt debe describir el resultado final completo, no solo el área a cambiar.
input_fidelitybroken: reportado roto con mask en early 2026 — omitir.- Verificación de organización obligatoria: sin verificación → 403.
| Síntoma | Causa | Fix |
|---|---|---|
| HTTP 403 | Org no verificada | Verificar ID en platform.openai.com |
| Imagen entera regenerada | Máscara mal construida (toda transparente) | Verificar inversión: ComfyUI blanco → alpha=0 |
| Imagen >4MB rechazada | PNG muy grande | Comprimir: pil.save(buf, "PNG", optimize=True) o bajar resolución |
input_fidelity ignorado |
Bug conocido | Omitir parámetro |
| 400 size inválido | Tamaño no en lista | Solo 1024x1024, 1536x1024, 1024x1536 |
max_attempts = 8
base_delay = 8
for attempt in range(1, max_attempts + 1):
r = requests.post(endpoint, json=payload, headers=headers, timeout=(30, 600))
if r.ok:
break
if is_transient_error(r):
time.sleep(base_delay)
continue
raise RuntimeError(f"API error {r.status_code}: {r.text}")Errores transitorios típicos:
- 429 (rate limit)
- 502/503 (gateway errors)
- Asset not ready en generación
Las APIs usan nombres inconsistentes: id, Id, ID, task_id, taskId, request_id, prediction_id.
def extract_id(resp, *keys):
def canon(s): return re.sub(r"[^a-z0-9]", "", str(s).lower())
# 1. Exact match
for k in keys:
if k in resp:
return resp[k]
# 2. Canonical match
cmap = {canon(k): v for k, v in resp.items()}
for k in keys:
if canon(k) in cmap:
return cmap[canon(k)]
# 3. Nested en resp["data"]
nested = resp.get("data", {})
if isinstance(nested, dict):
for k in keys:
if k in nested: return nested[k]
raise RuntimeError(f"ID not found. Keys tried: {keys}. Response: {resp}")def poll_until_done(url, headers, done_states, fail_states,
extract_status_fn, extract_url_fn, extract_error_fn,
timeout=1200, interval=5, initial_wait=3):
time.sleep(initial_wait)
deadline = time.time() + timeout
while time.time() < deadline:
r = requests.get(url, headers=headers, timeout=30)
r.raise_for_status()
body = r.json()
status = extract_status_fn(body).lower()
if status in done_states:
url = extract_url_fn(body)
if not url:
raise RuntimeError(f"Status done but no URL: {body}")
return url
if status in fail_states:
raise RuntimeError(f"Task failed: {extract_error_fn(body)}")
time.sleep(interval)
raise TimeoutError(f"Timeout after {timeout}s")1. Crear grupo (o reutilizar existing_group_id)
POST /volc/asset/CreateAssetGroup → Id
2. Subir asset
POST /volc/asset/CreateAsset (JSON para imágenes, multipart para video/audio)
→ Id (el asset_id)
3. Esperar Active (CRÍTICO — sin esto el asset no es usable)
POST /volc/asset/ListAssets con GroupIds=[group_id]
Poll cada 5s hasta Items[].Status == "Active"
Timeout: 300s
4. Referenciar en generación
"image_url": {"url": "asset://asset-id"} (lowercase)
role: "first_frame" | "last_frame" | "reference_image"
¿Necesitas first_frame o last_frame en AnyFast?
SÍ → ASSET UPLOAD (asset:// obligatorio)
NO → ¿son reference_images?
SÍ → BASE64 inline (SeedanceRefImages → reference_images)
más simple, no requiere upload
(base64 también funciona via anyfast_refs)
¿Necesitas reusar la misma imagen en múltiples generaciones?
SÍ → ASSET UPLOAD (guarda group_id y reúsalo)
NO → BASE64 inline (más simple)
# Declarar tipo propio: simplemente usar un string como nombre de tipo
RETURN_TYPES = ("SEEDANCE_API",) # dict con api_key, provider, base_url
RETURN_TYPES = ("ANYFAST_IMAGE_REFS",) # list of content-array dicts
RETURN_TYPES = ("SEEDANCE_IMAGE_LIST",) # list of IMAGE tensorsNo se necesita registro especial — ComfyUI conecta puertos del mismo string.
@classmethod
def INPUT_TYPES(cls):
return {
"required": {"api": ("SEEDANCE_API",)},
"optional": {
"first_frame": ("IMAGE",),
"existing_refs": ("ANYFAST_IMAGE_REFS", {"forceInput": True}),
"file_path": ("STRING", {"forceInput": True}),
}
}forceInput: True fuerza que el valor venga de conexión (no de widget inline).
OUTPUT_NODE = True
def my_func(self, ...):
return {
"ui": {"text": [str(value)]}, # mostrar en UI
"result": (value,) # pasar al siguiente nodo
}@classmethod
def IS_CHANGED(cls, **kwargs):
if kwargs.get("video") is not None:
return float("nan") # siempre re-ejecutar si hay input dinámico
return kwargs.get("video_file", "") # re-ejecutar solo si cambió el archivo# Tensor ComfyUI (B,H,W,C float32 0-1) → base64 data URI
def tensor_to_b64(tensor):
np_img = (tensor[0].numpy() * 255).clip(0, 255).astype(np.uint8)
pil = Image.fromarray(np_img).convert("RGB")
buf = io.BytesIO()
pil.save(buf, format="PNG")
return "data:image/png;base64," + base64.b64encode(buf.getvalue()).decode()
# Tensor → bytes raw (para multipart upload)
def tensor_to_bytes(tensor):
np_img = (tensor[0].numpy() * 255).clip(0, 255).astype(np.uint8)
pil = Image.fromarray(np_img).convert("RGB")
buf = io.BytesIO()
pil.save(buf, format="PNG")
return buf.getvalue(), "image.png", "image/png"
# Tensor MASK ComfyUI (B,H,W float32 0-1) → PIL L (grayscale)
def mask_tensor_to_pil(mask_tensor):
arr = (mask_tensor[0].numpy() * 255).clip(0, 255).astype(np.uint8)
return Image.fromarray(arr, "L")def video_to_path(video_input):
source = video_input.get_stream_source()
if isinstance(source, str):
return source, False # ya es path
source.seek(0)
tmp = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False)
tmp.write(source.read())
tmp.close()
return tmp.name, True # True = caller debe borrar
# En el nodo:
try:
path, cleanup = video_to_path(video)
# ... usar path ...
finally:
if cleanup and os.path.exists(path):
os.remove(path)- ✅ ¿API key correcta y del provider correcto?
- ✅ ¿Base URL correcta? (
https://www.anyfast.ainohttps://api.anyfast.ai) - ✅ ¿Content-Type: application/json en el header? (excepto multipart/form-data para gpt-image-1)
- ✅ ¿Todos los campos REQUERIDOS están presentes? (GroupId, AssetType, URL en CreateAsset)
- ✅ ¿El enum es exacto? (
"2k"no"2K","seedance-fast"no"seedance_fast") - ✅ ¿El array
contenttiene al menos un itemtype: text? - ✅ Para gpt-image-1: ¿la organización está verificada?
- ✅ ¿El asset alcanzó estado
Activeen ListAssets? - ✅ ¿
GroupTypeestá en el filtro de ListAssets? Si sí → eliminarlo - ✅ ¿El formato del asset URI es
asset://(lowercase) noAsset://? - ✅ ¿La URL format en el content item es exactamente
{"url": "asset://id"}? - ✅ ¿El
GroupIdexiste y corresponde al grupo donde se subió el asset?
- ✅ ¿Estás comparando status en lowercase?
- ✅ ¿Cubres todos los estados de éxito? (AnyFast:
success, fal.ai:completed, Replicate:succeeded) - ✅ ¿El BFS walker llega al campo correcto? Agregar print del body completo en primer poll.
- ✅ ¿El timeout es suficiente? Video generation = 1200s. Asset polling = 300s.
Revisar en este orden:
- AnyFast generation:
body.data.fail_reason - AnyFast generic:
body.error.message - fal.ai: status response
error+error_typecuandoCOMPLETEDcon fallo - Replicate:
body.error - gpt-image-1:
body.error.message(HTTP 4xx) obody.errorplano
| Aspecto | AnyFast | fal.ai | Replicate | NVIDIA NIM (LLM) | NVIDIA NIM (SVD) | gpt-image-1 |
|---|---|---|---|---|---|---|
| Auth header | Bearer {key} |
Key {key} |
Bearer {key} |
Bearer nvapi-{key} |
Bearer nvapi-{key} |
Bearer {key} |
| Request format | JSON | JSON | JSON | JSON (OpenAI compat) | JSON | multipart/form-data |
| Response mode | Async poll | Async poll | Async poll | Sync | Sync (lento) | Sync |
| Task ID field | id |
request_id |
id |
N/A | N/A | N/A |
| Status values | QUEUING/PROCESSING/SUCCESS/FAILED |
IN_QUEUE/IN_PROGRESS/COMPLETED |
starting/processing/succeeded/failed |
N/A | N/A | N/A |
| Output video | body.data.data.content.video_url |
body.video.url |
body.output[0] |
— | body.video (base64) |
— |
| Output image | — | — | — | — | — | data[0].b64_json |
| Output text | — | — | — | choices[0].message.content |
— | — |
| Error field | body.data.fail_reason |
body.error |
body.error |
body.error.message |
body.detail |
body.error.message |
| Asset management | Sí | No | No | No | No | No |
| Max resolution | 2k (Ultra) | 720p | Depende modelo | N/A | 576p aprox | 1536×1024 |
| Tier gratuito | No | No | No | Sí (limitado) | Sí (limitado) | No |
| Org verification | No | No | No | No | No | Sí (obligatorio) |
-
AnyFast:
GroupTypeen ListAssets — Si se incluyeGroupType: "AIGC"y los grupos fueron creados sin tipo →Items: []siempre. Omitir siempre. -
AnyFast: casing de
asset://— El endpoint de generación espera lowercaseasset://.Asset://puede ser rechazado. -
AnyFast:
AssetTypeen multipart — Sin este campo el tipo default es"Image". Video y Audio quedan mal registrados silenciosamente. -
AnyFast:
fail_reasonvserror— El mensaje de error real en tasks fallidos está enbody.data.fail_reason, no enbody.error.message. -
fal.ai:
KeynoBearer— El header de auth usa la palabraKey, noBearercomo el resto de APIs. -
fal.ai: status UPPERCASE con underscore —
IN_QUEUE,IN_PROGRESS,COMPLETED. Diferente al patrón de Replicate y AnyFast. -
Replicate: cold start — Estado
startingpuede durar 1-3 minutos en modelos poco usados. No es un error — esperar. -
Replicate:
outputsiempre es array — Incluso si hay un solo resultado, viene como["https://..."]. -
ComfyUI:
anyfast_refsanula todo — Si está conectado, ignora silenciosamentefirst_frame,last_frame,reference_images. Siempre dejar desconectado si no se usa. -
ComfyUI:
forceInput: True— Sin esto, un campo STRING puede ser un widget editable en lugar de puerto de conexión. Añadir cuando el valor siempre debe venir de otro nodo. -
Timeout (connect, read) tuple — Payloads con imágenes base64 pueden ser >1 MB. Un timeout plano de 30s puede cortar el upload. Usar
(30, 600). -
AnyFast 2.0 Ultra:
"2k"lowercase — El API enum es"2k". Enviar"2K"resulta en error de validación. -
Audio sin imagen/video — AnyFast rechaza audio-only. Siempre acompañar con al menos una imagen o video.
-
@image tags auto-append — El nodo Seedance2 añade
@image1…@imageNautomáticamente. Escribirlos explícitamente en el prompt da mejor control de posición pero no son requeridos manualmente. -
NVIDIA NIM SVD: imagen máx ~200 KB — La API rechaza imágenes grandes. Comprimir a JPEG quality=80 o bajar resolución antes de enviar.
-
NVIDIA NIM DeepSeek: thinking desactivado por default — Agregar
chat_template_kwargs: {enable_thinking: true, thinking: true}para activar razonamiento explícito<think>...</think>. -
gpt-image-1: solo 3 tamaños —
1024x1024,1536x1024,1024x1536. No acepta tamaños arbitrarios ni los de NB2. Seleccionar el más cercano al AR de la imagen. -
gpt-image-1: máscara invertida — ComfyUI: blanco=editar. GPT: transparente=editar. Siempre construir RGBA donde mask>0.5 → alpha=0.
-
gpt-image-1: soft masking — El modelo puede regenerar ligeramente fuera del área enmascarada. No es inpainting pixel-level. Usar prompts que describan la imagen completa.
-
gpt-image-1: PNG >4MB rechazado — Comprimir antes de enviar:
pil.save(buf, "PNG", optimize=True)o convertir a JPEG con quality=90.
nodes.py
├── _tensor_to_b64() Tensor → data URI
├── _find_ci() Lookup case-insensitive en dict
├── _walk_dicts() BFS walker para extracción robusta
├── _extract_poll_fields() Extrae status/url/progress de cualquier respuesta
├── _poll_v2() Poll loop para Seedance 2.0
├── _first_frame() Extrae primer frame de video (cv2)
├── _is_anyfast_asset_not_ready_error() Detecta "asset not found" errors
├── _submit_and_poll() Submit + poll + first_frame para AnyFast
├── _fal_generate() Submit + poll para fal.ai
├── _extract_id() Extracción robusta de ID con canonical matching
├── _ensure_group() Crea o reutiliza un asset group
├── _upload_asset() Sube imagen/video/audio a AnyFast assets
├── _wait_for_asset_active() Poll ListAssets hasta Status=Active
├── SeedanceApiKey Nodo: configura provider + key
├── Seedance2 / Fast / Ultra Nodos: generación (hereda _V2Base)
├── SeedanceUploadAsset Nodo: sube asset + espera Active
├── SeedanceAssetRef Nodo: envuelve asset_id en ANYFAST_IMAGE_REFS
├── SeedanceAnyfastImageUpload Nodo: convierte imágenes a base64 refs
├── SeedanceReferenceVideo Nodo: sube video como asset
├── SeedanceReferenceAudio Nodo: sube audio como asset
├── SeedanceRefImages Nodo: colecta hasta 9 imágenes en SEEDANCE_IMAGE_LIST
├── SeedanceExtend Nodo: extiende video por task_id
└── SeedanceSaveVideo Nodo: descarga y guarda el mp4
Flujo de datos de tipos:
SEEDANCE_API → todos los nodos que hacen llamadas HTTP
IMAGE tensors → tensor_to_b64() → data URI → content array
ANYFAST_IMAGE_REFS = list[{type, image_url, role}] → anyfast_refs port → _V2Base.generate()
SEEDANCE_IMAGE_LIST = list[IMAGE tensors] → reference_images port → _V2Base.generate()
STRING (asset://) → SeedanceAssetRef → ANYFAST_IMAGE_REFS