abstractvoice 0.4.1__py3-none-any.whl → 0.5.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.
@@ -1,384 +0,0 @@
1
- """Model management utilities for AbstractVoice.
2
-
3
- This module provides utilities for downloading, caching, and managing TTS models
4
- to ensure offline functionality and better user experience.
5
- """
6
-
7
- import os
8
- import sys
9
- import time
10
- import threading
11
- from typing import List, Optional, Dict, Any
12
- from pathlib import Path
13
-
14
-
15
- def _import_tts():
16
- """Import TTS with helpful error message if dependencies missing."""
17
- try:
18
- from TTS.api import TTS
19
- from TTS.utils.manage import ModelManager
20
- return TTS, ModelManager
21
- except ImportError as e:
22
- raise ImportError(
23
- "TTS functionality requires coqui-tts. Install with:\n"
24
- " pip install abstractvoice[tts] # For TTS only\n"
25
- " pip install abstractvoice[voice-full] # For complete voice functionality\n"
26
- " pip install abstractvoice[all] # For all features\n"
27
- f"Original error: {e}"
28
- ) from e
29
-
30
-
31
- class ModelManager:
32
- """Manages TTS model downloading, caching, and offline availability."""
33
-
34
- # Essential models for immediate functionality
35
- ESSENTIAL_MODELS = [
36
- "tts_models/en/ljspeech/fast_pitch", # Lightweight, no espeak dependency
37
- "tts_models/en/ljspeech/tacotron2-DDC", # Reliable fallback
38
- ]
39
-
40
- # Premium models for best quality (downloaded on-demand)
41
- PREMIUM_MODELS = [
42
- "tts_models/en/ljspeech/vits", # Best quality English
43
- "tts_models/fr/css10/vits", # Best quality French
44
- "tts_models/es/mai/tacotron2-DDC", # Best quality Spanish
45
- "tts_models/de/thorsten/vits", # Best quality German
46
- "tts_models/it/mai_male/vits", # Best quality Italian
47
- ]
48
-
49
- # All supported models
50
- ALL_MODELS = ESSENTIAL_MODELS + PREMIUM_MODELS
51
-
52
- def __init__(self, debug_mode: bool = False):
53
- self.debug_mode = debug_mode
54
- self._cache_dir = None
55
- self._model_manager = None
56
-
57
- @property
58
- def cache_dir(self) -> str:
59
- """Get the TTS model cache directory."""
60
- if self._cache_dir is None:
61
- # Check common cache locations
62
- import appdirs
63
- potential_dirs = [
64
- os.path.expanduser("~/.cache/tts"),
65
- appdirs.user_data_dir("tts"),
66
- os.path.expanduser("~/.local/share/tts"),
67
- ]
68
-
69
- # Find existing cache or use default
70
- for cache_dir in potential_dirs:
71
- if os.path.exists(cache_dir):
72
- self._cache_dir = cache_dir
73
- break
74
- else:
75
- # Use appdirs default
76
- self._cache_dir = appdirs.user_data_dir("tts")
77
-
78
- return self._cache_dir
79
-
80
- @property
81
- def model_manager(self):
82
- """Get TTS ModelManager instance."""
83
- if self._model_manager is None:
84
- _, ModelManagerClass = _import_tts()
85
- self._model_manager = ModelManagerClass()
86
- return self._model_manager
87
-
88
- def check_model_cache(self, model_name: str) -> bool:
89
- """Check if a model is already cached locally."""
90
- try:
91
- # Look for model files in cache
92
- model_path = self._get_model_path(model_name)
93
- if model_path and os.path.exists(model_path):
94
- # Check for essential model files
95
- model_files = ["model.pth", "config.json"]
96
- return any(
97
- os.path.exists(os.path.join(model_path, f))
98
- for f in model_files
99
- )
100
- return False
101
- except Exception as e:
102
- if self.debug_mode:
103
- print(f"Error checking cache for {model_name}: {e}")
104
- return False
105
-
106
- def _get_model_path(self, model_name: str) -> Optional[str]:
107
- """Get the expected cache path for a model."""
108
- # Convert model name to cache directory structure
109
- # e.g., "tts_models/en/ljspeech/vits" -> "tts_models--en--ljspeech--vits"
110
- cache_name = model_name.replace("/", "--")
111
- return os.path.join(self.cache_dir, cache_name)
112
-
113
- def get_cached_models(self) -> List[str]:
114
- """Get list of models that are cached locally."""
115
- if not os.path.exists(self.cache_dir):
116
- return []
117
-
118
- cached = []
119
- try:
120
- for item in os.listdir(self.cache_dir):
121
- if item.startswith("tts_models--"):
122
- # Convert cache name back to model name
123
- model_name = item.replace("--", "/")
124
- if self.check_model_cache(model_name):
125
- cached.append(model_name)
126
- except Exception as e:
127
- if self.debug_mode:
128
- print(f"Error listing cached models: {e}")
129
-
130
- return cached
131
-
132
- def download_model(self, model_name: str, force: bool = False) -> bool:
133
- """Download a specific model."""
134
- if not force and self.check_model_cache(model_name):
135
- if self.debug_mode:
136
- print(f"✅ {model_name} already cached")
137
- return True
138
-
139
- try:
140
- TTS, _ = _import_tts()
141
-
142
- print(f"📥 Downloading {model_name}...")
143
- start_time = time.time()
144
-
145
- # Initialize TTS to trigger download
146
- tts = TTS(model_name=model_name, progress_bar=True)
147
-
148
- download_time = time.time() - start_time
149
- print(f"✅ Downloaded {model_name} in {download_time:.1f}s")
150
- return True
151
-
152
- except Exception as e:
153
- print(f"❌ Failed to download {model_name}: {e}")
154
- return False
155
-
156
- def download_all_models(self) -> bool:
157
- """Download all supported models."""
158
- print("📦 Downloading all TTS models...")
159
-
160
- success_count = 0
161
- for model in self.ALL_MODELS:
162
- if self.download_model(model):
163
- success_count += 1
164
-
165
- print(f"✅ Downloaded {success_count}/{len(self.ALL_MODELS)} models")
166
- return success_count > 0
167
-
168
- def get_offline_model(self, preferred_models: List[str]) -> Optional[str]:
169
- """Get the best available cached model from a preference list."""
170
- cached_models = self.get_cached_models()
171
-
172
- # Return first preferred model that's cached
173
- for model in preferred_models:
174
- if model in cached_models:
175
- return model
176
-
177
- # Fallback to any cached model
178
- if cached_models:
179
- return cached_models[0]
180
-
181
- return None
182
-
183
- def print_status(self):
184
- """Print current model cache status."""
185
- print("🎭 TTS Model Cache Status")
186
- print("=" * 50)
187
-
188
- cached_models = self.get_cached_models()
189
-
190
- if not cached_models:
191
- print("❌ No models cached - first use will require internet")
192
- print("\nTo download essential models for offline use:")
193
- print(" abstractvoice download-models")
194
- return
195
-
196
- print(f"✅ {len(cached_models)} models cached for offline use:")
197
-
198
- # Group by category
199
- essential_cached = [m for m in cached_models if m in self.ESSENTIAL_MODELS]
200
- premium_cached = [m for m in cached_models if m in self.PREMIUM_MODELS]
201
- other_cached = [m for m in cached_models if m not in self.ALL_MODELS]
202
-
203
- if essential_cached:
204
- print(f"\n📦 Essential Models ({len(essential_cached)}):")
205
- for model in essential_cached:
206
- print(f" ✅ {model}")
207
-
208
- if premium_cached:
209
- print(f"\n✨ Premium Models ({len(premium_cached)}):")
210
- for model in premium_cached:
211
- print(f" ✅ {model}")
212
-
213
- if other_cached:
214
- print(f"\n🔧 Other Models ({len(other_cached)}):")
215
- for model in other_cached:
216
- print(f" ✅ {model}")
217
-
218
- print(f"\n💾 Cache location: {self.cache_dir}")
219
-
220
- # Check cache size
221
- try:
222
- total_size = 0
223
- for root, dirs, files in os.walk(self.cache_dir):
224
- for file in files:
225
- total_size += os.path.getsize(os.path.join(root, file))
226
- size_mb = total_size / (1024 * 1024)
227
- print(f"💽 Total cache size: {size_mb:.1f} MB")
228
- except:
229
- pass
230
-
231
- def clear_cache(self, confirm: bool = False) -> bool:
232
- """Clear the model cache."""
233
- if not confirm:
234
- print("⚠️ This will delete all cached TTS models.")
235
- print("Use clear_cache(confirm=True) to proceed.")
236
- return False
237
-
238
- try:
239
- import shutil
240
- if os.path.exists(self.cache_dir):
241
- shutil.rmtree(self.cache_dir)
242
- print(f"✅ Cleared model cache: {self.cache_dir}")
243
- return True
244
- else:
245
- print("ℹ️ No cache to clear")
246
- return True
247
- except Exception as e:
248
- print(f"❌ Failed to clear cache: {e}")
249
- return False
250
-
251
-
252
- def download_models_cli():
253
- """CLI entry point for downloading models."""
254
- import argparse
255
- import sys
256
-
257
- parser = argparse.ArgumentParser(description="Download TTS models for offline use")
258
- parser.add_argument("--essential", action="store_true",
259
- help="Download only essential models (recommended)")
260
- parser.add_argument("--all", action="store_true",
261
- help="Download all supported models")
262
- parser.add_argument("--model", type=str,
263
- help="Download specific model by name")
264
- parser.add_argument("--language", type=str,
265
- help="Download models for specific language (en, fr, es, de, it)")
266
- parser.add_argument("--status", action="store_true",
267
- help="Show current cache status")
268
- parser.add_argument("--clear", action="store_true",
269
- help="Clear model cache")
270
- parser.add_argument("--debug", action="store_true",
271
- help="Enable debug output")
272
-
273
- args = parser.parse_args()
274
-
275
- # Use VoiceManager for consistent programmatic API
276
- from abstractvoice.voice_manager import VoiceManager
277
-
278
- vm = VoiceManager(debug_mode=args.debug)
279
-
280
- if args.status:
281
- # Use VoiceManager's model status
282
- status = vm.get_cache_status()
283
- print("🎭 TTS Model Cache Status")
284
- print("=" * 50)
285
-
286
- if status['total_cached'] == 0:
287
- print("❌ No models cached - first use will require internet")
288
- print("\nTo download essential models for offline use:")
289
- print(" abstractvoice download-models --essential")
290
- return
291
-
292
- print(f"✅ {status['total_cached']} models cached for offline use")
293
- print(f"📦 Essential model cached: {status['essential_model_cached']}")
294
- print(f"🌐 Ready for offline: {status['ready_for_offline']}")
295
- print(f"💾 Cache location: {status['cache_dir']}")
296
- print(f"💽 Total cache size: {status['total_size_mb']} MB")
297
-
298
- # Show cached models
299
- cached_models = status['cached_models']
300
- essential_model = status['essential_model']
301
-
302
- print(f"\n📦 Essential Model:")
303
- if essential_model in cached_models:
304
- print(f" ✅ {essential_model}")
305
- else:
306
- print(f" 📥 {essential_model} (not cached)")
307
-
308
- print(f"\n📋 All Cached Models ({len(cached_models)}):")
309
- for model in sorted(cached_models)[:10]: # Show first 10
310
- print(f" ✅ {model}")
311
- if len(cached_models) > 10:
312
- print(f" ... and {len(cached_models) - 10} more")
313
- return
314
-
315
- if args.clear:
316
- # Use ModelManager for low-level cache operations
317
- manager = ModelManager(debug_mode=args.debug)
318
- manager.clear_cache(confirm=True)
319
- return
320
-
321
- if args.model:
322
- # Use ModelManager for direct model download
323
- manager = ModelManager(debug_mode=args.debug)
324
- success = manager.download_model(args.model)
325
- sys.exit(0 if success else 1)
326
-
327
- if args.language:
328
- # Use simple model download for language-specific models
329
- print(f"📦 Downloading models for {args.language}...")
330
-
331
- # Get available models for this language
332
- models = vm.list_available_models(args.language)
333
- if args.language not in models:
334
- print(f"❌ Language '{args.language}' not supported")
335
- print(f" Available languages: {list(vm.list_available_models().keys())}")
336
- sys.exit(1)
337
-
338
- # Download the default model for this language
339
- language_models = models[args.language]
340
- default_model = None
341
- for voice_id, voice_info in language_models.items():
342
- if voice_info.get('default', False):
343
- default_model = f"{args.language}.{voice_id}"
344
- break
345
-
346
- if not default_model:
347
- # Take the first available model
348
- first_voice = list(language_models.keys())[0]
349
- default_model = f"{args.language}.{first_voice}"
350
-
351
- print(f" 📥 Downloading {default_model}...")
352
- success = vm.download_model(default_model)
353
-
354
- if success:
355
- print(f"✅ Downloaded {default_model}")
356
- print(f"✅ {args.language.upper()} voice is now ready!")
357
- else:
358
- print(f"❌ Failed to download {default_model}")
359
- sys.exit(0 if success else 1)
360
-
361
- if args.all:
362
- # Use ModelManager for downloading all models
363
- manager = ModelManager(debug_mode=args.debug)
364
- success = manager.download_all_models()
365
- sys.exit(0 if success else 1)
366
-
367
- # Default to essential models via VoiceManager
368
- if args.essential or (not args.all and not args.model and not args.language):
369
- print("📦 Downloading essential TTS model for offline use...")
370
-
371
- # Use the simple ensure_ready method
372
- success = vm.ensure_ready(auto_download=True)
373
-
374
- if success:
375
- print("✅ Essential model downloaded successfully!")
376
- print("🎉 AbstractVoice is now ready for offline use!")
377
- else:
378
- print("❌ Essential model download failed")
379
- print(" Check your internet connection")
380
- sys.exit(0 if success else 1)
381
-
382
-
383
- if __name__ == "__main__":
384
- download_models_cli()