사용자가 가상의 성인 컴패니언 캐릭터 로완에게 "오늘 밤 천문대를 보여 줘"라고 말합니다. 채팅 모델은 캐릭터로서 답할 수 있지만, 그림을 만들고 장면의 일관성을 지키고 실제 에셋이 생기면 첨부하는 일은 애플리케이션의 몫입니다. 이미지와 영상 작업은 따로 실행하고, 그동안에도 채팅은 계속 응답하도록 하세요.
사용자가 두 번의 구매를 직접 결정하게 하세요.
- 스틸 승인: 무료 장면 카드를 보여 주고, 이미지 견적을 받고, 사용자가 가격을 수락한 뒤에만 만듭니다.
- 클립 승인: 완성된 스틸을 보여 주고, 그 애니메이션 견적을 받고, 사용자가 그 이미지와 가격을 수락한 뒤에만 영상을 만듭니다.
요청은 제출하기 전에 저장하고, 단계마다 별도의 키를 쓰고, 직접 저장한 사본으로 전달하세요. 도구 호출은 장면 카드를 시작할 뿐, 구매를 승인하거나 존재하지 않는 첨부를 알리지 않습니다.
구체적인 예산으로, 1k Qwen 편집($0.036)과 5초 720p Seedance 2.5 클립을 합하면 $1.4225입니다. 추가 참조 이미지 요금, 채팅 토큰, 재시도는 포함하지 않은 금액입니다. 사용자에게 가격을 보여 주기 전에 견적(quote)을 요청하세요.
전체 흐름은 다음과 같습니다.
1. Chat model calls request_scene_image(scene, details)
2. Server validates owner and scene -> free draft card -> tool result back to the chat
3. Gate 1: user approves the still's price -> Qwen edit task (own idempotency key)
4. Callback or poll -> still ready -> copy it to your own storage
5. Gate 2: user accepts the still -> quote the clip -> user approves its price
6. Seedance task (own key) -> callback or poll -> ready_for_review
7. Review the whole clip -> copy to your storage -> delivered채팅 요청을 앱이 실제로 만들 수 있는 이미지로 바꾸기
사용자가 "초록색 코트 입은 사진 하나 더 보내 줘"라고 쓰는 동안, 남색 코트 영상이 이미 생성 중일 수 있습니다. 채팅 답변과 미디어 요청이 하나의 바뀔 수 있는 설명을 공유해서는 안 됩니다. 앱은 어떤 가상의 성인인지, 어떤 코트인지, 어떤 배경인지, 사용자가 스틸을 원하는지 클립을 원하는지 알아야 합니다. 그렇지 않으면 "하나 더"가 실수로 새 얼굴을 만들거나, 오래된 장면을 다시 쓰거나, 사진을 요청했는데 영상을 구매할 수 있습니다.
영구적인 캐릭터 데이터는 작게 유지하세요. 승인된 초상, 성인이라는 사실, 안정적인 외모 메모, 사용자가 고른 캐릭터입니다. 현재 장면은 따로 두세요. 코트, 방, 카메라 시점, 요청한 동작입니다. 대화에서 장면을 제안할 수는 있지만, 저장된 캐릭터와 승인된 원본 에셋은 애플리케이션이 결정합니다. 채팅 모델이 참조 URL이나 작업 ID를 지어내거나 이미지가 존재한다고 말하게 두지 마세요.
천문대 예시에서는 견적을 받기 전에 요청을 명시적인 장면 설명으로 바꿉니다. 로완, 승인된 초상, 남색 코트, 천문대 발코니, 미디엄 샷, 스틸 한 장입니다. 나중에 "움직이게 해 줘"라는 요청이 오면 승인된 천문대 스틸과 작은 고개 돌림을 사용하세요. 대화 전체를 이미지 모델에 보내지 마세요. 다른 캐릭터나 이전 의상에 관한 무관한 설명이 현재 요청과 충돌할 수 있습니다.
이미지가 생성되는 동안 인터페이스에 장면 요약을 간단히 보여 줄 수 있습니다. 사용자가 초록색을 남색으로 바꾸면 새 리비전으로 만들고, 대기 중이던 이전 결과에 그에 맞는 표시를 붙이세요. 완성된 이전 리비전은 사용자의 비공개 기록에 남겨도 되지만, 최신 요청에 대한 답으로 조용히 나타나서는 안 됩니다.
NSFW 컴패니언 제품이라면 이미지와 영상의 적합성을 따로 확인하세요. 이미지 작업이 성인 콘텐츠를 받아 준다고 해서 다음 영상 모델이 같은 장면을 받아 준다는 뜻은 아닙니다. 단계마다 선택한 모델의 페이지를 확인하고, 선택한 모델이 지원하지 않는 성인 클립을 약속하는 대신 사용할 수 없는 작업은 솔직하게 알려 주세요.
먼저 Playground에서 시험해 보기
Qwen Image 2.1 edit Playground를 엽니다. Images에서 Add a file을 선택해 승인된 캐릭터 초상을 업로드하고, Prompt에 새 장면을 설명하고(아래 프롬프트를 쓸 수 있습니다), Resolution을 1k, Aspect ratio를 16:9로 설정한 뒤 Run model을 눌러 스틸을 평가합니다. 그 구도를 승인한 다음 Seedance 2.5 image-to-video Playground를 엽니다. 스틸을 Input image로 추가하고, Prompt에 움직임을 설명한 뒤 다시 Run model을 누르세요. Closing frame은 선택 사항입니다.
채팅이 도구 호출로 미디어를 요청하게 하기
채팅 모델에 애플리케이션이 장면 카드를 준비하도록 요청하는 request_scene_image 도구를 제공하세요. 채팅 답변에서 지어낸 ID가 아니라, 애플리케이션의 로그인한 사용자와 승인된 캐릭터 초상을 사용합니다.
- 채팅 모델: 장면을 설명하고 사용자가 만들기를 원하는지 묻습니다.
- 애플리케이션: 캐릭터 소유권을 확인하고, 가격을 보여 주고, 승인을 기다립니다.
- 미디어 작업: 승인된 뒤에만 이미지를 만듭니다. 애니메이션은 별도의 구매입니다.
Grok 4.6 chat은 OpenAI 형식의 도구 호출을 지원합니다. API는 대화 기억을 유지하지 않으므로 요청할 때마다 캐릭터 카드와 픽션 작성 맥락을 다시 보내세요. 도구 메시지 형식은 채팅 레시피에 나와 있습니다.
{
"type": "function",
"function": {
"name": "request_scene_image",
"description": "Ask the app to prepare a picture of the character. Call only when the user asks to see something. The app shows a free scene card first; nothing is made or charged until the user approves a quoted price.",
"parameters": {
"type": "object",
"properties": {
"scene": {"type": "string", "enum": ["observatory-balcony", "kitchen-morning", "rooftop-dusk"]},
"details": {"type": "string", "description": "Clothing, props and mood the user asked for, in one sentence."}
},
"required": ["scene"]
}
}
}이 호출에는 초안 카드가 승인을 기다리고 있다는 tool 메시지로 답하세요. 그러면 모델이 전달했다고 주장하는 대신 "장면 카드예요, 만들까요?" 같은 솔직한 한 줄을 쓸 수 있습니다.
캐릭터와 장면을 별도 레코드로 관리하기
승인된 초상과 요청된 장면을 분리해 두세요. 남색 코트, 천문대 발코니, 망원경 쪽으로 살짝 몸을 돌리는 동작입니다. 초상은 캐릭터의 기준이 되고, 승인된 장면 이미지는 영상의 첫 프레임이 됩니다. 새 장면마다 캐릭터 초상을 바꾸지 마세요.
요청마다 제출하기 전에 레코드를 하나 저장하세요(이것은 애플리케이션의 필드이며 API 페이로드가 아닙니다).
{
"mediaRequestId": "scene-104-r1",
"ownerId": "application-user-id",
"sceneRevision": 1,
"referenceAssetId": "approved-rowan-portrait",
"approvedStillAssetId": null,
"model": "alibaba/qwen-image-2.1/edit",
"inputSnapshot": {},
"idempotencyKey": "<uuid generated for this request>",
"taskId": null,
"state": "draft",
"deliveredAssetId": null
}사용자가 코트를 초록색으로 바꾸면 새 키로 리비전 2를 만드세요. 파란 코트 요청이 실행 중일 때는 절대 수정하지 마세요.
승인된 스틸은 직접 저장하세요. 작업 결과 링크는 약 20분, 생성된 파일은 약 14일 동안 유지되므로, 승인된 스틸을 자체 저장소에 복사하고 이미지 파일 자체를 반환하는 안정적인 공개 HTTPS URL을 Seedance에 전달하세요(공유 페이지 링크는 실패합니다). Qwen 결과 링크를 영상 단계에 그대로 넘기면 만료될 때까지만 작동합니다. 움직임 리비전에는 저장해 둔 스틸을 다시 사용하세요. 초상과 움직임을 동시에 바꾸면 어느 쪽이든 고치기 어려워집니다.
이미지와 영상 설정은 따로 하기
Qwen Image 2.1 edit 경로는 프롬프트와 1~10개의 image_urls를 받습니다. 참조 이미지를 추가하면 1k 가격이 올라가며, 2k는 참조 이미지 1~10개에 $0.048입니다. 첫 장면에는 승인된 초상 하나, 1k, 16:9, PNG, 프롬프트 확장 끄기를 사용하세요.
Use the supplied portrait as the identity reference for the same fictional adult Rowan. Create a medium shot on an observatory balcony, wearing a navy coat. Keep both hands relaxed and visible. A telescope rests on its tripod beside Rowan. Leave open space on the right for a small head turn. Warm doorway light, natural night background, no text or extra people.
사용자에게 스틸을 제안하기 전에 얼굴과 의상을 승인된 초상과 비교하세요. 게이트 2에서 사용자는 애니메이션 전에 그 스틸과 클립 가격을 수락합니다. 망원경이 떠 있거나 오른쪽에 움직일 공간이 없다면 이미지 단계에서 고치세요. 영상은 잘못된 첫 프레임을 바로잡을 수 없습니다.

