Sustitución de personas
Un mismo kit, dos formas. Los modelos de una llamada sustituyen una cara o una cabeza en una sola petición; los de dos llamadas analizan antes el clip para que elijas a quién cambiar.
Algunos modelos no generan una escena nueva: sustituyen a una persona. Una cara que pasa de una foto a otra, una cabeza entera o una figura completa dentro de un clip de vídeo. El catálogo los agrupa bajo una misma etiqueta, toolkit: "subject-swap", y existen en dos formas: una llamada o dos llamadas con una elección en medio.
No tienen nada más de especial. Son tareas asíncronas corrientes, sus parámetros salen de su propio inputSchema y sus resultados llegan en output.assets[].
toolkit te dice que el modelo es uno de estos
toolkit es una etiqueta de clasificación en una entrada del catálogo y aparece tanto en la respuesta autenticada de GET /api/v1/models como en el catálogo público. Hoy solo tiene un valor, subject-swap, y el campo no está en los modelos que no pertenecen a ningún kit: comprueba el valor, nunca la presencia de la clave.
Es un campo meramente informativo. No decide qué clave puede llamar a un modelo, ni cómo se cobra la petición, ni qué acepta el modelo. Trata un valor desconocido igual que una entrada desconocida de badges: ignóralo.
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 línea es un endpoint que puedes llamar. La columna tasks es la que decide todo lo demás de esta página: una family que tiene un endpoint video-analyze es una family de dos llamadas, y su endpoint video-edit asociado no se puede llamar por su cuenta.
¿Cuál de las dos formas tengo delante?
| Una llamada | Dos llamadas | |
|---|---|---|
| Endpoints | Un único endpoint de edición | video-analyze y después video-edit, dentro de la misma family |
| A quién se sustituye | A quien encuentre el modelo. La petición no tiene ningún campo para nombrar a una persona | A los objetivos que elijas, por su posición |
| Qué envías | La imagen o el clip de destino y tu imagen de referencia, en una sola petición | En la primera llamada, el clip; en la segunda, las posiciones elegidas, cada una con sus propias imágenes de referencia |
| Qué pagas | Una tarea | Dos tareas, cobradas por separado |
| Cuándo conviene | Cuando hay un único sujeto evidente en el encuadre | Cuando hay varias personas en el encuadre, o cuando equivocarse de persona sale caro |
Ambas formas son async: true: creas una tarea y recoges el resultado por sondeo o por webhook.
Una llamada
Aquí no hay nada distinto de cualquier otro modelo de edición. Lee el inputSchema del endpoint, rellénalo y crea la tarea:
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, // una de las entradas de la consulta al catálogo de arriba
input: {
image_url: 'spicy://f/fil_01k3m8x9q2z4v7n5p6r8s0t1w3',
reference_image_url: 'spicy://f/fil_01k3m8xb2c3d4e5f6g7h8j9k0m',
},
}),
});Los nombres de los campos cambian de un endpoint a otro: unos aceptan prompt, seed o un formato de salida, y otros no aceptan más que las dos imágenes. El esquema es la lista buena; no traslades nombres de campos de un endpoint a otro. Una referencia puede ser una URL HTTPS pública, una imagen pequeña en Base64 dentro de la propia petición o una URI spicy:// ya subida; consulta Medios.
Dos llamadas
La primera llamada mira tu clip e informa de lo que ha encontrado, en forma de conjunto ordenado de fotogramas de vista previa. Tú decides cuáles sustituir. La segunda llamada hace el trabajo.
tu clip ──▶ video-analyze ──▶ output.assets[0], [1], [2] … (un fotograma por objetivo)
│
eliges las posiciones │ 0 y 2
▼
video-edit (analysis_task_id + swap_targets) ──▶ el clip terminado¿Por qué no en una sola llamada? Porque en una escena con cuatro personas, una llamada única tendría que adivinar a cuál te referías, y una mala suposición es un vídeo terminado y cobrado en el que se ha sustituido a la persona equivocada. Partir el trabajo hace que la elección sea explícita y revisable.
Analiza el clip
Envía el clip al endpoint video-analyze. Fija resolution de forma explícita si el endpoint tiene ese campo: la segunda llamada tendrá que ejecutarse con el mismo valor, así que elige aquí la resolución que quieres para el clip final. En estos endpoints el precio del análisis no depende de ella.
const analysis = await createTask({
model: analyze.model, // la entrada cuyos tasks incluyen "video-analyze"
input: {
video_url: 'spicy://f/fil_01k3m8x9q2z4v7n5p6r8s0t1w3',
resolution: '720p',
},
});
const analysisTaskId = analysis.taskId;Lee lo que ha encontrado
Espera la tarea como siempre. Un análisis con éxito devuelve un fotograma de vista previa por cada objetivo que ha seguido, en 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
}El orden de ese array es el contrato. El índice de cada elemento es el object_index que devuelves en la segunda llamada: el primer fotograma es 0, el segundo 1, y así sucesivamente. Enseña los fotogramas a quien tenga que elegir o emparéjalos tú mismo; en cualquier caso, devuelve posiciones de ese mismo array.
Un análisis que no encuentra nada que seguir no termina con éxito y un array vacío: acaba en failed con errorCode: "generation_failed", y la reserva se libera íntegra. Por eso un análisis en succeeded siempre tiene al menos un fotograma, y lo único que tu código tiene que resolver es cuál elegir.
Sustituye los objetivos elegidos
Envía el identificador de la tarea de análisis y una entrada de swap_targets por cada objetivo que quieras sustituir. Los objetivos que no menciones quedan intactos.
const swapped = await createTask({
model: edit.model, // la entrada "video-edit" de la misma family
input: {
analysis_task_id: analysisTaskId,
resolution: '720p', // el mismo valor con el que se ejecutó el análisis
swap_targets: [
{ object_index: 0, image_urls: ['spicy://f/fil_01k3m8xb2c3d4e5f6g7h8j9k0m'] },
],
},
});El resultado es un clip en output.assets[], igual que en cualquier otra tarea de vídeo.
Qué comprueba la segunda llamada antes de aceptar
Estas comprobaciones se hacen al crear la tarea, así que una petición rechazada no crea nada ni reserva nada.
| Regla | Por qué |
|---|---|
| El análisis tiene que ser de tu cuenta y haber terminado con éxito | Un análisis en cola o fallido no tiene nada a lo que apuntar |
| Usa la misma clave API en las dos llamadas | Un análisis solo se puede releer con la clave que lo creó, así que con cualquier otra ni siquiera ves los fotogramas entre los que eliges |
Cada object_index tiene que estar dentro de rango y aparecer una sola vez | Fuera de rango se sustituiría a la persona equivocada sin avisar; repetido, se sustituiría dos veces a la misma. Ambas cosas terminarían y se cobrarían con normalidad |
Los campos que afectan al precio, como resolution, tienen que coincidir con el análisis | Si se heredaran, dejarías de ver en qué tramo de precio se te está cobrando |
| El clip de origen tiene que ser el mismo | Si omites video_url se usa el clip analizado; si lo envías, tiene que ser idéntico |
| El análisis sirve durante 24 horas desde que termina | Pasado ese plazo desaparecen los datos de seguimiento que lo sostienen y, a menudo, también el clip de origen |
Fija `resolution` en las dos llamadas, o lee el valor por defecto con atención
Los campos omitidos toman el valor default de su esquema antes de comparar las dos peticiones. Un análisis con el valor por defecto y una sustitución con el valor por defecto sí coinciden, pero un análisis en el que fijaste un valor y una sustitución que dejaste en blanco no, y esa discrepancia se rechaza en lugar de corregirse en silencio. Enviar el valor de forma explícita en ambas llamadas elimina la duda.
Cuando se rechaza la segunda llamada
Todos estos casos son HTTP 400 con código de negocio code: 400. El campo msg es uno de estos:
msg | Qué ha pasado | Qué hacer |
|---|---|---|
The referenced analysis task was not found. | No existe esa tarea en esta cuenta, o analysis_task_id no es un identificador de tarea | Usa el taskId que devolvió tu propio análisis. Una tarea de otra cuenta se comunica igual, y es intencionado |
The referenced task belongs to a different model. | El identificador apunta a otra tarea, muchas veces a la propia tarea de sustitución o al análisis de otra family | Apúntalo a la tarea video-analyze de la misma family |
The referenced analysis did not complete successfully. | El análisis sigue en marcha, o terminó en failed / expired | Espéralo o vuelve a lanzarlo |
That analysis has no reusable result. Run the analysis step again. | El análisis terminó bien pero no dejó nada que la sustitución pueda aprovechar | Vuelve a lanzar el análisis sobre el mismo clip |
That analysis result has expired. Run the analysis step again. | Han pasado más de 24 horas | Vuelve a lanzar el análisis |
"resolution" must match the analysis step. | Las dos llamadas no coinciden en un campo que afecta al precio | Envía el valor del análisis, o lanza un análisis nuevo con el valor que quieras |
"video_url" must match the analysis step. | Has enviado otro clip | Envía la misma URI, o deja el campo fuera |
That analysis found N target(s); "object_index" is out of range. | Una posición va más allá de lo que devolvió el análisis | Las posiciones empiezan en cero y acaban en assets.length - 1 |
"object_index" is listed more than once. | Dos entradas nombran al mismo objetivo | Deja una sola entrada por objetivo, con todas sus imágenes de referencia en el image_urls de esa entrada |
Cuánto cuestan las dos llamadas
El análisis y la sustitución son dos tareas distintas con dos precios distintos, cada una con su propia reserva y su propia liquidación. Una segunda llamada rechazada no cuesta nada; un análisis correcto se cobra aunque después decidas no sustituir nada.
- El análisis suele cobrarse
per_request: un cargo por llamada, dure lo que dure el clip y encuentre los objetivos que encuentre. - La sustitución suele cobrarse
per_second, en bloques enteros, y la duración sale del clip y no de un campo que rellenes tú. Por eso el importe que se te indica antes de enviar corresponde al clip más largo que acepta el endpoint: el importe indicado es el importe reservado, el cargo final se calcula sobre la duración redondeada y nunca supera la reserva, y la diferencia vuelve a tu saldo cuando la tarea se liquida. Ver aquí una reserva mayor que el cargo final es lo esperado. Lee Facturación antes de hacer cuentas.
Lee los dos precios del catálogo en lugar de darlos por supuestos y pide una cotización de cada llamada si el importe exacto importa.
Siguientes pasos
Catálogo de modelos y esquemas
Lee toolkit, tasks, precios y el esquema de entrada de cada endpoint
Tareas asíncronas
Crea cualquier tarea, consúltala y lee su salida
Medios
Sube un clip o una foto de referencia y obtén una URI spicy://
Facturación
Reservas, liquidación y cómo se cobra la duración por bloques
Guía: generar y editar imágenes
Cómo se comportan los endpoints de imagen como familia — las dos formas de petición, las tres maneras de escribir el tamaño de salida, lo que una imagen de referencia añade a la factura, los pesos de adaptación y cómo leer un rechazo.
Integración en producción
Una lista de comprobación para lanzar una integración capaz de recuperarse de los fallos, con el modelo adecuado, claves y saldo protegidos, costos confirmados, seguimiento duradero de las tareas, callbacks fiables y las pruebas que hay que hacer antes de salir a producción.

