HiAPI
Blog

Tutoriale

Come usare l'API Ray 3.2 image-to-video: curl, Python e una richiesta che funziona al primo colpo

Oct 20, 2026
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'header Authorization: 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 curl e 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:

  1. Nessun duration. La durata della clip è fissa (circa 5 secondi). Inviare duration non viene troncato — viene direttamente rifiutato. È l'esatto contrario di Ray 3.2 text-to-video, dove duration è un intero selezionabile (5 o 10).
  2. 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.)
  3. Lo schema è chiuso. seed, negative_prompt, prompt_optimizer e qualsiasi altro campo restituiscono un 400 con additional 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 (created alla creazione, completed al 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:

  1. Il rapporto d'aspetto dell'output corrisponde a quello dell'input — quadrato entra, quadrato esce. Ecco perché non c'è un campo aspect_ratio; inquadra image_url per la composizione che vuoi ottenere nella clip.
  2. resolution è una fascia di qualità, non un'altezza in pixel letterale. Richiedendo 540p per 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

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.

Inizia a creare con artefatti IA persistenti

Ottieni la tua chiave API e chiama modelli di immagini, video e audio tramite un'unica API basata sui task.

Leggi la documentazione API