lollms-client 1.6.4__py3-none-any.whl → 1.6.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of lollms-client might be problematic. Click here for more details.
- lollms_client/__init__.py +1 -1
- lollms_client/lollms_core.py +3 -1
- lollms_client/tti_bindings/diffusers/__init__.py +129 -59
- lollms_client/tti_bindings/diffusers/server/main.py +354 -118
- lollms_client/tti_bindings/gemini/__init__.py +179 -239
- lollms_client/tts_bindings/xtts/__init__.py +106 -81
- lollms_client/tts_bindings/xtts/server/main.py +128 -183
- {lollms_client-1.6.4.dist-info → lollms_client-1.6.6.dist-info}/METADATA +1 -1
- {lollms_client-1.6.4.dist-info → lollms_client-1.6.6.dist-info}/RECORD +12 -12
- {lollms_client-1.6.4.dist-info → lollms_client-1.6.6.dist-info}/WHEEL +0 -0
- {lollms_client-1.6.4.dist-info → lollms_client-1.6.6.dist-info}/licenses/LICENSE +0 -0
- {lollms_client-1.6.4.dist-info → lollms_client-1.6.6.dist-info}/top_level.txt +0 -0
|
@@ -18,9 +18,12 @@ import gc
|
|
|
18
18
|
import argparse
|
|
19
19
|
import uvicorn
|
|
20
20
|
from fastapi import FastAPI, APIRouter, HTTPException, UploadFile, Form
|
|
21
|
+
from fastapi import Request, Response
|
|
21
22
|
from fastapi.responses import Response
|
|
22
23
|
from pydantic import BaseModel, Field
|
|
23
24
|
import sys
|
|
25
|
+
import platform
|
|
26
|
+
import inspect
|
|
24
27
|
|
|
25
28
|
# Add binding root to sys.path to ensure local modules can be imported if structured that way.
|
|
26
29
|
binding_root = Path(__file__).resolve().parent.parent
|
|
@@ -53,13 +56,13 @@ except ImportError as e:
|
|
|
53
56
|
# --- Server Setup ---
|
|
54
57
|
app = FastAPI(title="Diffusers TTI Server")
|
|
55
58
|
router = APIRouter()
|
|
56
|
-
MODELS_PATH = Path("./models")
|
|
59
|
+
MODELS_PATH = Path("./models")
|
|
57
60
|
|
|
58
61
|
# --- START: Core Logic (Complete and Unabridged) ---
|
|
59
62
|
CIVITAI_MODELS = {
|
|
60
63
|
"realistic-vision-v6": {
|
|
61
64
|
"display_name": "Realistic Vision V6.0", "url": "https://civitai.com/api/download/models/501240?type=Model&format=SafeTensor&size=pruned&fp=fp16",
|
|
62
|
-
"filename": "realisticVisionV60_v60B1.
|
|
65
|
+
"filename": "realisticVisionV60_v60B1.safensors", "description": "Photorealistic SD1.5 checkpoint.", "owned_by": "civitai"
|
|
63
66
|
},
|
|
64
67
|
"absolute-reality": {
|
|
65
68
|
"display_name": "Absolute Reality", "url": "https://civitai.com/api/download/models/132760?type=Model&format=SafeTensor&size=pruned&fp=fp16",
|
|
@@ -119,6 +122,47 @@ CIVITAI_MODELS = {
|
|
|
119
122
|
},
|
|
120
123
|
}
|
|
121
124
|
|
|
125
|
+
HF_PUBLIC_MODELS = {
|
|
126
|
+
"General Purpose & SDXL": [
|
|
127
|
+
{"model_name": "stabilityai/stable-diffusion-xl-base-1.0", "display_name": "Stable Diffusion XL 1.0", "desc": "Official 1024x1024 text-to-image model from Stability AI."},
|
|
128
|
+
{"model_name": "stabilityai/sdxl-turbo", "display_name": "SDXL Turbo", "desc": "A fast, real-time text-to-image model based on SDXL."},
|
|
129
|
+
{"model_name": "kandinsky-community/kandinsky-3", "display_name": "Kandinsky 3", "desc": "A powerful multilingual model with strong prompt understanding and aesthetic quality."},
|
|
130
|
+
{"model_name": "playgroundai/playground-v2.5-1024px-aesthetic", "display_name": "Playground v2.5", "desc": "A high-quality model focused on aesthetic outputs."},
|
|
131
|
+
],
|
|
132
|
+
"Photorealistic": [
|
|
133
|
+
{"model_name": "emilianJR/epiCRealism", "display_name": "epiCRealism", "desc": "A popular community model for generating photorealistic images."},
|
|
134
|
+
{"model_name": "SG161222/Realistic_Vision_V5.1_noVAE", "display_name": "Realistic Vision 5.1", "desc": "One of the most popular realistic models, great for portraits and scenes."},
|
|
135
|
+
{"model_name": "Photon-v1", "display_name": "Photon", "desc": "A model known for high-quality, realistic images with good lighting and detail."},
|
|
136
|
+
],
|
|
137
|
+
"Anime & Illustration": [
|
|
138
|
+
{"model_name": "hakurei/waifu-diffusion", "display_name": "Waifu Diffusion 1.4", "desc": "A widely-used model for generating high-quality anime-style images."},
|
|
139
|
+
{"model_name": "gsdf/Counterfeit-V3.0", "display_name": "Counterfeit V3.0", "desc": "A strong model for illustrative and 2.5D anime styles."},
|
|
140
|
+
{"model_name": "cagliostrolab/animagine-xl-3.0", "display_name": "Animagine XL 3.0", "desc": "A state-of-the-art anime model on the SDXL architecture."},
|
|
141
|
+
],
|
|
142
|
+
"Artistic & Stylized": [
|
|
143
|
+
{"model_name": "wavymulder/Analog-Diffusion", "display_name": "Analog Diffusion", "desc": "Creates images with a vintage, analog film aesthetic."},
|
|
144
|
+
{"model_name": "dreamlike-art/dreamlike-photoreal-2.0", "display_name": "Dreamlike Photoreal 2.0", "desc": "Produces stunning, artistic, and photorealistic images."},
|
|
145
|
+
],
|
|
146
|
+
"Image Editing Tools": [
|
|
147
|
+
{"model_name": "stabilityai/stable-diffusion-xl-refiner-1.0", "display_name": "SDXL Refiner 1.0", "desc": "A dedicated refiner model to improve details in SDXL generations."},
|
|
148
|
+
{"model_name": "Qwen/Qwen-Image-Edit", "display_name": "Qwen Image Edit", "desc": "An instruction-based model for various image editing tasks."},
|
|
149
|
+
{"model_name": "Qwen/Qwen-Image-Edit-2509", "display_name": "Qwen Image Edit Plus", "desc": "Advanced multi-image editing, fusion, and pose transfer."},
|
|
150
|
+
],
|
|
151
|
+
"Legacy & Base Models": [
|
|
152
|
+
{"model_name": "runwayml/stable-diffusion-v1-5", "display_name": "Stable Diffusion 1.5", "desc": "The classic and versatile SD1.5 base model."},
|
|
153
|
+
{"model_name": "stabilityai/stable-diffusion-2-1", "display_name": "Stable Diffusion 2.1", "desc": "The 768x768 base model from the SD2.x series."},
|
|
154
|
+
]
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
HF_GATED_MODELS = {
|
|
158
|
+
"Next-Generation (Gated Access Required)": [
|
|
159
|
+
{"model_name": "stabilityai/stable-diffusion-3-medium-diffusers", "display_name": "Stable Diffusion 3 Medium", "desc": "State-of-the-art model with advanced prompt understanding. Requires free registration."},
|
|
160
|
+
{"model_name": "black-forest-labs/FLUX.1-schnell", "display_name": "FLUX.1 Schnell", "desc": "A powerful and extremely fast next-generation model. Requires access request."},
|
|
161
|
+
{"model_name": "black-forest-labs/FLUX.1-dev", "display_name": "FLUX.1 Dev", "desc": "The larger developer version of the FLUX.1 model. Requires access request."},
|
|
162
|
+
]
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
|
|
122
166
|
TORCH_DTYPE_MAP_STR_TO_OBJ = {
|
|
123
167
|
"float16": getattr(torch, 'float16', 'float16'), "bfloat16": getattr(torch, 'bfloat16', 'bfloat16'),
|
|
124
168
|
"float32": getattr(torch, 'float32', 'float32'), "auto": "auto"
|
|
@@ -141,6 +185,7 @@ SCHEDULER_USES_KARRAS_SIGMAS = [
|
|
|
141
185
|
"dpm++_2m_sde_karras","dpm2_karras","dpm2_a_karras"
|
|
142
186
|
]
|
|
143
187
|
|
|
188
|
+
|
|
144
189
|
class ModelManager:
|
|
145
190
|
def __init__(self, config: Dict[str, Any], models_path: Path, registry: 'PipelineRegistry'):
|
|
146
191
|
self.config = config
|
|
@@ -159,6 +204,7 @@ class ModelManager:
|
|
|
159
204
|
self._stop_monitor_event = threading.Event()
|
|
160
205
|
self._unload_monitor_thread = None
|
|
161
206
|
self._start_unload_monitor()
|
|
207
|
+
self.supported_args: Optional[set] = None
|
|
162
208
|
|
|
163
209
|
def acquire(self):
|
|
164
210
|
with self.lock:
|
|
@@ -208,9 +254,25 @@ class ModelManager:
|
|
|
208
254
|
if not local_path.exists():
|
|
209
255
|
self._download_civitai_model(model_name)
|
|
210
256
|
return local_path
|
|
257
|
+
|
|
258
|
+
# Search in extra models path
|
|
259
|
+
if state.extra_models_path and state.extra_models_path.exists():
|
|
260
|
+
found_paths = list(state.extra_models_path.rglob(model_name))
|
|
261
|
+
if found_paths:
|
|
262
|
+
ASCIIColors.info(f"Found model in extra path: {found_paths[0]}")
|
|
263
|
+
return found_paths[0]
|
|
264
|
+
|
|
265
|
+
# Search in primary models path
|
|
266
|
+
found_paths = list(self.models_path.rglob(model_name))
|
|
267
|
+
if found_paths:
|
|
268
|
+
ASCIIColors.info(f"Found model in primary path: {found_paths[0]}")
|
|
269
|
+
return found_paths[0]
|
|
270
|
+
|
|
271
|
+
# Fallback for HF hub models that are folders, not single files.
|
|
211
272
|
local_path = self.models_path / model_name
|
|
212
273
|
if local_path.exists():
|
|
213
274
|
return local_path
|
|
275
|
+
|
|
214
276
|
return model_name
|
|
215
277
|
|
|
216
278
|
def _download_civitai_model(self, model_key: str):
|
|
@@ -219,7 +281,7 @@ class ModelManager:
|
|
|
219
281
|
filename = model_info["filename"]
|
|
220
282
|
dest_path = self.models_path / filename
|
|
221
283
|
temp_path = dest_path.with_suffix(".temp")
|
|
222
|
-
ASCIIColors.cyan(f"Downloading '{filename}' from Civitai...")
|
|
284
|
+
ASCIIColors.cyan(f"Downloading '{filename}' from Civitai... to {dest_path}")
|
|
223
285
|
try:
|
|
224
286
|
with requests.get(url, stream=True) as r:
|
|
225
287
|
r.raise_for_status()
|
|
@@ -233,13 +295,13 @@ class ModelManager:
|
|
|
233
295
|
except Exception as e:
|
|
234
296
|
if temp_path.exists():
|
|
235
297
|
temp_path.unlink()
|
|
236
|
-
raise Exception(f"Failed to download model {filename}: {e}")
|
|
298
|
+
raise Exception(f"Failed to download model {filename}: {e}")
|
|
237
299
|
|
|
238
300
|
def _set_scheduler(self):
|
|
239
301
|
if not self.pipeline:
|
|
240
302
|
return
|
|
241
|
-
if "Qwen" in self.config.get("model_name", ""):
|
|
242
|
-
ASCIIColors.info("
|
|
303
|
+
if "Qwen" in self.config.get("model_name", "") or "FLUX" in self.config.get("model_name", ""):
|
|
304
|
+
ASCIIColors.info("Special model detected, skipping custom scheduler setup.")
|
|
243
305
|
return
|
|
244
306
|
scheduler_name_key = self.config["scheduler_name"].lower()
|
|
245
307
|
if scheduler_name_key == "default":
|
|
@@ -256,71 +318,116 @@ class ModelManager:
|
|
|
256
318
|
ASCIIColors.warning(f"Could not switch scheduler to {scheduler_name_key}: {e}. Using current default.")
|
|
257
319
|
|
|
258
320
|
def _execute_load_pipeline(self, task: str, model_path: Union[str, Path], torch_dtype: Any):
|
|
259
|
-
|
|
321
|
+
if platform.system() == "Windows":
|
|
322
|
+
os.environ["HF_HUB_ENABLE_SYMLINKS"] = "0"
|
|
323
|
+
|
|
324
|
+
model_name_from_config = self.config.get("model_name", "")
|
|
325
|
+
use_device_map = False
|
|
326
|
+
|
|
260
327
|
try:
|
|
261
|
-
|
|
328
|
+
load_params = {}
|
|
262
329
|
if self.config.get("hf_cache_path"):
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
elif task == "inpainting":
|
|
273
|
-
self.pipeline = AutoPipelineForInpainting.from_single_file(model_path, torch_dtype=torch_dtype, cache_dir=load_args.get("cache_dir"))
|
|
274
|
-
else:
|
|
275
|
-
common_args = {
|
|
276
|
-
"torch_dtype": torch_dtype,
|
|
330
|
+
load_params["cache_dir"] = str(self.config["hf_cache_path"])
|
|
331
|
+
load_params["torch_dtype"] = torch_dtype
|
|
332
|
+
|
|
333
|
+
is_qwen_model = "Qwen" in model_name_from_config
|
|
334
|
+
is_flux_model = "FLUX" in model_name_from_config
|
|
335
|
+
|
|
336
|
+
if is_qwen_model or is_flux_model:
|
|
337
|
+
ASCIIColors.info(f"Special model '{model_name_from_config}' detected. Using dedicated pipeline loader.")
|
|
338
|
+
load_params.update({
|
|
277
339
|
"use_safetensors": self.config["use_safetensors"],
|
|
278
340
|
"token": self.config["hf_token"],
|
|
279
341
|
"local_files_only": self.config["local_files_only"]
|
|
280
|
-
}
|
|
342
|
+
})
|
|
281
343
|
if self.config["hf_variant"]:
|
|
282
|
-
|
|
344
|
+
load_params["variant"] = self.config["hf_variant"]
|
|
283
345
|
if not self.config["safety_checker_on"]:
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
self.pipeline =
|
|
294
|
-
elif
|
|
295
|
-
self.pipeline =
|
|
296
|
-
elif
|
|
297
|
-
self.pipeline =
|
|
298
|
-
elif
|
|
299
|
-
self.pipeline =
|
|
346
|
+
load_params["safety_checker"] = None
|
|
347
|
+
|
|
348
|
+
should_offload = self.config["enable_cpu_offload"] or self.config["enable_sequential_cpu_offload"]
|
|
349
|
+
if should_offload:
|
|
350
|
+
ASCIIColors.info(f"Offload enabled. Forcing device_map='auto' for {model_name_from_config}.")
|
|
351
|
+
use_device_map = True
|
|
352
|
+
load_params["device_map"] = "auto"
|
|
353
|
+
|
|
354
|
+
if is_flux_model:
|
|
355
|
+
self.pipeline = AutoPipelineForText2Image.from_pretrained(model_name_from_config, **load_params)
|
|
356
|
+
elif "Qwen-Image-Edit-2509" in model_name_from_config:
|
|
357
|
+
self.pipeline = QwenImageEditPlusPipeline.from_pretrained(model_name_from_config, **load_params)
|
|
358
|
+
elif "Qwen-Image-Edit" in model_name_from_config:
|
|
359
|
+
self.pipeline = QwenImageEditPipeline.from_pretrained(model_name_from_config, **load_params)
|
|
360
|
+
elif "Qwen/Qwen-Image" in model_name_from_config:
|
|
361
|
+
self.pipeline = DiffusionPipeline.from_pretrained(model_name_from_config, **load_params)
|
|
362
|
+
|
|
363
|
+
else:
|
|
364
|
+
is_safetensors_file = str(model_path).endswith(".safetensors")
|
|
365
|
+
if is_safetensors_file:
|
|
366
|
+
ASCIIColors.info(f"Loading standard model from local .safetensors file: {model_path}")
|
|
367
|
+
try:
|
|
368
|
+
self.pipeline = AutoPipelineForText2Image.from_single_file(model_path, **load_params)
|
|
369
|
+
except Exception as e:
|
|
370
|
+
ASCIIColors.warning(f"Failed to load with AutoPipeline, falling back to StableDiffusionPipeline: {e}")
|
|
371
|
+
self.pipeline = StableDiffusionPipeline.from_single_file(model_path, **load_params)
|
|
372
|
+
else:
|
|
373
|
+
ASCIIColors.info(f"Loading standard model from Hub: {model_path}")
|
|
374
|
+
load_params.update({
|
|
375
|
+
"use_safetensors": self.config["use_safetensors"],
|
|
376
|
+
"token": self.config["hf_token"],
|
|
377
|
+
"local_files_only": self.config["local_files_only"]
|
|
378
|
+
})
|
|
379
|
+
if self.config["hf_variant"]:
|
|
380
|
+
load_params["variant"] = self.config["hf_variant"]
|
|
381
|
+
if not self.config["safety_checker_on"]:
|
|
382
|
+
load_params["safety_checker"] = None
|
|
383
|
+
|
|
384
|
+
is_large_model = "stable-diffusion-3" in str(model_path)
|
|
385
|
+
should_offload = self.config["enable_cpu_offload"] or self.config["enable_sequential_cpu_offload"]
|
|
386
|
+
if is_large_model and should_offload:
|
|
387
|
+
ASCIIColors.info(f"Large model '{model_path}' detected with offload enabled. Using device_map='auto'.")
|
|
388
|
+
use_device_map = True
|
|
389
|
+
load_params["device_map"] = "auto"
|
|
390
|
+
|
|
391
|
+
if task == "text2image":
|
|
392
|
+
self.pipeline = AutoPipelineForText2Image.from_pretrained(model_path, **load_params)
|
|
393
|
+
elif task == "image2image":
|
|
394
|
+
self.pipeline = AutoPipelineForImage2Image.from_pretrained(model_path, **load_params)
|
|
395
|
+
elif task == "inpainting":
|
|
396
|
+
self.pipeline = AutoPipelineForInpainting.from_pretrained(model_path, **load_params)
|
|
397
|
+
|
|
300
398
|
except Exception as e:
|
|
301
399
|
error_str = str(e).lower()
|
|
302
400
|
if "401" in error_str or "gated" in error_str or "authorization" in error_str:
|
|
303
|
-
msg = (
|
|
304
|
-
f"AUTHENTICATION FAILED for model '{model_name}'. "
|
|
305
|
-
"Please ensure you accepted the model license and provided a valid HF token."
|
|
306
|
-
)
|
|
401
|
+
msg = (f"AUTHENTICATION FAILED for model '{model_name_from_config}'. Please ensure you accepted the model license and provided a valid HF token.")
|
|
307
402
|
raise RuntimeError(msg)
|
|
308
403
|
raise e
|
|
404
|
+
|
|
309
405
|
self._set_scheduler()
|
|
310
|
-
|
|
311
|
-
if
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
self.
|
|
406
|
+
|
|
407
|
+
if not use_device_map:
|
|
408
|
+
self.pipeline.to(self.config["device"])
|
|
409
|
+
if self.config["enable_xformers"]:
|
|
410
|
+
try:
|
|
411
|
+
self.pipeline.enable_xformers_memory_efficient_attention()
|
|
412
|
+
except Exception as e:
|
|
413
|
+
ASCIIColors.warning(f"Could not enable xFormers: {e}.")
|
|
414
|
+
|
|
415
|
+
if self.config["enable_cpu_offload"] and self.config["device"] != "cpu":
|
|
416
|
+
self.pipeline.enable_model_cpu_offload()
|
|
417
|
+
elif self.config["enable_sequential_cpu_offload"] and self.config["device"] != "cpu":
|
|
418
|
+
self.pipeline.enable_sequential_cpu_offload()
|
|
419
|
+
else:
|
|
420
|
+
ASCIIColors.info("Device map handled device placement. Skipping manual pipeline.to() and offload calls.")
|
|
421
|
+
|
|
422
|
+
if self.pipeline:
|
|
423
|
+
sig = inspect.signature(self.pipeline.__call__)
|
|
424
|
+
self.supported_args = {p.name for p in sig.parameters.values()}
|
|
425
|
+
ASCIIColors.info(f"Pipeline supported arguments detected: {self.supported_args}")
|
|
426
|
+
|
|
320
427
|
self.is_loaded = True
|
|
321
428
|
self.current_task = task
|
|
322
429
|
self.last_used_time = time.time()
|
|
323
|
-
ASCIIColors.green(f"Model '{
|
|
430
|
+
ASCIIColors.green(f"Model '{model_name_from_config}' loaded successfully using '{'device_map' if use_device_map else 'standard'}' mode for task '{task}'.")
|
|
324
431
|
|
|
325
432
|
def _load_pipeline_for_task(self, task: str):
|
|
326
433
|
if self.pipeline and self.current_task == task:
|
|
@@ -346,10 +453,7 @@ class ModelManager:
|
|
|
346
453
|
|
|
347
454
|
ASCIIColors.warning(f"Failed to load '{model_name}' due to OOM. Attempting to unload other models to free VRAM.")
|
|
348
455
|
|
|
349
|
-
candidates_to_unload = [
|
|
350
|
-
m for m in self.registry.get_all_managers()
|
|
351
|
-
if m is not self and m.is_loaded
|
|
352
|
-
]
|
|
456
|
+
candidates_to_unload = [m for m in self.registry.get_all_managers() if m is not self and m.is_loaded]
|
|
353
457
|
candidates_to_unload.sort(key=lambda m: m.last_used_time)
|
|
354
458
|
|
|
355
459
|
if not candidates_to_unload:
|
|
@@ -378,6 +482,7 @@ class ModelManager:
|
|
|
378
482
|
model_name = self.config.get('model_name', 'Unknown')
|
|
379
483
|
del self.pipeline
|
|
380
484
|
self.pipeline = None
|
|
485
|
+
self.supported_args = None
|
|
381
486
|
gc.collect()
|
|
382
487
|
if torch and torch.cuda.is_available():
|
|
383
488
|
torch.cuda.empty_cache()
|
|
@@ -398,8 +503,16 @@ class ModelManager:
|
|
|
398
503
|
self.last_used_time = time.time()
|
|
399
504
|
if not self.is_loaded or self.current_task != task:
|
|
400
505
|
self._load_pipeline_for_task(task)
|
|
506
|
+
|
|
507
|
+
if self.supported_args:
|
|
508
|
+
filtered_args = {k: v for k, v in pipeline_args.items() if k in self.supported_args}
|
|
509
|
+
else:
|
|
510
|
+
ASCIIColors.warning("Supported argument set not found. Using unfiltered arguments.")
|
|
511
|
+
filtered_args = pipeline_args
|
|
512
|
+
|
|
401
513
|
with torch.no_grad():
|
|
402
|
-
output = self.pipeline(**
|
|
514
|
+
output = self.pipeline(**filtered_args)
|
|
515
|
+
|
|
403
516
|
pil = output.images[0]
|
|
404
517
|
buf = BytesIO()
|
|
405
518
|
pil.save(buf, format="PNG")
|
|
@@ -409,7 +522,6 @@ class ModelManager:
|
|
|
409
522
|
future.set_exception(e)
|
|
410
523
|
finally:
|
|
411
524
|
self.queue.task_done()
|
|
412
|
-
# Aggressive cleanup
|
|
413
525
|
if output is not None:
|
|
414
526
|
del output
|
|
415
527
|
gc.collect()
|
|
@@ -465,24 +577,25 @@ class PipelineRegistry:
|
|
|
465
577
|
return list(self._managers.values())
|
|
466
578
|
|
|
467
579
|
class ServerState:
|
|
468
|
-
def __init__(self, models_path: Path):
|
|
580
|
+
def __init__(self, models_path: Path, extra_models_path: Optional[Path] = None):
|
|
469
581
|
self.models_path = models_path
|
|
582
|
+
self.extra_models_path = extra_models_path
|
|
470
583
|
self.models_path.mkdir(parents=True, exist_ok=True)
|
|
584
|
+
if self.extra_models_path:
|
|
585
|
+
self.extra_models_path.mkdir(parents=True, exist_ok=True)
|
|
471
586
|
self.config_path = self.models_path.parent / "diffusers_server_config.json"
|
|
472
587
|
self.registry = PipelineRegistry()
|
|
473
588
|
self.manager: Optional[ModelManager] = None
|
|
474
589
|
self.config = {}
|
|
475
|
-
self.load_config()
|
|
590
|
+
self.load_config()
|
|
476
591
|
self._resolve_device_and_dtype()
|
|
477
|
-
|
|
478
|
-
# Eagerly acquire manager at startup if a model is configured
|
|
479
592
|
if self.config.get("model_name"):
|
|
480
593
|
try:
|
|
481
594
|
ASCIIColors.info(f"Acquiring initial model manager for '{self.config['model_name']}' on startup.")
|
|
482
595
|
self.manager = self.registry.get_manager(self.config, self.models_path)
|
|
483
596
|
except Exception as e:
|
|
484
597
|
ASCIIColors.error(f"Failed to acquire model manager on startup: {e}")
|
|
485
|
-
self.manager = None
|
|
598
|
+
self.manager = None
|
|
486
599
|
|
|
487
600
|
def get_default_config(self) -> Dict[str, Any]:
|
|
488
601
|
return {
|
|
@@ -495,7 +608,6 @@ class ServerState:
|
|
|
495
608
|
}
|
|
496
609
|
|
|
497
610
|
def save_config(self):
|
|
498
|
-
"""Saves the current configuration to a JSON file."""
|
|
499
611
|
try:
|
|
500
612
|
with open(self.config_path, 'w') as f:
|
|
501
613
|
json.dump(self.config, f, indent=4)
|
|
@@ -504,13 +616,11 @@ class ServerState:
|
|
|
504
616
|
ASCIIColors.error(f"Failed to save server config: {e}")
|
|
505
617
|
|
|
506
618
|
def load_config(self):
|
|
507
|
-
"""Loads configuration from JSON file, falling back to defaults."""
|
|
508
619
|
default_config = self.get_default_config()
|
|
509
620
|
if self.config_path.exists():
|
|
510
621
|
try:
|
|
511
622
|
with open(self.config_path, 'r') as f:
|
|
512
623
|
loaded_config = json.load(f)
|
|
513
|
-
# Merge loaded config into defaults to ensure all keys are present
|
|
514
624
|
default_config.update(loaded_config)
|
|
515
625
|
self.config = default_config
|
|
516
626
|
ASCIIColors.info(f"Loaded server configuration from {self.config_path}")
|
|
@@ -519,53 +629,45 @@ class ServerState:
|
|
|
519
629
|
self.config = default_config
|
|
520
630
|
else:
|
|
521
631
|
self.config = default_config
|
|
522
|
-
# Save back to ensure file exists and is up-to-date with all keys
|
|
523
632
|
self.save_config()
|
|
524
633
|
|
|
525
634
|
def _resolve_device_and_dtype(self):
|
|
526
635
|
if self.config.get("device", "auto").lower() == "auto":
|
|
527
636
|
self.config["device"] = "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu"
|
|
528
637
|
|
|
529
|
-
|
|
530
|
-
if "Qwen" in self.config.get("model_name", "") and self.config["device"] == "cuda":
|
|
638
|
+
if ("Qwen" in self.config.get("model_name", "") or "FLUX" in self.config.get("model_name", "")) and self.config["device"] == "cuda":
|
|
531
639
|
if hasattr(torch.cuda, 'is_bf16_supported') and torch.cuda.is_bf16_supported():
|
|
532
640
|
self.config["torch_dtype_str"] = "bfloat16"
|
|
533
|
-
ASCIIColors.info("
|
|
641
|
+
ASCIIColors.info("Special model detected on compatible hardware. Forcing dtype to bfloat16 for stability.")
|
|
534
642
|
return
|
|
535
643
|
|
|
536
644
|
if self.config["torch_dtype_str"].lower() == "auto":
|
|
537
645
|
self.config["torch_dtype_str"] = "float16" if self.config["device"] != "cpu" else "float32"
|
|
538
646
|
|
|
539
647
|
def update_settings(self, new_settings: Dict[str, Any]):
|
|
540
|
-
"""Updates settings, swaps the manager if critical settings change, and saves the config."""
|
|
541
648
|
if 'model' in new_settings and 'model_name' not in new_settings:
|
|
542
649
|
new_settings['model_name'] = new_settings.pop('model')
|
|
543
650
|
|
|
544
|
-
# Safeguard: If a model is already configured and the new settings don't specify one,
|
|
545
|
-
# keep the old one. This prevents a misconfigured client from wiping a valid server state.
|
|
546
651
|
if self.config.get("model_name") and not new_settings.get("model_name"):
|
|
547
652
|
ASCIIColors.info("Incoming settings have no model_name. Preserving existing model.")
|
|
548
653
|
new_settings["model_name"] = self.config["model_name"]
|
|
549
654
|
|
|
550
|
-
# Release old manager if it exists
|
|
551
655
|
if self.manager:
|
|
552
656
|
self.registry.release_manager(self.manager.config)
|
|
553
657
|
self.manager = None
|
|
554
658
|
|
|
555
|
-
# Update the config in memory
|
|
556
659
|
self.config.update(new_settings)
|
|
557
660
|
ASCIIColors.info(f"Server config updated. Current model_name: {self.config.get('model_name')}")
|
|
558
661
|
|
|
559
662
|
self._resolve_device_and_dtype()
|
|
560
663
|
|
|
561
|
-
# Acquire new manager with the updated config
|
|
562
664
|
if self.config.get("model_name"):
|
|
563
665
|
ASCIIColors.info("Acquiring model manager with updated configuration...")
|
|
564
666
|
self.manager = self.registry.get_manager(self.config, self.models_path)
|
|
565
667
|
else:
|
|
566
668
|
ASCIIColors.warning("No model_name in config after update, manager not acquired.")
|
|
567
669
|
|
|
568
|
-
self.save_config()
|
|
670
|
+
self.save_config()
|
|
569
671
|
return True
|
|
570
672
|
|
|
571
673
|
def get_active_manager(self) -> ModelManager:
|
|
@@ -586,77 +688,208 @@ class EditRequestPayload(BaseModel):
|
|
|
586
688
|
image_paths: List[str] = Field(default_factory=list)
|
|
587
689
|
params: Dict[str, Any] = Field(default_factory=dict)
|
|
588
690
|
|
|
691
|
+
class EditRequestJSON(BaseModel):
|
|
692
|
+
prompt: str
|
|
693
|
+
images_b64: List[str] = Field(description="A list of Base64 encoded image strings.")
|
|
694
|
+
params: Dict[str, Any] = Field(default_factory=dict)
|
|
695
|
+
def get_sanitized_request_for_logging(request_data: Any) -> Dict[str, Any]:
|
|
696
|
+
"""
|
|
697
|
+
Takes a request object (Pydantic model or dict) and returns a 'safe' dictionary
|
|
698
|
+
for logging, with long base64 strings replaced by placeholders.
|
|
699
|
+
"""
|
|
700
|
+
import copy
|
|
701
|
+
|
|
702
|
+
try:
|
|
703
|
+
if hasattr(request_data, 'model_dump'):
|
|
704
|
+
data = request_data.model_dump()
|
|
705
|
+
elif isinstance(request_data, dict):
|
|
706
|
+
data = copy.deepcopy(request_data)
|
|
707
|
+
else:
|
|
708
|
+
return {"error": "Unsupported data type for sanitization"}
|
|
709
|
+
|
|
710
|
+
# Sanitize the main list of images
|
|
711
|
+
if 'images_b64' in data and isinstance(data['images_b64'], list):
|
|
712
|
+
count = len(data['images_b64'])
|
|
713
|
+
data['images_b64'] = f"[<{count} base64 image(s) truncated>]"
|
|
714
|
+
|
|
715
|
+
# Sanitize a potential mask in the 'params' dictionary
|
|
716
|
+
if 'params' in data and isinstance(data.get('params'), dict):
|
|
717
|
+
if 'mask_image' in data['params'] and isinstance(data['params']['mask_image'], str):
|
|
718
|
+
original_len = len(data['params']['mask_image'])
|
|
719
|
+
data['params']['mask_image'] = f"[<base64 mask truncated, len={original_len}>]"
|
|
720
|
+
|
|
721
|
+
return data
|
|
722
|
+
except Exception:
|
|
723
|
+
return {"error": "Failed to sanitize request data."}
|
|
724
|
+
|
|
589
725
|
# --- API Endpoints ---
|
|
590
726
|
@router.post("/generate_image")
|
|
591
727
|
async def generate_image(request: T2IRequest):
|
|
728
|
+
manager = None
|
|
729
|
+
temp_config = None
|
|
592
730
|
try:
|
|
593
|
-
manager = state.get_active_manager()
|
|
594
731
|
params = request.params
|
|
595
|
-
|
|
732
|
+
|
|
733
|
+
# Determine which model manager to use for this specific request
|
|
734
|
+
if "model_name" in params and params["model_name"]:
|
|
735
|
+
temp_config = state.config.copy()
|
|
736
|
+
temp_config["model_name"] = params.pop("model_name") # Remove from params to avoid being passed to pipeline
|
|
737
|
+
manager = state.registry.get_manager(temp_config, state.models_path)
|
|
738
|
+
ASCIIColors.info(f"Using per-request model: {temp_config['model_name']}")
|
|
739
|
+
else:
|
|
740
|
+
manager = state.get_active_manager()
|
|
741
|
+
ASCIIColors.info(f"Using session-configured model: {manager.config.get('model_name')}")
|
|
742
|
+
|
|
743
|
+
seed = int(params.get("seed", manager.config.get("seed", -1)))
|
|
596
744
|
generator = None
|
|
597
745
|
if seed != -1:
|
|
598
|
-
generator = torch.Generator(device=
|
|
599
|
-
|
|
746
|
+
generator = torch.Generator(device=manager.config["device"]).manual_seed(seed)
|
|
747
|
+
|
|
748
|
+
width = int(params.get("width", manager.config.get("width", 512)))
|
|
749
|
+
height = int(params.get("height", manager.config.get("height", 512)))
|
|
750
|
+
|
|
600
751
|
pipeline_args = {
|
|
601
|
-
"prompt": request.prompt,
|
|
602
|
-
"
|
|
603
|
-
"
|
|
604
|
-
"
|
|
605
|
-
"
|
|
752
|
+
"prompt": request.prompt,
|
|
753
|
+
"negative_prompt": request.negative_prompt,
|
|
754
|
+
"width": width,
|
|
755
|
+
"height": height,
|
|
756
|
+
"num_inference_steps": int(params.get("num_inference_steps", manager.config.get("num_inference_steps", 25))),
|
|
757
|
+
"guidance_scale": float(params.get("guidance_scale", manager.config.get("guidance_scale", 7.0))),
|
|
606
758
|
"generator": generator
|
|
607
759
|
}
|
|
760
|
+
pipeline_args.update(params)
|
|
761
|
+
|
|
762
|
+
model_name = manager.config.get("model_name", "")
|
|
763
|
+
task = "text2image"
|
|
764
|
+
|
|
765
|
+
if "Qwen-Image-Edit" in model_name:
|
|
766
|
+
rng_seed = seed if seed != -1 else None
|
|
767
|
+
rng = np.random.default_rng(seed=rng_seed)
|
|
768
|
+
random_pixels = rng.integers(0, 256, size=(height, width, 3), dtype=np.uint8)
|
|
769
|
+
placeholder_image = Image.fromarray(random_pixels, 'RGB')
|
|
770
|
+
pipeline_args["image"] = placeholder_image
|
|
771
|
+
pipeline_args["strength"] = float(params.get("strength", 1.0))
|
|
772
|
+
task = "image2image"
|
|
608
773
|
|
|
609
774
|
future = Future()
|
|
610
|
-
manager.queue.put((future,
|
|
775
|
+
manager.queue.put((future, task, pipeline_args))
|
|
611
776
|
result_bytes = future.result()
|
|
612
777
|
return Response(content=result_bytes, media_type="image/png")
|
|
613
778
|
except Exception as e:
|
|
614
779
|
trace_exception(e)
|
|
615
780
|
raise HTTPException(status_code=500, detail=str(e))
|
|
781
|
+
finally:
|
|
782
|
+
if temp_config and manager:
|
|
783
|
+
state.registry.release_manager(temp_config)
|
|
784
|
+
ASCIIColors.info(f"Released per-request model: {temp_config['model_name']}")
|
|
785
|
+
|
|
616
786
|
|
|
617
787
|
@router.post("/edit_image")
|
|
618
|
-
async def edit_image(
|
|
788
|
+
async def edit_image(request: EditRequestJSON):
|
|
789
|
+
manager = None
|
|
790
|
+
temp_config = None
|
|
619
791
|
try:
|
|
620
|
-
|
|
621
|
-
|
|
792
|
+
params = request.params
|
|
793
|
+
|
|
794
|
+
if "model_name" in params and params["model_name"]:
|
|
795
|
+
temp_config = state.config.copy()
|
|
796
|
+
temp_config["model_name"] = params.pop("model_name")
|
|
797
|
+
manager = state.registry.get_manager(temp_config, state.models_path)
|
|
798
|
+
ASCIIColors.info(f"Using per-request model: {temp_config['model_name']}")
|
|
799
|
+
else:
|
|
800
|
+
manager = state.get_active_manager()
|
|
801
|
+
ASCIIColors.info(f"Using session-configured model: {manager.config.get('model_name')}")
|
|
802
|
+
|
|
803
|
+
model_name = manager.config.get("model_name", "")
|
|
622
804
|
|
|
623
805
|
pil_images = []
|
|
624
|
-
for
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
for path in data.image_paths:
|
|
629
|
-
pil_images.append(load_image(path).convert("RGB"))
|
|
806
|
+
for b64_string in request.images_b64:
|
|
807
|
+
b64_data = b64_string.split(";base64,")[1] if ";base64," in b64_string else b64_string
|
|
808
|
+
image_bytes = base64.b64decode(b64_data)
|
|
809
|
+
pil_images.append(Image.open(BytesIO(image_bytes)).convert("RGB"))
|
|
630
810
|
|
|
631
|
-
if not pil_images:
|
|
632
|
-
|
|
811
|
+
if not pil_images: raise HTTPException(status_code=400, detail="No valid images provided.")
|
|
812
|
+
|
|
813
|
+
pipeline_args = {"prompt": request.prompt}
|
|
814
|
+
seed = int(params.get("seed", -1))
|
|
815
|
+
if seed != -1: pipeline_args["generator"] = torch.Generator(device=manager.config["device"]).manual_seed(seed)
|
|
816
|
+
|
|
817
|
+
if "mask_image" in params and params["mask_image"]:
|
|
818
|
+
b64_mask = params["mask_image"]
|
|
819
|
+
b64_data = b64_mask.split(";base64,")[1] if ";base64," in b64_mask else b64_mask
|
|
820
|
+
mask_bytes = base64.b64decode(b64_data)
|
|
821
|
+
pipeline_args["mask_image"] = Image.open(BytesIO(mask_bytes)).convert("L")
|
|
633
822
|
|
|
634
|
-
task = "inpainting" if
|
|
823
|
+
task = "inpainting" if "mask_image" in pipeline_args else "image2image"
|
|
824
|
+
|
|
825
|
+
if "Qwen-Image-Edit-2509" in model_name:
|
|
826
|
+
task = "image2image"
|
|
827
|
+
pipeline_args.update({"true_cfg_scale": 4.0, "guidance_scale": 1.0, "num_inference_steps": 40, "negative_prompt": " "})
|
|
828
|
+
edit_mode = params.get("edit_mode", "fusion")
|
|
829
|
+
if edit_mode == "fusion": pipeline_args["image"] = pil_images
|
|
830
|
+
else:
|
|
831
|
+
pipeline_args.update({"image": pil_images[0], "strength": 0.8, "guidance_scale": 7.5, "num_inference_steps": 25})
|
|
635
832
|
|
|
636
|
-
pipeline_args
|
|
637
|
-
"prompt": data.prompt,
|
|
638
|
-
"image": pil_images[0], # Simple i2i for now
|
|
639
|
-
"strength": float(data.params.get("strength", 0.8)),
|
|
640
|
-
# Add other params like mask etc.
|
|
641
|
-
}
|
|
833
|
+
pipeline_args.update(params)
|
|
642
834
|
|
|
643
|
-
future = Future()
|
|
644
|
-
|
|
645
|
-
result_bytes = future.result()
|
|
646
|
-
return Response(content=result_bytes, media_type="image/png")
|
|
835
|
+
future = Future(); manager.queue.put((future, task, pipeline_args))
|
|
836
|
+
return Response(content=future.result(), media_type="image/png")
|
|
647
837
|
except Exception as e:
|
|
838
|
+
sanitized_payload = get_sanitized_request_for_logging(request)
|
|
839
|
+
ASCIIColors.error(f"Exception in /edit_image. Sanitized Payload: {json.dumps(sanitized_payload, indent=2)}")
|
|
648
840
|
trace_exception(e)
|
|
649
841
|
raise HTTPException(status_code=500, detail=str(e))
|
|
842
|
+
finally:
|
|
843
|
+
if temp_config and manager:
|
|
844
|
+
state.registry.release_manager(temp_config)
|
|
845
|
+
ASCIIColors.info(f"Released per-request model: {temp_config['model_name']}")
|
|
846
|
+
|
|
650
847
|
|
|
651
848
|
@router.get("/list_models")
|
|
652
849
|
def list_models_endpoint():
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
850
|
+
huggingface_models = []
|
|
851
|
+
# Add public models, organized by category
|
|
852
|
+
for category, models in HF_PUBLIC_MODELS.items():
|
|
853
|
+
for model_info in models:
|
|
854
|
+
huggingface_models.append({
|
|
855
|
+
'model_name': model_info['model_name'],
|
|
856
|
+
'display_name': model_info['display_name'],
|
|
857
|
+
'description': f"({category}) {model_info['desc']}",
|
|
858
|
+
'owned_by': 'huggingface'
|
|
859
|
+
})
|
|
860
|
+
|
|
861
|
+
# Conditionally add gated models if an HF token is provided in the server config
|
|
862
|
+
if state.config.get("hf_token"):
|
|
863
|
+
ASCIIColors.info("HF token detected, including gated models in the list.")
|
|
864
|
+
for category, models in HF_GATED_MODELS.items():
|
|
865
|
+
for model_info in models:
|
|
866
|
+
huggingface_models.append({
|
|
867
|
+
'model_name': model_info['model_name'],
|
|
868
|
+
'display_name': model_info['display_name'],
|
|
869
|
+
'description': f"({category}) {model_info['desc']}",
|
|
870
|
+
'owned_by': 'huggingface'
|
|
871
|
+
})
|
|
872
|
+
else:
|
|
873
|
+
ASCIIColors.info("No HF token found, showing public models only.")
|
|
874
|
+
|
|
875
|
+
civitai_models = [{'model_name': key, 'display_name': info['display_name'], 'description': f"(Civitai) {info['description']}", 'owned_by': info['owned_by']} for key, info in CIVITAI_MODELS.items()]
|
|
876
|
+
|
|
877
|
+
local_files = list_local_models_endpoint()
|
|
878
|
+
local_models = [{'model_name': filename, 'display_name': Path(filename).stem, 'description': '(Local) Local safetensors file.', 'owned_by': 'local_user'} for filename in local_files]
|
|
879
|
+
|
|
880
|
+
return huggingface_models + civitai_models + local_models
|
|
656
881
|
|
|
657
882
|
@router.get("/list_local_models")
|
|
658
883
|
def list_local_models_endpoint():
|
|
659
|
-
|
|
884
|
+
local_models = set()
|
|
885
|
+
# Main models path
|
|
886
|
+
for f in state.models_path.glob("**/*.safetensors"):
|
|
887
|
+
local_models.add(f.name)
|
|
888
|
+
# Extra models path
|
|
889
|
+
if state.extra_models_path and state.extra_models_path.exists():
|
|
890
|
+
for f in state.extra_models_path.glob("**/*.safetensors"):
|
|
891
|
+
local_models.add(f.name)
|
|
892
|
+
return sorted(list(local_models))
|
|
660
893
|
|
|
661
894
|
@router.get("/list_available_models")
|
|
662
895
|
def list_available_models_endpoint():
|
|
@@ -666,7 +899,6 @@ def list_available_models_endpoint():
|
|
|
666
899
|
@router.get("/get_settings")
|
|
667
900
|
def get_settings_endpoint():
|
|
668
901
|
settings_list = []
|
|
669
|
-
# Add options for dropdowns
|
|
670
902
|
available_models = list_available_models_endpoint()
|
|
671
903
|
schedulers = list(SCHEDULER_MAPPING.keys())
|
|
672
904
|
config_to_display = state.config or state.get_default_config()
|
|
@@ -714,14 +946,18 @@ if __name__ == "__main__":
|
|
|
714
946
|
parser.add_argument("--host", type=str, default="localhost", help="Host to bind to.")
|
|
715
947
|
parser.add_argument("--port", type=int, default=9630, help="Port to bind to.")
|
|
716
948
|
parser.add_argument("--models-path", type=str, required=True, help="Path to the models directory.")
|
|
949
|
+
parser.add_argument("--extra-models-path", type=str, default=None, help="Path to an extra models directory.")
|
|
717
950
|
args = parser.parse_args()
|
|
718
951
|
|
|
719
952
|
MODELS_PATH = Path(args.models_path)
|
|
720
|
-
|
|
953
|
+
EXTRA_MODELS_PATH = Path(args.extra_models_path) if args.extra_models_path else None
|
|
954
|
+
state = ServerState(MODELS_PATH, EXTRA_MODELS_PATH)
|
|
721
955
|
|
|
722
956
|
ASCIIColors.cyan(f"--- Diffusers TTI Server ---")
|
|
723
957
|
ASCIIColors.green(f"Starting server on http://{args.host}:{args.port}")
|
|
724
958
|
ASCIIColors.green(f"Serving models from: {MODELS_PATH.resolve()}")
|
|
959
|
+
if EXTRA_MODELS_PATH:
|
|
960
|
+
ASCIIColors.green(f"Serving extra models from: {EXTRA_MODELS_PATH.resolve()}")
|
|
725
961
|
if not DIFFUSERS_AVAILABLE:
|
|
726
962
|
ASCIIColors.error("Diffusers or its dependencies are not installed correctly in the server's environment!")
|
|
727
963
|
else:
|