import spaces # MUST be line 1 for ZeroGPU # --------------------------------------------------------------------------- # GRADIO SCHEMA BUG PATCH (Prevents TypeError on boolean schema definitions) # --------------------------------------------------------------------------- import gradio_client.utils as gc_utils _orig_get_type = gc_utils.get_type def _safe_get_type(schema): if not isinstance(schema, dict): return "Any" return _orig_get_type(schema) gc_utils.get_type = _safe_get_type _orig_json_schema_to_python_type = gc_utils._json_schema_to_python_type def _safe_json_schema_to_python_type(schema, defs=None): if not isinstance(schema, dict): return "Any" return _orig_json_schema_to_python_type(schema, defs) gc_utils._json_schema_to_python_type = _safe_json_schema_to_python_type # --------------------------------------------------------------------------- import tempfile import os import random import torch import numpy as np from PIL import Image, ImageEnhance import gradio as gr from diffusers import StableDiffusionInpaintPipeline pipe_inpaint = None def get_inpaint_pipeline(): global pipe_inpaint if pipe_inpaint is None: pipe_inpaint = StableDiffusionInpaintPipeline.from_pretrained( "runwayml/stable-diffusion-inpainting", torch_dtype=torch.float16, safety_checker=None ) pipe_inpaint.enable_attention_slicing() return pipe_inpaint def shift_periodic_canvas(pil_img, shift_x_pct, shift_y_pct): """ Moves/rolls the periodic texture along X and Y axes by custom percentages. Allows dynamic positioning of any seam or feature across the canvas. """ if pil_img is None: return None arr = np.array(pil_img.convert("RGB")) h, w, _ = arr.shape shift_x = int(w * (shift_x_pct / 100.0)) shift_y = int(h * (shift_y_pct / 100.0)) rolled = np.roll(np.roll(arr, shift_x, axis=1), shift_y, axis=0) return Image.fromarray(rolled) def create_smooth_cosine_mask(width, height, seam_pct_x=0.25, seam_pct_y=0.35, mode="Full 2D (Both Axes)"): """C1-continuous smooth cosine crosshair mask.""" x = np.linspace(-0.5, 0.5, width) y = np.linspace(-0.5, 0.5, height) xx, yy = np.meshgrid(x, y) rx = seam_pct_x / 2.0 ry = seam_pct_y / 2.0 sx = np.zeros_like(xx) mask_x = np.abs(xx) <= rx sx[mask_x] = 0.5 * (1.0 + np.cos(np.pi * np.abs(xx[mask_x]) / rx)) if "Horizontal" in mode: mask_final = sx else: sy = np.zeros_like(yy) mask_y = np.abs(yy) <= ry sy[mask_y] = 0.5 * (1.0 + np.cos(np.pi * np.abs(yy[mask_y]) / ry)) mask_final = 1.0 - (1.0 - sx) * (1.0 - sy) mask_uint8 = (np.clip(mask_final, 0, 1) * 255).astype(np.uint8) return Image.fromarray(mask_uint8, mode="L") def extract_brush_mask(editor_data): """Extracts background image and painted red brush layer from Gradio ImageEditor.""" if editor_data is None: return None, None if isinstance(editor_data, dict): bg = editor_data.get("background") or editor_data.get("composite") if bg is None: return None, None bg = bg.convert("RGB") layers = editor_data.get("layers", []) if layers and len(layers) > 0: combined_alpha = np.zeros((bg.height, bg.width), dtype=np.uint8) for layer in layers: if layer is not None: layer_rgba = layer.convert("RGBA") if layer_rgba.size != (bg.width, bg.height): layer_rgba = layer_rgba.resize((bg.width, bg.height), Image.Resampling.NEAREST) alpha = np.array(layer_rgba.split()[-1]) combined_alpha = np.maximum(combined_alpha, alpha) if np.max(combined_alpha) > 0: mask_arr = np.where(combined_alpha > 10, 255, 0).astype(np.uint8) return bg, Image.fromarray(mask_arr, mode="L") return editor_data.convert("RGB"), None def create_tiled_preview(pil_img, mode="Horizontal Only (Landscapes/City Facades)"): preview_img = pil_img.copy() if preview_img.width > 1024: preview_img = preview_img.resize((1024, 1024), Image.Resampling.LANCZOS) w, h = preview_img.size if "Horizontal" in mode: tiled = Image.new("RGB", (w * 3, h)) for c in range(3): tiled.paste(preview_img, (c * w, 0)) else: tiled = Image.new("RGB", (w * 3, h * 3)) for r in range(3): for c in range(3): tiled.paste(preview_img, (c * w, r * h)) return tiled def export_files(scaled_result, resolution_str, ut99_quantize): res_val = int(resolution_str.split("x")[0]) temp_dir = tempfile.mkdtemp() pcx_path = os.path.join(temp_dir, f"ut99_texture_{res_val}.pcx") bmp_path = os.path.join(temp_dir, f"ut99_texture_{res_val}.bmp") if ut99_quantize: quantized = scaled_result.convert("P", palette=Image.Palette.ADAPTIVE, colors=256) quantized.save(pcx_path, format="PCX") quantized.save(bmp_path, format="BMP") display_img = quantized.convert("RGB") else: scaled_result.save(pcx_path, format="PCX") scaled_result.save(bmp_path, format="BMP") display_img = scaled_result return display_img, pcx_path, bmp_path @spaces.GPU def auto_generate_seamless( input_image, prompt, negative_prompt, tiling_mode, shift_x_pct, shift_y_pct, seam_width_pct, seam_height_pct, steps, guidance_scale, seed_val, sharpness_boost, resolution_str, ut99_quantize ): if input_image is None: return None, None, None, None, None, "⚠️ Please upload a source photo first." actual_seed = random.randint(0, 2147483647) if (seed_val == -1 or seed_val is None) else int(seed_val) res_val = int(resolution_str.split("x")[0]) pipeline = get_inpaint_pipeline() pipeline.to("cuda") generator = torch.Generator("cuda").manual_seed(actual_seed) original_photo = input_image.convert("RGB") proc_photo = original_photo.resize((512, 512), Image.Resampling.LANCZOS) # Apply user-selected X and Y periodic movement/roll shifted_photo = shift_periodic_canvas(proc_photo, shift_x_pct, shift_y_pct) user_steps = max(10, int(steps)) user_guidance = float(guidance_scale) photo_prompt = prompt + ", raw photo, ultra realistic, sharp photographic detail, DSLR 8k, crisp texture, natural lighting" strict_negative = negative_prompt + ", oil painting, painting, illustration, drawing, 3d render, anime, smooth, blurry, artistic, cartoon, canvas texture, digital art, vector" seam_mask_cosine = create_smooth_cosine_mask( 512, 512, seam_pct_x=seam_width_pct / 100.0, seam_pct_y=seam_height_pct / 100.0, mode=tiling_mode ) inpainted_zone = pipeline( prompt=photo_prompt, negative_prompt=strict_negative, image=shifted_photo, mask_image=seam_mask_cosine, num_inference_steps=user_steps, guidance_scale=user_guidance, generator=generator ).images[0] seamless_result = Image.composite(inpainted_zone, shifted_photo, seam_mask_cosine) scaled_result = seamless_result.resize((res_val, res_val), Image.Resampling.LANCZOS) if sharpness_boost > 1.0: scaled_result = ImageEnhance.Sharpness(scaled_result).enhance(sharpness_boost) tiled_preview = create_tiled_preview(scaled_result, mode=tiling_mode) display_img, pcx_path, bmp_path = export_files(scaled_result, resolution_str, ut99_quantize) editor_update = {"background": scaled_result, "layers": [], "composite": scaled_result} status_msg = f"✅ Seamless Complete | Shift X: {shift_x_pct}%, Shift Y: {shift_y_pct}% | Seed: {actual_seed}" return editor_update, display_img, tiled_preview, pcx_path, bmp_path, status_msg @spaces.GPU def manual_edge_repair( editor_data, prompt, negative_prompt, tiling_mode, steps, guidance_scale, seed_val, sharpness_boost, resolution_str, ut99_quantize ): bg_img, mask_img = extract_brush_mask(editor_data) if bg_img is None or mask_img is None: return None, None, None, None, None, "⚠️ Paint red brush strokes over problem areas first." actual_seed = random.randint(0, 2147483647) if (seed_val == -1 or seed_val is None) else int(seed_val) res_val = int(resolution_str.split("x")[0]) pipeline = get_inpaint_pipeline() pipeline.to("cuda") generator = torch.Generator("cuda").manual_seed(actual_seed) init_512 = bg_img.resize((512, 512), Image.Resampling.LANCZOS) mask_512 = mask_img.resize((512, 512), Image.Resampling.NEAREST) user_steps = max(10, int(steps)) user_guidance = float(guidance_scale) photo_prompt = prompt + ", raw photo, ultra realistic, sharp photographic detail, DSLR 8k, seamless texture blending" strict_negative = negative_prompt + ", oil painting, painting, illustration, drawing, 3d render, anime, smooth, blurry, artistic, cartoon" repaired_512 = pipeline( prompt=photo_prompt, negative_prompt=strict_negative, image=init_512, mask_image=mask_512, num_inference_steps=user_steps, guidance_scale=user_guidance, generator=generator ).images[0] scaled_result = repaired_512.resize((res_val, res_val), Image.Resampling.LANCZOS) if sharpness_boost > 1.0: scaled_result = ImageEnhance.Sharpness(scaled_result).enhance(sharpness_boost) tiled_preview = create_tiled_preview(scaled_result, mode=tiling_mode) display_img, pcx_path, bmp_path = export_files(scaled_result, resolution_str, ut99_quantize) editor_update = {"background": scaled_result, "layers": [], "composite": scaled_result} status_msg = f"🩹 Edge Repair Complete | Seed: {actual_seed}" return editor_update, display_img, tiled_preview, pcx_path, bmp_path, status_msg def update_canvas_roll(editor_data, shift_x_pct, shift_y_pct): """Interactive button action to roll/move periodic texture tiles live on the editor canvas.""" if editor_data is None: return None if isinstance(editor_data, dict): bg = editor_data.get("background") or editor_data.get("composite") if bg is None: return None pil_img = bg.convert("RGB") else: pil_img = editor_data.convert("RGB") rolled = shift_periodic_canvas(pil_img, shift_x_pct, shift_y_pct) return {"background": rolled, "layers": [], "composite": rolled} # Mobile Clean Layout mobile_css = """ .gradio-container { padding: 6px !important; max-width: 100% !important; } .mobile-btn { width: 100% !important; font-size: 16px !important; padding: 12px !important; } """ with gr.Blocks(title="UT99 Mobile Texture Forge", css=mobile_css) as demo: gr.Markdown("### 📱 UT99 ZeroGPU Mobile Texture Forge") output_status = gr.Textbox(label="Status Log", value="Ready", interactive=False) with gr.Tabs(): # TAB 1: GENERATE & PERIODIC MOVEMENT with gr.Tab("1. Generate & Move Tiles"): input_img = gr.Image(type="pil", label="Upload Photo") tiling_mode = gr.Radio( choices=["Horizontal Only (Landscapes/City Facades)", "Full 2D (Both Axes)"], value="Full 2D (Both Axes)", label="Seamless Mode" ) gr.Markdown("#### ↔️ Periodic Tile Movement (Move seams/tiles on canvas)") with gr.Row(): shift_x_pct = gr.Slider(minimum=0, maximum=100, value=50, step=5, label="Move X-Axis (%)") shift_y_pct = gr.Slider(minimum=0, maximum=100, value=50, step=5, label="Move Y-Axis (%)") prompt = gr.Textbox( value="Detailed fabric curtain texture, sharp photographic pattern", label="AI Prompt" ) negative_prompt = gr.Textbox( value="seam line, border, frame, edge line, blur, watermark, repeating grid", label="Negative Prompt" ) with gr.Accordion("Advanced Options (Seam Mask Widths, CFG, Seed)", open=False): with gr.Row(): seam_width_pct = gr.Slider(minimum=10, maximum=50, value=30, step=5, label="Horiz Seam (%)") seam_height_pct = gr.Slider(minimum=10, maximum=60, value=35, step=5, label="Vert Seam (%)") with gr.Row(): steps = gr.Slider(minimum=10, maximum=50, value=25, step=1, label="Steps") guidance_scale = gr.Slider(minimum=1.0, maximum=15.0, value=7.5, step=0.5, label="CFG") with gr.Row(): sharpness_boost = gr.Slider(minimum=1.0, maximum=2.5, value=1.3, step=0.1, label="Sharpness") seed_val = gr.Number(value=-1, precision=0, label="Seed (-1 = Random)") resolution = gr.Dropdown( choices=["128x128", "256x256 (Default UT99)", "512x512 (HD UT99)", "1024x1024", "2048x2048 (2K)", "4096x4096 (4K)", "8192x8192 (8K)"], value="1024x1024", label="Resolution" ) ut99_quantize = gr.Checkbox(value=False, label="Convert to 8-Bit / 256 Colors (.PCX / .BMP)") btn_auto = gr.Button("🚀 Generate Seamless Texture", variant="primary", elem_classes=["mobile-btn"]) # TAB 2: MANUAL CANVAS TOUCH-UP with gr.Tab("2. Touch-Up & Move Canvas"): gr.Markdown("Draw red brush strokes over problem areas, or move the canvas tiles:") with gr.Row(): btn_roll_live = gr.Button("↔️ Roll / Move Canvas Tiles", variant="secondary") editor = gr.ImageEditor( type="pil", label="Touch-Up Canvas", brush=gr.Brush(colors=["#FF0000"], default_size=15), interactive=True ) btn_repair = gr.Button("🩹 Repair Red Painted Lines", variant="primary", elem_classes=["mobile-btn"]) # TAB 3: PREVIEW & DOWNLOAD with gr.Tab("3. Preview & Export"): output_texture = gr.Image(type="pil", label="Seamless Texture Output") output_preview = gr.Image(type="pil", label="Tiling Preview Test") with gr.Row(): pcx_file = gr.File(label="Download .PCX") bmp_file = gr.File(label="Download .BMP") # Action Connections btn_auto.click( fn=auto_generate_seamless, inputs=[ input_img, prompt, negative_prompt, tiling_mode, shift_x_pct, shift_y_pct, seam_width_pct, seam_height_pct, steps, guidance_scale, seed_val, sharpness_boost, resolution, ut99_quantize ], outputs=[editor, output_texture, output_preview, pcx_file, bmp_file, output_status] ) btn_roll_live.click( fn=update_canvas_roll, inputs=[editor, shift_x_pct, shift_y_pct], outputs=[editor] ) btn_repair.click( fn=manual_edge_repair, inputs=[ editor, prompt, negative_prompt, tiling_mode, steps, guidance_scale, seed_val, sharpness_boost, resolution, ut99_quantize ], outputs=[editor, output_texture, output_preview, pcx_file, bmp_file, output_status] ) if __name__ == "__main__": demo.launch(show_api=False)