HiAPI
Blogue

Tutorial

Usando Veo 3.1 Lite para imagem-para-vídeo no HiAPI: tutorial com curl e Python

Nov 3, 2026
午夜蓝底上铜色精细平行刻线由密到疏流动,抽象呼应图像转为视频的教程封面图。

O que veo-3.1-lite/image-to-video faz

veo-3.1-lite/image-to-video é um modelo de conversão de primeiro quadro em vídeo. Você fornece uma imagem estática e ele anima esse quadro, gerando um vídeo curto. A imagem que você fornece não é uma referência de estilo, nem uma fonte difusa de inspiração — ela é literalmente o quadro inicial do vídeo de saída, portanto a composição, a posição do sujeito e o enquadramento continuam diretamente nos primeiros segundos de movimento.

Essa escolha de design determina como você deve encarar a entrada. Uma imagem bem iluminada, nítida e bem recortada gera um vídeo que começa exatamente de onde você espera. Uma imagem desfocada ou com enquadramento estranho gera um vídeo que começa desfocado e com enquadramento estranho, e que ainda deriva mais à medida que o modelo gera o movimento. A maior parte do esforço para usar bem este modelo acontece antes de você escrever qualquer código.

No lado da saída, o modelo retorna um vídeo com áudio nativo. É exatamente isso que o diferencia dos pipelines antigos de imagem-para-vídeo — os fluxos antigos geravam primeiro o quadro sem som e depois montavam uma faixa de áudio separada. Aqui, o áudio é gerado como parte da mesma tarefa, então o som ambiente permanece coerente com a cena visual — sem necessidade de mixagem manual e sem desalinhamento temporal entre os efeitos sonoros e o movimento correspondente.

A geração é assíncrona. Você envia uma tarefa, recebe um identificador e faz polling até a tarefa terminar. É uma escolha arquitetural deliberada: a geração de vídeo leva muito mais tempo do que o timeout típico de uma requisição HTTP, então a API desacopla o envio da conclusão. Todos os exemplos deste tutorial seguem esse formato de "enviar primeiro, fazer polling depois".

Em resumo: entra uma imagem, sai um vídeo com áudio, entregue por meio de uma tarefa assíncrona. Guarde esse modelo mental e o restante da API se torna bastante direto.

Antes de enviar a primeira requisição

Para que a primeira chamada tenha sucesso, três coisas precisam estar prontas.

Uma chave de API

Toda requisição ao HiAPI.ai é autenticada com uma chave de API. Crie uma no painel da sua conta e carregue-a a partir de uma variável de ambiente, em vez de colá-la diretamente no arquivo-fonte:

export HIAPI_API_KEY="your_api_key_here"

Se alguma chave já foi enviada para um repositório, rotacione-a imediatamente. Substituir uma chave custa pouco; vazá-la custa caro.

Uma imagem de primeiro quadro acessível

O modelo precisa buscar a imagem por conta própria, então ela deve estar acessível via URL — um link de armazenamento de objetos público, um caminho de CDN ou qualquer endpoint HTTP que não exija autenticação interativa. URLs assinadas com longa validade funcionam; URLs que exigem uma sessão de login, não. Use formatos comuns como PNG ou JPEG e escolha uma resolução razoável: uma imagem nítida e de tamanho moderado fornece ao modelo detalhes limpos sem desperdiçar largura de banda.

Se sua imagem de origem estiver atrás de um serviço interno, faça upload para o armazenamento de objetos primeiro e passe a URL gerada. Essa costuma ser a causa mais comum de falha na primeira tentativa.

O caminho do endpoint

O modelo é identificado por veo-3.1-lite/image-to-video. Trate essa string como o identificador de modelo enviado com a tarefa, em vez de codificá-la em uma dúzia de lugares — coloque-a em uma constante, porque provavelmente você vai querer alternar entre variantes de modelo mais tarde.

Com a chave exportada, uma URL de imagem acessível em mãos e o identificador do modelo, você está pronto para enviar.

Uma requisição curl funcional

A chamada de envio cria uma tarefa de geração. Um exemplo completo e funcional é este:

curl -X POST "https://api.hiapi.ai/v1/video/generations" \
  -H "Authorization: Bearer $HIAPI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "veo-3.1-lite/image-to-video",
    "image": "https://your-bucket.example.com/first-frame.jpg",
    "prompt": "A luz da manhã se move lentamente pela cena, a câmera avança devagar",
    "duration": 5
  }'

