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

Свои эффекты

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

Все эндпоинты требуют 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). В обоих случаях история генераций и заработок сохраняются.

Перед формой: условия приёма

curl "$BASE_URL/v1/user-effects/config" -H "Authorization: Bearer $TOKEN"
{
  "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 минуты (авто-промптинг + пробная генерация), модерация после этого — до суток. Ждать статус нужно стримом, а не поллингом:

curl -N "$BASE_URL/v1/user-effects/$SUBMISSION_ID/stream" \
  -H "Authorization: Bearer $TOKEN"
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:

{ "submission_id": "<id заявки>", "status": "approved" }

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).
  • Правится только опубликованный: заявку в обработке или отклонённую не «правят», вместо этого подают новую.

Отправить шаблон на публикацию

curl -X POST "$BASE_URL/v1/user-effects/$ID/publish" -H "Authorization: Bearer $TOKEN"

Переводит свой 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) или действует пауза после отказа.

Убрать свой шаблон из каталога

curl -X POST "$BASE_URL/v1/user-effects/$ID/unpublish" -H "Authorization: Bearer $TOKEN"

Обратный переход, и он тоже авторский: работа принадлежит человеку, он и решает, висеть ей в витрине или нет. Шаблон становится 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; если больше нуля — честно объясни, что ближайший заработок уйдёт на погашение.

Отдельного «сколько из заработанного ещё осталось» здесь нет: кошелёк — одно число и не помнит, откуда пришёл каждый кредит. Текущий остаток бери из Кредитов.

История начислений

curl "$BASE_URL/v1/user-effects/earnings?limit=20&offset=0" -H "Authorization: Bearer $TOKEN"
{
  "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 со знаком минус — начисление сняли или отозвали после возврата денег или кредитов. Кто именно генерировал и что получил — не отдаётся никогда.

Статистика эффекта по дням

curl "$BASE_URL/v1/user-effects/$ID/stats?days=30" -H "Authorization: Bearer $TOKEN"
{
  "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.

Убрать эффект

curl -X DELETE "$BASE_URL/v1/user-effects/$ID" -H "Authorization: Bearer $TOKEN"

Эффект пропадает из каталога; в своём списке он остаётся со статусом archived и is_published: false — история чужих генераций и начислений по нему сохраняется и дальше видна в статистике. При удалении аккаунта опубликованные эффекты остаются в каталоге по принятой лицензии (авторство анонимизируется), неопубликованные заявки удаляются.