February 27, 2026

Seedance 2.0 API 튜토리얼: Python으로 첫 AI 비디오 생성하기

Python으로 Seedance 2.0 API를 호출하여 AI 비디오를 생성하는 단계별 튜토리얼. 텍스트-투-비디오, 이미지-투-비디오, 비동기 polling, webhook, 오류 처리를 다룹니다.

Seedance 2.0 API 튜토리얼: Python으로 첫 AI 비디오 생성하기

Seedance 2.0은 ByteDance의 가장 강력한 AI 비디오 생성 모델입니다 — 멀티모달 참조, 네이티브 오디오 생성, 시네마틱 카메라 제어, 최대 1080p에서 4~15초 비디오 생성을 지원합니다. 이 튜토리얼에서는 API Key 발급부터 첫 번째 비디오 다운로드까지 Python으로 전체 API 워크플로우를 안내합니다.

이 튜토리얼을 마치면 텍스트-투-비디오, 이미지-투-비디오, 비동기 polling, webhook 처리, 오류 복구를 위한 바로 실행할 수 있는 코드를 갖게 됩니다. 모든 코드 예제는 실제 API에서 테스트되었습니다.

참고 — Seedance 2.0 vs 1.5: Seedance 2.0은 점진적으로 출시 중입니다. 지금 바로 seedance-1.5-pro로 전체 워크플로우를 테스트할 수 있습니다 — 2.0이 완전히 출시되면 모델 이름만 변경하면 됩니다. 모든 엔드포인트, 파라미터, 응답 형식이 동일합니다. 2.0의 주요 차이점: 멀티모달 참조(이미지, 비디오, 오디오 혼합 입력), 네이티브 오디오 생성, 향상된 물리 시뮬레이션, 비디오 편집 기능. 이 튜토리얼의 모든 내용은 두 버전 모두에서 작동합니다.

무료 API Key 발급받기 — 튜토리얼을 따라해 보세요.


이 튜토리얼에서 만들 것 (그리고 필요한 것)

Seedance로 생성한 비디오가 어떤 모습인지 먼저 확인해 보세요 — 단 한 번의 API 호출로 만들어졌습니다:

이 튜토리얼에서 작성할 Python 코드:

  1. 텍스트 프롬프트 전송 → 생성된 비디오 수신
  2. 이미지 전송 → 비디오로 애니메이션화
  3. 비동기로 결과 polling
  4. 프로덕션 코드처럼 오류와 재시도 처리
  5. webhook으로 결과 수신 (polling 불필요)
  6. 진행 중인 작업 취소

사전 요구사항

  • Python 3.8+ (python3 --version으로 확인)
  • requests 라이브러리 (pip install requests)
  • EvoLink API Key (무료 가입 — 다음 섹션에서 발급 방법을 안내합니다)

GPU도, Docker도, 복잡한 설정도 필요 없습니다. Python과 API Key만 있으면 됩니다.

팁: 프로덕션 앱을 개발 중이라면 가상 환경으로 의존성을 격리하는 것을 권장합니다:

python3 -m venv seedance-env
source seedance-env/bin/activate  # macOS/Linux
seedance-env\Scripts\activate     # Windows
pip install requests flask

API Key 발급

Seedance 2.0은 EvoLink를 통해 제공됩니다. EvoLink는 하나의 API Key로 Seedance 2.0, Kling 등 여러 AI 비디오 모델에 통합 접근할 수 있는 API 게이트웨이입니다.

시작하는 방법:

  1. evolink.ai/early-access에서 계정 생성
  2. Dashboard → API Keys로 이동
  3. Create New Key 클릭
  4. Key 복사 — sk-로 시작합니다

Key를 안전하게 보관하세요. 버전 관리에 커밋하지 마세요. 환경 변수를 사용합니다:

export EVOLINK_API_KEY="sk-your-api-key-here"

이 명령은 현재 터미널 세션에서 EVOLINK_API_KEY 환경 변수를 설정합니다. macOS/Linux에서는 ~/.bashrc 또는 ~/.zshrc에 추가하여 영구적으로 유지할 수 있습니다. Windows에서는 명령 프롬프트에서 set EVOLINK_API_KEY=sk-your-api-key-here를 사용하거나, 시스템 속성 → 환경 변수에서 설정하세요.

계정에는 체험용 초기 크레딧이 포함되어 있습니다. 현재 가격 정보는 시작하기 문서를 확인하세요.

흔한 실수: API Key를 소스 파일에 하드코딩하지 마세요. GitHub에 푸시하면 자동화된 크롤러가 몇 분 안에 찾아냅니다. 반드시 환경 변수나 AWS Secrets Manager, HashiCorp Vault 같은 시크릿 관리 서비스를 사용하세요.


Python 환경 설정

필요한 유일한 의존성을 설치합니다:

pip install requests

seedance_tutorial.py라는 파일을 만들고 다음 기본 코드를 추가하세요. 이 튜토리얼의 모든 예제는 이 기반 위에 구축됩니다:

import requests
import time
import os
import json

# ── 설정 ─────────────────────────────────────────────────────
API_KEY = os.getenv("EVOLINK_API_KEY", "sk-your-api-key-here")
BASE_URL = "https://api.evolink.ai/v1"
HEADERS = {
    "Authorization": f"Bearer {API_KEY}",
    "Content-Type": "application/json"
}

한 줄씩 설명합니다:

  • os.getenv("EVOLINK_API_KEY", "sk-your-api-key-here") — 환경 변수에서 API Key를 읽습니다. 두 번째 인자는 기본값입니다(로컬 테스트 시에만 실제 Key로 교체하세요).
  • BASE_URL — 모든 EvoLink API 엔드포인트의 루트 URL입니다. 모든 요청은 https://api.evolink.ai/v1/...으로 전송됩니다.
  • HEADERS — 모든 요청에 포함되는 두 개의 헤더: Authorization은 Bearer Token 방식으로 API Key를 전달하고, Content-Type은 서버에 JSON을 보내고 있음을 알립니다.

다음으로 재사용 가능한 헬퍼 함수를 추가합니다:

# ── 재사용 가능한 Polling 헬퍼 ────────────────────────────────
def wait_for_video(task_id, poll_interval=10, timeout=600):
    """
    비디오 생성 작업을 완료 또는 실패할 때까지 polling합니다.
    
    Args:
        task_id: 생성 엔드포인트가 반환한 작업 ID.
        poll_interval: polling 간격 초(기본 10).
        timeout: 최대 대기 시간 초(기본 600).
    
    Returns:
        dict: 비디오 URL이 포함된 완료된 작업 응답.
    
    Raises:
        TimeoutError: 작업이 타임아웃 내에 완료되지 않은 경우.
        RuntimeError: 작업이 실패한 경우.
    """
    elapsed = 0
    while elapsed < timeout:
        # GET 요청으로 작업의 현재 상태 확인
        response = requests.get(
            f"{BASE_URL}/tasks/{task_id}",
            headers=HEADERS
        )
        # HTTP 상태 코드가 오류를 나타내면 예외 발생
        response.raise_for_status()
        task = response.json()

        # 응답에서 상태와 진행률 추출
        status = task["status"]
        progress = task.get("progress", 0)
        print(f"  [{elapsed}s] Status: {status} | Progress: {progress}%")

        # 종료 상태 확인
        if status == "completed":
            return task
        elif status == "failed":
            error_info = task.get("error", {})
            raise RuntimeError(
                f"Task {task_id} failed: {error_info.get('message', 'Unknown error')}"
            )

        # 다음 polling 전 대기
        time.sleep(poll_interval)
        elapsed += poll_interval

    raise TimeoutError(f"Task {task_id} timed out after {timeout}s")

