"""各 Seedance 模型的参数限制。 **为什么需要它**:已接入的两个模型限制不同——2.5 时长上限 30 秒但没有 4k, 2.0 有 4k 但时长上限 15 秒;2.5 在图生视频时 `ratio` 只能是 `adaptive`。超范围的 请求会被厂商以 400 拒绝,那要花一次往返、计入 RPM,并表现为一个失败的任务。 在本地先拦下来,错误信息还能具体指出是哪个模型的哪条限制。 **未知模型一律放行**(只记一条日志):账号新开通一个模型时,不应该因为这张表 没更新就用不了——那会让「开通即可用」变成「开通还得改代码发版」。代价只是失去 本地预校验,厂商仍会兜底。 限制取自官方文档,取证见 patbond-doc `docs/development/providers/volcengine-seedance.md` §5。 """ from __future__ import annotations import logging from dataclasses import dataclass log = logging.getLogger(__name__) @dataclass(frozen=True) class ModelLimits: """一个模型族的参数限制。 :param ratio_must_be_adaptive_with_first_frame: 2.5 在图生视频(首帧/首尾帧) 与视频编辑/延长任务中**只支持 `adaptive`**——模型自动保持输出宽高比与 首帧图片一致,不接受指定值。2.0 系列则可指定。 :param supports_output_format: `output_format`(mp4/mov)仅 2.5 支持。 """ resolutions: frozenset[str] min_duration_sec: int max_duration_sec: int ratio_must_be_adaptive_with_first_frame: bool supports_output_format: bool #: Seedance 2.5:时长可到 30 秒,但没有 4k;图生视频时宽高比锁 adaptive。 #: 1080p 输出为 10bit + H.265/HEVC,少数播放环境可能不兼容。 SEEDANCE_2_5 = ModelLimits( resolutions=frozenset({"480p", "720p", "1080p"}), min_duration_sec=4, max_duration_sec=30, ratio_must_be_adaptive_with_first_frame=True, supports_output_format=True, ) #: Seedance 2.0 系列:有 4k,但时长上限 15 秒。4k 输出为 10bit + H.265/HEVC。 SEEDANCE_2_0 = ModelLimits( resolutions=frozenset({"480p", "720p", "1080p", "4k"}), min_duration_sec=4, max_duration_sec=15, ratio_must_be_adaptive_with_first_frame=False, supports_output_format=False, ) #: 按模型族前缀匹配,而非精确匹配完整 model ID。 #: 完整 ID 形如 `doubao-seedance-2-5-260628`,尾部的日期版本会随厂商更新而变; #: 按前缀匹配使一次模型小版本升级不需要改这张表。 _BY_PREFIX: tuple[tuple[str, ModelLimits], ...] = ( ("doubao-seedance-2-5", SEEDANCE_2_5), ("doubao-seedance-2-0", SEEDANCE_2_0), ) #: `duration` 的特殊值:交给模型在有效范围内自选。不参与范围校验。 AUTO_DURATION = -1 def limits_for(model: str) -> ModelLimits | None: """查模型限制;未知模型返回 None(表示不做本地预校验)。""" for prefix, limits in _BY_PREFIX: if model.startswith(prefix): return limits return None def validate( model: str, *, resolution: str | None = None, duration_sec: int | None = None, ratio: str | None = None, has_first_frame: bool = False, ) -> None: """按模型限制校验参数,不合法则抛 `ValueError`。 未知模型直接放行——见模块 docstring。 :raises ValueError: 参数超出该模型的限制,消息里指明模型与具体限制 """ limits = limits_for(model) if limits is None: log.info("模型 %s 无本地限制表,跳过预校验,由厂商兜底", model) return if resolution and resolution not in limits.resolutions: raise ValueError( f"模型 {model} 不支持分辨率 {resolution}," f"可选:{sorted(limits.resolutions)}" ) if duration_sec is not None and duration_sec != AUTO_DURATION: if not limits.min_duration_sec <= duration_sec <= limits.max_duration_sec: raise ValueError( f"模型 {model} 的时长需在 " f"[{limits.min_duration_sec}, {limits.max_duration_sec}] 秒内" f"(或 {AUTO_DURATION} 表示由模型自选),当前 {duration_sec}" ) if ( has_first_frame and limits.ratio_must_be_adaptive_with_first_frame and ratio and ratio != "adaptive" ): raise ValueError( f"模型 {model} 在图生视频时只支持 ratio=adaptive" f"(输出宽高比自动与首帧图片一致),当前 {ratio}" )