Tutorial
Cómo llamar a hailuo-2.3-fast/image-to-video a través de la API de HiAPI: práctica con curl y Python
La primera solicitud ejecutable del nivel rápido y de bajo coste de imagen a vídeo: del ID del modelo a la descarga del MP4
Qué puede hacer hailuo-2.3-fast/image-to-video por ti
hailuo-2.3-fast/image-to-video es el nivel optimizado para velocidad dentro de la línea de productos de imagen a vídeo de HiAPI. Le das una imagen estática y un prompt de texto que describe el movimiento deseado, y devuelve un clip corto que cobra vida a partir de esa imagen. La palabra «fast» del ID del modelo es lo importante: este nivel intercambia parte de la fidelidad fina de los modelos de vídeo pesados por una menor latencia y un menor coste por renderizado, por lo que es ideal para iterar y para la producción por lotes.
Elige este modelo cuando necesites una primera versión funcional decente en lugar de una pieza final de «imagen principal»: previsualizaciones de storyboard, vídeos en bucle pensados para redes sociales, material de movimiento de fondo o una etapa de borrador dentro de un flujo de trabajo en el que los modelos pesados se encargan del renderizado final. Si tu resultado necesita planos largos, movimientos de cámara complejos o una consistencia estricta de identidad entre muchos fotogramas, deja esas tareas para los niveles más pesados, manteniendo la misma estructura de solicitud, de modo que cambiar sea solo cuestión de ajustar la configuración.
Todo lo que sigue gira en torno a una solicitud que realmente puedes ejecutar. El objetivo es que un MP4 acabe en el disco, no una captura de pantalla de una respuesta exitosa.
Vista general de la solicitud
Un renderizado de vídeo tarda de unos segundos a varios minutos, así que la API es asíncrona: envías una tarea, recibes un identificador de tarea y luego sondeas esa tarea hasta que informa que ha terminado y te entrega la salida. Esto significa que toda integración tiene dos fases —envío y sondeo— y solo la fase de envío varía según el modelo.
La solicitud de envío contiene cuatro elementos que tú controlas:
- Endpoint y método: realiza una solicitud
POSTal endpoint de generación de vídeo de HiAPI. - Cabecera de autenticación: tu clave, con la forma
Authorization: Bearer $HIAPI_API_KEY. Guarda la clave en una variable de entorno y nunca la incrustes en el código que envías. - ID del modelo: la cadena literal
hailuo-2.3-fast/image-to-video. Es lo que selecciona el nivel. - Entrada: un
promptque describe el movimiento y una imagen del primer fotograma. La imagen se proporciona como una URL de acceso público o como un data URI, según lo que acepte tu cuenta en ese endpoint; empezar con una URL es lo más sencillo. El cuerpo JSON se envía conContent-Type: application/json.
Coloca estos cuatro campos en un solo lugar de tu base de código —un diccionario, un archivo de configuración, un constructor alimentado por variables de entorno— para que cambiar más adelante a otro modelo de vídeo de HiAPI requiera cambiar una línea en lugar de reescribir todo. La respuesta es un pequeño sobre JSON en el que el identificador de tarea es el único campo que necesitas persistir; el resto son metadatos de estado.
La solicitud curl que funciona
Este es el paso de envío completo. Sustituye la URL de la imagen por una dirección a la que tu cuenta pueda acceder y exporta primero tu clave.
export HIAPI_API_KEY="your-key-here"
curl -sS -X POST "https://api.hiapi.ai/v1/video/generations" \
-H "Authorization: Bearer $HIAPI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "hailuo-2.3-fast/image-to-video",
"prompt": "slow push in, gentle breeze moving the fabric, film grain",
"image": "https://example.com/first-frame.jpg"
}'
Una respuesta exitosa es un JSON que contiene el identificador de tarea, normalmente con la forma {"id": "task_...", "status": "queued"}. La forma exacta de los campos del sobre puede variar, así que analízalo de forma defensiva: lee el ID de tarea por clave y no por posición, y trata los campos desconocidos como información.
Si estás depurando, añade -i para ver la línea de estado HTTP. Un 401 casi siempre significa que falta la cabecera de autenticación o que la variable se expandió a una cadena vacía; ejecuta echo $HIAPI_API_KEY antes de culpar al cuerpo de la solicitud. Un 400 suele apuntar al cuerpo: una URL de imagen inaccesible, la falta de model o un nombre de campo que no coincide con el endpoint.
Copia el ID de tarea devuelto en una variable de shell para no tener que volver a escribirlo en los pasos siguientes:
TASK_ID=$(curl -sS -X POST "https://api.hiapi.ai/v1/video/generations" \
-H "Authorization: Bearer $HIAPI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"hailuo-2.3-fast/image-to-video","prompt":"slow push in","image":"https://example.com/first-frame.jpg"}' \
| python -c 'import sys,json; print(json.load(sys.stdin)["id"])')
echo "$TASK_ID"
La misma solicitud, escrita en Python
La versión en Python mantiene el cuerpo de la solicitud equivalente byte a byte a la de curl, así que cuando rastrees un error podrás comparar directamente el resultado de curl con el de Python.
import os
import requests
API_BASE = "https://api.hiapi.ai/v1"
MODEL = "hailuo-2.3-fast/image-to-video"
def submit_video(prompt: str, image_url: str) -> str:
resp = requests.post(
f"{API_BASE}/video/generations",
headers={"Authorization": f"Bearer {os.environ['HIAPI_API_KEY']}"},
json={
"model": MODEL,
"prompt": prompt,
"image": image_url,
},
timeout=60,
)
resp.raise_for_status()
return resp.json()["id"]
Aquí hay tres hábitos importantes. os.environ[...] (en lugar de .get) falla ruidosamente cuando falta la variable, que es exactamente la forma en que quieres que falle. Un timeout explícito impide que un socket colgado ocupe un proceso de trabajo indefinidamente. Y raise_for_status() convierte un cuerpo de respuesta 4xx en una excepción registrable, en lugar de en el KeyError que la siguiente línea lanzaría silenciosamente por la falta del ID.
Esta función devuelve deliberadamente solo el ID de tarea. Enviar y esperar son preocupaciones separadas; si las mezclas, el bucle de sondeo se vuelve difícil de probar. Fíjate en que el ID del modelo aparece una sola vez, a nivel de módulo: ese es el único lugar que necesitas cambiar cuando migres a otro modelo de vídeo de HiAPI.
Sondear la tarea hasta que el vídeo esté listo
La comprobación de estado es una solicitud GET al recurso de la tarea. Una tarea en cola o en ejecución devuelve un estado no terminal; una tarea completada devuelve un estado terminal junto con la salida.
import time
def wait_for_video(task_id: str, timeout: float = 600.0) -> dict:
deadline = time.monotonic() + timeout
delay = 2.0
url = f"{API_BASE}/video/generations/{task_id}"
while True:
r = requests.get(
url,
headers={"Authorization": f"Bearer {os.environ['HIAPI_API_KEY']}"},
timeout=30,
)
r.raise_for_status()
task = r.json()
if task.get("status") == "succeeded":
return task
if task.get("status") in {"failed", "cancelled"}:
raise RuntimeError(f"task {task_id} ended: {task}")
if time.monotonic() >= deadline:
raise TimeoutError(f"task {task_id} still {task.get('status')} after {timeout}s")
time.sleep(delay)
delay = min(delay * 1.5, 15.0)
Este es un bucle de reintento, no una espera fija. El retroceso comienza en dos segundos y crece hasta un límite de quince segundos, de modo que los renderizados cortos terminan rápido y los largos no saturan el endpoint. La fecha límite se comprueba antes de dormir, así que el tiempo total de reloj se mantiene dentro del límite en lugar de superarlo en un intervalo. Usa time.monotonic() en lugar de time.time() para que los ajustes del reloj no alarguen el bucle.
Trata failed y cancelled de forma distinta a «seguir esperando»: son terminales pero no exitosos. Cuando ocurra, registra el objeto de tarea completo; es el único diagnóstico que obtendrás, y reproducir un fallo solo con un ID es muy doloroso. Trata las cadenas de estado desconocidas como no terminales y sigue sondeando, para que los nuevos estados intermedios no hagan caer tu proceso de trabajo.
Descargar y procesar la salida
Una tarea completada lleva una o varias URL de salida. Obtén los bytes y escríbelos donde tú tengas el control.
def download_video(task: dict, dest: str) -> str:
url = task["output"][0]["url"] if isinstance(task.get("output"), list) else task["output"]["url"]
with requests.get(url, stream=True, timeout=120) as r:
r.raise_for_status()
with open(dest, "wb") as f:
for chunk in r.iter_content(chunk_size=1 << 20):
f.write(chunk)
return dest
Descarga en streaming en lugar de llamar a .content; un vídeo puede ser lo bastante grande como para que almacenarlo entero en memoria sea un desperdicio en un proceso de trabajo que maneja varias tareas. La forma exacta de la URL de salida puede variar ligeramente según el nivel del modelo, así que comprueba si es una lista antes de indexar; la bifurcación anterior cubre ambos casos.
Regla importante: trata la URL devuelta como un identificador de corta duración, no como almacenamiento. En cuanto la tarea tenga éxito, copia el archivo a tu propio bucket o a una ruta local y sírvelo desde ahí. Persistir la URL en una base de datos y recuperarla unos días después es la razón por la que obtienes 403 y un panel vacío. Para el almacenamiento de objetos, el mismo bucle de bytes en streaming puede alimentar directamente el método de subida de tu cliente; si tu SDK acepta objetos tipo archivo, no necesitas un archivo temporal intermedio.
Consejos prácticos y trampas habituales
- Usa la cabecera de autenticación, no los parámetros de consulta. La clave va en
Authorization: Bearer .... Una clave pegada en una URL es una clave en los registros de acceso. - Que la imagen sea accesible importa más que si es la correcta. Un
400en el envío se debe mucho más a menudo a una URL que la API no puede obtener —localhost, un bucket privado, un host con protección contra hotlinking, un enlace que caduca— que a un problema con el prompt. Antes de depurar cualquier otra cosa, prueba esa URL en una sesión de navegador nueva. - El tipo de contenido del cuerpo. Envía el JSON con
Content-Type: application/json. Si olvidas-Hal enviar con curl, es posible que el servidor ni siquiera analice tu cuerpo. - Sondeo demasiado ajustado. Un
while Truesin pausa te hará sufrir limitación de peticiones y no hará que el renderizado termine antes. El retroceso más una fecha límite es todo el truco. - Sin tiempos de espera en ninguna parte. Establece tiempos de espera en las llamadas de envío, sondeo y descarga. La falta de tiempos de espera convierte a un proveedor lento en una cola bloqueada dentro de tu propio sistema.
- El prompt describe movimiento, no la imagen. Describe lo que cambia entre fotogramas —movimiento de cámara, acción del sujeto, elementos del entorno— y mantenlo breve. Repetir la imagen estática no aporta nada.
- Una configuración, varios modelos. Mantén el ID del modelo, el prompt y la imagen de entrada en el mismo constructor de carga útil. Pasar de este nivel rápido a uno más pesado es cuestión de cambiar una sola cadena.
La forma más rápida de validar la configuración de extremo a extremo es: ejecutar el envío con curl, pegar el ID de tarea en el bucle de sondeo y confirmar que un MP4 acaba en el disco; una vez que eso funciona, el resto es trabajo de empaquetado.
Artículos relacionados
Ver todos los artículos →-
Tutorial
Cómo llamar a hailuo-2.3/text-to-video mediante la API de HiAPI: curl, Python y ejemplos de solicitud ejecutables
-
Tutorial
Uso de Kandinsky 6.0 Pro texto a video en HiAPI: tutorial con curl y Python
Demostración práctica centrada en la solicitud: construir la llamada de prompt a clip de video, enviar la solicitud, sondear el estado de finalización y descargar el resultado
-
Tutorial
Cómo usar kling-4.0/text-to-video a través de la API de HiAPI: curl, Python y un ejemplo de solicitud ejecutable
Guía práctica para desarrolladores: prompts cinematográficos, audio sincronizado, sondeo asíncrono y descarga de resultados