Aller au contenu principal

API d’images et vidéos IA : de la première entrée au résultat vérifié

Créez une tasse bleue et animez-la avec MiniMax H3, dans le Playground puis avec Python, CLI ou un agent : prompts, coûts et vérification.

Auteur
Lydia
Publié
Lecture
13 min de lecture
Mis à jour
Une céramiste adulte fictive en tablier bleu place une tasse bleue près d’une caméra dans un atelier de poterie

Créez une image, validez-la, puis animez-la en un court clip. Ces étapes reprennent notre essai de la tasse bleue d’octobre 2026. Ce guide utilise une image fixe Z-Image à 0,010 $ et cinq secondes de MiniMax H3 en 768p à 0,185 $ : 0,195 $ pour un essai à chaque étape, sans extension du prompt pour l’image fixe. Essayez d’abord la même scène dans le Playground ; une fois les entrées au point, utilisez l’exemple Python ou confiez les appels à un agent.

Le résultat visé est précis : une tasse bien formée sur un établi, de la vapeur dérivant vers la droite et un lent travelling avant qui se termine à l’arrêt. Vous n’avez ni modèle à déployer ni serveur GPU à configurer.

Essayez le cas complet dans le Playground

Créez votre compte et ajoutez du solde, puis vérifiez votre adresse e-mail. Le Playground gère la requête ; une clé API n’est nécessaire que pour le code et les outils d’agent ci-dessous.

1. Générez l’image de départ

Ouvrez Z-Image texte vers image et utilisez son Playground. Collez ce prompt :

Photograph of a handmade blue ceramic mug of tea on a worn wooden workbench in a pottery studio, a thin curl of steam rising from the mug. Soft window light from the left, shallow depth of field, shelves of pots softly out of focus behind. The mug sits left of centre with empty bench to its right. Natural colours, no text, no logos.

Choisissez 16:9, PNG, le seed 42, et désactivez l’extension du prompt. Vérifiez le prix affiché, puis appuyez sur Run model. L’image de base coûte 0,010 $ ; l’extension porterait ce prix à 0,016 $. Gardez le seed 42 fixe lorsque vous comparez les changements du prompt.

Ouvrez le résultat avant de poursuivre. Vérifiez :

  • Objet : une tasse, une anse et une géométrie de céramique nette.
  • Possibilité de mouvement : une vapeur visible et de l’espace à droite pour sa dérive.
  • Composition : la tasse est à gauche du centre ; les étagères restent un fond discret.
  • Détails indésirables : aucune inscription parasite, aucun logo ni tasse supplémentaire.

Téléchargez le PNG validé sous image.png. Si la tasse est incorrecte, modifiez une phrase et générez une nouvelle image avant de dépenser pour la vidéo. Chaque nouvelle image de base ajoute 0,010 $.

2. Animez l’image validée

Ouvrez MiniMax H3 image vers vidéo, ajoutez image.png dans Input image et collez dans Prompt :

Steam rises from the mug and drifts to the right. The camera pushes in slowly toward the mug, then holds. The mug, bench and shelves stay where they are. Quiet room, a faint ceramic clink.

Réglez Duration sur 5 secondes et Resolution sur 768p. Vérifiez le prix et appuyez sur Run model. Ce clip coûte 0,185 $ ; le choix de 2k coûterait 0,290 $.

Le prompt sépare mouvement de vapeur, mouvement de caméra et son. Il demande à la tasse de rester en place plutôt que de la décrire de nouveau. Si elle traverse l’établi, simplifiez la consigne de caméra au lieu d’ajouter des actions à l’objet. Un lent travelling avant rapproche la caméra ; il ne demande pas à la tasse de bouger.

3. Vérifiez le clip, puis choisissez la prise suivante

Lisez tout le résultat avec le son. Vérifiez que la tasse ne glisse pas et ne change pas de forme, que la caméra avance puis s’arrête, qu’il n’y a pas de coupe indésirable et que le son convient à un atelier calme. Le livrable attendu est un MP4 d’environ cinq secondes avec une piste audio ; un fichier lisible ne suffit pas à rendre le plan exploitable.

Si l’image était bonne mais le mouvement incorrect, gardez l’image de départ validée et révisez le prompt de mouvement. Si l’image de départ était incorrecte, remplacez-la d’abord. H3 n’a pas de seed : une génération en 2k produit une nouvelle prise, dont les mouvements peuvent différer de la version 768p qui vous plaisait.

Ce que l’essai de la tasse bleue a produit

