Видеогенерация
Асинхронный API для генерации видео по тексту и изображениям через единую точку входа ZvenoAI с автоматическим выбором провайдера и оплатой в рублях.
ZvenoAI предоставляет OpenAI-совместимый видео-API: один эндпоинт, одна авторизация, оплата в рублях. Если у выбранного провайдера сбой — запрос автоматически уходит к резервному, а с баланса списывается только фактическая стоимость.
Как это устроено
В отличие от чат-комплишена видеогенерация асинхронна: ни один провайдер в индустрии не отдаёт видео синхронно — генерация занимает от десятков секунд до нескольких минут. ZvenoAI унифицирует жизненный цикл задачи в три шага.
Отправка задачи
POST /v1/videos — отправляете промпт и параметры. В ответ приходит идентификатор
vj_<uuid> и статус pending. На балансе резервируется стоимость выбранного
провайдера; если средств не хватает ни на одного из подходящих кандидатов —
запрос отклоняется с 402 insufficient_funds ещё до похода к провайдеру.
Опрос статуса
GET /v1/videos/{id} — опрашиваете задачу, пока статус не станет терминальным
(completed или failed). На стороне ZvenoAI воркер сам ходит к провайдеру с
ограничением частоты и экспоненциальной задержкой между попытками.
Скачивание
GET /v1/videos/{id}/content — стриминг MP4. Поддерживает HTTP Range для
частичной загрузки и перемотки (если умеет провайдер).
Воркер ZvenoAI сам ходит к провайдеру с ограничением частоты и задержкой между попытками. Клиенту
не нужно опрашивать /v1/videos/{id} чаще, чем раз в 5–10 секунд — это не ускорит генерацию.
Быстрый старт
import os, time, requests
BASE = "https://api.zveno.ai"
KEY = os.environ["ZVENO_API_KEY"]
H = {"Authorization": f"Bearer {KEY}", "Content-Type": "application/json"}
# 1. Отправка задачи
job = requests.post(f"{BASE}/v1/videos", headers=H, json={
"model": "openai/sora-2",
"prompt": "Wide shot of a child flying a red kite at sunset",
"size": "1280x720",
"duration": 8,
}).json()
print("отправлено:", job["id"], job["status"])
# 2. Опрос (терминальные статусы: completed | failed)
while job["status"] in ("pending", "in_progress"):
time.sleep(5)
job = requests.get(job["polling_url"], headers=H).json()
print("статус:", job["status"])
if job["status"] != "completed":
raise SystemExit(f"{job['status']}: {job.get('error') or '—'}")
# 3. Скачивание
video_url = job["unsigned_urls"][0]
with requests.get(video_url, headers=H, stream=True) as r:
with open("output.mp4", "wb") as f:
for chunk in r.iter_content(1 << 20):
f.write(chunk)
print(f"готово. стоимость: {job['usage']['cost']} RUB")const BASE = 'https://api.zveno.ai'
const KEY = process.env.ZVENO_API_KEY
const H = { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json' }
// 1. Отправка задачи
let job = await fetch(`${BASE}/v1/videos`, {
method: 'POST',
headers: H,
body: JSON.stringify({
model: 'openai/sora-2',
prompt: 'Wide shot of a child flying a red kite at sunset',
size: '1280x720',
duration: 8,
}),
}).then((r) => r.json())
// 2. Опрос (терминальные статусы: completed | failed)
while (['pending', 'in_progress'].includes(job.status)) {
await new Promise((r) => setTimeout(r, 5000))
job = await fetch(job.polling_url, { headers: H }).then((r) => r.json())
}
if (job.status !== 'completed') throw new Error(`${job.status}: ${job.error ?? '—'}`)
// 3. Скачивание
const res = await fetch(job.unsigned_urls[0], { headers: H })
const fs = await import('node:fs')
const { Readable } = await import('node:stream')
const { pipeline } = await import('node:stream/promises')
await pipeline(Readable.fromWeb(res.body), fs.createWriteStream('output.mp4'))
console.log(`готово. стоимость: ${job.usage.cost} RUB`)# 1. Отправка задачи
JOB=$(curl -sS https://api.zveno.ai/v1/videos \
-H "Authorization: Bearer $ZVENO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"openai/sora-2","prompt":"a child flying a red kite","size":"1280x720","duration":8}')
JOB_ID=$(echo "$JOB" | jq -r .id)
# 2. Опрос (терминальные статусы: completed | failed)
while :; do
STATUS=$(curl -sS "https://api.zveno.ai/v1/videos/$JOB_ID" \
-H "Authorization: Bearer $ZVENO_API_KEY" | jq -r .status)
echo "статус: $STATUS"
case "$STATUS" in completed|failed) break ;; esac
sleep 5
done
# 3. Скачивание (только если completed)
[[ "$STATUS" == "completed" ]] && curl -fsS "https://api.zveno.ai/v1/videos/$JOB_ID/content" \
-H "Authorization: Bearer $ZVENO_API_KEY" \
-o output.mp4Поиск моделей
Идентификатор модели — всегда в формате vendor/model (например, openai/sora-2, google/veo-3.1). Список доступных видеомоделей возвращает GET /v1/videos/models: эндпоинт публичный, авторизация не требуется, в ответе — массив моделей с их возможностями и ценами.
Поля видеомодели
| Поле | Описание |
|---|---|
id | Slug модели; используйте его в поле model при создании задачи |
canonical_slug | Стабильный идентификатор модели (не меняется при rename) |
name | Человекочитаемое название |
created | Unix timestamp добавления модели в каталог |
supported_resolutions | Разрешения, например ["720p", "1080p"] |
supported_aspect_ratios | Соотношения сторон, например ["16:9", "9:16"] |
supported_sizes | Точные пиксельные размеры WIDTHxHEIGHT |
supported_durations | Допустимые длительности в секундах |
supported_frame_images | Какие позиции кадра поддерживает модель: ["first"], ["last"] или ["first","last"] (null — генерация из кадра не поддерживается) |
generate_audio | Может ли модель генерировать аудиодорожку |
seed | Поддерживает ли модель параметр seed |
hugging_face_id | Hugging Face model identifier, если применимо |
allowed_passthrough_parameters | Имена параметров, которые можно передать через provider.options.<slug>.parameters |
Поля supported_frame_images отдают короткие лейблы ("first"/"last"), а в запросе
frame_images[].frame_type ожидаются полные значения ("first_frame"/"last_frame") — следите
за маппингом.
Параметры запроса
| Параметр | Тип | Описание |
|---|---|---|
model | string, обяз | vendor/model, см. каталог |
prompt | string, обяз | Текстовое описание сцены |
duration | int | Длительность в секундах. Допустимые значения — в supported_durations модели |
resolution | string | Логическое разрешение, см. таблицу ниже |
aspect_ratio | string | Соотношение сторон, см. таблицу ниже |
size | string | Точные пиксели WIDTHxHEIGHT, например 1280x720 (взаимозаменяемо с resolution+aspect_ratio) |
generate_audio | boolean | Сгенерировать ли звуковую дорожку (если модель умеет) |
seed | int | Детерминированная генерация (поддержка зависит от модели) |
frame_images | array | Опорные кадры для генерации из изображения (см. ниже) |
input_references | array | Изображения-референсы для подсказки стилистики (см. ниже) |
provider | object | Передача параметров провайдеру и предпочтения по выбору провайдера (см. ниже) |
Разрешения
| Значение | Описание |
|---|---|
480p | Стандартное низкое (640×480) |
720p | HD |
1080p | Full HD |
1K | ~1024 по короткой стороне |
2K | ~2048 по короткой стороне (QHD) |
4K | UHD (~3840×2160) |
Соотношения сторон
| Значение | Описание |
|---|---|
16:9 | Широкий горизонтальный (стандарт) |
9:16 | Вертикальный (Stories, Reels) |
1:1 | Квадрат |
4:3 | Классический горизонтальный |
3:4 | Классический вертикальный |
21:9 | Cinemascope, ультраширокий |
9:21 | Сверхвысокий вертикальный |
Допустимый набор для каждой модели — в её полях supported_resolutions,
supported_aspect_ratios, supported_sizes, supported_durations. Если передать значение вне
этого набора, ответ будет 400.
Использование изображений
Многие модели принимают изображения как часть подсказки. Поддерживаются два разных режима — у них разный смысл и не пересекающаяся семантика.
Генерация из изображения (frame_images)
Задаёт первый и/или последний кадр, провайдер достраивает движение между ними. Подходит для оживления статичной картинки или плавного перехода между двумя сценами.
{
"model": "google/veo-3.1",
"prompt": "transitioning from morning to night over the same valley",
"frame_images": [
{
"type": "image_url",
"image_url": { "url": "https://example.com/morning.png" },
"frame_type": "first_frame"
},
{
"type": "image_url",
"image_url": { "url": "https://example.com/night.png" },
"frame_type": "last_frame"
}
],
"resolution": "1080p"
}Какие frame_type доступны для конкретной модели — в её поле supported_frame_images. Значение null означает, что генерация из кадра не поддерживается.
Стилевые референсы (input_references)
Задаёт референсы для подсказки общей стилистики (персонаж, палитра, художественное направление) — провайдер сам решит, как их использовать. Композиция кадра при этом определяется промптом.
{
"model": "google/veo-3.1",
"prompt": "a colossal solar flare beside a planet",
"input_references": [
{
"type": "image_url",
"image_url": { "url": "https://example.com/style-ref.png" }
}
],
"resolution": "1080p"
}Передача параметров провайдеру
Каждый провайдер поддерживает свой набор расширенных параметров — например, negativePrompt, personGeneration, enhancePrompt у Google Veo. ZvenoAI готов пробрасывать их через provider.options.<provider-slug>.parameters, но допускает только параметры из белого списка модели — поля allowed_passthrough_parameters в каталоге.
{
"model": "google/veo-3.1",
"prompt": "A time-lapse of a flower blooming",
"provider": {
"options": {
"openrouter": {
"parameters": {
"personGeneration": "allow",
"negativePrompt": "blurry, low quality"
}
}
}
}
}Сейчас allowed_passthrough_parameters пустой для всех моделей в каталоге — passthrough-параметры
молча отбрасываются на стороне ZvenoAI. Список будет заполняться по мере подключения
провайдер-специфичных опций.
Выбор провайдера
Если одна и та же модель доступна у нескольких провайдеров, ZvenoAI по умолчанию выбирает одного из них случайно с весом, обратно пропорциональным квадрату цены — в среднем дешевле, но не «всегда самый дешёвый», чтобы распределять нагрузку.
Поведение настраивается блоком provider.preferences (совместим с OpenRouter):
{
"model": "openai/sora-2-pro",
"prompt": "...",
"provider": {
"preferences": {
"order": ["openrouter", "together"],
"only": ["openrouter"],
"ignore": ["together"],
"sort": "price",
"max_price": { "video": 500.0 },
"allow_fallbacks": true
}
}
}| Поле | Что делает |
|---|---|
order | Строгий приоритет. Подходящие провайдеры вне списка идут в конец (если allow_fallbacks: true) |
only | Белый список. Всё, чего нет в нём, исключается |
ignore | Чёрный список |
sort | "price" — детерминированный выбор по возрастанию цены за видео в RUB |
max_price.video | Жёсткий фильтр в RUB. Если ни один провайдер не вписывается — 503 service_unavailable (без тихого отказа) |
allow_fallbacks | По умолчанию true. Если false — после order/only маршрутизатор не пробует никого больше |
Реакция на сбой провайдера
Если провайдер только что вернул ошибку, ZvenoAI понижает его приоритет (но не блокирует) примерно на 30 секунд и пробует следующего. Это уменьшает p99-задержку без жёстких health-проверок.
Жизненный цикл задачи
отправка → pending → in_progress → completed ─→ готово к скачиванию
└→ failed ─→ возврат средств| Статус | Что значит |
|---|---|
pending | Задача принята, ждёт первого опроса провайдера |
in_progress | Провайдер генерирует видео |
completed | Видео готово, unsigned_urls и usage.cost заполнены |
failed | Провайдер вернул ошибку или истекли retry-попытки опроса; средства возвращаются |
Контракт перечисляет шесть значений статуса (включая cancelled и expired — для совместимости с
OpenRouter), но ZvenoAI отдаёт только эти четыре. «Истечение» ссылки на видео выражается через
404 с кодом video_expired на /content, а не через статус задачи.
Формат ответа
Ответ на отправку (202 Accepted)
{
"id": "vj_5f3a2c8e",
"polling_url": "https://api.zveno.ai/v1/videos/vj_5f3a2c8e",
"status": "pending"
}Ответ опроса (200 OK, статус completed)
{
"id": "vj_5f3a2c8e",
"polling_url": "https://api.zveno.ai/v1/videos/vj_5f3a2c8e",
"status": "completed",
"generation_id": "vj_5f3a2c8e",
"unsigned_urls": ["https://api.zveno.ai/v1/videos/vj_5f3a2c8e/content"],
"usage": {
"cost": 87.5,
"is_byok": false
}
}Ответ опроса при ошибке
{
"id": "vj_8a1b...",
"polling_url": "https://api.zveno.ai/v1/videos/vj_8a1b...",
"status": "failed",
"error": "content policy violation: requested content is not allowed"
}URL внутри unsigned_urls — это наш прокси-эндпоинт GET /v1/videos/{id}/content, а не
подписанная ссылка провайдера. Несмотря на название поля, запрос требует обычного
Authorization: Bearer <api-key>. Сам URL не истекает, но истекает ссылка провайдера, к
которой мы за вас пробрасываем поток — после её истечения вернётся 404 с кодом
video_expired.
Скачивание контента
GET /v1/videos/{id}/content стримит MP4. Заголовок Range пробрасывается к провайдеру:
# скачать первый мегабайт (например, чтобы прочитать заголовок видео)
curl -sS https://api.zveno.ai/v1/videos/vj_5f3a2c8e/content \
-H "Authorization: Bearer $ZVENO_API_KEY" \
-H "Range: bytes=0-1048575" \
-o head.mp4Если провайдер поддерживает Range, сервер ответит 206 Partial Content с Content-Range — HTML5-плеер сможет перематывать видео по таймлайну и возобновлять прерванные загрузки. Если провайдер Range не поддерживает, придёт обычный 200 OK с полным телом.
| Параметр | Где | Описание |
|---|---|---|
index | query | Для моделей с несколькими выходными видео. По умолчанию 0. Пока поддерживается только 0 |
Range | header | Стандартный HTTP Range — bytes=START-END или bytes=START- |
Биллинг и стоимость
Видео тарифицируются поштучно в рублях (а не за токены). Биллинг двухфазный:
- Резерв — при отправке задачи маршрутизатор выбирает первого подходящего провайдера и резервирует на счёте его стоимость. Если средств не хватает — пробуется следующий по приоритету. Когда ни один из кандидатов не проходит проверку баланса, ответ
402 insufficient_funds, ещё до похода к провайдеру. - Списание / возврат — после
completedрезерв заменяется фактической стоимостью провайдера и наценкой ZvenoAI; приfailedсумма возвращается полностью.
Контракт VideoUsage.cost совместим с OpenRouter по форме, но ZvenoAI возвращает значение в
рублях (валюте биллинга платформы), а не в долларах. Источник правды для финансовой отчётности
— таблица транзакций, доступная через GET /v1/credits и GET
/v1/activity.
Обработка ошибок
ZvenoAI стремится к совместимости с OpenRouter по статус-кодам. Тело — плоский объект:
{
"code": "video_not_ready",
"message": "video is not ready yet"
}| Эндпоинт | Код | Когда возникает |
|---|---|---|
POST /v1/videos | 400 | Невалидное тело запроса, неизвестный resolution/size, отсутствует prompt/model |
| 402 | Недостаточно средств для резервирования стоимости | |
| 404 | Идентификатор модели не найден или модель не относится к видеогенерации | |
| 429 | Превышен лимит частоты запросов на стороне ZvenoAI | |
| 503 | Нет провайдера, удовлетворяющего provider.preferences (например, по max_price) | |
GET /v1/videos/{id} | 404 | Задача не существует или принадлежит другому пользователю |
GET /v1/videos/{id}/content | 400 | index > 0 (несколько выходных видео пока не поддержаны) |
| 404 | video_not_ready (задача ещё не completed) или video_expired (ссылка истекла) | |
| 502 | Провайдер недоступен в момент стриминга |
Хорошие практики
- Подробный промпт. Описывайте сцену, ракурс, освещение, движение камеры. Видео-модели чувствительнее к деталям, чем чат-модели.
- Адекватное разрешение. Чем выше — тем дольше генерация и дороже. Для предпросмотра запускайте
720p, для финального результата —1080p/4K. - Интервал опроса. 5–10 секунд обычно достаточно: воркер ZvenoAI всё равно ходит к провайдеру с собственным rate-limit'ом, более частые запросы клиента не ускорят результат.
- Обработка ошибок. Терминальные статусы — только
completedиfailed. Приfailedполеerrorнесёт причину; резерв возвращается автоматически — повторная попытка списывает заново. - Фиксация провайдера на повторных запусках. Для воспроизводимости в проде используйте
provider.preferences.only, чтобыseedимел смысл (между провайдерами он не переносится). - Контроль стоимости. Если у проекта есть ценовой потолок на видео — задавайте
provider.preferences.max_price.video, чтобы маршрутизатор отказался дороже лимита (503), а не списал больше ожидаемого.
Решение типичных проблем
Чего пока нет
- Webhook-уведомления — только опрос через
GET /v1/videos/{id}. - BYOK (свой ключ провайдера) —
is_byokвсегдаfalse; биллинг идёт через ваш баланс ZvenoAI. - Несколько видео на одну задачу —
index > 0вернёт400. - Specialized-эндпоинты провайдеров (remix, extend, lipsync) пока не проксируются.
- Удаление задач —
DELETE /v1/videos/{id}пока не реализован.
SDK
Видео-API не входит в официальный OpenAI SDK (openai.videos ходит только к OpenAI). Используйте обычный HTTP-клиент: на большинстве языков ~30 строк кода покрывают отправку, опрос статуса и скачивание (см. быстрый старт).