February 27, 2026

Seedance 2.0 API 教程:用 Python 从零生成你的第一个 AI 视频

手把手教你用 Python 调用 Seedance 2.0 API 生成 AI 视频。涵盖文生视频、图生视频、异步轮询、Webhook 回调和错误处理的完整教程。

Seedance 2.0 API 教程:用 Python 从零生成你的第一个 AI 视频

Seedance 2.0 是字节跳动最强的 AI 视频生成模型——支持多模态引用、原生音频生成、电影级镜头控制,可生成 4–15 秒、最高 1080p 的视频。本教程将带你用 Python 走完整个 API 调用流程:从获取 API Key 到下载你生成的第一个视频。

读完这篇教程,你将拥有文生视频、图生视频、异步轮询、Webhook 回调和错误重试的完整可运行代码。所有代码示例都经过真实 API 测试验证。

关于 Seedance 2.0 与 1.5 的说明: Seedance 2.0 正在逐步开放中。你现在就可以用 seedance-1.5-pro 测试完整流程——等 2.0 全面上线后,只需改一下模型名称即可。所有接口、参数和返回格式完全一致。2.0 的主要升级:多模态引用(图片、视频、音频混合输入)、原生音频生成、更好的物理模拟和视频编辑能力。本教程的所有内容对两个版本都适用。

免费获取 API Key,跟着教程一起动手。


你将构建什么(以及你需要什么)

先看看 Seedance 生成的视频效果——只用了一次 API 调用:

在本教程中,你将编写 Python 代码实现以下功能:

  1. 发送文本提示词 → 获得生成的视频
  2. 发送图片 → 将静态图片转为动态视频
  3. 异步轮询获取结果
  4. 像生产环境一样处理错误和重试
  5. 通过 Webhook 接收结果(无需轮询)
  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 网关,通过一个 API Key 统一接入多个 AI 视频模型——包括 Seedance 2.0、Kling 等。

获取步骤:

  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 — 每个请求都携带的两个 Header:Authorization 使用 Bearer Token 方式携带 API Key,Content-Type 告诉服务器我们发送的是 JSON。

接下来添加可复用的辅助函数:

