lollms-client 1.0.0__py3-none-any.whl → 1.1.1__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.

@@ -4,693 +4,610 @@ import importlib
4
4
  from io import BytesIO
5
5
  from typing import Optional, List, Dict, Any, Union
6
6
  from pathlib import Path
7
-
8
-
9
- try:
10
- import pipmaster as pm
11
- import platform # For OS detection for torch index
12
-
13
- # Determine initial device preference to guide torch installation
14
- preferred_torch_device_for_install = "cpu" # Default assumption
15
-
16
- # Tentatively set preference based on OS, assuming user might want GPU if available
17
- if platform.system() == "Linux" or platform.system() == "Windows":
18
- # On Linux/Windows, CUDA is the primary GPU acceleration for PyTorch.
19
- # We will try to install a CUDA version of PyTorch.
20
- preferred_torch_device_for_install = "cuda"
21
- elif platform.system() == "Darwin":
22
- # On macOS, MPS is the acceleration. Standard torch install usually handles this.
23
- preferred_torch_device_for_install = "mps" # or keep cpu if mps detection is later
24
-
25
- torch_pkgs = ["torch", "torchaudio", "torchvision", "xformers"]
26
- diffusers_core_pkgs = ["diffusers", "Pillow", "transformers", "safetensors"]
27
-
28
- torch_index_url = None
29
- if preferred_torch_device_for_install == "cuda":
30
- # Specify a common CUDA version index. Pip should resolve the correct torch version.
31
- # As of late 2023/early 2024, cu118 or cu121 are common. Let's use cu126.
32
- # Users with different CUDA setups might need to pre-install torch manually.
33
- torch_index_url = "https://download.pytorch.org/whl/cu126"
34
- ASCIIColors.info(f"Attempting to ensure PyTorch with CUDA support (target index: {torch_index_url})")
35
- # Install torch and torchaudio first from the specific index
36
- pm.ensure_packages(torch_pkgs, index_url=torch_index_url)
37
- # Then install audiocraft and other dependencies; pip should use the already installed torch
38
- pm.ensure_packages(diffusers_core_pkgs)
39
- else:
40
- # For CPU, MPS, or if no specific CUDA preference was determined for install
41
- ASCIIColors.info("Ensuring PyTorch, AudioCraft, and dependencies using default PyPI index.")
42
- pm.ensure_packages(torch_pkgs + diffusers_core_pkgs)
43
-
44
- import whisper
45
- import torch
46
- _whisper_installed = True
47
- except Exception as e:
48
- _whisper_installation_error = str(e)
49
- whisper = None
50
- torch = None
51
-
52
-
7
+ import pipmaster as pm
8
+ # --- Concurrency Imports ---
9
+ import threading
10
+ import queue
11
+ from concurrent.futures import Future
12
+ import time
13
+ import hashlib
14
+ import re
15
+ # -------------------------
16
+ # --- Download Imports ---
17
+ import requests
18
+ from tqdm import tqdm
19
+ # --------------------
20
+
21
+ pm.ensure_packages(["torch","torchvision"],index_url="https://download.pytorch.org/whl/cu126")
22
+ pm.ensure_packages(["diffusers","pillow","transformers","safetensors", "requests", "tqdm"])
23
+
24
+ # Attempt to import core dependencies and set availability flag
53
25
  try:
54
26
  import torch
55
- from diffusers import AutoPipelineForText2Image, DiffusionPipeline
56
- from diffusers.utils import load_image # Potentially for future img2img etc.
27
+ from diffusers import AutoPipelineForText2Image, DiffusionPipeline, StableDiffusionPipeline
28
+ from diffusers.utils import load_image
57
29
  from PIL import Image
58
30
  DIFFUSERS_AVAILABLE = True
59
31
  except ImportError:
60
32
  torch = None
61
33
  AutoPipelineForText2Image = None
62
34
  DiffusionPipeline = None
35
+ StableDiffusionPipeline = None
63
36
  Image = None
64
37
  load_image = None
65
38
  DIFFUSERS_AVAILABLE = False
66
- # Detailed error will be raised in __init__ if user tries to use it
67
39
 
68
40
  from lollms_client.lollms_tti_binding import LollmsTTIBinding
69
41
  from ascii_colors import trace_exception, ASCIIColors
70
- import json # For potential JSONDecodeError and settings
42
+ import json
43
+ import shutil
71
44
 
72
45
  # Defines the binding name for the manager
73
46
  BindingName = "DiffusersTTIBinding_Impl"
74
47
 
48
+ # --- START: Civitai Model Definitions ---
49
+ # Expanded list of popular Civitai models (as single .safetensors files)
50
+ CIVITAI_MODELS = {
51
+ # Photorealistic
52
+ "realistic-vision-v6": {
53
+ "display_name": "Realistic Vision V6.0",
54
+ "url": "https://civitai.com/api/download/models/130072",
55
+ "filename": "realisticVisionV60_v60B1.safetensors",
56
+ "description": "One of the most popular photorealistic models.",
57
+ "owned_by": "civitai"
58
+ },
59
+ "absolute-reality": {
60
+ "display_name": "Absolute Reality",
61
+ "url": "https://civitai.com/api/download/models/132760",
62
+ "filename": "absolutereality_v181.safetensors",
63
+ "description": "A top-tier model for generating realistic images.",
64
+ "owned_by": "civitai"
65
+ },
66
+ # Artistic / General Purpose
67
+ "dreamshaper-8": {
68
+ "display_name": "DreamShaper 8",
69
+ "url": "https://civitai.com/api/download/models/128713",
70
+ "filename": "dreamshaper_8.safetensors",
71
+ "description": "A very popular and versatile general-purpose model.",
72
+ "owned_by": "civitai"
73
+ },
74
+ "juggernaut-xl": {
75
+ "display_name": "Juggernaut XL",
76
+ "url": "https://civitai.com/api/download/models/133005",
77
+ "filename": "juggernautXL_version6Rundiffusion.safetensors",
78
+ "description": "High-quality artistic model, great for cinematic styles (SDXL-based).",
79
+ "owned_by": "civitai"
80
+ },
81
+ # Anime
82
+ "anything-v5": {
83
+ "display_name": "Anything V5",
84
+ "url": "https://civitai.com/api/download/models/9409",
85
+ "filename": "anythingV5_PrtRE.safetensors",
86
+ "description": "A classic and highly popular model for anime-style generation.",
87
+ "owned_by": "civitai"
88
+ },
89
+ "lyriel-v1.6": {
90
+ "display_name": "Lyriel v1.6",
91
+ "url": "https://civitai.com/api/download/models/92407",
92
+ "filename": "lyriel_v16.safetensors",
93
+ "description": "A popular artistic model for fantasy and stylized images.",
94
+ "owned_by": "civitai"
95
+ }
96
+ }
97
+ # --- END: Civitai Model Definitions ---
98
+
75
99
  # Helper for torch.dtype string conversion
76
100
  TORCH_DTYPE_MAP_STR_TO_OBJ = {
77
- "float16": torch.float16 if torch else "float16", # Keep string if torch not loaded
78
- "bfloat16": torch.bfloat16 if torch else "bfloat16",
79
- "float32": torch.float32 if torch else "float32",
101
+ "float16": getattr(torch, 'float16', 'float16'),
102
+ "bfloat16": getattr(torch, 'bfloat16', 'bfloat16'),
103
+ "float32": getattr(torch, 'float32', 'float32'),
80
104
  "auto": "auto"
81
105
  }
82
106
  TORCH_DTYPE_MAP_OBJ_TO_STR = {v: k for k, v in TORCH_DTYPE_MAP_STR_TO_OBJ.items()}
83
- if torch: # Add None mapping if torch is loaded
107
+ if torch:
84
108
  TORCH_DTYPE_MAP_OBJ_TO_STR[None] = "None"
85
109
 
