HiAPI
Блог

Руководство

Happyhorse 1.1 API генерации видео из текста: рабочий туториал по curl и Python

Jun 26, 2026
Happyhorse 1.1 API генерации видео из текста: рабочий туториал по curl и Python

Эта страница проведёт вас через минимальный запрос на генерацию видео с помощью happyhorse-1.1/text-to-video, объяснит, какие значения реально принимает каждое поле ввода, и опишет два способа дождаться результата (опрос и callback). Всё ниже проверено на живом API, структура запросов и тексты ошибок приведены дословно.

Что вы создадите

Функцию, которая принимает текстовый промпт и возвращает URL загружаемого MP4. Модель — happyhorse-1.1/text-to-video, доступная через единый эндпоинт /v1/tasks. Вы отправляете задачу, сразу получаете taskId, а затем либо опрашиваете статус до завершения, либо позволяете hiapi отправить финальный результат POST-запросом на ваш webhook.

Требования:

  • API-ключ hiapi. Новым аккаунтам даётся 1 доллар бесплатного лимита — получите его на странице API Keys.
  • Любой способ отправки HTTPS-запросов. SDK устанавливать не нужно.

Не помещайте ключ в клиентский код и не коммитьте его в git. Читайте его из переменной окружения:

export HIAPI_API_KEY="sk-..."

Минимальный рабочий запрос

Единственное обязательное поле ввода — prompt. У всех остальных полей есть значения по умолчанию.

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

Ответ:

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

Этот taskId — ваш пропуск к работе с задачей. Сохраните его.

Python (только стандартная библиотека)

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

Полная таблица параметров

Структура параметров компактна и строга — неизвестные поля отклоняются с кодом 400, а не молча отбрасываются. Ниже перечислены поля внутри input, которые API реально принимает, взятые напрямую из сообщений об ошибках валидации:

Поле Тип Обязательно Допустимые значения Поведение по умолчанию
prompt string Да любой непустой текст, описывающий сцену —
aspect_ratio string Нет 16:9, 9:16, 3:4, 4:3, 4:5, 5:4, 1:1, 9:21, 21:9 значение модели по умолчанию
resolution string Нет 720p, 1080p значение модели по умолчанию
duration integer Нет от 3 до 15 (секунд) значение модели по умолчанию

Поля, которые кажутся очевидными, но сейчас не принимаются: size, audio, seed, negative_prompt. Отправка любого из них вернёт additional properties '<field>' not allowed. Если вы работали с другими моделями hiapi — qwen-image-2.0 использует size, z-image использует aspect_ratio — учтите, что happyhorse-1.1/text-to-video относится к семейству с соотношением сторон, а не к семейству с явными размерами.

Запрос со всеми поддерживаемыми параметрами:

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

О стоимости и длительности

Модель тарифицируется по секундам сгенерированного видео — текущий тариф смотрите на странице цен.duration — это рычаг стоимости, resolution — нет. Переход с 5 до 15 секунд утроит стоимость; а вот замена 720p на 1080p на момент написания статьи не влияет на цену. Во время разработки используйте по умолчанию duration: 3 и более низкое разрешение; повышайте до 1080p только на окончательно утверждённых промптах.

Получение результата: опрос

Самый простой способ получить результат — делать GET задачи по id, пока status не перейдёт в терминальное состояние.

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)

Несколько важных деталей:

  • status последовательно проходит queued → handling → archiving → success (или fail). Любое состояние до success или fail не должно считаться терминальным.
  • Время генерации растёт с duration и текущей глубиной очереди. Задача на 5 секунд 720p обычно завершается за несколько минут; для 1080p 15 секунд закладывайте больше времени.
  • output[].url хранится во временном хранилище. Сразу скачайте байты и разместите их самостоятельно; не сохраняйте только URL для передачи конечным пользователям — он истекает.
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")

Получение результата: callback (продакшен-режим)

Опрос подходит для скриптов и notebook, но в реальном бэкенде вы не хотите, чтобы worker был занят минутами на каждое видео. Передайте при отправке объект callback, и hiapi ровно один раз отправит тело завершённой задачи POST-запросом на ваш 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"
    }
  }'

Сейчас поддерживается только when: "final" — отправка других значений вернёт:

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

Проектирование надёжного получателя

