HiAPI
Blogue

Tutorial

Como usar a API de imagem para vídeo do Ray 3.2: curl, Python e uma requisição que funciona

Oct 20, 2026
Como usar a API de imagem para vídeo do Ray 3.2: curl, Python e uma requisição que funciona

O Ray 3.2 imagem para vídeo anima um quadro estático: você envia uma imagem mais um prompt descrevendo como ela deve se mover, e ele devolve um vídeo curto que começa a partir da sua imagem. No hiapi, o id do modelo é ray-3.2/image-to-video, chamado pela mesma API assíncrona de tarefas unificada de todos os outros modelos de vídeo — POST /v1/tasks, depois polling ou callback.

É um endpoint diferente do Ray 3.2 texto para vídeo, não apenas um prompt diferente. Seu schema de entrada é menor e mais rígido, e dois controles que você talvez espere do modelo de texto não estão lá. Este guia entrega uma requisição que funciona de primeira: o schema exato (verificado contra a API em produção em 2026-10-20), curl pronto para copiar, um script Python completo de envio/polling/download e detalhes de produção — callbacks, idempotência e respostas de erro reais, incluindo aquele modo de falha que não aparece onde você espera.

O que você vai construir e o que precisa ter

Objetivo: enviar uma imagem de primeiro quadro + um prompt de movimento, fazer polling da tarefa e baixar um .mp4 sem áudio que anima sua imagem.

Pré-requisitos:

  • Uma chave de API do hiapi — pegue uma no seu painel. A chave se parece com sk-... e vai no cabeçalho Authorization: Bearer. Novo na API de tarefas? Leia primeiro o quickstart.
  • Uma URL de imagem publicamente acessível para a imagem do primeiro quadro (detalhes abaixo). Qualquer cliente HTTP — vamos mostrar curl e Python (requests).

O Ray 3.2 imagem para vídeo é cobrado por clipe de vídeo. Como a duração do clipe é fixa (veja abaixo), resolution é a única alavanca de custo que você tem — um clipe em 1080p custa várias vezes mais que um em 540p. Consulte os preços atuais e prototipe com 540p enquanto itera nos prompts.

O schema de entrada exato (dois campos obrigatórios, um opcional)

Tudo fica dentro do objeto input. O schema é rígido — ele rejeita qualquer campo não reconhecido. Verificado campo a campo contra a API em produção:

Campo Tipo Obrigatório Valores permitidos Padrão
prompt string Sim Como a imagem deve se mover (câmera, movimento do sujeito) —
image_url string Sim URL pública do primeiro quadro —
resolution string Não 540p, 720p, 1080p 540p

Três armadilhas para quem vem do modelo de texto para vídeo:

  1. Sem duration. A duração do clipe é fixa (cerca de 5 segundos). Enviar duration não é truncado — é rejeitado de imediato. É o oposto do Ray 3.2 texto para vídeo, onde duration é um inteiro selecionável (5 ou 10).
  2. Sem aspect_ratio. O formato da saída segue sua imagem de entrada, então não há nada para definir. No modelo de texto você escolhe a proporção; aqui a imagem decide. (Confirmado abaixo com uma execução real: entrada quadrada gerou clipe quadrado.)
  3. O schema é fechado. seed, negative_prompt, prompt_optimizer e qualquer outro campo retornam 400 com additional properties '<field>' not allowed. Não porte um corpo de requisição de outro modelo supondo que chaves extras serão ignoradas.

O que a validação realmente retorna

Estas são respostas reais do endpoint em produção — enviando valores inválidos de propósito, sem criar tarefa e sem cobrança:

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

O message aponta o campo problemático, então a própria API é sua referência de schema quando você esquecer.

Preparando o primeiro quadro

image_url precisa ser uma URL que o worker do hiapi consiga buscar — um link público, não um caminho local nem um blob base64 data:. A imagem determina a proporção do clipe e a composição inicial, então parta do quadro exato em que você quer que o movimento comece.

Uma armadilha que vale antecipar, porque é fácil de ignorar: um image_url ruim não é capturado na criação da tarefa. A chamada de criação valida apenas a forma da requisição (é uma string?), não se a URL resolve. Uma URL de imagem inacessível ou 404 retorna um 200 perfeitamente normal mais um taskId — e então a tarefa vira fail alguns segundos depois. Veremos o payload exato de falha na seção de solução de problemas; a lição é sempre ramificar pelo status terminal da tarefa, nunca assumir que um 200 na criação significa sucesso.

Exemplo mínimo que funciona: curl

Crie a tarefa:

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

A resposta de criação traz um id de tarefa:

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

Faça polling até chegar a um estado terminal:

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

Enquanto a tarefa roda, data.status passa por vários estados em andamento (você verá valores como handling, depois archiving). Os dois estados terminais são success e fail. Em caso de sucesso:

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

Baixe data.output[0].url imediatamente. É uma URL assinada em temp.hiapi.ai com expireAt (timestamp Unix, cerca de uma semana depois) — persista os bytes no seu próprio armazenamento em vez de fazer hotlink. O Ray 3.2 retorna MP4 sem áudio; não há faixa de áudio para esperar.

Script Python completo (enviar → polling → baixar)

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

Execute com pip install requests, use sua chave e um image_url real, e você terá um .mp4 no disco quando a tarefa terminar.

Como é uma execução real

Para tornar os números concretos, aqui está uma execução real em 540p contra a API em produção em 2026-10-20. Primeiro quadro: uma imagem 1600×1600 (quadrada); prompt: push-in cinematográfico lento.

  • A tarefa passou por handling → archiving → success, com o trabalho do servidor concluído em cerca de 45 segundos (created criação, completed conclusão) — cerca de 60 segundos de tempo de relógio contando o laço de polling de 8 segundos. Tarefas de vídeo levam dezenas de segundos a minutos, não são instantâneas; reserve prazos generosos.
  • A saída foi um MP4 720×720, cerca de 1,3 MB, 5,04 segundos, sem áudio.