86
-
87
- # Common Schedulers mapping (User-friendly name to Class name)
110
+ # Common Schedulers mapping
88
111
  SCHEDULER_MAPPING = {
89
- "default": None, # Use model's default
90
- "ddim": "DDIMScheduler",
91
- "ddpm": "DDPMScheduler",
92
- "deis_multistep": "DEISMultistepScheduler",
93
- "dpm_multistep": "DPMSolverMultistepScheduler", # Alias
94
- "dpm_multistep_karras": "DPMSolverMultistepScheduler", # Configured with use_karras_sigmas=True
95
- "dpm_single": "DPMSolverSinglestepScheduler",
96
- "dpm_adaptive": "DP soluzioniPlusPlusScheduler", # DPM++ 2M Karras in A1111
97
- "dpm++_2m": "DPMSolverMultistepScheduler",
98
- "dpm++_2m_karras": "DPMSolverMultistepScheduler", # Configured with use_karras_sigmas=True
99
- "dpm++_2s_ancestral": "DPMSolverAncestralDiscreteScheduler",
100
- "dpm++_2s_ancestral_karras": "DPMSolverAncestralDiscreteScheduler", # Configured with use_karras_sigmas=True
101
- "dpm++_sde": "DPMSolverSDEScheduler",
102
- "dpm++_sde_karras": "DPMSolverSDEScheduler", # Configured with use_karras_sigmas=True
103
- "euler_ancestral_discrete": "EulerAncestralDiscreteScheduler",
104
- "euler_discrete": "EulerDiscreteScheduler",
105
- "heun_discrete": "HeunDiscreteScheduler",
106
- "heun_karras": "HeunDiscreteScheduler", # Configured with use_karras_sigmas=True
107
- "lms_discrete": "LMSDiscreteScheduler",
108
- "lms_karras": "LMSDiscreteScheduler", # Configured with use_karras_sigmas=True
109
- "pndm": "PNDMScheduler",
110
- "unipc_multistep": "UniPCMultistepScheduler",
112
+ "default": None, "ddim": "DDIMScheduler", "ddpm": "DDPMScheduler", "deis_multistep": "DEISMultistepScheduler",
113
+ "dpm_multistep": "DPMSolverMultistepScheduler", "dpm_multistep_karras": "DPMSolverMultistepScheduler",
114
+ "dpm_single": "DPMSolverSinglestepScheduler", "dpm_adaptive": "DPMSolverPlusPlusScheduler",
115
+ "dpm++_2m": "DPMSolverMultistepScheduler", "dpm++_2m_karras": "DPMSolverMultistepScheduler",
116
+ "dpm++_2s_ancestral": "DPMSolverAncestralDiscreteScheduler", "dpm++_2s_ancestral_karras": "DPMSolverAncestralDiscreteScheduler",
117
+ "dpm++_sde": "DPMSolverSDEScheduler", "dpm++_sde_karras": "DPMSolverSDEScheduler",
118
+ "euler_ancestral_discrete": "EulerAncestralDiscreteScheduler", "euler_discrete": "EulerDiscreteScheduler",
119
+ "heun_discrete": "HeunDiscreteScheduler", "heun_karras": "HeunDiscreteScheduler",
120
+ "lms_discrete": "LMSDiscreteScheduler", "lms_karras": "LMSDiscreteScheduler",
121
+ "pndm": "PNDMScheduler", "unipc_multistep": "UniPCMultistepScheduler",
111
122
  }
112
123
  SCHEDULER_USES_KARRAS_SIGMAS = [
113
124
  "dpm_multistep_karras", "dpm++_2m_karras", "dpm++_2s_ancestral_karras",
114
125
  "dpm++_sde_karras", "heun_karras", "lms_karras"
115
126
  ]
116
127
 
128
+ # --- START: Concurrency and Singleton Management ---
117
129
 
118
- class DiffusersTTIBinding_Impl(LollmsTTIBinding):
130
+ class ModelManager:
119
131
  """
120
- Concrete implementation of LollmsTTIBinding for Hugging Face Diffusers library.
121
- Allows running various text-to-image models locally.
132
+ Manages a single pipeline instance, its generation queue, and a worker thread.
133
+ This ensures all interactions with a specific model are thread-safe.
122
134
  """
123
- DEFAULT_CONFIG = {
124
- "model_id_or_path": "stabilityai/stable-diffusion-2-1-base",
125
- "device": "auto", # "auto", "cuda", "mps", "cpu"
126
- "torch_dtype_str": "auto", # "auto", "float16", "bfloat16", "float32"
127
- "use_safetensors": True,
128
- "scheduler_name": "default",
129
- "safety_checker_on": True, # Note: Diffusers default is ON
130
- "num_inference_steps": 25,
131
- "guidance_scale": 7.5,
132
- "default_width": 768, # Default for SD 2.1 base
133
- "default_height": 768, # Default for SD 2.1 base
134
- "seed": -1, # -1 for random on each call
135
- "enable_cpu_offload": False,
136
- "enable_sequential_cpu_offload": False,
137
- "enable_xformers": False, # Explicit opt-in for xformers
138
- "hf_variant": None, # e.g., "fp16"
139
- "hf_token": None,
140
- "local_files_only": False,
141
- }
142
-
143
- def __init__(self,
144
- config: Optional[Dict[str, Any]] = None,
145
- lollms_paths: Optional[Dict[str, Union[str, Path]]] = None,
146
- **kwargs # Catches other potential parameters like 'service_key' or 'client_id'
147
- ):
148
- """
149
- Initialize the Diffusers TTI binding.
150
-
151
- Args:
152
- config (Optional[Dict[str, Any]]): Configuration dictionary for the binding.
153
- Overrides DEFAULT_CONFIG.
154
- lollms_paths (Optional[Dict[str, Union[str, Path]]]): Dictionary of LOLLMS paths.
155
- Used for model/cache directories.
156
- **kwargs: Catches other parameters (e.g. service_key).
157
- """
158
- super().__init__(binding_name="diffusers")
159
-
160
- if not DIFFUSERS_AVAILABLE:
161
- ASCIIColors.error("Diffusers library or its dependencies (torch, Pillow, transformers) are not installed or failed to import.")
162
- ASCIIColors.info("Attempting to install/verify packages...")
163
- pm.ensure_packages(["torch", "diffusers", "Pillow", "transformers", "safetensors"])
164
- try:
165
- import torch as _torch
166
- from diffusers import AutoPipelineForText2Image as _AutoPipelineForText2Image
167
- from diffusers import DiffusionPipeline as _DiffusionPipeline
168
- from PIL import Image as _Image
169
- globals()['torch'] = _torch
170
- globals()['AutoPipelineForText2Image'] = _AutoPipelineForText2Image
171
- globals()['DiffusionPipeline'] = _DiffusionPipeline
172
- globals()['Image'] = _Image
173
-
174
- # Re-populate torch dtype maps if torch was just loaded
175
- global TORCH_DTYPE_MAP_STR_TO_OBJ, TORCH_DTYPE_MAP_OBJ_TO_STR
176
- TORCH_DTYPE_MAP_STR_TO_OBJ = {
177
- "float16": _torch.float16, "bfloat16": _torch.bfloat16,
178
- "float32": _torch.float32, "auto": "auto"
179
- }
180
- TORCH_DTYPE_MAP_OBJ_TO_STR = {v: k for k, v in TORCH_DTYPE_MAP_STR_TO_OBJ.items()}
181
- TORCH_DTYPE_MAP_OBJ_TO_STR[None] = "None"
182
- ASCIIColors.green("Dependencies seem to be available now.")
183
- except ImportError as e:
184
- trace_exception(e)
185
- raise ImportError(
186
- "Diffusers binding dependencies are still not met after trying to ensure them. "
187
- "Please install torch, diffusers, Pillow, and transformers manually. "
188
- f"Error: {e}"
189
- ) from e
190
-
191
- # Merge configs, lollms_paths, and kwargs
192
- self.config = {**self.DEFAULT_CONFIG, **(config or {}), **kwargs}
193
- self.lollms_paths = {k: Path(v) for k, v in (lollms_paths or {}).items()} if lollms_paths else {}
194
-
135
+ def __init__(self, config: Dict[str, Any], models_path: Path):
136
+ self.config = config
137
+ self.models_path = models_path
195
138
  self.pipeline: Optional[DiffusionPipeline] = None
