Spaces:
Running on Zero
Running on Zero
Update app.py
#1
by Matejstein - opened
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
|
| 31 |
|
| 32 |
-
|
|
|
|
| 33 |
|
| 34 |
-
def
|
| 35 |
-
global
|
| 36 |
-
if
|
| 37 |
-
|
| 38 |
-
"runwayml/stable-diffusion-
|
| 39 |
torch_dtype=torch.float16,
|
| 40 |
safety_checker=None
|
| 41 |
)
|
| 42 |
-
|
| 43 |
-
return
|
| 44 |
|
| 45 |
-
def
|
| 46 |
-
"""
|
| 47 |
-
|
| 48 |
-
|
| 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 |
-
|
| 67 |
-
x = np.linspace(-1, 1, w)
|
| 68 |
-
xx, yy = np.meshgrid(x, y)
|
| 69 |
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
|
| 74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
|
| 76 |
-
|
| 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,
|
| 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 =
|
| 110 |
pipeline.to("cuda")
|
| 111 |
generator = torch.Generator("cuda").manual_seed(actual_seed)
|
| 112 |
|
| 113 |
-
# 1.
|
| 114 |
-
|
| 115 |
-
|
| 116 |
|
| 117 |
-
# 2.
|
| 118 |
-
|
| 119 |
-
|
| 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 |
-
|
|
|
|
| 126 |
|
| 127 |
-
|
| 128 |
-
|
|
|
|
| 129 |
|
| 130 |
-
# 3.
|
| 131 |
-
|
| 132 |
prompt=full_prompt,
|
| 133 |
negative_prompt=negative_prompt,
|
| 134 |
-
image=
|
| 135 |
-
|
| 136 |
-
num_inference_steps=
|
| 137 |
guidance_scale=7.5,
|
| 138 |
generator=generator
|
| 139 |
).images[0]
|
| 140 |
|
| 141 |
-
# 4. Upscale to
|
| 142 |
-
final_img =
|
| 143 |
tiled_preview = create_tiled_preview(final_img, mode=tiling_mode)
|
| 144 |
|
| 145 |
-
# 5. Export 8-bit UT99
|
| 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 |
|
| 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
|
| 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="
|
| 180 |
-
label="AI Prompt"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
)
|
| 182 |
|
| 183 |
with gr.Row():
|
| 184 |
-
steps = gr.Slider(minimum=
|
| 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
|
| 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=[
|
|
|
|
|
|
|
|
|
|
| 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 |
|