"""Canonical capability catalog for the panel-style o1key video generator.""" from __future__ import annotations from typing import Any VIDEO_PROVIDER_OPTIONS = ["seedance"] SEEDANCE_MODEL_OPTIONS = [ "seedance-2.0", "seedance-2.0-fast", "seedance-2.0-mini", "seedance-2.5", ] SEEDANCE_ROUTE_OPTIONS = ["domestic", "overseas_hc"] SEEDANCE_ASSET_CREATION_MODE_OPTIONS = ["auto", "manual"] VIDEO_GENERATION_MODE_OPTIONS = [ "text", "first_frame", "first_last_frame", "multimodal", ] VIDEO_RESOLUTION_OPTIONS = ["480p", "720p", "1080p", "4k"] VIDEO_ASPECT_RATIO_OPTIONS = ["auto", "16:9", "9:16", "4:3", "3:4", "1:1", "21:9"] VIDEO_DURATION_OPTIONS = ["auto", *[str(value) for value in range(4, 31)]] MAX_VIDEO_PROMPT_CHARS = 20_000 SEEDANCE_REFERENCE_IMAGE_MAX_BYTES = 30 * 1024 * 1024 SEEDANCE_REFERENCE_VIDEO_MAX_BYTES = 512 * 1024 * 1024 SEEDANCE_REFERENCE_AUDIO_MAX_BYTES = 100 * 1024 * 1024 SEEDANCE_REFERENCE_MIN_DIMENSION = 300 SEEDANCE_REFERENCE_MAX_DIMENSION = 6000 SEEDANCE_REFERENCE_MIN_ASPECT_RATIO = 0.4 SEEDANCE_REFERENCE_MAX_ASPECT_RATIO = 2.5 SEEDANCE_REFERENCE_VIDEO_MIN_PIXELS = 407_696 SEEDANCE_REFERENCE_VIDEO_MAX_PIXELS = 8_295_044 SEEDANCE_MODEL_MATRIX = { ("seedance-2.0", "domestic"): "doubao-seedance-2-0-260128-max", ("seedance-2.0-fast", "domestic"): "doubao-seedance-2-0-fast-260128-max", ("seedance-2.0-mini", "domestic"): "doubao-seedance-2-0-mini-260615-max", ("seedance-2.5", "domestic"): "doubao-seedance-2-5-260628-max", ("seedance-2.0", "overseas_hc"): "dreamina-seedance-2-0-hc", ("seedance-2.0-fast", "overseas_hc"): "dreamina-seedance-2-0-fast-hc", ("seedance-2.0-mini", "overseas_hc"): "dreamina-seedance-2-0-mini-hc", ("seedance-2.5", "overseas_hc"): "dreamina-seedance-2-5-hc", } SEEDANCE_CAPABILITIES = { "seedance-2.0": { "duration_min": 4, "duration_max": 15, "images": 9, "videos": 3, "audios": 3, "resolutions": ["480p", "720p", "1080p", "4k"], }, "seedance-2.0-fast": { "duration_min": 4, "duration_max": 15, "images": 9, "videos": 3, "audios": 3, "resolutions": ["480p", "720p"], }, "seedance-2.0-mini": { "duration_min": 4, "duration_max": 15, "images": 9, "videos": 3, "audios": 3, "resolutions": ["480p", "720p"], }, "seedance-2.5": { "duration_min": 4, "duration_max": 30, "images": 30, "videos": 10, "audios": 10, "resolutions": ["480p", "720p", "1080p", "4k"], }, } def _as_bool(value: Any) -> bool: if isinstance(value, bool): return value return str(value or "").strip().lower() in {"1", "true", "yes", "on", "打开", "开启"} def resolve_seedance_model(model: str, route: str) -> str: try: return SEEDANCE_MODEL_MATRIX[(model, route)] except KeyError: raise ValueError(f"Seedance 模型与线路组合无效:{model} / {route}") from None def normalize_seedance_parameters(payload: dict[str, Any]) -> dict[str, Any]: """Validate scalar Seedance controls before media upload or paid requests.""" provider = str(payload.get("provider") or "seedance").strip() if provider != "seedance": raise ValueError(f"暂不支持的视频提供方:{provider}") model = str(payload.get("model") or "seedance-2.0").strip() if model not in SEEDANCE_MODEL_OPTIONS: raise ValueError(f"不支持的 Seedance 模型:{model}") route = str(payload.get("route") or "domestic").strip() if route not in SEEDANCE_ROUTE_OPTIONS: raise ValueError(f"不支持的 Seedance 线路:{route}") mode = str(payload.get("generation_mode") or "text").strip() if mode not in VIDEO_GENERATION_MODE_OPTIONS: raise ValueError(f"不支持的视频生成模式:{mode}") asset_creation_mode = str(payload.get("asset_creation_mode") or "auto").strip() if asset_creation_mode not in SEEDANCE_ASSET_CREATION_MODE_OPTIONS: raise ValueError(f"不支持的素材创建模式:{asset_creation_mode}") prompt = str(payload.get("prompt") or "").strip() if len(prompt) > MAX_VIDEO_PROMPT_CHARS: raise ValueError(f"提示词最多支持 {MAX_VIDEO_PROMPT_CHARS} 个字符") if mode == "text" and not prompt: raise ValueError("文生视频模式下提示词不能为空") resolution = str(payload.get("resolution") or "720p").strip().lower() capabilities = SEEDANCE_CAPABILITIES[model] if resolution not in capabilities["resolutions"]: supported = " / ".join(capabilities["resolutions"]) raise ValueError(f"{model} 分辨率仅支持 {supported}") aspect_ratio = str(payload.get("aspect_ratio") or "auto").strip() if aspect_ratio not in VIDEO_ASPECT_RATIO_OPTIONS: raise ValueError(f"不支持的宽高比:{aspect_ratio}") raw_duration = str(payload.get("duration") or "5").strip().lower() if raw_duration == "auto": duration = 5 else: try: duration = int(raw_duration.removesuffix("秒")) except ValueError: raise ValueError(f"无效的视频时长:{raw_duration}") from None if not capabilities["duration_min"] <= duration <= capabilities["duration_max"]: raise ValueError( f"{model} 的时长仅支持 {capabilities['duration_min']}-{capabilities['duration_max']} 秒" ) try: seed = int(payload.get("seed") or 0) except (TypeError, ValueError): raise ValueError("seed 必须是非负整数") from None if not 0 <= seed <= 0xFFFFFFFFFFFFFFFF: raise ValueError("seed 必须是 0 到 18446744073709551615 之间的整数") filename_prefix = str(payload.get("filename_prefix") or "o1key_video").strip() if not filename_prefix: filename_prefix = "o1key_video" if len(filename_prefix) > 240: raise ValueError("文件名前缀最多支持 240 个字符") return { "provider": provider, "model": model, "actual_model": resolve_seedance_model(model, route), "route": route, "generation_mode": mode, "asset_creation_mode": asset_creation_mode, "prompt": prompt, "resolution": resolution, "aspect_ratio": aspect_ratio, "duration": duration, "generate_audio": _as_bool(payload.get("generate_audio")), "return_last_frame": _as_bool(payload.get("return_last_frame")), "seed": seed, "filename_prefix": filename_prefix, "save_location": str(payload.get("save_location") or "").strip(), "capabilities": capabilities, } def validate_seedance_media_counts( model: str, image_count: int, video_count: int, audio_count: int, *, model_label: str | None = None, ) -> None: """Apply the shared per-type Seedance reference-item limits.""" if model not in SEEDANCE_CAPABILITIES: raise ValueError(f"不支持的 Seedance 模型:{model}") capabilities = SEEDANCE_CAPABILITIES[model] label = model_label or model for media_label, count, limit in ( ("参考图片", int(image_count), capabilities["images"]), ("参考视频", int(video_count), capabilities["videos"]), ("参考音频", int(audio_count), capabilities["audios"]), ): if count > limit: raise ValueError(f"{label} 最多支持 {limit} 个{media_label},当前输入 {count} 个") def validate_seedance_reference_dimensions( width: int, height: int, label: str, *, require_video_pixel_range: bool = False, ) -> None: """Apply the shared Seedance reference image/video dimension limits.""" width = int(width or 0) height = int(height or 0) if not ( SEEDANCE_REFERENCE_MIN_DIMENSION <= width <= SEEDANCE_REFERENCE_MAX_DIMENSION and SEEDANCE_REFERENCE_MIN_DIMENSION <= height <= SEEDANCE_REFERENCE_MAX_DIMENSION ): raise ValueError( f"{label}宽高必须分别在 {SEEDANCE_REFERENCE_MIN_DIMENSION}~" f"{SEEDANCE_REFERENCE_MAX_DIMENSION}px,当前为 {width}×{height}" ) aspect_ratio = width / height if not ( SEEDANCE_REFERENCE_MIN_ASPECT_RATIO <= aspect_ratio <= SEEDANCE_REFERENCE_MAX_ASPECT_RATIO ): raise ValueError( f"{label}宽高比必须在 {SEEDANCE_REFERENCE_MIN_ASPECT_RATIO}~" f"{SEEDANCE_REFERENCE_MAX_ASPECT_RATIO},当前为 {aspect_ratio:.3f}({width}:{height})" ) if require_video_pixel_range: pixel_count = width * height if not ( SEEDANCE_REFERENCE_VIDEO_MIN_PIXELS <= pixel_count <= SEEDANCE_REFERENCE_VIDEO_MAX_PIXELS ): raise ValueError( f"{label}总像素必须在 {SEEDANCE_REFERENCE_VIDEO_MIN_PIXELS:,}~" f"{SEEDANCE_REFERENCE_VIDEO_MAX_PIXELS:,} 之间,当前为 " f"{width}×{height}({pixel_count:,} 像素)" ) def public_video_capabilities() -> dict[str, Any]: return { "providers": [ { "value": "seedance", "label": "Seedance", "models": [ { "value": model, "label": model.replace("seedance", "Seedance", 1), **SEEDANCE_CAPABILITIES[model], } for model in SEEDANCE_MODEL_OPTIONS ], "routes": [ {"value": "domestic", "label": "国内"}, {"value": "overseas_hc", "label": "海外"}, ], "modes": [ {"value": "text", "label": "文生视频"}, {"value": "first_frame", "label": "首帧图生视频"}, {"value": "first_last_frame", "label": "首尾帧生视频"}, {"value": "multimodal", "label": "多模态参考生视频"}, ], "asset_creation_modes": [ {"value": "auto", "label": "自动创建"}, {"value": "manual", "label": "手动"}, ], "aspect_ratios": VIDEO_ASPECT_RATIO_OPTIONS, } ], } __all__ = [ "MAX_VIDEO_PROMPT_CHARS", "SEEDANCE_CAPABILITIES", "SEEDANCE_ASSET_CREATION_MODE_OPTIONS", "SEEDANCE_MODEL_MATRIX", "SEEDANCE_MODEL_OPTIONS", "SEEDANCE_REFERENCE_AUDIO_MAX_BYTES", "SEEDANCE_REFERENCE_IMAGE_MAX_BYTES", "SEEDANCE_REFERENCE_MAX_ASPECT_RATIO", "SEEDANCE_REFERENCE_MAX_DIMENSION", "SEEDANCE_REFERENCE_MIN_ASPECT_RATIO", "SEEDANCE_REFERENCE_MIN_DIMENSION", "SEEDANCE_REFERENCE_VIDEO_MAX_BYTES", "SEEDANCE_REFERENCE_VIDEO_MAX_PIXELS", "SEEDANCE_REFERENCE_VIDEO_MIN_PIXELS", "SEEDANCE_ROUTE_OPTIONS", "VIDEO_ASPECT_RATIO_OPTIONS", "VIDEO_DURATION_OPTIONS", "VIDEO_GENERATION_MODE_OPTIONS", "VIDEO_PROVIDER_OPTIONS", "VIDEO_RESOLUTION_OPTIONS", "normalize_seedance_parameters", "public_video_capabilities", "resolve_seedance_model", "validate_seedance_media_counts", "validate_seedance_reference_dimensions", ]