196
- self.current_model_id_or_path = None # To track if model needs reload
197
-
198
- # Resolve auto settings for device and dtype
199
- if self.config["device"].lower() == "auto":
200
- if torch.cuda.is_available():
201
- self.config["device"] = "cuda"
202
- elif torch.backends.mps.is_available():
203
- self.config["device"] = "mps"
204
- else:
205
- self.config["device"] = "cpu"
206
-
207
- if self.config["torch_dtype_str"].lower() == "auto":
208
- if self.config["device"] == "cpu":
209
- self.config["torch_dtype_str"] = "float32" # CPU usually float32
210
- else:
211
- self.config["torch_dtype_str"] = "float16" # Common default for GPU
139
+ self.ref_count = 0
140
+ self.lock = threading.Lock()
141
+ self.queue = queue.Queue()
142
+ self.worker_thread = threading.Thread(target=self._generation_worker, daemon=True)
143
+ self._stop_event = threading.Event()
144
+ self.is_loaded = False
145
+
146
+ self.worker_thread.start()
212
147
 
213
- self.torch_dtype = TORCH_DTYPE_MAP_STR_TO_OBJ.get(self.config["torch_dtype_str"].lower(), torch.float32)
214
- if self.torch_dtype == "auto": # Should have been resolved above
215
- self.torch_dtype = torch.float16 if self.config["device"] != "cpu" else torch.float32
216
- self.config["torch_dtype_str"] = TORCH_DTYPE_MAP_OBJ_TO_STR.get(self.torch_dtype, "float32")
148
+ def acquire(self):
149
+ with self.lock:
150
+ self.ref_count += 1
151
+ return self
217
152
 
218
- # For potential lollms client specific features
219
- self.client_id = kwargs.get("service_key", kwargs.get("client_id", "diffusers_client_user"))
153
+ def release(self):
154
+ with self.lock:
155
+ self.ref_count -= 1
156
+ return self.ref_count
220
157
 
221
- self.load_model()
158
+ def stop(self):
159
+ self._stop_event.set()
160
+ self.queue.put(None)
161
+ self.worker_thread.join(timeout=5)
222
162
 
163
+ def _load_pipeline(self):
164
+ if self.pipeline:
165
+ return
223
166
 
224
- def _resolve_model_path(self, model_id_or_path: str) -> str:
225
- """Resolves a model name/path against lollms_paths if not absolute."""
226
- if os.path.isabs(model_id_or_path):
227
- return model_id_or_path
228
-
229
- # Check personal_models_path/diffusers_models/<name>
230
- if self.lollms_paths.get('personal_models_path'):
231
- personal_diffusers_path = self.lollms_paths['personal_models_path'] / "diffusers_models" / model_id_or_path
232
- if personal_diffusers_path.exists() and personal_diffusers_path.is_dir():
233
- ASCIIColors.info(f"Found local model in personal_models_path: {personal_diffusers_path}")
234
- return str(personal_diffusers_path)
235
-
236
- # Check models_zoo_path/diffusers_models/<name> (if different from personal)
237
- if self.lollms_paths.get('models_zoo_path') and \
238
- self.lollms_paths.get('models_zoo_path') != self.lollms_paths.get('personal_models_path'):
239
- zoo_diffusers_path = self.lollms_paths['models_zoo_path'] / "diffusers_models" / model_id_or_path
240
- if zoo_diffusers_path.exists() and zoo_diffusers_path.is_dir():
241
- ASCIIColors.info(f"Found local model in models_zoo_path: {zoo_diffusers_path}")
242
- return str(zoo_diffusers_path)
167
+ model_name = self.config.get("model_name", "")
168
+ if not model_name:
169
+ raise ValueError("Model name cannot be empty for loading.")
170
+
171
+ ASCIIColors.info(f"Loading Diffusers model: {model_name}")
172
+ model_path = self._resolve_model_path(model_name)
173
+ torch_dtype = TORCH_DTYPE_MAP_STR_TO_OBJ.get(self.config["torch_dtype_str"].lower())
243
174
 
244
- ASCIIColors.info(f"Assuming '{model_id_or_path}' is a Hugging Face Hub ID or already fully qualified.")
245
- return model_id_or_path
246
-
247
- def load_model(self):
248
- """Loads the Diffusers pipeline based on current configuration."""
249
- ASCIIColors.info("Loading Diffusers model...")
250
- if self.pipeline is not None:
251
- self.unload_model() # Ensure old model is cleared
252
-
253
175
  try:
254
- model_path = self._resolve_model_path(self.config["model_id_or_path"])
255
- self.current_model_id_or_path = model_path # Store what's actually loaded
256
-
257
- load_args = {
258
- "torch_dtype": self.torch_dtype,
259
- "use_safetensors": self.config["use_safetensors"],
260
- "token": self.config["hf_token"],
261
- "local_files_only": self.config["local_files_only"],
262
- }
263
- if self.config["hf_variant"]:
264
- load_args["variant"] = self.config["hf_variant"]
265
-
266
- if not self.config["safety_checker_on"]:
267
- load_args["safety_checker"] = None
268
-
269
- if self.lollms_paths.get("shared_cache_path"):
270
- load_args["cache_dir"] = str(self.lollms_paths["shared_cache_path"] / "huggingface_diffusers")
176
+ if str(model_path).endswith(".safetensors"):
177
+ ASCIIColors.info(f"Loading from single safetensors file: {model_path}")
178
+ try:
179
+ # Modern, preferred method for newer diffusers versions
180
+ self.pipeline = AutoPipelineForText2Image.from_single_file(
181
+ model_path,
182
+ torch_dtype=torch_dtype,
183
+ cache_dir=self.config.get("hf_cache_path")
184
+ )
185
+ except AttributeError:
186
+ # Fallback for older diffusers versions
187
+ ASCIIColors.warning("AutoPipelineForText2Image.from_single_file not found. Falling back to StableDiffusionPipeline.")
188
+ ASCIIColors.warning("Consider updating diffusers for better compatibility: pip install --upgrade diffusers")
189
+ self.pipeline = StableDiffusionPipeline.from_single_file(
190
+ model_path,
191
+ torch_dtype=torch_dtype,
192
+ cache_dir=self.config.get("hf_cache_path")
193
+ )
194
+ else:
195
+ ASCIIColors.info(f"Loading from pretrained folder/repo: {model_path}")
196
+ load_args = {
197
+ "torch_dtype": torch_dtype, "use_safetensors": self.config["use_safetensors"],
198
+ "token": self.config["hf_token"], "local_files_only": self.config["local_files_only"],
199
+ }
200
+ if self.config["hf_variant"]: load_args["variant"] = self.config["hf_variant"]
201
+ if not self.config["safety_checker_on"]: load_args["safety_checker"] = None
202
+ if self.config.get("hf_cache_path"): load_args["cache_dir"] = str(self.config["hf_cache_path"])
203
+ self.pipeline = AutoPipelineForText2Image.from_pretrained(model_path, **load_args)
271
204
 
205
+ except Exception as e:
206
+ error_str = str(e).lower()
207
+ if "401" in error_str or "gated" in error_str or "authorization" in error_str:
208
+ auth_error_msg = (
209
+ f"AUTHENTICATION FAILED for model '{model_name}'. This is likely a 'gated' model on Hugging Face.\n"
210
+ "Please ensure you have accepted its license and provided a valid HF Access Token in the settings."
211
+ )
212
+ raise RuntimeError(auth_error_msg) from e
213
+ else:
214
+ raise e
272
215
 
273
- # Use AutoPipelineForText2Image for flexibility
274
- pipeline_class_to_load = AutoPipelineForText2Image
275
- custom_pipeline_class_name = self.config.get("pipeline_class_name")
216
+ self._set_scheduler()
217
+ self.pipeline.to(self.config["device"])
276
218
 
277
- if custom_pipeline_class_name:
278
- try:
279
- diffusers_module = importlib.import_module("diffusers")
280
- pipeline_class_to_load = getattr(diffusers_module, custom_pipeline_class_name)
281
- ASCIIColors.info(f"Using specified pipeline class: {custom_pipeline_class_name}")
282
- except (ImportError, AttributeError) as e:
283
- ASCIIColors.warning(f"Could not load custom pipeline class {custom_pipeline_class_name}: {e}. Falling back to AutoPipelineForText2Image.")
284
- pipeline_class_to_load = AutoPipelineForText2Image
285
-
286
- self.pipeline = pipeline_class_to_load.from_pretrained(model_path, **load_args)
287
-
288
- # Scheduler
289
- self._set_scheduler()
219
+ if self.config["enable_xformers"]:
220
+ try:
221
+ self.pipeline.enable_xformers_memory_efficient_attention()
222
+ except Exception as e:
223
+ ASCIIColors.warning(f"Could not enable xFormers: {e}.")
224
+
225
+ if self.config["enable_cpu_offload"] and self.config["device"] != "cpu":
226
+ self.pipeline.enable_model_cpu_offload()
227
+ elif self.config["enable_sequential_cpu_offload"] and self.config["device"] != "cpu":
228
+ self.pipeline.enable_sequential_cpu_offload()
229
+
230
+ self.is_loaded = True
231
+ ASCIIColors.green(f"Model '{model_name}' loaded successfully on '{self.config['device']}'.")
290
232
 