Три правила обеспечат безопасность пути через callback:

  1. Идемпотентность. Сохраняйте состояние по taskId. Если вы получите один и тот же callback дважды — что в итоге обязательно случится из-за повторов — повторная обработка должна быть no-op.
  2. Отделите подтверждение от обработки. Как только payload поставлен в очередь, верните из webhook 200 OK. Фактическое скачивание / переразмещение / запись в базу выполняйте в фоновой задаче. Медленный webhook провоцирует таймауты и повторную доставку.
  3. Откат к опросу. Сохраняйте каждый отправленный taskId вместе с created_at в своей базе. Запускайте сканер, который опрашивает /v1/tasks/{id} для любой строки, которая старше примерно 30 минут и ещё не имеет терминального состояния. Callback — это быстрый путь, а не единственный.

Минимальный получатель на FastAPI:

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}

Ошибки, с которыми вы реально столкнётесь

Форма конверта ошибок всегда одна и та же — {code, error_code, message, data} — поэтому можно написать один обработчик. Несколько случаев, заслуживающих особой обработки для этой модели:

HTTP error_code Условие возникновения Способ исправления
400 INVALID_REQUEST неизвестное поле, неверное значение enum, duration вне 3–15 посмотрите message, оно укажет поле — например, aspect_ratio: value must be one of '16:9'...
401 — ключ отсутствует или неверен, либо permission_denied перепроверьте заголовок Authorization: Bearer sk-...
402 — баланса не хватает для покрытия оценки стоимости пополните баланс на hiapi.ai/en/pricing и повторите
429 — вы слишком часто обращаетесь к эндпоинту используйте экспоненциальную задержку с джиттером

Реальный пример ошибки валидации — можно скопировать как тестовую фикстуру:

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

Обратите внимание, что все поля сообщаются в одном ответе. Валидируйте на стороне клиента, где это возможно, но когда не получается — полагайтесь на это.

Связанные материалы на hiapi

Часто задаваемые вопросы

Генерирует ли happyhorse-1.1/text-to-video аудио? Через API задач — нет; в структуре запроса сейчас нет поля audio, и отправка его вернёт additional properties 'audio' not allowed. Выход — немой MP4. Накладывайте музыку на последующих этапах.

Какие соотношения сторон доступны для вертикальных / коротких видео? Для вывода в стиле TikTok и Reels стандартным выбором будет 9:16. API также принимает 4:5 (вертикаль для ленты Instagram) и 9:21 (сверхдлинная вертикаль), если вам нужен более узкий кадр.

Какая минимальная и максимальная длительность? duration — целое число от 3 до 15 секунд включительно. Отправка 0 даст duration: minimum: got 0, want 3; отправка 99 даст duration: maximum: got 99, want 15. Выбирайте минимальное значение, capturing нужное движение — вы платите именно за длительность.

Можно ли передать seed для воспроизводимого результата? Эта модель сейчас это не поддерживает. Запрос {"input": {"prompt": "...", "seed": 42}} завершится ошибкой additional properties 'seed' not allowed. Если воспроизводимость важна, записывайте полное тело запроса и повторяйте с тем же промптом; вы получите похожий — но не попиксельно идентичный — результат.

Поддерживается ли negative_prompt? Нет. Направляйте вывод, добавляя ограничения в сам prompt (например, "...clean composition, no text, no watermarks").

Как долго действует URL вывода? Для задач на тарифах по умолчанию поле storage имеет значение temp, а output[].url — краткосрочная подписанная ссылка. Скачивайте файл, как только задача достигает success, и размещайте его заново. Не сохраняйте исходный URL для последующей выдачи.

Как контролировать расходы во время разработки? Две привычки для экономии: держите duration: 3, пока не зафиксируете промпт, и отправляйте с resolution: "720p", пока не утвердите раскадровку. Повышайте до 1080p и более длинной длительности только для финального рендера.

Опрос или callback — что выбрать? Используйте оба, в таком порядке. Сначала опрос — на один движущийся компонент меньше. Переходите к callback, когда у вас впервые появится бэкенд, где worker не может себе позволить блокироваться на wait_for_task. В любом случае сохраняйте опрос как резервный сканер; callback — это best-effort, а не гарантированная доставка.

Почему при наличии лимита вызовов возвращается 402? API оценивает верхнюю границу стоимости задачи — длительность × цена за секунду — и резервирует её заранее. Если баланс не покрывает эту оценку, сразу возвращается 402. Пополните баланс, повторите, и после успеха списывается только фактическая стоимость.

Похожие статьи

Все статьи →

Начните работать с постоянными артефактами ИИ

Получите API-ключ и вызывайте модели изображений, видео и аудио через один API на основе tasks.

Читать документацию API