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.
- lollms_client/__init__.py +1 -1
- lollms_client/llm_bindings/llamacpp/__init__.py +354 -233
- lollms_client/llm_bindings/lollms/__init__.py +152 -153
- lollms_client/lollms_core.py +163 -75
- lollms_client/lollms_discussion.py +2 -2
- lollms_client/lollms_llm_binding.py +3 -3
- lollms_client/lollms_tts_binding.py +80 -67
- lollms_client/tts_bindings/bark/__init__.py +110 -329
- lollms_client/tts_bindings/bark/server/install_bark.py +64 -0
- lollms_client/tts_bindings/bark/server/main.py +311 -0
- lollms_client/tts_bindings/piper_tts/__init__.py +115 -335
- lollms_client/tts_bindings/piper_tts/server/install_piper.py +92 -0
- lollms_client/tts_bindings/piper_tts/server/main.py +425 -0
- lollms_client/tts_bindings/piper_tts/server/setup_voices.py +67 -0
- lollms_client/tts_bindings/xtts/__init__.py +99 -305
- lollms_client/tts_bindings/xtts/server/main.py +314 -0
- lollms_client/tts_bindings/xtts/server/setup_voices.py +67 -0
- {lollms_client-1.3.3.dist-info → lollms_client-1.3.6.dist-info}/METADATA +1 -1
- {lollms_client-1.3.3.dist-info → lollms_client-1.3.6.dist-info}/RECORD +22 -15
- {lollms_client-1.3.3.dist-info → lollms_client-1.3.6.dist-info}/WHEEL +0 -0
- {lollms_client-1.3.3.dist-info → lollms_client-1.3.6.dist-info}/licenses/LICENSE +0 -0
- {lollms_client-1.3.3.dist-info → lollms_client-1.3.6.dist-info}/top_level.txt +0 -0
|
@@ -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()
|