cavargas10 commited on
Commit
3539b1a
·
verified ·
1 Parent(s): 50e4252

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +173 -143
app.py CHANGED
@@ -1,75 +1,62 @@
 
 
 
1
  import os
2
  import shlex
3
  import spaces
4
  import subprocess
5
- import time
6
- import uuid
7
- import torch
8
- import trimesh
9
- import argparse
10
- import numpy as np
11
- import gradio as gr
12
- from PIL import Image, ImageOps
13
 
14
- # --------------------------------------------------------------------------
15
- # 1. INSTALACIÓN DEL ENTORNO Y COMPILACIÓN DE EXTENSIONES
16
- # --------------------------------------------------------------------------
17
- def install_cuda_toolkit():
18
- print("Verificando e instalando CUDA Toolkit si es necesario...")
19
  CUDA_TOOLKIT_URL = "https://developer.download.nvidia.com/compute/cuda/12.4.0/local_installers/cuda_12.4.0_550.54.14_linux.run"
20
  CUDA_TOOLKIT_FILE = f"/tmp/{os.path.basename(CUDA_TOOLKIT_URL)}"
21
- if not os.path.exists("/usr/local/cuda/bin/nvcc"):
22
- subprocess.run(["wget", "-q", CUDA_TOOLKIT_URL, "-O", CUDA_TOOLKIT_FILE], check=True)
23
- subprocess.run(["chmod", "+x", CUDA_TOOLKIT_FILE], check=True)
24
- subprocess.run([CUDA_TOOLKIT_FILE, "--silent", "--toolkit"], check=True)
 
25
  else:
26
- print("CUDA Toolkit ya está instalado.")
27
 
28
  os.environ["CUDA_HOME"] = "/usr/local/cuda"
29
  os.environ["PATH"] = f"{os.environ['CUDA_HOME']}/bin:{os.environ['PATH']}"
30
  os.environ["LD_LIBRARY_PATH"] = f"{os.environ['CUDA_HOME']}/lib:{os.environ.get('LD_LIBRARY_PATH', '')}"
31
  os.environ["TORCH_CUDA_ARCH_LIST"] = "8.0;8.6"
 
 
 
 
 
 
 
 
 
32
 
33
- install_cuda_toolkit()
34
- print("--- Versiones de librerías ---")
35
- os.system("pip list | grep torch")
36
- os.system('nvcc -V')
37
- print("-----------------------------")
38
-
39
- # --- RUTAS CORREGIDAS ---
40
- # Construir rutas absolutas basadas en la ubicación del script app.py
41
- APP_DIR = os.path.dirname(os.path.abspath(__file__))
42
- PROJECT_ROOT = os.path.join(APP_DIR, "Step1X-3D")
43
- DIFFERENTIABLE_RENDERER_PATH = os.path.join(PROJECT_ROOT, "step1x3d_texture/differentiable_renderer/")
44
- CUSTOM_RASTERIZER_WHL_PATH = os.path.join(PROJECT_ROOT, "custom_rasterizer-0.1-cp310-cp310-linux_x86_64.whl")
45
-
46
- print(f"Ruta del renderizador diferenciable: {DIFFERENTIABLE_RENDERER_PATH}")
47
- print(f"Ruta del rasterizador personalizado (.whl): {CUSTOM_RASTERIZER_WHL_PATH}")
48
-
49
-
50
- print("Compilando extensión C++/CUDA para el renderizador diferenciable...")
51
- # Usamos cwd (current working directory) para ejecutar el comando en la carpeta correcta
52
- subprocess.run(
53
- "python setup.py install",
54
- shell=True, check=True, cwd=DIFFERENTIABLE_RENDERER_PATH
55
- )
56
-
57
- print("Instalando la extensión C++/CUDA para el rasterizador personalizado...")
58
- subprocess.run(
59
- shlex.split(f"pip install {CUSTOM_RASTERIZER_WHL_PATH}"),
60
- check=True
61
- )
62
- print("Compilaciones finalizadas.")
63
-
64
 
