lollms-client 1.6.4__py3-none-any.whl → 1.6.5__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 +104 -57
- lollms_client/tti_bindings/diffusers/server/main.py +264 -112
- lollms_client/tti_bindings/gemini/__init__.py +179 -239
- {lollms_client-1.6.4.dist-info → lollms_client-1.6.5.dist-info}/METADATA +1 -1
- {lollms_client-1.6.4.dist-info → lollms_client-1.6.5.dist-info}/RECORD +10 -10
- {lollms_client-1.6.4.dist-info → lollms_client-1.6.5.dist-info}/WHEEL +0 -0
- {lollms_client-1.6.4.dist-info → lollms_client-1.6.5.dist-info}/licenses/LICENSE +0 -0
- {lollms_client-1.6.4.dist-info → lollms_client-1.6.5.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,7 +56,7 @@ 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 = {
|
|
@@ -119,6 +122,21 @@ CIVITAI_MODELS = {
|
|
|
119
122
|
},
|
|
120
123
|
}
|
|
121
124
|
|
|
125
|
+
HF_DEFAULT_MODELS = [
|
|
126
|
+
{"family": "FLUX", "model_name": "black-forest-labs/FLUX.1-schnell", "display_name": "FLUX.1 Schnell", "desc": "A fast and powerful next-gen T2I model."},
|
|
127
|
+
{"family": "FLUX", "model_name": "black-forest-labs/FLUX.1-dev", "display_name": "FLUX.1 Dev", "desc": "The larger, developer version of the FLUX.1 model."},
|
|
128
|
+
{"family": "SDXL", "model_name": "stabilityai/stable-diffusion-xl-base-1.0", "display_name": "SDXL Base 1.0", "desc": "Text2Image 1024 native."},
|
|
129
|
+
{"family": "SDXL", "model_name": "stabilityai/stable-diffusion-xl-refiner-1.0", "display_name": "SDXL Refiner 1.0", "desc": "Refiner for SDXL."},
|
|
130
|
+
{"family": "SD 1.x", "model_name": "runwayml/stable-diffusion-v1-5", "display_name": "Stable Diffusion 1.5", "desc": "Classic SD1.5."},
|
|
131
|
+
{"family": "SD 2.x", "model_name": "stabilityai/stable-diffusion-2-1", "display_name": "Stable Diffusion 2.1", "desc": "SD2.1 base."},
|
|
132
|
+
{"family": "SD3", "model_name": "stabilityai/stable-diffusion-3-medium-diffusers", "display_name": "Stable Diffusion 3 Medium", "desc": "SD3 medium."},
|
|
133
|
+
{"family": "Qwen", "model_name": "Qwen/Qwen-Image", "display_name": "Qwen Image", "desc": "Dedicated image generation."},
|
|
134
|
+
{"family": "Specialized", "model_name": "playgroundai/playground-v2.5-1024px-aesthetic", "display_name": "Playground v2.5", "desc": "High aesthetic 1024."},
|
|
135
|
+
{"family": "Editors", "model_name": "Qwen/Qwen-Image-Edit", "display_name": "Qwen Image Edit", "desc": "Dedicated image editing."},
|
|
136
|
+
{"family": "Editors", "model_name": "Qwen/Qwen-Image-Edit-2509", "display_name": "Qwen Image Edit Plus (Multi-Image)", "desc": "Advanced multi-image editing, fusion, and pose transfer."}
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
|
|
122
140
|
TORCH_DTYPE_MAP_STR_TO_OBJ = {
|
|
123
141
|
"float16": getattr(torch, 'float16', 'float16'), "bfloat16": getattr(torch, 'bfloat16', 'bfloat16'),
|
|
124
142
|
"float32": getattr(torch, 'float32', 'float32'), "auto": "auto"
|
|
@@ -141,6 +159,7 @@ SCHEDULER_USES_KARRAS_SIGMAS = [
|
|
|
141
159
|
"dpm++_2m_sde_karras","dpm2_karras","dpm2_a_karras"
|
|
142
160
|
]
|
|
143
161
|
|
|
162
|
+
|
|
144
163
|
class ModelManager:
|
|
145
164
|
def __init__(self, config: Dict[str, Any], models_path: Path, registry: 'PipelineRegistry'):
|
|
146
165
|
self.config = config
|
|
@@ -159,6 +178,7 @@ class ModelManager:
|
|
|
159
178
|
self._stop_monitor_event = threading.Event()
|
|
160
179
|
self._unload_monitor_thread = None
|
|
161
180
|
self._start_unload_monitor()
|
|
181
|
+
self.supported_args: Optional[set] = None
|
|
162
182
|
|
|
163
183
|
def acquire(self):
|
|
164
184
|
with self.lock:
|
|
@@ -219,7 +239,7 @@ class ModelManager:
|
|
|
219
239
|
filename = model_info["filename"]
|
|
220
240
|
dest_path = self.models_path / filename
|
|
221
241
|
temp_path = dest_path.with_suffix(".temp")
|
|
222
|
-
ASCIIColors.cyan(f"Downloading '{filename}' from Civitai...")
|
|
242
|
+
ASCIIColors.cyan(f"Downloading '{filename}' from Civitai... to {dest_path}")
|
|
223
243
|
try:
|
|
224
244
|
with requests.get(url, stream=True) as r:
|
|
225
245
|
r.raise_for_status()
|
|
@@ -233,13 +253,13 @@ class ModelManager:
|
|
|
233
253
|
except Exception as e:
|
|
234
254
|
if temp_path.exists():
|
|
235
255
|
temp_path.unlink()
|
|
236
|
-
raise Exception(f"Failed to download model {filename}: {e}")
|
|
256
|
+
raise Exception(f"Failed to download model {filename}: {e}")
|
|
237
257
|
|
|
238
258
|
def _set_scheduler(self):
|
|
239
259
|
if not self.pipeline:
|
|
240
260
|
return
|
|
241
|
-
if "Qwen" in self.config.get("model_name", ""):
|
|
242
|
-
ASCIIColors.info("
|
|
261
|
+
if "Qwen" in self.config.get("model_name", "") or "FLUX" in self.config.get("model_name", ""):
|
|
262
|
+
ASCIIColors.info("Special model detected, skipping custom scheduler setup.")
|
|
243
263
|
return
|
|
244
264
|
scheduler_name_key = self.config["scheduler_name"].lower()
|
|
245
265
|
if scheduler_name_key == "default":
|
|
@@ -256,71 +276,116 @@ class ModelManager:
|
|
|
256
276
|
ASCIIColors.warning(f"Could not switch scheduler to {scheduler_name_key}: {e}. Using current default.")
|
|
257
277
|
|
|
258
278
|
def _execute_load_pipeline(self, task: str, model_path: Union[str, Path], torch_dtype: Any):
|
|
259
|
-
|
|
279
|
+
if platform.system() == "Windows":
|
|
280
|
+
os.environ["HF_HUB_ENABLE_SYMLINKS"] = "0"
|
|
281
|
+
|
|
282
|
+
model_name_from_config = self.config.get("model_name", "")
|
|
283
|
+
use_device_map = False
|
|
284
|
+
|
|
260
285
|
try:
|
|
261
|
-
|
|
286
|
+
load_params = {}
|
|
262
287
|
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,
|
|
288
|
+
load_params["cache_dir"] = str(self.config["hf_cache_path"])
|
|
289
|
+
load_params["torch_dtype"] = torch_dtype
|
|
290
|
+
|
|
291
|
+
is_qwen_model = "Qwen" in model_name_from_config
|
|
292
|
+
is_flux_model = "FLUX" in model_name_from_config
|
|
293
|
+
|
|
294
|
+
if is_qwen_model or is_flux_model:
|
|
295
|
+
ASCIIColors.info(f"Special model '{model_name_from_config}' detected. Using dedicated pipeline loader.")
|
|
296
|
+
load_params.update({
|
|
277
297
|
"use_safetensors": self.config["use_safetensors"],
|
|
278
298
|
"token": self.config["hf_token"],
|
|
279
299
|
"local_files_only": self.config["local_files_only"]
|
|
280
|
-
}
|
|
300
|
+
})
|
|
281
301
|
if self.config["hf_variant"]:
|
|
282
|
-
|
|
302
|
+
load_params["variant"] = self.config["hf_variant"]
|
|
283
303
|
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 =
|
|
304
|
+
load_params["safety_checker"] = None
|
|
305
|
+
|
|
306
|
+
should_offload = self.config["enable_cpu_offload"] or self.config["enable_sequential_cpu_offload"]
|
|
307
|
+
if should_offload:
|
|
308
|
+
ASCIIColors.info(f"Offload enabled. Forcing device_map='auto' for {model_name_from_config}.")
|
|
309
|
+
use_device_map = True
|
|
310
|
+
load_params["device_map"] = "auto"
|
|
311
|
+
|
|
312
|
+
if is_flux_model:
|
|
313
|
+
self.pipeline = AutoPipelineForText2Image.from_pretrained(model_name_from_config, **load_params)
|
|
314
|
+
elif "Qwen-Image-Edit-2509" in model_name_from_config:
|
|
315
|
+
self.pipeline = QwenImageEditPlusPipeline.from_pretrained(model_name_from_config, **load_params)
|
|
316
|
+
elif "Qwen-Image-Edit" in model_name_from_config:
|
|
317
|
+
self.pipeline = QwenImageEditPipeline.from_pretrained(model_name_from_config, **load_params)
|
|
318
|
+
elif "Qwen/Qwen-Image" in model_name_from_config:
|
|
319
|
+
self.pipeline = DiffusionPipeline.from_pretrained(model_name_from_config, **load_params)
|
|
320
|
+
|
|
321
|
+
else:
|
|
322
|
+
is_safetensors_file = str(model_path).endswith(".safetensors")
|
|
323
|
+
if is_safetensors_file:
|
|
324
|
+
ASCIIColors.info(f"Loading standard model from local .safetensors file: {model_path}")
|
|
325
|
+
try:
|
|
326
|
+
self.pipeline = AutoPipelineForText2Image.from_single_file(model_path, **load_params)
|
|
327
|
+
except Exception as e:
|
|
328
|
+
ASCIIColors.warning(f"Failed to load with AutoPipeline, falling back to StableDiffusionPipeline: {e}")
|
|
329
|
+
self.pipeline = StableDiffusionPipeline.from_single_file(model_path, **load_params)
|
|
330
|
+
else:
|
|
331
|
+
ASCIIColors.info(f"Loading standard model from Hub: {model_path}")
|
|
332
|
+
load_params.update({
|
|
333
|
+
"use_safetensors": self.config["use_safetensors"],
|
|
334
|
+
"token": self.config["hf_token"],
|
|
335
|
+
"local_files_only": self.config["local_files_only"]
|
|
336
|
+
})
|
|
337
|
+
if self.config["hf_variant"]:
|
|
338
|
+
load_params["variant"] = self.config["hf_variant"]
|
|
339
|
+
if not self.config["safety_checker_on"]:
|
|
340
|
+
load_params["safety_checker"] = None
|
|
341
|
+
|
|
342
|
+
is_large_model = "stable-diffusion-3" in str(model_path)
|
|
343
|
+
should_offload = self.config["enable_cpu_offload"] or self.config["enable_sequential_cpu_offload"]
|
|
344
|
+
if is_large_model and should_offload:
|
|
345
|
+
ASCIIColors.info(f"Large model '{model_path}' detected with offload enabled. Using device_map='auto'.")
|
|
346
|
+
use_device_map = True
|
|
347
|
+
load_params["device_map"] = "auto"
|
|
348
|
+
|
|
349
|
+
if task == "text2image":
|
|
350
|
+
self.pipeline = AutoPipelineForText2Image.from_pretrained(model_path, **load_params)
|
|
351
|
+
elif task == "image2image":
|
|
352
|
+
self.pipeline = AutoPipelineForImage2Image.from_pretrained(model_path, **load_params)
|
|
353
|
+
elif task == "inpainting":
|
|
354
|
+
self.pipeline = AutoPipelineForInpainting.from_pretrained(model_path, **load_params)
|
|
355
|
+
|
|
300
356
|
except Exception as e:
|
|
301
357
|
error_str = str(e).lower()
|
|
302
358
|
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
|
-
)
|
|
359
|
+
msg = (f"AUTHENTICATION FAILED for model '{model_name_from_config}'. Please ensure you accepted the model license and provided a valid HF token.")
|
|
307
360
|
raise RuntimeError(msg)
|
|
308
361
|
raise e
|
|
362
|
+
|
|
309
363
|
self._set_scheduler()
|
|
310
|
-
|
|
311
|
-
if
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
self.
|
|
364
|
+
|
|
365
|
+
if not use_device_map:
|
|
366
|
+
self.pipeline.to(self.config["device"])
|
|
367
|
+
if self.config["enable_xformers"]:
|
|
368
|
+
try:
|
|
369
|
+
self.pipeline.enable_xformers_memory_efficient_attention()
|
|
370
|
+
except Exception as e:
|
|
371
|
+
ASCIIColors.warning(f"Could not enable xFormers: {e}.")
|
|
372
|
+
|
|
373
|
+
if self.config["enable_cpu_offload"] and self.config["device"] != "cpu":
|
|
374
|
+
self.pipeline.enable_model_cpu_offload()
|
|
375
|
+
elif self.config["enable_sequential_cpu_offload"] and self.config["device"] != "cpu":
|
|
376
|
+
self.pipeline.enable_sequential_cpu_offload()
|
|
377
|
+
else:
|
|
378
|
+
ASCIIColors.info("Device map handled device placement. Skipping manual pipeline.to() and offload calls.")
|
|
379
|
+
|
|
380
|
+
if self.pipeline:
|
|
381
|
+
sig = inspect.signature(self.pipeline.__call__)
|
|
382
|
+
self.supported_args = {p.name for p in sig.parameters.values()}
|
|
383
|
+
ASCIIColors.info(f"Pipeline supported arguments detected: {self.supported_args}")
|
|
384
|
+
|
|
320
385
|
self.is_loaded = True
|
|
321
386
|
self.current_task = task
|
|
322
387
|
self.last_used_time = time.time()
|
|
323
|
-
ASCIIColors.green(f"Model '{
|
|
388
|
+
ASCIIColors.green(f"Model '{model_name_from_config}' loaded successfully using '{'device_map' if use_device_map else 'standard'}' mode for task '{task}'.")
|
|
324
389
|
|
|
325
390
|
def _load_pipeline_for_task(self, task: str):
|
|
326
391
|
if self.pipeline and self.current_task == task:
|
|
@@ -346,10 +411,7 @@ class ModelManager:
|
|
|
346
411
|
|
|
347
412
|
ASCIIColors.warning(f"Failed to load '{model_name}' due to OOM. Attempting to unload other models to free VRAM.")
|
|
348
413
|
|
|
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
|
-
]
|
|
414
|
+
candidates_to_unload = [m for m in self.registry.get_all_managers() if m is not self and m.is_loaded]
|
|
353
415
|
candidates_to_unload.sort(key=lambda m: m.last_used_time)
|
|
354
416
|
|
|
355
417
|
if not candidates_to_unload:
|
|
@@ -378,6 +440,7 @@ class ModelManager:
|
|
|
378
440
|
model_name = self.config.get('model_name', 'Unknown')
|
|
379
441
|
del self.pipeline
|
|
380
442
|
self.pipeline = None
|
|
443
|
+
self.supported_args = None
|
|
381
444
|
gc.collect()
|
|
382
445
|
if torch and torch.cuda.is_available():
|
|
383
446
|
torch.cuda.empty_cache()
|
|
@@ -398,8 +461,16 @@ class ModelManager:
|
|
|
398
461
|
self.last_used_time = time.time()
|
|
399
462
|
if not self.is_loaded or self.current_task != task:
|
|
400
463
|
self._load_pipeline_for_task(task)
|
|
464
|
+
|
|
465
|
+
if self.supported_args:
|
|
466
|
+
filtered_args = {k: v for k, v in pipeline_args.items() if k in self.supported_args}
|
|
467
|
+
else:
|
|
468
|
+
ASCIIColors.warning("Supported argument set not found. Using unfiltered arguments.")
|
|
469
|
+
filtered_args = pipeline_args
|
|
470
|
+
|
|
401
471
|
with torch.no_grad():
|
|
402
|
-
output = self.pipeline(**
|
|
472
|
+
output = self.pipeline(**filtered_args)
|
|
473
|
+
|
|
403
474
|
pil = output.images[0]
|
|
404
475
|
buf = BytesIO()
|
|
405
476
|
pil.save(buf, format="PNG")
|
|
@@ -409,7 +480,6 @@ class ModelManager:
|
|
|
409
480
|
future.set_exception(e)
|
|
410
481
|
finally:
|
|
411
482
|
self.queue.task_done()
|
|
412
|
-
# Aggressive cleanup
|
|
413
483
|
if output is not None:
|
|
414
484
|
del output
|
|
415
485
|
gc.collect()
|
|
@@ -472,17 +542,15 @@ class ServerState:
|
|
|
472
542
|
self.registry = PipelineRegistry()
|
|
473
543
|
self.manager: Optional[ModelManager] = None
|
|
474
544
|
self.config = {}
|
|
475
|
-
self.load_config()
|
|
545
|
+
self.load_config()
|
|
476
546
|
self._resolve_device_and_dtype()
|
|
477
|
-
|
|
478
|
-
# Eagerly acquire manager at startup if a model is configured
|
|
479
547
|
if self.config.get("model_name"):
|
|
480
548
|
try:
|
|
481
549
|
ASCIIColors.info(f"Acquiring initial model manager for '{self.config['model_name']}' on startup.")
|
|
482
550
|
self.manager = self.registry.get_manager(self.config, self.models_path)
|
|
483
551
|
except Exception as e:
|
|
484
552
|
ASCIIColors.error(f"Failed to acquire model manager on startup: {e}")
|
|
485
|
-
self.manager = None
|
|
553
|
+
self.manager = None
|
|
486
554
|
|
|
487
555
|
def get_default_config(self) -> Dict[str, Any]:
|
|
488
556
|
return {
|
|
@@ -495,7 +563,6 @@ class ServerState:
|
|
|
495
563
|
}
|
|
496
564
|
|
|
497
565
|
def save_config(self):
|
|
498
|
-
"""Saves the current configuration to a JSON file."""
|
|
499
566
|
try:
|
|
500
567
|
with open(self.config_path, 'w') as f:
|
|
501
568
|
json.dump(self.config, f, indent=4)
|
|
@@ -504,13 +571,11 @@ class ServerState:
|
|
|
504
571
|
ASCIIColors.error(f"Failed to save server config: {e}")
|
|
505
572
|
|
|
506
573
|
def load_config(self):
|
|
507
|
-
"""Loads configuration from JSON file, falling back to defaults."""
|
|
508
574
|
default_config = self.get_default_config()
|
|
509
575
|
if self.config_path.exists():
|
|
510
576
|
try:
|
|
511
577
|
with open(self.config_path, 'r') as f:
|
|
512
578
|
loaded_config = json.load(f)
|
|
513
|
-
# Merge loaded config into defaults to ensure all keys are present
|
|
514
579
|
default_config.update(loaded_config)
|
|
515
580
|
self.config = default_config
|
|
516
581
|
ASCIIColors.info(f"Loaded server configuration from {self.config_path}")
|
|
@@ -519,53 +584,45 @@ class ServerState:
|
|
|
519
584
|
self.config = default_config
|
|
520
585
|
else:
|
|
521
586
|
self.config = default_config
|
|
522
|
-
# Save back to ensure file exists and is up-to-date with all keys
|
|
523
587
|
self.save_config()
|
|
524
588
|
|
|
525
589
|
def _resolve_device_and_dtype(self):
|
|
526
590
|
if self.config.get("device", "auto").lower() == "auto":
|
|
527
591
|
self.config["device"] = "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu"
|
|
528
592
|
|
|
529
|
-
|
|
530
|
-
if "Qwen" in self.config.get("model_name", "") and self.config["device"] == "cuda":
|
|
593
|
+
if ("Qwen" in self.config.get("model_name", "") or "FLUX" in self.config.get("model_name", "")) and self.config["device"] == "cuda":
|
|
531
594
|
if hasattr(torch.cuda, 'is_bf16_supported') and torch.cuda.is_bf16_supported():
|
|
532
595
|
self.config["torch_dtype_str"] = "bfloat16"
|
|
533
|
-
ASCIIColors.info("
|
|
596
|
+
ASCIIColors.info("Special model detected on compatible hardware. Forcing dtype to bfloat16 for stability.")
|
|
534
597
|
return
|
|
535
598
|
|
|
536
599
|
if self.config["torch_dtype_str"].lower() == "auto":
|
|
537
600
|
self.config["torch_dtype_str"] = "float16" if self.config["device"] != "cpu" else "float32"
|
|
538
601
|
|
|
539
602
|
def update_settings(self, new_settings: Dict[str, Any]):
|
|
540
|
-
"""Updates settings, swaps the manager if critical settings change, and saves the config."""
|
|
541
603
|
if 'model' in new_settings and 'model_name' not in new_settings:
|
|
542
604
|
new_settings['model_name'] = new_settings.pop('model')
|
|
543
605
|
|
|
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
606
|
if self.config.get("model_name") and not new_settings.get("model_name"):
|
|
547
607
|
ASCIIColors.info("Incoming settings have no model_name. Preserving existing model.")
|
|
548
608
|
new_settings["model_name"] = self.config["model_name"]
|
|
549
609
|
|
|
550
|
-
# Release old manager if it exists
|
|
551
610
|
if self.manager:
|
|
552
611
|
self.registry.release_manager(self.manager.config)
|
|
553
612
|
self.manager = None
|
|
554
613
|
|
|
555
|
-
# Update the config in memory
|
|
556
614
|
self.config.update(new_settings)
|
|
557
615
|
ASCIIColors.info(f"Server config updated. Current model_name: {self.config.get('model_name')}")
|
|
558
616
|
|
|
559
617
|
self._resolve_device_and_dtype()
|
|
560
618
|
|
|
561
|
-
# Acquire new manager with the updated config
|
|
562
619
|
if self.config.get("model_name"):
|
|
563
620
|
ASCIIColors.info("Acquiring model manager with updated configuration...")
|
|
564
621
|
self.manager = self.registry.get_manager(self.config, self.models_path)
|
|
565
622
|
else:
|
|
566
623
|
ASCIIColors.warning("No model_name in config after update, manager not acquired.")
|
|
567
624
|
|
|
568
|
-
self.save_config()
|
|
625
|
+
self.save_config()
|
|
569
626
|
return True
|
|
570
627
|
|
|
571
628
|
def get_active_manager(self) -> ModelManager:
|
|
@@ -586,73 +643,169 @@ class EditRequestPayload(BaseModel):
|
|
|
586
643
|
image_paths: List[str] = Field(default_factory=list)
|
|
587
644
|
params: Dict[str, Any] = Field(default_factory=dict)
|
|
588
645
|
|
|
646
|
+
class EditRequestJSON(BaseModel):
|
|
647
|
+
prompt: str
|
|
648
|
+
images_b64: List[str] = Field(description="A list of Base64 encoded image strings.")
|
|
649
|
+
params: Dict[str, Any] = Field(default_factory=dict)
|
|
650
|
+
def get_sanitized_request_for_logging(request_data: Any) -> Dict[str, Any]:
|
|
651
|
+
"""
|
|
652
|
+
Takes a request object (Pydantic model or dict) and returns a 'safe' dictionary
|
|
653
|
+
for logging, with long base64 strings replaced by placeholders.
|
|
654
|
+
"""
|
|
655
|
+
import copy
|
|
656
|
+
|
|
657
|
+
try:
|
|
658
|
+
if hasattr(request_data, 'model_dump'):
|
|
659
|
+
data = request_data.model_dump()
|
|
660
|
+
elif isinstance(request_data, dict):
|
|
661
|
+
data = copy.deepcopy(request_data)
|
|
662
|
+
else:
|
|
663
|
+
return {"error": "Unsupported data type for sanitization"}
|
|
664
|
+
|
|
665
|
+
# Sanitize the main list of images
|
|
666
|
+
if 'images_b64' in data and isinstance(data['images_b64'], list):
|
|
667
|
+
count = len(data['images_b64'])
|
|
668
|
+
data['images_b64'] = f"[<{count} base64 image(s) truncated>]"
|
|
669
|
+
|
|
670
|
+
# Sanitize a potential mask in the 'params' dictionary
|
|
671
|
+
if 'params' in data and isinstance(data.get('params'), dict):
|
|
672
|
+
if 'mask_image' in data['params'] and isinstance(data['params']['mask_image'], str):
|
|
673
|
+
original_len = len(data['params']['mask_image'])
|
|
674
|
+
data['params']['mask_image'] = f"[<base64 mask truncated, len={original_len}>]"
|
|
675
|
+
|
|
676
|
+
return data
|
|
677
|
+
except Exception:
|
|
678
|
+
return {"error": "Failed to sanitize request data."}
|
|
679
|
+
|
|
589
680
|
# --- API Endpoints ---
|
|
590
681
|
@router.post("/generate_image")
|
|
591
682
|
async def generate_image(request: T2IRequest):
|
|
683
|
+
manager = None
|
|
684
|
+
temp_config = None
|
|
592
685
|
try:
|
|
593
|
-
manager = state.get_active_manager()
|
|
594
686
|
params = request.params
|
|
595
|
-
|
|
687
|
+
|
|
688
|
+
# Determine which model manager to use for this specific request
|
|
689
|
+
if "model_name" in params and params["model_name"]:
|
|
690
|
+
temp_config = state.config.copy()
|
|
691
|
+
temp_config["model_name"] = params.pop("model_name") # Remove from params to avoid being passed to pipeline
|
|
692
|
+
manager = state.registry.get_manager(temp_config, state.models_path)
|
|
693
|
+
ASCIIColors.info(f"Using per-request model: {temp_config['model_name']}")
|
|
694
|
+
else:
|
|
695
|
+
manager = state.get_active_manager()
|
|
696
|
+
ASCIIColors.info(f"Using session-configured model: {manager.config.get('model_name')}")
|
|
697
|
+
|
|
698
|
+
seed = int(params.get("seed", manager.config.get("seed", -1)))
|
|
596
699
|
generator = None
|
|
597
700
|
if seed != -1:
|
|
598
|
-
generator = torch.Generator(device=
|
|
599
|
-
|
|
701
|
+
generator = torch.Generator(device=manager.config["device"]).manual_seed(seed)
|
|
702
|
+
|
|
703
|
+
width = int(params.get("width", manager.config.get("width", 512)))
|
|
704
|
+
height = int(params.get("height", manager.config.get("height", 512)))
|
|
705
|
+
|
|
600
706
|
pipeline_args = {
|
|
601
|
-
"prompt": request.prompt,
|
|
602
|
-
"
|
|
603
|
-
"
|
|
604
|
-
"
|
|
605
|
-
"
|
|
707
|
+
"prompt": request.prompt,
|
|
708
|
+
"negative_prompt": request.negative_prompt,
|
|
709
|
+
"width": width,
|
|
710
|
+
"height": height,
|
|
711
|
+
"num_inference_steps": int(params.get("num_inference_steps", manager.config.get("num_inference_steps", 25))),
|
|
712
|
+
"guidance_scale": float(params.get("guidance_scale", manager.config.get("guidance_scale", 7.0))),
|
|
606
713
|
"generator": generator
|
|
607
714
|
}
|
|
715
|
+
pipeline_args.update(params)
|
|
716
|
+
|
|
717
|
+
model_name = manager.config.get("model_name", "")
|
|
718
|
+
task = "text2image"
|
|
719
|
+
|
|
720
|
+
if "Qwen-Image-Edit" in model_name:
|
|
721
|
+
rng_seed = seed if seed != -1 else None
|
|
722
|
+
rng = np.random.default_rng(seed=rng_seed)
|
|
723
|
+
random_pixels = rng.integers(0, 256, size=(height, width, 3), dtype=np.uint8)
|
|
724
|
+
placeholder_image = Image.fromarray(random_pixels, 'RGB')
|
|
725
|
+
pipeline_args["image"] = placeholder_image
|
|
726
|
+
pipeline_args["strength"] = float(params.get("strength", 1.0))
|
|
727
|
+
task = "image2image"
|
|
608
728
|
|
|
609
729
|
future = Future()
|
|
610
|
-
manager.queue.put((future,
|
|
730
|
+
manager.queue.put((future, task, pipeline_args))
|
|
611
731
|
result_bytes = future.result()
|
|
612
732
|
return Response(content=result_bytes, media_type="image/png")
|
|
613
733
|
except Exception as e:
|
|
614
734
|
trace_exception(e)
|
|
615
735
|
raise HTTPException(status_code=500, detail=str(e))
|
|
736
|
+
finally:
|
|
737
|
+
if temp_config and manager:
|
|
738
|
+
state.registry.release_manager(temp_config)
|
|
739
|
+
ASCIIColors.info(f"Released per-request model: {temp_config['model_name']}")
|
|
740
|
+
|
|
616
741
|
|
|
617
742
|
@router.post("/edit_image")
|
|
618
|
-
async def edit_image(
|
|
743
|
+
async def edit_image(request: EditRequestJSON):
|
|
744
|
+
manager = None
|
|
745
|
+
temp_config = None
|
|
619
746
|
try:
|
|
620
|
-
|
|
621
|
-
|
|
747
|
+
params = request.params
|
|
748
|
+
|
|
749
|
+
if "model_name" in params and params["model_name"]:
|
|
750
|
+
temp_config = state.config.copy()
|
|
751
|
+
temp_config["model_name"] = params.pop("model_name")
|
|
752
|
+
manager = state.registry.get_manager(temp_config, state.models_path)
|
|
753
|
+
ASCIIColors.info(f"Using per-request model: {temp_config['model_name']}")
|
|
754
|
+
else:
|
|
755
|
+
manager = state.get_active_manager()
|
|
756
|
+
ASCIIColors.info(f"Using session-configured model: {manager.config.get('model_name')}")
|
|
757
|
+
|
|
758
|
+
model_name = manager.config.get("model_name", "")
|
|
622
759
|
|
|
623
760
|
pil_images = []
|
|
624
|
-
for
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
for path in data.image_paths:
|
|
629
|
-
pil_images.append(load_image(path).convert("RGB"))
|
|
761
|
+
for b64_string in request.images_b64:
|
|
762
|
+
b64_data = b64_string.split(";base64,")[1] if ";base64," in b64_string else b64_string
|
|
763
|
+
image_bytes = base64.b64decode(b64_data)
|
|
764
|
+
pil_images.append(Image.open(BytesIO(image_bytes)).convert("RGB"))
|
|
630
765
|
|
|
631
|
-
if not pil_images:
|
|
632
|
-
|
|
766
|
+
if not pil_images: raise HTTPException(status_code=400, detail="No valid images provided.")
|
|
767
|
+
|
|
768
|
+
pipeline_args = {"prompt": request.prompt}
|
|
769
|
+
seed = int(params.get("seed", -1))
|
|
770
|
+
if seed != -1: pipeline_args["generator"] = torch.Generator(device=manager.config["device"]).manual_seed(seed)
|
|
771
|
+
|
|
772
|
+
if "mask_image" in params and params["mask_image"]:
|
|
773
|
+
b64_mask = params["mask_image"]
|
|
774
|
+
b64_data = b64_mask.split(";base64,")[1] if ";base64," in b64_mask else b64_mask
|
|
775
|
+
mask_bytes = base64.b64decode(b64_data)
|
|
776
|
+
pipeline_args["mask_image"] = Image.open(BytesIO(mask_bytes)).convert("L")
|
|
633
777
|
|
|
634
|
-
task = "inpainting" if
|
|
778
|
+
task = "inpainting" if "mask_image" in pipeline_args else "image2image"
|
|
779
|
+
|
|
780
|
+
if "Qwen-Image-Edit-2509" in model_name:
|
|
781
|
+
task = "image2image"
|
|
782
|
+
pipeline_args.update({"true_cfg_scale": 4.0, "guidance_scale": 1.0, "num_inference_steps": 40, "negative_prompt": " "})
|
|
783
|
+
edit_mode = params.get("edit_mode", "fusion")
|
|
784
|
+
if edit_mode == "fusion": pipeline_args["image"] = pil_images
|
|
785
|
+
else:
|
|
786
|
+
pipeline_args.update({"image": pil_images[0], "strength": 0.8, "guidance_scale": 7.5, "num_inference_steps": 25})
|
|
635
787
|
|
|
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
|
-
}
|
|
788
|
+
pipeline_args.update(params)
|
|
642
789
|
|
|
643
|
-
future = Future()
|
|
644
|
-
|
|
645
|
-
result_bytes = future.result()
|
|
646
|
-
return Response(content=result_bytes, media_type="image/png")
|
|
790
|
+
future = Future(); manager.queue.put((future, task, pipeline_args))
|
|
791
|
+
return Response(content=future.result(), media_type="image/png")
|
|
647
792
|
except Exception as e:
|
|
793
|
+
sanitized_payload = get_sanitized_request_for_logging(request)
|
|
794
|
+
ASCIIColors.error(f"Exception in /edit_image. Sanitized Payload: {json.dumps(sanitized_payload, indent=2)}")
|
|
648
795
|
trace_exception(e)
|
|
649
796
|
raise HTTPException(status_code=500, detail=str(e))
|
|
797
|
+
finally:
|
|
798
|
+
if temp_config and manager:
|
|
799
|
+
state.registry.release_manager(temp_config)
|
|
800
|
+
ASCIIColors.info(f"Released per-request model: {temp_config['model_name']}")
|
|
801
|
+
|
|
650
802
|
|
|
651
803
|
@router.get("/list_models")
|
|
652
804
|
def list_models_endpoint():
|
|
653
805
|
civitai = [{'model_name': key, 'display_name': info['display_name'], 'description': info['description'], 'owned_by': info['owned_by']} for key, info in CIVITAI_MODELS.items()]
|
|
654
806
|
local = [{'model_name': f.name, 'display_name': f.stem, 'description': 'Local safetensors file.', 'owned_by': 'local_user'} for f in state.models_path.glob("*.safetensors")]
|
|
655
|
-
|
|
807
|
+
huggingface = [{'model_name': m['model_name'], 'display_name': m['display_name'], 'description': m['desc'], 'owned_by': 'huggingface'} for m in HF_DEFAULT_MODELS]
|
|
808
|
+
return huggingface + civitai + local
|
|
656
809
|
|
|
657
810
|
@router.get("/list_local_models")
|
|
658
811
|
def list_local_models_endpoint():
|
|
@@ -666,7 +819,6 @@ def list_available_models_endpoint():
|
|
|
666
819
|
@router.get("/get_settings")
|
|
667
820
|
def get_settings_endpoint():
|
|
668
821
|
settings_list = []
|
|
669
|
-
# Add options for dropdowns
|
|
670
822
|
available_models = list_available_models_endpoint()
|
|
671
823
|
schedulers = list(SCHEDULER_MAPPING.keys())
|
|
672
824
|
config_to_display = state.config or state.get_default_config()
|