mirror of
				https://github.com/karl0ss/ai_image_frame_server.git
				synced 2025-10-24 20:24:07 +01:00 
			
		
		
		
	Compare commits
	
		
			10 Commits
		
	
	
		
			1468ac4bbe
			...
			918e37e077
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 918e37e077 | |||
| f5427d18ed | |||
| 34f8a05035 | |||
| 82f29a4fde | |||
| ad814855ab | |||
| 1b75417360 | |||
| 9f3cbf736a | |||
| 3e46b3363b | |||
| ff5dfbcbce | |||
| 14e69f7608 | 
| @ -1,5 +1,5 @@ | ||||
| [tool.bumpversion] | ||||
| current_version = "0.3.4" | ||||
| current_version = "0.3.8" | ||||
| parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)" | ||||
| serialize = ["{major}.{minor}.{patch}"] | ||||
| replace = "{new_version}" | ||||
|  | ||||
| @ -4,7 +4,7 @@ FROM python:3.11-slim | ||||
| # Set the working directory in the container | ||||
| WORKDIR /app | ||||
| # Set version label | ||||
| ARG VERSION="0.3.4" | ||||
| ARG VERSION="0.3.8" | ||||
| LABEL version=$VERSION | ||||
| 
 | ||||
| # Copy project files into the container | ||||
|  | ||||
| @ -122,6 +122,7 @@ def generate_image( | ||||
| def select_model(model: str) -> tuple[str, str]: | ||||
|     use_flux = json.loads(user_config["comfyui"].get("FLUX", "false").lower()) | ||||
|     only_flux = json.loads(user_config["comfyui"].get("ONLY_FLUX", "false").lower()) | ||||
|     use_qwen = json.loads(user_config["comfyui"].get("Qwen", "false").lower()) | ||||
| 
 | ||||
|     if model == "Random Image Model": | ||||
|         selected_workflow = "FLUX" if (use_flux and (only_flux or random.choice([True, False]))) else "SDXL" | ||||
| @ -133,6 +134,8 @@ def select_model(model: str) -> tuple[str, str]: | ||||
|     if model == "Random Image Model": | ||||
|         if selected_workflow == "FLUX": | ||||
|             valid_models = user_config["comfyui:flux"]["models"].split(",") | ||||
|         elif selected_workflow == "Qwen": | ||||
|             valid_models = user_config["comfyui:qwen"]["models"].split(",") | ||||
|         else:  # SDXL | ||||
|             available_model_list = user_config["comfyui"]["models"].split(",") | ||||
|             valid_models = list(set(get_available_models()) & set(available_model_list)) | ||||
| @ -173,7 +176,86 @@ def create_image(prompt: str | None = None, model: str = "Random Image Model") - | ||||
|             model_param="unet_name", | ||||
|             model=model | ||||
|         ) | ||||
|     elif selected_workflow == "Qwen": | ||||
|         generate_image( | ||||
|             file_name="image", | ||||
|             comfy_prompt=prompt, | ||||
|             workflow_path="./workflow_qwen.json", | ||||
|             prompt_node="Positive", | ||||
|             seed_node="KSampler", | ||||
|             seed_param="seed", | ||||
|             save_node="Save Image", | ||||
|             save_param="filename_prefix", | ||||
|             model_node="Load Checkpoint", | ||||
|             model_param="ckpt_name", | ||||
|             model=model | ||||
|         ) | ||||
|     else:  # SDXL | ||||
|         generate_image("image", comfy_prompt=prompt, model=model) | ||||
| 
 | ||||
|     logging.info(f"{selected_workflow} generation started with prompt: {prompt}") | ||||
| 
 | ||||
| def get_queue_count() -> int: | ||||
|     """Fetches the current queue count from ComfyUI (pending + running jobs).""" | ||||
|     url = user_config["comfyui"]["comfyui_url"] + "/queue" | ||||
|     try: | ||||
|         response = requests.get(url) | ||||
|         response.raise_for_status() | ||||
|         data = response.json() | ||||
|         pending = len(data.get("queue_pending", [])) | ||||
|         running = len(data.get("queue_running", [])) | ||||
|         return pending + running | ||||
|     except Exception as e: | ||||
|         logging.error(f"Error fetching queue count: {e}") | ||||
|         return 0 | ||||
| 
 | ||||
