Matejstein commited on
Commit
dc047ca
·
verified ·
1 Parent(s): 0749eb5

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +369 -0
app.py ADDED
@@ -0,0 +1,369 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import spaces # MUST be line 1 for ZeroGPU
2
+
3
+ # ---------------------------------------------------------------------------
4
+ # GRADIO SCHEMA BUG PATCH (Prevents TypeError on boolean schema definitions)
5
+ # ---------------------------------------------------------------------------
6
+ import gradio_client.utils as gc_utils
7
+
8
+ _orig_get_type = gc_utils.get_type
9
+ def _safe_get_type(schema):
10
+ if not isinstance(schema, dict):
11
+ return "Any"
12
+ return _orig_get_type(schema)
13
+ gc_utils.get_type = _safe_get_type
14
+
15
+ _orig_json_schema_to_python_type = gc_utils._json_schema_to_python_type
16
+ def _safe_json_schema_to_python_type(schema, defs=None):
17
+ if not isinstance(schema, dict):
18
+ return "Any"
19
+ return _orig_json_schema_to_python_type(schema, defs)
20
+ gc_utils._json_schema_to_python_type = _safe_json_schema_to_python_type
21
+ # ---------------------------------------------------------------------------
22
+
23
+ import tempfile
24
+ import os
25
+ import random
26
+ import torch
27
+ import numpy as np
28
+ from PIL import Image, ImageEnhance
29
+ import gradio as gr
30
+ from diffusers import StableDiffusionInpaintPipeline
31
+
32
+ pipe_inpaint = None
33
+
34
+ def get_inpaint_pipeline():
35
+ global pipe_inpaint
36
+ if pipe_inpaint is None:
37
+ pipe_inpaint = StableDiffusionInpaintPipeline.from_pretrained(
38
+ "runwayml/stable-diffusion-inpainting",
39
+ torch_dtype=torch.float16,
40
+ safety_checker=None
41
+ )
42
+ pipe_inpaint.enable_attention_slicing()
43
+ return pipe_inpaint
44
+
45
+ def shift_image_50(pil_img, mode):
46
+ arr = np.array(pil_img.convert("RGB"))
47
+ h, w, _ = arr.shape
48
+ if "Horizontal" in mode:
49
+ shifted = np.roll(arr, w // 2, axis=1)
50
+ else:
51
+ shifted = np.roll(np.roll(arr, h // 2, axis=0), w // 2, axis=1)
52
+ return Image.fromarray(shifted)
53
+
54
+ def unshift_image_50(pil_img, mode):
55
+ arr = np.array(pil_img.convert("RGB"))
56
+ h, w, _ = arr.shape
57
+ if "Horizontal" in mode:
58
+ unshifted = np.roll(arr, -(w // 2), axis=1)
59
+ else:
60
+ unshifted = np.roll(np.roll(arr, -(h // 2), axis=0), -(w // 2), axis=1)
61
+ return Image.fromarray(unshifted)
62
+
63
+ def create_smooth_cosine_mask(width, height, seam_pct_x=0.25, seam_pct_y=0.35, mode="Full 2D (Both Axes)"):
64
+ x = np.linspace(-0.5, 0.5, width)
65
+ y = np.linspace(-0.5, 0.5, height)
66
+ xx, yy = np.meshgrid(x, y)
67
+
68
+ rx = seam_pct_x / 2.0
69
+ ry = seam_pct_y / 2.0
70
+
71
+ sx = np.zeros_like(xx)
72
+ mask_x = np.abs(xx) <= rx
73
+ sx[mask_x] = 0.5 * (1.0 + np.cos(np.pi * np.abs(xx[mask_x]) / rx))
74
+
75
+ if "Horizontal" in mode:
76
+ mask_final = sx
77
+ else:
78
+ sy = np.zeros_like(yy)
79
+ mask_y = np.abs(yy) <= ry
80
+ sy[mask_y] = 0.5 * (1.0 + np.cos(np.pi * np.abs(yy[mask_y]) / ry))
81
+ mask_final = 1.0 - (1.0 - sx) * (1.0 - sy)
82
+
83
+ mask_uint8 = (np.clip(mask_final, 0, 1) * 255).astype(np.uint8)
84
+ return Image.fromarray(mask_uint8, mode="L")
85
+
86
+ def extract_brush_mask(editor_data):
87
+ """Extracts background image and painted red brush layer from Gradio ImageEditor."""
88
+ if editor_data is None:
89
+ return None, None
90
+
91
+ if isinstance(editor_data, dict):
92
+ bg = editor_data.get("background") or editor_data.get("composite")
93
+ if bg is None:
94
+ return None, None
95
+ bg = bg.convert("RGB")
96
+
97
+ layers = editor_data.get("layers", [])
98
+ if layers and len(layers) > 0:
99
+ combined_alpha = np.zeros((bg.height, bg.width), dtype=np.uint8)
100
+ for layer in layers:
101
+ if layer is not None:
102
+ layer_rgba = layer.convert("RGBA")
103
+ if layer_rgba.size != (bg.width, bg.height):
104
+ layer_rgba = layer_rgba.resize((bg.width, bg.height), Image.Resampling.NEAREST)
105
+ alpha = np.array(layer_rgba.split()[-1])
106
+ combined_alpha = np.maximum(combined_alpha, alpha)
107
+
108
+ if np.max(combined_alpha) > 0:
109
+ mask_arr = np.where(combined_alpha > 10, 255, 0).astype(np.uint8)
110
+ return bg, Image.fromarray(mask_arr, mode="L")
111
+
112
+ return editor_data.convert("RGB"), None
113
+
114
+ def create_tiled_preview(pil_img, mode="Horizontal Only (Landscapes/City Facades)"):
115
+ preview_img = pil_img.copy()
116
+ if preview_img.width > 1024:
117
+ preview_img = preview_img.resize((1024, 1024), Image.Resampling.LANCZOS)
118
+
119
+ w, h = preview_img.size
120
+
121
+ if "Horizontal" in mode:
122
+ tiled = Image.new("RGB", (w * 3, h))
123
+ for c in range(3):
124
+ tiled.paste(preview_img, (c * w, 0))
125
+ else:
126
+ tiled = Image.new("RGB", (w * 3, h * 3))
127
+ for r in range(3):
128
+ for c in range(3):
129
+ tiled.paste(preview_img, (c * w, r * h))
130
+
131
+ return tiled
132
+
133
+ def export_files(scaled_result, resolution_str, ut99_quantize):
134
+ res_val = int(resolution_str.split("x")[0])
135
+ temp_dir = tempfile.mkdtemp()
136
+ pcx_path = os.path.join(temp_dir, f"ut99_texture_{res_val}.pcx")
137
+ bmp_path = os.path.join(temp_dir, f"ut99_texture_{res_val}.bmp")
138
+
139
+ if ut99_quantize:
140
+ quantized = scaled_result.convert("P", palette=Image.Palette.ADAPTIVE, colors=256)
141
+ quantized.save(pcx_path, format="PCX")
142
+ quantized.save(bmp_path, format="BMP")
143
+ display_img = quantized.convert("RGB")
144
+ else:
145
+ scaled_result.save(pcx_path, format="PCX")
146
+ scaled_result.save(bmp_path, format="BMP")
147
+ display_img = scaled_result
148
+
149
+ return display_img, pcx_path, bmp_path
150
+
151
+ @spaces.GPU
152
+ def auto_generate_seamless(
153
+ input_image, prompt, negative_prompt, tiling_mode,
154
+ seam_width_pct, seam_height_pct, steps, guidance_scale, seed_val,
155
+ sharpness_boost, resolution_str, ut99_quantize
156
+ ):
157
+ if input_image is None:
158
+ return None, None, None, None, None, "⚠️ Please upload a source photo first."
159
+
160
+ actual_seed = random.randint(0, 2147483647) if (seed_val == -1 or seed_val is None) else int(seed_val)
161
+ res_val = int(resolution_str.split("x")[0])
162
+
163
+ pipeline = get_inpaint_pipeline()
164
+ pipeline.to("cuda")
165
+ generator = torch.Generator("cuda").manual_seed(actual_seed)
166
+
167
+ original_photo = input_image.convert("RGB")
168
+ proc_photo = original_photo.resize((512, 512), Image.Resampling.LANCZOS)
169
+
170
+ user_steps = max(10, int(steps))
171
+ user_guidance = float(guidance_scale)
172
+
173
+ photo_prompt = prompt + ", raw photo, ultra realistic, sharp photographic detail, DSLR 8k, crisp texture, natural lighting"
174
+ strict_negative = negative_prompt + ", oil painting, painting, illustration, drawing, 3d render, anime, smooth, blurry, artistic, cartoon, canvas texture, digital art, vector"
175
+
176
+ # HORIZONTAL MODE (UNTOUCHED)
177
+ if "Horizontal" in tiling_mode:
178
+ shifted_photo = shift_image_50(proc_photo, tiling_mode)
179
+ seam_mask_cosine = create_smooth_cosine_mask(512, 512, seam_pct_x=seam_width_pct / 100.0, mode=tiling_mode)
180
+
181
+ inpainted_zone = pipeline(
182
+ prompt=photo_prompt,
183
+ negative_prompt=strict_negative,
184
+ image=shifted_photo,
185
+ mask_image=seam_mask_cosine,
186
+ num_inference_steps=user_steps,
187
+ guidance_scale=user_guidance,
188
+ generator=generator
189
+ ).images[0]
190
+
191
+ seamless_result = Image.composite(inpainted_zone, shifted_photo, seam_mask_cosine)
192
+
193
+ # 2D MODE
194
+ else:
195
+ shifted_photo_2d = shift_image_50(proc_photo, tiling_mode)
196
+ seam_mask_2d_cosine = create_smooth_cosine_mask(
197
+ 512, 512,
198
+ seam_pct_x=seam_width_pct / 100.0,
199
+ seam_pct_y=seam_height_pct / 100.0,
200
+ mode=tiling_mode
201
+ )
202
+
203
+ inpainted_2d_zone = pipeline(
204
+ prompt=photo_prompt,
205
+ negative_prompt=strict_negative,
206
+ image=shifted_photo_2d,
207
+ mask_image=seam_mask_2d_cosine,
208
+ num_inference_steps=user_steps,
209
+ guidance_scale=user_guidance,
210
+ generator=generator
211
+ ).images[0]
212
+
213
+ blended_2d = Image.composite(inpainted_2d_zone, shifted_photo_2d, seam_mask_2d_cosine)
214
+ seamless_result = unshift_image_50(blended_2d, tiling_mode)
215
+
216
+ scaled_result = seamless_result.resize((res_val, res_val), Image.Resampling.LANCZOS)
217
+
218
+ if sharpness_boost > 1.0:
219
+ scaled_result = ImageEnhance.Sharpness(scaled_result).enhance(sharpness_boost)
220
+
221
+ tiled_preview = create_tiled_preview(scaled_result, mode=tiling_mode)
222
+ display_img, pcx_path, bmp_path = export_files(scaled_result, resolution_str, ut99_quantize)
223
+
224
+ # Populate editor canvas with generated result for optional manual touch-up
225
+ editor_update = {"background": scaled_result, "layers": [], "composite": scaled_result}
226
+ status_msg = f"✅ Auto Generation Complete | Loaded into Touch-Up Canvas | Seed: {actual_seed}"
227
+
228
+ return editor_update, display_img, tiled_preview, pcx_path, bmp_path, status_msg
229
+
230
+ @spaces.GPU
231
+ def manual_edge_repair(
232
+ editor_data, prompt, negative_prompt, tiling_mode,
233
+ steps, guidance_scale, seed_val, sharpness_boost, resolution_str, ut99_quantize
234
+ ):
235
+ bg_img, mask_img = extract_brush_mask(editor_data)
236
+
237
+ if bg_img is None or mask_img is None:
238
+ return None, None, None, None, None, "⚠️ Please draw red brush strokes over visible edges first."
239
+
240
+ actual_seed = random.randint(0, 2147483647) if (seed_val == -1 or seed_val is None) else int(seed_val)
241
+ res_val = int(resolution_str.split("x")[0])
242
+
243
+ pipeline = get_inpaint_pipeline()
244
+ pipeline.to("cuda")
245
+ generator = torch.Generator("cuda").manual_seed(actual_seed)
246
+
247
+ init_512 = bg_img.resize((512, 512), Image.Resampling.LANCZOS)
248
+ mask_512 = mask_img.resize((512, 512), Image.Resampling.NEAREST)
249
+
250
+ user_steps = max(10, int(steps))
251
+ user_guidance = float(guidance_scale)
252
+
253
+ photo_prompt = prompt + ", raw photo, ultra realistic, sharp photographic detail, DSLR 8k, seamless texture blending"
254
+ strict_negative = negative_prompt + ", oil painting, painting, illustration, drawing, 3d render, anime, smooth, blurry, artistic, cartoon"
255
+
256
+ # Inpaint specifically over user painted red mask
257
+ repaired_512 = pipeline(
258
+ prompt=photo_prompt,
259
+ negative_prompt=strict_negative,
260
+ image=init_512,
261
+ mask_image=mask_512,
262
+ num_inference_steps=user_steps,
263
+ guidance_scale=user_guidance,
264
+ generator=generator
265
+ ).images[0]
266
+
267
+ scaled_result = repaired_512.resize((res_val, res_val), Image.Resampling.LANCZOS)
268
+
269
+ if sharpness_boost > 1.0:
270
+ scaled_result = ImageEnhance.Sharpness(scaled_result).enhance(sharpness_boost)
271
+
272
+ tiled_preview = create_tiled_preview(scaled_result, mode=tiling_mode)
273
+ display_img, pcx_path, bmp_path = export_files(scaled_result, resolution_str, ut99_quantize)
274
+
275
+ editor_update = {"background": scaled_result, "layers": [], "composite": scaled_result}
276
+ status_msg = f"🩹 Manual Edge Repair Complete | Seed: {actual_seed} | Res: {res_val}x{res_val}"
277
+
278
+ return editor_update, display_img, tiled_preview, pcx_path, bmp_path, status_msg
279
+
280
+ # Gradio Interface
281
+ with gr.Blocks(title="UT99 ZeroGPU Hybrid Texture Forge") as demo:
282
+ gr.Markdown("# ⚡ UT99 ZeroGPU Hybrid Texture Forge")
283
+ gr.Markdown("Auto-generate seamless textures, then paint red lines over any remaining sharp edges to selectively repair them.")
284
+
285
+ with gr.Row():
286
+ with gr.Column():
287
+ input_img = gr.Image(type="pil", label="1. Upload Source Photo")
288
+
289
+ tiling_mode = gr.Radio(
290
+ choices=["Horizontal Only (Landscapes/City Facades)", "Full 2D (Both Axes)"],
291
+ value="Full 2D (Both Axes)",
292
+ label="Seamless Mode"
293
+ )
294
+
295
+ prompt = gr.Textbox(
296
+ value="Detailed fabric curtain texture, sharp photographic pattern",
297
+ label="AI Inpainting Prompt"
298
+ )
299
+
300
+ negative_prompt = gr.Textbox(
301
+ value="seam line, border, frame, edge line, blur, watermark, repeating grid",
302
+ label="Negative Prompt"
303
+ )
304
+
305
+ with gr.Row():
306
+ seam_width_pct = gr.Slider(minimum=10, maximum=50, value=30, step=5, label="Auto Horizontal Seam Width (%)")
307
+ seam_height_pct = gr.Slider(minimum=10, maximum=60, value=35, step=5, label="Auto Vertical Seam Height (%)")
308
+
309
+ with gr.Row():
310
+ steps = gr.Slider(minimum=10, maximum=50, value=25, step=1, label="Denoising Steps")
311
+ guidance_scale = gr.Slider(minimum=1.0, maximum=15.0, value=7.5, step=0.5, label="Guidance Scale (CFG)")
312
+
313
+ with gr.Row():
314
+ sharpness_boost = gr.Slider(minimum=1.0, maximum=2.5, value=1.3, step=0.1, label="Sharpness Restorer")
315
+ seed_val = gr.Number(value=-1, precision=0, label="Seed (-1 = Random)")
316
+
317
+ resolution = gr.Dropdown(
318
+ choices=["128x128", "256x256 (Default UT99)", "512x512 (HD UT99)", "1024x1024", "2048x2048 (2K)", "4096x4096 (4K)", "8192x8192 (8K)"],
319
+ value="1024x1024",
320
+ label="Output Resolution"
321
+ )
322
+
323
+ ut99_quantize = gr.Checkbox(value=False, label="Convert to 8-Bit / 256 Colors (.PCX / .BMP)")
324
+
325
+ btn_auto = gr.Button("🚀 1. Auto-Generate Seamless Texture", variant="primary")
326
+
327
+ with gr.Column():
328
+ output_status = gr.Textbox(label="Status / Applied Parameters", interactive=False)
329
+
330
+ editor = gr.ImageEditor(
331
+ type="pil",
332
+ label="2. Manual Seam Touch-Up Canvas (Draw red brush line over any visible seam cut)",
333
+ transforms=[],
334
+ brush=gr.Brush(colors=["#FF0000"], default_size=18)
335
+ )
336
+
337
+ btn_repair = gr.Button("🩹 2. Inpaint Red Painted Edges", variant="secondary")
338
+
339
+ output_texture = gr.Image(type="pil", label="Final Output Texture")
340
+ output_preview = gr.Image(type="pil", label="Tiling Preview Test (3x3 Grid / Panorama)")
341
+
342
+ with gr.Row():
343
+ pcx_file = gr.File(label="Download .PCX")
344
+ bmp_file = gr.File(label="Download .BMP")
345
+
346
+ # Auto generation
347
+ btn_auto.click(
348
+ fn=auto_generate_seamless,
349
+ inputs=[
350
+ input_img, prompt, negative_prompt, tiling_mode,
351
+ seam_width_pct, seam_height_pct, steps, guidance_scale, seed_val,
352
+ sharpness_boost, resolution, ut99_quantize
353
+ ],
354
+ outputs=[editor, output_texture, output_preview, pcx_file, bmp_file, output_status]
355
+ )
356
+
357
+ # Manual edge repair
358
+ btn_repair.click(
359
+ fn=manual_edge_repair,
360
+ inputs=[
361
+ editor, prompt, negative_prompt, tiling_mode,
362
+ steps, guidance_scale, seed_val, sharpness_boost, resolution, ut99_quantize
363
+ ],
364
+ outputs=[editor, output_texture, output_preview, pcx_file, bmp_file, output_status]
365
+ )
366
+
367
+ if __name__ == "__main__":
368
+ demo.launch(show_api=False)
369
+