본문으로 건너뛰기

AI 이미지·영상 API 입문: 첫 입력부터 결과 검토까지

파란 머그잔 이미지를 만들고 MiniMax H3로 영상화합니다. Playground부터 Python, CLI, 에이전트까지 프롬프트, 비용, 검토 방법을 안내합니다.

작성자
Lydia
발행
읽는 시간
약 13분
수정
파란 앞치마를 입은 가상의 성인 도예가가 도예 작업실에서 카메라 옆에 파란 컵을 놓는 모습

이미지 하나를 만들고 승인한 다음, 그 이미지로 짧은 영상을 만듭니다. 이 과정은 2026년 10월에 진행한 파란 머그잔 테스트를 따릅니다. 이 가이드는 $0.010의 Z-Image 정지 이미지와 $0.185의 MiniMax H3 768p 5초 영상을 사용합니다. 정지 이미지의 프롬프트 확장을 끄면 각 단계에서 한 번씩 시도하는 비용은 $0.195입니다. 먼저 Playground에서 같은 머그잔 장면을 시험하고, 입력이 제대로 작동하면 Python 예제를 사용하거나 에이전트에 호출을 맡기세요.

목표 결과는 구체적입니다. 작업대 위에 모양이 제대로 잡힌 머그잔이 있고, 김은 오른쪽으로 흐르며, 카메라는 천천히 다가간 뒤 멈춰 있어야 합니다. 모델을 배포하거나 GPU 서버를 설정할 필요는 없습니다.

Playground에서 전체 사례를 시험하기

계정을 만들고 잔액을 충전한 뒤 이메일을 인증하세요. Playground가 요청을 처리합니다. API 키는 아래 코드와 에이전트 도구를 사용할 때만 필요합니다.

1. 시작 이미지 생성하기

Z-Image 텍스트→이미지를 열고 Playground를 사용하세요. 다음 프롬프트를 붙여 넣습니다.

Photograph of a handmade blue ceramic mug of tea on a worn wooden workbench in a pottery studio, a thin curl of steam rising from the mug. Soft window light from the left, shallow depth of field, shelves of pots softly out of focus behind. The mug sits left of centre with empty bench to its right. Natural colours, no text, no logos.

16:9, PNG, 시드 42를 선택하고 프롬프트 확장을 끄세요. 표시된 가격을 확인한 뒤 Run model을 누릅니다. 기본 이미지 비용은 $0.010이며, 확장을 켜면 $0.016이 됩니다. 프롬프트 변경을 비교하는 동안 시드 42는 고정하세요.

다음 단계로 넘어가기 전에 결과를 열어 확인하세요.

  • 대상: 머그잔 하나와 손잡이 하나가 있고 도자기 형태가 깔끔해야 합니다.
  • 움직일 여지: 김이 보이고 오른쪽으로 흐를 공간이 있어야 합니다.
  • 구도: 머그잔이 중앙보다 왼쪽에 있고 선반은 차분한 배경으로 남아야 합니다.
  • 불필요한 요소: 원치 않는 글자, 로고, 추가 컵이 없어야 합니다.

승인한 PNG를 image.png로 다운로드하세요. 머그잔이 잘못됐다면 영상에 돈을 쓰기 전에 문장 하나를 바꾸고 이미지를 다시 만드세요. 새 기본 이미지를 만들 때마다 $0.010이 추가됩니다.

2. 승인한 이미지 영상화하기

MiniMax H3 이미지→영상을 열어 Input image에 image.png를 추가하고 Prompt에 다음 내용을 붙여 넣습니다.

Steam rises from the mug and drifts to the right. The camera pushes in slowly toward the mug, then holds. The mug, bench and shelves stay where they are. Quiet room, a faint ceramic clink.

Duration은 5초, Resolution은 768p로 설정합니다. 가격을 확인한 뒤 Run model을 누르세요. 이 영상은 $0.185이며, 2k를 선택하면 $0.290입니다.