L’image fixe a coûté 0,010 $ et a été livrée en PNG de 1280 × 720. La requête vidéo de cinq secondes a coûté 0,125 $ (en 480p, qui n’est plus proposé ; le même clip en 768p coûte 0,185 $) et a renvoyé un MP4 de 832 × 480 avec piste audio. Le total était de 0,135 $ ; en 768p, il serait de 0,195 $.

La tasse est restée sur l’établi tandis que la vapeur montait et dérivait à droite ; le travelling était discret. L’image fixe plaçait la tasse légèrement à gauche du centre, et la vapeur devenait moins distincte près de l’étagère vers la fin. Pour un plan produit plus serré, précisez davantage la position de la tasse dans le prompt d’image avant l’animation.

Pourquoi ces deux modèles conviennent au premier essai

Z-Image fournit une image peu coûteuse pour fixer le sujet et la composition. H3 accepte cette image et génère mouvement et bande-son. Leurs entrées diffèrent :

Z-Image texte vers imageH3 image vers vidéo
ID du modèlealibaba/z-image-turbo/text-to-imageminimax/h3/image-to-video
Entrée requiseprompt (1 à 1 000 caractères)prompt (jusqu’à 7 000) et image_url
Réglages utilisés iciaspect_ratio (1:1, 4:3, 3:4, 16:9 ou 9:16), output_format (uniquement png), seed, enable_prompt_expansionduration_seconds (4 à 15), resolution (768p ou 2k)
Non acceptétout autre format de sortieseed et aspect_ratio : l’image de départ définit la forme
Sortieun PNGun MP4 avec bande-son générée
Prix0,010 $ par image0,037 $ par seconde en 768p

Utilisez une image de départ PNG, JPEG, WebP ou GIF, dont chaque côté mesure entre 256 et 5 760 pixels, avec un rapport d’aspect entre 2:5 et 5:2 et un fichier de moins de 30 MB. Un PNG téléversé est l’entrée la plus simple. Si vous utilisez un lien public, il doit renvoyer l’image elle-même ; une page de partage d’un service cloud n’est pas une entrée image.

Exécutez le même cas avec Python

Utilisez Python 3.11 ou ultérieur, installez le paquet officiel avec python -m pip install spicyapi et disposez de l’utilitaire file. Définissez SPICY_API_KEY dans votre propre terminal, enregistrez ce code sous blue-mug.py et lancez python blue-mug.py. Il demande votre accord aux deux étapes payantes et s’interrompt pour la vérification de l’image. Gardez le dossier d’exécution créé pour reprendre ; ne choisissez un nouveau RUN_NAME que pour un nouvel essai délibéré. Le contrat API et les options SDK sont décrits dans la documentation du SDK Python.

python
import hashlib
import json
import os
from pathlib import Path
import subprocess
import tempfile
import time
import urllib.request
import uuid
from spicyapi import SpicyClient, output_assets

# Change RUN_NAME for a deliberately new pair of paid generations.
folder = Path(os.getenv("RUN_NAME", "blue-mug-1"))
folder.mkdir(exist_ok=True, mode=0o700)
state_file = folder / "state.json"
state = json.loads(state_file.read_text()) if state_file.exists() else {}
identity = hashlib.sha256(os.environ["SPICY_API_KEY"].encode()).hexdigest()
if state.get("identity", identity) != identity:
    raise RuntimeError("This saved run belongs to a different API key.")
state["identity"] = identity
client = SpicyClient()

def save() -> None:
    temp = state_file.with_suffix(".tmp")
    temp.write_text(json.dumps(state, indent=2))
    temp.chmod(0o600)
    temp.replace(state_file)

def run(stage: str, model: str, inputs: dict) -> dict:
    request = {"model": model, "input": inputs}
    if stage not in state:
        state[stage] = {"request": request, "key": str(uuid.uuid4())}
    saved = state[stage]
    if saved["request"] != request:
        raise RuntimeError("Input changed: choose a new RUN_NAME, not a new retry key.")
    save()  # The same request and key survive a lost create response.
    if not saved.get("taskId"):
        if "first_sent" in saved and time.time() - saved["first_sent"] >= 23 * 3600:
            raise RuntimeError("Unknown acceptance: reconcile task history before retrying.")
        if "quote" not in saved:
            quote = client.quote_task(model=model, input_data=inputs)
            if input(f"{stage}: quoted ${quote['estimatedCost']}. Create? [y/N] ") != "y":
                raise SystemExit("No task created for this stage.")
            saved["quote"] = quote
        saved.setdefault("first_sent", time.time())
        save()
        quote = saved["quote"]
        task = client.create_task(model=model, input_data=inputs,
                                  idempotency_key=saved["key"],
                                  quote_id=quote["quoteId"],
                                  expected_cost=quote["estimatedCost"])
        saved["taskId"] = task["taskId"]
        save()
    task_id = saved["taskId"]
    print(stage, "task:", task_id)
    deadline = time.monotonic() + 1200
    while time.monotonic() < deadline:
        task = client.get_task(task_id)
        if task["state"] in {"failed", "canceled", "expired"}:
            raise RuntimeError(f"{task_id}: {task.get('errorCode')} {task.get('errorMessage')}")
        if task["state"] == "succeeded":
            assets = output_assets(task)
            if not assets or assets[0].get("unavailable"):
                raise RuntimeError(f"{task_id}: no retrievable output; keep the task ID.")
            asset = assets[0]
            if not asset.get("pending") and asset.get("url"):
                print("Charge:", task.get("cost"), "settled:", task.get("settled"))
                return asset
        time.sleep(5)
    raise TimeoutError(f"Resume {task_id} by rerunning this file; do not start another task.")