| def get_queue_details() -> list: | ||||
|     """Fetches detailed queue information including model names and prompts.""" | ||||
|     url = user_config["comfyui"]["comfyui_url"] + "/queue" | ||||
|     try: | ||||
|         response = requests.get(url) | ||||
|         response.raise_for_status() | ||||
|         data = response.json() | ||||
|         jobs = [] | ||||
|         for job_list in [data.get("queue_running", []), data.get("queue_pending", [])]: | ||||
|             for job in job_list: | ||||
|                 # Extract prompt data (format: [priority, time, prompt]) | ||||
|                 prompt_data = job[2] | ||||
|                 model = "Unknown" | ||||
|                 prompt = "No prompt" | ||||
|                  | ||||
|                 # Find model loader node (works for SDXL/FLUX/Qwen workflows) | ||||
|                 for node in prompt_data.values(): | ||||
|                     if node.get("class_type") in ["CheckpointLoaderSimple", "UnetLoaderGGUFAdvancedDisTorchMultiGPU"]: | ||||
|                         model = node["inputs"].get("ckpt_name", "Unknown") | ||||
|                         break | ||||
|                  | ||||
|                 # Find prompt node using class_type pattern and title matching | ||||
|                 for node in prompt_data.values(): | ||||
|                     class_type = node.get("class_type", "") | ||||
|                     if "CLIPTextEncode" in class_type and "text" in node["inputs"]: | ||||
|                         meta = node.get('_meta', {}) | ||||
|                         title = meta.get('title', '').lower() | ||||
|                         if 'positive' in title or 'prompt' in title: | ||||
|                             prompt = node["inputs"]["text"] | ||||
|                             break | ||||
|                  | ||||
|                 jobs.append({ | ||||
|                     "id": job[0], | ||||
|                     "model": model.split(".")[0] if model != "Unknown" else model, | ||||
|                     "prompt": prompt | ||||
|                 }) | ||||
|         return jobs | ||||
|     except Exception as e: | ||||
|         logging.error(f"Error fetching queue details: {e}") | ||||
|         return [] | ||||
|     try: | ||||
|         response = requests.get(url) | ||||
|         response.raise_for_status() | ||||
|         data = response.json() | ||||
|         pending = len(data.get("queue_pending", [])) | ||||
|         running = len(data.get("queue_running", [])) | ||||
|         return pending + running | ||||
|     except Exception as e: | ||||
|         logging.error(f"Error fetching queue count: {e}") | ||||
|         return 0 | ||||
|  | ||||
| @ -113,11 +113,28 @@ def get_current_version(): | ||||
|         return "unknown" | ||||
| 
 | ||||
| def load_models_from_config(): | ||||
|     flux_models = load_config()["comfyui:flux"]["models"].split(",") | ||||
|     sdxl_models = load_config()["comfyui"]["models"].split(",") | ||||
|     config = load_config() | ||||
|      | ||||
|     # Only load FLUX models if FLUX feature is enabled | ||||
|     use_flux = config["comfyui"].get("flux", "False").lower() == "true" | ||||
|     if use_flux and "comfyui:flux" in config and "models" in config["comfyui:flux"]: | ||||
|         flux_models = config["comfyui:flux"]["models"].split(",") | ||||
|     else: | ||||
|         flux_models = [] | ||||
|      | ||||
|     sdxl_models = config["comfyui"]["models"].split(",") | ||||
|      | ||||
|     # Only load Qwen models if Qwen feature is enabled | ||||
|     use_qwen = config["comfyui"].get("qwen", "False").lower() == "true" | ||||
|     if use_qwen and "comfyui:qwen" in config and "models" in config["comfyui:qwen"]: | ||||
|         qwen_models = config["comfyui:qwen"]["models"].split(",") | ||||
|     else: | ||||
|         qwen_models = [] | ||||
|      | ||||
|     sorted_flux_models = sorted(flux_models, key=str.lower) | ||||
|     sorted_sdxl_models = sorted(sdxl_models, key=str.lower) | ||||
|     return sorted_sdxl_models, sorted_flux_models | ||||
|     sorted_qwen_models = sorted(qwen_models, key=str.lower) | ||||
|     return sorted_sdxl_models, sorted_flux_models, sorted_qwen_models | ||||
| 
 | ||||