Item por item:

  • A URL é o endpoint de geração no HiAPI. Todo o tráfego de modelos passa por essa única interface, então trocar de modelo depois exige apenas alterar o campo model, não o endpoint.
  • O cabeçalho Authorization carrega seu token bearer. A forma $HIAPI_API_KEY lê de uma variável de ambiente, de modo que a chave nunca aparece no histórico do shell.
  • model identifica veo-3.1-lite/image-to-video.
  • image é a URL do primeiro quadro. É esse campo que torna esta chamada imagem-para-vídeo em vez de texto-para-vídeo.
  • prompt é opcional, mas altamente recomendado. Ele descreve o movimento que você deseja — movimento de câmera, mudanças de iluminação, ação do sujeito —, não uma descrição da cena estática, já que o modelo já consegue ver a imagem.
  • duration solicita a duração do vídeo em segundos, limitada ao intervalo suportado pelo modelo.

A resposta é um objeto de tarefa contendo um id. Guarde-o:

{
  "id": "job_abc123",
  "status": "queued",
  "model": "veo-3.1-lite/image-to-video"
}

Um status queued ou processing é o resultado esperado — você ainda não tem o vídeo, apenas uma promessa. A próxima seção explica como transformar a promessa em arquivo.

A mesma chamada em Python

A chamada curl pode ser traduzida diretamente para Python com requests. A versão abaixo encapsula o envio em uma pequena função, facilitando a reutilização em pipelines:

import os
import requests

API_KEY = os.environ["HIAPI_API_KEY"]
BASE_URL = "https://api.hiapi.ai/v1"
HEADERS = {
    "Authorization": f"Bearer {API_KEY}",
    "Content-Type": "application/json",
}

def submit_video_job(image_url: str, prompt: str, duration: int = 5) -> str:
    payload = {
        "model": "veo-3.1-lite/image-to-video",
        "image": image_url,
        "prompt": prompt,
        "duration": duration,
    }
    response = requests.post(
        f"{BASE_URL}/video/generations",
        headers=HEADERS,
        json=payload,
        timeout=30,
    )
    response.raise_for_status()
    data = response.json()
    print(f"Submitted job {data['id']} ({data['status']})")
    return data["id"]

if __name__ == "__main__":
    job_id = submit_video_job(
        image_url="https://your-bucket.example.com/first-frame.jpg",
        prompt="A luz da manhã se move lentamente pela cena, a câmera avança devagar",
    )

Alguns detalhes merecem ser mantidos:

  • raise_for_status() transforma respostas não-2xx em exceções o mais cedo possível. Sem ele, erros de autenticação ou validação parecem uma chamada bem-sucedida que retornou um corpo de resposta inesperado.
  • O timeout de 30 segundos se aplica apenas ao envio. O envio deve retornar rapidamente; o trabalho longo acontece no servidor. Se o próprio envio der timeout, o problema está na conectividade ou no tamanho do payload da requisição, não na velocidade de geração.
  • Retornar o id da tarefa em vez da resposta inteira mantém a função componível — cabe ao chamador decidir o que fazer com a tarefa.

Com isso, a função pode entrar sem modificações em um worker de fila, em um endpoint Flask ou em uma célula de notebook.

Fazendo polling da tarefa assíncrona

A geração de vídeo leva minutos, não milissegundos. Faça polling do endpoint de status da tarefa em intervalos próximos do ritmo humano e pare imediatamente quando a tarefa atingir um estado terminal:

import time
import requests

def wait_for_video(job_id: str, timeout: int = 600, interval: int = 10) -> dict:
    deadline = time.time() + timeout
    url = f"{BASE_URL}/video/generations/{job_id}"
    while time.time() < deadline:
        response = requests.get(url, headers=HEADERS, timeout=30)
        response.raise_for_status()
        data = response.json()
        status = data["status"]
        print(f"Job {job_id}: {status}")
        if status == "completed":
            return data
        if status == "failed":
            raise RuntimeError(f"Generation failed: {data}")
        time.sleep(interval)
    raise TimeoutError(f"Job {job_id} did not finish within {timeout}s")

