Replace the prior release tree with the current plugin, frontend, tests, and documentation. Document retired node IDs and the public Gitea update source.
309 lines
11 KiB
Python
309 lines
11 KiB
Python
"""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",
|
||
]
|