HiAPI
Blog

Tutoriale

Come usare l'API hailuo 2.3 image-to-video: curl, Python e un esempio di richiesta funzionante

Jul 11, 2026
Come usare l'API hailuo 2.3 image-to-video: curl, Python e un esempio di richiesta funzionante

Riepilogo

  • Di cosa si tratta: una ricetta funzionante per usare l'API hailuo-2.3/image-to-video su hiapi — trasforma un'immagine statica in un breve video: crea l'attività, esegui il polling, scarica il file .mp4.
  • Endpoint: POST https://api.hiapi.ai/v1/tasks, nel corpo della richiesta scrivi "model": "hailuo-2.3/image-to-video", poi esegui il polling con GET /v1/tasks/<taskId> finché l'attività non raggiunge lo stato terminale.
  • Due campi di input obbligatori: prompt e image_url — un singolo URL pubblico che punta al tuo primo frame (non un array).
  • duration è una stringa, non un numero: "6" o "10". Passare l'intero 6 viene rifiutato con 400.
  • Lo schema è rigoroso. Non esistono parametri motion_strength, camera_movement o resolution — se li invii vengono rifiutati con 400. Il movimento è controllato interamente tramite il prompt.

Cosa stai costruendo e cosa devi preparare

Hai un'immagine statica — una foto prodotto, un render di un personaggio, una fotografia — e vuoi che hailuo-2.3 la animi, generando una clip video che inizia esattamente da quel frame. hailuo-2.3 è noto per il movimento fisicamente credibile (tessuti, capelli, meccanica del corpo), quindi è ideale per dare vita alle immagini statiche.

Ti servono due cose:

  1. Una API key di hiapi. Creala nella dashboard. La key ha la forma sk-... e va inserita nell'header Authorization: Bearer.
  2. Un URL pubblico dell'immagine del primo frame. L'API scarica l'immagine da image_url, quindi deve essere accessibile senza autenticazione — un link S3/R2/CDN va bene; un percorso localhost o un URL firmato scaduto no.

Come ogni modello di generazione su hiapi, anche questo usa l'interfaccia asincrona delle attività: esegui un POST di un'attività, ricevi subito un taskId, poi raccogli il risultato tramite polling o callback. Non esiste un endpoint sincrono per i video — il rendering della clip richiede tempo, oltre i tipici timeout HTTP.

Schema della richiesta

Lo schema di input completo di hailuo-2.3/image-to-video — verificato sull'API in produzione:

Campo Tipo Obbligatorio Descrizione
prompt string ✅ Descrivi il movimento, non la scena — la scena viene dalla tua immagine.
image_url string ✅ Un URL pubblico, cioè il primo frame. È singolare — non image_urls.
duration string Facoltativo "6" o "10" (secondi). Deve essere una stringa.
prompt_optimizer boolean Facoltativo Fa espandere il tuo prompt dalla piattaforma prima della generazione.

Lo schema è rigoroso: qualsiasi cosa non presente in questa tabella viene rifiutata. Inviando motion_strength, camera_movement o resolution, riceverai esattamente:

{
  "code": 400,
  "error_code": "INVALID_REQUEST",
  "message": "invalid input: <root>: additional properties 'resolution', 'camera_movement', 'motion_strength' not allowed"
}

Due conseguenze da tenere a mente:

  • Nessuna manopola per regolare il movimento. La fisica del movimento di hailuo-2.3 è guidata solo dal prompt — vedi la sezione sulla scrittura dei prompt qui sotto.
  • Su questo endpoint non c'è controllo di risoluzione o aspect ratio. Non costruire una UI per parametri che l'API rifiuterà con 400.

Esempio minimo funzionante: curl

Crea l'attività:

curl -X POST https://api.hiapi.ai/v1/tasks \
  -H "Authorization: Bearer sk-<your-key>" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "hailuo-2.3/image-to-video",
    "input": {
      "prompt": "The woman turns toward the camera and smiles; wind gently moves her hair; slow cinematic push-in",
      "image_url": "https://your-cdn.example.com/first-frame.jpg",
      "duration": "6",
      "prompt_optimizer": true
    }
  }'

