HiAPI
Blog

Tutoriel

Comment utiliser l'API Ray 3.2 image-to-video : curl, Python et une requête qui fonctionne du premier coup

Oct 20, 2026
Comment utiliser l'API Ray 3.2 image-to-video : curl, Python et une requête qui fonctionne du premier coup

Ray 3.2 image-to-video anime une image fixe : vous lui fournissez une image plus une invite décrivant le mouvement, et il renvoie une courte vidéo qui démarre à partir de votre image. Sur hiapi, l'id du modèle est ray-3.2/image-to-video, appelé via la même API de tâches asynchrones unifiée que tous les autres modèles vidéo — POST /v1/tasks, puis interrogation répétée ou réception d'un callback.

C'est un point de terminaison différent de Ray 3.2 text-to-video, et pas seulement une invite différente. Son schéma d'entrée est plus petit et plus strict, et deux contrôles auxquels vous êtes peut-être habitué avec le modèle texte ont disparu. Ce guide vous donne une requête qui fonctionne du premier coup : le schéma exact (vérifié contre l'API en ligne le 2026-10-20), un curl directement copiable, un script Python complet de soumission/interrogation/téléchargement, ainsi que les détails de production — callbacks, idempotence, et les vraies réponses d'erreur, y compris ce mode de défaillance qui n'apparaît pas là où vous l'attendez.

Ce que vous allez construire, et ce qu'il vous faut

Objectif : soumettre une image de première image + une invite de mouvement, interroger la tâche, puis télécharger un .mp4 muet qui anime votre image.

Prérequis :

  • Une clé API hiapi — obtenez-en une depuis votre tableau de bord. La clé ressemble à sk-... et se place dans l'en-tête Authorization: Bearer. Nouveau sur l'API de tâches ? Consultez d'abord le guide de démarrage rapide.
  • Une URL d'image accessible publiquement pour la première image (voir ci-dessous). N'importe quel client HTTP — nous montrerons curl et Python (requests).

Ray 3.2 image-to-video est facturé au clip vidéo. Comme la durée du clip est fixe (voir ci-dessous), resolution est votre seul levier de coût — un clip 1080p coûte plusieurs fois le prix d'un clip 540p. Consultez les tarifs actuels, et prototypez avec 540p pendant que vous itérez sur l'invite.

Le schéma d'entrée exact (deux champs obligatoires, un optionnel)

Tout se place dans l'objet input. Le schéma est strict — il rejette tout champ non reconnu. Vérifié champ par champ contre l'API en ligne :

Champ Type Obligatoire Valeurs autorisées Valeur par défaut
prompt string oui comment l'image doit bouger (caméra, mouvement du sujet) —
image_url string oui URL publique de la première image —
resolution string non 540p, 720p, 1080p 540p

Trois pièges dans lesquels tombent facilement ceux qui viennent du modèle text-to-video :

  1. Pas de duration. La durée du clip est fixe (environ 5 secondes). Envoyer duration ne sera pas tronqué — c'est directement rejeté. C'est l'inverse de Ray 3.2 text-to-video, où duration est un entier optionnel (5 ou 10).
  2. Pas d'aspect_ratio. Le cadrage de sortie suit votre image d'entrée, il n'y a donc rien à définir. Avec le modèle texte vous choisissez le ratio ; ici c'est l'image qui décide. (Confirmé ci-dessous par une exécution réelle : une entrée carrée a produit un clip carré.)
  3. Le schéma est fermé. seed, negative_prompt, prompt_optimizer et tout autre champ retourneront un 400 avec additional properties '<field>' not allowed. Ne transplantez pas des corps de requête d'autres modèles en supposant que les clés superflues seront ignorées.

Ce que la validation renvoie réellement

Voici de vraies réponses du point de terminaison en ligne — valeurs invalides soumises volontairement, aucune tâche créée, aucune facturation :

// 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"}

Le message pointe le champ fautif précis, donc l'API est elle-même votre référence de schéma quand vous oubliez quelque chose.

Préparer la première image

image_url doit être une URL que le worker hiapi peut récupérer — un lien public, pas un chemin local, pas un blob base64 data:. L'image détermine le ratio de la vidéo et la composition d'ouverture, donc partez de l'image exacte où vous voulez que le mouvement commence.

Un piège à signaler d'avance, car il est facile à manquer : une mauvaise image_url n'est pas capturée au moment de la création de la tâche. L'appel de création ne valide que la forme de la requête (est-ce une chaîne ?), pas si l'URL est résolvable. Une URL d'image inaccessible ou 404 renverra un 200 parfaitement normal et un taskId — puis la tâche basculera en fail quelques secondes plus tard. Nous verrons la charge de défaillance exacte dans la section dépannage ; l'essentiel est de toujours brancher sur le status terminal de la tâche, et de ne jamais supposer qu'une création 200 signifie un succès.

Exemple minimal fonctionnel : curl

Créer la tâche :

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 réponse de création porte un id de tâche :

{ "code": 200, "data": { "taskId": "tk-hiapi-01M5..." }, "message": "success" }

Interrogez jusqu'à l'atteinte d'un état terminal :

curl -s https://api.hiapi.ai/v1/tasks/tk-hiapi-01M5... \
  -H "Authorization: Bearer sk-YOUR_KEY"

Pendant l'exécution de la tâche, data.status passe par plusieurs états en cours (vous verrez des valeurs comme handling, puis archiving). Les deux états terminaux sont success et fail. En cas de succès :

{
  "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"
}

Téléchargez immédiatement data.output[0].url. C'est une URL signée sur temp.hiapi.ai avec un expireAt (horodatage Unix, environ une semaine plus tard) — persistez les octets dans votre propre stockage plutôt que de créer un lien direct. Ray 3.2 renvoie un MP4 muet ; il n'y a aucune piste audio à attendre.

Script Python complet (soumission → interrogation → téléchargement)

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)")

