Bossmarc747 commited on
Commit
32cd713
·
1 Parent(s): d196e98
.roo/mcp.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ {
2
+ "mcpServers": {}
3
+ }
README.md CHANGED
@@ -1,5 +1,5 @@
1
  ---
2
- title: Innovideo
3
  emoji: 🖼
4
  colorFrom: purple
5
  colorTo: red
@@ -8,7 +8,81 @@ sdk_version: 5.25.2
8
  app_file: app.py
9
  pinned: false
10
  license: mit
11
- short_description: Innovatehub Video Generator
12
  ---
13
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: ImageGen AI
3
  emoji: 🖼
4
  colorFrom: purple
5
  colorTo: red
 
8
  app_file: app.py
9
  pinned: false
10
  license: mit
11
+ short_description: AI-Powered Text-to-Image Generator
12
  ---
13
 
14
+ # ImageGen AI
15
+
16
+ A powerful text-to-image generation application built with Gradio and Hugging Face's diffusers library. This application allows users to generate high-quality images from text descriptions using Stability AI's SDXL-Turbo model.
17
+
18
+ ## Features
19
+
20
+ - **Text-to-Image Generation**: Create images from text descriptions
21
+ - **Customizable Parameters**: Adjust settings like image size, guidance scale, and inference steps
22
+ - **Negative Prompts**: Specify what you don't want in the generated image
23
+ - **Image History**: View and manage previously generated images
24
+ - **Save Images**: Save your favorite generations to disk
25
+ - **Example Prompts**: Get started quickly with example prompts
26
+
27
+ ## Technical Details
28
+
29
+ This application uses:
30
+ - **Stability AI's SDXL-Turbo model** for fast, high-quality image generation
31
+ - **Gradio** for the user interface
32
+ - **Hugging Face's diffusers library** for the AI backend
33
+ - **PyTorch** as the deep learning framework
34
+
35
+ ## Project Structure
36
+
37
+ - `app.py`: Main entry point for the application
38
+ - `model.py`: Model initialization and inference logic
39
+ - `ui.py`: Gradio UI components and layout
40
+ - `utils.py`: Utility functions for image saving and history management
41
+ - `config.py`: Configuration settings
42
+ - `test_app.py`: Unit tests for the application
43
+
44
+ ## Getting Started
45
+
46
+ 1. Install the required dependencies:
47
+ ```
48
+ pip install -r requirements.txt
49
+ ```
50
+
51
+ 2. Run the application:
52
+ ```
53
+ python app.py
54
+ ```
55
+
56
+ 3. Open your browser and navigate to the URL displayed in the terminal (the application will try ports 7860-7869 until it finds an available one)
57
+
58
+ You can also specify a custom port by setting the GRADIO_SERVER_PORT environment variable:
59
+ ```
60
+ GRADIO_SERVER_PORT=8000 python app.py
61
+ ```
62
+
63
+ ## Usage
64
+
65
+ 1. Enter a text description in the "Prompt" field
66
+ 2. (Optional) Enter what you want to avoid in the "Negative Prompt" field
67
+ 3. (Optional) Adjust advanced settings like image size, guidance scale, etc.
68
+ 4. Click "Generate Image" and wait for the result
69
+ 5. Save your favorite images using the "Save Image" button
70
+
71
+ ## Examples
72
+
73
+ Try these prompts:
74
+ - "Astronaut in a jungle, cold color palette, muted colors, detailed, 8k"
75
+ - "A serene mountain lake with reflections of pine trees"
76
+ - "Futuristic cityscape at sunset with flying cars"
77
+
78
+ ## License
79
+
80
+ This project is licensed under the MIT License - see the LICENSE file for details.
81
+
82
+ ## Acknowledgments
83
+
84
+ - [Hugging Face](https://huggingface.co/) for the diffusers library
85
+ - [Stability AI](https://stability.ai/) for the SDXL-Turbo model
86
+ - [Gradio](https://gradio.app/) for the UI framework
87
+
88
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
__pycache__/config.cpython-310.pyc ADDED
Binary file (1.25 kB). View file
 
__pycache__/model.cpython-310.pyc ADDED
Binary file (3.93 kB). View file
 
__pycache__/test_app.cpython-310.pyc ADDED
Binary file (4.91 kB). View file
 
__pycache__/ui.cpython-310.pyc ADDED
Binary file (6.52 kB). View file
 
__pycache__/utils.cpython-310.pyc ADDED
Binary file (4.38 kB). View file
 
app.py CHANGED
@@ -1,143 +1,41 @@
1
- import gradio as gr
2
- import numpy as np
3
- import random
4
-
5
- # import spaces #[uncomment to use ZeroGPU]
6
- from diffusers import DiffusionPipeline
7
- import torch
8
-
9
- device = "cuda" if torch.cuda.is_available() else "cpu"
10
- model_repo_id = "stabilityai/sdxl-turbo" # Replace to the model you would like to use
11
-
12
- if torch.cuda.is_available():
13
- torch_dtype = torch.float16
14
- else:
15
- torch_dtype = torch.float32
16
-
17
- pipe = DiffusionPipeline.from_pretrained(model_repo_id, torch_dtype=torch_dtype)
18
- pipe = pipe.to(device)
19
-
20
- MAX_SEED = np.iinfo(np.int32).max
21
- MAX_IMAGE_SIZE = 1024
22
-
23
-
24
- # @spaces.GPU #[uncomment to use ZeroGPU]
25
- def infer(
26
- prompt,
27
- negative_prompt,
28
- seed,
29
- randomize_seed,
30
- width,
31
- height,
32
- guidance_scale,
33
- num_inference_steps,
34
- progress=gr.Progress(track_tqdm=True),
35
- ):
36
- if randomize_seed:
37
- seed = random.randint(0, MAX_SEED)
38
-
39
- generator = torch.Generator().manual_seed(seed)
40
-
41
- image = pipe(
42
- prompt=prompt,
43
- negative_prompt=negative_prompt,
44
- guidance_scale=guidance_scale,
45
- num_inference_steps=num_inference_steps,
46
- width=width,
47
- height=height,
48
- generator=generator,
49
- ).images[0]
50
-
51
- return image, seed
52
-
53
-
54
- examples = [
55
- "Astronaut in a jungle, cold color palette, muted colors, detailed, 8k",
56
- "An astronaut riding a green horse",
57
- "A delicious ceviche cheesecake slice",
58
- ]
59
-
60
- css = """
61
- #col-container {
62
- margin: 0 auto;
63
- max-width: 640px;
64
- }
65
  """
 
66
 
67
- with gr.Blocks(css=css) as demo:
68
- with gr.Column(elem_id="col-container"):
69
- gr.Markdown(" # Text-to-Image Gradio Template")
70
-
71
- with gr.Row():
72
- prompt = gr.Text(
73
- label="Prompt",
74
- show_label=False,
75
- max_lines=1,
76
- placeholder="Enter your prompt",
77
- container=False,
78
- )
79
-
80
- run_button = gr.Button("Run", scale=0, variant="primary")
81
-
82
- result = gr.Image(label="Result", show_label=False)
83
-
84
- with gr.Accordion("Advanced Settings", open=False):
85
- negative_prompt = gr.Text(
86
- label="Negative prompt",
87
- max_lines=1,
88
- placeholder="Enter a negative prompt",
89
- visible=False,
90
- )
91
-
92
- seed = gr.Slider(
93
- label="Seed",
94
- minimum=0,
95
- maximum=MAX_SEED,
96
- step=1,
97
- value=0,
98
- )
99
-
100
- randomize_seed = gr.Checkbox(label="Randomize seed", value=True)
101
-
102
- with gr.Row():
103
- width = gr.Slider(
104
- label="Width",
105
- minimum=256,
106
- maximum=MAX_IMAGE_SIZE,
107
- step=32,
108
- value=1024, # Replace with defaults that work for your model
109
- )
110
-
111
- height = gr.Slider(
112
- label="Height",
113
- minimum=256,
114
- maximum=MAX_IMAGE_SIZE,
115
- step=32,
116
- value=1024, # Replace with defaults that work for your model
117
- )
118
-
119
- with gr.Row():
120
- guidance_scale = gr.Slider(
121
- label="Guidance scale",
122
- minimum=0.0,
123
- maximum=10.0,
124
- step=0.1,
125
- value=0.0, # Replace with defaults that work for your model
126
- )
127
-
128
- num_inference_steps = gr.Slider(
129
- label="Number of inference steps",
130
- minimum=1,
131
- maximum=50,
132
- step=1,
133
- value=2, # Replace with defaults that work for your model
134
- )
135
 
136
- gr.Examples(examples=examples, inputs=[prompt])
137
- gr.on(
138
- triggers=[run_button.click, prompt.submit],
139
- fn=infer,
140
- inputs=[
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
  prompt,
142
  negative_prompt,
143
  seed,
@@ -146,9 +44,43 @@ with gr.Blocks(css=css) as demo:
146
  height,
147
  guidance_scale,
148
  num_inference_steps,
149
- ],
150
- outputs=[result, seed],
151
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
 
153
  if __name__ == "__main__":
154
- demo.launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  """
2
+ ImageGen AI - A text-to-image generation application.
3
 
4
+ This is the main entry point for the application, which initializes
5
+ the model and UI components and launches the Gradio interface.
6
+ """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
 
8
+ import logging
9
+ import os
10
+ import sys
11
+
12
+ # Configure logging
13
+ logging.basicConfig(
14
+ level=logging.INFO,
15
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
16
+ handlers=[
17
+ logging.StreamHandler(sys.stdout)
18
+ ]
19
+ )
20
+ logger = logging.getLogger(__name__)
21
+
22
+ # Import application modules
23
+ from model import ModelManager
24
+ from ui import ImageGenUI
25
+
26
+
27
+ def main():
28
+ """Initialize and launch the application."""
29
+ try:
30
+ logger.info("Initializing ImageGen AI application")
31
+
32
+ # Initialize the model
33
+ logger.info("Loading AI model")
34
+ model_manager = ModelManager()
35
+ model_manager.load_model()
36
+
37
+ # Create wrapper function for image generation
38
+ def generate_image(
39
  prompt,
40
  negative_prompt,
41
  seed,
 
44
  height,
45
  guidance_scale,
46
  num_inference_steps,
47
+ progress_callback=None
48
+ ):
49
+ return model_manager.generate_image(
50
+ prompt,
51
+ negative_prompt,
52
+ seed,
53
+ randomize_seed,
54
+ width,
55
+ height,
56
+ guidance_scale,
57
+ num_inference_steps,
58
+ progress_callback
59
+ )
60
+
61
+ # Initialize and launch the UI
62
+ logger.info("Setting up user interface")
63
+ ui = ImageGenUI(generate_image)
64
+ ui.build_ui()
65
+
66
+ logger.info("Launching application")
67
+ # Try multiple ports in case some are already in use
68
+ for port in range(7860, 7870):
69
+ try:
70
+ logger.info(f"Attempting to launch on port {port}")
71
+ ui.launch(share=False, server_port=port)
72
+ logger.info(f"Successfully launched on port {port}")
73
+ break
74
+ except OSError as e:
75
+ logger.warning(f"Port {port} is in use, trying next port. Error: {str(e)}")
76
+ if port == 7869: # Last port in range
77
+ logger.error("Could not find an available port in range 7860-7869")
78
+ raise
79
+
80
+ except Exception as e:
81
+ logger.error(f"Error starting application: {str(e)}")
82
+ raise
83
+
84
 
85
  if __name__ == "__main__":
86
+ main()
config.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Configuration settings for the image generation application.
3
+
4
+ This module contains all the configuration parameters used throughout the application,
5
+ making it easier to modify settings in one place.
6
+ """
7
+
8
+ import numpy as np
9
+
10
+ # Model settings
11
+ MODEL_REPO_ID = "stabilityai/sdxl-turbo"
12
+ DEFAULT_GUIDANCE_SCALE = 0.0
13
+ DEFAULT_INFERENCE_STEPS = 2
14
+ DEFAULT_WIDTH = 1024
15
+ DEFAULT_HEIGHT = 1024
16
+
17
+ # UI settings
18
+ APP_TITLE = "ImageGen AI"
19
+ APP_DESCRIPTION = "Generate stunning images from text descriptions using SDXL-Turbo"
20
+ MAX_IMAGE_SIZE = 1024
21
+ MAX_SEED = np.iinfo(np.int32).max
22
+
23
+ # Example prompts
24
+ EXAMPLE_PROMPTS = [
25
+ "Astronaut in a jungle, cold color palette, muted colors, detailed, 8k",
26
+ "An astronaut riding a green horse",
27
+ "A delicious ceviche cheesecake slice",
28
+ "Futuristic cityscape at sunset with flying cars",
29
+ "A serene mountain lake with reflections of pine trees"
30
+ ]
31
+
32
+ # CSS for UI styling
33
+ CSS = """
34
+ #col-container {
35
+ margin: 0 auto;
36
+ max-width: 640px;
37
+ }
38
+ .output-image {
39
+ border-radius: 8px;
40
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
41
+ }
42
+ .footer {
43
+ text-align: center;
44
+ margin-top: 20px;
45
+ font-size: 0.8em;
46
+ color: #666;
47
+ }
48
+ """
model.py ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Model initialization and inference logic for image generation.
3
+
4
+ This module handles loading the diffusion model and provides functions
5
+ for generating images from text prompts with error handling.
6
+ """
7
+
8
+ import logging
9
+ import random
10
+ from typing import Tuple, Optional, Union
11
+
12
+ import numpy as np
13
+ import torch
14
+ from diffusers import DiffusionPipeline
15
+ from PIL import Image
16
+
17
+ from config import MODEL_REPO_ID, MAX_SEED
18
+
19
+ # Configure logging
20
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
21
+ logger = logging.getLogger(__name__)
22
+
23
+ class ModelManager:
24
+ """Manages the diffusion model for image generation."""
25
+
26
+ def __init__(self):
27
+ """Initialize the ModelManager and load the model."""
28
+ self.device = "cuda" if torch.cuda.is_available() else "cpu"
29
+ self.torch_dtype = torch.float16 if torch.cuda.is_available() else torch.float32
30
+ self.pipe = None
31
+
32
+ def load_model(self) -> None:
33
+ """
34
+ Load the diffusion model from the specified repository.
35
+
36
+ Handles potential errors during model loading.
37
+ """
38
+ try:
39
+ logger.info(f"Loading model {MODEL_REPO_ID} on {self.device} with {self.torch_dtype}")
40
+ self.pipe = DiffusionPipeline.from_pretrained(
41
+ MODEL_REPO_ID,
42
+ torch_dtype=self.torch_dtype
43
+ )
44
+ self.pipe = self.pipe.to(self.device)
45
+ logger.info("Model loaded successfully")
46
+ except Exception as e:
47
+ logger.error(f"Error loading model: {str(e)}")
48
+ raise RuntimeError(f"Failed to load model: {str(e)}")
49
+
50
+ def generate_image(
51
+ self,
52
+ prompt: str,
53
+ negative_prompt: str = "",
54
+ seed: int = 0,
55
+ randomize_seed: bool = True,
56
+ width: int = 1024,
57
+ height: int = 1024,
58
+ guidance_scale: float = 0.0,
59
+ num_inference_steps: int = 2,
60
+ progress_callback: Optional[callable] = None
61
+ ) -> Tuple[Union[Image.Image, None], int]:
62
+ """
63
+ Generate an image based on the provided prompt and parameters.
64
+
65
+ Args:
66
+ prompt: Text description of the desired image
67
+ negative_prompt: Text description of what to avoid in the image
68
+ seed: Random seed for reproducibility
69
+ randomize_seed: Whether to use a random seed
70
+ width: Width of the generated image
71
+ height: Height of the generated image
72
+ guidance_scale: How closely to follow the prompt
73
+ num_inference_steps: Number of denoising steps
74
+ progress_callback: Optional callback function for progress updates
75
+
76
+ Returns:
77
+ Tuple containing the generated image and the seed used
78
+ """
79
+ if self.pipe is None:
80
+ logger.error("Model not loaded. Call load_model() first.")
81
+ return None, seed
82
+
83
+ # Validate inputs
84
+ if not prompt or prompt.strip() == "":
85
+ logger.warning("Empty prompt provided, using default")
86
+ prompt = "A beautiful landscape"
87
+
88
+ # Handle seed randomization
89
+ if randomize_seed:
90
+ seed = random.randint(0, MAX_SEED)
91
+
92
+ # Set up generator for reproducibility
93
+ generator = torch.Generator(device=self.device).manual_seed(seed)
94
+
95
+ try:
96
+ logger.info(f"Generating image with prompt: '{prompt}'")
97
+
98
+ # Generate the image
99
+ result = self.pipe(
100
+ prompt=prompt,
101
+ negative_prompt=negative_prompt,
102
+ guidance_scale=guidance_scale,
103
+ num_inference_steps=num_inference_steps,
104
+ width=width,
105
+ height=height,
106
+ generator=generator,
107
+ callback=progress_callback
108
+ )
109
+
110
+ image = result.images[0]
111
+ logger.info(f"Image generated successfully with seed {seed}")
112
+ return image, seed
113
+
114
+ except Exception as e:
115
+ logger.error(f"Error generating image: {str(e)}")
116
+ return None, seed
requirements.txt CHANGED
@@ -1,6 +1,9 @@
1
  accelerate
2
  diffusers
 
3
  invisible_watermark
 
 
4
  torch
5
  transformers
6
  xformers
 
1
  accelerate
2
  diffusers
3
+ gradio>=5.25.2
4
  invisible_watermark
5
+ numpy
6
+ pillow
7
  torch
8
  transformers
9
  xformers
test_app.py ADDED
@@ -0,0 +1,180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Tests for the image generation application.
3
+
4
+ This module contains unit tests for the various components of the application.
5
+ """
6
+
7
+ import unittest
8
+ from unittest.mock import MagicMock, patch
9
+ import os
10
+ from pathlib import Path
11
+
12
+ import numpy as np
13
+ from PIL import Image
14
+
15
+ # Import application modules
16
+ from config import MODEL_REPO_ID, MAX_SEED
17
+ from model import ModelManager
18
+ from utils import save_image, format_generation_info, GenerationHistory
19
+
20
+
21
+ class TestConfig(unittest.TestCase):
22
+ """Test the configuration module."""
23
+
24
+ def test_config_values(self):
25
+ """Test that configuration values are properly set."""
26
+ from config import (
27
+ MODEL_REPO_ID,
28
+ DEFAULT_GUIDANCE_SCALE,
29
+ DEFAULT_INFERENCE_STEPS,
30
+ DEFAULT_WIDTH,
31
+ DEFAULT_HEIGHT,
32
+ MAX_IMAGE_SIZE,
33
+ EXAMPLE_PROMPTS
34
+ )
35
+
36
+ self.assertEqual(MODEL_REPO_ID, "stabilityai/sdxl-turbo")
37
+ self.assertEqual(DEFAULT_GUIDANCE_SCALE, 0.0)
38
+ self.assertEqual(DEFAULT_INFERENCE_STEPS, 2)
39
+ self.assertEqual(DEFAULT_WIDTH, 1024)
40
+ self.assertEqual(DEFAULT_HEIGHT, 1024)
41
+ self.assertEqual(MAX_IMAGE_SIZE, 1024)
42
+ self.assertIsInstance(EXAMPLE_PROMPTS, list)
43
+ self.assertTrue(len(EXAMPLE_PROMPTS) > 0)
44
+
45
+
46
+ class TestModelManager(unittest.TestCase):
47
+ """Test the ModelManager class."""
48
+
49
+ @patch('model.DiffusionPipeline')
50
+ def test_init(self, mock_pipeline):
51
+ """Test ModelManager initialization."""
52
+ manager = ModelManager()
53
+ self.assertIn(manager.device, ["cuda", "cpu"])
54
+ self.assertIsNone(manager.pipe)
55
+
56
+ @patch('model.DiffusionPipeline.from_pretrained')
57
+ def test_load_model(self, mock_from_pretrained):
58
+ """Test model loading."""
59
+ # Setup mock
60
+ mock_pipe = MagicMock()
61
+ mock_from_pretrained.return_value = mock_pipe
62
+ mock_pipe.to.return_value = mock_pipe
63
+
64
+ # Test loading
65
+ manager = ModelManager()
66
+ manager.load_model()
67
+
68
+ # Verify calls
69
+ mock_from_pretrained.assert_called_once_with(
70
+ MODEL_REPO_ID,
71
+ torch_dtype=manager.torch_dtype
72
+ )
73
+ mock_pipe.to.assert_called_once_with(manager.device)
74
+ self.assertEqual(manager.pipe, mock_pipe)
75
+
76
+ @patch('model.DiffusionPipeline')
77
+ def test_generate_image_with_randomize(self, mock_pipeline):
78
+ """Test image generation with randomized seed."""
79
+ # Setup mock
80
+ manager = ModelManager()
81
+ manager.pipe = MagicMock()
82
+ mock_image = MagicMock()
83
+ manager.pipe.return_value = MagicMock(images=[mock_image])
84
+
85
+ # Test generation with randomized seed
86
+ prompt = "test prompt"
87
+ image, seed = manager.generate_image(
88
+ prompt=prompt,
89
+ randomize_seed=True
90
+ )
91
+
92
+ # Verify result
93
+ self.assertEqual(image, mock_image)
94
+ self.assertGreaterEqual(seed, 0)
95
+ self.assertLessEqual(seed, MAX_SEED)
96
+
97
+
98
+ class TestUtils(unittest.TestCase):
99
+ """Test utility functions."""
100
+
101
+ def setUp(self):
102
+ """Set up test environment."""
103
+ # Create a test image
104
+ self.test_image = Image.new('RGB', (100, 100), color='red')
105
+
106
+ # Ensure test output directory exists
107
+ from utils import OUTPUTS_DIR
108
+ self.test_outputs_dir = OUTPUTS_DIR
109
+ self.test_outputs_dir.mkdir(exist_ok=True)
110
+
111
+ def test_save_image(self):
112
+ """Test image saving functionality."""
113
+ prompt = "test image prompt"
114
+ filepath = save_image(self.test_image, prompt)
115
+
116
+ # Check that file was created
117
+ self.assertTrue(os.path.exists(filepath))
118
+ self.assertTrue(filepath.endswith(".png"))
119
+
120
+ # Clean up
121
+ os.remove(filepath)
122
+
123
+ def test_format_generation_info(self):
124
+ """Test generation info formatting."""
125
+ prompt = "test prompt"
126
+ negative_prompt = "test negative"
127
+ seed = 42
128
+ width = 512
129
+ height = 512
130
+ guidance_scale = 7.5
131
+ steps = 30
132
+
133
+ info = format_generation_info(
134
+ prompt, negative_prompt, seed, width, height, guidance_scale, steps
135
+ )
136
+
137
+ # Check that all parameters are included in the info string
138
+ self.assertIn(prompt, info)
139
+ self.assertIn(negative_prompt, info)
140
+ self.assertIn(str(seed), info)
141
+ self.assertIn(str(width), info)
142
+ self.assertIn(str(height), info)
143
+ self.assertIn(str(guidance_scale), info)
144
+ self.assertIn(str(steps), info)
145
+
146
+ def test_generation_history(self):
147
+ """Test the GenerationHistory class."""
148
+ history = GenerationHistory(max_history=3)
149
+
150
+ # Test empty history
151
+ self.assertEqual(len(history.history), 0)
152
+ self.assertEqual(history.get_latest(), [])
153
+
154
+ # Add entries
155
+ for i in range(5):
156
+ history.add(
157
+ self.test_image,
158
+ f"prompt {i}",
159
+ f"negative {i}",
160
+ i,
161
+ 512,
162
+ 512,
163
+ 7.5,
164
+ 30
165
+ )
166
+
167
+ # Check that history is limited to max_history
168
+ self.assertEqual(len(history.history), 3)
169
+
170
+ # Check that entries are in correct order (newest last)
171
+ latest = history.get_latest(1)[0]
172
+ self.assertEqual(latest["prompt"], "prompt 4")
173
+
174
+ # Test clear
175
+ history.clear()
176
+ self.assertEqual(len(history.history), 0)
177
+
178
+
179
+ if __name__ == '__main__':
180
+ unittest.main()
ui.py ADDED
@@ -0,0 +1,301 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Gradio UI components and layout for the image generation application.
3
+
4
+ This module defines the user interface using Gradio components,
5
+ including input controls, output displays, and event handlers.
6
+ """
7
+
8
+ import gradio as gr
9
+ import time
10
+ from typing import Callable, Dict, Any, List, Tuple
11
+
12
+ from config import (
13
+ APP_TITLE,
14
+ APP_DESCRIPTION,
15
+ EXAMPLE_PROMPTS,
16
+ CSS,
17
+ MAX_IMAGE_SIZE,
18
+ MAX_SEED,
19
+ DEFAULT_WIDTH,
20
+ DEFAULT_HEIGHT,
21
+ DEFAULT_GUIDANCE_SCALE,
22
+ DEFAULT_INFERENCE_STEPS
23
+ )
24
+ from utils import save_image, format_generation_info, GenerationHistory
25
+
26
+
27
+ class ImageGenUI:
28
+ """Manages the Gradio UI for the image generation application."""
29
+
30
+ def __init__(self, generate_func: Callable):
31
+ """
32
+ Initialize the UI with the image generation function.
33
+
34
+ Args:
35
+ generate_func: Function to call for image generation
36
+ """
37
+ self.generate_func = generate_func
38
+ self.history = GenerationHistory(max_history=10)
39
+ self.demo = None
40
+
41
+ def build_ui(self) -> gr.Blocks:
42
+ """
43
+ Build and configure the Gradio UI.
44
+
45
+ Returns:
46
+ Configured Gradio Blocks interface
47
+ """
48
+ with gr.Blocks(css=CSS) as demo:
49
+ gr.Markdown(f"# {APP_TITLE}")
50
+ gr.Markdown(APP_DESCRIPTION)
51
+
52
+ with gr.Row():
53
+ with gr.Column(scale=3):
54
+ # Input controls
55
+ with gr.Group():
56
+ prompt = gr.Text(
57
+ label="Prompt",
58
+ placeholder="Describe the image you want to generate",
59
+ lines=2
60
+ )
61
+
62
+ negative_prompt = gr.Text(
63
+ label="Negative Prompt",
64
+ placeholder="Describe what you want to avoid in the image",
65
+ lines=2
66
+ )
67
+
68
+ with gr.Row():
69
+ generate_btn = gr.Button("Generate Image", variant="primary")
70
+ clear_btn = gr.Button("Clear")
71
+
72
+ # Advanced settings
73
+ with gr.Accordion("Advanced Settings", open=False):
74
+ with gr.Row():
75
+ with gr.Column():
76
+ seed = gr.Slider(
77
+ label="Seed",
78
+ minimum=0,
79
+ maximum=MAX_SEED,
80
+ step=1,
81
+ value=0
82
+ )
83
+ randomize_seed = gr.Checkbox(
84
+ label="Randomize seed",
85
+ value=True
86
+ )
87
+
88
+ with gr.Column():
89
+ width = gr.Slider(
90
+ label="Width",
91
+ minimum=256,
92
+ maximum=MAX_IMAGE_SIZE,
93
+ step=32,
94
+ value=DEFAULT_WIDTH
95
+ )
96
+ height = gr.Slider(
97
+ label="Height",
98
+ minimum=256,
99
+ maximum=MAX_IMAGE_SIZE,
100
+ step=32,
101
+ value=DEFAULT_HEIGHT
102
+ )
103
+
104
+ with gr.Row():
105
+ guidance_scale = gr.Slider(
106
+ label="Guidance Scale",
107
+ minimum=0.0,
108
+ maximum=10.0,
109
+ step=0.1,
110
+ value=DEFAULT_GUIDANCE_SCALE,
111
+ info="How closely to follow the prompt (higher = more faithful)"
112
+ )
113
+
114
+ num_inference_steps = gr.Slider(
115
+ label="Inference Steps",
116
+ minimum=1,
117
+ maximum=50,
118
+ step=1,
119
+ value=DEFAULT_INFERENCE_STEPS,
120
+ info="More steps = higher quality but slower generation"
121
+ )
122
+
123
+ with gr.Column(scale=4):
124
+ # Output display
125
+ with gr.Group():
126
+ result_image = gr.Image(
127
+ label="Generated Image",
128
+ elem_classes=["output-image"]
129
+ )
130
+ image_info = gr.Markdown(label="Image Details")
131
+
132
+ with gr.Row():
133
+ save_btn = gr.Button("Save Image")
134
+ save_status = gr.Markdown("")
135
+
136
+ # Example prompts
137
+ gr.Examples(
138
+ examples=EXAMPLE_PROMPTS,
139
+ inputs=prompt,
140
+ label="Example Prompts"
141
+ )
142
+
143
+ # Generation history
144
+ with gr.Accordion("Generation History", open=False):
145
+ history_gallery = gr.Gallery(
146
+ label="Previous Generations",
147
+ show_label=True,
148
+ elem_id="history-gallery",
149
+ columns=5,
150
+ height="auto"
151
+ )
152
+ refresh_history_btn = gr.Button("Refresh History")
153
+
154
+ # Footer
155
+ gr.Markdown(
156
+ "Made with ❤️ using Gradio and Hugging Face Diffusers",
157
+ elem_classes=["footer"]
158
+ )
159
+
160
+ # Event handlers
161
+ def generate_image(
162
+ prompt_text,
163
+ negative_prompt_text,
164
+ seed_val,
165
+ randomize,
166
+ width_val,
167
+ height_val,
168
+ guidance,
169
+ steps,
170
+ progress=gr.Progress(track_tqdm=True)
171
+ ):
172
+ """Handle image generation and update UI."""
173
+ # Generate the image
174
+ image, used_seed = self.generate_func(
175
+ prompt_text,
176
+ negative_prompt_text,
177
+ seed_val,
178
+ randomize,
179
+ width_val,
180
+ height_val,
181
+ guidance,
182
+ steps,
183
+ progress_callback=progress.tqdm
184
+ )
185
+
186
+ # Update info text
187
+ info = format_generation_info(
188
+ prompt_text,
189
+ negative_prompt_text,
190
+ used_seed,
191
+ width_val,
192
+ height_val,
193
+ guidance,
194
+ steps
195
+ )
196
+
197
+ # Add to history
198
+ if image is not None:
199
+ self.history.add(
200
+ image,
201
+ prompt_text,
202
+ negative_prompt_text,
203
+ used_seed,
204
+ width_val,
205
+ height_val,
206
+ guidance,
207
+ steps
208
+ )
209
+
210
+ return image, info, used_seed
211
+
212
+ def save_current_image(image, prompt_text):
213
+ """Save the current image and return status."""
214
+ if image is None:
215
+ return "No image to save"
216
+
217
+ try:
218
+ filepath = save_image(image, prompt_text)
219
+ return f"Image saved to {filepath}"
220
+ except Exception as e:
221
+ return f"Error saving image: {str(e)}"
222
+
223
+ def update_history():
224
+ """Update the history gallery."""
225
+ entries = self.history.get_latest(10)
226
+ if not entries:
227
+ return []
228
+
229
+ # Format for gallery
230
+ images = [entry["image"] for entry in entries]
231
+ labels = [f"{entry['prompt'][:30]}..." for entry in entries]
232
+ return gr.Gallery.update(value=images, label=labels)
233
+
234
+ def clear_inputs():
235
+ """Clear all input fields."""
236
+ return [
237
+ gr.Text.update(value=""), # prompt
238
+ gr.Text.update(value=""), # negative_prompt
239
+ gr.Slider.update(value=0), # seed
240
+ gr.Checkbox.update(value=True), # randomize_seed
241
+ gr.Markdown.update(value="") # image_info
242
+ ]
243
+
244
+ # Connect event handlers
245
+ generate_btn.click(
246
+ fn=generate_image,
247
+ inputs=[
248
+ prompt,
249
+ negative_prompt,
250
+ seed,
251
+ randomize_seed,
252
+ width,
253
+ height,
254
+ guidance_scale,
255
+ num_inference_steps
256
+ ],
257
+ outputs=[result_image, image_info, seed]
258
+ )
259
+
260
+ prompt.submit(
261
+ fn=generate_image,
262
+ inputs=[
263
+ prompt,
264
+ negative_prompt,
265
+ seed,
266
+ randomize_seed,
267
+ width,
268
+ height,
269
+ guidance_scale,
270
+ num_inference_steps
271
+ ],
272
+ outputs=[result_image, image_info, seed]
273
+ )
274
+
275
+ save_btn.click(
276
+ fn=save_current_image,
277
+ inputs=[result_image, prompt],
278
+ outputs=[save_status]
279
+ )
280
+
281
+ refresh_history_btn.click(
282
+ fn=update_history,
283
+ inputs=[],
284
+ outputs=[history_gallery]
285
+ )
286
+
287
+ clear_btn.click(
288
+ fn=clear_inputs,
289
+ inputs=[],
290
+ outputs=[prompt, negative_prompt, seed, randomize_seed, image_info]
291
+ )
292
+
293
+ self.demo = demo
294
+ return demo
295
+
296
+ def launch(self, **kwargs):
297
+ """Launch the Gradio interface with the specified parameters."""
298
+ if self.demo is None:
299
+ self.build_ui()
300
+
301
+ self.demo.launch(**kwargs)
utils.py ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Utility functions for the image generation application.
3
+
4
+ This module provides helper functions for tasks like image saving,
5
+ timestamp generation, and other common operations.
6
+ """
7
+
8
+ import os
9
+ import time
10
+ from datetime import datetime
11
+ from pathlib import Path
12
+ from typing import Optional
13
+
14
+ import gradio as gr
15
+ from PIL import Image
16
+
17
+ # Create output directory for saved images
18
+ OUTPUTS_DIR = Path("outputs")
19
+ OUTPUTS_DIR.mkdir(exist_ok=True)
20
+
21
+
22
+ def save_image(image: Image.Image, prompt: str) -> str:
23
+ """
24
+ Save the generated image to disk with a filename based on timestamp and prompt.
25
+
26
+ Args:
27
+ image: The PIL Image to save
28
+ prompt: The prompt used to generate the image
29
+
30
+ Returns:
31
+ Path to the saved image
32
+ """
33
+ if image is None:
34
+ return ""
35
+
36
+ # Create a filename from the timestamp and a shortened version of the prompt
37
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
38
+ # Clean prompt for filename use (first 20 chars, alphanumeric only)
39
+ clean_prompt = "".join(c for c in prompt if c.isalnum() or c.isspace())[:20].strip()
40
+ clean_prompt = clean_prompt.replace(" ", "_")
41
+
42
+ filename = f"{timestamp}_{clean_prompt}.png"
43
+ filepath = OUTPUTS_DIR / filename
44
+
45
+ # Save the image
46
+ image.save(filepath)
47
+ return str(filepath)
48
+
49
+
50
+ def format_generation_info(
51
+ prompt: str,
52
+ negative_prompt: str,
53
+ seed: int,
54
+ width: int,
55
+ height: int,
56
+ guidance_scale: float,
57
+ steps: int
58
+ ) -> str:
59
+ """
60
+ Format generation parameters into a readable string.
61
+
62
+ Args:
63
+ prompt: Text prompt used
64
+ negative_prompt: Negative prompt used
65
+ seed: Random seed used
66
+ width: Image width
67
+ height: Image height
68
+ guidance_scale: Guidance scale value
69
+ steps: Number of inference steps
70
+
71
+ Returns:
72
+ Formatted string with generation parameters
73
+ """
74
+ info = f"**Prompt:** {prompt}\n"
75
+ if negative_prompt:
76
+ info += f"**Negative prompt:** {negative_prompt}\n"
77
+ info += f"**Seed:** {seed}\n"
78
+ info += f"**Size:** {width}x{height}\n"
79
+ info += f"**Guidance scale:** {guidance_scale}\n"
80
+ info += f"**Steps:** {steps}\n"
81
+ return info
82
+
83
+
84
+ class GenerationHistory:
85
+ """Manages a history of generated images and their parameters."""
86
+
87
+ def __init__(self, max_history: int = 10):
88
+ """
89
+ Initialize the generation history.
90
+
91
+ Args:
92
+ max_history: Maximum number of items to keep in history
93
+ """
94
+ self.history = []
95
+ self.max_history = max_history
96
+
97
+ def add(
98
+ self,
99
+ image: Image.Image,
100
+ prompt: str,
101
+ negative_prompt: str,
102
+ seed: int,
103
+ width: int,
104
+ height: int,
105
+ guidance_scale: float,
106
+ steps: int
107
+ ) -> None:
108
+ """
109
+ Add a new generation to the history.
110
+
111
+ Args:
112
+ image: Generated image
113
+ prompt: Text prompt used
114
+ negative_prompt: Negative prompt used
115
+ seed: Random seed used
116
+ width: Image width
117
+ height: Image height
118
+ guidance_scale: Guidance scale value
119
+ steps: Number of inference steps
120
+ """
121
+ if image is None:
122
+ return
123
+
124
+ # Create entry with all relevant information
125
+ entry = {
126
+ "image": image,
127
+ "prompt": prompt,
128
+ "negative_prompt": negative_prompt,
129
+ "seed": seed,
130
+ "width": width,
131
+ "height": height,
132
+ "guidance_scale": guidance_scale,
133
+ "steps": steps,
134
+ "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
135
+ }
136
+
137
+ # Add to history and maintain max size
138
+ self.history.append(entry)
139
+ if len(self.history) > self.max_history:
140
+ self.history.pop(0)
141
+
142
+ def get_latest(self, n: int = 1) -> list:
143
+ """
144
+ Get the latest n entries from history.
145
+
146
+ Args:
147
+ n: Number of entries to retrieve
148
+
149
+ Returns:
150
+ List of history entries
151
+ """
152
+ return self.history[-n:] if self.history else []
153
+
154
+ def clear(self) -> None:
155
+ """Clear the generation history."""
156
+ self.history = []