La risposta (estratto) contiene l'id dell'attività sotto data.taskId:

{ "data": { "taskId": "..." } }

Esegui il polling:

curl -s https://api.hiapi.ai/v1/tasks/<taskId> \
  -H "Authorization: Bearer sk-<your-key>"

Durante il rendering della clip, data.status riporta uno stato non terminale. Quando passa a "success", l'URL del video si trova in data.output[0].url; se è "fail", data.error.code e data.error.message spiegano il motivo.

Scarica il file immediatamente dopo il successo dell'attività. L'URL di output è firmato e ha una scadenza (expireAt) — salva i byte nel tuo storage, non usare mai l'URL dell'attività come hotlink.

Esempio minimo funzionante: Python

Script completo dello stesso flusso — crea, esegui polling, scarica:

import time
import requests

API_BASE = "https://api.hiapi.ai/v1/tasks"
API_KEY = "sk-<your-key>"
HEADERS = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}


def create_task() -> str:
    payload = {
        "model": "hailuo-2.3/image-to-video",
        "input": {
            "prompt": (
                "The cat stretches, hops off the windowsill, and walks toward "
                "the camera; soft afternoon light, subtle handheld camera sway"
            ),
            "image_url": "https://your-cdn.example.com/first-frame.jpg",
            "duration": "6",          # string, not int: "6" or "10"
            "prompt_optimizer": True,
        },
    }
    r = requests.post(API_BASE, json=payload, headers=HEADERS, timeout=30)
    task_id = (r.json().get("data") or {}).get("taskId")
    if not task_id:
        raise RuntimeError(f"create failed ({r.status_code}): {r.text[:300]}")
    return task_id


def wait_task(task_id: str, timeout_s: int = 900, poll_s: int = 10) -> dict:
    deadline = time.time() + timeout_s
    while time.time() < deadline:
        r = requests.get(f"{API_BASE}/{task_id}", headers=HEADERS, timeout=30)
        task = r.json().get("data") or {}
        status = task.get("status")
        if status == "success":
            return task
        if status == "fail":
            err = task.get("error") or {}
            raise RuntimeError(f"task failed: {err.get('code')}: {err.get('message')}")
        time.sleep(poll_s)
    raise TimeoutError(f"task {task_id} not finished after {timeout_s}s")


def main() -> None:
    task_id = create_task()
    print("task created:", task_id)
    task = wait_task(task_id)
    url = task["output"][0]["url"]
    # The URL is signed and expires -- download immediately, store the bytes yourself.
    video = requests.get(url, timeout=120).content
    with open("hailuo-output.mp4", "wb") as f:
        f.write(video)
    print("saved hailuo-output.mp4")


if __name__ == "__main__":
    main()

Le attività video richiedono molto più tempo di quelle per immagini, quindi lo script esegue il polling ogni 10 secondi con un limite generoso di 15 minuti. Non eseguire polling più aggressivo di circa ogni 5 secondi — non renderà il rendering più veloce.

Scrivere prompt che controllano il movimento

Poiché non ci sono parametri di movimento, il prompt è il tuo unico volante — e per hailuo-2.3 è sufficiente. Tre regole pratiche:

  • Descrivi il movimento, non l'aspetto. Il modello ha già visto la tua immagine. "a beautiful woman in a red dress" spreca il prompt; "she turns her head slowly to the left as the camera pushes in" lo usa bene.
  • Indica il movimento di camera con un linguaggio semplice. "slow push-in", "orbit right", "handheld sway", "static camera" — vanno scritti nel testo del prompt, non in un campo (inesistente) camera_movement.
  • In caso di dubbio, lascia fare a prompt_optimizer. Con "prompt_optimizer": true la piattaforma espande i prompt brevi prima della generazione — un'ottima impostazione predefinita per input sintetici. Disattivalo quando vuoi che la tua formulazione accurata venga seguita alla lettera.