이 함수의 주요 설계 결정:

  • poll_interval=10 — 10초가 최적입니다. 더 빠르면 API 할당량을 낭비하고, 더 느리면 워크플로우가 지연됩니다.
  • timeout=600 — 10분은 충분히 여유 있습니다. 대부분의 비디오는 30~120초 내에 완료되지만, 큐 혼잡 같은 극단적인 경우를 커버합니다.
  • response.raise_for_status() — HTTP 오류(4xx/5xx)를 Python 예외로 변환하여 조용히 넘어가지 않게 합니다.
  • 진행률 출력[elapsed]s 접두사로 시간을 연관시킬 수 있습니다. 느린 생성을 디버깅할 때 유용합니다.
# ── 헬퍼: 비디오 다운로드 ─────────────────────────────────────
def download_video(url, filename="output.mp4"):
    """URL에서 비디오 파일을 다운로드합니다."""
    print(f"Downloading video to {filename}...")
    resp = requests.get(url, stream=True)
    resp.raise_for_status()
    with open(filename, "wb") as f:
        for chunk in resp.iter_content(chunk_size=8192):
            f.write(chunk)
    print(f"Saved: {filename} ({os.path.getsize(filename) / 1024:.0f} KB)")

이 함수는 전체 비디오를 메모리에 로드하는 대신 8 KB 청크 단위로 스트리밍 다운로드합니다. 이 점이 중요한데, 생성된 비디오는 10~50 MB가 될 수 있습니다. stream=True 파라미터는 requests에 점진적으로 다운로드하도록 지시합니다.

이 세 가지 — 설정, polling, 다운로드 — 가 기본 뼈대입니다. 아래의 모든 코드 예제에서 사용됩니다. 반복하지 않고 새로운 payload만 보여드리겠습니다.

전체 API 레퍼런스는 비디오 생성 문서를 참조하세요.


첫 번째 비디오 생성 (텍스트-투-비디오)

비디오를 생성해 봅시다. 스크립트에 다음 코드를 추가하세요:

# ── 텍스트-투-비디오 ──────────────────────────────────────────
def text_to_video():
    payload = {
        "model": "seedance-2.0",          # 사용할 AI 모델
        "prompt": (
            "A golden retriever puppy chases a butterfly through "
            "a sunlit meadow. The camera follows the puppy with a "
            "smooth tracking shot as wildflowers sway in the breeze."
        ),
        "duration": 5,                     # 비디오 길이: 4-15초
        "quality": "720p",                 # 해상도: 480p, 720p, 1080p
        "aspect_ratio": "16:9",            # 표준 와이드스크린
        "generate_audio": True             # AI가 매칭되는 오디오 생성
    }

    print("Submitting text-to-video request...")
    response = requests.post(
        f"{BASE_URL}/videos/generations",  # 비디오 생성 엔드포인트
        headers=HEADERS,                   # 인증 + content-type 헤더
        json=payload                       # 자동으로 JSON 직렬화
    )
    response.raise_for_status()            # 200이 아니면 예외 발생
    task = response.json()                 # JSON 응답 파싱

    # 응답의 주요 정보 출력
    print(f"Task created: {task['id']}")
    print(f"Estimated time: {task['task_info']['estimated_time']}s")
    print(f"Credits reserved: {task['usage']['credits_reserved']}")

    # 비디오가 준비될 때까지 polling
    result = wait_for_video(task["id"])

    # results 배열에 하나 이상의 비디오 URL 포함
    video_url = result["results"][0]
    print(f"\nVideo URL: {video_url}")
    download_video(video_url, "my_first_video.mp4")

    return result


if __name__ == "__main__":
    text_to_video()

payload의 각 파라미터를 설명합니다:

  • model — 사용할 Seedance 모델. 최신 버전은 seedance-2.0으로 설정하고, 해당 지역에서 2.0을 아직 사용할 수 없다면 seedance-1.5-pro를 사용하세요.
  • prompt — 비디오 설명. 주체, 동작, 카메라 움직임, 분위기를 구체적으로 기술하세요. 위 프롬프트는 세 부분 구조를 사용합니다: 주체("golden retriever puppy"), 동작("chases a butterfly"), 카메라("smooth tracking shot"). 고급 프롬프트 기법은 프롬프트 엔지니어링 가이드를 참조하세요.
  • duration — 비디오 길이(초, 4~15). 짧은 비디오가 더 빠르게 생성되고 크레딧도 적게 소모됩니다. 테스트에는 5초를 권장합니다.
  • quality — 해상도 등급. 720p는 개발 단계에서 품질과 속도의 최적 균형입니다. 480p는 빠른 반복에, 1080p는 최종 렌더링에 사용하세요.
  • aspect_ratio — 출력 비율. 16:9는 YouTube/가로, 9:16은 TikTok/Reels/Shorts, 1:1은 Instagram 피드에 적합합니다.
  • generate_audiotrue로 설정하면 Seedance가 시각적 콘텐츠에 맞는 환경음과 음악을 생성합니다. 생성 시간이 약 2초 추가됩니다.

실행:

python seedance_tutorial.py

API 응답 내용

생성 요청을 제출하면 즉시 task 객체를 받습니다 — 비디오는 아직 준비되지 않았습니다. 실제 응답은 다음과 같습니다:

{
  "created": 1772203771,
  "id": "task-unified-1772203771-yf1dxogh",
  "model": "seedance-2.0",
  "object": "video.generation.task",
  "progress": 0,
  "status": "pending",
  "task_info": {
    "can_cancel": true,
    "estimated_time": 132
  },
  "type": "video",
  "usage": {
    "billing_rule": "per_second",
    "credits_reserved": 17.784,
    "user_group": "default"
  }
}

주요 필드 설명:

필드의미
id작업 ID — 상태 확인과 결과 조회에 사용
statuspending으로 시작, processing을 거쳐 completed 또는 failed로 변경
progress0~100 백분율. processing 단계에서 실시간 업데이트
estimated_time예상 완료 시간(초), 서버 측 추정치
credits_reserved이 작업에 예약된 크레딧. 작업 실패 시 자동 환불
task_info.can_cancel작업 취소 가능 여부 (완료 전에는 항상 true)
created작업 제출 시간의 Unix 타임스탬프
usage.billing_rule과금 방식 — per_second는 비용이 길이에 비례

팁: 제출 후 즉시 id를 파일이나 데이터베이스에 저장하세요. polling 중 스크립트가 크래시되면 저장된 작업 ID로 wait_for_video()를 호출하여 복구할 수 있습니다. 작업은 서버에 24시간 보관됩니다.

Polling 과정

wait_for_video() 함수는 10초마다 polling합니다. 실제 출력은 다음과 같습니다:

Submitting text-to-video request...
Task created: task-unified-1772203771-yf1dxogh
Estimated time: 132s
Credits reserved: 17.784
  [0s] Status: pending | Progress: 0%
  [10s] Status: processing | Progress: 7%
  [20s] Status: processing | Progress: 13%
  [30s] Status: processing | Progress: 20%
  [40s] Status: processing | Progress: 27%
  [50s] Status: completed | Progress: 100%

Video URL: https://files.evolink.ai/.../cgt-20260227224931-8vl7s.mp4
Downloading video to my_first_video.mp4...
Saved: my_first_video.mp4 (2847 KB)

이것으로 끝입니다 — API 호출부터 비디오 파일 저장까지 약 50초.

중요: 비디오 URL은 24시간 후 만료됩니다. 반드시 즉시 파일을 다운로드하거나 자체 스토리지(S3, GCS, Cloudflare R2 등)에 저장하세요.

흔한 실수: 비디오 URL을 장기 저장용으로 사용하지 마세요. 파이프라인은 생성 완료 후 즉시 다운로드하도록 구축하세요. 비동기로 비디오를 처리하는 경우, webhook(아래에서 다룹니다)을 사용하여 준비되는 즉시 다운로드를 트리거하세요.

효과적인 프롬프트 작성 팁은 Seedance 2.0 프롬프트 가이드를 참조하세요 — 샷 스크립트 형식, 스타일 키워드, 타이밍 구문을 다룹니다.


결과 Polling: 비동기 워크플로우 이해하기