이 프롬프트는 김의 움직임, 카메라 움직임, 소리를 나눠 지시합니다. 머그잔을 다시 묘사하지 않고 제자리에 두도록 요청합니다. 머그잔이 작업대 위를 이동하면 대상의 동작을 더 추가하기보다 카메라 지시를 단순하게 바꾸세요. 천천히 다가가는 동작은 카메라를 가까이 움직이라는 뜻이며, 머그잔을 움직이라는 요청이 아닙니다.

3. 영상을 검토하고 다음 테이크 선택하기

결과 전체를 소리와 함께 재생하세요. 머그잔이 미끄러지거나 모양이 바뀌지 않는지, 카메라가 다가간 뒤 멈추는지, 원치 않는 컷 전환이 없는지, 오디오가 조용한 작업실에 어울리는지 확인합니다. 예상 결과물은 오디오 트랙이 있는 약 5초짜리 MP4입니다. 파일이 재생된다는 사실만으로 사용할 수 있는 숏이 되지는 않습니다.

이미지는 맞지만 움직임이 틀렸다면 승인한 시작 이미지를 유지하고 움직임 프롬프트를 수정하세요. 시작 이미지가 틀렸다면 먼저 교체하세요. H3에는 시드가 없습니다. 2k로 생성하면 새 테이크가 만들어지며, 마음에 들었던 768p 버전과 움직임이 다를 수 있습니다.

파란 머그잔 테스트에서 나온 결과

정지 이미지는 $0.010이었고 1280 × 720 PNG로 도착했습니다. 5초 영상 요청은 $0.125(480p 기준이며 480p는 더 이상 제공되지 않습니다. 같은 영상은 768p에서 $0.185)였고 오디오 트랙이 있는 832 × 480 MP4를 반환했습니다. 총비용은 $0.135였습니다(768p라면 $0.195).

머그잔은 작업대에 그대로 있었고 김은 위쪽과 오른쪽으로 움직였으며, 카메라의 접근은 미세했습니다. 정지 이미지에서는 머그잔이 중앙보다 조금 왼쪽에만 놓였고, 마지막에는 선반 근처의 김이 흐릿해졌습니다. 더 타이트한 제품 숏을 원한다면 영상화하기 전에 이미지 프롬프트에서 머그잔 위치를 더 명확히 지정하세요.

이 두 모델이 첫 시도에 적합한 이유

Z-Image는 대상과 구도를 정할 수 있는 저렴한 이미지를 제공합니다. H3는 그 이미지를 받아 움직임과 사운드트랙을 생성합니다. 입력은 다음과 같이 다릅니다.

Z-Image 텍스트→이미지H3 이미지→영상
모델 IDalibaba/z-image-turbo/text-to-imageminimax/h3/image-to-video
필수 입력prompt (1~1,000자)prompt (최대 7,000)와 image_url
여기서 사용한 설정aspect_ratio (1:1, 4:3, 3:4, 16:9 또는 9:16), output_format (png만 지원), seed, enable_prompt_expansionduration_seconds (4~15), resolution (768p 또는 2k)
지원하지 않는 입력그 외 모든 출력 형식seed와 aspect_ratio: 시작 이미지가 형태를 정합니다
반환 결과PNG 하나생성된 사운드트랙이 있는 MP4 하나
가격이미지당 $0.010768p에서 초당 $0.037

시작 이미지는 PNG, JPEG, WebP 또는 GIF를 사용하세요. 각 변은 256~5,760픽셀, 화면 비율은 2:5~5:2, 파일 크기는 30 MB 미만이어야 합니다. PNG를 업로드하는 것이 가장 간단합니다. 공개 이미지 링크를 사용한다면 실제 이미지를 반환해야 합니다. 클라우드 드라이브 공유 페이지는 이미지 입력이 아닙니다.

Python에서 같은 사례 실행하기

Python 3.11 이상을 사용하고 python -m pip install spicyapi로 공식 패키지를 설치하며 file 유틸리티를 준비하세요. 자신의 터미널에서 SPICY_API_KEY를 설정하고 아래 코드를 blue-mug.py로 저장한 뒤 python blue-mug.py를 실행합니다. 두 유료 단계에서 모두 승인을 요청하고 이미지 검토를 위해 일시 정지합니다. 재개할 때는 생성된 실행 폴더를 유지하세요. 의도적으로 새 시도를 할 때만 새 RUN_NAME을 선택합니다. API 규약과 SDK 옵션은 Python SDK 문서를 참고하세요.

