Tutorial de API de Seedance 2.0: De Cero a Su Primer Video con IA (Python)
Genere su primer video con IA usando la API de Seedance 2.0 en Python. Tutorial paso a paso: texto a video, imagen a video, polling asíncrono, webhooks y manejo de errores.

Seedance 2.0 es el modelo de video con IA más avanzado de ByteDance — referencias multimodales, audio nativo, control de cámara cinematográfico y generación de 4 a 15 segundos en hasta 1080p. Este tutorial le guía a través de todo el flujo de trabajo de la API en Python: desde obtener su clave API hasta descargar su primer video generado.
Al finalizar, tendrá código funcional para texto a video, imagen a video, polling asíncrono, manejo de webhooks y recuperación de errores. Cada ejemplo de código aquí fue probado contra una API en vivo.
Nota — Seedance 2.0 vs 1.5: Seedance 2.0 se está implementando progresivamente. Puede probar el flujo de trabajo completo ahora mismo usando
seedance-1.5-pro— cuando 2.0 esté completamente disponible, solo cambie el nombre del modelo. Todos los endpoints, parámetros y formatos de respuesta son idénticos. Las diferencias clave en 2.0: referencias multimodales (mezcle imágenes, videos y audio como entradas), generación de audio nativo, simulación de física mejorada y capacidades de edición de video. Todo en este tutorial funciona con ambas versiones.
Obtenga su clave API gratuita para seguir el tutorial.
Qué Construirá (y Qué Necesita)
Así se ve un video generado por Seedance — creado con una sola llamada a la API:
En este tutorial, escribirá código Python que:
- Envía un prompt de texto → recibe un video generado
- Envía una imagen → la anima en un video
- Realiza polling de resultados de forma asíncrona
- Maneja errores y reintentos como código de producción
- Recibe resultados vía webhook (sin necesidad de polling)
- Cancela tareas en progreso cuando sea necesario
Requisitos Previos
- Python 3.8+ (verifique con
python3 --version) - Biblioteca requests (
pip install requests) - Una clave API de EvoLink (registro gratuito — obtendremos esto en la siguiente sección)
Sin GPU, sin Docker, sin configuración compleja. Solo Python y una clave API.
Consejo Profesional: Si está construyendo una aplicación de producción, considere usar un entorno virtual para aislar dependencias:
python3 -m venv seedance-env source seedance-env/bin/activate # macOS/Linux seedance-env\Scripts\activate # Windows pip install requests flask
Obtenga Su Clave API
Seedance 2.0 está disponible a través de EvoLink, un gateway de API que proporciona acceso unificado a múltiples modelos de video con IA — incluyendo Seedance 2.0, Kling y otros — a través de una sola clave API.
Así es como comenzar:
- Vaya a evolink.ai/early-access y cree una cuenta
- Navegue a Dashboard → API Keys
- Haga clic en Create New Key
- Copie su clave — comienza con
sk-
Almacene su clave de forma segura. No la suba al control de versiones. Usaremos una variable de entorno:
export EVOLINK_API_KEY="sk-your-api-key-here"
En Windows (PowerShell):
$env:EVOLINK_API_KEY="sk-your-api-key-here"
Para hacerlo permanente, agregue la línea a su ~/.bashrc, ~/.zshrc o perfil de PowerShell.
Seguridad: Nunca incluya claves API directamente en su código fuente. Use variables de entorno, archivos
.env(conpython-dotenv), o servicios de gestión de secretos como AWS Secrets Manager o HashiCorp Vault para aplicaciones de producción.
Su Primer Video: Texto a Video
Comencemos con el caso de uso más simple: enviar un prompt de texto y obtener un video. Cree un nuevo archivo llamado seedance_tutorial.py:
import os
import time
import requests
# Configuración de la API
BASE_URL = "https://api.seedance2api.app/v1"
API_KEY = os.environ.get("EVOLINK_API_KEY")
if not API_KEY:
raise ValueError("EVOLINK_API_KEY no está configurada. Ejecute: export EVOLINK_API_KEY='sk-...'")
HEADERS = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json"
}
def wait_for_video(task_id, poll_interval=10):
"""Realiza polling de una tarea hasta que se complete o falle."""
start_time = time.time()
while True:
response = requests.get(
f"{BASE_URL}/tasks/{task_id}",
headers=HEADERS
)
response.raise_for_status()
task = response.json()
status = task["status"]
progress = task.get("progress", 0)
elapsed = int(time.time() - start_time)
print(f" [{elapsed}s] Estado: {status} | Progreso: {progress}%")
if status == "completed":
return task
elif status == "failed":
error_msg = task.get("error", {}).get("message", "Error desconocido")
raise Exception(f"La tarea falló: {error_msg}")
time.sleep(poll_interval)
def download_video(url, filename):
"""Descarga un video desde una URL a un archivo local."""
print(f"Descargando video a {filename}...")
response = requests.get(url, stream=True)
response.raise_for_status()
with open(filename, "wb") as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
size_kb = os.path.getsize(filename) / 1024
print(f"Guardado: {filename} ({size_kb:.0f} KB)")
def text_to_video():
payload = {
"model": "seedance-2.0", # El modelo de IA a usar
"prompt": (
"Un cachorro golden retriever persigue una mariposa a través "
"de un prado iluminado por el sol. La cámara sigue al cachorro con un "
"plano de seguimiento suave mientras las flores silvestres se mecen con la brisa."
),
"duration": 5, # Duración del video: 4-15 segundos
"quality": "720p", # Resolución: 480p, 720p, 1080p
"aspect_ratio": "16:9", # Pantalla ancha estándar
"generate_audio": True # La IA genera audio coincidente
}
print("Enviando solicitud de texto a video...")
response = requests.post(
f"{BASE_URL}/videos/generations", # El endpoint de generación de video
headers=HEADERS, # Headers de autenticación + content-type
json=payload # Serializa automáticamente a JSON
)
response.raise_for_status() # Lanza excepción si no es 200 OK
task = response.json() # Parsea la respuesta JSON
# Registra información clave de la respuesta
print(f"Tarea creada: {task['id']}")
print(f"Tiempo estimado: {task['task_info']['estimated_time']}s")
print(f"Créditos reservados: {task['usage']['credits_reserved']}")
# Realiza polling hasta que el video esté listo
result = wait_for_video(task["id"])
# El array results contiene una o más URLs de video
video_url = result["results"][0]
print(f"\nURL del video: {video_url}")
download_video(video_url, "mi_primer_video.mp4")
return result
if __name__ == "__main__":
text_to_video()
Analicemos cada parámetro en el payload:
model— Qué modelo de Seedance usar. Configureseedance-2.0para el más reciente; useseedance-1.5-prosi 2.0 aún no está disponible en su región.prompt— Su descripción del video. Sea específico sobre el sujeto, acción, movimiento de cámara y ambiente. El prompt anterior usa una estructura de tres partes: sujeto ("cachorro golden retriever"), acción ("persigue una mariposa"), y cámara ("plano de seguimiento suave"). Para técnicas avanzadas de prompts, vea nuestra Guía de Ingeniería de Prompts.duration— Duración del video en segundos (4–15). Videos más cortos se generan más rápido y cuestan menos créditos. Comience con 5 para pruebas.quality— Nivel de resolución.720pes el mejor balance de calidad y velocidad para desarrollo. Use480ppara iteración rápida,1080ppara renders finales.aspect_ratio— Dimensiones de salida.16:9para YouTube/horizontal,9:16para TikTok/Reels/Shorts,1:1para feed de Instagram.generate_audio— Cuando estrue, Seedance genera sonido ambiental y música que coincide con el contenido visual. Agrega ~2 segundos al tiempo de generación.
Ejecútelo:
python seedance_tutorial.py
Qué Devuelve la API
Cuando envía una solicitud de generación, recibe de vuelta un objeto de tarea inmediatamente — el video aún no está listo. Aquí está la respuesta real:
{
"created": 1772203771,
"id": "task-unified-1772203771-yf1dxogh",
"model": "seedance-2.0",
"object": "video.generation.task",
"progress": 0,
"status": "pending",
"task_info": {
"can_cancel": true,
"estimated_time": 132
},
"type": "video",
"usage": {
"billing_rule": "per_second",
"credits_reserved": 17.784,
"user_group": "default"
}
}
Campos clave explicados:
| Campo | Significado |
|---|---|
id | Su ID de tarea — úselo para verificar el estado y recuperar resultados |
status | Comienza como pending, pasa a processing, luego completed o failed |
progress | Porcentaje 0–100. Se actualiza en tiempo real durante processing |
estimated_time | Segundos aproximados hasta completarse (estimación del servidor) |
credits_reserved | Créditos retenidos para este trabajo. Se reembolsan automáticamente si la tarea falla |
task_info.can_cancel | Si puede cancelar esta tarea (siempre true antes de completarse) |
created | Timestamp Unix de cuando se envió la tarea |
usage.billing_rule | Cómo se calculan los créditos — per_second significa que el costo escala con la duración |
Consejo Profesional: Guarde el
iden un archivo o base de datos inmediatamente después del envío. Si su script falla durante el polling, puede reanudar llamando await_for_video()con el ID de tarea guardado. Las tareas persisten en el servidor durante 24 horas.
La Secuencia de Polling
La función wait_for_video() realiza polling cada 10 segundos. Así se ve la salida real:
Enviando solicitud de texto a video...
Tarea creada: task-unified-1772203771-yf1dxogh
Tiempo estimado: 132s
Créditos reservados: 17.784
[0s] Estado: pending | Progreso: 0%
[10s] Estado: processing | Progreso: 7%
[20s] Estado: processing | Progreso: 13%
[30s] Estado: processing | Progreso: 20%
[40s] Estado: processing | Progreso: 27%
[50s] Estado: completed | Progreso: 100%
URL del video: https://files.evolink.ai/.../cgt-20260227224931-8vl7s.mp4
Descargando video a mi_primer_video.mp4...
Guardado: mi_primer_video.mp4 (2847 KB)
Eso es todo — aproximadamente 50 segundos desde la llamada a la API hasta el archivo de video en disco.
Importante: Las URLs de video expiran después de 24 horas. Siempre descargue el archivo rápidamente o almacénelo en su propio almacenamiento (S3, GCS, Cloudflare R2, etc.).
Error Común: No dependa de la URL del video para almacenamiento a largo plazo. Construya su pipeline para descargar inmediatamente después de completarse. Si procesa videos de forma asíncrona, use webhooks (cubiertos a continuación) para activar descargas en el momento en que estén listos.
Para consejos sobre cómo escribir prompts efectivos, vea la Guía de Prompts de Seedance 2.0 — cubre el formato de guion de tomas, palabras clave de estilo y sintaxis de temporización.
Realizar Polling de Resultados: Comprensión del Flujo de Trabajo Asíncrono
La generación de video toma entre 30 y 120 segundos o más dependiendo de la duración y calidad. La API usa un patrón de tarea asíncrona — el mismo patrón utilizado por OpenAI, Stability AI y la mayoría de las demás APIs de IA generativa:
- Enviar → POST a
/v1/videos/generations→ obtiene un ID de tarea instantáneamente - Polling → GET
/v1/tasks/{task_id}→ verifica el estado periódicamente - Recuperar → Cuando
status: "completed", el arrayresultscontiene URLs de video
Este patrón existe porque la generación de video es computacionalmente costosa. Una solicitud HTTP síncrona expiraría mucho antes de que el video esté listo.
Ciclo de Vida del Estado de la Tarea
pending → processing → completed
↘ failed
| Estado | Qué Está Pasando | Duración Típica |
|---|---|---|
pending | La tarea está en cola, esperando recursos GPU | 0–30 segundos |
processing | El video se está generando — progress se actualiza en tiempo real | 30–120 segundos |
completed | ¡Listo! El array results tiene su(s) URL(s) de video | Estado terminal |
failed | Algo salió mal — revise los detalles del error | Estado terminal |
Mejores Prácticas de Polling
Intervalo de polling: 10 segundos es un buen valor predeterminado. Hacer polling demasiado rápido desperdicia solicitudes y podría activar límites de tasa; demasiado lento retrasa su pipeline. Para aplicaciones donde el tiempo es crítico, puede hacer polling cada 5 segundos, pero no hay beneficio en ir más rápido que eso.
Timeout: Establezca un límite superior razonable basado en sus parámetros:
| Configuración | Tiempo Esperado | Timeout Sugerido |
|---|---|---|
| 4s, 480p | 20–40 segundos | 120 segundos |
| 5s, 720p | 30–60 segundos | 180 segundos |
| 10s, 720p | 60–90 segundos | 300 segundos |
| 15s, 1080p | 90–180 segundos | 600 segundos |
Seguimiento del progreso: El campo progress (0–100) le brinda retroalimentación granular — útil para construir barras de progreso en una UI. El progreso se actualiza aproximadamente cada 5–7 segundos durante la fase processing.
Cancelar una Tarea
Si necesita detener una generación en progreso (prompt incorrecto, cambió de opinión), puede cancelarla:
def cancel_task(task_id):
"""Cancela una tarea pendiente o en procesamiento. Los créditos se reembolsan."""
response = requests.post(
f"{BASE_URL}/tasks/{task_id}/cancel",
headers=HEADERS
)
if response.status_code == 200:
print(f"Tarea {task_id} cancelada. Créditos reembolsados.")
else:
print(f"Cancelación fallida: {response.json()}")
La cancelación funciona cuando task_info.can_cancel es true. Una vez que una tarea alcanza completed o failed, no puede cancelarse. Los créditos reservados se reembolsan automáticamente al cancelar.
Consejo Profesional: Incorpore un mecanismo de cancelación en su UI desde el principio. Los usuarios inevitablemente enviarán prompts incorrectos, y esperar 2 minutos por un video malo desperdicia tanto tiempo como créditos.
La función wait_for_video() de nuestro código de configuración maneja el flujo estándar de polling. Si desea omitir el polling por completo, salte a la sección de Webhooks a continuación.
Animar una Imagen (Imagen a Video)
¿Tiene una foto de producto, ilustración de personaje o paisaje que quiera darle vida? Pásela como image_url y Seedance la animará. Esta es una de las características más poderosas para videos de productos de e-commerce — tome una foto estática del producto y conviértala en un anuncio de video atractivo.
Usa la misma configuración y función de polling del primer ejemplo anterior.
# ── Imagen a Video ────────────────────────────────────────────
def image_to_video():
payload = {
"model": "seedance-2.0",
"prompt": (
"@Image1 as the first frame. La escena cobra vida lentamente "
"— las hojas se mecen suavemente, la luz suave se desplaza "
"por el encuadre, y el sujeto parpadea de forma natural."
),
"image_urls": [
"https://example.com/your-image.jpg"
],
"duration": 5,
"quality": "720p",
"aspect_ratio": "16:9"
}
print("Enviando solicitud de imagen a video...")
response = requests.post(
f"{BASE_URL}/videos/generations",
headers=HEADERS,
json=payload
)
response.raise_for_status()
task = response.json()
print(f"Tarea creada: {task['id']}")
result = wait_for_video(task["id"])
video_url = result["results"][0]
download_video(video_url, "imagen_animada.mp4")
return result
Analicemos qué es diferente respecto a texto a video:
image_urls— Un array de URLs de imágenes públicamente accesibles. La API las recupera directamente, por lo que deben ser accesibles desde internet (nolocalhosto URLs de red privada).@Image1en el prompt — Esta etiqueta le dice a Seedance qué imagen referenciar y cómo. Corresponde a la primera URL enimage_urls. Si pasa tres imágenes, usaría@Image1,@Image2,@Image3.- Sin
generate_audio— Omitido aquí, por lo que toma el valortruepor defecto. Puede configurarlo enfalsepara animación silenciosa.
Cómo Funcionan las Etiquetas @Image
La etiqueta @Image1 en su prompt le dice a Seedance cómo usar la imagen. Referencia la primera URL en el array image_urls. Puede pasar hasta 9 imágenes (@Image1 a @Image9). Para una guía completa sobre etiquetas multimodales incluyendo @Video y @Audio, vea la Guía de @Tags Multimodal.
Patrones comunes:
| Patrón del Prompt | Qué Hace | Mejor Para |
|---|---|---|
@Image1 as first frame | Usa la imagen como fotograma de apertura | Exhibiciones de productos, configuración de escena |
@Image1 as last frame | Usa la imagen como fotograma de cierre | Revelaciones de logos, transiciones |
@Image1 as character reference | Mantiene la apariencia del personaje | Personajes consistentes entre clips |
@Image1 as style reference | Aplica el estilo visual de la imagen | Consistencia de marca, dirección de arte |
@Image1 as first frame, @Image2 as last frame | Crea una transición entre dos imágenes | Antes/después, transformaciones |
La respuesta real de nuestra prueba:
{
"created": 1772204037,
"id": "task-unified-1772204036-lify8u5p",
"model": "seedance-2.0",
"object": "video.generation.task",
"progress": 0,
"status": "pending",
"task_info": {
"can_cancel": true,
"estimated_time": 145
},
"type": "video",
"usage": {
"billing_rule": "per_second",
"credits_reserved": 17.784,
"user_group": "default"
}
}
Imagen a video sigue exactamente el mismo patrón asíncrono — enviar, polling, descargar. El estimated_time es ligeramente más largo porque el modelo necesita analizar la imagen de entrada.
Requisitos de Imagen
| Restricción | Valor |
|---|---|
| Máximo de imágenes | 9 por solicitud |
| Tamaño máximo de archivo | 30 MB por imagen |
| Formatos soportados | JPEG, PNG, WebP, BMP, TIFF, GIF |
| Requisito de URL | Debe ser públicamente accesible |
| Resolución recomendada | Al menos 720px en el lado más corto |
Error Común: Pasar una ruta de archivo local en lugar de una URL. El campo
image_urlsrequiere URLs HTTP/HTTPS públicamente accesibles. Si sus imágenes son locales, súbalas primero a S3, Cloudflare R2 o incluso un servicio de alojamiento de archivos temporal.
Restricción: Seedance no soporta subir imágenes de rostros humanos realistas. El sistema las rechaza automáticamente. Use personajes ilustrados o estilizados en su lugar.
Alojar Imágenes para la API
Si no tiene un CDN, aquí hay opciones rápidas para obtener una URL pública:
# Opción 1: Subir a S3 (si tiene AWS)
import boto3
s3 = boto3.client('s3')
s3.upload_file('imagen_local.jpg', 'mi-bucket', 'seedance/input.jpg')
image_url = f"https://mi-bucket.s3.amazonaws.com/seedance/input.jpg"
# Opción 2: Usar una API de alojamiento de archivos temporal
# Muchos servicios ofrecen alojamiento temporal gratuito para pruebas
Para técnicas avanzadas de imagen a video — control de primer-último fotograma, composición multi-imagen y animación de productos de e-commerce — vea la guía profunda de Imagen a Video.
Personalice Sus Videos
Cada parámetro que puede ajustar en una solicitud de generación:
| Parámetro | Tipo | Por Defecto | Opciones | Descripción |
|---|---|---|---|---|
model | string | — | seedance-2.0 | Requerido. El modelo a usar. |
prompt | string | — | ≤2000 tokens | Requerido. Descripción del video con @tags opcionales. |
duration | integer | 5 | 4–15 | Duración del video en segundos. |
quality | string | 720p | 480p, 720p, 1080p | Nivel de resolución. Mayor = más créditos. |
aspect_ratio | string | 16:9 | 16:9, 9:16, 1:1, 4:3, 3:4, 21:9 | Relación de aspecto de salida. |
generate_audio | boolean | true | true, false | Habilitar audio/música generado por IA. |
image_urls | array | — | ≤9 imágenes | Imágenes de referencia. Use @Image1, @Image2... en el prompt. |
video_urls | array | — | ≤3 videos | Videos de referencia. Use @Video1, @Video2... en el prompt. |
audio_urls | array | — | ≤3 archivos de audio | Audio de referencia. Use @Audio1, @Audio2... en el prompt. |
callback_url | string | — | URL HTTPS | Webhook para notificación de completado. |
Nota Seedance 2.0 vs 1.5: Todos los parámetros anteriores funcionan con
seedance-2.0yseedance-1.5-pro. La diferencia clave:video_urls,audio_urlsy referencias multi-imagen (@Image2a@Image9) son características exclusivas de 2.0. Si las usa con 1.5, la API devuelve un error400con un mensaje claro indicando que la característica no está soportada.
Ejemplos Rápidos
Video vertical para redes sociales (TikTok/Reels):
Usa la misma configuración y función de polling del primer ejemplo anterior.
payload = {
"model": "seedance-2.0",
"prompt": "Un barista vierte latte art en cámara lenta. Plano cenital cerrado.",
"duration": 8,
"quality": "1080p",
"aspect_ratio": "9:16", # Vertical para móvil
"generate_audio": True
}
La relación de aspecto 9:16 genera un video de 1080×1920 — resolución nativa para TikTok, Instagram Reels y YouTube Shorts. El nivel de calidad 1080p asegura visuales nítidos en pantallas móviles.
Pantalla ancha cinematográfica con movimiento de cámara:
payload = {
"model": "seedance-2.0",
"prompt": (
"Toma aérea con drone sobre una cordillera brumosa al amanecer. "
"La cámara avanza lentamente, revelando un valle oculto. "
"Gradación de color cinematográfica, iluminación volumétrica."
),
"duration": 10,
"quality": "1080p",
"aspect_ratio": "21:9", # Ultra-ancho cinematográfico
"generate_audio": True
}
Para control programático de cámara — zooms dolly, tomas orbitales y movimientos estilo Hitchcock — vea la Guía de API de Movimiento de Cámara.
Video silencioso para fondo de sitio web:
payload = {
"model": "seedance-2.0",
"prompt": "Partículas fluidas abstractas en azul profundo y dorado. Movimiento lento y meditativo.",
"duration": 15, # Duración máxima para bucles continuos
"quality": "720p",
"aspect_ratio": "21:9", # Fondo ancho
"generate_audio": False # Sin audio para fondos de reproducción automática
}
Borrador económico (iteración rápida):
payload = {
"model": "seedance-2.0",
"prompt": "Un gato con gafas de sol sentado frente a una mesa de DJ. Iluminación de club de neón.",
"duration": 4, # Duración mínima = generación más rápida
"quality": "480p", # Calidad más baja = créditos más baratos
"aspect_ratio": "16:9"
}
Consejo Profesional: Durante el desarrollo, use siempre
duration: 4yquality: "480p". Esta es la combinación más barata y rápida — ideal para iterar en prompts. Una vez satisfecho con el contenido, renderice la versión final en1080pcon la duración deseada.
Estimación del Costo en Créditos
Los créditos escalan con la duración y calidad. Aquí hay una guía aproximada:
| Calidad | 4s | 5s | 10s | 15s |
|---|---|---|---|---|
| 480p | ~8 | ~10 | ~20 | ~30 |
| 720p | ~14 | ~18 | ~36 | ~53 |
| 1080p | ~22 | ~28 | ~55 | ~83 |
Créditos aproximados. Los costos reales se muestran en el campo credits_reserved. Consulte el panel de EvoLink para las tarifas actuales.
El sistema de referencias multimodales — etiquetas @Image, @Video, @Audio — es donde Seedance 2.0 realmente brilla. Puede replicar movimientos de cámara de videos de referencia, mantener consistencia de personajes entre tomas y sincronizar con ritmos de audio. Para una guía completa, lea La Guía Definitiva de @Tags.
Manejar Errores con Elegancia
Las llamadas a la API fallan. Las redes se caen. Los límites de tasa se alcanzan. Así es como construir código resiliente que maneje cada escenario de error real.
Respuestas de Error Comunes
Cada error sigue el mismo formato:
{
"error": {
"message": "descripción de qué salió mal",
"type": "categoría_del_error",
"code": "código_de_error_específico"
}
}
El objeto error siempre contiene message y type. El campo code está presente para la mayoría de los errores pero no todos. Siempre verifique type primero, luego code para detalles específicos.
Aquí están las respuestas de error reales de la API:
401 — Clave API Inválida:
{
"error": {
"message": "Invalid token (request id: 20260227225245660301729AApJNAhJ)",
"type": "evo_api_error"
}
}
Esto significa que su clave API es incorrecta, expiró o fue revocada. Verifique dos veces la variable de entorno EVOLINK_API_KEY. Una causa común: copiar la clave con espacios en blanco al final.
400 — Campo Requerido Faltante:
{
"error": {
"code": "invalid_parameter",
"message": "prompt cannot be empty",
"type": "invalid_request_error"
}
}
El campo prompt es requerido para todas las solicitudes de generación. Esto también se activa si pasa una cadena vacía o un prompt con solo espacios en blanco.
400 — Valor de Parámetro Inválido:
{
"error": {
"code": "invalid_parameter",
"message": "duration must be between 4 and 15",
"type": "invalid_request_error"
}
}
Ocurre cuando pasa duration: 3 o duration: 20. El rango válido es de 4 a 15 segundos inclusive.
400 — Nivel de Calidad No Soportado:
{
"error": {
"code": "invalid_parameter",
"message": "quality must be one of: 480p, 720p, 1080p",
"type": "invalid_request_error"
}
}
Común cuando se pasa "quality": "4k" o "quality": "hd". Use las cadenas exactas: 480p, 720p o 1080p.
402 — Créditos Insuficientes:
{
"error": {
"message": "Insufficient credits. Required: 17.784, Available: 2.100",
"type": "insufficient_quota_error"
}
}
Su cuenta no tiene suficientes créditos. El mensaje le dice exactamente cuántos necesita versus cuántos tiene. Recargue en el panel de EvoLink.
404 — Tarea No Encontrada:
{
"error": {
"message": "Task not found",
"type": "invalid_request_error",
"code": "task_not_found"
}
}
Generalmente significa que el ID de tarea es incorrecto, o la tarea fue creada hace más de 24 horas (las tareas expiran). Verifique dos veces que está usando el campo id de la respuesta de creación, no algún otro campo.
413 — Imagen Demasiado Grande:
{
"error": {
"message": "Image file size exceeds 30MB limit",
"type": "request_too_large_error"
}
}
Comprima su imagen antes de subirla. Para la API, la calidad visual por encima de 2–3 MB rara vez mejora los resultados.
429 — Límite de Tasa Alcanzado:
{
"error": {
"message": "Rate limit exceeded. Please retry after 60 seconds.",
"type": "rate_limit_error"
}
}
Está enviando demasiadas solicitudes. El límite predeterminado es generoso para desarrollo, pero los scripts de procesamiento por lotes pueden alcanzarlo. Implemente retroceso exponencial (ver a continuación).
422 — Rechazo por Moderación de Contenido:
{
"error": {
"message": "Content rejected by safety filter",
"type": "content_policy_violation",
"code": "content_filtered"
}
}
Su prompt o imágenes de entrada activaron el sistema de moderación de contenido. Reformule su prompt para evitar contenido restringido. Los rostros humanos realistas en image_urls son rechazados automáticamente.
Tabla de Referencia de Errores
| Código HTTP | Tipo | Significado | ¿Reintentable? | Acción |
|---|---|---|---|---|
| 400 | invalid_request_error | Parámetros incorrectos | No | Corrija su payload |
| 401 | authentication_error | Clave API inválida | No | Verifique su clave |
| 402 | insufficient_quota_error | Sin créditos | No | Recargue su cuenta |
| 404 | not_found_error | Tarea o modelo no encontrado | No | Verifique task_id / nombre del modelo |
| 413 | request_too_large_error | Payload demasiado grande | No | Reduzca tamaños de archivo |
| 422 | content_policy_violation | Contenido filtrado | No | Reformule el prompt |
| 429 | rate_limit_error | Demasiadas solicitudes | Sí | Espere 60s, reintente |
| 500 | internal_server_error | Problema del servidor | Sí | Reintente después de unos segundos |
| 502 | bad_gateway | Error upstream | Sí | Reintente después de 5s |
| 503 | service_unavailable_error | Servicio caído | Sí | Reintente después de 30s |
Manejo de Errores Listo para Producción
Envuelva sus llamadas a la API con lógica de reintento para errores transitorios:
Usa la misma configuración y función de polling del primer ejemplo anterior.
import random
def generate_video_with_retry(payload, max_retries=3):
"""
Envía una solicitud de generación de video con reintento automático
para errores transitorios (429, 500, 502, 503).
Usa retroceso exponencial con jitter para evitar efecto de manada:
- Intento 1: espera ~1s
- Intento 2: espera ~2s
- Intento 3: espera ~4s
Los errores no reintentables (400, 401, 402, 404, 413, 422) fallan inmediatamente
porque reintentar no solucionará el problema subyacente.
"""
for attempt in range(max_retries):
try:
response = requests.post(
f"{BASE_URL}/videos/generations",
headers=HEADERS,
json=payload,
timeout=30 # Timeout de conexión de 30s
)
# Éxito — devuelve el objeto de tarea
if response.status_code == 200:
return response.json()
# Parsea la respuesta de error
error = response.json().get("error", {})
error_type = error.get("type", "")
error_msg = error.get("message", "Error desconocido")
# Errores no reintentables — falla inmediatamente
if response.status_code in (400, 401, 402, 404, 413, 422):
raise ValueError(
f"Error de API {response.status_code}: {error_msg}"
)
# Errores reintentables — retroceso exponencial con jitter
if response.status_code in (429, 500, 502, 503):
wait = (2 ** attempt) + random.uniform(0, 1)
print(f" Reintento {attempt + 1}/{max_retries} "
f"después de {wait:.1f}s ({error_type}: {error_msg})")
time.sleep(wait)
continue
except requests.exceptions.Timeout:
# El servidor no respondió en 30 segundos
wait = (2 ** attempt) + random.uniform(0, 1)
print(f" Timeout. Reintento {attempt + 1}/{max_retries} "
f"después de {wait:.1f}s")
time.sleep(wait)
continue
except requests.exceptions.ConnectionError as e:
# Fallo de DNS, conexión rechazada, etc.
wait = (2 ** attempt) + random.uniform(0, 1)
print(f" Error de conexión: {e}. Reintento {attempt + 1}/{max_retries} "
f"después de {wait:.1f}s")
time.sleep(wait)
continue
raise RuntimeError(f"Fallo tras {max_retries} reintentos")
Esto maneja:
- Límites de tasa (429) — el retroceso exponencial con jitter evita reintentos sincronizados de múltiples clientes
- Errores de servidor (500/502/503) — reintento automático con retraso creciente
- Timeouts — timeout de 30 segundos previene bloqueos en servidores que no responden
- Caídas de conexión — fallos de DNS, conexiones rechazadas, interrupciones de red
- Errores de cliente (400/401/402/404/413/422) — falla inmediatamente porque reintentar no solucionará la entrada incorrecta
Consejo Profesional: Para sistemas de producción, considere registrar las solicitudes fallidas con su payload completo y respuesta de error. Esto facilita mucho la depuración cuando las cosas salen mal a las 3 AM.
Validar Entrada Antes de Llamadas a la API
Ahorre créditos y tiempo detectando errores obvios localmente:
def validate_payload(payload):
"""
Valida un payload de generación antes de enviarlo a la API.
Detecta errores comunes que resultarían en errores 400.
"""
errors = []
# Campos requeridos
if not payload.get("model"):
errors.append("'model' es requerido")
if not payload.get("prompt") or not payload["prompt"].strip():
errors.append("'prompt' es requerido y no puede estar vacío")
# Rango de duración
duration = payload.get("duration", 5)
if duration < 4 or duration > 15:
errors.append(f"'duration' debe ser 4-15, se obtuvo {duration}")
# Valores de calidad
valid_qualities = {"480p", "720p", "1080p"}
quality = payload.get("quality", "720p")
if quality not in valid_qualities:
errors.append(f"'quality' debe ser uno de {valid_qualities}, se obtuvo '{quality}'")
# Valores de relación de aspecto
valid_ratios = {"16:9", "9:16", "1:1", "4:3", "3:4", "21:9"}
ratio = payload.get("aspect_ratio", "16:9")
if ratio not in valid_ratios:
errors.append(f"'aspect_ratio' debe ser uno de {valid_ratios}, se obtuvo '{ratio}'")
# Validación de URL de imagen
image_urls = payload.get("image_urls", [])
if len(image_urls) > 9:
errors.append(f"Máximo 9 imágenes permitidas, se obtuvieron {len(image_urls)}")
for i, url in enumerate(image_urls):
if not url.startswith(("http://", "https://")):
errors.append(f"image_urls[{i}] debe ser una URL HTTP(S)")
if errors:
raise ValueError(f"Error de validación del payload:\n" + "\n".join(f" - {e}" for e in errors))
return True
Error Común: Olvidar codificar URL los caracteres especiales en las URLs de imagen. Si la ruta de su imagen contiene espacios o caracteres no ASCII, use
urllib.parse.quote()para codificarla.
Configurar Webhooks (Omita el Polling)
El polling funciona bien para scripts y prototipos. Para sistemas de producción, los webhooks son más eficientes — la API envía el resultado a su servidor cuando el video está listo. Sin solicitudes desperdiciadas, sin retraso entre la finalización y la notificación.
Cómo Funciona
Agregue callback_url a su solicitud de generación:
Usa la misma configuración del primer ejemplo anterior.
payload = {
"model": "seedance-2.0",
"prompt": "Una nave espacial despega desde un paisaje desértico al atardecer.",
"duration": 8,
"quality": "720p",
"callback_url": "https://su-servidor.com/api/webhook/seedance"
}
response = requests.post(
f"{BASE_URL}/videos/generations",
headers=HEADERS,
json=payload
)
task = response.json()
print(f"Tarea enviada: {task['id']}")
# No se necesita polling — su webhook recibirá el resultado
Cuando el video está listo, la API envía una solicitud POST a su callback_url con el objeto de tarea completado — exactamente el mismo payload que obtendría del polling.
Requisitos del Webhook
| Requisito | Detalles |
|---|---|
| Protocolo | Solo HTTPS (sin HTTP) — requerido por seguridad |
| Respuesta | Devuelva 2xx dentro de 10 segundos |
| Reintentos | 3 intentos en caso de fallo (intervalos de 1s, 2s, 4s) |
| Longitud de URL | ≤ 2048 caracteres |
| Red | Sin IPs internas/privadas (localhost, 10.x.x.x, 192.168.x.x) |
| Cuerpo | POST JSON con el objeto de tarea completo |
Receptor de Webhook Flask Listo para Producción
Aquí hay un servidor de webhook completo usando Flask con validación adecuada, manejo de errores y descarga asíncrona de videos:
# webhook_server.py
"""
Receptor de webhook de Seedance — maneja callbacks de completado de video.
Ejecute: pip install flask requests
python webhook_server.py
"""
from flask import Flask, request, jsonify
import json
import os
import threading
import requests as req # renombrado para evitar conflicto con flask.request
app = Flask(__name__)
# Directorio para guardar videos completados
OUTPUT_DIR = os.getenv("VIDEO_OUTPUT_DIR", "./videos")
os.makedirs(OUTPUT_DIR, exist_ok=True)
def download_video_async(video_url, task_id):
"""Descarga video en un hilo en segundo plano para no bloquear la respuesta del webhook."""
try:
filename = os.path.join(OUTPUT_DIR, f"{task_id}.mp4")
print(f" Descargando {task_id} a {filename}...")
resp = req.get(video_url, stream=True, timeout=120)
resp.raise_for_status()
with open(filename, "wb") as f:
for chunk in resp.iter_content(chunk_size=8192):
f.write(chunk)
size_mb = os.path.getsize(filename) / (1024 * 1024)
print(f" Guardado: {filename} ({size_mb:.1f} MB)")
except Exception as e:
print(f" Descarga fallida para {task_id}: {e}")
@app.route("/api/webhook/seedance", methods=["POST"])
def handle_webhook():
"""
Maneja el webhook de completado de video de Seedance.
La API envía un POST con el objeto de tarea completo cuando
una generación de video se completa (éxito o fallo).
"""
# Parsea el objeto de tarea entrante
task = request.json
if not task:
return jsonify({"error": "Cuerpo vacío"}), 400
task_id = task.get("id", "desconocido")
status = task.get("status", "desconocido")
model = task.get("model", "desconocido")
print(f"\n{'='*50}")
print(f"Webhook recibido: tarea={task_id}")
print(f" Estado: {status}")
print(f" Modelo: {model}")
if status == "completed":
# Extrae URL(s) de video de los resultados
results = task.get("results", [])
if results:
video_url = results[0]
print(f" URL del video: {video_url}")
# Descarga en hilo en segundo plano para responder rápidamente
thread = threading.Thread(
target=download_video_async,
args=(video_url, task_id)
)
thread.start()
else:
print(f" ADVERTENCIA: Completado pero sin array de resultados!")
elif status == "failed":
error_info = task.get("error", {})
print(f" FALLIDO: {json.dumps(error_info, indent=2)}")
# TODO: Registrar en su sistema de seguimiento de errores (Sentry, etc.)
# TODO: Opcionalmente reintentar la generación con parámetros modificados
else:
print(f" Estado inesperado: {status}")
print(f" Payload completo: {json.dumps(task, indent=2)}")
# Siempre devuelve 200 rápidamente — la API espera una respuesta en 10s
return jsonify({"received": True, "task_id": task_id}), 200
@app.route("/health", methods=["GET"])
def health_check():
"""Endpoint de verificación de salud para balanceadores de carga."""
return jsonify({"status": "ok"}), 200
if __name__ == "__main__":
print(f"Iniciando servidor de webhook...")
print(f"Los videos se guardarán en: {os.path.abspath(OUTPUT_DIR)}")
print(f"URL del webhook: http://localhost:5000/api/webhook/seedance")
app.run(host="0.0.0.0", port=5000, debug=True)
Instale dependencias y ejecute:
pip install flask requests
python webhook_server.py
Decisiones clave de diseño en este servidor:
- Descargas en segundo plano — Creamos un hilo para descargar el video para que el manejador del webhook devuelva
200inmediatamente. La API espera una respuesta en 10 segundos; las descargas de video pueden tardar más. - Endpoint de verificación de salud —
/healthes útil cuando se despliega detrás de un balanceador de carga (ALB, nginx, etc.). - Registro de errores — Las tareas fallidas se imprimen con el payload de error completo. En producción, envíe esto a Sentry, Datadog o su stack de logging.
Exponer Localhost con ngrok
Para desarrollo local, use ngrok para crear una URL HTTPS pública que haga túnel a su servidor local:
# Instalar ngrok (macOS)
brew install ngrok
# O descargue desde https://ngrok.com/download
# Iniciar el túnel
ngrok http 5000
ngrok muestra algo como:
Forwarding https://a1b2c3d4.ngrok-free.app → http://localhost:5000
Use esa URL HTTPS como su callback_url:
payload = {
"model": "seedance-2.0",
"prompt": "Su prompt aquí",
"callback_url": "https://a1b2c3d4.ngrok-free.app/api/webhook/seedance"
}
Error Común: Usar la URL
http://de ngrok en lugar dehttps://. La API de Seedance requiere HTTPS para webhooks — rechazará las URLs de callback HTTP simple con un error 400.
Seguridad del Webhook
En producción, valide que las solicitudes de webhook realmente provienen de la API de EvoLink:
import hmac
import hashlib
def verify_webhook(request):
"""Verifica la autenticidad del webhook usando el patrón de ID de tarea."""
task = request.json
task_id = task.get("id", "")
# Los IDs de tarea de EvoLink siguen un formato específico
if not task_id.startswith("task-unified-"):
return False
# Validación adicional: verificar que existen los campos requeridos
required_fields = ["id", "status", "model", "created"]
if not all(field in task for field in required_fields):
return False
return True
Cuándo Usar Webhooks vs Polling
| Escenario | Recomendación | Por Qué |
|---|---|---|
| Prototipado rápido / scripts | Polling | Más simple, no necesita servidor |
| Aplicación web de producción | Webhooks | Escalable, sin solicitudes desperdiciadas |
| Procesamiento por lotes (100+ videos) | Webhooks + cola | Envíe todos, procese a medida que se completan |
| Herramientas CLI | Polling | No requiere infraestructura de servidor |
| Backend de aplicación móvil | Webhooks | Notificaciones push a usuarios al completarse |
| Sin servidor (Lambda/Cloud Functions) | Webhooks | Combinación perfecta — función activada por cada completado |
Consejo Profesional: Para procesamiento por lotes, combine webhooks con una cola de mensajes (Redis, RabbitMQ, SQS). Envíe todas las solicitudes de generación, luego procese los completados a medida que lleguen a la cola. Esto desacopla el envío del procesamiento y maneja los reintentos con elegancia.
Procesamiento por Lotes: Generar Múltiples Videos
Los casos de uso del mundo real a menudo implican generar muchos videos. Aquí hay un patrón para procesamiento por lotes con limitación de tasa:
Usa la misma configuración y funciones auxiliares del primer ejemplo anterior.
import concurrent.futures
def batch_generate(prompts, max_concurrent=3):
"""
Genera múltiples videos con concurrencia controlada.
Args:
prompts: Lista de cadenas de prompt.
max_concurrent: Máximo de generaciones simultáneas.
Returns:
Lista de tuplas (prompt, resultado_o_error).
"""
results = []
def generate_one(prompt, index):
"""Genera un solo video y devuelve el resultado."""
payload = {
"model": "seedance-2.0",
"prompt": prompt,
"duration": 5,
"quality": "720p"
}
try:
task = generate_video_with_retry(payload)
print(f"[{index}] Enviado: {task['id']}")
result = wait_for_video(task["id"])
video_url = result["results"][0]
download_video(video_url, f"lote_{index}.mp4")
return (prompt, result)
except Exception as e:
print(f"[{index}] Fallido: {e}")
return (prompt, str(e))
# Procesar en lotes para respetar los límites de tasa
with concurrent.futures.ThreadPoolExecutor(max_workers=max_concurrent) as executor:
futures = {
executor.submit(generate_one, prompt, i): i
for i, prompt in enumerate(prompts)
}
for future in concurrent.futures.as_completed(futures):
results.append(future.result())
# Resumen
succeeded = sum(1 for _, r in results if isinstance(r, dict))
print(f"\nLote completo: {succeeded}/{len(prompts)} exitosos")
return results
# Ejemplo de uso
prompts = [
"Un colibrí revoloteando cerca de una flor roja. Macro, poca profundidad de campo.",
"Olas oceánicas chocando contra rocas volcánicas al atardecer. Cámara lenta.",
"Un músico callejero tocando violín bajo la lluvia. Iluminación cinematográfica.",
]
batch_generate(prompts, max_concurrent=2)
Consideraciones clave para el procesamiento por lotes:
max_concurrent=3— No envíe demasiadas solicitudes simultáneamente. Comience con 2–3 y aumente según sus límites de tasa.- ThreadPoolExecutor — Usa hilos (no procesos) porque estamos limitados por I/O (esperando respuestas de API), no por CPU.
- Aislamiento de errores — Cada generación de video es independiente. Un fallo no detiene el lote.
Qué Sigue
Ya domina los fundamentos — texto a video, imagen a video, polling asíncrono, webhooks, manejo de errores y procesamiento por lotes. Aquí es dónde profundizar:
Explorar Características Avanzadas
- Guía de Referencia Multimodal @Tags — Domine el sistema de referencia @Image, @Video, @Audio para generación multi-modal
- Guía de API de Movimiento de Cámara — Replique zooms Hitchcock, tomas de seguimiento en una toma y cámaras orbitales programáticamente
- Guía Profunda de Imagen a Video — Control de primer-último fotograma, composición multi-imagen, videos de productos de e-commerce
- Guía de Video de Producto para E-commerce — Convierta fotos de productos en videos de marketing a escala
- Guía de Ingeniería de Prompts — Formato de guion de tomas, sintaxis de temporización y los prompts detrás de nuestros videos de demo
Documentación de Referencia
- Referencia de API de Generación de Video
- Especificaciones de Referencia Multimodal
- SDKs de Python y Node.js
Construya Algo
Combine lo que ha aprendido. Aquí hay algunas ideas de proyectos:
- Pipeline automatizado de video de producto — Suba fotos de productos, genere videos de marketing en lotes (vea nuestra Guía de Video para E-commerce)
- Motor de contenido para redes sociales — Genere videos verticales de formato corto a partir de descripciones de texto, publique directamente en TikTok/Reels
- Herramienta de storyboard a video — Convierta imágenes secuenciales en escenas animadas con control de movimiento de cámara
- Pipeline de edición de video con IA — Use la extensión de video de Seedance 2.0 para crear narrativas más largas a partir de clips más cortos
¿Listo para construir? Obtenga su clave API gratuita de EvoLink y comience a generar videos hoy.
Preguntas Frecuentes
¿Cuánto tarda la generación de video de Seedance 2.0?
Típicamente entre 30 y 120 segundos dependiendo de la duración y configuración de calidad. Un video de 5 segundos en 720p se completa en aproximadamente 50 segundos. Un video de 15 segundos en 1080p puede tardar de 2 a 3 minutos. La API devuelve un campo estimated_time con cada tarea para que pueda establecer timeouts apropiados. Durante las horas pico, los tiempos de espera en cola pueden agregar de 10 a 30 segundos al total.
¿Qué formatos de imagen acepta la API de Seedance 2.0?
JPEG, PNG, WebP, BMP, TIFF y GIF. Cada imagen debe pesar menos de 30 MB. Puede pasar hasta 9 imágenes por solicitud a través del parámetro image_urls. Las imágenes deben ser URLs públicamente accesibles — la API las recupera directamente. Para mejores resultados, use imágenes de al menos 720px en el lado más corto. Las imágenes de muy baja resolución (por debajo de 256px) pueden producir animaciones borrosas.
¿Puedo generar videos de más de 15 segundos?
El máximo de una sola generación es 15 segundos. Para contenido más largo, genere múltiples clips y concaténelos usando FFmpeg o cualquier editor de video. Seedance 2.0 soporta extensión de video — puede usar el último fotograma de un video generado como primer fotograma de la siguiente generación para crear continuidad sin fisuras. Aquí está el enfoque básico: genere el clip 1, extraiga el último fotograma, páselo como @Image1 as first frame para el clip 2.
¿Cuánto cuesta la API de Seedance 2.0 a través de EvoLink?
El precio se basa en la duración del video y el nivel de calidad. Un video de 5 segundos en 720p cuesta aproximadamente 18 créditos. EvoLink proporciona enrutamiento inteligente que puede reducir costos en comparación con el acceso directo a la API. Consulte su panel de control para las tarifas actuales por segundo. El campo credits_reserved en la respuesta de la API muestra el costo exacto antes de que comience la generación — nunca se le cobrará más de esa cantidad.
¿Cuál es la diferencia entre seedance-1.5-pro y seedance-2.0?
Seedance 2.0 agrega referencias multimodales (mezcle imágenes, videos y audio como entradas), generación de audio nativa, física y consistencia mejoradas, y capacidades de edición de video. La interfaz de la API es idéntica — mismo endpoint, mismos parámetros, mismo formato de respuesta. Puede probar con seedance-1.5-pro hoy y cambiar a seedance-2.0 cambiando el nombre del modelo. Limitaciones clave de 1.5: solo entrada de imagen única (sin @Image2–9), sin referencias de video/audio, sin generación de audio nativa. Vea la comparación de Seedance 2.0 vs Sora 2 para benchmarks.
¿Cómo manejo el error "content rejected by safety filter"?
El sistema de moderación de contenido rechaza prompts que involucren violencia realista, contenido explícito y figuras públicas reales. También rechaza imágenes de rostros humanos realistas subidas a través de image_urls. Para evitar restricciones de rostros, use imágenes de personajes ilustrados, estilizados o de estilo anime. Para rechazos de prompts, reformule para ser menos específico sobre temas restringidos. La respuesta de error incluye type: "content_policy_violation" — verifique esto en su código de manejo de errores para dar a los usuarios un mensaje claro.
¿Puedo usar la API de Seedance en un proyecto Node.js / JavaScript?
Sí. La API REST es independiente del lenguaje — cualquier cliente HTTP funciona. Los conceptos en este tutorial (polling asíncrono, webhooks, manejo de errores) se aplican directamente a Node.js con fetch o axios. EvoLink también proporciona SDKs oficiales de Node.js y Python que manejan el polling y los reintentos por usted.
¿Qué pasa si mi servidor de webhook está caído cuando el video se completa?
La API reintenta la entrega del webhook 3 veces con intervalos crecientes (1s, 2s, 4s). Si los 3 reintentos fallan, el webhook se abandona — pero el video sigue disponible. Siempre puede recurrir al polling con GET /v1/tasks/{task_id} para recuperar el resultado. Por esta razón, es una buena práctica almacenar el ID de tarea al enviar y tener un trabajo en segundo plano que verifique periódicamente si hay tareas que se completaron pero no se recibieron a través del webhook.
¿Hay un límite de tasa en las solicitudes de API?
Sí. El límite de tasa predeterminado es generoso para desarrollo y uso de producción moderado. Si recibe un error 429, implemente retroceso exponencial como se muestra en la sección de manejo de errores. Para casos de uso de alto volumen (miles de videos por día), contacte al soporte de EvoLink para discutir límites de tasa personalizados y capacidad dedicada.
¿Puedo usar Seedance 2.0 para proyectos comerciales?
Sí. Los videos generados a través de la API de EvoLink están licenciados para uso comercial. Usted es propietario de los resultados y puede usarlos en productos, materiales de marketing, entregas a clientes y contenido publicado. Vea la guía de derechos de autor de Seedance 2.0 para términos de licencia detallados y mejores prácticas para uso comercial.
Script Completo
Aquí está el código completo del tutorial en un solo archivo — copie, pegue, agregue su clave API y ejecute:
"""
Tutorial de API de Seedance 2.0 — Script Completo
Docs: https://seedance2api.app/docs/video-generation
Clave API: https://evolink.ai/early-access
"""
import requests
import time
import os
import json
import random
# ── Configuración ─────────────────────────────────────────────
API_KEY = os.getenv("EVOLINK_API_KEY", "sk-your-api-key-here")
BASE_URL = "https://api.evolink.ai/v1"
HEADERS = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json"
}
# ── Funciones Auxiliares Reutilizables ────────────────────────
def wait_for_video(task_id, poll_interval=10, timeout=600):
"""Realiza polling de una tarea de generación de video hasta completarse."""
elapsed = 0
while elapsed < timeout:
response = requests.get(
f"{BASE_URL}/tasks/{task_id}",
headers=HEADERS
)
response.raise_for_status()
task = response.json()
status = task["status"]
progress = task.get("progress", 0)
print(f" [{elapsed}s] Estado: {status} | Progreso: {progress}%")
if status == "completed":
return task
elif status == "failed":
raise RuntimeError(f"Tarea {task_id} fallida: {task}")
time.sleep(poll_interval)
elapsed += poll_interval
raise TimeoutError(f"Tarea {task_id} expiró después de {timeout}s")
def download_video(url, filename="output.mp4"):
"""Descarga un archivo de video desde una URL."""
print(f"Descargando a {filename}...")
resp = requests.get(url, stream=True)
resp.raise_for_status()
with open(filename, "wb") as f:
for chunk in resp.iter_content(chunk_size=8192):
f.write(chunk)
print(f"Guardado: {filename} ({os.path.getsize(filename) / 1024:.0f} KB)")
def generate_video_with_retry(payload, max_retries=3):
"""Envía una solicitud de generación con reintento para errores transitorios."""
for attempt in range(max_retries):
try:
response = requests.post(
f"{BASE_URL}/videos/generations",
headers=HEADERS,
json=payload,
timeout=30
)
if response.status_code == 200:
return response.json()
error = response.json().get("error", {})
if response.status_code in (400, 401, 402, 404, 413, 422):
raise ValueError(
f"Error de API {response.status_code}: "
f"{error.get('message', 'Desconocido')}"
)
if response.status_code in (429, 500, 502, 503):
wait = (2 ** attempt) + random.uniform(0, 1)
print(f" Reintento {attempt+1}/{max_retries} después de {wait:.1f}s")
time.sleep(wait)
continue
except requests.exceptions.RequestException:
wait = (2 ** attempt) + random.uniform(0, 1)
print(f" Reintento {attempt+1}/{max_retries} después de {wait:.1f}s")
time.sleep(wait)
continue
raise RuntimeError(f"Fallo tras {max_retries} reintentos")
def validate_payload(payload):
"""Valida el payload de generación antes de la llamada a la API."""
errors = []
if not payload.get("model"):
errors.append("'model' es requerido")
if not payload.get("prompt") or not payload["prompt"].strip():
errors.append("'prompt' es requerido")
duration = payload.get("duration", 5)
if duration < 4 or duration > 15:
errors.append(f"'duration' debe ser 4-15, se obtuvo {duration}")
quality = payload.get("quality", "720p")
if quality not in {"480p", "720p", "1080p"}:
errors.append(f"Calidad inválida: {quality}")
if errors:
raise ValueError("Validación fallida:\n" + "\n".join(f" - {e}" for e in errors))
def cancel_task(task_id):
"""Cancela una tarea pendiente o en procesamiento."""
response = requests.post(
f"{BASE_URL}/tasks/{task_id}/cancel",
headers=HEADERS
)
if response.status_code == 200:
print(f"Tarea {task_id} cancelada.")
else:
print(f"Cancelación fallida: {response.json()}")
# ── Ejemplo 1: Texto a Video ──────────────────────────────────
def text_to_video():
payload = {
"model": "seedance-2.0",
"prompt": (
"Un cachorro golden retriever persigue una mariposa a través "
"de un prado iluminado por el sol. La cámara sigue al cachorro con un "
"plano de seguimiento suave mientras las flores silvestres se mecen con la brisa."
),
"duration": 5,
"quality": "720p",
"aspect_ratio": "16:9",
"generate_audio": True
}
validate_payload(payload)
task = generate_video_with_retry(payload)
print(f"Tarea: {task['id']} (ETA: {task['task_info']['estimated_time']}s)")
result = wait_for_video(task["id"])
download_video(result["results"][0], "texto_a_video.mp4")
# ── Ejemplo 2: Imagen a Video ─────────────────────────────────
def image_to_video():
payload = {
"model": "seedance-2.0",
"prompt": (
"@Image1 as the first frame. La escena cobra vida lentamente "
"— las hojas se mecen suavemente, la luz suave se desplaza "
"por el encuadre."
),
"image_urls": ["https://example.com/your-image.jpg"],
"duration": 5,
"quality": "720p"
}
validate_payload(payload)
task = generate_video_with_retry(payload)
print(f"Tarea: {task['id']}")
result = wait_for_video(task["id"])
download_video(result["results"][0], "imagen_a_video.mp4")
# ── Ejemplo 3: Video Vertical para Redes Sociales ─────────────
def social_media_video():
payload = {
"model": "seedance-2.0",
"prompt": (
"Un barista vierte latte art en cámara lenta. "
"Plano cenital cerrado, iluminación cálida de café."
),
"duration": 8,
"quality": "1080p",
"aspect_ratio": "9:16",
"generate_audio": True
}
validate_payload(payload)
task = generate_video_with_retry(payload)
print(f"Tarea: {task['id']}")
result = wait_for_video(task["id"])
download_video(result["results"][0], "video_social.mp4")
if __name__ == "__main__":
print("=== Texto a Video ===")
text_to_video()
# print("\n=== Imagen a Video ===")
# image_to_video() # Descomente y configure su URL de imagen
# print("\n=== Video para Redes Sociales ===")
# social_media_video()
Consejo: Para probar con el modelo actualmente disponible, cambie
"seedance-2.0"a"seedance-1.5-pro". La interfaz de la API es idéntica — mismo endpoint, mismos parámetros, mismo formato de respuesta. Cuando Seedance 2.0 esté completamente implementado, simplemente vuelva a cambiar el nombre del modelo.
Empiece a construir → Obtenga su clave API gratuita en EvoLink
Ready to get started?
Top up and start generating cinematic AI videos in minutes.