비디오 생성은 길이와 품질 설정에 따라 30~120초 이상 소요됩니다. API는 비동기 작업 패턴을 사용합니다 — OpenAI, Stability AI 등 대부분의 생성형 AI API와 동일한 패턴입니다:

  1. 제출 → POST /v1/videos/generations → 즉시 작업 ID 수신
  2. Polling → GET /v1/tasks/{task_id} → 주기적으로 상태 확인
  3. 조회status: "completed"일 때 results 배열에 비디오 URL 포함

비디오 생성은 계산량이 매우 크기 때문에 이런 패턴을 사용합니다. 동기 HTTP 요청은 비디오가 준비되기 훨씬 전에 타임아웃됩니다.

작업 상태 생명주기

pending → processing → completed
                    ↘ failed
상태진행 상황일반적인 소요 시간
pending작업이 큐에 대기 중, GPU 리소스 대기0~30초
processing비디오 생성 중 — progress 실시간 업데이트30~120초
completed완료! results 배열에 비디오 URL 포함종료 상태
failed오류 발생 — 오류 상세 정보 확인종료 상태

Polling 모범 사례

Polling 간격: 10초가 적절한 기본값입니다. 너무 빠르면 요청을 낭비하고 속도 제한을 트리거할 수 있고, 너무 느리면 파이프라인이 지연됩니다. 시간에 민감한 애플리케이션에서는 5초마다 polling할 수 있지만, 그보다 빠르게 할 필요는 없습니다.

타임아웃: 파라미터에 따라 합리적인 상한을 설정하세요:

설정예상 소요 시간권장 타임아웃
4s, 480p20~40초120초
5s, 720p30~60초180초
10s, 720p60~90초300초
15s, 1080p90~180초600초

진행률 추적: progress 필드(0100)는 세밀한 피드백을 제공합니다 — UI에서 진행률 표시줄을 구축하는 데 유용합니다. processing 단계에서 약 57초마다 업데이트됩니다.

작업 취소

진행 중인 생성을 중지해야 할 때(잘못된 프롬프트, 마음이 바뀐 경우) 취소할 수 있습니다:

def cancel_task(task_id):
    """pending 또는 processing 상태의 작업을 취소합니다. 크레딧이 환불됩니다."""
    response = requests.post(
        f"{BASE_URL}/tasks/{task_id}/cancel",
        headers=HEADERS
    )
    if response.status_code == 200:
        print(f"Task {task_id} cancelled. Credits refunded.")
    else:
        print(f"Cancel failed: {response.json()}")

task_info.can_canceltrue일 때 취소가 가능합니다. 작업이 completed 또는 failed에 도달하면 취소할 수 없습니다. 취소 시 예약된 크레딧은 자동으로 환불됩니다.

팁: UI에 취소 기능을 일찍 구축하세요. 사용자는 필연적으로 잘못된 프롬프트를 제출하게 되고, 원하지 않는 비디오를 2분 동안 기다리는 것은 시간과 크레딧 모두 낭비입니다.

앞서 설정한 wait_for_video() 함수가 표준 polling 흐름을 처리합니다. polling을 완전히 건너뛰고 싶다면 아래 Webhook 섹션으로 이동하세요.


이미지 애니메이션 (이미지-투-비디오)

제품 사진, 캐릭터 일러스트, 풍경 사진을 살아 움직이게 만들고 싶으신가요? image_url로 전달하면 Seedance가 애니메이션화합니다. 이커머스 제품 비디오에서 가장 강력한 기능 중 하나입니다 — 정적인 제품 사진을 매력적인 비디오 광고로 변환합니다.

위 첫 번째 예제와 동일한 설정 및 polling 함수를 사용합니다.

# ── 이미지-투-비디오 ──────────────────────────────────────────
def image_to_video():
    payload = {
        "model": "seedance-2.0",
        "prompt": (
            "@Image1 as the first frame. The scene slowly comes "
            "to life — leaves rustle gently, soft light shifts "
            "across the frame, and the subject blinks naturally."
        ),
        "image_urls": [
            "https://example.com/your-image.jpg"
        ],
        "duration": 5,
        "quality": "720p",
        "aspect_ratio": "16:9"
    }

    print("Submitting image-to-video request...")
    response = requests.post(
        f"{BASE_URL}/videos/generations",
        headers=HEADERS,
        json=payload
    )
    response.raise_for_status()
    task = response.json()

    print(f"Task created: {task['id']}")
    result = wait_for_video(task["id"])

    video_url = result["results"][0]
    download_video(video_url, "animated_image.mp4")

    return result

텍스트-투-비디오와 다른 점을 살펴봅시다:

  • image_urls — 공개적으로 접근 가능한 이미지 URL 배열입니다. API가 직접 가져오므로 인터넷에서 접근 가능해야 합니다(localhost나 사설 네트워크 URL은 안 됩니다).
  • 프롬프트의 @Image1 — 이 태그는 Seedance에 어떤 이미지를 어떻게 참조할지 알려줍니다. image_urls의 첫 번째 URL에 해당합니다. 세 장의 이미지를 전달하면 @Image1, @Image2, @Image3을 사용합니다.
  • generate_audio 없음 — 여기서는 생략했으며, 기본값은 true입니다. 무음 애니메이션을 원하면 false로 설정하세요.

@Image 태그 작동 방식

프롬프트의 @Image1 태그는 Seedance에 이미지 사용 방법을 알려줍니다. image_urls 배열의 첫 번째 URL을 참조합니다. 최대 9장의 이미지를 전달할 수 있습니다(@Image1부터 @Image9까지). @Video@Audio를 포함한 멀티모달 태그의 전체 가이드는 멀티모달 @Tags 가이드를 참조하세요.

일반적인 패턴:

프롬프트 패턴효과적합한 용도
@Image1 as first frame이미지를 시작 프레임으로 사용제품 쇼케이스, 장면 설정
@Image1 as last frame이미지를 마지막 프레임으로 사용로고 공개, 전환
@Image1 as character reference캐릭터 외형 유지클립 간 캐릭터 일관성
@Image1 as style reference이미지의 시각적 스타일 적용브랜드 일관성, 아트 디렉션
@Image1 as first frame, @Image2 as last frame두 이미지 간 전환 생성전후 비교, 변형

테스트에서 받은 실제 응답:

{
  "created": 1772204037,
  "id": "task-unified-1772204036-lify8u5p",
  "model": "seedance-2.0",
  "object": "video.generation.task",
  "progress": 0,
  "status": "pending",
  "task_info": {
    "can_cancel": true,
    "estimated_time": 145
  },
  "type": "video",
  "usage": {
    "billing_rule": "per_second",
    "credits_reserved": 17.784,
    "user_group": "default"
  }
}

이미지-투-비디오는 텍스트-투-비디오와 완전히 동일한 비동기 패턴을 따릅니다 — 제출, polling, 다운로드. 모델이 입력 이미지를 분석해야 하므로 estimated_time이 약간 더 깁니다.

이미지 요구사항

제약
최대 이미지 수요청당 9장
최대 파일 크기이미지당 30 MB
지원 형식JPEG, PNG, WebP, BMP, TIFF, GIF
URL 요구사항공개적으로 접근 가능해야 함
권장 해상도짧은 변 최소 720px

흔한 실수: URL 대신 로컬 파일 경로를 전달하는 것. image_urls 필드는 공개적으로 접근 가능한 HTTP/HTTPS URL이 필요합니다. 이미지가 로컬에 있다면 먼저 S3, Cloudflare R2 또는 임시 파일 호스팅 서비스에 업로드하세요.

제한사항: Seedance는 사실적인 인물 얼굴 이미지 업로드를 지원하지 않습니다. 시스템이 자동으로 거부합니다. 일러스트나 스타일화된 캐릭터를 사용하세요.

API용 이미지 호스팅

CDN이 없다면 공개 URL을 빠르게 얻는 방법:

# 방법 1: S3에 업로드 (AWS가 있는 경우)
import boto3
s3 = boto3.client('s3')
s3.upload_file('local_image.jpg', 'my-bucket', 'seedance/input.jpg')
image_url = f"https://my-bucket.s3.amazonaws.com/seedance/input.jpg"

