abstractvoice 0.3.1__py3-none-any.whl → 0.4.6__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.
@@ -0,0 +1,500 @@
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
+ # Changed from fast_pitch to tacotron2-DDC because fast_pitch downloads are failing
35
+ ESSENTIAL_MODEL = "tts_models/en/ljspeech/tacotron2-DDC"
36
+
37
+ # Available models organized by language with metadata
38
+ AVAILABLE_MODELS = {
39
+ "en": {
40
+ "tacotron2": {
41
+ "model": "tts_models/en/ljspeech/tacotron2-DDC",
42
+ "name": "Linda (LJSpeech)",
43
+ "quality": "good",
44
+ "size_mb": 362,
45
+ "description": "Standard female voice (LJSpeech speaker)",
46
+ "requires_espeak": False,
47
+ "default": True
48
+ },
49
+ "jenny": {
50
+ "model": "tts_models/en/jenny/jenny",
51
+ "name": "Jenny",
52
+ "quality": "excellent",
53
+ "size_mb": 368,
54
+ "description": "Different female voice, clear and natural",
55
+ "requires_espeak": False,
56
+ "default": False
57
+ },
58
+ "ek1": {
59
+ "model": "tts_models/en/ek1/tacotron2",
60
+ "name": "Edward (EK1)",
61
+ "quality": "excellent",
62
+ "size_mb": 310,
63
+ "description": "Male voice with British accent",
64
+ "requires_espeak": False,
65
+ "default": False
66
+ },
67
+ "sam": {
68
+ "model": "tts_models/en/sam/tacotron-DDC",
69
+ "name": "Sam",
70
+ "quality": "good",
71
+ "size_mb": 370,
72
+ "description": "Different male voice, deeper tone",
73
+ "requires_espeak": False,
74
+ "default": False
75
+ },
76
+ "fast_pitch": {
77
+ "model": "tts_models/en/ljspeech/fast_pitch",
78
+ "name": "Linda Fast (LJSpeech)",
79
+ "quality": "good",
80
+ "size_mb": 107,
81
+ "description": "Same speaker as Linda but faster engine",
82
+ "requires_espeak": False,
83
+ "default": False
84
+ },
85
+ "vits": {
86
+ "model": "tts_models/en/ljspeech/vits",
87
+ "name": "Linda Premium (LJSpeech)",
88
+ "quality": "excellent",
89
+ "size_mb": 328,
90
+ "description": "Same speaker as Linda but premium quality",
91
+ "requires_espeak": True,
92
+ "default": False
93
+ }
94
+ },
95
+ "fr": {
96
+ "css10_vits": {
97
+ "model": "tts_models/fr/css10/vits",
98
+ "name": "CSS10 VITS (French)",
99
+ "quality": "excellent",
100
+ "size_mb": 548,
101
+ "description": "High-quality French voice",
102
+ "requires_espeak": True,
103
+ "default": True
104
+ },
105
+ "mai_tacotron2": {
106
+ "model": "tts_models/fr/mai/tacotron2-DDC",
107
+ "name": "MAI Tacotron2 (French)",
108
+ "quality": "good",
109
+ "size_mb": 362,
110
+ "description": "Reliable French voice",
111
+ "requires_espeak": False,
112
+ "default": False
113
+ }
114
+ },
115
+ "es": {
116
+ "mai_tacotron2": {
117
+ "model": "tts_models/es/mai/tacotron2-DDC",
118
+ "name": "MAI Tacotron2 (Spanish)",
119
+ "quality": "good",
120
+ "size_mb": 362,
121
+ "description": "Reliable Spanish voice",
122
+ "requires_espeak": False,
123
+ "default": True
124
+ },
125
+ "css10_vits": {
126
+ "model": "tts_models/es/css10/vits",
127
+ "name": "CSS10 VITS (Spanish)",
128
+ "quality": "excellent",
129
+ "size_mb": 548,
130
+ "description": "High-quality Spanish voice",
131
+ "requires_espeak": True,
132
+ "default": False
133
+ }
134
+ },
135
+ "de": {
136
+ "thorsten_vits": {
137
+ "model": "tts_models/de/thorsten/vits",
138
+ "name": "Thorsten VITS (German)",
139
+ "quality": "excellent",
140
+ "size_mb": 548,
141
+ "description": "High-quality German voice",
142
+ "requires_espeak": True,
143
+ "default": True
144
+ }
145
+ },
146
+ "it": {
147
+ "mai_male_vits": {
148
+ "model": "tts_models/it/mai_male/vits",
149
+ "name": "MAI Male VITS (Italian)",
150
+ "quality": "excellent",
151
+ "size_mb": 548,
152
+ "description": "High-quality Italian male voice",
153
+ "requires_espeak": True,
154
+ "default": True
155
+ },
156
+ "mai_female_vits": {
157
+ "model": "tts_models/it/mai_female/vits",
158
+ "name": "MAI Female VITS (Italian)",
159
+ "quality": "excellent",
160
+ "size_mb": 548,
161
+ "description": "High-quality Italian female voice",
162
+ "requires_espeak": True,
163
+ "default": False
164
+ }
165
+ }
166
+ }
167
+
168
+ def __init__(self, debug_mode: bool = False):
169
+ self.debug_mode = debug_mode
170
+ self._cache_dir = None
171
+
172
+ @property
173
+ def cache_dir(self) -> str:
174
+ """Get the TTS model cache directory."""
175
+ if self._cache_dir is None:
176
+ # Check common cache locations
177
+ import appdirs
178
+ potential_dirs = [
179
+ os.path.expanduser("~/.cache/tts"),
180
+ appdirs.user_data_dir("tts"),
181
+ os.path.expanduser("~/.local/share/tts"),
182
+ os.path.expanduser("~/Library/Application Support/tts"), # macOS
183
+ ]
184
+
185
+ # Find existing cache or use default
186
+ for cache_dir in potential_dirs:
187
+ if os.path.exists(cache_dir):
188
+ self._cache_dir = cache_dir
189
+ break
190
+ else:
191
+ # Use appdirs default
192
+ self._cache_dir = appdirs.user_data_dir("tts")
193
+
194
+ return self._cache_dir
195
+
196
+ def is_model_cached(self, model_name: str) -> bool:
197
+ """Check if a specific model is cached locally."""
198
+ try:
199
+ # Convert model name to cache directory structure
200
+ cache_name = model_name.replace("/", "--")
201
+ model_path = os.path.join(self.cache_dir, cache_name)
202
+
203
+ if not os.path.exists(model_path):
204
+ return False
205
+
206
+ # Check for essential model files
207
+ essential_files = ["model.pth", "config.json"]
208
+ return any(os.path.exists(os.path.join(model_path, f)) for f in essential_files)
209
+ except Exception as e:
210
+ if self.debug_mode:
211
+ print(f"Error checking cache for {model_name}: {e}")
212
+ return False
213
+
214
+ def download_model(self, model_name: str, progress_callback: Optional[Callable[[str, bool], None]] = None) -> bool:
215
+ """Download a specific model.
216
+
217
+ Args:
218
+ model_name: TTS model name (e.g., 'tts_models/en/ljspeech/fast_pitch')
219
+ progress_callback: Optional callback function(model_name, success)
220
+
221
+ Returns:
222
+ bool: True if successful
223
+ """
224
+ if self.is_model_cached(model_name):
225
+ if self.debug_mode:
226
+ print(f"✅ {model_name} already cached")
227
+ if progress_callback:
228
+ progress_callback(model_name, True)
229
+ return True
230
+
231
+ try:
232
+ TTS, _ = _import_tts()
233
+
234
+ if self.debug_mode:
235
+ print(f"📥 Downloading {model_name}...")
236
+
237
+ start_time = time.time()
238
+
239
+ # Initialize TTS to trigger download
240
+ tts = TTS(model_name=model_name, progress_bar=True)
241
+
242
+ download_time = time.time() - start_time
243
+ if self.debug_mode:
244
+ print(f"✅ Downloaded {model_name} in {download_time:.1f}s")
245
+
246
+ if progress_callback:
247
+ progress_callback(model_name, True)
248
+ return True
249
+
250
+ except Exception as e:
251
+ if self.debug_mode:
252
+ print(f"❌ Failed to download {model_name}: {e}")
253
+ if progress_callback:
254
+ progress_callback(model_name, False)
255
+ return False
256
+
257
+ def download_essential_model(self, progress_callback: Optional[Callable[[str, bool], None]] = None) -> bool:
258
+ """Download the essential English model for immediate functionality."""
259
+ return self.download_model(self.ESSENTIAL_MODEL, progress_callback)
260
+
261
+ def list_available_models(self, language: Optional[str] = None) -> Dict[str, Any]:
262
+ """Get list of available models with metadata.
263
+
264
+ Args:
265
+ language: Optional language filter
266
+
267
+ Returns:
268
+ dict: Model information in JSON-serializable format
269
+ """
270
+ if language:
271
+ if language in self.AVAILABLE_MODELS:
272
+ return {language: self.AVAILABLE_MODELS[language]}
273
+ else:
274
+ return {}
275
+
276
+ # Return all models with cache status
277
+ result = {}
278
+ for lang, models in self.AVAILABLE_MODELS.items():
279
+ result[lang] = {}
280
+ for model_id, model_info in models.items():
281
+ # Add cache status to each model
282
+ model_data = model_info.copy()
283
+ model_data["cached"] = self.is_model_cached(model_info["model"])
284
+ result[lang][model_id] = model_data
285
+
286
+ return result
287
+
288
+ def get_cached_models(self) -> List[str]:
289
+ """Get list of model names that are currently cached."""
290
+ if not os.path.exists(self.cache_dir):
291
+ return []
292
+
293
+ cached = []
294
+ try:
295
+ for item in os.listdir(self.cache_dir):
296
+ if item.startswith("tts_models--"):
297
+ # Convert cache name back to model name
298
+ model_name = item.replace("--", "/")
299
+ if self.is_model_cached(model_name):
300
+ cached.append(model_name)
301
+ except Exception as e:
302
+ if self.debug_mode:
303
+ print(f"Error listing cached models: {e}")
304
+
305
+ return cached
306
+
307
+ def get_status(self) -> Dict[str, Any]:
308
+ """Get comprehensive status information."""
309
+ cached_models = self.get_cached_models()
310
+ essential_cached = self.ESSENTIAL_MODEL in cached_models
311
+
312
+ # Calculate total cache size
313
+ total_size_mb = 0
314
+ if os.path.exists(self.cache_dir):
315
+ try:
316
+ for root, dirs, files in os.walk(self.cache_dir):
317
+ for file in files:
318
+ total_size_mb += os.path.getsize(os.path.join(root, file)) / (1024 * 1024)
319
+ except:
320
+ pass
321
+
322
+ return {
323
+ "cache_dir": self.cache_dir,
324
+ "cached_models": cached_models,
325
+ "total_cached": len(cached_models),
326
+ "essential_model_cached": essential_cached,
327
+ "essential_model": self.ESSENTIAL_MODEL,
328
+ "ready_for_offline": essential_cached,
329
+ "total_size_mb": round(total_size_mb, 1),
330
+ "available_languages": list(self.AVAILABLE_MODELS.keys()),
331
+ }
332
+
333
+ def clear_cache(self, confirm: bool = False) -> bool:
334
+ """Clear the model cache."""
335
+ if not confirm:
336
+ return False
337
+
338
+ try:
339
+ import shutil
340
+ if os.path.exists(self.cache_dir):
341
+ shutil.rmtree(self.cache_dir)
342
+ if self.debug_mode:
343
+ print(f"✅ Cleared model cache: {self.cache_dir}")
344
+ return True
345
+ return True
346
+ except Exception as e:
347
+ if self.debug_mode:
348
+ print(f"❌ Failed to clear cache: {e}")
349
+ return False
350
+
351
+ def ensure_essential_model(self, auto_download: bool = True) -> bool:
352
+ """Ensure the essential model is available.
353
+
354
+ Args:
355
+ auto_download: Whether to download if not cached
356
+
357
+ Returns:
358
+ bool: True if essential model is ready
359
+ """
360
+ if self.is_model_cached(self.ESSENTIAL_MODEL):
361
+ return True
362
+
363
+ if not auto_download:
364
+ return False
365
+
366
+ return self.download_essential_model()
367
+
368
+
369
+ # Global instance for easy access
370
+ _model_manager = None
371
+
372
+ def get_model_manager(debug_mode: bool = False) -> SimpleModelManager:
373
+ """Get the global model manager instance."""
374
+ global _model_manager
375
+ if _model_manager is None:
376
+ _model_manager = SimpleModelManager(debug_mode=debug_mode)
377
+ return _model_manager
378
+
379
+
380
+ # Simple API functions for third-party use
381
+ def list_models(language: Optional[str] = None) -> str:
382
+ """Get available models as JSON string.
383
+
384
+ Args:
385
+ language: Optional language filter
386
+
387
+ Returns:
388
+ str: JSON string of available models
389
+ """
390
+ manager = get_model_manager()
391
+ return json.dumps(manager.list_available_models(language), indent=2)
392
+
393
+
394
+ def download_model(model_name: str, progress_callback: Optional[Callable[[str, bool], None]] = None) -> bool:
395
+ """Download a specific model.
396
+
397
+ Args:
398
+ model_name: Model name or voice ID (e.g., 'en.vits' or 'tts_models/en/ljspeech/vits')
399
+ progress_callback: Optional progress callback
400
+
401
+ Returns:
402
+ bool: True if successful
403
+ """
404
+ manager = get_model_manager()
405
+
406
+ # Handle voice ID format (e.g., 'en.vits')
407
+ if '.' in model_name and not model_name.startswith('tts_models'):
408
+ lang, voice_id = model_name.split('.', 1)
409
+ if lang in manager.AVAILABLE_MODELS and voice_id in manager.AVAILABLE_MODELS[lang]:
410
+ model_name = manager.AVAILABLE_MODELS[lang][voice_id]["model"]
411
+ else:
412
+ return False
413
+
414
+ return manager.download_model(model_name, progress_callback)
415
+
416
+
417
+ def get_status() -> str:
418
+ """Get model cache status as JSON string."""
419
+ manager = get_model_manager()
420
+ return json.dumps(manager.get_status(), indent=2)
421
+
422
+
423
+ def is_ready() -> bool:
424
+ """Check if essential model is ready for immediate use."""
425
+ manager = get_model_manager()
426
+ return manager.is_model_cached(manager.ESSENTIAL_MODEL)
427
+
428
+
429
+ def download_models_cli():
430
+ """Simple CLI entry point for downloading models."""
431
+ import argparse
432
+ import sys
433
+
434
+ parser = argparse.ArgumentParser(description="Download TTS models for offline use")
435
+ parser.add_argument("--essential", action="store_true",
436
+ help="Download essential model (default)")
437
+ parser.add_argument("--all", action="store_true",
438
+ help="Download all available models")
439
+ parser.add_argument("--model", type=str,
440
+ help="Download specific model by name")
441
+ parser.add_argument("--language", type=str,
442
+ help="Download models for specific language (en, fr, es, de, it)")
443
+ parser.add_argument("--status", action="store_true",
444
+ help="Show current cache status")
445
+ parser.add_argument("--clear", action="store_true",
446
+ help="Clear model cache")
447
+
448
+ args = parser.parse_args()
449
+
450
+ manager = get_model_manager(debug_mode=True)
451
+
452
+ if args.status:
453
+ print(get_status())
454
+ return
455
+
456
+ if args.clear:
457
+ manager.clear_cache()
458
+ print("✅ Cache cleared")
459
+ return
460
+
461
+ if args.model:
462
+ success = download_model(args.model)
463
+ if success:
464
+ print(f"✅ Downloaded {args.model}")
465
+ else:
466
+ print(f"❌ Failed to download {args.model}")
467
+ sys.exit(0 if success else 1)
468
+
469
+ if args.language:
470
+ # Language-specific downloads using our simple API
471
+ lang_models = {
472
+ 'en': ['en.tacotron2', 'en.jenny', 'en.ek1'],
473
+ 'fr': ['fr.css10_vits', 'fr.mai_tacotron2'],
474
+ 'es': ['es.mai_tacotron2'],
475
+ 'de': ['de.thorsten_vits'],
476
+ 'it': ['it.mai_male_vits', 'it.mai_female_vits']
477
+ }
478
+
479
+ if args.language not in lang_models:
480
+ print(f"❌ Language '{args.language}' not supported")
481
+ print(f" Available: {list(lang_models.keys())}")
482
+ sys.exit(1)
483
+
484
+ success = False
485
+ for model_id in lang_models[args.language]:
486
+ if download_model(model_id):
487
+ print(f"✅ Downloaded {model_id}")
488
+ success = True
489
+ break
490
+
491
+ sys.exit(0 if success else 1)
492
+
493
+ # Default: download essential model
494
+ print("📦 Downloading essential TTS model...")
495
+ success = download_model(manager.ESSENTIAL_MODEL)
496
+ if success:
497
+ print("✅ Essential model ready!")
498
+ else:
499
+ print("❌ Failed to download essential model")
500
+ sys.exit(0 if success else 1)