lollms-client 1.4.1__py3-none-any.whl → 1.7.10__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.
- lollms_client/__init__.py +1 -1
- lollms_client/llm_bindings/azure_openai/__init__.py +2 -2
- lollms_client/llm_bindings/claude/__init__.py +125 -34
- lollms_client/llm_bindings/gemini/__init__.py +261 -159
- lollms_client/llm_bindings/grok/__init__.py +52 -14
- lollms_client/llm_bindings/groq/__init__.py +2 -2
- lollms_client/llm_bindings/hugging_face_inference_api/__init__.py +2 -2
- lollms_client/llm_bindings/litellm/__init__.py +1 -1
- lollms_client/llm_bindings/llamacpp/__init__.py +18 -11
- lollms_client/llm_bindings/lollms/__init__.py +151 -32
- lollms_client/llm_bindings/lollms_webui/__init__.py +1 -1
- lollms_client/llm_bindings/mistral/__init__.py +2 -2
- lollms_client/llm_bindings/novita_ai/__init__.py +439 -0
- lollms_client/llm_bindings/ollama/__init__.py +309 -93
- lollms_client/llm_bindings/open_router/__init__.py +2 -2
- lollms_client/llm_bindings/openai/__init__.py +148 -29
- lollms_client/llm_bindings/openllm/__init__.py +362 -506
- lollms_client/llm_bindings/openwebui/__init__.py +465 -0
- lollms_client/llm_bindings/perplexity/__init__.py +326 -0
- lollms_client/llm_bindings/pythonllamacpp/__init__.py +3 -3
- lollms_client/llm_bindings/tensor_rt/__init__.py +1 -1
- lollms_client/llm_bindings/transformers/__init__.py +428 -632
- lollms_client/llm_bindings/vllm/__init__.py +1 -1
- lollms_client/lollms_agentic.py +4 -2
- lollms_client/lollms_base_binding.py +61 -0
- lollms_client/lollms_core.py +516 -1890
- lollms_client/lollms_discussion.py +55 -18
- lollms_client/lollms_llm_binding.py +112 -261
- lollms_client/lollms_mcp_binding.py +34 -75
- lollms_client/lollms_personality.py +5 -2
- lollms_client/lollms_stt_binding.py +85 -52
- lollms_client/lollms_tti_binding.py +23 -37
- lollms_client/lollms_ttm_binding.py +24 -42
- lollms_client/lollms_tts_binding.py +28 -17
- lollms_client/lollms_ttv_binding.py +24 -42
- lollms_client/lollms_types.py +4 -2
- lollms_client/stt_bindings/whisper/__init__.py +108 -23
- lollms_client/stt_bindings/whispercpp/__init__.py +7 -1
- lollms_client/tti_bindings/diffusers/__init__.py +418 -810
- lollms_client/tti_bindings/diffusers/server/main.py +1051 -0
- lollms_client/tti_bindings/gemini/__init__.py +182 -239
- lollms_client/tti_bindings/leonardo_ai/__init__.py +127 -0
- lollms_client/tti_bindings/lollms/__init__.py +4 -1
- lollms_client/tti_bindings/novita_ai/__init__.py +105 -0
- lollms_client/tti_bindings/openai/__init__.py +10 -11
- lollms_client/tti_bindings/stability_ai/__init__.py +178 -0
- lollms_client/ttm_bindings/audiocraft/__init__.py +7 -12
- lollms_client/ttm_bindings/beatoven_ai/__init__.py +129 -0
- lollms_client/ttm_bindings/lollms/__init__.py +4 -17
- lollms_client/ttm_bindings/replicate/__init__.py +115 -0
- lollms_client/ttm_bindings/stability_ai/__init__.py +117 -0
- lollms_client/ttm_bindings/topmediai/__init__.py +96 -0
- lollms_client/tts_bindings/bark/__init__.py +7 -10
- lollms_client/tts_bindings/lollms/__init__.py +6 -1
- lollms_client/tts_bindings/piper_tts/__init__.py +8 -11
- lollms_client/tts_bindings/xtts/__init__.py +157 -74
- lollms_client/tts_bindings/xtts/server/main.py +241 -280
- {lollms_client-1.4.1.dist-info → lollms_client-1.7.10.dist-info}/METADATA +316 -6
- lollms_client-1.7.10.dist-info/RECORD +89 -0
- lollms_client/ttm_bindings/bark/__init__.py +0 -339
- lollms_client-1.4.1.dist-info/RECORD +0 -78
- {lollms_client-1.4.1.dist-info → lollms_client-1.7.10.dist-info}/WHEEL +0 -0
- {lollms_client-1.4.1.dist-info → lollms_client-1.7.10.dist-info}/licenses/LICENSE +0 -0
- {lollms_client-1.4.1.dist-info → lollms_client-1.7.10.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import requests
|
|
3
|
+
import time
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional, List, Dict, Any
|
|
6
|
+
|
|
7
|
+
from lollms_client.lollms_ttm_binding import LollmsTTMBinding
|
|
8
|
+
from ascii_colors import trace_exception, ASCIIColors
|
|
9
|
+
import pipmaster as pm
|
|
10
|
+
|
|
11
|
+
# Ensure required packages are installed
|
|
12
|
+
pm.ensure_packages(["requests"])
|
|
13
|
+
|
|
14
|
+
BindingName = "ReplicateTTMBinding"
|
|
15
|
+
|
|
16
|
+
# Popular music models available on Replicate
|
|
17
|
+
# Sourced from: https://replicate.com/collections/text-to-music
|
|
18
|
+
REPLICATE_MODELS = [
|
|
19
|
+
{"model_name": "meta/musicgen:b05b1dff1d8c6ac63d42422dd565e23b63869bf2d51acda751e04b5dd304535d", "display_name": "Meta - MusicGen", "description": "State-of-the-art controllable text-to-music model from Meta."},
|
|
20
|
+
{"model_name": "suno-ai/bark:b76242b40d67c76ab6742e987628a2a9ac019e11d56ab96c4e91ce03b79b2787", "display_name": "Suno - Bark", "description": "Text-to-audio model capable of music, voice, and sound effects."},
|
|
21
|
+
{"model_name": "joehoover/musicgen-melody:7a76a8258b23fae65c5a24debbe88414f9bed22c2422a63465731103f6990803", "display_name": "MusicGen Melody", "description": "MusicGen fine-tuned for generating melodies."},
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
class ReplicateTTMBinding(LollmsTTMBinding):
|
|
25
|
+
"""A Text-to-Music binding for models hosted on Replicate."""
|
|
26
|
+
|
|
27
|
+
def __init__(self,
|
|
28
|
+
**kwargs):
|
|
29
|
+
# Prioritize 'model_name' but accept 'model' as an alias from config files.
|
|
30
|
+
if 'model' in kwargs and 'model_name' not in kwargs:
|
|
31
|
+
kwargs['model_name'] = kwargs.pop('model')
|
|
32
|
+
self.api_key = self.config.get("api_key") or os.environ.get("REPLICATE_API_TOKEN")
|
|
33
|
+
if not self.api_key:
|
|
34
|
+
raise ValueError("Replicate API token is required. Please set it in config or as REPLICATE_API_TOKEN env var.")
|
|
35
|
+
self.model_version = self.config.get("model_name", "meta/musicgen:b05b1dff1d8c6ac63d42422dd565e23b63869bf2d51acda751e04b5dd304535d")
|
|
36
|
+
self.base_url = "https://api.replicate.com/v1"
|
|
37
|
+
self.headers = {"Authorization": f"Token {self.api_key}", "Content-Type": "application/json"}
|
|
38
|
+
|
|
39
|
+
def list_models(self, **kwargs) -> List[Dict[str, str]]:
|
|
40
|
+
return REPLICATE_MODELS
|
|
41
|
+
|
|
42
|
+
def generate_music(self, prompt: str, **kwargs) -> bytes:
|
|
43
|
+
"""
|
|
44
|
+
Generates music via Replicate by starting a prediction and polling for the result.
|
|
45
|
+
"""
|
|
46
|
+
model_id, version_id = self.model_version.split(":")
|
|
47
|
+
|
|
48
|
+
payload = {
|
|
49
|
+
"version": version_id,
|
|
50
|
+
"input": {
|
|
51
|
+
"prompt": prompt,
|
|
52
|
+
"duration": kwargs.get("duration", 8),
|
|
53
|
+
"temperature": kwargs.get("temperature", 1.0),
|
|
54
|
+
"top_p": kwargs.get("top_p", 0.9),
|
|
55
|
+
# Add other model-specific parameters here
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
# 1. Start the prediction
|
|
61
|
+
ASCIIColors.info(f"Submitting music generation job to Replicate ({model_id})...")
|
|
62
|
+
start_response = requests.post(f"{self.base_url}/predictions", json=payload, headers=self.headers)
|
|
63
|
+
start_response.raise_for_status()
|
|
64
|
+
job_data = start_response.json()
|
|
65
|
+
get_url = job_data["urls"]["get"]
|
|
66
|
+
ASCIIColors.info(f"Job submitted. Polling for results at: {get_url}")
|
|
67
|
+
|
|
68
|
+
# 2. Poll for the result
|
|
69
|
+
while True:
|
|
70
|
+
poll_response = requests.get(get_url, headers=self.headers)
|
|
71
|
+
poll_response.raise_for_status()
|
|
72
|
+
poll_data = poll_response.json()
|
|
73
|
+
status = poll_data["status"]
|
|
74
|
+
|
|
75
|
+
if status == "succeeded":
|
|
76
|
+
ASCIIColors.green("Generation successful!")
|
|
77
|
+
output_url = poll_data["output"]
|
|
78
|
+
# Download the resulting audio file
|
|
79
|
+
audio_response = requests.get(output_url)
|
|
80
|
+
audio_response.raise_for_status()
|
|
81
|
+
return audio_response.content
|
|
82
|
+
elif status in ["starting", "processing"]:
|
|
83
|
+
ASCIIColors.info(f"Job status: {status}. Waiting...")
|
|
84
|
+
time.sleep(3)
|
|
85
|
+
else: # failed, canceled
|
|
86
|
+
error_log = poll_data.get("logs", "No logs available.")
|
|
87
|
+
raise Exception(f"Replicate job failed with status '{status}'. Log: {error_log}")
|
|
88
|
+
|
|
89
|
+
except requests.exceptions.HTTPError as e:
|
|
90
|
+
error_details = e.response.json().get("detail", e.response.text)
|
|
91
|
+
raise Exception(f"Replicate API HTTP Error: {error_details}") from e
|
|
92
|
+
except Exception as e:
|
|
93
|
+
trace_exception(e)
|
|
94
|
+
raise
|
|
95
|
+
|
|
96
|
+
if __name__ == '__main__':
|
|
97
|
+
ASCIIColors.magenta("--- Replicate TTM Binding Test ---")
|
|
98
|
+
if "REPLICATE_API_TOKEN" not in os.environ:
|
|
99
|
+
ASCIIColors.error("REPLICATE_API_TOKEN environment variable not set. Cannot run test.")
|
|
100
|
+
exit(1)
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
binding = ReplicateTTMBinding()
|
|
104
|
+
|
|
105
|
+
ASCIIColors.cyan("\n--- Test: Music Generation with MusicGen ---")
|
|
106
|
+
prompt = "An epic cinematic orchestral piece, with soaring strings and dramatic percussion, fit for a movie trailer"
|
|
107
|
+
music_bytes = binding.generate_music(prompt, duration=10)
|
|
108
|
+
|
|
109
|
+
assert len(music_bytes) > 1000, "Generated music bytes are too small."
|
|
110
|
+
output_path = Path(__file__).parent / "tmp_replicate_music.wav"
|
|
111
|
+
with open(output_path, "wb") as f:
|
|
112
|
+
f.write(music_bytes)
|
|
113
|
+
ASCIIColors.green(f"Music generation OK. Audio saved to {output_path}")
|
|
114
|
+
|
|
115
|
+
except Exception as e:
|
|
116
|
+
trace_exception(e)
|
|
117
|
+
ASCIIColors.error(f"Replicate TTM binding test failed: {e}")
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import requests
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional, List, Dict, Any
|
|
5
|
+
|
|
6
|
+
from lollms_client.lollms_ttm_binding import LollmsTTMBinding
|
|
7
|
+
from ascii_colors import trace_exception, ASCIIColors
|
|
8
|
+
import pipmaster as pm
|
|
9
|
+
|
|
10
|
+
# Ensure required packages are installed
|
|
11
|
+
pm.ensure_packages(["requests"])
|
|
12
|
+
|
|
13
|
+
BindingName = "TopMediaiTTMBinding"
|
|
14
|
+
|
|
15
|
+
class TopMediaiTTMBinding(LollmsTTMBinding):
|
|
16
|
+
"""A Text-to-Music binding for the TopMediai API."""
|
|
17
|
+
|
|
18
|
+
def __init__(self,
|
|
19
|
+
**kwargs):
|
|
20
|
+
# Prioritize 'model_name' but accept 'model' as an alias from config files.
|
|
21
|
+
if 'model' in kwargs and 'model_name' not in kwargs:
|
|
22
|
+
kwargs['model_name'] = kwargs.pop('model')
|
|
23
|
+
self.api_key = self.config.get("api_key") or os.environ.get("TOPMEDIAI_API_KEY")
|
|
24
|
+
if not self.api_key:
|
|
25
|
+
raise ValueError("TopMediai API key is required. Please set it in config or as TOPMEDIAI_API_KEY env var.")
|
|
26
|
+
self.base_url = "https://api.topmediai.com/v1"
|
|
27
|
+
self.headers = {"x-api-key": self.api_key, "Content-Type": "application/json"}
|
|
28
|
+
|
|
29
|
+
def list_models(self, **kwargs) -> List[str]:
|
|
30
|
+
# The API does not provide a list of selectable models.
|
|
31
|
+
# It's a single, prompt-based system.
|
|
32
|
+
return ["default"]
|
|
33
|
+
|
|
34
|
+
def generate_music(self, prompt: str, **kwargs) -> bytes:
|
|
35
|
+
"""
|
|
36
|
+
Generates music using the TopMediai synchronous API.
|
|
37
|
+
"""
|
|
38
|
+
url = f"{self.base_url}/music"
|
|
39
|
+
duration = kwargs.get("duration", 30)
|
|
40
|
+
|
|
41
|
+
payload = {
|
|
42
|
+
"text": prompt,
|
|
43
|
+
"duration": f"{duration}", # API expects duration as a string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
ASCIIColors.info("Requesting music from TopMediai...")
|
|
48
|
+
response = requests.post(url, json=payload, headers=self.headers)
|
|
49
|
+
response.raise_for_status()
|
|
50
|
+
data = response.json()
|
|
51
|
+
|
|
52
|
+
if data.get("code") != 0:
|
|
53
|
+
raise Exception(f"TopMediai API returned an error: {data.get('message', 'Unknown error')}")
|
|
54
|
+
|
|
55
|
+
audio_url = data.get("data", {}).get("music_url")
|
|
56
|
+
if not audio_url:
|
|
57
|
+
raise Exception("API response did not contain a music URL.")
|
|
58
|
+
|
|
59
|
+
ASCIIColors.info(f"Downloading generated audio from {audio_url}")
|
|
60
|
+
audio_response = requests.get(audio_url)
|
|
61
|
+
audio_response.raise_for_status()
|
|
62
|
+
|
|
63
|
+
return audio_response.content
|
|
64
|
+
|
|
65
|
+
except requests.exceptions.HTTPError as e:
|
|
66
|
+
try:
|
|
67
|
+
error_details = e.response.json()
|
|
68
|
+
raise Exception(f"TopMediai API HTTP Error: {error_details}") from e
|
|
69
|
+
except:
|
|
70
|
+
raise Exception(f"TopMediai API HTTP Error: {e.response.text}") from e
|
|
71
|
+
except Exception as e:
|
|
72
|
+
trace_exception(e)
|
|
73
|
+
raise
|
|
74
|
+
|
|
75
|
+
if __name__ == '__main__':
|
|
76
|
+
ASCIIColors.magenta("--- TopMediai TTM Binding Test ---")
|
|
77
|
+
if "TOPMEDIAI_API_KEY" not in os.environ:
|
|
78
|
+
ASCIIColors.error("TOPMEDIAI_API_KEY environment variable not set. Cannot run test.")
|
|
79
|
+
exit(1)
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
binding = TopMediaiTTMBinding()
|
|
83
|
+
|
|
84
|
+
ASCIIColors.cyan("\n--- Test: Music Generation ---")
|
|
85
|
+
prompt = "lo-fi hip hop beat, chill, relaxing, perfect for studying"
|
|
86
|
+
music_bytes = binding.generate_music(prompt, duration=30)
|
|
87
|
+
|
|
88
|
+
assert len(music_bytes) > 1000, "Generated music bytes are too small."
|
|
89
|
+
output_path = Path(__file__).parent / "tmp_topmediai_music.mp3"
|
|
90
|
+
with open(output_path, "wb") as f:
|
|
91
|
+
f.write(music_bytes)
|
|
92
|
+
ASCIIColors.green(f"Music generation OK. Audio saved to {output_path}")
|
|
93
|
+
|
|
94
|
+
except Exception as e:
|
|
95
|
+
trace_exception(e)
|
|
96
|
+
ASCIIColors.error(f"TopMediai TTM binding test failed: {e}")
|
|
@@ -11,17 +11,14 @@ import pipmaster as pm
|
|
|
11
11
|
BindingName = "BarkClientBinding"
|
|
12
12
|
|
|
13
13
|
class BarkClientBinding(LollmsTTSBinding):
|
|
14
|
-
def __init__(self,
|
|
15
|
-
host: str = "localhost",
|
|
16
|
-
port: int = 8082,
|
|
17
|
-
auto_start_server: bool = True,
|
|
14
|
+
def __init__(self,
|
|
18
15
|
**kwargs):
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
self.host = host
|
|
23
|
-
self.port = port
|
|
24
|
-
self.auto_start_server = auto_start_server
|
|
16
|
+
# Prioritize 'model_name' but accept 'model' as an alias from config files.
|
|
17
|
+
if 'model' in kwargs and 'model_name' not in kwargs:
|
|
18
|
+
kwargs['model_name'] = kwargs.pop('model')
|
|
19
|
+
self.host = self.config.get("host", "http://localhost")
|
|
20
|
+
self.port = self.config.get("port", 9632)
|
|
21
|
+
self.auto_start_server = self.config.get("auto_start_server", True)
|
|
25
22
|
self.server_process = None
|
|
26
23
|
self.base_url = f"http://{self.host}:{self.port}"
|
|
27
24
|
|
|
@@ -29,6 +29,7 @@ class LollmsTTSBinding_Impl(LollmsTTSBinding):
|
|
|
29
29
|
model_name=model_name,
|
|
30
30
|
service_key=service_key, # Stored in the parent class
|
|
31
31
|
verify_ssl_certificate=verify_ssl_certificate)
|
|
32
|
+
self.host_address = host_address
|
|
32
33
|
# self.client_id = service_key # Can access via self.service_key from parent
|
|
33
34
|
|
|
34
35
|
def generate_audio(self, text: str, voice: Optional[str] = None, **kwargs) -> bytes:
|
|
@@ -142,4 +143,8 @@ class LollmsTTSBinding_Impl(LollmsTTSBinding):
|
|
|
142
143
|
except Exception as e:
|
|
143
144
|
ASCIIColors.error(f"An unexpected error occurred while listing voices: {e}")
|
|
144
145
|
trace_exception(e)
|
|
145
|
-
return ["main_voice"]
|
|
146
|
+
return ["main_voice"]
|
|
147
|
+
|
|
148
|
+
def list_models(self) -> list:
|
|
149
|
+
"""Lists models"""
|
|
150
|
+
return ["lollms"]
|
|
@@ -11,17 +11,14 @@ import pipmaster as pm
|
|
|
11
11
|
BindingName = "PiperClientBinding"
|
|
12
12
|
|
|
13
13
|
class PiperClientBinding(LollmsTTSBinding):
|
|
14
|
-
def __init__(self,
|
|
15
|
-
host: str = "localhost",
|
|
16
|
-
port: int = 8083,
|
|
17
|
-
auto_start_server: bool = True,
|
|
14
|
+
def __init__(self,
|
|
18
15
|
**kwargs):
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
self.host = host
|
|
23
|
-
self.port = port
|
|
24
|
-
self.auto_start_server = auto_start_server
|
|
16
|
+
# Prioritize 'model_name' but accept 'model' as an alias from config files.
|
|
17
|
+
if 'model' in kwargs and 'model_name' not in kwargs:
|
|
18
|
+
kwargs['model_name'] = kwargs.pop('model')
|
|
19
|
+
self.host = self.config.get("host", "http://localhost")
|
|
20
|
+
self.port = self.config.get("port", 9632)
|
|
21
|
+
self.auto_start_server = self.config.get("auto_start_server", True)
|
|
25
22
|
self.server_process = None
|
|
26
23
|
self.base_url = f"http://{self.host}:{self.port}"
|
|
27
24
|
|
|
@@ -104,7 +101,7 @@ class PiperClientBinding(LollmsTTSBinding):
|
|
|
104
101
|
response.raise_for_status()
|
|
105
102
|
return response.json().get("voices", [])
|
|
106
103
|
|
|
107
|
-
def list_models(self
|
|
104
|
+
def list_models(self) -> List[str]:
|
|
108
105
|
"""Get available models from the server"""
|
|
109
106
|
response = requests.get(f"{self.base_url}/list_models")
|
|
110
107
|
response.raise_for_status()
|
|
@@ -1,52 +1,132 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
from typing import Optional, List
|
|
4
|
-
from pathlib import Path
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
5
3
|
import requests
|
|
6
4
|
import subprocess
|
|
7
|
-
import sys
|
|
8
5
|
import time
|
|
9
|
-
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional, List
|
|
8
|
+
|
|
9
|
+
# Ensure filelock is available for process-safe server startup.
|
|
10
|
+
try:
|
|
11
|
+
from filelock import FileLock, Timeout
|
|
12
|
+
except ImportError:
|
|
13
|
+
print("FATAL: The 'filelock' library is required. Please install it by running: pip install filelock")
|
|
14
|
+
sys.exit(1)
|
|
15
|
+
|
|
16
|
+
from lollms_client.lollms_tts_binding import LollmsTTSBinding
|
|
17
|
+
from ascii_colors import ASCIIColors
|
|
10
18
|
|
|
11
19
|
BindingName = "XTTSClientBinding"
|
|
12
20
|
|
|
13
21
|
class XTTSClientBinding(LollmsTTSBinding):
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
22
|
+
"""
|
|
23
|
+
Client binding for a dedicated, managed XTTS server.
|
|
24
|
+
This architecture prevents the heavy XTTS model from being loaded into memory
|
|
25
|
+
by multiple worker processes, solving potential OOM errors and speeding up TTS generation.
|
|
26
|
+
"""
|
|
27
|
+
def __init__(self,
|
|
18
28
|
**kwargs):
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
self.
|
|
24
|
-
self.
|
|
29
|
+
# Prioritize 'model_name' but accept 'model' as an alias from config files.
|
|
30
|
+
if 'model' in kwargs and 'model_name' not in kwargs:
|
|
31
|
+
kwargs['model_name'] = kwargs.pop('model')
|
|
32
|
+
|
|
33
|
+
self.config = kwargs
|
|
34
|
+
self.host = kwargs.get("host", "localhost")
|
|
35
|
+
self.port = kwargs.get("port", 9633)
|
|
36
|
+
self.auto_start_server = kwargs.get("auto_start_server", True)
|
|
25
37
|
self.server_process = None
|
|
26
38
|
self.base_url = f"http://{self.host}:{self.port}"
|
|
39
|
+
self.binding_root = Path(__file__).parent
|
|
40
|
+
self.server_dir = self.binding_root / "server"
|
|
41
|
+
self.venv_dir = Path("./venv/tts_xtts_venv")
|
|
27
42
|
|
|
28
43
|
if self.auto_start_server:
|
|
29
|
-
self.
|
|
44
|
+
self.ensure_server_is_running()
|
|
45
|
+
|
|
46
|
+
def is_server_running(self) -> bool:
|
|
47
|
+
"""Checks if the server is already running and responsive."""
|
|
48
|
+
try:
|
|
49
|
+
response = requests.get(f"{self.base_url}/status", timeout=2)
|
|
50
|
+
if response.status_code == 200 and response.json().get("status") == "running":
|
|
51
|
+
return True
|
|
52
|
+
except requests.exceptions.RequestException:
|
|
53
|
+
return False
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
def ensure_server_is_running(self):
|
|
57
|
+
"""
|
|
58
|
+
Ensures the XTTS server is running. If not, it attempts to start it
|
|
59
|
+
in a process-safe manner using a file lock.
|
|
60
|
+
"""
|
|
61
|
+
self.server_dir.mkdir(exist_ok=True)
|
|
62
|
+
lock_path = self.server_dir / "xtts_server.lock"
|
|
63
|
+
lock = FileLock(lock_path)
|
|
64
|
+
|
|
65
|
+
ASCIIColors.info("Attempting to start or connect to the XTTS server...")
|
|
66
|
+
|
|
67
|
+
if self.is_server_running():
|
|
68
|
+
ASCIIColors.green("XTTS Server is already running and responsive.")
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
with lock.acquire(timeout=10):
|
|
73
|
+
if not self.is_server_running():
|
|
74
|
+
ASCIIColors.yellow("Lock acquired. Starting dedicated XTTS server...")
|
|
75
|
+
self.start_server()
|
|
76
|
+
self._wait_for_server()
|
|
77
|
+
else:
|
|
78
|
+
ASCIIColors.green("Server was started by another process while we waited. Connected successfully.")
|
|
79
|
+
except Timeout:
|
|
80
|
+
ASCIIColors.yellow("Could not acquire lock, another process is starting the server. Waiting...")
|
|
81
|
+
self._wait_for_server(timeout=60)
|
|
82
|
+
|
|
83
|
+
if not self.is_server_running():
|
|
84
|
+
raise RuntimeError("Failed to start or connect to the XTTS server after all attempts.")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def install_server_dependencies(self):
|
|
88
|
+
"""
|
|
89
|
+
Installs the server's dependencies into a dedicated virtual environment
|
|
90
|
+
using pipmaster, which handles complex packages like PyTorch.
|
|
91
|
+
"""
|
|
92
|
+
ASCIIColors.info(f"Setting up virtual environment in: {self.venv_dir}")
|
|
93
|
+
# Ensure pipmaster is available.
|
|
94
|
+
try:
|
|
95
|
+
import pipmaster as pm
|
|
96
|
+
except ImportError:
|
|
97
|
+
print("FATAL: pipmaster is not installed. Please install it using: pip install pipmaster")
|
|
98
|
+
raise Exception("pipmaster not found")
|
|
99
|
+
pm_v = pm.PackageManager(venv_path=str(self.venv_dir))
|
|
100
|
+
|
|
101
|
+
requirements_file = self.server_dir / "requirements.txt"
|
|
102
|
+
|
|
103
|
+
ASCIIColors.info("Installing server dependencies from requirements.txt...")
|
|
104
|
+
success = pm_v.ensure_requirements(str(requirements_file), verbose=True)
|
|
105
|
+
|
|
106
|
+
if not success:
|
|
107
|
+
ASCIIColors.error("Failed to install server dependencies. Please check the console output for errors.")
|
|
108
|
+
raise RuntimeError("XTTS server dependency installation failed.")
|
|
109
|
+
|
|
110
|
+
ASCIIColors.green("Server dependencies are satisfied.")
|
|
111
|
+
|
|
30
112
|
|
|
31
113
|
def start_server(self):
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
server_script = server_dir / "main.py"
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
# 2. Get the python executable from the venv
|
|
114
|
+
"""
|
|
115
|
+
Installs dependencies and launches the FastAPI server as a background subprocess.
|
|
116
|
+
This method should only be called from within a file lock.
|
|
117
|
+
"""
|
|
118
|
+
server_script = self.server_dir / "main.py"
|
|
119
|
+
if not server_script.exists():
|
|
120
|
+
raise FileNotFoundError(f"Server script not found at {server_script}.")
|
|
121
|
+
|
|
122
|
+
if not self.venv_dir.exists():
|
|
123
|
+
self.install_server_dependencies()
|
|
124
|
+
|
|
44
125
|
if sys.platform == "win32":
|
|
45
|
-
python_executable =
|
|
126
|
+
python_executable = self.venv_dir / "Scripts" / "python.exe"
|
|
46
127
|
else:
|
|
47
|
-
python_executable =
|
|
128
|
+
python_executable = self.venv_dir / "bin" / "python"
|
|
48
129
|
|
|
49
|
-
# 3. Launch the server as a subprocess with stdout/stderr forwarded to console
|
|
50
130
|
command = [
|
|
51
131
|
str(python_executable),
|
|
52
132
|
str(server_script),
|
|
@@ -54,58 +134,61 @@ class XTTSClientBinding(LollmsTTSBinding):
|
|
|
54
134
|
"--port", str(self.port)
|
|
55
135
|
]
|
|
56
136
|
|
|
57
|
-
#
|
|
58
|
-
|
|
59
|
-
command,
|
|
60
|
-
stdout=None, # Inherit parent's stdout (shows in console)
|
|
61
|
-
stderr=None, # Inherit parent's stderr (shows in console)
|
|
62
|
-
)
|
|
137
|
+
# Use DETACHED_PROCESS on Windows to allow the server to run independently.
|
|
138
|
+
creationflags = subprocess.DETACHED_PROCESS if sys.platform == "win32" else 0
|
|
63
139
|
|
|
64
|
-
|
|
65
|
-
|
|
140
|
+
self.server_process = subprocess.Popen(command, creationflags=creationflags)
|
|
141
|
+
ASCIIColors.info("XTTS server process launched in the background.")
|
|
66
142
|
|
|
67
|
-
def _wait_for_server(self, timeout=
|
|
143
|
+
def _wait_for_server(self, timeout=10):
|
|
144
|
+
"""Waits for the server to become responsive."""
|
|
145
|
+
ASCIIColors.info("Waiting for XTTS server to become available...")
|
|
68
146
|
start_time = time.time()
|
|
69
147
|
while time.time() - start_time < timeout:
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
time.sleep(1)
|
|
77
|
-
|
|
78
|
-
self.stop_server()
|
|
79
|
-
raise RuntimeError("Failed to start the XTTS server in the specified timeout.")
|
|
80
|
-
|
|
81
|
-
def stop_server(self):
|
|
82
|
-
if self.server_process:
|
|
83
|
-
print("XTTS Client: Stopping dedicated server...")
|
|
84
|
-
self.server_process.terminate()
|
|
85
|
-
self.server_process.wait()
|
|
86
|
-
self.server_process = None
|
|
87
|
-
print("Server stopped.")
|
|
88
|
-
|
|
148
|
+
if self.is_server_running():
|
|
149
|
+
ASCIIColors.green("XTTS Server is up and running.")
|
|
150
|
+
return
|
|
151
|
+
time.sleep(2)
|
|
152
|
+
raise RuntimeError("Failed to connect to the XTTS server within the specified timeout.")
|
|
153
|
+
|
|
89
154
|
def __del__(self):
|
|
90
|
-
#
|
|
91
|
-
|
|
155
|
+
# The client destructor does not stop the server,
|
|
156
|
+
# as it is a shared resource for other processes.
|
|
157
|
+
pass
|
|
92
158
|
|
|
93
159
|
def generate_audio(self, text: str, voice: Optional[str] = None, **kwargs) -> bytes:
|
|
94
160
|
"""Generate audio by calling the server's API"""
|
|
95
|
-
payload = {"text": text, "voice": voice
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
161
|
+
payload = {"text": text, "voice": voice}
|
|
162
|
+
# Pass other kwargs from the description file (language, split_sentences)
|
|
163
|
+
payload.update(kwargs)
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
response = requests.post(f"{self.base_url}/generate_audio", json=payload, timeout=300)
|
|
167
|
+
response.raise_for_status()
|
|
168
|
+
return response.content
|
|
169
|
+
except requests.exceptions.RequestException as e:
|
|
170
|
+
ASCIIColors.error(f"Failed to communicate with XTTS server at {self.base_url}.")
|
|
171
|
+
ASCIIColors.error(f"Error details: {e}")
|
|
172
|
+
raise RuntimeError("Communication with the XTTS server failed.") from e
|
|
173
|
+
|
|
99
174
|
|
|
100
175
|
def list_voices(self, **kwargs) -> List[str]:
|
|
101
176
|
"""Get available voices from the server"""
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
177
|
+
try:
|
|
178
|
+
response = requests.get(f"{self.base_url}/list_voices")
|
|
179
|
+
response.raise_for_status()
|
|
180
|
+
return response.json().get("voices", [])
|
|
181
|
+
except requests.exceptions.RequestException as e:
|
|
182
|
+
ASCIIColors.error(f"Failed to get voices from XTTS server: {e}")
|
|
183
|
+
return []
|
|
105
184
|
|
|
106
|
-
def list_models(self, **kwargs) -> List[str]:
|
|
107
|
-
"""Get available models from the server"""
|
|
108
|
-
response = requests.get(f"{self.base_url}/list_models")
|
|
109
|
-
response.raise_for_status()
|
|
110
|
-
return response.json().get("models", [])
|
|
111
185
|
|
|
186
|
+
def list_models(self, **kwargs) -> list:
|
|
187
|
+
"""Lists models supported by the server"""
|
|
188
|
+
try:
|
|
189
|
+
response = requests.get(f"{self.base_url}/list_models")
|
|
190
|
+
response.raise_for_status()
|
|
191
|
+
return response.json().get("models", [])
|
|
192
|
+
except requests.exceptions.RequestException as e:
|
|
193
|
+
ASCIIColors.error(f"Failed to get models from XTTS server: {e}")
|
|
194
|
+
return []
|