# 방법 2: 임시 파일 호스팅 API 사용
# 많은 서비스가 테스트용 무료 임시 호스팅을 제공합니다

고급 이미지-투-비디오 기법 — 첫/마지막 프레임 제어, 다중 이미지 합성, 이커머스 제품 애니메이션 — 은 이미지-투-비디오 상세 가이드를 참조하세요.


비디오 커스터마이징

생성 요청에서 조정할 수 있는 모든 파라미터:

파라미터타입기본값옵션설명
modelstringseedance-2.0필수. 사용할 모델.
promptstring≤2000 tokens필수. @tags를 포함한 비디오 설명.
durationinteger54–15비디오 길이(초).
qualitystring720p480p, 720p, 1080p해상도 등급. 높을수록 크레딧 소모 증가.
aspect_ratiostring16:916:9, 9:16, 1:1, 4:3, 3:4, 21:9출력 화면 비율.
generate_audiobooleantruetrue, falseAI 생성 오디오/음악 활성화.
image_urlsarray≤9장참조 이미지. 프롬프트에서 @Image1, @Image2...로 참조.
video_urlsarray≤3개참조 비디오. 프롬프트에서 @Video1, @Video2...로 참조.
audio_urlsarray≤3개참조 오디오. 프롬프트에서 @Audio1, @Audio2...로 참조.
callback_urlstringHTTPS URL완료 시 webhook 콜백 주소.

Seedance 2.0 vs 1.5 참고: 위의 모든 파라미터는 seedance-2.0seedance-1.5-pro 모두에서 작동합니다. 주요 차이점: video_urls, audio_urls, 다중 이미지 참조(@Image2~@Image9)는 2.0 전용 기능입니다. 1.5에서 사용하면 API가 해당 기능이 지원되지 않는다는 명확한 메시지와 함께 400 오류를 반환합니다.

빠른 예제

세로형 소셜 미디어 비디오 (TikTok/Reels):

위 첫 번째 예제와 동일한 설정 및 polling 함수를 사용합니다.

payload = {
    "model": "seedance-2.0",
    "prompt": "A barista pours latte art in slow motion. Close-up overhead shot.",
    "duration": 8,
    "quality": "1080p",
    "aspect_ratio": "9:16",       # 모바일용 세로형
    "generate_audio": True
}

9:16 비율은 1080×1920 비디오를 생성합니다 — TikTok, Instagram Reels, YouTube Shorts의 네이티브 해상도입니다. 1080p 품질 등급은 모바일 화면에서 선명한 비주얼을 보장합니다.

시네마틱 와이드스크린 + 카메라 움직임:

payload = {
    "model": "seedance-2.0",
    "prompt": (
        "Aerial drone shot over a misty mountain range at sunrise. "
        "Camera slowly pushes forward, revealing a hidden valley. "
        "Cinematic color grading, volumetric lighting."
    ),
    "duration": 10,
    "quality": "1080p",
    "aspect_ratio": "21:9",       # 울트라 와이드스크린 시네마틱
    "generate_audio": True
}

프로그래밍 방식의 카메라 제어 — 돌리 줌, 오비탈 샷, 히치콕 스타일 움직임 — 은 카메라 움직임 API 가이드를 참조하세요.

무음 웹사이트 배경 비디오:

payload = {
    "model": "seedance-2.0",
    "prompt": "Abstract flowing particles in deep blue and gold. Slow, meditative movement.",
    "duration": 15,               # 최대 길이, 심리스 루프에 적합
    "quality": "720p",
    "aspect_ratio": "21:9",       # 와이드 배경
    "generate_audio": False       # 자동 재생 배경에는 오디오 불필요
}

저비용 초안 (빠른 반복):

payload = {
    "model": "seedance-2.0",
    "prompt": "A cat wearing sunglasses sits at a DJ booth. Neon club lighting.",
    "duration": 4,                # 최소 길이 = 가장 빠른 생성
    "quality": "480p",            # 최저 품질 = 가장 저렴한 크레딧
    "aspect_ratio": "16:9"
}

팁: 개발 단계에서는 항상 duration: 4quality: "480p"를 사용하세요. 가장 저렴하고 빠른 조합으로 프롬프트 반복에 이상적입니다. 만족스러운 결과를 얻으면 원하는 길이와 1080p로 최종 버전을 렌더링하세요.

크레딧 비용 추정

크레딧은 길이와 품질에 비례하여 증가합니다. 대략적인 가이드:

품질4s5s10s15s
480p~8~10~20~30
720p~14~18~36~53
1080p~22~28~55~83

대략적인 크레딧. 실제 비용은 credits_reserved 필드에 표시됩니다. 현재 요금은 EvoLink 대시보드에서 확인하세요.

멀티모달 참조 시스템 — @Image, @Video, @Audio 태그 — 은 Seedance 2.0이 가장 돋보이는 부분입니다. 참조 비디오에서 카메라 움직임을 복제하고, 샷 간 캐릭터 일관성을 유지하며, 오디오 비트에 동기화할 수 있습니다. 전체 가이드는 @Tags 완벽 가이드를 참조하세요.


오류 처리 전략

API 호출은 실패합니다. 네트워크가 끊깁니다. 속도 제한에 걸립니다. 실제 발생하는 오류 시나리오에 대응하는 견고한 코드 작성법을 알아봅시다.

일반적인 오류 응답

모든 오류는 동일한 형식을 따릅니다:

{
  "error": {
    "message": "description of what went wrong",
    "type": "error_category",
    "code": "specific_error_code"
  }
}

error 객체는 항상 messagetype을 포함합니다. code 필드는 대부분의 오류에 존재하지만 전부는 아닙니다. 먼저 type을 확인하고, 세부 사항은 code를 확인하세요.

API에서 반환하는 실제 오류 응답들:

401 — 잘못된 API Key:

{
  "error": {
    "message": "Invalid token (request id: 20260227225245660301729AApJNAhJ)",
    "type": "evo_api_error"
  }
}

API Key가 잘못되었거나, 만료되었거나, 취소되었습니다. EVOLINK_API_KEY 환경 변수를 다시 확인하세요. 흔한 원인: Key 복사 시 뒤에 공백이 포함된 경우.

400 — 필수 필드 누락:

{
  "error": {
    "code": "invalid_parameter",
    "message": "prompt cannot be empty",
    "type": "invalid_request_error"
  }
}

prompt 필드는 모든 생성 요청에서 필수입니다. 빈 문자열이나 공백만 있는 프롬프트도 이 오류를 트리거합니다.

400 — 잘못된 파라미터 값:

{
  "error": {
    "code": "invalid_parameter",
    "message": "duration must be between 4 and 15",
    "type": "invalid_request_error"
  }
}

duration: 3 또는 duration: 20을 전달할 때 발생합니다. 유효 범위는 4~15초(포함)입니다.

400 — 지원하지 않는 품질 등급:

{
  "error": {
    "code": "invalid_parameter",
    "message": "quality must be one of: 480p, 720p, 1080p",
    "type": "invalid_request_error"
  }
}

"quality": "4k" 또는 "quality": "hd"를 전달할 때 흔히 발생합니다. 정확한 문자열을 사용하세요: 480p, 720p, 또는 1080p.

402 — 크레딧 부족:

{
  "error": {
    "message": "Insufficient credits. Required: 17.784, Available: 2.100",
    "type": "insufficient_quota_error"
  }
}

계정에 크레딧이 부족합니다. 메시지에 필요한 양과 보유한 양이 정확히 표시됩니다. EvoLink 대시보드에서 충전하세요.

404 — 작업을 찾을 수 없음:

{
  "error": {
    "message": "Task not found",
    "type": "invalid_request_error",
    "code": "task_not_found"
  }
}

보통 작업 ID가 잘못되었거나, 작업이 생성된 지 24시간이 지나 만료된 경우입니다. 생성 응답의 id 필드를 사용하고 있는지 확인하세요.

413 — 이미지가 너무 큼:

