"""批量变体配音严格守卫与解析(#1749 问题 A)。 事故:批量独立配音视频3 错绑视频1 配音——根因是 voice_library_ids 缺值/长度不符时静默 fallback 到 voice_library_id(单值),导致多个变体 共用同一条配音。 定稿规则: - 独立配音(voice_library_ids 非空):长度必须 == count,逐项非空, 否则 400;**禁止静默 fallback** 到单值 voice_library_id; - 统一配音(voice_library_id 非空、voice_library_ids 空):所有变体共用; - 两者都空:返回 [""] * count(无配音,渲染走原 BGM/原声路径)。 纯函数,不碰 DB;调用方(路由)负责把 VariantVoiceError 转 400。 """ from __future__ import annotations from typing import Optional class VariantVoiceError(ValueError): """配音参数错误(路由层捕获后返回 400)。""" def resolve_variant_voice_ids( *, count: int, voice_library_id: Optional[str] = None, voice_library_ids: Optional[list[str]] = None, ) -> list[str]: """解析每个变体使用的配音 voice_library_id。 Args: count: 变体数量(必须 >= 1)。 voice_library_id: 统一配音 ID(所有变体共用)。 voice_library_ids: 独立配音 ID 列表,长度必须 == count。 Returns: 长度 == count 的列表;元素为 "" 表示该变体无配音。 Raises: VariantVoiceError: count 非法 / 独立配音长度不符 / 存在缺值。 """ if not count or count < 1: raise VariantVoiceError("变体数量 count 必须 >= 1") single = (voice_library_id or "").strip() # 归一化:None / 空列表 / 全空白 → 未提供独立配音(走统一配音或无配音) raw_multi = [str(v or "").strip() for v in (voice_library_ids or [])] multi = [v for v in raw_multi if v] if multi: # 显式传了独立配音列表: # - 恰好 1 个非空且 count > 1:视为统一配音(与 cover_urls/titles 长度1=共用一致); # - 否则按独立配音严格校验:原始长度必须 == count 且逐项非空,禁止静默 fallback; # - 其余长度/缺值 → 400。 if len(raw_multi) == 1 and count > 1: return [multi[0]] * count if len(raw_multi) != count: raise VariantVoiceError( f"独立配音数量({len(raw_multi)})与视频数量({count})不一致;" f"批量独立配音必须为每个视频分别指定配音,或改用统一配音 voice_library_id" ) missing = [i for i, v in enumerate(raw_multi) if not v] if missing: raise VariantVoiceError( f"第 {[i + 1 for i in missing]} 个视频缺少独立配音 voice_library_id;" f"独立配音不允许缺值,也不会回退为统一配音" ) return list(raw_multi) if single: return [single] * count # 无配音 return [""] * count