Files
comfyui_o1key/utils/o1key_video_catalog.py
T
Jony ba920f2b66 Publish current ComfyUI O1Key code baseline
Replace the prior release tree with the current plugin, frontend, tests, and documentation. Document retired node IDs and the public Gitea update source.
2026-09-24 19:56:48 +08:00

309 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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",
]