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 と 1.5 について: Seedance 2.0 は段階的にリリース中です。今すぐ seedance-1.5-pro で完全なワークフローをテストできます — 2.0 が完全に利用可能になったら、モデル名を変更するだけです。すべてのエンドポイント、パラメータ、レスポンス形式は同一です。2.0 の主な違い:マルチモーダル参照(画像・動画・音声の混合入力)、ネイティブオーディオ生成、物理シミュレーションの改善、動画編集機能。このチュートリアルの内容はどちらのバージョンでも動作します。

無料 API Key を取得して、一緒に手を動かしましょう。


何を作るか(そして何が必要か)

まず Seedance で生成した動画を見てみましょう — たった 1 回の 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 だけで OK。

ヒント: プロダクションアプリを開発中なら、仮想環境で依存関係を分離することをお勧めします:

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 は 1 つの 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"
}

1 行ずつ解説します:

  • os.getenv("EVOLINK_API_KEY", "sk-your-api-key-here") — 環境変数から API Key を読み取ります。第 2 引数はデフォルト値です(ローカルテスト時のみ実際の Key に置き換えてください)。
  • BASE_URL — すべての EvoLink API エンドポイントのルート URL です。すべてのリクエストは https://api.evolink.ai/v1/... に送信されます。
  • HEADERS — すべてのリクエストに含まれる 2 つのヘッダー: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 に段階的にダウンロードするよう指示します。

以上の 3 つ — 設定、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 配列に 1 つ以上の動画 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 — 動画の説明。主体、アクション、カメラの動き、雰囲気を具体的に記述してください。上記のプロンプトは 3 部構成:主体("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 フィールド(0〜100)は細かいフィードバックを提供します — UI でプログレスバーを構築するのに便利です。processing フェーズ中、約 5〜7 秒ごとに更新されます。

タスクのキャンセル

進行中の生成を停止する必要がある場合(プロンプトを間違えた、気が変わった)、キャンセルできます:

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 がアニメーション化します。EC 商品動画で最も強力な機能の 1 つです — 静的な商品写真を魅力的な動画広告に変換します。

上記の最初の例と同じセットアップと 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 に対応します。3 枚の画像を渡す場合は @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 frame2 枚の画像間のトランジションを作成ビフォーアフター、変形

テストで得た実際のレスポンス:

{
  "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 を使用
# 多くのサービスがテスト用の無料一時ホスティングを提供しています

高度な画像から動画テクニック — 最初/最後のフレーム制御、マルチ画像合成、EC 商品アニメーション — は画像から動画の詳細ガイドを参照してください。


動画をカスタマイズする

生成リクエストで調整できるすべてのパラメータ:

パラメータデフォルトオプション説明
modelstringseedance-2.0必須。使用するモデル。
promptstring≤2000 tokens必須。@tags を含む動画の説明。
durationinteger54–15動画の長さ(秒)。
qualitystring720p480p720p1080p解像度ティア。高いほどクレジット消費増。
aspect_ratiostring16:916:99:161:14:33:421:9出力アスペクト比。
generate_audiobooleantruetruefalseAI 生成オーディオ/音楽を有効化。
image_urlsarray≤9 枚参照画像。プロンプトで @Image1、@Image2... で参照。
video_urlsarray≤3 本参照動画。プロンプトで @Video1、@Video2... で参照。
audio_urlsarray≤3 本参照オーディオ。プロンプトで @Audio1、@Audio2... で参照。
callback_urlstringHTTPS URL完了時の webhook コールバック URL。

Seedance 2.0 と 1.5 の違い: 上記のすべてのパラメータは seedance-2.0seedance-1.5-pro の両方で動作します。主な違い:video_urlsaudio_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: 3duration: 20 を渡した場合に発生します。有効範囲は 4〜15 秒(両端含む)です。

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 フィールドを使用しているか確認してください。

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)に対して自動リトライする
    動画生成リクエストの送信。
    
    Thundering Herd 問題を避けるため指数バックオフ + ジッターを使用:
    - 試行 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:
            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:
            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 の範囲
    duration = payload.get("duration", 5)
    if duration < 4 or duration > 15:
        errors.append(f"'duration' must be 4-15, got {duration}")
    
    # quality の値
    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}'")
    
    # aspect_ratio の値
    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シンプル、サーバー不要
プロダクション Web アプリWebhookスケーラブル、リクエストの無駄なし
バッチ処理(100+ 動画)Webhook + キューすべて送信、完了順に処理
CLI ツールPollingサーバーインフラ不要
モバイルアプリバックエンドWebhook完了時にユーザーにプッシュ通知
サーバーレス(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):
        """1 つの動画を生成して結果を返します。"""
        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 レスポンス待ち)であり CPU バウンドではないため、プロセスではなくスレッドを使用します。
  • エラー分離 — 各動画生成は独立しています。1 つの失敗がバッチ全体を止めることはありません。

次のステップ

基本をすべてカバーしました — テキストから動画、画像から動画、非同期 polling、webhook、エラー処理、バッチ処理。さらに深掘りするためのリソースを紹介します:

高度な機能を探索

リファレンスドキュメント

何か作ってみよう

学んだことを組み合わせてみましょう。プロジェクトのアイデア:

  • 自動化された商品動画パイプライン — 商品写真をアップロードし、マーケティング動画を大量生成(EC 動画ガイド参照)
  • ソーシャルメディアコンテンツエンジン — テキストブリーフから縦型ショート動画を生成し、TikTok/Reels に直接投稿
  • ストーリーボードから動画ツール — 連続画像をカメラムーブメント制御付きのアニメーションシーンに変換
  • AI 動画編集パイプライン — Seedance 2.0 の動画拡張機能を活用して、短いクリップからより長いストーリーを構成

準備はできましたか?無料 EvoLink API Key を取得して、今日から動画を生成しましょう。


よくある質問

Seedance 2.0 の動画生成にはどのくらい時間がかかりますか?

通常、長さと品質設定に応じて 30〜120 秒です。5 秒 720p の動画は約 50 秒で完了します。15 秒 1080p の動画は 2〜3 分かかることがあります。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 として渡します。

料金は動画の長さと品質ティアに基づきます。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 エラーが発生した場合は、エラー処理セクションで示した指数バックオフを実装してください。大規模な使用ケース(1 日数千本の動画)の場合は、EvoLink サポートに連絡してカスタムレート制限と専用キャパシティについて相談してください。

Seedance 2.0 を商用プロジェクトに使えますか?

はい。EvoLink API を通じて生成された動画は商用利用が許可されています。出力の所有権はユーザーにあり、製品、マーケティング資料、クライアント納品物、公開コンテンツに使用できます。詳細なライセンス条件と商用利用のベストプラクティスは Seedance 2.0 著作権ガイドを参照してください。


完全なスクリプト

このチュートリアルの全コードを 1 つのファイルにまとめました — コピー、ペースト、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.