Tutorial
Usando Veo 3.1 Lite para imagem-para-vídeo no HiAPI: tutorial com curl e Python
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
Authorizationcarrega seu token bearer. A forma$HIAPI_API_KEYlê de uma variável de ambiente, de modo que a chave nunca aparece no histórico do shell. modelidentificaveo-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.durationsolicita 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:
- 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. - Timeout rígido. Sempre limite o tempo total de espera. Uma tarefa presa em
processingdeve se manifestar como um erro que possa gerar alerta, não como um loop infinito. - 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=Trueacima, 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 →-
Tutorial
Como chamar hailuo-2.3-fast/image-to-video pela API HiAPI: curl e Python na prática
A primeira requisição executável do nível rápido e econômico de geração de vídeo a partir de imagem — do ID do modelo ao download do MP4
-
Tutorial
Como chamar hailuo-2.3/text-to-video pela API HiAPI: curl, Python e exemplos de requisição executáveis
-
Tutorial
Usar Kandinsky 6.0 Pro text-to-video na HiAPI: tutorial de curl e Python
Demonstração prática com foco na requisição: montar a chamada de prompt para clipe de vídeo, enviar a requisição, consultar o status de conclusão e baixar o resultado