返回 VideoClaw
image_seedream.py
根目录 / video-claw / video-claw / backend / models / image_seedream.py
1 """
2 Seedream 图像生成 API 客户端
3 字节跳动 ARK - doubao-seedream-5-0-260128 模型
4 """
5
6 import os
7 import sys
8
9 models_dir = os.path.dirname(os.path.abspath(__file__))
10 backend_dir = os.path.dirname(models_dir)
11 if backend_dir not in sys.path:
12 sys.path.insert(0, backend_dir)
13
14 import time
15 import logging
16 import httpx
17 from typing import Optional, List, Dict
18 from openai import OpenAI
19 from config import Config
20
21 # 模型名称映射表(旧名称 -> 新名称)
22 MODEL_NAME_MAP: Dict[str, str] = {
23 # doubao-seedream-5-0 系列
24 "doubao-seedream-5-0": "doubao-seedream-5-0-260128",
25 # doubao-seedream-4-5 系列
26 "doubao-seedream-4-5": "doubao-seedream-4-5-251128",
27 # doubao-seedream-4-0 系列
28 "doubao-seedream-4-0": "doubao-seedream-4-0-250828",
29 }
30
31
32 def normalize_model_name(model: str) -> str:
33 """
34 规范化模型名称
35
36 Args:
37 model: 传入的模型名称
38
39 Returns:
40 规范化后的模型名称
41 """
42 return MODEL_NAME_MAP.get(model, model)
43
44
45 class SeedreamClient:
46 """
47 Seedream 图像生成客户端(字节跳动 ARK)
48 支持文生图功能
49 """
50
51 def __init__(
52 self,
53 api_key: Optional[str] = None,
54 base_url: Optional[str] = None,
55 timeout: int = 120,
56 ) -> None:
57 """
58 初始化 Seedream 客户端
59
60 Args:
61 api_key: ARK API Key
62 base_url: ARK API 基础 URL
63 timeout: HTTP请求超时时间(秒)
64 """
65 self.api_key = api_key or Config.ARK_API_KEY
66 self.base_url = base_url or Config.ARK_BASE_URL or "https://ark.cn-beijing.volces.com/api/v3"
67 self.timeout = timeout
68
69 if not self.api_key:
70 logging.warning(
71 "SeedreamClient missing api_key. Set ARK_API_KEY."
72 )
73
74 kwargs = {
75 "base_url": self.base_url,
76 "api_key": self.api_key,
77 "timeout": timeout,
78 }
79 proxy = Config.provider_proxy("ark")
80 if proxy:
81 kwargs["http_client"] = httpx.Client(proxy=proxy, timeout=timeout)
82 self.client = OpenAI(**kwargs)
83
84 def generate_image(
85 self,
86 prompt: str,
87 session_id: str,
88 model: str = "doubao-seedream-4-5-251128",
89 size: str = "1920*1080",
90 image_paths: Optional[List[str]] = None,
91 **kwargs
92 ) -> List[str]:
93 """
94 生成图片
95
96 Args:
97 prompt: 提示词
98 session_id: 任务或会话ID,用于构建存储路径
99 model: 模型名称
100 size: 生成图片的分辨率,如 "1920*1080", "1024*1024"
101 image_paths: 参考图路径或URL列表 (图生图)
102 **kwargs: 其他生成参数
103
104 Returns:
105 生成的图片路径列表
106 """
107 if not self.api_key:
108 raise RuntimeError("ARK_API_KEY not set.")
109
110 # 规范化模型名称(旧名称 -> 新名称)
111 model = normalize_model_name(model)
112
113 # 处理分辨率 (Seedream 要求至少 3686400 像素)
114 # 常用 2K/4K 分辨率
115 size_map = {
116 # 16:9
117 "1920*1080": (1920, 1080),
118 "2048*1080": (2048, 1080), # 2K 电影
119 "2560*1440": (2560, 1440), # 2K QHD
120 "3840*2160": (3840, 2160), # 4K UHD
121 "4096*2160": (4096, 2160), # 4K 电影
122 # 9:16
123 "1080*1920": (1080, 1920),
124 "1080*2048": (1080, 2048),
125 "1440*2560": (1440, 2560),
126 "2160*3840": (2160, 3840),
127 "2160*4096": (2160, 4096),
128 # 1:1
129 "1024*1024": (1024, 1024),
130 "2048*2048": (2048, 2048), # 2K 正方
131 # 4:3
132 "1920*1440": (1920, 1440),
133 "2560*1920": (2560, 1920),
134 # 3:4
135 "1440*1920": (1440, 1920),
136 "1920*2560": (1920, 2560),
137 }
138
139 width, height = 1920, 1080 # 默认
140 min_pixels = 3686400
141
142 if size:
143 parts = size.split("*")
144 if len(parts) == 2:
145 w, h = int(parts[0]), int(parts[1])
146 width, height = w, h
147
148 # 确保满足最小像素要求
149 if width * height < min_pixels:
150 # 查找相同宽高比的常用分辨率
151 aspect_ratio = width / height
152 for (w, h) in size_map.values():
153 if abs(w / h - aspect_ratio) < 0.01 and w * h >= min_pixels:
154 width, height = w, h
155 break
156 else:
157 # 没有找到合适的,按比例放大
158 scale = (min_pixels / (width * height)) ** 0.5
159 width = int(width * scale)
160 height = int(height * scale)
161 width = width if width % 2 == 0 else width + 1
162 height = height if height % 2 == 0 else height + 1
163
164 # 构建 extra_body
165 extra_body = {
166 "watermark": False,
167 "sequential_image_generation": "disabled",
168 }
169
170 # 添加其他参数
171 if "seed" in kwargs:
172 extra_body["seed"] = kwargs["seed"]
173 if "quality" in kwargs:
174 extra_body["quality"] = kwargs["quality"]
175 if "style" in kwargs:
176 extra_body["style"] = kwargs["style"]
177
178 # 处理参考图 (图生图)
179 image_urls = []
180 if image_paths and len(image_paths) > 0:
181 # 处理参考图:支持 URL 和本地文件
182 ref_images = []
183 for p in image_paths:
184 if p.startswith("http"):
185 ref_images.append(p)
186 elif os.path.exists(p):
187 # 转换为 base64 URL
188 import base64
189 with open(p, "rb") as f:
190 img_data = base64.b64encode(f.read()).decode("utf-8")
191 ext = os.path.splitext(p)[1].lower()
192 mime = "image/png" if ext == ".png" else "image/jpeg"
193 ref_images.append(f"data:{mime};base64,{img_data}")
194 extra_body["image"] = ref_images
195
196 # 调用 API
197 if image_paths and len(image_paths) > 0:
198 # 图生图 - image 放在 extra_body 中
199 response = self.client.images.generate(
200 model=model,
201 prompt=prompt,
202 size=f"{width}x{height}",
203 response_format="url",
204 extra_body=extra_body,
205 )
206 else:
207 # 文生图
208 response = self.client.images.generate(
209 model=model,
210 prompt=prompt,
211 size=f"{width}x{height}",
212 response_format="url",
213 extra_body=extra_body,
214 )
215
216 # 下载图片到本地
217 generated_paths = []
218 if response.data:
219 for idx, img_data in enumerate(response.data):
220 if img_data.url:
221 local_path = self._download_image(
222 img_data.url, session_id, idx
223 )
224 if local_path:
225 generated_paths.append(local_path)
226
227 return generated_paths
228
229 def _download_image(self, url: str, session_id: str, idx: int) -> Optional[str]:
230 """从URL下载图片到本地"""
231 import requests
232
233 # 构建存储路径
234 base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
235 result_dir = os.path.join(base_dir, "code", "result", "image", str(session_id))
236 os.makedirs(result_dir, exist_ok=True)
237
238 file_name = f"seedream_{int(time.time())}_{idx}.png"
239 file_path = os.path.join(result_dir, file_name)
240
241 try:
242 response = requests.get(url, timeout=self.timeout, proxies=Config.requests_proxies("ark"))
243 response.raise_for_status()
244 with open(file_path, "wb") as f:
245 f.write(response.content)
246 return file_path
247 except Exception as e:
248 logging.error(f"Failed to download image from {url}: {e}")
249 return None
250
251
252 if __name__ == "__main__":
253 import sys
254 sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
255 from config import Config
256
257 print("=== Seedream 可用性测试 ===")
258 api_key = Config.ARK_API_KEY
259 base_url = Config.ARK_BASE_URL
260 if not api_key:
261 print("✗ ARK_API_KEY 未设置,跳过")
262 sys.exit(1)
263 print(f" API Key: {api_key[:6]}***{api_key[-4:]}")
264 print(f" Base URL: {base_url}")
265
266 client = SeedreamClient(api_key=api_key, base_url=base_url)
267
268 # === 测试1: 文生图 ===
269 prompt = "星际穿越,黑洞,黑洞里冲出一辆支离破碎的复古列车,视觉冲击力,电影大片,末日既视感"
270 print(f"\n[测试1: 文生图] Prompt: {prompt}")
271 t0 = time.time()
272 try:
273 paths = client.generate_image(
274 prompt=prompt,
275 session_id="test_avail",
276 model="doubao-seedream-5-0-260128",
277 size="1920*1080",
278 )
279 elapsed = time.time() - t0
280 if paths:
281 print(f"✓ 生成 {len(paths)} 张图片 ({elapsed:.1f}s): {paths}")
282 else:
283 print(f"✗ 返回空列表 ({elapsed:.1f}s)")
284 except Exception as e:
285 print(f"✗ 图片生成失败: {e}")
286
287 # === 测试2: 图生图 ===
288 # 需要一张已有的参考图路径
289 ref_image_path = "code/result/image/test_avail/test_input.png"
290 if os.path.exists(ref_image_path):
291 prompt_i2i = "将这只猫变成赛博朋克风格"
292 print(f"\n[测试2: 图生图] Prompt: {prompt_i2i}")
293 print(f" 参考图: {ref_image_path}")
294 t0 = time.time()
295 try:
296 paths = client.generate_image(
297 prompt=prompt_i2i,
298 session_id="test_avail",
299 model="doubao-seedream-5-0-260128",
300 size="1920*1080",
301 image_paths=[ref_image_path],
302 )
303 elapsed = time.time() - t0
304 if paths:
305 print(f"✓ 生成 {len(paths)} 张图片 ({elapsed:.1f}s): {paths}")
306 else:
307 print(f"✗ 返回空列表 ({elapsed:.1f}s)")
308 except Exception as e:
309 print(f"✗ 图生图失败: {e}")
310 else:
311 print(f"\n[测试2: 图生图] ✗ 参考图不存在: {ref_image_path}")
312 print(" 跳过图生图测试,请先运行文生图测试生成参考图")
313
313 lines PYTHON