291
- self.pipeline.to(self.config["device"])
233
+ def _unload_pipeline(self):
234
+ if self.pipeline:
235
+ del self.pipeline
236
+ self.pipeline = None
237
+ if torch and torch.cuda.is_available():
238
+ torch.cuda.empty_cache()
239
+ self.is_loaded = False
240
+ ASCIIColors.info(f"Model '{self.config.get('model_name')}' unloaded.")
292
241
 
293
- if self.config["enable_xformers"]:
242
+ def _generation_worker(self):
243
+ while not self._stop_event.is_set():
244
+ try:
245
+ job = self.queue.get(timeout=1)
246
+ if job is None:
247
+ break
248
+ future, pipeline_args = job
294
249
  try:
295
- self.pipeline.enable_xformers_memory_efficient_attention()
296
- ASCIIColors.info("xFormers memory efficient attention enabled.")
250
+ with self.lock:
251
+ if not self.pipeline:
252
+ self._load_pipeline()
253
+ with torch.no_grad():
254
+ pipeline_output = self.pipeline(**pipeline_args)
255
+ pil_image: Image.Image = pipeline_output.images[0]
256
+ img_byte_arr = BytesIO()
257
+ pil_image.save(img_byte_arr, format="PNG")
258
+ future.set_result(img_byte_arr.getvalue())
297
259
  except Exception as e:
298
- ASCIIColors.warning(f"Could not enable xFormers: {e}. Proceeding without it.")
260
+ trace_exception(e)
261
+ future.set_exception(e)
262
+ finally:
263
+ self.queue.task_done()
264
+ except queue.Empty:
265
+ continue
266
+
267
+ def _download_civitai_model(self, model_key: str):
268
+ model_info = CIVITAI_MODELS[model_key]
269
+ url = model_info["url"]
270
+ filename = model_info["filename"]
271
+ dest_path = self.models_path / filename
272
+ temp_path = dest_path.with_suffix(".temp")
273
+
274
+ ASCIIColors.cyan(f"Downloading '{filename}' from Civitai...")
275
+ try:
276
+ with requests.get(url, stream=True) as r:
277
+ r.raise_for_status()
278
+ total_size = int(r.headers.get('content-length', 0))
279
+ with open(temp_path, 'wb') as f, tqdm(
280
+ total=total_size, unit='iB', unit_scale=True, desc=f"Downloading {filename}"
281
+ ) as bar:
282
+ for chunk in r.iter_content(chunk_size=8192):
283
+ f.write(chunk)
284
+ bar.update(len(chunk))
299
285
 
300
- if self.config["enable_cpu_offload"] and self.config["device"] != "cpu":
301
- self.pipeline.enable_model_cpu_offload()
302
- ASCIIColors.info("Model CPU offload enabled.")
303
- elif self.config["enable_sequential_cpu_offload"] and self.config["device"] != "cpu":
304
- self.pipeline.enable_sequential_cpu_offload() # More aggressive
305
- ASCIIColors.info("Sequential CPU offload enabled.")
306
-
307
-
308
- ASCIIColors.green(f"Diffusers model '{model_path}' loaded successfully on device '{self.config['device']}' with dtype '{self.config['torch_dtype_str']}'.")
309
-
286
+ shutil.move(temp_path, dest_path)
287
+ ASCIIColors.green(f"Model '{filename}' downloaded successfully.")
310
288
  except Exception as e:
311
- trace_exception(e)
312
- self.pipeline = None
313
- raise RuntimeError(f"Failed to load Diffusers model '{self.config['model_id_or_path']}': {e}") from e
289
+ if temp_path.exists():
290
+ temp_path.unlink()
291
+ raise Exception(f"Failed to download model {filename}: {e}") from e
292
+
293
+ def _resolve_model_path(self, model_name: str) -> Union[str, Path]:
294
+ path_obj = Path(model_name)
295
+ if path_obj.is_absolute() and path_obj.exists():
296
+ return model_name
297
+
298
+ if model_name in CIVITAI_MODELS:
299
+ filename = CIVITAI_MODELS[model_name]["filename"]
300
+ local_path = self.models_path / filename
301
+ if not local_path.exists():
302
+ self._download_civitai_model(model_name)
303
+ return local_path
304
+
305
+ local_path = self.models_path / model_name
306
+ if local_path.exists():
307
+ return local_path
308
+
309
+ return model_name
314
310
 
315
311
  def _set_scheduler(self):
316
312
  if not self.pipeline: return
317
-
318
313
  scheduler_name_key = self.config["scheduler_name"].lower()
319
- if scheduler_name_key == "default":
320
- ASCIIColors.info(f"Using model's default scheduler: {self.pipeline.scheduler.__class__.__name__}")
321
- return
314
+ if scheduler_name_key == "default": return
322
315
 
323
316
  scheduler_class_name = SCHEDULER_MAPPING.get(scheduler_name_key)
324
317
  if scheduler_class_name:
325
318
  try:
326
- scheduler_module = importlib.import_module("diffusers.schedulers")
327
- SchedulerClass = getattr(scheduler_module, scheduler_class_name)
328
-
319
+ SchedulerClass = getattr(importlib.import_module("diffusers.schedulers"), scheduler_class_name)
329
320
  scheduler_config = self.pipeline.scheduler.config
330
- if scheduler_name_key in SCHEDULER_USES_KARRAS_SIGMAS:
331
- scheduler_config["use_karras_sigmas"] = True
332
- else: # Ensure it's False if not a karras variant for this scheduler
333
- if "use_karras_sigmas" in scheduler_config:
334
- scheduler_config["use_karras_sigmas"] = False
335
-
336
-
321
+ scheduler_config["use_karras_sigmas"] = scheduler_name_key in SCHEDULER_USES_KARRAS_SIGMAS
337
322
  self.pipeline.scheduler = SchedulerClass.from_config(scheduler_config)
338
- ASCIIColors.info(f"Switched scheduler to {scheduler_name_key} ({scheduler_class_name}).")
339
323
  except Exception as e:
340
- trace_exception(e)
341
324
  ASCIIColors.warning(f"Could not switch scheduler to {scheduler_name_key}: {e}. Using current default.")
342
- else:
343
- ASCIIColors.warning(f"Unknown scheduler name: {self.config['scheduler_name']}. Using model default.")
344
325
 
