HiAPI
Blogue

Tutorial

API de texto para vídeo Happyhorse 1.1: tutorial executável de curl e Python

Jun 26, 2026
API de texto para vídeo Happyhorse 1.1: tutorial executável de curl e Python

Esta página apresenta a requisição mínima para gerar um vídeo com happyhorse-1.1/text-to-video, explica o que cada campo de entrada aceita na prática e mostra as duas formas de esperar pelo resultado (polling e callback). Todo o conteúdo abaixo foi testado contra a API de produção; as estruturas de requisição e as mensagens de erro são reproduzidas literalmente.

O que você vai construir

Uma função: recebe um prompt de texto e devolve uma URL MP4 para download. O modelo é happyhorse-1.1/text-to-video, disponibilizado através do endpoint unificado /v1/tasks. Você submete uma tarefa, recebe imediatamente um taskId e depois faz polling até concluir ou deixa o hiapi enviar o resultado final por POST para o seu webhook.

Pré-requisitos:

  • Uma API key do hiapi. Contas novas recebem 1 dólar de crédito gratuito — obtenha na página de API Keys.
  • Uma forma de fazer requisições HTTPS. Não é necessário instalar SDK.

Não coloque a key no código do cliente nem a submeta ao git. Leia de uma variável de ambiente:

export HIAPI_API_KEY="sk-..."

Requisição mínima funcional

O único campo de entrada obrigatório é prompt. Todos os outros têm valores predefinidos.

curl

curl -X POST https://api.hiapi.ai/v1/tasks \
  -H "Authorization: Bearer $HIAPI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "happyhorse-1.1/text-to-video",
    "input": {
      "prompt": "a calm ocean wave at sunset, slow motion, golden hour"
    }
  }'

Resposta:

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

Este taskId é a sua credencial para operar essa tarefa. Guarde-o com cuidado.

Python (apenas biblioteca padrão)

import os
import urllib.request
import json

API_KEY = os.environ["HIAPI_API_KEY"]

def submit_task(prompt: str, **input_overrides) -> str:
    body = {
        "model": "happyhorse-1.1/text-to-video",
        "input": {"prompt": prompt, **input_overrides},
    }
    req = urllib.request.Request(
        "https://api.hiapi.ai/v1/tasks",
        method="POST",
        headers={
            "Authorization": f"Bearer {API_KEY}",
            "Content-Type": "application/json",
        },
        data=json.dumps(body).encode("utf-8"),
    )
    with urllib.request.urlopen(req) as resp:
        payload = json.loads(resp.read())
    if payload.get("code") != 200:
        raise RuntimeError(payload)
    return payload["data"]["taskId"]

task_id = submit_task("a calm ocean wave at sunset, slow motion, golden hour")
print(task_id)  # e.g. tk-hiapi-01KW23VDG2EZ20J7RQH8S67MNC

Tabela completa de parâmetros

A estrutura de parâmetros é pequena e estrita — campos desconhecidos são rejeitados com 400, não descartados silenciosamente. Seguem os campos dentro de input que a API aceita na prática, extraídos diretamente das mensagens de validação:

Campo Tipo Obrigatório Valores permitidos Comportamento padrão
prompt string Sim Qualquer texto não vazio descrevendo a cena —
aspect_ratio string Não 16:9, 9:16, 3:4, 4:3, 4:5, 5:4, 1:1, 9:21, 21:9 Padrão do modelo
resolution string Não 720p, 1080p Padrão do modelo
duration integer Não 3 a 15 (segundos) Padrão do modelo

Campos que parecem naturais, mas que atualmente não são aceitos: size, audio, seed, negative_prompt. Enviar qualquer um deles retorna additional properties '<field>' not allowed. Se você já usou outros modelos hiapi — qwen-image-2.0 usa size, z-image usa aspect_ratio — observe que happyhorse-1.1/text-to-video pertence à família da proporção de aspeto, não à família do tamanho explícito.

Uma requisição que usa todos os parâmetros suportados:

curl -X POST https://api.hiapi.ai/v1/tasks \
  -H "Authorization: Bearer $HIAPI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "happyhorse-1.1/text-to-video",
    "input": {
      "prompt": "a vintage paper plane gliding above neon Tokyo at night, cinematic, 24fps",
      "aspect_ratio": "16:9",
      "resolution": "1080p",
      "duration": 8
    }
  }'

