返回 Pixelle-Video
video_client.py
根目录 / pixelle_video / services / api_services / video_client.py
1 """
2 统一视频生成客户端
3 根据 model 名称自动路由到对应后端:
4 - wan* → DashscopeVideoClient (DashScope VideoSynthesis)
5 - kling* → KlingVideoClient (可灵 AI)
6 """
7
8 import os
9 import logging
10 from typing import Optional
11 from .config import Config
12
13 try:
14 from .video_dashscope import DashscopeVideoClient
15 from .video_kling import KlingVideoClient
16 from .video_seedance import SeedanceVideoClient
17 except ImportError:
18 from video_dashscope import DashscopeVideoClient
19 from video_kling import KlingVideoClient
20 from video_seedance import SeedanceVideoClient
21
22 logger = logging.getLogger(__name__)
23
24
25 class VideoClient:
26 """
27 统一视频生成客户端
28 参照 ImageClient 模式,按模型名路由到不同后端
29 """
30
31 def __init__(
32 self,
33 dashscope_api_key: Optional[str] = None,
34 dashscope_base_url: Optional[str] = None,
35 dashscope_local_proxy: Optional[str] = None,
36 kling_access_key: Optional[str] = None,
37 kling_secret_key: Optional[str] = None,
38 kling_base_url: Optional[str] = None,
39 kling_local_proxy: Optional[str] = None,
40 ark_api_key: Optional[str] = None,
41 ark_base_url: Optional[str] = None,
42 ark_local_proxy: Optional[str] = None,
43 ):
44 self._dashscope_api_key = dashscope_api_key or Config.DASHSCOPE_API_KEY
45 self._dashscope_base_url = dashscope_base_url or Config.DASHSCOPE_BASE_URL
46 self._dashscope_local_proxy = dashscope_local_proxy
47
48 self._kling_access_key = kling_access_key or Config.KLING_ACCESS_KEY
49 self._kling_secret_key = kling_secret_key or Config.KLING_SECRET_KEY
50 self._kling_base_url = kling_base_url or Config.KLING_BASE_URL
51 self._kling_local_proxy = kling_local_proxy
52
53 self._ark_api_key = ark_api_key or Config.ARK_API_KEY or os.getenv("ARK_API_KEY")
54 self._ark_base_url = ark_base_url or Config.ARK_BASE_URL or os.getenv("ARK_BASE_URL")
55 self._ark_local_proxy = ark_local_proxy
56
57 self._dashscope_client = None
58 self._kling_client = None
59 self._seedance_client = None
60
61 @property
62 def Dashscope_client(self):
63 """Create DashScope client only when a Wan/HappyHorse model is selected."""
64 if self._dashscope_client is None:
65 self._dashscope_client = DashscopeVideoClient(
66 api_key=self._dashscope_api_key,
67 base_url=self._dashscope_base_url,
68 local_proxy=self._dashscope_local_proxy,
69 )
70 return self._dashscope_client
71
72 @property
73 def kling_client(self):
74 """Create Kling client only when a Kling model is selected."""
75 if self._kling_client is None:
76 self._kling_client = KlingVideoClient(
77 access_key=self._kling_access_key,
78 secret_key=self._kling_secret_key,
79 base_url=self._kling_base_url,
80 local_proxy=self._kling_local_proxy,
81 )
82 return self._kling_client
83
84 @property
85 def seedance_client(self):
86 """Create Seedance client only when a Seedance/ARK model is selected."""
87 if self._seedance_client is None:
88 self._seedance_client = SeedanceVideoClient(
89 api_key=self._ark_api_key,
90 base_url=self._ark_base_url,
91 local_proxy=self._ark_local_proxy,
92 )
93 return self._seedance_client
94
95 def generate_video(
96 self,
97 prompt: str,
98 image_path: Optional[str],
99 save_path: str,
100 model: str = "wan2.7-i2v",
101 duration: int = 5,
102 shot_type: str = "multi",
103 sound: str = "",
104 video_ratio: str = "16:9",
105 resolution: Optional[str] = None,
106 last_image_path: Optional[str] = None,
107 first_clip_path: Optional[str] = None,
108 reference_image_path: Optional[str] = None,
109 reference_image_paths: Optional[list[str]] = None,
110 reference_video_paths: Optional[list[str]] = None,
111 reference_audio_path: Optional[str] = None,
112 audio_path: Optional[str] = None,
113 negative_prompt: Optional[str] = None,
114 prompt_extend: Optional[bool] = None,
115 watermark: Optional[bool] = None,
116 seed: Optional[int] = None,
117 mode: str = "pro",
118 cfg_scale: float = 0.5,
119 generate_audio: Optional[bool] = None,
120 audio: Optional[bool] = None,
121 ) -> str:
122 """
123 生成视频
124
125 Args:
126 prompt: 视频描述提示词
127 image_path: 输入图片本地路径;DashScope wan2.7 视频续写可为空并使用 first_clip_path
128 save_path: 输出视频保存路径
129 model: 模型名,决定使用哪个后端
130 duration: 视频时长(秒)
131 shot_type: 镜头类型 "single" / "multi"
132
133 Returns:
134 video_url: 远端视频 URL
135
136 Raises:
137 FileNotFoundError: 输入图片不存在
138 RuntimeError: 生成或下载失败
139 """
140 if not model:
141 model = "wan2.7-i2v"
142
143 if Config.PRINT_MODEL_INPUT:
144 print("---- VIDEO GENERATION REQUEST ----")
145 print(f"Prompt: {prompt}")
146 if image_path and str(image_path).startswith("data:"):
147 print(f"Image: [Base64图片]")
148 else:
149 print(f"Image: {image_path}")
150 print(f"Model: {model}")
151 print(f"Duration: {duration}s")
152 print(f"Shot Type: {shot_type}")
153 print(f"Video Ratio: {video_ratio}")
154 if resolution:
155 print(f"Resolution: {resolution}")
156 if last_image_path:
157 print(f"Last Image: {last_image_path}")
158 if first_clip_path:
159 print(f"First Clip: {first_clip_path}")
160 if reference_image_path:
161 print(f"Reference Image: {reference_image_path}")
162 if reference_image_paths:
163 print(f"Reference Images: {reference_image_paths}")
164 if reference_video_paths:
165 print(f"Reference Videos: {reference_video_paths}")
166 if reference_audio_path:
167 print(f"Reference Audio: {reference_audio_path}")
168 if audio_path:
169 print(f"Audio: {audio_path}")
170 if negative_prompt:
171 print(f"Negative Prompt: {negative_prompt}")
172 if sound:
173 print(f"Sound: {sound}")
174 print(f"Save: {save_path}")
175 print("-" * 30)
176
177 model_lower = model.lower()
178
179 if "kling" in model_lower:
180 return self._generate_kling(
181 prompt,
182 image_path,
183 save_path,
184 model,
185 duration,
186 sound,
187 mode,
188 cfg_scale,
189 negative_prompt or "",
190 video_ratio,
191 )
192 elif "seedance" in model_lower:
193 return self._generate_seedance(
194 prompt,
195 image_path,
196 save_path,
197 model,
198 duration,
199 video_ratio,
200 resolution,
201 seed,
202 watermark,
203 generate_audio,
204 )
205 elif "wan" in model_lower or "happyhorse" in model_lower:
206 return self._generate_wan(
207 prompt,
208 image_path,
209 save_path,
210 model,
211 duration,
212 shot_type,
213 video_ratio,
214 last_image_path,
215 first_clip_path,
216 reference_image_path,
217 reference_image_paths,
218 reference_video_paths,
219 reference_audio_path,
220 audio_path,
221 negative_prompt,
222 resolution,
223 prompt_extend,
224 watermark if watermark is not None else False,
225 seed,
226 audio,
227 )
228 else:
229 raise ValueError(f"未知的视频生成模型: {model}")
230
231 def _generate_wan(
232 self,
233 prompt: str,
234 image_path: Optional[str],
235 save_path: str,
236 model: str,
237 duration: int,
238 shot_type: str,
239 video_ratio: str,
240 last_image_path: Optional[str],
241 first_clip_path: Optional[str],
242 reference_image_path: Optional[str],
243 reference_image_paths: Optional[list[str]],
244 reference_video_paths: Optional[list[str]],
245 reference_audio_path: Optional[str],
246 audio_path: Optional[str],
247 negative_prompt: Optional[str],
248 resolution: Optional[str],
249 prompt_extend: Optional[bool],
250 watermark: bool,
251 seed: Optional[int],
252 audio: Optional[bool],
253 ) -> str:
254 """通过万象模型生成视频"""
255 logger.info(f"VideoClient: 路由至万象 model={model}")
256 return self.Dashscope_client.generate_video(
257 prompt=prompt,
258 image_path=image_path,
259 save_path=save_path,
260 model=model,
261 duration=duration,
262 shot_type=shot_type,
263 video_ratio=video_ratio,
264 last_image_path=last_image_path,
265 first_clip_path=first_clip_path,
266 reference_image_path=reference_image_path,
267 reference_image_paths=reference_image_paths,
268 reference_video_paths=reference_video_paths,
269 reference_audio_path=reference_audio_path,
270 audio_path=audio_path,
271 negative_prompt=negative_prompt,
272 resolution=resolution,
273 prompt_extend=prompt_extend,
274 watermark=watermark,
275 seed=seed,
276 audio=audio,
277 )
278
279 def _generate_kling(
280 self,
281 prompt: str,
282 image_path: Optional[str],
283 save_path: str,
284 model: str,
285 duration: int = 5,
286 sound: str = "",
287 mode: str = "pro",
288 cfg_scale: float = 0.5,
289 negative_prompt: str = "",
290 video_ratio: str = "16:9",
291 ) -> str:
292 """通过可灵模型生成视频"""
293 logger.info(f"VideoClient: 路由至可灵 model={model}")
294 return self.kling_client.generate_video(
295 prompt=prompt,
296 image_path=image_path,
297 save_path=save_path,
298 model=model,
299 duration=duration,
300 sound=sound,
301 mode=mode,
302 cfg_scale=cfg_scale,
303 negative_prompt=negative_prompt,
304 aspect_ratio=video_ratio,
305 )
306
307 def _generate_seedance(
308 self,
309 prompt: str,
310 image_path: Optional[str],
311 save_path: str,
312 model: str,
313 duration: int = 5,
314 video_ratio: str = "16:9",
315 resolution: Optional[str] = None,
316 seed: Optional[int] = None,
317 watermark: Optional[bool] = None,
318 generate_audio: Optional[bool] = None,
319 ) -> str:
320 """通过 Seedance 模型生成视频"""
321 logger.info(f"VideoClient: 路由至 Seedance model={model}")
322 return self.seedance_client.generate_video(
323 prompt=prompt,
324 image_path=image_path,
325 save_path=save_path,
326 model=model,
327 duration=duration,
328 ratio=video_ratio,
329 resolution=resolution or "720p",
330 seed=seed,
331 watermark=watermark,
332 generate_audio=generate_audio,
333 )
334
334 lines PYTHON