Перейти к содержанию

Студия: генерация под свою задачу

Второй способ генерировать, рядом с каталогом. В каталоге человек выбирает готовый эффект и присылает фото. В студии он выбирает задачу — «убрать фон», «карточка товара», — заполняет короткую форму, и промпт собирается на сервере по шаблону этой задачи.

Главное отличие от каталога в контракте: при запуске клиент передаёт не только фотографии, но и ответы формы и выбранные настройки (размер, качество).

Студию видно гостю, создавать эффекты — после регистрации

Гостевому аккаунту доступны список задач и оценка цены: экран студии можно показать целиком до регистрации, вместе со стоимостью. Сам запуск закрыт — гостю придёт 403 registration_required, и это повод показать экран регистрации, а не обновлять токен. После регистрации аккаунт тот же (user.id сохраняется), так что заполненная форма и кредиты никуда не деваются.

Форму рисует клиент по описанию с сервера

Список задач и состав их форм меняются в админке без релиза приложения. Поэтому экран строится из ответа GET /v1/studio/categories, а не из зашитой в клиент структуры. Набор ТИПОВ полей при этом фиксирован (таблица ниже) — новый тип означал бы новый релиз, и мы его не добавляем без предупреждения.

Поток

GET  /v1/studio/categories              что можно сделать + какая у задачи форма   (можно гостю)
POST /v1/uploads                        фото, которые просит форма (как обычно)
POST /v1/studio/generations/estimate    цена в кредитах для выбранных настроек     (можно гостю)
POST /v1/studio/generations             запуск → 202 {id, status, cost}            (нужен аккаунт)
GET  /v1/generations/{id}               статус и результат — уже обычными ручками

Всё, что после запуска, — общее с каталогом: статусы, SSE-стрим, push, возврат кредитов при ошибке. Смотри Генерацию и realtime; отдельного жизненного цикла у студии нет.

Категории задач

curl "$BASE_URL/v1/studio/categories" -H "Authorization: Bearer $TOKEN"
{
  "items": [
    {
      "id": "1a2b3c4d-5e6f-4071-8233-9a0b1c2d3e4f",
      "slug": "remove-background",
      "name": "Убрать фон",
      "icon": "https://cdn.example/icons/remove-bg.png",
      "fields": [
        {
          "key": "photo",
          "type": "photo",
          "label": "Фото",
          "required": true,
          "help": "Снимок, с которого убираем фон.",
          "choices": [],
          "default": null
        }
      ],
      "params": [
        {
          "key": "quality",
          "label": "Качество",
          "choices": ["low", "medium", "high", "auto"],
          "default": "low",
          "help": "Качество рендера. Сильно влияет на стоимость: чем выше, тем дороже."
        }
      ],
      "prompt": null
    }
  ],
  "total": 1, "limit": 1, "offset": 0
}
  • fields — вопросы формы. Рисуются по type, подписываются label, обязательность в required, подсказка под полем — help.
  • params — настройки модели, которые эта задача разрешает менять пользователю. Всё остальное решает сама задача, и прислать его нельзя (422). Рисуй выбором из choices, предвыбирай default.
  • prompt — промпт задачи, если она разрешает его править: покажи его заполненным, правку пришли при запуске (см. ниже). null — промпт задача держит у себя, поле ввода не показывай.
  • Неактивные категории в ответе не приходят. Список стоит перечитывать при входе в студию: задачу могли добавить или выключить.

Типы полей формы

type Что это Что прислать в values
photo снимок идентификатор загрузки: сначала POST /v1/uploads, потом её id строкой
text строка (название товара, пожелание) строку
enum выбор из choices одно из значений choices строкой
integer число число
boolean да/нет true / false

Поле с required: true обязано быть заполнено. Необязательное можно не присылать вовсе — но если оно упомянуто в шаблоне промпта, запуск откажет: незаполненная подстановка ушла бы в модель фигурными скобками, а прогон был бы оплачен.

Цена до запуска

curl -X POST "$BASE_URL/v1/studio/generations/estimate" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{
        "category_id": "1a2b3c4d-5e6f-4071-8233-9a0b1c2d3e4f",
        "params": {"quality": "medium"},
        "photos": 1
      }'
{ "cost": 23 }

Дёргай при каждом изменении выбора: цена зависит и от настроек, и от числа фотографий — входные картинки тарифицируются поштучно. В photos передавай, сколько снимков уйдёт в запрос; без него считается по обязательным полям формы.

Число из ответа — ровно то, которое спишется при запуске: оценка и списание считаются одним и тем же движком.

Запуск

curl -X POST "$BASE_URL/v1/studio/generations" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{
        "category_id": "1a2b3c4d-5e6f-4071-8233-9a0b1c2d3e4f",
        "values": {
          "photo": "b3f1c2a4-5d6e-4f70-8a91-0c2d3e4f5a6b",
          "product_name": "Электрочайник Bosch"
        },
        "params": {"quality": "medium"},
        "expected_cost": 23
      }'
{ "id": "7c9e…", "status": "pending", "cost": 23 }

Поле называется values

