diff --git a/ComfyUI/comfy_api_nodes/apis/request_logger.py b/ComfyUI/comfy_api_nodes/apis/request_logger.py
index 93517ede..cf9cc798 100644
--- a/ComfyUI/comfy_api_nodes/apis/request_logger.py
+++ b/ComfyUI/comfy_api_nodes/apis/request_logger.py
@@ -3,7 +3,7 @@
import json
import logging
import folder_paths
-
+from typing import Optional, Any
# Get the logger instance
logger = logging.getLogger(__name__)
@@ -40,13 +40,13 @@ def log_request_response(
operation_id: str,
request_method: str,
request_url: str,
- request_headers: dict | None = None,
- request_params: dict | None = None,
- request_data: any = None,
- response_status_code: int | None = None,
- response_headers: dict | None = None,
- response_content: any = None,
- error_message: str | None = None
+ request_headers: Optional[dict] = None,
+ request_params: Optional[dict] = None,
+ request_data: Any = None,
+ response_status_code: Optional[int] = None,
+ response_headers: Optional[dict] = None,
+ response_content: Any = None,
+ error_message: Optional[str] = None
):
"""
Logs API request and response details to a file in the temp/api_logs directory.
diff --git a/ComfyUI/comfy_api_nodes/nodes_gemini.py b/ComfyUI/comfy_api_nodes/nodes_gemini.py
index 2690b51b..ae7b0484 100644
--- a/ComfyUI/comfy_api_nodes/nodes_gemini.py
+++ b/ComfyUI/comfy_api_nodes/nodes_gemini.py
@@ -312,51 +312,41 @@ def api_call(
unique_id: Optional[str] = None,
**kwargs,
) -> tuple[str]:
- try:
- # Validate inputs
- validate_string(prompt, strip_whitespace=False)
-
- # Create parts list with text prompt as the first part
- parts: list[GeminiPart] = [self.create_text_part(prompt)]
-
- # Add other modal parts
- if images is not None:
- image_parts = self.create_image_parts(images)
- parts.extend(image_parts)
- if audio is not None:
- parts.extend(self.create_audio_parts(audio))
- if video is not None:
- parts.extend(self.create_video_parts(video))
- if files is not None:
- parts.extend(files)
-
- # Create response
- response = SynchronousOperation(
- endpoint=get_gemini_endpoint(model),
- request=GeminiGenerateContentRequest(
- contents=[
- GeminiContent(
- role="user",
- parts=parts,
- )
- ]
- ),
- auth_kwargs=kwargs,
- ).execute()
-
- # Get result output
- output_text = self.get_text_from_response(response)
- if unique_id and output_text:
- PromptServer.instance.send_progress_text(output_text, node_id=unique_id)
-
- except Exception as e:
- # Handle API errors, network issues, and validation failures gracefully
- error_message = f"Gemini API Error: {str(e)}"
- print(f"[GeminiNode] {error_message}") # Log full error for debugging
- sanitized_message = "An error occurred while processing your request. Please try again later."
- if unique_id:
- PromptServer.instance.send_progress_text(sanitized_message, node_id=unique_id)
- output_text = sanitized_message
+ # Validate inputs
+ validate_string(prompt, strip_whitespace=False)
+
+ # Create parts list with text prompt as the first part
+ parts: list[GeminiPart] = [self.create_text_part(prompt)]
+
+ # Add other modal parts
+ if images is not None:
+ image_parts = self.create_image_parts(images)
+ parts.extend(image_parts)
+ if audio is not None:
+ parts.extend(self.create_audio_parts(audio))
+ if video is not None:
+ parts.extend(self.create_video_parts(video))
+ if files is not None:
+ parts.extend(files)
+
+ # Create response
+ response = SynchronousOperation(
+ endpoint=get_gemini_endpoint(model),
+ request=GeminiGenerateContentRequest(
+ contents=[
+ GeminiContent(
+ role="user",
+ parts=parts,
+ )
+ ]
+ ),
+ auth_kwargs=kwargs,
+ ).execute()
+
+ # Get result output
+ output_text = self.get_text_from_response(response)
+ if unique_id and output_text:
+ PromptServer.instance.send_progress_text(output_text, node_id=unique_id)
return (output_text or "Empty response from Gemini model...",)
@@ -439,17 +429,10 @@ def prepare_files(
"""
Loads and formats input files for Gemini API.
"""
- try:
- file_path = folder_paths.get_annotated_filepath(file)
- input_file_content = self.create_file_part(file_path)
- files = [input_file_content] + GEMINI_INPUT_FILES
- return (files,)
- except Exception as e:
- # Handle file processing errors gracefully
- error_message = f"File processing error: {str(e)}"
- print(f"[GeminiInputFiles] {error_message}")
- # Return empty list on error
- return ([],)
+ file_path = folder_paths.get_annotated_filepath(file)
+ input_file_content = self.create_file_part(file_path)
+ files = [input_file_content] + GEMINI_INPUT_FILES
+ return (files,)
NODE_CLASS_MAPPINGS = {
diff --git a/ComfyUI/comfy_api_nodes/nodes_stability.py b/ComfyUI/comfy_api_nodes/nodes_stability.py
index 447c4df7..02e42167 100644
--- a/ComfyUI/comfy_api_nodes/nodes_stability.py
+++ b/ComfyUI/comfy_api_nodes/nodes_stability.py
@@ -30,8 +30,6 @@
import base64
from io import BytesIO
from enum import Enum
-import requests
-import os
class StabilityPollStatus(str, Enum):
@@ -69,24 +67,24 @@ def INPUT_TYPES(s):
"multiline": True,
"default": "",
"tooltip": "What you wish to see in the output image. A strong, descriptive prompt that clearly defines" +
- "What you wish to see in the output image. A strong, descriptive prompt that clearly defines" +
- "elements, colors, and subjects will lead to better results. " +
- "To control the weight of a given word use the format `(word:weight)`," +
- "where `word` is the word you'd like to control the weight of and `weight`" +
- "is a value between 0 and 1. For example: `The sky was a crisp (blue:0.3) and (green:0.8)`" +
- "would convey a sky that was blue and green, but more green than blue."
+ "What you wish to see in the output image. A strong, descriptive prompt that clearly defines" +
+ "elements, colors, and subjects will lead to better results. " +
+ "To control the weight of a given word use the format `(word:weight)`," +
+ "where `word` is the word you'd like to control the weight of and `weight`" +
+ "is a value between 0 and 1. For example: `The sky was a crisp (blue:0.3) and (green:0.8)`" +
+ "would convey a sky that was blue and green, but more green than blue."
},
),
"aspect_ratio": ([x.value for x in StabilityAspectRatio],
- {
- "default": StabilityAspectRatio.ratio_1_1,
- "tooltip": "Aspect ratio of generated image.",
- },
+ {
+ "default": StabilityAspectRatio.ratio_1_1,
+ "tooltip": "Aspect ratio of generated image.",
+ },
),
"style_preset": (get_stability_style_presets(),
- {
- "tooltip": "Optional desired style of generated image.",
- },
+ {
+ "tooltip": "Optional desired style of generated image.",
+ },
),
"seed": (
IO.INT,
@@ -121,30 +119,19 @@ def INPUT_TYPES(s):
),
},
"hidden": {
- # Changed from auth_token/comfy_api_key
- "stability_api_key": "STABILITY_API_KEY",
+ "auth_token": "AUTH_TOKEN_COMFY_ORG",
+ "comfy_api_key": "API_KEY_COMFY_ORG",
},
}
def api_call(self, prompt: str, aspect_ratio: str, style_preset: str, seed: int,
- negative_prompt: str = None, image: torch.Tensor = None, image_denoise: float = None,
+ negative_prompt: str=None, image: torch.Tensor = None, image_denoise: float=None,
**kwargs):
validate_string(prompt, strip_whitespace=False)
-
- # Get Stability API key from kwargs or environment
- stability_api_key = kwargs.get(
- 'stability_api_key') or os.environ.get('STABILITY_API_KEY')
- if not stability_api_key:
- raise Exception(
- "STABILITY_API_KEY not found. Please set it in your environment or .env file.")
-
- # Prepare image binary if image present
+ # prepare image binary if image present
image_binary = None
if image is not None:
- image_binary = tensor_to_bytesio(
- image, total_pixels=1504*1504).read()
- # Convert image to base64 for JSON payload
- image_base64 = base64.b64encode(image_binary).decode('utf-8')
+ image_binary = tensor_to_bytesio(image, total_pixels=1504*1504).read()
else:
image_denoise = None
@@ -153,72 +140,38 @@ def api_call(self, prompt: str, aspect_ratio: str, style_preset: str, seed: int,
if style_preset == "None":
style_preset = None
- # Prepare the request data for direct Stability AI API (JSON format)
- request_data = {
- "text_prompts": [
- {
- "text": prompt,
- "weight": 1.0
- }
- ],
- "aspect_ratio": aspect_ratio,
- "seed": seed,
- }
-
- if negative_prompt:
- request_data["text_prompts"].append({
- "text": negative_prompt,
- "weight": -1.0
- })
-
- if style_preset:
- request_data["style_preset"] = style_preset
-
- if image_denoise:
- request_data["image_strength"] = image_denoise
-
- # Add init_image if present (for image-to-image)
- if image_binary:
- request_data["init_image"] = image_base64
-
- # Make direct API call to Stability AI
- headers = {
- "Authorization": f"Bearer {stability_api_key}",
- "Accept": "application/json",
- "Content-Type": "application/json"
+ files = {
+ "image": image_binary
}
- try:
- print(
- f"[DEBUG] Making request to Stability AI API with data: {request_data}")
- response = requests.post(
- "https://api.stability.ai/v1/generation/stable-diffusion-xl-1024-v1-0/text-to-image",
- headers=headers,
- json=request_data,
- timeout=300
- )
-
- if response.status_code != 200:
- raise Exception(
- f"Stability AI API error: {response.status_code} - {response.text}")
-
- response_data = response.json()
-
- if "artifacts" not in response_data or not response_data["artifacts"]:
- raise Exception("No image generated in response")
+ operation = SynchronousOperation(
+ endpoint=ApiEndpoint(
+ path="/proxy/stability/v2beta/stable-image/generate/ultra",
+ method=HttpMethod.POST,
+ request_model=StabilityStableUltraRequest,
+ response_model=StabilityStableUltraResponse,
+ ),
+ request=StabilityStableUltraRequest(
+ prompt=prompt,
+ negative_prompt=negative_prompt,
+ aspect_ratio=aspect_ratio,
+ seed=seed,
+ strength=image_denoise,
+ style_preset=style_preset,
+ ),
+ files=files,
+ content_type="multipart/form-data",
+ auth_kwargs=kwargs,
+ )
+ response_api = operation.execute()
- # Get the first image from the response
- image_data = base64.b64decode(
- response_data["artifacts"][0]["base64"])
- returned_image = bytesio_to_image_tensor(BytesIO(image_data))
+ if response_api.finish_reason != "SUCCESS":
+ raise Exception(f"Stable Image Ultra generation failed: {response_api.finish_reason}.")
- return (returned_image,)
+ image_data = base64.b64decode(response_api.image)
+ returned_image = bytesio_to_image_tensor(BytesIO(image_data))
- except requests.exceptions.RequestException as e:
- raise Exception(
- f"Network error calling Stability AI API: {str(e)}")
- except Exception as e:
- raise Exception(f"Error generating image: {str(e)}")
+ return (returned_image,)
class StabilityStableImageSD_3_5Node:
@@ -246,15 +199,15 @@ def INPUT_TYPES(s):
),
"model": ([x.value for x in Stability_SD3_5_Model],),
"aspect_ratio": ([x.value for x in StabilityAspectRatio],
- {
- "default": StabilityAspectRatio.ratio_1_1,
- "tooltip": "Aspect ratio of generated image.",
- },
+ {
+ "default": StabilityAspectRatio.ratio_1_1,
+ "tooltip": "Aspect ratio of generated image.",
+ },
),
"style_preset": (get_stability_style_presets(),
- {
- "tooltip": "Optional desired style of generated image.",
- },
+ {
+ "tooltip": "Optional desired style of generated image.",
+ },
),
"cfg_scale": (
IO.FLOAT,
@@ -305,15 +258,14 @@ def INPUT_TYPES(s):
}
def api_call(self, model: str, prompt: str, aspect_ratio: str, style_preset: str, seed: int, cfg_scale: float,
- negative_prompt: str = None, image: torch.Tensor = None, image_denoise: float = None,
+ negative_prompt: str=None, image: torch.Tensor = None, image_denoise: float=None,
**kwargs):
validate_string(prompt, strip_whitespace=False)
# prepare image binary if image present
image_binary = None
mode = Stability_SD3_5_GenerationMode.text_to_image
if image is not None:
- image_binary = tensor_to_bytesio(
- image, total_pixels=1504*1504).read()
+ image_binary = tensor_to_bytesio(image, total_pixels=1504*1504).read()
mode = Stability_SD3_5_GenerationMode.image_to_image
aspect_ratio = None
else:
@@ -353,8 +305,7 @@ def api_call(self, model: str, prompt: str, aspect_ratio: str, style_preset: str
response_api = operation.execute()
if response_api.finish_reason != "SUCCESS":
- raise Exception(
- f"Stable Diffusion 3.5 Image generation failed: {response_api.finish_reason}.")
+ raise Exception(f"Stable Diffusion 3.5 Image generation failed: {response_api.finish_reason}.")
image_data = base64.b64decode(response_api.image)
returned_image = bytesio_to_image_tensor(BytesIO(image_data))
@@ -423,7 +374,7 @@ def INPUT_TYPES(s):
},
}
- def api_call(self, image: torch.Tensor, prompt: str, creativity: float, seed: int, negative_prompt: str = None,
+ def api_call(self, image: torch.Tensor, prompt: str, creativity: float, seed: int, negative_prompt: str=None,
**kwargs):
validate_string(prompt, strip_whitespace=False)
image_binary = tensor_to_bytesio(image, total_pixels=1024*1024).read()
@@ -445,7 +396,7 @@ def api_call(self, image: torch.Tensor, prompt: str, creativity: float, seed: in
request=StabilityUpscaleConservativeRequest(
prompt=prompt,
negative_prompt=negative_prompt,
- creativity=round(creativity, 2),
+ creativity=round(creativity,2),
seed=seed,
),
files=files,
@@ -455,8 +406,7 @@ def api_call(self, image: torch.Tensor, prompt: str, creativity: float, seed: in
response_api = operation.execute()
if response_api.finish_reason != "SUCCESS":
- raise Exception(
- f"Stability Upscale Conservative generation failed: {response_api.finish_reason}.")
+ raise Exception(f"Stability Upscale Conservative generation failed: {response_api.finish_reason}.")
image_data = base64.b64decode(response_api.image)
returned_image = bytesio_to_image_tensor(BytesIO(image_data))
@@ -499,9 +449,9 @@ def INPUT_TYPES(s):
},
),
"style_preset": (get_stability_style_presets(),
- {
- "tooltip": "Optional desired style of generated image.",
- },
+ {
+ "tooltip": "Optional desired style of generated image.",
+ },
),
"seed": (
IO.INT,
@@ -530,7 +480,7 @@ def INPUT_TYPES(s):
},
}
- def api_call(self, image: torch.Tensor, prompt: str, creativity: float, style_preset: str, seed: int, negative_prompt: str = None,
+ def api_call(self, image: torch.Tensor, prompt: str, creativity: float, style_preset: str, seed: int, negative_prompt: str=None,
**kwargs):
validate_string(prompt, strip_whitespace=False)
image_binary = tensor_to_bytesio(image, total_pixels=1024*1024).read()
@@ -554,7 +504,7 @@ def api_call(self, image: torch.Tensor, prompt: str, creativity: float, style_pr
request=StabilityUpscaleCreativeRequest(
prompt=prompt,
negative_prompt=negative_prompt,
- creativity=round(creativity, 2),
+ creativity=round(creativity,2),
style_preset=style_preset,
seed=seed,
),
@@ -580,8 +530,7 @@ def api_call(self, image: torch.Tensor, prompt: str, creativity: float, style_pr
response_poll: StabilityResultsGetResponse = operation.execute()
if response_poll.finish_reason != "SUCCESS":
- raise Exception(
- f"Stability Upscale Creative generation failed: {response_poll.finish_reason}.")
+ raise Exception(f"Stability Upscale Creative generation failed: {response_poll.finish_reason}.")
image_data = base64.b64decode(response_poll.result)
returned_image = bytesio_to_image_tensor(BytesIO(image_data))
@@ -637,8 +586,7 @@ def api_call(self, image: torch.Tensor,
response_api = operation.execute()
if response_api.finish_reason != "SUCCESS":
- raise Exception(
- f"Stability Upscale Fast failed: {response_api.finish_reason}.")
+ raise Exception(f"Stability Upscale Fast failed: {response_api.finish_reason}.")
image_data = base64.b64decode(response_api.image)
returned_image = bytesio_to_image_tensor(BytesIO(image_data))
diff --git a/ComfyUI/user/default/comfy.settings.json b/ComfyUI/user/default/comfy.settings.json
index 512a22bd..438b3dad 100644
--- a/ComfyUI/user/default/comfy.settings.json
+++ b/ComfyUI/user/default/comfy.settings.json
@@ -1,6 +1,3 @@
{
- "Comfy.TutorialCompleted": true,
- "Comfy.Queue.ImageFit": "cover",
- "Comfy.Release.Version": "0.3.48",
- "Comfy.Release.Timestamp": 1754374644423
+ "Comfy.TutorialCompleted": true
}
\ No newline at end of file
diff --git a/GEMINI_INTEGRATION_DEMO.md b/GEMINI_INTEGRATION_DEMO.md
deleted file mode 100644
index b1633c5d..00000000
--- a/GEMINI_INTEGRATION_DEMO.md
+++ /dev/null
@@ -1,188 +0,0 @@
-# ๐ฎ **Gemini Integration for DreamLayer AI**
-
-## ๐ฏ **Overview**
-
-This PR completes the Gemini integration for DreamLayer AI, adding Google's powerful multimodal AI capabilities to the platform. Users can now leverage Gemini Pro Vision for text generation, image analysis, document processing, and creative workflows.
-
-## โจ **What's New**
-
-### ๐ง **Backend Integration**
-- โ
**API Key Support**: Added `GEMINI_API_KEY` to environment configuration
-- โ
**Model Mapping**: Gemini Pro and Gemini Pro Vision available in model dropdown
-- โ
**Workflow Templates**: Created both simple and advanced workflow examples
-
-### ๐จ **Frontend Integration**
-- โ
**API Key Configuration**: Added Gemini to the API key management UI
-- โ
**Model Selection**: Gemini models appear in the model dropdown when API key is configured
-- โ
**User Experience**: Seamless integration with existing DreamLayer interface
-
-### ๐ **ComfyUI Node**
-- โ
**Multimodal Support**: Text + Image + Audio + Video + Files input
-- โ
**Node Chaining**: Text output can be consumed by downstream nodes
-- โ
**Advanced Features**: Deterministic seeding, multiple model options
-
-## ๐ **Node Chaining Demonstration**
-
-### **Simple Text Workflow**
-```json
-LoadImage โ GeminiNode โ (Text Output)
-```
-
-### **Advanced Multimodal Chain**
-```json
-LoadImage โ GeminiNode(analysis) โ GeminiNode(prompt_generation) โ CLIPTextEncode โ KSampler โ SaveImage
-```
-
-**Real-world example:**
-1. **Load Image** - User uploads photo
-2. **Gemini Analysis** - "Analyze this image's artistic elements"
-3. **Prompt Generation** - "Create an enhanced AI art prompt based on this analysis"
-4. **CLIP Encoding** - Convert Gemini-generated prompt to tokens
-5. **Image Generation** - Generate enhanced version using Stable Diffusion
-6. **Save Result** - Output the improved image
-
-## ๐งช **Test Case: Prompt โ Gemini โ Downstream Node**
-
-### **Test Workflow**
-```json
-{
- "1": {
- "class_type": "GeminiNode",
- "inputs": {
- "prompt": "Create a detailed prompt for generating a cyberpunk cityscape artwork",
- "model": "gemini-2.5-pro-preview-05-06"
- }
- },
- "2": {
- "class_type": "CLIPTextEncode",
- "inputs": {
- "clip": ["3", 1],
- "text": ["1", 0] // <-- Gemini text output feeds into CLIP
- }
- }
-}
-```
-
-### **Expected Behavior**
-1. User provides high-level request: "Create a detailed prompt for cyberpunk cityscape"
-2. Gemini generates: "A futuristic cyberpunk cityscape at night, neon-lit skyscrapers, flying cars, rain-soaked streets, purple and blue color palette, highly detailed, digital art, masterpiece"
-3. CLIP encodes this Gemini-generated prompt
-4. Stable Diffusion uses the encoding for image generation
-
-## ๐ฏ **Key Features Demonstrated**
-
-### **โ
Multimodal AI Integration**
-- Text generation and analysis
-- Image understanding and description
-- Creative prompt optimization
-- Professional workflow automation
-
-### **โ
Seamless Node Chaining**
-- Gemini output directly feeds downstream nodes
-- No manual copy/paste required
-- Automated creative pipelines
-- AI-assisted content creation
-
-### **โ
Production-Ready Implementation**
-- Error handling and validation
-- Comprehensive API key management
-- User-friendly interface integration
-- Professional documentation
-
-## ๐ **Setup Instructions**
-
-### **1. Get Gemini API Key**
-```bash
-# Visit: https://ai.google.dev/gemini-api/docs/api-key
-# Create account and generate API key
-```
-
-### **2. Configure DreamLayer**
-1. Start DreamLayer application
-2. Go to Model Selector โ "Add API KEY"
-3. Enter your Gemini API key in the "Gemini - Google AI" field
-4. Click Submit
-
-### **3. Test the Integration**
-1. Go to txt2img tab
-2. Select "Gemini Pro Vision" from model dropdown
-3. Enter prompt: "Analyze this image and suggest improvements"
-4. Upload an image (if using multimodal workflow)
-5. Generate and see Gemini's analysis
-
-## ๐จ **Usage Examples**
-
-### **Creative Writing**
-```
-Prompt: "Write a short story about AI and humans collaborating"
-โ Gemini generates creative, engaging narrative
-```
-
-### **Image Analysis**
-```
-Input: [User uploads artwork]
-Prompt: "Analyze this image's composition, style, and suggest improvements"
-โ Gemini provides detailed artistic critique
-```
-
-### **Code Generation**
-```
-Prompt: "Create a Python function that processes image metadata"
-โ Gemini generates functional, well-documented code
-```
-
-### **Prompt Engineering**
-```
-Prompt: "Transform this basic idea into a detailed AI art prompt: 'sunset landscape'"
-โ Gemini: "A breathtaking sunset landscape with golden hour lighting, rolling hills, dramatic clouds, warm color palette, highly detailed, digital painting, cinematic composition"
-```
-
-## ๐ **Integration Quality**
-
-### **โ
Follows Existing Patterns**
-- Uses same API key injection system as OpenAI/FLUX
-- Matches workflow template structure
-- Consistent with DreamLayer's design patterns
-
-### **โ
Professional Implementation**
-- Comprehensive error handling
-- Type-safe API integration
-- Scalable architecture
-- Production-ready code quality
-
-### **โ
Enhanced User Experience**
-- Intuitive API key configuration
-- Seamless model selection
-- Powerful multimodal capabilities
-- Professional workflow automation
-
-## ๐ **Impact & Value**
-
-### **For AI Artists**
-- Intelligent image analysis and critique
-- Automated prompt optimization
-- Creative workflow enhancement
-- Professional artistic guidance
-
-### **For Developers**
-- Multimodal AI integration example
-- Advanced node chaining patterns
-- API integration best practices
-- Extensible architecture foundation
-
-### **For Content Creators**
-- Text generation and refinement
-- Image understanding and enhancement
-- Automated content workflows
-- AI-assisted creative processes
-
-## ๐ฎ **Future Enhancements**
-
-- **File Processing**: Document analysis and summarization
-- **Video Analysis**: Frame-by-frame content understanding
-- **Audio Processing**: Speech recognition and analysis
-- **Advanced Chaining**: Multi-step AI reasoning workflows
-
----
-
-**๐ This integration demonstrates DreamLayer's commitment to cutting-edge AI technology and user-centric design, making advanced AI capabilities accessible through an intuitive, professional interface.**
\ No newline at end of file
diff --git a/README.md b/README.md
index 79295e0b..fa9ec09e 100644
--- a/README.md
+++ b/README.md
@@ -15,6 +15,8 @@
Product Vision:
AI Research
+ |
+ AI Art
@@ -27,9 +29,9 @@
DreamLayer AI is an open-source Stable Diffusion WebUI that keeps the familiar Automatic1111 โ Forge layout you know, replaces the clutter with a modern design system, and runs every generation step on ComfyUI in the background.
No node graph on screen, no server rental, just a lightning-fast local interface for:
-- **AI artists** producing portfolio-ready images
-- **Developers and prompt engineers** iterating on prompts and LoRAs
-- **Researchers** benchmarking new models and samplers
+* **AI artists** producing portfolio-ready images
+* **Developers and prompt engineers** iterating on prompts and LoRAs
+* **Researchers** benchmarking new models and samplers
> **Status:** โจ **Now live:** Open Alpha โข **Beta V1 ships:** **Mid-July 2025**
@@ -48,7 +50,6 @@ Easiest way to run DreamLayer ๐ Best for non-technical users
3. Type `run it` or press the **"Run"** button โ then follow the guided steps
Cursor will:
-
- Walk you through each setup step
- Install Python and Node dependencies
- Create a virtual environment
@@ -62,19 +63,16 @@ Cursor will:
### Installation
**linux:**
-
```bash
./install_linux_dependencies.sh
```
**macOS:**
-
```bash
./install_mac_dependencies.sh
```
**Windows:**
-
```bash
install_windows_dependencies.ps1
```
@@ -82,43 +80,39 @@ install_windows_dependencies.ps1
### Start Application
**linux:**
-
```bash
./start_dream_layer.sh
```
**macOS:**
-
```bash
./start_dream_layer.sh
```
**Windows:**
-
```bash
start_dream_layer.bat
```
-
### Env Variables
-
**install_dependencies_linux**
DLVENV_PATH // preferred path to python virtual env. default is /tmp/dlvenv
**start_dream_layer**
-DREAMLAYER_COMFYUI_CPU_MODE // if no nvidia drivers available run using CPU only. default is false
+DREAMLAYER_COMFYUI_CPU_MODE // if no nvidia drivers available run using CPU only. default is false
### Access
- **Frontend:** http://localhost:8080
- **ComfyUI:** http://localhost:8188
+
### Installing Models โญ๏ธ
DreamLayer ships without weights to keep the download small. You have two ways to add models:
### a) Closed-source API models
-DreamLayer can also call external APIs (OpenAIย DALLยทE, Flux, Ideogram).
+DreamLayer can also call external APIs (OpenAIย DALLยทE, Flux, Ideogram).
To enable them:
@@ -128,7 +122,6 @@ Edit your `.env` file at `dream_layer/.env`:
OPENAI_API_KEY=sk-...
BFL_API_KEY=flux-...
IDEOGRAM_API_KEY=id-...
-STABILITY_API_KEY=sk-...
```
Once a key is present, the model becomes visible in the dropdown.
@@ -137,13 +130,11 @@ No key = feature stays hidden.
### b) Open-source checkpoints (offline)
**Step 1:** Download .safetensors or .ckpt files from:
-
- Hugging Face
- Civitai
- Your own training runs
**Step 2:** Place the models in the appropriate folders (auto-created on first run):
-
- Checkpoints/ โ # full checkpoints (.safetensors)
- Lora/ โ # LoRA & LoCon files
- ControlNet/ โ # ControlNet models
@@ -153,19 +144,22 @@ No key = feature stays hidden.
> Tip: Use symbolic links if your checkpoints live on another drive.
-_The installation scripts will automatically install all dependencies and set up the environment._
+*The installation scripts will automatically install all dependencies and set up the environment.*
+
---
## Why DreamLayer AI?
-| ๐ Feature | ๐ How itโs better |
-| ------------------------------- | ----------------------------------------------------------------------------------------------------------- |
-| **Familiar Layout** | If youโve used A1111 or Forge, youโll feel at home in sec. Zero learning curve |
-| **Modern UX** | Responsive design with light & dark themes and a clutter-free interface that lets you work faster |
-| **ComfyUI Engine Inside** | All generation runs on a proven, modular, stable ComfyUI backend. Ready for custom nodes and advanced hacks |
-| **Closed-Source Model Support** | One-click swap to GPT-4o Image, Ideogram V3, Runway Gen-4, Recraft V3, and more |
-| **Local first** | Runs entirely on your GPU with no hosting fees, full privacy, and instant acceleration out of the box |
+| ๐ Feature | ๐ How itโs better |
+|------------|-----------|
+| **Familiar Layout** | If youโve used A1111 or Forge, youโll feel at home in sec. Zero learning curve |
+| **Modern UX** | Responsive design with light & dark themes and a clutter-free interface that lets you work faster |
+| **ComfyUI Engine Inside** | All generation runs on a proven, modular, stable ComfyUI backend. Ready for custom nodes and advanced hacks |
+| **Closed-Source Model Support** | One-click swap to GPT-4o Image, Ideogram V3, Runway Gen-4, Recraft V3, and more |
+| **Local first** | Runs entirely on your GPU with no hosting fees, full privacy, and instant acceleration out of the box |
+
+
---
@@ -182,11 +176,11 @@ _The installation scripts will automatically install all dependencies and set up
Starring helps us trend on GitHub which brings more contributors and faster features.
Early stargazers get perks:
-- **GitHub Hall of Fame**: Your handle listed forever in the README under Founding Supporter
-- **Early Builds**: Download private binaries before everyone else
-- **Community first hiring**: We prioritize contributors and stargazers for all freelance, full-time, and AI artist or engineering roles.
-- **Closed Beta Invites**: Give feedback that shapes 1.0
-- **Discord badge**: Exclusive Founding Supporter role
+* **GitHub Hall of Fame**: Your handle listed forever in the README under Founding Supporter
+* **Early Builds**: Download private binaries before everyone else
+* **Community first hiring**: We prioritize contributors and stargazers for all freelance, full-time, and AI artist or engineering roles.
+* **Closed Beta Invites**: Give feedback that shapes 1.0
+* **Discord badge**: Exclusive Founding Supporter role
> โญ **Hit the star button right now** and join us at the ground floor โบ๏ธ
@@ -194,8 +188,8 @@ Early stargazers get perks:
## Get Involved Today
-1. **Star** this repository.
-2. **Watch** releases for the July code drop.
+1. **Star** this repository.
+2. **Watch** releases for the July code drop.
3. **Join** the Discord (link coming soon) and say hi.
4. **Open issues** for ideas or feedback & Submit PRs once the code is live
5. **Share** the screenshot on X โ Twitter with `#DreamLayerAI` to spread the word.
@@ -215,6 +209,7 @@ Full docs will ship with the first code release.
[DreamLayer AI - Documentation](https://dreamlayer-ai.github.io/DreamLayer/)
+
---
## License
diff --git a/docs/api_reference.md b/docs/api_reference.md
index adb0f0eb..7840133d 100644
--- a/docs/api_reference.md
+++ b/docs/api_reference.md
@@ -9,7 +9,6 @@ Complete API documentation for DreamLayer AI based on the actual codebase implem
#### Models Management
**GET `/api/models`** - Get available checkpoint models
-
```python
# [Source: dream_layer.py lines 150-180]
def get_available_models():
@@ -20,24 +19,22 @@ def get_available_models():
```
**Response Format:**
-
```json
{
- "status": "success",
- "models": [
- {
- "id": "model_filename.safetensors",
- "name": "Model Display Name",
- "filename": "model_filename.safetensors"
- }
- ]
+ "status": "success",
+ "models": [
+ {
+ "id": "model_filename.safetensors",
+ "name": "Model Display Name",
+ "filename": "model_filename.safetensors"
+ }
+ ]
}
```
#### LoRA Models
**GET `/api/lora-models`** - Get available LoRA models
-
```python
# [Source: dream_layer.py lines 271-293]
def get_available_lora_models():
@@ -50,7 +47,6 @@ def get_available_lora_models():
#### Upscaler Models
**GET `/api/upscaler-models`** - Get available upscaler models
-
```python
# [Source: dream_layer.py lines 323-329]
def get_upscaler_models_endpoint():
@@ -63,7 +59,6 @@ def get_upscaler_models_endpoint():
#### ControlNet Models
**GET `/api/controlnet/models`** - Get available ControlNet models
-
```python
# [Source: dream_layer.py lines 466-486]
def get_controlnet_models_endpoint():
@@ -76,7 +71,6 @@ def get_controlnet_models_endpoint():
#### Settings Management
**POST `/api/settings/paths`** - Configure output and models directories
-
```python
# [Source: dream_layer.py lines 200-226]
def handle_path_settings():
@@ -87,18 +81,16 @@ def handle_path_settings():
```
**Request Format:**
-
```json
{
- "outputDirectory": "/path/to/output",
- "modelsDirectory": "/path/to/models"
+ "outputDirectory": "/path/to/output",
+ "modelsDirectory": "/path/to/models"
}
```
#### Prompt Generation
**GET `/api/fetch-prompt`** - Get random positive and negative prompts
-
```python
# [Source: dream_layer.py lines 311-322]
def fetch_prompt():
@@ -111,7 +103,6 @@ def fetch_prompt():
#### File Operations
**POST `/api/show-in-folder`** - Open file in system file manager
-
```python
# [Source: dream_layer.py lines 330-363]
def show_in_folder():
@@ -122,7 +113,6 @@ def show_in_folder():
```
**POST `/api/send-to-img2img`** - Send image to img2img workflow
-
```python
# [Source: dream_layer.py lines 364-381]
def send_to_img2img():
@@ -133,7 +123,6 @@ def send_to_img2img():
```
**POST `/api/send-to-extras`** - Send image to extras workflow
-
```python
# [Source: dream_layer.py lines 382-405]
def send_to_extras():
@@ -146,7 +135,6 @@ def send_to_extras():
#### File Upload
**POST `/api/upload-model`** - Upload model files to ComfyUI models directory
-
```python
# [Source: dream_layer.py lines 456-481]
def upload_model():
@@ -158,7 +146,6 @@ def upload_model():
```
**Request Format:**
-
```
Content-Type: multipart/form-data
@@ -167,22 +154,20 @@ model_type: checkpoints|loras|controlnet|upscale_models|vae|embeddings|hypernetw
```
**Response Format:**
-
```json
{
- "status": "success",
- "filename": "timestamped_filename.safetensors",
- "original_filename": "original_name.safetensors",
- "display_name": "Original Name",
- "model_type": "checkpoints",
- "filepath": "/path/to/saved/file",
- "size": 1234567890,
- "message": "Model uploaded successfully to checkpoints"
+ "status": "success",
+ "filename": "timestamped_filename.safetensors",
+ "original_filename": "original_name.safetensors",
+ "display_name": "Original Name",
+ "model_type": "checkpoints",
+ "filepath": "/path/to/saved/file",
+ "size": 1234567890,
+ "message": "Model uploaded successfully to checkpoints"
}
```
**POST `/api/upload-controlnet-image`** - Upload ControlNet image
-
```python
# [Source: dream_layer.py lines 406-448]
def upload_controlnet_image():
@@ -196,7 +181,6 @@ def upload_controlnet_image():
#### Image Serving
**GET `/api/images/`** - Serve generated images
-
```python
# [Source: dream_layer.py lines 449-465]
def serve_image(filename):
@@ -212,7 +196,6 @@ def serve_image(filename):
### Directory Management
**`get_directories()`** - Get output and models directories
-
```python
# [Source: dream_layer.py lines 35-75]
def get_directories() -> Tuple[str, Optional[str]]:
@@ -225,7 +208,6 @@ def get_directories() -> Tuple[str, Optional[str]]:
### ComfyUI Integration
**`import_comfyui_main()`** - Import ComfyUI main module
-
```python
# [Source: dream_layer.py lines 77-95]
def import_comfyui_main():
@@ -238,7 +220,6 @@ def import_comfyui_main():
### Settings Management
**`save_settings(settings)`** - Save path settings to file
-
```python
# [Source: dream_layer.py lines 182-190]
def save_settings(settings):
@@ -254,7 +235,6 @@ def save_settings(settings):
### ComfyUI Server
**`start_comfy_server()`** - Start ComfyUI server in background
-
```python
# [Source: dream_layer.py lines 227-265]
def start_comfy_server():
@@ -267,7 +247,6 @@ def start_comfy_server():
### Flask Server
**`start_flask_server()`** - Start Flask API server
-
```python
# [Source: dream_layer.py lines 266-270]
def start_flask_server():
@@ -293,10 +272,6 @@ API_KEY_TO_MODELS = {
],
"IDEOGRAM_API_KEY": [
{"id": "ideogram-v3", "name": "Ideogram V3", "filename": "ideogram-v3"},
- ],
- "STABILITY_API_KEY": [
- {"id": "stability-sdxl", "name": "Stability SDXL", "filename": "stability-sdxl"},
- {"id": "stability-sd-turbo", "name": "Stability SD Turbo", "filename": "stability-sd-turbo"},
]
}
```
@@ -320,4 +295,4 @@ CORS(app, resources={
- [Architecture Overview](architecture.md) - System design and component relationships
- [Usage Guide](usage.md) - How to use the API endpoints
-- [Installation Guide](installation.md) - Setup and configuration instructions
+- [Installation Guide](installation.md) - Setup and configuration instructions
\ No newline at end of file
diff --git a/docs/installation.md b/docs/installation.md
index eb95bdf4..ba690cae 100644
--- a/docs/installation.md
+++ b/docs/installation.md
@@ -5,7 +5,6 @@ Complete, OS-agnostic installation guide for DreamLayer AI with GPU/CPU setup in
## ๐ System Requirements
### Minimum Requirements
-
- **OS:** Windows 10+, macOS 10.15+, or Linux (Ubuntu 18.04+)
- **Python:** 3.8 or higher
- **Node.js:** 16.0 or higher
@@ -13,14 +12,12 @@ Complete, OS-agnostic installation guide for DreamLayer AI with GPU/CPU setup in
- **Storage:** 10GB free space
### Recommended Requirements
-
- **GPU:** NVIDIA RTX 3060+ (8GB+ VRAM) or Apple Silicon M1+
- **RAM:** 16GB or higher
- **Storage:** 50GB+ free space (for models)
- **Network:** Stable internet connection for model downloads
### GPU Support
-
- **NVIDIA:** CUDA 11.8+ with compatible drivers
- **Apple Silicon:** Native MPS acceleration
- **AMD:** ROCm support (experimental)
@@ -31,14 +28,12 @@ Complete, OS-agnostic installation guide for DreamLayer AI with GPU/CPU setup in
### 1. Install Python
**Windows:**
-
```bash
# Download from python.org or use winget
winget install Python.Python.3.11
```
**macOS:**
-
```bash
# Using Homebrew
brew install python@3.11
@@ -47,7 +42,6 @@ brew install python@3.11
```
**Linux (Ubuntu/Debian):**
-
```bash
sudo apt update
sudo apt install python3.11 python3.11-pip python3.11-venv
@@ -56,14 +50,12 @@ sudo apt install python3.11 python3.11-pip python3.11-venv
### 2. Install Node.js
**Windows:**
-
```bash
# Download from nodejs.org or use winget
winget install OpenJS.NodeJS
```
**macOS:**
-
```bash
# Using Homebrew
brew install node
@@ -72,7 +64,6 @@ brew install node
```
**Linux (Ubuntu/Debian):**
-
```bash
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
sudo apt-get install -y nodejs
@@ -81,14 +72,12 @@ sudo apt-get install -y nodejs
### 3. Install Git
**Windows:**
-
```bash
# Download from git-scm.com or use winget
winget install Git.Git
```
**macOS:**
-
```bash
# Using Homebrew
brew install git
@@ -97,7 +86,6 @@ brew install git
```
**Linux (Ubuntu/Debian):**
-
```bash
sudo apt install git
```
@@ -107,7 +95,6 @@ sudo apt install git
### Method 1: Automated Installation (Recommended)
#### macOS/Linux
-
```bash
# Clone the repository
git clone https://github.com/DreamLayer-AI/DreamLayer.git
@@ -122,7 +109,6 @@ chmod +x start_dream_layer.sh
```
#### Windows
-
```bash
# Clone the repository
git clone https://github.com/DreamLayer-AI/DreamLayer.git
@@ -135,14 +121,12 @@ install_windows_dependencies.bat
### Method 2: Manual Installation
#### 1. Clone Repository
-
```bash
git clone https://github.com/DreamLayer-AI/DreamLayer.git
cd DreamLayer
```
#### 2. Install Python Dependencies
-
```bash
# Create virtual environment
python -m venv venv
@@ -158,7 +142,6 @@ pip install -r requirements.txt
```
#### 3. Install Node.js Dependencies
-
```bash
cd dream_layer_frontend
npm install
@@ -166,7 +149,6 @@ cd ..
```
#### 4. Setup ComfyUI
-
```bash
# ComfyUI should be included in the repository
# If not, clone it manually:
@@ -184,7 +166,6 @@ Create a `.env` file in the root directory:
OPENAI_API_KEY=your_openai_api_key_here
IDEOGRAM_API_KEY=your_ideogram_api_key_here
BFL_API_KEY=your_bfl_api_key_here
-STABILITY_API_KEY=your_stability_api_key_here
# Server Configuration
FLASK_PORT=5000
@@ -205,7 +186,6 @@ Set up your directories in the web interface:
### 3. Model Setup
#### Download Models
-
```bash
# Create models directory
mkdir -p models/checkpoints
@@ -219,7 +199,6 @@ wget https://huggingface.co/runwayml/stable-diffusion-v1-5/resolve/main/v1-5-pru
```
#### Supported Model Formats
-
- **Checkpoints:** `.safetensors`, `.ckpt`
- **LoRA:** `.safetensors`, `.pt`
- **ControlNet:** `.safetensors`, `.pth`
@@ -230,13 +209,11 @@ wget https://huggingface.co/runwayml/stable-diffusion-v1-5/resolve/main/v1-5-pru
### Automated Start
**macOS/Linux:**
-
```bash
./start_dream_layer.sh
```
**Windows:**
-
```bash
start_dream_layer.bat
```
@@ -244,21 +221,18 @@ start_dream_layer.bat
### Manual Start
#### 1. Start ComfyUI Server
-
```bash
cd ComfyUI
python main.py --listen 0.0.0.0 --port 8188
```
#### 2. Start Flask API Server
-
```bash
cd dream_layer_backend
python dream_layer.py
```
#### 3. Start Frontend Development Server
-
```bash
cd dream_layer_frontend
npm run dev
@@ -267,7 +241,6 @@ npm run dev
### Access DreamLayer
Open your browser and navigate to:
-
- **Frontend:** http://localhost:8080
- **API:** http://localhost:5000
- **ComfyUI:** http://localhost:8188
@@ -277,14 +250,12 @@ Open your browser and navigate to:
### Check Installation
1. **Verify Python packages:**
-
```bash
python -c "import torch; print(f'PyTorch: {torch.__version__}')"
python -c "import torch; print(f'CUDA available: {torch.cuda.is_available()}')"
```
2. **Verify Node.js packages:**
-
```bash
cd dream_layer_frontend
npm list --depth=0
@@ -306,14 +277,12 @@ Open your browser and navigate to:
### Common Issues
#### Python Import Errors
-
```bash
# Reinstall packages
pip install --force-reinstall -r requirements.txt
```
#### CUDA Issues
-
```bash
# Check CUDA installation
nvidia-smi
@@ -324,7 +293,6 @@ pip install torch torchvision torchaudio --index-url https://download.pytorch.or
```
#### Port Conflicts
-
```bash
# Check port usage
netstat -tulpn | grep :8080
@@ -336,7 +304,6 @@ kill -9
```
#### Memory Issues
-
```bash
# Reduce batch size in settings
# Use CPU mode for testing
@@ -352,7 +319,6 @@ kill -9
## ๐ Updates
### Updating DreamLayer
-
```bash
# Pull latest changes
git pull origin main
@@ -362,7 +328,6 @@ git pull origin main
```
### Updating Models
-
```bash
# Models are automatically detected on restart
# Or refresh in the web interface
@@ -370,4 +335,4 @@ git pull origin main
---
-_Need help? Check out the [Quick Start Guide](quick_start.md) for a faster setup._
+*Need help? Check out the [Quick Start Guide](quick_start.md) for a faster setup.*
\ No newline at end of file
diff --git a/docs/modules/utils.md b/docs/modules/utils.md
index 20a70efd..4f9df258 100644
--- a/docs/modules/utils.md
+++ b/docs/modules/utils.md
@@ -15,51 +15,44 @@ Handles secure API key management for external services.
#### Key Functions
**`read_api_keys_from_env()`**
-
```python
def read_api_keys_from_env():
"""
Read API keys from environment variables
-
+
Returns:
dict: Dictionary of API key names and values
"""
api_keys = {}
-
+
# Check for OpenAI API key
openai_key = os.getenv('OPENAI_API_KEY')
if openai_key:
api_keys['OPENAI_API_KEY'] = openai_key
-
+
# Check for Ideogram API key
ideogram_key = os.getenv('IDEOGRAM_API_KEY')
if ideogram_key:
api_keys['IDEOGRAM_API_KEY'] = ideogram_key
-
+
# Check for BFL API key
bfl_key = os.getenv('BFL_API_KEY')
if bfl_key:
api_keys['BFL_API_KEY'] = bfl_key
-
- # Check for Stability AI API key
- stability_key = os.getenv('STABILITY_API_KEY')
- if stability_key:
- api_keys['STABILITY_API_KEY'] = stability_key
-
+
return api_keys
```
**`validate_api_key(api_key, service)`**
-
```python
def validate_api_key(api_key: str, service: str):
"""
Validate API key format for specific service
-
+
Parameters:
api_key (str): API key to validate
service (str): Service name (openai, ideogram, bfl)
-
+
Returns:
bool: True if valid, False otherwise
"""
@@ -72,72 +65,67 @@ Manages model discovery and loading from various sources.
#### Key Functions
**`get_lora_models(models_dir)`**
-
```python
def get_lora_models(models_dir: str):
"""
Fetch available LoRA models from the models directory
-
+
Parameters:
models_dir (str): Path to models directory
-
+
Returns:
list: List of LoRA model objects
"""
```
**`get_settings()`**
-
```python
def get_settings():
"""
Load application settings from settings.json
-
+
Returns:
dict: Application settings
"""
```
**`is_valid_directory(path)`**
-
```python
def is_valid_directory(path: str):
"""
Check if a directory path is valid and accessible
-
+
Parameters:
path (str): Directory path to validate
-
+
Returns:
bool: True if valid, False otherwise
"""
```
**`get_upscaler_models(models_dir)`**
-
```python
def get_upscaler_models(models_dir: str):
"""
Fetch available upscaler models from the models directory
-
+
Parameters:
models_dir (str): Path to models directory
-
+
Returns:
list: List of upscaler model objects
"""
```
**`get_controlnet_models(models_dir)`**
-
```python
def get_controlnet_models(models_dir: str):
"""
Fetch available ControlNet models from the models directory
-
+
Parameters:
models_dir (str): Path to models directory
-
+
Returns:
list: List of ControlNet model objects
"""
@@ -150,39 +138,36 @@ Generates random prompts for inspiration and testing.
#### Key Functions
**`fetch_positive_prompt()`**
-
```python
def fetch_positive_prompt():
"""
Fetch a random positive prompt from the prompt database
-
+
Returns:
str: Random positive prompt
"""
```
**`fetch_negative_prompt()`**
-
```python
def fetch_negative_prompt():
"""
Fetch a random negative prompt from the prompt database
-
+
Returns:
str: Random negative prompt
"""
```
**`load_prompts_from_file(file_path)`**
-
```python
def load_prompts_from_file(file_path: str):
"""
Load prompts from a text file
-
+
Parameters:
file_path (str): Path to prompt file
-
+
Returns:
list: List of prompts from file
"""
@@ -195,36 +180,312 @@ Handles file system operations and image processing.
#### Key Functions
**`save_image(image_data, filename, output_dir)`**
-
```python
def save_image(image_data: bytes, filename: str, output_dir: str):
"""
Save image data to file system
-
+
Parameters:
image_data (bytes): Image data to save
filename (str): Output filename
output_dir (str): Output directory path
-
+
Returns:
str: Path to saved image file
"""
```
**`copy_file_to_directory(source_path, target_dir)`**
-
```python
def copy_file_to_directory(source_path: str, target_dir: str):
"""
Copy a file to a target directory
-
+
Parameters:
source_path (str): Source file path
target_dir (str): Target directory path
-
+
Returns:
str: Path to copied file
"""
```
-\*\*`
+**`open_file_in_explorer(file_path)`**
+```python
+def open_file_in_explorer(file_path: str):
+ """
+ Open file in system file explorer
+
+ Parameters:
+ file_path (str): Path to file to open
+
+ Returns:
+ bool: True if successful, False otherwise
+ """
+```
+
+## Configuration Management
+
+### Settings Structure
+
+```python
+# Default settings structure
+DEFAULT_SETTINGS = {
+ "outputDirectory": "Dream_Layer_Resources/output",
+ "modelsDirectory": None,
+ "serverPort": 5000,
+ "comfyuiPort": 8188,
+ "frontendPort": 8080,
+ "enableLogging": True,
+ "logLevel": "INFO"
+}
+```
+
+### Settings Loading
+
+```python
+def load_settings():
+ """
+ Load settings from settings.json file
+
+ Returns:
+ dict: Application settings
+ """
+ settings_file = os.path.join(os.path.dirname(__file__), 'settings.json')
+
+ try:
+ with open(settings_file, 'r') as f:
+ settings = json.load(f)
+ return settings
+ except FileNotFoundError:
+ # Return default settings if file doesn't exist
+ return DEFAULT_SETTINGS.copy()
+ except json.JSONDecodeError:
+ print("Error: Invalid settings.json file")
+ return DEFAULT_SETTINGS.copy()
+```
+
+### Settings Saving
+
+```python
+def save_settings(settings: dict):
+ """
+ Save settings to settings.json file
+
+ Parameters:
+ settings (dict): Settings to save
+
+ Returns:
+ bool: True if successful, False otherwise
+ """
+ settings_file = os.path.join(os.path.dirname(__file__), 'settings.json')
+
+ try:
+ with open(settings_file, 'w') as f:
+ json.dump(settings, f, indent=2)
+ return True
+ except Exception as e:
+ print(f"Error saving settings: {e}")
+ return False
+```
+
+## Model Management
+
+### Model Discovery
+
+```python
+def discover_models(models_dir: str, model_type: str):
+ """
+ Discover models of specific type in directory
+
+ Parameters:
+ models_dir (str): Models directory path
+ model_type (str): Type of models to discover (checkpoint, lora, etc.)
+
+ Returns:
+ list: List of discovered model files
+ """
+ if not models_dir or not os.path.exists(models_dir):
+ return []
+
+ model_extensions = {
+ 'checkpoint': ['.safetensors', '.ckpt'],
+ 'lora': ['.safetensors', '.pt'],
+ 'controlnet': ['.safetensors', '.pth'],
+ 'upscaler': ['.pth', '.bin']
+ }
+
+ extensions = model_extensions.get(model_type, [])
+ models = []
+
+ for file in os.listdir(models_dir):
+ if any(file.endswith(ext) for ext in extensions):
+ models.append(file)
+
+ return models
+```
+
+### Model Validation
+
+```python
+def validate_model_file(file_path: str):
+ """
+ Validate model file integrity
+
+ Parameters:
+ file_path (str): Path to model file
+
+ Returns:
+ dict: Validation result with status and details
+ """
+ if not os.path.exists(file_path):
+ return {"valid": False, "error": "File not found"}
+
+ file_size = os.path.getsize(file_path)
+ if file_size == 0:
+ return {"valid": False, "error": "File is empty"}
+
+ # Check file extension
+ valid_extensions = ['.safetensors', '.ckpt', '.pt', '.pth', '.bin']
+ if not any(file_path.endswith(ext) for ext in valid_extensions):
+ return {"valid": False, "error": "Invalid file extension"}
+
+ return {"valid": True, "file_size": file_size}
+```
+
+## Error Handling
+
+### Utility Error Classes
+
+```python
+class ModelError(Exception):
+ """Raised when there's an error with model operations"""
+ pass
+
+class APIKeyError(Exception):
+ """Raised when there's an error with API key management"""
+ pass
+
+class FileOperationError(Exception):
+ """Raised when there's an error with file operations"""
+ pass
+```
+
+### Error Handling Functions
+
+```python
+def handle_model_error(error: Exception, model_name: str):
+ """
+ Handle model-related errors
+
+ Parameters:
+ error (Exception): The error that occurred
+ model_name (str): Name of the model that caused the error
+
+ Returns:
+ dict: Error response with details
+ """
+ return {
+ "status": "error",
+ "type": "model_error",
+ "model": model_name,
+ "message": str(error),
+ "timestamp": time.time()
+ }
+
+def handle_api_error(error: Exception, service: str):
+ """
+ Handle API-related errors
+
+ Parameters:
+ error (Exception): The error that occurred
+ service (str): Name of the service that caused the error
+
+ Returns:
+ dict: Error response with details
+ """
+ return {
+ "status": "error",
+ "type": "api_error",
+ "service": service,
+ "message": str(error),
+ "timestamp": time.time()
+ }
+```
+
+## Logging and Monitoring
+
+### Logging Configuration
+
+```python
+def setup_logging(log_level: str = "INFO"):
+ """
+ Setup logging configuration
+
+ Parameters:
+ log_level (str): Logging level (DEBUG, INFO, WARNING, ERROR)
+ """
+ logging.basicConfig(
+ level=getattr(logging, log_level.upper()),
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
+ handlers=[
+ logging.FileHandler('logs/dream_layer.log'),
+ logging.StreamHandler()
+ ]
+ )
+```
+
+### Performance Monitoring
+
+```python
+def monitor_function_performance(func):
+ """
+ Decorator to monitor function performance
+
+ Parameters:
+ func: Function to monitor
+
+ Returns:
+ Wrapped function with performance monitoring
+ """
+ def wrapper(*args, **kwargs):
+ start_time = time.time()
+ try:
+ result = func(*args, **kwargs)
+ execution_time = time.time() - start_time
+ logging.info(f"{func.__name__} executed in {execution_time:.2f} seconds")
+ return result
+ except Exception as e:
+ execution_time = time.time() - start_time
+ logging.error(f"{func.__name__} failed after {execution_time:.2f} seconds: {e}")
+ raise
+
+ return wrapper
+```
+
+## Best Practices
+
+### API Key Security
+
+1. **Environment Variables** - Store API keys in environment variables, not in code
+2. **Validation** - Always validate API keys before use
+3. **Rotation** - Regularly rotate API keys for security
+4. **Logging** - Never log API keys in plain text
+
+### File Operations
+
+1. **Path Validation** - Always validate file paths before operations
+2. **Error Handling** - Implement proper error handling for file operations
+3. **Permissions** - Check file permissions before read/write operations
+4. **Cleanup** - Clean up temporary files after use
+
+### Model Management
+
+1. **Validation** - Validate model files before loading
+2. **Caching** - Cache model metadata for faster discovery
+3. **Fallbacks** - Provide fallback models when primary models fail
+4. **Monitoring** - Monitor model loading times and errors
+
+---
+
+*For more details, see the [API Reference](../api_reference.md) and [Architecture Guide](../architecture.md).*
\ No newline at end of file
diff --git a/docs/usage.md b/docs/usage.md
index 4e25d91c..f49a7b60 100644
--- a/docs/usage.md
+++ b/docs/usage.md
@@ -29,13 +29,11 @@ Learn how to use DreamLayer AI effectively with API examples, workflow managemen
### Core Endpoints
#### Get Available Models
-
```bash
curl -X GET http://localhost:5000/api/models
```
**Response:**
-
```json
{
"status": "success",
@@ -55,31 +53,26 @@ curl -X GET http://localhost:5000/api/models
```
#### Get LoRA Models
-
```bash
curl -X GET http://localhost:5000/api/lora-models
```
#### Get Upscaler Models
-
```bash
curl -X GET http://localhost:5000/api/upscaler-models
```
#### Get ControlNet Models
-
```bash
curl -X GET http://localhost:5000/api/controlnet/models
```
#### Fetch Random Prompts
-
```bash
curl -X GET http://localhost:5000/api/fetch-prompt
```
**Response:**
-
```json
{
"positive": "A majestic dragon soaring through clouds",
@@ -90,14 +83,12 @@ curl -X GET http://localhost:5000/api/fetch-prompt
### File Operations
#### Upload ControlNet Image
-
```bash
curl -X POST http://localhost:5000/api/upload-controlnet-image \
-F "image=@path/to/your/image.png"
```
**Response:**
-
```json
{
"status": "success",
@@ -106,7 +97,6 @@ curl -X POST http://localhost:5000/api/upload-controlnet-image \
```
#### Send Image to img2img
-
```bash
curl -X POST http://localhost:5000/api/send-to-img2img \
-H "Content-Type: application/json" \
@@ -114,7 +104,6 @@ curl -X POST http://localhost:5000/api/send-to-img2img \
```
#### Send Image to Extras
-
```bash
curl -X POST http://localhost:5000/api/send-to-extras \
-H "Content-Type: application/json" \
@@ -122,7 +111,6 @@ curl -X POST http://localhost:5000/api/send-to-extras \
```
#### Show Image in Folder
-
```bash
curl -X POST http://localhost:5000/api/show-in-folder \
-H "Content-Type: application/json" \
@@ -132,7 +120,6 @@ curl -X POST http://localhost:5000/api/show-in-folder \
### Settings Management
#### Configure Paths
-
```bash
curl -X POST http://localhost:5000/api/settings/paths \
-H "Content-Type: application/json" \
@@ -180,7 +167,7 @@ import requests
# DALL-E 3 generation
def generate_dalle3(prompt, api_key):
- response = requests.post('https://api.openai.com/v1/images/generations',
+ response = requests.post('https://api.openai.com/v1/images/generations',
headers={'Authorization': f'Bearer {api_key}'},
json={
'model': 'dall-e-3',
@@ -226,7 +213,7 @@ response = requests.post('http://localhost:8188/prompt', json={
# Upload ControlNet image
with open('control_image.png', 'rb') as f:
files = {'image': f}
- response = requests.post('http://localhost:5000/api/upload-controlnet-image',
+ response = requests.post('http://localhost:5000/api/upload-controlnet-image',
files=files)
controlnet_image = response.json()['filename']
@@ -290,7 +277,6 @@ Set up API keys for cloud models:
OPENAI_API_KEY=your_openai_api_key_here
IDEOGRAM_API_KEY=your_ideogram_api_key_here
BFL_API_KEY=your_bfl_api_key_here
-STABILITY_API_KEY=your_stability_api_key_here
```
### Directory Structure
@@ -407,4 +393,4 @@ except Exception as e:
---
-_For more advanced usage, see the [API Reference](api_reference.md) and [Architecture Guide](architecture.md)._
+*For more advanced usage, see the [API Reference](api_reference.md) and [Architecture Guide](architecture.md).*
\ No newline at end of file
diff --git a/dream_layer_backend/controlnet.py b/dream_layer_backend/controlnet.py
index 8d3913bd..f88e193b 100644
--- a/dream_layer_backend/controlnet.py
+++ b/dream_layer_backend/controlnet.py
@@ -1,7 +1,9 @@
import os
+import base64
+import traceback
+import time
from PIL import Image, ImageDraw
-
def save_controlnet_image(image_data, unit_index):
"""
Save uploaded ControlNet image to ComfyUI input directory.
@@ -28,10 +30,6 @@ def save_controlnet_image(image_data, unit_index):
os.makedirs(input_dir, exist_ok=True)
print(f"โ
Directory exists: {os.path.exists(input_dir)}")
- # Generate unique filename
- import base64
- import time
-
# Create a unique filename based on timestamp and unit index
timestamp = int(time.time() * 1000)
filename = f"controlnet_unit_{unit_index}_{timestamp}.png"
@@ -65,7 +63,6 @@ def save_controlnet_image(image_data, unit_index):
except Exception as decode_error:
print(f"โ Error decoding base64 image: {decode_error}")
- import traceback
traceback.print_exc()
return None
else:
@@ -78,7 +75,6 @@ def save_controlnet_image(image_data, unit_index):
except Exception as e:
print(f"โ Error saving ControlNet image: {str(e)}")
- import traceback
traceback.print_exc()
return None
@@ -112,6 +108,5 @@ def create_test_controlnet_image():
except Exception as e:
print(f"โ Error creating test ControlNet image: {str(e)}")
- import traceback
traceback.print_exc()
return False
\ No newline at end of file
diff --git a/dream_layer_backend/dream_layer.py b/dream_layer_backend/dream_layer.py
index d5fb2a05..9e2e85d7 100644
--- a/dream_layer_backend/dream_layer.py
+++ b/dream_layer_backend/dream_layer.py
@@ -7,10 +7,18 @@
from flask import Flask, jsonify, request, send_from_directory
from flask_cors import CORS
import requests
+import importlib.util
import json
import subprocess
+import traceback
+from shared_utils import SERVED_IMAGES_DIR, MATRIX_GRIDS_DIR
from dream_layer_backend_utils.random_prompt_generator import fetch_positive_prompt, fetch_negative_prompt
from dream_layer_backend_utils.fetch_advanced_models import get_lora_models, get_settings, is_valid_directory, get_upscaler_models, get_controlnet_models
+from dream_layer_backend_utils import read_api_keys_from_env
+import base64
+import io
+from PIL import Image
+
# Add ComfyUI directory to Python path
current_dir = os.path.dirname(os.path.abspath(__file__))
parent_dir = os.path.dirname(current_dir)
@@ -29,6 +37,7 @@
],
"IDEOGRAM_API_KEY": [
{"id": "ideogram-v3", "name": "Ideogram V3", "filename": "ideogram-v3"},
+
],
"STABILITY_API_KEY": [
{"id": "stability-sdxl", "name": "Stability SDXL",
@@ -46,30 +55,27 @@
]
}
-
def get_directories() -> Tuple[str, Optional[str]]:
"""Get the absolute paths to the output and models directories from settings"""
settings = get_settings()
-
+
# Handle output directory
output_dir = settings.get('outputDirectory')
-
+
# Validate output directory
if not is_valid_directory(output_dir):
print("\nWarning: Invalid output directory (starts with '/path')")
- output_dir = os.path.join(
- parent_dir, 'Dream_Layer_Resources', 'output')
+ output_dir = os.path.join(parent_dir, 'Dream_Layer_Resources', 'output')
print(f"Using default output directory: {output_dir}")
-
+
# If output directory is not an absolute path, make it relative to parent_dir
if output_dir and not os.path.isabs(output_dir):
output_dir = os.path.join(parent_dir, output_dir)
-
+
# If no output directory specified, use default
if not output_dir:
- output_dir = os.path.join(
- parent_dir, 'Dream_Layer_Resources', 'output')
-
+ output_dir = os.path.join(parent_dir, 'Dream_Layer_Resources', 'output')
+
# Ensure output directory is absolute and exists
output_dir = os.path.abspath(output_dir)
os.makedirs(output_dir, exist_ok=True)
@@ -77,7 +83,7 @@ def get_directories() -> Tuple[str, Optional[str]]:
# Handle models directory
models_dir = settings.get('modelsDirectory')
-
+
# Validate models directory
if not is_valid_directory(models_dir):
print("\nWarning: Invalid models directory (starts with '/path')")
@@ -85,10 +91,9 @@ def get_directories() -> Tuple[str, Optional[str]]:
elif models_dir:
models_dir = os.path.abspath(models_dir)
print(f"Using models directory: {models_dir}")
-
+
return output_dir, models_dir
-
# Set directories before importing ComfyUI
output_dir, models_dir = get_directories()
sys.argv.extend(['--output-directory', output_dir])
@@ -105,17 +110,13 @@ def get_directories() -> Tuple[str, Optional[str]]:
sys.argv.extend(['--enable-cors-header', cors_origin])
# Only add ComfyUI to path if it exists and we need to start the server
-
-
def import_comfyui_main():
"""Import ComfyUI main module only when needed"""
if comfyui_dir not in sys.path:
sys.path.append(comfyui_dir)
-
+
try:
- import importlib.util
- spec = importlib.util.spec_from_file_location(
- "comfyui_main", os.path.join(comfyui_dir, "main.py"))
+ spec = importlib.util.spec_from_file_location("comfyui_main", os.path.join(comfyui_dir, "main.py"))
if spec is None or spec.loader is None:
print("Error: Could not create module spec for ComfyUI main.py")
return None
@@ -127,7 +128,6 @@ def import_comfyui_main():
print(f"Current Python path: {sys.path}")
return None
-
# Create Flask app
app = Flask(__name__)
@@ -144,14 +144,13 @@ def import_comfyui_main():
COMFY_API_URL = "http://127.0.0.1:8188"
-
def get_available_models():
"""
Fetch available checkpoint models from ComfyUI and append closed-source models
"""
from shared_utils import get_model_display_name
formatted_models = []
-
+
# Get ComfyUI models
try:
response = requests.get(f"{COMFY_API_URL}/models/checkpoints")
@@ -169,25 +168,23 @@ def get_available_models():
print(f"Error fetching ComfyUI models: {response.status_code}")
except Exception as e:
print(f"Error fetching ComfyUI models: {str(e)}")
-
+
# Get closed-source models based on available API keys
try:
from dream_layer_backend_utils import read_api_keys_from_env
api_keys = read_api_keys_from_env()
-
+
# Append models for each available API key
for api_key_name, api_key_value in api_keys.items():
if api_key_name in API_KEY_TO_MODELS:
formatted_models.extend(API_KEY_TO_MODELS[api_key_name])
- print(
- f"Added {len(API_KEY_TO_MODELS[api_key_name])} models for {api_key_name}")
-
+ print(f"Added {len(API_KEY_TO_MODELS[api_key_name])} models for {api_key_name}")
+
except Exception as e:
print(f"Error fetching closed-source models: {str(e)}")
-
+
return formatted_models
-
@app.route('/api/models', methods=['GET'])
def handle_get_models():
"""
@@ -205,12 +202,10 @@ def handle_get_models():
"message": str(e)
}), 500
-
def save_settings(settings):
"""Save path settings to a file"""
try:
- settings_file = os.path.join(
- os.path.dirname(__file__), 'settings.json')
+ settings_file = os.path.join(os.path.dirname(__file__), 'settings.json')
with open(settings_file, 'w') as f:
json.dump(settings, f, indent=2)
print("Settings saved successfully")
@@ -219,7 +214,6 @@ def save_settings(settings):
print(f"Error saving settings: {e}")
return False
-
@app.route('/api/settings/paths', methods=['POST'])
def handle_path_settings():
"""Endpoint to handle path configuration settings"""
@@ -230,7 +224,7 @@ def handle_path_settings():
"status": "error",
"message": "No JSON data received"
}), 400
-
+
print("\n=== Received Path Configuration Settings ===")
print("Output Directory:", settings.get('outputDirectory'))
print("Models Directory:", settings.get('modelsDirectory'))
@@ -244,8 +238,7 @@ def handle_path_settings():
if save_settings(settings):
# Execute the restart script
- script_path = os.path.join(
- os.path.dirname(__file__), 'restart_server.sh')
+ script_path = os.path.join(os.path.dirname(__file__), 'restart_server.sh')
subprocess.Popen([script_path])
return jsonify({
"status": "success",
@@ -261,31 +254,26 @@ def handle_path_settings():
"message": str(e)
}), 500
-
def start_comfy_server():
"""Start the ComfyUI server"""
try:
- # Import ComfyUI main module
start_comfyui = import_comfyui_main()
if start_comfyui is None:
print("Error: Could not import ComfyUI start_comfyui function")
return False
-
- # Change to ComfyUI directory
+
os.chdir(comfyui_dir)
-
- # Start ComfyUI in a thread
+
def run_comfyui():
loop, server, start_func = start_comfyui()
x = start_func()
loop.run_until_complete(x)
-
+
comfy_thread = threading.Thread(target=run_comfyui, daemon=True)
comfy_thread.start()
-
- # Wait for server to be ready
+
start_time = time.time()
- while time.time() - start_time < 60: # 60 second timeout
+ while time.time() - start_time < 60:
try:
response = requests.get(COMFY_API_URL)
if response.status_code == 200:
@@ -293,32 +281,29 @@ def run_comfyui():
return True
except requests.exceptions.ConnectionError:
time.sleep(1)
-
+
print("Error: ComfyUI server failed to start within the timeout period")
return False
-
+
except Exception as e:
print(f"Error starting ComfyUI server: {e}")
return False
-
def start_flask_server():
"""Start the Flask API server"""
print("\nStarting Flask API server on http://localhost:5002")
app.run(host='0.0.0.0', port=5002, debug=True, use_reloader=False)
-
def get_available_lora_models():
"""
Fetch available LoRA models from ComfyUI
"""
from shared_utils import get_model_display_name
formatted_models = []
-
+
try:
models = get_lora_models()
-
- # Convert filenames to more user-friendly names (using display name mapping when available)
+
for filename in models:
name = get_model_display_name(filename)
formatted_models.append({
@@ -328,16 +313,14 @@ def get_available_lora_models():
})
except Exception as e:
print(f"Error fetching LoRA models: {str(e)}")
-
+
return formatted_models
-
@app.route('/', methods=['GET'])
def is_server_running():
return jsonify({
"status": "success"
- })
-
+ })
@app.route('/api/lora-models', methods=['GET'])
def handle_get_lora_models():
@@ -355,8 +338,7 @@ def handle_get_lora_models():
"status": "error",
"message": str(e)
}), 500
-
-
+
@app.route('/api/add-api-key', methods=['POST'])
def add_api_key():
"""
@@ -371,11 +353,10 @@ def add_api_key():
if not alias or not api_key:
return jsonify({"status": "error", "message": "Missing alias or api_key"}), 400
- env_path = os.path.join(os.path.dirname(
- os.path.dirname(__file__)), '.env')
+ env_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), '.env')
lines = []
found = False
-
+
if os.path.exists(env_path):
with open(env_path, 'r') as f:
lines = f.readlines()
@@ -397,29 +378,25 @@ def add_api_key():
except Exception as e:
return jsonify({"status": "error", "message": str(e)}), 500
-
@app.route('/api/fetch-prompt', methods=['GET'])
def fetch_prompt():
"""
Endpoint to fetch random prompts
"""
-
+
prompt_type = request.args.get('type')
print(f"๐ฏ FETCH PROMPT CALLED - Type: {prompt_type}")
-
+
prompt = fetch_positive_prompt() if prompt_type == 'positive' else fetch_negative_prompt()
return jsonify({"status": "success", "prompt": prompt})
-
@app.route('/api/upscaler-models', methods=['GET'])
def get_upscaler_models_endpoint():
models = get_upscaler_models()
- formatted = [{"id": m, "name": m.replace(
- '.pth', ''), "filename": m} for m in models]
+ formatted = [{"id": m, "name": m.replace('.pth', ''), "filename": m} for m in models]
return jsonify({"status": "success", "models": formatted})
-
@app.route('/api/show-in-folder', methods=['POST'])
def show_in_folder():
"""Show image file in system file manager (cross-platform)"""
@@ -427,34 +404,65 @@ def show_in_folder():
filename = request.json.get('filename')
if not filename:
return jsonify({"status": "error", "message": "No filename provided"}), 400
-
- output_dir, _ = get_directories()
- print(f"DEBUG: output_dir='{output_dir}', filename='{filename}'")
- image_path = os.path.join(output_dir, filename)
-
- if not os.path.exists(image_path):
+
+ image_path = None
+
+ if filename.startswith('matrix-grid'):
+ matrix_filepath = os.path.join(MATRIX_GRIDS_DIR, filename)
+ if os.path.exists(matrix_filepath):
+ image_path = matrix_filepath
+ print(f"๐ Found matrix grid in permanent storage: {image_path}")
+
+ if not image_path:
+ served_filepath = os.path.join(SERVED_IMAGES_DIR, filename)
+ if os.path.exists(served_filepath):
+ image_path = served_filepath
+ print(f"๐ Found in served_images: {image_path}")
+
+ if not image_path:
+ output_dir, _ = get_directories()
+ output_filepath = os.path.join(output_dir, filename)
+ if os.path.exists(output_filepath):
+ image_path = output_filepath
+ print(f"๐ Found in output_dir: {image_path}")
+
+ if not image_path and not filename.startswith('matrix-grid'):
+ matrix_filepath = os.path.join(MATRIX_GRIDS_DIR, filename)
+ if os.path.exists(matrix_filepath):
+ image_path = matrix_filepath
+ print(f"๐ Found in matrix_grids (fallback): {image_path}")
+
+ if not image_path:
+ print(f"โ File not found: {filename}")
+ print(f" Checked matrix_grids: {os.path.join(MATRIX_GRIDS_DIR, filename)}")
+ print(f" Checked served_images: {os.path.join(SERVED_IMAGES_DIR, filename)}")
+ output_dir, _ = get_directories()
+ output_filepath = os.path.join(output_dir, filename)
+ print(f" Checked output_dir: {output_filepath}")
return jsonify({"status": "error", "message": "File not found"}), 404
-
- # Detect operating system and use appropriate command
+
system = platform.system()
-
+
if system == "Darwin": # macOS
- subprocess.run(['open', '-R', image_path])
- return jsonify({"status": "success", "message": f"Opened {filename} in Finder"})
- elif system == "Windows": # Windows
- subprocess.run(['explorer', '/select,', image_path])
- return jsonify({"status": "success", "message": f"Opened {filename} in File Explorer"})
- elif system == "Linux": # Linux
- # Open the directory containing the file (can't highlight specific file reliably)
- subprocess.run(['xdg-open', output_dir])
- return jsonify({"status": "success", "message": f"Opened directory containing {filename}"})
+ subprocess.run(["open", "-R", image_path])
+ elif system == "Windows":
+ subprocess.run(["explorer", "/select,", image_path])
+ elif system == "Linux":
+ subprocess.run(["xdg-open", os.path.dirname(image_path)])
else:
return jsonify({"status": "error", "message": f"Unsupported operating system: {system}"}), 400
-
+
+ return jsonify({
+ "status": "success",
+ "message": f"Opened {filename} in file manager",
+ "path": image_path,
+ "storage": "permanent" if image_path.startswith(MATRIX_GRIDS_DIR) else "temporary"
+ })
+
except Exception as e:
+ print(f"โ Error showing file in folder: {str(e)}")
return jsonify({"status": "error", "message": str(e)}), 500
-
@app.route('/api/send-to-img2img', methods=['POST'])
def send_to_img2img():
"""Send image to img2img tab"""
@@ -462,95 +470,80 @@ def send_to_img2img():
filename = request.json.get('filename')
if not filename:
return jsonify({"status": "error", "message": "No filename provided"}), 400
-
+
output_dir, _ = get_directories()
image_path = os.path.join(output_dir, filename)
-
+
if not os.path.exists(image_path):
return jsonify({"status": "error", "message": "File not found"}), 404
-
+
return jsonify({"status": "success", "message": "Image sent to img2img"})
except Exception as e:
return jsonify({"status": "error", "message": str(e)}), 500
-
@app.route('/api/send-to-extras', methods=['POST', 'OPTIONS'])
def send_to_extras():
"""Send image to extras tab"""
if request.method == 'OPTIONS':
- # Respond to preflight request
response = jsonify({'status': 'ok'})
response.headers.add('Access-Control-Allow-Methods', 'POST, OPTIONS')
response.headers.add('Access-Control-Allow-Headers', 'Content-Type')
return response
-
+
try:
filename = request.json.get('filename')
if not filename:
return jsonify({"status": "error", "message": "No filename provided"}), 400
-
+
output_dir, _ = get_directories()
image_path = os.path.join(output_dir, filename)
-
+
if not os.path.exists(image_path):
return jsonify({"status": "error", "message": "File not found"}), 404
-
+
return jsonify({"status": "success", "message": "Image sent to extras"})
except Exception as e:
return jsonify({"status": "error", "message": str(e)}), 500
-
-
@app.route('/api/upload-controlnet-image', methods=['POST'])
def upload_controlnet_image():
- """
- Endpoint to upload ControlNet images directly to ComfyUI input directory
- """
try:
if 'file' not in request.files:
return jsonify({
"status": "error",
"message": "No file provided"
}), 400
-
+
file = request.files['file']
if file.filename == '':
return jsonify({
"status": "error",
"message": "No file selected"
}), 400
-
+
unit_index = request.form.get('unit_index', '0')
try:
unit_index = int(unit_index)
except ValueError:
unit_index = 0
-
- # Use shared function
+
from shared_utils import upload_controlnet_image as upload_cn_image
result = upload_cn_image(file, unit_index)
-
+
if isinstance(result, tuple):
return jsonify(result[0]), result[1]
else:
return jsonify(result)
-
+
except Exception as e:
print(f"โ Error uploading ControlNet image: {str(e)}")
- import traceback
traceback.print_exc()
return jsonify({
"status": "error",
"message": str(e)
}), 500
-
@app.route('/api/upload-model', methods=['POST'])
def upload_model():
- """
- Endpoint to upload model files to ComfyUI models directory
- Supports formats: .safetensors, .ckpt, .pth, .pt, .bin
- Supports types: checkpoints, loras, controlnet, upscale_models, vae, embeddings, hypernetworks
- """
try:
from shared_utils import upload_model_file
@@ -573,24 +566,18 @@ def upload_model():
except Exception as e:
print(f"โ Error in model upload endpoint: {str(e)}")
- import traceback
traceback.print_exc()
return jsonify({
"status": "error",
"message": str(e)
}), 500
-
@app.route('/api/images/', methods=['GET'])
def serve_image(filename):
- """
- Serve images from multiple possible directories
- """
try:
- # Use shared function
from shared_utils import serve_image as serve_img
return serve_img(filename)
-
+
except Exception as e:
print(f"โ Error serving image {filename}: {str(e)}")
return jsonify({
@@ -598,10 +585,8 @@ def serve_image(filename):
"message": str(e)
}), 500
-
@app.route('/api/controlnet/models', methods=['GET'])
def get_controlnet_models_endpoint():
- """Get available ControlNet models"""
try:
models = get_controlnet_models()
return jsonify({
@@ -614,6 +599,74 @@ def get_controlnet_models_endpoint():
"message": f"Failed to fetch ControlNet models: {str(e)}"
}), 500
+@app.route('/api/save-single-image', methods=['POST'])
+def save_single_image():
+ """
+ Save single generated image to server storage for persistence
+ """
+ try:
+ print(f"๐ Single image save request received on port 5002")
+ data = request.json
+
+ if not data or 'imageData' not in data:
+ print("โ No image data in request")
+ return jsonify({
+ "status": "error",
+ "message": "No image data provided"
+ }), 400
+
+ image_data = data['imageData']
+ if image_data.startswith('data:'):
+ image_data = image_data.split(',')[1]
+
+ print(f"๐ Received base64 data length: {len(image_data)}")
+
+ # Decode base64 image
+ image_bytes = base64.b64decode(image_data)
+ print(f"๐ Decoded image bytes: {len(image_bytes)}")
+
+ image = Image.open(io.BytesIO(image_bytes))
+ print(f"๐ Image dimensions: {image.size}")
+
+ timestamp = int(time.time() * 1000)
+ original_filename = data.get('originalFilename', 'image.png')
+ filename = f"single_{timestamp}_{original_filename}"
+ print(f"๐ Generated filename: {filename}")
+
+ # Save to the same directory as Matrix grids for consistency
+ os.makedirs(MATRIX_GRIDS_DIR, exist_ok=True)
+ filepath = os.path.join(MATRIX_GRIDS_DIR, filename)
+ print(f"๐ Full save path: {filepath}")
+
+ image.save(filepath, format='PNG')
+ print(f"๐พ Single image saved to permanent storage")
+
+ if os.path.exists(filepath):
+ file_size = os.path.getsize(filepath)
+ print(f"โ
Successfully saved single image: {filename}")
+ print(f"๐ File size: {file_size} bytes")
+
+ return jsonify({
+ "status": "success",
+ "filename": filename,
+ "url": f"http://localhost:5002/api/images/{filename}",
+ "filesize": file_size,
+ "storage": "permanent"
+ })
+ else:
+ print(f"โ File was not created: {filepath}")
+ return jsonify({
+ "status": "error",
+ "message": "Failed to save single image to permanent storage"
+ }), 500
+
+ except Exception as e:
+ print(f"โ Error saving single image: {str(e)}")
+ traceback.print_exc()
+ return jsonify({
+ "status": "error",
+ "message": str(e)
+ }), 500
if __name__ == "__main__":
print("Starting Dream Layer backend services...")
@@ -621,4 +674,4 @@ def get_controlnet_models_endpoint():
start_flask_server()
else:
print("Failed to start ComfyUI server. Exiting...")
- sys.exit(1)
+ sys.exit(1)
\ No newline at end of file
diff --git a/dream_layer_backend/dream_layer_backend_utils/api_key_injector.py b/dream_layer_backend/dream_layer_backend_utils/api_key_injector.py
index 4abae938..94e27eff 100644
--- a/dream_layer_backend/dream_layer_backend_utils/api_key_injector.py
+++ b/dream_layer_backend/dream_layer_backend_utils/api_key_injector.py
@@ -13,17 +13,17 @@
NODE_TO_API_KEY_MAPPING = {
# BFL Nodes (use direct API, but still need api_key_comfy_org for compatibility)
"FluxProUltraImageNode": "BFL_API_KEY",
- "FluxKontextProImageNode": "BFL_API_KEY",
+ "FluxKontextProImageNode": "BFL_API_KEY",
"FluxKontextMaxImageNode": "BFL_API_KEY",
"FluxProImageNode": "BFL_API_KEY",
"FluxProExpandNode": "BFL_API_KEY",
- "FluxProFillNode": "BFL_API_KEY",
+ "FluxProFillNode": "BFL_API_KEY",
"FluxProCannyNode": "BFL_API_KEY",
"FluxProDepthNode": "BFL_API_KEY",
-
+
# OpenAI Nodes (use ComfyUI proxy, need api_key_comfy_org)
"OpenAIDalle2": "OPENAI_API_KEY",
- "OpenAIDalle3": "OPENAI_API_KEY",
+ "OpenAIDalle3": "OPENAI_API_KEY",
"OpenAIGPTImage1": "OPENAI_API_KEY",
"OpenAITextNode": "OPENAI_API_KEY",
"OpenAIChatNode": "OPENAI_API_KEY",
@@ -61,20 +61,21 @@
"BFL_API_KEY": "api_key_comfy_org",
"OPENAI_API_KEY": "api_key_comfy_org",
"IDEOGRAM_API_KEY": "api_key_comfy_org",
+
"STABILITY_API_KEY": "stability_api_key", # Changed from COMFY_API_KEY
"COMFY_API_KEY": "api_key_comfy_org",
"COMFY_AUTH_TOKEN": "auth_token_comfy_org",
"GEMINI_API_KEY": "api_key_comfy_org",
"LUMA_API_KEY": "luma_api_key", # Direct API key for Luma
# Future additions:
+ # "GEMINI_API_KEY": "api_key_gemini",
# "ANTHROPIC_API_KEY": "api_key_anthropic",
}
-
def read_api_keys_from_env() -> Dict[str, str]:
"""
Read all API keys from environment variables.
-
+
Returns:
Dict containing environment variable names mapped to their values.
Example: {"BFL_API_KEY": "sk-bfl-...", "OPENAI_API_KEY": "sk-openai-..."}
@@ -82,27 +83,26 @@ def read_api_keys_from_env() -> Dict[str, str]:
# Get the path to the project's root directory
current_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(os.path.dirname(current_dir))
-
+
# Construct the path to the .env file in the root directory
dotenv_path = os.path.join(project_root, '.env')
-
+
# Load environment variables from the .env file in the project root
load_dotenv(dotenv_path=dotenv_path)
-
+
api_keys = {}
-
+
# Read all API keys defined in the mapping
for env_key in ENV_KEY_TO_EXTRA_DATA_MAPPING.keys():
api_key = os.getenv(env_key)
if api_key:
api_keys[env_key] = api_key
# Safely truncate for display without assuming length
- display_key = api_key[:8] + "..." + \
- api_key[-4:] if len(api_key) > 12 else api_key
+ display_key = api_key[:8] + "..." + api_key[-4:] if len(api_key) > 12 else api_key
print(f"[DEBUG] Found {env_key}: {display_key}")
else:
print(f"[DEBUG] No {env_key} found in environment")
-
+
print(f"[DEBUG] Total API keys loaded: {len(api_keys)}")
return api_keys
@@ -110,9 +110,16 @@ def read_api_keys_from_env() -> Dict[str, str]:
def inject_api_keys_into_workflow(workflow: Dict[str, Any], all_api_keys: Dict[str, str] = None) -> Dict[str, Any]:
"""
Inject API keys from environment variables into workflow extra_data based on nodes present.
-
+
Args:
workflow: The workflow dictionary to inject keys into
+
+ Returns:
+ Workflow with appropriate API keys added to extra_data
+ """
+ # Read all available API keys from environment
+ all_api_keys = read_api_keys_from_env()
+
all_api_keys: Optional dictionary of API keys. If None, reads from environment.
Returns:
@@ -124,18 +131,18 @@ def inject_api_keys_into_workflow(workflow: Dict[str, Any], all_api_keys: Dict[s
# Create a copy to avoid modifying the original
workflow_with_keys = workflow.copy()
-
+
# Ensure extra_data exists
if "extra_data" not in workflow_with_keys:
workflow_with_keys["extra_data"] = {}
print("[DEBUG] Created new extra_data section")
else:
print("[DEBUG] Using existing extra_data section")
-
+
# Scan workflow for node types and determine which API keys are needed
needed_env_keys = set()
workflow_prompt = workflow.get('prompt', {})
-
+
print("[DEBUG] Scanning workflow for API nodes...")
for node_id, node_data in workflow_prompt.items():
if isinstance(node_data, dict):
@@ -143,8 +150,7 @@ def inject_api_keys_into_workflow(workflow: Dict[str, Any], all_api_keys: Dict[s
if class_type in NODE_TO_API_KEY_MAPPING:
required_env_key = NODE_TO_API_KEY_MAPPING[class_type]
needed_env_keys.add(required_env_key)
- print(
- f"[DEBUG] Found {class_type} node - needs {required_env_key}")
+ print(f"[DEBUG] Found {class_type} node - needs {required_env_key}")
# Decide which key to use for api_key_comfy_org
api_key_comfy_org = None
print(f"[DEBUG] needed_env_keys: {needed_env_keys}")
@@ -162,13 +168,15 @@ def inject_api_keys_into_workflow(workflow: Dict[str, Any], all_api_keys: Dict[s
api_key_comfy_org = all_api_keys["IDEOGRAM_API_KEY"]
print(f"[DEBUG] Using IDEOGRAM_API_KEY for api_key_comfy_org")
else:
- print(
- f"[DEBUG] No available API keys for needed services: {needed_env_keys}")
-
+ print(f"[DEBUG] No available API keys for needed services: {needed_env_keys}")
+
# Add the chosen key to extra_data
if api_key_comfy_org:
workflow_with_keys["extra_data"]["api_key_comfy_org"] = api_key_comfy_org
print(f"[DEBUG] Injected api_key_comfy_org into workflow")
+ else:
+ print("[DEBUG] No API keys needed for this workflow")
+
# Special handling for Stability AI nodes - inject stability_api_key directly
has_stability_nodes = False
@@ -211,5 +219,5 @@ def inject_api_keys_into_workflow(workflow: Dict[str, Any], all_api_keys: Dict[s
print("[DEBUG] No Luma nodes found in workflow")
print(f"[DEBUG] Final extra_data: {workflow_with_keys['extra_data']}")
-
- return workflow_with_keys
+
+ return workflow_with_keys
\ No newline at end of file
diff --git a/dream_layer_backend/dream_layer_backend_utils/workflow_loader.py b/dream_layer_backend/dream_layer_backend_utils/workflow_loader.py
index 977abf5c..a1e98d45 100644
--- a/dream_layer_backend/dream_layer_backend_utils/workflow_loader.py
+++ b/dream_layer_backend/dream_layer_backend_utils/workflow_loader.py
@@ -12,15 +12,13 @@
logger = logging.getLogger(__name__)
-
def _determine_workflow_path(workflow_request: Dict[str, Any]) -> str:
"""Determine the workflow file path based on request parameters."""
generation_flow = workflow_request.get('generation_flow')
- # Convert to lowercase for case-insensitive comparison
- model_name = workflow_request.get('model_name', '').lower()
+ model_name = workflow_request.get('model_name', '').lower() # Convert to lowercase for case-insensitive comparison
controlnet = workflow_request.get('controlnet', False)
lora = workflow_request.get('lora', False)
-
+
# Determine workflow filename based on parameters
if 'bfl' in model_name or 'flux' in model_name:
filename = "bfl_core_generation_workflow.json"
@@ -40,35 +38,32 @@ def _determine_workflow_path(workflow_request: Dict[str, Any]) -> str:
filename = "local_lora.json"
else:
filename = "core_generation_workflow.json"
-
+
# Build full path
current_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
- workflow_path = os.path.join(
- current_dir, 'workflows', generation_flow, filename)
-
+ workflow_path = os.path.join(current_dir, 'workflows', generation_flow, filename)
+
if not os.path.exists(workflow_path):
raise FileNotFoundError(f"Workflow file not found: {workflow_path}")
-
+
return workflow_path
-
def _load_workflow_json(workflow_path: str) -> Dict[str, Any]:
"""Load and parse workflow JSON file."""
with open(workflow_path, 'r') as file:
return json.load(file)
-
def load_workflow(workflow_request: Dict[str, Any]) -> Dict[str, Any]:
"""
Load and configure a workflow based on the request parameters.
-
+
Args:
workflow_request: Dictionary containing:
- generation_flow: txt2img/img2img
- model_name: bfl/dalle/other
- controlnet: true/false
- lora: true/false
-
+
Returns:
Dict: Loaded workflow configuration
"""
@@ -81,10 +76,8 @@ def load_workflow(workflow_request: Dict[str, Any]) -> Dict[str, Any]:
logger.error(f"Error loading workflow: {str(e)}")
raise
-
def analyze_workflow(workflow: Dict[str, Any]) -> Dict[str, Any]:
"""Analyze workflow to determine batch size and if it uses API nodes."""
is_api = bool(workflow.get('extra_data'))
- batch_size = next((node['inputs'].get('batch_size', 1) for node in workflow.get(
- 'prompt', {}).values() if 'batch_size' in node.get('inputs', {})), 1)
- return {'batch_size': batch_size, 'is_api': is_api}
+ batch_size = next((node['inputs'].get('batch_size', 1) for node in workflow.get('prompt', {}).values() if 'batch_size' in node.get('inputs', {})), 1)
+ return {'batch_size': batch_size, 'is_api': is_api}
\ No newline at end of file
diff --git a/dream_layer_backend/extras.py b/dream_layer_backend/extras.py
index d46beceb..6564d5f6 100644
--- a/dream_layer_backend/extras.py
+++ b/dream_layer_backend/extras.py
@@ -37,7 +37,7 @@
os.makedirs(SERVED_IMAGES_DIR, exist_ok=True)
# Server URL for image serving
-SERVER_URL = "http://localhost:5002/api"
+SERVER_URL = "http://localhost:5003"
def verify_input_directory():
"""Verify that the input directory exists and is writable"""
diff --git a/dream_layer_backend/img2img_server.py b/dream_layer_backend/img2img_server.py
index f16893e0..66b65cab 100644
--- a/dream_layer_backend/img2img_server.py
+++ b/dream_layer_backend/img2img_server.py
@@ -4,16 +4,14 @@
import json
import logging
import os
-import requests
from PIL import Image
import io
import time
+from shared_utils import serve_image
from shared_utils import send_to_comfyui
from img2img_workflow import transform_to_img2img_workflow
from shared_utils import COMFY_API_URL
from dream_layer_backend_utils.fetch_advanced_models import get_controlnet_models
-from run_registry import create_run_config_from_generation_data
-from dataclasses import asdict
# Configure logging
logging.basicConfig(
@@ -217,40 +215,11 @@ def handle_img2img():
logger.info(f" Subfolder: {img.get('subfolder', 'None')}")
logger.info(f" URL: {img.get('url')}")
- # Extract generated image filenames
- generated_images = []
- if comfy_response.get("generated_images"):
- for img_data in comfy_response["generated_images"]:
- if isinstance(img_data, dict) and "filename" in img_data:
- generated_images.append(img_data["filename"])
-
- # Register the completed run
- try:
- run_config = create_run_config_from_generation_data(
- data, generated_images, "img2img"
- )
-
- # Send to run registry
- registry_response = requests.post(
- "http://localhost:5005/api/runs",
- json=asdict(run_config),
- timeout=5
- )
-
- if registry_response.status_code == 200:
- logger.info(f"โ
Run registered successfully: {run_config.run_id}")
- else:
- logger.warning(f"โ ๏ธ Failed to register run: {registry_response.text}")
-
- except Exception as e:
- logger.warning(f"โ ๏ธ Error registering run: {str(e)}")
-
response = jsonify({
"status": "success",
"message": "Workflow sent to ComfyUI successfully",
"comfy_response": comfy_response,
- "workflow": workflow,
- "run_id": run_config.run_id if 'run_config' in locals() else None
+ "workflow": workflow
})
# Clean up the temporary image file
@@ -279,8 +248,6 @@ def handle_img2img_interrupt():
def serve_image_endpoint(filename):
"""Serve images from the served_images directory"""
try:
- # Use shared function
- from shared_utils import serve_image
return serve_image(filename)
except Exception as e:
logger.error(f"Error serving image {filename}: {e}")
diff --git a/dream_layer_backend/img2img_workflow.py b/dream_layer_backend/img2img_workflow.py
index 771048f7..694f7ea6 100644
--- a/dream_layer_backend/img2img_workflow.py
+++ b/dream_layer_backend/img2img_workflow.py
@@ -16,7 +16,7 @@
import random
import re
import logging
-from dream_layer import get_directories
+from dream_layer import get_directories
from extras import COMFY_INPUT_DIR
@@ -34,8 +34,7 @@ def transform_to_img2img_workflow(data):
use_lora = bool(data.get('lora'))
# Select the correct workflow template path
- workflow_template_path = get_img2img_workflow_template(
- model_name, use_controlnet, use_lora)
+ workflow_template_path = get_img2img_workflow_template(model_name, use_controlnet, use_lora)
# Load the workflow from the template file
with open(workflow_template_path, 'r') as f:
@@ -52,23 +51,21 @@ def transform_to_img2img_workflow(data):
output_dir, _ = get_directories()
logger.info(f"\nUsing output directory: {output_dir}")
- # Process ControlNet data if present
+ # Process ControlNet data if present
controlnet_data = data.get('controlnet')
if controlnet_data and validate_controlnet_config(controlnet_data):
logger.info("Processing ControlNet configuration...")
try:
- controlnet_data = process_controlnet_images(
- controlnet_data, COMFY_INPUT_DIR)
+ controlnet_data = process_controlnet_images(controlnet_data, COMFY_INPUT_DIR)
logger.info("ControlNet images processed successfully")
except Exception as e:
logger.error(f"Error processing ControlNet images: {str(e)}")
controlnet_data = None
else:
if controlnet_data:
- logger.warning(
- "Invalid ControlNet configuration, ignoring ControlNet")
+ logger.warning("Invalid ControlNet configuration, ignoring ControlNet")
controlnet_data = None
-
+
# Extract parameters with validation and type conversion
prompt = data.get('prompt', '')
negative_prompt = data.get('negative_prompt', '')
@@ -77,32 +74,30 @@ def transform_to_img2img_workflow(data):
batch_size = max(1, min(8, int(data.get('batch_size', 1))))
steps = max(1, min(150, int(data.get('steps', 20))))
cfg_scale = max(1.0, min(20.0, float(data.get('cfg_scale', 7.0))))
- denoising_strength = max(
- 0.0, min(1.0, float(data.get('denoising_strength', 0.75))))
+ denoising_strength = max(0.0, min(1.0, float(data.get('denoising_strength', 0.75))))
input_image = data.get('input_image', '')
model_name = data.get('model_name', 'v1-6-pruned-emaonly-fp16.safetensors')
sampler_name = data.get('sampler_name', 'euler')
scheduler = data.get('scheduler', 'normal')
-
+
# Advanced settings
vae_name = data.get('vae_name')
clip_skip = data.get('clip_skip', 1)
tiling = data.get('tiling', False)
hires_fix = data.get('hires_fix', False)
karras_sigmas = data.get('karras_sigmas', False)
-
+
# Handle seed - ensure it's a positive integer
try:
seed = int(data.get('seed', 0))
except (ValueError, TypeError):
seed = 0
-
+
# Generate a random positive seed if seed is 0 or negative
if seed <= 0:
- # Using 2^31-1 as max to ensure it's well within safe integer range
- seed = random.randint(1, 2**31 - 1)
+ seed = random.randint(1, 2**31 - 1) # Using 2^31-1 as max to ensure it's well within safe integer range
logger.info(f"Generated random seed: {seed}")
-
+
# Update the data with the actual seed used
data['seed'] = seed
@@ -126,7 +121,7 @@ def transform_to_img2img_workflow(data):
# Log the processed parameters
logger.info("Core Generation Settings")
logger.info(json.dumps(core_generation_settings, indent=4))
-
+
# Create the ComfyUI workflow
# The old hardcoded workflow dict is removed, so this block is now empty.
# The workflow object is now loaded directly from the template.
@@ -139,20 +134,18 @@ def transform_to_img2img_workflow(data):
"vae_name": vae_name
}
}
-
+
# The original workflow loading logic using load_workflow is removed.
# The workflow object is now directly loaded from the template.
# Check if custom workflow is provided and use it instead of the default workflow
custom_workflow = data.get('custom_workflow')
if custom_workflow and validate_custom_workflow(custom_workflow):
- logger.info(
- "Custom workflow detected, updating with current parameters...")
+ logger.info("Custom workflow detected, updating with current parameters...")
try:
# Update the custom workflow with the current parameters
workflow = update_custom_workflow(workflow, custom_workflow)
- logger.info(
- "Successfully updated custom workflow with current parameters")
+ logger.info("Successfully updated custom workflow with current parameters")
except Exception as e:
logger.error(f"Error updating custom workflow: {str(e)}")
logger.info("Falling back to default workflow")
@@ -160,29 +153,29 @@ def transform_to_img2img_workflow(data):
# Update the default workflow with the current parameters
workflow = override_workflow(workflow, core_generation_settings)
# Update image paths in the workflow
- workflow = update_image_paths_in_workflow(
- workflow, os.path.join(COMFY_INPUT_DIR, input_image))
+ workflow = update_image_paths_in_workflow(workflow, os.path.join(COMFY_INPUT_DIR, input_image))
logger.info("No valid custom workflow provided, using default workflow")
-
+
# Log the generated workflow
logger.info("Generated workflow:")
logger.info(json.dumps(workflow, indent=2))
- # Inject ControlNet into the workflow if present
+ # Inject ControlNet into the workflow if present
if controlnet_data:
logger.info("Injecting ControlNet into workflow...")
try:
- workflow = inject_controlnet_into_workflow(
- workflow, controlnet_data, COMFY_INPUT_DIR)
+ workflow = inject_controlnet_into_workflow(workflow, controlnet_data, COMFY_INPUT_DIR)
logger.info("ControlNet successfully injected into workflow")
-
+
+
except Exception as e:
logger.error(f"Error injecting ControlNet into workflow: {str(e)}")
else:
- logger.info(
- "No ControlNet data provided - skipping ControlNet injection")
-
+ logger.info("No ControlNet data provided - skipping ControlNet injection")
+
# Inject API keys from environment variables into the workflow
+ workflow = inject_api_keys_into_workflow(workflow)
+
all_api_keys = read_api_keys_from_env()
workflow = inject_api_keys_into_workflow(workflow, all_api_keys)
@@ -216,8 +209,7 @@ def transform_to_img2img_workflow(data):
# Inject advanced options if enabled
if face_restoration_data['restore_faces']:
logger.info("Injecting Face Restoration parameters...")
- workflow = inject_face_restoration_parameters(
- workflow, face_restoration_data)
+ workflow = inject_face_restoration_parameters(workflow, face_restoration_data)
if tiling_data['tiling']:
logger.info("Injecting Tiling parameters...")
workflow = inject_tiling_parameters(workflow, tiling_data)
@@ -227,29 +219,27 @@ def transform_to_img2img_workflow(data):
if refiner_data['refiner_enabled']:
logger.info("Injecting Refiner parameters...")
workflow = inject_refiner_parameters(workflow, refiner_data)
-
+
return workflow
-
def extract_filename_from_data_url(data_url):
"""Extract filename from data URL if present in the format data:image/...;name=filename.ext;base64,..."""
if not data_url:
return None
-
+
# Try to find name parameter in the data URL
name_match = re.search(r';name=(.*?);', data_url)
if name_match:
return name_match.group(1)
return None
-
def get_img2img_workflow_template(model_name, use_controlnet=False, use_lora=False):
model_name_lower = model_name.lower()
-
+
# Check for BFL/Flux models first (they have their own workflow)
if "bfl" in model_name_lower or "flux" in model_name_lower:
return "workflows/img2img/bfl_core_generation_workflow.json"
-
+
# Check for Ideogram models
elif "ideogram" in model_name_lower:
return "workflows/img2img/ideogram_core_generation_workflow.json"
diff --git a/dream_layer_backend/img2txt_server.py b/dream_layer_backend/img2txt_server.py
deleted file mode 100644
index cbe43a55..00000000
--- a/dream_layer_backend/img2txt_server.py
+++ /dev/null
@@ -1,208 +0,0 @@
-from flask import Flask, request, jsonify
-from flask_cors import CORS
-import json
-import os
-import base64
-import tempfile
-import requests
-from io import BytesIO
-from PIL import Image
-from dream_layer import get_directories
-from dream_layer_backend_utils import interrupt_workflow
-from dream_layer_backend_utils.api_key_injector import inject_api_keys_into_workflow
-from shared_utils import send_to_comfyui
-
-app = Flask(__name__)
-CORS(app, resources={
- r"/*": {
- "origins": ["http://localhost:*", "http://127.0.0.1:*"],
- "methods": ["GET", "POST", "OPTIONS"],
- "allow_headers": ["Content-Type"]
- }
-})
-
-# Get served images directory
-output_dir, _ = get_directories()
-SERVED_IMAGES_DIR = os.path.join(os.path.dirname(__file__), 'served_images')
-os.makedirs(SERVED_IMAGES_DIR, exist_ok=True)
-
-def load_img2txt_workflow():
- """Load the Gemini multimodal workflow for img2txt analysis"""
- workflow_path = os.path.join(os.path.dirname(__file__), 'workflows', 'img2img', 'gemini_multimodal_workflow.json')
- try:
- with open(workflow_path, 'r') as f:
- return json.load(f)
- except Exception as e:
- print(f"Error loading Gemini multimodal workflow: {e}")
- return None
-
-def transform_to_img2txt_workflow(data):
- """Transform frontend data to ComfyUI img2txt workflow using full Gemini multimodal workflow"""
- workflow = load_img2txt_workflow()
- if not workflow:
- raise Exception("Could not load Gemini multimodal workflow")
-
- # Use the full workflow - we'll extract just the text from GeminiNode later
- workflow_copy = json.loads(json.dumps(workflow))
-
- # Process and save the input image
- if 'input_image' in data and data['input_image']:
- try:
- # Decode base64 image
- image_data = data['input_image'].split(',')[1] if ',' in data['input_image'] else data['input_image']
- image_bytes = base64.b64decode(image_data)
-
- # Create temporary file
- with tempfile.NamedTemporaryFile(delete=False, suffix='.png', dir=SERVED_IMAGES_DIR) as temp_file:
- temp_file.write(image_bytes)
- temp_filename = os.path.basename(temp_file.name)
-
- # Update workflow with the uploaded image
- workflow_copy['prompt']['1']['inputs']['image'] = temp_filename
-
- except Exception as e:
- print(f"Error processing input image: {e}")
- raise Exception(f"Failed to process input image: {str(e)}")
- else:
- raise Exception("No input image provided")
-
- # Update Gemini prompt if provided
- if 'prompt' in data and data['prompt']:
- custom_prompt = data['prompt']
- else:
- custom_prompt = "Analyze this image and provide a detailed description. Include: what objects/people you see, the scene setting, colors, mood, style, composition, and any notable details. Be descriptive and thorough."
-
- workflow_copy['prompt']['2']['inputs']['prompt'] = custom_prompt
-
- # Update model if provided
- if 'model' in data and data['model']:
- workflow_copy['prompt']['2']['inputs']['model'] = data['model']
-
- # Update seed if provided
- if 'seed' in data and data['seed'] is not None:
- workflow_copy['prompt']['2']['inputs']['seed'] = int(data['seed'])
-
- return workflow_copy
-
-def call_gemini_api_directly(data):
- """Call Gemini API directly bypassing ComfyUI"""
- from dream_layer_backend_utils.api_key_injector import read_api_keys_from_env
-
- # Get API key
- api_keys = read_api_keys_from_env()
- gemini_key = api_keys.get('GEMINI_API_KEY')
- if not gemini_key:
- raise Exception("GEMINI_API_KEY not found in environment")
-
- # Convert image to base64 for Gemini
- image_b64 = data['input_image'].split(',')[1] if ',' in data['input_image'] else data['input_image']
-
- # Prepare prompt
- prompt_text = data.get('prompt', "Analyze this image and provide a detailed description. Include: what objects/people you see, the scene setting, colors, mood, style, composition, and any notable details. Be descriptive and thorough.")
-
- # Gemini API request
- url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent"
- headers = {'Content-Type': 'application/json', 'X-goog-api-key': gemini_key}
- payload = {
- "contents": [{
- "parts": [
- {"text": prompt_text},
- {"inline_data": {"mime_type": "image/png", "data": image_b64}}
- ]
- }]
- }
-
- response = requests.post(url, headers=headers, json=payload)
- if response.status_code == 200:
- result = response.json()
- return result['candidates'][0]['content']['parts'][0]['text']
- else:
- raise Exception(f"Gemini API error: {response.status_code} - {response.text}")
-
-@app.route('/api/img2txt', methods=['POST', 'OPTIONS'])
-def handle_img2txt():
- """Handle image-to-text generation requests"""
- if request.method == 'OPTIONS':
- return jsonify({"status": "ok"})
-
- try:
- data = request.json
- if data:
- print("Img2Txt Data:", json.dumps({
- **data,
- 'input_image': 'BASE64_IMAGE_DATA' if 'input_image' in data else None
- }, indent=2))
-
- # Validate required fields
- if 'input_image' not in data or not data['input_image']:
- return jsonify({
- "status": "error",
- "message": "Missing required field: input_image"
- }), 400
-
- # Direct Gemini API call (bypass ComfyUI)
- gemini_response = call_gemini_api_directly(data)
- print(f"โ
Direct Gemini API response received: {len(gemini_response)} chars")
-
- # Format response to match expected structure
- comfy_response = {
- "status": "success",
- "text_output": gemini_response,
- "all_images": []
- }
-
- if "error" in comfy_response:
- return jsonify({
- "status": "error",
- "message": comfy_response["error"]
- }), 500
-
- response = jsonify({
- "status": "success",
- "message": "Image analysis completed successfully",
- "comfy_response": comfy_response,
- "generated_text": comfy_response.get("text_output", "No text output received")
- })
-
- return response
-
- else:
- return jsonify({
- "status": "error",
- "message": "No data received"
- }), 400
-
- except Exception as e:
- print(f"Error in handle_img2txt: {str(e)}")
- import traceback
- traceback.print_exc()
- return jsonify({
- "status": "error",
- "message": str(e)
- }), 500
-
-@app.route('/api/img2txt/interrupt', methods=['POST'])
-def handle_img2txt_interrupt():
- """Handle interruption of img2txt generation"""
- print("Interrupting img2txt generation...")
- success = interrupt_workflow()
- return jsonify({"status": "received", "interrupted": success})
-
-@app.route('/api/images/', methods=['GET'])
-def serve_image_endpoint(filename):
- """
- Serve images from multiple possible directories
- This endpoint is needed here because the frontend expects it on this port
- """
- try:
- # Use shared function
- from shared_utils import serve_image
- return serve_image(filename)
-
- except Exception as e:
- print(f"Error serving image {filename}: {str(e)}")
- return jsonify({"error": str(e)}), 404
-
-if __name__ == '__main__':
- print("Starting Img2Txt server on port 5007...")
- app.run(debug=True, host='0.0.0.0', port=5007)
diff --git a/dream_layer_backend/report_bundle.py b/dream_layer_backend/report_bundle.py
deleted file mode 100644
index 0faf0107..00000000
--- a/dream_layer_backend/report_bundle.py
+++ /dev/null
@@ -1,367 +0,0 @@
-import os
-import csv
-import json
-import zipfile
-import shutil
-from datetime import datetime
-from typing import List, Dict, Any, Optional
-from dataclasses import asdict
-from flask import Flask, jsonify, request, send_file
-from flask_cors import CORS
-import requests
-from run_registry import RunRegistry, RunConfig
-
-class ReportBundleGenerator:
- """Generates report bundles with CSV, config, images, and README"""
-
- def __init__(self, output_dir: str = "Dream_Layer_Resources/output"):
- self.output_dir = output_dir
- self.registry = RunRegistry()
-
- def generate_csv(self, runs: List[RunConfig]) -> str:
- """Generate results.csv with required columns"""
- csv_path = "temp_results.csv"
-
- # Define required CSV columns based on schema
- required_columns = [
- 'run_id',
- 'timestamp',
- 'model',
- 'vae',
- 'prompt',
- 'negative_prompt',
- 'seed',
- 'sampler',
- 'steps',
- 'cfg_scale',
- 'width',
- 'height',
- 'batch_size',
- 'batch_count',
- 'generation_type',
- 'image_paths',
- 'loras',
- 'controlnets',
- 'workflow_hash'
- ]
-
- with open(csv_path, 'w', newline='', encoding='utf-8') as csvfile:
- writer = csv.DictWriter(csvfile, fieldnames=required_columns)
- writer.writeheader()
-
- for run in runs:
- # Prepare loras and controlnets as JSON strings
- loras_json = json.dumps(run.loras) if run.loras else "[]"
- controlnets_json = json.dumps(run.controlnets) if run.controlnets else "[]"
-
- # Create workflow hash for identification
- workflow_hash = str(hash(json.dumps(run.workflow, sort_keys=True)))
-
- # Join image paths
- image_paths = ";".join(run.generated_images) if run.generated_images else ""
-
- row = {
- 'run_id': run.run_id,
- 'timestamp': run.timestamp,
- 'model': run.model,
- 'vae': run.vae or "",
- 'prompt': run.prompt,
- 'negative_prompt': run.negative_prompt,
- 'seed': run.seed,
- 'sampler': run.sampler,
- 'steps': run.steps,
- 'cfg_scale': run.cfg_scale,
- 'width': run.width,
- 'height': run.height,
- 'batch_size': run.batch_size,
- 'batch_count': run.batch_count,
- 'generation_type': run.generation_type,
- 'image_paths': image_paths,
- 'loras': loras_json,
- 'controlnets': controlnets_json,
- 'workflow_hash': workflow_hash
- }
- writer.writerow(row)
-
- return csv_path
-
- def validate_csv_schema(self, csv_path: str) -> bool:
- """Validate that CSV has all required columns"""
- try:
- with open(csv_path, 'r', encoding='utf-8') as csvfile:
- reader = csv.DictReader(csvfile)
- fieldnames = reader.fieldnames
-
- required_columns = [
- 'run_id', 'timestamp', 'model', 'vae', 'prompt',
- 'negative_prompt', 'seed', 'sampler', 'steps', 'cfg_scale',
- 'width', 'height', 'batch_size', 'batch_count',
- 'generation_type', 'image_paths', 'loras', 'controlnets', 'workflow_hash'
- ]
-
- missing_columns = [col for col in required_columns if col not in fieldnames]
- if missing_columns:
- print(f"โ Missing required columns: {missing_columns}")
- return False
-
- print(f"โ
CSV schema validation passed")
- return True
-
- except Exception as e:
- print(f"โ CSV schema validation failed: {e}")
- return False
-
- def copy_images_to_bundle(self, runs: List[RunConfig], bundle_dir: str) -> List[str]:
- """Copy selected grid images to bundle directory"""
- copied_images = []
-
- for run in runs:
- for image_filename in run.generated_images:
- if image_filename:
- # Source path in output directory
- src_path = os.path.join(self.output_dir, image_filename)
-
- if os.path.exists(src_path):
- # Destination in bundle
- dest_path = os.path.join(bundle_dir, "images", image_filename)
- os.makedirs(os.path.dirname(dest_path), exist_ok=True)
-
- try:
- shutil.copy2(src_path, dest_path)
- copied_images.append(image_filename)
- print(f"โ
Copied image: {image_filename}")
- except Exception as e:
- print(f"โ Failed to copy {image_filename}: {e}")
- else:
- print(f"โ ๏ธ Image not found: {src_path}")
-
- return copied_images
-
- def create_config_json(self, runs: List[RunConfig]) -> str:
- """Create config.json with run configurations"""
- config_data = {
- "report_metadata": {
- "generated_at": datetime.now().isoformat(),
- "total_runs": len(runs),
- "generation_types": list(set(run.generation_type for run in runs)),
- "models_used": list(set(run.model for run in runs))
- },
- "runs": [asdict(run) for run in runs]
- }
-
- config_path = "temp_config.json"
- with open(config_path, 'w', encoding='utf-8') as f:
- json.dump(config_data, f, indent=2, ensure_ascii=False)
-
- return config_path
-
- def create_readme(self, runs: List[RunConfig], copied_images: List[str]) -> str:
- """Create README.md for the report bundle"""
- readme_content = f"""# Dream Layer Report Bundle
-
-Generated on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
-
-## Overview
-This report bundle contains {len(runs)} completed image generation runs with their configurations and results.
-
-## Contents
-- `results.csv` - Tabular data of all runs with metadata
-- `config.json` - Detailed configuration data for each run
-- `images/` - Generated images from all runs
-- `README.md` - This file
-
-## Statistics
-- Total runs: {len(runs)}
-- Generation types: {', '.join(set(run.generation_type for run in runs))}
-- Models used: {', '.join(set(run.model for run in runs))}
-- Images included: {len(copied_images)}
-
-## CSV Schema
-The results.csv file contains the following columns:
-- run_id: Unique identifier for each run
-- timestamp: When the run was executed
-- model: Model used for generation
-- vae: VAE model (if any)
-- prompt: Positive prompt
-- negative_prompt: Negative prompt
-- seed: Random seed used
-- sampler: Sampling method
-- steps: Number of sampling steps
-- cfg_scale: CFG scale value
-- width/height: Image dimensions
-- batch_size/batch_count: Batch settings
-- generation_type: txt2img or img2img
-- image_paths: Semicolon-separated list of generated image filenames
-- loras: JSON array of LoRA configurations
-- controlnets: JSON array of ControlNet configurations
-- workflow_hash: Hash of the workflow configuration
-
-## File Paths
-All image paths in the CSV resolve to files present in this zip bundle.
-"""
-
- readme_path = "temp_README.md"
- with open(readme_path, 'w', encoding='utf-8') as f:
- f.write(readme_content)
-
- return readme_path
-
- def create_report_bundle(self, run_ids: Optional[List[str]] = None) -> str:
- """Create a complete report bundle"""
-
- # Get runs to include
- if run_ids:
- runs = [self.registry.get_run(run_id) for run_id in run_ids if self.registry.get_run(run_id)]
- else:
- runs = self.registry.get_all_runs()
-
- if not runs:
- raise ValueError("No runs found to include in report")
-
- print(f"๐ Creating report bundle with {len(runs)} runs")
-
- # Create temporary directory for bundle
- bundle_dir = f"temp_report_bundle_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
- os.makedirs(bundle_dir, exist_ok=True)
-
- try:
- # Generate CSV
- print("๐ Generating results.csv...")
- csv_path = self.generate_csv(runs)
-
- # Validate CSV schema
- if not self.validate_csv_schema(csv_path):
- raise ValueError("CSV schema validation failed")
-
- # Copy CSV to bundle
- shutil.copy2(csv_path, os.path.join(bundle_dir, "results.csv"))
-
- # Create config.json
- print("โ๏ธ Creating config.json...")
- config_path = self.create_config_json(runs)
- shutil.copy2(config_path, os.path.join(bundle_dir, "config.json"))
-
- # Copy images
- print("๐ผ๏ธ Copying images...")
- copied_images = self.copy_images_to_bundle(runs, bundle_dir)
-
- # Create README
- print("๐ Creating README.md...")
- readme_path = self.create_readme(runs, copied_images)
- shutil.copy2(readme_path, os.path.join(bundle_dir, "README.md"))
-
- # Create zip file
- print("๐ฆ Creating report.zip...")
- zip_path = "report.zip"
- with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
- for root, dirs, files in os.walk(bundle_dir):
- for file in files:
- file_path = os.path.join(root, file)
- arcname = os.path.relpath(file_path, bundle_dir)
- zipf.write(file_path, arcname)
-
- # Cleanup temp files
- for temp_file in [csv_path, config_path, readme_path]:
- if os.path.exists(temp_file):
- os.remove(temp_file)
-
- # Cleanup temp directory
- shutil.rmtree(bundle_dir)
-
- print(f"โ
Report bundle created: {zip_path}")
- return zip_path
-
- except Exception as e:
- # Cleanup on error
- if os.path.exists(bundle_dir):
- shutil.rmtree(bundle_dir)
- raise e
-
-# Flask app for report bundle API
-app = Flask(__name__)
-CORS(app, resources={
- r"/*": {
- "origins": ["http://localhost:*", "http://127.0.0.1:*"],
- "methods": ["GET", "POST", "OPTIONS"],
- "allow_headers": ["Content-Type"]
- }
-})
-
-generator = ReportBundleGenerator()
-
-@app.route('/api/report-bundle', methods=['POST'])
-def create_report_bundle():
- """Create a report bundle with selected runs"""
- try:
- data = request.json or {}
- run_ids = data.get('run_ids', []) # Empty list means all runs
-
- zip_path = generator.create_report_bundle(run_ids)
-
- return jsonify({
- "status": "success",
- "message": "Report bundle created successfully",
- "file_path": zip_path
- })
-
- except Exception as e:
- return jsonify({
- "status": "error",
- "message": str(e)
- }), 500
-
-@app.route('/api/report-bundle/download', methods=['GET'])
-def download_report_bundle():
- """Download the generated report.zip file"""
- try:
- zip_path = "report.zip"
- if not os.path.exists(zip_path):
- return jsonify({
- "status": "error",
- "message": "Report bundle not found. Please generate one first."
- }), 404
-
- return send_file(
- zip_path,
- as_attachment=True,
- download_name="report.zip",
- mimetype="application/zip"
- )
-
- except Exception as e:
- return jsonify({
- "status": "error",
- "message": str(e)
- }), 500
-
-@app.route('/api/report-bundle/validate', methods=['POST'])
-def validate_report_bundle():
- """Validate a report bundle schema"""
- try:
- data = request.json or {}
- csv_content = data.get('csv_content', '')
-
- # Write CSV content to temp file for validation
- temp_csv = "temp_validation.csv"
- with open(temp_csv, 'w', encoding='utf-8') as f:
- f.write(csv_content)
-
- is_valid = generator.validate_csv_schema(temp_csv)
-
- # Cleanup
- if os.path.exists(temp_csv):
- os.remove(temp_csv)
-
- return jsonify({
- "status": "success",
- "valid": is_valid
- })
-
- except Exception as e:
- return jsonify({
- "status": "error",
- "message": str(e)
- }), 500
-
-if __name__ == '__main__':
- app.run(host='0.0.0.0', port=5006, debug=True)
\ No newline at end of file
diff --git a/dream_layer_backend/run_registry.py b/dream_layer_backend/run_registry.py
deleted file mode 100644
index e8ce4f20..00000000
--- a/dream_layer_backend/run_registry.py
+++ /dev/null
@@ -1,236 +0,0 @@
-import json
-import os
-import time
-import uuid
-from datetime import datetime
-from typing import Dict, List, Any, Optional
-from dataclasses import dataclass, asdict
-from flask import Flask, jsonify, request
-from flask_cors import CORS
-
-@dataclass
-class RunConfig:
- """Represents a frozen configuration for a completed run"""
- run_id: str
- timestamp: str
- model: str
- vae: Optional[str]
- loras: List[Dict[str, Any]]
- controlnets: List[Dict[str, Any]]
- prompt: str
- negative_prompt: str
- seed: int
- sampler: str
- steps: int
- cfg_scale: float
- width: int
- height: int
- batch_size: int
- batch_count: int
- workflow: Dict[str, Any]
- version: str
- generated_images: List[str]
- generation_type: str # "txt2img" or "img2img"
-
-class RunRegistry:
- """Manages completed runs and their configurations"""
-
- def __init__(self, storage_file: str = "run_registry.json"):
- self.storage_file = storage_file
- self.runs: Dict[str, RunConfig] = {}
- self.load_runs()
-
- def load_runs(self):
- """Load runs from storage file"""
- try:
- if os.path.exists(self.storage_file):
- with open(self.storage_file, 'r', encoding='utf-8') as f:
- data = json.load(f)
- for run_id, run_data in data.items():
- self.runs[run_id] = RunConfig(**run_data)
- except Exception as e:
- print(f"Error loading run registry: {e}")
-
- def save_runs(self):
- """Save runs to storage file"""
- try:
- data = {run_id: asdict(run_config) for run_id, run_config in self.runs.items()}
- with open(self.storage_file, 'w', encoding='utf-8') as f:
- json.dump(data, f, indent=2, ensure_ascii=False)
- except Exception as e:
- print(f"Error saving run registry: {e}")
-
- def add_run(self, config: RunConfig):
- """Add a completed run to the registry"""
- self.runs[config.run_id] = config
- self.save_runs()
-
- def get_run(self, run_id: str) -> Optional[RunConfig]:
- """Get a specific run by ID"""
- return self.runs.get(run_id)
-
- def get_all_runs(self) -> List[RunConfig]:
- """Get all runs sorted by timestamp (newest first)"""
- return sorted(self.runs.values(), key=lambda x: x.timestamp, reverse=True)
-
- def delete_run(self, run_id: str) -> bool:
- """Delete a run from the registry"""
- if run_id in self.runs:
- del self.runs[run_id]
- self.save_runs()
- return True
- return False
-
-# Global registry instance
-registry = RunRegistry()
-
-def create_run_config_from_generation_data(
- generation_data: Dict[str, Any],
- generated_images: List[str],
- generation_type: str
-) -> RunConfig:
- """Create a RunConfig from generation data"""
-
- # Extract configuration from generation data
- config = RunConfig(
- run_id=str(uuid.uuid4()),
- timestamp=datetime.now().isoformat(),
- model=generation_data.get('model_name', 'unknown'),
- vae=generation_data.get('vae_name'),
- loras=generation_data.get('lora', []),
- controlnets=generation_data.get('controlnet', {}).get('units', []),
- prompt=generation_data.get('prompt', ''),
- negative_prompt=generation_data.get('negative_prompt', ''),
- seed=generation_data.get('seed', 0),
- sampler=generation_data.get('sampler_name', 'euler'),
- steps=generation_data.get('steps', 20),
- cfg_scale=generation_data.get('cfg_scale', 7.0),
- width=generation_data.get('width', 512),
- height=generation_data.get('height', 512),
- batch_size=generation_data.get('batch_size', 1),
- batch_count=generation_data.get('batch_count', 1),
- workflow=generation_data.get('workflow', {}),
- version="1.0.0", # TODO: Get from app version
- generated_images=generated_images,
- generation_type=generation_type
- )
-
- return config
-
-# Flask app for run registry API
-app = Flask(__name__)
-CORS(app, resources={
- r"/*": {
- "origins": ["http://localhost:*", "http://127.0.0.1:*"],
- "methods": ["GET", "POST", "DELETE", "OPTIONS"],
- "allow_headers": ["Content-Type"]
- }
-})
-
-@app.route('/api/runs', methods=['GET'])
-def get_runs():
- """Get all completed runs"""
- try:
- runs = registry.get_all_runs()
- return jsonify({
- "status": "success",
- "runs": [asdict(run) for run in runs]
- })
- except Exception as e:
- return jsonify({
- "status": "error",
- "message": str(e)
- }), 500
-
-@app.route('/api/runs/', methods=['GET'])
-def get_run(run_id: str):
- """Get a specific run by ID"""
- try:
- run = registry.get_run(run_id)
- if run:
- return jsonify({
- "status": "success",
- "run": asdict(run)
- })
- else:
- return jsonify({
- "status": "error",
- "message": "Run not found"
- }), 404
- except Exception as e:
- return jsonify({
- "status": "error",
- "message": str(e)
- }), 500
-
-@app.route('/api/runs', methods=['POST'])
-def add_run():
- """Add a new completed run"""
- try:
- data = request.json
- if not data:
- return jsonify({
- "status": "error",
- "message": "No data provided"
- }), 400
-
- # Create run config from the provided data
- run_config = RunConfig(
- run_id=data.get('run_id', str(uuid.uuid4())),
- timestamp=data.get('timestamp', datetime.now().isoformat()),
- model=data.get('model', 'unknown'),
- vae=data.get('vae'),
- loras=data.get('loras', []),
- controlnets=data.get('controlnets', []),
- prompt=data.get('prompt', ''),
- negative_prompt=data.get('negative_prompt', ''),
- seed=data.get('seed', 0),
- sampler=data.get('sampler', 'euler'),
- steps=data.get('steps', 20),
- cfg_scale=data.get('cfg_scale', 7.0),
- width=data.get('width', 512),
- height=data.get('height', 512),
- batch_size=data.get('batch_size', 1),
- batch_count=data.get('batch_count', 1),
- workflow=data.get('workflow', {}),
- version=data.get('version', '1.0.0'),
- generated_images=data.get('generated_images', []),
- generation_type=data.get('generation_type', 'txt2img')
- )
-
- registry.add_run(run_config)
-
- return jsonify({
- "status": "success",
- "run_id": run_config.run_id,
- "message": "Run added successfully"
- })
- except Exception as e:
- return jsonify({
- "status": "error",
- "message": str(e)
- }), 500
-
-@app.route('/api/runs/', methods=['DELETE'])
-def delete_run(run_id: str):
- """Delete a run"""
- try:
- success = registry.delete_run(run_id)
- if success:
- return jsonify({
- "status": "success",
- "message": "Run deleted successfully"
- })
- else:
- return jsonify({
- "status": "error",
- "message": "Run not found"
- }), 404
- except Exception as e:
- return jsonify({
- "status": "error",
- "message": str(e)
- }), 500
-
-if __name__ == '__main__':
- app.run(host='0.0.0.0', port=5005, debug=True)
\ No newline at end of file
diff --git a/dream_layer_backend/shared_utils.py b/dream_layer_backend/shared_utils.py
index a5952bff..47348620 100644
--- a/dream_layer_backend/shared_utils.py
+++ b/dream_layer_backend/shared_utils.py
@@ -4,20 +4,21 @@
import requests
import copy
import json
+import sys
+import traceback
+from flask import send_file, jsonify
from typing import List, Dict, Any
from pathlib import Path
-from dream_layer import get_directories
+from dream_layer_backend_utils.workflow_loader import analyze_workflow
from dream_layer_backend_utils.update_custom_workflow import find_save_node
from dream_layer_backend_utils.shared_workflow_parameters import increment_seed_in_workflow
-# Global constants
COMFY_API_URL = "http://127.0.0.1:8188"
SERVED_IMAGES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'served_images')
+MATRIX_GRIDS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'matrix_grids')
-# Model display name mapping file
MODEL_DISPLAY_NAMES_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'model_display_names.json')
-# Display names are user-friendly names for models, stored in a JSON file
def load_model_display_names() -> Dict[str, str]:
"""Load the mapping of actual filenames to display names"""
try:
@@ -48,11 +49,9 @@ def get_model_display_name(filename: str) -> str:
if filename in mapping:
return mapping[filename]
- # Fallback: process the filename to create a display name
name = Path(filename).stem.replace('-', ' ').replace('_', ' ')
return ' '.join(word.capitalize() for word in name.split())
-# Sampler name mapping from frontend to ComfyUI
SAMPLER_NAME_MAP = {
'Euler': 'euler',
'Euler a': 'euler_ancestral',
@@ -74,38 +73,36 @@ def get_model_display_name(filename: str) -> str:
'DDIM': 'ddim',
'PLMS': 'plms'
}
+
os.makedirs(SERVED_IMAGES_DIR, exist_ok=True)
+os.makedirs(MATRIX_GRIDS_DIR, exist_ok=True)
def wait_for_image(prompt_id: str, save_node_id: str = "9", max_wait_time: int = 300) -> List[Dict[str, Any]]:
"""
Wait for image generation to complete and return the generated images
This is a shared function used by both txt2img and img2img servers
"""
- print("wait_for_image")
+ from dream_layer import get_directories
+
output_dir, _ = get_directories()
start_time = time.time()
while time.time() - start_time < max_wait_time:
try:
- # Check queue status
response = requests.get(f"{COMFY_API_URL}/queue")
if response.status_code == 200:
queue_data = response.json()
- # Check if our prompt is still in queue
running_queue = queue_data.get('queue_running', [])
pending_queue = queue_data.get('queue_pending', [])
- # Look for our prompt_id in running or pending queues
prompt_in_queue = any(item[1] == prompt_id for item in running_queue + pending_queue)
if not prompt_in_queue:
- # Prompt is no longer in queue, check for results
history_response = requests.get(f"{COMFY_API_URL}/history/{prompt_id}")
if history_response.status_code == 200:
history_data = history_response.json()
if prompt_id in history_data:
- # Get outputs from the save node
outputs = history_data[prompt_id].get('outputs', {})
if save_node_id in outputs:
images_data = outputs[save_node_id].get('images', [])
@@ -116,7 +113,6 @@ def wait_for_image(prompt_id: str, save_node_id: str = "9", max_wait_time: int =
filename = img_info.get('filename')
print(f"๐ Processing image: {filename}")
if filename:
- # Copy to served images directory
src_path = os.path.join(output_dir, filename)
dest_path = os.path.join(SERVED_IMAGES_DIR, filename)
@@ -129,7 +125,6 @@ def wait_for_image(prompt_id: str, save_node_id: str = "9", max_wait_time: int =
try:
shutil.copy2(src_path, dest_path)
print(f"โ
Successfully copied {filename} to served directory")
- # Create proper image object with URL
image_objects.append({
"filename": filename,
"url": f"http://localhost:5001/api/images/{filename}",
@@ -149,7 +144,7 @@ def wait_for_image(prompt_id: str, save_node_id: str = "9", max_wait_time: int =
else:
print("โ ๏ธ No images found in save node")
- time.sleep(2) # Wait 2 seconds before checking again
+ time.sleep(2)
except Exception as e:
print(f"Error checking image status: {e}")
@@ -169,25 +164,21 @@ def send_to_comfyui(workflow: Dict[str, Any]) -> Dict[str, Any]:
batch_size = workflow_info['batch_size']
if workflow_info['is_api']:
- # API workflows: remove batch_size, loop multiple times
for node in workflow.get('prompt', {}).values():
if 'batch_size' in node.get('inputs', {}):
del node['inputs']['batch_size']
break
iterations = batch_size
else:
- # Local workflows: keep batch_size, loop once
iterations = 1
all_images = []
last_response_data = None
for i in range(iterations):
- # Increment seed for variation
current_workflow = increment_seed_in_workflow(copy.deepcopy(workflow), i) if i > 0 else workflow
- # Send to ComfyUI
response = requests.post(f"{COMFY_API_URL}/prompt", json=current_workflow)
if response.status_code == 200:
@@ -209,7 +200,6 @@ def send_to_comfyui(workflow: Dict[str, Any]) -> Dict[str, Any]:
return {"error": error_msg}
if all_images:
- # Return the last valid ComfyUI response but with all images
if last_response_data:
last_response_data["all_images"] = all_images
last_response_data["generated_images"] = all_images
@@ -228,41 +218,62 @@ def serve_image(filename: str) -> Any:
This is a shared function used by all servers
"""
from flask import send_file, jsonify
-
+
try:
- # First check in served_images directory (for generated images)
+ print(f"๐ Attempting to serve image: {filename}")
+
+ if filename.startswith('matrix-grid'):
+ matrix_filepath = os.path.join(MATRIX_GRIDS_DIR, filename)
+ print(f"๐ Checking matrix_grids: {matrix_filepath}")
+ print(f"๐ Matrix grids directory exists: {os.path.exists(MATRIX_GRIDS_DIR)}")
+ if os.path.exists(MATRIX_GRIDS_DIR):
+ print(f"๐ Matrix grids directory contents: {os.listdir(MATRIX_GRIDS_DIR)}")
+
+ if os.path.exists(matrix_filepath):
+ print(f"โ
Found matrix grid: {matrix_filepath}")
+ print(f"๐ File size: {os.path.getsize(matrix_filepath)} bytes")
+ return send_file(matrix_filepath, mimetype='image/png')
+
served_filepath = os.path.join(SERVED_IMAGES_DIR, filename)
+ print(f"๐ Checking served_images: {served_filepath}")
+ print(f"๐ Served directory exists: {os.path.exists(SERVED_IMAGES_DIR)}")
+ print(f"๐ Served directory contents: {os.listdir(SERVED_IMAGES_DIR) if os.path.exists(SERVED_IMAGES_DIR) else 'Directory not found'}")
if os.path.exists(served_filepath):
+ print(f"โ
Found in served_images: {served_filepath}")
+ print(f"๐ File size: {os.path.getsize(served_filepath)} bytes")
return send_file(served_filepath, mimetype='image/png')
- # If not found in served_images, check in ComfyUI input directory (for ControlNet images)
current_dir = os.path.dirname(os.path.abspath(__file__))
parent_dir = os.path.dirname(current_dir)
input_dir = os.path.join(parent_dir, "ComfyUI", "input")
input_filepath = os.path.join(input_dir, filename)
+ print(f"๐ Checking ComfyUI input: {input_filepath}")
if os.path.exists(input_filepath):
+ print(f"โ
Found in ComfyUI input: {input_filepath}")
return send_file(input_filepath, mimetype='image/png')
- # If not found in either location, check ComfyUI output directory
output_dir, _ = get_directories()
output_filepath = os.path.join(output_dir, filename)
+ print(f"๐ Checking ComfyUI output: {output_filepath}")
if os.path.exists(output_filepath):
+ print(f"โ
Found in ComfyUI output: {output_filepath}")
return send_file(output_filepath, mimetype='image/png')
- # If still not found, return 404
- print(f"โ Image not found in any directory: {filename}")
- print(f" Checked: {served_filepath}")
- print(f" Checked: {input_filepath}")
- print(f" Checked: {output_filepath}")
+ if not filename.startswith('matrix-grid'):
+ matrix_filepath = os.path.join(MATRIX_GRIDS_DIR, filename)
+ if os.path.exists(matrix_filepath):
+ print(f"โ
Found in matrix_grids (fallback): {matrix_filepath}")
+ return send_file(matrix_filepath, mimetype='image/png')
+ print(f"โ Image not found in any directory: {filename}")
return jsonify({
"status": "error",
- "message": "Image not found"
+ "message": f"Image {filename} not found"
}), 404
-
+
except Exception as e:
print(f"โ Error serving image {filename}: {str(e)}")
return jsonify({
@@ -322,7 +333,6 @@ def upload_controlnet_image(file, unit_index: int = 0) -> Dict[str, Any]:
except Exception as e:
print(f"โ Error uploading ControlNet image: {str(e)}")
- import traceback
traceback.print_exc()
return {
"status": "error",
@@ -352,7 +362,6 @@ def upload_model_file(file, model_type: str = "checkpoints") -> Dict[str, Any]:
"message": "No file provided or no file selected"
}, 400
- # Validate file extension - support common model formats
allowed_extensions = {'.safetensors', '.ckpt', '.pth', '.pt', '.bin'}
file_ext = Path(file.filename).suffix.lower()
@@ -366,12 +375,10 @@ def upload_model_file(file, model_type: str = "checkpoints") -> Dict[str, Any]:
print(f"๐ File content type: {file.content_type}")
print(f"๐ท๏ธ Model type: {model_type}")
- # Get ComfyUI models directory structure
current_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(current_dir)
comfyui_models_dir = os.path.join(project_root, "ComfyUI", "models")
- # Map model types to directories
model_type_dirs = {
"checkpoints": "checkpoints",
"loras": "loras",
@@ -405,16 +412,13 @@ def upload_model_file(file, model_type: str = "checkpoints") -> Dict[str, Any]:
"message": "Invalid target directory"
}, 400
- # Create target directory if it doesn't exist
os.makedirs(target_dir, exist_ok=True)
print(f"๐ Target directory: {target_dir}")
- # Generate safe filename with timestamp for storage
timestamp = int(time.time() * 1000)
safe_filename = f"{Path(file.filename).stem}_{timestamp}{file_ext}"
target_path = Path(target_dir) / safe_filename
- # Create display name from original filename (without timestamp)
original_display_name = Path(file.filename).stem.replace('-', ' ').replace('_', ' ')
original_display_name = ' '.join(word.capitalize() for word in original_display_name.split())
@@ -433,7 +437,6 @@ def upload_model_file(file, model_type: str = "checkpoints") -> Dict[str, Any]:
print(f"๐ Saving to: {target_path}")
- # ๐ ATOMIC WRITE: Write to temporary file first
temp_path = target_path.with_suffix(target_path.suffix + '.tmp')
try:
@@ -443,34 +446,28 @@ def upload_model_file(file, model_type: str = "checkpoints") -> Dict[str, Any]:
f.flush()
os.fsync(f.fileno())
- # Atomic rename to final destination
temp_path.rename(target_path)
print(f"โ
Atomic write completed: {safe_filename}")
except Exception as e:
- # Clean up temp file if something went wrong
if temp_path.exists():
temp_path.unlink()
raise e
- # Verify file was created successfully
if target_path.exists():
file_size = target_path.stat().st_size
print(f"โ
Successfully saved model: {safe_filename}")
print(f"๐ File size: {file_size} bytes")
- # ๐พ DISPLAY NAME: Save the display name mapping
add_model_display_name(safe_filename, original_display_name)
print(f"๐ Display name mapping saved: {safe_filename} -> {original_display_name}")
- # ๐ WEBSOCKET: Emit model refresh event
try:
emit_model_refresh(model_type, safe_filename)
print(f"๐ก WebSocket event emitted: models-refresh for {model_type}")
except Exception as ws_error:
print(f"โ ๏ธ Warning: Failed to emit WebSocket event: {ws_error}")
- # Don't fail the upload if WebSocket fails
return {
"status": "success",
@@ -491,7 +488,6 @@ def upload_model_file(file, model_type: str = "checkpoints") -> Dict[str, Any]:
except Exception as e:
print(f"โ Error uploading model: {str(e)}")
- import traceback
traceback.print_exc()
return {
"status": "error",
@@ -507,11 +503,9 @@ def _setup_comfyui_websocket():
PromptServer instance if available, None otherwise
"""
try:
- # Import ComfyUI server here to avoid circular imports
+
import sys
import os
-
- # Add ComfyUI to path if not already there
current_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(current_dir)
comfyui_dir = os.path.join(project_root, "ComfyUI")
@@ -519,10 +513,8 @@ def _setup_comfyui_websocket():
if comfyui_dir not in sys.path:
sys.path.insert(0, comfyui_dir)
- # Import PromptServer from ComfyUI
from server import PromptServer
- # Check if PromptServer instance exists
if hasattr(PromptServer, 'instance') and PromptServer.instance is not None:
return PromptServer.instance
@@ -545,7 +537,6 @@ def emit_model_refresh(model_type: str, filename: str) -> None:
prompt_server = _setup_comfyui_websocket()
if prompt_server is not None:
- # Create the WebSocket event data
event_data = {
"model_type": model_type,
"filename": filename,
@@ -556,7 +547,6 @@ def emit_model_refresh(model_type: str, filename: str) -> None:
print("๐ก Emitting WebSocket event: models-refresh")
print(f"๐ Event data: {event_data}")
- # Emit the event using ComfyUI's WebSocket infrastructure
prompt_server.send_sync("models-refresh", event_data)
print("โ
WebSocket event sent successfully")
@@ -568,6 +558,4 @@ def emit_model_refresh(model_type: str, filename: str) -> None:
print(f"โ ๏ธ Warning: Could not import ComfyUI server for WebSocket: {e}")
except Exception as e:
print(f"โ Error emitting WebSocket event: {e}")
- import traceback
- traceback.print_exc()
- # Don't raise - we don't want WebSocket failures to break uploads
\ No newline at end of file
+ traceback.print_exc()
\ No newline at end of file
diff --git a/dream_layer_backend/tests/test_report_bundle.py b/dream_layer_backend/tests/test_report_bundle.py
deleted file mode 100644
index 52350888..00000000
--- a/dream_layer_backend/tests/test_report_bundle.py
+++ /dev/null
@@ -1,400 +0,0 @@
-import pytest
-import json
-import csv
-import tempfile
-import os
-import zipfile
-from datetime import datetime
-from report_bundle import ReportBundleGenerator, RunConfig
-
-class TestReportBundle:
- """Test cases for the Report Bundle functionality"""
-
- def setup_method(self):
- """Set up test fixtures"""
- # Create temporary directories for testing
- self.temp_output_dir = tempfile.mkdtemp()
- self.generator = ReportBundleGenerator(self.temp_output_dir)
-
- # Create test images
- self.test_images = []
- for i in range(3):
- image_path = os.path.join(self.temp_output_dir, f"test_image_{i}.png")
- with open(image_path, 'w') as f:
- f.write(f"fake image data {i}")
- self.test_images.append(f"test_image_{i}.png")
-
- def teardown_method(self):
- """Clean up test fixtures"""
- import shutil
- if os.path.exists(self.temp_output_dir):
- shutil.rmtree(self.temp_output_dir)
-
- def test_required_csv_columns_exist(self):
- """Test that CSV has all required columns"""
- # Create test runs
- test_runs = [
- RunConfig(
- run_id="test-run-1",
- timestamp=datetime.now().isoformat(),
- model="test-model.safetensors",
- vae=None,
- loras=[],
- controlnets=[],
- prompt="test prompt 1",
- negative_prompt="test negative 1",
- seed=12345,
- sampler="euler",
- steps=20,
- cfg_scale=7.0,
- width=512,
- height=512,
- batch_size=1,
- batch_count=1,
- workflow={},
- version="1.0.0",
- generated_images=self.test_images[:1],
- generation_type="txt2img"
- ),
- RunConfig(
- run_id="test-run-2",
- timestamp=datetime.now().isoformat(),
- model="test-model-2.safetensors",
- vae="test-vae.safetensors",
- loras=[{"name": "test-lora", "strength": 0.8}],
- controlnets=[{"model": "test-controlnet", "strength": 1.0, "enabled": True}],
- prompt="test prompt 2",
- negative_prompt="test negative 2",
- seed=67890,
- sampler="dpm++",
- steps=30,
- cfg_scale=8.5,
- width=768,
- height=768,
- batch_size=2,
- batch_count=3,
- workflow={"test": "workflow"},
- version="1.0.0",
- generated_images=self.test_images[1:],
- generation_type="img2img"
- )
- ]
-
- # Generate CSV
- csv_path = self.generator.generate_csv(test_runs)
-
- # Validate schema
- assert self.generator.validate_csv_schema(csv_path)
-
- # Check that all required columns exist
- with open(csv_path, 'r', encoding='utf-8') as f:
- reader = csv.DictReader(f)
- fieldnames = reader.fieldnames
-
- required_columns = [
- 'run_id', 'timestamp', 'model', 'vae', 'prompt',
- 'negative_prompt', 'seed', 'sampler', 'steps', 'cfg_scale',
- 'width', 'height', 'batch_size', 'batch_count',
- 'generation_type', 'image_paths', 'loras', 'controlnets', 'workflow_hash'
- ]
-
- for column in required_columns:
- assert column in fieldnames, f"Missing required column: {column}"
-
- # Cleanup
- if os.path.exists(csv_path):
- os.remove(csv_path)
-
- def test_empty_values_handled_without_crashes(self):
- """Test that empty values are handled without crashes"""
- # Create test run with empty values
- test_run = RunConfig(
- run_id="",
- timestamp="",
- model="",
- vae=None,
- loras=[],
- controlnets=[],
- prompt="",
- negative_prompt="",
- seed=0,
- sampler="",
- steps=0,
- cfg_scale=0.0,
- width=0,
- height=0,
- batch_size=0,
- batch_count=0,
- workflow={},
- version="",
- generated_images=[],
- generation_type=""
- )
-
- # Should not crash when generating CSV
- csv_path = self.generator.generate_csv([test_run])
-
- # Should not crash when validating schema
- assert self.generator.validate_csv_schema(csv_path)
-
- # Cleanup
- if os.path.exists(csv_path):
- os.remove(csv_path)
-
- def test_csv_schema_validation(self):
- """Test CSV schema validation with valid and invalid schemas"""
- # Test valid CSV
- valid_csv_content = """run_id,timestamp,model,vae,prompt,negative_prompt,seed,sampler,steps,cfg_scale,width,height,batch_size,batch_count,generation_type,image_paths,loras,controlnets,workflow_hash
-test-run,2023-01-01T00:00:00,model.safetensors,vae.safetensors,prompt,negative,123,euler,20,7.0,512,512,1,1,txt2img,image.png,[],[],hash123"""
-
- temp_csv = "temp_valid.csv"
- with open(temp_csv, 'w', encoding='utf-8') as f:
- f.write(valid_csv_content)
-
- assert self.generator.validate_csv_schema(temp_csv)
-
- # Test invalid CSV (missing columns)
- invalid_csv_content = """run_id,timestamp,model,prompt,seed
-test-run,2023-01-01T00:00:00,model.safetensors,prompt,123"""
-
- temp_csv_invalid = "temp_invalid.csv"
- with open(temp_csv_invalid, 'w', encoding='utf-8') as f:
- f.write(invalid_csv_content)
-
- assert not self.generator.validate_csv_schema(temp_csv_invalid)
-
- # Cleanup
- for temp_file in [temp_csv, temp_csv_invalid]:
- if os.path.exists(temp_file):
- os.remove(temp_file)
-
- def test_image_paths_resolve_to_files(self):
- """Test that all image paths in CSV resolve to files present in zip"""
- # Create test runs with images
- test_runs = [
- RunConfig(
- run_id="test-run-1",
- timestamp=datetime.now().isoformat(),
- model="test-model.safetensors",
- vae=None,
- loras=[],
- controlnets=[],
- prompt="test prompt",
- negative_prompt="",
- seed=12345,
- sampler="euler",
- steps=20,
- cfg_scale=7.0,
- width=512,
- height=512,
- batch_size=1,
- batch_count=1,
- workflow={},
- version="1.0.0",
- generated_images=self.test_images,
- generation_type="txt2img"
- )
- ]
-
- # Mock the registry to return our test runs
- self.generator.registry.runs = {run.run_id: run for run in test_runs}
-
- # Create report bundle
- zip_path = self.generator.create_report_bundle([test_run.run_id for test_run in test_runs])
-
- # Verify zip file exists
- assert os.path.exists(zip_path)
-
- # Extract and verify contents
- with zipfile.ZipFile(zip_path, 'r') as zipf:
- # Check that results.csv exists
- csv_files = [f for f in zipf.namelist() if f.endswith('results.csv')]
- assert len(csv_files) == 1
-
- # Read CSV and verify image paths
- with zipf.open(csv_files[0]) as csv_file:
- reader = csv.DictReader(csv_file.read().decode('utf-8').splitlines())
- for row in reader:
- image_paths = row['image_paths'].split(';') if row['image_paths'] else []
- for image_path in image_paths:
- if image_path.strip():
- # Check that image exists in zip
- image_in_zip = f"images/{image_path}"
- assert image_in_zip in zipf.namelist(), f"Image {image_path} not found in zip"
-
- # Check that config.json exists
- config_files = [f for f in zipf.namelist() if f.endswith('config.json')]
- assert len(config_files) == 1
-
- # Check that README.md exists
- readme_files = [f for f in zipf.namelist() if f.endswith('README.md')]
- assert len(readme_files) == 1
-
- # Cleanup
- if os.path.exists(zip_path):
- os.remove(zip_path)
-
- def test_deterministic_file_names_and_paths(self):
- """Test that file names and paths are deterministic"""
- test_runs = [
- RunConfig(
- run_id="test-run-1",
- timestamp=datetime.now().isoformat(),
- model="test-model.safetensors",
- vae=None,
- loras=[],
- controlnets=[],
- prompt="test prompt",
- negative_prompt="",
- seed=12345,
- sampler="euler",
- steps=20,
- cfg_scale=7.0,
- width=512,
- height=512,
- batch_size=1,
- batch_count=1,
- workflow={},
- version="1.0.0",
- generated_images=self.test_images[:1],
- generation_type="txt2img"
- )
- ]
-
- # Mock the registry to return our test runs
- self.generator.registry.runs = {run.run_id: run for run in test_runs}
-
- # Create two report bundles with same data
- zip_path_1 = self.generator.create_report_bundle([test_runs[0].run_id])
- zip_path_2 = self.generator.create_report_bundle([test_runs[0].run_id])
-
- # Verify both zip files exist
- assert os.path.exists(zip_path_1)
- assert os.path.exists(zip_path_2)
-
- # Check that both zips have same structure
- with zipfile.ZipFile(zip_path_1, 'r') as zip1, zipfile.ZipFile(zip_path_2, 'r') as zip2:
- files_1 = sorted(zip1.namelist())
- files_2 = sorted(zip2.namelist())
-
- # Should have same files
- assert files_1 == files_2
-
- # Should have expected file structure
- expected_files = [
- 'results.csv',
- 'config.json',
- 'README.md',
- 'images/test_image_0.png'
- ]
-
- for expected_file in expected_files:
- assert any(f.endswith(expected_file) for f in files_1), f"Missing {expected_file}"
-
- # Cleanup
- for zip_path in [zip_path_1, zip_path_2]:
- if os.path.exists(zip_path):
- os.remove(zip_path)
-
- def test_config_json_structure(self):
- """Test that config.json has correct structure"""
- test_runs = [
- RunConfig(
- run_id="test-run-1",
- timestamp=datetime.now().isoformat(),
- model="test-model.safetensors",
- vae=None,
- loras=[],
- controlnets=[],
- prompt="test prompt",
- negative_prompt="",
- seed=12345,
- sampler="euler",
- steps=20,
- cfg_scale=7.0,
- width=512,
- height=512,
- batch_size=1,
- batch_count=1,
- workflow={},
- version="1.0.0",
- generated_images=self.test_images[:1],
- generation_type="txt2img"
- )
- ]
-
- # Create config.json
- config_path = self.generator.create_config_json(test_runs)
-
- # Verify structure
- with open(config_path, 'r', encoding='utf-8') as f:
- config_data = json.load(f)
-
- # Check required top-level keys
- assert 'report_metadata' in config_data
- assert 'runs' in config_data
-
- # Check metadata structure
- metadata = config_data['report_metadata']
- assert 'generated_at' in metadata
- assert 'total_runs' in metadata
- assert 'generation_types' in metadata
- assert 'models_used' in metadata
-
- # Check runs structure
- runs = config_data['runs']
- assert len(runs) == 1
- assert runs[0]['run_id'] == 'test-run-1'
-
- # Cleanup
- if os.path.exists(config_path):
- os.remove(config_path)
-
- def test_readme_content(self):
- """Test that README.md has correct content"""
- test_runs = [
- RunConfig(
- run_id="test-run-1",
- timestamp=datetime.now().isoformat(),
- model="test-model.safetensors",
- vae=None,
- loras=[],
- controlnets=[],
- prompt="test prompt",
- negative_prompt="",
- seed=12345,
- sampler="euler",
- steps=20,
- cfg_scale=7.0,
- width=512,
- height=512,
- batch_size=1,
- batch_count=1,
- workflow={},
- version="1.0.0",
- generated_images=self.test_images[:1],
- generation_type="txt2img"
- )
- ]
-
- # Create README
- readme_path = self.generator.create_readme(test_runs, self.test_images[:1])
-
- # Verify content
- with open(readme_path, 'r', encoding='utf-8') as f:
- content = f.read()
-
- # Check for required sections
- assert '# Dream Layer Report Bundle' in content
- assert '## Overview' in content
- assert '## Contents' in content
- assert '## Statistics' in content
- assert '## CSV Schema' in content
- assert 'results.csv' in content
- assert 'config.json' in content
- assert 'images/' in content
- assert 'README.md' in content
-
- # Cleanup
- if os.path.exists(readme_path):
- os.remove(readme_path)
\ No newline at end of file
diff --git a/dream_layer_backend/tests/test_run_registry.py b/dream_layer_backend/tests/test_run_registry.py
deleted file mode 100644
index ad3895ef..00000000
--- a/dream_layer_backend/tests/test_run_registry.py
+++ /dev/null
@@ -1,325 +0,0 @@
-import pytest
-import json
-import tempfile
-import os
-from datetime import datetime
-from run_registry import RunConfig, RunRegistry, create_run_config_from_generation_data
-
-class TestRunRegistry:
- """Test cases for the Run Registry functionality"""
-
- def setup_method(self):
- """Set up test fixtures"""
- # Create a temporary file for testing
- self.temp_file = tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.json')
- self.temp_file.close()
- self.registry = RunRegistry(self.temp_file.name)
-
- def teardown_method(self):
- """Clean up test fixtures"""
- if os.path.exists(self.temp_file.name):
- os.unlink(self.temp_file.name)
-
- def test_required_keys_exist(self):
- """Test that RunConfig has all required keys"""
- # Create a minimal run config with all required fields
- config = RunConfig(
- run_id="test-run-123",
- timestamp=datetime.now().isoformat(),
- model="test-model.safetensors",
- vae=None,
- loras=[],
- controlnets=[],
- prompt="test prompt",
- negative_prompt="test negative",
- seed=12345,
- sampler="euler",
- steps=20,
- cfg_scale=7.0,
- width=512,
- height=512,
- batch_size=1,
- batch_count=1,
- workflow={},
- version="1.0.0",
- generated_images=[],
- generation_type="txt2img"
- )
-
- # Assert all required keys exist
- required_keys = [
- 'run_id', 'timestamp', 'model', 'vae', 'loras', 'controlnets',
- 'prompt', 'negative_prompt', 'seed', 'sampler', 'steps', 'cfg_scale',
- 'width', 'height', 'batch_size', 'batch_count', 'workflow', 'version',
- 'generated_images', 'generation_type'
- ]
-
- for key in required_keys:
- assert hasattr(config, key), f"Missing required key: {key}"
-
- def test_empty_values_handled(self):
- """Test that empty values are handled without crashes"""
- # Test with empty strings and None values
- config = RunConfig(
- run_id="",
- timestamp="",
- model="",
- vae=None,
- loras=[],
- controlnets=[],
- prompt="",
- negative_prompt="",
- seed=0,
- sampler="",
- steps=0,
- cfg_scale=0.0,
- width=0,
- height=0,
- batch_size=0,
- batch_count=0,
- workflow={},
- version="",
- generated_images=[],
- generation_type=""
- )
-
- # Should not crash when accessing any field
- assert config.run_id == ""
- assert config.prompt == ""
- assert config.negative_prompt == ""
- assert config.model == ""
- assert config.vae is None
- assert config.loras == []
- assert config.controlnets == []
- assert config.seed == 0
- assert config.sampler == ""
- assert config.steps == 0
- assert config.cfg_scale == 0.0
- assert config.width == 0
- assert config.height == 0
- assert config.batch_size == 0
- assert config.batch_count == 0
- assert config.workflow == {}
- assert config.version == ""
- assert config.generated_images == []
- assert config.generation_type == ""
-
- def test_create_run_config_from_generation_data(self):
- """Test creating run config from generation data"""
- # Test with minimal data
- generation_data = {
- 'prompt': 'test prompt',
- 'negative_prompt': 'test negative',
- 'model_name': 'test-model.safetensors',
- 'seed': 12345,
- 'sampler_name': 'euler',
- 'steps': 20,
- 'cfg_scale': 7.0,
- 'width': 512,
- 'height': 512,
- 'batch_size': 1,
- 'batch_count': 1
- }
-
- generated_images = ['test-image-1.png', 'test-image-2.png']
-
- config = create_run_config_from_generation_data(
- generation_data, generated_images, "txt2img"
- )
-
- # Assert required keys exist
- assert hasattr(config, 'run_id')
- assert hasattr(config, 'timestamp')
- assert hasattr(config, 'model')
- assert hasattr(config, 'prompt')
- assert hasattr(config, 'negative_prompt')
- assert hasattr(config, 'seed')
- assert hasattr(config, 'sampler')
- assert hasattr(config, 'steps')
- assert hasattr(config, 'cfg_scale')
- assert hasattr(config, 'width')
- assert hasattr(config, 'height')
- assert hasattr(config, 'batch_size')
- assert hasattr(config, 'batch_count')
- assert hasattr(config, 'workflow')
- assert hasattr(config, 'version')
- assert hasattr(config, 'generated_images')
- assert hasattr(config, 'generation_type')
-
- # Assert values are set correctly
- assert config.prompt == 'test prompt'
- assert config.negative_prompt == 'test negative'
- assert config.model == 'test-model.safetensors'
- assert config.seed == 12345
- assert config.sampler == 'euler'
- assert config.steps == 20
- assert config.cfg_scale == 7.0
- assert config.width == 512
- assert config.height == 512
- assert config.batch_size == 1
- assert config.batch_count == 1
- assert config.generated_images == generated_images
- assert config.generation_type == 'txt2img'
-
- def test_create_run_config_with_empty_data(self):
- """Test creating run config with empty/missing data"""
- # Test with minimal/empty data
- generation_data = {}
- generated_images = []
-
- config = create_run_config_from_generation_data(
- generation_data, generated_images, "img2img"
- )
-
- # Should not crash and should have default values
- assert config.prompt == ''
- assert config.negative_prompt == ''
- assert config.model == 'unknown'
- assert config.seed == 0
- assert config.sampler == 'euler'
- assert config.steps == 20
- assert config.cfg_scale == 7.0
- assert config.width == 512
- assert config.height == 512
- assert config.batch_size == 1
- assert config.batch_count == 1
- assert config.generated_images == []
- assert config.generation_type == 'img2img'
-
- def test_registry_save_and_load(self):
- """Test that registry can save and load runs"""
- # Create a test run
- config = RunConfig(
- run_id="test-run-123",
- timestamp=datetime.now().isoformat(),
- model="test-model.safetensors",
- vae=None,
- loras=[],
- controlnets=[],
- prompt="test prompt",
- negative_prompt="test negative",
- seed=12345,
- sampler="euler",
- steps=20,
- cfg_scale=7.0,
- width=512,
- height=512,
- batch_size=1,
- batch_count=1,
- workflow={},
- version="1.0.0",
- generated_images=[],
- generation_type="txt2img"
- )
-
- # Add to registry
- self.registry.add_run(config)
-
- # Create new registry instance to test loading
- new_registry = RunRegistry(self.temp_file.name)
-
- # Should load the saved run
- loaded_run = new_registry.get_run("test-run-123")
- assert loaded_run is not None
- assert loaded_run.run_id == "test-run-123"
- assert loaded_run.prompt == "test prompt"
- assert loaded_run.model == "test-model.safetensors"
-
- def test_registry_get_all_runs(self):
- """Test getting all runs"""
- # Add multiple runs
- config1 = RunConfig(
- run_id="test-run-1",
- timestamp="2023-01-01T00:00:00",
- model="model1.safetensors",
- vae=None,
- loras=[],
- controlnets=[],
- prompt="prompt 1",
- negative_prompt="",
- seed=1,
- sampler="euler",
- steps=20,
- cfg_scale=7.0,
- width=512,
- height=512,
- batch_size=1,
- batch_count=1,
- workflow={},
- version="1.0.0",
- generated_images=[],
- generation_type="txt2img"
- )
-
- config2 = RunConfig(
- run_id="test-run-2",
- timestamp="2023-01-02T00:00:00",
- model="model2.safetensors",
- vae=None,
- loras=[],
- controlnets=[],
- prompt="prompt 2",
- negative_prompt="",
- seed=2,
- sampler="euler",
- steps=20,
- cfg_scale=7.0,
- width=512,
- height=512,
- batch_size=1,
- batch_count=1,
- workflow={},
- version="1.0.0",
- generated_images=[],
- generation_type="img2img"
- )
-
- self.registry.add_run(config1)
- self.registry.add_run(config2)
-
- # Get all runs (should be sorted by timestamp, newest first)
- all_runs = self.registry.get_all_runs()
- assert len(all_runs) == 2
- assert all_runs[0].run_id == "test-run-2" # Newer timestamp
- assert all_runs[1].run_id == "test-run-1" # Older timestamp
-
- def test_registry_delete_run(self):
- """Test deleting a run"""
- config = RunConfig(
- run_id="test-run-to-delete",
- timestamp=datetime.now().isoformat(),
- model="test-model.safetensors",
- vae=None,
- loras=[],
- controlnets=[],
- prompt="test prompt",
- negative_prompt="",
- seed=12345,
- sampler="euler",
- steps=20,
- cfg_scale=7.0,
- width=512,
- height=512,
- batch_size=1,
- batch_count=1,
- workflow={},
- version="1.0.0",
- generated_images=[],
- generation_type="txt2img"
- )
-
- self.registry.add_run(config)
-
- # Verify run exists
- assert self.registry.get_run("test-run-to-delete") is not None
-
- # Delete run
- success = self.registry.delete_run("test-run-to-delete")
- assert success is True
-
- # Verify run is deleted
- assert self.registry.get_run("test-run-to-delete") is None
-
- # Try to delete non-existent run
- success = self.registry.delete_run("non-existent-run")
- assert success is False
\ No newline at end of file
diff --git a/dream_layer_backend/txt2img_server.py b/dream_layer_backend/txt2img_server.py
index b03617a4..00fe8702 100644
--- a/dream_layer_backend/txt2img_server.py
+++ b/dream_layer_backend/txt2img_server.py
@@ -2,15 +2,20 @@
from flask_cors import CORS
import json
import os
-import requests
from dream_layer import get_directories
from dream_layer_backend_utils import interrupt_workflow
from shared_utils import send_to_comfyui
from dream_layer_backend_utils.fetch_advanced_models import get_controlnet_models
from PIL import Image, ImageDraw
from txt2img_workflow import transform_to_txt2img_workflow
-from run_registry import create_run_config_from_generation_data
-from dataclasses import asdict
+import base64
+import io
+import time
+import platform
+import subprocess
+import traceback
+from shared_utils import SERVED_IMAGES_DIR, serve_image, MATRIX_GRIDS_DIR
+from shared_utils import upload_controlnet_image as upload_cn_image
app = Flask(__name__)
CORS(app, resources={
@@ -21,13 +26,6 @@
}
})
-# Get served images directory
-output_dir, _ = get_directories()
-SERVED_IMAGES_DIR = os.path.join(os.path.dirname(__file__), 'served_images')
-os.makedirs(SERVED_IMAGES_DIR, exist_ok=True)
-
-
-
@app.route('/api/txt2img', methods=['POST', 'OPTIONS'])
def handle_txt2img():
"""Handle text-to-image generation requests"""
@@ -39,14 +37,12 @@ def handle_txt2img():
if data:
print("Data:", json.dumps(data, indent=2))
- # Print specific fields of interest
print("\nKey Parameters:")
print("-"*20)
print(f"Prompt: {data.get('prompt', 'Not provided')}")
print(f"Negative Prompt: {data.get('negative_prompt', 'Not provided')}")
print(f"Batch Size: {data.get('batch_size', 'Not provided')}")
- # Check ControlNet data specifically
controlnet_data = data.get('controlnet', {})
print(f"\n๐ฎ ControlNet Data:")
print("-"*20)
@@ -63,14 +59,11 @@ def handle_txt2img():
else:
print("No ControlNet units found")
- # Transform to ComfyUI workflow
-
workflow = transform_to_txt2img_workflow(data)
print("\nGenerated ComfyUI Workflow:")
print("-"*20)
print(json.dumps(workflow, indent=2))
- # Send to ComfyUI server
comfy_response = send_to_comfyui(workflow)
if "error" in comfy_response:
@@ -79,41 +72,11 @@ def handle_txt2img():
"message": comfy_response["error"]
}), 500
- # Extract generated image filenames
- generated_images = []
- if comfy_response.get("all_images"):
- for img_data in comfy_response["all_images"]:
- if isinstance(img_data, dict) and "filename" in img_data:
- generated_images.append(img_data["filename"])
-
- print("Start register process")
- # Register the completed run
- try:
- run_config = create_run_config_from_generation_data(
- data, generated_images, "txt2img"
- )
-
- # Send to run registry
- registry_response = requests.post(
- "http://localhost:5005/api/runs",
- json=asdict(run_config),
- timeout=5
- )
-
- if registry_response.status_code == 200:
- print(f"โ
Run registered successfully: {run_config.run_id}")
- else:
- print(f"โ ๏ธ Failed to register run: {registry_response.text}")
-
- except Exception as e:
- print(f"โ ๏ธ Error registering run: {str(e)}")
-
response = jsonify({
"status": "success",
"message": "Workflow sent to ComfyUI successfully",
"comfy_response": comfy_response,
- "generated_images": comfy_response.get("all_images", []),
- "run_id": run_config.run_id if 'run_config' in locals() else None
+ "generated_images": comfy_response.get("all_images", [])
})
return response
@@ -126,7 +89,6 @@ def handle_txt2img():
except Exception as e:
print(f"Error in handle_txt2img: {str(e)}")
- import traceback
traceback.print_exc()
return jsonify({
"status": "error",
@@ -147,8 +109,6 @@ def serve_image_endpoint(filename):
This endpoint is needed here because the frontend expects it on this port
"""
try:
- # Use shared function
- from shared_utils import serve_image
return serve_image(filename)
except Exception as e:
@@ -199,8 +159,6 @@ def upload_controlnet_image_endpoint():
except ValueError:
unit_index = 0
- # Use shared function
- from shared_utils import upload_controlnet_image as upload_cn_image
result = upload_cn_image(file, unit_index)
if isinstance(result, tuple):
@@ -210,7 +168,82 @@ def upload_controlnet_image_endpoint():
except Exception as e:
print(f"โ Error uploading ControlNet image: {str(e)}")
- import traceback
+ traceback.print_exc()
+ return jsonify({
+ "status": "error",
+ "message": str(e)
+ }), 500
+
+@app.route('/api/save-matrix-grid', methods=['POST'])
+def save_matrix_grid():
+ """
+ Save Matrix grid image to server storage (permanent directory)
+ """
+ try:
+ print(f"๐ Matrix grid save request received")
+ data = request.json
+
+ if not data or 'imageData' not in data:
+ print("โ No image data in request")
+ return jsonify({
+ "status": "error",
+ "message": "No image data provided"
+ }), 400
+
+ image_data = data['imageData']
+ if image_data.startswith('data:'):
+ image_data = image_data.split(',')[1]
+
+ print(f"๐ Received base64 data length: {len(image_data)}")
+
+ # Decode base64 image
+ image_bytes = base64.b64decode(image_data)
+ print(f"๐ Decoded image bytes: {len(image_bytes)}")
+
+ image = Image.open(io.BytesIO(image_bytes))
+ print(f"๐ Image dimensions: {image.size}")
+
+ timestamp = int(time.time() * 1000)
+ matrix_id = data.get('matrixId', 'matrix')
+ filename = f"{matrix_id}_{timestamp}.png"
+ print(f"๐ Generated filename: {filename}")
+
+ os.makedirs(MATRIX_GRIDS_DIR, exist_ok=True)
+ print(f"๐ MATRIX_GRIDS_DIR: {MATRIX_GRIDS_DIR}")
+ print(f"๐ Directory exists: {os.path.exists(MATRIX_GRIDS_DIR)}")
+ print(f"๐ Directory is writable: {os.access(MATRIX_GRIDS_DIR, os.W_OK)}")
+
+ filepath = os.path.join(MATRIX_GRIDS_DIR, filename)
+ print(f"๐ Full save path: {filepath}")
+
+ image.save(filepath, format='PNG')
+ print(f"๐พ Matrix grid saved to permanent storage")
+
+ if os.path.exists(filepath):
+ file_size = os.path.getsize(filepath)
+ print(f"โ
Successfully saved Matrix grid to permanent storage: {filename}")
+ print(f"๐ File size: {file_size} bytes")
+ print(f"๐ Full path: {filepath}")
+
+ dir_contents = os.listdir(MATRIX_GRIDS_DIR)
+ print(f"๐ Matrix grids directory contents: {dir_contents}")
+
+ return jsonify({
+ "status": "success",
+ "filename": filename,
+ "url": f"http://localhost:5001/api/images/{filename}",
+ "filesize": file_size,
+ "storage": "permanent"
+ })
+ else:
+ print(f"โ File was not created: {filepath}")
+ return jsonify({
+ "status": "error",
+ "message": "Failed to save Matrix grid to permanent storage"
+ }), 500
+
+ except Exception as e:
+ print(f"โ Error saving Matrix grid: {str(e)}")
traceback.print_exc()
return jsonify({
"status": "error",
diff --git a/dream_layer_backend/txt2img_workflow.py b/dream_layer_backend/txt2img_workflow.py
index 24912b34..5365ef46 100644
--- a/dream_layer_backend/txt2img_workflow.py
+++ b/dream_layer_backend/txt2img_workflow.py
@@ -1,8 +1,11 @@
import json
import random
import os
+import traceback
import json
from dream_layer_backend_utils.workflow_loader import load_workflow
+from dream_layer_backend_utils.api_key_injector import inject_api_keys_into_workflow
+from dream_layer_backend_utils.update_custom_workflow import override_workflow
from dream_layer_backend_utils.api_key_injector import inject_api_keys_into_workflow, read_api_keys_from_env
from dream_layer_backend_utils.update_custom_workflow import override_workflow
from dream_layer_backend_utils.update_custom_workflow import update_custom_workflow, validate_custom_workflow
@@ -16,7 +19,6 @@
)
from shared_utils import SAMPLER_NAME_MAP
-
def transform_to_txt2img_workflow(data):
"""
Transform frontend data to ComfyUI txt2img workflow
@@ -26,39 +28,35 @@ def transform_to_txt2img_workflow(data):
print("\n๐ Transforming txt2img workflow:")
print("-" * 40)
print(f"๐ Data keys: {list(data.keys())}")
-
- # Extract and validate core parameters with smallFeatures improvements
+
prompt = data.get('prompt', '')
negative_prompt = data.get('negative_prompt', '')
-
- # Dimension validation
+
width = max(64, min(2048, int(data.get('width', 512))))
height = max(64, min(2048, int(data.get('height', 512))))
-
- # Batch parameters with validation (from smallFeatures)
- # Clamp between 1 and 8
+
batch_size = max(1, min(8, int(data.get('batch_size', 1))))
print(f"\nBatch size: {batch_size}")
-
- # Sampling parameters with validation
+
steps = max(1, min(150, int(data.get('steps', 20))))
cfg_scale = max(1.0, min(20.0, float(data.get('cfg_scale', 7.0))))
-
- # Get sampler name and map it to ComfyUI format (from smallFeatures)
+
frontend_sampler = data.get('sampler_name', 'euler')
sampler_name = SAMPLER_NAME_MAP.get(frontend_sampler, 'euler')
print(f"\nMapping sampler name: {frontend_sampler} -> {sampler_name}")
-
+
scheduler = data.get('scheduler', 'normal')
-
- # Handle seed - enhanced from smallFeatures for -1 values
+
try:
seed = int(data.get('seed', 0))
if seed < 0:
- # Generate random seed between 0 and 2^32-1
seed = random.randint(0, 2**32 - 1)
except (ValueError, TypeError):
seed = random.randint(0, 2**32 - 1)
+
+ model_name = data.get('model_name', 'juggernautXL_v8Rundiffusion.safetensors')
+
+ closed_source_models = ['dall-e-3', 'dall-e-2', 'flux-pro', 'flux-dev', 'ideogram-v3']
# Handle model name validation
model_name = data.get('model_name', 'juggernautXL_v8Rundiffusion.safetensors')
@@ -68,9 +66,9 @@ def transform_to_txt2img_workflow(data):
if model_name in closed_source_models:
print(f"๐จ Using closed-source model: {model_name}")
-
+
print(f"\nUsing model: {model_name}")
-
+
core_generation_settings = {
'prompt': prompt,
'negative_prompt': negative_prompt,
@@ -86,12 +84,10 @@ def transform_to_txt2img_workflow(data):
'denoise': 1.0
}
print(f"๐ฏ Core settings: {core_generation_settings}")
-
- # Extract ControlNet data
+
controlnet_data = data.get('controlnet', {})
print(f"๐ฎ ControlNet data: {controlnet_data}")
-
- # Extract Face Restoration data
+
face_restoration_data = {
'restore_faces': data.get('restore_faces', False),
'face_restoration_model': data.get('face_restoration_model', 'codeformer'),
@@ -99,16 +95,14 @@ def transform_to_txt2img_workflow(data):
'gfpgan_weight': data.get('gfpgan_weight', 0.5)
}
print(f"๐ค Face Restoration data: {face_restoration_data}")
-
- # Extract Tiling data
+
tiling_data = {
'tiling': data.get('tiling', False),
'tile_size': data.get('tile_size', 512),
'tile_overlap': data.get('tile_overlap', 64)
}
print(f"๐งฉ Tiling data: {tiling_data}")
-
- # Extract Hires.fix data
+
hires_fix_data = {
'hires_fix': data.get('hires_fix', False),
'hires_fix_upscale_method': data.get('hires_fix_upscale_method', 'upscale-by'),
@@ -120,34 +114,29 @@ def transform_to_txt2img_workflow(data):
'hires_fix_upscaler': data.get('hires_fix_upscaler', '4x-ultrasharp')
}
print(f"๐ผ๏ธ Hires.fix data: {hires_fix_data}")
-
- # Extract Refiner data
+
refiner_data = {
'refiner_enabled': data.get('refiner_enabled', False),
'refiner_model': data.get('refiner_model', 'none'),
'refiner_switch_at': data.get('refiner_switch_at', 0.8)
}
print(f"๐๏ธ Refiner data: {refiner_data}")
-
- # Determine workflow template based on features
- use_controlnet = controlnet_data.get(
- 'enabled', False) and controlnet_data.get('units')
+
+ use_controlnet = controlnet_data.get('enabled', False) and controlnet_data.get('units')
use_lora = data.get('lora') and data.get('lora').get('enabled', False)
- use_face_restoration = face_restoration_data.get(
- 'restore_faces', False)
+ use_face_restoration = face_restoration_data.get('restore_faces', False)
use_tiling = tiling_data.get('tiling', False)
-
+
print(f"๐ง Use ControlNet: {use_controlnet}")
print(f"๐ง Use LoRA: {use_lora}")
print(f"๐ง Use Face Restoration: {use_face_restoration}")
print(f"๐ง Use Tiling: {use_tiling}")
-
- # Create workflow request for the loader
+
if model_name in ['dall-e-3', 'dall-e-2']:
workflow_model_type = 'dalle'
elif model_name in ['flux-pro', 'flux-dev']:
workflow_model_type = 'bfl'
- elif 'ideogram' in model_name.lower(): # Added check for ideogram models
+ elif 'ideogram' in model_name.lower():
workflow_model_type = 'ideogram'
elif 'stability' in model_name.lower(): # Added check for Stability AI models
workflow_model_type = 'stability'
@@ -155,26 +144,26 @@ def transform_to_txt2img_workflow(data):
workflow_model_type = 'photon'
else:
workflow_model_type = 'local'
-
+
workflow_request = {
'generation_flow': 'txt2img',
'model_name': workflow_model_type,
'controlnet': use_controlnet,
'lora': use_lora
}
-
+
print(f"๐ Workflow request: {workflow_request}")
-
- # Load workflow using the workflow loader
+
workflow = load_workflow(workflow_request)
print(f"โ
Workflow loaded successfully")
+
+ workflow = inject_api_keys_into_workflow(workflow)
# Inject API keys if needed (for DALL-E, FLUX, etc.)
all_api_keys = read_api_keys_from_env()
workflow = inject_api_keys_into_workflow(workflow, all_api_keys)
print(f"โ
API keys injected")
-
- # Custom workflow support from smallFeatures
+
custom_workflow = data.get('custom_workflow')
if custom_workflow and validate_custom_workflow(custom_workflow):
print("Custom workflow detected, updating with current parameters...")
@@ -184,53 +173,44 @@ def transform_to_txt2img_workflow(data):
except Exception as e:
print(f"Error updating custom workflow: {str(e)}")
print("Falling back to default workflow override")
- workflow = override_workflow(
- workflow, core_generation_settings)
+ workflow = override_workflow(workflow, core_generation_settings)
else:
- # Apply overrides to loaded workflow
workflow = override_workflow(workflow, core_generation_settings)
- print(
- "No valid custom workflow provided, using default workflow with overrides")
-
+ print("No valid custom workflow provided, using default workflow with overrides")
+
print(f"โ
Core settings applied")
-
- # Apply LoRA parameters if enabled
+
if use_lora:
print(f"๐จ Applying LoRA parameters...")
workflow = inject_lora_parameters(workflow, data.get('lora', {}))
-
- # Apply ControlNet parameters if enabled
+
if use_controlnet:
print(f"๐ฎ Applying ControlNet parameters...")
workflow = inject_controlnet_parameters(workflow, controlnet_data)
-
- # Apply Face Restoration parameters if enabled
+
if use_face_restoration:
print(f"๐ค Applying Face Restoration parameters...")
- workflow = inject_face_restoration_parameters(
- workflow, face_restoration_data)
-
- # Apply Tiling parameters if enabled
+ workflow = inject_face_restoration_parameters(workflow, face_restoration_data)
+
if use_tiling:
print(f"๐งฉ Applying Tiling parameters...")
workflow = inject_tiling_parameters(workflow, tiling_data)
-
- # Apply Hires.fix parameters if enabled
+
if hires_fix_data.get('hires_fix', False):
print(f"โจ Applying Hires.fix parameters...")
workflow = inject_hires_fix_parameters(workflow, hires_fix_data)
-
- # Apply Refiner parameters if enabled
+
if refiner_data.get('refiner_enabled', False):
print(f"โจ Applying Refiner parameters...")
workflow = inject_refiner_parameters(workflow, refiner_data)
-
+
print(f"โ
Workflow transformation complete")
print(f"๐ Generated workflow: {json.dumps(workflow, indent=2)}")
return workflow
-
+
except Exception as e:
print(f"โ Error transforming workflow: {str(e)}")
- import traceback
traceback.print_exc()
return None
+
+
diff --git a/dream_layer_backend/workflows/img2img/gemini_multimodal_workflow.json b/dream_layer_backend/workflows/img2img/gemini_multimodal_workflow.json
deleted file mode 100644
index a2d397c9..00000000
--- a/dream_layer_backend/workflows/img2img/gemini_multimodal_workflow.json
+++ /dev/null
@@ -1,119 +0,0 @@
-{
- "prompt": {
- "1": {
- "class_type": "LoadImage",
- "inputs": {
- "image": "example_image.png"
- }
- },
- "2": {
- "class_type": "GeminiNode",
- "inputs": {
- "prompt": "Analyze this image in detail. Describe the composition, colors, style, and any artistic elements. Then suggest how to improve it using digital art techniques.",
- "model": "gemini-2.5-pro-preview-05-06",
- "images": ["1", 0],
- "seed": 42
- }
- },
- "3": {
- "class_type": "GeminiNode",
- "inputs": {
- "prompt": "Based on this image analysis, create a detailed AI art prompt. Include: style, composition, lighting, colors, and quality terms. Format: 'A [style] [subject] with [details], [lighting], [colors], masterpiece, high quality'",
- "model": "gemini-2.5-flash-preview-04-17",
- "seed": 43
- }
- },
- "4": {
- "class_type": "CLIPTextEncode",
- "inputs": {
- "clip": ["5", 1],
- "text": ["3", 0]
- }
- },
- "5": {
- "class_type": "CheckpointLoaderSimple",
- "inputs": {
- "ckpt_name": "juggernautXL_v8Rundiffusion.safetensors"
- }
- },
- "6": {
- "class_type": "EmptyLatentImage",
- "inputs": {
- "width": 1024,
- "height": 1024,
- "batch_size": 1
- }
- },
- "7": {
- "class_type": "KSampler",
- "inputs": {
- "model": ["5", 0],
- "positive": ["4", 0],
- "negative": ["8", 0],
- "latent_image": ["6", 0],
- "seed": 0,
- "steps": 25,
- "cfg": 7.5,
- "sampler_name": "euler",
- "scheduler": "normal",
- "denoise": 1.0
- }
- },
- "8": {
- "class_type": "CLIPTextEncode",
- "inputs": {
- "clip": ["5", 1],
- "text": "blurry, low quality, distorted, ugly, bad anatomy"
- }
- },
- "9": {
- "class_type": "VAEDecode",
- "inputs": {
- "samples": ["7", 0],
- "vae": ["5", 2]
- }
- },
- "10": {
- "class_type": "SaveImage",
- "inputs": {
- "images": ["9", 0],
- "filename_prefix": "DreamLayer_Gemini_Enhanced"
- }
- }
- },
- "meta": {
- "description": "Gemini Multimodal Analysis โ Enhanced Image Generation Workflow",
- "workflow_type": "advanced_multimodal_chain",
- "demonstrates": [
- "Image loading and analysis",
- "Gemini multimodal understanding",
- "Text-to-text refinement chaining",
- "AI-generated prompt optimization",
- "Integration with local Stable Diffusion",
- "Full end-to-end creative workflow"
- ],
- "node_chain": "LoadImage โ GeminiNode(analysis) โ GeminiNode(prompt_generation) โ CLIPTextEncode โ KSampler โ VAEDecode โ SaveImage",
- "core_settings": {
- "input_image": "Source image for Gemini to analyze",
- "analysis_prompt": "Instructions for Gemini to analyze the image",
- "prompt_generation": "Instructions for Gemini to create enhanced prompts",
- "model_1": "Primary Gemini model for detailed analysis",
- "model_2": "Secondary Gemini model for prompt optimization",
- "seed": "Random seed for deterministic output"
- },
- "gemini_capabilities_shown": [
- "Visual understanding and analysis",
- "Artistic critique and improvement suggestions",
- "Creative prompt generation",
- "Multi-step reasoning",
- "Integration with image generation pipeline"
- ],
- "technical_features": [
- "Two-stage Gemini processing",
- "Dynamic prompt generation from image analysis",
- "Seamless integration with ComfyUI workflow",
- "Multimodal AI โ traditional AI pipeline",
- "Professional creative workflow automation"
- ]
- }
-}
\ No newline at end of file
diff --git a/dream_layer_backend/workflows/img2img/stability_core_generation_workflow.json b/dream_layer_backend/workflows/img2img/stability_core_generation_workflow.json
deleted file mode 100644
index 668f6209..00000000
--- a/dream_layer_backend/workflows/img2img/stability_core_generation_workflow.json
+++ /dev/null
@@ -1,57 +0,0 @@
-{
- "prompt": {
- "1": {
- "class_type": "LoadImage",
- "inputs": {
- "image": "input_image.png",
- "batch_size": 1
- }
- },
- "2": {
- "class_type": "StabilityStableImageUltraNode",
- "inputs": {
- "prompt": "beautiful",
- "aspect_ratio": "1:1",
- "style_preset": "cinematic",
- "seed": 0,
- "negative_prompt": "ugly",
- "batch_size": 1,
- "image": ["1", 0],
- "image_denoise": 0.75
- }
- },
- "3": {
- "class_type": "SaveImage",
- "inputs": {
- "images": ["2", 0],
- "filename_prefix": "DreamLayer_StabilityAI_img2img"
- }
- }
- },
- "meta": {
- "description": "Stability AI Image-to-Image Generation Workflow",
- "model_options": {
- "stability_sdxl": "StabilityStableImageUltraNode",
- "stability_sd_turbo": "StabilityStableImageUltraNode"
- },
- "core_settings": {
- "prompt": "Main generation prompt",
- "aspect_ratio": "Image aspect ratio",
- "style_preset": "Style preset",
- "seed": "Random seed (0 for random)",
- "batch_size": "Number of images to generate (1-10)",
- "image_denoise": "Denoise strength (0.0-1.0)"
- },
- "aspect_ratios": [
- "1:1",
- "16:9",
- "9:16",
- "3:2",
- "2:3",
- "5:4",
- "4:5",
- "21:9",
- "9:21"
- ]
- }
-}
diff --git a/dream_layer_backend/workflows/txt2img/gemini_core_generation_workflow.json b/dream_layer_backend/workflows/txt2img/gemini_core_generation_workflow.json
deleted file mode 100644
index 6a2c9319..00000000
--- a/dream_layer_backend/workflows/txt2img/gemini_core_generation_workflow.json
+++ /dev/null
@@ -1,40 +0,0 @@
-{
- "prompt": {
- "1": {
- "class_type": "GeminiNode",
- "inputs": {
- "prompt": "Generate a creative story about a magical forest",
- "model": "gemini-2.5-pro-preview-05-06",
- "seed": 42
- }
- }
- },
- "meta": {
- "description": "Gemini Text Generation Workflow",
- "model_options": {
- "gemini-pro-vision": "GeminiNode",
- "gemini-pro": "GeminiNode"
- },
- "core_settings": {
- "prompt": "Text prompt for Gemini analysis or generation",
- "model": "Gemini model to use (gemini-2.5-pro-preview-05-06, gemini-2.5-flash-preview-04-17)",
- "seed": "Random seed for deterministic output (0 for random)",
- "images": "Optional images for multimodal analysis",
- "audio": "Optional audio input for analysis",
- "video": "Optional video input for analysis",
- "files": "Optional files for context"
- },
- "model_options_detailed": [
- "gemini-2.5-pro-preview-05-06",
- "gemini-2.5-flash-preview-04-17"
- ],
- "capabilities": [
- "Text generation",
- "Image analysis",
- "Multimodal understanding",
- "Document analysis",
- "Code generation",
- "Creative writing"
- ]
- }
-}
\ No newline at end of file
diff --git a/dream_layer_backend/workflows/txt2img/stability_core_generation_workflow.json b/dream_layer_backend/workflows/txt2img/stability_core_generation_workflow.json
deleted file mode 100644
index f16b5fc7..00000000
--- a/dream_layer_backend/workflows/txt2img/stability_core_generation_workflow.json
+++ /dev/null
@@ -1,47 +0,0 @@
-{
- "prompt": {
- "1": {
- "class_type": "StabilityStableImageUltraNode",
- "inputs": {
- "prompt": "beautiful",
- "aspect_ratio": "1:1",
- "style_preset": "cinematic",
- "seed": 0,
- "negative_prompt": "ugly",
- "batch_size": 1
- }
- },
- "2": {
- "class_type": "SaveImage",
- "inputs": {
- "images": ["1", 0],
- "filename_prefix": "DreamLayer_StabilityAI"
- }
- }
- },
- "meta": {
- "description": "Stability AI Core Generation Workflow",
- "model_options": {
- "stability_sdxl": "StabilityStableImageUltraNode",
- "stability_sd_turbo": "StabilityStableImageUltraNode"
- },
- "core_settings": {
- "prompt": "Main generation prompt",
- "aspect_ratio": "Image aspect ratio",
- "style_preset": "Style preset",
- "seed": "Random seed (0 for random)",
- "batch_size": "Number of images to generate (1-10)"
- },
- "aspect_ratios": [
- "1:1",
- "16:9",
- "9:16",
- "3:2",
- "2:3",
- "5:4",
- "4:5",
- "21:9",
- "9:21"
- ]
- }
-}
diff --git a/dream_layer_frontend/src/components/AliasKeyInputs.tsx b/dream_layer_frontend/src/components/AliasKeyInputs.tsx
index c0af222e..e26b1527 100644
--- a/dream_layer_frontend/src/components/AliasKeyInputs.tsx
+++ b/dream_layer_frontend/src/components/AliasKeyInputs.tsx
@@ -42,16 +42,10 @@ const fields = [
];
const ApiKeysForm: React.FC = () => {
- const [keys, setKeys] = useState(Array(fields.length).fill(""));
- const [submitted, setSubmitted] = useState(
- Array(fields.length).fill(false)
- );
- const [loading, setLoading] = useState(
- Array(fields.length).fill(false)
- );
- const [showText, setShowText] = useState(
- Array(fields.length).fill(false)
- );
+ const [keys, setKeys] = useState(["", "", ""]);
+ const [submitted, setSubmitted] = useState([false, false, false]);
+ const [loading, setLoading] = useState([false, false, false]);
+ const [showText, setShowText] = useState([false, false, false]);
const handleChange = (index: number, value: string) => {
setKeys((prev) => {
@@ -160,10 +154,7 @@ const ApiKeysForm: React.FC = () => {
)}
))}
-
- * Please Click of Refresh Models After Adding the Keys to View the
- Models
-
+ * Please Click of Refresh Models After Adding the Keys to View the Models
);
};
diff --git a/dream_layer_frontend/src/components/MatrixSettings.tsx b/dream_layer_frontend/src/components/MatrixSettings.tsx
new file mode 100644
index 00000000..d008ab13
--- /dev/null
+++ b/dream_layer_frontend/src/components/MatrixSettings.tsx
@@ -0,0 +1,231 @@
+import React, { useState, useEffect, useCallback, memo } from 'react';
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import { Input } from "@/components/ui/input";
+import { Checkbox } from "@/components/ui/checkbox";
+import { Card, CardContent } from "@/components/ui/card";
+import { Separator } from "@/components/ui/separator";
+import {
+ MatrixSettings as MatrixSettingsType,
+ MatrixParameter,
+ MATRIX_PARAMETERS,
+ defaultMatrixSettings,
+ CoreGenerationSettings,
+ MatrixParameterName
+} from '@/types/generationSettings';
+
+interface MatrixSettingsProps {
+ onSettingsChange: (settings: MatrixSettingsType) => void;
+}
+
+interface ParameterRowProps {
+ label: string;
+ axis: 'xAxis' | 'yAxis' | 'zAxis';
+ parameter: MatrixParameter;
+ onParameterUpdate: (axis: 'xAxis' | 'yAxis' | 'zAxis', updates: Partial) => void;
+ usedParameters: string[];
+}
+
+const ParameterRow = memo(({
+ label,
+ axis,
+ parameter,
+ onParameterUpdate,
+ usedParameters
+}) => {
+ const [localValue, setLocalValue] = useState(parameter.values);
+
+ useEffect(() => {
+ setLocalValue(parameter.values);
+ }, [parameter.values]);
+
+ const handleInputChange = (e: React.ChangeEvent) => {
+ const newValue = e.target.value;
+ setLocalValue(newValue);
+ onParameterUpdate(axis, { values: newValue });
+ };
+
+ const handleParameterChange = (value: MatrixParameterName) => {
+ onParameterUpdate(axis, {
+ name: value,
+ enabled: value !== 'Nothing',
+ values: value !== 'Nothing' ? parameter.values : ''
+ });
+ };
+
+ const handleTypeChange = (value: 'range' | 'list') => {
+ onParameterUpdate(axis, { type: value });
+ };
+
+ return (
+
+
{label}
+
+
+
+
+
+
+
+
+
+
+ );
+});
+
+ParameterRow.displayName = 'ParameterRow';
+
+const MatrixSettings: React.FC = ({
+ onSettingsChange
+}) => {
+ const [matrixSettings, setMatrixSettings] = useState(defaultMatrixSettings);
+
+ const updateMatrixSettings = useCallback((updates: Partial) => {
+ const newSettings = { ...matrixSettings, ...updates };
+ setMatrixSettings(newSettings);
+ onSettingsChange(newSettings);
+ }, [matrixSettings, onSettingsChange]);
+
+ const updateParameter = useCallback((axis: 'xAxis' | 'yAxis' | 'zAxis', updates: Partial) => {
+ const newParameter = { ...matrixSettings[axis], ...updates };
+ updateMatrixSettings({ [axis]: newParameter });
+ }, [matrixSettings, updateMatrixSettings]);
+
+ const getUsedParameters = (currentAxis: 'xAxis' | 'yAxis' | 'zAxis'): string[] => {
+ const used: string[] = [];
+ if (currentAxis !== 'xAxis' && matrixSettings.xAxis.name !== 'Nothing') {
+ used.push(matrixSettings.xAxis.name);
+ }
+ if (currentAxis !== 'yAxis' && matrixSettings.yAxis.name !== 'Nothing') {
+ used.push(matrixSettings.yAxis.name);
+ }
+ if (currentAxis !== 'zAxis' && matrixSettings.zAxis?.name !== 'Nothing') {
+ used.push(matrixSettings.zAxis.name);
+ }
+ return used;
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ updateMatrixSettings({ drawLegend: !!checked })}
+ />
+
+
+
+ updateMatrixSettings({ includeSubImages: !!checked })}
+ />
+
+
+
+ updateMatrixSettings({ includeSubgrids: !!checked })}
+ />
+
+
+
+ updateMatrixSettings({ keepSeedsForRows: !!checked })}
+ />
+
+
+
+
+
+ );
+};
+
+export default MatrixSettings;
\ No newline at end of file
diff --git a/dream_layer_frontend/src/components/Navigation/TabsNav.tsx b/dream_layer_frontend/src/components/Navigation/TabsNav.tsx
index 0dd92f2e..9a9d5c8a 100644
--- a/dream_layer_frontend/src/components/Navigation/TabsNav.tsx
+++ b/dream_layer_frontend/src/components/Navigation/TabsNav.tsx
@@ -7,19 +7,16 @@ import {
HardDrive,
History,
Download,
- MessageSquare
-} from "lucide-react";
+ MessageSquare,
+} from 'lucide-react';
const tabs = [
{ id: "txt2img", label: "Txt2Img", icon: FileText },
{ id: "img2img", label: "Img2Img", icon: ImageIcon },
- { id: "img2txt", label: "Img2Txt", icon: MessageSquare },
{ id: "extras", label: "Extras", icon: GalleryHorizontal },
{ id: "models", label: "Models", icon: HardDrive },
{ id: "pnginfo", label: "PNG Info", icon: FileText },
- { id: "configurations", label: "Configurations", icon: Settings },
- { id: "runregistry", label: "Run Registry", icon: History },
- { id: "reportbundle", label: "Report Bundle", icon: Download }
+ { id: "configurations", label: "Configurations", icon: Settings }
];
interface TabsNavProps {
diff --git a/dream_layer_frontend/src/components/tabs/txt2img/ImagePreview.tsx b/dream_layer_frontend/src/components/tabs/txt2img/ImagePreview.tsx
index 6f9c8f26..324884a1 100644
--- a/dream_layer_frontend/src/components/tabs/txt2img/ImagePreview.tsx
+++ b/dream_layer_frontend/src/components/tabs/txt2img/ImagePreview.tsx
@@ -24,31 +24,65 @@ const ImagePreview: React.FC = ({ onTabChange }) => {
const [selectedImageIndex, setSelectedImageIndex] = useState(0);
const [outputSettingsExpanded, setOutputSettingsExpanded] = useState(true);
const [thumbnailStartIndex, setThumbnailStartIndex] = useState(0);
+ const [imageError, setImageError] = useState(null);
const currentImage = images[selectedImageIndex] || images[0];
const maxThumbnails = 5;
const totalPages = Math.ceil(images.length / maxThumbnails);
const currentPage = Math.floor(thumbnailStartIndex / maxThumbnails) + 1;
+ const isMatrixGrid = (image: any) => {
+ return image?.id?.startsWith('matrix-grid') ||
+ image?.id?.startsWith('matrix-subgrid');
+ };
+
+ const handleImageError = (error: React.SyntheticEvent) => {
+ console.error('Error loading image:', error);
+ const img = error.target as HTMLImageElement;
+ console.log('Failed image URL:', img.src);
+ setImageError(`Failed to load image`);
+ };
+
+ const handleImageLoad = () => {
+ setImageError(null);
+ };
+
const handleDownload = async (format: 'png' | 'zip') => {
if (!currentImage) return;
if (format === 'png') {
- // Download single PNG
- const link = document.createElement('a');
- link.href = currentImage.url;
- link.target = '_blank';
- link.download = `generated-image-${currentImage.id}.png`;
- document.body.appendChild(link);
- link.click();
- document.body.removeChild(link);
+ try {
+ const response = await fetch(currentImage.url);
+ const blob = await response.blob();
+
+ const link = document.createElement('a');
+ link.href = URL.createObjectURL(blob);
+ link.download = `generated-image-${currentImage.id}.png`;
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+
+ URL.revokeObjectURL(link.href);
+ } catch (error) {
+ console.error('Download failed:', error);
+ const link = document.createElement('a');
+ link.href = currentImage.url;
+ link.target = '_blank';
+ link.download = `generated-image-${currentImage.id}.png`;
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ }
} else if (format === 'zip') {
- // Download all images as ZIP
const zip = new JSZip();
const promises = images.map(async (image, index) => {
- const response = await fetch(image.url);
- const blob = await response.blob();
- zip.file(`generated-image-${index + 1}.png`, blob);
+ try {
+ const response = await fetch(image.url);
+ const blob = await response.blob();
+ zip.file(`generated-image-${index + 1}.png`, blob);
+ } catch (error) {
+ console.error(`Failed to fetch image ${index + 1}:`, error);
+ }
});
await Promise.all(promises);
@@ -61,6 +95,8 @@ const ImagePreview: React.FC = ({ onTabChange }) => {
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
+
+ URL.revokeObjectURL(link.href);
}
};
@@ -68,23 +104,26 @@ const ImagePreview: React.FC = ({ onTabChange }) => {
if (!currentImage) return;
try {
- // Extract filename from URL (supports both old and new formats)
- const url = new URL(currentImage.url);
- let filename = url.searchParams.get('filename'); // Old ComfyUI format
- if (!filename) {
- // New format - extract from path
- filename = url.pathname.split('/').pop(); // Gets "image123.png"
+ let filename: string | null = null;
+
+ if (isMatrixGrid(currentImage)) {
+ const url = new URL(currentImage.url);
+ filename = url.pathname.split('/').pop();
+ } else {
+ const url = new URL(currentImage.url);
+ filename = url.searchParams.get('filename') || url.pathname.split('/').pop();
}
if (!filename) {
console.error('No filename found in URL:', currentImage.url);
return;
}
+
console.log('=== Show in Folder Debug ===');
console.log('Full URL:', currentImage.url);
console.log('Extracted filename:', filename);
console.log('Request body:', JSON.stringify({ filename }));
- console.log('Current Image:', currentImage);
+
const response = await fetch('http://localhost:5002/api/show-in-folder', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -103,12 +142,14 @@ const ImagePreview: React.FC = ({ onTabChange }) => {
if (destination === 'img2img') {
try {
- // Extract filename from URL (supports both old and new formats)
- const url = new URL(currentImage.url);
- let filename = url.searchParams.get('filename'); // Old ComfyUI format
- if (!filename) {
- // New format - extract from path
- filename = url.pathname.split('/').pop(); // Gets "image123.png"
+ let filename: string | null = null;
+
+ if (isMatrixGrid(currentImage)) {
+ const url = new URL(currentImage.url);
+ filename = url.pathname.split('/').pop();
+ } else {
+ const url = new URL(currentImage.url);
+ filename = url.searchParams.get('filename') || url.pathname.split('/').pop();
}
if (!filename) {
@@ -116,7 +157,6 @@ const ImagePreview: React.FC = ({ onTabChange }) => {
return;
}
- // Call our API endpoint
const response = await fetch('http://localhost:5002/api/send-to-img2img', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -125,7 +165,6 @@ const ImagePreview: React.FC = ({ onTabChange }) => {
const result = await response.json();
if (result.status === 'success') {
- // Use our server's images endpoint
const imageUrl = `http://localhost:5001/api/images/${filename}`;
const imageBlob = await fetch(imageUrl).then(r => r.blob());
setInputImage({
@@ -133,7 +172,6 @@ const ImagePreview: React.FC = ({ onTabChange }) => {
file: new File([imageBlob], filename)
});
- // Switch to img2img tab
onTabChange('img2img');
}
} catch (error) {
@@ -141,12 +179,14 @@ const ImagePreview: React.FC = ({ onTabChange }) => {
}
} else if (destination === 'extras') {
try {
- // Extract filename from URL (supports both old and new formats)
- const url = new URL(currentImage.url);
- let filename = url.searchParams.get('filename'); // Old ComfyUI format
- if (!filename) {
- // New format - extract from path
- filename = url.pathname.split('/').pop(); // Gets "image123.png"
+ let filename: string | null = null;
+
+ if (isMatrixGrid(currentImage)) {
+ const url = new URL(currentImage.url);
+ filename = url.pathname.split('/').pop();
+ } else {
+ const url = new URL(currentImage.url);
+ filename = url.searchParams.get('filename') || url.pathname.split('/').pop();
}
if (!filename) {
@@ -154,7 +194,6 @@ const ImagePreview: React.FC = ({ onTabChange }) => {
return;
}
- // Call our API endpoint
const response = await fetch('http://localhost:5002/api/send-to-extras', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -163,12 +202,10 @@ const ImagePreview: React.FC = ({ onTabChange }) => {
const result = await response.json();
if (result.status === 'success') {
- // Create a File object from the image URL
const imageUrl = `http://localhost:5001/api/images/${filename}`;
const imageBlob = await fetch(imageUrl).then(r => r.blob());
const file = new File([imageBlob], filename);
- // Set the image in Extras component's state
window.sessionStorage.setItem('extrasImage', JSON.stringify({
file: {
name: filename,
@@ -178,7 +215,6 @@ const ImagePreview: React.FC = ({ onTabChange }) => {
preview: imageUrl
}));
- // Switch to extras tab
onTabChange('extras');
}
} catch (error) {
@@ -211,14 +247,63 @@ const ImagePreview: React.FC = ({ onTabChange }) => {
const handleThumbnailClick = (index: number) => {
const actualIndex = thumbnailStartIndex + index;
setSelectedImageIndex(actualIndex);
+ setImageError(null);
};
const formatSettingsDisplay = () => {
if (!currentImage?.settings) return 'No settings available';
const settings = currentImage.settings;
- const settingsAny = settings as any; // Type assertion for additional properties
- return `Prompt: ${currentImage.prompt || 'N/A'}
+ const settingsAny = settings as any;
+
+ const isMatrix = isMatrixGrid(currentImage);
+
+ if (isMatrix && settingsAny.isMatrixGrid) {
+ let matrixInfo = `Matrix Grid Generation
+
+Prompt: ${currentImage.prompt || 'N/A'}
+
+Negative prompt: ${currentImage.negativePrompt || 'N/A'}
+
+Base Settings:
+Steps: ${settings.steps || 'N/A'}
+Sampler: ${settings.sampler_name || 'N/A'}
+CFG scale: ${settings.cfg_scale || 'N/A'}
+Size: ${settings.width || 'N/A'}x${settings.height || 'N/A'}
+Model: ${settings.model_name || 'N/A'}
+`;
+
+ if (settingsAny.matrixAxes) {
+ matrixInfo += '\nMatrix Configuration:\n';
+
+ if (settingsAny.matrixAxes.xAxis) {
+ matrixInfo += `X-Axis (${settingsAny.matrixAxes.xAxis.name}): ${settingsAny.matrixAxes.xAxis.values.join(', ')}\n`;
+ }
+
+ if (settingsAny.matrixAxes.yAxis) {
+ matrixInfo += `Y-Axis (${settingsAny.matrixAxes.yAxis.name}): ${settingsAny.matrixAxes.yAxis.values.join(', ')}\n`;
+ }
+
+ if (settingsAny.matrixAxes.zAxis) {
+ matrixInfo += `Z-Axis (${settingsAny.matrixAxes.zAxis.name}): ${settingsAny.matrixAxes.zAxis.values.join(', ')}\n`;
+ }
+ }
+
+ if (settingsAny.totalJobs) {
+ matrixInfo += `\nTotal Jobs: ${settingsAny.totalJobs}`;
+ }
+
+ if (settingsAny.matrixSettings) {
+ matrixInfo += `\nMatrix Options:
+Draw Legend: ${settingsAny.matrixSettings.drawLegend ? 'Yes' : 'No'}
+Keep Seeds Consistent: ${settingsAny.matrixSettings.keepSeedsConsistent ? 'Yes' : 'No'}
+Include Sub Images: ${settingsAny.matrixSettings.includeSubImages ? 'Yes' : 'No'}
+Include Sub Grids: ${settingsAny.matrixSettings.includeSubgrids ? 'Yes' : 'No'}`;
+ }
+
+ return matrixInfo;
+ } else {
+ return `Prompt: ${currentImage.prompt || 'N/A'}
Negative prompt: ${currentImage.negativePrompt || 'N/A'}
@@ -233,49 +318,103 @@ Denoising strength: ${settings.denoising_strength || 'N/A'}
Version: v1.6.0
Networks not found: add-detail-xl, Double_Exposure
Time taken: 31.1 sec.`;
+ }
};
- return (
-
- {/* Main Image Display */}
-
-
- {isLoading ? (
-
- ) : images.length > 0 && currentImage ? (
-
+ const renderImageContainer = () => {
+ if (!currentImage) return null;
+
+ const isMatrix = isMatrixGrid(currentImage);
+
+ if (isMatrix) {
+ return (
+
+
+
- ) : (
- <>
-
-
Generated Images Will Display Here
- >
+
+
+ {imageError && (
+
+ {imageError}
+
)}
+
-
+ );
+ } else {
+ return (
+
+
+
+

+ {imageError && (
+
+ {imageError}
+
+ )}
+
+
+
+ );
+ }
+ };
+
+ return (
+
+ {isLoading ? (
+
+
+
+
+
+ ) : images.length > 0 && currentImage ? (
+ renderImageContainer()
+ ) : (
+
+
+
+
Generated Images Will Display Here
+
+
+ )}
- {/* Thumbnail Navigation */}
{images.length > 0 && (
- {/* Left Arrow */}
{images.length > maxThumbnails && (