Студия: генерация под свою задачу¶
Второй способ генерировать, рядом с каталогом. В каталоге человек выбирает готовый эффект и присылает фото. В студии он выбирает задачу — «убрать фон», «карточка товара», — заполняет короткую форму, и промпт собирается на сервере по шаблону этой задачи.
Главное отличие от каталога в контракте: при запуске клиент передаёт не только фотографии, но и ответы формы и выбранные настройки (размер, качество).
Студию видно гостю, создавать эффекты — после регистрации
Гостевому аккаунту доступны список задач и оценка цены: экран студии можно
показать целиком до регистрации, вместе со стоимостью. Сам запуск закрыт — гостю
придёт 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; отдельного жизненного цикла у студии нет.
Категории задач¶
{
"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
}'
Дёргай при каждом изменении выбора: цена зависит и от настроек, и от числа фотографий —
входные картинки тарифицируются поштучно. В 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
}'
Поле называется 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. Ограничение общее с каталогом — его
текстовые модели тоже требуют фото. Если планируете экран «создать с нуля», напишите нам:
это отдельная работа на стороне бэкенда, а не настройка в админке.
Публикации результата в общую ленту. Галерея пока личная.