인물 교체
하나의 툴킷, 두 가지 형태. 한 번의 호출로 얼굴이나 머리를 바꾸는 모델과, 먼저 영상을 분석해 누구를 바꿀지 고르게 하는 두 번 호출 모델이 있습니다.
새 장면을 만들어 내는 대신 화면 속 인물을 바꾸는 모델이 있습니다. 한 사진의 얼굴을 다른 사진으로 옮기거나, 머리 전체를 갈아 끼우거나, 영상 클립 속 인물을 통째로 교체합니다. 카탈로그는 이런 모델을 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 열입니다. **video-analyze 엔드포인트가 있는 family는 두 번 호출하는 family**이며, 짝이 되는 video-edit 엔드포인트는 단독으로 호출할 수 없습니다.
지금 보고 있는 것은 어느 쪽인가
| 한 번 호출 | 두 번 호출 | |
|---|---|---|
| 엔드포인트 | 편집용 엔드포인트 하나 | 같은 family의 video-analyze, 그다음 video-edit |
| 누가 바뀌는가 | 모델이 찾아낸 대상. 인물을 지정하는 필드는 요청에 없습니다 | 위치로 직접 고른 대상 |
| 무엇을 보내는가 | 대상 이미지나 클립과 참조 이미지를 한 번의 요청에 | 첫 호출에는 클립, 두 번째 호출에는 고른 위치와 위치별 참조 이미지 |
| 비용 | 작업 한 건 | 작업 두 건, 각각 따로 과금 |
| 적합한 상황 | 화면에 대상이 한 명뿐일 때 | 화면에 여러 사람이 있을 때, 엉뚱한 사람을 바꾸면 손해가 클 때 |
두 형태 모두 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 URL, 작은 이미지의 인라인 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, 이런 식입니다. 고를 사람에게 스틸을 보여 주든 직접 대조하든 상관없지만, 돌려보내는 위치는 반드시 이 같은 배열에서 가져와야 합니다.
추적할 대상을 찾지 못한 분석은 빈 배열로 성공하지 않습니다. errorCode: "generation_failed"와 함께 failed로 끝나고, 예약된 금액이 전액 해제됩니다. 즉 succeeded인 분석에는 스틸이 최소 한 장 있으며, 코드가 다뤄야 할 것은 그중 무엇을 고르느냐뿐입니다.
고른 대상 교체하기
분석 작업 ID와 함께, 바꾸려는 대상마다 swap_targets 항목을 하나씩 보냅니다. 넣지 않은 대상은 그대로 남습니다.
const swapped = await createTask({
model: edit.model, // 같은 family에 있는 "video-edit" 항목
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가 작업 ID가 아닙니다 | 내 분석이 돌려준 taskId를 쓰세요. 다른 계정의 작업도 의도적으로 똑같이 응답합니다 |
The referenced task belongs to a different model. | ID가 다른 작업을 가리킵니다. 대개 교체 작업 자신이거나 다른 family의 분석입니다 | **같은 family**의 video-analyze 작업을 가리키게 하세요 |
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. | 분석이 돌려준 개수를 넘는 위치를 지정했습니다 | 위치는 0부터 시작해 assets.length - 1에서 끝납니다 |
"object_index" is listed more than once. | 두 항목이 같은 대상을 가리킵니다 | 대상 하나에 항목 하나만 두고, 그 대상의 참조 이미지는 모두 그 항목의 image_urls에 모으세요 |
두 호출의 비용
분석과 교체는 가격이 각각 따로 붙는 두 개의 작업이며, 예약과 정산도 따로 이루어집니다. 두 번째 호출이 거절되면 비용은 들지 않지만, 분석이 성공한 뒤 교체를 그만두더라도 분석 비용은 청구됩니다.
- 분석은 보통
per_request입니다. 클립 길이나 찾아낸 대상 수와 무관하게 호출 한 번에 한 건입니다. - 교체는 보통
per_second이며 정해진 묶음(블록) 단위로 셉니다. 길이는 여러분이 지정하는 필드가 아니라 클립 자체에서 나오므로, 미리 안내되는 금액은 그 엔드포인트가 받아들이는 가장 긴 클립을 기준으로 한 값입니다. 안내된 금액이 그대로 예약되고, 최종 청구액은 올림한 길이로 계산되어 예약 금액을 넘지 않으며, 차액은 작업이 정산될 때 잔액으로 돌아옵니다. 여기서 예약 금액이 최종 청구액보다 크게 보이는 것은 정상입니다. 예산을 잡기 전에 과금을 먼저 읽어 보세요.
두 가격 모두 짐작하지 말고 카탈로그에서 읽고, 정확한 금액이 중요하다면 호출마다 견적을 받아 두세요.