python
import hashlib
import json
import os
from pathlib import Path
import subprocess
import tempfile
import time
import urllib.request
import uuid
from spicyapi import SpicyClient, output_assets

# Change RUN_NAME for a deliberately new pair of paid generations.
folder = Path(os.getenv("RUN_NAME", "blue-mug-1"))
folder.mkdir(exist_ok=True, mode=0o700)
state_file = folder / "state.json"
state = json.loads(state_file.read_text()) if state_file.exists() else {}
identity = hashlib.sha256(os.environ["SPICY_API_KEY"].encode()).hexdigest()
if state.get("identity", identity) != identity:
    raise RuntimeError("This saved run belongs to a different API key.")
state["identity"] = identity
client = SpicyClient()

def save() -> None:
    temp = state_file.with_suffix(".tmp")
    temp.write_text(json.dumps(state, indent=2))
    temp.chmod(0o600)
    temp.replace(state_file)

def run(stage: str, model: str, inputs: dict) -> dict:
    request = {"model": model, "input": inputs}
    if stage not in state:
        state[stage] = {"request": request, "key": str(uuid.uuid4())}
    saved = state[stage]
    if saved["request"] != request:
        raise RuntimeError("Input changed: choose a new RUN_NAME, not a new retry key.")
    save()  # The same request and key survive a lost create response.
    if not saved.get("taskId"):
        if "first_sent" in saved and time.time() - saved["first_sent"] >= 23 * 3600:
            raise RuntimeError("Unknown acceptance: reconcile task history before retrying.")
        if "quote" not in saved:
            quote = client.quote_task(model=model, input_data=inputs)
            if input(f"{stage}: quoted ${quote['estimatedCost']}. Create? [y/N] ") != "y":
                raise SystemExit("No task created for this stage.")
            saved["quote"] = quote
        saved.setdefault("first_sent", 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()
    task_id = saved["taskId"]
    print(stage, "task:", task_id)
    deadline = time.monotonic() + 1200
    while time.monotonic() < deadline:
        task = client.get_task(task_id)
        if task["state"] in {"failed", "canceled", "expired"}:
            raise RuntimeError(f"{task_id}: {task.get('errorCode')} {task.get('errorMessage')}")
        if task["state"] == "succeeded":
            assets = output_assets(task)
            if not assets or assets[0].get("unavailable"):
                raise RuntimeError(f"{task_id}: no retrievable output; keep the task ID.")
            asset = assets[0]
            if not asset.get("pending") and asset.get("url"):
                print("Charge:", task.get("cost"), "settled:", task.get("settled"))
                return asset
        time.sleep(5)
    raise TimeoutError(f"Resume {task_id} by rerunning this file; do not start another task.")

def download(asset: dict, destination: Path, mime: str) -> None:
    temporary = None
    try:
        request = urllib.request.Request(asset["url"], headers={"User-Agent": "SpicyAPI-Blog-Example/1.0"})
        with urllib.request.urlopen(request, timeout=300) as response:
            if response.status != 200 or response.headers.get_content_type() != mime:
                raise RuntimeError("Unexpected download status or content type.")
            with tempfile.NamedTemporaryFile(dir=folder, delete=False) as handle:
                temporary = Path(handle.name)
                while chunk := response.read(1024 * 1024):
                    handle.write(chunk)
        size = temporary.stat().st_size
        expected = int(asset.get("bytes") or 0)
        detected = subprocess.check_output(["file", "-b", "--mime-type", str(temporary)], text=True).strip()
        if not size or (expected and size != expected) or detected != mime:
            raise RuntimeError("Incomplete or incorrectly formatted download.")
        temporary.replace(destination)  # An interrupted transfer leaves the old file alone.
    finally:
        if temporary is not None:
            temporary.unlink(missing_ok=True)

image_input = {
    "prompt": (
        'Photograph of a handmade blue ceramic mug of tea on a worn wooden workbench '
        'in a pottery studio, a thin curl of steam rising from the mug. Soft window '
        'light from the left, shallow depth of field, shelves of pots softly out of '
        'focus behind. The mug sits left of centre with empty bench to its right. '
        'Natural colours, no text, no logos.'
    ),
    "aspect_ratio": "16:9",
    "output_format": "png",
    "seed": 42,
    "enable_prompt_expansion": False,
}
image_asset = run("image", "alibaba/z-image-turbo/text-to-image", image_input)
download(image_asset, folder / "image.png", "image/png")
if input(f"Open {folder / 'image.png'}. Is the mug/frame approved? [y/N] ") != "y":
    raise SystemExit("Stopped before video generation.")
if not state.get("image_uri"):
    state["image_uri"] = client.upload_file(str(folder / "image.png"))["uri"]
    save()
video_input = {"image_url": state["image_uri"], "prompt": 'Steam rises from the mug and drifts to the right. The camera pushes in slowly toward the mug, then holds. The mug, bench and shelves stay where they are. Quiet room, a faint ceramic clink.',
               "duration_seconds": 5, "resolution": "768p"}
video_asset = run("video", "minimax/h3/image-to-video", video_input)
download(video_asset, folder / "video.mp4", "video/mp4")
print("Play", folder / "video.mp4", "with sound and review it before using it.")

API 키는 자신의 서버나 직접 제어하는 컴퓨터에 보관하고, 공개 웹페이지나 모바일 앱에는 절대 넣지 마세요. 다른 모델을 통합하기 전에 현재 이용 가능 여부와 허용 입력은 위 모델 페이지에서 확인합니다.

에이전트에 호출을 맡기기

가격과 창작 결정을 직접 승인하면서 입력 준비와 결과 반환을 맡기고 싶다면 Agent를 사용하세요. 영상 생성을 허가하기 전에 시작 이미지를 직접 검토합니다.

  • CLI: 같은 입력 객체를 input.json으로 저장하고 npx @spicyapi/cli tasks create --model alibaba/z-image-turbo/text-to-image --input-file input.json --wait를 사용합니다. CLI가 가격을 확인합니다. 복구를 위해 반환된 작업 ID를 보관하세요. 영상에서는 승인한 정지 이미지를 npx @spicyapi/cli files upload image.png로 업로드한 뒤, 같은 방식으로 minimax/h3/image-to-video를 생성하면서 반환된 spicy:// URI를 image_url로 사용합니다.
  • MCP: 어시스턴트에게 spicyapi_task_create로 alibaba/z-image-turbo/text-to-image를 호출하고 이어서 spicyapi_task_wait를 사용하도록 요청하세요. 영상에서는 spicyapi_upload_file로 승인한 image.png를 업로드한 뒤 spicyapi_task_create로 minimax/h3/image-to-video를 호출하고 5초, 768p, 위 움직임 프롬프트를 지정합니다. MCP 서버는 생성 전에 가격 확인을 요청합니다.
  • Skill: npx skills add spicyapi-ai/spicy-skill로 설치하고 $spicyapi에 같은 워크플로를 실행하되 승인을 위해 멈추도록 요청하세요. Skill은 설정된 CLI 또는 MCP 연결을 사용합니다.

최종 영상뿐 아니라 시도 횟수까지 예산 잡기

다음은 미국 달러 기준 예상 비용입니다. 생성 전에 현재 가격을 확인하세요.

요청예상 가격
Z-Image 정지 이미지 하나, 16:9 PNG$0.010
같은 정지 이미지에 프롬프트 확장 사용$0.016
H3, 768p에서 4 s$0.148
H3, 768p에서 5 s (resolution을 생략하면 기본값)$0.185
H3, 2k에서 5 s$0.290
H3, 768p에서 10 s$0.370
H3, 2k에서 15 s$0.870
  • 길이와 해상도: H3는 768p, 2k에서 각각 초당 $0.037, $0.058입니다.
  • 지원하지 않는 조합: 2k의 4초 영상은 40004 오류로 거절됩니다.
  • 초안 두 개: 정지 이미지 하나와 768p 5초 영상 두 개는 $0.380입니다.
  • 초안과 납품용 테이크: 승인한 이미지를 재사용해 정지 이미지 하나, 768p 5초 확인 영상, 2k 5초 테이크를 만들면 $0.010 + $0.185 + $0.290 = $0.485입니다.

작업이 수락되면 금액이 예약되고, 작업이 끝나면 정산됩니다. 실패하거나 만료된 작업은 일반적으로 예약 금액을 해제합니다. 일부 콘텐츠 거절은 모델이 명시한 정책에 따라 과금됩니다. 청구를 조사할 때는 작업의 실제 cost와 settled 상태를 확인하세요.

올바른 단계에서 문제 해결하기

증상바꿀 내용
머그잔에 손잡이가 두 개이거나 불필요한 글자가 있음이미지 프롬프트를 수정하고 영상 생성 전에 새 시작 이미지를 승인하세요
카메라를 움직이려 했는데 머그잔이 움직임이미지는 유지하고 카메라 움직임 하나를 요청하며 머그잔을 고정한다고 명시하세요
768p 영상은 좋지만 새 2k 테이크가 달라짐새 숏으로 보고 승인 전에 움직임을 확인하세요
이미지 링크가 실패함파일을 업로드하거나 공유 페이지 대신 직접 접근할 수 있는 이미지 URL을 사용하세요
계정이 401 또는 40310을 반환함키를 확인하거나 이메일 인증을 완료하세요
40301 / 40302 / 40303모델 권한, 키의 IP 허용 목록 또는 지역별 이용 가능 여부를 확인하세요
40201 / 40202사용 가능한 잔액 또는 계정/키 지출 한도를 확인하고, 실패한 전송을 대조하는 동안 저장한 요청을 유지하세요
요청이 seed, aspect_ratio 또는 output_format을 거절함선택한 작업의 지원 입력을 확인하고 Z-Image 필드를 H3에 복사하지 마세요
400042k의 4초라는 호환되지 않는 조합을 바꾸세요
40901 또는 멱등성 충돌견적을 다시 받으세요. 작업 ID가 저장되지 않았다면 state.json에서 해당 단계 항목을 삭제하고 다시 실행합니다. 이전 시도가 수락됐을 가능성이 있으면 다른 작업을 만들기 전에 작업 목록을 확인하세요. 오류 참조를 참고하세요
429 또는 연결 오류실행 폴더와 작업 ID를 유지하고, 결과 복구만을 위해 다른 유료 시도를 시작하지 마세요
작업이 content_rejected 또는 timeout을 보고함오류와 요금을 읽고 프롬프트 변경, 원본 이미지 변경 또는 숏 단축을 의도적으로 선택하세요
생성은 성공했지만 결과가 준비되지 않음저장된 같은 작업을 재개하세요. 다운로드를 얻기 위해 다른 영상을 생성하지 마세요

첫 워크플로는 작게 유지하기

숏이 제대로 나오면 납품 길이와 해상도를 정한 뒤 해당 테이크를 생성하고 검토합니다. H3의 last_image_url로 선택적인 종료 이미지를 지정하면 끝부분을 유도할 수 있지만, 정확한 마지막 프레임이나 프레임 사이 움직임을 보장하지는 않습니다.

애플리케이션에서는 두 작업 ID를 보관하고 승인한 파일을 자체 스토리지에 저장하세요. 결과 수집 방법을 선택하려면 폴링과 Webhook 비교를, 파일을 보관하려면 미디어 저장을, 여러 숏이 필요하면 배치 생성을 참고하세요. 첫 목표는 같습니다. 승인한 이미지 하나와 의도한 숏을 실제로 따르는 영상 하나입니다.

다른 언어 English · 日本語 · Deutsch · Français · Español · Português (Brasil) · Русский · Italiano · Polski

직접 해 보기

실행한 만큼만 결제하세요. 요금제도, 구독도 없습니다.

사용 모델: MiniMax H3 · ByteDance Seedance 2.0 Fast