Tutoriale
Come usare l'API Ray 3.2 image-to-video: curl, Python e una richiesta che funziona al primo colpo
Ray 3.2 image-to-video anima un fotogramma statico: gli fornisci un'immagine più un prompt che descrive come dovrebbe muoversi, e restituisce un breve video che parte dalla tua immagine. Su hiapi l'id del modello è ray-3.2/image-to-video, richiamato tramite la stessa API asincrona di task unificata di tutti gli altri modelli video — POST /v1/tasks, poi polling o callback.
È un endpoint diverso da Ray 3.2 text-to-video, non solo un prompt diverso. Il suo schema di input è più piccolo e più rigido, e mancano due controlli a cui potresti essere abituato dal modello testuale. Questa guida ti fornisce una richiesta che funziona al primo tentativo: lo schema esatto (verificato contro l'API live il 2026-10-20), un curl pronto da copiare, uno script Python completo di invio/polling/download e i dettagli per la produzione — callback, idempotenza e risposte di errore reali, incluso quel tipo di fallimento che non si presenta dove te lo aspetti.
Cosa costruirai e cosa ti serve
Obiettivo: inviare un'immagine del primo fotogramma + un prompt di movimento, fare polling del task, quindi scaricare un .mp4 muto che anima la tua immagine.
Prerequisiti:
- Una chiave API hiapi — ottienine una dalla tua dashboard. Le chiavi hanno la forma
sk-...e vanno nell'headerAuthorization: Bearer. Prima volta con l'API dei task? Inizia dalla guida rapida. - Un URL di immagine accessibile pubblicamente per il fotogramma iniziale (vedi sotto per i dettagli). Qualsiasi client HTTP — mostreremo
curle Python (requests).
Ray 3.2 image-to-video si fattura a clip video. Poiché la durata della clip è fissa (vedi sotto), resolution è l'unica leva di costo che controlli — una clip a 1080p costa diverse volte una a 540p. Controlla i prezzi attuali e fai prototipi con 540p mentre iteri sui prompt.
Lo schema di input esatto (due campi obbligatori, uno opzionale)
Tutto va dentro l'oggetto input. Lo schema è rigoroso — rifiuta qualsiasi campo non riconosciuto. Verificato campo per campo contro l'API live:
| Campo | Tipo | Obbligatorio | Valori ammessi | Predefinito |
|---|---|---|---|---|
prompt |
string | Sì | Come dovrebbe muoversi l'inquadratura (movimento di camera, movimento del soggetto) | — |
image_url |
string | Sì | URL pubblico del primo fotogramma | — |
resolution |
string | No | 540p, 720p, 1080p |
540p |
Tre trappole in cui incappano facilmente le persone che arrivano dal modello text-to-video:
- Nessun
duration. La durata della clip è fissa (circa 5 secondi). Inviaredurationnon viene troncato — viene direttamente rifiutato. È l'esatto contrario di Ray 3.2 text-to-video, dovedurationè un intero selezionabile (5 o 10). - Nessun
aspect_ratio. Il formato di output segue la tua immagine di input, quindi non c'è nulla da impostare. Nel modello testuale scegli il rapporto d'aspetto; qui lo decide l'immagine. (Confermato sotto con una run reale: un input quadrato ha prodotto una clip quadrata.) - Lo schema è chiuso.
seed,negative_prompt,prompt_optimizere qualsiasi altro campo restituiscono un 400 conadditional properties '<field>' not allowed. Non portare corpi di richiesta da altri modelli dando per scontato che le chiavi extra vengano ignorate.
Cosa restituisce effettivamente la validazione
Queste sono risposte reali dall'endpoint live — inviando deliberatamente valori non validi, senza creare task né addebitare nulla:
// POST with no image_url
{"code":400,"data":null,"error_code":"INVALID_REQUEST",
"message":"invalid input: image_url: missing required field \"image_url\""}
// POST with "resolution": "2k"
{"code":400,"data":null,"error_code":"INVALID_REQUEST",
"message":"invalid input: resolution: value must be one of '540p', '720p', '1080p'"}
// POST with a "duration" field
{"code":400,"data":null,"error_code":"INVALID_REQUEST",
"message":"invalid input: <root>: additional properties 'duration' not allowed"}
// POST with an "aspect_ratio" field
{"code":400,"data":null,"error_code":"INVALID_REQUEST",
"message":"invalid input: <root>: additional properties 'aspect_ratio' not allowed"}
Il message indica il campo specifico problematico, quindi quando te ne dimentichi, l'API stessa è il tuo riferimento per lo schema.
Preparare il primo fotogramma
image_url deve essere un URL che il worker hiapi può recuperare — un link pubblico, non un percorso locale né un blob base64 data:. L'immagine determina il rapporto d'aspetto e la composizione iniziale della clip, quindi parti dal fotogramma esatto da cui vuoi che inizi il movimento.
Una trappola da segnalare subito, perché è facile ignorarla: un image_url errato non viene intercettato alla creazione del task. La chiamata di creazione valida solo la forma della richiesta (è una stringa?), non se l'URL è risolvibile. Un URL di immagine irraggiungibile o 404 restituisce un 200 perfettamente normale con un taskId — poi il task passa a fail dopo qualche secondo. Vedremo il payload esatto del fallimento nella sezione di troubleshooting; il punto è: ramifica sempre sullo status terminale del task, non dare mai per scontato che un 200 alla creazione significhi successo.
Esempio minimo funzionante: curl
Crea un task:
curl -s -X POST https://api.hiapi.ai/v1/tasks \
-H "Authorization: Bearer sk-YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "ray-3.2/image-to-video",
"input": {
"prompt": "slow cinematic push-in, gentle parallax, soft drifting light, the subject breathing almost imperceptibly",
"image_url": "https://your-cdn.example.com/first-frame.jpg",
"resolution": "540p"
}
}'
La risposta di creazione porta un id di task:
{ "code": 200, "data": { "taskId": "tk-hiapi-01M5..." }, "message": "success" }
Fai polling finché non raggiunge lo stato terminale:
curl -s https://api.hiapi.ai/v1/tasks/tk-hiapi-01M5... \
-H "Authorization: Bearer sk-YOUR_KEY"
Mentre il task è in esecuzione, data.status attraversa diversi stati in corso (vedrai valori come handling, poi archiving). I due stati terminali sono success e fail. In caso di successo:
{
"code": 200,
"data": {
"taskId": "tk-hiapi-01M5...",
"status": "success",
"created": 1792435735,
"completed": 1792435781,
"storage": "temp",
"output": [
{ "type": "video", "artifactId": "278504",
"expireAt": 1793040580,
"url": "https://temp.hiapi.ai/.../clip.mp4" }
]
},
"message": "success"
}
Scarica subito data.output[0].url. È un URL firmato su temp.hiapi.ai dotato di expireAt (timestamp Unix, circa una settimana) — salva i byte sul tuo storage invece di fare hotlink. Ray 3.2 restituisce MP4 muti; non c'è traccia audio da attendere.
Script Python completo (invio → polling → download)
import time
import requests
API_BASE = "https://api.hiapi.ai/v1"
API_KEY = "sk-YOUR_KEY"
HEADERS = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}
def create_task(image_url: str, prompt: str, resolution: str = "540p") -> str:
resp = requests.post(
f"{API_BASE}/tasks",
headers=HEADERS,
json={
"model": "ray-3.2/image-to-video",
"input": {
"prompt": prompt,
"image_url": image_url, # public URL of the first frame (required)
"resolution": resolution, # "540p" | "720p" | "1080p"
# no "duration" and no "aspect_ratio" — both are rejected
},
},
timeout=30,
)
resp.raise_for_status()
return resp.json()["data"]["taskId"]
def wait_task(task_id: str, timeout_s: int = 900, poll_interval: int = 8) -> dict:
"""Poll until terminal. Tolerates transient network blips by re-polling
the SAME saved task_id instead of aborting the whole job."""
deadline = time.time() + timeout_s
while time.time() < deadline:
try:
resp = requests.get(f"{API_BASE}/tasks/{task_id}", headers=HEADERS, timeout=30)
resp.raise_for_status()
except requests.RequestException:
time.sleep(poll_interval) # a dropped poll is not a failed task
continue
task = resp.json()["data"]
if task["status"] == "success":
return task
if task["status"] == "fail":
err = task.get("error") or {}
raise RuntimeError(f"task failed: {err.get('code')}: {err.get('message')}")
time.sleep(poll_interval) # any non-terminal status = still running
raise TimeoutError(f"task {task_id} not finished after {timeout_s}s")
task_id = create_task(
image_url="https://your-cdn.example.com/first-frame.jpg",
prompt="slow cinematic push-in, gentle parallax, soft drifting light",
)
print("task created:", task_id)
task = wait_task(task_id)
video_url = task["output"][0]["url"]
# Output URLs expire — download right away and store the bytes yourself.
video = requests.get(video_url, timeout=120).content
with open("ray32-i2v.mp4", "wb") as f:
f.write(video)
print(f"saved ray32-i2v.mp4 ({len(video) / 1e6:.1f} MB)")
Eseguilo con pip install requests, inserisci la tua chiave e un image_url reale, e al termine del task avrai un .mp4 sul disco.
Com'è una run reale
Per rendere concreti i numeri, ecco una run reale a 540p contro l'API live il 2026-10-20. Primo fotogramma: un'immagine 1600×1600 (quadrata); prompt: lento push-in cinematografico.
- Il task è passato per
handling → archiving → success, con il lavoro lato server completato in circa 45 secondi (createdalla creazione,completedal completamento) — con un ciclo di polling a 8 secondi il tempo effettivo è stato di circa 60 secondi. I task video richiedono da decine di secondi a qualche minuto, non sono istantanei; prevedi scadenze ampie. - L'output è un MP4 720×720, circa 1,3 MB, 5,04 secondi, muto.
Questa run ti dice due cose che la tabella dei parametri non spiega:
- Il rapporto d'aspetto dell'output corrisponde a quello dell'input — quadrato entra, quadrato esce. Ecco perché non c'è un campo
aspect_ratio; inquadraimage_urlper la composizione che vuoi ottenere nella clip. resolutionè una fascia di qualità, non un'altezza in pixel letterale. Richiedendo540pper un input 1600×1600, la risposta è 720×720 — la fascia scala in base alla forma dell'input, non impone una dimensione fissa. Se ti serve una dimensione di output esatta, ridimensiona tu stesso la clip consegnata.
Note per la produzione: callback, idempotenza, errori
Preferisci le callback al polling
Per qualsiasi scenario al di fuori di uno script, registra un webhook al momento della creazione invece di fare polling:
{
"model": "ray-3.2/image-to-video",
"input": {
"prompt": "...",
"image_url": "https://your-cdn.example.com/first-frame.jpg",
"resolution": "720p"
},
"callback": { "url": "https://your-app.example.com/hooks/hiapi", "when": "final" }
}
Con "when": "final", hiapi chiama il tuo endpoint una volta quando il task raggiunge lo stato terminale — nessun traffico di polling e nessun completamento perso tra un intervallo e l'altro. Il tuo handler esegue poi GET /v1/tasks/<id> per ottenere output[0].url. Per CLI e notebook che non possono esporre un endpoint HTTPS, il polling va comunque bene.
Rendi i retry idempotenti
Persisti il taskId nel momento stesso in cui la chiamata di creazione restituisce. Se il tuo processo va in crash durante la generazione, riprendi facendo polling dell'id salvato — non ricreare il task, altrimenti pagherai per una seconda clip. Ricrea solo se la richiesta di creazione originale è fallita prima di restituire l'id del task. Il ciclo di polling tollerante agli errori sopra segue già questa regola: al blip di rete rifai polling dello stesso id invece di ricominciare da zero.
Gli errori che vedrai realmente
Ogni riga della tabella seguente è una forma di risposta live verificata:
| Sintomo | Risposta | Correzione |
|---|---|---|
| Chiave mancante/non valida | 401, annidato {"error":{"code":"permission_denied","type":"hiapi_error","request_id":"...","message":"This API key is invalid..."}} |
Controlla l'header Authorization: Bearer sk-... e che la chiave abbia accesso. Includi il request_id quando contatti il supporto. |
image_url mancante |
400 INVALID_REQUEST, message: "image_url: missing required field" |
Questo modello richiede image_url — non c'è una modalità solo testo. |
| Enum errato / campo extra | 400 INVALID_REQUEST, piatto {"code":400,"error_code":"INVALID_REQUEST","message":"..."} |
Leggi il campo indicato nel message. Per duration/aspect_ratio dirà additional properties ... not allowed — rimuovili. |
| Id di task errato durante il polling | 404, piatto {"code":404,"data":null,"message":"task not found"} (nota: il 404 non ha error_code) |
Id errato, oppure stai facendo polling con la chiave di un altro account. |
image_url irraggiungibile |
La creazione restituisce 200 + un taskId; il task poi termina con data.status:"fail" e data.error:{"code":"INVALID_REQUEST","message":"Invalid request parameters for this model..."} |
Questa è la trappola. La chiamata di creazione non distingue un link morto da uno vivo. Verifica che la tua immagine sia pubblicamente accessibile e leggi sempre lo data.status terminale. |
L'ultima riga merita di essere tenuta a mente: anche quando il task fallisce, l'involucro HTTP del polling resta 200 — il fallimento risiede in data.status e data.error, non nel codice di stato HTTP. I task falliti vengono rimborsati, ma il tuo codice deve comunque accorgersene. Ramifica su data.status == "fail", non fidarti del 200.
Risorse correlate
- Pagina del modello Ray 3.2 image-to-video — playground, stato in tempo reale e tabella prezzi completa per risoluzione
- Pagina del modello Ray 3.2 text-to-video — l'endpoint gemello; parte dal testo invece che da un'immagine (aggiunge
durationeaspect_ratio, rimuoveimage_url) - Come usare l'API Hailuo 2.3 image-to-video — un altro tutorial image-to-video sulla stessa API dei task, utile per confrontare gli schemi
- Come usare l'API grok-imagine image-to-video — un terzo modello i2v per il confronto
- Prezzi hiapi — prezzo video per clip in base alla risoluzione
Domande frequenti
Qual è l'id del modello e quale endpoint devo chiamare?
ray-3.2/image-to-video, inviato a POST https://api.hiapi.ai/v1/tasks, con i parametri sotto input. È asincrono — ricevi un taskId, poi fai polling di GET /v1/tasks/<id> (oppure registri una callback).
Cosa richiede in input?
Esattamente due campi obbligatori — prompt (movimento) e image_url (URL pubblico del primo fotogramma) — più resolution opzionale (540p/720p/1080p). Qualsiasi altra cosa viene rifiutata.
Perché non posso impostare la durata o il rapporto d'aspetto?
La durata della clip è fissa (circa 5 secondi), quindi non c'è un campo duration; il rapporto d'aspetto dell'output deriva dalla tua immagine di input, quindi non c'è un campo aspect_ratio. Inviarli restituisce entrambi 400. Se ti serve una durata selezionabile, usa Ray 3.2 text-to-video.
La mia chiamata di creazione ha restituito 200, ma il video non arriva mai. Perché?
Molto probabilmente l'image_url è irraggiungibile. La chiamata di creazione valida solo la forma della richiesta, quindi un link morto restituisce comunque un taskId — poi il task termina con fail. Fai polling fino allo stato terminale e leggi data.error. Assicurati che l'immagine sia recuperabile pubblicamente (senza autenticazione, non un percorso locale, non base64).
La clip ha audio? No — Ray 3.2 image-to-video restituisce un MP4 muto. Aggiungi il suono in post-produzione, oppure usa un modello audio separato.
Quanto costa una clip?
Si fattura a clip, e poiché la durata è fissa resolution è l'unica leva — una clip a 1080p costa diverse volte una a 540p. I prezzi cambiano, quindi consulta la pagina dei prezzi invece di codificare numeri fissi, e fai prototipi con 540p.
Quanto devo aspettare prima di arrendermi?
Da decine di secondi a qualche minuto è normale. Tollera gli data.status non terminali nel ciclo di polling e usa scadenze ampie (10–15 minuti), oppure registra una callback con "when": "final" per farti notificare da hiapi invece di indovinare gli intervalli.
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