Essa execução revela duas coisas que a tabela de parâmetros não diz:

  1. A proporção da saída corresponde à da entrada — quadrado entra, quadrado sai. É por isso que não há campo aspect_ratio; enquadre o image_url na composição que você quer no clipe.
  2. resolution é um nível de qualidade, não uma altura literal em pixels. Pedir 540p para uma entrada 1600×1600 retornou 720×720 — o nível escala pela forma da entrada em vez de forçar um tamanho fixo. Se você precisa de dimensões exatas de saída, redimensione o clipe entregue.

Notas de produção: callbacks, idempotência, erros

Prefira callbacks ao polling

Para qualquer coisa além de um script, registre um webhook na criação em vez de fazer 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" }
}

Com "when": "final", o hiapi chama seu endpoint uma vez quando a tarefa atinge um estado terminal — sem tráfego de polling e sem perder a conclusão entre intervalos. Seu handler então faz GET /v1/tasks/<id> para buscar output[0].url. Para CLIs e notebooks que não conseguem expor um endpoint HTTPS, o polling continua bom.

Faça os retries serem idempotentes

Persista o taskId no momento em que a chamada de criação retorna. Se seu processo cair no meio da geração, retome fazendo polling do id armazenado — não recrie a tarefa, ou você pagará por um segundo clipe. Só recrie se a requisição de criação original falhou antes de retornar um id de tarefa. O laço de polling tolerante acima já segue essa regra: em uma requisição de rede interrompida, refaça o polling do mesmo id em vez de recomeçar do zero.

Erros que você realmente verá

Cada linha da tabela abaixo é uma forma de resposta verificada em produção:

Sintoma Resposta Correção
Chave ausente/inválida 401, aninhado {"error":{"code":"permission_denied","type":"hiapi_error","request_id":"...","message":"This API key is invalid..."}} Verifique o cabeçalho Authorization: Bearer sk-... e se a chave tem acesso. Informe o request_id ao contatar o suporte.
image_url ausente 400 INVALID_REQUEST, message: "image_url: missing required field" Este modelo exige image_url — não há modo somente texto.
Enum errado / campo extra 400 INVALID_REQUEST, plano {"code":400,"error_code":"INVALID_REQUEST","message":"..."} Leia o campo apontado no message. Para duration/aspect_ratio, ele dirá additional properties ... not allowed — remova-os.
Id de tarefa errado no polling 404, plano {"code":404,"data":null,"message":"task not found"} (atenção: não há error_code no 404) Id errado, ou você está fazendo polling com a chave de uma conta diferente.
image_url inacessível A criação retorna 200 + um taskId; a tarefa depois termina com data.status:"fail" e data.error:{"code":"INVALID_REQUEST","message":"Invalid request parameters for this model..."} É a armadilha. A chamada de criação não consegue distinguir link morto de link vivo. Verifique se sua imagem é publicamente acessível e sempre leia o data.status terminal.

A última linha merece ser memorizada: mesmo quando a tarefa falha, o envelope HTTP do polling continua 200 — a falha está em data.status e data.error, não no código de status HTTP. Tarefas com falha são reembolsadas, mas seu código ainda precisa notá-las. Ramifique por data.status == "fail", não confie no 200.

Recursos relacionados

Perguntas frequentes

Qual é o id do modelo e qual endpoint devo chamar? ray-3.2/image-to-video, enviado para POST https://api.hiapi.ai/v1/tasks, com os parâmetros sob input. É assíncrono — você recebe um taskId e depois faz polling em GET /v1/tasks/<id> (ou registra um callback).

O que a entrada exige? Exatamente dois campos obrigatórios — prompt (movimento) e image_url (URL pública do primeiro quadro) — mais o opcional resolution (540p/720p/1080p). Qualquer outra coisa é rejeitada.

Por que não posso definir duração ou proporção? A duração do clipe é fixa (cerca de 5 segundos), então não há campo duration; a proporção da saída é herdada da sua imagem de entrada, então não há campo aspect_ratio. Enviá-los retorna 400. Se você precisa de duração selecionável, use o Ray 3.2 texto para vídeo.

Minha chamada de criação retornou 200, mas o vídeo nunca chegou. Por quê? O mais comum é image_url inacessível. A chamada de criação valida apenas a forma da requisição, então um link morto ainda retorna um taskId — e a tarefa termina em fail. Faça polling até o estado terminal e leia data.error. Garanta que a imagem é publicamente acessível (sem autenticação, não é caminho local, não é base64).

O clipe tem áudio? Não — o Ray 3.2 imagem para vídeo retorna MP4 sem áudio. Adicione som no pós-processamento ou use um modelo de áudio separado.

Quanto custa um clipe? A cobrança é por clipe e, como a duração é fixa, resolution é a única alavanca — um clipe em 1080p custa várias vezes mais que um em 540p. Os preços mudam, então consulte a página de preços em vez de codificar números fixos, e prototipe com 540p.

Quanto tempo devo esperar antes de desistir? Dezenas de segundos a alguns minutos é normal. Tolere data.status não terminais no laço de polling e use um prazo generoso (10–15 minutos), ou registre um callback com "when": "final" para o hiapi avisar você em vez de você adivinhar intervalos.

Artigos relacionados

Ver todos os artigos →

Comece a criar com artefactos de IA persistentes

Obtenha a sua chave de API e chame modelos de imagem, vídeo e áudio através de uma única API baseada em tasks.

Ler a documentação da API