Sobre custo e duração

O modelo cobra pelos segundos de vídeo gerado — consulte a página de preços para a tarifa atual. duration é a alavanca de custo; resolution não é. Passar de 5 para 15 segundos triplica o custo; no momento da redação, passar de 720p para 1080p não. Durante o desenvolvimento, use duration: 3 e uma resolução mais baixa por padrão; só suba para 1080p com prompts finalizados.

Obter o resultado: polling

A forma mais simples de buscar a saída é fazer GET da tarefa por id até que status atinja um estado terminal.

import time
import urllib.request
import json

def get_task(task_id: str) -> dict:
    req = urllib.request.Request(
        f"https://api.hiapi.ai/v1/tasks/{task_id}",
        headers={"Authorization": f"Bearer {API_KEY}"},
    )
    with urllib.request.urlopen(req) as resp:
        return json.loads(resp.read())["data"]

def wait_for_task(task_id: str, timeout_s: int = 600, poll_interval_s: int = 10) -> dict:
    deadline = time.time() + timeout_s
    while time.time() < deadline:
        task = get_task(task_id)
        status = task["status"]
        if status == "success":
            return task
        if status == "fail":
            raise RuntimeError(f"task failed: {task}")
        time.sleep(poll_interval_s)
    raise TimeoutError(f"task {task_id} still {status} after {timeout_s}s")

task = wait_for_task(submit_task("a quiet coffee shop on a rainy afternoon"))
video_url = task["output"][0]["url"]
print(video_url)

Alguns detalhes importantes:

  • status passa por queued → handling → archiving → success (ou fail). Qualquer estado anterior a success ou fail não deve ser considerado terminal.
  • O tempo de geração aumenta com duration e com a profundidade atual da fila. Tarefas de 5 segundos a 720p normalmente terminam em poucos minutos; 1080p a 15 segundos exige reservar mais tempo.
  • output[].url fica em armazenamento temporário. Baixe os bytes imediatamente e re-hospede por conta própria; não guarde apenas a URL para entregar ao utilizador final — ela expira.
import urllib.request

def download(video_url: str, out_path: str) -> None:
    with urllib.request.urlopen(video_url) as resp, open(out_path, "wb") as f:
        f.write(resp.read())

download(video_url, "ocean.mp4")

Obter o resultado: callback (modo de produção)

O polling é adequado para scripts e notebooks, mas num backend real você não vai querer que um worker fique ocupado durante minutos por vídeo. Passe um objeto callback na submissão e o hiapi fará POST do corpo da tarefa concluída exatamente uma vez para a sua URL.

curl -X POST https://api.hiapi.ai/v1/tasks \
  -H "Authorization: Bearer $HIAPI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "happyhorse-1.1/text-to-video",
    "input": {
      "prompt": "an astronaut planting a flag on a glass beach",
      "aspect_ratio": "16:9",
      "resolution": "1080p",
      "duration": 6
    },
    "callback": {
      "url": "https://example.com/hooks/hiapi",
      "when": "final"
    }
  }'

Atualmente apenas when: "final" é suportado — enviar outro valor retorna:

{ "code": 400, "error_code": "INVALID_REQUEST",
  "message": "invalid callback.when: only 'final' is supported" }

Projete um recetor robusto

Três regras garantem a segurança do caminho de callback:

  1. Idempotência. Persista por taskId. Se receber o mesmo callback duas vezes — o que acabará por acontecer, devido a novas tentativas — o processamento repetido deve ser uma operação sem efeito.
  2. Desacople a confirmação do processamento. Assim que colocar o payload na fila, devolva 200 OK a partir do webhook. O download / reenvio / gravação no banco de dados real fica numa tarefa em segundo plano. Webhooks lentos atraem timeouts e entregas duplicadas.
  3. Recorra ao polling. Guarde cada taskId que submeter, junto com created_at, na sua própria base de dados. Execute um varredor que faça polling de /v1/tasks/{id} para qualquer linha com mais de cerca de 30 minutos que ainda não tenha estado terminal. O callback é o caminho rápido, não o único caminho.

Um recetor FastAPI mínimo:

from fastapi import FastAPI, Request

app = FastAPI()