{
  "error": {
    "message": "Image file size exceeds 30MB limit",
    "type": "request_too_large_error"
  }
}

업로드 전에 이미지를 압축하세요. API에서는 2~3 MB 이상의 이미지 품질 향상이 거의 없습니다.

429 — 속도 제한:

{
  "error": {
    "message": "Rate limit exceeded. Please retry after 60 seconds.",
    "type": "rate_limit_error"
  }
}

요청을 너무 자주 보내고 있습니다. 기본 제한은 개발에 충분히 여유롭지만, 배치 스크립트에서는 걸릴 수 있습니다. 지수 백오프를 구현하세요(아래 참조).

422 — 콘텐츠 필터 거부:

{
  "error": {
    "message": "Content rejected by safety filter",
    "type": "content_policy_violation",
    "code": "content_filtered"
  }
}

프롬프트나 입력 이미지가 콘텐츠 안전 필터에 감지되었습니다. 제한된 콘텐츠를 피하도록 프롬프트를 수정하세요. image_urls의 사실적인 인물 얼굴 이미지는 자동으로 거부됩니다.

오류 참조 표

HTTP 코드타입의미재시도 가능?조치
400invalid_request_error잘못된 파라미터아니오payload 수정
401authentication_error잘못된 API Key아니오Key 확인
402insufficient_quota_error크레딧 부족아니오충전
404not_found_error작업 또는 모델 없음아니오task_id / 모델명 확인
413request_too_large_error요청이 너무 큼아니오파일 크기 줄이기
422content_policy_violation콘텐츠 필터링됨아니오프롬프트 수정
429rate_limit_error요청이 너무 빈번함60초 후 재시도
500internal_server_error서버 문제몇 초 후 재시도
502bad_gateway업스트림 오류5초 후 재시도
503service_unavailable_error서비스 불가30초 후 재시도

프로덕션 수준 오류 처리

일시적 오류에 대한 재시도 로직으로 API 호출을 감싸세요:

위 첫 번째 예제와 동일한 설정 및 polling 함수를 사용합니다.

import random

def generate_video_with_retry(payload, max_retries=3):
    """
    일시적 오류(429, 500, 502, 503)에 대해 자동 재시도하는
    비디오 생성 요청 제출.
    
    썬더링 허드를 피하기 위해 지수 백오프 + 지터 사용:
    - 시도 1: ~1초 대기
    - 시도 2: ~2초 대기
    - 시도 3: ~4초 대기
    
    재시도 불가능한 오류(400, 401, 402, 404, 413, 422)는
    근본적인 문제를 재시도로 해결할 수 없으므로 즉시 실패합니다.
    """
    for attempt in range(max_retries):
        try:
            response = requests.post(
                f"{BASE_URL}/videos/generations",
                headers=HEADERS,
                json=payload,
                timeout=30       # 30초 연결 타임아웃
            )

            # 성공 — task 객체 반환
            if response.status_code == 200:
                return response.json()

            # 오류 응답 파싱
            error = response.json().get("error", {})
            error_type = error.get("type", "")
            error_msg = error.get("message", "Unknown error")

            # 재시도 불가능한 오류 — 즉시 실패
            if response.status_code in (400, 401, 402, 404, 413, 422):
                raise ValueError(
                    f"API error {response.status_code}: {error_msg}"
                )

            # 재시도 가능한 오류 — 지수 백오프 + 지터
            if response.status_code in (429, 500, 502, 503):
                wait = (2 ** attempt) + random.uniform(0, 1)
                print(f"  Retry {attempt + 1}/{max_retries} "
                      f"after {wait:.1f}s ({error_type}: {error_msg})")
                time.sleep(wait)
                continue

        except requests.exceptions.Timeout:
            # 서버가 30초 내에 응답하지 않음
            wait = (2 ** attempt) + random.uniform(0, 1)
            print(f"  Timeout. Retry {attempt + 1}/{max_retries} "
                  f"after {wait:.1f}s")
            time.sleep(wait)
            continue

        except requests.exceptions.ConnectionError as e:
            # DNS 실패, 연결 거부 등
            wait = (2 ** attempt) + random.uniform(0, 1)
            print(f"  Connection error: {e}. Retry {attempt + 1}/{max_retries} "
                  f"after {wait:.1f}s")
            time.sleep(wait)
            continue

    raise RuntimeError(f"Failed after {max_retries} retries")

이 코드가 처리하는 것:

  • 속도 제한 (429) — 지수 백오프 + 지터로 여러 클라이언트의 동기화된 재시도 방지
  • 서버 오류 (500/502/503) — 증가하는 지연으로 자동 재시도
  • 타임아웃 — 30초 타임아웃으로 응답 없는 서버에서 멈추는 것 방지
  • 연결 끊김 — DNS 실패, 연결 거부, 네트워크 불안정
  • 클라이언트 오류 (400/401/402/404/413/422) — 잘못된 입력은 재시도로 해결할 수 없으므로 즉시 실패

팁: 프로덕션 시스템에서는 실패한 요청의 전체 payload와 오류 응답을 로깅하는 것을 권장합니다. 새벽 3시에 문제가 발생했을 때 디버깅이 훨씬 쉬워집니다.

API 호출 전 입력 검증

API를 호출하기 전에 명백한 오류를 로컬에서 잡아 크레딧과 시간을 절약하세요:

def validate_payload(payload):
    """
    API로 보내기 전에 생성 payload를 검증합니다.
    400 오류를 유발하는 일반적인 실수를 잡습니다.
    """
    errors = []
    
    # 필수 필드
    if not payload.get("model"):
        errors.append("'model' is required")
    if not payload.get("prompt") or not payload["prompt"].strip():
        errors.append("'prompt' is required and cannot be empty")
    
    # 길이 범위
    duration = payload.get("duration", 5)
    if duration < 4 or duration > 15:
        errors.append(f"'duration' must be 4-15, got {duration}")
    
    # 품질 값
    valid_qualities = {"480p", "720p", "1080p"}
    quality = payload.get("quality", "720p")
    if quality not in valid_qualities:
        errors.append(f"'quality' must be one of {valid_qualities}, got '{quality}'")
    
    # 화면 비율 값
    valid_ratios = {"16:9", "9:16", "1:1", "4:3", "3:4", "21:9"}
    ratio = payload.get("aspect_ratio", "16:9")
    if ratio not in valid_ratios:
        errors.append(f"'aspect_ratio' must be one of {valid_ratios}, got '{ratio}'")
    
    # 이미지 URL 검증
    image_urls = payload.get("image_urls", [])
    if len(image_urls) > 9:
        errors.append(f"Maximum 9 images allowed, got {len(image_urls)}")
    for i, url in enumerate(image_urls):
        if not url.startswith(("http://", "https://")):
            errors.append(f"image_urls[{i}] must be an HTTP(S) URL")
    
    if errors:
        raise ValueError(f"Payload validation failed:\n" + "\n".join(f"  - {e}" for e in errors))
    
    return True

흔한 실수: 이미지 URL의 특수 문자를 URL 인코딩하는 것을 잊는 경우. 이미지 경로에 공백이나 비ASCII 문자가 포함되어 있다면 urllib.parse.quote()로 인코딩하세요.


Webhook 설정 (Polling 건너뛰기)

Polling은 스크립트와 프로토타이핑에 충분합니다. 프로덕션 시스템에서는 webhook이 더 효율적입니다 — 비디오가 준비되면 API가 결과를 서버로 직접 푸시합니다. 낭비되는 요청 없이, 완료와 알림 사이에 지연도 없습니다.

작동 방식

생성 요청에 callback_url을 추가합니다:

위 첫 번째 예제와 동일한 설정을 사용합니다.

payload = {
    "model": "seedance-2.0",
    "prompt": "A spaceship launches from a desert landscape at sunset.",
    "duration": 8,
    "quality": "720p",
    "callback_url": "https://your-server.com/api/webhook/seedance"
}

response = requests.post(
    f"{BASE_URL}/videos/generations",
    headers=HEADERS,
    json=payload
)
task = response.json()
print(f"Task submitted: {task['id']}")
# Polling 불필요 — webhook이 결과를 수신합니다

