lollms-client 1.5.6__py3-none-any.whl → 1.7.13__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 (63) 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 -35
  4. lollms_client/llm_bindings/gemini/__init__.py +261 -159
  5. lollms_client/llm_bindings/grok/__init__.py +52 -15
  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/llama_cpp_server/__init__.py +605 -0
  10. lollms_client/llm_bindings/llamacpp/__init__.py +18 -11
  11. lollms_client/llm_bindings/lollms/__init__.py +76 -21
  12. lollms_client/llm_bindings/lollms_webui/__init__.py +1 -1
  13. lollms_client/llm_bindings/mistral/__init__.py +2 -2
  14. lollms_client/llm_bindings/novita_ai/__init__.py +142 -6
  15. lollms_client/llm_bindings/ollama/__init__.py +345 -89
  16. lollms_client/llm_bindings/open_router/__init__.py +2 -2
  17. lollms_client/llm_bindings/openai/__init__.py +81 -20
  18. lollms_client/llm_bindings/openllm/__init__.py +362 -506
  19. lollms_client/llm_bindings/openwebui/__init__.py +333 -171
  20. lollms_client/llm_bindings/perplexity/__init__.py +2 -2
  21. lollms_client/llm_bindings/pythonllamacpp/__init__.py +3 -3
  22. lollms_client/llm_bindings/tensor_rt/__init__.py +1 -1
  23. lollms_client/llm_bindings/transformers/__init__.py +428 -632
  24. lollms_client/llm_bindings/vllm/__init__.py +1 -1
  25. lollms_client/lollms_agentic.py +4 -2
  26. lollms_client/lollms_base_binding.py +61 -0
  27. lollms_client/lollms_core.py +512 -1890
  28. lollms_client/lollms_discussion.py +65 -39
  29. lollms_client/lollms_llm_binding.py +126 -261
  30. lollms_client/lollms_mcp_binding.py +49 -77
  31. lollms_client/lollms_stt_binding.py +99 -52
  32. lollms_client/lollms_tti_binding.py +38 -38
  33. lollms_client/lollms_ttm_binding.py +38 -42
  34. lollms_client/lollms_tts_binding.py +43 -18
  35. lollms_client/lollms_ttv_binding.py +38 -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 +464 -803
  40. lollms_client/tti_bindings/diffusers/server/main.py +1062 -0
  41. lollms_client/tti_bindings/gemini/__init__.py +182 -239
  42. lollms_client/tti_bindings/leonardo_ai/__init__.py +6 -3
  43. lollms_client/tti_bindings/lollms/__init__.py +4 -1
  44. lollms_client/tti_bindings/novita_ai/__init__.py +5 -2
  45. lollms_client/tti_bindings/openai/__init__.py +10 -11
  46. lollms_client/tti_bindings/stability_ai/__init__.py +5 -3
  47. lollms_client/ttm_bindings/audiocraft/__init__.py +7 -12
  48. lollms_client/ttm_bindings/beatoven_ai/__init__.py +7 -3
  49. lollms_client/ttm_bindings/lollms/__init__.py +4 -17
  50. lollms_client/ttm_bindings/replicate/__init__.py +7 -4
  51. lollms_client/ttm_bindings/stability_ai/__init__.py +7 -4
  52. lollms_client/ttm_bindings/topmediai/__init__.py +6 -3
  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.5.6.dist-info → lollms_client-1.7.13.dist-info}/METADATA +113 -5
  59. lollms_client-1.7.13.dist-info/RECORD +90 -0
  60. lollms_client-1.5.6.dist-info/RECORD +0 -87
  61. {lollms_client-1.5.6.dist-info → lollms_client-1.7.13.dist-info}/WHEEL +0 -0
  62. {lollms_client-1.5.6.dist-info → lollms_client-1.7.13.dist-info}/licenses/LICENSE +0 -0
  63. {lollms_client-1.5.6.dist-info → lollms_client-1.7.13.dist-info}/top_level.txt +0 -0
@@ -1,843 +1,504 @@
1
- # lollms_client/tti_bindings/diffusers/__init__.py
2
1
  import os
3
- import importlib
4
- from io import BytesIO
5
- from typing import Optional, List, Dict, Any, Union, Tuple
6
- from pathlib import Path
2
+ import sys
7
3
  import base64
8
- import pipmaster as pm
9
- import threading
10
- import queue
11
- from concurrent.futures import Future
12
- import time
13
- import hashlib
14
4
  import requests
15
- from tqdm import tqdm
5
+ import subprocess
6
+ import time
16
7
  import json
17
- import shutil
18
- from lollms_client.lollms_tti_binding import LollmsTTIBinding
19
- from ascii_colors import trace_exception, ASCIIColors
8
+ from io import BytesIO
9
+ from pathlib import Path
10
+ from typing import Optional, List, Dict, Any, Union, Callable
20
11
 
21
- pm.ensure_packages(["torch","torchvision"],index_url="https://download.pytorch.org/whl/cu126")
22
- pm.ensure_packages(["diffusers","pillow","transformers","safetensors","requests","tqdm"])
12
+ # Ensure pipmaster is available.
13
+ try:
14
+ import pipmaster as pm
15
+ except ImportError:
16
+ print("FATAL: pipmaster is not installed. Please install it using: pip install pipmaster")
17
+ sys.exit(1)
23
18
 
19
+ # Ensure filelock is available for process-safe server startup.
24
20
  try:
25
- import torch
26
- from diffusers import (
27
- AutoPipelineForText2Image,
28
- AutoPipelineForImage2Image,
29
- AutoPipelineForInpainting,
30
- DiffusionPipeline,
31
- StableDiffusionPipeline,
32
-
33
- )
34
- from diffusers.utils import load_image
35
- from PIL import Image
36
- DIFFUSERS_AVAILABLE = True
21
+ from filelock import FileLock, Timeout
37
22
  except ImportError:
38
- torch = None
39
- AutoPipelineForText2Image = None
40
- AutoPipelineForImage2Image = None
41
- AutoPipelineForInpainting = None
42
- DiffusionPipeline = None
43
- StableDiffusionPipeline = None
44
- Image = None
45
- load_image = None
46
- DIFFUSERS_AVAILABLE = False
47
-
48
- BindingName = "DiffusersTTIBinding_Impl"
49
-
50
- CIVITAI_MODELS = {
51
- "realistic-vision-v6": {
52
- "display_name": "Realistic Vision V6.0",
53
- "url": "https://civitai.com/api/download/models/501240?type=Model&format=SafeTensor&size=pruned&fp=fp16",
54
- "filename": "realisticVisionV60_v60B1.safetensors",
55
- "description": "Photorealistic SD1.5 checkpoint.",
56
- "owned_by": "civitai"
57
- },
58
- "absolute-reality": {
59
- "display_name": "Absolute Reality",
60
- "url": "https://civitai.com/api/download/models/132760?type=Model&format=SafeTensor&size=pruned&fp=fp16",
61
- "filename": "absolutereality_v181.safetensors",
62
- "description": "General realistic SD1.5.",
63
- "owned_by": "civitai"
64
- },
65
- "dreamshaper-8": {
66
- "display_name": "DreamShaper 8",
67
- "url": "https://civitai.com/api/download/models/128713",
68
- "filename": "dreamshaper_8.safetensors",
69
- "description": "Versatile SD1.5 style model.",
70
- "owned_by": "civitai"
71
- },
72
- "juggernaut-xl": {
73
- "display_name": "Juggernaut XL",
74
- "url": "https://civitai.com/api/download/models/133005",
75
- "filename": "juggernautXL_version6Rundiffusion.safetensors",
76
- "description": "Artistic SDXL.",
77
- "owned_by": "civitai"
78
- },
79
- "lyriel-v1.6": {
80
- "display_name": "Lyriel v1.6",
81
- "url": "https://civitai.com/api/download/models/72396?type=Model&format=SafeTensor&size=full&fp=fp16",
82
- "filename": "lyriel_v16.safetensors",
83
- "description": "Fantasy/stylized SD1.5.",
84
- "owned_by": "civitai"
85
- },
86
- "ui_icons": {
87
- "display_name": "UI Icons",
88
- "url": "https://civitai.com/api/download/models/367044?type=Model&format=SafeTensor&size=full&fp=fp16",
89
- "filename": "uiIcons_v10.safetensors",
90
- "description": "A model for generating UI icons.",
91
- "owned_by": "civitai"
92
- },
93
- "meinamix": {
94
- "display_name": "MeinaMix",
95
- "url": "https://civitai.com/api/download/models/948574?type=Model&format=SafeTensor&size=pruned&fp=fp16",
96
- "filename": "meinamix_meinaV11.safetensors",
97
- "description": "Anime/illustration SD1.5.",
98
- "owned_by": "civitai"
99
- },
100
- "rpg-v5": {
101
- "display_name": "RPG v5",
102
- "url": "https://civitai.com/api/download/models/124626?type=Model&format=SafeTensor&size=pruned&fp=fp16",
103
- "filename": "rpg_v5.safetensors",
104
- "description": "RPG assets SD1.5.",
105
- "owned_by": "civitai"
106
- },
107
- "pixel-art-xl": {
108
- "display_name": "Pixel Art XL",
109
- "url": "https://civitai.com/api/download/models/135931?type=Model&format=SafeTensor",
110
- "filename": "pixelartxl_v11.safetensors",
111
- "description": "Pixel art SDXL.",
112
- "owned_by": "civitai"
113
- },
114
- "lowpoly-world": {
115
- "display_name": "Lowpoly World",
116
- "url": "https://civitai.com/api/download/models/146502?type=Model&format=SafeTensor",
117
- "filename": "LowpolySDXL.safetensors",
118
- "description": "Lowpoly style SD1.5.",
119
- "owned_by": "civitai"
120
- },
121
- "toonyou": {
122
- "display_name": "ToonYou",
123
- "url": "https://civitai.com/api/download/models/125771?type=Model&format=SafeTensor&size=pruned&fp=fp16",
124
- "filename": "toonyou_beta6.safetensors",
125
- "description": "Cartoon/Disney SD1.5.",
126
- "owned_by": "civitai"
127
- },
128
- "papercut": {
129
- "display_name": "Papercut",
130
- "url": "https://civitai.com/api/download/models/133503?type=Model&format=SafeTensor",
131
- "filename": "papercut.safetensors",
132
- "description": "Paper cutout SD1.5.",
133
- "owned_by": "civitai"
134
- },
135
- "fantassifiedIcons": {
136
- "display_name": "Fantassified Icons",
137
- "url": "https://civitai.com/api/download/models/67584?type=Model&format=SafeTensor&size=pruned&fp=fp16",
138
- "filename": "fantassifiedIcons_fantassifiedIconsV20.safetensors",
139
- "description": "Flat, modern Icons.",
140
- "owned_by": "civitai"
141
- },
142
- "game_icon_institute": {
143
- "display_name": "Game icon institute",
144
- "url": "https://civitai.com/api/download/models/158776?type=Model&format=SafeTensor&size=full&fp=fp16",
145
- "filename": "gameIconInstituteV10_v10.safetensors",
146
- "description": "Flat, modern game Icons.",
147
- "owned_by": "civitai"
148
- },
149
- "M4RV3LS_DUNGEONS": {
150
- "display_name": "M4RV3LS & DUNGEONS",
151
- "url": "https://civitai.com/api/download/models/139417?type=Model&format=SafeTensor&size=pruned&fp=fp16",
152
- "filename": "M4RV3LSDUNGEONSNEWV40COMICS_mD40.safetensors",
153
- "description": "comics.",
154
- "owned_by": "civitai"
155
- },
156
- }
157
-
158
- TORCH_DTYPE_MAP_STR_TO_OBJ = {
159
- "float16": getattr(torch, 'float16', 'float16'),
160
- "bfloat16": getattr(torch, 'bfloat16', 'bfloat16'),
161
- "float32": getattr(torch, 'float32', 'float32'),
162
- "auto": "auto"
163
- }
164
- TORCH_DTYPE_MAP_OBJ_TO_STR = {v: k for k, v in TORCH_DTYPE_MAP_STR_TO_OBJ.items()}
165
- if torch:
166
- TORCH_DTYPE_MAP_OBJ_TO_STR[None] = "None"
167
-
168
- SCHEDULER_MAPPING = {
169
- "default": None,
170
- "ddim": "DDIMScheduler",
171
- "ddpm": "DDPMScheduler",
172
- "deis_multistep": "DEISMultistepScheduler",
173
- "dpm_multistep": "DPMSolverMultistepScheduler",
174
- "dpm_multistep_karras": "DPMSolverMultistepScheduler",
175
- "dpm_single": "DPMSolverSinglestepScheduler",
176
- "dpm_adaptive": "DPMSolverPlusPlusScheduler",
177
- "dpm++_2m": "DPMSolverMultistepScheduler",
178
- "dpm++_2m_karras": "DPMSolverMultistepScheduler",
179
- "dpm++_2s_ancestral": "DPMSolverAncestralDiscreteScheduler",
180
- "dpm++_2s_ancestral_karras": "DPMSolverAncestralDiscreteScheduler",
181
- "dpm++_sde": "DPMSolverSDEScheduler",
182
- "dpm++_sde_karras": "DPMSolverSDEScheduler",
183
- "euler_ancestral_discrete": "EulerAncestralDiscreteScheduler",
184
- "euler_discrete": "EulerDiscreteScheduler",
185
- "heun_discrete": "HeunDiscreteScheduler",
186
- "heun_karras": "HeunDiscreteScheduler",
187
- "lms_discrete": "LMSDiscreteScheduler",
188
- "lms_karras": "LMSDiscreteScheduler",
189
- "pndm": "PNDMScheduler",
190
- "unipc_multistep": "UniPCMultistepScheduler",
191
- "dpm++_2m_sde": "DPMSolverMultistepScheduler",
192
- "dpm++_2m_sde_karras": "DPMSolverMultistepScheduler",
193
- "dpm2": "KDPM2DiscreteScheduler",
194
- "dpm2_karras": "KDPM2DiscreteScheduler",
195
- "dpm2_a": "KDPM2AncestralDiscreteScheduler",
196
- "dpm2_a_karras": "KDPM2AncestralDiscreteScheduler",
197
- "euler": "EulerDiscreteScheduler",
198
- "euler_a": "EulerAncestralDiscreteScheduler",
199
- "heun": "HeunDiscreteScheduler",
200
- "lms": "LMSDiscreteScheduler"
201
- }
202
- SCHEDULER_USES_KARRAS_SIGMAS = [
203
- "dpm_multistep_karras","dpm++_2m_karras","dpm++_2s_ancestral_karras",
204
- "dpm++_sde_karras","heun_karras","lms_karras",
205
- "dpm++_2m_sde_karras","dpm2_karras","dpm2_a_karras"
206
- ]
207
-
208
- class ModelManager:
209
- def __init__(self, config: Dict[str, Any], models_path: Path):
210
- self.config = config
211
- self.models_path = models_path
212
- self.pipeline: Optional[DiffusionPipeline] = None
213
- self.current_task: Optional[str] = None
214
- self.ref_count = 0
215
- self.lock = threading.Lock()
216
- self.queue = queue.Queue()
217
- self.is_loaded = False
218
- self.last_used_time = time.time()
219
- self._stop_event = threading.Event()
220
- self.worker_thread = threading.Thread(target=self._generation_worker, daemon=True)
221
- self.worker_thread.start()
222
- self._stop_monitor_event = threading.Event()
223
- self._unload_monitor_thread = None
224
- self._start_unload_monitor()
225
-
226
- def acquire(self):
227
- with self.lock:
228
- self.ref_count += 1
229
- return self
230
-
231
- def release(self):
232
- with self.lock:
233
- self.ref_count -= 1
234
- return self.ref_count
235
-
236
- def stop(self):
237
- self._stop_event.set()
238
- if self._unload_monitor_thread:
239
- self._stop_monitor_event.set()
240
- self._unload_monitor_thread.join(timeout=2)
241
- self.queue.put(None)
242
- self.worker_thread.join(timeout=5)
243
-
244
- def _start_unload_monitor(self):
245
- unload_after = self.config.get("unload_inactive_model_after", 0)
246
- if unload_after > 0 and self._unload_monitor_thread is None:
247
- self._stop_monitor_event.clear()
248
- self._unload_monitor_thread = threading.Thread(target=self._unload_monitor, daemon=True)
249
- self._unload_monitor_thread.start()
250
-
251
- def _unload_monitor(self):
252
- unload_after = self.config.get("unload_inactive_model_after", 0)
253
- if unload_after <= 0:
254
- return
255
- ASCIIColors.info(f"Starting inactivity monitor for '{self.config['model_name']}' (timeout: {unload_after}s).")
256
- while not self._stop_monitor_event.wait(timeout=5.0):
257
- with self.lock:
258
- if not self.is_loaded:
259
- continue
260
- if time.time() - self.last_used_time > unload_after:
261
- ASCIIColors.info(f"Model '{self.config['model_name']}' has been inactive. Unloading.")
262
- self._unload_pipeline()
263
-
264
- def _resolve_model_path(self, model_name: str) -> Union[str, Path]:
265
- path_obj = Path(model_name)
266
- if path_obj.is_absolute() and path_obj.exists():
267
- return model_name
268
- if model_name in CIVITAI_MODELS:
269
- filename = CIVITAI_MODELS[model_name]["filename"]
270
- local_path = self.models_path / filename
271
- if not local_path.exists():
272
- self._download_civitai_model(model_name)
273
- return local_path
274
- local_path = self.models_path / model_name
275
- if local_path.exists():
276
- return local_path
277
- return model_name
278
-
279
- def _download_civitai_model(self, model_key: str):
280
- model_info = CIVITAI_MODELS[model_key]
281
- url = model_info["url"]
282
- filename = model_info["filename"]
283
- dest_path = self.models_path / filename
284
- temp_path = dest_path.with_suffix(".temp")
285
- ASCIIColors.cyan(f"Downloading '{filename}' from Civitai...")
286
- try:
287
- with requests.get(url, stream=True) as r:
288
- r.raise_for_status()
289
- total_size = int(r.headers.get('content-length', 0))
290
- with open(temp_path, 'wb') as f, tqdm(total=total_size, unit='iB', unit_scale=True, desc=f"Downloading {filename}") as bar:
291
- for chunk in r.iter_content(chunk_size=8192):
292
- f.write(chunk)
293
- bar.update(len(chunk))
294
- shutil.move(temp_path, dest_path)
295
- ASCIIColors.green(f"Model '{filename}' downloaded successfully.")
296
- except Exception as e:
297
- if temp_path.exists():
298
- temp_path.unlink()
299
- raise Exception(f"Failed to download model {filename}: {e}") from e
23
+ print("FATAL: The 'filelock' library is required. Please install it by running: pip install filelock")
24
+ sys.exit(1)
300
25
 
