Замена человека в кадре
Один набор инструментов, две формы. Модели с одним вызовом меняют лицо или голову за один запрос; модели с двумя вызовами сначала разбирают клип, чтобы вы выбрали, кого менять.
Некоторые модели не создают новую сцену, а заменяют человека: переносят лицо с одной фотографии на другую, меняют голову целиком или подставляют другую фигуру в видеоклипе. В каталоге они собраны под одной меткой — toolkit: "subject-swap" — и встречаются в двух формах: один вызов или два вызова с выбором посередине.
Больше в них ничего особенного нет. Это обычные асинхронные задачи, параметры берутся из собственного inputSchema модели, а результат приходит в output.assets[].
toolkit показывает, что модель из этого набора
toolkit — это классифицирующая метка на элементе каталога; она есть и в ответе GET /api/v1/models с аутентификацией, и в публичном каталоге. Сегодня у неё единственное значение subject-swap, а у моделей вне наборов поле отсутствует целиком, поэтому проверяйте значение, а не наличие ключа.
Поле носит справочный характер. Оно не определяет, каким ключом можно вызвать модель, как считается стоимость запроса и что модель принимает на вход. Незнакомое значение стоит воспринимать так же, как незнакомую запись в badges, — просто пропустить.
curl -s "https://api.spicyapi.ai/api/v1/models?modality=video" \
-H "Authorization: Bearer $SPICY_API_KEY" \
| jq -r '.data.items[]
| select(.toolkit == "subject-swap" and .enabled and .available)
| [.model, (.tasks | join(",")), .family] | @tsv'Каждая строка — один вызываемый эндпоинт. Всё остальное на этой странице определяет колонка tasks: family с эндпоинтом video-analyze — это family на два вызова, и парный к нему video-edit сам по себе не вызывается.
Какая форма передо мной
| Один вызов | Два вызова | |
|---|---|---|
| Эндпоинты | Один эндпоинт редактирования | video-analyze, затем video-edit в том же family |
| Кого заменяют | Того, кого найдёт модель. В запросе нет поля, которым можно указать человека | Выбранные вами цели — по их позиции |
| Что вы отправляете | Исходное изображение или клип и ваш референс, в одном запросе | В первом вызове — клип; во втором — выбранные позиции, каждая со своими референсами |
| Сколько платите | Одна задача | Две задачи, стоимость считается отдельно |
| Когда подходит | В кадре один очевидный объект | В кадре несколько человек либо цена ошибки высока |
Обе формы работают в режиме async: true: вы создаёте задачу, а результат забираете опросом или через webhook.
Один вызов
Здесь нет ничего, что отличало бы такую модель от любой другой модели редактирования. Прочитайте inputSchema эндпоинта, заполните поля, создайте задачу:
const created = await fetch('https://api.spicyapi.ai/api/v1/jobs/createTask', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.SPICY_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': 'order-8814-swap-1',
},
body: JSON.stringify({
model: swap.model, // один из элементов запроса к каталогу выше
input: {
image_url: 'spicy://f/fil_01k3m8x9q2z4v7n5p6r8s0t1w3',
reference_image_url: 'spicy://f/fil_01k3m8xb2c3d4e5f6g7h8j9k0m',
},
}),
});Имена полей у эндпоинтов разные: одни принимают prompt, seed или формат вывода, другие — только две картинки. Список задаёт схема; не переносите имена полей с одного эндпоинта на другой. Референсом может быть публичный HTTPS-адрес, небольшое изображение в Base64 прямо в запросе или загруженный spicy:// URI — см. Медиа.
Два вызова
Первый вызов разбирает клип и сообщает, что он там нашёл, — упорядоченным набором кадров предпросмотра. Что из этого заменять, решаете вы. Саму работу делает второй вызов.
ваш клип ──▶ video-analyze ──▶ output.assets[0], [1], [2] … (по кадру на каждую цель)
│
вы выбираете позиции │ 0 и 2
▼
video-edit (analysis_task_id + swap_targets) ──▶ готовый клипПочему не одним вызовом? Потому что в сцене с четырьмя людьми один вызов вынужден был бы угадывать, кого вы имели в виду, а неверная догадка — это готовое оплаченное видео, в котором заменили не того человека. Разделение на два шага делает выбор явным и проверяемым.
Разберите клип
Отправьте клип на эндпоинт video-analyze. Если у эндпоинта есть поле resolution, задайте его явно: второй вызов должен выполняться с тем же значением, поэтому выбирайте здесь то разрешение, которое нужно готовому клипу. На этих эндпоинтах стоимость разбора от него не зависит.
const analysis = await createTask({
model: analyze.model, // элемент, у которого в tasks есть "video-analyze"
input: {
video_url: 'spicy://f/fil_01k3m8x9q2z4v7n5p6r8s0t1w3',
resolution: '720p',
},
});
const analysisTaskId = analysis.taskId;Прочитайте, что он нашёл
Дождитесь задачи обычным способом. Успешный разбор возвращает по одному кадру предпросмотра на каждую отслеженную цель в output.assets[]:
{
"state": "succeeded",
"output": {
"assets": [
{ "url": "https://…", "expiresAt": "2026-09-20T09:32:11Z", "mime": "image/jpeg" },
{ "url": "https://…", "expiresAt": "2026-09-20T09:32:11Z", "mime": "image/jpeg" }
]
},
"settled": true
}Порядок этого массива и есть контракт. Индекс элемента — это object_index, который вы отправляете обратно во втором вызове: первый кадр — 0, второй — 1 и так далее. Покажите кадры тому, кто выбирает, или сопоставьте их сами — в любом случае отправляйте назад позиции именно из этого массива.
Разбор, который не нашёл ничего для отслеживания, не завершается успехом с пустым массивом: он заканчивается как failed с errorCode: "generation_failed", а резерв освобождается полностью. Поэтому у разбора со статусом succeeded всегда есть хотя бы один кадр, и вашему коду остаётся единственное — выбрать из них.
Замените выбранные цели
Отправьте идентификатор задачи разбора и по одному элементу swap_targets на каждую цель, которую нужно заменить. Цели, которые вы не перечислили, останутся нетронутыми.
const swapped = await createTask({
model: edit.model, // элемент "video-edit" того же family
input: {
analysis_task_id: analysisTaskId,
resolution: '720p', // то же значение, с которым выполнялся разбор
swap_targets: [
{ object_index: 0, image_urls: ['spicy://f/fil_01k3m8xb2c3d4e5f6g7h8j9k0m'] },
],
},
});Результат — один клип в output.assets[], как у любой другой видеозадачи.
Что второй вызов проверяет перед приёмом
Эти проверки выполняются при создании, поэтому отклонённый запрос ничего не создаёт и ничего не резервирует.
| Правило | Зачем |
|---|---|
| Разбор должен принадлежать вашему аккаунту и быть успешным | У задачи в очереди или у неудавшейся ссылаться не на что |
| Оба вызова делайте одним и тем же API-ключом | Разбор читается только тем ключом, который его создал, иначе вы вообще не увидите кадры, из которых выбираете |
Каждый object_index должен попадать в диапазон и встречаться один раз | Выход за диапазон молча заменил бы не того человека, дубликат заменил бы одного и того же дважды. И то и другое завершилось бы и было бы оплачено как обычно |
Поля, влияющие на цену, например resolution, должны совпадать с разбором | Если наследовать их, станет невидно, по какому тарифному уровню вас считают |
| Исходный клип должен быть тем же самым | Опустите video_url — возьмётся разобранный клип; передадите — он должен совпадать полностью |
| Разбор пригоден 24 часа после завершения | Позже данные отслеживания за ним пропадают, а нередко и сам исходный клип |
Задавайте `resolution` в обоих вызовах или внимательно читайте значение по умолчанию
Опущенные поля получают значение default из схемы ещё до сравнения двух запросов. Поэтому разбор со значением по умолчанию и замена со значением по умолчанию совпадут — а вот разбор, где вы задали значение, и замена, где поле оставлено пустым, не совпадут, и расхождение будет отклонено, а не исправлено молча. Если передавать значение явно в обоих вызовах, вопрос снимается.
Когда второй вызов отклонён
Все эти случаи — HTTP 400 с бизнес-кодом code: 400. Поле msg принимает одно из значений:
msg | Что произошло | Что делать |
|---|---|---|
The referenced analysis task was not found. | В этом аккаунте такой задачи нет либо analysis_task_id вообще не идентификатор задачи | Возьмите taskId, который вернул ваш собственный разбор. Задача из чужого аккаунта намеренно отдаётся так же |
The referenced task belongs to a different model. | Идентификатор указывает на другую задачу — часто на саму задачу замены или на разбор другого family | Укажите задачу video-analyze из того же family |
The referenced analysis did not complete successfully. | Разбор ещё выполняется либо завершился как failed / expired | Дождитесь его или запустите заново |
That analysis has no reusable result. Run the analysis step again. | Разбор прошёл успешно, но не оставил ничего, что пригодится замене | Запустите разбор по тому же клипу ещё раз |
That analysis result has expired. Run the analysis step again. | Прошло больше 24 часов | Запустите разбор заново |
"resolution" must match the analysis step. | Два вызова расходятся в поле, влияющем на цену | Передайте значение из разбора либо запустите новый разбор с нужным значением |
"video_url" must match the analysis step. | Вы отправили другой клип | Передайте тот же URI либо опустите поле |
That analysis found N target(s); "object_index" is out of range. | Позиция выходит за пределы того, что вернул разбор | Позиции нумеруются с нуля и заканчиваются на assets.length - 1 |
"object_index" is listed more than once. | Два элемента указывают на одну и ту же цель | Оставьте по одному элементу на цель, а все её референсы соберите в image_urls этого элемента |
Сколько стоят два вызова
Разбор и замена — это две отдельные задачи с двумя отдельными ценами, каждая со своим резервом и своим списанием. Отклонённый второй вызов не стоит ничего; успешный разбор оплачивается, даже если вы потом откажетесь что-либо заменять.
- Разбор обычно тарифицируется
per_request: одно списание за вызов, независимо от длины клипа и количества найденных целей. - Замена обычно тарифицируется
per_secondи считается целыми блоками, а длительность берётся из клипа, а не из поля, которое задаёте вы. Поэтому сумма, названная до отправки, рассчитана на самый длинный клип, который принимает эндпоинт: названная сумма и есть зарезервированная, итоговое списание считается по округлённой длительности и никогда не превышает резерв, а разница возвращается на баланс при закрытии задачи. Резерв, превышающий итоговое списание, здесь ожидаем. Прежде чем планировать бюджет, прочитайте Оплату.
Обе цены читайте из каталога, а не предполагайте, и, если важна точная сумма, запрашивайте расчёт стоимости для каждого вызова.
Дальше
Руководство: генерация и редактирование изображений
Как ведут себя эндпоинты изображений как класс — две формы запроса, три способа записать размер вывода, что референсная картинка добавляет к счёту, веса адаптеров и как читать отказ.
Интеграция в рабочей среде
Чек-лист перед запуском для интеграции, которая умеет восстанавливаться, — подходящая модель, защищённые ключи и баланс, подтверждённая стоимость, надёжный учёт задач, устойчивые обратные вызовы и тесты, которые нужно пройти до выхода в работу.