def download(asset: dict, destination: Path, mime: str) -> None:
    temporary = None
    try:
        request = urllib.request.Request(asset["url"], headers={"User-Agent": "SpicyAPI-Blog-Example/1.0"})
        with urllib.request.urlopen(request, timeout=300) as response:
            if response.status != 200 or response.headers.get_content_type() != mime:
                raise RuntimeError("Unexpected download status or content type.")
            with tempfile.NamedTemporaryFile(dir=folder, delete=False) as handle:
                temporary = Path(handle.name)
                while chunk := response.read(1024 * 1024):
                    handle.write(chunk)
        size = temporary.stat().st_size
        expected = int(asset.get("bytes") or 0)
        detected = subprocess.check_output(["file", "-b", "--mime-type", str(temporary)], text=True).strip()
        if not size or (expected and size != expected) or detected != mime:
            raise RuntimeError("Incomplete or incorrectly formatted download.")
        temporary.replace(destination)  # An interrupted transfer leaves the old file alone.
    finally:
        if temporary is not None:
            temporary.unlink(missing_ok=True)

image_input = {
    "prompt": (
        'Photograph of a handmade blue ceramic mug of tea on a worn wooden workbench '
        'in a pottery studio, a thin curl of steam rising from the mug. Soft window '
        'light from the left, shallow depth of field, shelves of pots softly out of '
        'focus behind. The mug sits left of centre with empty bench to its right. '
        'Natural colours, no text, no logos.'
    ),
    "aspect_ratio": "16:9",
    "output_format": "png",
    "seed": 42,
    "enable_prompt_expansion": False,
}
image_asset = run("image", "alibaba/z-image-turbo/text-to-image", image_input)
download(image_asset, folder / "image.png", "image/png")
if input(f"Open {folder / 'image.png'}. Is the mug/frame approved? [y/N] ") != "y":
    raise SystemExit("Stopped before video generation.")
if not state.get("image_uri"):
    state["image_uri"] = client.upload_file(str(folder / "image.png"))["uri"]
    save()
video_input = {"image_url": state["image_uri"], "prompt": 'Steam rises from the mug and drifts to the right. The camera pushes in slowly toward the mug, then holds. The mug, bench and shelves stay where they are. Quiet room, a faint ceramic clink.',
               "duration_seconds": 5, "resolution": "768p"}
video_asset = run("video", "minimax/h3/image-to-video", video_input)
download(video_asset, folder / "video.mp4", "video/mp4")
print("Play", folder / "video.mp4", "with sound and review it before using it.")

Gardez la clé API sur votre serveur ou un ordinateur que vous contrôlez, jamais dans une page web publique ou une application mobile. Consultez les pages des modèles ci-dessus pour vérifier la disponibilité et les entrées acceptées avant d’intégrer un autre modèle.

Confiez les appels à un agent si cela vous simplifie la tâche

Utilisez un Agent pour préparer les entrées et renvoyer le résultat pendant que vous validez le prix et les choix créatifs. Examinez vous-même l’image de départ avant d’autoriser la vidéo.

  • CLI : enregistrez le même objet d’entrée dans input.json et utilisez npx @spicyapi/cli tasks create --model alibaba/z-image-turbo/text-to-image --input-file input.json --wait. La CLI confirme le prix ; gardez l’ID de tâche renvoyé pour la reprise. Pour le clip, téléversez l’image validée avec npx @spicyapi/cli files upload image.png, puis créez minimax/h3/image-to-video de la même façon, avec l’URI spicy:// renvoyée comme image_url.
  • MCP : demandez à votre assistant d’utiliser spicyapi_task_create pour alibaba/z-image-turbo/text-to-image, puis spicyapi_task_wait ; pour le clip, ajoutez spicyapi_upload_file pour votre image.png validé, puis spicyapi_task_create avec minimax/h3/image-to-video, 5 secondes, 768p et le prompt de mouvement ci-dessus. Le serveur MCP demande confirmation du prix avant la création.
  • Skill : installez avec npx skills add spicyapi-ai/spicy-skill, puis demandez à $spicyapi d’exécuter le même flux et de s’arrêter pour votre accord. Le Skill utilise une connexion CLI ou MCP configurée.

