미디어 업로드
사전 서명 R2 직접 업로드로 로컬 이미지, 비디오, 오디오를 AllToken에 전송합니다.
개요
비디오 생성과 이미지 편집 모두 미디어 자료를 사용합니다 — 참조 이미지, 참조 비디오와 오디오, 소스 이미지, 마스크. 이를 전송하는 방법에는 정확히 한 가지 규칙이 있습니다:
- 자료가 이미 공개 인터넷에 있는 경우(당신의 CDN / 객체 스토리지) — 업로드 부담 없이
http(s)URL을 직접 전달합니다. 비디오 참조 소재에만 해당합니다. - 자료가 로컬에 있는 경우 — 사전 서명 R2 직접 업로드로 먼저 업로드한 다음, 반환된
upload_id를 참조합니다. 이것이 로컬 파일의 유일한 경로이며, 이미지 편집(소스와 마스크)의 유일한 경로입니다.
경고
data: base64 인라인 경로는 비디오 참조 소재에서 2026-08-06에 제거되었습니다. 이는 이미지 쪽(레거시 multipart 업로드가 앞서 제거됨)과 일치합니다. 로컬 미디어는 이제 항상 사전 서명 업로드를 거칩니다.
엔드포인트는 POST https://api.alltoken.ai/v1/uploads/presign입니다(복수형 uploads). 계정 서비스의 /upload/presign과 혼동하지 마세요. 후자는 사이트 내 아바타용이며 이미지 MIME 타입만 받습니다.
경로 선택
| 상황 | 경로 | 이유 |
|---|---|---|
| 비디오 참조 소재가 이미 당신의 CDN / OSS에 있음 | url 전달 | 업로드가 전혀 없음 — 바이트가 우리 게이트웨이나 스토리지를 거치지 않음 |
| 비디오 참조 소재가 로컬에 있음(이미지 / 비디오 / 오디오) | 사전 서명 업로드 | 로컬 파일의 유일한 경로 |
이미지 편집(/images/edits, /images/variations)의 소스와 마스크 | 사전 서명 업로드 | 파일 크기와 무관하게 유일한 진입점 |
3단계 업로드
파일은 우리 게이트웨이를 거치지 않습니다. 일회성 직접 업로드 URL을 발급받아 원시 바이트를 객체 스토리지로 직접 PUT한 다음, 작업을 생성할 때 반환된 upload_id를 참조합니다. 게이트웨이는 수십 바이트짜리 ID만 운반합니다. 동일한 메커니즘이 비디오 참조 소재와 이미지 편집에 모두 쓰이며, 차이는 purpose와 그것을 참조하는 필드뿐입니다.
1단계 — 직접 업로드 URL 요청:
$POST https://api.alltoken.ai/v1/uploads/presign본문: purpose(허용되는 MIME와 크기를 결정 — 아래 참조), content_type(파일 MIME, 소문자), content_length(정확한 바이트 수), content_md5, checksum_sha256.
경고
두 다이제스트는 인코딩 방식이 다릅니다: content_md5는 base64, checksum_sha256은 hex입니다. 둘을 바꿔 넣으면 presign 단계에서 400으로 거부됩니다.
응답은 upload_id(upl_로 시작), upload_url(사전 서명된 PUT 대상), required_headers(2단계에서 그대로 재전송), expires_in(URL 유효 기간, 300초), claim_expires_at(upload_id 유효 기간, 2시간), max_content_length를 반환합니다.
2단계 — 원시 바이트를 upload_url로 PUT하며, required_headers의 모든 헤더를 그대로 재전송합니다. 본문은 원시 파일 바이트입니다 — base64도 multipart도 아닙니다:
| 1 | PUT <upload_url> |
| 2 | Content-Type: video/mp4 |
| 3 | Content-MD5: N0xUuLBP5C0uKJ2i0fA1kA== |
| 4 | |
| 5 | <원시 바이트> |
경고
필수 헤더를 빠뜨리거나 바꿔 쓰면 객체 스토리지가 403을 반환합니다 — 사전 서명 URL이 그 헤더들도 서명하기 때문입니다. Content-Encoding을 임의로 추가하지 말고, gzip을 쓰지 말고, multipart를 쓰지 마세요.
3단계 — upload_id를 참조하여 작업을 생성합니다. 비디오는 이를 content[].upload_id에 넣고, 이미지 편집은 image_upload_id / mask_upload_id 최상위 필드를 사용합니다. 게이트웨이는 바이트 수와 ETag를 검증하고, 객체를 해당 작업의 영구 영역으로 이동시키고, 업스트림에서 접근 가능한 주소를 제출합니다 — 이 모든 과정이 당신에게 투명하며, 당신은 202 + task_id만 받습니다.
기억해야 할 세 가지 규칙:
- 한 항목 안에서
upload_id와 URL 필드는 상호 배타적입니다 — 둘 다 채우면 400upload_url_conflict를 반환합니다. - presign과 작업 생성은 같은 API 키를 사용해야 합니다 — 소유권은 (사용자 + API 키 + purpose)로 확인되며, 키를 바꾸면 404
upload_not_found를 반환합니다. purpose는 실제 용도와 일치해야 하며, 그렇지 않으면 마찬가지로 404upload_not_found가 반환됩니다.
purpose 참조
각 purpose는 허용되는 MIME 타입과 파일당 크기 상한을 고정합니다.
purpose | 용도 | 허용 MIME | 파일당 상한 |
|---|---|---|---|
video_input_image | 비디오 참조 이미지 / 첫·마지막 프레임 | image/png, image/jpeg, image/webp, image/gif | 20 MiB |
video_input_video | 비디오 참조 비디오 | video/mp4, video/quicktime, video/webm | 100 MiB |
video_input_audio | 비디오 참조 오디오 | audio/wav, audio/mpeg, audio/mp4, audio/aac, audio/ogg | 20 MiB |
image_edit_source | 이미지 편집 소스 | image/png, image/jpeg, image/webp | 25 MiB |
image_edit_mask | 이미지 편집 마스크 | image/png, image/webp (jpeg 불가) | 25 MiB |
image_variation_source | 이미지 변형 소스 | image/png, image/jpeg, image/webp | 25 MiB |
마스크가 image/jpeg를 거부하는 이유는 알파 채널로 "어디를 바꿀지"를 표현해야 하는데, jpeg에는 투명도가 없기 때문입니다.
시간 창과 쿼터
| 시간 창 | 길이 | 만료 후 |
|---|---|---|
upload_url(PUT 대상) | 5분 | PUT가 403 반환 — 다시 presign |
upload_id(자료 클레임) | 2시간 | 작업 생성이 409 upload_expired 반환 |
따라서: URL을 받는 즉시 업로드하되, 그 이후에는 두 시간 동안 여유롭게 생성 요청을 조립하여 제출할 수 있습니다.
upload_id는 정확히 하나의 작업에만 바인딩됩니다. 재사용하면 409 upload_already_used를 반환합니다. 이는 의도된 설계입니다 — 하나의 자료, 하나의 작업, 하나의 청구. 작업 생성이 실패하면 임시 객체는 자동으로 회수됩니다.
| 쿼터 | 임계값 | 초과 시 |
|---|---|---|
동시에 미사용 상태인 upload_id | 100 | 429 quota_exceeded |
| UTC 하루당 업로드 총 바이트 | 2 GiB | 429 quota_exceeded |
정상적인 "업로드 후 사용" 페이스라면 이 한도에 닿을 일이 없습니다. 대규모 이미지 편집 배치에서는 일일 상한에 유의하세요: 25 MiB × 약 80개 파일이면 2 GiB에 도달합니다.
업로드 오류 코드
Presign 단계:
| HTTP | code | 의미 |
|---|---|---|
| 400 | invalid_purpose | purpose를 잘못 입력했거나 지원되지 않음 |
| 400 | invalid_content_type | MIME가 해당 purpose 화이트리스트에 없음(마스크를 jpeg로 보내면 여기에 해당) |
| 400 | invalid_content_md5 | MD5가 유효한 base64가 아님(hex를 보내지 마세요) |
| 400 | invalid_checksum | SHA-256이 64자 소문자 hex가 아님(base64를 보내지 마세요) |
| 413 | payload_too_large | content_length가 해당 purpose 상한을 초과 |
| 429 | quota_exceeded | pending 100개 초과, 또는 당일 2 GiB 초과 |
| 503 | storage_unavailable | 스토리지 일시 중단 — 재시도 |
바인딩 단계(작업 생성 시):
| HTTP | code | 의미 |
|---|---|---|
| 404 | upload_not_found | 알 수 없는 upload_id, 잘못된 API 키, 또는 purpose 불일치 |
| 409 | upload_already_used | 이미 작업에 바인딩됨 — 다시 presign |
| 409 | upload_expired | 2시간 초과 — 다시 presign |
| 400 | upload_object_missing | presign만 하고 PUT하지 않은 채 작업 생성 |
| 400 | upload_size_mismatch | 실제 바이트 수가 content_length와 다름 |
| 409 | upload_modified | 업로드 후 객체가 변경됨(ETag 불일치) |
| 400 | upload_url_conflict | 한 항목에 upload_id와 URL 필드를 모두 설정 |