cavargas10 commited on
Commit
fcdb260
·
verified ·
1 Parent(s): 8d0e192

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +262 -258
app.py CHANGED
@@ -1,163 +1,159 @@
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
- import random
10
  import uuid
 
 
 
11
 
12
- # Configuración del logging para una mejor depuración
13
- logging.basicConfig(level=logging.INFO, format='%(asctime)s - Step1X-3D - %(levelname)s - %(message)s')
14
-
15
- @spaces.GPU
16
- def install_dependencies():
17
- """
18
- Instala de forma robusta el toolkit de CUDA y compila las extensiones C++/CUDA.
19
- Usa subprocess.run para capturar errores.
20
- """
21
- logging.info("Iniciando la instalación de dependencias...")
22
-
23
- # Instalar CUDA Toolkit si no está presente
24
- CUDA_TOOLKIT_URL = "https://developer.download.nvidia.com/compute/cuda/12.4.0/local_installers/cuda_12.4.0_550.54.14_linux.run"
25
- CUDA_TOOLKIT_FILE = f"/tmp/{os.path.basename(CUDA_TOOLKIT_URL)}"
26
- if not os.path.exists("/usr/local/cuda"):
27
- logging.info("Descargando e instalando CUDA Toolkit...")
28
- subprocess.run(["wget", "-q", CUDA_TOOLKIT_URL, "-O", CUDA_TOOLKIT_FILE], check=True)
29
- subprocess.run(["chmod", "+x", CUDA_TOOLKIT_FILE], check=True)
30
- subprocess.run([CUDA_TOOLKIT_FILE, "--silent", "--toolkit"], check=True)
31
- else:
32
- logging.info("CUDA Toolkit ya está instalado.")
33
-
34
- # Configurar variables de entorno para la compilación
35
- os.environ["CUDA_HOME"] = "/usr/local/cuda"
36
- os.environ["PATH"] = f"{os.environ.get('CUDA_HOME', '')}/bin:{os.environ.get('PATH', '')}"
37
- os.environ["LD_LIBRARY_PATH"] = f"{os.environ.get('CUDA_HOME', '')}/lib:{os.environ.get('LD_LIBRARY_PATH', '')}"
38
- os.environ["TORCH_CUDA_ARCH_LIST"] = "8.0;8.6"
39
-
40
- # Compilar extensiones personalizadas con manejo de errores
41
- logging.info("Compilando extensión 'differentiable_renderer'...")
42
- renderer_path = "/home/user/app/step1x3d_texture/differentiable_renderer/"
43
- try:
44
- subprocess.run(f"cd {renderer_path} && python setup.py install", shell=True, check=True, capture_output=True, text=True)
45
- logging.info("Extensión 'differentiable_renderer' compilada con éxito.")
46
- except subprocess.CalledProcessError as e:
47
- logging.error("¡FALLÓ LA COMPILACIÓN de 'differentiable_renderer'!")
48
- logging.error(f"STDOUT: {e.stdout}")
49
- logging.error(f"STDERR: {e.stderr}")
50
- raise # Detiene la aplicación si la compilación falla
51
-
52
- try:
53
- subprocess.run(shlex.split("pip install custom_rasterizer-0.1-cp310-cp310-linux_x86_64.whl"), check=True)
54
- logging.info("Extensión 'custom_rasterizer' instalada con éxito.")
55
- except subprocess.CalledProcessError as e:
56
- logging.error("¡FALLÓ LA INSTALACIÓN de 'custom_rasterizer'!")
57
- raise
58
-
59
- logging.info("Instalación de dependencias completada.")
60
- subprocess.run(['nvcc', '--version'], check=True)
61
-
62
- # Llama a la función de instalación solo una vez al iniciar
63
- install_dependencies()
64
-
65
- import torch
66
- import trimesh
67
- import argparse
68
- import numpy as np
69
  import gradio as gr
70
  from PIL import Image
71
- from diffusers import DiffusionPipeline
 
 
 
 
 
 
 
 
72
  from step1x3d_geometry.models.pipelines.pipeline import Step1X3DGeometryPipeline
