Каталог¶
Категории и эффекты. Публично, без токена — можно показывать витрину до входа.
Данные локализованы: name/description приходят уже на нужном языке.
Локализация¶
Язык задаётся параметром locale (по умолчанию ru):
Категории¶
[
{ "id": "…", "slug": "portraits", "name": "Портреты", "icon": "…",
"position": 0, "effect_count": 12, "gen_count": 3400 }
]
Порядок категорий задаёт оператор — массив уже приходит в нужном порядке, и проще
всего показывать его как есть. position (0 — первая) — то же самое числом: он нужен,
если список у тебя всё-таки пересортировывается — после локального кеша, слияния с
чем-то ещё или сортировки по популярности. Сортировка по position по возрастанию
возвращает ровно тот порядок, который выставлен в админке. Сортировать по «Порядку»
как-то иначе нельзя: сырое значение из админки наружу не отдаётся, там уровни с
повторами, и по ним однозначного порядка не построить.
effect_count/gen_count — про популярность и наполненность, не про порядок: они
помогают собрать блок «популярное» и спрятать пустые категории. Если ты сортируешь
главный экран по gen_count, кураторский порядок теряется — это осознанный выбор,
а не поведение по умолчанию.
Эффект может лежать в нескольких категориях сразу и считается в effect_count каждой из
них, поэтому сумма счётчиков по категориям бывает больше числа эффектов в каталоге.
Эффекты¶
Список с пагинацией, фильтром по категории и поиском (текст для search можно
получить из голоса, см. Голосовой ввод):
{
"items": [
{
"id": "1a2b3c4d-…", "slug": "anime",
"category_id": "7f3e…", "category_ids": ["7f3e…", "c41a…"],
"name": "Аниме", "description": "Превращает фото в аниме-портрет",
"preview_url": "https://…/anime.jpg",
"preview_video_url": "https://…/anime.mp4",
"preview_animation_url": "https://…/anime.webp",
"preview": {
"thumb": "https://…/thumbs/480/effects/preview/anime.webp",
"card": "https://…/thumbs/720/effects/preview/anime.webp",
"full": "https://…/thumbs/1080/effects/preview/anime.webp",
"animation": "https://…/anime-480-loop.mp4",
"video": "https://…/anime-720.mp4",
"width": 1024,
"height": 1536,
"blurhash": "LEHV6nWB2yk8pyo0adR*.7kCMdnj",
"focus": {"x": 0.63, "y": 0.34},
"video_focus": {"x": 0.41, "y": 0.22}
},
"cost": 5,
"characters": [{"min_photos": 1, "max_photos": 1}],
"max_inputs": 1,
"min_inputs": 1,
"output_type": "image",
"author_name": null
}
],
"total": 42, "limit": 20, "offset": 0
}
category_ids- все категории эффекта, их может быть несколько.GET /v1/effects?category=находит эффект по любой из них, поidили поslugкатегории. В плоском списке безcategoryэффект при этом приходит один раз.category_id- первая изcategory_ids, поле оставлено для сборок, которые читают одну категорию; в новом коде бери список. Категория в списке может быть выключена: вGET /v1/categoriesеё тогда нет, и такой id нужно просто пропустить. Включённые категории стоят в списке раньше выключенных, поэтому у эффекта из списковcategory_idвсегда есть вGET /v1/categories. У эффекта, открытого по прямой ссылке, список бывает пустым, аcategory_idравенnull.- В списки попадает только эффект, у которого есть хотя бы одна включённая категория.
Эффект без категории или только в выключенных не приходит ни в
GET /v1/effects(с фильтрами и поиском), ни в счётчики категорий и тегов, ни на страницу автора. По прямомуidилиslugон открывается и запускается как обычно. cost— цена в кредитах; ровно столько спишется при генерации.preview_url— превью-постер, есть всегда.preview_video_url— опциональный клип: MP4 (H.264), 720 по короткой стороне, не больше 60 fps, без звука, индекс в начале файла, так что плеер стартует до полной докачки. Пропорция как у загруженного ролика, без кропа.preview_animation_url— опциональная облегчённая анимация (animated WebP), генерится из клипа автоматически; показывай её обычным<img>в гриде вместо<video>(дешевле по CPU/батарее), а полноценныйpreview_video_url— на детальном экране.preview— тот же постер в размерах тайла, WebP. Грузи его вместоpreview_url: оригинал весит мегабайты, копия в сетке - десятки килобайт.thumb/card/full- копии по короткой стороне 480 / 720 / 1080 (у постера 2:3 это 480×720, 720×1080, 1080×1620). Копия не бывает больше оригинала: у постера 1024×1536fullотдаётся в 1024×1536. Пропорция всегда как у оригинала, без кропа.width/height- размер оригинала: по нему резервируй место под тайл до загрузки.blurhash- заглушка на время загрузки, может бытьnull.focus- точка постера, которая должна остаться в кадре, когда плитка другой формы, чем постер (карточка 3:4, квадратное промо):xиyв долях ширины и высоты от левого верхнего угла, от 0 до 1. Окно обрезки ставь центром на точку и сдвигай обратно в кадр, если оно вышло за край. Для заполнения плитки с обрезкой (cover) это выравнивание по той оси, вдоль которой постер не помещается: если по ней видна доляvпостера, окно начинается вs = clamp(f - v / 2, 0, 1 - v), а выравнивание от -1 до 1 равно2 * s / (1 - v) - 1. По второй оси постер виден целиком, там выравнивание любое. Точку ставит контент: сначала она сама встаёт на лицо, потом её можно подвинуть.null- точки нет (лица на постере не нашлось и руками её не ставили): режь по центру, как раньше. Точка относится к постеру:thumb,card,full.video_focus- такая же точка для клипа, одна наvideoиanimation: петли нарезаны из клипа без обрезки, раскладка кадра у них общая. Правило обрезки то же.null- точки нет или у эффекта нет клипа. По умолчанию постер эффекта с клипом - первый кадр клипа: форма та же, точка та же, и клип проявляется поверх постера без скачка кадра. Контент может поставить свой постер, тогда форма и точка у него свои (клип 9:16 при постере 3:4), поэтомуfocusиvideo_focusчитай раздельно.animation- петля для сетки: MP4 (H.264) без звука, индекс в начале файла. Играй без звука и по кругу. Какую петлю отдавать, контент выбирает для каждого эффекта: весь клип в своём темпе (по умолчанию), весь клип, ускоренный до 3 секунд, или первые 3 секунды клипа. Все три - 480 по короткой стороне, не больше 60 fps, у целых не больше 30, и это всегда отдельный файл, неvideo. Длина поэтому бывает больше 3 секунд, а вес - до сотен килобайт. Весит в 3-5 раз меньше анимированного WebP того же размера, поэтому это видео, а не картинка. Это не тот же файл, чтоpreview_animation_url: там WebP для старых сборок.null, если клипа нет или он загружен до перекодирования.video- тот же файл, чтоpreview_video_url(клип для карточки),null, если клипа нет.- URL неизменяемые: новый постер получает новые адреса, поэтому кэшировать можно навсегда.
- Сам
previewбываетnull, пока копии не готовы (свежий эффект, постер на чужом хосте). Тогда показывайpreview_url, как раньше. Старые поляpreview_*не меняются.
characters- люди, которых эффект берёт в прогон, по порядку, и сколько фото каждого:min_photos..max_photos. У обычного эффекта один персонаж с одним фото. Групповой эффект «от 1 до 4 человек» - четыре персонажа по одному фото, у первогоmin_photos = 1, у остальных0: такой человек необязателен. Несколько фото одного человека (max_photos > 1)- это ракурсы одного лица, модель держит по ним сходство. Первый персонаж обязателен всегда.
Показывай выбор снимков по персонажам и отправляй их полем
charactersпри генерации: промпт эффекта зовёт людей по их месту, и фото, отданное не тому персонажу, ставит не то лицо. Превью показывает результат на всех персонажей. max_inputs/min_inputs- суммыmax_photosиmin_photosпо персонажам: сколько фото всего можно и сколько нужно. Оставлены для сборок, которые читают только их; в новом коде бериcharacters. Всегда1 ≤ min_inputs ≤ max_inputs.output_type— что вернёт эффект:"image"(по умолчанию) или"video". По нему клиент выбирает, рисовать<img>или видеоплеер дляresult_url. Видео-эффект всё равно берёт от пользователя только фото (поcharacters): референс-клип с движениями настраивает администратор в самом эффекте, пользователь его не загружает.author_id— id автора для эффектов сообщества (nullу официальных). По нему открывается публичный профиль: именно он делает подпись под карточкой кликабельной, без него deep link упирается в одну карточку.author_name— имя автора для эффектов сообщества.nullу официальных эффектов, а также если автор не заполнил имя в профиле или удалил аккаунт — тогда показывай карточку без подписи (или своё «Автор сообщества»), но не подставляй email. Такие эффекты живут в категории «От сообщества» и создаются в приложении — см. Свои эффекты; для генерации они ничем не отличаются от официальных.tags- тематические метки эффекта (neon,winter,portrait…). Теги режут каталог поперёк категорий, и фильтры складываются:GET /v1/effects?category=<slug>&tag=winter. Метка пишется латиницей в нижнем регистре, одним словом: это ключ фильтра, а не подпись. Название для показа бери изGET /v1/tags(см. ниже), иначе чип получится с английскимneonвместо «Неон».- Порядок выдачи внутри категории сервер задаёт сам, и он учитывает ручную расстановку
оператора: сначала эффекты, поставленные оператором наверх, потом несколько мест для
новых эффектов (иначе свежий эффект никогда не покажут и он не наберёт генераций),
дальше по числу успешных генераций, затем эффекты, поставленные оператором вниз, и в
самом конце те, что понижены по жалобам. Расстановка своя в каждой категории: один и тот
же эффект может стоять первым в одной и идти по генерациям в другой. Не пересортировывай
список на клиенте. Плоский список без категории сохраняет кураторский порядок.
Поля вроде
position(как у категорий) здесь поэтому и нет: порядок эффектов это результат ранжирования, и в части «по генерациям» он свой для каждой страницы выдачи. Показывайitemsтак, как они пришли. Формат ответа от расстановки не меняется. - Параметры списков везде единообразны:
limit(1–100, по умолчанию 20) иoffset(0–10000, по умолчанию 0). Значение вне границ -422 validation_error.totalсчитает все строки, а получить можно первые10000 + limit: листание заканчивается наoffset = 10000.
Поиск¶
search - строка, которую пользователь набрал или надиктовал. Передавай её как есть:
регистр, знаки препинания, лишние пробелы и слова самой просьбы («покажи», «найди мне»,
«хочу эффект») сервер убирает сам.
- Ищется по названию эффекта, его тегам, включённым категориям и описанию на языке
locale. Слова запроса могут стоять в разных полях и в любом порядке: «неон кот» найдёт эффект «Кот в очках» с тегом «Неон». - Форма слова и опечатка в одну-две буквы не мешают: «супергероя» и «супергирой» находят «Супергерой». В словах короче четырёх букв опечатка не угадывается.
- Искать можно по мере набора: слово запроса находит и начало слова в названии, теге или категории, «суперг» находит «Супергерой». В описании слово ищется целиком. Не шли запрос на каждую букву: подожди 250-300 мс после последнего нажатия.
- Если эффектов со всеми словами запроса нет, приходят те, где нашлась часть слов: «хочу стать супергероем» покажет «Супергерой».
- Порядок - по близости к запросу: совпадение в названии выше совпадения в теге или
категории, оно выше совпадения в описании. При равной близости сохраняется обычный порядок
каталога. Ранжирование внутри категории (новые эффекты, число генераций) при поиске не
действует. Показывай
itemsтак, как они пришли. - Поиск по теме: запрос может найти эффект без единого общего слова, «шпион» покажет «Секретный агент». Такие эффекты стоят после тех, что запрос называет прямо, и их не больше десяти. По теме находится только то, что заметно ближе к запросу, чем остальной каталог: на запрос, которому ничего не подходит, эта часть поиска не добавляет ничего. Она включается на сервере отдельно; в приложении для неё ничего делать не нужно, заметной задержки к ответу она не добавляет. Запрос короче трёх символов ищется только по словам. Новый эффект находится по словам сразу, по теме - в течение десяти минут.
categoryиtagсужают поиск так же, как список, пагинация та же. Пустойsearchили строка из пробелов - обычный список. Запрос без единой буквы или цифры ничего не находит:items: [],total: 0.- Длина - до 300 символов, длиннее придёт
422 validation_error. Расшифровку диктовки длиннее 300 символов обрежь по границе слова до отправки. В поиске участвуют первые восемь значимых слов запроса, остальные не учитываются.
Теги¶
Чипы фильтра: какие теги вообще есть и как их подписать.
[
{"slug": "neon", "name": "Неон", "effect_count": 12},
{"slug": "winter", "name": "Зима", "effect_count": 4}
]
slugуходит вGET /v1/effects?tag=<slug>,nameпоказывается пользователю.- В ответе только теги, которые стоят хотя бы на одном доступном эффекте: чип из этого списка всегда что-то находит. Порядок — по числу эффектов, самые частые первыми.
effect_countсчитается по всему каталогу, без учёта категории. Если чипы показываются внутри категории, число под ней будет меньше — не выводи его как счётчик у чипа.- Список меняется редко: кешируй его вместе с категориями и обновляй при запуске.
Деталь эффекта¶
По id или slug:
Нет такого эффекта — 404 not_found. Полная схема EffectOut — в референсе.
Эндпоинт публичный, но если токен есть — передавай его: по нему открытие карточки community-эффекта засчитывается автору как уникальный просмотр за сутки (см. Свои эффекты). Просроченный или битый токен ничего не ломает — ответ тот же, просто просмотр не учтётся.
Профиль автора¶
{
"id": "…", "name": "Мария", "avatar_url": "https://…/avatar.jpg",
"effects": { "items": [ { "id": "…", "name": "Акварельная аллея", "…": "…" } ],
"total": 4, "limit": 20, "offset": 0 }
}
Публичный, без токена — это страница, которой автор приводит свою аудиторию, поэтому
она должна открываться по ссылке. Отдаётся только то, что автор про себя сам показал:
имя (может быть null, если он его не заполнил), аватар и его опубликованные эффекты.
Email, заработок, заявки и жалобы не отдаются никогда — это его личный кабинет.
404 — автора нет, он отключён или у него нет ни одного опубликованного эффекта
(пустой профиль подтверждал бы существование аккаунта, поэтому его нет).
Пожаловаться на эффект¶
curl -X POST "$BASE_URL/v1/effects/$ID/complaints" -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' -d '{"reason": "quality", "note": "лицо не похоже"}'
Ответ — 204. Коды причин: unsafe (запрещённый контент), my_face (на референсе или
результате моё лицо/фото), brand (чужой бренд, логотип, персонаж), quality (эффект не
работает как обещано), other. Незнакомый код не ошибка — он сохранится как other.
- Требуется токен: анонимные жалобы нечем дедуплицировать, поэтому не принимаются (
401). - Повторная жалоба того же человека на тот же эффект — тоже
204, но пока разбор не закрыт, считается один раз. Приложению не нужно помнить, на что пользователь уже жаловался. После того как модератор разобрал случай, тот же человек может пожаловаться снова — эффект мог измениться. 404— эффекта нет или он уже скрыт.- Что происходит дальше: когда на эффект пожаловалось достаточно разных людей, он понижается в выдаче каталога и уходит на разбор модератору. Понижение — не блокировка: по прямой ссылке эффект продолжает работать, пока человек не решит иначе.
- Жалоба принимается всегда и всегда доходит до модератора, но автоматическое понижение реагирует только на жалобы от давно зарегистрированных аккаунтов: иначе пачка свежих регистраций убирала бы из выдачи чужую работу. Для клиента разницы нет — ответ и поведение одинаковые.