Tutoriale
Usare Veo 3.1 Lite image-to-video su HiAPI: tutorial con curl e Python
Cosa può fare veo-3.1-lite/image-to-video
veo-3.1-lite/image-to-video è un modello che converte il primo fotogramma in video. Gli si fornisce un'immagine statica e la anima, generando un breve video. L'immagine fornita non è un riferimento di stile né una vaga fonte di ispirazione: è letteralmente il fotogramma iniziale del video di output, quindi composizione, posizione del soggetto e inquadratura si prolungano direttamente nei primi secondi di movimento.
Questa scelta progettuale determina come considerare l'input. Un'immagine ben illuminata, nitida e correttamente ritagliata genera un video che inizia dal punto previsto. Un'immagine sfocata o con un'inquadratura strana genera un video che inizia sfocato e con un'inquadratura strana, e che tende a derivare ulteriormente man mano che il modello genera il movimento. Gran parte del lavoro per usare bene questo modello avviene prima di scrivere qualsiasi riga di codice.
Lato output, il modello restituisce un video con audio nativo. È proprio questo a distinguerlo dalle vecchie pipeline di image-to-video — quelle che prima generavano immagini mute e poi montavano separatamente una traccia audio indipendente. Qui l'audio viene generato come parte dello stesso task, quindi i suoni ambientali restano coerenti con la scena visiva — senza missaggio manuale e senza sfasamenti temporali tra effetti sonori e movimento corrispondente.
La generazione è asincrona. Si invia un task, si ottiene un identificatore e si esegue il polling finché il task non è completato. È una scelta architetturale deliberata: la generazione video richiede molto più tempo del timeout tipico di una richiesta HTTP, quindi l'API disaccoppia l'invio dal completamento. Ogni esempio di questo tutorial segue la forma "prima invia, poi esegui il polling".
In sintesi: si inserisce un'immagine, si ottiene un video con audio, consegnato tramite task asincrono. Tenendo a mente questo modello mentale, il resto dell'API diventa molto lineare.
Prima di inviare la prima richiesta
Per riuscire al primo tentativo servono tre cose.
Una chiave API
Ogni richiesta a HiAPI.ai viene autenticata con una chiave API. Creane una nella dashboard dell'account e caricala da una variabile d'ambiente, invece di incollarla direttamente nel file sorgente:
export HIAPI_API_KEY="your_api_key_here"
Se una chiave è mai stata committata in un repository, ruotala immediatamente. Sostituire una chiave costa poco, una fuga di chiave costa molto.
Un'immagine del primo fotogramma accessibile
Il modello deve recuperare l'immagine da solo, quindi l'immagine deve essere accessibile tramite URL — un link a un object storage pubblico, un percorso CDN o qualsiasi endpoint HTTP che non richieda autenticazione interattiva. Gli URL firmati con lunga scadenza vanno bene; gli URL che richiedono una sessione di login no. Usa formati comuni come PNG o JPEG e scegli una risoluzione ragionevole: un'immagine nitida e di dimensioni moderate fornisce al modello dettagli puliti senza sprecare banda.
Se l'immagine di origine si trova dietro un servizio interno, caricala prima su un object storage e poi passa l'URL generato. Questo è di solito il motivo più comune di fallimento al primo tentativo.
Il percorso dell'endpoint
Il modello è identificato da veo-3.1-lite/image-to-video. Tratta questa stringa come identificatore del modello da inviare con il task, senza codificarla a mano in una dozzina di punti — mettila in una costante, perché in futuro vorrai probabilmente passare tra varianti diverse del modello.
Con la chiave esportata, l'URL immagine recuperabile e l'identificatore del modello a disposizione, sei pronto per inviare.
Una richiesta curl funzionante
La chiamata di invio crea un task di generazione. Un esempio completo e funzionante è il seguente:
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": "La luce del mattino si muove lentamente nella scena, la camera si avvicina lentamente",
"duration": 5
}'
Analizzando voce per voce:
- URL è l'endpoint di generazione su HiAPI. Tutto il traffico dei modelli passa da questa unica interfaccia, quindi in futuro cambiare modello richiederà solo di modificare il campo
modele non l'endpoint. - Header
Authorizationtrasporta il tuo bearer token. La forma$HIAPI_API_KEYlegge dalla variabile d'ambiente, così la chiave non appare mai nella cronologia della shell. modelidentificaveo-3.1-lite/image-to-video.imageè l'URL del primo fotogramma. È proprio questo campo a rendere la chiamata image-to-video anziché text-to-video.promptè opzionale ma fortemente consigliato. Descrive il movimento che desideri — movimento di camera, cambiamenti di illuminazione, azioni del soggetto — non la descrizione dell'immagine statica, perché il modello vede già quell'immagine.durationè la durata richiesta del video in secondi, entro i limiti supportati dal modello.
La risposta è un oggetto task contenente un id. Salvalo:
{
"id": "job_abc123",
"status": "queued",
"model": "veo-3.1-lite/image-to-video"
}
Lo stato queued o processing è il risultato atteso — non hai ancora il video, hai solo una promessa. La sezione successiva spiega come trasformare la promessa in un file.
La stessa chiamata scritta in Python
Questa chiamata curl si traduce direttamente in Python con requests. La versione seguente incapsula l'invio in una piccola funzione, facile da riutilizzare in una pipeline:
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="La luce del mattino si muove lentamente nella scena, la camera si avvicina lentamente",
)
Alcuni dettagli che vale la pena mantenere:
raise_for_status()trasforma subito le risposte non 2xx in eccezioni. Senza di esso, errori di autenticazione o di validazione sembrano chiamate riuscite che restituiscono un body inatteso.- Il timeout di 30 secondi si applica solo all'invio. L'invio deve tornare rapidamente; il lungo lavoro avviene lato server. Se l'invio stesso va in timeout, il problema è nella connettività o nella dimensione del payload, non nella velocità di generazione.
- Restituire l'id del task anziché l'intera risposta mantiene la funzione componibile — è il chiamante a decidere come gestire il task.
A questo punto la funzione può essere inserita senza modifiche in un queue worker, in un endpoint Flask o in una cella di notebook.
Polling del task asincrono
La generazione video richiede minuti, non millisecondi. Esegui il polling dell'endpoint di stato del task a intervalli vicini al ritmo umano e fermati appena il task raggiunge uno stato terminale:
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")
Tre pratiche rendono questo approccio affidabile in produzione:
- Backoff esponenziale. Parti da un intervallo breve e aumentalo gradualmente, con un limite massimo ragionevole. Il primo polling di solito restituisce ancora
queued, quindi martellare l'endpoint nei primi secondi non porta alcun beneficio. - Timeout rigido. Metti sempre un limite al tempo di attesa totale. Un task bloccato in
processingdeve manifestarsi come errore da segnalare, non come un ciclo infinito. - Persistenza dell'id del task. Scrivilo nel database prima di iniziare il polling. Se il processo cade durante l'attesa, al riavvio puoi riprendere il polling senza inviare di nuovo e pagare due volte lo stesso lavoro.
Considera solo completed e failed come stati che terminano il ciclo. Tutti gli altri — queued, processing — significano che bisogna continuare ad attendere.
Un altro promemoria: se esegui questo codice su un server web, non fare polling all'interno del gestore delle richieste. Invia, restituisci l'id del task al client e lascia che un worker in background completi l'attesa. Altrimenti una generazione lenta occuperà la connessione per minuti.
Scaricare il video di output
Al completamento del task, la risposta di stato contiene l'URL del video finito. Scaricalo come dati binari e scrivilo su disco con l'estensione appropriata:
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
Ci sono alcune cose da fare correttamente:
- Scrivi in modalità binaria. Aprire con
"w"invece di"wb"corrompe silenziosamente il file; il download sembra riuscito ma il video non si riproduce. - Gestisci in streaming i file grandi. L'uso di
stream=Truecon la copia a blocchi mantiene la memoria stabile indipendentemente dalla dimensione del video. - Calcola un checksum se archivi gli output su larga scala. È una sola riga di codice ma permette di intercettare i download troncati prima che raggiungano l'utente.
Sull'audio: il video restituito contiene già la sua traccia audio nativa. Non c'è un file audio separato da scaricare né un passaggio di missaggio. Se rimuxi il file a valle — ad esempio cambiando il formato del contenitore — porta con te anche il flusso audio. Un errore comune è eseguire una conversione che rimuove l'audio per impostazione predefinita e poi chiedersi dove sia finito.
Infine, l'URL di output di solito ha scadenza. Scarica e salva al più presto nel tuo bucket, invece di conservare l'URL e recuperarlo giorni dopo; a quel punto il link potrebbe essere scaduto.
Punti di fallimento comuni
Ecco gli errori che fanno perdere più tempo durante la risoluzione dei problemi delle chiamate image-to-video:
URL immagine non accessibile
È di gran lunga la causa più comune di fallimento. Il modello recupera l'immagine lato server, quindi un URL che funziona nel tuo browser già autenticato non dimostra nulla. Prima di inviare il task, testalo con una normale richiesta senza autenticazione. La scadenza di un URL firmato deve essere significativamente più lunga del tempo di generazione.
Errori di autenticazione che sembrano errori del modello
Un header Authorization mancante o malformato di solito restituisce un chiaro 401, ma a volte gli sviluppatori avvolgono il codice di inizializzazione in una logica che inghiotte gli status code e poi segnalano un "guasto del modello". Prima di eseguire il debug del payload, controlla lo status code grezzo.
Polling troppo frequente o assente
Eseguire il polling ogni 100 millisecondi spreca quota e può attivare limiti di velocità. Eseguirlo una sola volta, vedere processing e arrendersi significa che il task riesce ma nessuno lo sa. Segui lo schema di backoff con timeout descritto sopra.
Confondere l'invio con il completamento
Un invio riuscito restituisce un task, non un video. Il codice che legge il campo url dalla risposta di invio non troverà nulla. Solo la risposta di stato completato contiene l'output.
Prompt che descrivono l'immagine
Un prompt come "un gatto seduto sul davanzale" ripete solo ciò che il modello vede già. Un prompt che descrive il movimento — "la camera si allontana lentamente mentre inizia a piovere" — produce risultati migliori. Descrivi i cambiamenti, non il contenuto.
Perdita dell'id del task in caso di crash
Se il processo muore tra l'invio e il completamento, un id del task non persistito significa che il lavoro non può essere recuperato. Rendilo persistente ed esegui il polling in modo asincrono da un worker. Una piccola scelta progettuale evita il debug più lungo.
Articoli correlati
Vedi tutti gli articoli →-
Tutoriale
Come chiamare hailuo-2.3-fast/image-to-video tramite l'API HiAPI: pratica con curl e Python
La prima richiesta funzionante per il livello image-to-video veloce ed economico — dall'ID del modello al download di un MP4
-
Tutoriale
Come chiamare hailuo-2.3/text-to-video tramite l'API HiAPI: curl, Python ed esempi di richiesta eseguibili
-
Tutoriale
Utilizzare Kandinsky 6.0 Pro su HiAPI per la generazione di video da testo: tutorial con curl e Python
Dimostrazione pratica guidata dalla richiesta: costruire la chiamata da prompt a clip video, inviare la richiesta, monitorare lo stato di completamento e scaricare il risultato