Ответы формы лежат в values, ключи — это key из fields. Частая ошибка на старте: прислать answers или вложить ответы в fields. Тело разберётся, все поля окажутся незаполненными, и придёт честный, но сбивающий с толку отказ «Заполни поле «Фото»» — хотя фото вы как раз приложили.

  • cost кредитов резервируется сразу, как и в обычной генерации. Не хватает — 402 insufficient_credits, ничего не создаётся.
  • Дальше всё как в каталоге: id → статус, SSE и push.
  • values принимает до 32 ответов, params — до 16 настроек. Текстовый ответ — до 1 000 символов; запрос, который получился после подстановки ответов, — до 10 000.
  • Цена считается на каждый запуск: те же ответы с другим числом фото стоят столько, сколько показывает оценка для этого числа фото.

prompt: свой текст вместо промпта задачи

Необязательное поле, только для задач, у которых в списке пришёл prompt не null.

  • Не прислан или пустой — запуск идёт по промпту задачи.
  • Прислан — уходит текст пользователя. Подстановки вида {product_name} в нём заполняются ответами формы, как в промпте задачи. Фигурные скобки вокруг слов, которых нет среди полей формы, уходят как есть.
  • Не длиннее 10 000 символов.
  • Цена от текста не зависит: оценка и списание те же, что без него.
{
  "category_id": "1a2b3c4d-5e6f-4071-8233-9a0b1c2d3e4f",
  "values": {"photo": "b3f1c2a4-5d6e-4f70-8a91-0c2d3e4f5a6b", "product_name": "Электрочайник Bosch"},
  "prompt": "Поставь {product_name} на кухонный стол, утренний свет из окна."
}

expected_cost: не списать больше, чем показали

Необязательное поле. Присылай в нём ту цену, которую показал пользователю (из оценки).

Что произошло между оценкой и запуском Что будет
цена не изменилась запуск идёт
цена выросла (оператор поправил задачу) 422 с новой ценой, списания нет
цена упала запуск идёт, спишется новая, меньшая
expected_cost не прислан запуск идёт по актуальной цене

Асимметрия намеренная: списать больше показанного — претензия, списать меньше — подарок.

Это не торг: заниженное число приведёт к отказу, а не к скидке. Поле защищает пользователя от неожиданного списания, а не назначает цену.

Результат приватный

Студийный запуск заводит приватный шаблон-«рецепт» — чем запускали. Пользователю его заполнять не нужно и знать о нём не обязательно.

  • в каталоге такого шаблона нет — ни списком, ни по прямой ссылке;
  • результаты приходят в обычную личную историю генераций;
  • одинаковые запуски (та же задача, те же ответы и настройки) переиспользуют один рецепт — это внутренняя деталь, на клиенте не отражается.

Опубликовать своё в каталог — по-прежнему отдельный путь, заявкой автора.

Отказы

Все приходят до списания кредитов, единым конвертом ошибок. Тексты message написаны для показа пользователю.

Ситуация Код Что придёт в message
не заполнено обязательное поле 422 validation_error Заполни поле «Фото».
поле упомянуто в шаблоне, но пустое 422 validation_error Заполни поле «Пожелание» — оно подставляется в запрос, и без него модель получит незаконченный текст.
прислан ключ, которого нет в форме 422 validation_error Поле «whatever» эта категория не спрашивает.
настройка не из числа открытых задачей 422 validation_error Настройка «image_size» в этой категории не выбирается.
значение настройки не из choices 422 validation_error Настройка «Качество»: значение «ultra» не из списка (low, medium, high, auto).
prompt прислан в задачу, которая его не отдаёт 422 validation_error Промпт в этой категории не редактируется.
в prompt подстановка незаполненного поля 422 validation_error Заполни поле «Пожелание» — оно подставляется в запрос, и без него модель получит незаконченный текст.
prompt длиннее 10 000 символов 422 validation_error —
текстовый ответ длиннее 1 000 символов 422 validation_error —
запрос после подстановки ответов длиннее 10 000 символов 422 validation_error Запрос получился слишком длинным. Сократи ответы и запусти снова.
ни одного фото 422 validation_error Приложи фото: без него запускать нечего.
цена выросла (см. expected_cost) 422 validation_error Цена изменилась: сейчас 31 кредитов вместо 23. Обнови оценку и запусти снова.
запуск с гостевого аккаунта 403 registration_required — (вести на регистрацию, не обновлять токен)
задача выключена или не существует 404 not_found —
загрузка чужая или не завершена 404 not_found / 422 validation_error —
не хватает кредитов 402 insufficient_credits —
Idempotency-Key уже приходил с другим телом 409 idempotency_key_reused пользователю не показывать: ошибка в приложении

Ветвись по code, а не по тексту: тексты мы можем поправить.

Запуск списывает кредиты, поэтому шли его с заголовком Idempotency-Key: повтор с тем же ключом и тем же телом вернёт уже созданную генерацию без второго списания, даже если задачу за это время выключили или цена выросла. Правила ключа - Ошибки, «Повтор запроса».

Чего пока нет

Задачи без фотографии. Путь генерации стартует от загрузки пользователя, поэтому категория, которой не нужен ни один снимок («сгенерировать картинку с нуля по описанию»), сейчас не заводится: запуск такой формы ответит 422. Ограничение общее с каталогом — его текстовые модели тоже требуют фото. Если планируете экран «создать с нуля», напишите нам: это отдельная работа на стороне бэкенда, а не настройка в админке.

Публикации результата в общую ленту. Галерея пока личная.