HiAPI
블로그

튜토리얼

Ray 3.2 이미지-투-비디오 API 사용법: curl, Python 그리고 한 번에 성공하는 요청

Oct 20, 2026
Ray 3.2 이미지-투-비디오 API 사용법: curl, Python 그리고 한 번에 성공하는 요청

Ray 3.2 이미지-투-비디오는 정지 이미지 한 프레임에 애니메이션을 입힙니다. 이미지를 주고 움직임을 설명하는 프롬프트를 함께 보내면, 당신의 이미지에서 시작하는 짧은 영상을 반환합니다. hiapi에서 모델 id는 ray-3.2/image-to-video이며, 다른 모든 비디오 모델과 동일한 통합 비동기 작업 API로 호출합니다—POST /v1/tasks를 보낸 뒤 폴링하거나 콜백을 받습니다.

이것은 Ray 3.2 텍스트-투-비디오와 다른 엔드포인트이며, 단지 프롬프트만 다른 것이 아닙니다. 입력 스키마가 더 작고 더 엄격하며, 텍스트 모델에서 익숙했을 두 가지 제어 항목이 사라졌습니다. 이 가이드는 한 번에 통과하는 요청을 제공합니다: 정확한 스키마(2026-10-20 라이브 API 대조 검증 완료), 바로 복사할 수 있는 curl, 완전한 Python 제출/폴링/다운로드 스크립트, 그리고 프로덕션 세부 사항—콜백, 멱등성, 그리고 실제 오류 응답, 그중에는 예상치 못한 위치에 나타나지 않는 실패 모드도 포함됩니다.

무엇을 만들고 무엇을 준비해야 하는가

목표: 첫 프레임 이미지 + 움직임 프롬프트를 제출하고, 작업을 폴링한 뒤, 이미지가 움직이기 시작하는 무음 .mp4를 다운로드합니다.

사전 조건:

  • hiapi API 키—대시보드에서 발급받으세요. 키는 sk-... 형태이며 Authorization: Bearer 헤더에 넣습니다. 작업 API가 처음이라면 먼저 빠른 시작을 보세요.
  • 첫 프레임 이미지의 공개적으로 접근 가능한 이미지 URL(아래 참조). 아무 HTTP 클라이언트—우리는 curl과 Python(requests)을 보여줍니다.

Ray 3.2 이미지-투-비디오는 비디오 클립 단위로 과금됩니다. 클립 길이가 고정되어 있으므로(아래 참조), resolution이 비용을 조절하는 유일한 수단입니다—1080p 클립 비용은 540p의 몇 배입니다. 현재 가격은 가격에서 확인하고, 프롬프트를 반복할 때는 540p로 프로토타이핑하세요.

정확한 입력 스키마(필수 2개, 선택 1개)

모든 것은 input 객체 안에 들어갑니다. 스키마는 엄격합니다—인식할 수 없는 필드는 거부합니다. 필드별로 라이브 API와 대조 검증했습니다:

필드 타입 필수 여부 허용 값 기본값
prompt string 예 화면이 어떻게 움직여야 하는지(카메라, 피사체 움직임) —
image_url string 예 첫 프레임의 공개 URL —
resolution string 아니오 540p, 720p, 1080p 540p

텍스트-투-비디오 모델에서 넘어온 사람이 자주 밟는 세 가지 함정:

  1. duration 없음. 클립 길이는 고정(약 5초)입니다. duration을 보내면 잘리는 것이 아니라 바로 거부됩니다. 이는 Ray 3.2 텍스트-투-비디오와 정반대이며, 거기서는 duration이 선택 가능한 정수(5 또는 10)입니다.
  2. aspect_ratio 없음. 출력 화면 비율은 입력 이미지를 따르므로 설정할 것이 없습니다. 텍스트 모델에서는 화면 비율을 선택하지만, 여기서는 이미지가 결정합니다.(아래 실제 실행으로 확인: 정사각형 입력이 정사각형 클립을 생성했습니다.)
  3. 스키마는 닫혀 있음. seed, negative_prompt, prompt_optimizer 및 그 밖의 어떤 필드도 400과 함께 additional properties '<field>' not allowed를 반환합니다. 다른 모델에서 요청 본문을 이식하고 여분 키가 무시될 것이라 가정하지 마세요.

검증이 실제로 반환하는 것

다음은 라이브 엔드포인트의 실제 응답입니다—고의로 잘못된 값을 제출했고, 작업은 생성되지 않았으며 과금도 없었습니다:

// POST with no image_url
{"code":400,"data":null,"error_code":"INVALID_REQUEST",
 "message":"invalid input: image_url: missing required field \"image_url\""}

