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.

@@ -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") # Default, will be overridden by command-line arg
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("Qwen model detected, skipping custom scheduler setup.")
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
- model_name = self.config.get("model_name", "")
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
- load_args = {}
286
+ load_params = {}
262
287
  if self.config.get("hf_cache_path"):
263
- load_args["cache_dir"] = str(self.config["hf_cache_path"])
264
- if str(model_path).endswith(".safetensors"):
265
- if task == "text2image":
266
- try:
267
- self.pipeline = AutoPipelineForText2Image.from_single_file(model_path, torch_dtype=torch_dtype, cache_dir=load_args.get("cache_dir"))
268
- except AttributeError:
269
- self.pipeline = StableDiffusionPipeline.from_single_file(model_path, torch_dtype=torch_dtype, cache_dir=load_args.get("cache_dir"))
270
- elif task == "image2image":
271
- self.pipeline = AutoPipelineForImage2Image.from_single_file(model_path, torch_dtype=torch_dtype, cache_dir=load_args.get("cache_dir"))
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
- common_args["variant"] = self.config["hf_variant"]
302
+ load_params["variant"] = self.config["hf_variant"]
283
303
  if not self.config["safety_checker_on"]:
284
- common_args["safety_checker"] = None
285
- if self.config.get("hf_cache_path"):
286
- common_args["cache_dir"] = str(self.config["hf_cache_path"])
287
-
288
- if "Qwen-Image-Edit-2509" in str(model_path):
289
- self.pipeline = QwenImageEditPlusPipeline.from_pretrained(model_path, **common_args)
290
- elif "Qwen-Image-Edit" in str(model_path):
291
- self.pipeline = QwenImageEditPipeline.from_pretrained(model_path, **common_args)
292
- elif "Qwen/Qwen-Image" in str(model_path):
293
- self.pipeline = DiffusionPipeline.from_pretrained(model_path, **common_args)
294
- elif task == "text2image":
295
- self.pipeline = AutoPipelineForText2Image.from_pretrained(model_path, **common_args)
296
- elif task == "image2image":
297
- self.pipeline = AutoPipelineForImage2Image.from_pretrained(model_path, **common_args)
298
- elif task == "inpainting":
299
- self.pipeline = AutoPipelineForInpainting.from_pretrained(model_path, **common_args)
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
- self.pipeline.to(self.config["device"])
311
- if self.config["enable_xformers"]:
312
- try:
313
- self.pipeline.enable_xformers_memory_efficient_attention()
314
- except Exception as e:
315
- ASCIIColors.warning(f"Could not enable xFormers: {e}.")
316
- if self.config["enable_cpu_offload"] and self.config["device"] != "cpu":
317
- self.pipeline.enable_model_cpu_offload()
318
- elif self.config["enable_sequential_cpu_offload"] and self.config["device"] != "cpu":
319
- self.pipeline.enable_sequential_cpu_offload()
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 '{model_name}' loaded successfully on '{self.config['device']}' for task '{task}'.")
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(**pipeline_args)
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() # This will set self.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 # Ensure manager is None on failure
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
- # Prioritize bfloat16 for Qwen models on supported hardware, as it's more stable
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("Qwen model detected on compatible hardware. Forcing dtype to bfloat16 for stability.")
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() # Persist the new state
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
- seed = int(params.get("seed", state.config.get("seed", -1)))
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=state.config["device"]).manual_seed(seed)
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, "negative_prompt": request.negative_prompt,
602
- "width": int(params.get("width", state.config.get("width", 512))),
603
- "height": int(params.get("height", state.config.get("height", 512))),
604
- "num_inference_steps": int(params.get("num_inference_steps", state.config.get("num_inference_steps", 25))),
605
- "guidance_scale": float(params.get("guidance_scale", state.config.get("guidance_scale", 7.0))),
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,"text2image", pipeline_args))
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(json_payload: str = Form(...), files: List[UploadFile] = []):
743
+ async def edit_image(request: EditRequestJSON):
744
+ manager = None
745
+ temp_config = None
619
746
  try:
620
- data = EditRequestPayload.parse_raw(json_payload)
621
- manager = state.get_active_manager()
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 file in files:
625
- contents = await file.read()
626
- pil_images.append(Image.open(BytesIO(contents)).convert("RGB"))
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
- raise HTTPException(status_code=400, detail="No images provided for editing.")
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 data.params.get("mask") else "image2image"
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
- manager.queue.put((future, task, pipeline_args))
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
- return civitai + local
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()