65
- # --------------------------------------------------------------------------
66
- # 2. IMPORTACIONES PRINCIPALES Y CARGA DE MODELOS
67
- # --------------------------------------------------------------------------
 
 
 
 
68
  from diffusers import ControlNetModel, StableDiffusionXLControlNetPipeline, AutoencoderKL, EulerAncestralDiscreteScheduler
69
  from step1x3d_geometry.models.pipelines.pipeline import Step1X3DGeometryPipeline
70
  from step1x3d_texture.pipelines.step1x_3d_texture_synthesis_pipeline import Step1X3DTexturePipeline
71
  from step1x3d_geometry.models.pipelines.pipeline_utils import reduce_face, remove_degenerate_face
72
 
 
 
 
 
73
  parser = argparse.ArgumentParser()
74
  parser.add_argument("--geometry_model", type=str, default="Step1X-3D-Geometry-Label-1300m")
75
  parser.add_argument("--texture_model", type=str, default="Step1X-3D-Texture")
@@ -78,40 +65,43 @@ args = parser.parse_args()
78
 
79
  os.makedirs(args.cache_dir, exist_ok=True)
80
  device = "cuda" if torch.cuda.is_available() else "cpu"
81
- print(f"Usando dispositivo: {device}")
 
 
82
 
83
- # --- Carga de Modelos Step1X-3D ---
84
- print("Cargando pipeline de geometría Step1X-3D...")
85
- geometry_model_path = os.path.join(PROJECT_ROOT) # La ruta base para from_pretrained
86
  geometry_model = Step1X3DGeometryPipeline.from_pretrained(
87
  "stepfun-ai/Step1X-3D", subfolder=args.geometry_model
88
  ).to(device)
89
- print("Pipeline de geometría cargado.")
90
 
91
- print("Cargando pipeline de texturizado Step1X-3D...")
92
- texture_model_path = os.path.join(PROJECT_ROOT) # La ruta base para from_pretrained
93
  texture_model = Step1X3DTexturePipeline.from_pretrained("stepfun-ai/Step1X-3D", subfolder=args.texture_model)
94
- print("Pipeline de texturizado cargado.")
95
-
96
 
97
- # --- Carga de Modelos de ControlNet para el pre-procesamiento ---
98
- print("Cargando modelos para el pre-procesamiento de bocetos (ControlNet)...")
99
- controlnet = ControlNetModel.from_pretrained("xinsir/controlnet-scribble-sdxl-1.0", torch_dtype=torch.float16)
100
- vae = AutoencoderKL.from_pretrained("madebyollin/sdxl-vae-fp16-fix", torch_dtype=torch.float16)
101
  pipe_control = StableDiffusionXLControlNetPipeline.from_pretrained(
102
- "sd-community/sdxl-flash", controlnet=controlnet, vae=vae, torch_dtype=torch.float16
103
  )
104
  pipe_control.scheduler = EulerAncestralDiscreteScheduler.from_config(pipe_control.scheduler.config)
105
  pipe_control.to(device)
106
- print("Modelos de ControlNet cargados y listos.")
107
 
108
 
109
- # --------------------------------------------------------------------------
110
- # 3. FUNCIONES DE BACKEND PARA CADA ETAPA
111
- # --------------------------------------------------------------------------
 
 
 
 
 
 
112
 
113
  @spaces.GPU(duration=60)
114
- def enhance_sketch(image, prompt):
115
  """
116
  Paso 0: Convierte un boceto en una imagen de alta calidad usando ControlNet.
117
  """
@@ -120,43 +110,48 @@ def enhance_sketch(image, prompt):
120
 
121
  input_image = image.convert("RGB")
122
 
 
123
  width, height = input_image.size
124
  ratio = np.sqrt(1024.0 * 1024.0 / (width * height))
125
  new_width, new_height = int(width * ratio), int(height * ratio)
126
  input_image = input_image.resize((new_width, new_height))
127
  input_image = ImageOps.invert(input_image)
128
 
129
- final_prompt = f"professional 3d model {prompt} . octane render, highly detailed, volumetric, dramatic lighting"
130
- negative_prompt = "ugly, deformed, noisy, low poly, blurry, painting"
 
 
 
131
 
132
- print(f"Mejorando boceto con prompt: '{final_prompt}'")
 