| 
 | ||||
| def load_topics_from_config(): | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| from flask import Blueprint, request, render_template, redirect, url_for, session | ||||
| import threading | ||||
| from libs.comfyui import create_image, select_model, get_available_models | ||||
| from libs.comfyui import create_image, select_model, get_available_models, get_queue_count | ||||
| from libs.openwebui import create_prompt_on_openwebui | ||||
| from libs.generic import load_models_from_config, load_topics_from_config, load_openrouter_models_from_config, load_openwebui_models_from_config, create_prompt_with_random_model | ||||
| import os | ||||
| @ -35,17 +35,20 @@ def create(): | ||||
|         threading.Thread(target=lambda: create_image(prompt, model)).start() | ||||
|         return redirect(url_for("create_routes.image_queued", prompt=prompt, model=model.split(".")[0])) | ||||
| 
 | ||||
|     # Load all models (SDXL and FLUX only) | ||||
|     sdxl_models, flux_models = load_models_from_config() | ||||
|     # Load all models (SDXL, FLUX, and Qwen) | ||||
|     sdxl_models, flux_models, qwen_models = load_models_from_config() | ||||
|     openwebui_models = load_openwebui_models_from_config() | ||||
|     openrouter_models = load_openrouter_models_from_config() | ||||
|      | ||||
|     queue_count = get_queue_count() | ||||
|     return render_template("create_image.html", | ||||
|                          sdxl_models=sdxl_models, | ||||
|                          sdxx_models=sdxl_models, | ||||
|                          flux_models=flux_models, | ||||
|                          qwen_models=qwen_models, | ||||
|                          openwebui_models=openwebui_models, | ||||
|                          openrouter_models=openrouter_models, | ||||
|                          topics=load_topics_from_config()) | ||||
|                          topics=load_topics_from_config(), | ||||
|                          queue_count=queue_count) | ||||
| 
 | ||||
| @bp.route("/image_queued") | ||||
| def image_queued(): | ||||
| @ -62,17 +65,20 @@ def create_image_page(): | ||||
|     if user_config["frame"]["create_requires_auth"] == "True" and not session.get("authenticated"): | ||||
|         return redirect(url_for("auth_routes.login", next=request.path)) | ||||
|      | ||||
|     # Load all models (SDXL and FLUX only) | ||||
|     sdxl_models, flux_models = load_models_from_config() | ||||
|     # Load all models (SDXL, FLUX, and Qwen) | ||||
|     sdxl_models, flux_models, qwen_models = load_models_from_config() | ||||
|     openwebui_models = load_openwebui_models_from_config() | ||||
|     openrouter_models = load_openrouter_models_from_config() | ||||
|      | ||||
|     queue_count = get_queue_count() | ||||
|     return render_template("create_image.html", | ||||
|                          sdxl_models=sdxl_models, | ||||
|                          flux_models=flux_models, | ||||
|                          qwen_models=qwen_models, | ||||
|                          openwebui_models=openwebui_models, | ||||
|                          openrouter_models=openrouter_models, | ||||
|                          topics=load_topics_from_config()) | ||||
|                          topics=load_topics_from_config(), | ||||
|                          queue_count=queue_count) | ||||
| 
 | ||||
| 
 | ||||
| def init_app(config): | ||||
|  | ||||
| @ -1,8 +1,12 @@ | ||||
| from flask import Blueprint | ||||
| from libs.comfyui import cancel_current_job | ||||
| from flask import Blueprint, jsonify | ||||
| from libs.comfyui import cancel_current_job, get_queue_details | ||||
| 
 | ||||
