"""TTS 配音配置模型.""" from __future__ import annotations from dataclasses import dataclass from typing import Any, Optional @dataclass(slots=True) class TtsConfig: """TTS 配音配置. Attributes: enabled: 是否启用配音 voice_id: 音色 ID speed: 语速 (0.5 ~ 2.0) pitch: 语调 (-12 ~ 12 半音) volume: 音量 (0.0 ~ 1.0) text: 配音文本(整段配音时使用) align_mode: 对齐模式 - "subtitle"=按字幕对齐 / "full"=整段配音 overlap_mode: 与原音的叠加模式 - "replace"=替换 / "mix"=混音 """ enabled: bool = False voice_id: str = "" speed: float = 1.0 pitch: float = 0.0 volume: float = 0.8 text: str = "" align_mode: str = "full" # subtitle / full overlap_mode: str = "replace" # replace / mix @classmethod def parse(cls, data: Optional[dict[str, Any]]) -> "TtsConfig": """从 dict 解析配置,无效值回退到默认.""" if not data or not isinstance(data, dict): return cls() enabled = data.get("enabled", False) if not isinstance(enabled, bool): enabled = False if not enabled: return cls(enabled=False) voice_id = data.get("voice_id", "") if not isinstance(voice_id, str): voice_id = "" speed = data.get("speed", 1.0) if not isinstance(speed, (int, float)): speed = 1.0 pitch = data.get("pitch", 0.0) if not isinstance(pitch, (int, float)): pitch = 0.0 volume = data.get("volume", 0.8) if not isinstance(volume, (int, float)): volume = 0.8 text = data.get("text", "") if not isinstance(text, str): text = "" align_mode = data.get("align_mode", "full") if align_mode not in ("subtitle", "full"): align_mode = "full" overlap_mode = data.get("overlap_mode", "replace") if overlap_mode not in ("replace", "mix"): overlap_mode = "replace" config = cls( enabled=enabled, voice_id=voice_id, speed=float(speed), pitch=float(pitch), volume=float(volume), text=text, align_mode=align_mode, overlap_mode=overlap_mode, ) config._clamp() return config def _clamp(self) -> None: """边界钳制.""" if self.speed < 0.5: self.speed = 0.5 elif self.speed > 2.0: self.speed = 2.0 if self.pitch < -12: self.pitch = -12 elif self.pitch > 12: self.pitch = 12 if self.volume < 0.0: self.volume = 0.0 elif self.volume > 1.0: self.volume = 1.0