""" 统一视频生成客户端 根据 model 名称自动路由到对应后端: - wan* → DashscopeVideoClient (DashScope VideoSynthesis) - kling* → KlingVideoClient (可灵 AI) """ import os import logging from typing import Optional from .config import Config try: from .video_dashscope import DashscopeVideoClient from .video_kling import KlingVideoClient from .video_seedance import SeedanceVideoClient except ImportError: from video_dashscope import DashscopeVideoClient from video_kling import KlingVideoClient from video_seedance import SeedanceVideoClient logger = logging.getLogger(__name__) class VideoClient: """ 统一视频生成客户端 参照 ImageClient 模式,按模型名路由到不同后端 """ def __init__( self, dashscope_api_key: Optional[str] = None, dashscope_base_url: Optional[str] = None, dashscope_local_proxy: Optional[str] = None, kling_access_key: Optional[str] = None, kling_secret_key: Optional[str] = None, kling_base_url: Optional[str] = None, kling_local_proxy: Optional[str] = None, ark_api_key: Optional[str] = None, ark_base_url: Optional[str] = None, ark_local_proxy: Optional[str] = None, ): self._dashscope_api_key = dashscope_api_key or Config.DASHSCOPE_API_KEY self._dashscope_base_url = dashscope_base_url or Config.DASHSCOPE_BASE_URL self._dashscope_local_proxy = dashscope_local_proxy self._kling_access_key = kling_access_key or Config.KLING_ACCESS_KEY self._kling_secret_key = kling_secret_key or Config.KLING_SECRET_KEY self._kling_base_url = kling_base_url or Config.KLING_BASE_URL self._kling_local_proxy = kling_local_proxy self._ark_api_key = ark_api_key or Config.ARK_API_KEY or os.getenv("ARK_API_KEY") self._ark_base_url = ark_base_url or Config.ARK_BASE_URL or os.getenv("ARK_BASE_URL") self._ark_local_proxy = ark_local_proxy self._dashscope_client = None self._kling_client = None self._seedance_client = None @property def Dashscope_client(self): """Create DashScope client only when a Wan/HappyHorse model is selected.""" if self._dashscope_client is None: self._dashscope_client = DashscopeVideoClient( api_key=self._dashscope_api_key, base_url=self._dashscope_base_url, local_proxy=self._dashscope_local_proxy, ) return self._dashscope_client @property def kling_client(self): """Create Kling client only when a Kling model is selected.""" if self._kling_client is None: self._kling_client = KlingVideoClient( access_key=self._kling_access_key, secret_key=self._kling_secret_key, base_url=self._kling_base_url, local_proxy=self._kling_local_proxy, ) return self._kling_client @property def seedance_client(self): """Create Seedance client only when a Seedance/ARK model is selected.""" if self._seedance_client is None: self._seedance_client = SeedanceVideoClient( api_key=self._ark_api_key, base_url=self._ark_base_url, local_proxy=self._ark_local_proxy, ) return self._seedance_client def generate_video( self, prompt: str, image_path: Optional[str], save_path: str, model: str = "wan2.7-i2v", duration: int = 5, shot_type: str = "multi", sound: str = "", video_ratio: str = "16:9", resolution: Optional[str] = None, last_image_path: Optional[str] = None, first_clip_path: Optional[str] = None, reference_image_path: Optional[str] = None, reference_image_paths: Optional[list[str]] = None, reference_video_paths: Optional[list[str]] = None, reference_audio_path: Optional[str] = None, audio_path: Optional[str] = None, negative_prompt: Optional[str] = None, prompt_extend: Optional[bool] = None, watermark: Optional[bool] = None, seed: Optional[int] = None, mode: str = "pro", cfg_scale: float = 0.5, generate_audio: Optional[bool] = None, audio: Optional[bool] = None, ) -> str: """ 生成视频 Args: prompt: 视频描述提示词 image_path: 输入图片本地路径;DashScope wan2.7 视频续写可为空并使用 first_clip_path save_path: 输出视频保存路径 model: 模型名,决定使用哪个后端 duration: 视频时长(秒) shot_type: 镜头类型 "single" / "multi" Returns: video_url: 远端视频 URL Raises: FileNotFoundError: 输入图片不存在 RuntimeError: 生成或下载失败 """ if not model: model = "wan2.7-i2v" if Config.PRINT_MODEL_INPUT: print("---- VIDEO GENERATION REQUEST ----") print(f"Prompt: {prompt}") if image_path and str(image_path).startswith("data:"): print(f"Image: [Base64图片]") else: print(f"Image: {image_path}") print(f"Model: {model}") print(f"Duration: {duration}s") print(f"Shot Type: {shot_type}") print(f"Video Ratio: {video_ratio}") if resolution: print(f"Resolution: {resolution}") if last_image_path: print(f"Last Image: {last_image_path}") if first_clip_path: print(f"First Clip: {first_clip_path}") if reference_image_path: print(f"Reference Image: {reference_image_path}") if reference_image_paths: print(f"Reference Images: {reference_image_paths}") if reference_video_paths: print(f"Reference Videos: {reference_video_paths}") if reference_audio_path: print(f"Reference Audio: {reference_audio_path}") if audio_path: print(f"Audio: {audio_path}") if negative_prompt: print(f"Negative Prompt: {negative_prompt}") if sound: print(f"Sound: {sound}") print(f"Save: {save_path}") print("-" * 30) model_lower = model.lower() if "kling" in model_lower: return self._generate_kling( prompt, image_path, save_path, model, duration, sound, mode, cfg_scale, negative_prompt or "", video_ratio, ) elif "seedance" in model_lower: return self._generate_seedance( prompt, image_path, save_path, model, duration, video_ratio, resolution, seed, watermark, generate_audio, ) elif "wan" in model_lower or "happyhorse" in model_lower: return self._generate_wan( prompt, image_path, save_path, model, duration, shot_type, video_ratio, last_image_path, first_clip_path, reference_image_path, reference_image_paths, reference_video_paths, reference_audio_path, audio_path, negative_prompt, resolution, prompt_extend, watermark if watermark is not None else False, seed, audio, ) else: raise ValueError(f"未知的视频生成模型: {model}") def _generate_wan( self, prompt: str, image_path: Optional[str], save_path: str, model: str, duration: int, shot_type: str, video_ratio: str, last_image_path: Optional[str], first_clip_path: Optional[str], reference_image_path: Optional[str], reference_image_paths: Optional[list[str]], reference_video_paths: Optional[list[str]], reference_audio_path: Optional[str], audio_path: Optional[str], negative_prompt: Optional[str], resolution: Optional[str], prompt_extend: Optional[bool], watermark: bool, seed: Optional[int], audio: Optional[bool], ) -> str: """通过万象模型生成视频""" logger.info(f"VideoClient: 路由至万象 model={model}") return self.Dashscope_client.generate_video( prompt=prompt, image_path=image_path, save_path=save_path, model=model, duration=duration, shot_type=shot_type, video_ratio=video_ratio, last_image_path=last_image_path, first_clip_path=first_clip_path, reference_image_path=reference_image_path, reference_image_paths=reference_image_paths, reference_video_paths=reference_video_paths, reference_audio_path=reference_audio_path, audio_path=audio_path, negative_prompt=negative_prompt, resolution=resolution, prompt_extend=prompt_extend, watermark=watermark, seed=seed, audio=audio, ) def _generate_kling( self, prompt: str, image_path: Optional[str], save_path: str, model: str, duration: int = 5, sound: str = "", mode: str = "pro", cfg_scale: float = 0.5, negative_prompt: str = "", video_ratio: str = "16:9", ) -> str: """通过可灵模型生成视频""" logger.info(f"VideoClient: 路由至可灵 model={model}") return self.kling_client.generate_video( prompt=prompt, image_path=image_path, save_path=save_path, model=model, duration=duration, sound=sound, mode=mode, cfg_scale=cfg_scale, negative_prompt=negative_prompt, aspect_ratio=video_ratio, ) def _generate_seedance( self, prompt: str, image_path: Optional[str], save_path: str, model: str, duration: int = 5, video_ratio: str = "16:9", resolution: Optional[str] = None, seed: Optional[int] = None, watermark: Optional[bool] = None, generate_audio: Optional[bool] = None, ) -> str: """通过 Seedance 模型生成视频""" logger.info(f"VideoClient: 路由至 Seedance model={model}") return self.seedance_client.generate_video( prompt=prompt, image_path=image_path, save_path=save_path, model=model, duration=duration, ratio=video_ratio, resolution=resolution or "720p", seed=seed, watermark=watermark, generate_audio=generate_audio, )