abstractvoice 0.3.0__py3-none-any.whl → 0.4.1__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.
- abstractvoice/__init__.py +5 -2
- abstractvoice/examples/cli_repl.py +81 -44
- abstractvoice/examples/voice_cli.py +56 -20
- abstractvoice/model_manager.py +384 -0
- abstractvoice/simple_model_manager.py +398 -0
- abstractvoice/tts/tts_engine.py +139 -22
- abstractvoice/voice_manager.py +83 -2
- {abstractvoice-0.3.0.dist-info → abstractvoice-0.4.1.dist-info}/METADATA +121 -23
- abstractvoice-0.4.1.dist-info/RECORD +23 -0
- abstractvoice-0.3.0.dist-info/RECORD +0 -21
- {abstractvoice-0.3.0.dist-info → abstractvoice-0.4.1.dist-info}/WHEEL +0 -0
- {abstractvoice-0.3.0.dist-info → abstractvoice-0.4.1.dist-info}/entry_points.txt +0 -0
- {abstractvoice-0.3.0.dist-info → abstractvoice-0.4.1.dist-info}/licenses/LICENSE +0 -0
- {abstractvoice-0.3.0.dist-info → abstractvoice-0.4.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Simple Model Manager for AbstractVoice
|
|
3
|
+
|
|
4
|
+
Provides clean, simple APIs for model management that can be used by both
|
|
5
|
+
CLI commands and third-party applications.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import json
|
|
10
|
+
import time
|
|
11
|
+
import threading
|
|
12
|
+
from typing import Dict, List, Optional, Callable, Any
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _import_tts():
|
|
17
|
+
"""Import TTS with helpful error message if dependencies missing."""
|
|
18
|
+
try:
|
|
19
|
+
from TTS.api import TTS
|
|
20
|
+
from TTS.utils.manage import ModelManager
|
|
21
|
+
return TTS, ModelManager
|
|
22
|
+
except ImportError as e:
|
|
23
|
+
raise ImportError(
|
|
24
|
+
"TTS functionality requires coqui-tts. Install with:\n"
|
|
25
|
+
" pip install abstractvoice[tts]\n"
|
|
26
|
+
f"Original error: {e}"
|
|
27
|
+
) from e
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SimpleModelManager:
|
|
31
|
+
"""Simple, clean model manager for AbstractVoice."""
|
|
32
|
+
|
|
33
|
+
# Essential model - guaranteed to work everywhere, reasonable size
|
|
34
|
+
ESSENTIAL_MODEL = "tts_models/en/ljspeech/fast_pitch"
|
|
35
|
+
|
|
36
|
+
# Available models organized by language with metadata
|
|
37
|
+
AVAILABLE_MODELS = {
|
|
38
|
+
"en": {
|
|
39
|
+
"fast_pitch": {
|
|
40
|
+
"model": "tts_models/en/ljspeech/fast_pitch",
|
|
41
|
+
"name": "Fast Pitch (English)",
|
|
42
|
+
"quality": "good",
|
|
43
|
+
"size_mb": 107,
|
|
44
|
+
"description": "Lightweight, reliable English voice",
|
|
45
|
+
"requires_espeak": False,
|
|
46
|
+
"default": True
|
|
47
|
+
},
|
|
48
|
+
"vits": {
|
|
49
|
+
"model": "tts_models/en/ljspeech/vits",
|
|
50
|
+
"name": "VITS (English)",
|
|
51
|
+
"quality": "excellent",
|
|
52
|
+
"size_mb": 328,
|
|
53
|
+
"description": "High-quality English voice with natural prosody",
|
|
54
|
+
"requires_espeak": True,
|
|
55
|
+
"default": False
|
|
56
|
+
},
|
|
57
|
+
"tacotron2": {
|
|
58
|
+
"model": "tts_models/en/ljspeech/tacotron2-DDC",
|
|
59
|
+
"name": "Tacotron2 (English)",
|
|
60
|
+
"quality": "good",
|
|
61
|
+
"size_mb": 362,
|
|
62
|
+
"description": "Classic English voice, reliable",
|
|
63
|
+
"requires_espeak": False,
|
|
64
|
+
"default": False
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
"fr": {
|
|
68
|
+
"css10_vits": {
|
|
69
|
+
"model": "tts_models/fr/css10/vits",
|
|
70
|
+
"name": "CSS10 VITS (French)",
|
|
71
|
+
"quality": "excellent",
|
|
72
|
+
"size_mb": 548,
|
|
73
|
+
"description": "High-quality French voice",
|
|
74
|
+
"requires_espeak": True,
|
|
75
|
+
"default": True
|
|
76
|
+
},
|
|
77
|
+
"mai_tacotron2": {
|
|
78
|
+
"model": "tts_models/fr/mai/tacotron2-DDC",
|
|
79
|
+
"name": "MAI Tacotron2 (French)",
|
|
80
|
+
"quality": "good",
|
|
81
|
+
"size_mb": 362,
|
|
82
|
+
"description": "Reliable French voice",
|
|
83
|
+
"requires_espeak": False,
|
|
84
|
+
"default": False
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
"es": {
|
|
88
|
+
"mai_tacotron2": {
|
|
89
|
+
"model": "tts_models/es/mai/tacotron2-DDC",
|
|
90
|
+
"name": "MAI Tacotron2 (Spanish)",
|
|
91
|
+
"quality": "good",
|
|
92
|
+
"size_mb": 362,
|
|
93
|
+
"description": "Reliable Spanish voice",
|
|
94
|
+
"requires_espeak": False,
|
|
95
|
+
"default": True
|
|
96
|
+
},
|
|
97
|
+
"css10_vits": {
|
|
98
|
+
"model": "tts_models/es/css10/vits",
|
|
99
|
+
"name": "CSS10 VITS (Spanish)",
|
|
100
|
+
"quality": "excellent",
|
|
101
|
+
"size_mb": 548,
|
|
102
|
+
"description": "High-quality Spanish voice",
|
|
103
|
+
"requires_espeak": True,
|
|
104
|
+
"default": False
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
"de": {
|
|
108
|
+
"thorsten_vits": {
|
|
109
|
+
"model": "tts_models/de/thorsten/vits",
|
|
110
|
+
"name": "Thorsten VITS (German)",
|
|
111
|
+
"quality": "excellent",
|
|
112
|
+
"size_mb": 548,
|
|
113
|
+
"description": "High-quality German voice",
|
|
114
|
+
"requires_espeak": True,
|
|
115
|
+
"default": True
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
"it": {
|
|
119
|
+
"mai_male_vits": {
|
|
120
|
+
"model": "tts_models/it/mai_male/vits",
|
|
121
|
+
"name": "MAI Male VITS (Italian)",
|
|
122
|
+
"quality": "excellent",
|
|
123
|
+
"size_mb": 548,
|
|
124
|
+
"description": "High-quality Italian male voice",
|
|
125
|
+
"requires_espeak": True,
|
|
126
|
+
"default": True
|
|
127
|
+
},
|
|
128
|
+
"mai_female_vits": {
|
|
129
|
+
"model": "tts_models/it/mai_female/vits",
|
|
130
|
+
"name": "MAI Female VITS (Italian)",
|
|
131
|
+
"quality": "excellent",
|
|
132
|
+
"size_mb": 548,
|
|
133
|
+
"description": "High-quality Italian female voice",
|
|
134
|
+
"requires_espeak": True,
|
|
135
|
+
"default": False
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
def __init__(self, debug_mode: bool = False):
|
|
141
|
+
self.debug_mode = debug_mode
|
|
142
|
+
self._cache_dir = None
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def cache_dir(self) -> str:
|
|
146
|
+
"""Get the TTS model cache directory."""
|
|
147
|
+
if self._cache_dir is None:
|
|
148
|
+
# Check common cache locations
|
|
149
|
+
import appdirs
|
|
150
|
+
potential_dirs = [
|
|
151
|
+
os.path.expanduser("~/.cache/tts"),
|
|
152
|
+
appdirs.user_data_dir("tts"),
|
|
153
|
+
os.path.expanduser("~/.local/share/tts"),
|
|
154
|
+
os.path.expanduser("~/Library/Application Support/tts"), # macOS
|
|
155
|
+
]
|
|
156
|
+
|
|
157
|
+
# Find existing cache or use default
|
|
158
|
+
for cache_dir in potential_dirs:
|
|
159
|
+
if os.path.exists(cache_dir):
|
|
160
|
+
self._cache_dir = cache_dir
|
|
161
|
+
break
|
|
162
|
+
else:
|
|
163
|
+
# Use appdirs default
|
|
164
|
+
self._cache_dir = appdirs.user_data_dir("tts")
|
|
165
|
+
|
|
166
|
+
return self._cache_dir
|
|
167
|
+
|
|
168
|
+
def is_model_cached(self, model_name: str) -> bool:
|
|
169
|
+
"""Check if a specific model is cached locally."""
|
|
170
|
+
try:
|
|
171
|
+
# Convert model name to cache directory structure
|
|
172
|
+
cache_name = model_name.replace("/", "--")
|
|
173
|
+
model_path = os.path.join(self.cache_dir, cache_name)
|
|
174
|
+
|
|
175
|
+
if not os.path.exists(model_path):
|
|
176
|
+
return False
|
|
177
|
+
|
|
178
|
+
# Check for essential model files
|
|
179
|
+
essential_files = ["model.pth", "config.json"]
|
|
180
|
+
return any(os.path.exists(os.path.join(model_path, f)) for f in essential_files)
|
|
181
|
+
except Exception as e:
|
|
182
|
+
if self.debug_mode:
|
|
183
|
+
print(f"Error checking cache for {model_name}: {e}")
|
|
184
|
+
return False
|
|
185
|
+
|
|
186
|
+
def download_model(self, model_name: str, progress_callback: Optional[Callable[[str, bool], None]] = None) -> bool:
|
|
187
|
+
"""Download a specific model.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
model_name: TTS model name (e.g., 'tts_models/en/ljspeech/fast_pitch')
|
|
191
|
+
progress_callback: Optional callback function(model_name, success)
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
bool: True if successful
|
|
195
|
+
"""
|
|
196
|
+
if self.is_model_cached(model_name):
|
|
197
|
+
if self.debug_mode:
|
|
198
|
+
print(f"✅ {model_name} already cached")
|
|
199
|
+
if progress_callback:
|
|
200
|
+
progress_callback(model_name, True)
|
|
201
|
+
return True
|
|
202
|
+
|
|
203
|
+
try:
|
|
204
|
+
TTS, _ = _import_tts()
|
|
205
|
+
|
|
206
|
+
if self.debug_mode:
|
|
207
|
+
print(f"📥 Downloading {model_name}...")
|
|
208
|
+
|
|
209
|
+
start_time = time.time()
|
|
210
|
+
|
|
211
|
+
# Initialize TTS to trigger download
|
|
212
|
+
tts = TTS(model_name=model_name, progress_bar=True)
|
|
213
|
+
|
|
214
|
+
download_time = time.time() - start_time
|
|
215
|
+
if self.debug_mode:
|
|
216
|
+
print(f"✅ Downloaded {model_name} in {download_time:.1f}s")
|
|
217
|
+
|
|
218
|
+
if progress_callback:
|
|
219
|
+
progress_callback(model_name, True)
|
|
220
|
+
return True
|
|
221
|
+
|
|
222
|
+
except Exception as e:
|
|
223
|
+
if self.debug_mode:
|
|
224
|
+
print(f"❌ Failed to download {model_name}: {e}")
|
|
225
|
+
if progress_callback:
|
|
226
|
+
progress_callback(model_name, False)
|
|
227
|
+
return False
|
|
228
|
+
|
|
229
|
+
def download_essential_model(self, progress_callback: Optional[Callable[[str, bool], None]] = None) -> bool:
|
|
230
|
+
"""Download the essential English model for immediate functionality."""
|
|
231
|
+
return self.download_model(self.ESSENTIAL_MODEL, progress_callback)
|
|
232
|
+
|
|
233
|
+
def list_available_models(self, language: Optional[str] = None) -> Dict[str, Any]:
|
|
234
|
+
"""Get list of available models with metadata.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
language: Optional language filter
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
dict: Model information in JSON-serializable format
|
|
241
|
+
"""
|
|
242
|
+
if language:
|
|
243
|
+
if language in self.AVAILABLE_MODELS:
|
|
244
|
+
return {language: self.AVAILABLE_MODELS[language]}
|
|
245
|
+
else:
|
|
246
|
+
return {}
|
|
247
|
+
|
|
248
|
+
# Return all models with cache status
|
|
249
|
+
result = {}
|
|
250
|
+
for lang, models in self.AVAILABLE_MODELS.items():
|
|
251
|
+
result[lang] = {}
|
|
252
|
+
for model_id, model_info in models.items():
|
|
253
|
+
# Add cache status to each model
|
|
254
|
+
model_data = model_info.copy()
|
|
255
|
+
model_data["cached"] = self.is_model_cached(model_info["model"])
|
|
256
|
+
result[lang][model_id] = model_data
|
|
257
|
+
|
|
258
|
+
return result
|
|
259
|
+
|
|
260
|
+
def get_cached_models(self) -> List[str]:
|
|
261
|
+
"""Get list of model names that are currently cached."""
|
|
262
|
+
if not os.path.exists(self.cache_dir):
|
|
263
|
+
return []
|
|
264
|
+
|
|
265
|
+
cached = []
|
|
266
|
+
try:
|
|
267
|
+
for item in os.listdir(self.cache_dir):
|
|
268
|
+
if item.startswith("tts_models--"):
|
|
269
|
+
# Convert cache name back to model name
|
|
270
|
+
model_name = item.replace("--", "/")
|
|
271
|
+
if self.is_model_cached(model_name):
|
|
272
|
+
cached.append(model_name)
|
|
273
|
+
except Exception as e:
|
|
274
|
+
if self.debug_mode:
|
|
275
|
+
print(f"Error listing cached models: {e}")
|
|
276
|
+
|
|
277
|
+
return cached
|
|
278
|
+
|
|
279
|
+
def get_status(self) -> Dict[str, Any]:
|
|
280
|
+
"""Get comprehensive status information."""
|
|
281
|
+
cached_models = self.get_cached_models()
|
|
282
|
+
essential_cached = self.ESSENTIAL_MODEL in cached_models
|
|
283
|
+
|
|
284
|
+
# Calculate total cache size
|
|
285
|
+
total_size_mb = 0
|
|
286
|
+
if os.path.exists(self.cache_dir):
|
|
287
|
+
try:
|
|
288
|
+
for root, dirs, files in os.walk(self.cache_dir):
|
|
289
|
+
for file in files:
|
|
290
|
+
total_size_mb += os.path.getsize(os.path.join(root, file)) / (1024 * 1024)
|
|
291
|
+
except:
|
|
292
|
+
pass
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
"cache_dir": self.cache_dir,
|
|
296
|
+
"cached_models": cached_models,
|
|
297
|
+
"total_cached": len(cached_models),
|
|
298
|
+
"essential_model_cached": essential_cached,
|
|
299
|
+
"essential_model": self.ESSENTIAL_MODEL,
|
|
300
|
+
"ready_for_offline": essential_cached,
|
|
301
|
+
"total_size_mb": round(total_size_mb, 1),
|
|
302
|
+
"available_languages": list(self.AVAILABLE_MODELS.keys()),
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
def clear_cache(self, confirm: bool = False) -> bool:
|
|
306
|
+
"""Clear the model cache."""
|
|
307
|
+
if not confirm:
|
|
308
|
+
return False
|
|
309
|
+
|
|
310
|
+
try:
|
|
311
|
+
import shutil
|
|
312
|
+
if os.path.exists(self.cache_dir):
|
|
313
|
+
shutil.rmtree(self.cache_dir)
|
|
314
|
+
if self.debug_mode:
|
|
315
|
+
print(f"✅ Cleared model cache: {self.cache_dir}")
|
|
316
|
+
return True
|
|
317
|
+
return True
|
|
318
|
+
except Exception as e:
|
|
319
|
+
if self.debug_mode:
|
|
320
|
+
print(f"❌ Failed to clear cache: {e}")
|
|
321
|
+
return False
|
|
322
|
+
|
|
323
|
+
def ensure_essential_model(self, auto_download: bool = True) -> bool:
|
|
324
|
+
"""Ensure the essential model is available.
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
auto_download: Whether to download if not cached
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
bool: True if essential model is ready
|
|
331
|
+
"""
|
|
332
|
+
if self.is_model_cached(self.ESSENTIAL_MODEL):
|
|
333
|
+
return True
|
|
334
|
+
|
|
335
|
+
if not auto_download:
|
|
336
|
+
return False
|
|
337
|
+
|
|
338
|
+
return self.download_essential_model()
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
# Global instance for easy access
|
|
342
|
+
_model_manager = None
|
|
343
|
+
|
|
344
|
+
def get_model_manager(debug_mode: bool = False) -> SimpleModelManager:
|
|
345
|
+
"""Get the global model manager instance."""
|
|
346
|
+
global _model_manager
|
|
347
|
+
if _model_manager is None:
|
|
348
|
+
_model_manager = SimpleModelManager(debug_mode=debug_mode)
|
|
349
|
+
return _model_manager
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
# Simple API functions for third-party use
|
|
353
|
+
def list_models(language: Optional[str] = None) -> str:
|
|
354
|
+
"""Get available models as JSON string.
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
language: Optional language filter
|
|
358
|
+
|
|
359
|
+
Returns:
|
|
360
|
+
str: JSON string of available models
|
|
361
|
+
"""
|
|
362
|
+
manager = get_model_manager()
|
|
363
|
+
return json.dumps(manager.list_available_models(language), indent=2)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def download_model(model_name: str, progress_callback: Optional[Callable[[str, bool], None]] = None) -> bool:
|
|
367
|
+
"""Download a specific model.
|
|
368
|
+
|
|
369
|
+
Args:
|
|
370
|
+
model_name: Model name or voice ID (e.g., 'en.vits' or 'tts_models/en/ljspeech/vits')
|
|
371
|
+
progress_callback: Optional progress callback
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
bool: True if successful
|
|
375
|
+
"""
|
|
376
|
+
manager = get_model_manager()
|
|
377
|
+
|
|
378
|
+
# Handle voice ID format (e.g., 'en.vits')
|
|
379
|
+
if '.' in model_name and not model_name.startswith('tts_models'):
|
|
380
|
+
lang, voice_id = model_name.split('.', 1)
|
|
381
|
+
if lang in manager.AVAILABLE_MODELS and voice_id in manager.AVAILABLE_MODELS[lang]:
|
|
382
|
+
model_name = manager.AVAILABLE_MODELS[lang][voice_id]["model"]
|
|
383
|
+
else:
|
|
384
|
+
return False
|
|
385
|
+
|
|
386
|
+
return manager.download_model(model_name, progress_callback)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def get_status() -> str:
|
|
390
|
+
"""Get model cache status as JSON string."""
|
|
391
|
+
manager = get_model_manager()
|
|
392
|
+
return json.dumps(manager.get_status(), indent=2)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def is_ready() -> bool:
|
|
396
|
+
"""Check if essential model is ready for immediate use."""
|
|
397
|
+
manager = get_model_manager()
|
|
398
|
+
return manager.is_model_cached(manager.ESSENTIAL_MODEL)
|
abstractvoice/tts/tts_engine.py
CHANGED
|
@@ -466,34 +466,21 @@ class TTSEngine:
|
|
|
466
466
|
try:
|
|
467
467
|
if self.debug_mode:
|
|
468
468
|
print(f" > Loading TTS model: {model_name}")
|
|
469
|
-
|
|
470
|
-
# Try
|
|
469
|
+
|
|
470
|
+
# Try simple, effective initialization strategy
|
|
471
471
|
try:
|
|
472
472
|
TTS = _import_tts()
|
|
473
|
-
|
|
473
|
+
success, final_model = self._load_with_simple_fallback(TTS, model_name, debug_mode)
|
|
474
|
+
if not success:
|
|
475
|
+
# If all fails, provide actionable guidance
|
|
476
|
+
self._handle_model_load_failure(debug_mode)
|
|
477
|
+
elif self.debug_mode and final_model != model_name:
|
|
478
|
+
print(f" > Loaded fallback model: {final_model}")
|
|
474
479
|
except Exception as e:
|
|
475
480
|
error_msg = str(e).lower()
|
|
476
481
|
# Check if this is an espeak-related error
|
|
477
482
|
if ("espeak" in error_msg or "phoneme" in error_msg):
|
|
478
|
-
|
|
479
|
-
if not debug_mode:
|
|
480
|
-
sys.stdout = sys.__stdout__
|
|
481
|
-
|
|
482
|
-
print("\n" + "="*70)
|
|
483
|
-
print("⚠️ VITS Model Requires espeak-ng (Not Found)")
|
|
484
|
-
print("="*70)
|
|
485
|
-
print("\nFor BEST voice quality, install espeak-ng:")
|
|
486
|
-
print(" • macOS: brew install espeak-ng")
|
|
487
|
-
print(" • Linux: sudo apt-get install espeak-ng")
|
|
488
|
-
print(" • Windows: conda install espeak-ng (or see README)")
|
|
489
|
-
print("\nFalling back to fast_pitch (lower quality, but works)")
|
|
490
|
-
print("="*70 + "\n")
|
|
491
|
-
|
|
492
|
-
if not debug_mode:
|
|
493
|
-
sys.stdout = null_out
|
|
494
|
-
|
|
495
|
-
# Fallback to fast_pitch
|
|
496
|
-
self.tts = TTS(model_name="tts_models/en/ljspeech/fast_pitch", progress_bar=self.debug_mode)
|
|
483
|
+
self._handle_espeak_fallback(debug_mode)
|
|
497
484
|
else:
|
|
498
485
|
# Different error, re-raise
|
|
499
486
|
raise
|
|
@@ -520,6 +507,136 @@ class TTSEngine:
|
|
|
520
507
|
# Pause/resume state
|
|
521
508
|
self.pause_lock = threading.Lock() # Thread-safe pause operations
|
|
522
509
|
self.is_paused_state = False # Explicit paused state tracking
|
|
510
|
+
|
|
511
|
+
def _load_with_simple_fallback(self, TTS, preferred_model: str, debug_mode: bool) -> tuple[bool, str]:
|
|
512
|
+
"""Load TTS model with simple, effective strategy."""
|
|
513
|
+
from ..simple_model_manager import get_model_manager
|
|
514
|
+
|
|
515
|
+
model_manager = get_model_manager(debug_mode=debug_mode)
|
|
516
|
+
|
|
517
|
+
# Strategy 1: Try preferred model if cached
|
|
518
|
+
if model_manager.is_model_cached(preferred_model):
|
|
519
|
+
try:
|
|
520
|
+
if debug_mode:
|
|
521
|
+
print(f" > Using cached model: {preferred_model}")
|
|
522
|
+
self.tts = TTS(model_name=preferred_model, progress_bar=self.debug_mode)
|
|
523
|
+
return True, preferred_model
|
|
524
|
+
except Exception as e:
|
|
525
|
+
if debug_mode:
|
|
526
|
+
print(f" > Cached model failed: {e}")
|
|
527
|
+
|
|
528
|
+
# Strategy 2: Try essential model if cached
|
|
529
|
+
essential_model = model_manager.ESSENTIAL_MODEL
|
|
530
|
+
if essential_model != preferred_model and model_manager.is_model_cached(essential_model):
|
|
531
|
+
try:
|
|
532
|
+
if debug_mode:
|
|
533
|
+
print(f" > Using cached essential model: {essential_model}")
|
|
534
|
+
self.tts = TTS(model_name=essential_model, progress_bar=self.debug_mode)
|
|
535
|
+
return True, essential_model
|
|
536
|
+
except Exception as e:
|
|
537
|
+
if debug_mode:
|
|
538
|
+
print(f" > Essential model failed: {e}")
|
|
539
|
+
|
|
540
|
+
# Strategy 3: Download essential model (guaranteed to work)
|
|
541
|
+
try:
|
|
542
|
+
if debug_mode:
|
|
543
|
+
print(f" > Downloading essential model: {essential_model}")
|
|
544
|
+
success = model_manager.download_model(essential_model)
|
|
545
|
+
if success:
|
|
546
|
+
self.tts = TTS(model_name=essential_model, progress_bar=self.debug_mode)
|
|
547
|
+
return True, essential_model
|
|
548
|
+
except Exception as e:
|
|
549
|
+
if debug_mode:
|
|
550
|
+
print(f" > Essential model download failed: {e}")
|
|
551
|
+
|
|
552
|
+
# Strategy 4: Try downloading preferred model
|
|
553
|
+
try:
|
|
554
|
+
if debug_mode:
|
|
555
|
+
print(f" > Attempting preferred model download: {preferred_model}")
|
|
556
|
+
self.tts = TTS(model_name=preferred_model, progress_bar=self.debug_mode)
|
|
557
|
+
return True, preferred_model
|
|
558
|
+
except Exception as e:
|
|
559
|
+
if debug_mode:
|
|
560
|
+
print(f" > Preferred model download failed: {e}")
|
|
561
|
+
|
|
562
|
+
return False, None
|
|
563
|
+
|
|
564
|
+
def _handle_espeak_fallback(self, debug_mode: bool):
|
|
565
|
+
"""Handle espeak-related errors with fallback to non-phoneme models."""
|
|
566
|
+
# Restore stdout to show user-friendly message
|
|
567
|
+
if not debug_mode:
|
|
568
|
+
sys.stdout = sys.__stdout__
|
|
569
|
+
|
|
570
|
+
print("\n" + "="*70)
|
|
571
|
+
print("⚠️ VITS Model Requires espeak-ng (Not Found)")
|
|
572
|
+
print("="*70)
|
|
573
|
+
print("\nFor BEST voice quality, install espeak-ng:")
|
|
574
|
+
print(" • macOS: brew install espeak-ng")
|
|
575
|
+
print(" • Linux: sudo apt-get install espeak-ng")
|
|
576
|
+
print(" • Windows: conda install espeak-ng (or see README)")
|
|
577
|
+
print("\nFalling back to fast_pitch (no espeak dependency)")
|
|
578
|
+
print("="*70 + "\n")
|
|
579
|
+
|
|
580
|
+
if not debug_mode:
|
|
581
|
+
import os
|
|
582
|
+
null_out = open(os.devnull, 'w')
|
|
583
|
+
sys.stdout = null_out
|
|
584
|
+
|
|
585
|
+
# Try non-phoneme models that don't require espeak
|
|
586
|
+
from TTS.api import TTS
|
|
587
|
+
fallback_models = [
|
|
588
|
+
"tts_models/en/ljspeech/fast_pitch",
|
|
589
|
+
"tts_models/en/ljspeech/tacotron2-DDC",
|
|
590
|
+
"tts_models/en/ljspeech/glow-tts"
|
|
591
|
+
]
|
|
592
|
+
|
|
593
|
+
tts_loaded = False
|
|
594
|
+
for fallback_model in fallback_models:
|
|
595
|
+
try:
|
|
596
|
+
if debug_mode:
|
|
597
|
+
print(f"Trying fallback model: {fallback_model}")
|
|
598
|
+
self.tts = TTS(model_name=fallback_model, progress_bar=self.debug_mode)
|
|
599
|
+
tts_loaded = True
|
|
600
|
+
break
|
|
601
|
+
except Exception as fallback_error:
|
|
602
|
+
if debug_mode:
|
|
603
|
+
print(f"Fallback {fallback_model} failed: {fallback_error}")
|
|
604
|
+
continue
|
|
605
|
+
|
|
606
|
+
if not tts_loaded:
|
|
607
|
+
self._handle_model_load_failure(debug_mode)
|
|
608
|
+
|
|
609
|
+
def _handle_model_load_failure(self, debug_mode: bool):
|
|
610
|
+
"""Handle complete model loading failure with actionable guidance."""
|
|
611
|
+
# Restore stdout to show user-friendly message
|
|
612
|
+
if not debug_mode:
|
|
613
|
+
sys.stdout = sys.__stdout__
|
|
614
|
+
|
|
615
|
+
print("\n" + "="*70)
|
|
616
|
+
print("❌ TTS Model Loading Failed")
|
|
617
|
+
print("="*70)
|
|
618
|
+
print("\nNo TTS models could be loaded (offline or online).")
|
|
619
|
+
print("\nQuick fixes:")
|
|
620
|
+
print(" 1. Download essential models:")
|
|
621
|
+
print(" abstractvoice download-models")
|
|
622
|
+
print(" 2. Check internet connectivity")
|
|
623
|
+
print(" 3. Clear corrupted cache:")
|
|
624
|
+
print(" rm -rf ~/.cache/tts ~/.local/share/tts")
|
|
625
|
+
print(" 4. Reinstall TTS:")
|
|
626
|
+
print(" pip install --force-reinstall coqui-tts")
|
|
627
|
+
print(" 5. Use text-only mode:")
|
|
628
|
+
print(" abstractvoice --no-tts")
|
|
629
|
+
print("="*70)
|
|
630
|
+
|
|
631
|
+
raise RuntimeError(
|
|
632
|
+
"❌ Failed to load any TTS model.\n"
|
|
633
|
+
"This typically means:\n"
|
|
634
|
+
" • No models cached locally AND no internet connection\n"
|
|
635
|
+
" • Corrupted model cache\n"
|
|
636
|
+
" • Insufficient disk space\n"
|
|
637
|
+
" • Network firewall blocking downloads\n\n"
|
|
638
|
+
"Run 'abstractvoice download-models' when you have internet access."
|
|
639
|
+
)
|
|
523
640
|
|
|
524
641
|
def _on_playback_complete(self):
|
|
525
642
|
"""Callback when audio playback completes."""
|
abstractvoice/voice_manager.py
CHANGED
|
@@ -823,14 +823,95 @@ class VoiceManager:
|
|
|
823
823
|
return self.voice_recognizer.change_vad_aggressiveness(aggressiveness)
|
|
824
824
|
return False
|
|
825
825
|
|
|
826
|
+
# ===== SIMPLE MODEL MANAGEMENT METHODS =====
|
|
827
|
+
# Clean, simple APIs for both CLI and third-party applications
|
|
828
|
+
|
|
829
|
+
def list_available_models(self, language: str = None) -> dict:
|
|
830
|
+
"""Get available models with metadata.
|
|
831
|
+
|
|
832
|
+
Args:
|
|
833
|
+
language: Optional language filter
|
|
834
|
+
|
|
835
|
+
Returns:
|
|
836
|
+
dict: Model information with cache status
|
|
837
|
+
|
|
838
|
+
Example:
|
|
839
|
+
>>> vm = VoiceManager()
|
|
840
|
+
>>> models = vm.list_available_models('en')
|
|
841
|
+
>>> print(json.dumps(models, indent=2))
|
|
842
|
+
"""
|
|
843
|
+
from .simple_model_manager import get_model_manager
|
|
844
|
+
manager = get_model_manager(self.debug_mode)
|
|
845
|
+
return manager.list_available_models(language)
|
|
846
|
+
|
|
847
|
+
def download_model(self, model_name: str, progress_callback=None) -> bool:
|
|
848
|
+
"""Download a specific model.
|
|
849
|
+
|
|
850
|
+
Args:
|
|
851
|
+
model_name: Model name or voice ID (e.g., 'en.vits' or full model path)
|
|
852
|
+
progress_callback: Optional function(model_name, success)
|
|
853
|
+
|
|
854
|
+
Returns:
|
|
855
|
+
bool: True if successful
|
|
856
|
+
|
|
857
|
+
Example:
|
|
858
|
+
>>> vm = VoiceManager()
|
|
859
|
+
>>> vm.download_model('en.vits') # or 'tts_models/en/ljspeech/vits'
|
|
860
|
+
"""
|
|
861
|
+
from .simple_model_manager import download_model
|
|
862
|
+
return download_model(model_name, progress_callback)
|
|
863
|
+
|
|
864
|
+
def is_model_ready(self) -> bool:
|
|
865
|
+
"""Check if essential model is ready for immediate use.
|
|
866
|
+
|
|
867
|
+
Returns:
|
|
868
|
+
bool: True if can speak immediately without download
|
|
869
|
+
"""
|
|
870
|
+
from .simple_model_manager import is_ready
|
|
871
|
+
return is_ready()
|
|
872
|
+
|
|
873
|
+
def ensure_ready(self, auto_download: bool = True) -> bool:
|
|
874
|
+
"""Ensure TTS is ready for immediate use.
|
|
875
|
+
|
|
876
|
+
Args:
|
|
877
|
+
auto_download: Whether to download essential model if needed
|
|
878
|
+
|
|
879
|
+
Returns:
|
|
880
|
+
bool: True if TTS is ready
|
|
881
|
+
|
|
882
|
+
Example:
|
|
883
|
+
>>> vm = VoiceManager()
|
|
884
|
+
>>> if vm.ensure_ready():
|
|
885
|
+
... vm.speak("Ready to go!")
|
|
886
|
+
"""
|
|
887
|
+
if self.is_model_ready():
|
|
888
|
+
return True
|
|
889
|
+
|
|
890
|
+
if not auto_download:
|
|
891
|
+
return False
|
|
892
|
+
|
|
893
|
+
from .simple_model_manager import get_model_manager
|
|
894
|
+
manager = get_model_manager(self.debug_mode)
|
|
895
|
+
return manager.download_essential_model()
|
|
896
|
+
|
|
897
|
+
def get_cache_status(self) -> dict:
|
|
898
|
+
"""Get model cache status.
|
|
899
|
+
|
|
900
|
+
Returns:
|
|
901
|
+
dict: Cache information including total models, sizes, etc.
|
|
902
|
+
"""
|
|
903
|
+
from .simple_model_manager import get_model_manager
|
|
904
|
+
manager = get_model_manager(self.debug_mode)
|
|
905
|
+
return manager.get_status()
|
|
906
|
+
|
|
826
907
|
def cleanup(self):
|
|
827
908
|
"""Clean up resources.
|
|
828
|
-
|
|
909
|
+
|
|
829
910
|
Returns:
|
|
830
911
|
True if cleanup successful
|
|
831
912
|
"""
|
|
832
913
|
if self.voice_recognizer:
|
|
833
914
|
self.voice_recognizer.stop()
|
|
834
|
-
|
|
915
|
+
|
|
835
916
|
self.stop_speaking()
|
|
836
917
|
return True
|