73
- from step1x3d_texture.pipelines.step1x_3d_texture_synthesis_pipeline import Step1X3DTexturePipeline
 
 
74
  from step1x3d_geometry.models.pipelines.pipeline_utils import reduce_face, remove_degenerate_face
75
 
76
- # ==============================================================================
77
- # 2. CONFIGURACIÓN Y CARGA DE MODELOS
78
- # ==============================================================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
 
80
- parser = argparse.ArgumentParser()
81
- parser.add_argument("--geometry_model", type=str, default="Step1X-3D-Geometry-Label-1300m")
82
- parser.add_argument("--texture_model", type=str, default="Step1X-3D-Texture")
83
- parser.add_argument("--cache_dir", type=str, default="cache")
84
- args = parser.parse_args()
85
-
86
- os.makedirs(args.cache_dir, exist_ok=True)
87
  device = "cuda" if torch.cuda.is_available() else "cpu"
88
- torch_dtype = torch.float16
89
- MAX_SEED = np.iinfo(np.int32).max
90
-
91
- logging.info("Cargando modelos... Este proceso puede tardar varios minutos.")
92
-
93
- # Carga de modelo de Texto a Imagen (FLUX - versión rápida)
94
- logging.info("Cargando pipeline de Texto a Imagen: FLUX.1-schnell")
95
- flux_pipeline = DiffusionPipeline.from_pretrained(
96
- "black-forest-labs/FLUX.1-schnell",
97
- torch_dtype=torch_dtype,
98
- use_safetensors=True
99
- ).to(device)
100
- logging.info("Pipeline FLUX cargado.")
101
-
102
- # Carga de modelos de Step1X-3D
103
- logging.info(f"Cargando modelo de geometría: {args.geometry_model}")
104
- geometry_model = Step1X3DGeometryPipeline.from_pretrained(
105
- "stepfun-ai/Step1X-3D", subfolder=args.geometry_model
106
- ).to(device)
107
-
108
- logging.info(f"Cargando modelo de textura: {args.texture_model}")
109
- texture_model = Step1X3DTexturePipeline.from_pretrained("stepfun-ai/Step1X-3D", subfolder=args.texture_model)
110
-
111
- logging.info("Todos los modelos han sido cargados correctamente y están listos.")
112
-
113
-
114
- # ==============================================================================
115
- # 3. FUNCIONES DE GENERACIÓN POR PASOS
116
- # ==============================================================================
117
-
118
- @spaces.GPU(duration=60)
119
- def generate_image(prompt: str, randomize_seed: bool, seed: int):
120
- """Paso 0: Genera una imagen a partir de un texto usando FLUX."""
121
- if not prompt:
122
- raise gr.Error("Por favor, introduce un prompt.")
123
-
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  if randomize_seed:
125
- seed = random.randint(0, MAX_SEED)
126
-
127
- generator = torch.Generator(device=device).manual_seed(int(seed))
128
-
129
- final_prompt = f"wbgmsst, professional 3d model {prompt}, octane render, highly detailed, volumetric, dramatic lighting, white background"
130
- negative_prompt = "ugly, deformed, noisy, low poly, blurry, painting, text, watermark, signature, jpeg artifacts"
131
-
132
- logging.info(f"Generando imagen con FLUX. Seed: {seed}, Prompt: '{final_prompt}'")
133
-
134
- image = flux_pipeline(
135
- prompt=final_prompt,
136
- negative_prompt=negative_prompt,
137
- num_inference_steps=28,
138
- guidance_scale=7.5,
139
  generator=generator,
140
- ).images[0]
141
-
142
- save_name = str(uuid.uuid4())
143
- image_path = f"{args.cache_dir}/{save_name}_flux_generated.png"
144
- image.save(image_path)
145
-
146
- logging.info(f"Imagen generada y guardada en: {image_path}")
147
- return image_path, seed
148
 
 
149
  @spaces.GPU(duration=180)
