lollms-client 1.3.3__py3-none-any.whl → 1.3.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.

Potentially problematic release.


This version of lollms-client might be problematic. Click here for more details.

@@ -0,0 +1,425 @@
1
+ # File: lollms_client/tts_bindings/piper/server/main.py
2
+
3
+ import uvicorn
4
+ from fastapi import FastAPI, APIRouter, HTTPException
5
+ from pydantic import BaseModel
6
+ import argparse
7
+ import sys
8
+ from pathlib import Path
9
+ import asyncio
10
+ import traceback
11
+ import os
12
+ import json
13
+ from typing import Optional, List, Dict
14
+ import io
15
+ import aiohttp
16
+ import aiofiles
17
+
18
+ # --- Piper TTS Implementation ---
19
+ try:
20
+ print("Server: Loading Piper dependencies...")
21
+ import piper
22
+ import numpy as np
23
+ import soundfile as sf
24
+ print("Server: Piper dependencies loaded successfully")
25
+ piper_available = True
26
+
27
+ except Exception as e:
28
+ print(f"Server: Failed to load Piper dependencies: {e}")
29
+ print(f"Server: Traceback:\n{traceback.format_exc()}")
30
+ piper_available = False
31
+
32
+ # --- API Models ---
33
+ class GenerationRequest(BaseModel):
34
+ text: str
35
+ voice: Optional[str] = None
36
+ speaker_id: Optional[int] = None
37
+ length_scale: Optional[float] = 1.0
38
+ noise_scale: Optional[float] = 0.667
39
+ noise_w: Optional[float] = 0.8
40
+
41
+ class VoiceRequest(BaseModel):
42
+ voice: str
43
+
44
+ class DownloadRequest(BaseModel):
45
+ voice: str
46
+
47
+ class PiperServer:
48
+ def __init__(self):
49
+ self.models_dir = Path(__file__).parent / "models"
50
+ self.models_dir.mkdir(exist_ok=True)
51
+
52
+ self.current_voice = None
53
+ self.current_model = None
54
+ self.loaded_models = {} # Cache for loaded models
55
+
56
+ # Available voice models (subset of popular ones)
57
+ self.available_voices = self._get_available_voice_list()
58
+ self.installed_voices = self._scan_installed_models()
59
+
60
+ # Auto-download a default voice if none installed
61
+ if not self.installed_voices and piper_available:
62
+ asyncio.create_task(self._download_default_voice())
63
+
64
+ def _get_available_voice_list(self) -> Dict[str, Dict]:
65
+ """Get list of available voice models from Piper repository"""
66
+ # Popular high-quality voices across different languages
67
+ return {
68
+ # English voices
69
+ "en_US-lessac-medium": {
70
+ "language": "en_US",
71
+ "quality": "medium",
72
+ "description": "US English, female, clear",
73
+ "url": "https://huggingface.co/rhasspy/piper-voices/resolve/v1.0.0/en/en_US/lessac/medium/en_US-lessac-medium.onnx"
74
+ },
75
+ "en_US-lessac-low": {
76
+ "language": "en_US",
77
+ "quality": "low",
78
+ "description": "US English, female, fast",
79
+ "url": "https://huggingface.co/rhasspy/piper-voices/resolve/v1.0.0/en/en_US/lessac/low/en_US-lessac-low.onnx"
80
+ },
81
+ "en_US-ryan-high": {
82
+ "language": "en_US",
83
+ "quality": "high",
84
+ "description": "US English, male, high quality",
85
+ "url": "https://huggingface.co/rhasspy/piper-voices/resolve/v1.0.0/en/en_US/ryan/high/en_US-ryan-high.onnx"
86
+ },
87
+ "en_US-ryan-medium": {
88
+ "language": "en_US",
89
+ "quality": "medium",
90
+ "description": "US English, male, balanced",
91
+ "url": "https://huggingface.co/rhasspy/piper-voices/resolve/v1.0.0/en/en_US/ryan/medium/en_US-ryan-medium.onnx"
92
+ },
93
+ "en_GB-alan-medium": {
94
+ "language": "en_GB",
95
+ "quality": "medium",
96
+ "description": "British English, male",
97
+ "url": "https://huggingface.co/rhasspy/piper-voices/resolve/v1.0.0/en/en_GB/alan/medium/en_GB-alan-medium.onnx"
98
+ },
99
+
100
+ # French voices
101
+ "fr_FR-siwis-medium": {
102
+ "language": "fr_FR",
103
+ "quality": "medium",
104
+ "description": "French, female",
105
+ "url": "https://huggingface.co/rhasspy/piper-voices/resolve/v1.0.0/fr/fr_FR/siwis/medium/fr_FR-siwis-medium.onnx"
106
+ },
107
+
108
+ # German voices
109
+ "de_DE-thorsten-medium": {
110
+ "language": "de_DE",
111
+ "quality": "medium",
112
+ "description": "German, male",
113
+ "url": "https://huggingface.co/rhasspy/piper-voices/resolve/v1.0.0/de/de_DE/thorsten/medium/de_DE-thorsten-medium.onnx"
114
+ },
115
+
116
+ # Spanish voices
117
+ "es_ES-mls_9972-low": {
118
+ "language": "es_ES",
119
+ "quality": "low",
120
+ "description": "Spanish, female",
121
+ "url": "https://huggingface.co/rhasspy/piper-voices/resolve/v1.0.0/es/es_ES/mls_9972/low/es_ES-mls_9972-low.onnx"
122
+ },
123
+
124
+ # Italian voices
125
+ "it_IT-riccardo-x_low": {
126
+ "language": "it_IT",
127
+ "quality": "x_low",
128
+ "description": "Italian, male, fast",
129
+ "url": "https://huggingface.co/rhasspy/piper-voices/resolve/v1.0.0/it/it_IT/riccardo/x_low/it_IT-riccardo-x_low.onnx"
130
+ },
131
+
132
+ # Dutch voices
133
+ "nl_NL-mls_5809-low": {
134
+ "language": "nl_NL",
135
+ "quality": "low",
136
+ "description": "Dutch, female",
137
+ "url": "https://huggingface.co/rhasspy/piper-voices/resolve/v1.0.0/nl/nl_NL/mls_5809/low/nl_NL-mls_5809-low.onnx"
138
+ },
139
+
140
+ # Portuguese voices
141
+ "pt_BR-faber-medium": {
142
+ "language": "pt_BR",
143
+ "quality": "medium",
144
+ "description": "Brazilian Portuguese, male",
145
+ "url": "https://huggingface.co/rhasspy/piper-voices/resolve/v1.0.0/pt/pt_BR/faber/medium/pt_BR-faber-medium.onnx"
146
+ }
147
+ }
148
+
149
+ def _scan_installed_models(self) -> List[str]:
150
+ """Scan for already downloaded models"""
151
+ installed = []
152
+ for onnx_file in self.models_dir.glob("*.onnx"):
153
+ voice_name = onnx_file.stem
154
+ # Check if corresponding JSON config exists
155
+ json_file = onnx_file.with_suffix('.onnx.json')
156
+ if json_file.exists():
157
+ installed.append(voice_name)
158
+ return installed
159
+
160
+ async def _download_default_voice(self):
161
+ """Download a default voice if none are installed"""
162
+ try:
163
+ print("Server: No voices installed, downloading default voice...")
164
+ await self.download_voice("en_US-lessac-medium")
165
+ except Exception as e:
166
+ print(f"Server: Failed to download default voice: {e}")
167
+
168
+ async def download_voice(self, voice_name: str) -> bool:
169
+ """Download a voice model and its config"""
170
+ if voice_name not in self.available_voices:
171
+ raise ValueError(f"Voice '{voice_name}' not available")
172
+
173
+ voice_info = self.available_voices[voice_name]
174
+ model_url = voice_info["url"]
175
+ config_url = model_url + ".json"
176
+
177
+ model_path = self.models_dir / f"{voice_name}.onnx"
178
+ config_path = self.models_dir / f"{voice_name}.onnx.json"
179
+
180
+ # Check if already downloaded
181
+ if model_path.exists() and config_path.exists():
182
+ print(f"Server: Voice '{voice_name}' already downloaded")
183
+ if voice_name not in self.installed_voices:
184
+ self.installed_voices.append(voice_name)
185
+ return True
186
+
187
+ try:
188
+ print(f"Server: Downloading voice '{voice_name}'...")
189
+
190
+ async with aiohttp.ClientSession() as session:
191
+ # Download model file
192
+ print(f"Server: Downloading model from {model_url}")
193
+ async with session.get(model_url) as response:
194
+ response.raise_for_status()
195
+ async with aiofiles.open(model_path, 'wb') as f:
196
+ async for chunk in response.content.iter_chunked(8192):
197
+ await f.write(chunk)
198
+
199
+ # Download config file
200
+ print(f"Server: Downloading config from {config_url}")
201
+ async with session.get(config_url) as response:
202
+ response.raise_for_status()
203
+ async with aiofiles.open(config_path, 'wb') as f:
204
+ async for chunk in response.content.iter_chunked(8192):
205
+ await f.write(chunk)
206
+
207
+ # Update installed voices list
208
+ if voice_name not in self.installed_voices:
209
+ self.installed_voices.append(voice_name)
210
+
211
+ print(f"Server: Successfully downloaded voice '{voice_name}'")
212
+ return True
213
+
214
+ except Exception as e:
215
+ print(f"Server: Failed to download voice '{voice_name}': {e}")
216
+ # Clean up partial downloads
217
+ for path in [model_path, config_path]:
218
+ if path.exists():
219
+ path.unlink()
220
+ raise
221
+
222
+ def _load_model(self, voice_name: str):
223
+ """Load a Piper model"""
224
+ if voice_name in self.loaded_models:
225
+ return self.loaded_models[voice_name]
226
+
227
+ model_path = self.models_dir / f"{voice_name}.onnx"
228
+ config_path = self.models_dir / f"{voice_name}.onnx.json"
229
+
230
+ if not (model_path.exists() and config_path.exists()):
231
+ raise FileNotFoundError(f"Voice '{voice_name}' not found. Please download it first.")
232
+
233
+ print(f"Server: Loading model for voice '{voice_name}'...")
234
+
235
+ # Load the model using piper
236
+ voice = piper.PiperVoice.load(str(model_path), config_path=str(config_path))
237
+
238
+ self.loaded_models[voice_name] = voice
239
+ print(f"Server: Model '{voice_name}' loaded successfully")
240
+
241
+ return voice
242
+
243
+ def generate_audio(self, text: str, voice: Optional[str] = None,
244
+ speaker_id: Optional[int] = None, length_scale: float = 1.0,
245
+ noise_scale: float = 0.667, noise_w: float = 0.8) -> bytes:
246
+ """Generate audio from text using Piper"""
247
+ if not piper_available:
248
+ raise RuntimeError("Piper library not available")
249
+
250
+ # Use provided voice or current default
251
+ target_voice = voice or self.current_voice
252
+
253
+ # If no voice specified and no default, use first available
254
+ if not target_voice and self.installed_voices:
255
+ target_voice = self.installed_voices[0]
256
+ self.current_voice = target_voice
257
+
258
+ if not target_voice:
259
+ raise RuntimeError("No voice available. Please download a voice first.")
260
+
261
+ if target_voice not in self.installed_voices:
262
+ raise RuntimeError(f"Voice '{target_voice}' not installed. Please download it first.")
263
+
264
+ try:
265
+ print(f"Server: Generating audio for: '{text[:50]}{'...' if len(text) > 50 else ''}'")
266
+ print(f"Server: Using voice: {target_voice}")
267
+
268
+ # Load the model
269
+ voice_model = self._load_model(target_voice)
270
+
271
+ # Generate audio
272
+ audio_stream = io.BytesIO()
273
+
274
+ # Synthesize to the stream
275
+ voice_model.synthesize(
276
+ text,
277
+ audio_stream,
278
+ speaker_id=speaker_id,
279
+ length_scale=length_scale,
280
+ noise_scale=noise_scale,
281
+ noise_w=noise_w
282
+ )
283
+
284
+ # Get the raw audio data
285
+ audio_stream.seek(0)
286
+ audio_data = audio_stream.getvalue()
287
+
288
+ print(f"Server: Generated {len(audio_data)} bytes of audio")
289
+ return audio_data
290
+
291
+ except Exception as e:
292
+ print(f"Server: Error generating audio: {e}")
293
+ print(f"Server: Traceback:\n{traceback.format_exc()}")
294
+ raise
295
+
296
+ def set_voice(self, voice: str) -> bool:
297
+ """Set the current default voice"""
298
+ if voice in self.installed_voices:
299
+ self.current_voice = voice
300
+ print(f"Server: Voice changed to: {voice}")
301
+ return True
302
+ else:
303
+ print(f"Server: Voice '{voice}' not installed")
304
+ return False
305
+
306
+ def list_voices(self) -> List[str]:
307
+ """Return list of installed voices"""
308
+ return self.installed_voices.copy()
309
+
310
+ def list_available_voices(self) -> Dict[str, Dict]:
311
+ """Return list of all available voices for download"""
312
+ return self.available_voices.copy()
313
+
314
+ def list_models(self) -> List[str]:
315
+ """Return list of available models"""
316
+ return ["piper"]
317
+
318
+ # --- Globals ---
319
+ app = FastAPI(title="Piper TTS Server")
320
+ router = APIRouter()
321
+ piper_server = PiperServer()
322
+ model_lock = asyncio.Lock() # Ensure thread-safe access
323
+
324
+ # --- API Endpoints ---
325
+ @router.post("/generate_audio")
326
+ async def generate_audio(request: GenerationRequest):
327
+ async with model_lock:
328
+ try:
329
+ audio_bytes = piper_server.generate_audio(
330
+ text=request.text,
331
+ voice=request.voice,
332
+ speaker_id=request.speaker_id,
333
+ length_scale=request.length_scale,
334
+ noise_scale=request.noise_scale,
335
+ noise_w=request.noise_w
336
+ )
337
+ from fastapi.responses import Response
338
+ return Response(content=audio_bytes, media_type="audio/wav")
339
+ except Exception as e:
340
+ print(f"Server: ERROR in generate_audio endpoint: {e}")
341
+ print(f"Server: ERROR traceback:\n{traceback.format_exc()}")
342
+ raise HTTPException(status_code=500, detail=str(e))
343
+
344
+ @router.post("/download_voice")
345
+ async def download_voice(request: DownloadRequest):
346
+ try:
347
+ success = await piper_server.download_voice(request.voice)
348
+ return {"success": success, "message": f"Voice '{request.voice}' downloaded successfully"}
349
+ except Exception as e:
350
+ print(f"Server: ERROR in download_voice endpoint: {e}")
351
+ raise HTTPException(status_code=500, detail=str(e))
352
+
353
+ @router.post("/set_voice")
354
+ async def set_voice(request: VoiceRequest):
355
+ try:
356
+ success = piper_server.set_voice(request.voice)
357
+ if success:
358
+ return {"success": True, "message": f"Voice set to {request.voice}"}
359
+ else:
360
+ return {"success": False, "message": f"Voice {request.voice} not installed"}
361
+ except Exception as e:
362
+ print(f"Server: ERROR in set_voice endpoint: {e}")
363
+ raise HTTPException(status_code=500, detail=str(e))
364
+
365
+ @router.get("/list_voices")
366
+ async def list_voices():
367
+ try:
368
+ voices = piper_server.list_voices()
369
+ print(f"Server: Returning {len(voices)} installed voices")
370
+ return {"voices": voices}
371
+ except Exception as e:
372
+ print(f"Server: ERROR in list_voices endpoint: {e}")
373
+ raise HTTPException(status_code=500, detail=str(e))
374
+
375
+ @router.get("/list_available_voices")
376
+ async def list_available_voices():
377
+ try:
378
+ voices = piper_server.list_available_voices()
379
+ return {"voices": voices}
380
+ except Exception as e:
381
+ print(f"Server: ERROR in list_available_voices endpoint: {e}")
382
+ raise HTTPException(status_code=500, detail=str(e))
383
+
384
+ @router.get("/list_models")
385
+ async def list_models():
386
+ try:
387
+ models = piper_server.list_models()
388
+ return {"models": models}
389
+ except Exception as e:
390
+ print(f"Server: ERROR in list_models endpoint: {e}")
391
+ raise HTTPException(status_code=500, detail=str(e))
392
+
393
+ @router.get("/status")
394
+ async def status():
395
+ return {
396
+ "status": "running",
397
+ "piper_available": piper_available,
398
+ "current_voice": piper_server.current_voice,
399
+ "installed_voices_count": len(piper_server.installed_voices),
400
+ "available_voices_count": len(piper_server.available_voices),
401
+ "installed_voices": piper_server.installed_voices
402
+ }
403
+
404
+ app.include_router(router)
405
+
406
+ # --- Server Startup ---
407
+ if __name__ == '__main__':
408
+ parser = argparse.ArgumentParser(description="Piper TTS Server")
409
+ parser.add_argument("--host", type=str, default="localhost", help="Host to bind the server to.")
410
+ parser.add_argument("--port", type=int, default=8083, help="Port to bind the server to.")
411
+
412
+ args = parser.parse_args()
413
+
414
+ print(f"Server: Starting Piper TTS server on {args.host}:{args.port}")
415
+ print(f"Server: Piper available: {piper_available}")
416
+ print(f"Server: Models directory: {piper_server.models_dir}")
417
+ print(f"Server: Installed voices: {len(piper_server.installed_voices)}")
418
+ print(f"Server: Available voices for download: {len(piper_server.available_voices)}")
419
+
420
+ if piper_server.installed_voices:
421
+ print(f"Server: Current voice: {piper_server.current_voice or piper_server.installed_voices[0]}")
422
+ else:
423
+ print("Server: No voices installed - will download default voice on startup")
424
+
425
+ uvicorn.run(app, host=args.host, port=args.port)
@@ -0,0 +1,67 @@
1
+ # File: lollms_client/tts_bindings/xtts/server/setup_voices.py
2
+ #!/usr/bin/env python3
3
+ """
4
+ Helper script to set up XTTS voices directory with sample speaker files
5
+ """
6
+
7
+ import os
8
+ import urllib.request
9
+ from pathlib import Path
10
+
11
+ def download_sample_voices():
12
+ """Download some sample voice files for XTTS"""
13
+
14
+ voices_dir = Path(__file__).parent / "voices"
15
+ voices_dir.mkdir(exist_ok=True)
16
+
17
+ print(f"Setting up voices in: {voices_dir}")
18
+
19
+ # You can add URLs to sample speaker voice files here
20
+ # For now, let's create instructions for users
21
+
22
+ readme_content = """
23
+ # XTTS Voices Directory
24
+
25
+ Place your speaker reference WAV files in this directory.
26
+
27
+ ## How to add voices:
28
+
29
+ 1. Record or find WAV files of speakers you want to clone (5-30 seconds recommended)
30
+ 2. Name them descriptively (e.g., "john.wav", "sarah.wav", "narrator.wav")
31
+ 3. Place them in this directory
32
+ 4. The voice name will be the filename without extension
33
+
34
+ ## Requirements for voice files:
35
+ - WAV format
36
+ - 22050 Hz sample rate (recommended)
37
+ - Mono or stereo
38
+ - Good quality, clear speech
39
+ - 5-30 seconds duration
40
+ - Single speaker
41
+
42
+ ## Example usage:
43
+ ```python
44
+ # Use a custom voice file named "john.wav"
45
+ audio = tts.generate_audio("Hello world", voice="john")
46
+ ```
47
+
48
+ ## Getting sample voices:
49
+ You can:
50
+ 1. Record your own voice
51
+ 2. Use text-to-speech to create reference voices
52
+ 3. Extract audio clips from videos/podcasts (respect copyright)
53
+ 4. Use royalty-free voice samples
54
+
55
+ Note: XTTS works by cloning the voice characteristics from the reference file.
56
+ """
57
+
58
+ readme_path = voices_dir / "README.md"
59
+ with open(readme_path, 'w') as f:
60
+ f.write(readme_content)
61
+
62
+ print("✓ Created voices directory and README")
63
+ print(f"📁 Add your WAV voice files to: {voices_dir}")
64
+ print("📖 See README.md for detailed instructions")
65
+
66
+ if __name__ == "__main__":
67
+ download_sample_voices()