# Flow API — документация API

Этот API генерирует **картинки и видео через Google Flow** (модели Nano Banana, Veo 3.1, Omni Flash).
Генерации идут от имени **вашего собственного Google-аккаунта** и тратят **ваши кредиты Google Flow**.

Документ написан так, чтобы по нему мог работать и человек без опыта, и ИИ-агент. Если вы ИИ-агент —
обязательно прочитайте раздел [«Правила для ИИ-агентов»](#правила-для-ии-агентов).

> **Адрес API:** `https://flow.178-151-26-131.sslip.io`
> **Telegram-бот:** @googleflowapibot — https://t.me/googleflowapibot
> **Этот документ одним файлом:** `https://flow.178-151-26-131.sslip.io/docs.md`

---

## Быстрый старт за 3 минуты

### Шаг 1. Подключите Google-аккаунт

1. Откройте бота @googleflowapibot в Telegram и нажмите **«Подключить Google Flow»**.
2. Бот даст ссылку. На открывшейся странице войдите в свой Google-аккаунт (тот, где у вас подписка Google AI и Google Flow).
3. Когда страница напишет «Готово», бот пришлёт сообщение «Google-аккаунт подключён».

Это делается **один раз**. Куки копировать не нужно.

### Шаг 2. Создайте API-ключ

1. В боте нажмите **«🔑 API-ключи» → «➕ Создать ключ»**.
2. Придумайте название (или нажмите «Пропустить»).
3. Бот покажет ключ вида `flow_sk_AbCd...`. **Скопируйте его сразу** — полностью ключ показывается только один раз.

### Шаг 3. Сделайте первый запрос

Проверьте, что ключ работает (замените `ВАШ_КЛЮЧ` на свой ключ):

```bash
curl https://flow.178-151-26-131.sslip.io/v1/me -H "Authorization: Bearer ВАШ_КЛЮЧ"
```

Если в ответе есть `"status": "linked"` и `"balance"` — всё готово. Сгенерируйте картинку и дождитесь результата
одним запросом:

```bash
curl -X POST https://flow.178-151-26-131.sslip.io/v1/images \
  -H "Authorization: Bearer ВАШ_КЛЮЧ" \
  -H "Content-Type: application/json" \
  -d '{"prompt": "рыжий кот в очках читает газету, фотореализм", "wait": true}'
```

В ответе будет `"status": "completed"` и ссылка `"file_url"` на готовую картинку. Всё!

---

## Главное, что нужно понимать

1. **Одна генерация = один файл.** Один запрос создаёт ровно одну картинку или одно видео.
2. **Генерация занимает время.** Картинка — 10–30 секунд, видео — обычно 1–3 минуты (иногда до 10).
   Поэтому генерация работает в два шага: *запустить* → *забрать результат*. Или можно передать `"wait": true`,
   и сервер сам дождётся результата (удобно, но соединение будет долго открыто).
3. **Всё стоит кредитов вашего аккаунта Google Flow.** Сколько именно — зависит от модели, длительности, качества
   и вашей подписки. Узнать цену **до** запуска: `POST /v1/estimate`. Картинки Nano Banana обычно бесплатные (0 кредитов).
4. **Модели и цены у всех разные** — они зависят от подписки Google. Единственный правильный источник: `GET /v1/models`.
5. **Одновременно у одного Google-аккаунта идёт одна генерация.** Если отправить несколько, они встанут в очередь
   (статус `queued`) и выполнятся по очереди.
6. **Ссылки на файлы живут около 6 часов.** Скачивайте результат сразу.

---

## Авторизация: как передавать ключ

Каждый запрос к `/v1/...` должен содержать API-ключ в заголовке. Подходит любой из двух вариантов:

```
Authorization: Bearer flow_sk_ВАШ_КЛЮЧ
```

```
X-API-Key: flow_sk_ВАШ_КЛЮЧ
```

Правила безопасности:

- Ключ — это пароль. Не публикуйте его, не вставляйте в код сайта, который видят посетители, не коммитьте в Git.
- Храните ключ в переменной окружения, например `FLOW_API_KEY`.
- Если ключ утёк — в боте: «API-ключи» → ключ → **«Перевыпустить»** (старый сразу перестанет работать) или **«Отозвать»**.

---

## Как устроена генерация (схема)

```
1. POST /v1/estimate          → узнать цену и хватит ли кредитов (необязательно, но рекомендуется)
2. POST /v1/images            → запустить картинку   ─┐
   POST /v1/videos            → запустить видео      ─┴→ в ответе "id": "gen_..."
3. GET  /v1/generations/{id}  → проверять статус раз в 5 секунд, пока не станет completed или failed
4. GET  file_url              → скачать файл (или GET /v1/generations/{id}/file с ключом)
```

Статусы генерации:

| Статус | Что значит | Что делать |
|---|---|---|
| `queued` | Ждёт очереди (у аккаунта уже идёт другая генерация) | Подождать и проверить снова |
| `running` | Google генерирует | Подождать и проверить снова |
| `completed` | Готово, есть `file_url` | Скачать файл |
| `failed` | Не получилось, причина в `error` | Прочитать `error`, исправить (например, промпт) и запустить заново |

---

## Эндпоинты

Общие правила для всех запросов:

- Тело запросов — JSON, заголовок `Content-Type: application/json`.
- Все ответы — JSON (кроме скачивания файла).
- Время (`created_at`, `finished_at`, `expires_at`) — Unix-время в секундах.
- При ошибке HTTP-код не 2xx, а в теле `{"error": {...}}` — см. [«Ошибки»](#ошибки).

### GET /v1/me — проверить ключ и аккаунт

Показывает, какой ключ используется, подключён ли Google-аккаунт и какой баланс.

```bash
curl https://flow.178-151-26-131.sslip.io/v1/me -H "Authorization: Bearer ВАШ_КЛЮЧ"
```

Ответ:

```json
{
  "key": {
    "name": "Мой сайт",
    "hint": "flow_sk_AbCd…9xYz",
    "expires_at": null,
    "allowed_models": null,
    "quota": {"unit": "credits", "period": "month", "limit": null, "used": 27, "remaining": null}
  },
  "google_account": {"status": "linked", "email": "you@gmail.com"},
  "balance": 1024
}
```

| Поле | Что значит |
|---|---|
| `key.expires_at` | Когда ключ перестанет работать. `null` — бессрочно |
| `key.allowed_models` | Какие модели разрешены ключу. `null` — все |
| `key.quota.limit` | Лимит квоты. `null` — без ограничений |
| `key.quota.used` | Сколько уже потрачено за текущий период |
| `key.quota.remaining` | Сколько осталось. `null` — без ограничений |
| `google_account.status` | `linked` — всё хорошо; `expired` — нужно переподключиться в боте; `none` — не подключён |
| `balance` | Кредиты Google Flow на аккаунте прямо сейчас (есть только при `linked`) |

### GET /v1/balance — баланс кредитов

```bash
curl https://flow.178-151-26-131.sslip.io/v1/balance -H "Authorization: Bearer ВАШ_КЛЮЧ"
```

```json
{"balance": 1024, "quota": {"unit": "credits", "period": "month", "limit": 500, "used": 27, "remaining": 473}}
```

### GET /v1/models — модели, параметры и цены

Возвращает **только те модели, которые доступны вашему аккаунту и разрешены этому ключу**, с точными ценами.
Всегда смотрите сюда, а не в примеры из документации — у разных подписок разные варианты.

```bash
curl https://flow.178-151-26-131.sslip.io/v1/models -H "Authorization: Bearer ВАШ_КЛЮЧ"
```

```json
{
  "balance": 1024,
  "models": [
    {
      "id": "nano-banana-2", "type": "image", "name": "Nano Banana 2",
      "description": "Быстрая и качественная генерация картинок. Лучший выбор по умолчанию.",
      "aspect_ratios": ["16:9", "4:3", "1:1", "3:4", "9:16"], "cost": 0
    },
    {
      "id": "omni-1.1-flash", "type": "video", "name": "Omni 1.1 Flash",
      "description": "Дешёвая и быстрая модель видео со звуком. Есть 360p и длительность до 10 секунд.",
      "aspect_ratios": ["16:9", "9:16"], "cost": 4,
      "durations": [4, 6, 8, 10], "resolutions": ["720p", "360p"],
      "prices": [
        {"aspect_ratio": "16:9", "duration": 4, "resolution": "360p", "cost": 4},
        {"aspect_ratio": "16:9", "duration": 4, "resolution": "720p", "cost": 7}
      ]
    }
  ]
}
```

| Поле | Что значит |
|---|---|
| `id` | То, что нужно передавать в поле `model` |
| `type` | `image` — картинки (запускать через `/v1/images`), `video` — видео (через `/v1/videos`) |
| `aspect_ratios` | Допустимые форматы кадра |
| `cost` | Минимальная цена одной генерации в кредитах |
| `durations` | Только видео: допустимые длительности в секундах |
| `resolutions` | Только видео: допустимое качество |
| `prices` | Только видео: точная цена каждой комбинации формат + длительность + качество. **Используйте только комбинации из этого списка** |

Какие модели бывают (точный список для вас — только в `/v1/models`):

| `id` | Тип | Кратко |
|---|---|---|
| `nano-banana-2` | картинка | Лучший выбор по умолчанию |
| `nano-banana-pro` | картинка | Максимальное качество |
| `nano-banana-2-lite` | картинка | Самая быстрая |
| `omni-1.1-flash` | видео | Дёшево, со звуком, 4–10 сек, 360p/720p |
| `veo-3.1-lite` | видео | Veo подешевле |
| `veo-3.1-fast` | видео | Хорошее качество за разумную цену |
| `veo-3.1-quality` | видео | Лучшее качество, дорого |

### POST /v1/estimate — цена до запуска

Ничего не генерирует и ничего не тратит. Показывает цену, текущий баланс, остаток после генерации и хватит ли квоты ключа.

Поля тела:

| Поле | Обязательно | Тип | По умолчанию | Описание |
|---|---|---|---|---|
| `type` | да | строка | — | `image` или `video` |
| `model` | да | строка | — | `id` модели из `/v1/models` |
| `aspect_ratio` | нет | строка | `16:9` | Формат кадра |
| `duration` | нет | число | самая короткая доступная | Только видео, секунды |
| `resolution` | нет | строка | `720p` | Только видео: `720p` или `360p` |

```bash
curl -X POST https://flow.178-151-26-131.sslip.io/v1/estimate \
  -H "Authorization: Bearer ВАШ_КЛЮЧ" \
  -H "Content-Type: application/json" \
  -d '{"type": "video", "model": "omni-1.1-flash", "aspect_ratio": "9:16", "duration": 4, "resolution": "720p"}'
```

```json
{
  "type": "video", "model": "omni-1.1-flash", "model_name": "Omni 1.1 Flash",
  "aspect_ratio": "9:16", "duration": 4, "resolution": "720p", "count": 1,
  "cost": 7, "balance": 1024, "balance_after": 1017, "enough_credits": true,
  "quota": {"unit": "credits", "period": "month", "limit": null, "used": 27, "remaining": null, "enough": true}
}
```

Запускайте генерацию, только если `enough_credits` = `true` **и** `quota.enough` = `true`.

### POST /v1/images — сгенерировать картинку

Поля тела:

| Поле | Обязательно | Тип | По умолчанию | Описание |
|---|---|---|---|---|
| `prompt` | да | строка, 1–4000 символов | — | Что должно быть на картинке. Можно на русском |
| `model` | нет | строка | `nano-banana-2` | Модель картинок из `/v1/models` |
| `aspect_ratio` | нет | строка | `16:9` | `16:9`, `4:3`, `1:1`, `3:4` или `9:16` |
| `wait` | нет | true/false | `false` | `true` — ответить только когда картинка готова |

Пример без ожидания (рекомендуется для программ):

```bash
curl -X POST https://flow.178-151-26-131.sslip.io/v1/images \
  -H "Authorization: Bearer ВАШ_КЛЮЧ" \
  -H "Content-Type: application/json" \
  -d '{"prompt": "маяк на скале в шторм, закат, кинематографично", "model": "nano-banana-pro", "aspect_ratio": "1:1"}'
```

Ответ — HTTP **202** (принято, генерация идёт):

```json
{
  "id": "gen_3f9a1c2b7d4e5f6a7b8c",
  "status": "queued",
  "type": "image",
  "model": "nano-banana-pro",
  "prompt": "маяк на скале в шторм, закат, кинематографично",
  "aspect_ratio": "1:1",
  "cost_estimated": 0,
  "credits_spent": null,
  "balance_before": null,
  "balance_after": null,
  "created_at": 1789420000,
  "finished_at": null,
  "error": null
}
```

Запомните `id` и проверяйте статус через [`GET /v1/generations/{id}`](#get-v1generationsid-статус-и-результат).
С `"wait": true` сразу придёт ответ HTTP **200** со `status` = `completed` или `failed`.

### POST /v1/videos — сгенерировать видео

Поля тела:

| Поле | Обязательно | Тип | По умолчанию | Описание |
|---|---|---|---|---|
| `prompt` | да | строка, 1–4000 символов | — | Что происходит в видео: сцена, действие, движение камеры, звук |
| `model` | нет | строка | `omni-1.1-flash` | Модель видео из `/v1/models` |
| `aspect_ratio` | нет | строка | `16:9` | `16:9` (горизонтально) или `9:16` (вертикально) |
| `duration` | нет | число | самая короткая доступная | Секунды. Только значения из `durations` модели |
| `resolution` | нет | строка | `720p` | `720p` или `360p` (360p есть не у всех моделей) |
| `wait` | нет | true/false | `false` | `true` — ответить только когда видео готово (может занять до 15 минут) |

```bash
curl -X POST https://flow.178-151-26-131.sslip.io/v1/videos \
  -H "Authorization: Bearer ВАШ_КЛЮЧ" \
  -H "Content-Type: application/json" \
  -d '{"prompt": "дрон пролетает над заснеженными горами на закате, мягкий свет", "model": "omni-1.1-flash", "aspect_ratio": "16:9", "duration": 6, "resolution": "720p"}'
```

Ответ — такой же, как у картинок (HTTP 202, `status` = `queued`), плюс поля `duration` и `resolution`.

### GET /v1/generations/{id} — статус и результат

```bash
curl https://flow.178-151-26-131.sslip.io/v1/generations/gen_3f9a1c2b7d4e5f6a7b8c -H "Authorization: Bearer ВАШ_КЛЮЧ"
```

Необязательный параметр `?wait=СЕКУНДЫ` (0–600): сервер подождёт до указанного времени и ответит раньше, если генерация закончится.
Например `?wait=60` — удобный способ «ждать» без частых запросов.

Ответ, когда готово:

```json
{
  "id": "gen_3f9a1c2b7d4e5f6a7b8c",
  "status": "completed",
  "type": "video",
  "model": "omni-1.1-flash",
  "prompt": "дрон пролетает над заснеженными горами на закате, мягкий свет",
  "aspect_ratio": "16:9",
  "duration": 6,
  "resolution": "720p",
  "cost_estimated": 10,
  "credits_spent": 10,
  "balance_before": 1024,
  "balance_after": 1014,
  "created_at": 1789420000,
  "finished_at": 1789420095,
  "error": null,
  "file_url": "https://flow-content.google/video/...",
  "download_url": "https://flow.178-151-26-131.sslip.io/v1/generations/gen_3f9a1c2b7d4e5f6a7b8c/file"
}
```

| Поле | Что значит |
|---|---|
| `status` | `queued`, `running`, `completed` или `failed` |
| `cost_estimated` | Цена, посчитанная перед запуском |
| `credits_spent` | Сколько кредитов реально списалось (баланс до минус баланс после) |
| `balance_before` / `balance_after` | Баланс до и после генерации |
| `error` | Причина, если `status` = `failed`. Текст на русском, его можно показать человеку |
| `file_url` | Прямая ссылка на файл (картинка или mp4). Работает без ключа, живёт около 6 часов |
| `download_url` | Скачивание через этот сервис (нужен ключ в заголовке) |

Ответ при неудаче:

```json
{"id": "gen_...", "status": "failed", "error": "Google отказался генерировать: промпт не прошёл модерацию. Измените описание.", "credits_spent": null}
```

### GET /v1/generations/{id}/file — скачать файл

Отдаёт сам файл (`image/jpeg` или `video/mp4`). Нужен ключ.

```bash
curl -L https://flow.178-151-26-131.sslip.io/v1/generations/gen_3f9a1c2b7d4e5f6a7b8c/file \
  -H "Authorization: Bearer ВАШ_КЛЮЧ" -o result.mp4
```

### GET /v1/generations — последние генерации ключа

```bash
curl "https://flow.178-151-26-131.sslip.io/v1/generations?limit=10" -H "Authorization: Bearer ВАШ_КЛЮЧ"
```

Ответ: `{"generations": [ ...объекты как в GET /v1/generations/{id}... ]}`, новые первыми. `limit` — от 1 до 100 (по умолчанию 20).

---

## Ошибки

При ошибке HTTP-код не 2xx, а тело всегда такое:

```json
{
  "error": {
    "code": "insufficient_credits",
    "message": "Не хватает кредитов: нужно 100, на балансе 40.",
    "hint": "Необязательная подсказка, что сделать",
    "cost": 100,
    "balance": 40
  }
}
```

- `code` — постоянный код, по нему удобно проверять ошибку в программе.
- `message` — понятное описание на русском.
- `hint` — что сделать (есть не всегда).
- Иногда есть дополнительные поля (`cost`, `balance`, `quota`).

| HTTP | `code` | Что случилось | Что делать |
|---|---|---|---|
| 401 | `missing_api_key` | Нет заголовка с ключом | Добавьте `Authorization: Bearer ВАШ_КЛЮЧ` |
| 401 | `invalid_api_key` | Ключ не найден | Проверьте, что скопировали ключ целиком; перевыпустите в боте |
| 401 | `key_revoked` | Ключ отозван | Создайте новый ключ в боте |
| 401 | `key_expired` | Закончился срок ключа | Продлите срок в боте |
| 402 | `insufficient_credits` | Не хватает кредитов Google Flow | Выберите модель дешевле или пополните кредиты. **Не повторяйте запрос** |
| 403 | `model_not_allowed` | Эта модель запрещена ключу | Используйте разрешённую модель или разрешите её в боте |
| 404 | `generation_not_found` | Нет генерации с таким `id` | Проверьте `id` |
| 409 | `flow_not_connected` | Google-аккаунт не подключён | Подключите в боте |
| 409 | `flow_session_expired` | Google разлогинил сессию | В боте: «Google-аккаунт» → «Переподключить» |
| 409 | `not_ready` | Файл ещё не готов | Дождитесь `status` = `completed` |
| 400 | `wrong_model_type` | Модель картинок в `/v1/videos` или наоборот | Возьмите модель нужного `type` из `/v1/models` |
| 410 | `file_expired` | Ссылка на файл устарела | Скачивайте результат сразу после готовности |
| 422 | `invalid_request` | Неверное тело запроса (нет `prompt`, неправильный тип поля) | Исправьте запрос по таблицам полей |
| 422 | `invalid_options` | Недопустимая комбинация формата / длительности / качества | Возьмите комбинацию из `prices` в `/v1/models` |
| 422 | `empty_prompt` / `prompt_too_long` | Пустой или слишком длинный промпт | Промпт от 1 до 4000 символов |
| 429 | `quota_exceeded` | Исчерпана квота ключа | Дождитесь нового периода или увеличьте квоту в боте. **Не повторяйте запрос** |
| 502 | `flow_error` / `flow_unavailable` | Google Flow вернул ошибку или недоступен | Прочитайте `message`; повторите через 1–2 минуты |
| 503 | `browser_error` | Сервис не смог открыть сессию Google | Повторите через минуту |

Важно: если генерация **запустилась**, а потом не получилась (например, промпт не прошёл модерацию Google),
это не HTTP-ошибка — генерация получит `status` = `failed` и причину в поле `error`.

---

## Квота, срок и разрешённые модели ключа

Всё настраивается в боте: «🔑 API-ключи» → ключ.

- **Квота** — сколько ключ может потратить за период.
  - Единица: **кредиты** (сумма потраченных кредитов) или **генерации** (количество запусков).
  - Период: день, неделя, месяц (последние 30 дней) или всё время. Считается «скользящим окном» от текущего момента.
  - Лимит: число или «без лимита».
  - Картинки часто стоят 0 кредитов — чтобы ограничить картинки, выбирайте единицу «генерации».
- **Срок действия** — дата, после которой ключ перестанет работать (`key_expired`).
- **Модели** — список разрешённых моделей. Остальные будут отклоняться с `model_not_allowed`, а в `/v1/models` их не будет видно.
- **Перевыпустить** — новый секрет с теми же настройками; старый сразу перестаёт работать.
- **Отозвать** — ключ навсегда перестаёт работать.

Текущее состояние квоты видно в `GET /v1/me` и `GET /v1/balance`.

---

## Готовые примеры

Во всех примерах ключ берётся из переменной окружения `FLOW_API_KEY`.
Как её задать:

- Linux / macOS: `export FLOW_API_KEY="flow_sk_ВАШ_КЛЮЧ"`
- Windows PowerShell: `$env:FLOW_API_KEY = "flow_sk_ВАШ_КЛЮЧ"`

### Bash + curl: видео от запуска до файла

Нужны `curl` и `jq`.

```bash
#!/usr/bin/env bash
set -euo pipefail
API="https://flow.178-151-26-131.sslip.io"
AUTH="Authorization: Bearer $FLOW_API_KEY"

# 1. Цена
curl -s -X POST "$API/v1/estimate" -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"type":"video","model":"omni-1.1-flash","aspect_ratio":"16:9","duration":4}' | jq

# 2. Запуск
ID=$(curl -s -X POST "$API/v1/videos" -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"prompt":"волны разбиваются о скалы, замедленная съёмка","model":"omni-1.1-flash","duration":4}' | jq -r .id)
echo "Генерация: $ID"

# 3. Ожидание
while true; do
  RESP=$(curl -s "$API/v1/generations/$ID?wait=30" -H "$AUTH")
  STATUS=$(echo "$RESP" | jq -r .status)
  echo "Статус: $STATUS"
  [ "$STATUS" = "completed" ] && break
  [ "$STATUS" = "failed" ] && { echo "Ошибка: $(echo "$RESP" | jq -r .error)"; exit 1; }
done

# 4. Скачивание
curl -s -L "$(echo "$RESP" | jq -r .file_url)" -o video.mp4
echo "Готово: video.mp4, списано $(echo "$RESP" | jq -r .credits_spent) кр."
```

### PowerShell (Windows)

```powershell
$Api = "https://flow.178-151-26-131.sslip.io"
$Headers = @{ Authorization = "Bearer $env:FLOW_API_KEY" }

$body = @{ prompt = "уютная кофейня в дождь, вид из окна"; model = "nano-banana-2"; aspect_ratio = "16:9" } | ConvertTo-Json
$gen = Invoke-RestMethod -Method Post -Uri "$Api/v1/images" -Headers $Headers -ContentType "application/json; charset=utf-8" -Body ([Text.Encoding]::UTF8.GetBytes($body))

do {
    $gen = Invoke-RestMethod -Uri "$Api/v1/generations/$($gen.id)?wait=30" -Headers $Headers
    Write-Host "Статус: $($gen.status)"
} while ($gen.status -in @("queued", "running"))

if ($gen.status -eq "failed") { throw $gen.error }
Invoke-WebRequest -Uri $gen.file_url -OutFile "image.jpg"
Write-Host "Готово: image.jpg"
```

### Python

Нужна библиотека `requests` (`pip install requests`).

```python
import os
import time
import requests

API = "https://flow.178-151-26-131.sslip.io"
HEADERS = {"Authorization": f"Bearer {os.environ['FLOW_API_KEY']}"}


def call(method, path, **kwargs):
    r = requests.request(method, API + path, headers=HEADERS, timeout=660, **kwargs)
    if r.status_code >= 400:
        err = r.json()["error"]
        raise RuntimeError(f"{err['code']}: {err['message']} {err.get('hint', '')}")
    return r.json()


def generate(kind, prompt, save_as, **options):
    """kind: 'image' или 'video'. options: model, aspect_ratio, duration, resolution."""
    est = call("POST", "/v1/estimate", json={"type": kind, **options})
    print(f"Цена: {est['cost']} кр., баланс: {est['balance']}, останется: {est['balance_after']}")
    if not est["enough_credits"] or not est["quota"]["enough"]:
        raise RuntimeError("Не хватает кредитов или квоты ключа")

    gen = call("POST", "/v1/images" if kind == "image" else "/v1/videos", json={"prompt": prompt, **options})
    while gen["status"] in ("queued", "running"):
        gen = call("GET", f"/v1/generations/{gen['id']}", params={"wait": 30})
        print("Статус:", gen["status"])

    if gen["status"] == "failed":
        raise RuntimeError(gen["error"])

    with open(save_as, "wb") as f:
        f.write(requests.get(gen["file_url"], timeout=120).content)
    print(f"Готово: {save_as}, списано {gen['credits_spent']} кр., баланс {gen['balance_after']}")
    return gen


generate("image", "акварельный рисунок старого Тбилиси", "city.jpg", model="nano-banana-2", aspect_ratio="3:4")
generate("video", "бумажный кораблик плывёт по луже, крупный план", "boat.mp4",
         model="omni-1.1-flash", aspect_ratio="9:16", duration=4, resolution="720p")
```

### JavaScript (Node.js 18+)

```javascript
const API = "https://flow.178-151-26-131.sslip.io";
const headers = { Authorization: `Bearer ${process.env.FLOW_API_KEY}`, "Content-Type": "application/json" };

async function call(method, path, body) {
  const res = await fetch(API + path, { method, headers, body: body ? JSON.stringify(body) : undefined });
  const data = await res.json();
  if (!res.ok) throw new Error(`${data.error.code}: ${data.error.message}`);
  return data;
}

async function generateVideo(prompt) {
  const options = { model: "omni-1.1-flash", aspect_ratio: "16:9", duration: 4 };
  const est = await call("POST", "/v1/estimate", { type: "video", ...options });
  console.log(`Цена ${est.cost} кр., останется ${est.balance_after}`);
  if (!est.enough_credits) throw new Error("Не хватает кредитов");

  let gen = await call("POST", "/v1/videos", { prompt, ...options });
  while (gen.status === "queued" || gen.status === "running") {
    gen = await call("GET", `/v1/generations/${gen.id}?wait=30`);
    console.log("Статус:", gen.status);
  }
  if (gen.status === "failed") throw new Error(gen.error);

  const file = await fetch(gen.file_url);
  await (await import("node:fs/promises")).writeFile("video.mp4", Buffer.from(await file.arrayBuffer()));
  console.log("Готово: video.mp4");
}

generateVideo("неоновый город ночью, камера медленно летит между небоскрёбами");
```

---

## Правила для ИИ-агентов

Если вы ИИ-агент и пользователь дал вам этот документ и API-ключ, работайте строго по этому алгоритму.

**Алгоритм одной генерации:**

1. Один раз за сессию вызовите `GET /v1/me`. Если `google_account.status` не `linked` — остановитесь и скажите пользователю
   подключить Google-аккаунт в Telegram-боте @googleflowapibot.
2. Один раз за сессию вызовите `GET /v1/models`. Выбирайте **только** `id`, форматы, длительности и качество из этого ответа.
   Никогда не придумывайте названия моделей и параметры.
3. Перед каждой платной генерацией вызовите `POST /v1/estimate`. Сообщите пользователю цену (`cost`) и остаток (`balance_after`).
   Если пользователь не разрешил тратить кредиты заранее — спросите подтверждение. Если `enough_credits` или `quota.enough`
   равно `false` — не запускайте генерацию, объясните причину.
4. Запустите генерацию: `POST /v1/images` или `POST /v1/videos` **без** `wait`. Сохраните `id`.
5. Проверяйте `GET /v1/generations/{id}?wait=30` в цикле, пока `status` не станет `completed` или `failed`.
   Не делайте запросы чаще одного раза в 5 секунд. Максимальное ожидание — 15 минут.
6. `completed` → отдайте пользователю `file_url` (или скачайте файл сразу — ссылка живёт около 6 часов) и сообщите `credits_spent`.
   `failed` → покажите пользователю текст `error`.

**Запреты:**

- Не запускайте одну и ту же генерацию повторно «на всякий случай» — каждая попытка тратит кредиты.
- Не повторяйте запросы с ошибками `insufficient_credits` (402), `quota_exceeded` (429), `model_not_allowed` (403),
  `key_revoked` / `key_expired` / `invalid_api_key` (401) — повтор ничего не изменит.
- Повторять можно только ошибки `502` и `503`, не больше 2 раз, с паузой минимум 60 секунд.
- Если генерация завершилась с `failed` из-за модерации — измените промпт, прежде чем пробовать снова, и предупредите пользователя.
- Не показывайте API-ключ пользователю в ответах и не записывайте его в файлы.
- Один запрос = одна картинка или одно видео. Если нужно несколько — запускайте по одному и дожидайтесь результата.

**Как писать хорошие промпты:**

- Картинка: объект + окружение + стиль + свет + ракурс. Пример: «старый маяк на скале, шторм, закат, фотореализм, широкий угол».
- Видео: что происходит + движение камеры + атмосфера + звук. Пример: «дрон медленно облетает маяк во время шторма, волны бьются о скалы, слышен ветер».
- Можно писать на русском или английском.

---

## Частые вопросы

**Нужно ли копировать куки Google?**
Нет. Вы один раз входите в Google через страницу, которую даёт бот. Сессия хранится на сервере сервиса в отдельном профиле
вашего аккаунта и обновляется сама.

**Google «разлогинил» — что делать?**
API начнёт отвечать `flow_session_expired`, а бот пришлёт уведомление. В боте: «Google-аккаунт» → «Переподключить».

**Почему у меня нет модели X или длительности Y?**
Набор моделей и цены зависят от вашей подписки Google. Смотрите `GET /v1/models`.

**Можно ли сгенерировать сразу 4 картинки одним запросом?**
Нет, один запрос = один файл. В Telegram-боте можно выбрать количество ×1–×4.

**Сколько запросов можно делать одновременно?**
Сколько угодно, но генерации одного Google-аккаунта выполняются по очереди (`queued` → `running`).

**Списываются ли кредиты за неудачную генерацию?**
Решает Google. Обычно за генерацию, отклонённую модерацией, кредиты не списываются. Точное значение всегда в `credits_spent`.

**Где посмотреть интерактивную схему API?**
Swagger: `https://flow.178-151-26-131.sslip.io/swagger`.