@app.post("/hooks/hiapi")
async def hiapi_callback(req: Request):
    body = await req.json()
    task = body.get("data", body)  # callback wraps the same shape as GET
    task_id = task["taskId"]
    if task["status"] == "success":
        url = task["output"][0]["url"]
        # enqueue, don't download here
        await queue.put({"task_id": task_id, "url": url})
    return {"ok": True}

Erros que você realmente vai encontrar

A forma do envelope de erro é sempre consistente — {code, error_code, message, data} — portanto você pode escrever um único handler. Alguns que merecem tratamento especial para este modelo:

HTTP error_code Condição de disparo Como corrigir
400 INVALID_REQUEST Campo desconhecido, valor de enumeração incorreto, duration fora de 3–15 Verifique o message, que indicará o campo — por exemplo aspect_ratio: value must be one of '16:9'...
401 — Key ausente ou incorreta, ou permission_denied Verifique novamente o cabeçalho Authorization: Bearer sk-...
402 — Saldo insuficiente para cobrir o custo estimado Recarregue em hiapi.ai/en/pricing e tente novamente
429 — Você está martelando o endpoint Recue e tente novamente com jitter

Uma falha de validação real é mostrada abaixo — pode copiá-la como fixture de teste:

{
  "code": 400,
  "data": null,
  "error_code": "INVALID_REQUEST",
  "message": "invalid input: aspect_ratio: value must be one of '16:9', '9:16', '3:4', '4:3', '4:5', '5:4', '1:1', '9:21', '21:9'; resolution: value must be one of '720p', '1080p'; duration: maximum: got 99, want 15"
}

Note que cada campo é reportado na mesma resposta. Valide localmente quando puder, mas confie nisto quando não puder.

Conteúdo relacionado no hiapi

Perguntas frequentes

O happyhorse-1.1/text-to-video gera áudio? Através da API de tarefas, não — a estrutura de requisição atualmente não tem campo audio, e enviá-lo retorna additional properties 'audio' not allowed. A saída é um MP4 sem som. Adicione a trilha sonora a jusante.

Quais proporções de aspeto estão disponíveis para formato vertical / vídeos curtos? Para saídas no estilo TikTok e Reels, 9:16 é a escolha padrão. A API também aceita 4:5 (vertical para o feed do Instagram) e 9:21 (vertical extra longo), caso precise de um enquadramento mais estreito.

Qual é a duração mínima e máxima? duration é um inteiro de 3 a 15 segundos (inclusive). Enviar 0 resulta em duration: minimum: got 0, want 3; enviar 99 resulta em duration: maximum: got 99, want 15. Escolha o menor valor que capture o movimento necessário — a duração é a parte pela qual você paga.

É possível passar seed para obter saídas reprodutíveis? Atualmente o modelo não suporta isso. Pedir {"input": {"prompt": "...", "seed": 42}} falha com additional properties 'seed' not allowed. Se a reprodutibilidade for importante, registe o corpo completo da requisição e tente novamente com o mesmo prompt; obterá resultados semelhantes — mas não idênticos pixel a pixel.

Suporta negative_prompt? Não. Guie a saída adicionando restrições no próprio prompt (por exemplo "...clean composition, no text, no watermarks").

Quanto tempo dura a URL de saída? No plano padrão, o campo storage da tarefa é temp e output[].url é um link assinado de curta duração. Baixe o ficheiro e re-hospede assim que a tarefa atingir success. Não guarde a URL original para servir mais tarde.

Como controlar os gastos durante o desenvolvimento? Dois hábitos que poupam dinheiro: mantenha duration: 3 até finalizar o prompt e submeta com resolution: "720p" até confirmar o storyboard. Só a renderização final merece 1080p e durações maiores.

Polling ou callback — qual usar? Use ambos, nesta ordem. Primeiro o polling, porque tem menos peças móveis. Quando tiver o primeiro backend que não pode dar-se ao luxo de bloquear um worker em wait_for_task, mude para callback. Em qualquer caso, mantenha o polling como varredor de recurso; o callback é melhor esforço, não entrega garantida.

Por que recebo 402 mesmo tendo crédito para chamadas? A API estima o teto de custo da tarefa — duração × preço por segundo — e reserva antecipadamente. Se o saldo não cobrir essa estimativa, retorna 402 imediatamente. Recarregue, tente novamente e, após o sucesso, apenas o custo real é debitado.

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