// POST with "resolution": "2k"
{"code":400,"data":null,"error_code":"INVALID_REQUEST",
 "message":"invalid input: resolution: value must be one of '540p', '720p', '1080p'"}

// POST with a "duration" field
{"code":400,"data":null,"error_code":"INVALID_REQUEST",
 "message":"invalid input: <root>: additional properties 'duration' not allowed"}

// POST with an "aspect_ratio" field
{"code":400,"data":null,"error_code":"INVALID_REQUEST",
 "message":"invalid input: <root>: additional properties 'aspect_ratio' not allowed"}

message는 구체적인 문제 필드를 지적하므로, 잊어버렸을 때 API 자체가 스키마 참고서가 됩니다.

첫 프레임 준비

image_url은 hiapi worker가 가져올 수 있는 URL이어야 합니다—공개 링크여야 하며, 로컬 경로나 data: base64 blob이 아닙니다. 이미지는 클립의 화면 비율과 오프닝 구도를 결정하므로, 실제로 움직임이 시작되길 원하는 그 프레임에서 출발하세요.

미리 짚고 넘어갈 함정이 하나 있습니다. 놓치기 쉬운데: 잘못된 image_url은 작업 생성 시 잡히지 않습니다. 생성 호출은 요청의 형태만 검증합니다(문자열인가?). URL이 해석 가능한지는 검사하지 않습니다. 도달 불가능하거나 404인 이미지 URL은 완전히 정상적인 200과 taskId를 반환합니다—그러고 몇 초 뒤 작업이 fail로 뒤집힙니다. 정확한 실패 페이로드는 문제 해결 섹션에서 보게 될 것입니다. 핵심은 항상 작업의 종료 상태 status를 기준으로 분기하고, 생성이 200을 반환했다고 성공이라고 가정하지 않는 것입니다.

최소 동작 예제: curl

작업 생성:

curl -s -X POST https://api.hiapi.ai/v1/tasks \
  -H "Authorization: Bearer sk-YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "ray-3.2/image-to-video",
    "input": {
      "prompt": "slow cinematic push-in, gentle parallax, soft drifting light, the subject breathing almost imperceptibly",
      "image_url": "https://your-cdn.example.com/first-frame.jpg",
      "resolution": "540p"
    }
  }'

생성 응답은 작업 id를 담고 있습니다:

{ "code": 200, "data": { "taskId": "tk-hiapi-01M5..." }, "message": "success" }

종료 상태에 도달할 때까지 폴링:

curl -s https://api.hiapi.ai/v1/tasks/tk-hiapi-01M5... \
  -H "Authorization: Bearer sk-YOUR_KEY"

작업이 실행되는 동안 data.status는 여러 진행 중 상태를 거칩니다(handling 같은 값을 보게 된 뒤 archiving). 두 가지 종료 상태는 success와 fail입니다. 성공 시:

{
  "code": 200,
  "data": {
    "taskId": "tk-hiapi-01M5...",
    "status": "success",
    "created": 1792435735,
    "completed": 1792435781,
    "storage": "temp",
    "output": [
      { "type": "video", "artifactId": "278504",
        "expireAt": 1793040580,
        "url": "https://temp.hiapi.ai/.../clip.mp4" }
    ]
  },
  "message": "success"
}

data.output[0].url을 즉시 다운로드하세요. temp.hiapi.ai의 서명된 URL이며 expireAt(Unix 타임스탬프, 약 일주일 뒤)이 붙어 있습니다—핫링크하지 말고 바이트를 자체 스토리지에 저장하세요. Ray 3.2는 무음 MP4를 반환하며, 기다릴 오디오 트랙은 없습니다.

완전한 Python 스크립트(제출 → 폴링 → 다운로드)

import time
import requests

API_BASE = "https://api.hiapi.ai/v1"
API_KEY = "sk-YOUR_KEY"
HEADERS = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}


def create_task(image_url: str, prompt: str, resolution: str = "540p") -> str:
    resp = requests.post(
        f"{API_BASE}/tasks",
        headers=HEADERS,
        json={
            "model": "ray-3.2/image-to-video",
            "input": {
                "prompt": prompt,
                "image_url": image_url,     # public URL of the first frame (required)
                "resolution": resolution,   # "540p" | "720p" | "1080p"
                # no "duration" and no "aspect_ratio" — both are rejected
            },
        },
        timeout=30,
    )
    resp.raise_for_status()
    return resp.json()["data"]["taskId"]