326
+ class PipelineRegistry:
327
+ _instance = None
328
+ _lock = threading.Lock()
329
+
330
+ def __new__(cls, *args, **kwargs):
331
+ with cls._lock:
332
+ if cls._instance is None:
333
+ cls._instance = super().__new__(cls)
334
+ cls._instance._managers = {}
335
+ cls._instance._registry_lock = threading.Lock()
336
+ return cls._instance
337
+
338
+ def _get_config_key(self, config: Dict[str, Any]) -> str:
339
+ critical_keys = [
340
+ "model_name", "device", "torch_dtype_str", "use_safetensors",
341
+ "safety_checker_on", "hf_variant", "enable_cpu_offload",
342
+ "enable_sequential_cpu_offload", "enable_xformers",
343
+ "local_files_only", "hf_cache_path"
344
+ ]
345
+ key_data = tuple(sorted((k, config.get(k)) for k in critical_keys))
346
+ return hashlib.sha256(str(key_data).encode('utf-8')).hexdigest()
347
+
348
+ def get_manager(self, config: Dict[str, Any], models_path: Path) -> ModelManager:
349
+ key = self._get_config_key(config)
350
+ with self._registry_lock:
351
+ if key not in self._managers:
352
+ self._managers[key] = ModelManager(config.copy(), models_path)
353
+ return self._managers[key].acquire()
354
+
355
+ def release_manager(self, config: Dict[str, Any]):
356
+ key = self._get_config_key(config)
357
+ with self._registry_lock:
358
+ if key in self._managers:
359
+ manager = self._managers[key]
360
+ ref_count = manager.release()
361
+ if ref_count == 0:
362
+ ASCIIColors.info(f"Reference count for model '{config.get('model_name')}' is zero. Cleaning up.")
363
+ manager.stop()
364
+ manager._unload_pipeline()
365
+ del self._managers[key]
366
+
367
+ class DiffusersTTIBinding_Impl(LollmsTTIBinding):
368
+ DEFAULT_CONFIG = {
369
+ "model_name": "", "device": "auto", "torch_dtype_str": "auto", "use_safetensors": True,
370
+ "scheduler_name": "default", "safety_checker_on": True, "num_inference_steps": 25,
371
+ "guidance_scale": 7.0, "default_width": 512, "default_height": 512, "seed": -1,
372
+ "enable_cpu_offload": False, "enable_sequential_cpu_offload": False, "enable_xformers": False,
373
+ "hf_variant": None, "hf_token": None, "hf_cache_path": None, "local_files_only": False,
374
+ }
375
+
376
+ def __init__(self, **kwargs):
377
+ super().__init__(binding_name=BindingName)
378
+
379
+ if not DIFFUSERS_AVAILABLE:
380
+ raise ImportError(
381
+ "Diffusers or its dependencies not installed. "
382
+ "Please run: pip install torch torchvision diffusers Pillow transformers safetensors requests tqdm"
383
+ )
384
+
385
+ self.config = {**self.DEFAULT_CONFIG, **kwargs}
386
+ self.model_name = self.config.get("model_name", "")
387
+ self.models_path = Path(kwargs.get("models_path", Path(__file__).parent / "models"))
388
+ self.models_path.mkdir(parents=True, exist_ok=True)
389
+
390
+ self.registry = PipelineRegistry()
391
+ self.manager: Optional[ModelManager] = None
392
+
393
+ self._resolve_device_and_dtype()
394
+ if self.model_name:
395
+ self._acquire_manager()
396
+
397
+ def _acquire_manager(self):
398
+ if self.manager:
399
+ self.registry.release_manager(self.manager.config)
400
+ self.manager = self.registry.get_manager(self.config, self.models_path)
401
+ ASCIIColors.info(f"Binding instance acquired manager for '{self.config['model_name']}'.")
402
+
403
+ def _resolve_device_and_dtype(self):
404
+ if self.config["device"].lower() == "auto":
405
+ self.config["device"] = "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu"
406
+
407
+ if self.config["torch_dtype_str"].lower() == "auto":
408
+ self.config["torch_dtype_str"] = "float16" if self.config["device"] != "cpu" else "float32"
409
+
410
+ def list_safetensor_models(self) -> List[str]:
411
+ if not self.models_path.exists(): return []
412
+ return sorted([f.name for f in self.models_path.iterdir() if f.is_file() and f.suffix == ".safetensors"])
413
+
414
+ def listModels(self) -> list:
415
+ # Start with hardcoded Civitai and Hugging Face models
416
+ civitai_list = [
417
+ {'model_name': key, 'display_name': info['display_name'], 'description': info['description'], 'owned_by': info['owned_by']}
418
+ for key, info in CIVITAI_MODELS.items()
419
+ ]
420
+ hf_default_list = [
421
+ # SDXL Models (1024x1024 native)
422
+ {'model_name': "stabilityai/stable-diffusion-xl-base-1.0", 'display_name': "Stable Diffusion XL 1.0", 'description': "Official SDXL base model from Stability AI. Native resolution is 1024x1024.", 'owned_by': 'HuggingFace'},
423
+ {'model_name': "playgroundai/playground-v2.5-1024px-aesthetic", 'display_name': "Playground v2.5", 'description': "Known for high aesthetic quality. Native resolution is 1024x1024.", 'owned_by': 'HuggingFace'},
424
+ # SD 1.5 Models (512x512 native)
425
+ {'model_name': "runwayml/stable-diffusion-v1-5", 'display_name': "Stable Diffusion 1.5", 'description': "A popular and versatile open-access text-to-image model.", 'owned_by': 'HuggingFace'},
426
+ {'model_name': "dataautogpt3/OpenDalleV1.1", 'display_name': "OpenDalle v1.1", 'description': "An open-source reproduction of DALL-E 3, good for prompt adherence.", 'owned_by': 'HuggingFace'},
427
+ {'model_name': "stabilityai/stable-diffusion-2-1-base", 'display_name': "Stable Diffusion 2.1 (512px)", 'description': "A 512x512 resolution model from Stability AI.", 'owned_by': 'HuggingFace'},
428
+ {'model_name': "CompVis/stable-diffusion-v1-4", 'display_name': "Stable Diffusion 1.4 (Gated)", 'description': "Original SD v1.4. Requires accepting license on Hugging Face and an HF token.", 'owned_by': 'HuggingFace'}
429
+ ]
430
+
431
+ # Discover local .safetensors files
432
+ custom_local_models = []
433
+ civitai_filenames = {info['filename'] for info in CIVITAI_MODELS.values()}
434
+ local_safetensors = self.list_safetensor_models()
435
+
436
+ for filename in local_safetensors:
437
+ if filename not in civitai_filenames:
438
+ custom_local_models.append({
439
+ 'model_name': filename,
440
+ 'display_name': filename,
441
+ 'description': 'Local safetensors file from your models folder.',
442
+ 'owned_by': 'local_user'
443
+ })
444
+
445
+ return civitai_list + hf_default_list + custom_local_models
446
+
447
+ def load_model(self):
448
+ ASCIIColors.info("load_model() called. Loading is now automatic.")
449
+ if self.model_name and not self.manager:
450
+ self._acquire_manager()
345
451
 
346
452
  def unload_model(self):
347
- if self.pipeline is not None:
348
- del self.pipeline
349
- self.pipeline = None
350
- ASCIIColors.info("Diffusers pipeline unloaded.")
351
- if torch and torch.cuda.is_available():
352
- torch.cuda.empty_cache()
353
-
354
- def generate_image(self,
355
- prompt: str,
356
- negative_prompt: Optional[str] = "",
357
- width: Optional[int] = None, # Uses default from config if None
358
- height: Optional[int] = None, # Uses default from config if None
359
- **kwargs) -> bytes:
360
- """
361
- Generates image data using the Diffusers pipeline.
362
-
363
- Args:
364
- prompt (str): The positive text prompt.
365
- negative_prompt (Optional[str]): The negative prompt.
366
- width (int): Image width. Overrides default.
367
- height (int): Image height. Overrides default.
368
- **kwargs: Additional parameters for the pipeline:
369
- - num_inference_steps (int)
370
- - guidance_scale (float)
371
- - seed (int)
372
- - eta (float, for DDIM)
373
- - num_images_per_prompt (int, though this binding returns one)
374
- - clip_skip (int, if supported by pipeline - advanced)
375
- Returns:
376
- bytes: The generated image data (PNG format).
377
- Raises:
378
- Exception: If the request fails or image generation fails.
379
- """
380
- if not self.pipeline:
381
- raise RuntimeError("Diffusers pipeline is not loaded. Cannot generate image.")
382
-
383
- # Use call-specific or configured defaults
384
- _width = width if width is not None else self.config["default_width"]
385
- _height = height if height is not None else self.config["default_height"]
453
+ if self.manager:
454
+ ASCIIColors.info(f"Binding instance releasing manager for '{self.manager.config['model_name']}'.")
455
+ self.registry.release_manager(self.manager.config)
456
+ self.manager = None
457
+
458
+ def generate_image(self, prompt: str, negative_prompt: str = "", width: int = None, height: int = None, **kwargs) -> bytes:
459
+ if not self.model_name:
460
+ raise RuntimeError("No model_name configured. Please select a model in settings.")
461
+
462
+ if not self.manager:
463
+ self._acquire_manager()
464
+
465
+ _width = width or self.config["default_width"]
466
+ _height = height or self.config["default_height"]
386
467
  _num_inference_steps = kwargs.get("num_inference_steps", self.config["num_inference_steps"])
387
468
  _guidance_scale = kwargs.get("guidance_scale", self.config["guidance_scale"])
388
469
  _seed = kwargs.get("seed", self.config["seed"])
389
470
 
390
- generator = None
391
- if _seed != -1: # -1 means random seed
392
- generator = torch.Generator(device=self.config["device"]).manual_seed(_seed)
471
+ generator = torch.Generator(device=self.config["device"]).manual_seed(_seed) if _seed != -1 else None
393
472
 
