영상 생성하고 파일 받기
Seedance로 영상을 생성하고 완성된 파일을 다운로드하세요.
작업 ID로 진행 상태를 조회한 뒤 파일이 준비되면 다운로드 링크를 발급받습니다.
작업을 새로 시작하거나 이어받기
| 지금 가진 정보 | 다음 행동 |
|---|---|
| 아직 영상 요청을 보내지 않음 | 아래 시작 조건을 확인한 뒤 한 번 제출하고 작업 ID를 저장 |
| 이미 작업 ID가 있음 | 같은 작업의 상태 확인 → 준비된 파일 받기 → 비용 확인 |
| 요청 응답을 받지 못해 ID가 없음 | 같은 생성을 자동으로 다시 보내지 말고 접수 여부 확인 |
시작하기 전에
AI 게이트웨이 API 키와 Seedance 모델 byteplus-seedance-2.5를 사용할 수 있는 스페이스가 필요합니다. 모델 선택에서 이용 가능 여부와 단가를 확인하고, 키가 없다면 API 키에서 발급합니다. 이용 조건이 불분명하면 플랫폼에 로그인한 뒤 플랫폼 내 문의 기능을 사용합니다.
영상 생성 요청을 보내면 완성된 파일 대신 작업 ID를 받습니다. 이 ID로 진행 상태를 조회하고, 파일이 준비되면 다운로드합니다. 작업의 생성 성공, 파일 준비와 비용은 각각 확인해야 합니다.
아래 요청 본문을 자신의 서버 코드에서 사용할 수 있습니다. 로컬에서 직접 실행하려면 Python 예제를 사용합니다.
영상 생성 요청하기
다음 JSON 본문을 Video Generations에 보냅니다. 요청 주소와 인증 헤더는 해당 API 문서에서 확인합니다. 텍스트만 입력하는 예제이므로 파일 업로드는 필요 없습니다.
{
"model": "byteplus-seedance-2.5",
"prompt": "바람에 천천히 흔들리는 초원의 풀을 고정 카메라로 촬영한 영상",
"duration": 5,
"resolution": "480p",
"store_media": false
}이 요청은 5초, 480p 영상을 생성하며 기본 비율은 16:9, 오디오 생성은 켜진 상태입니다. 생성 요청에는 비용이 발생할 수 있습니다. 이미지나 영상에서 시작하려면 Seedance 입력 방식을 확인합니다.
store_media: false는 결과를 바로 다운로드하기 위한 임시 보관입니다. 임시 보관기간은 약 1시간이며 파일이 준비된 순간부터 1시간을 보장하지 않습니다. 나중에 받거나 다시 사용할 결과는 보관 조건을 확인해 true를 지정합니다. 옵션을 생략하면 스페이스 설정을 따르므로 필요한 보관기간을 먼저 확인하세요.
HTTP 200 응답의 id를 저장합니다. 예를 들어 {"id": "lst-example-job-id"}를 받았다면 이후에는 이 ID로 같은 작업을 조회합니다. 응답을 받지 못했다고 생성 요청을 바로 다시 보내면 중복 작업과 비용이 발생할 수 있습니다. ID도 받지 못했다면 요청 시각과 스페이스 이름으로 플랫폼 내 문의 기능에서 접수 여부를 확인합니다.
작업 진행 상태 확인하기
저장한 작업 ID로 Jobs를 조회합니다. 응답의 data.job.status를 확인하세요.
| 작업 상태 | 다음 행동 |
|---|---|
queued, running | 간격을 두고 같은 ID로 다시 조회 |
succeeded | data.job.result.media에서 영상 파일의 준비 상태 확인 |
failed | data.job.error로 실패 원인 확인 후 입력 수정 또는 문의 |
| HTTP 조회 오류 | 작업 ID, 스페이스와 오류 코드 확인. 새 영상을 자동으로 생성하지 않음 |
succeeded여도 파일 복사가 끝나지 않았을 수 있습니다. result.media의 영상 항목이 pending이면 기다렸다가 조회하고, ready이면 그 항목의 media_id를 저장합니다. unavailable이면 파일을 받을 수 있는 상태가 아닙니다. 응답에 Retry-After가 있으면 안내한 시간 뒤 다시 확인합니다.
결과 파일 받기
영상의 media_id로 Media Links를 호출합니다. 받은 url을 그대로 사용하고 리다이렉트를 따라 파일을 다운로드합니다. 이 링크에는 접근 권한이 포함되어 있으므로 다운로드 요청에 AI 게이트웨이 API 키를 추가하지 않습니다.
저장된 파일의 크기와 영상 재생을 확인하면 다운로드가 완료된 것입니다. 애플리케이션에서 사용할 영상은 자신의 저장소에 보관하세요. 임시 서명 링크를 장기 재생 주소로 사용하면 링크가 만료된 뒤 영상을 열 수 없습니다.
| 파일을 받지 못한 이유 | 다음 행동 |
|---|---|
| 파일 준비 중 | Retry-After가 있으면 안내한 시간 뒤 같은 미디어 ID로 링크 요청 |
| 다운로드 링크 만료 | 파일 보관기간 안이면 같은 미디어 ID로 새 링크 발급 |
| 파일 보관 만료 | 새 링크로 복구할 수 없음. 이미 받은 사본을 확인하거나 문의 |
| 파일 사본 사용 불가 | 작업 ID와 오류 코드로 문의 |
링크 만료시각은 url_expires_at, 파일 보관 만료시각은 expires_at입니다. 링크 재발급은 파일 보관기간을 늘리지 않습니다. 자세한 오류 응답은 Media Links에서 확인합니다.
작업 비용 확인하기
비용 확인은 파일 다운로드와 별도로 수행합니다. 파일을 받지 못했거나 비용 필드가 없다고 무료 또는 자동 환불로 판단하지 않습니다.
usage는 나중에 추가될 수 있습니다. 아직 없다면 확인하지 못한 비용으로 남기고 다시 조회합니다. 작업별로 추적하려면 작업 ID, 모델, 요청 시각과 확인한 비용을 함께 기록합니다. 작업 비용과 최종 차감 및 집계의 차이는 요청별 비용을 확인합니다.
Python으로 실행하기
Python 3.8 이상과 AI 게이트웨이 API 키가 필요합니다. 표준 라이브러리만 사용하므로 추가 패키지를 설치하지 않습니다. 아래 코드를 video_job.py로 저장하세요. 앞의 JSON 본문은 store_media: false로 임시 보관을 선택했습니다. 이 선택 실행 예제는 store_media를 생략해 스페이스의 보관 설정을 따릅니다. 임시 보관을 선택하려면 코드의 생성 요청 본문에 "store_media": False를 추가합니다.
서버 환경 변수 LETSUR_API_KEY에서 키를 읽습니다. 변수가 없으면 키를 비밀번호 방식으로 입력받으며 화면에 표시하지 않습니다. 신뢰할 수 있는 터미널에서 실행하고 키를 코드나 셸 명령에 직접 쓰지 마세요.
video_job.py 전체 코드
import getpass
import json
import os
import sys
from pathlib import Path
from urllib.error import HTTPError
from urllib.parse import quote
from urllib.request import Request, urlopen
BASE = "https://gw.letsur.ai"
RECORD = Path("video-job.json")
key = os.environ.get("LETSUR_API_KEY") or getpass.getpass("AI 게이트웨이 API 키: ")
if not key.strip():
raise SystemExit("API 키가 필요합니다.")
def api(method, path, body=None):
headers = {"Authorization": "Bearer " + key}
data = None
if body is not None:
headers["Content-Type"] = "application/json"
data = json.dumps(body).encode("utf-8")
request = Request(BASE + path, data=data, headers=headers, method=method)
try:
with urlopen(request, timeout=60) as response:
return json.load(response)
except HTTPError as error:
try:
payload = json.load(error)
except (ValueError, OSError):
payload = {}
# source_url 등 민감한 값을 통째로 출력하지 않습니다.
problem = payload.get("error", payload)
code = problem.get("code", problem.get("type", "unknown"))
retry_after = error.headers.get("Retry-After")
if retry_after:
print("다음 조회 또는 링크 요청까지 기다릴 시간:", retry_after)
raise SystemExit(f"HTTP {error.code}, 오류: {code}. 새 생성을 자동 재시도하지 않습니다.")
def current_job():
record = json.loads(RECORD.read_text())
job_id = record["id"]
return api("GET", "/v1/jobs/" + quote(job_id, safe=""))["data"]["job"]
command = sys.argv[1] if len(sys.argv) == 2 else ""
if command == "submit":
# 기존 작업 ID가 있으면 덮어쓰거나 새 요청을 보내지 않습니다.
with RECORD.open("x") as saved:
saved.write('{}')
accepted = api("POST", "/v1/video/generations", {
"model": "byteplus-seedance-2.5",
"prompt": "바람에 천천히 흔들리는 초원의 풀을 고정 카메라로 촬영한 영상",
"duration": 5,
"resolution": "480p",
"aspect_ratio": "16:9",
"generate_audio": True,
})
RECORD.write_text(json.dumps({"id": accepted["id"]}))
print("접수한 작업 ID:", accepted["id"])
elif command == "status":
job = current_job()
print("작업:", job["id"], "상태:", job["status"])
for media in job.get("result", {}).get("media", []):
print("파일:", media["media_id"], media["kind"], media["state"])
if media.get("url_unavailable_reason"):
print("파일 링크를 사용할 수 없는 이유:", media["url_unavailable_reason"])
if "usage" in job:
print("작업 비용:", json.dumps(job["usage"], ensure_ascii=False))
else:
print("작업 비용이 아직 응답에 없습니다.")
if "error" in job:
print("작업 오류 유형:", job["error"]["type"])
elif command == "download":
job = current_job()
if job["status"] == "failed":
raise SystemExit("작업이 실패했습니다. status로 같은 작업 ID의 오류 유형을 확인하세요.")
if job["status"] != "succeeded":
raise SystemExit("작업이 대기 또는 처리 중입니다. 잠시 뒤 status로 같은 작업 ID를 다시 확인하세요.")
videos = [m for m in job.get("result", {}).get("media", []) if m["kind"] == "video"]
if not videos:
raise SystemExit("영상 결과가 없습니다. status로 작업 상태를 확인하세요.")
if any(media.get("state") == "unavailable" for media in videos):
raise SystemExit("영상 파일을 지금 사용할 수 없습니다. status로 같은 작업 ID의 파일 상태와 이유를 확인하고 필요하면 문의하세요.")
if any(media.get("state") != "ready" for media in videos):
raise SystemExit("영상 파일이 준비 중입니다. 잠시 뒤 status로 같은 작업 ID의 파일 상태를 다시 확인하세요.")
for index, media in enumerate(videos, 1):
link = api("POST", "/v1/media/" + quote(media["media_id"], safe="") + "/link")
output = Path(f"video-{index}.mp4")
# 다운로드 URL에는 API 키 헤더를 보내지 않습니다.
with urlopen(link["url"], timeout=60) as response, output.open("xb") as target:
while chunk := response.read(1024 * 1024):
target.write(chunk)
print("다운로드 완료:", output, output.stat().st_size, "bytes")
else:
raise SystemExit("사용법: python3 video_job.py submit|status|download")Bash 또는 zsh에서 파일을 저장한 디렉터리로 이동한 뒤 다음 순서로 실행합니다. submit만 새 영상을 생성합니다.
| 명령 | 확인할 결과 |
|---|---|
python3 video_job.py submit | 작업 ID 출력, video-job.json에 ID 저장 |
python3 video_job.py status | 같은 작업의 상태와 제공되는 경우 비용 출력 |
python3 video_job.py download | 작업이 succeeded이고 영상 파일이 ready일 때 video-1.mp4부터 저장, 다운로드 완료 메시지 출력 |
작업이 대기 또는 처리 중이면 잠시 뒤 status를 다시 실행합니다. 코드의 60초는 HTTP 요청 제한시간이며 영상 생성 완료시간이 아닙니다. 다운로드 뒤 파일 크기와 영상 재생도 확인하세요.
예제는 기존 작업 기록과 영상 파일을 덮어쓰지 않습니다. 응답 유실로 video-job.json이 {}인 채 남았다면 기록을 지우고 다시 생성하지 말고 접수 여부부터 확인합니다. 나중에 ID를 확보했다면 {"id": "확보한 작업 ID"}로 기록을 복원합니다. 다운로드 중단으로 일부만 받은 파일은 확인 후 다른 위치로 옮기고 재실행합니다.
문제가 계속될 때
입력 거절은 요청 필드와 조합을, 조회 불가는 작업 ID와 스페이스를, 다운로드 실패는 파일 준비와 만료 상태를 확인합니다. 비용이 계속 없거나 집계와 다르면 조회 시각과 범위도 함께 확인합니다.
Python 예제는 오류 코드와 Retry-After만 표시합니다. 상세 응답이 필요하면 자신의 서버에서 비공개로 확인합니다. 해결되지 않으면 플랫폼 내 문의 기능에 스페이스 이름, 요청 시각, 오류 코드와 확보한 작업 ID를 남깁니다. API 키, 서명 링크, 고객 입력 원문과 전체 응답을 공개 로그나 문의 내용에 넣지 마세요.
다음 영상을 이어 만들려면 마지막 프레임으로 다음 영상 만들기에서 앞 영상의 마지막 이미지를 사용하는 방법을 확인합니다.