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.
Files changed (64) hide show
  1. lollms_client/__init__.py +1 -1
  2. lollms_client/llm_bindings/azure_openai/__init__.py +2 -2
  3. lollms_client/llm_bindings/claude/__init__.py +125 -34
  4. lollms_client/llm_bindings/gemini/__init__.py +261 -159
  5. lollms_client/llm_bindings/grok/__init__.py +52 -14
  6. lollms_client/llm_bindings/groq/__init__.py +2 -2
  7. lollms_client/llm_bindings/hugging_face_inference_api/__init__.py +2 -2
  8. lollms_client/llm_bindings/litellm/__init__.py +1 -1
  9. lollms_client/llm_bindings/llamacpp/__init__.py +18 -11
  10. lollms_client/llm_bindings/lollms/__init__.py +151 -32
  11. lollms_client/llm_bindings/lollms_webui/__init__.py +1 -1
  12. lollms_client/llm_bindings/mistral/__init__.py +2 -2
  13. lollms_client/llm_bindings/novita_ai/__init__.py +439 -0
  14. lollms_client/llm_bindings/ollama/__init__.py +309 -93
  15. lollms_client/llm_bindings/open_router/__init__.py +2 -2
  16. lollms_client/llm_bindings/openai/__init__.py +148 -29
  17. lollms_client/llm_bindings/openllm/__init__.py +362 -506
  18. lollms_client/llm_bindings/openwebui/__init__.py +465 -0
  19. lollms_client/llm_bindings/perplexity/__init__.py +326 -0
  20. lollms_client/llm_bindings/pythonllamacpp/__init__.py +3 -3
  21. lollms_client/llm_bindings/tensor_rt/__init__.py +1 -1
  22. lollms_client/llm_bindings/transformers/__init__.py +428 -632
  23. lollms_client/llm_bindings/vllm/__init__.py +1 -1
  24. lollms_client/lollms_agentic.py +4 -2
  25. lollms_client/lollms_base_binding.py +61 -0
  26. lollms_client/lollms_core.py +516 -1890
  27. lollms_client/lollms_discussion.py +55 -18
  28. lollms_client/lollms_llm_binding.py +112 -261
  29. lollms_client/lollms_mcp_binding.py +34 -75
  30. lollms_client/lollms_personality.py +5 -2
  31. lollms_client/lollms_stt_binding.py +85 -52
  32. lollms_client/lollms_tti_binding.py +23 -37
  33. lollms_client/lollms_ttm_binding.py +24 -42
  34. lollms_client/lollms_tts_binding.py +28 -17
  35. lollms_client/lollms_ttv_binding.py +24 -42
  36. lollms_client/lollms_types.py +4 -2
  37. lollms_client/stt_bindings/whisper/__init__.py +108 -23
  38. lollms_client/stt_bindings/whispercpp/__init__.py +7 -1
  39. lollms_client/tti_bindings/diffusers/__init__.py +418 -810
  40. lollms_client/tti_bindings/diffusers/server/main.py +1051 -0
  41. lollms_client/tti_bindings/gemini/__init__.py +182 -239
  42. lollms_client/tti_bindings/leonardo_ai/__init__.py +127 -0
  43. lollms_client/tti_bindings/lollms/__init__.py +4 -1
  44. lollms_client/tti_bindings/novita_ai/__init__.py +105 -0
  45. lollms_client/tti_bindings/openai/__init__.py +10 -11
  46. lollms_client/tti_bindings/stability_ai/__init__.py +178 -0
  47. lollms_client/ttm_bindings/audiocraft/__init__.py +7 -12
  48. lollms_client/ttm_bindings/beatoven_ai/__init__.py +129 -0
  49. lollms_client/ttm_bindings/lollms/__init__.py +4 -17
  50. lollms_client/ttm_bindings/replicate/__init__.py +115 -0
  51. lollms_client/ttm_bindings/stability_ai/__init__.py +117 -0
  52. lollms_client/ttm_bindings/topmediai/__init__.py +96 -0
  53. lollms_client/tts_bindings/bark/__init__.py +7 -10
  54. lollms_client/tts_bindings/lollms/__init__.py +6 -1
  55. lollms_client/tts_bindings/piper_tts/__init__.py +8 -11
  56. lollms_client/tts_bindings/xtts/__init__.py +157 -74
  57. lollms_client/tts_bindings/xtts/server/main.py +241 -280
  58. {lollms_client-1.4.1.dist-info → lollms_client-1.7.10.dist-info}/METADATA +316 -6
  59. lollms_client-1.7.10.dist-info/RECORD +89 -0
  60. lollms_client/ttm_bindings/bark/__init__.py +0 -339
  61. lollms_client-1.4.1.dist-info/RECORD +0 -78
  62. {lollms_client-1.4.1.dist-info → lollms_client-1.7.10.dist-info}/WHEEL +0 -0
  63. {lollms_client-1.4.1.dist-info → lollms_client-1.7.10.dist-info}/licenses/LICENSE +0 -0
  64. {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
- binding_name = "bark"
21
- super().__init__(binding_name=binding_name, **kwargs)
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
- 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
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, **kwargs) -> List[str]:
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
- # File: lollms_client/tts_bindings/xtts/__init__.py
2
- from lollms_client.lollms_tts_binding import LollmsTTSBinding
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
- import pipmaster as pm
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
- def __init__(self,
15
- host: str = "localhost",
16
- port: int = 8081,
17
- auto_start_server: bool = True,
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
- binding_name = "xtts"
21
- super().__init__(binding_name=binding_name, **kwargs)
22
- self.host = host
23
- self.port = port
24
- self.auto_start_server = auto_start_server
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.start_server()
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
- print("XTTS 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))
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 = venv_path / "Scripts" / "python.exe"
126
+ python_executable = self.venv_dir / "Scripts" / "python.exe"
46
127
  else:
47
- python_executable = venv_path / "bin" / "python"
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
- # 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)
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
- # 4. Wait for the server to be ready
65
- self._wait_for_server()
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=60):
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
- try:
71
- response = requests.get(f"{self.base_url}/status")
72
- if response.status_code == 200 and response.json().get("status") == "running":
73
- print("XTTS Server is up and running.")
74
- return
75
- except requests.ConnectionError:
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
- # Ensure the server is stopped when the object is destroyed
91
- self.stop_server()
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, **kwargs}
96
- response = requests.post(f"{self.base_url}/generate_audio", json=payload)
97
- response.raise_for_status()
98
- return response.content
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
- response = requests.get(f"{self.base_url}/list_voices")
103
- response.raise_for_status()
104
- return response.json().get("voices", [])
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 []