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.
- lollms_client/__init__.py +1 -1
- lollms_client/llm_bindings/ollama/__init__.py +1 -1
- lollms_client/lollms_core.py +19 -15
- lollms_client/lollms_llm_binding.py +3 -2
- lollms_client/lollms_tti_binding.py +107 -2
- lollms_client/tti_bindings/dalle/__init__.py +24 -4
- lollms_client/tti_bindings/diffusers/__init__.py +494 -577
- lollms_client/tti_bindings/gemini/__init__.py +241 -136
- {lollms_client-1.0.0.dist-info → lollms_client-1.1.1.dist-info}/METADATA +1 -1
- {lollms_client-1.0.0.dist-info → lollms_client-1.1.1.dist-info}/RECORD +13 -13
- {lollms_client-1.0.0.dist-info → lollms_client-1.1.1.dist-info}/WHEEL +0 -0
- {lollms_client-1.0.0.dist-info → lollms_client-1.1.1.dist-info}/licenses/LICENSE +0 -0
- {lollms_client-1.0.0.dist-info → lollms_client-1.1.1.dist-info}/top_level.txt +0 -0
|
@@ -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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
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
|
|
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
|
|
78
|
-
"bfloat16": torch
|
|
79
|
-
"float32": torch
|
|
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:
|
|
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,
|
|
90
|
-
"
|
|
91
|
-
"
|
|
92
|
-
"
|
|
93
|
-
"
|
|
94
|
-
"
|
|
95
|
-
"
|
|
96
|
-
"
|
|
97
|
-
"
|
|
98
|
-
"
|
|
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
|
|
130
|
+
class ModelManager:
|
|
119
131
|
"""
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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.
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
self.
|
|
216
|
-
|
|
148
|
+
def acquire(self):
|
|
149
|
+
with self.lock:
|
|
150
|
+
self.ref_count += 1
|
|
151
|
+
return self
|
|
217
152
|
|
|
218
|
-
|
|
219
|
-
self.
|
|
153
|
+
def release(self):
|
|
154
|
+
with self.lock:
|
|
155
|
+
self.ref_count -= 1
|
|
156
|
+
return self.ref_count
|
|
220
157
|
|
|
221
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
|
|
274
|
-
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
self.pipeline
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
296
|
-
|
|
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
|
-
|
|
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
|
-
|
|
301
|
-
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
raise
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
348
|
-
|
|
349
|
-
self.
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
-
"
|
|
397
|
-
"
|
|
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
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
-
|
|
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
|
|
425
|
-
|
|
486
|
+
return image_bytes
|
|
426
487
|
except Exception as e:
|
|
427
|
-
|
|
428
|
-
|
|
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
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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
|
|
468
|
-
{"name": "default_width", "type": "int", "value": self.config["default_width"], "description": "Default width
|
|
469
|
-
{"name": "default_height", "type": "int", "value": self.config["default_height"], "description": "Default height
|
|
470
|
-
{"name": "num_inference_steps", "type": "int", "value": self.config["num_inference_steps"], "description": "Default
|
|
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
|
|
473
|
-
{"name": "hf_token", "type": "str", "value": self.config["hf_token"], "description": "
|
|
474
|
-
{"name": "
|
|
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
|
-
|
|
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
|
-
|
|
494
|
-
|
|
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
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
if "
|
|
513
|
-
|
|
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
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
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
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
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
|
-
|
|
653
|
-
|
|
654
|
-
|
|
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
|
-
|
|
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 ---")
|