prajwaluppoor commited on
Commit
42ec1b6
·
1 Parent(s): 89f403e

fix: ZeroGPU spaces decorator + force rebuild

Browse files
Files changed (3) hide show
  1. .gitattributes +4 -35
  2. app.py +159 -155
  3. requirements.txt +1 -0
.gitattributes CHANGED
@@ -1,35 +1,4 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
1
+ * text=auto eol=lf
2
+ *.py text eol=lf
3
+ *.txt text eol=lf
4
+ *.md text eol=lf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app.py CHANGED
@@ -1,24 +1,33 @@
 
 
 
 
1
  import gradio as gr
 
 
2
  import numpy as np
3
  from PIL import Image
4
  import cv2
5
  import torch
6
- from diffusers import StableDiffusionPipeline, DPMSolverMultistepScheduler
7
  import zipfile
8
  import io
9
  import tempfile
10
- import spaces # Required for HF ZeroGPU
11
 
12
- # ─── Model Loading ────────────────────────────────────────────────────────────
13
- # On ZeroGPU, CUDA is allocated per-request via @spaces.GPU
14
- # Do NOT call torch.cuda.is_available() or .to("cuda") at module import time
 
 
 
 
15
 
 
16
  pipe = None
17
 
18
  def load_model():
19
  global pipe
20
  if pipe is None:
21
- print("Loading Stable Diffusion model...")
22
  pipe = StableDiffusionPipeline.from_pretrained(
23
  "runwayml/stable-diffusion-v1-5",
24
  torch_dtype=torch.float16,
@@ -27,67 +36,63 @@ def load_model():
27
  pipe.scheduler = DPMSolverMultistepScheduler.from_config(pipe.scheduler.config)
28
  pipe = pipe.to("cuda")
29
  pipe.enable_attention_slicing()
30
- print("Model loaded.")
31
  return pipe
32
 
33
- # ─── PBR Map Generation (CPU no GPU needed) ─────────────────────────────────
34
 
35
  def make_seamless(img: Image.Image) -> Image.Image:
36
  arr = np.array(img)
37
  h, w = arr.shape[:2]
38
  result = arr.copy()
39
- blend_size = min(h, w) // 4
40
- for x in range(blend_size):
41
- alpha = x / blend_size
42
- result[:, x] = (arr[:, x] * alpha + arr[:, w - blend_size + x] * (1 - alpha)).astype(np.uint8)
43
- for y in range(blend_size):
44
- alpha = y / blend_size
45
- result[y, :] = (result[y, :] * alpha + result[h - blend_size + y, :] * (1 - alpha)).astype(np.uint8)
46
  return Image.fromarray(result)
47
 
48
- def generate_normal_map(albedo: Image.Image) -> Image.Image:
49
- gray = cv2.GaussianBlur(np.array(albedo.convert("L")).astype(np.float32) / 255.0, (3, 3), 0)
50
- strength = 4.0
51
- dx = cv2.Sobel(gray, cv2.CV_32F, 1, 0, ksize=3) * strength
52
- dy = cv2.Sobel(gray, cv2.CV_32F, 0, 1, ksize=3) * strength
53
  dz = np.ones_like(dx)
54
- length = np.sqrt(dx**2 + dy**2 + dz**2)
55
- normal = np.stack([
56
- ((-dx / length + 1) / 2 * 255).astype(np.uint8),
57
- ((-dy / length + 1) / 2 * 255).astype(np.uint8),
58
- ((dz / length) * 255).astype(np.uint8),
59
- ], axis=-1)
60
- return Image.fromarray(normal, mode="RGB")
61
-
62
- def generate_roughness_map(albedo: Image.Image) -> Image.Image:
63
- gray = np.array(albedo.convert("L")).astype(np.float32) / 255.0
64
- roughness = (np.abs(gray - cv2.GaussianBlur(gray, (5, 5), 0)) * 8).clip(0.2, 1.0)
65
- return Image.fromarray((roughness * 255).astype(np.uint8), mode="L")
66
-
67
- def generate_metallic_map(albedo: Image.Image) -> Image.Image:
68
- rgb = np.array(albedo).astype(np.float32) / 255.0
69
- hsv = cv2.cvtColor(rgb, cv2.COLOR_RGB2HSV)
70
- metallic = (hsv[:, :, 2] * (1.0 - hsv[:, :, 1]) * 0.6).clip(0, 1)
71
- return Image.fromarray((metallic * 255).astype(np.uint8), mode="L")
72
-
73
- def generate_ao_map(albedo: Image.Image) -> Image.Image:
74
- gray = np.array(albedo.convert("L")).astype(np.float32) / 255.0
75
- ao = (gray / (cv2.GaussianBlur(gray, (31, 31), 0) + 0.01)).clip(0, 1)
76
  ao = cv2.normalize(ao, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)
77
  return Image.fromarray((ao.astype(np.float32) * 0.85 + 20).clip(0, 255).astype(np.uint8), mode="L")
78
 
79
- def generate_height_map(albedo: Image.Image) -> Image.Image:
80
- gray = cv2.GaussianBlur(np.array(albedo.convert("L")), (3, 3), 0)
81
- clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
82
- return Image.fromarray(clahe.apply(gray), mode="L")
83
 
84
- def build_pbr_maps(albedo: Image.Image, seamless: bool):
85
  if seamless:
86
  albedo = make_seamless(albedo)
87
- return albedo, generate_normal_map(albedo), generate_roughness_map(albedo), \
88
- generate_metallic_map(albedo), generate_ao_map(albedo), generate_height_map(albedo)
89
 
90
- def pack_zip(maps: dict) -> str:
91
  tmp = tempfile.NamedTemporaryFile(suffix=".zip", delete=False)
92
  with zipfile.ZipFile(tmp.name, "w", zipfile.ZIP_DEFLATED) as zf:
93
  for name, img in maps.items():
@@ -96,145 +101,144 @@ def pack_zip(maps: dict) -> str:
96
  zf.writestr(f"{name}.png", buf.getvalue())
97
  return tmp.name
98
 
99
- # ─── Generation Pipelines ─────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
100
 
101
  @spaces.GPU(duration=120)
102
- def generate_from_prompt(prompt, negative_prompt, style_preset, resolution, steps, guidance, seamless, seed):
103
- style_map = {
104
- "None": "", "Stone / Rock": "stone texture, rocky surface, seamless",
105
- "Wood": "wood grain texture, natural planks, seamless",
106
- "Metal": "brushed metal surface, industrial, seamless",
107
- "Fabric / Cloth": "woven fabric texture, textile, seamless",
108
- "Concrete": "concrete cement surface, urban, seamless",
109
- "Brick": "brick wall texture, masonry, seamless",
110
- "Ground / Dirt": "dirt soil ground texture, natural, seamless",
111
- "Sci-Fi / Tech": "sci-fi futuristic panel texture, seamless",
112
- }
113
- full_prompt = f"{prompt}, {style_map.get(style_preset,'')}, seamless texture, top-down, PBR material, flat lighting, photorealistic".strip(", ")
114
  neg = f"{negative_prompt}, shadows, 3d render, person, face, vignette"
115
- generator = torch.Generator(device="cuda").manual_seed(int(seed)) if seed >= 0 else None
116
-
117
- model = load_model()
118
- albedo = model(
119
  prompt=full_prompt, negative_prompt=neg,
120
  width=int(resolution), height=int(resolution),
121
- num_inference_steps=int(steps), guidance_scale=guidance,
122
- generator=generator,
123
  ).images[0]
 
 
124
 
125
- a, n, r, m, ao, h = build_pbr_maps(albedo, seamless)
126
- zip_path = pack_zip({"T_Albedo": a, "T_Normal": n, "T_Roughness": r, "T_Metallic": m, "T_AO": ao, "T_Height": h})
127
- return a, n, r, m, ao, h, zip_path
128
-
129
- def generate_from_image(uploaded_img, seamless):
130
- if uploaded_img is None:
131
- raise gr.Error("Please upload an image first.")
132
- a, n, r, m, ao, h = build_pbr_maps(uploaded_img, seamless)
133
- zip_path = pack_zip({"T_Albedo": a, "T_Normal": n, "T_Roughness": r, "T_Metallic": m, "T_AO": ao, "T_Height": h})
134
- return a, n, r, m, ao, h, zip_path
135
 
136
- # ─── Gradio UI ────────────────────────────────────────────────────────────────
137
 
138
- STYLE_CHOICES = ["None","Stone / Rock","Wood","Metal","Fabric / Cloth","Concrete","Brick","Ground / Dirt","Sci-Fi / Tech"]
139
- RES_CHOICES = [256, 512, 768, 1024]
140
 
141
  css = """
142
  @import url('https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Syne:wght@400;700;800&display=swap');
143
- :root { --bg:#0a0a0f; --surface:#12121a; --border:#2a2a3a; --accent:#7c6af7; --accent2:#f7a26a; --text:#e8e8f0; --muted:#6a6a8a; }
144
- body, .gradio-container { background:var(--bg) !important; font-family:'Syne',sans-serif !important; color:var(--text) !important; }
145
- h1,h2,h3 { font-family:'Syne',sans-serif !important; }
146
- code { font-family:'Space Mono',monospace !important; }
147
- .gr-button-primary { background:linear-gradient(135deg,var(--accent),#a56bf7) !important; border:none !important; font-family:'Syne',sans-serif !important; font-weight:700 !important; border-radius:8px !important; }
148
- label { color:var(--muted) !important; font-size:0.8rem !important; letter-spacing:0.08em !important; text-transform:uppercase !important; }
149
  """
150
 
151
- with gr.Blocks(css=css, theme=gr.themes.Base(), title="TextureForge — PBR Texture Generator") as demo:
152
-
153
  gr.HTML("""
154
- <div style="text-align:center;padding:2rem 0 1rem;">
155
- <p style="font-family:'Syne',sans-serif;font-size:0.75rem;letter-spacing:0.3em;color:#7c6af7;text-transform:uppercase;margin-bottom:0.5rem;">PBR MATERIAL SYSTEM</p>
156
- <h1 style="font-family:'Syne',sans-serif;font-size:3rem;font-weight:800;margin:0;background:linear-gradient(135deg,#e8e8f0,#7c6af7);-webkit-background-clip:text;-webkit-text-fill-color:transparent;">TextureForge</h1>
157
- <p style="color:#6a6a8a;font-family:'Space Mono',monospace;font-size:0.8rem;margin-top:0.5rem;">Generate full PBR map sets · Ready for Unity3D</p>
158
- </div>
159
- """)
160
 
161
  with gr.Tabs():
 
162
  with gr.Tab("Text to Texture"):
163
  with gr.Row():
164
  with gr.Column(scale=1):
165
- prompt_in = gr.Textbox(label="Texture Description", placeholder="weathered oak floorboards with visible grain and knots...", lines=3)
166
- neg_in = gr.Textbox(label="Negative Prompt", lines=2, value="blurry, low quality, watermark, text, 3d render")
167
- style_in = gr.Dropdown(label="Style Preset", choices=STYLE_CHOICES, value="None")
168
  with gr.Row():
169
- res_in = gr.Dropdown(label="Resolution", choices=RES_CHOICES, value=512)
170
- steps_in = gr.Slider(label="Steps", minimum=15, maximum=50, step=1, value=25)
171
  with gr.Row():
172
- guidance_in = gr.Slider(label="Guidance Scale", minimum=3.0, maximum=15.0, step=0.5, value=7.5)
173
- seed_in = gr.Number(label="Seed (-1 = random)", value=-1)
174
- seamless_in = gr.Checkbox(label="Make Seamless / Tileable", value=True)
175
- gen_btn = gr.Button("Generate PBR Maps", variant="primary", size="lg")
176
  with gr.Column(scale=2):
177
  with gr.Row():
178
- out_albedo = gr.Image(label="Albedo (T_Albedo)", type="pil")
179
- out_normal = gr.Image(label="Normal (T_Normal)", type="pil")
180
- out_roughness = gr.Image(label="Roughness (T_Roughness)", type="pil")
181
  with gr.Row():
182
- out_metallic = gr.Image(label="Metallic (T_Metallic)", type="pil")
183
- out_ao = gr.Image(label="AO (T_AO)", type="pil")
184
- out_height = gr.Image(label="Height (T_Height)", type="pil")
185
- dl_zip = gr.File(label="Download All Maps (ZIP — Unity-ready)", file_types=[".zip"])
186
- gen_btn.click(fn=generate_from_prompt,
187
- inputs=[prompt_in,neg_in,style_in,res_in,steps_in,guidance_in,seamless_in,seed_in],
188
- outputs=[out_albedo,out_normal,out_roughness,out_metallic,out_ao,out_height,dl_zip])
189
-
 
190
  with gr.Tab("Image to PBR Maps"):
191
  with gr.Row():
192
  with gr.Column(scale=1):
193
- img_in = gr.Image(label="Upload Albedo / Base Texture", type="pil")
194
- seamless_img = gr.Checkbox(label="Make Seamless / Tileable", value=True)
195
- img_btn = gr.Button("Extract PBR Maps", variant="primary", size="lg")
196
- gr.HTML("""<div style="margin-top:1rem;padding:1rem;background:#0d0d14;border:1px solid #2a2a3a;border-radius:8px;">
197
- <p style="color:#6a6a8a;font-family:'Space Mono',monospace;font-size:0.7rem;line-height:1.7;margin:0;">
198
- <span style="color:#f7a26a;">TIP:</span> Upload any real-world surface photo. Works best with flat, evenly-lit top-down shots.<br><br>
199
- <span style="color:#7c6af7;">Unity naming:</span> T_Albedo · T_Normal · T_Roughness · T_Metallic · T_AO · T_Height</p></div>""")
 
200
  with gr.Column(scale=2):
201
  with gr.Row():
202
- out2_albedo = gr.Image(label="Albedo (T_Albedo)", type="pil")
203
- out2_normal = gr.Image(label="Normal (T_Normal)", type="pil")
204
- out2_roughness = gr.Image(label="Roughness (T_Roughness)", type="pil")
205
  with gr.Row():
206
- out2_metallic = gr.Image(label="Metallic (T_Metallic)", type="pil")
207
- out2_ao = gr.Image(label="AO (T_AO)", type="pil")
208
- out2_height = gr.Image(label="Height (T_Height)", type="pil")
209
- dl_zip2 = gr.File(label="Download All Maps (ZIP — Unity-ready)", file_types=[".zip"])
210
- img_btn.click(fn=generate_from_image, inputs=[img_in,seamless_img],
211
- outputs=[out2_albedo,out2_normal,out2_roughness,out2_metallic,out2_ao,out2_height,dl_zip2])
212
-
 
213
  with gr.Tab("Unity3D Setup Guide"):
214
  gr.HTML("""
215
- <div style="max-width:780px;margin:0 auto;padding:1.5rem;font-family:'Space Mono',monospace;font-size:0.78rem;line-height:1.9;color:#c0c0d8;">
216
- <h2 style="font-family:'Syne',sans-serif;color:#7c6af7;font-size:1.4rem;">Using Generated Textures in Unity3D</h2>
217
- <h3 style="color:#f7a26a;">1. Extract the ZIP</h3>
218
- <p>Unzip into <code style="background:#1a1a2e;padding:2px 6px;border-radius:4px;">Assets/Textures/MaterialName/</code></p>
219
- <h3 style="color:#f7a26a;">2. Import Settings per map</h3>
220
- <table style="width:100%;border-collapse:collapse;margin-bottom:1rem;">
221
- <tr style="border-bottom:1px solid #2a2a3a;"><th style="text-align:left;padding:6px;color:#7c6af7;">File</th><th style="padding:6px;color:#7c6af7;">Texture Type</th><th style="padding:6px;color:#7c6af7;">sRGB</th></tr>
222
- <tr style="border-bottom:1px solid #1a1a2a;"><td style="padding:5px;">T_Albedo</td><td style="padding:5px;">Default</td><td style="padding:5px;">ON</td></tr>
223
- <tr style="border-bottom:1px solid #1a1a2a;"><td style="padding:5px;">T_Normal</td><td style="padding:5px;">Normal Map</td><td style="padding:5px;">OFF</td></tr>
224
- <tr><td style="padding:5px;">T_Roughness, T_Metallic, T_AO, T_Height</td><td style="padding:5px;">Default</td><td style="padding:5px;">OFF</td></tr>
225
- </table>
226
- <h3 style="color:#f7a26a;">3. URP/Lit Material</h3>
227
- <p>Shader: <code style="background:#1a1a2e;padding:2px 6px;border-radius:4px;">Universal Render Pipeline/Lit</code><br>
228
- Base Map=T_Albedo | Normal=T_Normal | Metallic=T_Metallic | Occlusion=T_AO</p>
229
- <h3 style="color:#f7a26a;">4. Roughness Smoothness</h3>
230
- <p>Unity uses Smoothness (= 1 - Roughness). Use the included <b>TextureForgeHDRPPacker</b> Unity Editor script to pack all channels into a Mask Map automatically.</p>
231
- <div style="margin-top:1.5rem;padding:1rem;background:#0d0d14;border-left:3px solid #7c6af7;border-radius:0 8px 8px 0;">
232
- <p style="margin:0;color:#7c6af7;">Pro Tip — HDRP Mask Map</p>
233
- <p style="margin:0.4rem 0 0;color:#a0a0c0;">Pack R=Metallic, G=AO, B=0, A=Smoothness using <b>Tools > TextureForge > Pack HDRP Mask Map</b> in the Unity Editor.</p>
234
- </div>
235
  </div>""")
236
 
237
- gr.HTML("""<div style="text-align:center;padding:1.5rem;color:#3a3a5a;font-family:'Space Mono',monospace;font-size:0.65rem;">
238
- TextureForge · Built for Unity3D · Powered by Stable Diffusion + OpenCV</div>""")
239
 
 
240
  demo.launch()
 
1
+ import sys
2
+ print("=== TextureForge: Python started ===", flush=True)
3
+ print(f"Python version: {sys.version}", flush=True)
4
+
5
  import gradio as gr
6
+ print(f"Gradio version: {gr.__version__}", flush=True)
7
+
8
  import numpy as np
9
  from PIL import Image
10
  import cv2
11
  import torch
 
12
  import zipfile
13
  import io
14
  import tempfile
 
15
 
16
+ print("Core imports OK", flush=True)
17
+
18
+ import spaces
19
+ print("spaces import OK", flush=True)
20
+
21
+ from diffusers import StableDiffusionPipeline, DPMSolverMultistepScheduler
22
+ print("diffusers import OK", flush=True)
23
 
24
+ # ─── Model (lazy, loaded only inside @spaces.GPU context) ────────────────────
25
  pipe = None
26
 
27
  def load_model():
28
  global pipe
29
  if pipe is None:
30
+ print("Loading SD model...", flush=True)
31
  pipe = StableDiffusionPipeline.from_pretrained(
32
  "runwayml/stable-diffusion-v1-5",
33
  torch_dtype=torch.float16,
 
36
  pipe.scheduler = DPMSolverMultistepScheduler.from_config(pipe.scheduler.config)
37
  pipe = pipe.to("cuda")
38
  pipe.enable_attention_slicing()
39
+ print("SD model loaded.", flush=True)
40
  return pipe
41
 
42
+ # ─── PBR Map Generation (pure CPU / OpenCV) ─────────────────────────────────
43
 
44
  def make_seamless(img: Image.Image) -> Image.Image:
45
  arr = np.array(img)
46
  h, w = arr.shape[:2]
47
  result = arr.copy()
48
+ blend = min(h, w) // 4
49
+ for x in range(blend):
50
+ a = x / blend
51
+ result[:, x] = (arr[:, x] * a + arr[:, w - blend + x] * (1 - a)).astype(np.uint8)
52
+ for y in range(blend):
53
+ a = y / blend
54
+ result[y, :] = (result[y, :] * a + result[h - blend + y, :] * (1 - a)).astype(np.uint8)
55
  return Image.fromarray(result)
56
 
57
+ def gen_normal(img: Image.Image) -> Image.Image:
58
+ gray = cv2.GaussianBlur(np.array(img.convert("L")).astype(np.float32) / 255.0, (3, 3), 0)
59
+ dx = cv2.Sobel(gray, cv2.CV_32F, 1, 0, ksize=3) * 4.0
60
+ dy = cv2.Sobel(gray, cv2.CV_32F, 0, 1, ksize=3) * 4.0
 
61
  dz = np.ones_like(dx)
62
+ L = np.sqrt(dx**2 + dy**2 + dz**2)
63
+ return Image.fromarray(np.stack([
64
+ ((-dx/L + 1)/2*255).astype(np.uint8),
65
+ ((-dy/L + 1)/2*255).astype(np.uint8),
66
+ ((dz/L)*255).astype(np.uint8),
67
+ ], axis=-1), mode="RGB")
68
+
69
+ def gen_roughness(img: Image.Image) -> Image.Image:
70
+ g = np.array(img.convert("L")).astype(np.float32) / 255.0
71
+ r = (np.abs(g - cv2.GaussianBlur(g, (5,5), 0)) * 8).clip(0.2, 1.0)
72
+ return Image.fromarray((r * 255).astype(np.uint8), mode="L")
73
+
74
+ def gen_metallic(img: Image.Image) -> Image.Image:
75
+ hsv = cv2.cvtColor(np.array(img).astype(np.float32) / 255.0, cv2.COLOR_RGB2HSV)
76
+ m = (hsv[:,:,2] * (1.0 - hsv[:,:,1]) * 0.6).clip(0, 1)
77
+ return Image.fromarray((m * 255).astype(np.uint8), mode="L")
78
+
79
+ def gen_ao(img: Image.Image) -> Image.Image:
80
+ g = np.array(img.convert("L")).astype(np.float32) / 255.0
81
+ ao = (g / (cv2.GaussianBlur(g, (31,31), 0) + 0.01)).clip(0, 1)
 
 
82
  ao = cv2.normalize(ao, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)
83
  return Image.fromarray((ao.astype(np.float32) * 0.85 + 20).clip(0, 255).astype(np.uint8), mode="L")
84
 
85
+ def gen_height(img: Image.Image) -> Image.Image:
86
+ g = cv2.GaussianBlur(np.array(img.convert("L")), (3,3), 0)
87
+ clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
88
+ return Image.fromarray(clahe.apply(g), mode="L")
89
 
90
+ def all_maps(albedo: Image.Image, seamless: bool):
91
  if seamless:
92
  albedo = make_seamless(albedo)
93
+ return albedo, gen_normal(albedo), gen_roughness(albedo), gen_metallic(albedo), gen_ao(albedo), gen_height(albedo)
 
94
 
95
+ def to_zip(maps: dict) -> str:
96
  tmp = tempfile.NamedTemporaryFile(suffix=".zip", delete=False)
97
  with zipfile.ZipFile(tmp.name, "w", zipfile.ZIP_DEFLATED) as zf:
98
  for name, img in maps.items():
 
101
  zf.writestr(f"{name}.png", buf.getvalue())
102
  return tmp.name
103
 
104
+ # ─── Gradio Functions ─────────────────────────────────────────────────────────
105
+
106
+ STYLE_MAP = {
107
+ "None": "",
108
+ "Stone / Rock": "stone texture, rocky surface, seamless",
109
+ "Wood": "wood grain texture, natural planks, seamless",
110
+ "Metal": "brushed metal surface, industrial, seamless",
111
+ "Fabric / Cloth": "woven fabric texture, textile, seamless",
112
+ "Concrete": "concrete cement surface, seamless",
113
+ "Brick": "brick wall, masonry, seamless",
114
+ "Ground / Dirt": "dirt soil ground, natural, seamless",
115
+ "Sci-Fi / Tech": "sci-fi futuristic panel texture, seamless",
116
+ }
117
 
118
  @spaces.GPU(duration=120)
119
+ def from_prompt(prompt, negative_prompt, style_preset, resolution, steps, guidance, seamless, seed):
120
+ full_prompt = f"{prompt}, {STYLE_MAP.get(style_preset,'')}, seamless texture, top-down, PBR, flat lighting, photorealistic".strip(", ")
 
 
 
 
 
 
 
 
 
 
121
  neg = f"{negative_prompt}, shadows, 3d render, person, face, vignette"
122
+ gen = torch.Generator(device="cuda").manual_seed(int(seed)) if seed >= 0 else None
123
+ albedo = load_model()(
 
 
124
  prompt=full_prompt, negative_prompt=neg,
125
  width=int(resolution), height=int(resolution),
126
+ num_inference_steps=int(steps), guidance_scale=guidance, generator=gen,
 
127
  ).images[0]
128
+ a, n, r, m, ao, h = all_maps(albedo, seamless)
129
+ return a, n, r, m, ao, h, to_zip({"T_Albedo":a,"T_Normal":n,"T_Roughness":r,"T_Metallic":m,"T_AO":ao,"T_Height":h})
130
 
131
+ def from_image(img, seamless):
132
+ if img is None:
133
+ raise gr.Error("Please upload an image.")
134
+ a, n, r, m, ao, h = all_maps(img, seamless)
135
+ return a, n, r, m, ao, h, to_zip({"T_Albedo":a,"T_Normal":n,"T_Roughness":r,"T_Metallic":m,"T_AO":ao,"T_Height":h})
 
 
 
 
 
136
 
137
+ # ─── UI ───────────────────────────────────────────────────────────────────────
138
 
139
+ STYLES = list(STYLE_MAP.keys())
140
+ RES = [256, 512, 768, 1024]
141
 
142
  css = """
143
  @import url('https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Syne:wght@400;700;800&display=swap');
144
+ :root{--bg:#0a0a0f;--surface:#12121a;--border:#2a2a3a;--accent:#7c6af7;--text:#e8e8f0;--muted:#6a6a8a}
145
+ body,.gradio-container{background:var(--bg)!important;font-family:'Syne',sans-serif!important;color:var(--text)!important}
146
+ h1,h2,h3{font-family:'Syne',sans-serif!important}
147
+ code{font-family:'Space Mono',monospace!important}
148
+ .gr-button-primary{background:linear-gradient(135deg,var(--accent),#a56bf7)!important;border:none!important;font-family:'Syne',sans-serif!important;font-weight:700!important;border-radius:8px!important}
149
+ label{color:var(--muted)!important;font-size:.8rem!important;letter-spacing:.08em!important;text-transform:uppercase!important}
150
  """
151
 
152
+ with gr.Blocks(css=css, theme=gr.themes.Base(), title="TextureForge") as demo:
 
153
  gr.HTML("""
154
+ <div style="text-align:center;padding:2rem 0 1rem">
155
+ <p style="font-size:.75rem;letter-spacing:.3em;color:#7c6af7;text-transform:uppercase;margin-bottom:.5rem">PBR MATERIAL SYSTEM</p>
156
+ <h1 style="font-size:3rem;font-weight:800;margin:0;background:linear-gradient(135deg,#e8e8f0,#7c6af7);-webkit-background-clip:text;-webkit-text-fill-color:transparent">TextureForge</h1>
157
+ <p style="color:#6a6a8a;font-family:'Space Mono',monospace;font-size:.8rem;margin-top:.5rem">Generate full PBR map sets · Ready for Unity3D</p>
158
+ </div>""")
 
159
 
160
  with gr.Tabs():
161
+ # Tab 1
162
  with gr.Tab("Text to Texture"):
163
  with gr.Row():
164
  with gr.Column(scale=1):
165
+ t_prompt = gr.Textbox(label="Texture Description", placeholder="weathered oak floorboards with knots...", lines=3)
166
+ t_neg = gr.Textbox(label="Negative Prompt", value="blurry, low quality, watermark, 3d render", lines=2)
167
+ t_style = gr.Dropdown(label="Style Preset", choices=STYLES, value="None")
168
  with gr.Row():
169
+ t_res = gr.Dropdown(label="Resolution", choices=RES, value=512)
170
+ t_steps = gr.Slider(label="Steps", minimum=15, maximum=50, step=1, value=25)
171
  with gr.Row():
172
+ t_guid = gr.Slider(label="Guidance Scale", minimum=3.0, maximum=15.0, step=0.5, value=7.5)
173
+ t_seed = gr.Number(label="Seed (-1=random)", value=-1)
174
+ t_seam = gr.Checkbox(label="Make Seamless / Tileable", value=True)
175
+ t_btn = gr.Button("Generate PBR Maps", variant="primary", size="lg")
176
  with gr.Column(scale=2):
177
  with gr.Row():
178
+ o_alb = gr.Image(label="Albedo · T_Albedo", type="pil")
179
+ o_nrm = gr.Image(label="Normal · T_Normal", type="pil")
180
+ o_rgh = gr.Image(label="Roughness · T_Roughness", type="pil")
181
  with gr.Row():
182
+ o_met = gr.Image(label="Metallic · T_Metallic", type="pil")
183
+ o_ao = gr.Image(label="AO · T_AO", type="pil")
184
+ o_hgt = gr.Image(label="Height · T_Height", type="pil")
185
+ o_zip = gr.File(label="Download All Maps (ZIP)", file_types=[".zip"])
186
+ t_btn.click(fn=from_prompt,
187
+ inputs=[t_prompt,t_neg,t_style,t_res,t_steps,t_guid,t_seam,t_seed],
188
+ outputs=[o_alb,o_nrm,o_rgh,o_met,o_ao,o_hgt,o_zip])
189
+
190
+ # Tab 2
191
  with gr.Tab("Image to PBR Maps"):
192
  with gr.Row():
193
  with gr.Column(scale=1):
194
+ i_img = gr.Image(label="Upload Base Texture", type="pil")
195
+ i_seam = gr.Checkbox(label="Make Seamless / Tileable", value=True)
196
+ i_btn = gr.Button("Extract PBR Maps", variant="primary", size="lg")
197
+ gr.HTML("""<div style="margin-top:1rem;padding:1rem;background:#0d0d14;border:1px solid #2a2a3a;border-radius:8px">
198
+ <p style="color:#6a6a8a;font-family:'Space Mono',monospace;font-size:.7rem;line-height:1.7;margin:0">
199
+ <span style="color:#f7a26a">TIP:</span> Upload any real surface photo works best flat-lit and top-down.<br><br>
200
+ <span style="color:#7c6af7">Unity files:</span> T_Albedo · T_Normal · T_Roughness · T_Metallic · T_AO · T_Height
201
+ </p></div>""")
202
  with gr.Column(scale=2):
203
  with gr.Row():
204
+ i_alb = gr.Image(label="Albedo · T_Albedo", type="pil")
205
+ i_nrm = gr.Image(label="Normal · T_Normal", type="pil")
206
+ i_rgh = gr.Image(label="Roughness · T_Roughness", type="pil")
207
  with gr.Row():
208
+ i_met = gr.Image(label="Metallic · T_Metallic", type="pil")
209
+ i_ao = gr.Image(label="AO · T_AO", type="pil")
210
+ i_hgt = gr.Image(label="Height · T_Height", type="pil")
211
+ i_zip = gr.File(label="Download All Maps (ZIP)", file_types=[".zip"])
212
+ i_btn.click(fn=from_image, inputs=[i_img,i_seam],
213
+ outputs=[i_alb,i_nrm,i_rgh,i_met,i_ao,i_hgt,i_zip])
214
+
215
+ # Tab 3 — Unity Guide
216
  with gr.Tab("Unity3D Setup Guide"):
217
  gr.HTML("""
218
+ <div style="max-width:780px;margin:0 auto;padding:1.5rem;font-family:'Space Mono',monospace;font-size:.78rem;line-height:1.9;color:#c0c0d8">
219
+ <h2 style="font-family:'Syne',sans-serif;color:#7c6af7;font-size:1.4rem">Using TextureForge Maps in Unity3D</h2>
220
+ <h3 style="color:#f7a26a">1. Extract the ZIP</h3>
221
+ <p>Place into <code style="background:#1a1a2e;padding:2px 6px;border-radius:4px">Assets/Textures/MaterialName/</code></p>
222
+ <h3 style="color:#f7a26a">2. Texture Import Settings</h3>
223
+ <table style="width:100%;border-collapse:collapse;margin-bottom:1rem">
224
+ <tr style="border-bottom:1px solid #2a2a3a"><th style="text-align:left;padding:6px;color:#7c6af7">File</th><th style="padding:6px;color:#7c6af7">Type</th><th style="padding:6px;color:#7c6af7">sRGB</th></tr>
225
+ <tr style="border-bottom:1px solid #1a1a2a"><td style="padding:5px">T_Albedo</td><td style="padding:5px">Default</td><td style="padding:5px">ON</td></tr>
226
+ <tr style="border-bottom:1px solid #1a1a2a"><td style="padding:5px">T_Normal</td><td style="padding:5px">Normal Map</td><td style="padding:5px">OFF</td></tr>
227
+ <tr><td style="padding:5px">T_Roughness, T_Metallic, T_AO, T_Height</td><td style="padding:5px">Default</td><td style="padding:5px">OFF</td></tr>
228
+ </table>
229
+ <h3 style="color:#f7a26a">3. Create URP/Lit Material</h3>
230
+ <p>Shader: <code style="background:#1a1a2e;padding:2px 6px;border-radius:4px">Universal Render Pipeline/Lit</code><br>
231
+ Base Map=T_Albedo | Normal Map=T_Normal | Metallic=T_Metallic | Occlusion=T_AO</p>
232
+ <h3 style="color:#f7a26a">4. Roughness to Smoothness</h3>
233
+ <p>Unity uses Smoothness (= 1 minus Roughness). Use <b>Tools > TextureForge > Pack HDRP Mask Map</b> in the Unity Editor to auto-pack channels.</p>
234
+ <div style="margin-top:1.5rem;padding:1rem;background:#0d0d14;border-left:3px solid #7c6af7;border-radius:0 8px 8px 0">
235
+ <p style="margin:0;color:#7c6af7">HDRP Mask Map</p>
236
+ <p style="margin:.4rem 0 0;color:#a0a0c0">Pack R=Metallic, G=AO, B=0, A=Smoothness using the included Unity Editor script (TextureForgeHDRPPacker).</p>
237
+ </div>
238
  </div>""")
239
 
240
+ gr.HTML("""<div style="text-align:center;padding:1.5rem;color:#3a3a5a;font-family:'Space Mono',monospace;font-size:.65rem">
241
+ TextureForge · Unity3D · Stable Diffusion + OpenCV PBR</div>""")
242
 
243
+ print("=== Launching Gradio demo ===", flush=True)
244
  demo.launch()
requirements.txt CHANGED
@@ -8,3 +8,4 @@ opencv-python-headless>=4.8.0
8
  Pillow>=10.0.0
9
  numpy>=1.24.0
10
  safetensors>=0.4.0
 
 
8
  Pillow>=10.0.0
9
  numpy>=1.24.0
10
  safetensors>=0.4.0
11
+ # rebuild-1772183179