394
473
  pipeline_args = {
395
- "prompt": prompt,
396
- "negative_prompt": negative_prompt if negative_prompt else None,
397
- "width": _width,
398
- "height": _height,
399
- "num_inference_steps": _num_inference_steps,
400
- "guidance_scale": _guidance_scale,
401
- "generator": generator,
402
- "num_images_per_prompt": kwargs.get("num_images_per_prompt", 1)
474
+ "prompt": prompt, "negative_prompt": negative_prompt or None, "width": _width,
475
+ "height": _height, "num_inference_steps": _num_inference_steps,
476
+ "guidance_scale": _guidance_scale, "generator": generator,
403
477
  }
404
- if "eta" in kwargs: pipeline_args["eta"] = kwargs["eta"]
405
- if "clip_skip" in kwargs and hasattr(self.pipeline, "clip_skip"): # Handle clip_skip if supported
406
- pipeline_args["clip_skip"] = kwargs["clip_skip"]
407
-
408
-
409
- ASCIIColors.info(f"Generating image with prompt: '{prompt[:100]}...'")
410
- ASCIIColors.debug(f"Pipeline args: {pipeline_args}")
411
-
478
+
479
+ future = Future()
480
+ self.manager.queue.put((future, pipeline_args))
481
+ ASCIIColors.info(f"Job for prompt '{prompt[:50]}...' queued. Waiting...")
482
+
412
483
  try:
413
- with torch.no_grad(): # Important for inference
414
- pipeline_output = self.pipeline(**pipeline_args)
415
-
416
- pil_image: Image.Image = pipeline_output.images[0]
417
-
418
- # Convert PIL Image to bytes (PNG)
419
- img_byte_arr = BytesIO()
420
- pil_image.save(img_byte_arr, format="PNG")
421
- img_bytes = img_byte_arr.getvalue()
422
-
484
+ image_bytes = future.result()
423
485
  ASCIIColors.green("Image generated successfully.")
424
- return img_bytes
425
-
486
+ return image_bytes
426
487
  except Exception as e:
427
- trace_exception(e)
428
- raise Exception(f"Diffusers image generation failed: {e}") from e
488
+ raise Exception(f"Image generation failed: {e}") from e
489
+
490
+ def list_local_models(self) -> List[str]:
491
+ if not self.models_path.exists(): return []
492
+
493
+ folders = [
494
+ d.name for d in self.models_path.iterdir()
495
+ if d.is_dir() and ((d / "model_index.json").exists() or (d / "unet" / "config.json").exists())
496
+ ]
497
+ safetensors = self.list_safetensor_models()
498
+ return sorted(folders + safetensors)
499
+
500
+ def list_available_models(self) -> List[str]:
501
+ discoverable_models = [m['model_name'] for m in self.listModels()]
502
+ local_models = self.list_local_models()
503
+
504
+ combined_list = sorted(list(set(local_models + discoverable_models)))
505
+ return combined_list
429
506
 
430
507
  def list_services(self, **kwargs) -> List[Dict[str, str]]:
431
- """
432
- Lists the currently loaded model as the available service.
433
- Future: Could scan local model directories or list known HF models.
434
- """
435
- if self.pipeline and self.current_model_id_or_path:
436
- return [{
437
- "name": os.path.basename(self.current_model_id_or_path),
438
- "caption": f"Diffusers: {os.path.basename(self.current_model_id_or_path)}",
439
- "help": (f"Currently loaded model. Path/ID: {self.current_model_id_or_path}. "
440
- f"Device: {self.config['device']}. DType: {self.config['torch_dtype_str']}. "
441
- f"Scheduler: {self.pipeline.scheduler.__class__.__name__}.")
442
- }]
443
- return [{"name": "diffusers_unloaded", "caption": "No Diffusers model loaded", "help": "Configure a model in settings."}]
508
+ models = self.list_available_models()
509
+ local_models = self.list_local_models()
510
+
511
+ if not models:
512
+ return [{"name": "diffusers_no_models", "caption": "No models found", "help": f"Place models in '{self.models_path.resolve()}'."}]
513
+
514
+ services = []
515
+ for m in models:
516
+ help_text = "Hugging Face model ID"
517
+ if m in local_models:
518
+ help_text = f"Local model from: {self.models_path.resolve()}"
519
+ elif m in CIVITAI_MODELS:
520
+ filename = CIVITAI_MODELS[m]['filename']
521
+ help_text = f"Civitai model (downloads as {filename})"
522
+
523
+ services.append({"name": m, "caption": f"Diffusers: {m}", "help": help_text})
524
+ return services
444
525
 
445
526
  def get_settings(self, **kwargs) -> List[Dict[str, Any]]:
446
- """
447
- Retrieves the current configurable settings for the Diffusers binding.
448
- """
449
- # Actual device and dtype after auto-resolution
450
- resolved_device = self.config['device']
451
- resolved_dtype_str = self.config['torch_dtype_str']
452
-
453
- # For display, show the original 'auto' if it was set that way, plus the resolved value
454
- display_device = self.config['device'] if self.config['device'].lower() != 'auto' else f"auto ({resolved_device})"
455
- display_dtype = self.config['torch_dtype_str'] if self.config['torch_dtype_str'].lower() != 'auto' else f"auto ({resolved_dtype_str})"
456
-
457
- settings = [
458
- {"name": "model_id_or_path", "type": "str", "value": self.config["model_id_or_path"], "description": "Hugging Face model ID or local path to Diffusers model directory."},
459
- {"name": "device", "type": "str", "value": self.config["device"], "description": f"Device for inference. Current resolved: {resolved_device}", "options": ["auto", "cuda", "mps", "cpu"]},
460
- {"name": "torch_dtype_str", "type": "str", "value": self.config["torch_dtype_str"], "description": f"Torch dtype for model. Current resolved: {resolved_dtype_str}", "options": ["auto", "float16", "bfloat16", "float32"]},
461
- {"name": "hf_variant", "type": "str", "value": self.config["hf_variant"], "description": "Model variant (e.g., 'fp16', 'bf16'). Optional."},
462
- {"name": "use_safetensors", "type": "bool", "value": self.config["use_safetensors"], "description": "Prefer loading models from .safetensors files."},
463
- {"name": "scheduler_name", "type": "str", "value": self.config["scheduler_name"], "description": "Scheduler to use for diffusion.", "options": list(SCHEDULER_MAPPING.keys())},
464
- {"name": "safety_checker_on", "type": "bool", "value": self.config["safety_checker_on"], "description": "Enable the safety checker (if model has one)."},
527
+ available_models = self.list_available_models()
528
+ return [
529
+ {"name": "model_name", "type": "str", "value": self.model_name, "description": "Local, Civitai, or Hugging Face model.", "options": available_models},
530
+ {"name": "device", "type": "str", "value": self.config["device"], "description": f"Inference device. Resolved: {self.config['device']}", "options": ["auto", "cuda", "mps", "cpu"]},
531
+ {"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"]},
532
+ {"name": "hf_variant", "type": "str", "value": self.config["hf_variant"], "description": "HF model variant (e.g., 'fp16')."},
533
+ {"name": "use_safetensors", "type": "bool", "value": self.config["use_safetensors"], "description": "Prefer .safetensors when loading from Hugging Face."},
534
+ {"name": "scheduler_name", "type": "str", "value": self.config["scheduler_name"], "description": "Scheduler for diffusion.", "options": list(SCHEDULER_MAPPING.keys())},
535
+ {"name": "safety_checker_on", "type": "bool", "value": self.config["safety_checker_on"], "description": "Enable the safety checker."},
465
536
  {"name": "enable_cpu_offload", "type": "bool", "value": self.config["enable_cpu_offload"], "description": "Enable model CPU offload (saves VRAM, slower)."},
466
537
  {"name": "enable_sequential_cpu_offload", "type": "bool", "value": self.config["enable_sequential_cpu_offload"], "description": "Enable sequential CPU offload (more VRAM savings, much slower)."},
467
- {"name": "enable_xformers", "type": "bool", "value": self.config["enable_xformers"], "description": "Enable xFormers memory efficient attention (if available)."},
468
- {"name": "default_width", "type": "int", "value": self.config["default_width"], "description": "Default width for generated images."},
469
- {"name": "default_height", "type": "int", "value": self.config["default_height"], "description": "Default height for generated images."},
470
- {"name": "num_inference_steps", "type": "int", "value": self.config["num_inference_steps"], "description": "Default number of inference steps."},
538
+ {"name": "enable_xformers", "type": "bool", "value": self.config["enable_xformers"], "description": "Enable xFormers memory efficient attention."},
539
+ {"name": "default_width", "type": "int", "value": self.config["default_width"], "description": "Default image width. Note: SDXL models prefer 1024."},
540
+ {"name": "default_height", "type": "int", "value": self.config["default_height"], "description": "Default image height. Note: SDXL models prefer 1024."},
541
+ {"name": "num_inference_steps", "type": "int", "value": self.config["num_inference_steps"], "description": "Default inference steps."},
471
542
  {"name": "guidance_scale", "type": "float", "value": self.config["guidance_scale"], "description": "Default guidance scale (CFG)."},
472
- {"name": "seed", "type": "int", "value": self.config["seed"], "description": "Default seed for generation (-1 for random)."},
473
- {"name": "hf_token", "type": "str", "value": self.config["hf_token"], "description": "Hugging Face API token (for private/gated models). Set to 'None' or empty if not needed. Store securely.", "is_secret": True},
474
- {"name": "local_files_only", "type": "bool", "value": self.config["local_files_only"], "description": "Only use local files, do not try to download."},
543
+ {"name": "seed", "type": "int", "value": self.config["seed"], "description": "Default seed (-1 for random)."},
544
+ {"name": "hf_token", "type": "str", "value": self.config["hf_token"], "description": "HF API token (for private/gated models).", "is_secret": True},
545
+ {"name": "hf_cache_path", "type": "str", "value": self.config["hf_cache_path"], "description": "Path to HF cache."},
546
+ {"name": "local_files_only", "type": "bool", "value": self.config["local_files_only"], "description": "Do not download from Hugging Face."},
475
547
  ]