제안하는 첫 프레임의 일러스트입니다. 캐릭터가 작게 돌아볼 공간이 있고 망원경은 물리적으로 지지되어 있습니다. 이것은 Qwen 출력이나 생성 테스트가 아닙니다.
Seedance 2.5 image-to-video 경로는 image_url, 4~30초, 480p·720p·1080p, 오디오 토글, 선택 사항인 시드를 받습니다. aspect_ratio는 adaptive만 허용하므로 클립은 스틸의 비율을 따릅니다. 먼저 스틸을 전달 형식에 맞게 잘라 두세요. reference-to-video 경로는 계약이 다르므로 그 필드를 섞지 마세요.
결정 시점에 견적 받기
Qwen 편집은 참조 이미지 1개 포함으로 1k가 $0.036, 2k가 $0.048, 1.5k가 $0.072입니다. 참조 이미지를 추가하면 1k와 1.5k 가격이 올라갑니다. Seedance는 초당 480p가 $0.1234, 720p가 $0.2773, 1080p가 $0.6823이며, 오디오를 켜든 끄든 같은 요금입니다.
움직임을 다시 시도할 때는 승인된 스틸을 재사용하세요.
| 플랜 | 비용 |
|---|---|
| 1k 스틸만 | $0.036 |
| 스틸 + 5초 클립(480p) | $0.653 |
| 스틸 + 5초 클립(720p) | $1.4225 |
| 스틸 + 5초 클립(1080p) | $3.4475 |
| 스틸 + 720p 클립 2회 시도 | $2.809 |
이 금액에는 1k 편집의 참조 이미지 1개가 포함되며, 추가 참조 이미지 요금, 채팅 토큰, 스틸 추가 편집은 제외됩니다.
스틸 가격은 생성 전에, 클립 가격은 사용자가 스틸을 수락한 뒤에 보여 주세요. 채팅 턴마다 보여 주는 것이 아닙니다. 견적은 무료이고, 아무것도 예약하지 않으며, 해당 계정, API 키, 모델, 입력에 대해 5분 동안 유효합니다. 길이, 해상도, 참조 이미지를 바꾸면 새 견적이 필요합니다. 제출된 작업은 추정 비용을 즉시 보류하며, 그 보류액이 청구될 수 있는 최대 금액입니다. 승인된 이미지를 재사용하면 움직임 리비전은 영상 요금만으로 끝납니다.
응답이 사라져도 견디는 영상 단계 클라이언트
spicyapi를 python -m pip install spicyapi로 설치하고, SPICY_API_KEY와 APPROVED_STILL_URL을 설정한 뒤, 스틸을 승인한 후에 이것을 실행하세요. 저장된 실행 파일을 보관하면 같은 시도를 이어서 할 수 있습니다. 다른 창작 테이크를 원하면 새 실행 이름을 사용하세요.
import json
import os
import time
import uuid
from pathlib import Path
from spicyapi import SpicyClient, output_assets
client = SpicyClient()
def run(model, inputs, name):
path = Path(name + ".json")
request = {"model": model, "input": inputs}
saved = json.loads(path.read_text()) if path.exists() else {"request": request, "key": str(uuid.uuid4())}
if saved["request"] != request:
raise ValueError("Changed input needs a new run name; preserve the old attempt.")
def save():
temporary = path.with_suffix(".tmp")
temporary.write_text(json.dumps(saved))
temporary.chmod(0o600)
temporary.replace(path)
if not saved.get("taskId"):
if time.time() - saved.get("sentAt", time.time()) >= 23 * 3600:
raise RuntimeError("Reconcile the saved attempt before submitting again.")
if "quote" not in saved:
saved["quote"] = client.quote_task(model=model, input_data=inputs)
if input(f"Maximum ${saved['quote']['maxCharge']}. Create? [y/N] ") != "y":
raise SystemExit("Stopped without creating this task.")
saved.setdefault("sentAt", time.time())
save()
quote = saved["quote"]
task = client.create_task(model=model, input_data=inputs,
idempotency_key=saved["key"], quote_id=quote["quoteId"], expected_cost=quote["estimatedCost"])
saved["taskId"] = task["taskId"]
save()
print("Task:", saved["taskId"])
for _ in range(360):
task = client.get_task(saved["taskId"])
if task["state"] in {"failed", "expired", "canceled"}:
raise RuntimeError(str(task.get("errorMessage", task["state"])))
if task["state"] == "succeeded":
assets = output_assets(task)
if assets and assets[0].get("unavailable"):
raise RuntimeError("Output unavailable; keep the task ID for reconciliation.")
if assets and assets[0].get("url") and not assets[0].get("pending"):
return assets[0]
time.sleep(5)
raise TimeoutError("Rerun the same saved attempt to resume; no new task is needed.")
asset = run("bytedance/seedance-2.5/image-to-video", {
"image_url": os.environ["APPROVED_STILL_URL"],
"prompt": "The adult character slowly turns toward the telescope. Hold the camera steady. Keep the navy coat and balcony unchanged.",
"duration_seconds": 5, "resolution": "720p", "aspect_ratio": "adaptive", "generate_audio": True,
}, "companion-video")
print(asset["url"])프로덕션 통합은 Python SDK와 작업 문서를 참고하세요.
폴링과 콜백을 위한 하나의 업데이트 함수
폴링으로 얻은 결과와 검증된 완료 이벤트 모두에 애플리케이션의 같은 핸들러를 사용하세요. 이 핸들러는 에셋을 한 번만 첨부하고, 비공개로 유지하고, 검토할 준비가 되었을 때만 채팅에 알려야 합니다. 중복 이벤트가 갤러리 항목을 하나 더 만들어서는 안 됩니다.
실서비스 컴패니언 앱에서는 완료 알림을 검증된 Webhook으로 보내고, 도착하지 않는 콜백에 대비해 주기적인 상태 점검도 유지하세요. 콜백 본문에는 프롬프트와 서명된 결과 링크가 들어 있으므로 비공개로 취급하세요. 설정은 작업 문서에 나와 있습니다. 조정용 점검을 곁들인 Webhook(글 "async-task-api-polling-vs-webhook" 참조)은 프로덕션에 적합하고, 프로토타입에는 폴링으로 충분합니다. 많은 사용자가 동시에 미디어를 요청할 때는 제한된 디스패치를 사용하세요.
전달 상태, 실패, 취소
draft, submitted, generating, ready_for_review, delivered, failed, rejected를 사용하세요. API의 succeeded는 결과 처리를 시작한다는 뜻이며, 사용자에게 보인다는 뜻이 아닙니다. delivered로 바꾸기 전에 클립 전체를 보고 얼굴 드리프트, 움직이는 소품, 요청하지 않은 컷이 없는지 검토하세요.
| API 결과 | 보여 줄 내용 | 과금 |
|---|---|---|
succeeded, 에셋 준비됨 | 검토 후 전달 | settled와 cost 확인 |
succeeded, 에셋 pending | "아직 준비 중"으로 표시하고 몇 초 뒤 다시 조회 | 아직 확정 아님 |
succeeded, 에셋 unavailable | 이미 저장한 사본을 복원하거나, 의도적인 새 생성을 제안 | cost 확인 |
failed 또는 expired | 명확한 실패 안내와 재시도 버튼 | 보류는 보통 해제됨. 모델 페이지에 명시된 경우 일부 콘텐츠 거부는 과금됨 |
만료된 견적에는 새 견적과 새 승인이 필요합니다. 이미 수락된 요청은 원래 키로 복구하세요. 수락된 작업은 취소할 수 없습니다. 사용자가 "됐어"라고 하면 대기 중인 첨부를 숨기고 작업은 끝까지 진행되게 두세요. 채팅을 닫으면 과금이 해제된다고 약속하지 마세요. 재시도는 자체 견적이 있는 새 요청입니다.
컴패니언의 비공개 미디어를 비공개로 유지하기
장면을 제공하기 전에 로그인한 사용자의 것인지 확인하세요. 승인된 미디어는 자체 접근 규칙이 있는 저장소로 복사하세요. 임시 결과 링크가 공개 갤러리 주소가 되어서는 안 됩니다.
삭제할 때는 플랫폼 사본과 직접 만든 사본을 따로 처리하세요. 작업별 보존 설정으로 플랫폼이 결과를 보관하는 기간을 줄이고, 사용자가 장면이나 계정을 삭제하면 완료된 각 작업의 저장된 콘텐츠를 파기하세요(청구 기록은 남습니다). 보존 기간을 0으로 하면 워커가 복사하기 전에 결과가 사라질 수 있습니다. 직접 만든 사본의 삭제와 그 전달 접근 권한의 취소는 따로 하세요. 자세한 내용은 보존 문서에 있습니다. 제품에 맞는 보존 제어와 내구성 있는 저장소 파이프라인을 사용하세요.
픽스처로 네 가지 실패 테스트하기
더블 클릭, 사라진 수락 응답, 중복 성공 이벤트, 사용할 수 없는 에셋을 재현하세요. 논리적 요청은 하나, 원래 키로의 복구, 갤러리 첨부는 하나, 그리고 솔직한 "사용 불가" 상태가 나와야 합니다. 어느 것도 유료 생성이 필요하지 않습니다. 스틸과 클립에 걸친 얼굴의 일관성은 눈으로 확인해야 하는 문제이므로 모든 출력을 검사하세요. "영상이 준비되었습니다"라는 채팅 메시지가 실제 작업이나 에셋 상태보다 우선하지 않게 하세요. 천문대 일러스트는 편집용 장면이며 Qwen이나 Seedance의 출력이 아닙니다.
Agent에게 호출 맡기기
입력 준비와 결과 반환은 Agent에게 맡기고 가격과 창작상의 결정은 직접 승인하고 싶을 때 사용하세요.
- CLI:
input.json을{"image_urls": ["<approved portrait URL>"], "prompt": "<the scene prompt above>", "resolution": "1k", "aspect_ratio": "16:9", "output_format": "png", "enable_prompt_expansion": false}같은 내용으로 준비하고npx @spicyapi/cli tasks create --model alibaba/qwen-image-2.1/edit --input-file input.json --wait를 실행하세요. CLI가 가격을 확인합니다. 복구를 위해 반환된 작업 ID를 보관하세요. - MCP: 어시스턴트에게
spicyapi_task_create를alibaba/qwen-image-2.1/edit에 사용하고 이어서spicyapi_task_wait를 사용하라고 요청하세요. MCP 서버는 생성 전에 가격 확인을 요구합니다. - Skill:
npx skills add spicyapi-ai/spicy-skill로 설치한 뒤,$spicyapi에게 같은 워크플로를 실행하고 승인을 위해 멈추라고 요청하세요. Skill은 구성된 CLI 또는 MCP 연결을 사용합니다.








