teachingAssistant / src /domain /models /voice_settings.py
Michael Hu
refactor based on DDD
5009cb8
raw
history blame
3.64 kB
"""VoiceSettings value object for TTS voice configuration with validation."""
from dataclasses import dataclass
from typing import Optional
import re
@dataclass(frozen=True)
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
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}")
@property
def is_default_speed(self) -> bool:
"""Check if speed is at default value (1.0)."""
return abs(self.speed - 1.0) < 0.01
@property
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
@property
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
)
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
)