Prévoyez le budget des essais, pas seulement du clip final

Ces estimations sont en dollars américains. Confirmez le prix en vigueur avant de générer.

RequêtePrix estimé
Une image fixe Z-Image, PNG 16:90,010 $
La même image avec extension du prompt0,016 $
H3, 4 s en 768p0,148 $
H3, 5 s en 768p (valeur par défaut si resolution est omis)0,185 $
H3, 5 s en 2k0,290 $
H3, 10 s en 768p0,370 $
H3, 15 s en 2k0,870 $
  • Durée et résolution : H3 coûte 0,037 $ ou 0,058 $ par seconde en 768p ou 2k.
  • Combinaisons non prises en charge : un clip de 4 secondes en 2k est refusé avec l’erreur 40004.
  • Deux brouillons : une image fixe et deux vidéos de cinq secondes en 768p coûtent 0,380 $.
  • Brouillon puis prise de livraison : une image fixe, un contrôle de cinq secondes en 768p et une prise de cinq secondes en 2k coûtent 0,010 $ + 0,185 $ + 0,290 $ = 0,485 $, en réutilisant l’image validée.

L’argent est réservé à l’acceptation d’une tâche, puis le montant est réglé à sa fin. Les tâches échouées ou expirées libèrent normalement la réservation ; certains refus de contenu sont facturés selon la politique annoncée du modèle. Vérifiez le cost réel et l’état settled de la tâche lorsque vous examinez une facturation.

Corrigez le problème à la bonne étape

SymptômeModification à apporter
La tasse a deux anses ou du texte parasiteRévisez le prompt d’image et validez une nouvelle image de départ avant de générer la vidéo
La tasse bouge alors que vous vouliez déplacer la caméraGardez l’image ; demandez un mouvement de caméra et explicitement une tasse immobile
La vidéo est bonne en 768p mais la nouvelle prise 2k diffèreTraitez-la comme une nouvelle prise et vérifiez le mouvement avant de l’accepter
Le lien de l’image échoueTéléversez le fichier ou utilisez une URL d’image directe et accessible plutôt qu’une page de partage
Le compte renvoie 401 ou 40310Vérifiez la clé ou terminez la vérification de l’adresse e-mail
40301 / 40302 / 40303Vérifiez les permissions du modèle, la liste d’IP autorisées de la clé ou la disponibilité régionale
40201 / 40202Vérifiez le solde disponible ou le plafond de dépenses du compte/de la clé ; gardez la requête sauvegardée lors du rapprochement d’un envoi échoué
Une requête rejette seed, aspect_ratio ou output_formatVérifiez les entrées acceptées par la tâche choisie ; ne copiez pas les champs de Z-Image dans H3
40004Changez la combinaison incompatible : 4 secondes en 2k
40901 ou conflit d’idempotenceDemandez un nouveau devis : si aucun ID de tâche n’a été sauvegardé, supprimez l’entrée de cette étape dans state.json et relancez. Si l’essai précédent a pu être accepté, consultez votre liste de tâches avant toute autre création ; voir la référence des erreurs
429 ou erreur de connexionGardez le dossier d’exécution et l’ID de tâche ; ne lancez pas un nouvel essai payant uniquement pour récupérer le résultat
Une tâche signale content_rejected ou timeoutLisez son erreur et son coût, puis choisissez délibérément un prompt modifié, une autre image source ou un plan plus court
La génération a réussi mais le résultat n’est pas prêtReprenez la même tâche sauvegardée ; ne générez pas un autre clip pour obtenir un téléchargement

Gardez le premier flux de travail simple

Une fois le plan satisfaisant, choisissez sa durée et sa résolution de livraison, puis générez et vérifiez cette prise. Une image finale facultative peut guider la fin via last_image_url de H3 ; elle ne garantit ni une dernière image exacte ni le mouvement entre les images.

Pour une application, gardez les deux ID de tâche et stockez les fichiers validés dans votre propre stockage. Utilisez la comparaison entre interrogation et Webhooks pour choisir la collecte des résultats, le stockage des médias pour conserver les fichiers et la génération par lots pour de nombreux plans. Le premier jalon reste identique : une image validée et un clip qui suit réellement le plan voulu.

Aussi en English · 日本語 · 한국어 · Deutsch · Español · Português (Brasil) · Русский · Italiano · Polski