lollms-client 1.3.4__py3-none-any.whl → 1.3.7__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 +162 -76
- 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.4.dist-info → lollms_client-1.3.7.dist-info}/METADATA +1 -1
- {lollms_client-1.3.4.dist-info → lollms_client-1.3.7.dist-info}/RECORD +22 -15
- {lollms_client-1.3.4.dist-info → lollms_client-1.3.7.dist-info}/WHEEL +0 -0
- {lollms_client-1.3.4.dist-info → lollms_client-1.3.7.dist-info}/licenses/LICENSE +0 -0
- {lollms_client-1.3.4.dist-info → lollms_client-1.3.7.dist-info}/top_level.txt +0 -0
|
@@ -1,343 +1,123 @@
|
|
|
1
|
-
# lollms_client/tts_bindings/piper/__init__.py
|
|
2
|
-
import io
|
|
3
|
-
import os
|
|
4
|
-
import wave # Standard Python library for WAV files
|
|
5
|
-
import json
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
from typing import Optional, List, Union, Dict, Any
|
|
8
|
-
|
|
9
|
-
from ascii_colors import trace_exception, ASCIIColors
|
|
10
|
-
|
|
11
|
-
# --- Package Management and Conditional Imports ---
|
|
12
|
-
_piper_tts_installed = False
|
|
13
|
-
_piper_tts_installation_error = ""
|
|
14
|
-
try:
|
|
15
|
-
import pipmaster as pm
|
|
16
|
-
# piper-tts should handle onnxruntime, but ensure it's there if needed
|
|
17
|
-
# We might need specific onnxruntime for CUDA/DirectML later if we extend device support
|
|
18
|
-
pm.ensure_packages(["piper-tts", "onnxruntime"])
|
|
19
|
-
|
|
20
|
-
from piper import PiperVoice
|
|
21
|
-
import numpy as np # For converting audio samples if needed
|
|
22
|
-
|
|
23
|
-
_piper_tts_installed = True
|
|
24
|
-
except Exception as e:
|
|
25
|
-
_piper_tts_installation_error = str(e)
|
|
26
|
-
PiperVoice = None
|
|
27
|
-
np = None # Piper often returns bytes, but numpy can be handy for sample rate conversion if needed
|
|
28
|
-
# --- End Package Management ---
|
|
29
|
-
|
|
1
|
+
# File: lollms_client/tts_bindings/piper/__init__.py
|
|
30
2
|
from lollms_client.lollms_tts_binding import LollmsTTSBinding
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
**kwargs):
|
|
47
|
-
|
|
48
|
-
super().__init__(binding_name="piper")
|
|
49
|
-
|
|
50
|
-
if not _piper_tts_installed:
|
|
51
|
-
raise ImportError(f"Piper TTS binding dependencies not met. Error: {_piper_tts_installation_error}")
|
|
52
|
-
|
|
53
|
-
self.piper_voices_dir = Path(piper_voices_dir).resolve() if piper_voices_dir else None
|
|
54
|
-
if self.piper_voices_dir and not self.piper_voices_dir.is_dir():
|
|
55
|
-
ASCIIColors.warning(f"Piper voices directory does not exist: {self.piper_voices_dir}. Voice listing will be limited.")
|
|
56
|
-
self.piper_voices_dir = None
|
|
57
|
-
|
|
58
|
-
self.current_voice_model_path: Optional[Path] = None
|
|
59
|
-
self.piper_voice: Optional[PiperVoice] = None
|
|
60
|
-
self.voice_config: Optional[Dict] = None # To store sample rate, channels etc.
|
|
61
|
-
|
|
62
|
-
if default_voice_model_path:
|
|
63
|
-
self._load_piper_voice(default_voice_model_path)
|
|
64
|
-
else:
|
|
65
|
-
ASCIIColors.info("No default_voice_model_path provided for Piper. Load a voice via generate_audio or ensure piper_voices_dir is set.")
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def _load_piper_voice(self, voice_model_identifier: Union[str, Path]):
|
|
69
|
-
"""
|
|
70
|
-
Loads a Piper voice model.
|
|
71
|
-
identifier can be a full path to .onnx or a filename to be found in piper_voices_dir.
|
|
72
|
-
"""
|
|
73
|
-
voice_model_path_onnx: Optional[Path] = None
|
|
74
|
-
voice_model_path_json: Optional[Path] = None
|
|
75
|
-
|
|
76
|
-
potential_path = Path(voice_model_identifier)
|
|
77
|
-
|
|
78
|
-
if potential_path.is_absolute() and potential_path.suffix == ".onnx" and potential_path.exists():
|
|
79
|
-
voice_model_path_onnx = potential_path
|
|
80
|
-
voice_model_path_json = potential_path.with_suffix(".onnx.json")
|
|
81
|
-
elif self.piper_voices_dir and (self.piper_voices_dir / voice_model_identifier).exists():
|
|
82
|
-
# Assume voice_model_identifier is a filename like "en_US-ryan-medium.onnx"
|
|
83
|
-
p = self.piper_voices_dir / voice_model_identifier
|
|
84
|
-
if p.suffix == ".onnx":
|
|
85
|
-
voice_model_path_onnx = p
|
|
86
|
-
voice_model_path_json = p.with_suffix(".onnx.json")
|
|
87
|
-
elif potential_path.suffix == ".onnx" and potential_path.exists(): # Relative path
|
|
88
|
-
voice_model_path_onnx = potential_path.resolve()
|
|
89
|
-
voice_model_path_json = voice_model_path_onnx.with_suffix(".onnx.json")
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if not voice_model_path_onnx or not voice_model_path_onnx.exists():
|
|
93
|
-
raise FileNotFoundError(f"Piper ONNX voice model not found: {voice_model_identifier}")
|
|
94
|
-
if not voice_model_path_json or not voice_model_path_json.exists():
|
|
95
|
-
raise FileNotFoundError(f"Piper voice JSON config not found for {voice_model_path_onnx} (expected: {voice_model_path_json})")
|
|
96
|
-
|
|
97
|
-
if self.piper_voice and self.current_voice_model_path == voice_model_path_onnx:
|
|
98
|
-
ASCIIColors.info(f"Piper voice '{voice_model_path_onnx.name}' already loaded.")
|
|
99
|
-
return
|
|
100
|
-
|
|
101
|
-
ASCIIColors.info(f"Loading Piper voice: {voice_model_path_onnx.name}...")
|
|
102
|
-
try:
|
|
103
|
-
# Piper documentation often shows use_cuda=True for GPU with onnxruntime-gpu.
|
|
104
|
-
# For simplicity and Piper's primary CPU strength, we'll omit it for now.
|
|
105
|
-
# onnxruntime will use CPU by default.
|
|
106
|
-
# To enable GPU: user needs onnxruntime-gpu and then `PiperVoice.from_files(..., use_cuda=True)`
|
|
107
|
-
self.piper_voice = PiperVoice.from_files(
|
|
108
|
-
onnx_path=str(voice_model_path_onnx),
|
|
109
|
-
config_path=str(voice_model_path_json)
|
|
110
|
-
# use_cuda=True # if onnxruntime-gpu is installed and desired
|
|
111
|
-
)
|
|
112
|
-
with open(voice_model_path_json, 'r', encoding='utf-8') as f:
|
|
113
|
-
self.voice_config = json.load(f)
|
|
114
|
-
|
|
115
|
-
self.current_voice_model_path = voice_model_path_onnx
|
|
116
|
-
ASCIIColors.green(f"Piper voice '{voice_model_path_onnx.name}' loaded successfully.")
|
|
117
|
-
except Exception as e:
|
|
118
|
-
self.piper_voice = None
|
|
119
|
-
self.current_voice_model_path = None
|
|
120
|
-
self.voice_config = None
|
|
121
|
-
ASCIIColors.error(f"Failed to load Piper voice '{voice_model_path_onnx.name}': {e}"); trace_exception(e)
|
|
122
|
-
raise RuntimeError(f"Failed to load Piper voice '{voice_model_path_onnx.name}'") from e
|
|
123
|
-
|
|
124
|
-
def generate_audio(self,
|
|
125
|
-
text: str,
|
|
126
|
-
voice: Optional[Union[str, Path]] = None, # Filename or path to .onnx
|
|
127
|
-
**kwargs) -> bytes: # kwargs can include Piper synthesis options
|
|
128
|
-
if voice:
|
|
129
|
-
try:
|
|
130
|
-
self._load_piper_voice(voice) # Attempt to switch voice
|
|
131
|
-
except Exception as e_load:
|
|
132
|
-
ASCIIColors.error(f"Failed to switch to Piper voice '{voice}': {e_load}. Using previously loaded voice if available.")
|
|
133
|
-
if not self.piper_voice: # If no voice was previously loaded either
|
|
134
|
-
raise RuntimeError("No Piper voice loaded and failed to switch.") from e_load
|
|
135
|
-
|
|
136
|
-
if not self.piper_voice or not self.voice_config:
|
|
137
|
-
raise RuntimeError("Piper voice model not loaded. Cannot generate audio.")
|
|
138
|
-
|
|
139
|
-
ASCIIColors.info(f"Generating speech with Piper voice '{self.current_voice_model_path.name}': '{text[:60]}...'")
|
|
3
|
+
from typing import Optional, List
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import requests
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
import pipmaster as pm
|
|
10
|
+
|
|
11
|
+
BindingName = "PiperClientBinding"
|
|
12
|
+
|
|
13
|
+
class PiperClientBinding(LollmsTTSBinding):
|
|
14
|
+
def __init__(self,
|
|
15
|
+
host: str = "localhost",
|
|
16
|
+
port: int = 8083,
|
|
17
|
+
auto_start_server: bool = True,
|
|
18
|
+
**kwargs):
|
|
140
19
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
return wav_bytes
|
|
178
|
-
except Exception as e:
|
|
179
|
-
ASCIIColors.error(f"Piper TTS audio generation failed: {e}"); trace_exception(e)
|
|
180
|
-
raise RuntimeError(f"Piper TTS audio generation error: {e}") from e
|
|
181
|
-
|
|
182
|
-
def list_voices(self, **kwargs) -> List[str]:
|
|
183
|
-
"""
|
|
184
|
-
Lists available Piper voice models found in the piper_voices_dir.
|
|
185
|
-
Returns a list of .onnx filenames.
|
|
186
|
-
"""
|
|
187
|
-
voices = []
|
|
188
|
-
if self.piper_voices_dir and self.piper_voices_dir.is_dir():
|
|
189
|
-
for item in self.piper_voices_dir.iterdir():
|
|
190
|
-
if item.is_file() and item.suffix == ".onnx":
|
|
191
|
-
json_config_path = item.with_suffix(".onnx.json")
|
|
192
|
-
if json_config_path.exists():
|
|
193
|
-
voices.append(item.name) # Return just the filename
|
|
20
|
+
binding_name = "piper"
|
|
21
|
+
super().__init__(binding_name=binding_name, **kwargs)
|
|
22
|
+
self.host = host
|
|
23
|
+
self.port = port
|
|
24
|
+
self.auto_start_server = auto_start_server
|
|
25
|
+
self.server_process = None
|
|
26
|
+
self.base_url = f"http://{self.host}:{self.port}"
|
|
27
|
+
|
|
28
|
+
if self.auto_start_server:
|
|
29
|
+
self.start_server()
|
|
30
|
+
|
|
31
|
+
def start_server(self):
|
|
32
|
+
print("Piper Client: Starting dedicated server...")
|
|
33
|
+
binding_root = Path(__file__).parent
|
|
34
|
+
server_dir = binding_root / "server"
|
|
35
|
+
requirements_file = server_dir / "requirements.txt"
|
|
36
|
+
server_script = server_dir / "main.py"
|
|
37
|
+
|
|
38
|
+
# 1. Ensure a virtual environment and dependencies
|
|
39
|
+
venv_path = server_dir / "venv"
|
|
40
|
+
pm_v = pm.PackageManager(venv_path=venv_path)
|
|
41
|
+
pm_v.ensure_requirements(str(requirements_file), verbose=True)
|
|
42
|
+
|
|
43
|
+
# 2. Get the python executable from the venv
|
|
44
|
+
if sys.platform == "win32":
|
|
45
|
+
python_executable = venv_path / "Scripts" / "python.exe"
|
|
46
|
+
else:
|
|
47
|
+
python_executable = venv_path / "bin" / "python"
|
|
48
|
+
|
|
49
|
+
# 3. Launch the server as a subprocess with stdout/stderr forwarded to console
|
|
50
|
+
command = [
|
|
51
|
+
str(python_executable),
|
|
52
|
+
str(server_script),
|
|
53
|
+
"--host", self.host,
|
|
54
|
+
"--port", str(self.port)
|
|
55
|
+
]
|
|
194
56
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
voices.append(self.current_voice_model_path.name) # Add the default loaded one if dir is empty
|
|
201
|
-
|
|
202
|
-
return sorted(list(set(voices))) # Ensure unique and sorted
|
|
203
|
-
|
|
204
|
-
def __del__(self):
|
|
205
|
-
# PiperVoice objects don't have an explicit close/del, Python's GC should handle C extensions
|
|
206
|
-
if hasattr(self, 'piper_voice') and self.piper_voice is not None:
|
|
207
|
-
del self.piper_voice
|
|
208
|
-
self.piper_voice = None
|
|
209
|
-
ASCIIColors.info(f"PiperTTSBinding voice '{getattr(self, 'current_voice_model_path', 'N/A')}' resources released.")
|
|
210
|
-
|
|
211
|
-
# --- Main Test Block ---
|
|
212
|
-
if __name__ == '__main__':
|
|
213
|
-
if not _piper_tts_installed:
|
|
214
|
-
print(f"{ASCIIColors.RED}Piper TTS binding dependencies not met. Skipping tests. Error: {_piper_tts_installation_error}{ASCIIColors.RESET}")
|
|
215
|
-
exit()
|
|
216
|
-
|
|
217
|
-
ASCIIColors.yellow("--- PiperTTSBinding Test ---")
|
|
218
|
-
|
|
219
|
-
# --- USER CONFIGURATION FOR TEST ---
|
|
220
|
-
# 1. Create a directory to store Piper voices, e.g., "./test_piper_voices"
|
|
221
|
-
TEST_PIPER_VOICES_DIR = Path("./test_piper_voices")
|
|
222
|
-
TEST_PIPER_VOICES_DIR.mkdir(exist_ok=True)
|
|
223
|
-
|
|
224
|
-
# 2. Download at least one voice model (ONNX + JSON files) into that directory.
|
|
225
|
-
# From: https://rhasspy.github.io/piper-voices/
|
|
226
|
-
# Example: Download en_US-lessac-medium.onnx and en_US-lessac-medium.onnx.json
|
|
227
|
-
# and place them in TEST_PIPER_VOICES_DIR
|
|
228
|
-
# Or find direct links on Hugging Face: e.g., from https://huggingface.co/rhasspy/piper-voices/tree/main/en/en_US/lessac/medium
|
|
229
|
-
# Let's pick a common English voice for testing.
|
|
230
|
-
DEFAULT_TEST_VOICE_FILENAME = "en_US-lessac-medium.onnx" # Ensure this (and .json) is in TEST_PIPER_VOICES_DIR
|
|
231
|
-
DEFAULT_TEST_VOICE_ONNX_URL = f"{PIPER_VOICES_BASE_URL}en/en_US/lessac/medium/en_US-lessac-medium.onnx"
|
|
232
|
-
DEFAULT_TEST_VOICE_JSON_URL = f"{PIPER_VOICES_BASE_URL}en/en_US/lessac/medium/en_US-lessac-medium.onnx.json"
|
|
233
|
-
|
|
234
|
-
# Function to download test voice if missing
|
|
235
|
-
def ensure_test_voice(voices_dir: Path, voice_filename: str, onnx_url: str, json_url: str):
|
|
236
|
-
onnx_path = voices_dir / voice_filename
|
|
237
|
-
json_path = voices_dir / f"{voice_filename}.json"
|
|
238
|
-
if not onnx_path.exists() or not json_path.exists():
|
|
239
|
-
ASCIIColors.info(f"Test voice '{voice_filename}' not found. Attempting to download...")
|
|
240
|
-
try:
|
|
241
|
-
import requests
|
|
242
|
-
# Download ONNX
|
|
243
|
-
if not onnx_path.exists():
|
|
244
|
-
ASCIIColors.info(f"Downloading {onnx_url} to {onnx_path}")
|
|
245
|
-
r_onnx = requests.get(onnx_url, stream=True)
|
|
246
|
-
r_onnx.raise_for_status()
|
|
247
|
-
with open(onnx_path, 'wb') as f:
|
|
248
|
-
for chunk in r_onnx.iter_content(chunk_size=8192): f.write(chunk)
|
|
249
|
-
# Download JSON
|
|
250
|
-
if not json_path.exists():
|
|
251
|
-
ASCIIColors.info(f"Downloading {json_url} to {json_path}")
|
|
252
|
-
r_json = requests.get(json_url)
|
|
253
|
-
r_json.raise_for_status()
|
|
254
|
-
with open(json_path, 'w', encoding='utf-8') as f: f.write(r_json.text)
|
|
255
|
-
ASCIIColors.green(f"Test voice '{voice_filename}' downloaded successfully.")
|
|
256
|
-
except Exception as e_download:
|
|
257
|
-
ASCIIColors.error(f"Failed to download test voice '{voice_filename}': {e_download}")
|
|
258
|
-
ASCIIColors.warning(f"Please manually download '{voice_filename}' and '{voice_filename}.json' "
|
|
259
|
-
f"from {PIPER_VOICES_BASE_URL} (or rhasspy.github.io/piper-voices/) "
|
|
260
|
-
f"and place them in {voices_dir.resolve()}")
|
|
261
|
-
return False
|
|
262
|
-
return True
|
|
263
|
-
|
|
264
|
-
if not ensure_test_voice(TEST_PIPER_VOICES_DIR, DEFAULT_TEST_VOICE_FILENAME, DEFAULT_TEST_VOICE_ONNX_URL, DEFAULT_TEST_VOICE_JSON_URL):
|
|
265
|
-
ASCIIColors.error("Cannot proceed with test without a default voice model.")
|
|
266
|
-
exit(1)
|
|
267
|
-
|
|
268
|
-
# Optional: Download a second voice for testing voice switching
|
|
269
|
-
SECOND_TEST_VOICE_FILENAME = "de_DE-thorsten-medium.onnx" # Example German voice
|
|
270
|
-
SECOND_TEST_VOICE_ONNX_URL = f"{PIPER_VOICES_BASE_URL}de/de_DE/thorsten/medium/de_DE-thorsten-medium.onnx"
|
|
271
|
-
SECOND_TEST_VOICE_JSON_URL = f"{PIPER_VOICES_BASE_URL}de/de_DE/thorsten/medium/de_DE-thorsten-medium.onnx.json"
|
|
272
|
-
ensure_test_voice(TEST_PIPER_VOICES_DIR, SECOND_TEST_VOICE_FILENAME, SECOND_TEST_VOICE_ONNX_URL, SECOND_TEST_VOICE_JSON_URL)
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
test_output_dir = Path("./test_piper_tts_output")
|
|
276
|
-
test_output_dir.mkdir(exist_ok=True)
|
|
277
|
-
tts_binding = None
|
|
278
|
-
# --- END USER CONFIGURATION FOR TEST ---
|
|
279
|
-
|
|
280
|
-
try:
|
|
281
|
-
ASCIIColors.cyan(f"\n--- Initializing PiperTTSBinding ---")
|
|
282
|
-
# Initialize with the path to the ONNX file for the default voice
|
|
283
|
-
tts_binding = PiperTTSBinding(
|
|
284
|
-
default_voice_model_path = TEST_PIPER_VOICES_DIR / DEFAULT_TEST_VOICE_FILENAME,
|
|
285
|
-
piper_voices_dir = TEST_PIPER_VOICES_DIR
|
|
57
|
+
# Forward stdout and stderr to the parent process console
|
|
58
|
+
self.server_process = subprocess.Popen(
|
|
59
|
+
command,
|
|
60
|
+
stdout=None, # Inherit parent's stdout (shows in console)
|
|
61
|
+
stderr=None, # Inherit parent's stderr (shows in console)
|
|
286
62
|
)
|
|
63
|
+
|
|
64
|
+
# 4. Wait for the server to be ready
|
|
65
|
+
self._wait_for_server()
|
|
287
66
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
texts_to_synthesize = [
|
|
295
|
-
("english_hello", "Hello world, this is a test of the Piper text to speech binding."),
|
|
296
|
-
("english_question", "Can you generate speech quickly and efficiently? Let's find out!"),
|
|
297
|
-
]
|
|
298
|
-
if (TEST_PIPER_VOICES_DIR / SECOND_TEST_VOICE_FILENAME).exists():
|
|
299
|
-
texts_to_synthesize.append(
|
|
300
|
-
("german_greeting", "Hallo Welt, wie geht es Ihnen heute?", SECOND_TEST_VOICE_FILENAME)
|
|
301
|
-
)
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
for name, text, *voice_file_arg in texts_to_synthesize:
|
|
305
|
-
voice_to_use_filename = voice_file_arg[0] if voice_file_arg else None # Filename like "en_US-lessac-medium.onnx"
|
|
306
|
-
|
|
307
|
-
ASCIIColors.cyan(f"\n--- Synthesizing TTS for: '{name}' (Voice file: {voice_to_use_filename or DEFAULT_TEST_VOICE_FILENAME}) ---")
|
|
308
|
-
print(f"Text: {text}")
|
|
67
|
+
def _wait_for_server(self, timeout=60): # Piper is fast to load
|
|
68
|
+
start_time = time.time()
|
|
69
|
+
print("Piper Client: Waiting for server to initialize...")
|
|
70
|
+
while time.time() - start_time < timeout:
|
|
309
71
|
try:
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
print(f"{ASCIIColors.YELLOW}Check the audio files in '{test_output_dir.resolve()}'!{ASCIIColors.RESET}")
|
|
332
|
-
# Optional: Clean up downloaded test voices
|
|
333
|
-
# if input("Clean up downloaded test voices? (y/N): ").lower() == 'y':
|
|
334
|
-
# for f_name in [DEFAULT_TEST_VOICE_FILENAME, SECOND_TEST_VOICE_FILENAME]:
|
|
335
|
-
# onnx_p = TEST_PIPER_VOICES_DIR / f_name
|
|
336
|
-
# json_p = TEST_PIPER_VOICES_DIR / f"{f_name}.json"
|
|
337
|
-
# if onnx_p.exists(): onnx_p.unlink()
|
|
338
|
-
# if json_p.exists(): json_p.unlink()
|
|
339
|
-
# if not any(TEST_PIPER_VOICES_DIR.iterdir()): TEST_PIPER_VOICES_DIR.rmdir()
|
|
340
|
-
# ASCIIColors.info("Cleaned up test voices.")
|
|
72
|
+
response = requests.get(f"{self.base_url}/status")
|
|
73
|
+
if response.status_code == 200 and response.json().get("status") == "running":
|
|
74
|
+
print("Piper Server is up and running.")
|
|
75
|
+
return
|
|
76
|
+
except requests.ConnectionError:
|
|
77
|
+
time.sleep(1)
|
|
78
|
+
|
|
79
|
+
self.stop_server()
|
|
80
|
+
raise RuntimeError("Failed to start the Piper server in the specified timeout.")
|
|
81
|
+
|
|
82
|
+
def stop_server(self):
|
|
83
|
+
if self.server_process:
|
|
84
|
+
print("Piper Client: Stopping dedicated server...")
|
|
85
|
+
self.server_process.terminate()
|
|
86
|
+
self.server_process.wait()
|
|
87
|
+
self.server_process = None
|
|
88
|
+
print("Server stopped.")
|
|
89
|
+
|
|
90
|
+
def __del__(self):
|
|
91
|
+
# Ensure the server is stopped when the object is destroyed
|
|
92
|
+
self.stop_server()
|
|
341
93
|
|
|
94
|
+
def generate_audio(self, text: str, voice: Optional[str] = None, **kwargs) -> bytes:
|
|
95
|
+
"""Generate audio by calling the server's API"""
|
|
96
|
+
payload = {"text": text, "voice": voice, **kwargs}
|
|
97
|
+
response = requests.post(f"{self.base_url}/generate_audio", json=payload, timeout=30)
|
|
98
|
+
response.raise_for_status()
|
|
99
|
+
return response.content
|
|
342
100
|
|
|
343
|
-
|
|
101
|
+
def list_voices(self, **kwargs) -> List[str]:
|
|
102
|
+
"""Get available voices from the server"""
|
|
103
|
+
response = requests.get(f"{self.base_url}/list_voices")
|
|
104
|
+
response.raise_for_status()
|
|
105
|
+
return response.json().get("voices", [])
|
|
106
|
+
|
|
107
|
+
def list_models(self, **kwargs) -> List[str]:
|
|
108
|
+
"""Get available models from the server"""
|
|
109
|
+
response = requests.get(f"{self.base_url}/list_models")
|
|
110
|
+
response.raise_for_status()
|
|
111
|
+
return response.json().get("models", [])
|
|
112
|
+
|
|
113
|
+
def download_voice(self, voice_name: str):
|
|
114
|
+
"""Download a specific voice model"""
|
|
115
|
+
response = requests.post(f"{self.base_url}/download_voice", json={"voice": voice_name})
|
|
116
|
+
response.raise_for_status()
|
|
117
|
+
return response.json()
|
|
118
|
+
|
|
119
|
+
def set_voice(self, voice: str):
|
|
120
|
+
"""Set the default voice for future generations"""
|
|
121
|
+
response = requests.post(f"{self.base_url}/set_voice", json={"voice": voice})
|
|
122
|
+
response.raise_for_status()
|
|
123
|
+
return response.json()
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# File: lollms_client/tts_bindings/piper/server/install_piper.py
|
|
2
|
+
#!/usr/bin/env python3
|
|
3
|
+
"""
|
|
4
|
+
Piper TTS installation script
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import os
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
def install_piper():
|
|
13
|
+
"""Install Piper TTS and dependencies"""
|
|
14
|
+
|
|
15
|
+
print("=== Piper TTS Installation ===")
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
print("Step 1: Installing Piper TTS...")
|
|
19
|
+
subprocess.check_call([
|
|
20
|
+
sys.executable, "-m", "pip", "install",
|
|
21
|
+
"piper-tts>=1.2.0"
|
|
22
|
+
])
|
|
23
|
+
|
|
24
|
+
print("Step 2: Installing audio processing dependencies...")
|
|
25
|
+
subprocess.check_call([
|
|
26
|
+
sys.executable, "-m", "pip", "install",
|
|
27
|
+
"soundfile>=0.12.1", "numpy>=1.21.0"
|
|
28
|
+
])
|
|
29
|
+
|
|
30
|
+
print("Step 3: Installing web server dependencies...")
|
|
31
|
+
subprocess.check_call([
|
|
32
|
+
sys.executable, "-m", "pip", "install",
|
|
33
|
+
"fastapi>=0.68.0", "uvicorn[standard]>=0.15.0", "pydantic>=1.8.0"
|
|
34
|
+
])
|
|
35
|
+
|
|
36
|
+
print("Step 4: Installing HTTP client dependencies...")
|
|
37
|
+
subprocess.check_call([
|
|
38
|
+
sys.executable, "-m", "pip", "install",
|
|
39
|
+
"requests>=2.25.0", "aiohttp>=3.8.0", "aiofiles>=0.7.0"
|
|
40
|
+
])
|
|
41
|
+
|
|
42
|
+
print("Step 5: Testing installation...")
|
|
43
|
+
|
|
44
|
+
# Test Piper import
|
|
45
|
+
import piper
|
|
46
|
+
print(f"✓ Piper imported successfully!")
|
|
47
|
+
|
|
48
|
+
# Create models directory
|
|
49
|
+
models_dir = Path(__file__).parent / "models"
|
|
50
|
+
models_dir.mkdir(exist_ok=True)
|
|
51
|
+
print(f"✓ Models directory created: {models_dir}")
|
|
52
|
+
|
|
53
|
+
print("Step 6: Testing voice synthesis...")
|
|
54
|
+
|
|
55
|
+
# We can't easily test actual synthesis here without downloading a model
|
|
56
|
+
# But we can test that the basic components work
|
|
57
|
+
print("✓ Piper TTS is ready to use!")
|
|
58
|
+
|
|
59
|
+
return True
|
|
60
|
+
|
|
61
|
+
except subprocess.CalledProcessError as e:
|
|
62
|
+
print(f"✗ Installation failed: {e}")
|
|
63
|
+
return False
|
|
64
|
+
except Exception as e:
|
|
65
|
+
print(f"✗ Unexpected error: {e}")
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
if __name__ == "__main__":
|
|
69
|
+
print("This script will install Piper TTS and its dependencies.")
|
|
70
|
+
print("Piper is lightweight and fast - installation should be quick.")
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
input("Press Enter to continue or Ctrl+C to cancel...")
|
|
74
|
+
except KeyboardInterrupt:
|
|
75
|
+
print("\nInstallation cancelled.")
|
|
76
|
+
sys.exit(0)
|
|
77
|
+
|
|
78
|
+
success = install_piper()
|
|
79
|
+
if success:
|
|
80
|
+
print("\n🎉 Piper TTS installation completed!")
|
|
81
|
+
print("✓ Lightweight and fast TTS")
|
|
82
|
+
print("✓ High-quality neural voices")
|
|
83
|
+
print("✓ 50+ languages supported")
|
|
84
|
+
print("\n📁 Voice models will be downloaded to: server/models/")
|
|
85
|
+
print("🚀 The server will automatically download a default English voice on first run.")
|
|
86
|
+
print("\nUsage tips:")
|
|
87
|
+
print("- Use download_voice() to get additional languages")
|
|
88
|
+
print("- Piper is very fast compared to other TTS engines")
|
|
89
|
+
print("- Models are small (20-40MB each)")
|
|
90
|
+
else:
|
|
91
|
+
print("\n❌ Installation failed. Please check the error messages above.")
|
|
92
|
+
sys.exit(1)
|