| bp = Blueprint("job_routes", __name__) | ||||
| 
 | ||||
| @bp.route("/cancel", methods=["GET"]) | ||||
| def cancel_job(): | ||||
|     return cancel_current_job() | ||||
| 
 | ||||
| @bp.route("/api/queue", methods=["GET"]) | ||||
| def api_queue(): | ||||
|     return jsonify(get_queue_details()) | ||||
|  | ||||
| @ -73,6 +73,7 @@ | ||||
|         background: #555; | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     #spinner-overlay { | ||||
|         position: fixed; | ||||
|         inset: 0; | ||||
| @ -131,10 +132,66 @@ | ||||
|             height: 150px; | ||||
|         } | ||||
|     } | ||||
|     .queue-dropdown { | ||||
|         position: absolute; | ||||
|         top: 100%; | ||||
|         right: 0; | ||||
|         background: #222; | ||||
|         border: 1px solid #444; | ||||
|         border-radius: 5px; | ||||
|         padding: 10px; | ||||
|         z-index: 1001; | ||||
|         display: none; | ||||
|         max-height: 300px; | ||||
|         overflow-y: auto; | ||||
|         width: 400px; | ||||
|     } | ||||
| 
 | ||||
|     .queue-item { | ||||
|         margin-bottom: 5px; | ||||
|         padding: 5px; | ||||
|         border-bottom: 1px solid #333; | ||||
|     } | ||||
| 
 | ||||
|     .queue-item:last-child { | ||||
|         border-bottom: none; | ||||
|     } | ||||
| 
 | ||||
|     .queue-item .prompt { | ||||
|         font-size: 0.9em; | ||||
|         color: #aaa; | ||||
|         white-space: normal; | ||||
|         word-wrap: break-word; | ||||
|         position: relative; | ||||
|         cursor: pointer; | ||||
|     } | ||||
|      | ||||
|     .queue-item .prompt:hover::after { | ||||
|         content: "Model: " attr(data-model); | ||||
|         position: absolute; | ||||
|         bottom: 100%; | ||||
|         left: 0; | ||||
|         background: #333; | ||||
|         color: #00aaff; | ||||
|         padding: 4px 8px; | ||||
|         border-radius: 4px; | ||||
|         font-size: 0.8em; | ||||
|         white-space: nowrap; | ||||
|         z-index: 1002; | ||||
|         box-shadow: 0 2px 4px rgba(0,0,0,0.3); | ||||
|     } | ||||
| </style> | ||||
| {% endblock %} | ||||
| 
 | ||||
| {% block content %} | ||||
| <div class="queue-container" style="position: fixed; top: 20px; right: 20px; z-index: 1000;"> | ||||
|     <button id="queue-btn" style="background: #333; color: white; border: none; padding: 5px 10px; border-radius: 5px; cursor: pointer;"> | ||||
|         Queue: <span id="queue-count">{{ queue_count | default(0) }}</span> | ||||
|     </button> | ||||
|     <div id="queue-dropdown" class="queue-dropdown"> | ||||
|         <!-- Queue items will be populated here --> | ||||
|     </div> | ||||
| </div> | ||||
| <h1 style="margin-bottom: 20px;">Create An Image</h1> | ||||
| 
 | ||||
