lollms-client 1.6.4__py3-none-any.whl → 1.6.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/lollms_core.py +3 -1
- lollms_client/tti_bindings/diffusers/__init__.py +129 -59
- lollms_client/tti_bindings/diffusers/server/main.py +354 -118
- lollms_client/tti_bindings/gemini/__init__.py +179 -239
- lollms_client/tts_bindings/xtts/__init__.py +106 -81
- lollms_client/tts_bindings/xtts/server/main.py +128 -183
- {lollms_client-1.6.4.dist-info → lollms_client-1.6.6.dist-info}/METADATA +1 -1
- {lollms_client-1.6.4.dist-info → lollms_client-1.6.6.dist-info}/RECORD +12 -12
- {lollms_client-1.6.4.dist-info → lollms_client-1.6.6.dist-info}/WHEEL +0 -0
- {lollms_client-1.6.4.dist-info → lollms_client-1.6.6.dist-info}/licenses/LICENSE +0 -0
- {lollms_client-1.6.4.dist-info → lollms_client-1.6.6.dist-info}/top_level.txt +0 -0
lollms_client/__init__.py
CHANGED
|
@@ -8,7 +8,7 @@ from lollms_client.lollms_utilities import PromptReshaper # Keep general utiliti
|
|
|
8
8
|
from lollms_client.lollms_mcp_binding import LollmsMCPBinding, LollmsMCPBindingManager
|
|
9
9
|
from lollms_client.lollms_llm_binding import LollmsLLMBindingManager
|
|
10
10
|
|
|
11
|
-
__version__ = "1.6.
|
|
11
|
+
__version__ = "1.6.6" # Updated version
|
|
12
12
|
|
|
13
13
|
# Optionally, you could define __all__ if you want to be explicit about exports
|
|
14
14
|
__all__ = [
|
lollms_client/lollms_core.py
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
# lollms_client/lollms_core.py
|
|
2
|
+
# author: ParisNeo
|
|
3
|
+
# description: LollmsClient definition file
|
|
2
4
|
import requests
|
|
3
5
|
from ascii_colors import ASCIIColors, trace_exception
|
|
4
6
|
from lollms_client.lollms_types import MSG_TYPE, ELF_COMPLETION_FORMAT
|
|
@@ -519,7 +521,7 @@ class LollmsClient():
|
|
|
519
521
|
Union[str, dict]: Generated text or error dictionary if failed.
|
|
520
522
|
"""
|
|
521
523
|
if self.llm:
|
|
522
|
-
|
|
524
|
+
images = [str(image) for image in images] if images else None
|
|
523
525
|
ctx_size = ctx_size if ctx_size is not None else self.llm.default_ctx_size if self.llm.default_ctx_size else None
|
|
524
526
|
if ctx_size is None:
|
|
525
527
|
ctx_size = self.llm.get_ctx_size()
|
|
@@ -44,15 +44,17 @@ class DiffusersBinding(LollmsTTIBinding):
|
|
|
44
44
|
kwargs['model_name'] = kwargs.pop('model')
|
|
45
45
|
|
|
46
46
|
self.config = kwargs
|
|
47
|
-
self.host = kwargs.get("host", "localhost")
|
|
47
|
+
self.host = kwargs.get("host", "localhost")
|
|
48
48
|
self.port = kwargs.get("port", 9630)
|
|
49
49
|
self.auto_start_server = kwargs.get("auto_start_server", True)
|
|
50
50
|
self.server_process = None
|
|
51
51
|
self.base_url = f"http://{self.host}:{self.port}"
|
|
52
52
|
self.binding_root = Path(__file__).parent
|
|
53
53
|
self.server_dir = self.binding_root / "server"
|
|
54
|
-
self.venv_dir = Path("./venv/
|
|
55
|
-
self.models_path = Path(kwargs.get("models_path", "./diffusers_models")).resolve()
|
|
54
|
+
self.venv_dir = Path("./venv/tti_diffusers_venv")
|
|
55
|
+
self.models_path = Path(kwargs.get("models_path", "./data/models/diffusers_models")).resolve()
|
|
56
|
+
self.extra_models_path = kwargs.get("extra_models_path")
|
|
57
|
+
self.models_path.mkdir(exist_ok=True, parents=True)
|
|
56
58
|
if self.auto_start_server:
|
|
57
59
|
self.ensure_server_is_running()
|
|
58
60
|
|
|
@@ -66,27 +68,48 @@ class DiffusersBinding(LollmsTTIBinding):
|
|
|
66
68
|
return False
|
|
67
69
|
return False
|
|
68
70
|
|
|
71
|
+
|
|
69
72
|
def ensure_server_is_running(self):
|
|
70
73
|
"""
|
|
71
74
|
Ensures the Diffusers server is running. If not, it attempts to start it
|
|
72
|
-
in a process-safe manner using a file lock.
|
|
75
|
+
in a process-safe manner using a file lock. This method is designed to
|
|
76
|
+
prevent race conditions in multi-worker environments.
|
|
73
77
|
"""
|
|
74
78
|
self.server_dir.mkdir(exist_ok=True)
|
|
79
|
+
# Use a lock file in the binding's server directory for consistency across instances
|
|
75
80
|
lock_path = self.server_dir / "diffusers_server.lock"
|
|
76
|
-
lock = FileLock(lock_path
|
|
81
|
+
lock = FileLock(lock_path)
|
|
77
82
|
|
|
78
83
|
ASCIIColors.info("Attempting to start or connect to the Diffusers server...")
|
|
84
|
+
|
|
85
|
+
# First, perform a quick check without the lock to avoid unnecessary waiting.
|
|
86
|
+
if self.is_server_running():
|
|
87
|
+
ASCIIColors.green("Diffusers Server is already running and responsive.")
|
|
88
|
+
return
|
|
89
|
+
|
|
79
90
|
try:
|
|
80
|
-
with
|
|
91
|
+
# Try to acquire the lock with a timeout. If another process is starting
|
|
92
|
+
# the server, this will wait until it's finished.
|
|
93
|
+
with lock.acquire(timeout=60):
|
|
94
|
+
# After acquiring the lock, we MUST re-check if the server is running.
|
|
95
|
+
# Another process might have started it and released the lock while we were waiting.
|
|
81
96
|
if not self.is_server_running():
|
|
82
97
|
ASCIIColors.yellow("Lock acquired. Starting dedicated Diffusers server...")
|
|
83
98
|
self.start_server()
|
|
99
|
+
# The process that starts the server is responsible for waiting for it to be ready
|
|
100
|
+
# BEFORE releasing the lock. This is the key to preventing race conditions.
|
|
101
|
+
self._wait_for_server()
|
|
84
102
|
else:
|
|
85
|
-
ASCIIColors.green("Server was started by another process. Connected successfully.")
|
|
103
|
+
ASCIIColors.green("Server was started by another process while we waited. Connected successfully.")
|
|
86
104
|
except Timeout:
|
|
87
|
-
|
|
105
|
+
# This happens if the process holding the lock takes more than 60 seconds to start the server.
|
|
106
|
+
# We don't try to start another one. We just wait for the existing one to be ready.
|
|
107
|
+
ASCIIColors.yellow("Could not acquire lock, another process is taking a long time to start the server. Waiting...")
|
|
108
|
+
self._wait_for_server(timeout=300) # Give it a longer timeout here just in case.
|
|
88
109
|
|
|
89
|
-
|
|
110
|
+
# A final verification to ensure we are connected.
|
|
111
|
+
if not self.is_server_running():
|
|
112
|
+
raise RuntimeError("Failed to start or connect to the Diffusers server after all attempts.")
|
|
90
113
|
|
|
91
114
|
def install_server_dependencies(self):
|
|
92
115
|
"""
|
|
@@ -97,6 +120,24 @@ class DiffusersBinding(LollmsTTIBinding):
|
|
|
97
120
|
pm_v = pm.PackageManager(venv_path=str(self.venv_dir))
|
|
98
121
|
|
|
99
122
|
# --- PyTorch Installation ---
|
|
123
|
+
ASCIIColors.info(f"Installing server dependencies")
|
|
124
|
+
pm_v.ensure_packages([
|
|
125
|
+
"requests", "uvicorn", "fastapi", "python-multipart", "filelock"
|
|
126
|
+
])
|
|
127
|
+
ASCIIColors.info(f"Installing parisneo libraries")
|
|
128
|
+
pm_v.ensure_packages([
|
|
129
|
+
"ascii_colors","pipmaster"
|
|
130
|
+
])
|
|
131
|
+
ASCIIColors.info(f"Installing misc libraries (numpy, tqdm...)")
|
|
132
|
+
pm_v.ensure_packages([
|
|
133
|
+
"tqdm", "numpy"
|
|
134
|
+
])
|
|
135
|
+
ASCIIColors.info(f"Installing Pillow")
|
|
136
|
+
pm_v.ensure_packages([
|
|
137
|
+
"pillow"
|
|
138
|
+
])
|
|
139
|
+
|
|
140
|
+
ASCIIColors.info(f"Installing pytorch")
|
|
100
141
|
torch_index_url = None
|
|
101
142
|
if sys.platform == "win32":
|
|
102
143
|
try:
|
|
@@ -104,21 +145,27 @@ class DiffusersBinding(LollmsTTIBinding):
|
|
|
104
145
|
result = subprocess.run(["nvidia-smi"], capture_output=True, text=True, check=True)
|
|
105
146
|
ASCIIColors.green("NVIDIA GPU detected. Installing CUDA-enabled PyTorch.")
|
|
106
147
|
# Using a common and stable CUDA version. Adjust if needed.
|
|
107
|
-
torch_index_url = "https://download.pytorch.org/whl/
|
|
148
|
+
torch_index_url = "https://download.pytorch.org/whl/cu128"
|
|
108
149
|
except (FileNotFoundError, subprocess.CalledProcessError):
|
|
109
150
|
ASCIIColors.yellow("`nvidia-smi` not found or failed. Installing standard PyTorch. If you have an NVIDIA GPU, please ensure drivers are installed and in PATH.")
|
|
110
|
-
|
|
151
|
+
|
|
111
152
|
# Base packages including torch. pm.ensure_packages handles verbose output.
|
|
112
153
|
pm_v.ensure_packages(["torch", "torchvision"], index_url=torch_index_url)
|
|
113
154
|
|
|
114
|
-
|
|
115
155
|
# Standard dependencies
|
|
156
|
+
ASCIIColors.info(f"Installing transformers dependencies")
|
|
116
157
|
pm_v.ensure_packages([
|
|
117
|
-
"
|
|
118
|
-
"accelerate", "uvicorn", "fastapi", "python-multipart", "filelock", "ascii_colors"
|
|
158
|
+
"transformers", "safetensors", "accelerate"
|
|
119
159
|
])
|
|
120
|
-
|
|
160
|
+
ASCIIColors.info(f"[Optional] Installing xformers")
|
|
161
|
+
try:
|
|
162
|
+
pm_v.ensure_packages([
|
|
163
|
+
"xformers"
|
|
164
|
+
])
|
|
165
|
+
except:
|
|
166
|
+
pass
|
|
121
167
|
# Git-based diffusers to get the latest version
|
|
168
|
+
ASCIIColors.info(f"Installing diffusers library from github")
|
|
122
169
|
pm_v.ensure_packages([
|
|
123
170
|
{
|
|
124
171
|
"name": "diffusers",
|
|
@@ -127,14 +174,6 @@ class DiffusersBinding(LollmsTTIBinding):
|
|
|
127
174
|
}
|
|
128
175
|
])
|
|
129
176
|
|
|
130
|
-
# XFormers (optional but recommended for NVIDIA)
|
|
131
|
-
if torch_index_url: # Only try to install xformers if CUDA is likely present
|
|
132
|
-
try:
|
|
133
|
-
ASCIIColors.info("Attempting to install xformers for performance optimization...")
|
|
134
|
-
pm_v.ensure_packages(["xformers"], upgrade=True)
|
|
135
|
-
except Exception as e:
|
|
136
|
-
ASCIIColors.warning(f"Could not install xformers. It's optional but recommended for performance on NVIDIA GPUs. Error: {e}")
|
|
137
|
-
|
|
138
177
|
ASCIIColors.green("Server dependencies are satisfied.")
|
|
139
178
|
|
|
140
179
|
def start_server(self):
|
|
@@ -164,10 +203,14 @@ class DiffusersBinding(LollmsTTIBinding):
|
|
|
164
203
|
"--models-path", str(self.models_path.resolve()) # Pass models_path to server
|
|
165
204
|
]
|
|
166
205
|
|
|
206
|
+
if self.extra_models_path:
|
|
207
|
+
resolved_extra_path = Path(self.extra_models_path).resolve()
|
|
208
|
+
command.extend(["--extra-models-path", str(resolved_extra_path)])
|
|
209
|
+
|
|
167
210
|
# Use DETACHED_PROCESS on Windows to allow the server to run independently of the parent process.
|
|
168
211
|
# On Linux/macOS, the process will be daemonized enough to not be killed with the worker.
|
|
169
212
|
creationflags = subprocess.DETACHED_PROCESS if sys.platform == "win32" else 0
|
|
170
|
-
|
|
213
|
+
|
|
171
214
|
self.server_process = subprocess.Popen(command, creationflags=creationflags)
|
|
172
215
|
ASCIIColors.info("Diffusers server process launched in the background.")
|
|
173
216
|
|
|
@@ -191,11 +234,11 @@ class DiffusersBinding(LollmsTTIBinding):
|
|
|
191
234
|
time.sleep(2)
|
|
192
235
|
raise RuntimeError("Failed to connect to the Diffusers server within the specified timeout.")
|
|
193
236
|
|
|
194
|
-
def
|
|
195
|
-
"""Helper to make POST requests
|
|
237
|
+
def _post_json_request(self, endpoint: str, data: Optional[dict] = None) -> requests.Response:
|
|
238
|
+
"""Helper to make POST requests with a JSON body."""
|
|
196
239
|
try:
|
|
197
240
|
url = f"{self.base_url}{endpoint}"
|
|
198
|
-
response = requests.post(url, json=data,
|
|
241
|
+
response = requests.post(url, json=data, timeout=3600) # Long timeout for generation
|
|
199
242
|
response.raise_for_status()
|
|
200
243
|
return response
|
|
201
244
|
except requests.exceptions.RequestException as e:
|
|
@@ -208,6 +251,24 @@ class DiffusersBinding(LollmsTTIBinding):
|
|
|
208
251
|
ASCIIColors.error(f"Server raw response: {e.response.text}")
|
|
209
252
|
raise RuntimeError("Communication with the Diffusers server failed.") from e
|
|
210
253
|
|
|
254
|
+
def _post_multipart_request(self, endpoint: str, data: Optional[dict] = None, files: Optional[list] = None) -> requests.Response:
|
|
255
|
+
"""Helper to make multipart/form-data POST requests for file uploads."""
|
|
256
|
+
try:
|
|
257
|
+
url = f"{self.base_url}{endpoint}"
|
|
258
|
+
response = requests.post(url, data=data, files=files, timeout=3600)
|
|
259
|
+
response.raise_for_status()
|
|
260
|
+
return response
|
|
261
|
+
except requests.exceptions.RequestException as e:
|
|
262
|
+
# (Error handling is the same as above)
|
|
263
|
+
ASCIIColors.error(f"Failed to communicate with Diffusers server at {url}.")
|
|
264
|
+
ASCIIColors.error(f"Error details: {e}")
|
|
265
|
+
if hasattr(e, 'response') and e.response:
|
|
266
|
+
try:
|
|
267
|
+
ASCIIColors.error(f"Server response: {e.response.json().get('detail', e.response.text)}")
|
|
268
|
+
except json.JSONDecodeError:
|
|
269
|
+
ASCIIColors.error(f"Server raw response: {e.response.text}")
|
|
270
|
+
raise RuntimeError("Communication with the Diffusers server failed.") from e
|
|
271
|
+
|
|
211
272
|
def _get_request(self, endpoint: str, params: Optional[dict] = None) -> requests.Response:
|
|
212
273
|
"""Helper to make GET requests to the server."""
|
|
213
274
|
try:
|
|
@@ -222,69 +283,78 @@ class DiffusersBinding(LollmsTTIBinding):
|
|
|
222
283
|
def unload_model(self):
|
|
223
284
|
ASCIIColors.info("Requesting server to unload the current model...")
|
|
224
285
|
try:
|
|
225
|
-
self.
|
|
286
|
+
self._post_json_request("/unload_model")
|
|
226
287
|
except Exception as e:
|
|
227
288
|
ASCIIColors.warning(f"Could not send unload request to server: {e}")
|
|
228
289
|
pass
|
|
229
290
|
|
|
230
291
|
def generate_image(self, prompt: str, negative_prompt: str = "", **kwargs) -> bytes:
|
|
231
|
-
|
|
292
|
+
params = kwargs.copy()
|
|
293
|
+
if "model_name" not in params and self.config.get("model_name"):
|
|
294
|
+
params["model_name"] = self.config["model_name"]
|
|
295
|
+
|
|
296
|
+
response = self._post_json_request("/generate_image", data={
|
|
232
297
|
"prompt": prompt,
|
|
233
298
|
"negative_prompt": negative_prompt,
|
|
234
|
-
"params":
|
|
299
|
+
"params": params
|
|
235
300
|
})
|
|
236
301
|
return response.content
|
|
237
302
|
|
|
238
303
|
def edit_image(self, images: Union[str, List[str], "Image.Image", List["Image.Image"]], prompt: str, **kwargs) -> bytes:
|
|
239
|
-
|
|
240
|
-
image_paths = []
|
|
241
|
-
|
|
304
|
+
images_b64 = []
|
|
242
305
|
if not isinstance(images, list):
|
|
243
306
|
images = [images]
|
|
244
307
|
|
|
245
|
-
|
|
246
|
-
|
|
308
|
+
|
|
309
|
+
for img in images:
|
|
310
|
+
# Case 1: Input is a PIL Image object
|
|
311
|
+
if hasattr(img, 'save'):
|
|
247
312
|
buffer = BytesIO()
|
|
248
313
|
img.save(buffer, format="PNG")
|
|
249
|
-
buffer.
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
elif isinstance(img, str): # Handle base64 strings
|
|
314
|
+
b64_string = base64.b64encode(buffer.getvalue()).decode('utf-8')
|
|
315
|
+
images_b64.append(b64_string)
|
|
316
|
+
|
|
317
|
+
# Case 2: Input is a string (could be path or already base64)
|
|
318
|
+
elif isinstance(img, str):
|
|
255
319
|
try:
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
img_bytes = base64.b64decode(b64_data)
|
|
260
|
-
files[f"image_{i}"] = (f"image_{i}.png", img_bytes, "image/png")
|
|
320
|
+
b64_string = img.split(";base64,")[1] if ";base64," in img else img
|
|
321
|
+
base64.b64decode(b64_string) # Validate
|
|
322
|
+
images_b64.append(b64_string)
|
|
261
323
|
except Exception:
|
|
262
|
-
|
|
324
|
+
ASCIIColors.warning(f"Warning: A string input was not a valid file path or base64. Skipping.")
|
|
263
325
|
else:
|
|
264
|
-
|
|
326
|
+
raise ValueError(f"Unsupported image type in edit_image: {type(img)}")
|
|
327
|
+
if not images_b64:
|
|
328
|
+
raise ValueError("No valid images were provided to the edit_image function.")
|
|
329
|
+
|
|
330
|
+
params = kwargs.copy()
|
|
331
|
+
if "model_name" not in params and self.config.get("model_name"):
|
|
332
|
+
params["model_name"] = self.config["model_name"]
|
|
265
333
|
|
|
266
|
-
|
|
334
|
+
# Translate "mask" to "mask_image" for server compatibility
|
|
335
|
+
if "mask" in params and params["mask"]:
|
|
336
|
+
params["mask_image"] = params.pop("mask")
|
|
337
|
+
|
|
338
|
+
json_payload = {
|
|
267
339
|
"prompt": prompt,
|
|
268
|
-
"
|
|
269
|
-
"params":
|
|
340
|
+
"images_b64": images_b64,
|
|
341
|
+
"params": params
|
|
270
342
|
}
|
|
271
|
-
|
|
272
|
-
# FastAPI needs separate form fields for json and files
|
|
273
|
-
response = self._post_request("/edit_image", data={"json_payload": json.dumps(data_payload)}, files=files)
|
|
343
|
+
response = self._post_json_request("/edit_image", data=json_payload)
|
|
274
344
|
return response.content
|
|
275
|
-
|
|
345
|
+
|
|
276
346
|
def list_models(self) -> List[Dict[str, Any]]:
|
|
277
347
|
return self._get_request("/list_models").json()
|
|
278
348
|
|
|
279
349
|
def list_local_models(self) -> List[str]:
|
|
280
350
|
return self._get_request("/list_local_models").json()
|
|
281
|
-
|
|
351
|
+
|
|
282
352
|
def list_available_models(self) -> List[str]:
|
|
283
353
|
return self._get_request("/list_available_models").json()
|
|
284
|
-
|
|
354
|
+
|
|
285
355
|
def list_services(self, **kwargs) -> List[Dict[str, str]]:
|
|
286
356
|
return self._get_request("/list_models").json()
|
|
287
|
-
|
|
357
|
+
|
|
288
358
|
def get_settings(self, **kwargs) -> List[Dict[str, Any]]:
|
|
289
359
|
# The server holds the state, so we fetch it.
|
|
290
360
|
return self._get_request("/get_settings").json()
|
|
@@ -292,7 +362,7 @@ class DiffusersBinding(LollmsTTIBinding):
|
|
|
292
362
|
def set_settings(self, settings: Union[Dict[str, Any], List[Dict[str, Any]]], **kwargs) -> bool:
|
|
293
363
|
# Normalize settings from list of dicts to a single dict if needed
|
|
294
364
|
parsed_settings = settings if isinstance(settings, dict) else {s["name"]: s["value"] for s in settings if "name" in s and "value" in s}
|
|
295
|
-
response = self.
|
|
365
|
+
response = self._post_json_request("/set_settings", data=parsed_settings)
|
|
296
366
|
return response.json().get("success", False)
|
|
297
367
|
|
|
298
368
|
def ps(self) -> List[dict]:
|