Files changed (1) hide show
  1. app.py +77 -71
app.py CHANGED
@@ -27,54 +27,55 @@ import torch
27
  import numpy as np
28
  from PIL import Image
29
  import gradio as gr
30
- from diffusers import StableDiffusionImg2ImgPipeline
31
 
32
- pipe = None
 
33
 
34
- def get_pipeline():
35
- global pipe
36
- if pipe is None:
37
- pipe = StableDiffusionImg2ImgPipeline.from_pretrained(
38
- "runwayml/stable-diffusion-v1-5",
39
  torch_dtype=torch.float16,
40
  safety_checker=None
41
  )
42
- pipe.enable_attention_slicing()
43
- return pipe
44
 
45
- def create_seamless_matrix(pil_img, blend_factor=0.35, mode="Horizontal Only (Landscapes/City Facades)"):
46
- """
47
- Crossfades edge boundaries.
48
- Supports both 2D wrapping and 1D horizontal-only wrapping.
49
- """
50
- img = np.array(pil_img.convert("RGB")).astype(np.float32) / 255.0
51
- h, w, c = img.shape
52
 
53
  if "Horizontal" in mode:
54
- # Shift 50% along width (X-axis) only
55
  shifted = np.roll(img, w // 2, axis=1)
56
-
57
- x = np.linspace(-1, 1, w)
58
- xx = np.tile(x, (h, 1))
59
-
60
- mask_x = np.clip(np.abs(xx) / blend_factor, 0, 1)[:, :, np.newaxis]
61
- blended = shifted * mask_x + img * (1.0 - mask_x)
62
  else:
63
- # Full 2D Shift (X and Y axes)
64
  shifted = np.roll(np.roll(img, h // 2, axis=0), w // 2, axis=1)
65
 
66
- y = np.linspace(-1, 1, h)
67
- x = np.linspace(-1, 1, w)
68
- xx, yy = np.meshgrid(x, y)
69
 
70
- mask_x = np.clip(np.abs(xx) / blend_factor, 0, 1)
71
- mask_y = np.clip(np.abs(yy) / blend_factor, 0, 1)
72
- mask = np.minimum(mask_x, mask_y)[:, :, np.newaxis]
 
 
 
 
73
 
74
- blended = shifted * mask + img * (1.0 - mask)
 
 
 
 
 
 
 
 
 
 
75
 
76
- result = (np.clip(blended, 0, 1) * 255).astype(np.uint8)
77
- return Image.fromarray(result)
78
 
79
  def create_tiled_preview(pil_img, mode="Horizontal Only (Landscapes/City Facades)"):
80
  """Generates 1x3 horizontal panorama or 3x3 grid preview."""
@@ -85,12 +86,10 @@ def create_tiled_preview(pil_img, mode="Horizontal Only (Landscapes/City Facades
85
  w, h = preview_img.size
86
 
87
  if "Horizontal" in mode:
88
- # 1x3 Horizontal panorama test strip
89
  tiled = Image.new("RGB", (w * 3, h))
90
  for c in range(3):
91
  tiled.paste(preview_img, (c * w, 0))
92
  else:
93
- # 3x3 Full 2D grid test
94
  tiled = Image.new("RGB", (w * 3, h * 3))
95
  for r in range(3):
96
  for c in range(3):
@@ -99,50 +98,48 @@ def create_tiled_preview(pil_img, mode="Horizontal Only (Landscapes/City Facades
99
  return tiled
100
 
101
  @spaces.GPU
102
- def generate_ai_texture(input_image, prompt, tiling_mode, steps, seed_val, strength, resolution_str, ut99_quantize):
103
  if input_image is None:
104
  return None, None, None, None, "⚠️ Please upload a source photo first."
105
 
106
  actual_seed = random.randint(0, 2147483647) if (seed_val == -1 or seed_val is None) else int(seed_val)
107
  res_val = int(resolution_str.split("x")[0])
108
 
109
- pipeline = get_pipeline()
110
  pipeline.to("cuda")
111
  generator = torch.Generator("cuda").manual_seed(actual_seed)
112
 
113
- # 1. Axis-aware seamless edge pre-processing
114
- seamless_input = create_seamless_matrix(input_image, blend_factor=0.30, mode=tiling_mode)
115
- init_img = seamless_input.resize((512, 512), Image.Resampling.LANCZOS)
116
 
117
- # 2. Step math guardrail
118
- user_steps = max(5, int(steps))
119
- user_strength = max(0.1, float(strength))
120
- calculated_steps = max(int(user_steps / user_strength), user_steps + 5)
121
-
122
- if "Horizontal" in tiling_mode:
123
- axis_prompt = "horizontally seamless texture, panorama horizon, landscape game asset"
124
  else:
125
- axis_prompt = "fully seamless tileable texture, 2D pattern, game asset"
 
126
 
127
- full_prompt = prompt + ", " + axis_prompt
128
- negative_prompt = "blurry, distorted, frames, borders, window glare, watermark"
 
129
 
130
- # 3. GPU Diffusion Execution
131
- result = pipeline(
132
  prompt=full_prompt,
133
  negative_prompt=negative_prompt,
134
- image=init_img,
135
- strength=user_strength,
136
- num_inference_steps=calculated_steps,
137
  guidance_scale=7.5,
138
  generator=generator
139
  ).images[0]
140
 
141
- # 4. Upscale to requested resolution (up to 8K)
142
- final_img = result.resize((res_val, res_val), Image.Resampling.LANCZOS)
143
  tiled_preview = create_tiled_preview(final_img, mode=tiling_mode)
144
 
145
- # 5. Export 8-bit UT99 formats (.PCX / .BMP)
146
  temp_dir = tempfile.mkdtemp()
147
  pcx_path = os.path.join(temp_dir, f"ut99_texture_{res_val}.pcx")
148
  bmp_path = os.path.join(temp_dir, f"ut99_texture_{res_val}.bmp")
@@ -157,18 +154,24 @@ def generate_ai_texture(input_image, prompt, tiling_mode, steps, seed_val, stren
157
  final_img.save(bmp_path, format="BMP")
158
  display_img = final_img
159
 
160
- status_msg = f"✅ Success | Mode: {tiling_mode} | Seed: {actual_seed} | Output: {res_val}x{res_val}"
161
  return display_img, tiled_preview, pcx_path, bmp_path, status_msg
162
 
163
  # Gradio Interface
164
  with gr.Blocks(title="UT99 ZeroGPU AI Texture Forge") as demo:
165
  gr.Markdown("# ⚡ UT99 ZeroGPU AI Texture Forge")
166
- gr.Markdown("Generate seamless 8-bit UT99 textures up to 8K with 1D Horizontal or 2D Full Tiling.")
167
 
168
  with gr.Row():
169
  with gr.Column():
170
  input_img = gr.Image(type="pil", label="Upload Source Photo")
171
-
 
 
 
 
 
 
172
  tiling_mode = gr.Radio(
173
  choices=["Horizontal Only (Landscapes/City Facades)", "Full 2D (Both Axes)"],
174
  value="Horizontal Only (Landscapes/City Facades)",
@@ -176,19 +179,19 @@ with gr.Blocks(title="UT99 ZeroGPU AI Texture Forge") as demo:
176
  )
177
 
178
  prompt = gr.Textbox(
179
- value="Canyon mountains horizon vista, landscape panorama texture",
180
- label="AI Prompt"
 
 
 
 
 
181
  )
182
 
183
  with gr.Row():
184
- steps = gr.Slider(minimum=5, maximum=30, value=15, step=1, label="Denoising Steps")
185
  seed_val = gr.Number(value=-1, precision=0, label="Seed (-1 = Random)")
186
 
187
- strength = gr.Slider(
188
- minimum=0.1, maximum=0.9, value=0.35, step=0.05,
189
- label="AI Strength"
190
- )
191
-
192
  resolution = gr.Dropdown(
193
  choices=[
194
  "128x128",
@@ -208,7 +211,7 @@ with gr.Blocks(title="UT99 ZeroGPU AI Texture Forge") as demo:
208
  label="Convert to 8-Bit / 256 Colors (.PCX / .BMP)"
209
  )
210
 
211
- btn = gr.Button("🚀 Generate AI Texture on GPU", variant="primary")
212
 
213
  with gr.Column():
214
  output_status = gr.Textbox(label="Status / Seed", interactive=False)
@@ -221,7 +224,10 @@ with gr.Blocks(title="UT99 ZeroGPU AI Texture Forge") as demo:
221
 
222
  btn.click(
223
  fn=generate_ai_texture,
224
- inputs=[input_img, prompt, tiling_mode, steps, seed_val, strength, resolution, ut99_quantize],
 
 
 
225
  outputs=[output_texture, output_preview, pcx_file, bmp_file, output_status]
226
  )
227
 
 
27
  import numpy as np
28
  from PIL import Image
29
  import gradio as gr
30
+ from diffusers import StableDiffusionInpaintPipeline
31
 
32
+ # Lazy pipeline loader for ZeroGPU
33
+ pipe_inpaint = None
34
 
35
+ def get_inpaint_pipeline():
36
+ global pipe_inpaint
37
+ if pipe_inpaint is None:
38
+ pipe_inpaint = StableDiffusionInpaintPipeline.from_pretrained(
39
+ "runwayml/stable-diffusion-inpainting",
40
  torch_dtype=torch.float16,
41
  safety_checker=None
42
  )
43
+ pipe_inpaint.enable_attention_slicing()
44
+ return pipe_inpaint
45
 
46
+ def shift_image(pil_img, mode="Horizontal Only (Landscapes/City Facades)"):
47
+ """Shifts image by 50% along selected axes to position outer seams at canvas center."""
48
+ img = np.array(pil_img.convert("RGB"))
49
+ h, w, _ = img.shape
 
 
 
50
 
51
  if "Horizontal" in mode:
 
52
  shifted = np.roll(img, w // 2, axis=1)
 
 
 
 
 
 
53
  else:
 
54
  shifted = np.roll(np.roll(img, h // 2, axis=0), w // 2, axis=1)
55
 
56
+ return Image.fromarray(shifted)
 
 
57
 
58
+ def create_seam_mask(width, height, seam_width_pct=0.25, mode="Horizontal Only (Landscapes/City Facades)"):
59
+ """
60
+ Creates a black-and-white mask.
61
+ White = Area to be inpainted by AI (center seam join zone).
62
+ Black = Area to keep 100% original photo intact.
63
+ """
64
+ mask = np.zeros((height, width), dtype=np.uint8)
65
 
66
+ if "Horizontal" in mode:
67
+ w_seam = int(width * seam_width_pct)
68
+ x_start = (width - w_seam) // 2
69
+ mask[:, x_start:x_start + w_seam] = 255
70
+ else:
71
+ w_seam = int(width * seam_width_pct)
72
+ h_seam = int(height * seam_width_pct)
73
+ x_start = (width - w_seam) // 2
74
+ y_start = (height - h_seam) // 2
75
+ mask[:, x_start:x_start + w_seam] = 255
76
+ mask[y_start:y_start + h_seam, :] = 255
77
 
78
+ return Image.fromarray(mask)
 
79
 
80
  def create_tiled_preview(pil_img, mode="Horizontal Only (Landscapes/City Facades)"):
81
  """Generates 1x3 horizontal panorama or 3x3 grid preview."""
 
86
  w, h = preview_img.size
87
 
88
  if "Horizontal" in mode:
 
89
  tiled = Image.new("RGB", (w * 3, h))
90
  for c in range(3):
91
  tiled.paste(preview_img, (c * w, 0))
92
  else:
 
93
  tiled = Image.new("RGB", (w * 3, h * 3))
94
  for r in range(3):
95
  for c in range(3):
 
98
  return tiled
99
 
100
  @spaces.GPU
101
+ def generate_ai_texture(input_image, prompt, method_mode, tiling_mode, seam_width_pct, steps, seed_val, resolution_str, ut99_quantize):
102
  if input_image is None:
103
  return None, None, None, None, "⚠️ Please upload a source photo first."
104
 
105
  actual_seed = random.randint(0, 2147483647) if (seed_val == -1 or seed_val is None) else int(seed_val)
106
  res_val = int(resolution_str.split("x")[0])
107
 
108
+ pipeline = get_inpaint_pipeline()
109
  pipeline.to("cuda")
110
  generator = torch.Generator("cuda").manual_seed(actual_seed)
111
 
112
+ # 1. Prepare shifted image and resolution
113
+ init_img = input_image.resize((512, 512), Image.Resampling.LANCZOS)
114
+ shifted_init = shift_image(init_img, mode=tiling_mode)
115
 
116
+ # 2. Build Inpainting Mask vs Full Mask
117
+ if "Inpainting" in method_mode:
118
+ seam_mask = create_seam_mask(512, 512, seam_width_pct=seam_width_pct / 100.0, mode=tiling_mode)
 
 
 
 
119
  else:
120
+ # Full mask regenerates entire canvas
121
+ seam_mask = Image.new("L", (512, 512), 255)
122
 
123
+ user_steps = max(10, int(steps))
124
+ full_prompt = prompt + ", seamless tileable game asset, detailed continuous pattern, high quality"
125
+ negative_prompt = "seam line, border, frame, edge line, blur, watermark, distortion"
126
 
127
+ # 3. Perform AI Inpainting
128
+ inpainted_result = pipeline(
129
  prompt=full_prompt,
130
  negative_prompt=negative_prompt,
131
+ image=shifted_init,
132
+ mask_image=seam_mask,
133
+ num_inference_steps=user_steps,
134
  guidance_scale=7.5,
135
  generator=generator
136
  ).images[0]
137
 
138
+ # 4. Upscale to target resolution (up to 8K)
139
+ final_img = inpainted_result.resize((res_val, res_val), Image.Resampling.LANCZOS)
140
  tiled_preview = create_tiled_preview(final_img, mode=tiling_mode)
141
 
142
+ # 5. Export 8-bit UT99 Formats (.PCX / .BMP)
143
  temp_dir = tempfile.mkdtemp()
144
  pcx_path = os.path.join(temp_dir, f"ut99_texture_{res_val}.pcx")
145
  bmp_path = os.path.join(temp_dir, f"ut99_texture_{res_val}.bmp")
 
154
  final_img.save(bmp_path, format="BMP")
155
  display_img = final_img
156
 
157
+ status_msg = f"✅ Success | Method: {method_mode} | Seed: {actual_seed} | Output: {res_val}x{res_val}"
158
  return display_img, tiled_preview, pcx_path, bmp_path, status_msg
159
 
160
  # Gradio Interface
161
  with gr.Blocks(title="UT99 ZeroGPU AI Texture Forge") as demo:
162
  gr.Markdown("# ⚡ UT99 ZeroGPU AI Texture Forge")
163
+ gr.Markdown("Generate seamless 8-bit UT99 textures using Smart Seam Inpainting or Full AI Synthesis.")
164
 
165
  with gr.Row():
166
  with gr.Column():
167
  input_img = gr.Image(type="pil", label="Upload Source Photo")
168
+
169
+ method_mode = gr.Radio(
170
+ choices=["⚡ Smart Seam Inpainting (Preserves Photo)", "🎨 Full Canvas Generation"],
171
+ value="⚡ Smart Seam Inpainting (Preserves Photo)",
172
+ label="Generation Method"
173
+ )
174
+
175
  tiling_mode = gr.Radio(
176
  choices=["Horizontal Only (Landscapes/City Facades)", "Full 2D (Both Axes)"],
177
  value="Horizontal Only (Landscapes/City Facades)",
 
179
  )
180
 
181
  prompt = gr.Textbox(
182
+ value="Mountain ridge horizon panorama, detailed rocky landscape texture",
183
+ label="AI Inpainting Prompt (Describe how the seam should blend)"
184
+ )
185
+
186
+ seam_width_pct = gr.Slider(
187
+ minimum=10, maximum=50, value=25, step=5,
188
+ label="Inpaint Seam Width (% of image centered over join)"
189
  )
190
 
191
  with gr.Row():
192
+ steps = gr.Slider(minimum=10, maximum=40, value=20, step=1, label="Denoising Steps")
193
  seed_val = gr.Number(value=-1, precision=0, label="Seed (-1 = Random)")
194
 
 
 
 
 
 
195
  resolution = gr.Dropdown(
196
  choices=[
197
  "128x128",
 
211
  label="Convert to 8-Bit / 256 Colors (.PCX / .BMP)"
212
  )
213
 
214
+ btn = gr.Button("🚀 Generate AI Inpainted Texture", variant="primary")
215
 
216
  with gr.Column():
217
  output_status = gr.Textbox(label="Status / Seed", interactive=False)
 
224
 
225
  btn.click(
226
  fn=generate_ai_texture,
227
+ inputs=[
228
+ input_img, prompt, method_mode, tiling_mode,
229
+ seam_width_pct, steps, seed_val, resolution, ut99_quantize
230
+ ],
231
  outputs=[output_texture, output_preview, pcx_file, bmp_file, output_status]
232
  )
233