Spaces:
Sleeping
Sleeping
| """VoiceSettings value object for TTS voice configuration with validation.""" | |
| from dataclasses import dataclass | |
| from typing import Optional | |
| import re | |
| class VoiceSettings: | |
| """Value object representing voice settings for text-to-speech synthesis.""" | |
| voice_id: str | |
| speed: float | |
| language: str | |
| pitch: Optional[float] = None | |
| volume: Optional[float] = None | |
| audio_prompt_path: Optional[str] = None # For voice cloning (e.g., Chatterbox) | |
| def __post_init__(self): | |
| """Validate voice settings after initialization.""" | |
| self._validate() | |
| def _validate(self): | |
| """Validate voice settings properties.""" | |
| if not isinstance(self.voice_id, str): | |
| raise TypeError("Voice ID must be a string") | |
| if not self.voice_id.strip(): | |
| raise ValueError("Voice ID cannot be empty") | |
| # Voice ID should be alphanumeric with possible underscores/hyphens | |
| if not re.match(r'^[a-zA-Z0-9_-]+$', self.voice_id): | |
| raise ValueError(f"Invalid voice ID format: {self.voice_id}. Must contain only letters, numbers, underscores, and hyphens") | |
| if not isinstance(self.speed, (int, float)): | |
| raise TypeError("Speed must be a number") | |
| if not 0.1 <= self.speed <= 3.0: | |
| raise ValueError(f"Speed must be between 0.1 and 3.0, got {self.speed}") | |
| if not isinstance(self.language, str): | |
| raise TypeError("Language must be a string") | |
| if not self.language.strip(): | |
| raise ValueError("Language cannot be empty") | |
| # Validate language code format (ISO 639-1 or ISO 639-3) | |
| if not re.match(r'^[a-z]{2,3}(-[A-Z]{2})?$', self.language): | |
| raise ValueError(f"Invalid language code format: {self.language}. Expected format: 'en', 'en-US', etc.") | |
| if self.pitch is not None: | |
| if not isinstance(self.pitch, (int, float)): | |
| raise TypeError("Pitch must be a number") | |
| if not -2.0 <= self.pitch <= 2.0: | |
| raise ValueError(f"Pitch must be between -2.0 and 2.0, got {self.pitch}") | |
| if self.volume is not None: | |
| if not isinstance(self.volume, (int, float)): | |
| raise TypeError("Volume must be a number") | |
| if not 0.0 <= self.volume <= 2.0: | |
| raise ValueError(f"Volume must be between 0.0 and 2.0, got {self.volume}") | |
| if self.audio_prompt_path is not None: | |
| if not isinstance(self.audio_prompt_path, str): | |
| raise TypeError("Audio prompt path must be a string") | |
| if not self.audio_prompt_path.strip(): | |
| raise ValueError("Audio prompt path cannot be empty") | |
| def is_default_speed(self) -> bool: | |
| """Check if speed is at default value (1.0).""" | |
| return abs(self.speed - 1.0) < 0.01 | |
| def is_default_pitch(self) -> bool: | |
| """Check if pitch is at default value (0.0 or None).""" | |
| return self.pitch is None or abs(self.pitch) < 0.01 | |
| def is_default_volume(self) -> bool: | |
| """Check if volume is at default value (1.0 or None).""" | |
| return self.volume is None or abs(self.volume - 1.0) < 0.01 | |
| def with_speed(self, speed: float) -> 'VoiceSettings': | |
| """Create a new VoiceSettings with different speed.""" | |
| return VoiceSettings( | |
| voice_id=self.voice_id, | |
| speed=speed, | |
| language=self.language, | |
| pitch=self.pitch, | |
| volume=self.volume, | |
| audio_prompt_path=self.audio_prompt_path | |
| ) | |
| def with_pitch(self, pitch: Optional[float]) -> 'VoiceSettings': | |
| """Create a new VoiceSettings with different pitch.""" | |
| return VoiceSettings( | |
| voice_id=self.voice_id, | |
| speed=self.speed, | |
| language=self.language, | |
| pitch=pitch, | |
| volume=self.volume, | |
| audio_prompt_path=self.audio_prompt_path | |
| ) | |
| def with_audio_prompt(self, audio_prompt_path: Optional[str]) -> 'VoiceSettings': | |
| """Create a new VoiceSettings with different audio prompt path.""" | |
| return VoiceSettings( | |
| voice_id=self.voice_id, | |
| speed=self.speed, | |
| language=self.language, | |
| pitch=self.pitch, | |
| volume=self.volume, | |
| audio_prompt_path=audio_prompt_path | |
| ) |