wavhost 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
wavhost/__init__.py ADDED
@@ -0,0 +1,31 @@
1
+ """Wavhost - Local-first TTS runtime."""
2
+
3
+ from wavhost.backends import TTSBackend, ChatterboxBackend, create_backend
4
+ from wavhost.config import VERSION
5
+ from wavhost.exceptions import (
6
+ WavhostError,
7
+ ModelNotFoundError,
8
+ ModelNotInstalledError,
9
+ BackendError,
10
+ StorageError,
11
+ ValidationError,
12
+ )
13
+ from wavhost.registry import ModelRegistry, ModelInfo
14
+ from wavhost.storage import WavhostStorage
15
+
16
+ __version__ = VERSION
17
+ __all__ = [
18
+ "__version__",
19
+ "TTSBackend",
20
+ "ChatterboxBackend",
21
+ "create_backend",
22
+ "ModelRegistry",
23
+ "ModelInfo",
24
+ "WavhostStorage",
25
+ "WavhostError",
26
+ "ModelNotFoundError",
27
+ "ModelNotInstalledError",
28
+ "BackendError",
29
+ "StorageError",
30
+ "ValidationError",
31
+ ]
wavhost/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Main entry point for python -m wavhost."""
2
+
3
+ from wavhost.cli import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
wavhost/backends.py ADDED
@@ -0,0 +1,274 @@
1
+ """TTS backend protocol and implementations."""
2
+
3
+ from abc import abstractmethod
4
+ from pathlib import Path
5
+ from typing import Optional, Protocol, Union
6
+
7
+ import torch
8
+
9
+ from wavhost import dependencies
10
+ from wavhost.config import CPU_DEVICE, DEFAULT_DEVICE, DEFAULT_SAMPLE_RATE
11
+ from wavhost.exceptions import BackendError
12
+ from wavhost.logging_config import get_logger
13
+ from wavhost.registry import ModelInfo
14
+
15
+ logger = get_logger(__name__)
16
+
17
+ BACKEND_NAME = "chatterbox"
18
+
19
+
20
+ class TTSBackend(Protocol):
21
+ """Protocol for TTS backend implementations.
22
+
23
+ All TTS backends must implement this interface for consistency.
24
+ """
25
+
26
+ @abstractmethod
27
+ def generate(
28
+ self,
29
+ text: str,
30
+ voice: Optional[str] = None,
31
+ **kwargs
32
+ ) -> tuple[torch.Tensor, int]:
33
+ """Generate speech from text.
34
+
35
+ Args:
36
+ text: Text to synthesize
37
+ voice: Optional voice identifier or reference audio path
38
+ **kwargs: Backend-specific parameters
39
+
40
+ Returns:
41
+ Tuple of (audio_tensor, sample_rate)
42
+
43
+ Raises:
44
+ BackendError: If generation fails
45
+ """
46
+ ...
47
+
48
+ @property
49
+ @abstractmethod
50
+ def sample_rate(self) -> int:
51
+ """Get the sample rate of generated audio.
52
+
53
+ Returns:
54
+ Sample rate in Hz
55
+ """
56
+ ...
57
+
58
+
59
+ class ChatterboxBackend:
60
+ """Chatterbox TTS backend implementation.
61
+
62
+ Loads weights from a local checkpoint directory produced by `wavhost pull`
63
+ (never calls the Hugging Face Hub client at runtime).
64
+ """
65
+
66
+ def __init__(
67
+ self,
68
+ model_class: str,
69
+ checkpoint_path: Union[str, Path],
70
+ model_kwargs: Optional[dict] = None,
71
+ device: Optional[str] = None
72
+ ):
73
+ """Initialize Chatterbox backend.
74
+
75
+ Args:
76
+ model_class: Name of the model class ('ChatterboxTTS' or 'ChatterboxTurboTTS')
77
+ checkpoint_path: Local directory with model files (from pull)
78
+ model_kwargs: Unused; kept for API compatibility
79
+ device: Device to run on ('cuda', 'cpu', or 'mps'). Auto-detects if None.
80
+
81
+ Raises:
82
+ BackendError: If model class is invalid or checkpoint is missing
83
+ """
84
+ self._validate_model_class(model_class)
85
+
86
+ self._model_class = model_class
87
+ self._model_kwargs = model_kwargs or {}
88
+ self._checkpoint_path = Path(checkpoint_path)
89
+ self._device = device or self._detect_device()
90
+ self._model = None
91
+ self._sr = DEFAULT_SAMPLE_RATE
92
+
93
+ if not self._checkpoint_path.is_dir():
94
+ raise BackendError(
95
+ f"Checkpoint directory not found: {self._checkpoint_path}. "
96
+ f"Pull the model first with: wavhost pull <model>"
97
+ )
98
+
99
+ logger.info(
100
+ f"Initialized {model_class} backend on {self._device} "
101
+ f"from {self._checkpoint_path}"
102
+ )
103
+
104
+ @staticmethod
105
+ def _validate_model_class(model_class: str) -> None:
106
+ """Validate that the model class is supported.
107
+
108
+ Args:
109
+ model_class: Model class name
110
+
111
+ Raises:
112
+ BackendError: If model class is invalid
113
+ """
114
+ valid_classes = {"ChatterboxTTS", "ChatterboxTurboTTS"}
115
+ if model_class not in valid_classes:
116
+ raise BackendError(
117
+ f"Invalid model class '{model_class}'. "
118
+ f"Must be one of: {', '.join(valid_classes)}"
119
+ )
120
+
121
+ @staticmethod
122
+ def _detect_device() -> str:
123
+ """Detect the best available device.
124
+
125
+ Returns:
126
+ Device string ('cuda', 'mps', or 'cpu')
127
+ """
128
+ if torch.cuda.is_available():
129
+ return DEFAULT_DEVICE
130
+ elif hasattr(torch.backends, 'mps') and torch.backends.mps.is_available():
131
+ return "mps"
132
+ return CPU_DEVICE
133
+
134
+ def _load_model(self) -> None:
135
+ """Lazy load the model on first use from the local checkpoint.
136
+
137
+ Raises:
138
+ BackendError: If model loading fails
139
+ """
140
+ if self._model is not None:
141
+ return
142
+
143
+ if not dependencies.is_installed(BACKEND_NAME):
144
+ raise BackendError(dependencies.missing_engine_message(BACKEND_NAME))
145
+
146
+ logger.info(
147
+ f"Loading {self._model_class} from {self._checkpoint_path}..."
148
+ )
149
+
150
+ try:
151
+ if self._model_class == "ChatterboxTurboTTS":
152
+ from chatterbox.tts_turbo import ChatterboxTurboTTS
153
+ self._model = ChatterboxTurboTTS.from_local(
154
+ self._checkpoint_path,
155
+ self._device,
156
+ )
157
+ else:
158
+ from chatterbox.tts import ChatterboxTTS
159
+ self._model = ChatterboxTTS.from_local(
160
+ self._checkpoint_path,
161
+ self._device,
162
+ )
163
+
164
+ self._sr = self._model.sr
165
+ logger.info(f"Model loaded successfully (sample rate: {self._sr} Hz)")
166
+
167
+ except ImportError as e:
168
+ raise BackendError(
169
+ f"Failed to import Chatterbox. "
170
+ f"Install with: {dependencies.install_hint(BACKEND_NAME)}\n"
171
+ f"Error: {e}"
172
+ ) from e
173
+ except Exception as e:
174
+ # Keep the original type and chain the cause so the real frame
175
+ # (e.g. a broken transitive dependency) is recoverable from logs.
176
+ logger.debug("Chatterbox model load failed", exc_info=True)
177
+ raise BackendError(
178
+ f"Failed to load Chatterbox model: {type(e).__name__}: {e}"
179
+ ) from e
180
+
181
+ def generate(
182
+ self,
183
+ text: str,
184
+ voice: Optional[str] = None,
185
+ **kwargs
186
+ ) -> tuple[torch.Tensor, int]:
187
+ """Generate speech from text using Chatterbox.
188
+
189
+ Args:
190
+ text: Text to synthesize
191
+ voice: Optional path to reference voice audio (for voice cloning)
192
+ **kwargs: Additional parameters passed to model.generate()
193
+
194
+ Returns:
195
+ Tuple of (audio_tensor, sample_rate)
196
+
197
+ Raises:
198
+ BackendError: If generation fails
199
+ """
200
+ self._load_model()
201
+
202
+ generate_kwargs = kwargs.copy()
203
+
204
+ if voice:
205
+ voice_path = Path(voice)
206
+ if not voice_path.exists():
207
+ raise BackendError(f"Reference voice audio not found: {voice}")
208
+
209
+ logger.debug(f"Cloning voice from {voice_path}")
210
+ generate_kwargs['audio_prompt_path'] = str(voice_path)
211
+
212
+ try:
213
+ logger.debug(f"Generating speech for text (length: {len(text)})")
214
+ wav = self._model.generate(text, **generate_kwargs)
215
+ return wav, self._sr
216
+
217
+ except Exception as e:
218
+ raise BackendError(f"Speech generation failed: {e}")
219
+
220
+ @property
221
+ def sample_rate(self) -> int:
222
+ """Get the sample rate of generated audio.
223
+
224
+ Returns:
225
+ Sample rate in Hz
226
+ """
227
+ return self._sr
228
+
229
+
230
+ def create_backend(
231
+ model_info: ModelInfo,
232
+ device: Optional[str] = None,
233
+ checkpoint_path: Optional[Union[str, Path]] = None,
234
+ ) -> TTSBackend:
235
+ """Factory function to create a TTS backend from model info.
236
+
237
+ Args:
238
+ model_info: Model information from registry
239
+ device: Optional device override ('cuda', 'cpu', or 'mps')
240
+ checkpoint_path: Local checkpoint directory from `wavhost pull`
241
+
242
+ Returns:
243
+ Initialized backend instance
244
+
245
+ Raises:
246
+ BackendError: If backend type is not supported or checkpoint is missing
247
+ """
248
+ backend_type = model_info.backend
249
+
250
+ if backend_type == BACKEND_NAME:
251
+ if checkpoint_path is None:
252
+ raise BackendError(
253
+ "checkpoint_path is required. "
254
+ "Pull the model first with: wavhost pull <model>"
255
+ )
256
+
257
+ actual_device = device or model_info.recommended_device
258
+
259
+ if actual_device == DEFAULT_DEVICE and not torch.cuda.is_available():
260
+ message = "GPU not available, falling back to CPU"
261
+ gpu_hint = dependencies.gpu_build_warning()
262
+ if gpu_hint:
263
+ message = f"{message}\n{gpu_hint}"
264
+ logger.warning(message)
265
+ actual_device = CPU_DEVICE
266
+
267
+ return ChatterboxBackend(
268
+ model_class=model_info.model_class,
269
+ checkpoint_path=checkpoint_path,
270
+ model_kwargs=model_info.model_kwargs,
271
+ device=actual_device
272
+ )
273
+
274
+ raise BackendError(f"Unsupported backend type: {backend_type}")