301
- def _set_scheduler(self):
302
- if not self.pipeline:
303
- return
304
- scheduler_name_key = self.config["scheduler_name"].lower()
305
- if scheduler_name_key == "default":
26
+ from lollms_client.lollms_tti_binding import LollmsTTIBinding
27
+ from ascii_colors import ASCIIColors
28
+
29
+ BindingName = "DiffusersTTIBinding"
30
+
31
+ class DiffusersTTIBinding(LollmsTTIBinding):
32
+ def __init__(self, **kwargs):
33
+ # Prioritize 'model_name' but accept 'model' as an alias from config files.
34
+ if 'model' in kwargs and 'model_name' not in kwargs:
35
+ kwargs['model_name'] = kwargs.pop('model')
36
+ super().__init__(binding_name=BindingName, config=kwargs)
37
+
38
+ self.config = kwargs
39
+ self.host = kwargs.get("host", "localhost")
40
+ self.port = kwargs.get("port", 9632)
41
+ self.auto_start_server = kwargs.get("auto_start_server", False)
42
+ self.wait_for_server = kwargs.get("wait_for_server", False)
43
+ self.server_process = None
44
+ self.base_url = f"http://{self.host}:{self.port}"
45
+ self.binding_root = Path(__file__).parent
46
+ self.server_dir = self.binding_root / "server"
47
+ self.venv_dir = Path("./venv/tti_diffusers_venv")
48
+ self.models_path = Path(kwargs.get("models_path", "./data/models/diffusers_models")).resolve()
49
+ self.extra_models_path = kwargs.get("extra_models_path")
50
+ self.hf_token = kwargs.get("hf_token", "") # NEW
51
+ self.models_path.mkdir(exist_ok=True, parents=True)
52
+ if self.auto_start_server:
53
+ self.ensure_server_is_running(self.wait_for_server)
54
+
55
+
56
+ def is_server_running(self) -> bool:
57
+ """Checks if the server is already running and responsive."""
58
+ try:
59
+ response = requests.get(f"{self.base_url}/status", timeout=4)
60
+ if response.status_code == 200 and response.json().get("status") == "running":
61
+ return True
62
+ except requests.exceptions.RequestException:
63
+ return False
64
+ return False
65
+
66
+
67
+ def ensure_server_is_running(self, wait= False):
68
+ """
69
+ Ensures the Diffusers server is running. If not, it attempts to start it
70
+ in a process-safe manner using a file lock. This method is designed to
71
+ prevent race conditions in multi-worker environments.
72
+ """
73
+ self.server_dir.mkdir(exist_ok=True)
74
+ ASCIIColors.info("Attempting to start or connect to the Diffusers server...")
75
+
76
+ # First, perform a quick check without the lock to avoid unnecessary waiting.
77
+ if self.is_server_running():
78
+ ASCIIColors.green("Diffusers Server is already running and responsive.")
306
79
  return
307
- scheduler_class_name = SCHEDULER_MAPPING.get(scheduler_name_key)
308
- if scheduler_class_name:
80
+ else:
81
+ self.start_server(wait)
82
+
83
+ def install_server_dependencies(self):
84
+ """
85
+ Installs the server's dependencies into a dedicated virtual environment
86
+ using pipmaster, which handles complex packages like PyTorch.
87
+ """
88
+ ASCIIColors.info(f"Setting up virtual environment in: {self.venv_dir}")
89
+ pm_v = pm.PackageManager(venv_path=str(self.venv_dir))
90
+
91
+ # --- PyTorch Installation ---
92
+ ASCIIColors.info(f"Installing server dependencies")
93
+ pm_v.ensure_packages([
94
+ "requests", "uvicorn", "fastapi", "python-multipart", "filelock"
95
+ ])
96
+ ASCIIColors.info(f"Installing parisneo libraries")
97
+ pm_v.ensure_packages([
98
+ "ascii_colors","pipmaster"
99
+ ])
100
+ ASCIIColors.info(f"Installing misc libraries (numpy, tqdm...)")
101
+ pm_v.ensure_packages([
102
+ "tqdm", "numpy"
103
+ ])
104
+ ASCIIColors.info(f"Installing Pillow")
105
+ pm_v.ensure_packages([
106
+ "pillow"
107
+ ])
108
+
109
+ ASCIIColors.info(f"Installing pytorch")
110
+ torch_index_url = None
111
+ if sys.platform == "win32":
309
112
  try:
310
- SchedulerClass = getattr(importlib.import_module("diffusers.schedulers"), scheduler_class_name)
311
- scheduler_config = self.pipeline.scheduler.config
312
- scheduler_config["use_karras_sigmas"] = scheduler_name_key in SCHEDULER_USES_KARRAS_SIGMAS
313
- self.pipeline.scheduler = SchedulerClass.from_config(scheduler_config)
314
- ASCIIColors.info(f"Switched scheduler to {scheduler_class_name}")
315
- except Exception as e:
316
- ASCIIColors.warning(f"Could not switch scheduler to {scheduler_name_key}: {e}. Using current default.")
317
-
318
- def _load_pipeline_for_task(self, task: str):
319
- if self.pipeline and self.current_task == task:
320
- return
321
- if self.pipeline:
322
- self._unload_pipeline()
323
- model_name = self.config.get("model_name", "")
324
- if not model_name:
325
- raise ValueError("Model name cannot be empty for loading.")
326
- ASCIIColors.info(f"Loading Diffusers model: {model_name} for task: {task}")
327
- model_path = self._resolve_model_path(model_name)
328
- torch_dtype = TORCH_DTYPE_MAP_STR_TO_OBJ.get(self.config["torch_dtype_str"].lower())
113
+ # Use nvidia-smi to detect CUDA
114
+ result = subprocess.run(["nvidia-smi"], capture_output=True, text=True, check=True)
115
+ ASCIIColors.green("NVIDIA GPU detected. Installing CUDA-enabled PyTorch.")
116
+ # Using a common and stable CUDA version. Adjust if needed.
117
+ torch_index_url = "https://download.pytorch.org/whl/cu128"
118
+ except (FileNotFoundError, subprocess.CalledProcessError):
119
+ 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.")
120
+
121
+ # Base packages including torch. pm.ensure_packages handles verbose output.
122
+ pm_v.ensure_packages(["torch", "torchvision"], index_url=torch_index_url)
123
+
124
+ # Standard dependencies
125
+ ASCIIColors.info(f"Installing transformers dependencies")
126
+ pm_v.ensure_packages([
127
+ "transformers", "safetensors", "accelerate"
128
+ ])
129
+ ASCIIColors.info(f"[Optional] Installing xformers")
329
130
  try:
330
- load_args = {}
331
- if self.config.get("hf_cache_path"):
332
- load_args["cache_dir"] = str(self.config["hf_cache_path"])
333
- if str(model_path).endswith(".safetensors"):
334
- if task == "text2image":
335
- try:
336
- self.pipeline = AutoPipelineForText2Image.from_single_file(model_path, torch_dtype=torch_dtype, cache_dir=load_args.get("cache_dir"))
337
- except AttributeError:
338
- self.pipeline = StableDiffusionPipeline.from_single_file(model_path, torch_dtype=torch_dtype, cache_dir=load_args.get("cache_dir"))
339
- elif task == "image2image":
340
- self.pipeline = AutoPipelineForImage2Image.from_single_file(model_path, torch_dtype=torch_dtype, cache_dir=load_args.get("cache_dir"))
341
- elif task == "inpainting":
342
- self.pipeline = AutoPipelineForInpainting.from_single_file(model_path, torch_dtype=torch_dtype, cache_dir=load_args.get("cache_dir"))
343
- else:
344
- common_args = {
345
- "torch_dtype": torch_dtype,
346
- "use_safetensors": self.config["use_safetensors"],
347
- "token": self.config["hf_token"],
348
- "local_files_only": self.config["local_files_only"]
349
- }
350
- if self.config["hf_variant"]:
351
- common_args["variant"] = self.config["hf_variant"]
352
- if not self.config["safety_checker_on"]:
353
- common_args["safety_checker"] = None
354
- if self.config.get("hf_cache_path"):
355
- common_args["cache_dir"] = str(self.config["hf_cache_path"])
356
- if task == "text2image":
357
- self.pipeline = AutoPipelineForText2Image.from_pretrained(model_path, **common_args)
358
- elif task == "image2image":
359
- self.pipeline = AutoPipelineForImage2Image.from_pretrained(model_path, **common_args)
360
- elif task == "inpainting":
361
- self.pipeline = AutoPipelineForInpainting.from_pretrained(model_path, **common_args)
362
- except Exception as e:
363
- error_str = str(e).lower()
364
- if "401" in error_str or "gated" in error_str or "authorization" in error_str:
365
- msg = (
366
- f"AUTHENTICATION FAILED for model '{model_name}'. "
367
- "Please ensure you accepted the model license and provided a valid HF token."
368
- )
369
- raise RuntimeError(msg) from e
370
- raise e
371
- self._set_scheduler()
372
- self.pipeline.to(self.config["device"])
373
- if self.config["enable_xformers"]:
374
- try:
375
- self.pipeline.enable_xformers_memory_efficient_attention()
376
- except Exception as e:
377
- ASCIIColors.warning(f"Could not enable xFormers: {e}.")
378
- if self.config["enable_cpu_offload"] and self.config["device"] != "cpu":
379
- self.pipeline.enable_model_cpu_offload()
380
- elif self.config["enable_sequential_cpu_offload"] and self.config["device"] != "cpu":
381
- self.pipeline.enable_sequential_cpu_offload()
382
- self.is_loaded = True
383
- self.current_task = task
384
- self.last_used_time = time.time()
385
- ASCIIColors.green(f"Model '{model_name}' loaded successfully on '{self.config['device']}' for task '{task}'.")
386
-
387
- def _unload_pipeline(self):
388
- if self.pipeline:
389
- model_name = self.config.get('model_name', 'Unknown')
390
- del self.pipeline
391
- self.pipeline = None
392
- if torch and torch.cuda.is_available():
393
- torch.cuda.empty_cache()
394
- self.is_loaded = False
395
- self.current_task = None
396
- ASCIIColors.info(f"Model '{model_name}' unloaded and VRAM cleared.")
397
-
398
- def _generation_worker(self):
399
- while not self._stop_event.is_set():
400
- try:
401
- job = self.queue.get(timeout=1)
402
- if job is None:
403
- break
404
- future, task, pipeline_args = job
131
+ pm_v.ensure_packages([
132
+ "xformers"
133
+ ])
134
+ except:
135
+ pass
136
+ # Git-based diffusers to get the latest version
137
+ ASCIIColors.info(f"Installing diffusers library from github")
138
+ pm_v.ensure_packages([
139
+ {
140
+ "name": "diffusers",
141
+ "vcs": "git+https://github.com/huggingface/diffusers.git",
142
+ "condition": ">=0.35.1"
143
+ }
144
+ ])
145
+
146
+ ASCIIColors.green("Server dependencies are satisfied.")
147
+
148
+ def start_server(self, wait=True, timeout_s=20):
149
+ """
150
+ Launches the FastAPI server in a background thread and returns immediately.
151
+ This method should only be called from within a file lock.
152
+ """
153
+ import threading
154
+
155
+
156
+ def _start_server_background():
157
+ """Helper method to start the server in a background thread."""
158
+ # Use a lock file in the binding's server directory for consistency across instances
159
+ lock_path = self.server_dir / "diffusers_server.lock"
160
+ lock = FileLock(lock_path)
161
+ with lock.acquire(timeout=0):
405
162
  try:
406
- with self.lock:
407
- self.last_used_time = time.time()
408
- if not self.is_loaded or self.current_task != task:
409
- self._load_pipeline_for_task(task)
410
- with torch.no_grad():
411
- output = self.pipeline(**pipeline_args)
412
- pil = output.images[0]
413
- buf = BytesIO()
414
- pil.save(buf, format="PNG")
415
- future.set_result(buf.getvalue())
163
+ server_script = self.server_dir / "main.py"
164
+ if not server_script.exists():
165
+ # Fallback for old structure
166
+ server_script = self.binding_root / "server.py"
167
+ if not server_script.exists():
168
+ raise FileNotFoundError(f"Server script not found at {server_script}. Make sure it's in a 'server' subdirectory.")
169
+ if not self.venv_dir.exists():
170
+ self.install_server_dependencies()
171
+
172
+ if sys.platform == "win32":
173
+ python_executable = self.venv_dir / "Scripts" / "python.exe"
174
+ else:
175
+ python_executable = self.venv_dir / "bin" / "python"
176
+
177
+ command = [
178
+ str(python_executable),
179
+ str(server_script),
180
+ "--host", self.host,
181
+ "--port", str(self.port),
182
+ "--models-path", str(self.models_path.resolve())
183
+ ]
184
+
185
+ if self.extra_models_path:
186
+ resolved_extra_path = Path(self.extra_models_path).resolve()
187
+ command.extend(["--extra-models-path", str(resolved_extra_path)])
188
+
189
+ if self.hf_token:
190
+ command.extend(["--hf-token", self.hf_token])
191
+
192
+ if self.extra_models_path:
193
+ resolved_extra_path = Path(self.extra_models_path).resolve()
194
+ command.extend(["--extra-models-path", str(resolved_extra_path)])
195
+
196
+ creationflags = subprocess.DETACHED_PROCESS if sys.platform == "win32" else 0
197
+ self.server_process = subprocess.Popen(command, creationflags=creationflags)
198
+ ASCIIColors.info("Diffusers server process launched in the background.")
199
+ while(not self.is_server_running()):
200
+ time.sleep(1)
201
+
416
202
  except Exception as e:
