February 27, 2026

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.

Tutorial de API de Seedance 2.0: De Cero a Su Primer Video con IA (Python)

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:

  1. Envía un prompt de texto → recibe un video generado
  2. Envía una imagen → la anima en un video
  3. Realiza polling de resultados de forma asíncrona
  4. Maneja errores y reintentos como código de producción
  5. Recibe resultados vía webhook (sin necesidad de polling)
  6. 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:

  1. Vaya a evolink.ai/early-access y cree una cuenta
  2. Navegue a Dashboard → API Keys
  3. Haga clic en Create New Key
  4. 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 (con python-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. Configure seedance-2.0 para el más reciente; use seedance-1.5-pro si 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. 720p es el mejor balance de calidad y velocidad para desarrollo. Use 480p para iteración rápida, 1080p para renders finales.
  • aspect_ratio — Dimensiones de salida. 16:9 para YouTube/horizontal, 9:16 para TikTok/Reels/Shorts, 1:1 para feed de Instagram.
  • generate_audio — Cuando es true, 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:

CampoSignificado
idSu ID de tarea — úselo para verificar el estado y recuperar resultados
statusComienza como pending, pasa a processing, luego completed o failed
progressPorcentaje 0–100. Se actualiza en tiempo real durante processing
estimated_timeSegundos aproximados hasta completarse (estimación del servidor)
credits_reservedCréditos retenidos para este trabajo. Se reembolsan automáticamente si la tarea falla
task_info.can_cancelSi puede cancelar esta tarea (siempre true antes de completarse)
createdTimestamp Unix de cuando se envió la tarea
usage.billing_ruleCómo se calculan los créditos — per_second significa que el costo escala con la duración

Consejo Profesional: Guarde el id en un archivo o base de datos inmediatamente después del envío. Si su script falla durante el polling, puede reanudar llamando a wait_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:

  1. Enviar → POST a /v1/videos/generations → obtiene un ID de tarea instantáneamente
  2. Polling → GET /v1/tasks/{task_id} → verifica el estado periódicamente
  3. Recuperar → Cuando status: "completed", el array results contiene 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
EstadoQué Está PasandoDuración Típica
pendingLa tarea está en cola, esperando recursos GPU0–30 segundos
processingEl video se está generando — progress se actualiza en tiempo real30–120 segundos
completed¡Listo! El array results tiene su(s) URL(s) de videoEstado terminal
failedAlgo salió mal — revise los detalles del errorEstado 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ónTiempo EsperadoTimeout Sugerido
4s, 480p20–40 segundos120 segundos
5s, 720p30–60 segundos180 segundos
10s, 720p60–90 segundos300 segundos
15s, 1080p90–180 segundos600 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 (no localhost o URLs de red privada).
  • @Image1 en el prompt — Esta etiqueta le dice a Seedance qué imagen referenciar y cómo. Corresponde a la primera URL en image_urls. Si pasa tres imágenes, usaría @Image1, @Image2, @Image3.
  • Sin generate_audio — Omitido aquí, por lo que toma el valor true por defecto. Puede configurarlo en false para 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 PromptQué HaceMejor Para
@Image1 as first frameUsa la imagen como fotograma de aperturaExhibiciones de productos, configuración de escena
@Image1 as last frameUsa la imagen como fotograma de cierreRevelaciones de logos, transiciones
@Image1 as character referenceMantiene la apariencia del personajePersonajes consistentes entre clips
@Image1 as style referenceAplica el estilo visual de la imagenConsistencia de marca, dirección de arte
@Image1 as first frame, @Image2 as last frameCrea una transición entre dos imágenesAntes/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ónValor
Máximo de imágenes9 por solicitud
Tamaño máximo de archivo30 MB por imagen
Formatos soportadosJPEG, PNG, WebP, BMP, TIFF, GIF
Requisito de URLDebe ser públicamente accesible
Resolución recomendadaAl 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_urls requiere 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ámetroTipoPor DefectoOpcionesDescripción
modelstringseedance-2.0Requerido. El modelo a usar.
promptstring≤2000 tokensRequerido. Descripción del video con @tags opcionales.
durationinteger54–15Duración del video en segundos.
qualitystring720p480p, 720p, 1080pNivel de resolución. Mayor = más créditos.
aspect_ratiostring16:916:9, 9:16, 1:1, 4:3, 3:4, 21:9Relación de aspecto de salida.
generate_audiobooleantruetrue, falseHabilitar audio/música generado por IA.
image_urlsarray≤9 imágenesImágenes de referencia. Use @Image1, @Image2... en el prompt.
video_urlsarray≤3 videosVideos de referencia. Use @Video1, @Video2... en el prompt.
audio_urlsarray≤3 archivos de audioAudio de referencia. Use @Audio1, @Audio2... en el prompt.
callback_urlstringURL HTTPSWebhook para notificación de completado.

Nota Seedance 2.0 vs 1.5: Todos los parámetros anteriores funcionan con seedance-2.0 y seedance-1.5-pro. La diferencia clave: video_urls, audio_urls y referencias multi-imagen (@Image2 a @Image9) son características exclusivas de 2.0. Si las usa con 1.5, la API devuelve un error 400 con 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: 4 y quality: "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 en 1080p con 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:

Calidad4s5s10s15s
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 HTTPTipoSignificado¿Reintentable?Acción
400invalid_request_errorParámetros incorrectosNoCorrija su payload
401authentication_errorClave API inválidaNoVerifique su clave
402insufficient_quota_errorSin créditosNoRecargue su cuenta
404not_found_errorTarea o modelo no encontradoNoVerifique task_id / nombre del modelo
413request_too_large_errorPayload demasiado grandeNoReduzca tamaños de archivo
422content_policy_violationContenido filtradoNoReformule el prompt
429rate_limit_errorDemasiadas solicitudesEspere 60s, reintente
500internal_server_errorProblema del servidorReintente después de unos segundos
502bad_gatewayError upstreamReintente después de 5s
503service_unavailable_errorServicio caídoReintente 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

RequisitoDetalles
ProtocoloSolo HTTPS (sin HTTP) — requerido por seguridad
RespuestaDevuelva 2xx dentro de 10 segundos
Reintentos3 intentos en caso de fallo (intervalos de 1s, 2s, 4s)
Longitud de URL≤ 2048 caracteres
RedSin IPs internas/privadas (localhost, 10.x.x.x, 192.168.x.x)
CuerpoPOST 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 200 inmediatamente. La API espera una respuesta en 10 segundos; las descargas de video pueden tardar más.
  • Endpoint de verificación de salud/health es ú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 de https://. 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

EscenarioRecomendaciónPor Qué
Prototipado rápido / scriptsPollingMás simple, no necesita servidor
Aplicación web de producciónWebhooksEscalable, sin solicitudes desperdiciadas
Procesamiento por lotes (100+ videos)Webhooks + colaEnvíe todos, procese a medida que se completan
Herramientas CLIPollingNo requiere infraestructura de servidor
Backend de aplicación móvilWebhooksNotificaciones push a usuarios al completarse
Sin servidor (Lambda/Cloud Functions)WebhooksCombinació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

Documentación de Referencia

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.

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.