Spaces:
Build error
Build error
"""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 | |
) |