417
- trace_exception(e)
418
- future.set_exception(e)
419
- finally:
420
- self.queue.task_done()
421
- except queue.Empty:
422
- continue
423
-
424
- class PipelineRegistry:
425
- _instance = None
426
- _lock = threading.Lock()
427
- def __new__(cls, *args, **kwargs):
428
- with cls._lock:
429
- if cls._instance is None:
430
- cls._instance = super().__new__(cls)
431
- cls._instance._managers = {}
432
- cls._instance._registry_lock = threading.Lock()
433
- return cls._instance
434
- @staticmethod
435
- def _get_critical_keys():
436
- return [
437
- "model_name","device","torch_dtype_str","use_safetensors",
438
- "safety_checker_on","hf_variant","enable_cpu_offload",
439
- "enable_sequential_cpu_offload","enable_xformers",
440
- "local_files_only","hf_cache_path","unload_inactive_model_after"
441
- ]
442
- def _get_config_key(self, config: Dict[str, Any]) -> str:
443
- key_data = tuple(sorted((k, config.get(k)) for k in self._get_critical_keys()))
444
- return hashlib.sha256(str(key_data).encode('utf-8')).hexdigest()
445
- def get_manager(self, config: Dict[str, Any], models_path: Path) -> ModelManager:
446
- key = self._get_config_key(config)
447
- with self._registry_lock:
448
- if key not in self._managers:
449
- self._managers[key] = ModelManager(config.copy(), models_path)
450
- return self._managers[key].acquire()
451
- def release_manager(self, config: Dict[str, Any]):
452
- key = self._get_config_key(config)
453
- with self._registry_lock:
454
- if key in self._managers:
455
- manager = self._managers[key]
456
- ref_count = manager.release()
457
- if ref_count == 0:
458
- ASCIIColors.info(f"Reference count for model '{config.get('model_name')}' is zero. Cleaning up manager.")
459
- manager.stop()
460
- with manager.lock:
461
- manager._unload_pipeline()
462
- del self._managers[key]
463
- def get_active_managers(self) -> List[ModelManager]:
464
- with self._registry_lock:
465
- return [m for m in self._managers.values() if m.is_loaded]
466
-
467
- class DiffusersTTIBinding_Impl(LollmsTTIBinding):
468
- DEFAULT_CONFIG = {
469
- "model_name": "",
470
- "device": "auto",
471
- "torch_dtype_str": "auto",
472
- "use_safetensors": True,
473
- "scheduler_name": "default",
474
- "safety_checker_on": True,
475
- "num_inference_steps": 25,
476
- "guidance_scale": 7.0,
477
- "width": 512,
478
- "height": 512,
479
- "seed": -1,
480
- "enable_cpu_offload": False,
481
- "enable_sequential_cpu_offload": False,
482
- "enable_xformers": False,
483
- "hf_variant": None,
484
- "hf_token": None,
485
- "hf_cache_path": None,
486
- "local_files_only": False,
487
- "unload_inactive_model_after": 0
488
- }
489
- HF_DEFAULT_MODELS = [
490
- {"family": "SDXL", "model_name": "stabilityai/stable-diffusion-xl-base-1.0", "display_name": "SDXL Base 1.0", "desc": "Text2Image 1024 native."},
491
- {"family": "SDXL", "model_name": "stabilityai/stable-diffusion-xl-refiner-1.0", "display_name": "SDXL Refiner 1.0", "desc": "Refiner for SDXL."},
492
- {"family": "SD 1.x", "model_name": "runwayml/stable-diffusion-v1-5", "display_name": "Stable Diffusion 1.5", "desc": "Classic SD1.5."},
493
- {"family": "SD 2.x", "model_name": "stabilityai/stable-diffusion-2-1", "display_name": "Stable Diffusion 2.1", "desc": "SD2.1 base."},
494
- {"family": "SD3", "model_name": "stabilityai/stable-diffusion-3-medium-diffusers", "display_name": "Stable Diffusion 3 Medium", "desc": "SD3 medium."},
495
- {"family": "Qwen", "model_name": "Qwen/Qwen-Image", "display_name": "Qwen Image Edit", "desc": "Dedicated image generation."},
496
- {"family": "Specialized", "model_name": "playgroundai/playground-v2.5-1024px-aesthetic", "display_name": "Playground v2.5", "desc": "High aesthetic 1024."},
497
- {"family": "Editors", "model_name": "Qwen/Qwen-Image-Edit", "display_name": "Qwen Image Edit", "desc": "Dedicated image editing."}
498
- ]
203
+ ASCIIColors.error(f"Failed to start Diffusers server: {e}")
204
+ raise
205
+
206
+ # Start the server in a background thread
207
+ thread = threading.Thread(target=_start_server_background, daemon=True)
208
+ thread.start()
209
+ if wait:
210
+ thread.join()
211
+
212
+
213
+ def _wait_for_server(self, timeout=30):
214
+ """Waits for the server to become responsive."""
215
+ ASCIIColors.info("Waiting for Diffusers server to become available...")
216
+ start_time = time.time()
217
+ while time.time() - start_time < timeout:
218
+ if self.is_server_running():
219
+ ASCIIColors.green("Diffusers Server is up and running.")
220
+ # Set initial settings from the binding's config, but only if a model is specified.
221
+ if self.config.get("model_name"):
222
+ try:
223
+ ASCIIColors.info(f"Syncing initial client settings to server (model: {self.config['model_name']})...")
224
+ self.set_settings(self.config)
225
+ except Exception as e:
226
+ ASCIIColors.warning(f"Could not sync initial settings to server: {e}")
227
+ else:
228
+ ASCIIColors.warning("Client has no model_name configured, skipping initial settings sync.")
229
+ return
230
+ time.sleep(2)
231
+ raise RuntimeError("Failed to connect to the Diffusers server within the specified timeout.")
232
+
233
+ def _post_json_request(self, endpoint: str, data: Optional[dict] = None) -> requests.Response:
234
+ """Helper to make POST requests with a JSON body."""
235
+ try:
236
+ url = f"{self.base_url}{endpoint}"
237
+ response = requests.post(url, json=data, timeout=3600) # Long timeout for generation
238
+ response.raise_for_status()
239
+ return response
240
+ except requests.exceptions.RequestException as e:
241
+ ASCIIColors.error(f"Failed to communicate with Diffusers server at {url}.")
242
+ ASCIIColors.error(f"Error details: {e}")
243
+ if hasattr(e, 'response') and e.response:
244
+ try:
245
+ ASCIIColors.error(f"Server response: {e.response.json().get('detail', e.response.text)}")
246
+ except json.JSONDecodeError:
247
+ ASCIIColors.error(f"Server raw response: {e.response.text}")
248
+ raise RuntimeError("Communication with the Diffusers server failed.") from e
499
249
 
500
- def __init__(self, **kwargs):
501
- super().__init__(binding_name=BindingName)
502
- self.manager: Optional[ModelManager] = None
503
- if not DIFFUSERS_AVAILABLE:
504
- raise ImportError("Diffusers not available. Please install required packages.")
505
- self.config = self.DEFAULT_CONFIG.copy()
506
- self.config.update(kwargs)
507
- self.model_name = self.config.get("model_name", "")
250
+ def _post_multipart_request(self, endpoint: str, data: Optional[dict] = None, files: Optional[list] = None) -> requests.Response:
251
+ """Helper to make multipart/form-data POST requests for file uploads."""
252
+ try:
253
+ url = f"{self.base_url}{endpoint}"
254
+ response = requests.post(url, data=data, files=files, timeout=3600)
255
+ response.raise_for_status()
256
+ return response
257
+ except requests.exceptions.RequestException as e:
258
+ # (Error handling is the same as above)
259
+ ASCIIColors.error(f"Failed to communicate with Diffusers server at {url}.")
260
+ ASCIIColors.error(f"Error details: {e}")
261
+ if hasattr(e, 'response') and e.response:
262
+ try:
263
+ ASCIIColors.error(f"Server response: {e.response.json().get('detail', e.response.text)}")
264
+ except json.JSONDecodeError:
265
+ ASCIIColors.error(f"Server raw response: {e.response.text}")
266
+ raise RuntimeError("Communication with the Diffusers server failed.") from e
508
267
 
509
- models_path_str = kwargs.get("models_path", str(Path(__file__).parent / "models"))
510
- self.models_path = Path(models_path_str)
511
- self.models_path.mkdir(parents=True, exist_ok=True)
512
- self.registry = PipelineRegistry()
513
- self._resolve_device_and_dtype()
514
- if self.model_name:
515
- self._acquire_manager()
268
+ def _get_request(self, endpoint: str, params: Optional[dict] = None) -> requests.Response:
269
+ """Helper to make GET requests to the server."""
270
+ try:
271
+ url = f"{self.base_url}{endpoint}"
272
+ response = requests.get(url, params=params, timeout=60)
273
+ response.raise_for_status()
274
+ return response
275
+ except requests.exceptions.RequestException as e:
276
+ ASCIIColors.error(f"Failed to communicate with Diffusers server at {url}.")
277
+ raise RuntimeError("Communication with the Diffusers server failed.") from e
516
278
 
517
- def ps(self) -> List[dict]:
518
- if not self.registry:
519
- return []
279
+ def unload_model(self):
280
+ ASCIIColors.info("Requesting server to unload the current model...")
520
281
  try:
521
- active = self.registry.get_active_managers()
522
- out = []
523
- for m in active:
524
- with m.lock:
525
- cfg = m.config
526
- pipe = m.pipeline
527
- vram_usage_bytes = 0
528
- if torch.cuda.is_available() and cfg.get("device") == "cuda" and pipe:
529
- for comp in pipe.components.values():
530
- if hasattr(comp, 'parameters'):
531
- mem_params = sum(p.nelement() * p.element_size() for p in comp.parameters())
532
- mem_bufs = sum(b.nelement() * b.element_size() for b in comp.buffers())
533
- vram_usage_bytes += (mem_params + mem_bufs)
534
- out.append({
535
- "model_name": cfg.get("model_name"),
536
- "vram_size": vram_usage_bytes,
537
- "device": cfg.get("device"),
538
- "torch_dtype": str(pipe.dtype) if pipe else cfg.get("torch_dtype_str"),
539
- "pipeline_type": pipe.__class__.__name__ if pipe else "N/A",
540
- "scheduler_class": pipe.scheduler.__class__.__name__ if pipe and hasattr(pipe, 'scheduler') else "N/A",
541
- "status": "Active" if m.is_loaded else "Idle",
542
- "queue_size": m.queue.qsize(),
543
- "task": m.current_task or "N/A"
544
- })
545
- return out
282
+ self._post_json_request("/unload_model")
546
283
  except Exception as e:
547
- ASCIIColors.error(f"Failed to list running models: {e}")
548
- return []
284
+ ASCIIColors.warning(f"Could not send unload request to server: {e}")
285
+ pass
286
+
287
+ def generate_image(self, prompt: str, negative_prompt: str = "", **kwargs) -> bytes:
288
+ self.ensure_server_is_running(True)
289
+ params = kwargs.copy()
290
+ if "model_name" not in params and self.config.get("model_name"):
291
+ params["model_name"] = self.config["model_name"]
292
+
293
+ response = self._post_json_request("/generate_image", data={
294
+ "prompt": prompt,
295
+ "negative_prompt": negative_prompt,
296
+ "params": params
297
+ })
298
+ return response.content
299
+
300
+ def edit_image(self, images: Union[str, List[str], "Image.Image", List["Image.Image"]], prompt: str, **kwargs) -> bytes:
301
+ self.ensure_server_is_running(True)
302
+ images_b64 = []
303
+ if not isinstance(images, list):
304
+ images = [images]
305
+
306
+
307
+ for img in images:
308
+ # Case 1: Input is a PIL Image object
309
+ if hasattr(img, 'save'):
310
+ buffer = BytesIO()
311
+ img.save(buffer, format="PNG")
312
+ b64_string = base64.b64encode(buffer.getvalue()).decode('utf-8')
313
+ images_b64.append(b64_string)
314
+
315
+ # Case 2: Input is a string (could be path or already base64)
316
+ elif isinstance(img, str):
317
+ try:
318
+ b64_string = img.split(";base64,")[1] if ";base64," in img else img
319
+ base64.b64decode(b64_string) # Validate
320
+ images_b64.append(b64_string)
321
+ except Exception:
322
+ ASCIIColors.warning(f"Warning: A string input was not a valid file path or base64. Skipping.")
323
+ else:
324
+ raise ValueError(f"Unsupported image type in edit_image: {type(img)}")
325
+ if not images_b64:
326
+ raise ValueError("No valid images were provided to the edit_image function.")
327
+
328
+ params = kwargs.copy()
329
+ if "model_name" not in params and self.config.get("model_name"):
330
+ params["model_name"] = self.config["model_name"]
549
331
 
550
- def _acquire_manager(self):
551
- if self.manager:
552
- self.registry.release_manager(self.manager.config)
553
- self.manager = self.registry.get_manager(self.config, self.models_path)
554
- ASCIIColors.info(f"Binding instance acquired manager for '{self.config['model_name']}'.")
555
-
556
- def _resolve_device_and_dtype(self):
557
- if self.config["device"].lower() == "auto":
558
- self.config["device"] = "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu"
559
- if self.config["torch_dtype_str"].lower() == "auto":
560
- self.config["torch_dtype_str"] = "float16" if self.config["device"] != "cpu" else "float32"
561
-
562
- def _decode_image_input(self, item: str) -> Image.Image:
563
- s = item.strip()
564
- if s.startswith("data:image/") and ";base64," in s:
565
- b64 = s.split(";base64,")[-1]
566
- raw = base64.b64decode(b64)
567
- return Image.open(BytesIO(raw)).convert("RGB")
568
- if re_b64 := (s[:30].replace("\n","")):
569
- try:
570
- raw = base64.b64decode(s, validate=True)
571
- return Image.open(BytesIO(raw)).convert("RGB")
572
- except Exception:
573
- pass
574
- try:
575
- return load_image(s).convert("RGB")
576
- except Exception:
577
- return Image.open(s).convert("RGB")
332
+ # Translate "mask" to "mask_image" for server compatibility
333
+ if "mask" in params and params["mask"]:
334
+ params["mask_image"] = params.pop("mask")
578
335
 
579
- def _prepare_seed(self, kwargs: Dict[str, Any]) -> Optional[torch.Generator]:
580
- seed = kwargs.pop("seed", self.config["seed"])
581
- if seed == -1:
582
- return None
583
- return torch.Generator(device=self.config["device"]).manual_seed(seed)
336
+ json_payload = {
337
+ "prompt": prompt,
338
+ "images_b64": images_b64,
339
+ "params": params
340
+ }
341
+ response = self._post_json_request("/edit_image", data=json_payload)
342
+ return response.content
584
343
 
585
- def list_safetensor_models(self) -> List[str]:
586
- if not self.models_path.exists():
587
- return []
588
- return sorted([f.name for f in self.models_path.iterdir() if f.is_file() and f.suffix == ".safetensors"])
344
+ def list_models(self) -> list:
345
+ """
346
+ Lists only models that are available locally on disk.
589
347
 
590
- def listModels(self) -> list:
591
- civitai_list = [
592
- {'model_name': key, 'display_name': info['display_name'], 'description': info['description'], 'owned_by': info['owned_by']}
593
- for key, info in CIVITAI_MODELS.items()
594
- ]
595
- hf_list = [
596
- {'model_name': m["model_name"], 'display_name': m["display_name"], 'description': m["desc"], 'owned_by': 'HuggingFace', 'family': m["family"]}
597
- for m in self.HF_DEFAULT_MODELS
598
- ]
599
- custom_local = []
600
- civitai_filenames = {info['filename'] for info in CIVITAI_MODELS.values()}
601
- for filename in self.list_safetensor_models():
602
- if filename not in civitai_filenames:
603
- custom_local.append({'model_name': filename, 'display_name': filename, 'description': 'Local safetensors file.', 'owned_by': 'local_user'})
604
- return hf_list + civitai_list + custom_local
605
-
606
- def load_model(self):
607
- ASCIIColors.info("load_model() called. Loading is automatic on first use.")
608
- if self.model_name and not self.manager:
609
- self._acquire_manager()
348
+ The Diffusers server scans `models_path` and `extra_models_path` for:
349
+ - Diffusers pipeline folders (with model_index.json, etc.)
350
+ - .safetensors checkpoints and associated configs.
610
351
 
611
- def unload_model(self):
612
- if self.manager:
613
- ASCIIColors.info(f"Binding instance releasing manager for '{self.manager.config['model_name']}'.")
614
- self.registry.release_manager(self.manager.config)
615
- self.manager = None
616
-
617
- def generate_image(self, prompt: str, negative_prompt: str = "", width: int|None = None, height: int|None = None, **kwargs) -> bytes:
618
- if not self.model_name:
619
- raise RuntimeError("No model_name configured. Please select a model in settings.")
620
- if not self.manager:
621
- self._acquire_manager()
622
- generator = self._prepare_seed(kwargs)
623
- pipeline_args = {
624
- "prompt": prompt,
625
- "negative_prompt": negative_prompt or self.config.get("negative_prompt", ""),
626
- "width": width if width is not None else self.config.get("width", 512),
627
- "height": height if height is not None else self.config.get("height", 512),
628
- "num_inference_steps": kwargs.pop("num_inference_steps", self.config.get("num_inference_steps",25)),
629
- "guidance_scale": kwargs.pop("guidance_scale", self.config.get("guidance_scale",6.5)),
630
- "generator": generator
631
- }
632
- pipeline_args.update(kwargs)
633
- future = Future()
634
- self.manager.queue.put((future, "text2image", pipeline_args))
635
- ASCIIColors.info(f"Job (t2i) '{prompt[:50]}...' queued.")
636
- try:
637
- return future.result()
638
- except Exception as e:
639
- raise Exception(f"Image generation failed: {e}") from e
640
-
641
- def _encode_image_to_latents(self, pil: Image.Image, width: int, height: int) -> Tuple[torch.Tensor, Tuple[int,int]]:
642
- pil = pil.convert("RGB").resize((width, height))
643
- with self.manager.lock:
644
- self.manager._load_pipeline_for_task("text2image")
645
- vae = self.manager.pipeline.vae
646
- img = torch.from_numpy(torch.ByteTensor(bytearray(pil.tobytes())).numpy()).float() # not efficient but avoids np dep
647
- img = img.view(pil.height, pil.width, 3).permute(2,0,1).unsqueeze(0) / 255.0
648
- img = (img * 2.0) - 1.0
649
- img = img.to(self.config["device"], dtype=getattr(torch, self.config["torch_dtype_str"]))
650
- with torch.no_grad():
651
- posterior = vae.encode(img)
652
- latents = posterior.latent_dist.sample()
653
- sf = getattr(vae.config, "scaling_factor", 0.18215)
654
- latents = latents * sf
655
- return latents, (pil.width, pil.height)
656
-
657
- def edit_image(self,
658
- images: Union[str, List[str]],
659
- prompt: str,
660
- negative_prompt: Optional[str] = "",
661
- mask: Optional[str] = None,
662
- width: Optional[int] = None,
663
- height: Optional[int] = None,
664
- **kwargs) -> bytes:
665
- if not self.model_name:
666
- raise RuntimeError("No model_name configured. Please select a model in settings.")
667
- if not self.manager:
668
- self._acquire_manager()
669
- imgs = [images] if isinstance(images, str) else list(images)
670
- pil_images = [self._decode_image_input(s) for s in imgs]
671
- out_w = width if width is not None else self.config["width"]
672
- out_h = height if height is not None else self.config["height"]
673
- generator = self._prepare_seed(kwargs)
674
- steps = kwargs.pop("num_inference_steps", self.config["num_inference_steps"])
675
- guidance = kwargs.pop("guidance_scale", self.config["guidance_scale"])
676
- if mask is not None and len(pil_images) == 1:
677
- try:
678
- mask_img = self._decode_image_input(mask).convert("L")
679
- except Exception as e:
680
- raise ValueError(f"Failed to decode mask image: {e}") from e
681
- pipeline_args = {
682
- "image": pil_images[0],
683
- "mask_image": mask_img,
684
- "prompt": prompt,
685
- "negative_prompt": negative_prompt or None,
686
- "width": out_w,
687
- "height": out_h,
688
- "num_inference_steps": steps,
689
- "guidance_scale": guidance,
690
- "generator": generator
691
- }
692
- pipeline_args.update(kwargs)
693
- future = Future()
694
- self.manager.queue.put((future, "inpainting", pipeline_args))
695
- ASCIIColors.info("Job (inpaint) queued.")
696
- return future.result()
352
+ Returns list of dicts: {"model_name": str, "display_name": str, "description": str}
353
+ """
354
+ self.ensure_server_is_running(True)
697
355
  try:
698
- pipeline_args = {
699
- "image": pil_images if len(pil_images) > 1 else pil_images[0],
700
- "prompt": prompt,
701
- "negative_prompt": negative_prompt or None,
702
- "strength": kwargs.pop("strength", 0.6),
703
- "width": out_w,
704
- "height": out_h,
705
- "num_inference_steps": steps,
706
- "guidance_scale": guidance,
707
- "generator": generator
708
- }
709
- pipeline_args.update(kwargs)
710
- future = Future()
711
- self.manager.queue.put((future, "image2image", pipeline_args))
712
- ASCIIColors.info("Job (i2i) queued.")
713
- return future.result()
714
- except Exception:
715
- pass
716
- try:
717
- base = pil_images[0]
718
- latents, _ = self._encode_image_to_latents(base, out_w, out_h)
719
- pipeline_args = {
720
- "prompt": prompt,
721
- "negative_prompt": negative_prompt or None,
722
- "latents": latents,
723
- "num_inference_steps": steps,
724
- "guidance_scale": guidance,
725
- "generator": generator,
726
- "width": out_w,
727
- "height": out_h
728
- }
729
- pipeline_args.update(kwargs)
730
- future = Future()
731
- self.manager.queue.put((future, "text2image", pipeline_args))
732
- ASCIIColors.info("Job (t2i with init latents) queued.")
733
- return future.result()
356
+ response = self._get_request("/list_models")
357
+ data = response.json()
358
+ if not isinstance(data, list):
359
+ return []
360
+ return data
734
361
  except Exception as e:
735
- raise Exception(f"Image edit failed: {e}") from e
362
+ ASCIIColors.warning(f"Failed to list local Diffusers models: {e}")
363
+ return []
364
+
736
365
 
737
366
  def list_local_models(self) -> List[str]:
738
- if not self.models_path.exists():
739
- return []
740
- folders = [
741
- d.name for d in self.models_path.iterdir()
742
- if d.is_dir() and ((d / "model_index.json").exists() or (d / "unet" / "config.json").exists())
743
- ]
744
- safetensors = self.list_safetensor_models()
745
- return sorted(folders + safetensors)
367
+ self.ensure_server_is_running(True)
368
+ return self._get_request("/list_local_models").json()
746
369
 
747
370
  def list_available_models(self) -> List[str]:
748
- discoverable = [m['model_name'] for m in self.listModels()]
749
- local_models = self.list_local_models()
750
- return sorted(list(set(local_models + discoverable)))
371
+ self.ensure_server_is_running(True)
372
+ return self._get_request("/list_available_models").json()
751
373
 
752
374
  def list_services(self, **kwargs) -> List[Dict[str, str]]:
753
- models = self.list_available_models()
754
- local_models = self.list_local_models()
755
- if not models:
756
- return [{"name": "diffusers_no_models", "caption": "No models found", "help": f"Place models in '{self.models_path.resolve()}'."}]
757
- services = []
758
- for m in models:
759
- help_text = "Hugging Face model ID"
760
- if m in local_models:
761
- help_text = f"Local model from: {self.models_path.resolve()}"
762
- elif m in CIVITAI_MODELS:
763
- help_text = f"Civitai model (downloads as {CIVITAI_MODELS[m]['filename']})"
764
- services.append({"name": m, "caption": f"Diffusers: {m}", "help": help_text})
765
- return services
375
+ self.ensure_server_is_running(True)
376
+ return self._get_request("/list_models").json()
766
377
 
767
378
  def get_settings(self, **kwargs) -> List[Dict[str, Any]]:
768
- available_models = self.list_available_models()
379
+ self.ensure_server_is_running(True)
380
+ # The server holds the state, so we fetch it.
381
+ return self._get_request("/get_settings").json()
382
+
383
+ def get_zoo(self):
769
384
  return [
770
- {"name": "model_name", "type": "str", "value": self.model_name, "description": "Local, Civitai, or Hugging Face model.", "options": available_models},
771
- {"name": "unload_inactive_model_after", "type": "int", "value": self.config["unload_inactive_model_after"], "description": "Unload model after X seconds of inactivity (0 to disable)."},
772
- {"name": "device", "type": "str", "value": self.config["device"], "description": f"Inference device. Resolved: {self.config['device']}", "options": ["auto","cuda","mps","cpu"]},
773
- {"name": "torch_dtype_str", "type": "str", "value": self.config["torch_dtype_str"], "description": f"Torch dtype. Resolved: {self.config['torch_dtype_str']}", "options": ["auto","float16","bfloat16","float32"]},
774
- {"name": "hf_variant", "type": "str", "value": self.config["hf_variant"], "description": "HF model variant (e.g., 'fp16')."},
775
- {"name": "use_safetensors", "type": "bool", "value": self.config["use_safetensors"], "description": "Prefer .safetensors when loading from Hugging Face."},
776
- {"name": "scheduler_name", "type": "str", "value": self.config["scheduler_name"], "description": "Scheduler for diffusion.", "options": list(SCHEDULER_MAPPING.keys())},
777
- {"name": "safety_checker_on", "type": "bool", "value": self.config["safety_checker_on"], "description": "Enable the safety checker."},
778
- {"name": "enable_cpu_offload", "type": "bool", "value": self.config["enable_cpu_offload"], "description": "Enable model CPU offload (saves VRAM, slower)."},
779
- {"name": "enable_sequential_cpu_offload", "type": "bool", "value": self.config["enable_sequential_cpu_offload"], "description": "Enable sequential CPU offload."},
780
- {"name": "enable_xformers", "type": "bool", "value": self.config["enable_xformers"], "description": "Enable xFormers memory efficient attention."},
781
- {"name": "width", "type": "int", "value": self.config["width"], "description": "Default image width."},
782
- {"name": "height", "type": "int", "value": self.config["height"], "description": "Default image height."},
783
- {"name": "num_inference_steps", "type": "int", "value": self.config["num_inference_steps"], "description": "Default inference steps."},
784
- {"name": "guidance_scale", "type": "float", "value": self.config["guidance_scale"], "description": "Default guidance scale (CFG)."},
785
- {"name": "seed", "type": "int", "value": self.config["seed"], "description": "Default seed (-1 for random)."},
786
- {"name": "hf_token", "type": "str", "value": self.config["hf_token"], "description": "HF API token (for private/gated models).", "is_secret": True},
787
- {"name": "hf_cache_path", "type": "str", "value": self.config["hf_cache_path"], "description": "Path to HF cache."},
788
- {"name": "local_files_only", "type": "bool", "value": self.config["local_files_only"], "description": "Do not download from Hugging Face."}
385
+ {"name": "Stable Diffusion 1.5", "description": "The classic and versatile SD1.5 base model.", "size": "4GB", "type": "checkpoint", "link": "runwayml/stable-diffusion-v1-5"},
386
+ {"name": "Stable Diffusion 2.1", "description": "The 768x768 base model from the SD2.x series.", "size": "5GB", "type": "checkpoint", "link": "stabilityai/stable-diffusion-2-1"},
387
+ {"name": "Stable Diffusion XL 1.0", "description": "Official 1024x1024 text-to-image model from Stability AI.", "size": "7GB", "type": "checkpoint", "link": "stabilityai/stable-diffusion-xl-base-1.0"},
388
+ {"name": "SDXL Turbo", "description": "A fast, real-time text-to-image model based on SDXL.", "size": "7GB", "type": "checkpoint", "link": "stabilityai/sdxl-turbo"},
389
+ {"name": "Kandinsky 3", "description": "A powerful multilingual model with strong prompt understanding.", "size": "Unknown", "type": "checkpoint", "link": "kandinsky-community/kandinsky-3"},
390
+ {"name": "Playground v2.5", "description": "A high-quality model focused on aesthetic outputs.", "size": "Unknown", "type": "checkpoint", "link": "playgroundai/playground-v2.5-1024px-aesthetic"},
391
+ {"name": "epiCRealism", "description": "A popular community model for generating photorealistic images.", "size": "2GB", "type": "checkpoint", "link": "emilianJR/epiCRealism"},
392
+ {"name": "Realistic Vision 5.1", "description": "One of the most popular realistic models, great for portraits.", "size": "2GB", "type": "checkpoint", "link": "SG161222/Realistic_Vision_V5.1_noVAE"},
393
+ {"name": "Photon", "description": "A model known for high-quality, realistic images with good lighting.", "size": "2GB", "type": "checkpoint", "link": "Photon-v1"},
394
+ {"name": "Waifu Diffusion 1.4", "description": "A widely-used model for generating high-quality anime-style images.", "size": "2GB", "type": "checkpoint", "link": "hakurei/waifu-diffusion"},
395
+ {"name": "Counterfeit V3.0", "description": "A strong model for illustrative and 2.5D anime styles.", "size": "2GB", "type": "checkpoint", "link": "gsdf/Counterfeit-V3.0"},
396
+ {"name": "Animagine XL 3.0", "description": "A state-of-the-art anime model on the SDXL architecture.", "size": "7GB", "type": "checkpoint", "link": "cagliostrolab/animagine-xl-3.0"},
397
+ {"name": "DreamShaper 8", "description": "Versatile SD1.5 style model (CivitAI).", "size": "2GB", "type": "checkpoint", "link": "https://civitai.com/api/download/models/128713"},
398
+ {"name": "Juggernaut XL", "description": "Artistic SDXL (CivitAI).", "size": "7GB", "type": "checkpoint", "link": "https://civitai.com/api/download/models/133005"},
399
+ {"name": "Stable Diffusion 3 Medium", "description": "SOTA model with advanced prompt understanding (Gated).", "size": "Unknown", "type": "checkpoint", "link": "stabilityai/stable-diffusion-3-medium-diffusers"},
400
+ {"name": "FLUX.1 Schnell", "description": "Powerful and fast next-gen model (Gated).", "size": "Unknown", "type": "checkpoint", "link": "black-forest-labs/FLUX.1-schnell"},
401
+ {"name": "FLUX.1 Dev", "description": "Larger developer version of FLUX.1 (Gated).", "size": "Unknown", "type": "checkpoint", "link": "black-forest-labs/FLUX.1-dev"},
789
402
  ]
