abstractvoice 0.3.1__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.1.dist-info → abstractvoice-0.4.1.dist-info}/METADATA +117 -19
- abstractvoice-0.4.1.dist-info/RECORD +23 -0
- abstractvoice-0.3.1.dist-info/RECORD +0 -21
- {abstractvoice-0.3.1.dist-info → abstractvoice-0.4.1.dist-info}/WHEEL +0 -0
- {abstractvoice-0.3.1.dist-info → abstractvoice-0.4.1.dist-info}/entry_points.txt +0 -0
- {abstractvoice-0.3.1.dist-info → abstractvoice-0.4.1.dist-info}/licenses/LICENSE +0 -0
- {abstractvoice-0.3.1.dist-info → abstractvoice-0.4.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,384 @@
|
|
|
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()
|