476
- return settings
477
548
 
478
549
  def set_settings(self, settings: Union[Dict[str, Any], List[Dict[str, Any]]], **kwargs) -> bool:
479
- """
480
- Applies new settings to the Diffusers binding. Some settings may trigger a model reload.
481
- """
482
- if isinstance(settings, list): # Convert from ConfigTemplate list format
483
- parsed_settings = {item["name"]: item["value"] for item in settings if "name" in item and "value" in item}
484
- elif isinstance(settings, dict):
485
- parsed_settings = settings
486
- else:
487
- ASCIIColors.error("Invalid settings format. Expected a dictionary or list of dictionaries.")
488
- return False
489
-
490
- old_config = self.config.copy()
491
- needs_reload = False
550
+ parsed_settings = settings if isinstance(settings, dict) else \
551
+ {item["name"]: item["value"] for item in settings if "name" in item and "value" in item}
492
552
 
493
- for key, value in parsed_settings.items():
494
- if key in self.config:
495
- if self.config[key] != value:
496
- self.config[key] = value
497
- ASCIIColors.info(f"Setting '{key}' changed to: {value}")
498
- if key in ["model_id_or_path", "device", "torch_dtype_str",
499
- "use_safetensors", "safety_checker_on", "hf_variant",
500
- "enable_cpu_offload", "enable_sequential_cpu_offload", "enable_xformers",
501
- "hf_token", "local_files_only"]:
502
- needs_reload = True
503
- elif key == "scheduler_name" and self.pipeline: # Scheduler can be changed on loaded pipeline
504
- self._set_scheduler() # Attempt to apply immediately
505
- else:
506
- ASCIIColors.warning(f"Unknown setting '{key}' ignored.")
553
+ critical_keys = self.registry._get_config_key({}).__self__.critical_keys
554
+ needs_manager_swap = False
507
555
 
508
- if needs_reload:
509
- ASCIIColors.info("Reloading model due to settings changes...")
510
- try:
511
- # Resolve auto device/dtype again if they were part of the change
512
- if "device" in parsed_settings and self.config["device"].lower() == "auto":
513
- if torch.cuda.is_available(): self.config["device"] = "cuda"
514
- elif torch.backends.mps.is_available(): self.config["device"] = "mps"
515
- else: self.config["device"] = "cpu"
516
-
517
- if "torch_dtype_str" in parsed_settings and self.config["torch_dtype_str"].lower() == "auto":
518
- self.config["torch_dtype_str"] = "float16" if self.config["device"] != "cpu" else "float32"
556
+ for key, value in parsed_settings.items():
557
+ if self.config.get(key) != value:
558
+ ASCIIColors.info(f"Setting '{key}' changed to: {value}")
559
+ self.config[key] = value
560
+ if key == "model_name": self.model_name = value
561
+ if key in critical_keys: needs_manager_swap = True
519
562
 
520
- # Update torch_dtype object from string
521
- self.torch_dtype = TORCH_DTYPE_MAP_STR_TO_OBJ.get(self.config["torch_dtype_str"].lower(), torch.float32)
522
- if self.torch_dtype == "auto": # Should be resolved by now
523
- self.torch_dtype = torch.float16 if self.config["device"] != "cpu" else torch.float32
524
- self.config["torch_dtype_str"] = TORCH_DTYPE_MAP_OBJ_TO_STR.get(self.torch_dtype, "float32")
525
-
563
+ if needs_manager_swap and self.model_name:
564
+ ASCIIColors.info("Critical settings changed. Swapping model manager...")
565
+ self._resolve_device_and_dtype()
566
+ self._acquire_manager()
567
+
568
+ if not needs_manager_swap and self.manager:
569
+ self.manager.config.update(parsed_settings)
570
+ if 'scheduler_name' in parsed_settings and self.manager.pipeline:
571
+ with self.manager.lock:
572
+ self.manager._set_scheduler()
526
573
 
527
- self.load_model()
528
- ASCIIColors.green("Model reloaded successfully with new settings.")
529
- except Exception as e:
530
- trace_exception(e)
531
- ASCIIColors.error(f"Failed to reload model with new settings: {e}. Reverting critical settings.")
532
- # Revert critical settings and try to reload with old config
533
- self.config = old_config
534
- self.torch_dtype = TORCH_DTYPE_MAP_STR_TO_OBJ.get(self.config["torch_dtype_str"].lower(), torch.float32)
535
- try:
536
- self.load_model()
537
- ASCIIColors.info("Reverted to previous model configuration.")
538
- except Exception as e_revert:
539
- trace_exception(e_revert)
540
- ASCIIColors.error(f"Failed to revert to previous model configuration: {e_revert}. Binding may be unstable.")
541
- return False
542
574
  return True
543
575
 
544
576
  def __del__(self):
545
577
  self.unload_model()
546
578
 
547
- # Example Usage (for testing within this file)
579
+ # Example Usage
548
580
  if __name__ == '__main__':
549
581
  ASCIIColors.magenta("--- Diffusers TTI Binding Test ---")
550
582
 
551
583
  if not DIFFUSERS_AVAILABLE:
552
- ASCIIColors.error("Diffusers or its dependencies are not available. Cannot run test.")
553
- # Attempt to guide user for installation
554
- print("Please ensure PyTorch, Diffusers, Pillow, and Transformers are installed.")
555
- print("For PyTorch with CUDA: visit https://pytorch.org/get-started/locally/")
556
- print("Then: pip install diffusers Pillow transformers safetensors")
584
+ ASCIIColors.error("Diffusers not available. Cannot run test.")
557
585
  exit(1)
558
586
 
559
- # --- Configuration ---
560
- # Small, fast model for testing. Replace with a full model for real use.
561
- # "CompVis/stable-diffusion-v1-4" is ~5GB
562
- # "google/ddpm-cat-256" is smaller, but a DDPM, not Stable Diffusion.
563
- # Using a tiny SD model if one exists, or a small variant.
564
- # For a quick test, let's try a small LCM LoRA with SD1.5 if possible or just a base model.
565
- # Note: "runwayml/stable-diffusion-v1-5" is a good standard test model.
566
- # For a *very* quick CI-like test, one might use a dummy model or a very small one.
567
- # Let's use a smaller SD variant if available, otherwise default to 2.1-base.
568
- test_model_id = "runwayml/stable-diffusion-v1-5" # ~4GB download. Use a smaller one if you have it locally.
569
- # test_model_id = "hf-internal-testing/tiny-stable-diffusion-pipe" # Very small, for testing structure
570
-
571
- # Create dummy lollms_paths
572
587
  temp_paths_dir = Path(__file__).parent / "temp_lollms_paths_diffusers"