Exécutez-le avec pip install requests, remplacez votre clé et une vraie image_url, et une fois la tâche terminée vous aurez un .mp4 sur votre disque.

À quoi ressemble une exécution réelle

Pour concrétiser les chiffres, voici une vraie exécution 540p contre l'API en ligne le 2026-10-20. Première image : une image 1600×1600 (carrée) ; invite : un lent travelling avant cinématographique.

  • La tâche a traversé handling → archiving → success, le travail côté serveur s'achevant en environ 45 secondes (created création, completed achèvement) — soit environ 60 secondes réelles avec la boucle d'interrogation de 8 secondes. Les tâches vidéo prennent des dizaines de secondes à quelques minutes, pas un instant ; prévoyez des délais généreux.
  • La sortie était un MP4 720×720, environ 1,3 Mo, 5,04 secondes, muet.

Cette exécution vous apprend deux choses que le tableau des paramètres ne dit pas :

  1. Le ratio de sortie correspond à l'entrée — carré en entrée, carré en sortie. C'est pourquoi il n'y a pas de champ aspect_ratio ; cadrez votre image_url selon la composition de clip que vous souhaitez.
  2. resolution est un palier de qualité, pas une hauteur littérale en pixels. En demandant 540p pour une entrée 1600×1600, on obtient du 720×720 — ce palier s'adapte à la forme de l'entrée au lieu d'imposer une taille fixe. Si vous avez besoin de dimensions de sortie exactes, redimensionnez vous-même le clip livré.

Considérations de production : callbacks, idempotence, erreurs

Préférez les callbacks à l'interrogation

Pour tout ce qui sort d'un script, enregistrez un webhook à la création plutôt que d'interroger :

{
  "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" }
}

Avec "when": "final", hiapi appelle votre point de terminaison une fois lorsque la tâche atteint un état terminal — pas de trafic d'interrogation, et pas de fin ratée entre deux intervalles. Votre handler exécute ensuite GET /v1/tasks/<id> pour récupérer output[0].url. Pour les CLI et notebooks qui ne peuvent pas exposer de point de terminaison HTTPS, l'interrogation reste très bien.

Rendez les réessais idempotents

Persistez le taskId dès le retour de l'appel de création. Si votre processus plante en cours de production, reprenez en interrogeant l'id stocké — ne recréez pas la tâche, sinon vous paierez un second clip. Ne recréez que si la requête de création initiale a échoué avant de renvoyer un id de tâche. La boucle d'interrogation tolérante ci-dessus suit déjà cette règle : en cas de coupure réseau, réinterroger le même id au lieu de repartir de zéro.