Três práticas tornam isso confiável em produção:

  1. Backoff exponencial. Comece com intervalos curtos e aumente gradualmente, com um limite razoável. O primeiro polling normalmente ainda retorna queued, então martelar o endpoint nos primeiros segundos não traz benefício.
  2. Timeout rígido. Sempre limite o tempo total de espera. Uma tarefa presa em processing deve se manifestar como um erro que possa gerar alerta, não como um loop infinito.
  3. Persistência do id da tarefa. Grave-o no banco de dados antes de iniciar o polling. Se o processo falhar no meio da espera, após reiniciar você pode continuar o polling em vez de reenviar e pagar duas vezes pelo mesmo trabalho.

Considere apenas completed e failed como estados que encerram o loop. Todo o resto — queued, processing — significa continuar esperando.

Mais um lembrete: se você executar esse código em um servidor web, não faça polling dentro do handler da requisição. Envie, devolva o id da tarefa ao cliente e deixe um worker em segundo plano concluir a espera. Caso contrário, uma geração lenta ocupará a conexão por minutos.

Baixando o vídeo de saída

Quando a tarefa termina, a resposta de status inclui a URL do vídeo finalizado. Baixe como dados binários e grave em disco, usando a extensão adequada:

def download_video(job: dict, path: str = "output.mp4") -> str:
    video_url = job["output"]["url"]
    response = requests.get(video_url, headers=HEADERS, timeout=120, stream=True)
    response.raise_for_status()
    with open(path, "wb") as f:
        for chunk in response.iter_content(chunk_size=1 << 20):
            f.write(chunk)
    print(f"Saved {path}")
    return path

Algumas coisas precisam ser feitas corretamente:

  • Escreva em modo binário. Abrir com "w" em vez de "wb" corrompe o arquivo silenciosamente; o download parece ter sucesso, mas o vídeo não reproduz.
  • Trate arquivos grandes em streaming. O stream=True acima, combinado com a cópia em chunks, mantém o uso de memória estável independentemente do tamanho do vídeo.
  • Calcule um checksum se você for arquivar as saídas em grande escala. Custa uma linha de código e captura downloads truncados antes que cheguem aos usuários.

Sobre o áudio: o vídeo retornado já contém sua faixa de áudio nativa. Não há arquivo de áudio separado para obter, nem etapa de mixagem. Se você remuxar o arquivo em etapas posteriores — por exemplo, para mudar o formato do contêiner —, inclua o fluxo de áudio. Um erro comum é executar uma conversão que remove o áudio por padrão e depois se perguntar onde ele foi parar.

Por fim, as URLs de saída costumam ter validade limitada. Baixe e salve no seu próprio bucket o quanto antes, em vez de guardar a URL e tentar buscá-la dias depois; nessa altura o link provavelmente já expirou.

Pontos comuns de falha

A seguir estão os erros que mais consomem tempo ao solucionar problemas de chamadas de imagem-para-vídeo:

URL de imagem inacessível

De longe, a causa mais comum de falha. O modelo busca sua imagem no servidor, portanto uma URL que funciona no seu navegador autenticado não prova nada. Antes de enviar a tarefa, teste-a com uma requisição simples, sem autenticação. URLs assinadas devem ter validade significativamente maior que o tempo de geração.

Erros de autenticação que parecem erros do modelo

Um cabeçalho Authorization ausente ou malformado normalmente retorna um 401 claro, mas às vezes o desenvolvedor envolve o código de inicialização em uma lógica que engole códigos de status e depois relata "falha do modelo". Antes de depurar o payload da requisição, verifique o código de status bruto.

Polling excessivo ou inexistente

Fazer polling a cada 100 ms desperdiça cota e pode acionar limites de taxa. Já fazer polling uma única vez e desistir ao ver processing significa que a tarefa teve sucesso sem que ninguém soubesse. Siga o padrão de backoff mais timeout descrito acima.

Confundir envio com conclusão

Um envio bem-sucedido retorna uma tarefa, não um vídeo. O código que lê o campo url da resposta de envio não encontra nada. Apenas a resposta de status de conclusão carrega a saída.

Prompt descrevendo a imagem

Um prompt como "um gato sentado no parapeito" apenas repete o que o modelo já vê. Prompts que descrevem movimento — "a câmera recua lentamente enquanto a chuva começa a cair" — produzem resultados melhores. Descreva mudança, não conteúdo.

Perder o id da tarefa em caso de falha

Se o processo morrer entre o envio e a conclusão, um id de tarefa não persistido significa que o trabalho não pode ser recuperado. Persista-o e faça polling de forma assíncrona a partir de um worker. Uma pequena decisão de design evita o processo de depuração mais longo.

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