返回 Pixelle-Video
video_seedance.py
根目录 / pixelle_video / services / api_services / video_seedance.py
1 """
2 Seedance 视频生成 API 客户端 (字节跳动 ARK)
3
4 """
5
6 import os
7 import time
8 import logging
9 import requests
10 import base64
11 from typing import Optional
12
13 logger = logging.getLogger(__name__)
14
15 class SeedanceVideoClient:
16 """
17 Seedance 视频生成客户端(字节跳动 ARK)
18 支持图生视频功能,采用 提交任务 -> 轮询 -> 下载 的异步流程
19 """
20
21 def __init__(
22 self,
23 api_key: Optional[str] = None,
24 base_url: Optional[str] = None,
25 local_proxy: Optional[str] = None,
26 timeout: int = 120,
27 ) -> None:
28 self.api_key = api_key or os.getenv("ARK_API_KEY")
29 self.base_url = (base_url or os.getenv("ARK_BASE_URL") or "https://ark.cn-beijing.volces.com/api/v3").rstrip("/")
30 self.local_proxy = local_proxy
31 self.timeout = timeout
32
33 if not self.api_key:
34 logger.warning("SeedanceVideoClient: ARK_API_KEY 未设置")
35
36 def _headers(self) -> dict:
37 return {
38 "Authorization": f"Bearer {self.api_key}",
39 "Content-Type": "application/json",
40 }
41
42 def _proxies(self) -> Optional[dict]:
43 if not self.local_proxy:
44 return None
45 return {"http": self.local_proxy, "https": self.local_proxy}
46
47 def generate_video(
48 self,
49 prompt: str,
50 image_path: Optional[str],
51 save_path: str,
52 model: str = "doubao-seedance-2-0-260128",
53 duration: int = 5,
54 **kwargs
55 ) -> str:
56 """
57 图生视频完整流程
58
59 Args:
60 prompt: 提示词
61 image_path: 输入图片本地路径;为空时走文生视频
62 save_path: 输出视频保存路径
63 model: 模型名称
64 duration: 视频时长
65 """
66 if not self.api_key:
67 raise RuntimeError("ARK_API_KEY not set.")
68
69 # 1. 提交任务
70 task_id = self._submit_task(prompt, image_path, model, duration, **kwargs)
71
72 # 2. 轮询等待
73 video_url = self._poll_until_done(task_id)
74
75 # 3. 下载视频
76 self._download_video(video_url, save_path)
77
78 return video_url
79
80 def _submit_task(self, prompt: str, image_path: Optional[str], model: str, duration: int, **kwargs) -> str:
81 # 根据 Seedance 2.0 文档更新接口路径
82 url = f"{self.base_url}/contents/generations/tasks"
83
84 # 构建 content 数组
85 content = []
86 if prompt:
87 content.append({
88 "type": "text",
89 "text": prompt
90 })
91
92 if image_path:
93 if not os.path.exists(image_path):
94 raise FileNotFoundError(f"输入图片不存在: {image_path}")
95
96 with open(image_path, "rb") as f:
97 img_data = base64.b64encode(f.read()).decode("utf-8")
98 ext = os.path.splitext(image_path)[1].lower()
99 mime = "image/png" if ext == ".png" else "image/jpeg"
100 image_base64 = f"data:{mime};base64,{img_data}"
101
102 # 图生视频-首帧
103 content.append({
104 "type": "image_url",
105 "image_url": {
106 "url": image_base64
107 },
108 "role": "first_frame"
109 })
110
111 payload = {
112 "model": model,
113 "content": content,
114 "duration": duration,
115 "ratio": kwargs.get("ratio", "adaptive"),
116 "resolution": kwargs.get("resolution", "720p")
117 }
118
119 # 合并其他可选参数 (如 seed, watermark)
120 for key in ["seed", "watermark", "generate_audio"]:
121 if key in kwargs and kwargs[key] is not None:
122 payload[key] = kwargs[key]
123
124 logger.info(f"SeedanceVideoClient: 提交任务 model={model}, duration={duration}s")
125 resp = requests.post(
126 url,
127 headers=self._headers(),
128 json=payload,
129 timeout=self.timeout,
130 proxies=self._proxies(),
131 )
132
133 if not resp.ok:
134 logger.error(f"Seedance 提交失败: {resp.text}")
135 resp.raise_for_status()
136
137 data = resp.json()
138 task_id = data.get("id")
139 if not task_id:
140 raise RuntimeError(f"Seedance API 未返回任务 ID: {data}")
141
142 return task_id
143
144 def _poll_until_done(self, task_id: str, max_polls: int = 120, interval: int = 5) -> str:
145 # 同步更新查询接口路径
146 url = f"{self.base_url}/contents/generations/tasks/{task_id}"
147
148 for i in range(max_polls):
149 resp = requests.get(url, headers=self._headers(), timeout=30, proxies=self._proxies())
150 resp.raise_for_status()
151 data = resp.json()
152
153 status = data.get("status")
154 if status == "succeeded":
155 # 根据实际返回体,URL 位于 content.video_url 或 video_url
156 video_url = data.get("content", {}).get("video_url") or data.get("video_url")
157 if not video_url:
158 raise RuntimeError(f"Seedance 任务成功但未返回视频 URL: {data}")
159 return video_url
160 elif status in ("failed", "expired"):
161 error_msg = data.get("error", {}).get("message") or data.get("status_msg") or "未知错误"
162 raise RuntimeError(f"Seedance 视频生成{status}: {error_msg}")
163
164 logger.debug(f"SeedanceVideoClient: 任务进行中 {task_id}, status={status}, poll={i+1}")
165 time.sleep(interval)
166
167 raise TimeoutError(f"Seedance 视频生成超时 (task_id={task_id})")
168
169 def _download_video(self, url: str, save_path: str):
170 os.makedirs(os.path.dirname(save_path), exist_ok=True)
171 resp = requests.get(url, stream=True, timeout=120, proxies=self._proxies())
172 resp.raise_for_status()
173 with open(save_path, "wb") as f:
174 for chunk in resp.iter_content(chunk_size=8192):
175 if chunk:
176 f.write(chunk)
177 logger.info(f"SeedanceVideoClient: 视频已保存: {save_path}")
178
179 if __name__ == "__main__":
180 import sys
181 sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
182 from config import Config
183
184 logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
185
186 # ── 测试参数(按需修改) ──
187 # IMAGE_PATH = "code/result/image/test_avail/test_input.png"
188 IMAGE_PATH = "code/result/image/test_avail/test_input_human.jpg"
189 OUTPUT_PATH = "code/result/video/test_avail/seedance_test_output.mp4"
190 PROMPT = "女生把财务报表交给男生,男生看到后喜极而泣"
191 # MODELS = ["doubao-seedance-2-0-fast-260128", "doubao-seedance-2-0-260128"]
192 MODELS = ["doubao-seedance-2-0-fast-260128"]
193 DURATION = 5
194
195 print("=== Seedance (ARK) 图生视频测试 ===")
196 api_key = Config.ARK_API_KEY
197 base_url = Config.ARK_BASE_URL
198
199 if not api_key:
200 print("✗ ARK_API_KEY 未设置,请检查 .env 配置")
201 sys.exit(1)
202
203 if not os.path.exists(IMAGE_PATH):
204 print(f"✗ 输入图片不存在: {IMAGE_PATH}")
205 sys.exit(1)
206
207 print(f" API Key : {api_key[:6]}***{api_key[-4:]}")
208 print(f" Base URL : {base_url}")
209
210 for model in MODELS:
211 print("\n" + "="*40)
212 print(f" 输入图片 : {IMAGE_PATH}")
213 print(f" 输出路径 : {OUTPUT_PATH}")
214 print(f" 模型 : {model}")
215 print(f" 时长 : {DURATION}s")
216 if PROMPT:
217 print(f" 提示词 : {PROMPT[:80]}")
218
219 try:
220 client = SeedanceVideoClient(api_key=api_key, base_url=base_url)
221 print("✓ 客户端初始化成功")
222
223 start = time.time()
224 video_url = client.generate_video(
225 prompt=PROMPT,
226 image_path=IMAGE_PATH,
227 save_path=OUTPUT_PATH,
228 model=model,
229 duration=DURATION,
230 )
231 elapsed = time.time() - start
232
233 print(f"✓ 视频生成完成!耗时 {elapsed:.1f}s")
234 print(f" 远端 URL : {video_url}")
235 print(f" 本地文件 : {os.path.abspath(OUTPUT_PATH)}")
236 print(f" 文件大小 : {os.path.getsize(OUTPUT_PATH) / 1024 / 1024:.2f} MB")
237 except Exception as e:
238 print(f"✗ 失败: {e}")
239 sys.exit(1)
240 break # 只测试第一个模型
241
241 lines PYTHON