비디오가 준비되면 API가 callback_url로 완료된 task 객체를 담은 POST 요청을 보냅니다 — polling으로 받는 것과 완전히 동일한 내용입니다.

Webhook 요구사항

요구사항상세
프로토콜HTTPS만 지원 (HTTP 불가) — 보안 요구사항
응답10초 내에 2xx 반환
재시도실패 시 3회 재시도 (1초, 2초, 4초 간격)
URL 길이≤ 2048자
네트워크내부/사설 IP 불가 (localhost, 10.x.x.x, 192.168.x.x)
본문전체 task 객체를 담은 JSON POST

프로덕션 Flask Webhook 수신기

적절한 검증, 오류 처리, 비동기 비디오 다운로드를 갖춘 완전한 webhook 서버:

# webhook_server.py
"""
Seedance webhook 수신기 — 비디오 생성 완료 콜백을 처리합니다.
실행: pip install flask requests
      python webhook_server.py
"""
from flask import Flask, request, jsonify
import json
import os
import threading
import requests as req  # flask.request와의 충돌을 피하기 위해 이름 변경

app = Flask(__name__)

# 완료된 비디오를 저장할 디렉토리
OUTPUT_DIR = os.getenv("VIDEO_OUTPUT_DIR", "./videos")
os.makedirs(OUTPUT_DIR, exist_ok=True)


def download_video_async(video_url, task_id):
    """백그라운드 스레드에서 비디오를 다운로드하여 webhook 응답을 차단하지 않습니다."""
    try:
        filename = os.path.join(OUTPUT_DIR, f"{task_id}.mp4")
        print(f"  Downloading {task_id} to {filename}...")
        resp = req.get(video_url, stream=True, timeout=120)
        resp.raise_for_status()
        with open(filename, "wb") as f:
            for chunk in resp.iter_content(chunk_size=8192):
                f.write(chunk)
        size_mb = os.path.getsize(filename) / (1024 * 1024)
        print(f"  Saved: {filename} ({size_mb:.1f} MB)")
    except Exception as e:
        print(f"  Download failed for {task_id}: {e}")


@app.route("/api/webhook/seedance", methods=["POST"])
def handle_webhook():
    """
    Seedance 비디오 생성 완료 webhook을 처리합니다.
    
    비디오 생성이 완료(성공 또는 실패)되면
    API가 전체 task 객체를 담은 POST를 보냅니다.
    """
    # 수신된 task 객체 파싱
    task = request.json
    if not task:
        return jsonify({"error": "Empty body"}), 400
    
    task_id = task.get("id", "unknown")
    status = task.get("status", "unknown")
    model = task.get("model", "unknown")

    print(f"\n{'='*50}")
    print(f"Webhook received: task={task_id}")
    print(f"  Status: {status}")
    print(f"  Model: {model}")

    if status == "completed":
        # results에서 비디오 URL 추출
        results = task.get("results", [])
        if results:
            video_url = results[0]
            print(f"  Video URL: {video_url}")
            
            # 백그라운드 스레드에서 다운로드하여 빠르게 응답
            thread = threading.Thread(
                target=download_video_async,
                args=(video_url, task_id)
            )
            thread.start()
        else:
            print(f"  WARNING: Completed but no results array!")

    elif status == "failed":
        error_info = task.get("error", {})
        print(f"  FAILED: {json.dumps(error_info, indent=2)}")
        # TODO: 오류 추적 시스템에 기록 (Sentry 등)
        # TODO: 선택적으로 수정된 파라미터로 재생성 시도

    else:
        print(f"  Unexpected status: {status}")
        print(f"  Full payload: {json.dumps(task, indent=2)}")

    # 항상 빠르게 200 반환 — API는 10초 내 응답을 기대합니다
    return jsonify({"received": True, "task_id": task_id}), 200


@app.route("/health", methods=["GET"])
def health_check():
    """로드 밸런서용 헬스 체크 엔드포인트."""
    return jsonify({"status": "ok"}), 200


if __name__ == "__main__":
    print(f"Starting webhook server...")
    print(f"Videos will be saved to: {os.path.abspath(OUTPUT_DIR)}")
    print(f"Webhook URL: http://localhost:5000/api/webhook/seedance")
    app.run(host="0.0.0.0", port=5000, debug=True)

의존성 설치 및 실행:

pip install flask requests
python webhook_server.py

이 서버의 주요 설계 결정:

  • 백그라운드 다운로드 — 스레드를 생성하여 비디오를 다운로드하므로 webhook 핸들러가 즉시 200을 반환합니다. API는 10초 내 응답을 기대하지만, 비디오 다운로드는 더 오래 걸릴 수 있습니다.
  • 헬스 체크 엔드포인트/health는 로드 밸런서(ALB, nginx 등) 뒤에 배포할 때 유용합니다.
  • 오류 로깅 — 실패한 작업은 전체 오류 payload와 함께 출력됩니다. 프로덕션에서는 Sentry, Datadog 또는 로깅 스택으로 연결하세요.

ngrok으로 로컬호스트 노출

로컬 개발 시 ngrok을 사용하여 로컬 서버로 터널링하는 공개 HTTPS URL을 생성합니다:

# ngrok 설치 (macOS)
brew install ngrok

# 또는 https://ngrok.com/download 에서 다운로드

# 터널 시작
ngrok http 5000

ngrok 출력 예시:

Forwarding  https://a1b2c3d4.ngrok-free.app → http://localhost:5000

이 HTTPS URL을 callback_url로 사용하세요:

payload = {
    "model": "seedance-2.0",
    "prompt": "Your prompt here",
    "callback_url": "https://a1b2c3d4.ngrok-free.app/api/webhook/seedance"
}

흔한 실수: ngrok의 https:// URL 대신 http:// URL을 사용하는 것. Seedance API는 webhook에 HTTPS를 요구합니다 — 일반 HTTP 콜백 URL은 400 오류를 반환합니다.

Webhook 보안

프로덕션에서는 webhook 요청이 실제로 EvoLink API에서 온 것인지 검증하세요:

import hmac
import hashlib

def verify_webhook(request):
    """작업 ID 패턴으로 webhook 진위를 검증합니다."""
    task = request.json
    task_id = task.get("id", "")
    
    # EvoLink 작업 ID는 특정 형식을 따릅니다
    if not task_id.startswith("task-unified-"):
        return False
    
    # 추가 검증: 필수 필드 존재 확인
    required_fields = ["id", "status", "model", "created"]
    if not all(field in task for field in required_fields):
        return False
    
    return True

Webhook vs Polling: 언제 무엇을 선택할까?

시나리오권장 방식이유
빠른 프로토타이핑 / 스크립트Polling더 간단, 서버 불필요
프로덕션 웹 앱Webhook확장 가능, 요청 낭비 없음
배치 처리 (100+ 비디오)Webhook + 메시지 큐모두 제출, 완료 순서대로 처리
CLI 도구Polling서버 인프라 불필요
모바일 앱 백엔드Webhook완료 시 사용자에게 푸시 알림
Serverless (Lambda/Cloud Functions)Webhook완벽한 매칭 — 완료마다 함수 트리거

팁: 배치 처리에는 webhook과 메시지 큐(Redis, RabbitMQ, SQS)를 결합하세요. 모든 생성 요청을 제출한 후 큐에 도착하는 순서대로 처리합니다. 이 방식은 제출과 처리를 분리하고 재시도를 깔끔하게 처리합니다.


배치 처리: 여러 비디오 생성

실제 사용 사례에서는 많은 비디오를 생성해야 하는 경우가 많습니다. 속도 제한을 고려한 배치 처리 패턴:

위 첫 번째 예제와 동일한 설정 및 헬퍼 함수를 사용합니다.

import concurrent.futures