# ── 可复用的轮询辅助函数 ─────────────────────────────────────
def wait_for_video(task_id, poll_interval=10, timeout=600):
    """
    轮询视频生成任务,直到完成或失败。
    
    Args:
        task_id: 生成接口返回的任务 ID。
        poll_interval: 轮询间隔秒数(默认 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')}"
            )

        # 等待后再次轮询
        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 增量下载。

以上三部分——配置、轮询、下载——就是基础框架。后面的每个代码示例都会用到它们,不再重复,只展示新的 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']}")

    # 轮询直到视频就绪
    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 适合抖音/Reels/Shorts,1:1 适合 Instagram 信息流。
  • generate_audio — 设为 true 时,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——用它来查询状态和获取结果
status初始为 pending,然后变为 processing,最终变为 completedfailed
progress0–100 的百分比。在 processing 阶段实时更新
estimated_time预计完成时间(秒),服务端估算
credits_reserved本次任务预扣的额度。任务失败时自动退还
task_info.can_cancel是否可以取消该任务(完成前始终为 true
created任务提交时间的 Unix 时间戳
usage.billing_rule计费方式——per_second 表示按时长计费

小贴士: 提交后立即把 id 保存到文件或数据库。如果你的脚本在轮询过程中崩溃了,可以用保存的任务 ID 调用 wait_for_video() 恢复。任务在服务器上保留 24 小时。

轮询过程

wait_for_video() 函数每 10 秒轮询一次。以下是真实的输出:

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 提示词指南——涵盖分镜脚本格式、风格关键词和时间轴语法。


轮询结果:理解异步工作流

视频生成需要 30–120+ 秒,取决于时长和质量设置。API 使用异步任务模式——和 OpenAI、Stability AI 以及大多数生成式 AI API 用的是同一套模式:

  1. 提交 → POST 到 /v1/videos/generations → 立即获得任务 ID
  2. 轮询 → 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出错了——查看错误详情终态

轮询最佳实践

轮询间隔: 10 秒是合理的默认值。太快浪费请求次数,还可能触发限流;太慢则拖延你的流水线。对于时间敏感的应用,可以 5 秒一次,但没必要更快了。

超时时间: 根据你的参数设置合理的上限:

配置预期耗时建议超时
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 时有效。任务达到 completedfailed 后无法取消。取消时预扣的额度会自动退还。

小贴士: 尽早在你的 UI 中加入取消功能。用户难免会提交错误的提示词,等 2 分钟生成一个不想要的视频既浪费时间又浪费额度。

前面搭建的 wait_for_video() 函数已经处理了标准轮询流程。如果你想完全跳过轮询,请直接跳到下面的 Webhook 章节


让图片动起来(图生视频)

有产品图、角色插画或风景照想让它动起来?传入 image_url,Seedance 就会把它动画化。这是电商产品视频最强大的功能之一——把一张静态产品图变成吸引眼球的视频广告。

使用前面第一个示例中相同的配置和轮询函数。

# ── 图生视频 ─────────────────────────────────────────────────
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 或内网地址)。
  • @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把图片用作结尾帧Logo 揭示、转场
@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"
  }
}

图生视频和文生视频遵循完全相同的异步模式——提交、轮询、下载。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 的方法:

# 方式一:上传到 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"

# 方式二:使用临时文件托管 API
# 很多服务提供免费的临时托管用于测试

更多图生视频高级技巧——首尾帧控制、多图合成、电商产品动画——请看图生视频详解


自定义你的视频

生成请求中可调整的所有参数:

参数类型默认值选项说明
modelstringseedance-2.0必填。使用的模型。
promptstring≤2000 tokens必填。视频描述,可使用 @tags。
durationinteger54–15视频时长(秒)。
qualitystring720p480p720p1080p分辨率档位。越高消耗越多额度。
aspect_ratiostring16:916:99:161:14:33:421:9输出宽高比。
generate_audiobooleantruetruefalse启用 AI 生成音频/音乐。
image_urlsarray≤9 张图片参考图片。在提示词中用 @Image1、@Image2... 引用。
video_urlsarray≤3 个视频参考视频。在提示词中用 @Video1、@Video2... 引用。
audio_urlsarray≤3 个音频参考音频。在提示词中用 @Audio1、@Audio2... 引用。
callback_urlstringHTTPS URL完成时的 Webhook 回调地址。

Seedance 2.0 与 1.5 的区别: 以上所有参数对 seedance-2.0seedance-1.5-pro 都适用。主要区别在于:video_urlsaudio_urls 和多图引用(@Image2@Image9)是 2.0 独有功能。在 1.5 上使用它们,API 会返回 400 错误,并明确提示该功能不支持。

快速示例

竖版社交媒体视频(抖音/Reels):

使用前面第一个示例中相同的配置和轮询函数。

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 的视频——抖音、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 对象始终包含 messagetypecode 字段在大多数错误中存在但不是全部。优先检查 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 秒(含)。

400 — 不支持的质量档位:

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

常见于传 "quality": "4k""quality": "hd" 时。必须使用精确的字符串:480p720p1080p

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_errorAPI 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 调用,处理瞬态错误:

使用前面第一个示例中相同的配置和轮询函数。

import random

def generate_video_with_retry(payload, max_retries=3):
    """
    提交视频生成请求,对瞬态错误(429、500、502、503)自动重试。
    
    使用指数退避 + 随机抖动避免惊群效应:
    - 第 1 次:等待 ~1s
    - 第 2 次:等待 ~2s  
    - 第 3 次:等待 ~4s
    
    不可重试的错误(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 请求前先在本地捕获明显的错误,节省额度和时间:

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(跳过轮询)

轮询对脚本和原型开发来说完全够用。对于生产系统,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']}")
# 不需要轮询——你的 Webhook 会收到结果

视频就绪后,API 会向你的 callback_url 发送一个 POST 请求,携带完整的 task 对象——和你通过轮询拿到的内容完全一样。

Webhook 要求

要求详情
协议仅限 HTTPS(不支持 HTTP)——安全要求
响应10 秒内返回 2xx
重试失败时重试 3 次(间隔 1s、2s、4s)
URL 长度≤ 2048 字符
网络不支持内网/私有 IP(localhost、10.x.x.x、192.168.x.x)
请求体JSON POST,携带完整 task 对象

生产级 Flask Webhook 接收器

以下是一个完整的 Webhook 服务器,使用 Flask 实现,包含校验、错误处理和异步视频下载:

# 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 的 http:// URL 而不是 https://。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 轮询:怎么选?

场景推荐方案原因
快速原型 / 脚本轮询更简单,不需要服务器
生产级 Web 应用Webhook可扩展,不浪费请求
批量处理(100+ 视频)Webhook + 消息队列全部提交,按完成顺序处理
CLI 工具轮询不需要服务器基础设施
移动应用后端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 响应),而不是 CPU 密集型。
  • 错误隔离 — 每个视频生成都是独立的。一个失败不会中断整个批次。

下一步

你已经掌握了基础——文生视频、图生视频、异步轮询、Webhook、错误处理和批量处理。以下是深入学习的方向:

探索高级功能

参考文档

动手做点什么

把你学到的东西组合起来。以下是一些项目灵感:

  • 自动化产品视频流水线 — 上传产品图,批量生成营销视频(参考电商视频指南
  • 社交媒体内容引擎 — 从文本简报生成竖版短视频,直接发布到抖音/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,提取最后一帧,作为 @Image1 as first frame 传入片段 2。

定价基于视频时长和质量档位。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 客户端都能用。本教程中的概念(异步轮询、Webhook、错误处理)可以直接用 Node.js 的 fetchaxios 实现。EvoLink 还提供官方的 Node.js 和 Python SDK,帮你处理轮询和重试。

视频完成时我的 Webhook 服务器挂了怎么办?

API 会以递增间隔重试 3 次 Webhook 投递(1s、2s、4s)。如果 3 次都失败,Webhook 就放弃了——但视频仍然可用。你随时可以用 GET /v1/tasks/{task_id} 轮询获取结果作为兜底。因此,最佳实践是在提交时保存任务 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):
    """轮询视频生成任务直到完成。"""
    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.