790
403
 
404
+ def download_from_zoo(self, index: int, progress_callback: Callable[[dict], None] = None) -> dict:
405
+ zoo = self.get_zoo()
406
+ if index < 0 or index >= len(zoo):
407
+ msg = "Index out of bounds"
408
+ ASCIIColors.error(msg)
409
+ return {"status": False, "message": msg}
410
+ item = zoo[index]
411
+ return self.pull_model(item["link"], progress_callback=progress_callback)
412
+
791
413
  def set_settings(self, settings: Union[Dict[str, Any], List[Dict[str, Any]]], **kwargs) -> bool:
792
- parsed = settings if isinstance(settings, dict) else {i["name"]: i["value"] for i in settings if "name" in i and "value" in i}
793
- critical_keys = self.registry._get_critical_keys()
794
- needs_swap = False
795
- for key, value in parsed.items():
796
- if self.config.get(key) != value:
797
- ASCIIColors.info(f"Setting '{key}' changed to: {value}")
798
- self.config[key] = value
799
- if key == "model_name":
800
- self.model_name = value
801
- if key in critical_keys:
802
- needs_swap = True
803
- if needs_swap and self.model_name:
804
- ASCIIColors.info("Critical settings changed. Swapping model manager...")
805
- self._resolve_device_and_dtype()
806
- self._acquire_manager()
807
- if not needs_swap and self.manager:
808
- self.manager.config.update(parsed)
809
- if 'scheduler_name' in parsed and self.manager.pipeline:
810
- with self.manager.lock:
811
- self.manager._set_scheduler()
812
- return True
414
+ self.ensure_server_is_running(True)
415
+ # Normalize settings from list of dicts to a single dict if needed
416
+ parsed_settings = settings if isinstance(settings, dict) else {s["name"]: s["value"] for s in settings if "name" in s and "value" in s}
417
+ response = self._post_json_request("/set_settings", data=parsed_settings)
418
+ return response.json().get("success", False)
419
+
420
+ def ps(self) -> List[dict]:
421
+ try:
422
+ return self._get_request("/ps").json()
423
+ except Exception:
424
+ return [{"error": "Could not connect to server to get process status."}]
425
+
426
+ def pull_model(self, model_name: str, local_name: Optional[str] = None, progress_callback: Callable[[dict], None] = None) -> dict:
427
+ """
428
+ Pulls a model from Hugging Face or URL via the server.
429
+ """
430
+ payload = {}
431
+ if model_name.startswith("http") and "huggingface.co" not in model_name:
432
+ # Assume direct file URL if not huggingface repo url (roughly)
433
+ if model_name.endswith(".safetensors"):
434
+ payload["safetensors_url"] = model_name
435
+ else:
436
+ payload["hf_id"] = model_name
437
+ else:
438
+ # Clean up URL if provided as https://huggingface.co/publisher/model
439
+ if "huggingface.co/" in model_name:
440
+ model_name = model_name.split("huggingface.co/")[-1]
441
+ payload["hf_id"] = model_name
442
+
443
+ if local_name:
444
+ payload["local_name"] = local_name
445
+
446
+ try:
447
+ if progress_callback:
448
+ progress_callback({"status": "starting", "message": f"Sending pull request for {model_name}..."})
449
+
450
+ ASCIIColors.info(f"Sending pull request for {model_name}...")
451
+ # Use a very long timeout as downloads can be huge (GBs)
452
+ response = requests.post(f"{self.base_url}/pull_model", json=payload, timeout=7200)
453
+ response.raise_for_status()
454
+
455
+ msg = "Model pulled successfully."
456
+ ASCIIColors.success(msg)
457
+ if progress_callback:
458
+ progress_callback({"status": "success", "message": msg, "completed": 100, "total": 100})
459
+ return {"status": True, "message": msg}
460
+ except Exception as e:
461
+ error_msg = f"Failed to pull model: {e}"
462
+ if hasattr(e, 'response') and e.response:
463
+ error_msg += f" Server response: {e.response.text}"
464
+ ASCIIColors.error(error_msg)
465
+ if progress_callback:
466
+ progress_callback({"status": "error", "message": error_msg})
467
+ return {"status": False, "message": error_msg}
468
+
469
+ def upgrade_diffusers(self, progress_callback: Callable[[dict], None] = None) -> dict:
470
+ """
471
+ Upgrades the diffusers library in the virtual environment.
472
+ """
473
+ try:
474
+ if progress_callback:
475
+ progress_callback({"status": "starting", "message": "Upgrading diffusers..."})
476
+
477
+ ASCIIColors.info("Upgrading diffusers from GitHub...")
478
+ if sys.platform == "win32":
479
+ python_executable = self.venv_dir / "Scripts" / "python.exe"
480
+ else:
481
+ python_executable = self.venv_dir / "bin" / "python"
482
+
483
+ subprocess.check_call([
484
+ str(python_executable), "-m", "pip", "install", "--upgrade",
485
+ "git+https://github.com/huggingface/diffusers.git"
486
+ ])
487
+ msg = "Diffusers upgraded successfully."
488
+ ASCIIColors.success(msg)
489
+ ASCIIColors.info("Please restart the application/server to apply changes.")
490
+
491
+ if progress_callback:
492
+ progress_callback({"status": "success", "message": msg})
493
+ return {"status": True, "message": msg}
494
+ except Exception as e:
495
+ error_msg = f"Failed to upgrade diffusers: {e}"
496
+ ASCIIColors.error(error_msg)
497
+ if progress_callback:
498
+ progress_callback({"status": "error", "message": error_msg})
499
+ return {"status": False, "message": error_msg}
813
500
 
814
501
  def __del__(self):
815
- self.unload_model()
816
-
817
- if __name__ == '__main__':
818
- ASCIIColors.magenta("--- Diffusers TTI Binding Test ---")
819
- if not DIFFUSERS_AVAILABLE:
820
- ASCIIColors.error("Diffusers not available. Cannot run test.")
821
- exit(1)
822
- temp_paths_dir = Path(__file__).parent / "tmp"
823
- temp_models_path = temp_paths_dir / "models"
824
- if temp_paths_dir.exists():
825
- shutil.rmtree(temp_paths_dir)
826
- temp_models_path.mkdir(parents=True, exist_ok=True)
827
- try:
828
- ASCIIColors.cyan("\n--- Test: Loading a small HF model ---")
829
- cfg = {"models_path": str(temp_models_path), "model_name": "hf-internal-testing/tiny-stable-diffusion-torch"}
830
- binding = DiffusersTTIBinding_Impl(**cfg)
831
- img_bytes = binding.generate_image("a tiny robot", width=64, height=64, num_inference_steps=2)
832
- assert len(img_bytes) > 1000
833
- ASCIIColors.green("HF t2i generation OK.")
834
- del binding
835
- time.sleep(0.1)
836
- except Exception as e:
837
- trace_exception(e)
838
- ASCIIColors.error(f"Diffusers binding test failed: {e}")
839
- finally:
840
- ASCIIColors.cyan("\nCleaning up temporary directories...")
841
- if temp_paths_dir.exists():
842
- shutil.rmtree(temp_paths_dir)
843
- ASCIIColors.magenta("--- Diffusers TTI Binding Test Finished ---")
502
+ # The client destructor does not stop the server,
503
+ # as it is a shared resource for all worker processes.
504
+ pass