def batch_generate(prompts, max_concurrent=3):
    """
    제어된 동시성으로 여러 비디오를 생성합니다.
    
    Args:
        prompts: 프롬프트 문자열 리스트.
        max_concurrent: 최대 동시 생성 수.
    
    Returns:
        (prompt, result_or_error) 튜플 리스트.
    """
    results = []
    
    def generate_one(prompt, index):
        """단일 비디오를 생성하고 결과를 반환합니다."""
        payload = {
            "model": "seedance-2.0",
            "prompt": prompt,
            "duration": 5,
            "quality": "720p"
        }
        try:
            task = generate_video_with_retry(payload)
            print(f"[{index}] Submitted: {task['id']}")
            result = wait_for_video(task["id"])
            video_url = result["results"][0]
            download_video(video_url, f"batch_{index}.mp4")
            return (prompt, result)
        except Exception as e:
            print(f"[{index}] Failed: {e}")
            return (prompt, str(e))
    
    # 속도 제한을 준수하며 배치 처리
    with concurrent.futures.ThreadPoolExecutor(max_workers=max_concurrent) as executor:
        futures = {
            executor.submit(generate_one, prompt, i): i
            for i, prompt in enumerate(prompts)
        }
        for future in concurrent.futures.as_completed(futures):
            results.append(future.result())
    
    # 요약
    succeeded = sum(1 for _, r in results if isinstance(r, dict))
    print(f"\nBatch complete: {succeeded}/{len(prompts)} succeeded")
    return results


# 사용 예시
prompts = [
    "A hummingbird hovering near a red flower. Macro lens, shallow depth of field.",
    "Ocean waves crashing on volcanic rocks at sunset. Slow motion.",
    "A street musician playing violin in the rain. Cinematic lighting.",
]
batch_generate(prompts, max_concurrent=2)

배치 처리의 주요 고려사항:

  • max_concurrent=3 — 너무 많은 요청을 동시에 제출하지 마세요. 2~3으로 시작하고 속도 제한에 따라 늘리세요.
  • ThreadPoolExecutor — I/O 바운드(API 응답 대기)이므로 프로세스가 아닌 스레드를 사용합니다.
  • 오류 격리 — 각 비디오 생성은 독립적입니다. 하나의 실패가 배치 전체를 중단시키지 않습니다.

다음 단계

기본기를 모두 다뤘습니다 — 텍스트-투-비디오, 이미지-투-비디오, 비동기 polling, webhook, 오류 처리, 배치 처리. 더 깊이 들어갈 방향을 안내합니다:

고급 기능 탐색

참조 문서

직접 만들어 보세요

배운 것을 조합해 보세요. 프로젝트 아이디어:

  • 자동화된 제품 비디오 파이프라인 — 제품 사진을 업로드하고 마케팅 비디오를 대량 생성 (이커머스 비디오 가이드 참조)
  • 소셜 미디어 콘텐츠 엔진 — 텍스트 브리프에서 세로형 숏폼 비디오를 생성하여 TikTok/Reels에 직접 게시
  • 스토리보드-투-비디오 도구 — 연속 이미지를 카메라 움직임 제어가 적용된 애니메이션 장면으로 변환
  • AI 비디오 편집 파이프라인 — Seedance 2.0의 비디오 확장 기능을 활용하여 짧은 클립에서 더 긴 내러티브 생성

시작할 준비가 되셨나요? 무료 EvoLink API Key 발급받기 — 오늘부터 비디오를 생성하세요.


자주 묻는 질문

Seedance 2.0 비디오 생성은 얼마나 걸리나요?

일반적으로 길이와 품질 설정에 따라 30120초입니다. 5초 720p 비디오는 약 50초에 완료됩니다. 15초 1080p 비디오는 23분이 걸릴 수 있습니다. API는 각 작업에 estimated_time 필드를 반환하므로 적절한 타임아웃을 설정할 수 있습니다. 피크 시간대에는 큐 대기 시간이 10~30초 추가될 수 있습니다.

Seedance 2.0 API는 어떤 이미지 형식을 지원하나요?

JPEG, PNG, WebP, BMP, TIFF, GIF. 각 이미지는 30 MB 이하여야 합니다. image_urls 파라미터로 요청당 최대 9장의 이미지를 전달할 수 있습니다. 이미지는 공개적으로 접근 가능한 URL이어야 합니다 — API가 직접 가져옵니다. 최상의 결과를 위해 짧은 변이 최소 720px인 이미지를 사용하세요. 매우 낮은 해상도의 이미지(256px 미만)는 흐릿한 애니메이션을 생성할 수 있습니다.

15초보다 긴 비디오를 생성할 수 있나요?

단일 생성의 최대 길이는 15초입니다. 더 긴 콘텐츠를 만들려면 여러 클립을 생성한 후 FFmpeg이나 비디오 편집기로 연결하세요. Seedance 2.0은 비디오 확장을 지원합니다 — 생성된 비디오의 마지막 프레임을 다음 생성의 첫 프레임으로 사용하여 매끄러운 연속성을 만들 수 있습니다. 기본 접근법: 클립 1을 생성하고, 마지막 프레임을 추출하여 클립 2에 @Image1 as first frame으로 전달합니다.

EvoLink를 통한 Seedance 2.0 API 비용은 얼마인가요?

가격은 비디오 길이와 품질 등급에 따라 결정됩니다. 5초 720p 비디오는 약 18 크레딧입니다. EvoLink는 직접 API 접근 대비 비용을 절감할 수 있는 스마트 라우팅을 제공합니다. 현재 초당 요금은 대시보드에서 확인하세요. API 응답의 credits_reserved 필드가 생성 전 정확한 비용을 보여줍니다 — 이 금액 이상 청구되지 않습니다.

seedance-1.5-pro와 seedance-2.0의 차이점은 무엇인가요?

Seedance 2.0은 멀티모달 참조(이미지, 비디오, 오디오 혼합 입력), 네이티브 오디오 생성, 향상된 물리 일관성, 비디오 편집 기능을 추가합니다. API 인터페이스는 동일합니다 — 같은 엔드포인트, 같은 파라미터, 같은 응답 형식. 지금 seedance-1.5-pro로 테스트하고 모델 이름만 변경하여 seedance-2.0으로 전환할 수 있습니다. 1.5의 주요 제한: 단일 이미지 입력만 지원(@Image2~9 불가), 비디오/오디오 참조 불가, 네이티브 오디오 생성 불가. 자세한 비교는 Seedance 2.0 vs Sora 2 비교를 참조하세요.

"content rejected by safety filter" 오류는 어떻게 처리하나요?

콘텐츠 안전 필터는 사실적인 폭력, 노골적인 콘텐츠, 실제 공인을 포함하는 프롬프트를 거부합니다. image_urls를 통해 업로드된 사실적인 인물 얼굴 이미지도 거부됩니다. 얼굴 제한을 우회하려면 일러스트, 스타일화된 또는 애니메이션 스타일의 캐릭터 이미지를 사용하세요. 프롬프트 거부의 경우 제한된 주제를 피하도록 문구를 수정하세요. 오류 응답에 type: "content_policy_violation"이 포함됩니다 — 오류 처리 코드에서 이를 확인하여 사용자에게 명확한 메시지를 제공하세요.

Node.js / JavaScript 프로젝트에서 Seedance API를 사용할 수 있나요?

네. REST API는 언어에 구애받지 않습니다 — 어떤 HTTP 클라이언트든 사용할 수 있습니다. 이 튜토리얼의 개념(비동기 polling, webhook, 오류 처리)은 Node.js의 fetchaxios로 직접 적용할 수 있습니다. EvoLink는 polling과 재시도를 자동으로 처리하는 공식 Node.js 및 Python SDK도 제공합니다.

비디오 완료 시 webhook 서버가 다운되어 있으면 어떻게 되나요?

API는 증가하는 간격(1초, 2초, 4초)으로 webhook 전달을 3회 재시도합니다. 3회 모두 실패하면 webhook은 포기됩니다 — 하지만 비디오는 여전히 사용 가능합니다. GET /v1/tasks/{task_id}로 polling하여 결과를 조회할 수 있습니다. 따라서 제출 시 작업 ID를 저장하고, 완료되었지만 webhook으로 수신되지 않은 작업을 주기적으로 확인하는 백그라운드 작업을 설정하는 것이 좋습니다.