| <textarea id="prompt-box" placeholder="Enter your custom prompt here..."></textarea> | ||||
| @ -157,6 +214,13 @@ | ||||
|                 {% endfor %} | ||||
|             </optgroup> | ||||
|             {% endif %} | ||||
|             {% if qwen_models %} | ||||
|             <optgroup label="Qwen"> | ||||
|                 {% for m in qwen_models %} | ||||
|                 <option value="{{ m }}">{{ m.rsplit('.', 1)[0] if '.' in m else m }}</option> | ||||
|                 {% endfor %} | ||||
|             </optgroup> | ||||
|             {% endif %} | ||||
|             {% if sdxl_models %} | ||||
|             <optgroup label="SDXL"> | ||||
|                 {% for m in sdxl_models %} | ||||
| @ -262,5 +326,59 @@ | ||||
|                 alert("Error requesting random prompt: " + error); | ||||
|             }); | ||||
|     } | ||||
|     document.addEventListener('DOMContentLoaded', function() { | ||||
|         const queueBtn = document.getElementById('queue-btn'); | ||||
|         const queueDropdown = document.getElementById('queue-dropdown'); | ||||
|         const queueCountSpan = document.getElementById('queue-count'); | ||||
| 
 | ||||
|         // Toggle dropdown visibility | ||||
|         queueBtn.addEventListener('click', function(e) { | ||||
|             e.stopPropagation(); | ||||
|             if (queueDropdown.style.display === 'block') { | ||||
|                 queueDropdown.style.display = 'none'; | ||||
|             } else { | ||||
|                 fetchQueueDetails(); | ||||
|                 queueDropdown.style.display = 'block'; | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         // Close dropdown when clicking outside | ||||
|         document.addEventListener('click', function() { | ||||
|             queueDropdown.style.display = 'none'; | ||||
|         }); | ||||
| 
 | ||||
|         // Prevent dropdown from closing when clicking inside it | ||||
|         queueDropdown.addEventListener('click', function(e) { | ||||
|             e.stopPropagation(); | ||||
|         }); | ||||
| 
 | ||||
|         function fetchQueueDetails() { | ||||
|             fetch('/api/queue') | ||||
|                 .then(response => response.json()) | ||||
|                 .then(jobs => { | ||||
|                     queueCountSpan.textContent = jobs.length; | ||||
|                     const container = queueDropdown; | ||||
|                     container.innerHTML = ''; | ||||
| 
 | ||||
|                     if (jobs.length === 0) { | ||||
|                         container.innerHTML = '<div class="queue-item">No jobs in queue</div>'; | ||||
|                         return; | ||||
|                     } | ||||
| 
 | ||||
|                     jobs.forEach(job => { | ||||
|                         const item = document.createElement('div'); | ||||
|                         item.className = 'queue-item'; | ||||
|                         item.innerHTML = ` | ||||
|                             <div class="prompt" data-model="${job.model}">${job.prompt}</div> | ||||
|                         `; | ||||
|                         container.appendChild(item); | ||||
|                     }); | ||||
|                 }) | ||||
|                 .catch(error => { | ||||
|                     console.error('Error fetching queue:', error); | ||||
|                     queueDropdown.innerHTML = '<div class="queue-item">Error loading queue</div>'; | ||||
|                 }); | ||||
|         } | ||||
|     }); | ||||
| </script> | ||||
| {% endblock %} | ||||
							
								
								
									
										147
									
								
								workflow_qwen.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										147
									
								
								workflow_qwen.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,147 @@ | ||||
| { | ||||
|   "93": { | ||||
|     "inputs": { | ||||
|       "text": "jpeg compression", | ||||
|       "speak_and_recognation": { | ||||
|         "__value__": [ | ||||
|           false, | ||||
|           true | ||||
|         ] | ||||
|       }, | ||||
|       "clip": [ | ||||
|         "126", | ||||
|         0 | ||||
|       ] | ||||
|     }, | ||||
|     "class_type": "CLIPTextEncode", | ||||
|     "_meta": { | ||||
|       "title": "CLIP Text Encode (Prompt)" | ||||
|     } | ||||
|   }, | ||||
|   "95": { | ||||
|     "inputs": { | ||||
|       "seed": 22, | ||||
|       "steps": 10, | ||||
|       "cfg": 4.5, | ||||
|       "sampler_name": "euler", | ||||
|       "scheduler": "normal", | ||||
|       "denoise": 1, | ||||
|       "model": [ | ||||
|         "127", | ||||
|         0 | ||||
|       ], | ||||
|       "positive": [ | ||||
|         "100", | ||||
|         0 | ||||
|       ], | ||||
|       "negative": [ | ||||
|         "93", | ||||
|         0 | ||||
|       ], | ||||
|       "latent_image": [ | ||||
|         "97", | ||||
|         0 | ||||
|       ] | ||||
|     }, | ||||
|     "class_type": "KSampler", | ||||
|     "_meta": { | ||||
|       "title": "KSampler" | ||||
|     } | ||||
|   }, | ||||
|   "97": { | ||||
|     "inputs": { | ||||
|       "width": 1280, | ||||
|       "height": 768, | ||||
|       "length": 1, | ||||
|       "batch_size": 1 | ||||
|     }, | ||||
|     "class_type": "EmptyHunyuanLatentVideo", | ||||
|     "_meta": { | ||||
|       "title": "EmptyHunyuanLatentVideo" | ||||
|     } | ||||
|   }, | ||||
|   "98": { | ||||
|     "inputs": { | ||||
|       "samples": [ | ||||
|         "95", | ||||
|         0 | ||||
|       ], | ||||
|       "vae": [ | ||||
|         "128", | ||||
|         0 | ||||
|       ] | ||||
|     }, | ||||
|     "class_type": "VAEDecode", | ||||
|     "_meta": { | ||||
|       "title": "VAE Decode" | ||||
|     } | ||||
|   }, | ||||
|   "100": { | ||||
|     "inputs": { | ||||
|       "text": "Terminator riding a push bike", | ||||
|       "speak_and_recognation": { | ||||
|         "__value__": [ | ||||
|           false, | ||||
|           true | ||||
|         ] | ||||
|       }, | ||||
|       "clip": [ | ||||
|         "126", | ||||
|         0 | ||||
|       ] | ||||
|     }, | ||||
|     "class_type": "CLIPTextEncode", | ||||
|     "_meta": { | ||||
|       "title": "CLIP Text Encode (Prompt)" | ||||
|     } | ||||
|   }, | ||||
|   "102": { | ||||
|     "inputs": { | ||||
|       "images": [ | ||||
|         "98", | ||||
|         0 | ||||
|       ] | ||||
|     }, | ||||
|     "class_type": "PreviewImage", | ||||
|     "_meta": { | ||||
|       "title": "Preview Image" | ||||
|     } | ||||
|   }, | ||||
|   "126": { | ||||
|     "inputs": { | ||||
|       "clip_name": "Qwen2.5-VL-7B-Instruct-Q3_K_M.gguf", | ||||
|       "type": "qwen_image", | ||||
|       "device": "cuda:1", | ||||
|       "virtual_vram_gb": 6, | ||||
|       "use_other_vram": true, | ||||
|       "expert_mode_allocations": "" | ||||
|     }, | ||||
|     "class_type": "CLIPLoaderGGUFDisTorchMultiGPU", | ||||
|     "_meta": { | ||||
|       "title": "CLIPLoaderGGUFDisTorchMultiGPU" | ||||
|     } | ||||
|   }, | ||||
|   "127": { | ||||
|     "inputs": { | ||||
|       "unet_name": "qwen-image-Q2_K.gguf", | ||||
|       "device": "cuda:0", | ||||
|       "virtual_vram_gb": 6, | ||||
|       "use_other_vram": true, | ||||
|       "expert_mode_allocations": "" | ||||
|     }, | ||||
|     "class_type": "UnetLoaderGGUFDisTorchMultiGPU", | ||||
|     "_meta": { | ||||
|       "title": "UnetLoaderGGUFDisTorchMultiGPU" | ||||
|     } | ||||
|   }, | ||||
|   "128": { | ||||
|     "inputs": { | ||||
|       "vae_name": "qwen_image_vae.safetensors", | ||||
|       "device": "cuda:1" | ||||
|     }, | ||||
|     "class_type": "VAELoaderMultiGPU", | ||||
|     "_meta": { | ||||
|       "title": "VAELoaderMultiGPU" | ||||
|     } | ||||
|   } | ||||
| } | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user