Tutorial
Generación de video a partir de imagen con Veo 3.1 Lite en HiAPI: tutorial de curl y Python
Qué hace veo-3.1-lite/image-to-video
veo-3.1-lite/image-to-video es un modelo de conversión de primer fotograma a video. Le das una imagen estática y anima ese fotograma para generar un video corto. La imagen que proporcionas no es una referencia de estilo ni una fuente de inspiración difusa: es literalmente el fotograma inicial del video de salida, por lo que la composición, la posición del sujeto y el encuadre se prolongan directamente en los primeros segundos de movimiento.
Esta decisión de diseño determina cómo debes considerar la entrada. Una imagen bien iluminada, nítida y bien recortada generará un video que comienza donde esperas. Una imagen borrosa o con un encuadre extraño generará un video que comienza borroso y con un encuadre extraño, y que además se desviará aún más a medida que el modelo genere movimiento. La mayor parte del trabajo para usar bien este modelo ocurre antes de que escribas cualquier código.
En el lado de la salida, el modelo devuelve un video con audio nativo. Esto es precisamente lo que lo distingue de las canalizaciones antiguas de imagen a video, en las que primero se generaba una imagen muda y luego se empalmaba una pista de audio independiente. Aquí, el audio se genera como parte de la misma tarea, por lo que el sonido ambiental y la escena visual se mantienen coherentes: no hay que mezclar manualmente ni se producen desajustes temporales entre los efectos de sonido y el movimiento al que corresponden.
La generación es asíncrona. Envías una tarea, obtienes un identificador y luego consultas el estado hasta que la tarea finaliza. Es una elección arquitectónica deliberada: la generación de video tarda mucho más que el tiempo de espera típico de una solicitud HTTP, por lo que la API desacopla el envío de la finalización. Todos los ejemplos de este tutorial siguen este patrón de «primero enviar, luego consultar».
En resumen: entra una imagen, sale un video con audio, entregado mediante una tarea asíncrona. Ten presente este modelo mental y el resto de la API resultará muy directo.
Antes de enviar la primera solicitud
Para que la primera llamada tenga éxito, hay que preparar tres cosas.
Una clave de API
Cada solicitud a HiAPI.ai se autentica con una clave de API. Créala en el panel de tu cuenta y cárgala desde una variable de entorno, en lugar de pegarla directamente en el código fuente:
export HIAPI_API_KEY="your_api_key_here"
Si alguna clave se ha llegado a confirmar en un repositorio, rótala de inmediato. Sustituir una clave cuesta poco; filtrarla cuesta mucho.
Una imagen de primer fotograma accesible
El modelo necesita descargar la imagen por sí mismo, así que la imagen debe ser accesible mediante URL: un enlace a almacenamiento de objetos público, una ruta de CDN o cualquier endpoint HTTP que no requiera autenticación interactiva. Una URL firmada con una validez prolongada sirve; una URL que requiere una sesión iniciada, no. Usa un formato común como PNG o JPEG y elige una resolución razonable: una imagen nítida y de tamaño moderado proporciona al modelo detalles limpios sin desperdiciar ancho de banda.
Si tu imagen de origen está detrás de un servicio interno, súbela primero al almacenamiento de objetos y pasa la URL generada. Esta suele ser la causa más habitual de que falle el primer intento.
La ruta del endpoint
El modelo se identifica con veo-3.1-lite/image-to-video. Trata esta cadena como el identificador del modelo que se envía con la tarea y no la codifiques de forma fija en una docena de sitios: colócala en una constante, porque es probable que más adelante quieras cambiar entre distintas variantes del modelo.
Con la clave exportada, una URL de imagen descargable y el identificador del modelo a mano, ya estás listo para enviar.
Una solicitud curl funcional
La llamada de envío crea una tarea de generación. Un ejemplo completo y ejecutable es el siguiente:
curl -X POST "https://api.hiapi.ai/v1/video/generations" \
-H "Authorization: Bearer $HIAPI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "veo-3.1-lite/image-to-video",
"image": "https://your-bucket.example.com/first-frame.jpg",
"prompt": "晨光在场景中缓缓移动,镜头缓慢推进",
"duration": 5
}'
Analicemos cada elemento:
- La URL es el endpoint de generación de HiAPI. Todo el tráfico de modelos pasa por esta única interfaz, por lo que cambiar de modelo más adelante solo requiere modificar el campo
model, no el endpoint. - La cabecera
Authorizationlleva tu token de portador. La forma$HIAPI_API_KEYlee desde una variable de entorno, por lo que la clave nunca aparece en el historial del shell. modelidentificaveo-3.1-lite/image-to-video.imagees la URL del primer fotograma. Es este campo el que convierte la llamada en imagen a video en lugar de texto a video.promptes opcional, pero muy recomendable. Describe el movimiento que deseas —movimiento de cámara, cambios de iluminación, acciones del sujeto— y no una descripción de la imagen estática, porque el modelo ya puede ver esa imagen.durationsolicita la duración del video en segundos, limitada por el rango admitido por el modelo.
La respuesta es un objeto de tarea que contiene un id. Guárdalo:
{
"id": "job_abc123",
"status": "queued",
"model": "veo-3.1-lite/image-to-video"
}
Un estado queued o processing es el resultado esperado: aún no tienes el video, solo una promesa. La siguiente sección explica cómo convertir esa promesa en un archivo.
La misma llamada escrita en Python
Esta llamada curl se puede traducir directamente a Python con requests. La versión siguiente encapsula el envío en una pequeña función, fácil de reutilizar en una canalización:
import os
import requests
API_KEY = os.environ["HIAPI_API_KEY"]
BASE_URL = "https://api.hiapi.ai/v1"
HEADERS = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
}
def submit_video_job(image_url: str, prompt: str, duration: int = 5) -> str:
payload = {
"model": "veo-3.1-lite/image-to-video",
"image": image_url,
"prompt": prompt,
"duration": duration,
}
response = requests.post(
f"{BASE_URL}/video/generations",
headers=HEADERS,
json=payload,
timeout=30,
)
response.raise_for_status()
data = response.json()
print(f"Submitted job {data['id']} ({data['status']})")
return data["id"]
if __name__ == "__main__":
job_id = submit_video_job(
image_url="https://your-bucket.example.com/first-frame.jpg",
prompt="晨光在场景中缓缓移动,镜头缓慢推进",
)
Hay varios detalles que conviene conservar:
raise_for_status()convierte las respuestas que no son 2xx en excepciones lo antes posible. Sin ello, un error de autenticación o de validación parece una llamada exitosa que devolvió un cuerpo de respuesta inesperado.- El tiempo de espera de 30 segundos solo se aplica al envío. El envío debe devolver rápido; el trabajo prolongado ocurre en el servidor. Si el propio envío agota el tiempo de espera, el problema está en la conectividad o en el tamaño de la carga útil de la solicitud, no en la velocidad de generación.
- Devolver el id de la tarea en lugar de toda la respuesta mantiene la función componible: es el llamador quien decide cómo gestionar la tarea.
Con esto, la función se puede incorporar sin modificaciones a un worker de cola, un endpoint de Flask o una celda de notebook.
Consultar el estado de una tarea asíncrona
La generación de video tarda minutos, no milisegundos. Consulta el endpoint de estado de la tarea a intervalos cercanos al ritmo humano y detente en cuanto la tarea alcance un estado terminal:
import time
import requests
def wait_for_video(job_id: str, timeout: int = 600, interval: int = 10) -> dict:
deadline = time.time() + timeout
url = f"{BASE_URL}/video/generations/{job_id}"
while time.time() < deadline:
response = requests.get(url, headers=HEADERS, timeout=30)
response.raise_for_status()
data = response.json()
status = data["status"]
print(f"Job {job_id}: {status}")
if status == "completed":
return data
if status == "failed":
raise RuntimeError(f"Generation failed: {data}")
time.sleep(interval)
raise TimeoutError(f"Job {job_id} did not finish within {timeout}s")
Tres prácticas hacen que esto funcione de forma fiable en producción:
- Retroceso exponencial. Empieza con un intervalo corto y auméntalo gradualmente, con un límite máximo razonable. La primera consulta suele devolver todavía
queued, así que bombardear el endpoint durante los primeros segundos no aporta nada. - Tiempo de espera máximo. Limita siempre el tiempo total de espera. Una tarea atascada en
processingdebe manifestarse como un error que active una alerta, no como un bucle infinito. - Persistir el id de la tarea. Guárdalo en la base de datos antes de empezar a consultar. Si el proceso falla a mitad de la espera, tras reiniciarlo se puede seguir consultando sin tener que reenviar la tarea y pagar dos veces por el mismo trabajo.
Considera solo completed y failed como estados que finalizan el bucle. Todo lo demás —queued, processing— significa seguir esperando.
Una advertencia más: si ejecutas este código en un servidor web, no consultes el estado dentro del manejador de la solicitud. Envía la tarea, devuelve el id al cliente y deja que un worker en segundo plano realice la espera. De lo contrario, una generación lenta ocupará la conexión durante minutos.
Descargar el video de salida
Cuando la tarea finaliza, la respuesta de estado incluye la URL del video terminado. Descárgalo como datos binarios y escríbelo en disco con la extensión adecuada:
def download_video(job: dict, path: str = "output.mp4") -> str:
video_url = job["output"]["url"]
response = requests.get(video_url, headers=HEADERS, timeout=120, stream=True)
response.raise_for_status()
with open(path, "wb") as f:
for chunk in response.iter_content(chunk_size=1 << 20):
f.write(chunk)
print(f"Saved {path}")
return path
Hay varias cosas que hacer bien:
- Escribir en modo binario. Abrir con
"w"en lugar de"wb"corrompe el archivo silenciosamente: la descarga parece exitosa, pero el video no se reproduce. - Procesar archivos grandes en streaming. La combinación de
stream=Truey la copia por bloques anterior mantiene la memoria estable sea cual sea el tamaño del video. - Calcular una suma de verificación si vas a archivar salidas a gran escala. Es una sola línea de código, pero detecta descargas truncadas antes de que lleguen al usuario.
Sobre el audio: el video devuelto ya incluye su pista de audio nativa. No hay un archivo de audio separado que obtener ni un paso de mezcla. Si vuelves a empaquetar el archivo aguas abajo —por ejemplo, para cambiar el formato del contenedor—, incluye también el flujo de audio. Un error habitual es ejecutar una conversión que elimina el audio de forma predeterminada y luego preguntarse adónde fue.
Por último, la URL de salida suele tener caducidad. Descarga y guarda cuanto antes en tu propio bucket, en lugar de almacenar la URL y recuperarla días después, cuando el enlace probablemente ya haya caducado.
Puntos de fallo habituales
A continuación se enumeran los errores que más tiempo hacen perder al solucionar problemas de llamadas de imagen a video:
URL de imagen inaccesible
Es, con diferencia, la causa de fallo más común. El modelo descarga tu imagen en el servidor, así que una URL que funciona en tu navegador con la sesión iniciada no demuestra nada. Antes de enviar la tarea, pruébala con una solicitud simple sin autenticación. La validez de una URL firmada debe ser considerablemente mayor que el tiempo de generación.
Errores de autenticación que parecen errores del modelo
Una cabecera Authorization ausente o mal formada normalmente devuelve un 401 claro, pero a veces los desarrolladores envuelven el código de inicialización en una lógica que se traga los códigos de estado y luego informan de un «fallo del modelo». Antes de depurar la carga útil de la solicitud, comprueba el código de estado original.
Consultar con demasiada frecuencia o no consultar en absoluto
Consultar cada 100 milisegundos desperdicia cuota y puede activar limitaciones de velocidad. Consultar una sola vez, ver processing y abandonar significa que la tarea se completa sin que nadie lo sepa. Sigue el patrón de retroceso con tiempo de espera descrito anteriormente.
Confundir el envío con la finalización
Un envío correcto devuelve una tarea, no un video. El código que lee el campo url de la respuesta de envío no encontrará nada. Solo la respuesta del estado completado incluye la salida.
Prompts que describen la imagen
Un prompt como «un gato sentado en el alféizar de la ventana» solo repite lo que el modelo ya ve. Un prompt que describe movimiento —«la cámara se aleja lentamente mientras empieza a llover»— produce mejores resultados. Describe los cambios, no el contenido.
Perder el id de la tarea en caso de fallo
Si el proceso muere entre el envío y la finalización, un id de tarea no persistido significa que el trabajo no se puede recuperar. Persístelo y consulta de forma asíncrona desde el worker. Una pequeña decisión de diseño evita la depuración más larga.
Artículos relacionados
Ver todos los artículos →-
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
-
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