튜토리얼
HiAPI에서 Veo 3.1 Lite 이미지-투-비디오 사용하기: curl 및 Python 튜토리얼
veo-3.1-lite/image-to-video가 할 수 있는 일
veo-3.1-lite/image-to-video는 첫 프레임을 비디오로 변환하는 모델입니다. 정지 이미지를 주면 그 프레임을 애니메이션화하여 짧은 비디오를 생성합니다. 제공하는 이미지는 스타일 참조나 모호한 영감의 원천이 아니라, 출력 비디오의 문자 그대로의 시작 프레임입니다. 따라서 구도, 피사체 위치, 프레이밍이 움직임의 첫 몇 초로 직접 이어집니다.
이 설계 선택은 입력을 어떻게 바라봐야 하는지를 결정합니다. 조명이 충분하고 선명하며 적절히 크롭된 이미지는 예상한 위치에서 시작하는 비디오를 생성합니다. 반면 흐릿하거나 이상하게 프레이밍된 이미지는 흐릿하고 이상하게 프레이밍된 채로 시작하는 비디오를 생성하며, 모델이 움직임을 생성하면서 더욱 흘러가게 됩니다. 이 모델을 잘 활용하는 노력의 대부분은 코드를 한 줄도 작성하기 전에 이루어집니다.
출력 측면에서 모델은 네이티브 오디오가 포함된 비디오를 반환합니다. 이것이 구식 이미지-투-비디오 파이프라인과 다른 점입니다. 구식 흐름은 먼저 무음 영상을 생성한 뒤 별도의 독립 오디오 트랙을 이어 붙입니다. 반면 여기서는 오디오가 동일한 작업의 일부로 생성되므로, 환경음이 시각적 장면과 일관성을 유지합니다. 수동 믹싱이 필요 없고, 음향 효과와 그에 대응하는 움직임 사이에 시간적 어긋남도 발생하지 않습니다.
생성은 비동기입니다. 작업을 제출하고 식별자를 받은 다음 완료될 때까지 폴링합니다. 이는 의도적인 아키텍처 선택입니다. 비디오 생성은 일반적인 HTTP 요청 타임아웃보다 훨씬 오래 걸리므로, API는 제출과 완료를 분리합니다. 이 튜토리얼의 모든 예제는 이러한 '먼저 제출, 그다음 폴링' 형태를 따릅니다.
요약하자면: 이미지를 하나 입력하고, 오디오가 포함된 비디오 하나를 출력받으며, 비동기 작업을 통해 전달됩니다. 이 멘탈 모델만 기억하면 API의 나머지 부분은 모두 직관적입니다.
첫 요청을 보내기 전에
첫 호출에 성공하려면 세 가지를 준비해야 합니다.
API 키
HiAPI.ai에 대한 모든 요청은 API 키로 인증합니다. 계정 대시보드에서 하나를 생성한 다음, 소스 파일에 직접 붙여넣지 말고 환경 변수에서 불러오세요:
export HIAPI_API_KEY="your_api_key_here"
키가 리포지토리에 커밋된 적이 있다면 즉시 교체하세요. 키 교체 비용은 저렴하지만 유출 비용은 비쌉니다.
접근 가능한 첫 프레임 이미지
모델이 이미지를 직접 가져와야 하므로, 이미지는 URL을 통해 접근 가능해야 합니다. 공개 객체 스토리지 링크, CDN 경로, 또는 대화형 인증이 필요 없는 HTTP 엔드포인트면 됩니다. 유효 기간이 긴 서명 URL은 괜찮지만, 로그인 세션이 필요한 URL은 안 됩니다. PNG나 JPEG 같은 일반적인 형식을 사용하고, 적절한 해상도를 선택하세요. 선명하고 적당한 크기의 이미지는 대역폭을 낭비하지 않으면서 모델에 깨끗한 세부 정보를 제공합니다.
원본 이미지가 내부 서비스 뒤에 있다면 먼저 객체 스토리지에 업로드한 다음 생성된 URL을 전달하세요. 이는 첫 시도 실패의 가장 흔한 원인입니다.
엔드포인트 경로
이 모델은 veo-3.1-lite/image-to-video로 식별됩니다. 이 문자열을 작업과 함께 전송되는 모델 식별자로 취급하고, 열두 군데에 하드코딩하지 마세요. 나중에 다른 모델 변형으로 전환할 가능성이 높으므로 상수에 넣어두세요.
키를 내보내고, 가져올 수 있는 이미지 URL을 확보하고, 모델 식별자를 파악했다면 제출할 준비가 된 것입니다.
작동하는 curl 요청
제출 호출은 생성 작업을 만듭니다. 완전하고 실행 가능한 예시는 다음과 같습니다:
curl -X POST "https://api.hiapi.ai/v1/video/generations" \
-H "Authorization: Bearer $HIAPI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "veo-3.1-lite/image-to-video",
"image": "https://your-bucket.example.com/first-frame.jpg",
"prompt": "아침 햇살이 장면을 따라 천천히 움직이고, 카메라가 서서히 다가간다",
"duration": 5
}'
하나씩 살펴보겠습니다:
- URL은 HiAPI의 생성 엔드포인트입니다. 모든 모델 트래픽이 이 하나의 인터페이스를 통과하므로, 이후 모델을 바꿀 때는 엔드포인트가 아니라
model필드만 변경하면 됩니다. Authorization헤더는 Bearer 토큰을 전달합니다.$HIAPI_API_KEY표기는 환경 변수에서 읽으므로 키가 셸 기록에 남지 않습니다.model은veo-3.1-lite/image-to-video를 식별합니다.image는 첫 프레임 URL입니다. 바로 이 필드 덕분에 이 호출이 텍스트-투-비디오가 아닌 이미지-투-비디오가 됩니다.prompt는 선택 사항이지만 강력히 권장합니다. 모델이 이미 그 이미지를 볼 수 있으므로, 정지 화면에 대한 설명이 아니라 원하는 움직임, 즉 카메라 이동, 조명 변화, 피사체 동작을 설명합니다.duration은 요청하는 비디오 길이(초)이며, 모델 지원 범위에 제한됩니다.
응답은 id를 포함하는 작업 객체입니다. 저장해 두세요:
{
"id": "job_abc123",
"status": "queued",
"model": "veo-3.1-lite/image-to-video"
}
상태가 queued 또는 processing인 것은 예상된 결과입니다. 아직 비디오를 받은 것이 아니라 약속만 받은 것입니다. 다음 섹션에서는 그 약속을 파일로 바꾸는 방법을 설명합니다.
같은 호출을 Python으로 작성하기
이 curl 호출은 requests로 그대로 Python으로 옮길 수 있습니다. 아래 버전은 제출을 작은 함수로 캡슐화하여 파이프라인에서 재사용하기 쉽게 만듭니다:
import os
import requests
API_KEY = os.environ["HIAPI_API_KEY"]
BASE_URL = "https://api.hiapi.ai/v1"
HEADERS = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
}
def submit_video_job(image_url: str, prompt: str, duration: int = 5) -> str:
payload = {
"model": "veo-3.1-lite/image-to-video",
"image": image_url,
"prompt": prompt,
"duration": duration,
}
response = requests.post(
f"{BASE_URL}/video/generations",
headers=HEADERS,
json=payload,
timeout=30,
)
response.raise_for_status()
data = response.json()
print(f"Submitted job {data['id']} ({data['status']})")
return data["id"]
if __name__ == "__main__":
job_id = submit_video_job(
image_url="https://your-bucket.example.com/first-frame.jpg",
prompt="아침 햇살이 장면을 따라 천천히 움직이고, 카메라가 서서히 다가간다",
)
몇 가지 세부 사항을 유지할 가치가 있습니다:
raise_for_status()는 비 2xx 응답을 조기에 예외로 바꿉니다. 이것이 없으면 인증 오류나 유효성 검사 오류가 성공했지만 예상치 못한 응답 본문을 반환한 호출처럼 보입니다.- 30초 타임아웃은 제출에만 적용됩니다. 제출은 빠르게 반환되어야 하며, 오래 걸리는 작업은 서버 측에서 일어납니다. 제출 자체가 타임아웃된다면 문제는 생성 속도가 아니라 연결성이나 요청 페이로드 크기입니다.
- 전체 응답 대신 작업 id를 반환하면 함수가 조합 가능한 상태로 유지됩니다. 이 작업을 어떻게 처리할지는 호출자가 결정합니다.
이 시점에서 이 함수는 수정 없이 큐 워커, Flask 엔드포인트, 또는 노트북 셀에 넣을 수 있습니다.
비동기 작업 폴링
비디오 생성은 몇 밀리초가 아니라 몇 분이 걸립니다. 인간의 리듬에 가까운 간격으로 작업 상태 엔드포인트를 폴링하고, 작업이 종료 상태에 도달하면 즉시 중단하세요:
import time
import requests
def wait_for_video(job_id: str, timeout: int = 600, interval: int = 10) -> dict:
deadline = time.time() + timeout
url = f"{BASE_URL}/video/generations/{job_id}"
while time.time() < deadline:
response = requests.get(url, headers=HEADERS, timeout=30)
response.raise_for_status()
data = response.json()
status = data["status"]
print(f"Job {job_id}: {status}")
if status == "completed":
return data
if status == "failed":
raise RuntimeError(f"Generation failed: {data}")
time.sleep(interval)
raise TimeoutError(f"Job {job_id} did not finish within {timeout}s")
세 가지 실천이 프로덕션 환경에서 이를 안정적으로 작동하게 합니다:
- 지수 백오프. 짧은 간격으로 시작해 점진적으로 늘리고, 합리적인 상한을 설정하세요. 첫 폴링은 보통 여전히
queued를 반환하므로, 처음 몇 초 동안 엔드포인트를 두드리는 것은 아무 이득이 없습니다. - 하드 타임아웃. 항상 총 대기 시간에 상한을 두세요.
processing에 갇힌 작업은 무한 루프가 아니라 경보를 울릴 수 있는 오류로 나타나야 합니다. - 작업 id 영속화. 폴링을 시작하기 전에 데이터베이스에 기록하세요. 프로세스가 대기 도중 충돌하더라도, 재시작 후 다시 제출하여 같은 작업에 두 번 비용을 지불하지 않고 폴링을 계속할 수 있습니다.
루프를 끝내는 상태로는 completed와 failed만 간주하세요. queued, processing 등 나머지는 모두 계속 기다려야 한다는 의미입니다.
한 가지 더: 웹 서버에서 이 코드를 실행한다면 요청 핸들러 안에서 폴링하지 마세요. 제출하고, 작업 id를 클라이언트에 반환하고, 백그라운드 워커가 대기를 완료하게 하세요. 그렇지 않으면 느린 생성 하나가 연결을 몇 분 동안 점유합니다.
출력 비디오 다운로드
작업이 완료되면 상태 응답에 완성된 비디오의 URL이 포함됩니다. 바이너리 데이터로 다운로드하고 적절한 확장자로 디스크에 쓰세요:
def download_video(job: dict, path: str = "output.mp4") -> str:
video_url = job["output"]["url"]
response = requests.get(video_url, headers=HEADERS, timeout=120, stream=True)
response.raise_for_status()
with open(path, "wb") as f:
for chunk in response.iter_content(chunk_size=1 << 20):
f.write(chunk)
print(f"Saved {path}")
return path
몇 가지 제대로 해야 할 일이 있습니다:
- 바이너리 모드로 쓰기.
"wb"대신"w"로 열면 파일이 조용히 손상됩니다. 다운로드는 성공한 것처럼 보이지만 비디오가 재생되지 않습니다. - 대용량 파일 스트리밍. 위의
stream=True와 청크 복사는 비디오 크기에 관계없이 메모리를 안정적으로 유지합니다. - 체크섬 계산은 대규모로 출력을 아카이빙할 때 하세요. 한 줄의 코드지만 잘린 다운로드가 사용자에게 도달하기 전에 잡아낼 수 있습니다.
오디오에 관해: 반환되는 비디오에는 이미 네이티브 오디오 트랙이 포함되어 있습니다. 가져와야 할 별도의 오디오 파일도, 믹싱 단계도 없습니다. 다운스트림에서 파일을 다시 컨테이너화한다면(예: 컨테이너 형식 변경) 오디오 스트림도 함께 가져가세요. 흔한 실수는 기본적으로 오디오를 제거하는 변환을 실행한 뒤 오디오가 어디로 갔는지 의아해하는 것입니다.
마지막으로, 출력 URL은 보통 유효 기간이 있습니다. URL을 며칠 후에 가져오려 하지 말고, 가능한 한 빨리 다운로드하여 자체 스토리지 버킷에 저장하세요. 그때쯤이면 링크가 만료되었을 수 있습니다.
일반적인 실패 지점
이미지-투-비디오 호출 문제를 해결할 때 가장 많은 시간을 소비하게 하는 오류는 다음과 같습니다:
이미지 URL에 접근 불가
이는 단연 가장 흔한 실패 원인입니다. 모델이 서버 측에서 이미지를 가져오므로, 로그인된 브라우저에서 작동하는 URL은 아무것도 증명하지 못합니다. 작업을 제출하기 전에 인증 없는 일반 요청으로 테스트하세요. 서명 URL의 유효 기간은 생성 시간보다 훨씬 길어야 합니다.
모델 오류처럼 보이는 인증 오류
누락되었거나 형식이 잘못된 Authorization 헤더는 보통 명확한 401을 반환하지만, 때때로 개발자가 상태 코드를 삼키는 로직으로 초기화 코드를 감싼 뒤 '모델 장애'라고 보고합니다. 요청 페이로드를 디버깅하기 전에 원시 상태 코드를 먼저 확인하세요.
너무 자주 폴링하거나 전혀 폴링하지 않음
100밀리초마다 폴링하는 것은 할당량을 낭비하고 속도 제한을 유발할 수 있습니다. 한 번만 폴링하고 processing을 보고 포기하는 것은 작업이 성공했지만 아무도 모르는 상태를 의미합니다. 위의 백오프와 타임아웃 패턴을 따르세요.
제출을 완료로 착각함
제출이 성공적으로 반환하는 것은 비디오가 아니라 작업입니다. 제출 응답에서 url 필드를 읽는 코드는 아무것도 찾지 못합니다. 출력을 담고 있는 것은 완료 상태 응답뿐입니다.
프롬프트가 이미지를 설명함
"고양이가 창턱에 앉아 있다" 같은 프롬프트는 모델이 이미 본 내용을 되풀이할 뿐입니다. 움직임을 설명하는 프롬프트, 예를 들어 "비가 내리기 시작하며 카메라가 서서히 멀어진다"는 더 나은 결과를 만듭니다. 내용이 아니라 변화를 설명하세요.
충돌 시 작업 id 손실
프로세스가 제출과 완료 사이에 죽으면, 영속화되지 않은 작업 id는 작업을 복구할 수 없게 만듭니다. 이를 영속화하고 워커에서 비동기로 폴링하세요. 작은 설계 선택이 가장 긴 디버깅 과정을 피하게 해줍니다.
관련 글
모든 아티클 보기 →-
튜토리얼
HiAPI API로 hailuo-2.3-fast/image-to-video 호출하는 방법: curl과 Python 실전
빠르고 저렴한 이미지-투-비디오 등급의 첫 실행 요청 — 모델 ID부터 MP4 다운로드까지
-
튜토리얼
HiAPI API로 hailuo-2.3/text-to-video를 호출하는 방법: curl, Python 및 실행 가능한 요청 예시
-
튜토리얼
HiAPI에서 Kandinsky 6.0 Pro 텍스트-투-비디오 사용하기: curl 및 Python 튜토리얼
요청 우선 실전 데모: 프롬프트에서 비디오 클립까지의 호출 구성, 요청 제출, 완료 상태 폴링 및 결과 다운로드