Substituição de pessoas
Um kit, dois formatos. Modelos de uma chamada trocam um rosto ou uma cabeça em uma única requisição; modelos de duas chamadas analisam o clipe antes para que você escolha quem muda.
Alguns modelos não geram uma cena nova: eles substituem uma pessoa. Um rosto levado de uma foto para outra, uma cabeça inteira ou uma figura completa dentro de um clipe de vídeo. O catálogo reúne todos eles sob um mesmo rótulo, toolkit: "subject-swap", e eles existem em dois formatos — uma chamada ou duas chamadas com uma escolha no meio.
Fora isso, não têm nada de especial. São tarefas assíncronas comuns, seus parâmetros vêm do próprio inputSchema e seus resultados chegam em output.assets[].
toolkit avisa que o modelo é um desses
toolkit é um rótulo de classificação em um item do catálogo, presente tanto na resposta autenticada de GET /api/v1/models quanto no catálogo público. Hoje ele tem um único valor, subject-swap, e o campo não aparece nos modelos que não pertencem a nenhum kit — teste o valor, nunca a presença da chave.
O campo é apenas informativo. Ele não decide qual chave pode chamar um modelo, como a requisição é cobrada nem o que o modelo aceita. Trate um valor desconhecido como trataria um item desconhecido de badges: ignore.
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'Cada linha é um endpoint que pode ser chamado. Quem define o resto desta página é a coluna tasks: uma family que tem um endpoint video-analyze é uma family de duas chamadas, e o endpoint video-edit correspondente não pode ser chamado sozinho.
Qual dos dois formatos estou vendo?
| Uma chamada | Duas chamadas | |
|---|---|---|
| Endpoints | Um único endpoint de edição | video-analyze e depois video-edit, na mesma family |
| Quem é substituído | Quem o modelo encontrar. A requisição não tem campo algum para nomear uma pessoa | Os alvos que você escolheu, pela posição |
| O que você envia | A imagem ou o clipe de destino mais a sua imagem de referência, em uma requisição | O clipe na primeira chamada; as posições escolhidas, cada uma com suas imagens de referência, na segunda |
| O que você paga | Uma tarefa | Duas tarefas, cobradas separadamente |
| Indicado para | Um único sujeito evidente no enquadramento | Várias pessoas no enquadramento, ou quando trocar a pessoa errada sai caro |
Os dois formatos são async: true: você cria uma tarefa e depois busca o resultado por polling ou webhook.
Uma chamada
Aqui não há nada diferente de qualquer outro modelo de edição. Leia o inputSchema do endpoint, preencha e crie a tarefa:
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, // um dos itens da consulta ao catálogo acima
input: {
image_url: 'spicy://f/fil_01k3m8x9q2z4v7n5p6r8s0t1w3',
reference_image_url: 'spicy://f/fil_01k3m8xb2c3d4e5f6g7h8j9k0m',
},
}),
});Os nomes dos campos mudam de um endpoint para outro: alguns aceitam prompt, seed ou um formato de saída, outros não aceitam nada além das duas imagens. A lista válida é o schema; não leve nomes de campo de um endpoint para outro. Uma referência pode ser uma URL HTTPS pública, uma imagem pequena em Base64 dentro da própria requisição ou uma URI spicy:// já enviada; veja Mídia.
Duas chamadas
A primeira chamada olha o seu clipe e informa o que encontrou, como um conjunto ordenado de quadros de pré-visualização. Você decide quais serão substituídos. A segunda chamada faz o trabalho.
seu clipe ──▶ video-analyze ──▶ output.assets[0], [1], [2] … (um quadro por alvo)
│
você escolhe as posições │ 0 e 2
▼
video-edit (analysis_task_id + swap_targets) ──▶ o clipe prontoPor que não fazer tudo em uma chamada? Porque, em uma cena com quatro pessoas, uma chamada única teria de adivinhar a quem você se referia — e um palpite errado é um vídeo pronto e cobrado com a pessoa errada substituída. Dividir o trabalho torna a escolha explícita e conferível.
Analise o clipe
Envie o clipe ao endpoint video-analyze. Defina resolution explicitamente se o endpoint tiver esse campo: a segunda chamada precisa rodar no mesmo valor, então escolha aqui a resolução que o clipe final deve ter. Nesses endpoints o preço da análise não depende dela.
const analysis = await createTask({
model: analyze.model, // o item cujos tasks incluem "video-analyze"
input: {
video_url: 'spicy://f/fil_01k3m8x9q2z4v7n5p6r8s0t1w3',
resolution: '720p',
},
});
const analysisTaskId = analysis.taskId;Leia o que ela encontrou
Aguarde a tarefa do jeito de sempre. Uma análise bem-sucedida devolve um quadro de pré-visualização para cada alvo que ela rastreou, em 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
}A ordem desse array é o contrato. O índice de cada item é o object_index que você devolve na segunda chamada: o primeiro quadro é 0, o segundo é 1, e assim por diante. Mostre os quadros para quem vai escolher, ou faça a correspondência você mesmo; de um jeito ou de outro, devolva posições desse mesmo array.
Uma análise que não encontra nada para rastrear não termina com sucesso e array vazio: ela termina em failed com errorCode: "generation_failed", e a reserva é liberada integralmente. Ou seja, uma análise em succeeded sempre tem pelo menos um quadro, e a única coisa que o seu código precisa tratar é a escolha entre eles.
Substitua os alvos escolhidos
Envie o ID da tarefa de análise e um item de swap_targets para cada alvo que deve ser substituído. Os alvos que você deixar de fora permanecem intactos.
const swapped = await createTask({
model: edit.model, // o item "video-edit" da mesma family
input: {
analysis_task_id: analysisTaskId,
resolution: '720p', // o mesmo valor em que a análise rodou
swap_targets: [
{ object_index: 0, image_urls: ['spicy://f/fil_01k3m8xb2c3d4e5f6g7h8j9k0m'] },
],
},
});O resultado é um clipe em output.assets[], como em qualquer outra tarefa de vídeo.
O que a segunda chamada confere antes de aceitar
Essas conferências acontecem na criação, então uma requisição recusada não cria nada e não reserva nada.
| Regra | Por quê |
|---|---|
| A análise precisa ser da sua conta e ter terminado com sucesso | Uma análise na fila ou que falhou não tem para onde apontar |
| Use a mesma chave de API nas duas chamadas | Uma análise só pode ser relida com a chave que a criou; com outra, você nem enxerga os quadros entre os quais está escolhendo |
Todo object_index precisa estar dentro do intervalo e aparecer uma única vez | Fora do intervalo, a pessoa errada seria substituída sem aviso; repetido, a mesma pessoa seria substituída duas vezes. Os dois casos terminariam e seriam cobrados normalmente |
Campos que afetam o preço, como resolution, precisam coincidir com a análise | Se fossem herdados, você deixaria de ver em qual faixa de preço está sendo cobrado |
| O clipe de origem precisa ser o mesmo | Deixe video_url de fora e o clipe analisado é usado; se enviar, ele precisa ser idêntico |
| A análise pode ser usada por 24 horas depois de terminar | Passado esse prazo, os dados de rastreamento por trás dela somem — e, muitas vezes, o clipe de origem também |
Defina `resolution` nas duas chamadas, ou leia o valor padrão com cuidado
Campos omitidos assumem o valor default do schema antes de as duas requisições serem comparadas. Uma análise no padrão e uma substituição no padrão realmente coincidem — mas uma análise em que você definiu um valor e uma substituição deixada em branco não, e a divergência é recusada em vez de corrigida em silêncio. Enviar o valor explicitamente nas duas chamadas elimina a dúvida.
Quando a segunda chamada é recusada
Todos esses casos são HTTP 400 com código de negócio code: 400. O msg é um destes:
msg | O que aconteceu | O que fazer |
|---|---|---|
The referenced analysis task was not found. | Não existe essa tarefa nesta conta, ou analysis_task_id não é um ID de tarefa | Use o taskId devolvido pela sua própria análise. Uma tarefa de outra conta é informada do mesmo jeito, de propósito |
The referenced task belongs to a different model. | O ID aponta para outra tarefa — muitas vezes a própria tarefa de substituição, ou a análise de outra family | Aponte para a tarefa video-analyze da mesma family |
The referenced analysis did not complete successfully. | A análise ainda está rodando, ou terminou em failed / expired | Espere por ela ou rode de novo |
That analysis has no reusable result. Run the analysis step again. | A análise terminou bem, mas não deixou nada que a substituição possa usar | Rode a análise novamente no mesmo clipe |
That analysis result has expired. Run the analysis step again. | Passaram-se mais de 24 horas | Rode a análise novamente |
"resolution" must match the analysis step. | As duas chamadas divergem em um campo que afeta o preço | Envie o valor da análise, ou rode uma análise nova com o valor que você quer |
"video_url" must match the analysis step. | Você enviou outro clipe | Envie a mesma URI, ou deixe o campo de fora |
That analysis found N target(s); "object_index" is out of range. | Uma posição vai além do que a análise devolveu | As posições começam em zero e param em assets.length - 1 |
"object_index" is listed more than once. | Dois itens nomeiam o mesmo alvo | Dê um item por alvo, com todas as imagens de referência dele no image_urls desse item |
Quanto custam as duas chamadas
A análise e a substituição são duas tarefas separadas com dois preços separados, cada uma reservada e liquidada por conta própria. Uma segunda chamada recusada não custa nada; uma análise bem-sucedida é cobrada mesmo que você depois desista de substituir qualquer coisa.
- A análise costuma ser cobrada
per_request: uma cobrança por chamada, não importa a duração do clipe nem quantos alvos ela encontre. - A substituição costuma ser cobrada
per_second, em blocos inteiros, e a duração vem do clipe e não de um campo que você preenche. Por isso o valor informado antes do envio corresponde ao clipe mais longo que o endpoint aceita: o valor informado é o valor reservado, a cobrança final sai da duração arredondada e nunca ultrapassa a reserva, e a diferença volta para o seu saldo quando a tarefa é liquidada. Ver aqui uma reserva maior que a cobrança final é o comportamento esperado. Leia Cobrança antes de fazer as contas.
Leia os dois preços no catálogo em vez de supô-los e peça uma cotação de cada chamada se o valor exato importar.
Próximos passos
Guia: gerar e editar imagens
Como os endpoints de imagem se comportam como categoria — as duas formas de requisição, as três maneiras de escrever o tamanho da saída, o que uma imagem de referência acrescenta à conta, pesos de adaptação e como ler uma recusa.
Integração em produção
Um checklist de lançamento para uma integração que se recupera de falhas — o modelo certo, chaves e saldo protegidos, custos confirmados, acompanhamento durável das tarefas, callbacks confiáveis e os testes a rodar antes de entrar no ar.