150
- def generate_geometry(input_image_path, guidance_scale, inference_steps, max_facenum, symmetry, edge_type):
151
- """Paso 1: Genera la geometría a partir de la imagen procesada."""
152
- if not input_image_path or not os.path.exists(input_image_path):
153
- raise gr.Error("Primero debes generar una imagen.")
154
-
155
- logging.info(f"Iniciando generación de geometría desde: {os.path.basename(input_image_path)}")
156
-
157
- if "Label" in args.geometry_model:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
  symmetry_values = ["x", "asymmetry"]
159
  out = geometry_model(
160
- input_image_path,
161
  label={"symmetry": symmetry_values[int(symmetry)], "edge_type": edge_type},
162
  guidance_scale=float(guidance_scale),
163
  octree_resolution=384,
@@ -166,153 +162,161 @@ def generate_geometry(input_image_path, guidance_scale, inference_steps, max_fac
166
  )
167
  else:
168
  out = geometry_model(
169
- input_image_path,
170
  guidance_scale=float(guidance_scale),
171
  num_inference_steps=int(inference_steps),
172
  max_facenum=int(max_facenum),
173
  )
174
 
175
- save_name = os.path.basename(input_image_path).replace("_flux_generated.png", "")
176
- geometry_save_path = f"{args.cache_dir}/{save_name}_geometry.glb"
177
  geometry_mesh = out.mesh[0]
178
  geometry_mesh.export(geometry_save_path)
179
-
180
  torch.cuda.empty_cache()
181
- logging.info(f"Geometría guardada en: {geometry_save_path}")
182
- return geometry_save_path
 
 
183
 
 
184
  @spaces.GPU(duration=120)
185
- def generate_texture(input_image_path, geometry_path):
186
- """Paso 2: Aplica la textura a la geometría generada."""
 
 
 
187
  if not geometry_path or not os.path.exists(geometry_path):
188
- raise gr.Error("Por favor, primero genera la geometría.")
189
- if not input_image_path or not os.path.exists(input_image_path):
190
- raise gr.Error("Se necesita la imagen generada para el texturizado.")
191
-
192
- logging.info(f"Iniciando texturizado para la malla: {os.path.basename(geometry_path)}")
 
 
 
 
193
  geometry_mesh = trimesh.load(geometry_path)
194
-
195
  geometry_mesh = remove_degenerate_face(geometry_mesh)
196
  geometry_mesh = reduce_face(geometry_mesh)
197
-
198
- textured_mesh = texture_model(input_image_path, geometry_mesh)
199
-
200
- save_name = os.path.basename(geometry_path).replace("_geometry.glb", "")
201
- textured_save_path = f"{args.cache_dir}/{save_name}_textured.glb"
202
  textured_mesh.export(textured_save_path)
203
-
204
  torch.cuda.empty_cache()
205
- logging.info(f"Malla texturizada guardada en: {textured_save_path}")
 
206
  return textured_save_path
207
 
208
- # ==============================================================================
209
- # 4. INTERFAZ DE GRADIO
210
- # ==============================================================================
 
211
 
212
- with gr.Blocks(title="Step1X-3D", css="footer {display: none !important;} a {text-decoration: none !important;}") as demo:
213
- gr.Markdown("# Step1X-3D: Flujo de Texto a 3D")
214
- gr.Markdown("Flujo de trabajo en 3 pasos: **0. Generar Imagen → 1. Generar Geometría → 2. Generar Textura**")
215
-
216
- image_path_state = gr.State()
217
  geometry_path_state = gr.State()
 
218
 
219
  with gr.Row():
220
  with gr.Column(scale=2):
221
- prompt = gr.Textbox(label="Paso 0: Describe el objeto", value="a comfortable armchair")
222
-
223
- with gr.Accordion(label="Opciones Avanzadas", open=False):
224
- seed = gr.Slider(0, MAX_SEED, label="Seed (para Imagen 2D)", value=42, step=1)
225
  randomize_seed = gr.Checkbox(label="Randomize Seed", value=True)
226
- gr.Markdown("---")
227
- gr.Markdown("### Opciones de Generación 3D (Pasos 1 y 2)")
228
- guidance_3d = gr.Number(label="Guidance Scale (3D)", value="7.5")
229
- steps_3d = gr.Slider(label="Inference Steps (3D)", minimum=1, maximum=100, value=50)
230
- max_facenum = gr.Number(label="Max Face Num", value="200000")
231
- symmetry = gr.Radio(choices=["symmetry", "asymmetry"], label="Symmetry", value="symmetry", type="index")
232
- edge_type = gr.Radio(choices=["sharp", "normal", "smooth"], label="Edge Type", value="sharp", type="value")
233
-
234
- with gr.Row():
235
- btn_image = gr.Button("0. Generate Image", variant="primary")
236
- with gr.Row():
237
- btn_geo = gr.Button("1. Generate Geometry", interactive=False)
238
- btn_tex = gr.Button("2. Generate Texture", interactive=False)
239
 
240
  with gr.Column(scale=3):
241
- image_preview = gr.Image(label="Resultado de la Imagen Generada", type="filepath", interactive=False, height=400)
242
- geometry_preview = gr.Model3D(label="Vista Previa de la Geometría", height=400, clear_color=[0.0, 0.0, 0.0, 0.0])
243
- textured_preview = gr.Model3D(label="Vista Previa del Modelo Texturizado", height=400, clear_color=[0.0, 0.0, 0.0, 0.0])
244
-
245
  with gr.Column(scale=1):
246
- gr.Examples(
247
- examples=[
248
- ["a futuristic spaceship"],
249
- ["a cartoon style monster"],
250
- ["a red sports car"],
251
- ["a medieval sword"],
252
- ["a vintage camera"],
253
- ],
254
- inputs=[prompt], cache_examples=False
255
- )
256
- current_seed = gr.Textbox(label="Seed Usada", interactive=False)
257
-
258
- # --- Lógica de la Interfaz ---
259
- def on_image_generated(path, used_seed):
260
- return {
261
- image_path_state: path,
262
- current_seed: used_seed,
263
- btn_image: gr.update(interactive=True),
264
- btn_geo: gr.update(interactive=True, variant="primary"),
265
- btn_tex: gr.update(interactive=False),
266
- geometry_preview: gr.update(value=None),
267
- textured_preview: gr.update(value=None),
268
- }
269
-
270
- def on_geometry_generated(path):
271
- return {
272
- geometry_path_state: path,
273
- btn_geo: gr.update(interactive=True, variant="secondary"),
274
- btn_tex: gr.update(interactive=True, variant="primary"),
275
- }
276
-
277
- def on_texture_generated():
278
- return {
279
- btn_tex: gr.update(interactive=True, variant="secondary")
280
- }
281
-
282
- btn_image.click(
283
- fn=lambda: gr.update(interactive=False), outputs=[btn_image]
284
- ).then(
285
- fn=generate_image,
286
- inputs=[prompt, randomize_seed, seed],
287
- outputs=[image_preview, current_seed]
288
  ).then(
289
- fn=on_image_generated,
290
- inputs=[image_preview, current_seed],
291
- outputs=[image_path_state, current_seed, btn_image, btn_geo, btn_tex, geometry_preview, textured_preview]
292
  )
293
-
294
- btn_geo.click(
295
- fn=lambda: (gr.update(interactive=False), gr.update(interactive=False)),
296
- outputs=[btn_geo, btn_tex]
297
- ).then(
298
  fn=generate_geometry,
299
- inputs=[image_path_state, guidance_3d, steps_3d, max_facenum, symmetry, edge_type],
300
- outputs=[geometry_preview]
 
 
 
 
 
 
 
301
  ).then(
302
- fn=on_geometry_generated,
303
- inputs=[geometry_preview],
304
- outputs=[geometry_path_state, btn_geo, btn_tex]
305
  )
306
-
307
- btn_tex.click(
308
- fn=lambda: gr.update(interactive=False), outputs=[btn_tex]
309
- ).then(
310
  fn=generate_texture,
311
- inputs=[image_path_state, geometry_path_state],
312
  outputs=[textured_preview],
313
- ).then(
314
- fn=on_texture_generated,
315
- outputs=[btn_tex]
316
  )
317
 
318
- demo.launch(ssr_mode=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app.py
2
+ # Fusion: Texto -> Imagen -> Geometría -> Textura
3
+ # Requisitos (resumido):
4
+ # - diffusers / FluxPipeline o el pipeline que uses para generar la imagen
5
+ # - step1x3d_geometry y step1x3d_texture (pipelines que ya usabas)
6
+ # - trellis si lo usas (opcional)
7
+ # Ajusta nombres de modelos, tokens y paths según tu entorno.
8
+
9
  import os
 
 
 
 
 
10
  import uuid
11
+ import logging
12
+ import shutil
13
+ from typing import Tuple, Union
14
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  import gradio as gr
16
  from PIL import Image
17
+ import numpy as np
18
+ import torch
19
+ import imageio
20
+
21
+ # Si tienes decoradores de spaces definidos (como spaces.GPU), impórtalos.
22
+ # from spaces import GPU # si usas spaces.GPU
23
+ import spaces # si lo necesitas por compatibilidad con tus decoradores
24
+
25
+ # Importa tus pipelines Step1X (o los que uses)
26
  from step1x3d_geometry.models.pipelines.pipeline import Step1X3DGeometryPipeline
27
+ from step1x3d_texture.pipelines.step1x_3d_texture_synthesis_pipeline import (
28
+ Step1X3DTexturePipeline,
29
+ )
30
  from step1x3d_geometry.models.pipelines.pipeline_utils import reduce_face, remove_degenerate_face
31
 
32
+ # Si usas FluxPipeline/FluxTransformer como en tu ejemplo:
33
+ from diffusers import DiffusionPipeline # fallback genérico; puedes dejar FluxPipeline si lo tienes
34
+
35
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s - APP - %(levelname)s - %(message)s")
36
+ logger = logging.getLogger(__name__)
37
+
38
+ # -------- CONFIG (ajusta) ----------
39
+ HUGGINGFACE_TOKEN = os.getenv("HUGGINGFACE_TOKEN", None)
40
+ # Model names (ajusta a los subfolders que usas)
41
+ GEOMETRY_SUBFOLDER = "Step1X-3D-Geometry-Label-1300m"
42
+ TEXTURE_SUBFOLDER = "Step1X-3D-Texture"
43
+ STEP1X_MODEL_REPO = "stepfun-ai/Step1X-3D" # repo base
44
+ # Para text-to-image: usa tu modelo preferido; aquí dejo un placeholder
45
+ IMAGE_GEN_MODEL = "camenduru/FLUX.1-dev-diffusers" # si usas Flux o diffusers
46
+ # Folder para caché/temporales (por sesión)
47
+ TMP_ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "tmp")
48
+ os.makedirs(TMP_ROOT, exist_ok=True)
49
+ # -----------------------------------
50
 
 
 
 
 
 
 
 
51
  device = "cuda" if torch.cuda.is_available() else "cpu"
52
+ logger.info(f"Device: {device}")
53
+
54
+ # ---------- Inicialización de modelos (se hace en __main__) -----------
55
+ geometry_model = None
56
+ texture_model = None
57
+ image_gen_pipeline = None
58
+
59
+ # ---------- FUNCIONES ----------
60
+ def start_session(req: gr.Request):
61
+ session_hash = str(req.session_hash)
62
+ user_dir = os.path.join(TMP_ROOT, session_hash)
63
+ logger.info(f"[{session_hash}] start_session -> {user_dir}")
64
+ os.makedirs(user_dir, exist_ok=True)
65
+
66
+ def end_session(req: gr.Request):
67
+ session_hash = str(req.session_hash)
68
+ user_dir = os.path.join(TMP_ROOT, session_hash)
69
+ logger.info(f"[{session_hash}] end_session -> {user_dir}")
70
+ if os.path.exists(user_dir):
71
+ try:
72
+ shutil.rmtree(user_dir)
73
+ logger.info(f"[{session_hash}] user dir removed")
74
+ except Exception as e:
75
+ logger.warning(f"[{session_hash}] failed removing user dir: {e}")
76
+
77
+ def save_pil_image_for_session(img: Image.Image, req: gr.Request, name="generated.png") -> str:
78
+ session_hash = str(req.session_hash)
79
+ user_dir = os.path.join(TMP_ROOT, session_hash)
80
+ os.makedirs(user_dir, exist_ok=True)
81
+ path = os.path.join(user_dir, name)
82
+ img.save(path)
83
+ return path
84
+
85
+ # ---------- Generar imagen desde prompt ----------
86
+ @spaces.GPU # si usas spaces.GPU; si no, puedes quitar
87
+ def generate_image_from_text(
88
+ prompt: str,
89
+ seed: int,
90
+ randomize_seed: bool,
91
+ width: int,
92
+ height: int,
93
+ guidance_scale: float,
94
+ req: gr.Request,
95
+ progress: gr.Progress = gr.Progress(track_tqdm=True),
96
+ ) -> Image.Image:
97
+ """Genera una imagen 2D desde prompt y devuelve PIL.Image"""
98
+ global image_gen_pipeline
99
+ session_hash = str(req.session_hash)
100
+ logger.info(f"[{session_hash}] Generando imagen desde texto: '{prompt[:80]}'")
101
  if randomize_seed:
102
+ seed = int(np.random.randint(0, np.iinfo(np.int32).max))
103
+ logger.info(f"[{session_hash}] seed aleatorio -> {seed}")
104
+ generator = torch.Generator(device=device).manual_seed(seed) if device == "cuda" else torch.manual_seed(seed)
105
+
106
+ # Ajusta la llamada según el pipeline que uses (FluxPipeline, DiffusionPipeline, etc.)
107
+ # Ejemplo genérico con DiffusionPipeline (puede necesitar rename de args)
108
+ result = image_gen_pipeline(
109
+ prompt,
110
+ guidance_scale=float(guidance_scale),
111
+ num_inference_steps=8,
112
+ width=int(width),
113
+ height=int(height),
 
 
114
  generator=generator,
115
+ )
116
+ image = result.images[0] if hasattr(result, "images") else result # compat
117
+ # Guarda la imagen en carpeta de sesión
118
+ path = save_pil_image_for_session(image, req, name="generated_2d_image.png")
119
+ logger.info(f"[{session_hash}] imagen guardada en: {path}")
120
+ return image
 
 
121
 
122
+ # ---------- Generar geometría a partir de imagen ----------
123
  @spaces.GPU(duration=180)
124
+ def generate_geometry(
125
+ input_image: Union[str, Image.Image],
126
+ guidance_scale,
127
+ inference_steps,
128
+ max_facenum,
129
+ symmetry,
130
+ edge_type,
131
+ req: gr.Request,
132
+ progress: gr.Progress = gr.Progress(track_tqdm=True),
133
+ ):
134
+ """
135
+ Genera la geometría usando Step1X geometry pipeline.
136
+ input_image puede ser path (str) o PIL.Image.
137
+ Devuelve (geometry_preview_path, geometry_path_state)
138
+ """
139
+ global geometry_model
140
+ session_hash = str(req.session_hash)
141
+ logger.info(f"[{session_hash}] Iniciando generación de geometría...")
142
+
143
+ if isinstance(input_image, str):
144
+ image_input = input_image
145
+ else:
146
+ # PIL.Image
147
+ image_input = save_pil_image_for_session(input_image, req, name="for_geometry.png")
148
+
149
+ if input_image is None:
150
+ raise gr.Error("Por favor, sube o genera una imagen antes de generar la geometría.")
151
+
152
+ # Lógica adaptada según si el modelo espera label u otros args
153
+ if "Label" in GEOMETRY_SUBFOLDER:
154
  symmetry_values = ["x", "asymmetry"]
155
  out = geometry_model(
156
+ image_input,
157
  label={"symmetry": symmetry_values[int(symmetry)], "edge_type": edge_type},
158
  guidance_scale=float(guidance_scale),
159
  octree_resolution=384,
 
162
  )
163
  else:
164
  out = geometry_model(
165
+ image_input,
166
  guidance_scale=float(guidance_scale),
167
  num_inference_steps=int(inference_steps),
168
  max_facenum=int(max_facenum),
169
  )
170
 
171
+ save_name = str(uuid.uuid4())
172
+ geometry_save_path = os.path.join(TMP_ROOT, session_hash, f"{save_name}.glb")
173
  geometry_mesh = out.mesh[0]
174
  geometry_mesh.export(geometry_save_path)
175
+
176
  torch.cuda.empty_cache()
177
+ logger.info(f"[{session_hash}] Geometría guardada en: {geometry_save_path}")
178
+
179
+ # Devuelve la ruta para preview (Model3D) y para guardar en el state
180
+ return geometry_save_path, geometry_save_path
181
 
182
+ # ---------- Generar textura a partir de geometría ----------
183
  @spaces.GPU(duration=120)
184
+ def generate_texture(input_image: Union[str, Image.Image], geometry_path: str, req: gr.Request, progress: gr.Progress = gr.Progress(track_tqdm=True)):
185
+ global texture_model
186
+ session_hash = str(req.session_hash)
187
+ logger.info(f"[{session_hash}] Iniciando texturizado para: {geometry_path}")
188
+
189
  if not geometry_path or not os.path.exists(geometry_path):
190
+ raise gr.Error("Por favor, primero genera la geometría antes de texturizar.")
191
+
192
+ if isinstance(input_image, str):
193
+ img_path = input_image
194
+ else:
195
+ img_path = save_pil_image_for_session(input_image, req, name="for_texture.png")
196
+
197
+ # Carga y postprocesado
198
+ import trimesh
199
  geometry_mesh = trimesh.load(geometry_path)
 
200
  geometry_mesh = remove_degenerate_face(geometry_mesh)
201
  geometry_mesh = reduce_face(geometry_mesh)
202
+
203
+ textured_mesh = texture_model(img_path, geometry_mesh)
204
+
205
+ save_name = os.path.basename(geometry_path).replace(".glb", "")
206
+ textured_save_path = os.path.join(TMP_ROOT, session_hash, f"{save_name}-textured.glb")
207
  textured_mesh.export(textured_save_path)
208
+
209
  torch.cuda.empty_cache()
210
+ logger.info(f"[{session_hash}] Malla texturizada guardada en: {textured_save_path}")
211
+
212
  return textured_save_path
213
 
214
+ # ---------- Interfaz Gradio ----------
215
+ with gr.Blocks(title="Text Image → 3D (Step1X Flow)") as demo:
216
+ gr.Markdown("# Text → Image → 3D (Step1X) - Demo integrada")
217
+ gr.Markdown("Flujo: Texto → Generar imagen → Generar geometría → Texturizar")
218
 
 
 
 
 
 
219
  geometry_path_state = gr.State()
220
+ generated_image_state = gr.State()
221
 
222
  with gr.Row():
223
  with gr.Column(scale=2):
224
+ prompt = gr.Textbox(label="Prompt", placeholder="Describe el asset que quieres generar")
225
+ with gr.Accordion("Image Generation Settings", open=False):
226
+ seed = gr.Slider(0, int(2**31-1), label="Seed", value=42, step=1)
 
227
  randomize_seed = gr.Checkbox(label="Randomize Seed", value=True)
228
+ with gr.Row():
229
+ width = gr.Slider(256, 1024, label="Width", value=512, step=64)
230
+ height = gr.Slider(256, 1024, label="Height", value=512, step=64)
231
+ guidance_scale = gr.Slider(0.0, 10.0, label="Guidance Scale", value=3.5, step=0.1)
232
+
233
+ generate_image_btn = gr.Button("1. Generar Imagen")
234
+ generate_geo_btn = gr.Button("2. Generar Geometría", interactive=False, visible=True)
235
+ generate_tex_btn = gr.Button("3. Generar Textura", interactive=False, visible=False)
 
 
 
 
 
236
 
237
  with gr.Column(scale=3):
238
+ generated_image = gr.Image(label="Imagen generada (2D)", type="pil")
239
+ geometry_preview = gr.Model3D(label="Geometría (GLB)", height=360)
240
+ textured_preview = gr.Model3D(label="Modelo texturizado (GLB)", height=360)
241
+
242
  with gr.Column(scale=1):
243
+ gr.Markdown("**Parámetros Geometría**")
244
+ guidance_geom = gr.Number(label="Guidance Scale geom", value=7.5)
245
+ inference_steps_geom = gr.Slider(1, 100, label="Pasos inferencia geom", value=50)
246
+ max_facenum = gr.Number(label="Máx. número de caras", value=400000)
247
+ symmetry = gr.Radio(choices=["symmetry", "asymmetry"], label="Tipo de simetría", value="symmetry", type="index")
248
+ edge_type = gr.Radio(choices=["sharp", "normal", "smooth"], label="Tipo de borde", value="sharp", type="value")
249
+
250
+ # Session handlers
251
+ demo.load(start_session)
252
+ demo.unload(end_session)
253
+
254
+ # 1) Generar imagen desde texto
255
+ generate_image_btn.click(
256
+ fn=generate_image_from_text,
257
+ inputs=[prompt, seed, randomize_seed, width, height, guidance_scale],
258
+ outputs=[generated_image],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
259
  ).then(
260
+ # cuando la imagen esté lista, habilitar el botón de generar geometría
261
+ lambda: gr.update(interactive=True),
262
+ outputs=[generate_geo_btn],
263
  )
264
+
265
+ # 2) Generar geometría desde la imagen generada
266
+ # usamos generated_image (PIL) y enviamos a generate_geometry
267
+ generate_geo_btn.click(
 
268
  fn=generate_geometry,
269
+ inputs=[
270
+ generated_image, # PIL image
271
+ guidance_geom,
272
+ inference_steps_geom,
273
+ max_facenum,
274
+ symmetry,
275
+ edge_type,
276
+ ],
277
+ outputs=[geometry_preview, geometry_path_state],
278
  ).then(
279
+ # habilitar el botón de generar textura
280
+ lambda: (gr.update(interactive=True), gr.update(visible=True)),
281
+ outputs=[generate_tex_btn, textured_preview]
282
  )
283
+
284
+ # 3) Texturizar la geometría
285
+ generate_tex_btn.click(
 
286
  fn=generate_texture,
287
+ inputs=[generated_image, geometry_path_state],
288
  outputs=[textured_preview],
 
 
 
289
  )
290
 
291
+ # ---------- Carga de modelos en main ----------
292
+ if __name__ == "__main__":
293
+ # --------- Inicializar image generation pipeline ----------
294
+ try:
295
+ # Si tienes un pipeline específico (FluxPipeline) reemplaza la línea siguiente
296
+ logger.info("Inicializando pipeline de generación de imágenes...")
297
+ image_gen_pipeline = DiffusionPipeline.from_pretrained(IMAGE_GEN_MODEL, use_auth_token=HUGGINGFACE_TOKEN)
298
+ image_gen_pipeline = image_gen_pipeline.to(device)
299
+ logger.info("Pipeline de imagen cargado.")
300
+ except Exception as e:
301
+ logger.error(f"Error cargando pipeline de imágenes: {e}")
302
+ image_gen_pipeline = None
303
+
304
+ # --------- Inicializar Step1X modelos ----------
305
+ try:
306
+ logger.info("Cargando modelo de geometría Step1X...")
307
+ geometry_model = Step1X3DGeometryPipeline.from_pretrained(STEP1X_MODEL_REPO, subfolder=GEOMETRY_SUBFOLDER).to(device)
308
+ logger.info("Modelo de geometría cargado.")
309
+ except Exception as e:
310
+ logger.error(f"Error cargando modelo de geometría: {e}")
311
+ geometry_model = None
312
+
313
+ try:
314
+ logger.info("Cargando modelo de textura Step1X...")
315
+ texture_model = Step1X3DTexturePipeline.from_pretrained(STEP1X_MODEL_REPO, subfolder=TEXTURE_SUBFOLDER)
316
+ logger.info("Modelo de textura cargado.")
317
+ except Exception as e:
318
+ logger.error(f"Error cargando modelo de textura: {e}")
319
+ texture_model = None
320
+
321
+ # Lanzar app
322
+ demo.launch(show_error=True)