133
  output_image = pipe_control(
134
  prompt=final_prompt,
135
- negative_prompt=negative_prompt,
136
  image=input_image,
137
- num_inference_steps=20,
138
- controlnet_conditioning_scale=0.85,
139
- guidance_scale=5.0,
140
  width=new_width,
141
  height=new_height,
 
142
  ).images[0]
143
 
144
  save_name = str(uuid.uuid4())
145
  processed_image_path = f"{args.cache_dir}/{save_name}_processed.png"
146
  output_image.save(processed_image_path)
147
 
148
- print(f"Boceto mejorado y guardado en: {processed_image_path}")
149
- return processed_image_path, processed_image_path
150
 
151
  @spaces.GPU(duration=180)
152
  def generate_geometry(input_image_path, guidance_scale, inference_steps, max_facenum, symmetry, edge_type):
153
- """
154
- Paso 1: Genera la geometría a partir de una imagen.
155
- """
156
  if not input_image_path or not os.path.exists(input_image_path):
157
- raise gr.Error("Primero debes proporcionar una imagen o mejorar un boceto.")
158
 
159
- print("Iniciando generación de geometría...")
 
160
  if "Label" in args.geometry_model:
161
  symmetry_values = ["x", "asymmetry"]
162
  out = geometry_model(
@@ -175,108 +170,143 @@ def generate_geometry(input_image_path, guidance_scale, inference_steps, max_fac
175
  max_facenum=int(max_facenum),
176
  )
177
 
178
- save_name = str(uuid.uuid4())
179
- geometry_save_path = f"{args.cache_dir}/{save_name}.glb"
180
  geometry_mesh = out.mesh[0]
181
  geometry_mesh.export(geometry_save_path)
182
 
183
  torch.cuda.empty_cache()
184
- print(f"Geometría guardada en: {geometry_save_path}")
185
- return geometry_save_path, geometry_save_path
186
 
187
  @spaces.GPU(duration=120)
188
  def generate_texture(input_image_path, geometry_path):
189
- """
190
- Paso 2: Aplica la textura a una geometría existente.
191
- """
192
  if not geometry_path or not os.path.exists(geometry_path):
193
  raise gr.Error("Por favor, primero genera la geometría antes de texturizar.")
194
  if not input_image_path or not os.path.exists(input_image_path):
195
- raise gr.Error("Se necesita una imagen de entrada para el texturizado.")
196
 
197
- print(f"Iniciando texturizado para la malla: {geometry_path}")
198
  geometry_mesh = trimesh.load(geometry_path)
199
 
 
200
  geometry_mesh = remove_degenerate_face(geometry_mesh)
201
  geometry_mesh = reduce_face(geometry_mesh)
202
 
203
  textured_mesh = texture_model(input_image_path, geometry_mesh)
204
 
205
- save_name = os.path.basename(geometry_path).replace(".glb", "")
206
- textured_save_path = f"{args.cache_dir}/{save_name}-textured.glb"
207
  textured_mesh.export(textured_save_path)
208
 
209
  torch.cuda.empty_cache()
210
- print(f"Malla texturizada guardada en: {textured_save_path}")
211
  return textured_save_path
212
 
213
- # --------------------------------------------------------------------------
214
- # 4. INTERFAZ DE USUARIO CON GRADIO
215
- # --------------------------------------------------------------------------
216
 
217
- with gr.Blocks(title="Step1X-3D: Flujo de Boceto a 3D") as demo:
218
- gr.Markdown("# Step1X-3D: Flujo de Boceto a 3D")
219
- gr.Markdown("Flujo de trabajo inspirado en TRELLIS. **Paso 0 (Opcional):** Sube un boceto y un prompt para generar una imagen de referencia. **Paso 1:** Genera la geometría 3D a partir de la imagen. **Paso 2:** Genera la textura para la geometría.")
220
 
 
221
  processed_image_path_state = gr.State()
222
  geometry_path_state = gr.State()
223
 
224
  with gr.Row():
225
  with gr.Column(scale=2):
226
- gr.Markdown("### **Entrada Principal**")
227
- input_image = gr.Image(label="Sube tu boceto o imagen de referencia", type="pil", image_mode="RGBA")
228
- prompt = gr.Textbox(label="Prompt (describe tu objeto)", value="a detailed medieval sword")
229
-
230
- with gr.Accordion(label="Opciones de Generación 3D", open=True):
231
- guidance_scale = gr.Number(label="Guidance Scale (3D)", value="7.5")
232
- inference_steps = gr.Slider(label="Inference Steps (3D)", minimum=1, maximum=100, value=50, step=1)
233
- max_facenum = gr.Number(label="Max Face Num", value="400000")
234
- symmetry = gr.Radio(choices=["Symmetry", "Asymmetry"], label="Symmetry", value="Symmetry", type="index")
 
 
 
 
 
 
 
 
235
  edge_type = gr.Radio(choices=["sharp", "normal", "smooth"], label="Edge Type", value="sharp", type="value")
236
-
237
  with gr.Row():
238
- btn_enhance = gr.Button("Paso 0: Mejorar Boceto")
239
  with gr.Row():
240
- btn_geo = gr.Button("Paso 1: Generar Geometría", interactive=False)
241
- btn_tex = gr.Button("Paso 2: Generar Textura", visible=False, interactive=False)
242
-
243
- with gr.Column(scale=4):
244
- gr.Markdown("### **Resultados**")
245
- processed_image_preview = gr.Image(label="Boceto Mejorado (Entrada para 3D)", type="filepath", interactive=False)
246
- textured_preview = gr.Model3D(label="Modelo Texturizado Final", height=380, clear_color=[0.0, 0.0, 0.0, 0.0])
247
- geometry_preview = gr.Model3D(label="Modelo Geométrico", height=380, clear_color=[0.0, 0.0, 0.0, 0.0])
248
 
249
  with gr.Column(scale=1):
250
  gr.Examples(
251
  examples=[
252
- [os.path.join(PROJECT_ROOT, "examples/images/000.png"), "a toy car"],
253
- [os.path.join(PROJECT_ROOT, "examples/images/001.png"), "a red fire hydrant"],
254
- [os.path.join(PROJECT_ROOT, "examples/images/004.png"), "a detailed, ornate, golden picture frame"],
 
 
 
 
 
255
  ],
256
- inputs=[input_image, prompt],
257
- cache_examples=False
258
  )
259
 
260
  # --- Lógica de la Interfaz ---
261
 
262
- def handle_image_upload(img):
263
- if img is None:
264
- return None, gr.update(interactive=False)
265
- save_name = str(uuid.uuid4())
266
- original_path = f"{args.cache_dir}/{save_name}_original.png"
267
- img.save(original_path)
268
- return original_path, gr.update(interactive=True)
269
-
270
- input_image.upload(fn=handle_image_upload, inputs=[input_image], outputs=[processed_image_path_state, btn_geo])
271
-
272
- btn_enhance.click(fn=enhance_sketch, inputs=[input_image, prompt], outputs=[processed_image_preview, processed_image_path_state]).then(
273
- fn=lambda: gr.update(interactive=True), outputs=[btn_geo]
 
 
 
 
 
 
 
 
 
 
 
 
 
274
  )
275
 
276
- btn_geo.click(fn=generate_geometry, inputs=[processed_image_path_state, guidance_scale, inference_steps, max_facenum, symmetry, edge_type], outputs=[geometry_preview, geometry_path_state]).then(
277
- fn=lambda: {btn_tex: gr.update(visible=True, interactive=True), textured_preview: gr.update(value=None)}, outputs=[btn_tex, textured_preview]
 
 
 
 
 
 
278
  )
279
 
280
- btn_tex.click(fn=generate_texture, inputs=[processed_image_path_state, geometry_path_state], outputs=[textured_preview])
 
 
 
 
281
 
282
- demo.launch(ssr_mode=False, share=True)
 
1
+ # ==============================================================================
2
+ # 1. INSTALACIÓN DEL ENTORNO Y DEPENDENCIAS
3
+ # ==============================================================================
4
  import os
5
  import shlex
6
  import spaces
7
  import subprocess
8
+ import logging
9
+
10
+ # Configuración del logging para depuración
11
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - Step1X-3D - %(levelname)s - %(message)s')
 
 
 
 
12
 
13
+ def install_dependencies():
14
+ """Instala el toolkit de CUDA y compila las extensiones C++/CUDA necesarias."""
15
+ logging.info("Iniciando la instalación de dependencias...")
16
+
17
+ # Instalar CUDA Toolkit
18
  CUDA_TOOLKIT_URL = "https://developer.download.nvidia.com/compute/cuda/12.4.0/local_installers/cuda_12.4.0_550.54.14_linux.run"
19
  CUDA_TOOLKIT_FILE = f"/tmp/{os.path.basename(CUDA_TOOLKIT_URL)}"
20
+ if not os.path.exists("/usr/local/cuda"):
21
+ logging.info("Descargando e instalando CUDA Toolkit...")
22
+ subprocess.call(["wget", "-q", CUDA_TOOLKIT_URL, "-O", CUDA_TOOLKIT_FILE])
23
+ subprocess.call(["chmod", "+x", CUDA_TOOLKIT_FILE])
24
+ subprocess.call([CUDA_TOOLKIT_FILE, "--silent", "--toolkit"])
25
  else:
26
+ logging.info("CUDA Toolkit ya está instalado.")
27
 
28
  os.environ["CUDA_HOME"] = "/usr/local/cuda"
29
  os.environ["PATH"] = f"{os.environ['CUDA_HOME']}/bin:{os.environ['PATH']}"
30
  os.environ["LD_LIBRARY_PATH"] = f"{os.environ['CUDA_HOME']}/lib:{os.environ.get('LD_LIBRARY_PATH', '')}"
31
  os.environ["TORCH_CUDA_ARCH_LIST"] = "8.0;8.6"
32
+
33
+ # Compilar extensiones personalizadas
34
+ logging.info("Compilando extensiones de renderizado...")
35
+ renderer_path = "/home/user/app/step1x3d_texture/differentiable_renderer/"
36
+ subprocess.run(f"cd {renderer_path} && python setup.py install", shell=True, check=True)
37
+ subprocess.run(shlex.split("pip install custom_rasterizer-0.1-cp310-cp310-linux_x86_64.whl"), check=True)
38
+
39
+ logging.info("Instalación completada.")
40
+ os.system('nvcc -V')
41
 
42
+ install_dependencies()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
 
44
+ import uuid
45
+ import torch
46
+ import trimesh
47
+ import argparse
48
+ import numpy as np
49
+ import gradio as gr
50
+ from PIL import Image, ImageOps
51
  from diffusers import ControlNetModel, StableDiffusionXLControlNetPipeline, AutoencoderKL, EulerAncestralDiscreteScheduler
52
  from step1x3d_geometry.models.pipelines.pipeline import Step1X3DGeometryPipeline
53
  from step1x3d_texture.pipelines.step1x_3d_texture_synthesis_pipeline import Step1X3DTexturePipeline
54
  from step1x3d_geometry.models.pipelines.pipeline_utils import reduce_face, remove_degenerate_face
55
 
56
+ # ==============================================================================
57
+ # 2. CONFIGURACIÓN Y CARGA DE MODELOS
58
+ # ==============================================================================
59
+
60
  parser = argparse.ArgumentParser()
61
  parser.add_argument("--geometry_model", type=str, default="Step1X-3D-Geometry-Label-1300m")
62
  parser.add_argument("--texture_model", type=str, default="Step1X-3D-Texture")
 
65
 
66
  os.makedirs(args.cache_dir, exist_ok=True)
67
  device = "cuda" if torch.cuda.is_available() else "cpu"
68
+ torch_dtype = torch.float16
69
+
70
+ logging.info("Cargando modelos... Este proceso puede tardar varios minutos.")
71
 
72
+ # Carga de modelos de Step1X-3D
73
+ logging.info(f"Cargando modelo de geometría: {args.geometry_model}")
 
74
  geometry_model = Step1X3DGeometryPipeline.from_pretrained(
75
  "stepfun-ai/Step1X-3D", subfolder=args.geometry_model
76
  ).to(device)
 
77
 
78
+ logging.info(f"Cargando modelo de textura: {args.texture_model}")
 
79
  texture_model = Step1X3DTexturePipeline.from_pretrained("stepfun-ai/Step1X-3D", subfolder=args.texture_model)
 
 
80
 
81
+ # Carga de modelos de ControlNet para el pre-procesamiento de bocetos
82
+ logging.info("Cargando modelos para el pre-procesamiento de bocetos (SDXL + ControlNet)...")
83
+ controlnet = ControlNetModel.from_pretrained("xinsir/controlnet-scribble-sdxl-1.0", torch_dtype=torch_dtype)
84
+ vae = AutoencoderKL.from_pretrained("madebyollin/sdxl-vae-fp16-fix", torch_dtype=torch_dtype)
85
  pipe_control = StableDiffusionXLControlNetPipeline.from_pretrained(
86
+ "sd-community/sdxl-flash", controlnet=controlnet, vae=vae, torch_dtype=torch_dtype
87
  )
88
  pipe_control.scheduler = EulerAncestralDiscreteScheduler.from_config(pipe_control.scheduler.config)
89
  pipe_control.to(device)
90
+ logging.info("Todos los modelos han sido cargados correctamente.")
91
 
92
 
93
+ # ==============================================================================
94
+ # 3. FUNCIONES DE GENERACIÓN POR PASOS
95
+ # ==============================================================================
96
+
97
+ def apply_3d_style(prompt: str) -> tuple[str, str]:
98
+ """Aplica el estilo '3D Model' por defecto al prompt."""
99
+ style_prompt = "professional 3d model {prompt} . octane render, highly detailed, volumetric, dramatic lighting"
100
+ negative_prompt = "ugly, deformed, noisy, low poly, blurry, painting"
101
+ return style_prompt.replace("{prompt}", prompt), negative_prompt
102
 
103
  @spaces.GPU(duration=60)
104
+ def process_sketch(image, prompt, negative_prompt, guidance_scale, num_steps, controlnet_scale):
105
  """
106
  Paso 0: Convierte un boceto en una imagen de alta calidad usando ControlNet.
107
  """
 
110
 
111
  input_image = image.convert("RGB")
112
 
113
+ # Pre-procesamiento de la imagen de entrada (invertir y redimensionar)
114
  width, height = input_image.size
115
  ratio = np.sqrt(1024.0 * 1024.0 / (width * height))
116
  new_width, new_height = int(width * ratio), int(height * ratio)
117
  input_image = input_image.resize((new_width, new_height))
118
  input_image = ImageOps.invert(input_image)
119
 
120
+ final_prompt, final_negative_prompt = apply_3d_style(prompt)
121
+ if negative_prompt: # Añadir negativo del usuario si existe
122
+ final_negative_prompt = f"{final_negative_prompt}, {negative_prompt}"
123
+
124
+ logging.info(f"Mejorando boceto con prompt: '{final_prompt}'")
125
 
126
+ generator = torch.Generator(device=device).manual_seed(np.random.randint(0, 2**32 - 1))
127
+
128
  output_image = pipe_control(
129
  prompt=final_prompt,
130
+ negative_prompt=final_negative_prompt,
131
  image=input_image,
132
+ num_inference_steps=int(num_steps),
133
+ controlnet_conditioning_scale=float(controlnet_scale),
134
+ guidance_scale=float(guidance_scale),
135
  width=new_width,
136
  height=new_height,
137
+ generator=generator,
138
  ).images[0]
139
 
140
  save_name = str(uuid.uuid4())
141
  processed_image_path = f"{args.cache_dir}/{save_name}_processed.png"
142
  output_image.save(processed_image_path)
143
 
144
+ logging.info(f"Boceto mejorado y guardado en: {processed_image_path}")
145
+ return processed_image_path
146
 
147
  @spaces.GPU(duration=180)
148
  def generate_geometry(input_image_path, guidance_scale, inference_steps, max_facenum, symmetry, edge_type):
149
+ """Paso 1: Genera la geometría a partir de la imagen procesada."""
 
 
150
  if not input_image_path or not os.path.exists(input_image_path):
151
+ raise gr.Error("Primero debes procesar un boceto o proporcionar una imagen de entrada válida.")
152
 
153
+ logging.info(f"Iniciando generación de geometría desde: {os.path.basename(input_image_path)}")
154
+
155
  if "Label" in args.geometry_model:
156
  symmetry_values = ["x", "asymmetry"]
157
  out = geometry_model(
 
170
  max_facenum=int(max_facenum),
171
  )
172
 
173
+ save_name = os.path.basename(input_image_path).replace("_processed.png", "")
174
+ geometry_save_path = f"{args.cache_dir}/{save_name}_geometry.glb"
175
  geometry_mesh = out.mesh[0]
176
  geometry_mesh.export(geometry_save_path)
177
 
178
  torch.cuda.empty_cache()
179
+ logging.info(f"Geometría guardada en: {geometry_save_path}")
180
+ return geometry_save_path
181
 
182
  @spaces.GPU(duration=120)
183
  def generate_texture(input_image_path, geometry_path):
184
+ """Paso 2: Aplica la textura a la geometría generada."""
 
 
185
  if not geometry_path or not os.path.exists(geometry_path):
186
  raise gr.Error("Por favor, primero genera la geometría antes de texturizar.")
187
  if not input_image_path or not os.path.exists(input_image_path):
188
+ raise gr.Error("Se necesita la imagen procesada para el texturizado.")
189
 
190
+ logging.info(f"Iniciando texturizado para la malla: {os.path.basename(geometry_path)}")
191
  geometry_mesh = trimesh.load(geometry_path)
192
 
193
+ # Post-procesamiento
194
  geometry_mesh = remove_degenerate_face(geometry_mesh)
195
  geometry_mesh = reduce_face(geometry_mesh)
196
 
197
  textured_mesh = texture_model(input_image_path, geometry_mesh)
198
 
199
+ save_name = os.path.basename(geometry_path).replace("_geometry.glb", "")
200
+ textured_save_path = f"{args.cache_dir}/{save_name}_textured.glb"
201
  textured_mesh.export(textured_save_path)
202
 
203
  torch.cuda.empty_cache()
204
+ logging.info(f"Malla texturizada guardada en: {textured_save_path}")
205
  return textured_save_path
206
 
207
+ # ==============================================================================
208
+ # 4. INTERFAZ DE GRADIO
209
+ # ==============================================================================
210
 
211
+ with gr.Blocks(title="Step1X-3D", css="footer {display: none !important;} a {text-decoration: none !important;}") as demo:
212
+ gr.Markdown("# Step1X-3D: De Boceto a Malla 3D Texturizada")
213
+ gr.Markdown("Flujo de trabajo en 3 pasos: **0. Procesar Boceto 1. Generar Geometría 2. Generar Textura**")
214
 
215
+ # Estados para mantener las rutas de los archivos entre pasos
216
  processed_image_path_state = gr.State()
217
  geometry_path_state = gr.State()
218
 
219
  with gr.Row():
220
  with gr.Column(scale=2):
221
+ # --- Panel de Entradas ---
222
+ input_image = gr.Image(label="Paso 0: Carga tu boceto o imagen", type="pil", image_mode="RGB")
223
+ prompt = gr.Textbox(label="Describe tu objeto", value="a comfortable armchair")
224
+
225
+ with gr.Accordion(label="Opciones Avanzadas", open=False):
226
+ gr.Markdown("### Opciones de Procesado de Boceto (Paso 0)")
227
+ neg_prompt_sketch = gr.Textbox(label="Negative Prompt (Boceto)", value="text, signature, watermark")
228
+ guidance_sketch = gr.Slider(0.1, 10.0, label="Guidance Scale (Boceto)", value=5.0, step=0.1)
229
+ steps_sketch = gr.Slider(1, 50, label="Steps (Boceto)", value=25, step=1)
230
+ controlnet_scale = gr.Slider(0.1, 2.0, label="ControlNet Scale", value=0.85, step=0.05)
231
+
232
+ gr.Markdown("---")
233
+ gr.Markdown("### Opciones de Generación 3D (Paso 1)")
234
+ guidance_3d = gr.Number(label="Guidance Scale (3D)", value="7.5")
235
+ steps_3d = gr.Slider(label="Inference Steps (3D)", minimum=1, maximum=100, value=50)
236
+ max_facenum = gr.Number(label="Max Face Num", value="200000")
237
+ symmetry = gr.Radio(choices=["symmetry", "asymmetry"], label="Symmetry", value="symmetry", type="index")
238
  edge_type = gr.Radio(choices=["sharp", "normal", "smooth"], label="Edge Type", value="sharp", type="value")
239
+
240
  with gr.Row():
241
+ btn_process_sketch = gr.Button("0. Procesar Boceto", variant="secondary")
242
  with gr.Row():
243
+ btn_geo = gr.Button("1. Generar Geometría", interactive=False)
244
+ btn_tex = gr.Button("2. Generar Textura", interactive=False)
245
+
246
+ with gr.Column(scale=3):
247
+ # --- Panel de Salidas ---
248
+ processed_image_preview = gr.Image(label="Resultado del Boceto Procesado", type="filepath", interactive=False, height=400)
249
+ geometry_preview = gr.Model3D(label="Vista Previa de la Geometría", height=400, clear_color=[0.0, 0.0, 0.0, 0.0])
250
+ textured_preview = gr.Model3D(label="Vista Previa del Modelo Texturizado", height=400, clear_color=[0.0, 0.0, 0.0, 0.0])
251
 
252
  with gr.Column(scale=1):
253
  gr.Examples(
254
  examples=[
255
+ ["examples/images/000.png", "a futuristic spaceship"],
256
+ ["examples/images/001.png", "a cartoon style monster"],
257
+ ["examples/images/004.png", "a red sports car"],
258
+ ["examples/images/008.png", "a medieval sword"],
259
+ ["examples/images/028.png", "a vintage camera"],
260
+ ["examples/images/032.png", "a cute robot"],
261
+ ["examples/images/061.png", "a delicious hamburger"],
262
+ ["examples/images/107.png", "a golden trophy"],
263
  ],
264
+ inputs=[input_image, prompt], cache_examples=False
 
265
  )
266
 
267
  # --- Lógica de la Interfaz ---
268
 
269
+ def on_sketch_processed(path):
270
+ """Función a ejecutar cuando el boceto se ha procesado."""
271
+ return {
272
+ processed_image_path_state: path,
273
+ btn_geo: gr.update(interactive=True, variant="primary"),
274
+ btn_tex: gr.update(interactive=False),
275
+ geometry_preview: gr.update(value=None),
276
+ textured_preview: gr.update(value=None),
277
+ }
278
+
279
+ def on_geometry_generated(path):
280
+ """Función a ejecutar cuando la geometría se ha generado."""
281
+ return {
282
+ geometry_path_state: path,
283
+ btn_tex: gr.update(interactive=True, variant="primary"),
284
+ }
285
+
286
+ btn_process_sketch.click(
287
+ fn=process_sketch,
288
+ inputs=[input_image, prompt, neg_prompt_sketch, guidance_sketch, steps_sketch, controlnet_scale],
289
+ outputs=[processed_image_preview]
290
+ ).then(
291
+ fn=on_sketch_processed,
292
+ inputs=[processed_image_preview],
293
+ outputs=[processed_image_path_state, btn_geo, btn_tex, geometry_preview, textured_preview]
294
  )
295
 
296
+ btn_geo.click(
297
+ fn=generate_geometry,
298
+ inputs=[processed_image_path_state, guidance_3d, steps_3d, max_facenum, symmetry, edge_type],
299
+ outputs=[geometry_preview]
300
+ ).then(
301
+ fn=on_geometry_generated,
302
+ inputs=[geometry_preview],
303
+ outputs=[geometry_path_state, btn_tex]
304
  )
305
 
306
+ btn_tex.click(
307
+ fn=generate_texture,
308
+ inputs=[processed_image_path_state, geometry_path_state],
309
+ outputs=[textured_preview],
310
+ )
311
 
312
+ demo.launch(ssr_mode=False)