Les erreurs que vous verrez réellement

Chaque ligne du tableau ci-dessous est une forme de réponse en ligne vérifiée :

Symptôme Réponse Correctif
Clé manquante/invalide 401, imbriqué {"error":{"code":"permission_denied","type":"hiapi_error","request_id":"...","message":"This API key is invalid..."}} Vérifiez l'en-tête Authorization: Bearer sk-... et que la clé a les droits d'accès. Fournissez request_id au support.
image_url manquant 400 INVALID_REQUEST, message: "image_url: missing required field" Ce modèle exige image_url — pas de mode texte seul.
Mauvaise énumération / champ superflu 400 INVALID_REQUEST, plat {"code":400,"error_code":"INVALID_REQUEST","message":"..."} Lisez le champ indiqué dans message. Pour duration/aspect_ratio, il dira additional properties ... not allowed — supprimez-les.
Mauvais id de tâche lors de l'interrogation 404, plat {"code":404,"data":null,"message":"task not found"} (note : pas d'error_code sur les 404) Id erroné, ou vous interrogez avec la clé d'un autre compte.
image_url inaccessible La création renvoie 200 + un taskId ; la tâche se termine ensuite en data.status:"fail" avec data.error:{"code":"INVALID_REQUEST","message":"Invalid request parameters for this model..."} C'est le piège. L'appel de création ne peut pas distinguer un lien mort d'un lien vivant. Vérifiez que votre image est accessible publiquement et lisez toujours le data.status terminal.

La dernière ligne mérite d'être retenue : même quand la tâche échoue, l'enveloppe HTTP de l'interrogation reste 200 — l'échec réside dans data.status et data.error, pas dans le code de statut HTTP. Les tâches échouées sont remboursées, mais votre code doit quand même les remarquer. Branchez sur data.status == "fail", ne croyez pas le 200.

Ressources connexes

FAQ

Quel est l'id du modèle, et quel point de terminaison appeler ? ray-3.2/image-to-video, soumis à POST https://api.hiapi.ai/v1/tasks, avec les paramètres sous input. C'est asynchrone — vous récupérez un taskId, puis interrogez GET /v1/tasks/<id> (ou enregistrez un callback).

Que faut-il en entrée ? Exactement deux champs obligatoires — prompt (mouvement) et image_url (URL publique de la première image) — plus resolution optionnel (540p/720p/1080p). Tout le reste sera rejeté.

Pourquoi ne puis-je pas définir la durée ou le ratio ? La durée du clip est fixe (environ 5 secondes), d'où l'absence de champ duration ; le ratio de sortie hérite de votre image d'entrée, d'où l'absence de champ aspect_ratio. Les envoyer retourne 400. Si vous avez besoin d'une durée sélectionnable, utilisez plutôt Ray 3.2 text-to-video.

Mon appel de création a renvoyé 200, mais la vidéo n'arrive jamais. Pourquoi ? Le plus souvent, c'est une image_url inaccessible. L'appel de création ne valide que la forme de la requête, donc un lien mort renvoie quand même un taskId — puis la tâche finit en fail. Interrogez jusqu'à un état terminal et lisez data.error. Assurez-vous que l'image est récupérable publiquement (sans authentification, pas de chemin local, pas de base64).

Le clip a-t-il de l'audio ? Non — Ray 3.2 image-to-video renvoie un MP4 muet. Ajoutez le son en post-traitement, ou utilisez un modèle audio séparé.

Combien coûte un clip ? Facturé au clip, et comme la durée est fixe, resolution est le seul levier — un clip 1080p coûte plusieurs fois le prix d'un 540p. Les prix changent, donc consultez la page de tarifs au lieu de coder les chiffres en dur, et prototypez avec 540p.

Combien de temps attendre avant d'abandonner ? Des dizaines de secondes à quelques minutes sont normales. Tolérez les data.status non terminaux dans la boucle d'interrogation et utilisez des délais généreux (10–15 minutes), ou enregistrez un callback avec "when": "final" pour que hiapi vous notifie au lieu de deviner les intervalles.

Articles similaires

Voir tous les articles →

Commencez avec des artefacts IA persistants

Obtenez votre clé API et appelez des modèles d'image, de vidéo et d'audio via une API unique basée sur les tasks.

Lire la documentation de l'API