File size: 4,415 Bytes
5009cb8
 
 
 
 
 
 
 
 
 
0f99c8d
5009cb8
 
 
 
 
0f99c8d
 
5009cb8
 
 
0f99c8d
5009cb8
 
 
 
0f99c8d
5009cb8
 
0f99c8d
5009cb8
 
 
0f99c8d
5009cb8
 
0f99c8d
5009cb8
 
0f99c8d
5009cb8
 
0f99c8d
5009cb8
 
0f99c8d
5009cb8
 
 
0f99c8d
5009cb8
 
 
0f99c8d
5009cb8
 
0f99c8d
5009cb8
 
 
0f99c8d
5009cb8
 
0f99c8d
 
 
 
 
 
 
 
5009cb8
 
 
 
0f99c8d
5009cb8
 
 
 
0f99c8d
5009cb8
 
 
 
0f99c8d
5009cb8
 
 
 
 
 
 
0f99c8d
 
5009cb8
0f99c8d
5009cb8
 
 
 
 
 
 
0f99c8d
 
 
 
 
 
 
 
 
 
 
 
 
5009cb8
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
"""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
    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")

    @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,
            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
        )