API 요청에 속도 제한이 있나요?

네. 기본 속도 제한은 개발과 중간 규모 프로덕션 사용에 충분히 여유롭습니다. 429 오류가 발생하면 오류 처리 섹션에서 보여준 대로 지수 백오프를 구현하세요. 대규모 사용(하루 수천 건의 비디오 생성)의 경우 EvoLink 지원팀에 연락하여 맞춤 속도 제한과 전용 용량을 논의하세요.

Seedance 2.0을 상업 프로젝트에 사용할 수 있나요?

네. EvoLink API를 통해 생성된 비디오는 상업적 사용이 허가됩니다. 출력물의 소유권은 사용자에게 있으며, 제품, 마케팅 자료, 클라이언트 납품물, 게시 콘텐츠에 사용할 수 있습니다. 자세한 라이선스 조건과 상업적 사용 모범 사례는 Seedance 2.0 저작권 가이드를 참조하세요.


전체 스크립트

이 튜토리얼의 전체 코드를 하나의 파일로 정리했습니다 — 복사, 붙여넣기, API Key 입력 후 바로 실행하세요:

"""
Seedance 2.0 API 튜토리얼 — 전체 스크립트
문서: https://seedance2api.app/docs/video-generation
API Key: https://evolink.ai/early-access
"""
import requests
import time
import os
import json
import random

# ── 설정 ─────────────────────────────────────────────────────
API_KEY = os.getenv("EVOLINK_API_KEY", "sk-your-api-key-here")
BASE_URL = "https://api.evolink.ai/v1"
HEADERS = {
    "Authorization": f"Bearer {API_KEY}",
    "Content-Type": "application/json"
}


# ── 재사용 가능한 헬퍼 ────────────────────────────────────────
def wait_for_video(task_id, poll_interval=10, timeout=600):
    """비디오 생성 작업을 완료까지 polling합니다."""
    elapsed = 0
    while elapsed < timeout:
        response = requests.get(
            f"{BASE_URL}/tasks/{task_id}",
            headers=HEADERS
        )
        response.raise_for_status()
        task = response.json()
        status = task["status"]
        progress = task.get("progress", 0)
        print(f"  [{elapsed}s] Status: {status} | Progress: {progress}%")
        if status == "completed":
            return task
        elif status == "failed":
            raise RuntimeError(f"Task {task_id} failed: {task}")
        time.sleep(poll_interval)
        elapsed += poll_interval
    raise TimeoutError(f"Task {task_id} timed out after {timeout}s")


def download_video(url, filename="output.mp4"):
    """URL에서 비디오 파일을 다운로드합니다."""
    print(f"Downloading to {filename}...")
    resp = requests.get(url, stream=True)
    resp.raise_for_status()
    with open(filename, "wb") as f:
        for chunk in resp.iter_content(chunk_size=8192):
            f.write(chunk)
    print(f"Saved: {filename} ({os.path.getsize(filename) / 1024:.0f} KB)")


def generate_video_with_retry(payload, max_retries=3):
    """일시적 오류에 대해 자동 재시도하는 생성 요청 제출."""
    for attempt in range(max_retries):
        try:
            response = requests.post(
                f"{BASE_URL}/videos/generations",
                headers=HEADERS,
                json=payload,
                timeout=30
            )
            if response.status_code == 200:
                return response.json()
            error = response.json().get("error", {})
            if response.status_code in (400, 401, 402, 404, 413, 422):
                raise ValueError(
                    f"API error {response.status_code}: "
                    f"{error.get('message', 'Unknown')}"
                )
            if response.status_code in (429, 500, 502, 503):
                wait = (2 ** attempt) + random.uniform(0, 1)
                print(f"  Retry {attempt+1}/{max_retries} after {wait:.1f}s")
                time.sleep(wait)
                continue
        except requests.exceptions.RequestException:
            wait = (2 ** attempt) + random.uniform(0, 1)
            print(f"  Retry {attempt+1}/{max_retries} after {wait:.1f}s")
            time.sleep(wait)
            continue
    raise RuntimeError(f"Failed after {max_retries} retries")


def validate_payload(payload):
    """API 호출 전 생성 payload를 검증합니다."""
    errors = []
    if not payload.get("model"):
        errors.append("'model' is required")
    if not payload.get("prompt") or not payload["prompt"].strip():
        errors.append("'prompt' is required")
    duration = payload.get("duration", 5)
    if duration < 4 or duration > 15:
        errors.append(f"'duration' must be 4-15, got {duration}")
    quality = payload.get("quality", "720p")
    if quality not in {"480p", "720p", "1080p"}:
        errors.append(f"Invalid quality: {quality}")
    if errors:
        raise ValueError("Validation failed:\n" + "\n".join(f"  - {e}" for e in errors))


def cancel_task(task_id):
    """pending 또는 processing 상태의 작업을 취소합니다."""
    response = requests.post(
        f"{BASE_URL}/tasks/{task_id}/cancel",
        headers=HEADERS
    )
    if response.status_code == 200:
        print(f"Task {task_id} cancelled.")
    else:
        print(f"Cancel failed: {response.json()}")


# ── 예제 1: 텍스트-투-비디오 ──────────────────────────────────
def text_to_video():
    payload = {
        "model": "seedance-2.0",
        "prompt": (
            "A golden retriever puppy chases a butterfly through "
            "a sunlit meadow. The camera follows the puppy with a "
            "smooth tracking shot as wildflowers sway in the breeze."
        ),
        "duration": 5,
        "quality": "720p",
        "aspect_ratio": "16:9",
        "generate_audio": True
    }
    validate_payload(payload)
    task = generate_video_with_retry(payload)
    print(f"Task: {task['id']} (ETA: {task['task_info']['estimated_time']}s)")
    result = wait_for_video(task["id"])
    download_video(result["results"][0], "text_to_video.mp4")


# ── 예제 2: 이미지-투-비디오 ──────────────────────────────────
def image_to_video():
    payload = {
        "model": "seedance-2.0",
        "prompt": (
            "@Image1 as the first frame. The scene slowly comes "
            "to life — leaves rustle gently, soft light shifts "
            "across the frame."
        ),
        "image_urls": ["https://example.com/your-image.jpg"],
        "duration": 5,
        "quality": "720p"
    }
    validate_payload(payload)
    task = generate_video_with_retry(payload)
    print(f"Task: {task['id']}")
    result = wait_for_video(task["id"])
    download_video(result["results"][0], "image_to_video.mp4")


# ── 예제 3: 세로형 소셜 미디어 비디오 ─────────────────────────
def social_media_video():
    payload = {
        "model": "seedance-2.0",
        "prompt": (
            "A barista pours latte art in slow motion. "
            "Close-up overhead shot, warm cafe lighting."
        ),
        "duration": 8,
        "quality": "1080p",
        "aspect_ratio": "9:16",
        "generate_audio": True
    }
    validate_payload(payload)
    task = generate_video_with_retry(payload)
    print(f"Task: {task['id']}")
    result = wait_for_video(task["id"])
    download_video(result["results"][0], "social_video.mp4")


if __name__ == "__main__":
    print("=== 텍스트-투-비디오 ===")
    text_to_video()
    # print("\n=== 이미지-투-비디오 ===")
    # image_to_video()  # 주석 해제 후 이미지 URL 설정
    # print("\n=== 소셜 미디어 비디오 ===")
    # social_media_video()

팁: 현재 사용 가능한 모델로 테스트하려면 "seedance-2.0""seedance-1.5-pro"로 변경하세요. API 인터페이스는 동일합니다 — 같은 엔드포인트, 같은 파라미터, 같은 응답 형식. Seedance 2.0이 완전히 출시되면 모델 이름만 다시 변경하면 됩니다.

시작하기 → EvoLink에서 무료 API Key 발급받기

Ready to get started?

Top up and start generating cinematic AI videos in minutes.