573
- temp_paths_dir.mkdir(parents=True, exist_ok=True)
574
- mock_lollms_paths = {
575
- "personal_models_path": temp_paths_dir / "personal_models",
576
- "models_zoo_path": temp_paths_dir / "models_zoo",
577
- "shared_cache_path": temp_paths_dir / "shared_cache", # For Hugging Face cache
578
- }
579
- for p in mock_lollms_paths.values(): Path(p).mkdir(parents=True, exist_ok=True)
580
- (Path(mock_lollms_paths["personal_models_path"]) / "diffusers_models").mkdir(exist_ok=True)
581
-
582
-
583
- binding_config = {
584
- "model_id_or_path": test_model_id,
585
- "device": "auto", # Let it auto-detect
586
- "torch_dtype_str": "auto",
587
- "num_inference_steps": 10, # Faster for testing
588
- "default_width": 256, # Smaller for faster testing
589
- "default_height": 256,
590
- "safety_checker_on": False, # Often disabled for local use flexibility
591
- "hf_variant": "fp16" if test_model_id == "runwayml/stable-diffusion-v1-5" else None, # SD 1.5 has fp16 variant
592
- }
593
-
594
- try:
595
- ASCIIColors.cyan("\n1. Initializing DiffusersTTIBinding_Impl...")
596
- binding = DiffusersTTIBinding_Impl(config=binding_config, lollms_paths=mock_lollms_paths)
597
- ASCIIColors.green("Initialization successful.")
598
- ASCIIColors.info(f"Loaded model: {binding.current_model_id_or_path}")
599
- ASCIIColors.info(f"Device: {binding.config['device']}, DType: {binding.config['torch_dtype_str']}")
600
- ASCIIColors.info(f"Scheduler: {binding.pipeline.scheduler.__class__.__name__ if binding.pipeline else 'N/A'}")
601
-
602
-
603
- ASCIIColors.cyan("\n2. Listing services...")
604
- services = binding.list_services()
605
- ASCIIColors.info(json.dumps(services, indent=2))
606
- assert services and services[0]["name"] == os.path.basename(binding.current_model_id_or_path)
607
-
608
- ASCIIColors.cyan("\n3. Getting settings...")
609
- settings_list = binding.get_settings()
610
- ASCIIColors.info(json.dumps(settings_list, indent=2, default=str)) # default=str for Path objects if any
611
- # Find model_id_or_path in settings
612
- found_model_setting = any(s['name'] == 'model_id_or_path' and s['value'] == test_model_id for s in settings_list)
613
- assert found_model_setting, "Model ID not found or incorrect in get_settings"
614
-
615
-
616
- ASCIIColors.cyan("\n4. Generating an image...")
617
- test_prompt = "A vibrant cat astronaut exploring a neon galaxy"
618
- test_negative_prompt = "blurry, low quality, text, watermark"
588
+ temp_models_path = temp_paths_dir / "models"
589
+
590
+ if temp_paths_dir.exists(): shutil.rmtree(temp_paths_dir)
591
+ temp_models_path.mkdir(parents=True, exist_ok=True)
619
592
 
620
- # Use smaller dimensions for test if default are large
621
- gen_width = min(binding.config["default_width"], 256)
622
- gen_height = min(binding.config["default_height"], 256)
623
-
624
- image_bytes = binding.generate_image(
625
- prompt=test_prompt,
626
- negative_prompt=test_negative_prompt,
627
- width=gen_width, height=gen_height,
628
- num_inference_steps=8 # Even fewer for speed
629
- )
630
- assert image_bytes and isinstance(image_bytes, bytes)
631
- ASCIIColors.green(f"Image generated successfully (size: {len(image_bytes)} bytes).")
632
- # Save the image for verification
633
- test_image_path = Path(__file__).parent / "test_diffusers_image.png"
634
- with open(test_image_path, "wb") as f:
635
- f.write(image_bytes)
636
- ASCIIColors.info(f"Test image saved to: {test_image_path.resolve()}")
637
-
638
-
639
- ASCIIColors.cyan("\n5. Setting new settings (changing scheduler and guidance_scale)...")
640
- new_settings_dict = {
641
- "scheduler_name": "ddim", # Change scheduler
642
- "guidance_scale": 5.0, # Change guidance scale
643
- "num_inference_steps": 12 # Change inference steps
644
- }
645
- binding.set_settings(new_settings_dict)
646
- assert binding.config["scheduler_name"] == "ddim"
647
- assert binding.config["guidance_scale"] == 5.0
648
- assert binding.config["num_inference_steps"] == 12
649
- ASCIIColors.info(f"New scheduler (intended): ddim, Actual: {binding.pipeline.scheduler.__class__.__name__}")
650
- ASCIIColors.info(f"New guidance_scale: {binding.config['guidance_scale']}")
593
+ try:
594
+ ASCIIColors.cyan("\n--- Test: Loading a Hugging Face model ---")
595
+ # Using a very small model for fast testing
596
+ binding_config = {"models_path": str(temp_models_path), "model_name": "hf-internal-testing/tiny-stable-diffusion-torch"}
597
+ binding = DiffusersTTIBinding_Impl(**binding_config)
651
598
 
652
- ASCIIColors.cyan("\n6. Generating another image with new settings...")
653
- image_bytes_2 = binding.generate_image(
654
- prompt="A serene landscape with a crystal river",
655
- width=gen_width, height=gen_height
656
- )
657
- assert image_bytes_2 and isinstance(image_bytes_2, bytes)
658
- ASCIIColors.green(f"Second image generated successfully (size: {len(image_bytes_2)} bytes).")
659
- test_image_path_2 = Path(__file__).parent / "test_diffusers_image_2.png"
660
- with open(test_image_path_2, "wb") as f:
661
- f.write(image_bytes_2)
662
- ASCIIColors.info(f"Second test image saved to: {test_image_path_2.resolve()}")
663
-
664
- # Test model reload by changing a critical parameter (e.g. safety_checker_on)
665
- # This requires a different model or a config that can be easily toggled.
666
- # For now, assume reload on critical param change works if no error is thrown.
667
- ASCIIColors.cyan("\n7. Testing settings change requiring model reload (safety_checker_on)...")
668
- current_safety_on = binding.config["safety_checker_on"]
669
- binding.set_settings({"safety_checker_on": not current_safety_on})
670
- assert binding.config["safety_checker_on"] == (not current_safety_on)
671
- ASCIIColors.green("Model reload due to safety_checker_on change seems successful.")
599
+ img_bytes = binding.generate_image("a tiny robot", width=64, height=64, num_inference_steps=2)
600
+ assert len(img_bytes) > 1000, "Image generation from HF model should succeed."
601
+ ASCIIColors.green("HF model loading and generation successful.")
672
602
 
603
+ del binding
604
+ time.sleep(0.1)
673
605
 
674
606
  except Exception as e:
675
607
  trace_exception(e)
676
608
  ASCIIColors.error(f"Diffusers binding test failed: {e}")
677
609
  finally:
678
- ASCIIColors.cyan("\nCleaning up...")
679
- if 'binding' in locals() and binding:
680
- binding.unload_model()
681
-
682
- # Clean up temp_lollms_paths
683
- import shutil
610
+ ASCIIColors.cyan("\nCleaning up temporary directories...")
684
611
  if temp_paths_dir.exists():
685
- try:
686
- shutil.rmtree(temp_paths_dir)
687
- ASCIIColors.info(f"Cleaned up temporary directory: {temp_paths_dir}")
688
- except Exception as e_clean:
689
- ASCIIColors.warning(f"Could not fully clean up {temp_paths_dir}: {e_clean}")
690
- if 'test_image_path' in locals() and test_image_path.exists():
691
- # os.remove(test_image_path) # Keep for manual check
692
- pass
693
- if 'test_image_path_2' in locals() and test_image_path_2.exists():
694
- # os.remove(test_image_path_2) # Keep for manual check
695
- pass
612
+ shutil.rmtree(temp_paths_dir)
696
613
  ASCIIColors.magenta("--- Diffusers TTI Binding Test Finished ---")