import os import gc import gradio as gr import numpy as np import spaces import torch import random import base64 import json import html as html_lib from io import BytesIO from pathlib import Path from PIL import Image from manga_colorizer import ( MANGA_CACHE_ROOT, MANGA_DEFAULT_PROMPT, MANGA_NEGATIVE_PROMPT, MangaColorizerError, extract_and_cache_manga, get_colorized_path, load_manga_metadata, make_colorized_zip, manga_pages_from_metadata, page_choice_label, page_preview_caption, ) MAX_SEED = np.iinfo(np.int32).max LANCZOS = getattr(Image, "Resampling", Image).LANCZOS device = torch.device("cuda" if torch.cuda.is_available() else "cpu") print("CUDA_VISIBLE_DEVICES=", os.environ.get("CUDA_VISIBLE_DEVICES")) print("torch.__version__ =", torch.__version__) print("torch.version.cuda =", torch.version.cuda) print("cuda available:", torch.cuda.is_available()) print("cuda device count:", torch.cuda.device_count()) if torch.cuda.is_available(): print("current device:", torch.cuda.current_device()) print("device name:", torch.cuda.get_device_name(torch.cuda.current_device())) print("Using device:", device) from diffusers import FlowMatchEulerDiscreteScheduler from qwenimage.pipeline_qwenimage_edit_plus import QwenImageEditPlusPipeline from qwenimage.transformer_qwenimage import QwenImageTransformer2DModel from qwenimage.qwen_fa3_processor import QwenDoubleStreamAttnProcessorFA3 dtype = torch.bfloat16 pipe = QwenImageEditPlusPipeline.from_pretrained( "FireRedTeam/FireRed-Image-Edit-1.1", transformer=QwenImageTransformer2DModel.from_pretrained( "prithivMLmods/Qwen-Image-Edit-Rapid-AIO-V19", torch_dtype=dtype, device_map="cuda", ), torch_dtype=dtype, ).to(device) try: pipe.transformer.set_attn_processor(QwenDoubleStreamAttnProcessorFA3()) print("Flash Attention 3 Processor set successfully.") except Exception as e: print(f"Warning: Could not set FA3 processor: {e}") EXAMPLES_CONFIG = [ { "images": ["examples/1.jpg"], "prompt": "cinematic polaroid with soft grain subtle vignette gentle lighting white frame handwritten photographed 'Fire-Edit' preserving realistic texture and details.", }, { "images": ["examples/2.jpg"], "prompt": "Transform the image into a dotted cartoon style.", }, { "images": ["examples/3.jpeg"], "prompt": "Convert it to black and white.", }, { "images": ["examples/4.jpg", "examples/5.jpg"], "prompt": "Replace her glasses with the new glasses from image 1.", }, { "images": ["examples/8.jpg", "examples/9.png"], "prompt": "Replace the current clothing with the clothing from the reference image 2. Keep the person's face, hairstyle, body pose, background, lighting, and camera angle unchanged. Ensure the new outfit fits naturally with realistic fabric texture, proper shadows, folds, and accurate proportions. Match the lighting, color tone, and overall style for a seamless and high-quality result.", }, { "images": ["examples/10.jpg", "examples/11.png"], "prompt": "Replace the current clothing with the clothing from the reference image 2. Keep the person's face, hairstyle, body pose, background, lighting, and camera angle unchanged. Ensure the new outfit fits naturally with realistic fabric texture, proper shadows, folds, and accurate proportions. Match the lighting, color tone, and overall style for a seamless and high-quality result.", }, ] def make_thumb_b64(path, max_dim=220): if not os.path.exists(path): return "" try: img = Image.open(path).convert("RGB") img.thumbnail((max_dim, max_dim), LANCZOS) buf = BytesIO() img.save(buf, format="JPEG", quality=65) return f"data:image/jpeg;base64,{base64.b64encode(buf.getvalue()).decode()}" except Exception as e: print(f"Thumbnail error for {path}: {e}") return "" def encode_full_image(path): if not os.path.exists(path): return "" try: with open(path, "rb") as f: data = f.read() ext = path.rsplit(".", 1)[-1].lower() mime = {"jpg": "image/jpeg", "jpeg": "image/jpeg", "png": "image/png", "webp": "image/webp"}.get(ext, "image/jpeg") return f"data:{mime};base64,{base64.b64encode(data).decode()}" except Exception as e: print(f"Encode error for {path}: {e}") return "" def build_example_cards_html(): cards = "" for i, ex in enumerate(EXAMPLES_CONFIG): thumbs_html = "" for path in ex["images"]: thumb = make_thumb_b64(path) if thumb: thumbs_html += f'' else: thumbs_html += '
Preview
' n = len(ex["images"]) badge = f'{n} image{"s" if n > 1 else ""}' prompt_short = html_lib.escape(ex["prompt"][:90]) if len(ex["prompt"]) > 90: prompt_short += "..." cards += f'''
{thumbs_html}
{badge}
{prompt_short}
''' return cards def load_example_data(idx_str): try: idx = int(float(idx_str)) if idx_str and idx_str.strip() else -1 except (ValueError, TypeError): idx = -1 if idx < 0 or idx >= len(EXAMPLES_CONFIG): return json.dumps({"images": [], "prompt": "", "names": [], "status": "error"}) ex = EXAMPLES_CONFIG[idx] b64_list, names = [], [] for path in ex["images"]: b64 = encode_full_image(path) if b64: b64_list.append(b64) names.append(os.path.basename(path)) return json.dumps({"images": b64_list, "prompt": ex["prompt"], "names": names, "status": "ok"}) print("Building example thumbnails...") EXAMPLE_CARDS_HTML = build_example_cards_html() print(f"Built {len(EXAMPLES_CONFIG)} example cards.") def b64_to_pil_list(b64_json_str): if not b64_json_str or b64_json_str.strip() in ("", "[]"): return [] try: b64_list = json.loads(b64_json_str) except Exception: return [] pil_images = [] for b64_str in b64_list: if not b64_str or not isinstance(b64_str, str): continue try: if b64_str.startswith("data:image"): _, data = b64_str.split(",", 1) else: data = b64_str image_data = base64.b64decode(data) pil_images.append(Image.open(BytesIO(image_data)).convert("RGB")) except Exception as e: print(f"Error decoding image: {e}") return pil_images def update_dimensions_on_upload(image): if image is None: return 1024, 1024 w, h = image.size if w > h: nw = 1024 nh = int(nw * h / w) else: nh = 1024 nw = int(nh * w / h) return (nw // 8) * 8, (nh // 8) * 8 @spaces.GPU def infer(images_b64_json, prompt, seed, randomize_seed, guidance_scale, steps, progress=gr.Progress(track_tqdm=True)): gc.collect() torch.cuda.empty_cache() pil_images = b64_to_pil_list(images_b64_json) if not pil_images: raise gr.Error("Please upload at least one image to edit.") if not prompt or prompt.strip() == "": raise gr.Error("Please enter an edit prompt.") if randomize_seed: seed = random.randint(0, MAX_SEED) generator = torch.Generator(device=device).manual_seed(seed) negative_prompt = "worst quality, low quality, bad anatomy, bad hands, text, error, missing fingers, extra digit, fewer digits, cropped, jpeg artifacts, signature, watermark, username, blurry" width, height = update_dimensions_on_upload(pil_images[0]) try: result_image = pipe( image=pil_images, prompt=prompt, negative_prompt=negative_prompt, height=height, width=width, num_inference_steps=steps, generator=generator, true_cfg_scale=guidance_scale, ).images[0] return result_image, seed except Exception as e: raise e finally: gc.collect() torch.cuda.empty_cache() def extract_manga_for_ui(url, max_pages): try: status, _, metadata_json = extract_and_cache_manga(url, max_pages) metadata = load_manga_metadata(metadata_json) pages = metadata.get("pages") or [] preview_items = _manga_preview_items(pages) choices = _manga_choice_labels(pages) return ( status, preview_items, metadata_json, gr.update(choices=choices, value=choices), ) except MangaColorizerError as exc: raise gr.Error(str(exc)) from exc def _gallery_path(path): if not path: return path local_path = Path(path) if local_path.exists(): return str(local_path.resolve()) return path def _manga_preview_items(pages): items = [] for page in pages: path = page.get("original_path") if path and os.path.exists(path): items.append((_gallery_path(path), page_preview_caption(page))) return items def _manga_choice_labels(pages): return [page_choice_label(page) for page in pages] def _selected_manga_pages(pages, selected_pages): if isinstance(selected_pages, str): selected_pages = [selected_pages] selected = set(selected_pages or []) return [page for page in pages if page_choice_label(page) in selected] def _refresh_manga_metadata(metadata, status): pages = metadata.get("pages") or [] metadata["original_paths"] = [page.get("original_path") for page in pages if page.get("original_path")] metadata["image_urls"] = [page.get("source_url") for page in pages if page.get("source_url")] metadata_json = json.dumps(metadata, indent=2) choices = _manga_choice_labels(pages) return ( status, _manga_preview_items(pages), metadata_json, gr.update(choices=choices, value=choices), ) def select_all_manga_pages(metadata_json): try: choices = _manga_choice_labels(manga_pages_from_metadata(metadata_json)) return gr.update(choices=choices, value=choices) except MangaColorizerError as exc: raise gr.Error(str(exc)) from exc def clear_manga_page_selection(): return gr.update(value=[]) def toggle_manga_page_from_gallery(metadata_json, selected_pages, evt): try: pages = manga_pages_from_metadata(metadata_json) except MangaColorizerError as exc: raise gr.Error(str(exc)) from exc if not pages: raise gr.Error("No hay paginas cargadas para seleccionar.") gallery_index = getattr(evt, "index", None) if isinstance(gallery_index, (list, tuple)): gallery_index = gallery_index[0] if gallery_index else None try: page_index = int(gallery_index) except (TypeError, ValueError): raise gr.Error("No se pudo identificar la pagina seleccionada.") if page_index < 0 or page_index >= len(pages): raise gr.Error("Pagina seleccionada fuera de rango.") if isinstance(selected_pages, str): selected_pages = [selected_pages] choices = _manga_choice_labels(pages) selected = list(selected_pages or []) label = page_choice_label(pages[page_index]) if label in selected: selected = [item for item in selected if item != label] status = f"Pagina quitada de la seleccion: {label}" else: selected.append(label) selected = [choice for choice in choices if choice in selected] status = f"Pagina seleccionada: {label}" return gr.update(choices=choices, value=selected), status def remove_selected_manga_pages(metadata_json, selected_pages): try: metadata = load_manga_metadata(metadata_json) except MangaColorizerError as exc: raise gr.Error(str(exc)) from exc pages = metadata.get("pages") or [] selected = set(selected_pages or []) if not selected: raise gr.Error("Selecciona al menos una pagina para eliminar de la lista.") remaining_pages = [page for page in pages if page_choice_label(page) not in selected] for index, page in enumerate(remaining_pages): page["index"] = index metadata["pages"] = remaining_pages removed_count = len(pages) - len(remaining_pages) status = f"Eliminadas {removed_count} pagina(s) de la lista. No se borraron archivos del cache." return _refresh_manga_metadata(metadata, status) def colorize_manga_page(image_path: str, prompt: str, seed: int, guidance_scale: float, steps: int) -> Image.Image: input_image = Image.open(image_path).convert("RGB") width, height = update_dimensions_on_upload(input_image) generator = torch.Generator(device=device).manual_seed(int(seed)) result_image = pipe( image=[input_image], prompt=prompt, negative_prompt=MANGA_NEGATIVE_PROMPT, height=height, width=width, num_inference_steps=int(steps), generator=generator, true_cfg_scale=float(guidance_scale), ).images[0] output_path = get_colorized_path(image_path) result_image.save(output_path) return result_image @spaces.GPU def colorize_cached_manga( metadata_json, prompt, selected_pages, colorize_all, seed, randomize_seed, guidance_scale, steps, max_color_pages, progress=gr.Progress(track_tqdm=True), ): if not torch.cuda.is_available(): raise gr.Error("No hay GPU disponible para colorear manga.") try: metadata = load_manga_metadata(metadata_json) except MangaColorizerError as exc: raise gr.Error(str(exc)) from exc pages = metadata.get("pages") or [] existing_pages = [page for page in pages if page.get("original_path") and os.path.exists(page.get("original_path"))] if not existing_pages: raise gr.Error("No hay imagenes en cache. Extrae las paginas primero.") try: limit = int(max_color_pages) except (TypeError, ValueError): limit = 5 limit = max(1, min(limit, 30)) if colorize_all: pages_to_process = existing_pages[:limit] else: pages_to_process = _selected_manga_pages(existing_pages, selected_pages)[:limit] if not pages_to_process: raise gr.Error("No hay paginas seleccionadas. Selecciona paginas o activa colorear todas.") prompt = (prompt or MANGA_DEFAULT_PROMPT).strip() or MANGA_DEFAULT_PROMPT try: base_seed = int(seed) except (TypeError, ValueError): base_seed = 0 colorized_paths = [] colorized_items = [] total = len(pages_to_process) for progress_index, page in enumerate(pages_to_process, start=1): page_seed = random.randint(0, MAX_SEED) if randomize_seed else base_seed page_path = page.get("original_path") page_label = page_choice_label(page) progress(progress_index / total, desc=f"Colorizing {page_label}") try: colorize_manga_page(page_path, prompt, page_seed, guidance_scale, steps) finally: gc.collect() torch.cuda.empty_cache() colorized_path = str(get_colorized_path(page_path)) colorized_paths.append(colorized_path) colorized_items.append((_gallery_path(colorized_path), f"Colorized {page_label}")) try: zip_path = make_colorized_zip(colorized_paths) except MangaColorizerError as exc: raise gr.Error(str(exc)) from exc status = ( f"Listo: {len(colorized_paths)} pagina(s) coloreada(s). " f"ZIP generado. Solo usar con contenido propio o con permiso." ) return status, colorized_items, _gallery_path(zip_path) css = r""" @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap'); *{box-sizing:border-box;margin:0;padding:0} body,.gradio-container{ background:#0f0f13!important;font-family:'Inter',system-ui,-apple-system,sans-serif!important; font-size:14px!important;color:#e4e4e7!important;min-height:100vh; } .dark body,.dark .gradio-container{background:#0f0f13!important;color:#e4e4e7!important} footer{display:none!important} .hidden-input{display:none!important;height:0!important;overflow:hidden!important;margin:0!important;padding:0!important} .manga-colorizer-shell{ max-width:1400px;margin:12px auto 24px;padding:0;background:#18181b; border:1px solid #27272a;border-radius:16px;overflow:hidden; box-shadow:0 25px 50px -12px rgba(0,0,0,.58),0 0 0 1px rgba(255,255,255,.03); } .manga-colorizer-shell h2{color:#ffffff!important;font-size:22px!important;margin:0!important} .manga-colorizer-shell h3{color:#ffffff!important;font-size:13px!important;margin:0!important} .manga-colorizer-shell p,.manga-colorizer-shell label,.manga-colorizer-shell span{ color:#d4d4d8!important; } .manga-colorizer-shell textarea,.manga-colorizer-shell input{ background:#09090b!important;color:#f4f4f5!important;border-color:#3f3f46!important; } .manga-colorizer-shell button{ border-radius:8px!important;font-weight:700!important;transition:all .16s ease!important; } .manga-hero{ padding:18px 22px;background:linear-gradient(135deg,#18181b,#1f1f25); border-bottom:1px solid #27272a; } .manga-hero p{margin-top:6px!important;color:#a1a1aa!important;max-width:860px!important} .manga-workbench{padding:16px 20px 20px;display:flex;flex-direction:column;gap:14px} .manga-control-band{ padding:14px;background:#09090b;border:1px solid #27272a;border-radius:12px; } .manga-gallery-frame{ background:#09090b;border:1px solid #27272a;border-radius:12px;padding:10px; } .manga-gallery-frame .grid-wrap,.manga-gallery-frame .wrap{ background:#09090b!important;border-radius:10px!important; } .manga-selection-panel{ background:#101014;border:1px solid #27272a;border-radius:12px;padding:12px; } .manga-selection-title{ display:flex;align-items:center;justify-content:space-between;margin-bottom:10px; color:#71717a!important;font-size:12px!important;font-weight:700!important;text-transform:uppercase!important; } .manga-page-selector{ padding:0!important;background:transparent!important;border:none!important; } .manga-page-selector fieldset{border:none!important;padding:0!important} .manga-page-selector .wrap{ display:grid!important;grid-template-columns:repeat(auto-fill,minmax(170px,1fr))!important; gap:8px!important;max-height:240px!important;overflow:auto!important;padding:2px 4px 4px 2px!important; } .manga-page-selector label{ position:relative!important;display:flex!important;align-items:center!important;gap:8px!important; min-height:38px!important;padding:8px 10px 8px 34px!important;background:#18181b!important; border:1px solid #303036!important;border-radius:9px!important;color:#d4d4d8!important; font-family:'JetBrains Mono',monospace!important;font-size:12px!important;line-height:1.2!important; cursor:pointer!important;overflow:hidden!important;white-space:nowrap!important;text-overflow:ellipsis!important; } .manga-page-selector label:hover{ border-color:#1E90FF!important;background:rgba(30,144,255,.08)!important;transform:translateY(-1px); } .manga-page-selector label:has(input:checked){ border-color:#1E90FF!important;background:rgba(30,144,255,.18)!important; box-shadow:0 0 0 2px rgba(30,144,255,.14)!important;color:#ffffff!important; } .manga-page-selector input[type="checkbox"]{ position:absolute!important;left:10px!important;top:50%!important;transform:translateY(-50%)!important; width:16px!important;height:16px!important;accent-color:#1E90FF!important; } .manga-actions{display:grid!important;grid-template-columns:repeat(3,minmax(0,1fr))!important;gap:10px!important;margin-top:10px!important} .manga-action-btn,.manga-action-btn button,.manga-soft-btn,.manga-soft-btn button{ background:#27272a!important;border:1px solid #3f3f46!important;color:#ffffff!important;min-height:38px!important; } .manga-action-btn:hover,.manga-action-btn button:hover,.manga-soft-btn:hover,.manga-soft-btn button:hover{ background:rgba(30,144,255,.18)!important;border-color:#1E90FF!important;transform:translateY(-1px)!important; } .manga-danger-btn:hover,.manga-danger-btn button:hover{ background:rgba(239,68,68,.16)!important;border-color:#ef4444!important; } .manga-primary-btn,.manga-primary-btn button{ background:linear-gradient(135deg,#1E90FF,#1873CC)!important;border:none!important;color:#fff!important; min-height:42px!important;box-shadow:0 4px 16px rgba(30,144,255,.26)!important; } .manga-primary-btn:hover,.manga-primary-btn button:hover{ background:linear-gradient(135deg,#47A3FF,#1E90FF)!important;transform:translateY(-1px)!important; } .manga-prompt-box textarea{min-height:150px!important;line-height:1.5!important;font-size:13px!important} .manga-run-row{align-items:end!important} .manga-advanced-label{margin:2px 0 -6px!important;color:#71717a!important;font-size:12px!important;font-weight:700!important} @media (max-width:760px){ .manga-actions{grid-template-columns:1fr!important} .manga-page-selector .wrap{grid-template-columns:1fr!important} } #example-load-btn{ position:absolute!important;left:-9999px!important;top:-9999px!important; width:1px!important;height:1px!important;opacity:0.01!important; pointer-events:none!important;overflow:hidden!important; } #gradio-run-btn{ position:absolute;left:-9999px;top:-9999px;width:1px;height:1px; opacity:0.01;pointer-events:none;overflow:hidden; } .app-shell{ background:#18181b;border:1px solid #27272a;border-radius:16px; margin:12px auto;max-width:1400px;overflow:hidden; box-shadow:0 25px 50px -12px rgba(0,0,0,.6),0 0 0 1px rgba(255,255,255,.03); } .app-header{ background:linear-gradient(135deg,#18181b,#1e1e24);border-bottom:1px solid #27272a; padding:14px 24px;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px; } .app-header-left{display:flex;align-items:center;gap:12px} .app-logo{ width:36px;height:36px;background:linear-gradient(135deg,#1E90FF,#47A3FF,#7CB8FF); border-radius:10px;display:flex;align-items:center;justify-content:center; box-shadow:0 4px 12px rgba(30,144,255,.35); } .app-logo svg{width:20px;height:20px;fill:#fff;flex-shrink:0} .app-title{ font-size:18px;font-weight:700;background:linear-gradient(135deg,#e4e4e7,#a1a1aa); -webkit-background-clip:text;-webkit-text-fill-color:transparent;letter-spacing:-.3px; } .app-badge{ font-size:11px;font-weight:600;padding:3px 10px;border-radius:20px; background:rgba(30,144,255,.15);color:#47A3FF;border:1px solid rgba(30,144,255,.25);letter-spacing:.3px; } .app-badge.fast{background:rgba(34,197,94,.12);color:#4ade80;border:1px solid rgba(34,197,94,.25)} .app-toolbar{ background:#18181b;border-bottom:1px solid #27272a;padding:8px 16px; display:flex;gap:4px;align-items:center;flex-wrap:wrap; } .tb-sep{width:1px;height:28px;background:#27272a;margin:0 8px} .modern-tb-btn{ display:inline-flex;align-items:center;justify-content:center;gap:6px; min-width:32px;height:34px;background:transparent;border:1px solid transparent; border-radius:8px;cursor:pointer;font-size:13px;font-weight:600;padding:0 12px; font-family:'Inter',sans-serif;color:#ffffff!important;-webkit-text-fill-color:#ffffff!important; transition:all .15s ease; } .modern-tb-btn:hover{background:rgba(30,144,255,.15);border-color:rgba(30,144,255,.3)} .modern-tb-btn:active,.modern-tb-btn.active{background:rgba(30,144,255,.25);border-color:rgba(30,144,255,.45)} .modern-tb-btn .tb-label{font-size:13px;color:#ffffff!important;-webkit-text-fill-color:#ffffff!important;font-weight:600} .modern-tb-btn .tb-svg{width:15px;height:15px;flex-shrink:0;color:#ffffff!important} .modern-tb-btn .tb-svg, .modern-tb-btn .tb-svg *{stroke:#ffffff!important;fill:none!important} .tb-info{font-family:'JetBrains Mono',monospace;font-size:12px;color:#71717a;padding:0 8px;display:flex;align-items:center} body:not(.dark) .modern-tb-btn,body:not(.dark) .modern-tb-btn *{color:#ffffff!important;-webkit-text-fill-color:#ffffff!important} body:not(.dark) .modern-tb-btn .tb-svg,body:not(.dark) .modern-tb-btn .tb-svg *{stroke:#ffffff!important} .dark .modern-tb-btn,.dark .modern-tb-btn *{color:#ffffff!important;-webkit-text-fill-color:#ffffff!important} .dark .modern-tb-btn .tb-svg,.dark .modern-tb-btn .tb-svg *{stroke:#ffffff!important} .gradio-container .modern-tb-btn,.gradio-container .modern-tb-btn *{color:#ffffff!important;-webkit-text-fill-color:#ffffff!important} .gradio-container .modern-tb-btn .tb-svg,.gradio-container .modern-tb-btn .tb-svg *{stroke:#ffffff!important} .app-main-row{display:flex;gap:0;flex:1;overflow:hidden} .app-main-left{flex:1;display:flex;flex-direction:column;min-width:0;border-right:1px solid #27272a} .app-main-right{width:420px;display:flex;flex-direction:column;flex-shrink:0;background:#18181b} #gallery-drop-zone{position:relative;background:#09090b;min-height:440px;overflow:auto} #gallery-drop-zone.drag-over{outline:2px solid #1E90FF;outline-offset:-2px;background:rgba(30,144,255,.04)} .upload-prompt-modern{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);z-index:20} .upload-click-area{ display:flex;flex-direction:column;align-items:center;justify-content:center; cursor:pointer;padding:36px 52px;border:2px dashed #3f3f46;border-radius:16px; background:rgba(30,144,255,.03);transition:all .2s ease;gap:8px; } .upload-click-area:hover{background:rgba(30,144,255,.08);border-color:#1E90FF;transform:scale(1.03)} .upload-click-area:active{background:rgba(30,144,255,.12);transform:scale(.98)} .upload-click-area svg{width:80px;height:80px} .upload-main-text{color:#71717a;font-size:14px;font-weight:500;margin-top:4px} .upload-sub-text{color:#52525b;font-size:12px} .image-gallery-grid{ display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr)); gap:12px;padding:16px;align-content:start; } .gallery-thumb{ position:relative;aspect-ratio:1;border-radius:10px;overflow:hidden; cursor:pointer;border:2px solid #27272a;transition:all .2s ease;background:#18181b; } .gallery-thumb:hover{border-color:#3f3f46;transform:translateY(-2px);box-shadow:0 4px 12px rgba(0,0,0,.4)} .gallery-thumb.selected{border-color:#1E90FF!important;box-shadow:0 0 0 3px rgba(30,144,255,.2)} .gallery-thumb img{width:100%;height:100%;object-fit:cover} .thumb-badge{ position:absolute;top:6px;left:6px;background:#1E90FF;color:#fff; padding:2px 8px;border-radius:4px;font-family:'JetBrains Mono',monospace;font-size:11px;font-weight:600; } .thumb-remove{ position:absolute;top:6px;right:6px;width:24px;height:24px;background:rgba(0,0,0,.75); color:#fff;border:1px solid rgba(255,255,255,.15);border-radius:50%;cursor:pointer; display:none;align-items:center;justify-content:center;font-size:12px;transition:all .15s;line-height:1; } .gallery-thumb:hover .thumb-remove{display:flex} .thumb-remove:hover{background:#1E90FF;border-color:#1E90FF} .gallery-add-card{ aspect-ratio:1;border-radius:10px;border:2px dashed #3f3f46; display:flex;flex-direction:column;align-items:center;justify-content:center; cursor:pointer;transition:all .2s ease;background:rgba(30,144,255,.03);gap:4px; } .gallery-add-card:hover{border-color:#1E90FF;background:rgba(30,144,255,.08)} .gallery-add-card .add-icon{font-size:28px;color:#71717a;font-weight:300} .gallery-add-card .add-text{font-size:12px;color:#71717a;font-weight:500} .hint-bar{ background:rgba(30,144,255,.06);border-top:1px solid #27272a;border-bottom:1px solid #27272a; padding:10px 20px;font-size:13px;color:#a1a1aa;line-height:1.7; } .hint-bar b{color:#7CB8FF;font-weight:600} .hint-bar kbd{ display:inline-block;padding:1px 6px;background:#27272a;border:1px solid #3f3f46; border-radius:4px;font-family:'JetBrains Mono',monospace;font-size:11px;color:#a1a1aa; } .suggestions-section{border-top:1px solid #27272a;padding:12px 16px} .suggestions-title,.examples-title{ font-size:12px;font-weight:600;color:#71717a;text-transform:uppercase; letter-spacing:.8px;margin-bottom:10px; } .suggestions-wrap{display:flex;flex-wrap:wrap;gap:6px} .suggestion-chip{ display:inline-flex;align-items:center;gap:4px;padding:5px 12px; background:rgba(30,144,255,.08);border:1px solid rgba(30,144,255,.2);border-radius:20px; color:#7CB8FF;font-size:12px;font-weight:500;font-family:'Inter',sans-serif; cursor:pointer;transition:all .15s;white-space:nowrap; } .suggestion-chip:hover{background:rgba(30,144,255,.15);border-color:rgba(30,144,255,.35);color:#47A3FF;transform:translateY(-1px)} .examples-section{border-top:1px solid #27272a;padding:12px 16px} .examples-scroll{display:flex;gap:10px;overflow-x:auto;padding-bottom:8px} .examples-scroll::-webkit-scrollbar{height:6px} .examples-scroll::-webkit-scrollbar-track{background:#09090b;border-radius:3px} .examples-scroll::-webkit-scrollbar-thumb{background:#27272a;border-radius:3px} .examples-scroll::-webkit-scrollbar-thumb:hover{background:#3f3f46} .example-card{ flex-shrink:0;width:210px;background:#09090b;border:1px solid #27272a; border-radius:10px;overflow:hidden;cursor:pointer;transition:all .2s ease; } .example-card:hover{border-color:#1E90FF;transform:translateY(-2px);box-shadow:0 4px 12px rgba(30,144,255,.15)} .example-card.loading{opacity:.5;pointer-events:none} .example-thumbs{display:flex;height:110px;overflow:hidden;background:#18181b} .example-thumbs img{flex:1;object-fit:cover;min-width:0;border-bottom:1px solid #27272a} .example-thumb-placeholder{ flex:1;display:flex;align-items:center;justify-content:center; background:#18181b;color:#3f3f46;font-size:11px;min-width:0; } .example-meta{padding:6px 10px;display:flex;align-items:center;gap:6px} .example-badge{ display:inline-flex;padding:2px 7px;background:rgba(30,144,255,.1);border-radius:4px; font-size:10px;font-weight:600;color:#47A3FF;font-family:'JetBrains Mono',monospace;white-space:nowrap; } .example-prompt-text{ padding:0 10px 8px;font-size:11px;color:#a1a1aa;line-height:1.4; display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden; } .panel-card{border-bottom:1px solid #27272a} .panel-card-title{ padding:12px 20px;font-size:12px;font-weight:600;color:#71717a; text-transform:uppercase;letter-spacing:.8px;border-bottom:1px solid rgba(39,39,42,.6); } .panel-card-body{padding:16px 20px;display:flex;flex-direction:column;gap:8px} .modern-label{font-size:13px;font-weight:500;color:#a1a1aa;margin-bottom:4px;display:block} .modern-textarea{ width:100%;background:#09090b;border:1px solid #27272a;border-radius:8px; padding:10px 14px;font-family:'Inter',sans-serif;font-size:14px;color:#e4e4e7; resize:vertical;outline:none;min-height:42px;transition:border-color .2s; } .modern-textarea:focus{border-color:#1E90FF;box-shadow:0 0 0 3px rgba(30,144,255,.15)} .modern-textarea::placeholder{color:#3f3f46} .modern-textarea.error-flash{ border-color:#ef4444!important;box-shadow:0 0 0 3px rgba(239,68,68,.2)!important;animation:shake .4s ease; } @keyframes shake{0%,100%{transform:translateX(0)}20%,60%{transform:translateX(-4px)}40%,80%{transform:translateX(4px)}} .toast-notification{ position:fixed;top:24px;left:50%;transform:translateX(-50%) translateY(-120%); z-index:9999;padding:10px 24px;border-radius:10px;font-family:'Inter',sans-serif; font-size:14px;font-weight:600;display:flex;align-items:center;gap:8px; box-shadow:0 8px 24px rgba(0,0,0,.5); transition:transform .35s cubic-bezier(.34,1.56,.64,1),opacity .35s ease;opacity:0;pointer-events:none; } .toast-notification.visible{transform:translateX(-50%) translateY(0);opacity:1;pointer-events:auto} .toast-notification.error{background:linear-gradient(135deg,#dc2626,#b91c1c);color:#fff;border:1px solid rgba(255,255,255,.15)} .toast-notification.warning{background:linear-gradient(135deg,#d97706,#b45309);color:#fff;border:1px solid rgba(255,255,255,.15)} .toast-notification.info{background:linear-gradient(135deg,#2563eb,#1d4ed8);color:#fff;border:1px solid rgba(255,255,255,.15)} .toast-notification.success{background:linear-gradient(135deg,#16a34a,#15803d);color:#fff;border:1px solid rgba(255,255,255,.15)} .toast-notification .toast-icon{font-size:16px;line-height:1} .toast-notification .toast-text{line-height:1.3} .btn-run{ display:flex;align-items:center;justify-content:center;gap:8px;width:100%; background:linear-gradient(135deg,#1E90FF,#1873CC);border:none;border-radius:10px; padding:12px 24px;cursor:pointer;font-size:15px;font-weight:600;font-family:'Inter',sans-serif; color:#ffffff!important;-webkit-text-fill-color:#ffffff!important;transition:all .2s ease;letter-spacing:-.2px; box-shadow:0 4px 16px rgba(30,144,255,.3),inset 0 1px 0 rgba(255,255,255,.1); } .btn-run:hover{ background:linear-gradient(135deg,#47A3FF,#1E90FF);transform:translateY(-1px); box-shadow:0 6px 24px rgba(30,144,255,.45),inset 0 1px 0 rgba(255,255,255,.15); } .btn-run:active{transform:translateY(0);box-shadow:0 2px 8px rgba(30,144,255,.3)} .btn-run svg{width:18px;height:18px;fill:#ffffff!important} .btn-run svg path{fill:#ffffff!important} #custom-run-btn,#custom-run-btn *,#custom-run-btn span,#custom-run-btn svg, #custom-run-btn svg path,#run-btn-label,.btn-run,.btn-run *{ color:#ffffff!important;-webkit-text-fill-color:#ffffff!important;fill:#ffffff!important; } body:not(.dark) .btn-run,body:not(.dark) .btn-run *,body:not(.dark) #custom-run-btn, body:not(.dark) #custom-run-btn *{color:#ffffff!important;-webkit-text-fill-color:#ffffff!important;fill:#ffffff!important} .dark .btn-run,.dark .btn-run *,.dark #custom-run-btn,.dark #custom-run-btn *{ color:#ffffff!important;-webkit-text-fill-color:#ffffff!important;fill:#ffffff!important; } .gradio-container .btn-run,.gradio-container .btn-run *,.gradio-container #custom-run-btn, .gradio-container #custom-run-btn *{color:#ffffff!important;-webkit-text-fill-color:#ffffff!important;fill:#ffffff!important} .output-frame{border-bottom:1px solid #27272a;display:flex;flex-direction:column;position:relative} .output-frame .out-title{ padding:10px 20px;font-size:13px;font-weight:700;color:#ffffff!important; -webkit-text-fill-color:#ffffff!important;text-transform:uppercase;letter-spacing:.8px; border-bottom:1px solid rgba(39,39,42,.6);display:flex;align-items:center;justify-content:space-between; } .output-frame .out-title span{color:#ffffff!important;-webkit-text-fill-color:#ffffff!important} .output-frame .out-body{ flex:1;background:#09090b;display:flex;align-items:center;justify-content:center; overflow:hidden;min-height:240px;position:relative; } .output-frame .out-body img{max-width:100%;max-height:460px;image-rendering:auto} .output-frame .out-placeholder{color:#3f3f46;font-size:13px;text-align:center;padding:20px} .out-download-btn{ display:none;align-items:center;justify-content:center;background:rgba(30,144,255,.1); border:1px solid rgba(30,144,255,.2);border-radius:6px;cursor:pointer;padding:3px 10px; font-size:11px;font-weight:500;color:#7CB8FF!important;gap:4px;height:24px;transition:all .15s; } .out-download-btn:hover{background:rgba(30,144,255,.2);border-color:rgba(30,144,255,.35);color:#ffffff!important} .out-download-btn.visible{display:inline-flex} .out-download-btn svg{width:12px;height:12px;fill:#7CB8FF} .modern-loader{ display:none;position:absolute;top:0;left:0;right:0;bottom:0;background:rgba(9,9,11,.92); z-index:15;flex-direction:column;align-items:center;justify-content:center;gap:16px;backdrop-filter:blur(4px); } .modern-loader.active{display:flex} .modern-loader .loader-spinner{ width:36px;height:36px;border:3px solid #27272a;border-top-color:#1E90FF; border-radius:50%;animation:spin .8s linear infinite; } @keyframes spin{to{transform:rotate(360deg)}} .modern-loader .loader-text{font-size:13px;color:#a1a1aa;font-weight:500} .loader-bar-track{width:200px;height:4px;background:#27272a;border-radius:2px;overflow:hidden} .loader-bar-fill{ height:100%;background:linear-gradient(90deg,#1E90FF,#47A3FF,#1E90FF); background-size:200% 100%;animation:shimmer 1.5s ease-in-out infinite;border-radius:2px; } @keyframes shimmer{0%{background-position:200% 0}100%{background-position:-200% 0}} .settings-group{border:1px solid #27272a;border-radius:10px;margin:12px 16px;padding:0;overflow:hidden} .settings-group-title{ font-size:12px;font-weight:600;color:#71717a;text-transform:uppercase;letter-spacing:.8px; padding:10px 16px;border-bottom:1px solid #27272a;background:rgba(24,24,27,.5); } .settings-group-body{padding:14px 16px;display:flex;flex-direction:column;gap:12px} .slider-row{display:flex;align-items:center;gap:10px;min-height:28px} .slider-row label{font-size:13px;font-weight:500;color:#a1a1aa;min-width:72px;flex-shrink:0} .slider-row input[type="range"]{ flex:1;-webkit-appearance:none;appearance:none;height:6px;background:#27272a; border-radius:3px;outline:none;min-width:0; } .slider-row input[type="range"]::-webkit-slider-thumb{ -webkit-appearance:none;width:16px;height:16px;background:linear-gradient(135deg,#1E90FF,#1873CC); border-radius:50%;cursor:pointer;box-shadow:0 2px 6px rgba(30,144,255,.4);transition:transform .15s; } .slider-row input[type="range"]::-webkit-slider-thumb:hover{transform:scale(1.2)} .slider-row input[type="range"]::-moz-range-thumb{ width:16px;height:16px;background:linear-gradient(135deg,#1E90FF,#1873CC); border-radius:50%;cursor:pointer;border:none;box-shadow:0 2px 6px rgba(30,144,255,.4); } .slider-row .slider-val{ min-width:52px;text-align:right;font-family:'JetBrains Mono',monospace;font-size:12px; font-weight:500;padding:3px 8px;background:#09090b;border:1px solid #27272a; border-radius:6px;color:#a1a1aa;flex-shrink:0; } .checkbox-row{display:flex;align-items:center;gap:8px;font-size:13px;color:#a1a1aa} .checkbox-row input[type="checkbox"]{accent-color:#1E90FF;width:16px;height:16px;cursor:pointer} .checkbox-row label{color:#a1a1aa;font-size:13px;cursor:pointer} .app-statusbar{ background:#18181b;border-top:1px solid #27272a;padding:6px 20px; display:flex;gap:12px;height:34px;align-items:center;font-size:12px; } .app-statusbar .sb-section{ padding:0 12px;flex:1;display:flex;align-items:center;font-family:'JetBrains Mono',monospace; font-size:12px;color:#52525b;overflow:hidden;white-space:nowrap; } .app-statusbar .sb-section.sb-fixed{ flex:0 0 auto;min-width:90px;text-align:center;justify-content:center; padding:3px 12px;background:rgba(30,144,255,.08);border-radius:6px;color:#47A3FF;font-weight:500; } .exp-note{padding:10px 20px;font-size:12px;color:#52525b;border-top:1px solid #27272a;text-align:center} .exp-note a{color:#47A3FF;text-decoration:none} .exp-note a:hover{text-decoration:underline} .dark .app-shell{background:#18181b} .dark .upload-prompt-modern{background:transparent} .dark .panel-card{background:#18181b} .dark .settings-group{background:#18181b} .dark .output-frame .out-title{color:#ffffff!important} .dark .output-frame .out-title span{color:#ffffff!important} .dark .out-download-btn{color:#7CB8FF!important} .dark .out-download-btn:hover{color:#ffffff!important} ::-webkit-scrollbar{width:8px;height:8px} ::-webkit-scrollbar-track{background:#09090b} ::-webkit-scrollbar-thumb{background:#27272a;border-radius:4px} ::-webkit-scrollbar-thumb:hover{background:#3f3f46} @media(max-width:840px){ .app-main-row{flex-direction:column} .app-main-right{width:100%} .app-main-left{border-right:none;border-bottom:1px solid #27272a} } """ gallery_js = r""" () => { function init() { if (window.__fireRedInitDone) return; const galleryGrid = document.getElementById('image-gallery-grid'); const dropZone = document.getElementById('gallery-drop-zone'); const uploadPrompt = document.getElementById('upload-prompt'); const uploadClick = document.getElementById('upload-click-area'); const fileInput = document.getElementById('custom-file-input'); const btnUpload = document.getElementById('tb-upload'); const btnPaste = document.getElementById('tb-paste'); const btnRemove = document.getElementById('tb-remove'); const btnClear = document.getElementById('tb-clear'); const promptInput = document.getElementById('custom-prompt-input'); const runBtnEl = document.getElementById('custom-run-btn'); const imgCountTb = document.getElementById('tb-image-count'); const imgCountSb = document.getElementById('sb-image-count'); if (!galleryGrid || !fileInput || !dropZone) { setTimeout(init, 250); return; } window.__fireRedInitDone = true; let images = []; window.__uploadedImages = images; let selectedIdx = -1; let toastTimer = null; function showToast(message, type) { let toast = document.getElementById('app-toast'); if (!toast) { toast = document.createElement('div'); toast.id = 'app-toast'; toast.className = 'toast-notification'; toast.innerHTML = ''; document.body.appendChild(toast); } const icon = toast.querySelector('.toast-icon'); const text = toast.querySelector('.toast-text'); toast.className = 'toast-notification ' + (type || 'error'); if (type === 'warning') icon.textContent = '\u26A0'; else if (type === 'info') icon.textContent = '\u2139'; else if (type === 'success') icon.textContent = '\u2713'; else icon.textContent = '\u2717'; text.textContent = message; if (toastTimer) clearTimeout(toastTimer); void toast.offsetWidth; toast.classList.add('visible'); toastTimer = setTimeout(() => toast.classList.remove('visible'), 3500); } window.__showToast = showToast; function flashPromptError() { if (!promptInput) return; promptInput.classList.add('error-flash'); promptInput.focus(); setTimeout(() => promptInput.classList.remove('error-flash'), 800); } function setGradioValue(containerId, value) { const container = document.getElementById(containerId); if (!container) return; container.querySelectorAll('input, textarea').forEach(el => { if (el.type === 'file' || el.type === 'range' || el.type === 'checkbox') return; const proto = el.tagName === 'TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype; const ns = Object.getOwnPropertyDescriptor(proto, 'value'); if (ns && ns.set) { ns.set.call(el, value); el.dispatchEvent(new Event('input', {bubbles:true, composed:true})); el.dispatchEvent(new Event('change', {bubbles:true, composed:true})); } }); } window.__setGradioValue = setGradioValue; function syncImagesToGradio() { window.__uploadedImages = images; const b64Array = images.map(img => img.b64); setGradioValue('hidden-images-b64', JSON.stringify(b64Array)); updateCounts(); } function syncPromptToGradio() { if (promptInput) setGradioValue('prompt-gradio-input', promptInput.value); } function updateCounts() { const n = images.length; const txt = n > 0 ? n + ' image' + (n > 1 ? 's' : '') : 'No images'; if (imgCountTb) imgCountTb.textContent = txt; if (imgCountSb) imgCountSb.textContent = n > 0 ? txt + ' uploaded' : 'No images uploaded'; } function addImage(b64, name) { images.push({id: Date.now() + Math.random(), b64: b64, name: name}); renderGallery(); syncImagesToGradio(); } window.__addImage = addImage; function removeImage(idx) { images.splice(idx, 1); if (selectedIdx === idx) selectedIdx = -1; else if (selectedIdx > idx) selectedIdx--; renderGallery(); syncImagesToGradio(); } function clearAll() { images = []; window.__uploadedImages = images; selectedIdx = -1; renderGallery(); syncImagesToGradio(); } window.__clearAll = clearAll; function selectImage(idx) { selectedIdx = (selectedIdx === idx) ? -1 : idx; renderGallery(); } function renderGallery() { if (images.length === 0) { galleryGrid.innerHTML = ''; galleryGrid.style.display = 'none'; if (uploadPrompt) uploadPrompt.style.display = ''; return; } if (uploadPrompt) uploadPrompt.style.display = 'none'; galleryGrid.style.display = 'grid'; let html = ''; images.forEach((img, i) => { const sel = i === selectedIdx ? ' selected' : ''; html += ''; }); html += ''; galleryGrid.innerHTML = html; galleryGrid.querySelectorAll('.gallery-thumb').forEach(thumb => { thumb.addEventListener('click', (e) => { if (e.target.closest('.thumb-remove')) return; selectImage(parseInt(thumb.dataset.idx)); }); }); galleryGrid.querySelectorAll('.thumb-remove').forEach(btn => { btn.addEventListener('click', (e) => { e.stopPropagation(); removeImage(parseInt(btn.dataset.remove)); }); }); const addCard = document.getElementById('gallery-add-card'); if (addCard) addCard.addEventListener('click', () => fileInput.click()); } function processFiles(files) { Array.from(files).forEach(file => { if (!file.type.startsWith('image/')) return; const reader = new FileReader(); reader.onload = (e) => addImage(e.target.result, file.name); reader.readAsDataURL(file); }); } // ============ PEGAR DESDE PORTAPAPELES (Ctrl+V) ============ function handlePasteEvent(e) { const items = (e.clipboardData || window.clipboardData)?.items; if (!items) return; let pastedCount = 0; for (const item of items) { if (item.kind === 'file' && item.type.startsWith('image/')) { const file = item.getAsFile(); if (!file) continue; e.preventDefault(); const reader = new FileReader(); const ext = (file.type.split('/')[1] || 'png').split(';')[0]; const name = file.name && file.name !== 'image.png' ? file.name : 'pasted_' + Date.now() + '.' + ext; reader.onload = (ev) => addImage(ev.target.result, name); reader.readAsDataURL(file); pastedCount++; } } if (pastedCount > 0) { showToast('Imagen pegada desde portapapeles', 'success'); } } // Listener global: funciona con Ctrl+V en cualquier parte (excepto cuando se pega texto en textarea/input) document.addEventListener('paste', (e) => { const target = e.target; const isTextField = target && (target.tagName === 'TEXTAREA' || target.tagName === 'INPUT'); const items = (e.clipboardData || window.clipboardData)?.items; if (!items) return; let hasImage = false; for (const item of items) { if (item.kind === 'file' && item.type.startsWith('image/')) { hasImage = true; break; } } // Si está enfocado un campo de texto y NO hay imagen, dejar pegar texto normal if (isTextField && !hasImage) return; handlePasteEvent(e); }); // Botón "Paste" del toolbar: lee del portapapeles vía Clipboard API async function pasteFromClipboardAPI() { if (!navigator.clipboard || !navigator.clipboard.read) { showToast('Pulsa Ctrl+V para pegar la imagen', 'info'); return; } try { const items = await navigator.clipboard.read(); let pastedCount = 0; for (const item of items) { for (const type of item.types) { if (type.startsWith('image/')) { const blob = await item.getType(type); const reader = new FileReader(); const ext = (type.split('/')[1] || 'png').split(';')[0]; const name = 'pasted_' + Date.now() + '.' + ext; await new Promise((res) => { reader.onload = (ev) => { addImage(ev.target.result, name); res(); }; reader.readAsDataURL(blob); }); pastedCount++; break; } } } if (pastedCount > 0) showToast('Imagen pegada desde portapapeles', 'success'); else showToast('No hay imagen en el portapapeles', 'warning'); } catch (err) { console.error('Clipboard read error:', err); showToast('Pulsa Ctrl+V para pegar la imagen', 'info'); } } if (btnPaste) btnPaste.addEventListener('click', pasteFromClipboardAPI); // ============ FIN PEGAR DESDE PORTAPAPELES ============ fileInput.addEventListener('change', (e) => { processFiles(e.target.files); e.target.value = ''; }); if (uploadClick) uploadClick.addEventListener('click', () => fileInput.click()); if (btnUpload) btnUpload.addEventListener('click', () => fileInput.click()); if (btnRemove) btnRemove.addEventListener('click', () => { if (selectedIdx >= 0 && selectedIdx < images.length) removeImage(selectedIdx); }); if (btnClear) btnClear.addEventListener('click', clearAll); dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('drag-over'); }); dropZone.addEventListener('dragleave', (e) => { e.preventDefault(); dropZone.classList.remove('drag-over'); }); dropZone.addEventListener('drop', (e) => { e.preventDefault(); dropZone.classList.remove('drag-over'); if (e.dataTransfer.files.length) processFiles(e.dataTransfer.files); }); if (promptInput) promptInput.addEventListener('input', syncPromptToGradio); window.__setPrompt = function(text) { if (promptInput) { promptInput.value = text; syncPromptToGradio(); } }; document.querySelectorAll('.example-card[data-idx]').forEach(card => { card.addEventListener('click', () => { const idx = card.getAttribute('data-idx'); document.querySelectorAll('.example-card.loading').forEach(c => c.classList.remove('loading')); card.classList.add('loading'); showToast('Loading example...', 'info'); setGradioValue('example-result-data', ''); setGradioValue('example-idx-input', idx); setTimeout(() => { const btn = document.getElementById('example-load-btn'); if (btn) { const b = btn.querySelector('button'); if (b) b.click(); else btn.click(); } }, 150); setTimeout(() => card.classList.remove('loading'), 12000); }); }); function syncSlider(customId, gradioId) { const slider = document.getElementById(customId); const valSpan = document.getElementById(customId + '-val'); if (!slider) return; slider.addEventListener('input', () => { if (valSpan) valSpan.textContent = slider.value; const container = document.getElementById(gradioId); if (!container) return; container.querySelectorAll('input[type="range"],input[type="number"]').forEach(el => { const ns = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value'); if (ns && ns.set) { ns.set.call(el, slider.value); el.dispatchEvent(new Event('input', {bubbles:true, composed:true})); el.dispatchEvent(new Event('change', {bubbles:true, composed:true})); } }); }); } syncSlider('custom-seed', 'gradio-seed'); syncSlider('custom-guidance', 'gradio-guidance'); syncSlider('custom-steps', 'gradio-steps'); const randCheck = document.getElementById('custom-randomize'); if (randCheck) { randCheck.addEventListener('change', () => { const container = document.getElementById('gradio-randomize'); if (!container) return; const cb = container.querySelector('input[type="checkbox"]'); if (cb && cb.checked !== randCheck.checked) cb.click(); }); } function showLoader() { const l = document.getElementById('output-loader'); if (l) l.classList.add('active'); const sb = document.querySelector('.sb-fixed'); if (sb) sb.textContent = 'Processing...'; } function hideLoader() { const l = document.getElementById('output-loader'); if (l) l.classList.remove('active'); const sb = document.querySelector('.sb-fixed'); if (sb) sb.textContent = 'Done'; } window.__showLoader = showLoader; window.__hideLoader = hideLoader; function validateBeforeRun() { const promptVal = promptInput ? promptInput.value.trim() : ''; const hasImages = images.length > 0; if (!hasImages && !promptVal) { showToast('Please upload an image and enter a prompt', 'error'); flashPromptError(); return false; } if (!hasImages) { showToast('Please upload at least one image', 'error'); return false; } if (!promptVal) { showToast('Please enter an edit prompt', 'warning'); flashPromptError(); return false; } return true; } window.__clickGradioRunBtn = function() { if (!validateBeforeRun()) return; syncPromptToGradio(); syncImagesToGradio(); showLoader(); setTimeout(() => { const gradioBtn = document.getElementById('gradio-run-btn'); if (!gradioBtn) return; const btn = gradioBtn.querySelector('button'); if (btn) btn.click(); else gradioBtn.click(); }, 200); }; if (runBtnEl) runBtnEl.addEventListener('click', () => window.__clickGradioRunBtn()); renderGallery(); updateCounts(); } init(); } """ wire_outputs_js = r""" () => { function watchOutputs() { const resultContainer = document.getElementById('gradio-result'); const outBody = document.getElementById('output-image-container'); const outPh = document.getElementById('output-placeholder'); const dlBtn = document.getElementById('dl-btn-output'); if (!resultContainer || !outBody) { setTimeout(watchOutputs, 500); return; } if (dlBtn) { dlBtn.addEventListener('click', (e) => { e.stopPropagation(); const img = outBody.querySelector('img.modern-out-img'); if (img && img.src) { const a = document.createElement('a'); a.href = img.src; a.download = 'firered_output.png'; document.body.appendChild(a); a.click(); document.body.removeChild(a); } }); } function syncImage() { const resultImg = resultContainer.querySelector('img'); if (resultImg && resultImg.src) { if (outPh) outPh.style.display = 'none'; let existing = outBody.querySelector('img.modern-out-img'); if (!existing) { existing = document.createElement('img'); existing.className = 'modern-out-img'; outBody.appendChild(existing); } if (existing.src !== resultImg.src) { existing.src = resultImg.src; if (dlBtn) dlBtn.classList.add('visible'); if (window.__hideLoader) window.__hideLoader(); } } } const observer = new MutationObserver(syncImage); observer.observe(resultContainer, {childList:true, subtree:true, attributes:true, attributeFilter:['src']}); setInterval(syncImage, 800); } watchOutputs(); function watchSeed() { const seedContainer = document.getElementById('gradio-seed'); const seedSlider = document.getElementById('custom-seed'); const seedVal = document.getElementById('custom-seed-val'); if (!seedContainer || !seedSlider) { setTimeout(watchSeed, 500); return; } function sync() { const el = seedContainer.querySelector('input[type="range"],input[type="number"]'); if (el && el.value) { seedSlider.value = el.value; if (seedVal) seedVal.textContent = el.value; } } const obs = new MutationObserver(sync); obs.observe(seedContainer, {childList:true, subtree:true, attributes:true, attributeFilter:['value']}); setInterval(sync, 1000); } watchSeed(); function watchExampleResults() { const container = document.getElementById('example-result-data'); if (!container) { setTimeout(watchExampleResults, 500); return; } let lastProcessed = ''; function checkResult() { const el = container.querySelector('textarea') || container.querySelector('input'); if (!el) return; const val = el.value; if (!val || val === lastProcessed || val.length < 20) return; try { const data = JSON.parse(val); if (data.status === 'ok' && data.images && data.images.length > 0) { lastProcessed = val; if (window.__clearAll) window.__clearAll(); if (window.__setPrompt && data.prompt) window.__setPrompt(data.prompt); data.images.forEach((b64, i) => { if (b64 && window.__addImage) { const name = (data.names && data.names[i]) ? data.names[i] : ('example_' + (i+1) + '.jpg'); window.__addImage(b64, name); } }); document.querySelectorAll('.example-card.loading').forEach(c => c.classList.remove('loading')); if (window.__showToast) window.__showToast('Example loaded \u2014 ' + data.images.length + ' image(s)', 'info'); } else if (data.status === 'error') { document.querySelectorAll('.example-card.loading').forEach(c => c.classList.remove('loading')); if (window.__showToast) window.__showToast('Could not load example images', 'error'); } } catch(e) { console.error('Example parse error:', e); } } const obs = new MutationObserver(checkResult); obs.observe(container, {childList:true, subtree:true, characterData:true, attributes:true}); setInterval(checkResult, 500); } watchExampleResults(); } """ DOWNLOAD_SVG = '' UPLOAD_SVG = '' PASTE_SVG = '' REMOVE_SVG = '' CLEAR_SVG = '' FIRE_LOGO_SVG = '' with gr.Blocks() as demo: hidden_images_b64 = gr.Textbox(value="[]", elem_id="hidden-images-b64", elem_classes="hidden-input", container=False) prompt = gr.Textbox(value="", elem_id="prompt-gradio-input", elem_classes="hidden-input", container=False) seed = gr.Slider(minimum=0, maximum=MAX_SEED, step=1, value=0, elem_id="gradio-seed", elem_classes="hidden-input", container=False) randomize_seed = gr.Checkbox(value=True, elem_id="gradio-randomize", elem_classes="hidden-input", container=False) guidance_scale = gr.Slider(minimum=1.0, maximum=10.0, step=0.1, value=1.0, elem_id="gradio-guidance", elem_classes="hidden-input", container=False) steps = gr.Slider(minimum=1, maximum=50, step=1, value=4, elem_id="gradio-steps", elem_classes="hidden-input", container=False) result = gr.Image(elem_id="gradio-result", elem_classes="hidden-input", container=False, format="png") example_idx = gr.Textbox(value="", elem_id="example-idx-input", elem_classes="hidden-input", container=False) example_result = gr.Textbox(value="", elem_id="example-result-data", elem_classes="hidden-input", container=False) example_load_btn = gr.Button("Load Example", elem_id="example-load-btn") with gr.Tabs(elem_id="main-tabs"): with gr.Tab("Image Editor"): gr.HTML(f"""
FireRed-Image-Edit v1.1 4-Step Fast
No images
Upload: Click or drag to add images  ·  Paste: Ctrl+V pega imagen del portapapeles  ·  Multi-image: Upload multiple images for reference-based editing  ·  Remove deletes selected  ·  Clear All removes everything
Quick Prompts
Quick Examples
{EXAMPLE_CARDS_HTML}
Edit Instruction
Output {DOWNLOAD_SVG} Save
Processing image...
Result will appear here
Advanced Settings
0
1.0
4
Experimental Space for FireRed-Image-Edit-1.1 · Open on GitHub
No images uploaded
Ready
""") with gr.Tab("Manga URL Colorizer"): with gr.Column(elem_classes="manga-colorizer-shell"): gr.Markdown( "## Manga URL Colorizer\n" "Cache, preview, seleccion y colorizacion por lote. " "**Solo usar con contenido propio o con permiso.**", elem_classes="manga-hero", ) with gr.Column(elem_classes="manga-workbench"): with gr.Group(elem_classes="manga-control-band"): with gr.Row(): manga_url = gr.Textbox( label="Manga URL", placeholder="https://vermangasporno.com/doujins/okumoto-yuuta-amazing-sex-prez/", scale=4, ) manga_max_pages = gr.Slider( label="Maximo de paginas a descargar", minimum=1, maximum=120, step=1, value=10, scale=1, ) manga_extract_btn = gr.Button( "1. Extraer imagenes", variant="primary", elem_classes="manga-primary-btn", ) manga_status = gr.Markdown("Ready.") manga_original_gallery = gr.Gallery( label="Preview de paginas originales", columns=6, height=360, object_fit="contain", elem_classes="manga-gallery-frame", ) manga_metadata = gr.Textbox(label="Manga metadata", visible=False) with gr.Group(elem_classes="manga-selection-panel"): gr.Markdown("Paginas a colorear", elem_classes="manga-selection-title") manga_selected_pages = gr.CheckboxGroup( label="", choices=[], value=[], elem_classes="manga-page-selector", ) with gr.Row(elem_classes="manga-actions"): manga_select_all_btn = gr.Button("Seleccionar todas", elem_classes="manga-action-btn") manga_clear_selection_btn = gr.Button("Limpiar seleccion", elem_classes="manga-action-btn") manga_remove_selected_btn = gr.Button( "Eliminar seleccionadas de la lista", elem_classes=["manga-action-btn", "manga-danger-btn"], ) manga_prompt = gr.Textbox( label="Prompt de colorizacion", value=MANGA_DEFAULT_PROMPT, lines=6, elem_classes="manga-prompt-box", ) with gr.Group(elem_classes="manga-control-band"): with gr.Row(elem_classes="manga-run-row"): manga_colorize_all = gr.Checkbox( label="Colorear todas las paginas descargadas", value=False, scale=1, ) manga_max_color_pages = gr.Slider( label="Maximo de paginas a colorear", minimum=1, maximum=30, step=1, value=5, scale=1, ) gr.Markdown("Advanced settings", elem_classes="manga-advanced-label") with gr.Row(): manga_seed = gr.Slider( label="Seed", minimum=0, maximum=MAX_SEED, step=1, value=0, ) manga_randomize_seed = gr.Checkbox(label="Randomize seed", value=True) with gr.Row(): manga_guidance_scale = gr.Slider( label="Guidance scale", minimum=1.0, maximum=10.0, step=0.1, value=1.0, ) manga_steps = gr.Slider( label="Steps", minimum=1, maximum=50, step=1, value=4, ) manga_colorize_btn = gr.Button( "2. Colorear manga", variant="primary", elem_classes="manga-primary-btn", ) manga_colorized_gallery = gr.Gallery( label="Resultados coloreados", columns=6, height=360, object_fit="contain", elem_classes="manga-gallery-frame", ) manga_zip = gr.File(label="Descargar ZIP coloreado") run_btn = gr.Button("Run", elem_id="gradio-run-btn") demo.load(fn=None, js=gallery_js) demo.load(fn=None, js=wire_outputs_js) run_btn.click( fn=infer, inputs=[hidden_images_b64, prompt, seed, randomize_seed, guidance_scale, steps], outputs=[result, seed], js=r"""(imgs, p, s, rs, gs, st) => { const images = window.__uploadedImages || []; const b64Array = images.map(img => img.b64); const imgsJson = JSON.stringify(b64Array); const promptEl = document.getElementById('custom-prompt-input'); const promptVal = promptEl ? promptEl.value : p; return [imgsJson, promptVal, s, rs, gs, st]; }""", ) example_load_btn.click( fn=load_example_data, inputs=[example_idx], outputs=[example_result], queue=False, ) manga_extract_btn.click( fn=extract_manga_for_ui, inputs=[manga_url, manga_max_pages], outputs=[manga_status, manga_original_gallery, manga_metadata, manga_selected_pages], queue=False, ) manga_original_gallery.select( fn=toggle_manga_page_from_gallery, inputs=[manga_metadata, manga_selected_pages], outputs=[manga_selected_pages, manga_status], queue=False, ) manga_select_all_btn.click( fn=select_all_manga_pages, inputs=[manga_metadata], outputs=[manga_selected_pages], queue=False, ) manga_clear_selection_btn.click( fn=clear_manga_page_selection, inputs=[], outputs=[manga_selected_pages], queue=False, ) manga_remove_selected_btn.click( fn=remove_selected_manga_pages, inputs=[manga_metadata, manga_selected_pages], outputs=[manga_status, manga_original_gallery, manga_metadata, manga_selected_pages], queue=False, ) manga_colorize_btn.click( fn=colorize_cached_manga, inputs=[ manga_metadata, manga_prompt, manga_selected_pages, manga_colorize_all, manga_seed, manga_randomize_seed, manga_guidance_scale, manga_steps, manga_max_color_pages, ], outputs=[manga_status, manga_colorized_gallery, manga_zip], ) if __name__ == "__main__": MANGA_CACHE_ROOT.mkdir(parents=True, exist_ok=True) demo.queue(max_size=50).launch( css=css, mcp_server=True, ssr_mode=False, show_error=True, allowed_paths=[str(Path("examples").resolve()), str(MANGA_CACHE_ROOT.resolve())], )