Свои эффекты¶
Пользователь подаёт заявку на собственный эффект: загружает референс-фото, описывает своими словами, что должно получиться, — а всё остальное делает система: пишет промпт, выбирает модель, считает цену и прогоняет тест. После одобрения модератором эффект публикуется в категорию «От сообщества», и автору может начисляться процент кредитами за каждую успешную генерацию других пользователей.
Все эндпоинты требуют Authorization: Bearer $TOKEN.
Жизненный цикл заявки¶
подача ──▶ processing (авто-промпт + тест-прогон) ──▶ pending (модерация)
│ │ │
│ └──▶ private (остался у автора) ──────┘
▼ ▼
rejected ◀──────────────────────────── approved (в каталоге)
processing— система строит эффект и прогоняет его на эталонном фото; результат прогона становится превью карточки. Небезопасный референс или провал теста отклоняются автоматически.private— шаблон готов и остался у автора: в каталоге его нет, запускать его может только он сам. Сюда попадает заявка, поданная сpublish: false; шаблон, который автор убрал из каталога сам (POST …/unpublish); и шаблон, снятый оператором — в последнем случае придёт ещё и push «Эффект убран из каталога», см. «Push о решении». Отправить такой шаблон на публикацию автор может сам — см. «Отправить шаблон на публикацию».pending— ждёт модератора.approved— эффект опубликован и виден всем;rejected— вreject_reasonтипизированный код причины, вreject_note— комментарий модератора.archived— эффект убран из каталога (автором или системой). Заявка остаётся в списке и читается по id: генерации и заработок по ней никуда не делись.- Промпт, модель и параметры — внутренние данные системы, в API они не отдаются никогда.
Рядом со status приходит visibility — private, moderation или published.
Это ответ на вопрос «кому эффект виден», без примеси того, чем закончилась проверка: status
говорит, что с заявкой происходит, visibility — где она относительно каталога.
Отклонённая заявка, например, приходит как status: "rejected",
visibility: "private" — из каталога её убрали, но автору её не вернули (запускать
отклонённый эффект нельзя и ему).
- Опубликованный эффект не навсегда: по жалобам пользователей его могут снять
(придёт push со статусом rejected и типизированной причиной, как при отказе на
модерации), а если по нему долго никто не генерирует — он уходит в архив
(archived). В обоих случаях история генераций и заработок сохраняются.
Перед формой: условия приёма¶
{
"is_open": true,
"can_submit": true,
"daily_limit": 3,
"submitted_today": 1,
"remaining_today": 2,
"max_pending": 3,
"pending_now": 1,
"cooldown_until": null,
"banned_until": null,
"description_min_length": 10,
"description_max_length": 500
}
Запрашивай это до показа формы: is_open: false — маркетплейс на этой
площадке ещё не запущен, точку входа лучше не показывать вовсе; can_submit:
false при открытом маркетплейсе — покажи причину из полей (remaining_today: 0
— лимит на сегодня, pending_now == max_pending — уже висят заявки,
cooldown_until — пауза после отказа до указанного времени, banned_until — подача
закрыта на несколько дней после серии отказов). Так пользователь узнаёт об ограничении
до загрузки фото, а не из ошибки после отправки.
banned_until — более жёсткая блокировка, чем cooldown_until: она включается, когда
модерация подряд отклонила несколько заявок автора. Машинные авто-отказы
(test_failed, pipeline_error, original_unavailable) в этот счёт не идут — за наши
сбои и за снятую карточку автора не наказываем.
Подать заявку¶
Референс (и опциональный пример «было → стало») сначала загружаются как обычные фото через Загрузку фото, затем:
curl -X POST "$BASE_URL/v1/user-effects" -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' -d '{
"reference_upload_id": "<upload_id>",
"description": "Хочу эффект как на этом фото: неоновый киберпанк-город ночью",
"example_source_upload_id": null,
"example_result_upload_id": null,
"accept_license": true,
"confirm_adult": true,
"publish": true
}'
description(10–500 символов, любой язык) — главный вход авто-промптинга: чем точнее описано, что эффект делает с человеком, тем лучше результат.- Пример «было → стало» опционален, но повышает и качество промпта, и шансы на модерации.
accept_license: trueобязателен — автор подтверждает права на референс и даёт платформе неисключительную лицензию (без него — 422).confirm_adult: true— подтверждение совершеннолетия, нужно один раз на автора. Спрашивать его или нет, говоритadult_confirmation_requiredиз/config: покаtrue— покажи галочку в форме (без неё — 422), после первой принятой заявки поле станетfalse, и повторно спрашивать не нужно.publishнеобязателен и по умолчаниюtrue— заявка идёт в каталог, как и раньше. Передайfalse, если человек делает шаблон для себя: система так же соберёт и оттестирует его, но на модерацию он не уйдёт и останется вprivate, пока автор не отправит его сам.- Лимиты (см.
/configвыше) — при превышении вернётся 429, но правильный путь не доводить до этого: гейтить кнопку поcan_submit. 503 provider_unavailable— приём заявок ещё не открыт на этой площадке (то же, чтоis_open: false); показывай «скоро откроем».
Ответ — 201 с объектом заявки в статусе processing.
Шаблоны, оставленные себе, лимит max_pending не занимают: pending_now считает
только то, что реально ждёт модератора. Дневной лимит подачи они расходуют — сборка
и тест-прогон стоят одинаково в обоих случаях.
Пока идёт обработка¶
processing занимает обычно 1–3 минуты (авто-промптинг + пробная генерация),
модерация после этого — до суток. Ждать статус нужно стримом, а не поллингом:
event: status
data: {"id":"<submission_id>","status":"processing","effect_id":"<effect_id>"}
event: status
data: {"id":"<submission_id>","status":"pending","effect_id":"<effect_id>"}
Контракт тот же, что у стрима генерации — переиспользуй уже
написанный SSE-клиент. Первым событием всегда приходит текущий статус (можно открывать
стрим сразу после 201, отдельный GET не нужен), дальше — каждая смена. На
approved / rejected / archived / private сервер закрывает стрим сам: дальше
ничего не произойдёт само. У шаблона, поданного с publish: false, это private —
сборка закончилась, и следующий шаг за автором. Разрыв связи — переоткрой стрим,
первое событие вернёт то, что успело произойти (истории у стрима нет, но состояние
всегда актуальное).
Заявка не может зависнуть: если обработка не удалась, она сама переходит в
rejected с техническим кодом (см. ниже). На approved и rejected приходит ещё и
push на устройство — стрим нужен для экрана ожидания, push для закрытого приложения.
А вот private приходит по-разному: у собранного и оставленного себе шаблона push'а нет
(это конец сборки, а не решение), у возвращённого оператором — есть. Поэтому экран
ожидания закрывай по стриму; подробности в «Push о решении».
Причины отказа¶
reject_reason — код, reject_reason_text — готовый текст на языке автора
(показывай его), reject_note — необязательный комментарий модератора.
| Код | Что значит | Что предложить пользователю |
|---|---|---|
unsafe |
запрещённый контент на референсе | выбрать другой референс |
third_party |
узнаваемое чужое лицо без прав | выбрать другой референс |
brand |
бренды, логотипы, защищённые персонажи | выбрать другой референс |
duplicate |
такой эффект уже есть: автоматом — та же картинка, иначе — решение модератора | взять свой референс или придумать другую идею |
low_quality |
референс слишком плохого качества | загрузить фото получше |
mismatch |
описание не соответствует референсу | переписать описание |
test_failed |
пробная генерация не получилась | попробовать другой референс |
pipeline_error |
наш технический сбой | «подать ещё раз» — кулдаун не действует |
original_unavailable |
правка не опубликована: пока она ждала, эффект, который она меняла, ушёл из каталога (снят, отклонён или в архиве) | править больше нечего; подать как новую заявку |
Коды test_failed, pipeline_error и original_unavailable — машинные, они не
включают паузу после отказа: пользователь может подать заявку сразу.
Доверенные авторы¶
Некоторым авторам платформа выдаёт статус доверенного: их заявки публикуются сразу
после автоматических проверок, без ожидания модератора (модератор смотрит их уже
после публикации). Для приложения это выглядит как быстрый переход processing →
approved без задержки на pending — отдельного поля или экрана не требуется. Статус
выдаётся вручную платформой и снимается, если эффект снимут по жалобе.
Push о решении¶
Когда модератор одобрил или отклонил заявку, на устройства приходит push с
data:
status — approved, rejected, archived или private; используй submission_id для
перехода на экран заявки, детали подтягивай через GET /v1/user-effects/{id}. Экран
открывается в любом из этих статусов, включая archived.
private придёт и тем, кто ничего не менял
Терминальных статусов четыре: approved / rejected / archived / private. И
private встречается в трёх разных ситуациях, которые ведут себя по-разному:
- шаблон собран и остался у автора (заявка с
publish: false) — приходит только стримом, push'а нет: это не решение по заявке, а конец сборки; - автор сам убрал шаблон из каталога (
POST …/unpublish) — тоже только стримом: push'ом сообщать человеку о том, что он только что сделал сам, незачем; - оператор снял шаблон (жалоба, кураторский эффект) — приходит и стримом, и push'ом «Эффект убран из каталога».
Третий случай не требует вашего участия вообще: клиент мог никогда не присылать
publish: false — достаточно, чтобы оператор снял с витрины уже опубликованный
шаблон. Так что обрабатывать private нужно в любом случае, а не только если
используете новые ручки.
И закрывайте экран ожидания по событию стрима, а не по приходу push'а: в первых двух случаях push'а не будет вовсе, и экран, который его ждёт, останется висеть навсегда.
Заголовок push'а зависит от того, что произошло: новая заявка одобрена — «Эффект
опубликован!», одобрена правка — «Правка опубликована» (в status в обоих случаях
approved; различить можно по replaces_submission_id заявки).
Новости по опубликованному эффекту¶
Кроме решения по заявке автору приходят три уведомления о том, что происходит с уже
опубликованной работой — с ключом effect_id и полем milestone:
milestone |
Когда |
|---|---|
first_generation |
кто-то впервые сделал генерацию этим эффектом |
earned |
начислены кредиты за чужую генерацию |
promoted |
эффект перенесён из «От сообщества» в тематическую категорию |
Статус заявки они не меняют и в SSE-стрим не идут — стрим закрывается на решении
модератора, а это новости уже опубликованной карточки. first_generation приходит один
раз (на первую чужую успешную генерацию), earned — по одному на генерацию, повторная
обработка задачи второго пуша не даёт.
archived приходит, когда эффектом давно никто не генерировал и он ушёл в архив:
карточка пропадает из каталога, но история генераций и весь заработок сохраняются, и
экран заявки продолжает открываться (status: "archived"). Чтобы вернуться в каталог,
автор подаёт заявку заново.
Изменить опубликованный эффект¶
Опубликованный эффект правится не на месте, а предложением изменения: подай новую заявку и укажи, какую заменяешь.
curl -X POST "$BASE_URL/v1/user-effects" -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' -d '{
"reference_upload_id": "<новый референс>",
"description": "Уточнил описание: больше неона, меньше дождя",
"accept_license": true,
"replaces_submission_id": "<id опубликованной заявки>"
}'
Пока изменение проходит проверку, старый эффект продолжает работать — люди генерируют по нему как обычно. После одобрения содержимое заменяет старое прямо в той же карточке: id, ссылка, история генераций и заработок сохраняются.
- В списке у заявки-правки есть
replaces_submission_id, а у опубликованной —revision_pending: true, пока изменение в проверке. - После одобрения правка сливается с оригиналом: в стрим приходит
approved, push говорит «Правка опубликована», а из списка заявка-правка исчезает — дальше живёт карточка оригинала (её id вы уже знаете изreplaces_submission_id). - Одновременно можно править только один эффект (вторая правка того же — 422).
- Правится только опубликованный: заявку в обработке или отклонённую не «правят», вместо этого подают новую.
Отправить шаблон на публикацию¶
Переводит свой private-шаблон в каталог тем же путём, что и обычная заявка:
visibility становится moderation, status — pending, дальше модерация. У
доверенного автора шаблон публикуется сразу (approved). Ответ — объект заявки,
как у GET /v1/user-effects/{id}.
422— шаблон ещё строится (status: "processing",preview_url: null), уже отправлен или опубликован, либо он отклонён. Кнопку показывай только наstatus: "private".429— у автора уже висит максимум заявок на модерации (pending_now == max_pendingв/config) или действует пауза после отказа.
Убрать свой шаблон из каталога¶
Обратный переход, и он тоже авторский: работа принадлежит человеку, он и решает, висеть
ей в витрине или нет. Шаблон становится private — из каталога пропадает, у автора
остаётся: он им генерирует и может отправить на публикацию снова, тем же
POST …/publish. Уже идущие генерации не прерываются: снятие убирает карточку из
витрины, а не отменяет запущенное.
422— шаблон и так не в каталоге (statusнеapproved). Кнопку показывай только на опубликованном.404— чужая заявка. Как и все авторские действия, ручка не подтверждает даже существование чужой.
Что может оператор, чего не может автор
Асимметрия осталась ровно одна: оператор снимает чужой шаблон — по жалобе, и
у кураторских эффектов, где автора нет вовсе. Тогда заявка приходит автору в
status: "private" с push'ом «Эффект убран из каталога». Своё же автор снимает сам,
и push при этом не шлётся — он и так знает.
Отдельно от снятия есть DELETE (архив, см. «Убрать эффект»): он убирает шаблон и из
каталога, и из работы — обратно из архива автор его уже не вернёт.
Мои эффекты и заработок¶
curl "$BASE_URL/v1/user-effects" -H "Authorization: Bearer $TOKEN"
curl "$BASE_URL/v1/user-effects/$ID" -H "Authorization: Bearer $TOKEN"
Каждый элемент: status, visibility, name (название придумывает система),
preview_url (появляется после тест-прогона), preview (тот же постер в размерах тайла,
объект как в каталоге; null, пока копии не готовы), cost (цена
генерации для других),
reject_reason/reject_note, is_published, effect_slug (deep-link в
каталог после публикации), gen_count — успешные генерации других
пользователей, earned — заработано кредитов.
Список отдаёт заявки во всех статусах, включая archived, и не отдаёт заявку-правку
после того, как её содержимое перенесено на оригинал (см. «Изменить опубликованный
эффект»). GET /v1/user-effects/{id} открывает любую свою заявку, кроме той же
перенесённой правки — по ней 404, потому что отдельного объекта больше нет.
Прогнать свой эффект до публикации
Пока заявка идёт (processing, pending) и пока шаблон остаётся приватным
(private), автор может генерировать своим ещё не опубликованным эффектом —
обычным POST /v1/generations по effect_id, за свои кредиты. В каталоге такого
эффекта нет: GET /v1/effects/{id} и {slug} отдают 404 даже автору, свои
шаблоны он открывает через этот раздел.
Цена у эффекта есть с момента подачи заявки: cost нулём не бывает. Пока идёт
processing, она может вырасти: если на референсе несколько людей, эффект считается
на фото каждого из них.
В первые секунды-минуты processing, пока система разбирает референс и ещё не собрала
эффект, POST /v1/generations по нему отвечает 422 (validation_error) и ничего не
списывает. По cost этот момент не отличить: показывай такой отказ как «эффект ещё
собирается».
После отказа эффект перестаёт работать и для автора тоже: POST
/v1/generations вернёт 404, хотя visibility у него тоже private. Гейти
кнопку «попробовать» по status, а не по visibility — не давай нажать на
отклонённой и на архивной карточке.
Опубликованный эффект в общем каталоге несёт имя автора
(author_name); начисления видны в истории кредитов —
kind: "effect_earned".
Доля приходит в кошелёк после холда
Начисление появляется в кабинете автора сразу после чужой
генерации, но в кошелёк попадает только по окончании холда (сейчас 14 дней, срок
задаёт сервер): пока покупатель может вернуть деньги, доля ждёт. Ожидающая доля
видна как pending в счёте и как settles_at у строки истории; в
истории кредитов effect_earned и push о начислении приходят в момент
зачисления, а не генерации.
Как считается доля
Процент от токен-цены генерации; ставка и потолок задаются на сервере и могут быть нулевыми (выплаты включаются отдельным решением платформы). Начисляется только за чужие успешные платные генерации: свои прогоны, системные тесты и неудачные генерации не приносят ничего.
«Платные» здесь про плательщика, а не про кредит: доля начисляется, только если
запустивший генерацию хоть раз покупал кредиты. Прогоны тех, кто живёт на
приветственных и реферальных кредитах, автору не приносят ничего — так кредиты не
ходят по кругу между авторами, не превращаясь в выручку. Поэтому gen_count может
расти быстрее, чем earned; объясняй разрыв этим, а не ошибкой.
Когда доля не начисляется
Кроме генераций от неплативших (выше) есть ещё два случая: генерация пришла со связанного
с автором аккаунта (тот же телефон или приглашённый по его реф-ссылке) и выплаты
автору заморожены после срабатывания анти-фрода или решения поддержки. В обоих
случаях генерация проходит как обычно — не приносит только автору. Отдельного
статуса в API нет: это видно как gen_count без роста earned.
Начисление могут отозвать
Если покупатель вернёт деньги за кредиты, все его доли, которые ещё на холде, снимаются
и в кошелёк автора не придут. Если поддержка вернёт пользователю кредиты за генерацию,
доля автора за неё отзывается в любом случае. В обоих случаях в истории начислений
появится строка revoked, а earned эффекта уменьшится. Снятая на холде доля кошелёк
не трогает; отзыв уже зачисленной оставляет в истории кредитов
kind: "effect_revoked". Когда отзывать уже нечего (автор потратил заработанное),
кошелёк в минус не уходит: долг гасится ближайшими будущими начислениями, и
какое-то время генерации по эффекту не приносят кредитов. Баланс при этом
остаётся корректным — отдельного «отрицательного счёта» приложение не показывает.
Кабинет автора¶
Три чтения поверх списка эффектов: счёт, история начислений и воронка эффекта по дням.
Авторский счёт¶
curl "$BASE_URL/v1/user-effects/account" -H "Authorization: Bearer $TOKEN"
# {"earned": 128, "revoked": 5, "debt": 0, "pending": 12}
earned— начислено за всё время,revoked— отозвано (оба положительные).pending— частьearned, которая ещё на холде и в кошелёк пока не пришла. Показывай как «ожидает зачисления»: если покупатель за это время вернёт деньги, эта часть уйдёт вrevoked.debt— отозванное, что автор уже потратил; гасится ближайшими начислениями, пока не обнулится. В норме 0; если больше нуля — честно объясни, что ближайший заработок уйдёт на погашение.
Отдельного «сколько из заработанного ещё осталось» здесь нет: кошелёк — одно число и не помнит, откуда пришёл каждый кредит. Текущий остаток бери из Кредитов.
История начислений¶
{
"items": [
{ "kind": "earned", "amount": 4, "effect_id": "…", "created_at": "…", "settles_at": "…" },
{ "kind": "earned", "amount": 3, "effect_id": "…", "created_at": "…", "settles_at": null },
{ "kind": "revoked", "amount": -3, "effect_id": "…", "created_at": "…", "settles_at": null }
],
"total": 3, "limit": 20, "offset": 0
}
Одна строка — одна чужая генерация. settles_at заполнено, пока начисление на холде:
это момент, когда оно придёт в кошелёк. null — уже пришло или снято. revoked со
знаком минус — начисление сняли или отозвали после возврата денег или кредитов. Кто именно генерировал и что
получил — не отдаётся никогда.
Статистика эффекта по дням¶
{
"effect_id": "…", "days": 30,
"items": [
{ "day": "2026-07-26", "views": 120, "generations": 14, "succeeded": 13, "earned": 26 }
],
"views": 340, "generations": 41, "succeeded": 38, "earned": 74
}
days— 1…90, по умолчанию 30. Дни без активности пропускаются, не приходят нулями: рисуй ряд сам по запрошенному диапазону.views— уникальные зрители за день: один пользователь, открывший карточку пять раз, считается один раз. Просмотры без токена не считаются вовсе — иначе число накручивалось бы кем угодно, а конверсия зависела бы от привычки листать. Считается «на лету», в отчёт попадает с задержкой до ~5 минут — сегодняшнее число может слегка отставать.generations— чужие генерации за день,succeeded— из них успешные (доля успешных =succeeded / generations, конверсия =generations / views). Свои прогоны автора не считаются ни в том, ни в другом.earned— начислено за день; может быть отрицательным, если в этот день отозвали начисление.- Верхние
views/generations/succeeded/earned— суммы за окно, складывать строки не нужно.
Чужой эффект по своему токену не читается — 404.
Убрать эффект¶
Эффект пропадает из каталога; в своём списке он остаётся со статусом archived и
is_published: false — история чужих генераций и начислений по нему сохраняется и
дальше видна в статистике. При удалении аккаунта опубликованные эффекты остаются в каталоге
по принятой лицензии (авторство анонимизируется), неопубликованные заявки
удаляются.