spicyapiDocumentación
Guías
Contenido principal

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 llamadaDos llamadas
EndpointsUn único endpoint de ediciónvideo-analyze y después video-edit, dentro de la misma family
A quién se sustituyeA quien encuentre el modelo. La petición no tiene ningún campo para nombrar a una personaA los objetivos que elijas, por su posición
Qué envíasLa imagen o el clip de destino y tu imagen de referencia, en una sola peticiónEn la primera llamada, el clip; en la segunda, las posiciones elegidas, cada una con sus propias imágenes de referencia
Qué pagasUna tareaDos tareas, cobradas por separado
Cuándo convieneCuando hay un único sujeto evidente en el encuadreCuando 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.

ReglaPor qué
El análisis tiene que ser de tu cuenta y haber terminado con éxitoUn análisis en cola o fallido no tiene nada a lo que apuntar
Usa la misma clave API en las dos llamadasUn 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 vezFuera 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álisisSi se heredaran, dejarías de ver en qué tramo de precio se te está cobrando
El clip de origen tiene que ser el mismoSi 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 terminaPasado 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:

msgQué ha pasadoQué hacer
The referenced analysis task was not found.No existe esa tarea en esta cuenta, o analysis_task_id no es un identificador de tareaUsa 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 familyApú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 / expiredEspé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 aprovecharVuelve 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 horasVuelve a lanzar el análisis
"resolution" must match the analysis step.Las dos llamadas no coinciden en un campo que afecta al precioEnví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 clipEnví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álisisLas posiciones empiezan en cero y acaban en assets.length - 1
"object_index" is listed more than once.Dos entradas nombran al mismo objetivoDeja 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

En esta página