def wait_task(task_id: str, timeout_s: int = 900, poll_interval: int = 8) -> dict:
    """Poll until terminal. Tolerates transient network blips by re-polling
    the SAME saved task_id instead of aborting the whole job."""
    deadline = time.time() + timeout_s
    while time.time() < deadline:
        try:
            resp = requests.get(f"{API_BASE}/tasks/{task_id}", headers=HEADERS, timeout=30)
            resp.raise_for_status()
        except requests.RequestException:
            time.sleep(poll_interval)      # a dropped poll is not a failed task
            continue
        task = resp.json()["data"]
        if task["status"] == "success":
            return task
        if task["status"] == "fail":
            err = task.get("error") or {}
            raise RuntimeError(f"task failed: {err.get('code')}: {err.get('message')}")
        time.sleep(poll_interval)           # any non-terminal status = still running
    raise TimeoutError(f"task {task_id} not finished after {timeout_s}s")


task_id = create_task(
    image_url="https://your-cdn.example.com/first-frame.jpg",
    prompt="slow cinematic push-in, gentle parallax, soft drifting light",
)
print("task created:", task_id)

task = wait_task(task_id)
video_url = task["output"][0]["url"]

# Output URLs expire — download right away and store the bytes yourself.
video = requests.get(video_url, timeout=120).content
with open("ray32-i2v.mp4", "wb") as f:
    f.write(video)
print(f"saved ray32-i2v.mp4 ({len(video) / 1e6:.1f} MB)")

pip install requests로 실행하고, 키와 실제 image_url로 바꾸면 작업이 끝난 뒤 디스크에 .mp4가 생깁니다.

실제 실행은 어떤 모습인가

숫자를 구체화하기 위해 2026-10-20 라이브 API에 대한 실제 540p 실행을 보여드립니다. 첫 프레임: 1600×1600(정사각형) 이미지; 프롬프트: 느린 영화식 푸시인.

  • 작업은 handling → archiving → success를 거쳤고, 서버 측 작업은 약 45초에 완료됐습니다(created 생성, completed 완료)—8초 폴링 루프를 포함하면 실제 소요는 약 60초였습니다. 비디오 작업은 즉시가 아니라 수십 초에서 몇 분이 걸립니다. 넉넉한 마감 시간을 두세요.
  • 출력은 720×720 MP4, 약 1.3 MB, 5.04초, 무음이었습니다.

이 실행은 파라미터 표가 말해주지 않는 두 가지를 알려줍니다:

  1. 출력 화면 비율은 입력과 일치—정사각형 들어가면 정사각형 나옵니다. 이것이 왜 aspect_ratio 필드가 없는지입니다. 원하는 클립 구도에 맞춰 image_url을 프레이밍하세요.
  2. resolution은 품질 등급이지 문자 그대로의 픽셀 높이가 아닙니다. 1600×1600 입력에 540p를 요청했더니 720×720이 반환됐습니다—이 등급은 입력 형태에 따라 스케일링되며 고정 치수를 강제하지 않습니다. 정확한 출력 치수가 필요하면 납품 클립의 크기를 직접 조정하세요.

프로덕션 주의 사항: 콜백, 멱등성, 오류

폴링보다 콜백 우선

스크립트가 아닌 모든 상황에서는 폴링 대신 생성 시 webhook을 등록하세요:

{
  "model": "ray-3.2/image-to-video",
  "input": {
    "prompt": "...",
    "image_url": "https://your-cdn.example.com/first-frame.jpg",
    "resolution": "720p"
  },
  "callback": { "url": "https://your-app.example.com/hooks/hiapi", "when": "final" }
}

"when": "final"을 사용하면 hiapi가 작업이 종료 상태에 도달했을 때 엔드포인트를 한 번 호출합니다—폴링 트래픽도 없고 간격 사이에 완료를 놓치지도 않습니다. 그런 다음 핸들러가 GET /v1/tasks/<id>를 실행해 output[0].url을 가져옵니다. HTTPS 엔드포인트를 노출할 수 없는 CLI와 노트북에서는 폴링이 여전히 괜찮습니다.

재시도를 멱등하게 만들기

생성 호출이 반환되는 순간 taskId를 영속화하세요. 프로세스가 생성 도중에 죽으면 저장된 id를 폴링해 복구하세요—작업을 다시 생성하지 마세요. 그러면 두 번째 클립에 비용을 지불하게 됩니다. 원래 생성 요청이 작업 id를 반환하기 전에 실패한 경우에만 다시 생성하세요. 위의 내결함성 폴링 루프는 이미 이 규칙을 따릅니다: 네트워크 요청이 끊기면 처음부터가 아니라 같은 id를 다시 폴링합니다.

실제로 보게 될 오류

아래 표의 각 행은 검증된 라이브 응답 형태입니다:

증상 응답 해결
키 누락/유효하지 않음 401, 중첩 {"error":{"code":"permission_denied","type":"hiapi_error","request_id":"...","message":"This API key is invalid..."}} Authorization: Bearer sk-... 헤더와 키에 접근 권한이 있는지 확인하세요. 지원에 문의할 때 request_id를 제공하세요.
image_url 누락 400 INVALID_REQUEST, message: "image_url: missing required field" 이 모델은 image_url이 필수입니다—텍스트 전용 모드는 없습니다.
잘못된 열거 값 / 여분 필드 400 INVALID_REQUEST, 평탄 {"code":400,"error_code":"INVALID_REQUEST","message":"..."} message가 지적하는 필드를 읽으세요. duration/aspect_ratio의 경우 additional properties ... not allowed라고 말합니다—삭제하세요.
폴링 시 잘못된 작업 id 404, 평탄 {"code":404,"data":null,"message":"task not found"}(참고: 404에는 error_code가 없습니다) id가 틀렸거나, 다른 계정의 키로 폴링했습니다.
image_url 도달 불가 생성은 200 + taskId를 반환하고, 작업은 이후 data.status:"fail"과 함께 종료되며 data.error:{"code":"INVALID_REQUEST","message":"Invalid request parameters for this model..."}를 담습니다 이것이 함정입니다. 생성 호출은 죽은 링크와 살아 있는 링크를 구분하지 못합니다. 이미지가 공개적으로 접근 가능한지 검증하고 항상 종료 상태 data.status를 읽으세요.

마지막 행은 명심할 가치가 있습니다. 작업이 실패해도 폴링의 HTTP 외피는 여전히 200입니다—실패는 HTTP 상태 코드가 아니라 data.status와 data.error 안에 있습니다. 실패한 작업은 환불되지만, 코드는 여전히 그것을 알아차려야 합니다. data.status == "fail"을 기준으로 분기하고, 200을 믿지 마세요.

관련 리소스

자주 묻는 질문

모델 id는 무엇이고 어느 엔드포인트를 호출해야 합니까? ray-3.2/image-to-video, POST https://api.hiapi.ai/v1/tasks에 제출하고 파라미터는 input 아래에 둡니다. 비동기입니다—taskId를 돌려받은 뒤 GET /v1/tasks/<id>를 폴링하거나(또는 콜백을 등록) 합니다.

입력에 무엇이 필요한가? 정확히 두 개의 필수 필드—prompt(움직임)와 image_url(첫 프레임의 공개 URL)—에 선택적 resolution(540p/720p/1080p)이 더해집니다. 그 밖의 모든 것은 거부됩니다.

왜 길이나 화면 비율을 설정할 수 없나? 클립 길이가 고정(약 5초)이라 duration 필드가 없고, 출력 화면 비율은 입력 이미지에서 상속되므로 aspect_ratio 필드가 없습니다. 보내면 둘 다 400을 반환합니다. 선택 가능한 길이가 필요하면 Ray 3.2 텍스트-투-비디오를 사용하세요.

생성 호출은 200을 반환했는데 비디오가 오지 않습니다. 왜? 가장 흔한 원인은 image_url 도달 불가입니다. 생성 호출은 요청 형태만 검증하므로 죽은 링크도 taskId를 반환합니다—그러고 작업이 fail로 끝납니다. 종료 상태까지 폴링하고 data.error를 읽으세요. 이미지가 공개적으로 가져올 수 있는지 확인하세요(인증 불필요, 로컬 경로 아님, base64 아님).

클립에 오디오가 있나? 없습니다—Ray 3.2 이미지-투-비디오는 무음 MP4를 반환합니다. 후처리에서 사운드를 추가하거나 별도 오디오 모델을 사용하세요.

클립 하나에 얼마인가? 클립 단위로 과금되며, 길이가 고정이므로 resolution이 유일한 수단입니다—1080p 클립 비용은 540p의 몇 배입니다. 가격은 변동되므로 숫자를 하드코딩하지 말고 가격 페이지를 확인하고, 먼저 540p로 프로토타이핑하세요.

얼마나 기다린 뒤 포기해야 하나? 수십 초에서 몇 분이 정상입니다. 폴링 루프에서 비종료 data.status를 허용하고 넉넉한 마감 시간(10–15분)을 두거나, "when": "final"이 붙은 callback을 등록해 간격을 추측하지 말고 hiapi가 알려주게 하세요.

영구 AI 아티팩트로 빌드 시작하기

API 키를 받아 태스크 기반의 하나의 API로 이미지, 비디오, 오디오 모델을 호출하세요.

API 문서 보기