import os, sys, json, re, time, base64, random, shutil import gradio as gr import numpy as np import requests from requests import Session from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry # More direct import from datetime import datetime import urllib.parse from pathlib import Path from groq import Groq from exif import Image from PIL import Image as PILImage, ExifTags as PILExifTags, PngImagePlugin from io import BytesIO import colorsys import spaces if not "SPACE" in os.environ: from dotenv import load_dotenv load_dotenv() # MARK: INIT MAX_SEED = np.iinfo(np.int32).max MAX_IMAGE_SIZE = 2048 GROQ_APIKEY_PROMPTENHANCE = os.getenv("GROQ_APIKEY_PROMPTENHANCE") API1 = urllib.parse.unquote(os.getenv("API1")) BASE_DIR = Path(__file__).resolve().parent RES = BASE_DIR / "_res" ASSETS = RES / "assets" IMAGE_DIR = BASE_DIR / "cache" / "images" IMAGE_DIR.mkdir(parents=True, exist_ok=True) gr.set_static_paths(paths=[str(RES), str("cache/images"), str(ASSETS)]) enhance_systemmessage_json = RES / "groq_systemmessage_prompt_enhance_new.json" with open(enhance_systemmessage_json, "r") as f: enhance_systemmessage = json.load(f) custom_css_path = RES / "_custom.css" custom_js_path = RES / "_custom.js" with open(custom_css_path, "r") as f: custom_css = f.read() with open(custom_js_path, "r") as f: custom_js = f.read() custom_head = f""" """ theme = gr.themes.Soft( radius_size="sm", neutral_hue=gr.themes.Color( c100="#a6adc8", c200="#9399b2", c300="#7f849c", c400="#6c7086", c50="#cdd6f4", c500="#585b70", c600="#45475a", c700="#313244", c800="#1e1e2e", c900="#181825", c950="#11111b", ), ) title = "Bilder Builder" # MARK: READ EXIF def read_exif(image_path): try: with open(image_path, "rb") as src: img = Image(src) # This is where TiffByteOrder error can happen from exif library if not hasattr(img, "user_comment") or not img.user_comment: print(f"Warning: No user_comment found in EXIF for {image_path}") return {} # Return empty dict if no user_comment try: img_comment = json.loads(img.user_comment) except json.JSONDecodeError: print(f"Warning: Could not decode user_comment JSON from EXIF for {image_path}") return {} # Return empty dict if JSON is invalid if "concept" in img_comment: # checking if the key exists before removing img_comment.pop("concept") return img_comment except ValueError as ve: # Catch specific errors like TiffByteOrder from the exif library print(f"EXIF parsing ValueError for {image_path}: {ve}") return {} # Return empty dict on this error except Exception as e: # Catch other potential errors during EXIF processing print(f"Unexpected error reading EXIF for {image_path}: {e}") return {} def read_png_metadata(image_path): try: img_pil = PILImage.open(image_path) if "user_comment" in img_pil.info: metadata_json_string = img_pil.info["user_comment"] try: img_comment = json.loads(metadata_json_string) if "concept" in img_comment: img_comment.pop("concept") return img_comment except json.JSONDecodeError: print(f"Warning: Could not decode user_comment JSON from PNG info for {image_path}") return {} else: print(f"Warning: No 'user_comment' found in PNG info for {image_path}") return {} except Exception as e: print(f"Error reading PNG metadata for {image_path}: {e}") return {} # MARK: GROQ PROMPT ENHANCE def groq_enhance_process(Prompt=""): client = Groq(api_key=GROQ_APIKEY_PROMPTENHANCE) Prompt = "random prompt" if Prompt == "" else Prompt completion = client.chat.completions.create( model="meta-llama/llama-4-scout-17b-16e-instruct", messages=[enhance_systemmessage, {"role": "user", "content": Prompt}], temperature=1, max_tokens=512, top_p=0.9, stream=False, seed=random.randint(0, MAX_SEED), stop=None, ) if completion.choices[0].message.content != "": enhanced_prompt = completion.choices[0].message.content enhanced_prompt = re.sub(r"[\.\"]+", "", enhanced_prompt) return enhanced_prompt def image_get_size(image_path): img = PILImage.open(image_path) width, height = img.size return width, height # MARK: DOMINANT COLOR def image_get_dominant_color(image_path): img = PILImage.open(image_path) img = img.convert("RGB") img = img.resize((100, 100), resample=PILImage.Resampling.NEAREST) # Use Resampling enum pixels = list(img.getdata()) colors = [] for pixel in pixels: r, g, b = pixel h, s, v = colorsys.rgb_to_hsv(r / 255, g / 255, b / 255) if v > 0.5: continue if v > 0.99: continue colors.append((h, s, v)) if not colors: # Handle case where all pixels are filtered out return "rgb(0,0,0)" # Default to black or handle as error dominant_color = max(colors, key=lambda x: x[2]) dominant_color_rgb = colorsys.hsv_to_rgb(dominant_color[0], dominant_color[1], dominant_color[2]) dominant_color_rgb = [int(c * 255) for c in dominant_color_rgb] dominant_color_rgb_str = f"rgb({dominant_color_rgb[0]}, {dominant_color_rgb[1]}, {dominant_color_rgb[2]})" return dominant_color_rgb_str # MARK: CLEAR COMPONENTS def clear_components(): return gr.update(value=None, visible=False), gr.update(visible=False) def process(Prompt, used_model, image_width, image_height, image_seed, p_randomize_seed, current_gallery_items): if Prompt == "": gr.Info("Kein Prompt angegeben, es wird ein zufälliger Prompt generiert.", duration=12) Prompt = groq_enhance_process("random prompt") used_seed = random.randint(0, MAX_SEED) if image_seed == 0 or p_randomize_seed else image_seed used_model = "turbo" if "schnell" in used_model.lower() else "flux" timestamp = datetime.now().strftime("%Y-%m-%d-%H-%M-%S") filename_prompt = re.sub(r"[^\w\s-]", "", Prompt).strip().replace(" ", "_") filename = timestamp + "_" + filename_prompt[:100] + ".png" absolute_file_path = IMAGE_DIR / filename relative_image_path = f"cache/images/{filename}" session = Session() retries = Retry(total=3, status_forcelist=[429, 500, 502, 503, 504], backoff_factor=0.3, respect_retry_after_header=True) adapter = HTTPAdapter(max_retries=retries) session.mount("https://", adapter) session.mount("http://", adapter) REQUEST_URL = ( f"{API1}{urllib.parse.quote(Prompt)}?model={used_model}&width={image_width}&height={image_height}&nologo=true&enhance=false&nofeed=true&safe=false&seed={used_seed}" ) try: response = session.get(REQUEST_URL, timeout=60) if response.status_code == 200: print("REQUEST URL:\n" + REQUEST_URL + "\n\nImagine API Request solved") img_pil = PILImage.open(BytesIO(response.content)) img_pil.save(absolute_file_path, "png") print("Save image to: ", absolute_file_path) img_dominant_color = image_get_dominant_color(absolute_file_path) img_width_actual, img_height_actual = image_get_size(absolute_file_path) # Use actual size image_metadata = { "prompt": Prompt, "seed": used_seed, "model": used_model, "dominant_color": img_dominant_color, "width": img_width_actual, "height": img_height_actual, "timestamp": timestamp, } metadata_json_string = json.dumps(image_metadata) png_info_to_add = PngImagePlugin.PngInfo() png_info_to_add.add_text("user_comment", metadata_json_string) try: # Re-save the image, this time embedding the PngInfo metadata img_pil.save(absolute_file_path, "png", pnginfo=png_info_to_add) print(f"Image re-saved with PNG metadata: {absolute_file_path}") except Exception as e: # This error would be if saving with PngInfo fails for some reason print(f"Warning: Could not save PNG with embedded metadata for {absolute_file_path}. Error: {e}") # The file 'absolute_file_path' would still exist from the temporary save, but without our metadata. new_gallery_item_state_data = (str(absolute_file_path), Prompt, used_seed) updated_gallery_state_list = [item for item in current_gallery_items] if current_gallery_items else [] updated_gallery_state_list.append(new_gallery_item_state_data) all_image_filepaths_for_gallery_files = [item[0] for item in updated_gallery_state_list][::-1] gallery_display_list = [(item[0], item[1]) for item in updated_gallery_state_list][::-1] return ( updated_gallery_state_list, # 1 generated_image all_image_filepaths_for_gallery_files, # 2 gallery_files gr.update(value=gallery_display_list, selected_index=0), # 3 gallery gr.update(value=Prompt), # 4 Text Prompt gr.update(value=str(absolute_file_path), visible=False), # 5 output_image gr.update(value=relative_image_path, visible=True), gr.update(value=Prompt, visible=True), img_width_actual, # Use actual width img_height_actual, # Use actual height used_seed, gr.update(value=str(absolute_file_path), visible=True), img_dominant_color, used_seed, gr.update(visible=True), ) else: print(f"Imagine API Request ERROR, Status: {response.status_code}, Response: {response.text}") raise gr.Error(f"Imagine API-Aufruf fehlgeschlagen (Code: {response.status_code}) 💥!", duration=15) except requests.exceptions.Timeout: raise gr.Error("⏰ Zeitüberschreitung beim API-Aufruf", duration=15) except requests.exceptions.RequestException as e: print(f"Unbekannter Fehler beim API-Aufruf: {e}") raise gr.Error(f"Unbekannter Fehler beim API-Aufruf! 🤷‍♂️ {e}", duration=15) def check_api(url): try: response = requests.get(url, timeout=8) print(f"Checking Status for: {url}, it returns statuscode {response.status_code}") return response.status_code except requests.exceptions.RequestException as e: print(f"An error occurred: {e}") return 999 def get_inference_models(): status_api_1 = check_api(API1) info_api_1 = "🟢API1👍" if status_api_1 == 200 else "🔴API1👎🔌" api_models_1 = ["FLUX Dev", "Flux Schnell (Low-Res)"] models = [model for model in api_models_1 if status_api_1 == 200] info_api_status = f"Status: {info_api_1}" default_model = models[0] if models else None return gr.update(choices=models, value=default_model, interactive=bool(models), info=info_api_status) # MARK: Gradio BLOCKS UI css=custom_css with gr.Blocks(theme=theme, head=custom_head, js=custom_js, css=custom_css, title=title) as demo: # ... (Your existing UI header markdown) ... with gr.Row(elem_classes="row-header"): gr.Markdown( f"""

