Преглед изворни кода

Implement model existence checking logic

- Add ModelDetails structure to represent detailed model information
- Implement checkRequiredModelsExistence method in ModelManager
- Integrate existence checking into model detection process
- Update cache handling to include missing models information
- Check models in appropriate subdirectories (vae/, clip/, t5xxl/, qwen2vl/)
- Use absolute paths as required by project guidelines
- Handle file system errors gracefully

This implementation allows the system to identify which required models
are missing and populate the missingModels field in ModelInfo.
Fszontagh пре 3 месеци
родитељ
комит
b864306631

+ 128 - 0
README.md

@@ -16,6 +16,9 @@ A C++ based REST API wrapper for the [stable-diffusion.cpp](https://github.com/l
 - [Web UI](#web-ui)
 - [Architecture](#architecture)
 - [Model Detection and Architecture Support](#model-detection-and-architecture-support)
+- [Model Architecture Requirements](#model-architecture-requirements)
+- [Context Creation Methods per Architecture](#context-creation-methods-per-architecture)
+- [Model Quantization and Conversion](#model-quantization-and-conversion)
 - [Technical Requirements](#technical-requirements)
 - [Project Structure](#project-structure)
 - [Model Types and File Extensions](#model-types-and-file-extensions)
@@ -203,6 +206,131 @@ These models are loaded using the modern `ctxParams.diffusion_model_path` parame
 - **Future-Proof**: Easy to add support for new architectures
 - **Backward Compatible**: Existing models continue to work without changes
 
+## Model Architecture Requirements
+
+> **Note:** The following tables contain extensive information and may require horizontal scrolling to view all columns properly.
+
+| Architecture | Extra VAE | Standalone High Noise | T5XXL | CLIP-Vision | CLIP-G | CLIP-L | Model Files | Example Commands |
+|--------------|-----------|----------------------|-------|-------------|--------|--------|-------------|------------------|
+| SD 1.x | No | No | No | No | No | No | sd-v1-4.ckpt, v1-5-pruned-emaonly.safetensors | `./bin/sd -m ../models/sd-v1-4.ckpt -p "a lovely cat"` |
+| SD 2.x | No | No | No | No | No | No | (Similar to SD 1.x) | `./bin/sd -m ../models/sd2-model.ckpt -p "a lovely cat"` |
+| SDXL | Yes | No | No | No | No | No | sd_xl_base_1.0.safetensors, sdxl_vae-fp16-fix.safetensors | `./bin/sd -m ../models/sd_xl_base_1.0.safetensors --vae ../models/sdxl_vae-fp16-fix.safetensors -H 1024 -W 1024 -p "a lovely cat" -v` |
+| SD3 | No | No | Yes | No | No | No | sd3_medium_incl_clips_t5xxlfp16.safetensors | `./bin/sd -m ../models/sd3_medium_incl_clips_t5xxlfp16.safetensors -H 1024 -W 1024 -p 'a lovely cat holding a sign says "Stable Diffusion CPP"' --cfg-scale 4.5 --sampling-method euler -v --clip-on-cpu` |
+| SD3.5 Large | No | No | Yes | No | Yes | Yes | sd3.5_large.safetensors, clip_l.safetensors, clip_g.safetensors, t5xxl_fp16.safetensors | `./bin/sd -m ../models/sd3.5_large.safetensors --clip_l ../models/clip_l.safetensors --clip_g ../models/clip_g.safetensors --t5xxl ../models/t5xxl_fp16.safetensors -H 1024 -W 1024 -p 'a lovely cat holding a sign says "Stable diffusion 3.5 Large"' --cfg-scale 4.5 --sampling-method euler -v --clip-on-cpu` |
+| FLUX Models | Yes | No | Yes | No | No | Yes | flux1-dev-q3_k.gguf, flux1-dev-q8_0.gguf, flux1-schnell-q8_0.gguf, ae.sft, clip_l.safetensors, t5xxl_fp16.safetensors | `./bin/sd --diffusion-model ../models/flux1-dev-q8_0.gguf --vae ../models/ae.sft --clip_l ../models/clip_l.safetensors --t5xxl ../models/t5xxl_fp16.safetensors -p "a lovely cat holding a sign says 'flux.cpp'" --cfg-scale 1.0 --sampling-method euler -v --clip-on-cpu` |
+| Kontext | Yes | No | Yes | No | No | Yes | flux1-kontext-dev-q8_0.gguf, ae.sft, clip_l.safetensors, t5xxl_fp16.safetensors | `./bin/sd -r ./flux1-dev-q8_0.png --diffusion-model ../models/flux1-kontext-dev-q8_0.gguf --vae ../models/ae.sft --clip_l ../models/clip_l.safetensors --t5xxl ../models/t5xxl_fp16.safetensors -p "change 'flux.cpp' to 'kontext.cpp'" --cfg-scale 1.0 --sampling-method euler -v --clip-on-cpu` |
+| Chroma | Yes | No | Yes | No | No | No | chroma-unlocked-v40-q8_0.gguf, ae.sft, t5xxl_fp16.safetensors | `./bin/sd --diffusion-model ../models/chroma-unlocked-v40-q8_0.gguf --vae ../models/ae.sft --t5xxl ../models/t5xxl_fp16.safetensors -p "a lovely cat holding a sign says 'chroma.cpp'" --cfg-scale 4.0 --sampling-method euler -v --chroma-disable-dit-mask --clip-on-cpu` |
+| Wan Models | Yes | No | Yes | Yes | No | No | wan2.1_t2v_1.3B_fp16.safetensors, wan_2.1_vae.safetensors, umt5-xxl-encoder-Q8_0.gguf | `./bin/sd -M vid_gen --diffusion-model ../models/wan2.1_t2v_1.3B_fp16.safetensors --vae ../models/wan_2.1_vae.safetensors --t5xxl ../models/umt5-xxl-encoder-Q8_0.gguf -p "a lovely cat" --cfg-scale 6.0 --sampling-method euler -v -W 832 -H 480 --diffusion-fa --video-frames 33 --flow-shift 3.0` |
+| Wan2.2 T2V A14B | No | Yes | No | No | No | No | Wan2.2-T2V-A14B-LowNoise-Q8_0.gguf, Wan2.2-T2V-A14B-HighNoise-Q8_0.gguf | `./bin/sd -M vid_gen --diffusion-model ../models/Wan2.2-T2V-A14B-LowNoise-Q8_0.gguf -p "a lovely cat" --cfg-scale 6.0 --sampling-method euler -v -W 832 -H 480 --diffusion-fa --video-frames 33 --flow-shift 3.0` |
+| Wan2.2 I2V A14B | No | Yes | No | No | No | No | Wan2.2-I2V-A14B-LowNoise-Q8_0.gguf, Wan2.2-I2V-A14B-HighNoise-Q8_0.gguf | `./bin/sd -M vid_gen --diffusion-model ../models/Wan2.2-I2V-A14B-LowNoise-Q8_0.gguf -r input.png -p "a lovely cat" --cfg-scale 6.0 --sampling-method euler -v -W 832 -H 480 --diffusion-fa --video-frames 33 --flow-shift 3.0` |
+| Qwen Image Models | Yes | No | No | No | No | No | qwen-image-Q8_0.gguf, qwen_image_vae.safetensors, Qwen2.5-VL-7B-Instruct-Q8_0.gguf | `./bin/sd --diffusion-model ../models/qwen-image-Q8_0.gguf --vae ../models/qwen_image_vae.safetensors --qwen2vl ../models/Qwen2.5-VL-7B-Instruct-Q8_0.gguf -p '一个穿着"QWEN"标志的T恤的中国美女正拿着黑色的马克笔面相镜头微笑。' --cfg-scale 2.5 --sampling-method euler -v --offload-to-cpu -H 1024 -W 1024 --diffusion-fa --flow-shift 3` |
+| Qwen Image Edit | Yes | No | No | No | No | No | Qwen_Image_Edit-Q8_0.gguf, qwen_image_vae.safetensors, qwen_2.5_vl_7b.safetensors | `./bin/sd --diffusion-model ../models/Qwen_Image_Edit-Q8_0.gguf --vae ../models/qwen_image_vae.safetensors --qwen2vl ../models/qwen_2.5_vl_7b.safetensors --cfg-scale 2.5 --sampling-method euler -v --offload-to-cpu --diffusion-fa --flow-shift 3 -r ../assets/flux/flux1-dev-q8_0.png -p "change 'flux.cpp' to 'edit.cpp'"` |
+| PhotoMaker | No | No | No | No | No | No | sdxlUnstableDiffusers_v11.safetensors, sdxl_vae.safetensors, photomaker-v1.safetensors | `./bin/sd -m ../models/sdxlUnstableDiffusers_v11.safetensors --vae ../models/sdxl_vae.safetensors --photo-maker ../models/photomaker-v1.safetensors --pm-id-images-dir ../assets/photomaker_examples/scarletthead_woman -p "a girl img, retro futurism" --cfg-scale 5.0 --sampling-method euler -H 1024 -W 1024 --pm-style-strength 10 --vae-on-cpu --steps 50` |
+| LCM | No | No | No | No | No | No | lcm-lora-sdv1-5 | `./bin/sd -m ../models/v1-5-pruned-emaonly.safetensors -p "a lovely cat<lora:lcm-lora-sdv1-5:1>" --steps 4 --lora-model-dir ../models -v --cfg-scale 1` |
+| SSD1B | No | No | No | No | No | No | (Various SSD-1B models) | `./bin/sd -m ../models/ssd-1b.safetensors -p "a lovely cat"` |
+| Tiny SD | No | No | No | No | No | No | (Various Tiny SD models) | `./bin/sd -m ../models/tiny-sd.safetensors -p "a lovely cat"` |
+
+## Context Creation Methods per Architecture
+
+| Architecture | Context Creation Method | Special Parameters | Model Files | Example Commands |
+|--------------|------------------------|-------------------|-------------|------------------|
+| SD 1.x, SD 2.x, SDXL | Standard prompt-based generation | --cfg-scale, --sampling-method, --steps | sd-v1-4.ckpt, v1-5-pruned-emaonly.safetensors, sd_xl_base_1.0.safetensors | `./bin/sd -m ../models/sd-v1-4.ckpt -p "a lovely cat"` |
+| SD3 | Multiple text encoders | --clip-on-cpu recommended | sd3_medium_incl_clips_t5xxlfp16.safetensors | `./bin/sd -m ../models/sd3_medium_incl_clips_t5xxlfp16.safetensors -H 1024 -W 1024 -p 'a lovely cat holding a sign says "Stable Diffusion CPP"' --cfg-scale 4.5 --sampling-method euler -v --clip-on-cpu` |
+| SD3.5 Large | Multiple text encoders | --clip-on-cpu recommended | sd3.5_large.safetensors, clip_l.safetensors, clip_g.safetensors, t5xxl_fp16.safetensors | `./bin/sd -m ../models/sd3.5_large.safetensors --clip_l ../models/clip_l.safetensors --clip_g ../models/clip_g.safetensors --t5xxl ../models/t5xxl_fp16.safetensors -H 1024 -W 1024 -p 'a lovely cat holding a sign says "Stable diffusion 3.5 Large"' --cfg-scale 4.5 --sampling-method euler -v --clip-on-cpu` |
+| FLUX Models | Text-to-image generation | --cfg-scale 1.0 recommended, --clip-on-cpu for memory efficiency | flux1-dev-q3_k.gguf, flux1-dev-q8_0.gguf, flux1-schnell-q8_0.gguf, ae.sft, clip_l.safetensors, t5xxl_fp16.safetensors | `./bin/sd --diffusion-model ../models/flux1-dev-q8_0.gguf --vae ../models/ae.sft --clip_l ../models/clip_l.safetensors --t5xxl ../models/t5xxl_fp16.safetensors -p "a lovely cat holding a sign says 'flux.cpp'" --cfg-scale 1.0 --sampling-method euler -v --clip-on-cpu` |
+| Kontext | Image-to-image transformation | -r for reference image, --cfg-scale 1.0 recommended | flux1-kontext-dev-q8_0.gguf, ae.sft, clip_l.safetensors, t5xxl_fp16.safetensors | `./bin/sd -r ./flux1-dev-q8_0.png --diffusion-model ../models/flux1-kontext-dev-q8_0.gguf --vae ../models/ae.sft --clip_l ../models/clip_l.safetensors --t5xxl ../models/t5xxl_fp16.safetensors -p "change 'flux.cpp' to 'kontext.cpp'" --cfg-scale 1.0 --sampling-method euler -v --clip-on-cpu` |
+| Chroma | Text-to-image generation | --cfg-scale 1.0 recommended, --clip-on-cpu for memory efficiency | chroma-unlocked-v40-q8_0.gguf, ae.sft, t5xxl_fp16.safetensors | `./bin/sd --diffusion-model ../models/chroma-unlocked-v40-q8_0.gguf --vae ../models/ae.sft --t5xxl ../models/t5xxl_fp16.safetensors -p "a lovely cat holding a sign says 'chroma.cpp'" --cfg-scale 4.0 --sampling-method euler -v --chroma-disable-dit-mask --clip-on-cpu` |
+| Chroma1-Radiance | Text-to-image generation | --cfg-scale 4.0 recommended | Chroma1-Radiance-v0.4-Q8_0.gguf, t5xxl_fp16.safetensors | `./bin/sd --diffusion-model ../models/Chroma1-Radiance-v0.4-Q8_0.gguf --t5xxl ../models/clip/t5xxl_fp16.safetensors -p "a lovely cat holding a sign says 'chroma radiance cpp'" --cfg-scale 4.0 --sampling-method euler -v` |
+| Wan Models | Video generation with text prompts | -M vid_gen, --video-frames, --flow-shift, --diffusion-fa | wan2.1_t2v_1.3B_fp16.safetensors, wan_2.1_vae.safetensors, umt5-xxl-encoder-Q8_0.gguf | `./bin/sd -M vid_gen --diffusion-model ../models/wan2.1_t2v_1.3B_fp16.safetensors --vae ../models/wan_2.1_vae.safetensors --t5xxl ../models/umt5-xxl-encoder-Q8_0.gguf -p "a lovely cat" --cfg-scale 6.0 --sampling-method euler -v -W 832 -H 480 --diffusion-fa --video-frames 33 --flow-shift 3.0` |
+| Wan2.1 I2V Models | Image-to-video generation | Requires clip_vision_h.safetensors | wan2.1-i2v-14b-480p-Q8_0.gguf, wan_2.1_vae.safetensors, clip_vision_h.safetensors | `./bin/sd -M vid_gen --diffusion-model ../models/wan2.1-i2v-14b-480p-Q8_0.gguf --vae ../models/wan_2.1_vae.safetensors -r input.png -p "a lovely cat" --cfg-scale 6.0 --sampling-method euler -v -W 832 -H 480 --diffusion-fa --video-frames 33 --flow-shift 3.0` |
+| Wan2.1 FLF2V Models | Flow-to-video generation | Requires clip_vision_h.safetensors | wan2.1-flf2v-14b-720p-Q8_0.gguf, wan_2.1_vae.safetensors, clip_vision_h.safetensors | `./bin/sd -M vid_gen --diffusion-model ../models/wan2.1-flf2v-14b-720p-Q8_0.gguf --vae ../models/wan_2.1_vae.safetensors -r flow.png -p "a lovely cat" --cfg-scale 6.0 --sampling-method euler -v -W 832 -H 480 --diffusion-fa --video-frames 33 --flow-shift 3.0` |
+| Wan2.2 T2V A14B | Text-to-video generation | Uses dual diffusion models | Wan2.2-T2V-A14B-LowNoise-Q8_0.gguf, Wan2.2-T2V-A14B-HighNoise-Q8_0.gguf | `./bin/sd -M vid_gen --diffusion-model ../models/Wan2.2-T2V-A14B-LowNoise-Q8_0.gguf -p "a lovely cat" --cfg-scale 6.0 --sampling-method euler -v -W 832 -H 480 --diffusion-fa --video-frames 33 --flow-shift 3.0` |
+| Wan2.2 I2V A14B | Image-to-video generation | Uses dual diffusion models | Wan2.2-I2V-A14B-LowNoise-Q8_0.gguf, Wan2.2-I2V-A14B-HighNoise-Q8_0.gguf | `./bin/sd -M vid_gen --diffusion-model ../models/Wan2.2-I2V-A14B-LowNoise-Q8_0.gguf -r input.png -p "a lovely cat" --cfg-scale 6.0 --sampling-method euler -v -W 832 -H 480 --diffusion-fa --video-frames 33 --flow-shift 3.0` |
+| Qwen Image Models | Text-to-image generation with Chinese language support | --qwen2vl for the language model, --diffusion-fa, --flow-shift | qwen-image-Q8_0.gguf, qwen_image_vae.safetensors, Qwen2.5-VL-7B-Instruct-Q8_0.gguf | `./bin/sd --diffusion-model ../models/qwen-image-Q8_0.gguf --vae ../models/qwen_image_vae.safetensors --qwen2vl ../models/Qwen2.5-VL-7B-Instruct-Q8_0.gguf -p '一个穿着"QWEN"标志的T恤的中国美女正拿着黑色的马克笔面相镜头微笑。' --cfg-scale 2.5 --sampling-method euler -v --offload-to-cpu -H 1024 -W 1024 --diffusion-fa --flow-shift 3` |
+| Qwen Image Edit | Image editing with reference image | -r for reference image, --qwen2vl_vision for vision model | Qwen_Image_Edit-Q8_0.gguf, qwen_image_vae.safetensors, qwen_2.5_vl_7b.safetensors | `./bin/sd --diffusion-model ../models/Qwen_Image_Edit-Q8_0.gguf --vae ../models/qwen_image_vae.safetensors --qwen2vl ../models/qwen_2.5_vl_7b.safetensors --cfg-scale 2.5 --sampling-method euler -v --offload-to-cpu --diffusion-fa --flow-shift 3 -r ../assets/flux/flux1-dev-q8_0.png -p "change 'flux.cpp' to 'edit.cpp'"` |
+| PhotoMaker | Personalized image generation with ID images | --photo-maker, --pm-id-images-dir, --pm-style-strength | sdxlUnstableDiffusers_v11.safetensors, sdxl_vae.safetensors, photomaker-v1.safetensors | `./bin/sd -m ../models/sdxlUnstableDiffusers_v11.safetensors --vae ../models/sdxl_vae.safetensors --photo-maker ../models/photomaker-v1.safetensors --pm-id-images-dir ../assets/photomaker_examples/scarletthead_woman -p "a girl img, retro futurism" --cfg-scale 5.0 --sampling-method euler -H 1024 -W 1024 --pm-style-strength 10 --vae-on-cpu --steps 50` |
+| LCM | Fast generation with LoRA | --cfg-scale 1.0, --steps 2-8, --sampling-method lcm/euler_a | lcm-lora-sdv1-5 | `./bin/sd -m ../models/v1-5-pruned-emaonly.safetensors -p "a lovely cat<lora:lcm-lora-sdv1-5:1>" --steps 4 --lora-model-dir ../models -v --cfg-scale 1` |
+| SSD1B | Standard prompt-based generation | Standard SD parameters | (Various SSD-1B models) | `./bin/sd -m ../models/ssd-1b.safetensors -p "a lovely cat"` |
+| Tiny SD | Standard prompt-based generation | Standard SD parameters | (Various Tiny SD models) | `./bin/sd -m ../models/tiny-sd.safetensors -p "a lovely cat"` |
+
+## Model Quantization and Conversion
+
+### Quantization Levels Supported
+
+The stable-diffusion.cpp library supports various quantization levels to balance model size and performance:
+
+| Quantization Level | Description | Model Size Reduction | Quality Impact |
+|-------------------|-------------|----------------------|----------------|
+| `f32` | 32-bit floating-point | None (original) | No quality loss |
+| `f16` | 16-bit floating-point | ~50% | Minimal quality loss |
+| `q8_0` | 8-bit integer quantization | ~75% | Slight quality loss |
+| `q5_0`, `q5_1` | 5-bit integer quantization | ~80% | Moderate quality loss |
+| `q4_0`, `q4_1` | 4-bit integer quantization | ~85% | Noticeable quality loss |
+| `q3_k` | 3-bit K-quantization | ~87% | Significant quality loss |
+| `q4_k` | 4-bit K-quantization | ~85% | Good balance of size/quality |
+| `q2_k` | 2-bit K-quantization | ~90% | Major quality loss |
+| `Q4_K_S` | 4-bit K-quantization Small | ~85% | Optimized for smaller models |
+
+### Model Conversion Commands
+
+To convert models from their original format to quantized GGUF format, use the following commands:
+
+#### Stable Diffusion Models
+```bash
+# Convert SD 1.5 model to 8-bit quantization
+./bin/sd -M convert -m ../models/v1-5-pruned-emaonly.safetensors -o ../models/v1-5-pruned-emaonly.q8_0.gguf -v --type q8_0
+
+# Convert SDXL model to 4-bit quantization
+./bin/sd -M convert -m ../models/sd_xl_base_1.0.safetensors -o ../models/sd_xl_base_1.0.q4_0.gguf -v --type q4_0
+```
+
+#### Flux Models
+```bash
+# Convert Flux Dev model to 8-bit quantization
+./bin/sd -M convert -m ../models/flux1-dev.sft -o ../models/flux1-dev-q8_0.gguf -v --type q8_0
+
+# Convert Flux Schnell model to 3-bit K-quantization
+./bin/sd -M convert -m ../models/flux1-schnell.sft -o ../models/flux1-schnell-q3_k.gguf -v --type q3_k
+```
+
+#### Chroma Models
+```bash
+# Convert Chroma model to 8-bit quantization
+./bin/sd -M convert -m ../models/chroma-unlocked-v40.safetensors -o ../models/chroma-unlocked-v40-q8_0.gguf -v --type q8_0
+```
+
+#### Kontext Models
+```bash
+# Convert Kontext model to 8-bit quantization
+./bin/sd -M convert -m ../models/flux1-kontext-dev.safetensors -o ../models/flux1-kontext-dev-q8_0.gguf -v --type q8_0
+```
+
+### LoRA Models
+
+The project supports LoRA (Low-Rank Adaptation) models for fine-tuning and style transfer:
+
+| LoRA Model | Compatible Base Models | Example Usage |
+|------------|----------------------|----------------|
+| `marblesh.safetensors` | SD 1.5, SD 2.1 | `./bin/sd -m ../models/v1-5-pruned-emaonly.safetensors -p "a lovely cat<lora:marblesh:1>" --lora-model-dir ../models` |
+| `lcm-lora-sdv1-5` | SD 1.5 | `./bin/sd -m ../models/v1-5-pruned-emaonly.safetensors -p "a lovely cat<lora:lcm-lora-sdv1-5:1>" --steps 4 --lora-model-dir ../models -v --cfg-scale 1` |
+| `realism_lora_comfy_converted` | FLUX Models | `./bin/sd --diffusion-model ../models/flux1-dev-q8_0.gguf --vae ../models/ae.sft --clip_l ../models/clip_l.safetensors --t5xxl ../models/t5xxl_fp16.safetensors -p "a lovely cat holding a sign says 'flux.cpp'<lora:realism_lora_comfy_converted:1>" --cfg-scale 1.0 --sampling-method euler -v --lora-model-dir ../models --clip-on-cpu` |
+| `wan2.2_t2v_lightx2v_4steps_lora_v1.1_low_noise` | Wan2.2 T2V Models | `./bin/sd -M vid_gen --diffusion-model ../models/Wan2.2-T2V-A14B-LowNoise-Q8_0.gguf --lora-model-dir ../models -p "a lovely cat<lora:wan2.2_t2v_lightx2v_4steps_lora_v1.1_low_noise:1>" --steps 4` |
+| `wan2.2_t2v_lightx2v_4steps_lora_v1.1_high_noise` | Wan2.2 T2V Models | `./bin/sd -M vid_gen --diffusion-model ../models/Wan2.2-T2V-A14B-HighNoise-Q8_0.gguf --lora-model-dir ../models -p "a lovely cat<lora:wan2.2_t2v_lightx2v_4steps_lora_v1.1_high_noise:1>" --steps 4` |
+
+### Additional Model Types
+
+#### Upscaling (ESRGAN)
+```bash
+# Use ESRGAN for upscaling generated images
+./bin/sd -m ../models/v1-5-pruned-emaonly.safetensors -p "a lovely cat" --upscale-model ../models/RealESRGAN_x4plus_anime_6B.pth
+```
+
+#### Fast Decoding (TAESD)
+```bash
+# Use TAESD for faster VAE decoding
+./bin/sd -m ../models/v1-5-pruned-emaonly.safetensors -p "a lovely cat" --taesd ../models/diffusion_pytorch_model.safetensors
+```
+
 ## Model Types and File Extensions
 
 The project supports various model types, each with specific file extensions:

+ 1 - 0
auth/api_keys.json

@@ -0,0 +1 @@
+{}

+ 1 - 0
auth/users.json

@@ -0,0 +1 @@
+{}

+ 0 - 1
include/model_detector.h

@@ -4,7 +4,6 @@
 #include <string>
 #include <vector>
 #include <map>
-#include <optional>
 
 /**
  * @brief Detected model architecture types

+ 22 - 0
include/model_manager.h

@@ -94,6 +94,20 @@ public:
         std::string detectionSource;     ///< Source of detection: "folder", "architecture", "fallback"
     };
 
+    /**
+     * @brief Model details structure for existence checking
+     */
+    struct ModelDetails {
+        std::string name;           ///< Model name
+        bool exists;                ///< Whether the model exists
+        std::string type;           ///< Model type ("VAE", "CLIP-L", "CLIP-G", "T5XXL", "CLIP-Vision", "Qwen2VL")
+        std::string path;           ///< Absolute path to the model file (empty if doesn't exist)
+        size_t file_size;           ///< File size in bytes (0 if doesn't exist)
+        std::string sha256;         ///< SHA256 hash (empty if doesn't exist)
+        bool is_required;           ///< True for required models
+        bool is_recommended;        ///< True for recommended models
+    };
+
     /**
      * @brief Construct a new Model Manager object
      */
@@ -315,6 +329,14 @@ public:
      */
     std::string ensureModelHash(const std::string& modelName, bool forceCompute = false);
 
+    /**
+     * @brief Check if required models exist in the appropriate directories
+     *
+     * @param requiredModels List of required model names with types (e.g., "VAE: model.safetensors")
+     * @return std::vector<ModelDetails> List of model details with existence information
+     */
+    std::vector<ModelDetails> checkRequiredModelsExistence(const std::vector<std::string>& requiredModels);
+
 private:
     class Impl;
     std::unique_ptr<Impl> pImpl; // Pimpl idiom

+ 5 - 0
include/server.h

@@ -201,6 +201,11 @@ private:
      */
     void handleDownloadOutput(const httplib::Request& req, httplib::Response& res);
 
+    /**
+     * @brief Get job output by job ID endpoint handler
+     */
+    void handleJobOutput(const httplib::Request& req, httplib::Response& res);
+
     /**
      * @brief Download image from URL and return as base64 endpoint handler
      */

+ 24 - 0
model_index.json

@@ -0,0 +1,24 @@
+{
+  "_class_name": "QwenImagePipeline",
+  "_diffusers_version": "0.34.0.dev0",
+  "scheduler": [
+    "diffusers",
+    "FlowMatchEulerDiscreteScheduler"
+  ],
+  "text_encoder": [
+    "transformers",
+    "Qwen2_5_VLForConditionalGeneration"
+  ],
+  "tokenizer": [
+    "transformers",
+    "Qwen2Tokenizer"
+  ],
+  "transformer": [
+    "diffusers",
+    "QwenImageTransformer2DModel"
+  ],
+  "vae": [
+    "diffusers",
+    "AutoencoderKLQwenImage"
+  ]
+}

BIN
simple_architecture_test


+ 206 - 0
simple_architecture_test.cpp

@@ -0,0 +1,206 @@
+#include "model_detector.h"
+#include <iostream>
+#include <vector>
+#include <filesystem>
+#include <map>
+
+namespace fs = std::filesystem;
+
+// Test results structure
+struct TestResult {
+    std::string modelPath;
+    std::string expectedArchitecture;
+    ModelArchitecture detectedEnum;
+    std::string detectedName;
+    bool passed;
+    std::string notes;
+};
+
+// Test function for each model
+void testModelArchitecture(const std::string& modelPath, 
+                          ModelArchitecture expectedEnum,
+                          const std::string& expectedName,
+                          std::vector<TestResult>& results) {
+    
+    TestResult result;
+    result.modelPath = modelPath;
+    result.expectedArchitecture = expectedName;
+    result.detectedEnum = ModelArchitecture::UNKNOWN;
+    result.detectedName = "Unknown";
+    result.passed = false;
+    
+    std::cout << "\n=== Testing: " << fs::path(modelPath).filename().string() << " ===" << std::endl;
+    std::cout << "Expected: " << expectedName << std::endl;
+    
+    if (!fs::exists(modelPath)) {
+        std::cout << "❌ Model file does not exist!" << std::endl;
+        result.notes = "File not found";
+        results.push_back(result);
+        return;
+    }
+    
+    try {
+        ModelDetectionResult detectionResult = ModelDetector::detectModel(modelPath);
+        result.detectedEnum = detectionResult.architecture;
+        result.detectedName = detectionResult.architectureName;
+        
+        std::cout << "Detected: " << result.detectedName << std::endl;
+        
+        // Check if detection matches expected
+        if (result.detectedEnum == expectedEnum) {
+            std::cout << "✅ PASS: Correctly detected as " << expectedName << std::endl;
+            result.passed = true;
+            result.notes = "Correctly detected";
+        } else {
+            std::cout << "❌ FAIL: Expected " << expectedName << " but got " << result.detectedName << std::endl;
+            result.notes = "Incorrect detection - expected " + expectedName + " but got " + result.detectedName;
+            
+            // Show some tensor names for debugging
+            std::cout << "  First 5 tensor names:" << std::endl;
+            for (size_t i = 0; i < std::min(size_t(5), detectionResult.tensorNames.size()); ++i) {
+                std::cout << "    " << detectionResult.tensorNames[i] << std::endl;
+            }
+        }
+        
+        // Show key metadata if available
+        if (!detectionResult.metadata.empty()) {
+            std::cout << "  Key metadata:" << std::endl;
+            for (const auto& [key, value] : detectionResult.metadata) {
+                if (key.find("architecture") != std::string::npos || 
+                    key.find("model") != std::string::npos ||
+                    key.find("_model_name") != std::string::npos) {
+                    std::cout << "    " << key << ": " << value << std::endl;
+                }
+            }
+        }
+        
+    } catch (const std::exception& e) {
+        std::cout << "❌ ERROR: " << e.what() << std::endl;
+        result.notes = "Exception: " + std::string(e.what());
+    }
+    
+    results.push_back(result);
+}
+
+int main() {
+    std::cout << "🧪 Simple Model Architecture Detection Test" << std::endl;
+    std::cout << "===========================================" << std::endl;
+    std::cout << "Testing all model types to verify Qwen fix doesn't break other architectures" << std::endl;
+    
+    std::vector<TestResult> results;
+    
+    // Test Stable Diffusion 1.5 models
+    std::cout << "\n📋 Testing Stable Diffusion 1.5 Models..." << std::endl;
+    testModelArchitecture("/data/SD_MODELS/checkpoints/v1-5-pruned-emaonly.safetensors", 
+                         ModelArchitecture::SD_1_5, "Stable Diffusion 1.5", results);
+    testModelArchitecture("/data/SD_MODELS/checkpoints/sd_15_base.safetensors", 
+                         ModelArchitecture::SD_1_5, "Stable Diffusion 1.5", results);
+    
+    // Test SDXL models
+    std::cout << "\n📋 Testing SDXL Models..." << std::endl;
+    testModelArchitecture("/data/SD_MODELS/checkpoints/sd_xl_base_1.0_0.9vae.safetensors", 
+                         ModelArchitecture::SDXL_BASE, "Stable Diffusion XL Base", results);
+    testModelArchitecture("/data/SD_MODELS/checkpoints/sd_xl_refiner_1.0_0.9vae.safetensors", 
+                         ModelArchitecture::SDXL_REFINER, "Stable Diffusion XL Refiner", results);
+    testModelArchitecture("/data/SD_MODELS/checkpoints/juggernautXL_v8Rundiffusion.safetensors", 
+                         ModelArchitecture::SDXL_BASE, "Stable Diffusion XL Base", results);
+    testModelArchitecture("/data/SD_MODELS/checkpoints/realDream_sdxl6.safetensors", 
+                         ModelArchitecture::SDXL_BASE, "Stable Diffusion XL Base", results);
+    
+    // Test Flux models
+    std::cout << "\n📋 Testing Flux Models..." << std::endl;
+    testModelArchitecture("/data/SD_MODELS/checkpoints/chroma-unlocked-v40-detail-calibrated-Q4_0.gguf", 
+                         ModelArchitecture::FLUX_CHROMA, "Flux Chroma (Unlocked)", results);
+    testModelArchitecture("/data/SD_MODELS/checkpoints/flux1-kontext-dev-Q5_K_S.gguf", 
+                         ModelArchitecture::FLUX_DEV, "Flux Dev", results);
+    testModelArchitecture("/data/SD_MODELS/checkpoints/gonzalomoXLFluxPony_v20PonyDMD.safetensors", 
+                         ModelArchitecture::FLUX_DEV, "Flux Dev", results);
+    
+    // Test Qwen models (to verify the fix still works)
+    std::cout << "\n📋 Testing Qwen Models (verifying fix)..." << std::endl;
+    testModelArchitecture("/data/SD_MODELS/diffusion_models/Qwen-Image-Edit-2509-Q3_K_S.gguf", 
+                         ModelArchitecture::QWEN2VL, "Qwen2-VL", results);
+    testModelArchitecture("/data/SD_MODELS/diffusion_models/Qwen-Image-Pruning-13b-Q4_0.gguf", 
+                         ModelArchitecture::QWEN2VL, "Qwen2-VL", results);
+    testModelArchitecture("/data/SD_MODELS/diffusion_models/qwen-image-Q2_K.gguf", 
+                         ModelArchitecture::QWEN2VL, "Qwen2-VL", results);
+    
+    // Summary by architecture
+    std::map<ModelArchitecture, std::pair<int, int>> archStats; // {passed, total}
+    
+    std::cout << "\n📊 DETAILED RESULTS BY ARCHITECTURE:" << std::endl;
+    std::cout << "======================================" << std::endl;
+    
+    for (const auto& result : results) {
+        archStats[result.detectedEnum].first += result.passed ? 1 : 0;
+        archStats[result.detectedEnum].second += 1;
+        
+        std::cout << "\n📁 Model: " << fs::path(result.modelPath).filename().string() << std::endl;
+        std::cout << "   Expected: " << result.expectedArchitecture << std::endl;
+        std::cout << "   Detected: " << result.detectedName << std::endl;
+        std::cout << "   Status: " << (result.passed ? "✅ PASS" : "❌ FAIL") << std::endl;
+        if (!result.notes.empty()) {
+            std::cout << "   Notes: " << result.notes << std::endl;
+        }
+    }
+    
+    // Overall summary
+    std::cout << "\n🎯 OVERALL TEST SUMMARY:" << std::endl;
+    std::cout << "========================" << std::endl;
+    
+    int totalTests = 0;
+    int totalPassed = 0;
+    
+    for (const auto& [arch, stats] : archStats) {
+        std::string archName = ModelDetector::getArchitectureName(arch);
+        int passed = stats.first;
+        int total = stats.second;
+        
+        std::cout << archName << ": " << passed << "/" << total << " tests passed";
+        if (passed == total && total > 0) {
+            std::cout << " ✅";
+        } else if (total > 0) {
+            std::cout << " ❌";
+        }
+        std::cout << std::endl;
+        
+        totalTests += total;
+        totalPassed += passed;
+    }
+    
+    std::cout << "\n📈 FINAL RESULTS:" << std::endl;
+    std::cout << "   Total models tested: " << totalTests << std::endl;
+    std::cout << "   Successfully detected: " << totalPassed << std::endl;
+    std::cout << "   Success rate: " << (totalTests > 0 ? (totalPassed * 100 / totalTests) : 0) << "%" << std::endl;
+    
+    // Check for specific issues with the Qwen fix
+    std::cout << "\n🔍 QWEN FIX IMPACT ANALYSIS:" << std::endl;
+    std::cout << "=============================" << std::endl;
+    
+    bool qwenWorks = false;
+    bool othersWork = true;
+    
+    for (const auto& result : results) {
+        if (result.expectedArchitecture == "Qwen2-VL") {
+            qwenWorks = result.passed;
+        } else if (result.expectedArchitecture != "Unknown" && !result.passed) {
+            othersWork = false;
+        }
+    }
+    
+    if (qwenWorks) {
+        std::cout << "✅ Qwen detection fix is working correctly" << std::endl;
+    } else {
+        std::cout << "❌ Qwen detection fix is NOT working" << std::endl;
+    }
+    
+    if (othersWork) {
+        std::cout << "✅ Other model architectures are still working correctly" << std::endl;
+    } else {
+        std::cout << "❌ Some other model architectures are broken after the Qwen fix" << std::endl;
+    }
+    
+    std::cout << "\n🏁 TEST COMPLETE!" << std::endl;
+    
+    return (totalPassed == totalTests && totalTests > 0) ? 0 : 1;
+}


+ 80 - 0
simple_qwen_test.cpp

@@ -0,0 +1,80 @@
+#include "model_detector.h"
+#include <iostream>
+#include <vector>
+#include <filesystem>
+
+namespace fs = std::filesystem;
+
+int main() {
+    std::cout << "🧪 Simple Qwen Detection Test" << std::endl;
+    std::cout << "============================" << std::endl;
+
+    // Test with available Qwen models
+    std::vector<std::string> qwenModelPaths = {
+        "/data/SD_MODELS/diffusion_models/Qwen-Image-Edit-2509-Q3_K_S.gguf",
+        "/data/SD_MODELS/diffusion_models/Qwen-Image-Pruning-13b-Q4_0.gguf",
+        "/data/SD_MODELS/diffusion_models/qwen-image-Q2_K.gguf"
+    };
+
+    int successCount = 0;
+    int totalTests = 0;
+
+    for (const auto& modelPath : qwenModelPaths) {
+        if (fs::exists(modelPath)) {
+            totalTests++;
+            std::cout << "\n=== Testing: " << fs::path(modelPath).filename().string() << " ===" << std::endl;
+            
+            try {
+                ModelDetectionResult result = ModelDetector::detectModel(modelPath);
+                
+                std::cout << "📋 Detection Results:" << std::endl;
+                std::cout << "  Architecture: " << result.architectureName << std::endl;
+                std::cout << "  Architecture Enum: " << static_cast<int>(result.architecture) << std::endl;
+                
+                // Check if it's correctly detected as QWEN2VL
+                bool isCorrectlyDetected = (result.architecture == ModelArchitecture::QWEN2VL);
+                std::cout << "  ✅ Correctly detected as QWEN2VL: " << (isCorrectlyDetected ? "YES" : "NO") << std::endl;
+                
+                if (isCorrectlyDetected) {
+                    successCount++;
+                    std::cout << "  🎉 SUCCESS!" << std::endl;
+                } else {
+                    std::cout << "  ❌ FAILED: Expected QWEN2VL but got " << result.architectureName << std::endl;
+                }
+
+                // Show some tensor names for verification
+                std::cout << "  🔍 Sample tensors (first 5):" << std::endl;
+                int count = 0;
+                for (const auto& tensorName : result.tensorNames) {
+                    if (count >= 5) break;
+                    std::cout << "    " << (count + 1) << ". " << tensorName << std::endl;
+                    count++;
+                }
+                
+            } catch (const std::exception& e) {
+                std::cout << "❌ Error during detection: " << e.what() << std::endl;
+            }
+        } else {
+            std::cout << "\n⚠️  Skipping non-existent model: " << modelPath << std::endl;
+        }
+    }
+
+    // Summary
+    std::cout << "\n🎯 Test Summary:" << std::endl;
+    std::cout << "  Total Qwen models tested: " << totalTests << std::endl;
+    std::cout << "  Successfully detected as QWEN2VL: " << successCount << std::endl;
+    std::cout << "  Success rate: " << (totalTests > 0 ? (successCount * 100 / totalTests) : 0) << "%" << std::endl;
+
+    if (successCount == totalTests && totalTests > 0) {
+        std::cout << "  🎉 ALL TESTS PASSED! Qwen detection fix is working correctly." << std::endl;
+        std::cout << "  ✅ Qwen models are no longer misidentified as Stable Diffusion 1.5" << std::endl;
+    } else if (totalTests > 0) {
+        std::cout << "  ❌ Some tests failed. The fix may need adjustment." << std::endl;
+    } else {
+        std::cout << "  ⚠️  No Qwen models found to test." << std::endl;
+    }
+
+    std::cout << "\n🏁 Qwen Detection Test Complete!" << std::endl;
+
+    return (successCount == totalTests) ? 0 : 1;
+}

+ 172 - 0
src/model_manager.cpp

@@ -401,6 +401,7 @@ public:
                                     info.recommendedSteps = cachedEntry.recommendedSteps;
                                     info.recommendedSampler = cachedEntry.recommendedSampler;
                                     info.requiredModels = cachedEntry.requiredModels;
+                                    info.missingModels = cachedEntry.missingModels;
                                     info.cacheValid = true;
                                     info.cacheModifiedAt = cachedEntry.cachedAt;
                                     info.cachePathType = cachedEntry.pathType;
@@ -510,6 +511,25 @@ public:
                                             info.requiredModels.push_back("Qwen2-VL-Vision: " + detection.suggestedParams.at("qwen2vl_vision_required"));
                                         }
 
+                                        // Check if required models exist
+                                        if (!info.requiredModels.empty()) {
+                                            // Create a temporary ModelManager instance to check existence
+                                            ModelManager tempManager;
+                                            std::vector<ModelDetails> modelDetails = tempManager.checkRequiredModelsExistence(info.requiredModels);
+                                            
+                                            // Clear missing models and repopulate based on existence check
+                                            info.missingModels.clear();
+                                            
+                                            for (const auto& detail : modelDetails) {
+                                                if (!detail.exists) {
+                                                    info.missingModels.push_back(detail.type + ": " + detail.name);
+                                                }
+                                            }
+                                            
+                                            std::cout << "Model " << info.name << " requires " << info.requiredModels.size()
+                                                      << " models, " << info.missingModels.size() << " are missing" << std::endl;
+                                        }
+
                                         // Cache the detection result
                                         ModelDetectionCache::cacheDetectionResult(
                                             info.fullPath, detection, pathType, detectionSource, info.modifiedAt);
@@ -1333,6 +1353,59 @@ void ModelManager::ModelDetectionCache::cacheDetectionResult(
         entry.requiredModels.push_back("Qwen2-VL-Vision: " + detection.suggestedParams.at("qwen2vl_vision_required"));
     }
     
+    // Check for missing models and store in cache
+    if (!entry.requiredModels.empty()) {
+        // Create a temporary ModelManager instance to check existence
+        // Note: This is a simplified approach - in a production environment,
+        // we might want to pass the models directory or use a different approach
+        std::string baseModelsDir = "/data/SD_MODELS";
+        
+        for (const auto& requiredModel : entry.requiredModels) {
+            size_t colonPos = requiredModel.find(':');
+            if (colonPos == std::string::npos) continue;
+            
+            std::string modelType = requiredModel.substr(0, colonPos);
+            std::string modelName = requiredModel.substr(colonPos + 1);
+            
+            // Trim whitespace
+            modelType.erase(0, modelType.find_first_not_of(" \t"));
+            modelType.erase(modelType.find_last_not_of(" \t") + 1);
+            modelName.erase(0, modelName.find_first_not_of(" \t"));
+            modelName.erase(modelName.find_last_not_of(" \t") + 1);
+            
+            // Determine the appropriate subdirectory
+            std::string subdirectory;
+            if (modelType == "VAE") {
+                subdirectory = "vae";
+            } else if (modelType == "CLIP-L" || modelType == "CLIP-G") {
+                subdirectory = "clip";
+            } else if (modelType == "T5XXL") {
+                subdirectory = "t5xxl";
+            } else if (modelType == "CLIP-Vision") {
+                subdirectory = "clip";
+            } else if (modelType == "Qwen2-VL" || modelType == "Qwen2-VL-Vision") {
+                subdirectory = "qwen2vl";
+            }
+            
+            // Check if model exists
+            std::string fullPath;
+            if (!subdirectory.empty()) {
+                fullPath = baseModelsDir + "/" + subdirectory + "/" + modelName;
+            } else {
+                fullPath = baseModelsDir + "/" + modelName;
+            }
+            
+            try {
+                if (!fs::exists(fullPath) || !fs::is_regular_file(fullPath)) {
+                    entry.missingModels.push_back(requiredModel);
+                }
+            } catch (const fs::filesystem_error&) {
+                // If we can't check, assume it's missing
+                entry.missingModels.push_back(requiredModel);
+            }
+        }
+    }
+    
     cache_[modelPath] = entry;
     std::cout << "Cached detection result for: " << modelPath
               << " (source: " << detectionSource << ", path type: " << pathType << ")" << std::endl;
@@ -1355,3 +1428,102 @@ void ModelManager::ModelDetectionCache::clearAllCache() {
     cache_.clear();
     std::cout << "Cleared " << count << " cache entries" << std::endl;
 }
+
+std::vector<ModelManager::ModelDetails> ModelManager::checkRequiredModelsExistence(const std::vector<std::string>& requiredModels) {
+    std::vector<ModelDetails> modelDetails;
+    
+    // Base models directory according to project guidelines
+    std::string baseModelsDir = "/data/SD_MODELS";
+    
+    for (const auto& requiredModel : requiredModels) {
+        ModelDetails details;
+        
+        // Parse the required model string (format: "TYPE: filename")
+        size_t colonPos = requiredModel.find(':');
+        if (colonPos == std::string::npos) {
+            // Invalid format, skip
+            continue;
+        }
+        
+        std::string modelType = requiredModel.substr(0, colonPos);
+        std::string modelName = requiredModel.substr(colonPos + 1);
+        
+        // Trim whitespace
+        modelType.erase(0, modelType.find_first_not_of(" \t"));
+        modelType.erase(modelType.find_last_not_of(" \t") + 1);
+        modelName.erase(0, modelName.find_first_not_of(" \t"));
+        modelName.erase(modelName.find_last_not_of(" \t") + 1);
+        
+        details.name = modelName;
+        details.type = modelType;
+        details.is_required = true;
+        details.is_recommended = false;
+        details.exists = false;
+        details.file_size = 0;
+        details.path = "";
+        details.sha256 = "";
+        
+        // Determine the appropriate subdirectory based on model type
+        std::string subdirectory;
+        if (modelType == "VAE") {
+            subdirectory = "vae";
+        } else if (modelType == "CLIP-L" || modelType == "CLIP-G") {
+            subdirectory = "clip";
+        } else if (modelType == "T5XXL") {
+            subdirectory = "t5xxl";
+        } else if (modelType == "CLIP-Vision") {
+            subdirectory = "clip";
+        } else if (modelType == "Qwen2-VL" || modelType == "Qwen2-VL-Vision") {
+            subdirectory = "qwen2vl";
+        } else {
+            // For unknown types, check in root directory
+            subdirectory = "";
+        }
+        
+        // Construct the full path to check
+        std::string fullPath;
+        if (!subdirectory.empty()) {
+            fullPath = baseModelsDir + "/" + subdirectory + "/" + modelName;
+        } else {
+            fullPath = baseModelsDir + "/" + modelName;
+        }
+        
+        // Check if the file exists
+        try {
+            if (fs::exists(fullPath) && fs::is_regular_file(fullPath)) {
+                details.exists = true;
+                details.path = fs::absolute(fullPath).string();
+                details.file_size = fs::file_size(fullPath);
+                
+                // Try to get cached hash
+                std::string jsonPath = fullPath + ".json";
+                if (fs::exists(jsonPath)) {
+                    try {
+                        std::ifstream jsonFile(jsonPath);
+                        if (jsonFile.is_open()) {
+                            nlohmann::json j;
+                            jsonFile >> j;
+                            jsonFile.close();
+                            
+                            if (j.contains("sha256") && j["sha256"].is_string()) {
+                                details.sha256 = j["sha256"].get<std::string>();
+                            }
+                        }
+                    } catch (const std::exception& e) {
+                        std::cerr << "Error loading hash for " << fullPath << ": " << e.what() << std::endl;
+                    }
+                }
+                
+                std::cout << "Found required model: " << modelType << " at " << details.path << std::endl;
+            } else {
+                std::cout << "Missing required model: " << modelType << " - expected at " << fullPath << std::endl;
+            }
+        } catch (const fs::filesystem_error& e) {
+            std::cerr << "Error checking model existence for " << fullPath << ": " << e.what() << std::endl;
+        }
+        
+        modelDetails.push_back(details);
+    }
+    
+    return modelDetails;
+}

+ 170 - 0
src/server.cpp

@@ -301,6 +301,11 @@ void Server::registerEndpoints() {
         handleDownloadOutput(req, res);
     });
 
+    // Get job output by job ID endpoint (public to allow frontend to display generated images without authentication)
+    m_httpServer->Get("/api/v1/jobs/(.*)/output", [this](const httplib::Request& req, httplib::Response& res) {
+        handleJobOutput(req, res);
+    });
+
     // Download image from URL endpoint (public for CORS-free image handling)
     m_httpServer->Get("/api/image/download", [this](const httplib::Request& req, httplib::Response& res) {
         handleDownloadImageFromUrl(req, res);
@@ -1487,6 +1492,171 @@ void Server::handleDownloadOutput(const httplib::Request& req, httplib::Response
     }
 }
 
+void Server::handleJobOutput(const httplib::Request& req, httplib::Response& res) {
+    std::string requestId = generateRequestId();
+    
+    try {
+        // Extract job ID from URL path
+        if (req.matches.size() < 2) {
+            sendErrorResponse(res, "Invalid request: job ID required", 400, "INVALID_REQUEST", requestId);
+            return;
+        }
+
+        std::string jobId = req.matches[1].str();
+
+        // Validate job ID
+        if (jobId.empty()) {
+            sendErrorResponse(res, "Job ID cannot be empty", 400, "INVALID_PARAMETERS", requestId);
+            return;
+        }
+
+        // Log the request for debugging
+        std::cout << "Job output request: jobId=" << jobId << std::endl;
+
+        // Get job information to check if it exists and is completed
+        if (!m_generationQueue) {
+            sendErrorResponse(res, "Generation queue not available", 500, "QUEUE_UNAVAILABLE", requestId);
+            return;
+        }
+
+        auto jobInfo = m_generationQueue->getJobInfo(jobId);
+        if (jobInfo.id.empty()) {
+            sendErrorResponse(res, "Job not found", 404, "JOB_NOT_FOUND", requestId);
+            return;
+        }
+
+        // Check if job is completed
+        if (jobInfo.status != GenerationStatus::COMPLETED) {
+            std::string statusStr;
+            switch (jobInfo.status) {
+                case GenerationStatus::QUEUED: statusStr = "queued"; break;
+                case GenerationStatus::PROCESSING: statusStr = "processing"; break;
+                case GenerationStatus::FAILED: statusStr = "failed"; break;
+                default: statusStr = "unknown"; break;
+            }
+            
+            nlohmann::json response = {
+                {"error", {
+                    {"message", "Job not completed yet"},
+                    {"status_code", 400},
+                    {"error_code", "JOB_NOT_COMPLETED"},
+                    {"request_id", requestId},
+                    {"timestamp", std::chrono::duration_cast<std::chrono::seconds>(
+                        std::chrono::system_clock::now().time_since_epoch()).count()},
+                    {"job_status", statusStr}
+                }}
+            };
+            sendJsonResponse(res, response, 400);
+            return;
+        }
+
+        // Check if job has output files
+        if (jobInfo.outputFiles.empty()) {
+            sendErrorResponse(res, "No output files found for completed job", 404, "NO_OUTPUT_FILES", requestId);
+            return;
+        }
+
+        // For simplicity, return the first output file
+        // In a more complex implementation, we could return all files or allow file selection
+        std::string firstOutputFile = jobInfo.outputFiles[0];
+        
+        // Extract filename from full path
+        std::filesystem::path filePath(firstOutputFile);
+        std::string filename = filePath.filename().string();
+        
+        // Construct absolute file path
+        std::string fullPath = std::filesystem::absolute(firstOutputFile).string();
+
+        // Check if file exists
+        if (!std::filesystem::exists(fullPath)) {
+            std::cerr << "Output file not found: " << fullPath << std::endl;
+            sendErrorResponse(res, "Output file not found: " + filename, 404, "FILE_NOT_FOUND", requestId);
+            return;
+        }
+
+        // Check file size to detect zero-byte files
+        auto fileSize = std::filesystem::file_size(fullPath);
+        if (fileSize == 0) {
+            std::cerr << "Output file is zero bytes: " << fullPath << std::endl;
+            sendErrorResponse(res, "Output file is empty (corrupted generation)", 500, "EMPTY_FILE", requestId);
+            return;
+        }
+
+        // Check if file is accessible
+        std::ifstream file(fullPath, std::ios::binary);
+        if (!file.is_open()) {
+            std::cerr << "Failed to open output file: " << fullPath << std::endl;
+            sendErrorResponse(res, "Output file not accessible", 500, "FILE_ACCESS_ERROR", requestId);
+            return;
+        }
+
+        // Read file contents
+        std::string fileContent;
+        try {
+            fileContent = std::string(
+                std::istreambuf_iterator<char>(file),
+                std::istreambuf_iterator<char>()
+            );
+            file.close();
+        } catch (const std::exception& e) {
+            std::cerr << "Failed to read file content: " << e.what() << std::endl;
+            sendErrorResponse(res, "Failed to read file content", 500, "FILE_READ_ERROR", requestId);
+            return;
+        }
+
+        // Verify we actually read data
+        if (fileContent.empty()) {
+            std::cerr << "File content is empty after read: " << fullPath << std::endl;
+            sendErrorResponse(res, "File content is empty after read", 500, "EMPTY_CONTENT", requestId);
+            return;
+        }
+
+        // Determine content type based on file extension
+        std::string contentType = "application/octet-stream";
+
+        if (Utils::endsWith(filename, ".png")) {
+            contentType = "image/png";
+        } else if (Utils::endsWith(filename, ".jpg") || Utils::endsWith(filename, ".jpeg")) {
+            contentType = "image/jpeg";
+        } else if (Utils::endsWith(filename, ".mp4")) {
+            contentType = "video/mp4";
+        } else if (Utils::endsWith(filename, ".gif")) {
+            contentType = "image/gif";
+        } else if (Utils::endsWith(filename, ".webp")) {
+            contentType = "image/webp";
+        }
+
+        // Set response headers for proper browser handling
+        res.set_header("Content-Type", contentType);
+        res.set_header("Content-Length", std::to_string(fileContent.length()));
+        res.set_header("Cache-Control", "public, max-age=3600"); // Cache for 1 hour
+        res.set_header("Access-Control-Allow-Origin", "*"); // CORS for image access
+        
+        // Set additional metadata headers
+        res.set_header("X-Job-ID", jobId);
+        res.set_header("X-Filename", filename);
+        res.set_header("X-File-Size", std::to_string(fileSize));
+        
+        // If there are multiple files, indicate this
+        if (jobInfo.outputFiles.size() > 1) {
+            res.set_header("X-Total-Files", std::to_string(jobInfo.outputFiles.size()));
+            res.set_header("X-File-Index", "1");
+        }
+
+        // Set the content
+        res.set_content(fileContent, contentType);
+        res.status = 200;
+
+        std::cout << "Successfully served job output: jobId=" << jobId
+                  << ", filename=" << filename
+                  << " (" << fileContent.length() << " bytes)" << std::endl;
+
+    } catch (const std::exception& e) {
+        std::cerr << "Exception in handleJobOutput: " << e.what() << std::endl;
+        sendErrorResponse(res, std::string("Failed to get job output: ") + e.what(), 500, "OUTPUT_ERROR", requestId);
+    }
+}
+
 void Server::handleImageResize(const httplib::Request& req, httplib::Response& res) {
     std::string requestId = generateRequestId();
 

+ 214 - 0
test_all_architectures.cpp

@@ -0,0 +1,214 @@
+#include "model_detector.h"
+#include <iostream>
+#include <vector>
+#include <filesystem>
+#include <map>
+
+namespace fs = std::filesystem;
+
+// Test results structure
+struct TestResult {
+    std::string modelPath;
+    std::string expectedArchitecture;
+    ModelArchitecture detectedEnum;
+    std::string detectedName;
+    bool passed;
+    std::string notes;
+};
+
+// Test function for each model
+void testModelArchitecture(const std::string& modelPath, 
+                          ModelArchitecture expectedEnum,
+                          const std::string& expectedName,
+                          std::vector<TestResult>& results) {
+    
+    TestResult result;
+    result.modelPath = modelPath;
+    result.expectedArchitecture = expectedName;
+    result.detectedEnum = ModelArchitecture::UNKNOWN;
+    result.detectedName = "Unknown";
+    result.passed = false;
+    
+    std::cout << "\n=== Testing: " << fs::path(modelPath).filename().string() << " ===" << std::endl;
+    std::cout << "Expected: " << expectedName << std::endl;
+    
+    if (!fs::exists(modelPath)) {
+        std::cout << "❌ Model file does not exist!" << std::endl;
+        result.notes = "File not found";
+        results.push_back(result);
+        return;
+    }
+    
+    try {
+        ModelDetectionResult detectionResult = ModelDetector::detectModel(modelPath);
+        result.detectedEnum = detectionResult.architecture;
+        result.detectedName = detectionResult.architectureName;
+        
+        std::cout << "Detected: " << result.detectedName << std::endl;
+        
+        // Check if detection matches expected
+        if (result.detectedEnum == expectedEnum) {
+            std::cout << "✅ PASS: Correctly detected as " << expectedName << std::endl;
+            result.passed = true;
+            result.notes = "Correctly detected";
+        } else {
+            std::cout << "❌ FAIL: Expected " << expectedName << " but got " << result.detectedName << std::endl;
+            result.notes = "Incorrect detection - expected " + expectedName + " but got " + result.detectedName;
+            
+            // Show some tensor names for debugging
+            std::cout << "  First 5 tensor names:" << std::endl;
+            for (size_t i = 0; i < std::min(size_t(5), detectionResult.tensorNames.size()); ++i) {
+                std::cout << "    " << detectionResult.tensorNames[i] << std::endl;
+            }
+        }
+        
+        // Show key metadata if available
+        if (!detectionResult.metadata.empty()) {
+            std::cout << "  Key metadata:" << std::endl;
+            for (const auto& [key, value] : detectionResult.metadata) {
+                if (key.find("architecture") != std::string::npos || 
+                    key.find("model") != std::string::npos ||
+                    key.find("_model_name") != std::string::npos) {
+                    std::cout << "    " << key << ": " << value << std::endl;
+                }
+            }
+        }
+        
+    } catch (const std::exception& e) {
+        std::cout << "❌ ERROR: " << e.what() << std::endl;
+        result.notes = "Exception: " + std::string(e.what());
+    }
+    
+    results.push_back(result);
+}
+
+int main() {
+    std::cout << "🧪 Comprehensive Model Architecture Detection Test" << std::endl;
+    std::cout << "==================================================" << std::endl;
+    std::cout << "Testing all model types to verify Qwen fix doesn't break other architectures" << std::endl;
+    
+    std::vector<TestResult> results;
+    
+    // Test Stable Diffusion 1.5 models
+    std::cout << "\n📋 Testing Stable Diffusion 1.5 Models..." << std::endl;
+    testModelArchitecture("/data/SD_MODELS/checkpoints/v1-5-pruned-emaonly.safetensors", 
+                         ModelArchitecture::SD_1_5, "Stable Diffusion 1.5", results);
+    testModelArchitecture("/data/SD_MODELS/checkpoints/sd_15_base.safetensors", 
+                         ModelArchitecture::SD_1_5, "Stable Diffusion 1.5", results);
+    
+    // Test Stable Diffusion 2.1 models (if available)
+    std::cout << "\n📋 Testing Stable Diffusion 2.1 Models..." << std::endl;
+    // Note: No SD 2.1 models found in the current directory, but keeping the test structure
+    
+    // Test SDXL models
+    std::cout << "\n📋 Testing SDXL Models..." << std::endl;
+    testModelArchitecture("/data/SD_MODELS/checkpoints/sd_xl_base_1.0_0.9vae.safetensors", 
+                         ModelArchitecture::SDXL_BASE, "Stable Diffusion XL Base", results);
+    testModelArchitecture("/data/SD_MODELS/checkpoints/sd_xl_refiner_1.0_0.9vae.safetensors", 
+                         ModelArchitecture::SDXL_REFINER, "Stable Diffusion XL Refiner", results);
+    testModelArchitecture("/data/SD_MODELS/checkpoints/juggernautXL_v8Rundiffusion.safetensors", 
+                         ModelArchitecture::SDXL_BASE, "Stable Diffusion XL Base", results);
+    testModelArchitecture("/data/SD_MODELS/checkpoints/realDream_sdxl6.safetensors", 
+                         ModelArchitecture::SDXL_BASE, "Stable Diffusion XL Base", results);
+    
+    // Test Flux models
+    std::cout << "\n📋 Testing Flux Models..." << std::endl;
+    testModelArchitecture("/data/SD_MODELS/checkpoints/chroma-unlocked-v40-detail-calibrated-Q4_0.gguf", 
+                         ModelArchitecture::FLUX_CHROMA, "Flux Chroma (Unlocked)", results);
+    testModelArchitecture("/data/SD_MODELS/checkpoints/flux1-kontext-dev-Q5_K_S.gguf", 
+                         ModelArchitecture::FLUX_DEV, "Flux Dev", results);
+    testModelArchitecture("/data/SD_MODELS/checkpoints/gonzalomoXLFluxPony_v20PonyDMD.safetensors", 
+                         ModelArchitecture::FLUX_DEV, "Flux Dev", results);
+    
+    // Test SD3 models (if available)
+    std::cout << "\n📋 Testing SD3 Models..." << std::endl;
+    // Note: No SD3 models found in the current directory, but keeping the test structure
+    
+    // Test Qwen models (to verify the fix still works)
+    std::cout << "\n📋 Testing Qwen Models (verifying fix)..." << std::endl;
+    testModelArchitecture("/data/SD_MODELS/diffusion_models/Qwen-Image-Edit-2509-Q3_K_S.gguf", 
+                         ModelArchitecture::QWEN2VL, "Qwen2-VL", results);
+    testModelArchitecture("/data/SD_MODELS/diffusion_models/Qwen-Image-Pruning-13b-Q4_0.gguf", 
+                         ModelArchitecture::QWEN2VL, "Qwen2-VL", results);
+    testModelArchitecture("/data/SD_MODELS/diffusion_models/qwen-image-Q2_K.gguf", 
+                         ModelArchitecture::QWEN2VL, "Qwen2-VL", results);
+    
+    // Summary by architecture
+    std::map<ModelArchitecture, std::pair<int, int>> archStats; // {passed, total}
+    
+    std::cout << "\n📊 DETAILED RESULTS BY ARCHITECTURE:" << std::endl;
+    std::cout << "======================================" << std::endl;
+    
+    for (const auto& result : results) {
+        archStats[result.detectedEnum].first += result.passed ? 1 : 0;
+        archStats[result.detectedEnum].second += 1;
+        
+        std::cout << "\n📁 Model: " << fs::path(result.modelPath).filename().string() << std::endl;
+        std::cout << "   Expected: " << result.expectedArchitecture << std::endl;
+        std::cout << "   Detected: " << result.detectedName << std::endl;
+        std::cout << "   Status: " << (result.passed ? "✅ PASS" : "❌ FAIL") << std::endl;
+        if (!result.notes.empty()) {
+            std::cout << "   Notes: " << result.notes << std::endl;
+        }
+    }
+    
+    // Overall summary
+    std::cout << "\n🎯 OVERALL TEST SUMMARY:" << std::endl;
+    std::cout << "========================" << std::endl;
+    
+    int totalTests = 0;
+    int totalPassed = 0;
+    
+    for (const auto& [arch, stats] : archStats) {
+        std::string archName = ModelDetector::getArchitectureName(arch);
+        int passed = stats.first;
+        int total = stats.second;
+        
+        std::cout << archName << ": " << passed << "/" << total << " tests passed";
+        if (passed == total && total > 0) {
+            std::cout << " ✅";
+        } else if (total > 0) {
+            std::cout << " ❌";
+        }
+        std::cout << std::endl;
+        
+        totalTests += total;
+        totalPassed += passed;
+    }
+    
+    std::cout << "\n📈 FINAL RESULTS:" << std::endl;
+    std::cout << "   Total models tested: " << totalTests << std::endl;
+    std::cout << "   Successfully detected: " << totalPassed << std::endl;
+    std::cout << "   Success rate: " << (totalTests > 0 ? (totalPassed * 100 / totalTests) : 0) << "%" << std::endl;
+    
+    // Check for specific issues with the Qwen fix
+    std::cout << "\n🔍 QWEN FIX IMPACT ANALYSIS:" << std::endl;
+    std::cout << "=============================" << std::endl;
+    
+    bool qwenWorks = false;
+    bool othersWork = true;
+    
+    for (const auto& result : results) {
+        if (result.expectedArchitecture == "Qwen2-VL") {
+            qwenWorks = result.passed;
+        } else if (result.expectedArchitecture != "Unknown" && !result.passed) {
+            othersWork = false;
+        }
+    }
+    
+    if (qwenWorks) {
+        std::cout << "✅ Qwen detection fix is working correctly" << std::endl;
+    } else {
+        std::cout << "❌ Qwen detection fix is NOT working" << std::endl;
+    }
+    
+    if (othersWork) {
+        std::cout << "✅ Other model architectures are still working correctly" << std::endl;
+    } else {
+        std::cout << "❌ Some other model architectures are broken after the Qwen fix" << std::endl;
+    }
+    
+    std::cout << "\n🏁 TEST COMPLETE!" << std::endl;
+    
+    return (totalPassed == totalTests && totalTests > 0) ? 0 : 1;
+}

+ 5 - 1
test_model_detection.cpp

@@ -188,7 +188,11 @@ int main() {
     std::vector<std::string> modelPaths = {
         "/data/SD_MODELS/stable-diffusion/sd15.ckpt",
         "/data/SD_MODELS/stable-diffusion/realistic_vision_v60B1_vae.ckpt",
-        "/data/SD_MODELS/stable-diffusion/sdxl_v1-5-pruned.safetensors"
+        "/data/SD_MODELS/stable-diffusion/sdxl_v1-5-pruned.safetensors",
+        // Test Qwen models specifically
+        "/data/SD_MODELS/diffusion_models/Qwen-Image-Edit-2509-Q3_K_S.gguf",
+        "/data/SD_MODELS/diffusion_models/Qwen-Image-Pruning-13b-Q4_0.gguf",
+        "/data/SD_MODELS/diffusion_models/qwen-image-Q2_K.gguf"
     };
 
     for (const auto& modelPath : modelPaths) {

+ 114 - 0
test_qwen_detection.cpp

@@ -0,0 +1,114 @@
+#include "model_detector.h"
+#include <iostream>
+#include <vector>
+#include <filesystem>
+
+namespace fs = std::filesystem;
+
+// Test function specifically for Qwen model detection
+void testQwenDetection(const std::string& modelPath) {
+    std::cout << "\n=== Testing Qwen Model Detection for: " << modelPath << " ===" << std::endl;
+
+    if (!fs::exists(modelPath)) {
+        std::cout << "ERROR: Model file does not exist!" << std::endl;
+        return;
+    }
+
+    try {
+        // Test ModelDetector
+        ModelDetectionResult result = ModelDetector::detectModel(modelPath);
+
+        std::cout << "📋 Detection Results:" << std::endl;
+        std::cout << "  Architecture: " << result.architectureName << std::endl;
+        std::cout << "  Architecture Enum: " << static_cast<int>(result.architecture) << std::endl;
+        
+        // Check if it's correctly detected as QWEN2VL
+        bool isCorrectlyDetected = (result.architecture == ModelArchitecture::QWEN2VL);
+        std::cout << "  ✅ Correctly detected as QWEN2VL: " << (isCorrectlyDetected ? "YES" : "NO") << std::endl;
+        
+        if (!isCorrectlyDetected) {
+            std::cout << "  ❌ FAILED: Expected QWEN2VL but got " << result.architectureName << std::endl;
+            std::cout << "  💡 This indicates the fix is not working properly" << std::endl;
+        } else {
+            std::cout << "  🎉 SUCCESS: Qwen model correctly detected!" << std::endl;
+        }
+
+        // Show tensor names for debugging
+        std::cout << "\n🔍 Tensor Analysis (first 10 tensors):" << std::endl;
+        int count = 0;
+        for (const auto& tensorName : result.tensorNames) {
+            if (count >= 10) break;
+            std::cout << "  " << (count + 1) << ". " << tensorName << std::endl;
+            count++;
+        }
+        
+        if (result.tensorNames.size() > 10) {
+            std::cout << "  ... and " << (result.tensorNames.size() - 10) << " more tensors" << std::endl;
+        }
+
+        // Show metadata
+        std::cout << "\n📄 Metadata:" << std::endl;
+        for (const auto& [key, value] : result.metadata) {
+            std::cout << "  " << key << ": " << value << std::endl;
+        }
+
+        // Show suggested parameters
+        if (!result.suggestedParams.empty()) {
+            std::cout << "\n⚙️  Suggested Parameters:" << std::endl;
+            for (const auto& [key, value] : result.suggestedParams) {
+                std::cout << "  " << key << ": " << value << std::endl;
+            }
+        }
+
+    } catch (const std::exception& e) {
+        std::cout << "❌ Error during model detection: " << e.what() << std::endl;
+    }
+}
+
+int main() {
+    std::cout << "🧪 Qwen Model Detection Test Suite" << std::endl;
+    std::cout << "===================================" << std::endl;
+
+    // Test with available Qwen models
+    std::vector<std::string> qwenModelPaths = {
+        "/data/SD_MODELS/diffusion_models/Qwen-Image-Edit-2509-Q3_K_S.gguf",
+        "/data/SD_MODELS/diffusion_models/Qwen-Image-Pruning-13b-Q4_0.gguf",
+        "/data/SD_MODELS/diffusion_models/qwen-image-Q2_K.gguf"
+    };
+
+    int successCount = 0;
+    int totalTests = 0;
+
+    for (const auto& modelPath : qwenModelPaths) {
+        if (fs::exists(modelPath)) {
+            totalTests++;
+            testQwenDetection(modelPath);
+            
+            // Check if detection was successful
+            ModelDetectionResult result = ModelDetector::detectModel(modelPath);
+            if (result.architecture == ModelArchitecture::QWEN2VL) {
+                successCount++;
+            }
+        } else {
+            std::cout << "\n⚠️  Skipping test for non-existent model: " << modelPath << std::endl;
+        }
+    }
+
+    // Summary
+    std::cout << "\n🎯 Test Summary:" << std::endl;
+    std::cout << "  Total Qwen models tested: " << totalTests << std::endl;
+    std::cout << "  Successfully detected as QWEN2VL: " << successCount << std::endl;
+    std::cout << "  Success rate: " << (totalTests > 0 ? (successCount * 100 / totalTests) : 0) << "%" << std::endl;
+
+    if (successCount == totalTests && totalTests > 0) {
+        std::cout << "  🎉 ALL TESTS PASSED! Qwen detection fix is working correctly." << std::endl;
+    } else if (totalTests > 0) {
+        std::cout << "  ❌ Some tests failed. The fix may need adjustment." << std::endl;
+    } else {
+        std::cout << "  ⚠️  No Qwen models found to test." << std::endl;
+    }
+
+    std::cout << "\n🏁 Qwen Detection Test Complete!" << std::endl;
+
+    return (successCount == totalTests) ? 0 : 1;
+}

+ 135 - 9
webui/components/enhanced-queue-list.tsx

@@ -22,7 +22,11 @@ import {
   Settings,
   FileText,
   Zap,
-  ArrowRight
+  ArrowRight,
+  Download,
+  X,
+  Play,
+  Video
 } from 'lucide-react';
 import { cn } from '@/lib/utils';
 
@@ -36,6 +40,84 @@ interface EnhancedQueueListProps {
   onCopyParameters?: (job: JobInfo) => void;
 }
 
+interface ImageModalProps {
+  isOpen: boolean;
+  onClose: () => void;
+  imageUrl: string;
+  title: string;
+  isVideo?: boolean;
+}
+
+function ImageModal({ isOpen, onClose, imageUrl, title, isVideo = false }: ImageModalProps) {
+  if (!isOpen) return null;
+
+  return (
+    <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 p-4">
+      <div className="relative max-w-7xl max-h-full w-full">
+        <Button
+          variant="ghost"
+          size="icon"
+          onClick={onClose}
+          className="absolute -top-12 right-0 text-white hover:bg-white/20"
+        >
+          <X className="h-6 w-6" />
+        </Button>
+        
+        <div className="bg-background rounded-lg shadow-2xl overflow-hidden">
+          <div className="p-4 border-b">
+            <h3 className="text-lg font-semibold truncate">{title}</h3>
+          </div>
+          
+          <div className="p-4">
+            <div className="relative flex items-center justify-center min-h-[200px] max-h-[80vh]">
+              {isVideo ? (
+                <video
+                  src={imageUrl}
+                  controls
+                  className="max-w-full max-h-[80vh] rounded-lg"
+                  preload="metadata"
+                >
+                  Your browser does not support the video tag.
+                </video>
+              ) : (
+                <img
+                  src={imageUrl}
+                  alt={title}
+                  className="max-w-full max-h-[80vh] object-contain rounded-lg"
+                  onLoad={(e) => {
+                    const target = e.target as HTMLImageElement;
+                    target.style.opacity = '1';
+                  }}
+                  onError={(e) => {
+                    const target = e.target as HTMLImageElement;
+                    target.style.display = 'none';
+                    const parent = target.parentElement;
+                    if (parent) {
+                      parent.innerHTML = `
+                        <div class="flex flex-col items-center justify-center text-muted-foreground p-8">
+                          <Image class="h-16 w-16 mb-4" />
+                          <p class="text-center">Failed to load image</p>
+                        </div>
+                      `;
+                    }
+                  }}
+                  style={{ opacity: '0', transition: 'opacity 0.3s ease-in-out' }}
+                />
+              )}
+            </div>
+          </div>
+          
+          <div className="p-4 border-t flex justify-end">
+            <Button variant="outline" onClick={onClose}>
+              Close
+            </Button>
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+}
+
 type ViewMode = 'compact' | 'detailed';
 
 // Debounce utility for frequent updates
@@ -83,6 +165,17 @@ export function EnhancedQueueList({
   const [viewMode, setViewMode] = useState<ViewMode>('detailed');
   const [selectedJob, setSelectedJob] = useState<string | null>(null);
   const [lastUpdateTime, setLastUpdateTime] = useState(Date.now());
+  const [modalState, setModalState] = useState<{
+    isOpen: boolean;
+    imageUrl: string;
+    title: string;
+    isVideo: boolean;
+  }>({
+    isOpen: false,
+    imageUrl: '',
+    title: '',
+    isVideo: false
+  });
 
   // Debounce the queue status to prevent excessive updates
   const debouncedQueueStatus = useDebounce(queueStatus, 100);
@@ -252,14 +345,38 @@ export function EnhancedQueueList({
     }
   }, [getJobType]);
 
-  // Generate image URL from file path
-  const getImageUrl = useCallback((jobId: string, output: { url: string; path: string }) => {
-    // If we have a URL, use it directly. Otherwise construct from jobId and filename
-    if (output.url) {
-      return output.url;
-    }
-    const filename = output.path.split('/').pop();
-    return `/api/queue/job/${jobId}/output/${filename}`;
+  // Generate image URL from file path using new API endpoint
+  const getImageUrl = useCallback((jobId: string, output: { url: string; path: string; filename?: string }) => {
+    // Use the new API endpoint
+    const filename = output.filename || output.path.split('/').pop();
+    return `/api/v1/jobs/${jobId}/output/${filename}`;
+  }, []);
+
+  // Check if file is a video based on extension
+  const isVideoFile = useCallback((filename: string) => {
+    const videoExtensions = ['.mp4', '.avi', '.mov', '.mkv', '.webm', '.gif'];
+    const ext = filename.toLowerCase().substring(filename.lastIndexOf('.'));
+    return videoExtensions.includes(ext);
+  }, []);
+
+  // Open image modal
+  const openImageModal = useCallback((imageUrl: string, title: string, isVideo: boolean = false) => {
+    setModalState({
+      isOpen: true,
+      imageUrl,
+      title,
+      isVideo
+    });
+  }, []);
+
+  // Close image modal
+  const closeImageModal = useCallback(() => {
+    setModalState({
+      isOpen: false,
+      imageUrl: '',
+      title: '',
+      isVideo: false
+    });
   }, []);
 
   // Optimized parameter extraction
@@ -723,6 +840,15 @@ export function EnhancedQueueList({
           </CardContent>
         </Card>
       )}
+
+      {/* Image Modal */}
+      <ImageModal
+        isOpen={modalState.isOpen}
+        onClose={closeImageModal}
+        imageUrl={modalState.imageUrl}
+        title={modalState.title}
+        isVideo={modalState.isVideo}
+      />
     </div>
   );
 }

+ 137 - 30
webui/components/model-list.tsx

@@ -5,6 +5,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
 import { Button } from '@/components/ui/button';
 import { Input } from '@/components/ui/input';
 import { Label } from '@/components/ui/label';
+import { Badge } from '@/components/ui/badge';
 import { type ModelInfo, apiClient } from '@/lib/api';
 import {
   Search,
@@ -26,7 +27,12 @@ import {
   List,
   ChevronLeft,
   ChevronRight,
-  Image
+  Image,
+  Star,
+  Calendar,
+  Folder,
+  Info,
+  TrendingUp
 } from 'lucide-react';
 import { cn } from '@/lib/utils';
 
@@ -118,6 +124,7 @@ export function ModelList({
     };
   }, [models, availableTypes]);
 
+
   // Filter models
   const filteredModels = useMemo(() => {
     return models.filter(model => {
@@ -165,6 +172,67 @@ export function ModelList({
     return `${(size / (1024 * 1024)).toFixed(1)} MB`;
   };
 
+  // Format date
+  const formatDate = (dateString?: string) => {
+    if (!dateString) return 'Unknown';
+    try {
+      return new Date(dateString).toLocaleDateString();
+    } catch {
+      return 'Unknown';
+    }
+  };
+
+  // Get model type badge with appropriate color
+  const getModelTypeBadge = (type: string) => {
+    const typeLower = type.toLowerCase();
+    if (typeLower.includes('checkpoint') || typeLower.includes('stable-diffusion')) {
+      return (
+        <Badge variant="secondary" className="text-xs bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200">
+          <Image className="h-3 w-3 mr-1" />
+          Checkpoint
+        </Badge>
+      );
+    }
+    if (typeLower.includes('vae')) {
+      return (
+        <Badge variant="secondary" className="text-xs bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200">
+          <Zap className="h-3 w-3 mr-1" />
+          VAE
+        </Badge>
+      );
+    }
+    if (typeLower.includes('lora')) {
+      return (
+        <Badge variant="secondary" className="text-xs bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
+          <Star className="h-3 w-3 mr-1" />
+          LoRA
+        </Badge>
+      );
+    }
+    if (typeLower.includes('embedding') || typeLower.includes('textual')) {
+      return (
+        <Badge variant="secondary" className="text-xs bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200">
+          <FileText className="h-3 w-3 mr-1" />
+          Embedding
+        </Badge>
+      );
+    }
+    if (typeLower.includes('esrgan') || typeLower.includes('upscaler')) {
+      return (
+        <Badge variant="secondary" className="text-xs bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200">
+          <TrendingUp className="h-3 w-3 mr-1" />
+          Upscaler
+        </Badge>
+      );
+    }
+    return (
+      <Badge variant="secondary" className="text-xs">
+        <HardDrive className="h-3 w-3 mr-1" />
+        {type}
+      </Badge>
+    );
+  };
+
   return (
     <div className="space-y-6">
       {/* Controls */}
@@ -263,7 +331,7 @@ export function ModelList({
         <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
           {filteredModels.map(model => (
             <Card key={model.id || model.name} className={cn(
-              'transition-all hover:shadow-md',
+              'transition-all hover:shadow-md relative',
               model.loaded ? 'border-green-500 bg-green-50/50 dark:bg-green-950/20' : ''
             )}>
               <CardContent className="p-4">
@@ -282,29 +350,45 @@ export function ModelList({
                     </div>
                   </div>
 
-                  <div className="space-y-1">
-                    <h3 className="font-semibold text-sm truncate" title={getDisplayName(model)}>
-                      {getDisplayName(model)}
-                    </h3>
-                    <div className="flex items-center gap-2 text-xs text-muted-foreground">
-                      <HardDrive className="h-3 w-3" />
-                      <span>{formatFileSize(model.size || model.file_size)}</span>
+                  <div className="space-y-2">
+                    <div className="flex items-start justify-between gap-2">
+                      <h3 className="font-semibold text-sm truncate" title={getDisplayName(model)}>
+                        {getDisplayName(model)}
+                      </h3>
+                      {getModelTypeBadge(model.type)}
                     </div>
-                    {model.sha256_short && (
-                      <div className="flex items-center gap-1">
-                        <span className="text-xs text-muted-foreground font-mono">
-                          {model.sha256_short}
-                        </span>
-                        <Button
-                          variant="ghost"
-                          size="sm"
-                          className="h-4 w-4 p-0"
-                          onClick={() => navigator.clipboard.writeText(model.sha256_short!)}
-                        >
-                          <Copy className="h-3 w-3" />
-                        </Button>
+                    
+                    {/* Enhanced model information */}
+                    <div className="space-y-1 text-xs text-muted-foreground">
+                      <div className="flex items-center gap-2">
+                        <HardDrive className="h-3 w-3" />
+                        <span>{formatFileSize(model.size || model.file_size)}</span>
                       </div>
-                    )}
+                      
+                      {model.path && (
+                        <div className="flex items-center gap-2">
+                          <Folder className="h-3 w-3" />
+                          <span className="truncate" title={model.path}>
+                            {showFullPaths ? model.path : model.path.split('/').pop()}
+                          </span>
+                        </div>
+                      )}
+                      
+                      {model.sha256_short && (
+                        <div className="flex items-center gap-1">
+                          <span className="font-mono">{model.sha256_short}</span>
+                          <Button
+                            variant="ghost"
+                            size="sm"
+                            className="h-4 w-4 p-0"
+                            onClick={() => navigator.clipboard.writeText(model.sha256_short!)}
+                          >
+                            <Copy className="h-3 w-3" />
+                          </Button>
+                        </div>
+                      )}
+                      
+                    </div>
                   </div>
 
                   <div className="flex gap-2 pt-2">
@@ -414,7 +498,7 @@ export function ModelList({
         <div className="space-y-2">
           {filteredModels.map(model => (
             <Card key={model.id || model.name} className={cn(
-              'transition-all hover:shadow-md',
+              'transition-all hover:shadow-md relative',
               model.loaded ? 'border-green-500 bg-green-50/50 dark:bg-green-950/20' : ''
             )}>
               <CardContent className="p-4">
@@ -428,13 +512,36 @@ export function ModelList({
                       )}
                       <div className="flex items-center gap-2">
                         {getTypeIcon(model.type)}
-                        <div>
-                          <h3 className="font-semibold">{getDisplayName(model)}</h3>
-                          <div className="flex items-center gap-4 text-sm text-muted-foreground">
-                            <span>{model.type}</span>
-                            <span>{formatFileSize(model.size || model.file_size)}</span>
+                        <div className="flex-1">
+                          <div className="flex items-center gap-2 mb-2">
+                            <h3 className="font-semibold">{getDisplayName(model)}</h3>
+                            {getModelTypeBadge(model.type)}
+                          </div>
+                          <div className="space-y-1 text-sm text-muted-foreground">
+                            <div className="flex items-center gap-4">
+                              <span className="flex items-center gap-1">
+                                <HardDrive className="h-3 w-3" />
+                                {formatFileSize(model.size || model.file_size)}
+                              </span>
+                              {model.path && (
+                                <span className="flex items-center gap-1">
+                                  <Folder className="h-3 w-3" />
+                                  {showFullPaths ? model.path : model.path.split('/').pop()}
+                                </span>
+                              )}
+                            </div>
                             {model.sha256_short && (
-                              <span className="font-mono">{model.sha256_short}</span>
+                              <div className="flex items-center gap-2">
+                                <span className="font-mono">{model.sha256_short}</span>
+                                <Button
+                                  variant="ghost"
+                                  size="sm"
+                                  className="h-4 w-4 p-0"
+                                  onClick={() => navigator.clipboard.writeText(model.sha256_short!)}
+                                >
+                                  <Copy className="h-4 w-4" />
+                                </Button>
+                              </div>
                             )}
                           </div>
                         </div>