Considerazioni per la produzione

Preferisci i callback al polling. Allega un callback alla creazione dell'attività e hiapi invierà via POST il risultato dello stato terminale al tuo endpoint, così non dovrai mantenere un ciclo aperto:

{
  "model": "hailuo-2.3/image-to-video",
  "input": { "...": "..." },
  "callback": { "url": "https://your-app.example.com/hooks/hiapi", "when": "final" }
}

"when": "final" scatta una sola volta allo stato terminale. Il polling va bene per script e notebook; i callback sono la forma corretta per i server — il rendering video è abbastanza lungo che un worker bloccato in polling è un costo reale.

Gestisci esplicitamente questi errori:

  • 401 con "code": "permission_denied" — problema con la key, o la key non ha l'autorizzazione per usare questo modello. Il messaggio contiene un request_id che puoi inviare al supporto.
  • 400 INVALID_REQUEST — violazione dello schema. Il messaggio è preciso, ad esempio duration: got number, want string o image_url: missing required field "image_url". Registralo così com'è.
  • 402 alla creazione dell'attività — credito insufficiente. Nota che i video costano più delle immagini per attività, quindi una key che riesce ancora a eseguire attività per immagini potrebbe iniziare a fallire sulla creazione dei video. Ricarica, o controlla l'utilizzo nella dashboard.
  • L'attività passa a "fail" — errore lato generazione; controlla data.error. In particolare, una causa comune per image-to-video: image_url non è scaricabile (bucket privato, URL firmato scaduto).

Rendi idempotente il flusso attorno ai retry. Se il tuo processo muore tra la creazione e il download, un riavvio ingenuo creerebbe (e pagherebbe) un secondo rendering. Persisti il taskId subito dopo che create_task restituisce, e al riavvio continua il polling invece di ricreare.

Letture correlate

Domande frequenti

Quali durate supporta hailuo-2.3 image-to-video? Due: "6" e "10" secondi, passate come stringhe. Qualsiasi altro valore — o lo stesso valore scritto come numero — restituisce 400 INVALID_REQUEST.

Perché "duration": 6 fallisce mentre "duration": "6" funziona? Lo schema definisce duration come enumerazione di stringhe. L'API te lo dice esplicitamente: duration: got number, want string. È il 400 più comune su questo endpoint.

Posso impostare la risoluzione o l'aspect ratio dell'output? No. L'endpoint non espone i campi resolution o aspect_ratio, e inviarne uno dà 400 — lo schema rifiuta le proprietà sconosciute invece di ignorarle.

Come posso controllare il movimento di camera o l'intensità del movimento? Tramite il prompt. Non ci sono parametri camera_movement o motion_strength — scrivi le istruzioni di camera e movimento in linguaggio semplice ("slow push-in", "she walks toward the camera").

Posso passare più immagini di riferimento? No — image_url è una singola stringa, cioè il primo frame. Questo è diverso da altri modelli su hiapi che accettano un array image_urls; lo schema è definito per modello, quindi controlla sempre la pagina del modello.

Quali formati immagine supporta il primo frame? I formati web standard (JPEG/PNG), serviti da un URL accessibile pubblicamente. Se la piattaforma non riesce a scaricare quell'URL, l'attività fallisce dopo la creazione — quindi verifica che il link sia accessibile pubblicamente prima di inviare.

In cosa differisce da hailuo-2.3/text-to-video? Il text-to-video inventa la scena solo dal tuo prompt; l'image-to-video fissa il primo frame sulla tua immagine e anima a partire da essa. Il ciclo di vita dell'attività è identico — cambia solo lo schema di input (qui image_url è obbligatorio, mentre nel text-to-video non esiste affatto un campo immagine).

Quanto costa? I video sono fatturati sui secondi di output, e le tariffe cambiano — consulta la pagina del modello hailuo-2.3/image-to-video e la pagina dei prezzi per i valori correnti.

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