{title}

Aktuell funktioniert die APP nicht, da ich heute wenig Zeit habe werde ich es erst morgen schaffen die Probleme zu beheben.

LG Sebastian gib dem Space gerne ein

""", elem_classes="md-header", ) with gr.Row(elem_classes="row-main"): with gr.Column(scale=2): generated_images = gr.State([]) # generated_images_count = gr.State(0) # This state was unused with gr.Row(): placeholder_text = "[???] Generiert dir einen zufälligen Prompt.\n[STERN] optimiert deinen eignen Prompt.\n[RUN] generiert dein Bild." text_prompt = gr.Textbox( label="Prompt", show_label=False, lines=12, max_lines=18, placeholder=placeholder_text, elem_id="prompt_input", elem_classes="prompt-input hide-progress", autofocus=True, ) with gr.Row(): random_prompt_button = gr.Button("", variant="secondary", elem_id="random_prompt_btn", elem_classes="random-prompt-btn", icon=str(ASSETS / "random.png")) enhance_prompt_button = gr.Button("", variant="secondary", elem_id="enhance_prompt_btn", elem_classes="enhance-prompt-btn", icon=str(ASSETS / "star_light_48.png")) run_button = gr.Button("Erstellen", variant="primary", elem_id="run_btn", elem_classes="run-btn", interactive=False) with gr.Row(elem_classes="image_size_selctor_wrapper"): with gr.Column(scale=1): with gr.Row(): with gr.Column(): select_model = gr.Dropdown(label="Model", elem_id="select_model", elem_classes="select-model") image_width = gr.Number( label="Breite", minimum=256, maximum=MAX_IMAGE_SIZE, value=576, step=32, elem_id="image_width_selector", elem_classes="image-width-selector", scale=1, visible=False, ) image_height = gr.Number( label="Höhe", minimum=256, maximum=MAX_IMAGE_SIZE, value=1024, step=32, elem_id="image_height_selector", elem_classes="image-height-selector", scale=1, visible=False, ) with gr.Row(): image_ratio_buttons = gr.Radio( ["9:16", "3:4", "2:3", "1:1"], value="9:16", label="Hochformat", show_label=True, info="Seitenverhältniss drehen", interactive=True, elem_id="image_ratio_buttons", elem_classes="image-ratio-buttons", container=True, scale=2, ) switch_width_height = gr.Button("", size="sm", elem_id="switch_width_height", elem_classes="switch-ratio-btn", variant="primary", scale=1) with gr.Column(): randomize_seed = gr.Checkbox(label="Randomize seed", value=True, elem_classes="random-seed-cb toggle-btn") image_seed = gr.Slider( label="Seed", info="Jeder Seed generiert ein anderes Bild mit dem selben Prompt", minimum=0, step=1, value=42, maximum=MAX_SEED, elem_id="image_seed", elem_classes="image-seed hide-progress", interactive=False, ) with gr.Column(scale=4): with gr.Row(): with gr.Column(scale=3): # Changed scale for better layout with info panel with gr.Row(): output_image = gr.Image( show_label=False, min_width=320, elem_id="output_image", elem_classes="output-image", type="filepath", format="webp", visible=False ) # format="png" might be better if webp causes issues with EXIF gallery = gr.Gallery( label="Bisher erstellte Bilder", show_label=False, format="png", elem_id="gallery", columns=[4], rows=[4], object_fit="contain", height="auto", type="filepath", preview=True, allow_preview=True, ) with gr.Row(): gallery_files = gr.Files(visible=False, elem_id="gallery_files", elem_classes="gallery-files", interactive=False, label="Generierte Bilddateien") with gr.Column(scale=1, visible=False, elem_classes="image-info-wrapper") as image_info_wrapper: with gr.Accordion("Bild Informationen", open=False): # with gr.Group(): # gr.Markdown(f"""## Bildinformationen""") # Removed visible=False as parent controls it image_info_tb_prompt = gr.Textbox(label="Bild Prompt", lines=18, interactive=False, elem_classes="hide-progress", show_copy_button=True, visible=True) with gr.Row(elem_classes="img-size-wrapper"): image_info_tb_width = gr.Textbox(label="Breite", lines=1, max_lines=1, interactive=False, show_copy_button=True, elem_classes="image-info-tb-width") image_info_tb_height = gr.Textbox(label="Höhe", lines=1, max_lines=1, interactive=False, show_copy_button=True, elem_classes="image-info-tb-height") with gr.Row(elem_classes="img-seed-wrapper"): image_info_tb_seed = gr.Textbox(label="Seed", lines=1, max_lines=1, interactive=False, show_copy_button=True, elem_classes="image-info-tb-seed") # Made visible as parent controls wrapper image_download_button = gr.DownloadButton("Bild herunterladen", value=None, elem_classes="download-button", variant="primary", visible=True) # Made visible output_url = gr.Textbox(label="Output URL", show_label=True, interactive=False, visible=False) # outpu_image_comment = gr.Json(visible=False) # This was unused output_dominant_image_color = gr.Textbox(show_label=False, elem_id="dominant_image_color", visible=True, elem_classes="output-dominant-image-color") # MARK: Functionen after demo.load( lambda: (gr.update(interactive=False), gr.update(interactive=False), gr.update(interactive=False)), outputs=[run_button, enhance_prompt_button, random_prompt_button], ).then(get_inference_models, outputs=[select_model]).then( lambda: (gr.update(interactive=True), gr.update(interactive=True), gr.update(interactive=True)), outputs=[run_button, enhance_prompt_button, random_prompt_button], ) def gallery_select_handler(evt: gr.SelectData, gallery_files): # For debugging, to understand what evt.value is: # print(f"DEBUG: gallery_select_handler called. evt: {evt}") # print(f"DEBUG: evt.value: {evt.value}, type(evt.value): {type(evt.value)}") # print(f"DEBUG: evt.index: {evt.index}, evt.selected: {evt.selected}") selected_absolute_path = None # Safely try to get the filepath from evt.value # evt.value is expected to be a tuple: (filepath, caption) if isinstance(gallery_files[evt.index], str) and gallery_files[evt.index]: selected_absolute_path = gallery_files[evt.index] # If no valid path was extracted, or if evt.value was not as expected (e.g. None, empty dict, etc.) if not selected_absolute_path: gr.Info("Kein Bild aus der Galerie ausgewählt, Pfad ist leer oder ungültiger Wert.") return ( gr.update(value=None, visible=False), # 1. output_image gr.update(value="", visible=False), # 2. output_url gr.update(value="", visible=True), # 3. image_info_tb_prompt (clear value) "", # 4. image_info_tb_width "", # 5. image_info_tb_height "", # 6. image_info_tb_seed gr.update(value=None, visible=False), # 7. image_download_button gr.update(visible=False), # 8. image_info_wrapper (hide the whole section) ) # print(f"Gallery selected image path: {selected_absolute_path}") # Keep for your debugging if not os.path.exists(selected_absolute_path): gr.Warning(f"Ausgewählte Bilddatei nicht gefunden: {selected_absolute_path}") return ( gr.update(value=None, visible=False), gr.update(value="", visible=False), gr.update(value="Datei nicht gefunden", visible=True), "N/A", "N/A", "N/A", gr.update(value=None, visible=False), gr.update(visible=True), # Show wrapper with error ) relative_path_for_url_tb = "cache/images/" + Path(selected_absolute_path).name try: exif_data = read_png_metadata(selected_absolute_path) selected_prompt = exif_data.get("prompt", "N/A (EXIF Fehler)") img_width = exif_data.get("width", "N/A") img_height = exif_data.get("height", "N/A") selected_seed = exif_data.get("seed", "N/A") return ( gr.update(value=selected_absolute_path, visible=False), gr.update(value=relative_path_for_url_tb, visible=False), # output_url can be visible gr.update(value=selected_prompt, visible=True), str(img_width), str(img_height), str(selected_seed), gr.update(value=selected_absolute_path, visible=False), gr.update(visible=True), ) except Exception as e: # Catch any other unexpected error during this block print(f"Error in gallery_select_handler after path validation: {e}") gr.Warning(f"Fehler beim Verarbeiten der Bilddetails: {e}") # Fallback if try block fails return ( gr.update(value=selected_absolute_path, visible=False), # Still show image if path is valid gr.update(value=relative_path_for_url_tb, visible=True), gr.update(value="Fehler beim Lesen der Details", visible=True), "Fehler", "Fehler", "Fehler", gr.update(value=selected_absolute_path, visible=True), # Download button for the image gr.update(visible=True), # Show info wrapper ) gallery.select( fn=gallery_select_handler, inputs=[gallery_files], # evt is passed automatically by gr.SelectData # outputs=[debug_text], outputs=[ output_image, output_url, image_info_tb_prompt, image_info_tb_width, image_info_tb_height, image_info_tb_seed, image_download_button, output_dominant_image_color, # Add if you want to update this too ], show_progress="hidden", ) # gallery_files.change(fn=lambda state_data: [item[0] for item in state_data], inputs=[gallery_files], outputs=[gallery_files], show_progress="hidden", api_name=False) def switch_image_size_values(w, h): return h, w def switch_image_ratio_buttons(ratio_val): parts = ratio_val.split(":") new_ratio = f"{parts[1]}:{parts[0]}" is_landscape = int(parts[1]) > int(parts[0]) is_portrait = int(parts[1]) < int(parts[0]) choices = ["16:9", "4:3", "3:2", "1:1"] if is_landscape else ["9:16", "3:4", "2:3", "1:1"] label = "Querformat" if is_landscape else "Hochformat" if is_portrait else "Quadratisch" return gr.update(choices=choices, value=new_ratio, label=label) def calculate_ratio_values(ratio_str): w_s, h_s = map(int, ratio_str.split(":")) base_dim = 1024 if w_s > h_s: # Landscape or square new_w = base_dim new_h = round(base_dim * h_s / w_s / 8) * 8 else: # Portrait new_h = base_dim new_w = round(base_dim * w_s / h_s / 8) * 8 label = "Querformat" if w_s > h_s else "Hochformat" if w_s < h_s else "Quadratisch" return gr.update(label=label), new_w, new_h switch_width_height.click(fn=switch_image_size_values, inputs=[image_width, image_height], outputs=[image_width, image_height], show_progress="hidden", api_name=False) switch_width_height.click(fn=switch_image_ratio_buttons, inputs=[image_ratio_buttons], outputs=[image_ratio_buttons], show_progress="hidden", api_name=False) image_ratio_buttons.input( fn=calculate_ratio_values, inputs=[image_ratio_buttons], outputs=[image_ratio_buttons, image_width, image_height], show_progress="hidden", api_name=False ) run_button.click( fn=lambda: (gr.update(interactive=False), gr.update(interactive=False), gr.update(interactive=False)), outputs=[run_button, enhance_prompt_button, random_prompt_button], api_name=False, js="() => { const player = document.querySelector('dotlottie-player'); if (player) player.play(); }", ).then( fn=process, inputs=[text_prompt, select_model, image_width, image_height, image_seed, randomize_seed, generated_images], outputs=[ generated_images, # 1 generated_image gallery_files, # 2 gallery_files gallery, # 3 gallery text_prompt, # 4 Text Prompt output_image, # 5 output_image output_url, image_info_tb_prompt, image_info_tb_width, image_info_tb_height, image_info_tb_seed, image_download_button, output_dominant_image_color, image_seed, image_info_wrapper, ], show_progress="hidden", ).then( fn=lambda: (gr.update(interactive=True), gr.update(interactive=True), gr.update(interactive=True)), outputs=[run_button, enhance_prompt_button, random_prompt_button], api_name=False, js="() => { const player = document.querySelector('dotlottie-player'); if (player) player.stop(); }", ) generated_images.change(fn=lambda state_data: [item[0] for item in state_data] if state_data else [], inputs=[generated_images], outputs=[gallery_files], api_name=False) randomize_seed.input(lambda x: gr.update(interactive=not x), inputs=[randomize_seed], outputs=[image_seed], api_name=False) enhance_prompt_button.click(fn=groq_enhance_process, inputs=[text_prompt], outputs=[text_prompt], api_name=False) random_prompt_button.click(fn=groq_enhance_process, inputs=None, outputs=[text_prompt], api_name=False) # MARK: Gradio LAUNCH demo.launch(show_api=False, debug=True)