""" QR Code Monster — ControlNet Space Modell: monster-labs/control_v1p_sd15_qrcode_monster (v2) """ import os import random import gradio as gr import qrcode import torch from PIL import Image from qrcode.constants import ( ERROR_CORRECT_L, ERROR_CORRECT_M, ERROR_CORRECT_Q, ERROR_CORRECT_H, ) from diffusers import ( ControlNetModel, StableDiffusionControlNetImg2ImgPipeline, DPMSolverMultistepScheduler, EulerAncestralDiscreteScheduler, ) # -------------------------------------------------------------------------- # ZeroGPU-Support (funktioniert auch lokal ohne das "spaces"-Paket) # -------------------------------------------------------------------------- try: import spaces gpu_decorator = spaces.GPU(duration=90) except Exception: # lokal / eigene GPU def gpu_decorator(fn): return fn # -------------------------------------------------------------------------- # Konfiguration # -------------------------------------------------------------------------- BASE_MODEL = os.environ.get("BASE_MODEL", "stable-diffusion-v1-5/stable-diffusion-v1-5") CONTROLNET_REPO = "monster-labs/control_v1p_sd15_qrcode_monster" CONTROLNET_SUBFOLDER = "v2" # v2 ist deutlich besser als v1 DEVICE = "cuda" if torch.cuda.is_available() else "cpu" DTYPE = torch.float16 if DEVICE == "cuda" else torch.float32 GRAY = (128, 128, 128) # #808080 – laut Model Card ideal zum "Verschmelzen" MAX_SEED = 2**31 - 1 ERROR_LEVELS = { "L (7 %)": ERROR_CORRECT_L, "M (15 %)": ERROR_CORRECT_M, "Q (25 %)": ERROR_CORRECT_Q, "H (30 %) – empfohlen": ERROR_CORRECT_H, } SCHEDULERS = { "DPM++ 2M Karras": lambda cfg: DPMSolverMultistepScheduler.from_config( cfg, use_karras_sigmas=True, algorithm_type="dpmsolver++" ), "Euler a": lambda cfg: EulerAncestralDiscreteScheduler.from_config(cfg), } # -------------------------------------------------------------------------- # Pipeline laden (einmalig beim Start) # -------------------------------------------------------------------------- controlnet = ControlNetModel.from_pretrained( CONTROLNET_REPO, subfolder=CONTROLNET_SUBFOLDER, torch_dtype=DTYPE, ) pipe = StableDiffusionControlNetImg2ImgPipeline.from_pretrained( BASE_MODEL, controlnet=controlnet, torch_dtype=DTYPE, safety_checker=None, requires_safety_checker=False, ) pipe.scheduler = DPMSolverMultistepScheduler.from_config( pipe.scheduler.config, use_karras_sigmas=True, algorithm_type="dpmsolver++" ) pipe.to(DEVICE) if DEVICE == "cuda": pipe.enable_vae_tiling() pipe.enable_attention_slicing() # -------------------------------------------------------------------------- # Hilfsfunktionen # -------------------------------------------------------------------------- def make_qr_image(content: str, size: int, error_level: int, quiet_zone: int = 4) -> Image.Image: """Erzeugt ein QR-Bild mit Modulgröße ~16 px auf grauem Hintergrund.""" qr = qrcode.QRCode( version=None, error_correction=error_level, box_size=16, # Model Card: module size 16px border=quiet_zone, ) qr.add_data(content) qr.make(fit=True) img = qr.make_image(fill_color="black", back_color="white").convert("RGB") # QR mittig auf graue Leinwand legen, ohne Kanten zu verwaschen (NEAREST!) inner = int(size * 0.9) img = img.resize((inner, inner), Image.NEAREST) canvas = Image.new("RGB", (size, size), GRAY) offset = (size - inner) // 2 canvas.paste(img, (offset, offset)) return canvas def prepare_init_image(image: Image.Image | None, size: int) -> Image.Image: if image is None: return Image.new("RGB", (size, size), GRAY) return image.convert("RGB").resize((size, size), Image.LANCZOS) # -------------------------------------------------------------------------- # Generierung # -------------------------------------------------------------------------- @gpu_decorator def generate( qr_content, prompt, negative_prompt, controlnet_scale, guidance_scale, steps, strength, seed, randomize_seed, size, error_level_name, scheduler_name, init_image, num_images, progress=gr.Progress(track_tqdm=True), ): if not qr_content or not qr_content.strip(): raise gr.Error("Bitte Text oder URL für den QR-Code eingeben.") if not prompt or not prompt.strip(): raise gr.Error("Bitte einen Prompt eingeben.") if randomize_seed: seed = random.randint(0, MAX_SEED) seed = int(seed) pipe.scheduler = SCHEDULERS[scheduler_name](pipe.scheduler.config) size = int(size) control_image = make_qr_image(qr_content, size, ERROR_LEVELS[error_level_name]) init = prepare_init_image(init_image, size) generator = torch.Generator(device=DEVICE).manual_seed(seed) result = pipe( prompt=prompt, negative_prompt=negative_prompt or None, image=init, control_image=control_image, width=size, height=size, num_inference_steps=int(steps), guidance_scale=float(guidance_scale), controlnet_conditioning_scale=float(controlnet_scale), strength=float(strength), num_images_per_prompt=int(num_images), generator=generator, ) return result.images, control_image, seed # -------------------------------------------------------------------------- # UI # -------------------------------------------------------------------------- DEFAULT_NEGATIVE = ( "ugly, disfigured, low quality, blurry, jpeg artifacts, watermark, text, " "worst quality, lowres, deformed" ) EXAMPLES = [ ["https://qrcode.monster", "a japanese zen garden with raked sand, moss, soft morning light, 8k photo"], ["https://huggingface.co", "an ancient stone mosaic floor in a roman villa, intricate, weathered"], ["https://example.com", "aerial view of a snowy forest, winding paths, cinematic, highly detailed"], ] # Gradio 6: theme/css/title gehören in launch(), nicht mehr in gr.Blocks() with gr.Blocks() as demo: gr.Markdown( """ # 🧟 QR Code Monster Künstlerische, **scanbare** QR-Codes mit [`control_v1p_sd15_qrcode_monster`](https://huggingface.co/monster-labs/control_v1p_sd15_qrcode_monster) (v2). **Tipp:** Nicht jeder Code scannt beim ersten Versuch. Mehrere Seeds generieren, oder ControlNet-Stärke hoch + Denoising runter drehen. """ ) with gr.Row(): with gr.Column(scale=1): qr_content = gr.Textbox( label="QR-Inhalt (URL oder Text)", value="https://qrcode.monster", placeholder="https://…", ) prompt = gr.Textbox( label="Prompt", lines=3, placeholder="z. B. a lush jungle with ancient ruins, cinematic lighting", ) negative_prompt = gr.Textbox( label="Negativer Prompt", value=DEFAULT_NEGATIVE, lines=2 ) with gr.Row(): controlnet_scale = gr.Slider( 0.5, 2.5, value=1.4, step=0.05, label="ControlNet-Stärke (hoch = besser scanbar)", ) strength = gr.Slider( 0.5, 1.0, value=0.9, step=0.01, label="Denoising-Stärke", ) with gr.Accordion("Erweiterte Einstellungen", open=False): with gr.Row(): guidance_scale = gr.Slider(1, 20, value=7.5, step=0.5, label="CFG Guidance") steps = gr.Slider(10, 60, value=30, step=1, label="Steps") with gr.Row(): seed = gr.Number(value=0, label="Seed", precision=0) randomize_seed = gr.Checkbox(value=True, label="Zufälliger Seed") with gr.Row(): size = gr.Radio([512, 640, 768], value=768, label="Auflösung") num_images = gr.Slider(1, 4, value=1, step=1, label="Anzahl Bilder") error_level_name = gr.Dropdown( list(ERROR_LEVELS), value="H (30 %) – empfohlen", label="Fehlerkorrektur", ) scheduler_name = gr.Dropdown( list(SCHEDULERS), value="DPM++ 2M Karras", label="Sampler" ) init_image = gr.Image( label="Optionales Start-/Referenzbild (img2img)", type="pil" ) run = gr.Button("QR-Code generieren", variant="primary") with gr.Column(scale=1): gallery = gr.Gallery(label="Ergebnisse", columns=2, height=520) control_preview = gr.Image(label="Verwendeter QR-Code (Condition)") used_seed = gr.Number(label="Verwendeter Seed", interactive=False) gr.Examples(examples=EXAMPLES, inputs=[qr_content, prompt]) run.click( fn=generate, inputs=[ qr_content, prompt, negative_prompt, controlnet_scale, guidance_scale, steps, strength, seed, randomize_seed, size, error_level_name, scheduler_name, init_image, num_images, ], outputs=[gallery, control_preview, used_seed], ) if __name__ == "__main__": # Queueing ist seit Gradio 5 standardmäßig aktiv. demo.launch(theme=gr.themes.Soft())