From f250516b8e1b69bccc89e1c85b8d81b0240f6f75 Mon Sep 17 00:00:00 2001 From: Robin Tuszik <47579899+rtuszik@users.noreply.github.com> Date: Sun, 25 Aug 2024 23:26:30 +0200 Subject: [PATCH 1/7] refactor: use env_file for Docker environment vars Update README.md and docker-compose.yml to use env_file instead of inline environment variables. This improves security and configuration management by centralizing environment variables in a separate .env file. --- README.md | 21 ++++++++++----------- docker-compose.yml | 3 +-- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 9c37074..faa4833 100644 --- a/README.md +++ b/README.md @@ -22,17 +22,16 @@ Docker is the recommended way to run this application. It ensures consistent env ### Docker Compose Setup 1. Create a `docker-compose.yml` file with the following content: ```yaml - services: - replicate-flux-lora: - image: ghcr.io/rtuszik/replicate-flux-lora:latest - container_name: replicate-flux-lora - environment: - - REPLICATE_API_TOKEN=${REPLICATE_API_TOKEN} - ports: - - "8080:8080" - volumes: - - ${HOST_OUTPUT_DIR}:/app/output - restart: unless-stopped + services: + replicate-flux-lora: + image: ghcr.io/rtuszik/replicate-flux-lora:latest + container_name: replicate-flux-lora + env_file: .env + ports: + - "8080:8080" + volumes: + - ${HOST_OUTPUT_DIR}:/app/output + restart: unless-stopped ``` 2. Create a `.env` file in the same directory with the following content: diff --git a/docker-compose.yml b/docker-compose.yml index e9c72bf..c0d58b9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,8 +3,7 @@ services: replicate-flux-lora: image: ghcr.io/rtuszik/replicate-flux-lora:latest container_name: replicate-flux-lora - environment: - - REPLICATE_API_TOKEN=${REPLICATE_API_TOKEN} + env_file: .env ports: - "8080:8080" volumes: From 21a9c5038d19fbc4761fa714fcc3647e741ac612 Mon Sep 17 00:00:00 2001 From: Robin Tuszik <47579899+rtuszik@users.noreply.github.com> Date: Sun, 25 Aug 2024 23:32:56 +0200 Subject: [PATCH 2/7] ci(workflow): optimize Docker build and push process - Add path filters to trigger workflow only on relevant changes - Enhance metadata tagging for better versioning and traceability - Implement GitHub Actions cache for faster builds --- .github/workflows/docker-build-push.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml index 39bc980..0a71eae 100644 --- a/.github/workflows/docker-build-push.yml +++ b/.github/workflows/docker-build-push.yml @@ -4,6 +4,9 @@ on: push: branches: - main + paths: + - src/** + - Dockerfile env: REGISTRY: ghcr.io @@ -35,6 +38,12 @@ jobs: tags: | type=raw,value=latest type=sha,prefix={{branch}}- + type=ref,event=branch + type=semver,pattern={{version}} + labels: | + org.opencontainers.image.source=${{ github.repositoryUrl }} + org.opencontainers.image.revision=${{ github.sha }} + org.opencontainers.image.created=${{ steps.meta.outputs.created }} - name: Build and push Docker image uses: docker/build-push-action@v4 @@ -43,3 +52,5 @@ jobs: push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max From 81efcda083f3b2c9e0d50e727e34c955d4dc6152 Mon Sep 17 00:00:00 2001 From: Robin Tuszik <47579899+rtuszik@users.noreply.github.com> Date: Mon, 26 Aug 2024 16:36:39 +0200 Subject: [PATCH 3/7] refactor(gui): optimize ImageGeneratorGUI class - Add dynamic attribute handling for settings --- src/gui.py | 42 +++++++++++++++++++++++++++++++++++------- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/src/gui.py b/src/gui.py index 8044f57..1208b1b 100644 --- a/src/gui.py +++ b/src/gui.py @@ -10,8 +10,7 @@ from loguru import logger from nicegui import events, ui -# Configure Loguru -logger.remove() # Remove the default handler +logger.remove() logger.add( sys.stderr, format="{time} {level} {message}", filter="my_module", level="INFO" ) @@ -57,9 +56,42 @@ def __init__(self, image_generator): self.load_settings() self.flux_fine_tune_models = self.image_generator.get_flux_fine_tune_models() self.user_added_models = self.settings.get("user_added_models", []) + + self._attributes = [ + "prompt", + "flux_model", + "aspect_ratio", + "num_outputs", + "lora_scale", + "num_inference_steps", + "guidance_scale", + "output_format", + "output_quality", + "disable_safety_checker", + "width", + "height", + "seed", + ] + + for attr in self._attributes: + setattr(self, attr, self.settings.get(attr, None)) + self.setup_ui() logger.info("ImageGeneratorGUI initialized") + def __setattr__(self, name, value): + if name in getattr(self, "_attributes", []): + self.__dict__[name] = value + else: + super().__setattr__(name, value) + + def __getattr__(self, name): + if name in self._attributes: + return self.__dict__.get(name, None) + raise AttributeError( + f"'{self.__class__.__name__}' object has no attribute '{name}'" + ) + def setup_ui(self): ui.dark_mode().enable() @@ -288,7 +320,6 @@ def setup_right_panel(self): self.spinner = ui.spinner(type="infinity", size="xl") self.spinner.visible = False - # Add gallery view self.gallery_container = ui.column().classes("w-full mt-4") self.lightbox = Lightbox() @@ -342,7 +373,6 @@ async def start_generation(self): ) return - # Ensure the model is set in the ImageGenerator self.image_generator.set_model(self.replicate_model_input.value) self.save_settings() @@ -392,9 +422,7 @@ async def download_and_display_images(self, image_urls): response = await client.get(url) if response.status_code == 200: timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - url_part = urllib.parse.urlparse(url).path.split("/")[-2][ - :8 - ] # Get first 8 chars of the unique part + url_part = urllib.parse.urlparse(url).path.split("/")[-2][:8] file_name = f"generated_image_{timestamp}_{url_part}_{i+1}.png" file_path = Path(self.folder_path) / file_name with open(file_path, "wb") as f: From 65a1fb7aea3441538fee4f18813dffdfdd516409 Mon Sep 17 00:00:00 2001 From: Robin Tuszik <47579899+rtuszik@users.noreply.github.com> Date: Mon, 26 Aug 2024 18:29:17 +0200 Subject: [PATCH 4/7] feat(gui): enhance ImageGeneratorGUI with dynamic attribute handling and improved tooltips - Add dynamic attribute handling for settings to simplify code - Improve tooltips for various GUI elements to provide better guidance to users - Refactor `__getattr__` method to use `super().__getattribute__` for better performance - Set `disable_safety_checker` default to `True` for improved safety --- src/gui.py | 51 ++++++++++++++++++++++----------------------------- 1 file changed, 22 insertions(+), 29 deletions(-) diff --git a/src/gui.py b/src/gui.py index 1208b1b..b0b7b24 100644 --- a/src/gui.py +++ b/src/gui.py @@ -79,18 +79,10 @@ def __init__(self, image_generator): self.setup_ui() logger.info("ImageGeneratorGUI initialized") - def __setattr__(self, name, value): - if name in getattr(self, "_attributes", []): - self.__dict__[name] = value - else: - super().__setattr__(name, value) - def __getattr__(self, name): if name in self._attributes: return self.__dict__.get(name, None) - raise AttributeError( - f"'{self.__class__.__name__}' object has no attribute '{name}'" - ) + return super().__getattribute__(name) def setup_ui(self): ui.dark_mode().enable() @@ -107,9 +99,11 @@ def setup_ui(self): logger.info("UI setup completed") def setup_left_panel(self): - self.replicate_model_input = ui.input( - "Replicate Model", value=self.settings.get("replicate_model", "") - ).classes("w-full") + self.replicate_model_input = ( + ui.input("Replicate Model", value=self.settings.get("replicate_model", "")) + .classes("w-full") + .tooltip("Enter the Replicate model URL or identifier") + ) self.replicate_model_input.on("change", self.update_replicate_model) self.flux_models_select = ui.select( @@ -117,8 +111,7 @@ def setup_left_panel(self): label="Flux Fine-Tune Models", value=None, on_change=self.select_flux_model, - ).classes("w-full") - + ).classes("w-full").tooltip("Select Model") with ui.row().classes("w-full"): self.new_model_input = ui.input(label="New Model").classes("w-3/4") ui.button("Add Model", on_click=self.add_user_model).classes("w-1/4") @@ -152,9 +145,9 @@ def setup_left_panel(self): label="Flux Model", value=self.settings.get("flux_model", "dev"), ) - .classes("w-full") - .bind_value(self, "flux_model") - ) + .classes("w-full").tooltip("Which model to run inferences with. the dev model needs around 28 steps but the schnell model only needs around 4 steps.") + .bind_value(self, "flux_model") + ) self.aspect_ratio_select = ( ui.select( @@ -176,8 +169,8 @@ def setup_left_panel(self): value=self.settings.get("aspect_ratio", "1:1"), ) .classes("w-full") - .bind_value(self, "aspect_ratio") - ) + .bind_value(self, "aspect_ratio").tooltip("Width of the generated image. Optional, only used when aspect_ratio=custom. Must be a multiple of 16 (if it's not, it will be rounded to nearest multiple of 16)") + ) self.aspect_ratio_select.on("change", self.toggle_custom_dimensions) with ui.column().classes("w-full").bind_visibility_from( @@ -188,14 +181,14 @@ def setup_left_panel(self): "Width", value=self.settings.get("width", 1024), min=256, max=1440 ) .classes("w-full") - .bind_value(self, "width") + .bind_value(self, "width").tooltip("Width of the generated image. Optional, only used when aspect_ratio=custom. Must be a multiple of 16 (if it's not, it will be rounded to nearest multiple of 16)") ) self.height_input = ( ui.number( "Height", value=self.settings.get("height", 1024), min=256, max=1440 ) .classes("w-full") - .bind_value(self, "height") + .bind_value(self, "height").tooltip("Height of the generated image. Optional, only used when aspect_ratio=custom. Must be a multiple of 16 (if it's not, it will be rounded to nearest multiple of 16)") ) self.num_outputs_input = ( @@ -203,7 +196,7 @@ def setup_left_panel(self): "Num Outputs", value=self.settings.get("num_outputs", 1), min=1, max=4 ) .classes("w-full") - .bind_value(self, "num_outputs") + .bind_value(self, "num_outputs").tooltip("Number of images to output.") ) self.lora_scale_input = ( ui.number( @@ -213,7 +206,7 @@ def setup_left_panel(self): max=2, step=0.1, ) - .classes("w-full") + .classes("w-full").tooltip("Determines how strongly the LoRA should be applied. Sane results between 0 and 1.") .bind_value(self, "lora_scale") ) self.num_inference_steps_input = ( @@ -223,7 +216,7 @@ def setup_left_panel(self): min=1, max=50, ) - .classes("w-full") + .classes("w-full").tooltip("Number of Inference Steps") .bind_value(self, "num_inference_steps") ) self.guidance_scale_input = ( @@ -234,7 +227,7 @@ def setup_left_panel(self): max=10, step=0.1, ) - .classes("w-full") + .classes("w-full").tooltip("Guidance Scale for the diffusion process") .bind_value(self, "guidance_scale") ) self.seed_input = ( @@ -253,7 +246,7 @@ def setup_left_panel(self): label="Output Format", value=self.settings.get("output_format", "webp"), ) - .classes("w-full") + .classes("w-full").tooltip("Format of the output images") .bind_value(self, "output_format") ) self.output_quality_input = ( @@ -263,15 +256,15 @@ def setup_left_panel(self): min=0, max=100, ) - .classes("w-full") + .classes("w-full").tooltip("Quality when saving the output images, from 0 to 100. 100 is best quality, 0 is lowest quality. Not relevant for .png outputs") .bind_value(self, "output_quality") ) self.disable_safety_checker_switch = ( ui.switch( "Disable Safety Checker", - value=self.settings.get("disable_safety_checker", False), + value=self.settings.get("disable_safety_checker", True), ) - .classes("w-full") + .classes("w-full").tooltip("Disable safety checker for generated images.") .bind_value(self, "disable_safety_checker") ) From c8362c32fe8c7a5d68b90d40c6bc96fc5655e4a5 Mon Sep 17 00:00:00 2001 From: Robin Tuszik <47579899+rtuszik@users.noreply.github.com> Date: Mon, 26 Aug 2024 18:31:26 +0200 Subject: [PATCH 5/7] style: reorganize imports and remove comments Reorder imports for better readability and remove unnecessary comments throughout the main.py file. This change improves code organization without altering functionality. --- src/main.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/main.py b/src/main.py index 3eff329..f92b29f 100644 --- a/src/main.py +++ b/src/main.py @@ -1,12 +1,12 @@ import sys -from gui import create_gui from loguru import logger from nicegui import ui + +from gui import create_gui from replicate_api import ImageGenerator -# Configure Loguru -logger.remove() # Remove the default handler +logger.remove() logger.add( sys.stderr, format="{time} {level} {message}", filter="my_module", level="INFO" ) @@ -14,11 +14,11 @@ "main.log", rotation="10 MB", format="{time} {level} {message}", level="INFO" ) -# Create the ImageGenerator instance + logger.info("Initializing ImageGenerator") generator = ImageGenerator() -# Create and setup the GUI + logger.info("Creating and setting up GUI") @@ -28,7 +28,6 @@ async def main_page(): logger.info("NiceGUI server is running") -# Run the NiceGUI server logger.info("Starting NiceGUI server") -# ui.run(port=8080) + ui.run(title="Replicate Flux LoRA", port=8080) From 1a3747056d70d89fb77038671b961801976b21c8 Mon Sep 17 00:00:00 2001 From: Robin Tuszik <47579899+rtuszik@users.noreply.github.com> Date: Mon, 26 Aug 2024 18:49:31 +0200 Subject: [PATCH 6/7] refactor(gui): improve UI layout and tooltips - Adjust column and row classes for better spacing - Enhance tooltips with more detailed information - Improve code formatting and readability - Add clearable property to prompt textarea --- src/gui.py | 75 +++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 52 insertions(+), 23 deletions(-) diff --git a/src/gui.py b/src/gui.py index b0b7b24..27bc5f1 100644 --- a/src/gui.py +++ b/src/gui.py @@ -87,14 +87,15 @@ def __getattr__(self, name): def setup_ui(self): ui.dark_mode().enable() - with ui.column().classes("w-full max-w-full mx-auto p-4 space-y-4"): + with ui.column().classes("w-full max-w-full mx-auto space-y-8"): with ui.card().classes("w-full"): - ui.label("Image Generator").classes("text-2xl font-bold mb-4") - with ui.row().classes("w-full"): - with ui.column().classes("w-1/2 pr-2"): + ui.label("Flux LoRA API").classes("text-2xl font-bold mb-4") + with ui.row(): + with ui.column(wrap=False): self.setup_left_panel() - with ui.column().classes("w-1/2 pl-2"): + with ui.column(wrap=False): self.setup_right_panel() + ui.separator() self.setup_bottom_panel() logger.info("UI setup completed") @@ -106,12 +107,16 @@ def setup_left_panel(self): ) self.replicate_model_input.on("change", self.update_replicate_model) - self.flux_models_select = ui.select( - options=self.flux_fine_tune_models, - label="Flux Fine-Tune Models", - value=None, - on_change=self.select_flux_model, - ).classes("w-full").tooltip("Select Model") + self.flux_models_select = ( + ui.select( + options=self.flux_fine_tune_models, + label="Flux Fine-Tune Models", + value=None, + on_change=self.select_flux_model, + ) + .classes("w-full") + .tooltip("Select Model") + ) with ui.row().classes("w-full"): self.new_model_input = ui.input(label="New Model").classes("w-3/4") ui.button("Add Model", on_click=self.add_user_model).classes("w-1/4") @@ -145,9 +150,12 @@ def setup_left_panel(self): label="Flux Model", value=self.settings.get("flux_model", "dev"), ) - .classes("w-full").tooltip("Which model to run inferences with. the dev model needs around 28 steps but the schnell model only needs around 4 steps.") - .bind_value(self, "flux_model") + .classes("w-full") + .tooltip( + "Which model to run inferences with. the dev model needs around 28 steps but the schnell model only needs around 4 steps." ) + .bind_value(self, "flux_model") + ) self.aspect_ratio_select = ( ui.select( @@ -169,8 +177,11 @@ def setup_left_panel(self): value=self.settings.get("aspect_ratio", "1:1"), ) .classes("w-full") - .bind_value(self, "aspect_ratio").tooltip("Width of the generated image. Optional, only used when aspect_ratio=custom. Must be a multiple of 16 (if it's not, it will be rounded to nearest multiple of 16)") + .bind_value(self, "aspect_ratio") + .tooltip( + "Width of the generated image. Optional, only used when aspect_ratio=custom. Must be a multiple of 16 (if it's not, it will be rounded to nearest multiple of 16)" ) + ) self.aspect_ratio_select.on("change", self.toggle_custom_dimensions) with ui.column().classes("w-full").bind_visibility_from( @@ -181,14 +192,20 @@ def setup_left_panel(self): "Width", value=self.settings.get("width", 1024), min=256, max=1440 ) .classes("w-full") - .bind_value(self, "width").tooltip("Width of the generated image. Optional, only used when aspect_ratio=custom. Must be a multiple of 16 (if it's not, it will be rounded to nearest multiple of 16)") + .bind_value(self, "width") + .tooltip( + "Width of the generated image. Optional, only used when aspect_ratio=custom. Must be a multiple of 16 (if it's not, it will be rounded to nearest multiple of 16)" + ) ) self.height_input = ( ui.number( "Height", value=self.settings.get("height", 1024), min=256, max=1440 ) .classes("w-full") - .bind_value(self, "height").tooltip("Height of the generated image. Optional, only used when aspect_ratio=custom. Must be a multiple of 16 (if it's not, it will be rounded to nearest multiple of 16)") + .bind_value(self, "height") + .tooltip( + "Height of the generated image. Optional, only used when aspect_ratio=custom. Must be a multiple of 16 (if it's not, it will be rounded to nearest multiple of 16)" + ) ) self.num_outputs_input = ( @@ -196,7 +213,8 @@ def setup_left_panel(self): "Num Outputs", value=self.settings.get("num_outputs", 1), min=1, max=4 ) .classes("w-full") - .bind_value(self, "num_outputs").tooltip("Number of images to output.") + .bind_value(self, "num_outputs") + .tooltip("Number of images to output.") ) self.lora_scale_input = ( ui.number( @@ -206,7 +224,10 @@ def setup_left_panel(self): max=2, step=0.1, ) - .classes("w-full").tooltip("Determines how strongly the LoRA should be applied. Sane results between 0 and 1.") + .classes("w-full") + .tooltip( + "Determines how strongly the LoRA should be applied. Sane results between 0 and 1." + ) .bind_value(self, "lora_scale") ) self.num_inference_steps_input = ( @@ -216,7 +237,8 @@ def setup_left_panel(self): min=1, max=50, ) - .classes("w-full").tooltip("Number of Inference Steps") + .classes("w-full") + .tooltip("Number of Inference Steps") .bind_value(self, "num_inference_steps") ) self.guidance_scale_input = ( @@ -227,7 +249,8 @@ def setup_left_panel(self): max=10, step=0.1, ) - .classes("w-full").tooltip("Guidance Scale for the diffusion process") + .classes("w-full") + .tooltip("Guidance Scale for the diffusion process") .bind_value(self, "guidance_scale") ) self.seed_input = ( @@ -246,7 +269,8 @@ def setup_left_panel(self): label="Output Format", value=self.settings.get("output_format", "webp"), ) - .classes("w-full").tooltip("Format of the output images") + .classes("w-full") + .tooltip("Format of the output images") .bind_value(self, "output_format") ) self.output_quality_input = ( @@ -256,7 +280,10 @@ def setup_left_panel(self): min=0, max=100, ) - .classes("w-full").tooltip("Quality when saving the output images, from 0 to 100. 100 is best quality, 0 is lowest quality. Not relevant for .png outputs") + .classes("w-full") + .tooltip( + "Quality when saving the output images, from 0 to 100. 100 is best quality, 0 is lowest quality. Not relevant for .png outputs" + ) .bind_value(self, "output_quality") ) self.disable_safety_checker_switch = ( @@ -264,7 +291,8 @@ def setup_left_panel(self): "Disable Safety Checker", value=self.settings.get("disable_safety_checker", True), ) - .classes("w-full").tooltip("Disable safety checker for generated images.") + .classes("w-full") + .tooltip("Disable safety checker for generated images.") .bind_value(self, "disable_safety_checker") ) @@ -321,6 +349,7 @@ def setup_bottom_panel(self): ui.textarea("Prompt", value=self.settings.get("prompt", "")) .classes("w-full") .bind_value(self, "prompt") + .props("clearable") ) self.generate_button = ui.button( "Generate Images", on_click=self.start_generation From d77aab63c61463bfc4b90496eeb587aa5eb9d066 Mon Sep 17 00:00:00 2001 From: Robin Tuszik <47579899+rtuszik@users.noreply.github.com> Date: Mon, 26 Aug 2024 18:51:23 +0200 Subject: [PATCH 7/7] ci: simplify Docker image metadata configuration Remove semver pattern and labels from the metadata action to streamline the Docker image build process in the GitHub Actions workflow. --- .github/workflows/docker-build-push.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml index 0a71eae..050fff6 100644 --- a/.github/workflows/docker-build-push.yml +++ b/.github/workflows/docker-build-push.yml @@ -39,11 +39,7 @@ jobs: type=raw,value=latest type=sha,prefix={{branch}}- type=ref,event=branch - type=semver,pattern={{version}} - labels: | - org.opencontainers.image.source=${{ github.repositoryUrl }} - org.opencontainers.image.revision=${{ github.sha }} - org.opencontainers.image.created=${{ steps.meta.outputs.created }} + - name: Build and push Docker image uses: docker/build-push-action@v4