bhatanerohan commited on
Commit
8481e27
Β·
verified Β·
1 Parent(s): 166bb4e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +157 -0
app.py CHANGED
@@ -7,13 +7,67 @@ import os
7
  import io
8
  import json
9
  import tempfile
 
 
 
 
10
  import gradio as gr
11
  from google import genai
12
  from google.genai import types
13
  from PIL import Image
14
  import modal
15
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  # Initialize Gemini client
 
 
17
  client = None
18
 
19
  def init_gemini():
@@ -22,7 +76,9 @@ def init_gemini():
22
  if api_key:
23
  os.environ["GEMINI_API_KEY"] = api_key
24
  client = genai.Client()
 
25
  return True
 
26
  return False
27
 
28
  def image_to_bytes(image):
@@ -33,6 +89,7 @@ def image_to_bytes(image):
33
 
34
  def run_sam3d(image, mask):
35
  """Send image and mask to SAM-3D on Modal"""
 
36
  img_bytes = image_to_bytes(image.convert("RGB"))
37
  mask_bytes = image_to_bytes(mask)
38
 
@@ -40,6 +97,7 @@ def run_sam3d(image, mask):
40
  model = SAM3DModel()
41
  ply_bytes, glb_bytes = model.reconstruct.remote(img_bytes, mask_bytes)
42
 
 
43
  return ply_bytes, glb_bytes
44
 
45
 
@@ -57,6 +115,9 @@ def generate_3d_model(prompt: str) -> str:
57
  Returns:
58
  JSON string with paths to generated files
59
  """
 
 
 
60
  if not client:
61
  if not init_gemini():
62
  return json.dumps({"error": "GEMINI_API_KEY not configured"})
@@ -64,6 +125,7 @@ def generate_3d_model(prompt: str) -> str:
64
  try:
65
  # STEP 1: Generate image
66
  initial_prompt = f"{prompt}, three-quarter front view angle, natural daylight, soft shadows showing depth and contours, clean simple background, full object visible, photorealistic"
 
67
 
68
  response_gen = client.models.generate_content(
69
  model="gemini-2.5-flash-image",
@@ -78,9 +140,13 @@ def generate_3d_model(prompt: str) -> str:
78
  break
79
 
80
  if initial_image is None:
 
81
  return json.dumps({"error": "Image generation failed"})
82
 
 
 
83
  # STEP 2: Remove background
 
84
  edit_prompt = "Remove the background completely, make the background transparent. Preserve the object's shadow for realism."
85
  image_part = types.Part.from_bytes(
86
  data=image_to_bytes(initial_image),
@@ -100,8 +166,11 @@ def generate_3d_model(prompt: str) -> str:
100
  break
101
 
102
  if final_image is None:
 
103
  return json.dumps({"error": "Background removal failed"})
104
 
 
 
105
  # STEP 3: Create grayscale mask
106
  gray = final_image.convert("L")
107
 
@@ -129,6 +198,22 @@ def generate_3d_model(prompt: str) -> str:
129
  with open(glb_path, 'wb') as f:
130
  f.write(glb_bytes)
131
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
  return json.dumps({
133
  "success": True,
134
  "prompt": prompt,
@@ -137,10 +222,12 @@ def generate_3d_model(prompt: str) -> str:
137
  "mask_image": mask_path,
138
  "ply_model": ply_path,
139
  "glb_model": glb_path,
 
140
  "message": f"Successfully generated 3D model for: {prompt}"
141
  })
142
 
143
  except Exception as e:
 
144
  return json.dumps({"error": str(e)})
145
 
146
 
@@ -155,12 +242,16 @@ def edit_3d_model(edit_prompt: str, transparent_image_path: str) -> str:
155
  Returns:
156
  JSON string with paths to the new edited files
157
  """
 
 
 
158
  if not client:
159
  if not init_gemini():
160
  return json.dumps({"error": "GEMINI_API_KEY not configured"})
161
 
162
  try:
163
  current_image = Image.open(transparent_image_path)
 
164
 
165
  image_part = types.Part.from_bytes(
166
  data=image_to_bytes(current_image),
@@ -168,6 +259,7 @@ def edit_3d_model(edit_prompt: str, transparent_image_path: str) -> str:
168
  )
169
 
170
  full_edit_prompt = f"{edit_prompt}. Keep the background transparent. Maintain image quality and lighting."
 
171
 
172
  response_edit = client.models.generate_content(
173
  model="gemini-3-pro-image-preview",
@@ -182,8 +274,11 @@ def edit_3d_model(edit_prompt: str, transparent_image_path: str) -> str:
182
  break
183
 
184
  if edited_image is None:
 
185
  return json.dumps({"error": "Edit failed"})
186
 
 
 
187
  gray = edited_image.convert("L")
188
  ply_bytes, glb_bytes = run_sam3d(edited_image, gray)
189
 
@@ -205,6 +300,19 @@ def edit_3d_model(edit_prompt: str, transparent_image_path: str) -> str:
205
  with open(glb_path, 'wb') as f:
206
  f.write(glb_bytes)
207
 
 
 
 
 
 
 
 
 
 
 
 
 
 
208
  return json.dumps({
209
  "success": True,
210
  "edit_prompt": edit_prompt,
@@ -212,10 +320,12 @@ def edit_3d_model(edit_prompt: str, transparent_image_path: str) -> str:
212
  "mask_image": mask_path,
213
  "ply_model": ply_path,
214
  "glb_model": glb_path,
 
215
  "message": f"Successfully applied edit: {edit_prompt}"
216
  })
217
 
218
  except Exception as e:
 
219
  return json.dumps({"error": str(e)})
220
 
221
 
@@ -225,6 +335,9 @@ def edit_3d_model(edit_prompt: str, transparent_image_path: str) -> str:
225
 
226
  def generate_3d_ui(prompt, progress=gr.Progress()):
227
  """UI wrapper with progress updates"""
 
 
 
228
  if not client:
229
  if not init_gemini():
230
  raise gr.Error("GEMINI_API_KEY not set in Space secrets")
@@ -248,8 +361,11 @@ def generate_3d_ui(prompt, progress=gr.Progress()):
248
 
249
  if initial_image is None:
250
  raise gr.Error("Image generation failed")
 
 
251
 
252
  except Exception as e:
 
253
  raise gr.Error(f"Image generation failed: {e}")
254
 
255
  progress(0.3, desc="Removing background...")
@@ -274,8 +390,11 @@ def generate_3d_ui(prompt, progress=gr.Progress()):
274
 
275
  if final_image is None:
276
  raise gr.Error("Background removal failed")
 
 
277
 
278
  except Exception as e:
 
279
  raise gr.Error(f"Background removal failed: {e}")
280
 
281
  progress(0.4, desc="Creating mask...")
@@ -286,6 +405,7 @@ def generate_3d_ui(prompt, progress=gr.Progress()):
286
  try:
287
  ply_bytes, glb_bytes = run_sam3d(final_image, gray)
288
  except Exception as e:
 
289
  raise gr.Error(f"SAM-3D failed: {e}")
290
 
291
  progress(0.9, desc="Saving outputs...")
@@ -310,7 +430,22 @@ def generate_3d_ui(prompt, progress=gr.Progress()):
310
  with open(glb_path, 'wb') as f:
311
  f.write(glb_bytes)
312
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
313
  progress(1.0, desc="Done!")
 
314
 
315
  return (
316
  original_path,
@@ -326,6 +461,9 @@ def generate_3d_ui(prompt, progress=gr.Progress()):
326
 
327
  def edit_3d_ui(edit_prompt, current_image, edit_count, progress=gr.Progress()):
328
  """UI wrapper for editing"""
 
 
 
329
  if current_image is None:
330
  raise gr.Error("No image to edit. Generate a 3D model first!")
331
 
@@ -357,8 +495,11 @@ def edit_3d_ui(edit_prompt, current_image, edit_count, progress=gr.Progress()):
357
 
358
  if edited_image is None:
359
  raise gr.Error("Edit failed")
 
 
360
 
361
  except Exception as e:
 
362
  raise gr.Error(f"Edit failed: {e}")
363
 
364
  progress(0.3, desc="Creating new mask...")
@@ -369,6 +510,7 @@ def edit_3d_ui(edit_prompt, current_image, edit_count, progress=gr.Progress()):
369
  try:
370
  ply_bytes, glb_bytes = run_sam3d(edited_image, gray)
371
  except Exception as e:
 
372
  raise gr.Error(f"SAM-3D failed: {e}")
373
 
374
  progress(0.9, desc="Saving outputs...")
@@ -392,7 +534,21 @@ def edit_3d_ui(edit_prompt, current_image, edit_count, progress=gr.Progress()):
392
  f.write(glb_bytes)
393
 
394
  new_edit_count = edit_count + 1
 
 
 
 
 
 
 
 
 
 
 
 
 
395
  progress(1.0, desc=f"Edit #{new_edit_count} complete!")
 
396
 
397
  return (
398
  nobg_path,
@@ -529,4 +685,5 @@ demo = gr.TabbedInterface(
529
  )
530
 
531
  if __name__ == "__main__":
 
532
  demo.launch(mcp_server=True)
 
7
  import io
8
  import json
9
  import tempfile
10
+ import logging
11
+ from datetime import datetime
12
+ import hashlib
13
+ import shutil
14
  import gradio as gr
15
  from google import genai
16
  from google.genai import types
17
  from PIL import Image
18
  import modal
19
 
20
+ # ============================================================
21
+ # LOGGING SETUP
22
+ # ============================================================
23
+
24
+ # Setup logging for console output (visible in HF Spaces logs)
25
+ logging.basicConfig(
26
+ level=logging.INFO,
27
+ format='%(asctime)s - %(levelname)s - %(message)s',
28
+ handlers=[
29
+ logging.StreamHandler() # This goes to HF Spaces logs
30
+ ]
31
+ )
32
+ logger = logging.getLogger(__name__)
33
+
34
+ # Create persistent logs directory (use HF Datasets for permanent storage)
35
+ LOGS_DIR = "generation_logs"
36
+ os.makedirs(LOGS_DIR, exist_ok=True)
37
+
38
+ def save_generation_log(prompt, images_dict, metadata=None):
39
+ """Save generation logs with images"""
40
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
41
+ session_id = hashlib.md5(f"{prompt}{timestamp}".encode()).hexdigest()[:8]
42
+
43
+ session_dir = os.path.join(LOGS_DIR, f"{timestamp}_{session_id}")
44
+ os.makedirs(session_dir, exist_ok=True)
45
+
46
+ # Save metadata
47
+ log_data = {
48
+ "timestamp": timestamp,
49
+ "session_id": session_id,
50
+ "prompt": prompt,
51
+ "metadata": metadata or {}
52
+ }
53
+
54
+ log_file = os.path.join(session_dir, "metadata.json")
55
+ with open(log_file, 'w') as f:
56
+ json.dump(log_data, f, indent=2)
57
+
58
+ # Save images
59
+ for name, image_path in images_dict.items():
60
+ if image_path and os.path.exists(image_path):
61
+ dest = os.path.join(session_dir, f"{name}.png")
62
+ shutil.copy(image_path, dest)
63
+
64
+ logger.info(f"βœ“ Saved logs to: {session_dir}")
65
+ return session_dir
66
+
67
+ # ============================================================
68
  # Initialize Gemini client
69
+ # ============================================================
70
+
71
  client = None
72
 
73
  def init_gemini():
 
76
  if api_key:
77
  os.environ["GEMINI_API_KEY"] = api_key
78
  client = genai.Client()
79
+ logger.info("βœ“ Gemini client initialized")
80
  return True
81
+ logger.error("βœ— GEMINI_API_KEY not found")
82
  return False
83
 
84
  def image_to_bytes(image):
 
89
 
90
  def run_sam3d(image, mask):
91
  """Send image and mask to SAM-3D on Modal"""
92
+ logger.info("β†’ Sending to SAM-3D on Modal...")
93
  img_bytes = image_to_bytes(image.convert("RGB"))
94
  mask_bytes = image_to_bytes(mask)
95
 
 
97
  model = SAM3DModel()
98
  ply_bytes, glb_bytes = model.reconstruct.remote(img_bytes, mask_bytes)
99
 
100
+ logger.info(f"βœ“ SAM-3D complete - PLY: {len(ply_bytes)} bytes, GLB: {len(glb_bytes) if glb_bytes else 0} bytes")
101
  return ply_bytes, glb_bytes
102
 
103
 
 
115
  Returns:
116
  JSON string with paths to generated files
117
  """
118
+ logger.info(f"=== MCP TOOL: generate_3d_model ===")
119
+ logger.info(f"Prompt: {prompt}")
120
+
121
  if not client:
122
  if not init_gemini():
123
  return json.dumps({"error": "GEMINI_API_KEY not configured"})
 
125
  try:
126
  # STEP 1: Generate image
127
  initial_prompt = f"{prompt}, three-quarter front view angle, natural daylight, soft shadows showing depth and contours, clean simple background, full object visible, photorealistic"
128
+ logger.info("β†’ Generating initial image...")
129
 
130
  response_gen = client.models.generate_content(
131
  model="gemini-2.5-flash-image",
 
140
  break
141
 
142
  if initial_image is None:
143
+ logger.error("βœ— Image generation failed")
144
  return json.dumps({"error": "Image generation failed"})
145
 
146
+ logger.info(f"βœ“ Initial image generated: {initial_image.size}")
147
+
148
  # STEP 2: Remove background
149
+ logger.info("β†’ Removing background...")
150
  edit_prompt = "Remove the background completely, make the background transparent. Preserve the object's shadow for realism."
151
  image_part = types.Part.from_bytes(
152
  data=image_to_bytes(initial_image),
 
166
  break
167
 
168
  if final_image is None:
169
+ logger.error("βœ— Background removal failed")
170
  return json.dumps({"error": "Background removal failed"})
171
 
172
+ logger.info("βœ“ Background removed")
173
+
174
  # STEP 3: Create grayscale mask
175
  gray = final_image.convert("L")
176
 
 
198
  with open(glb_path, 'wb') as f:
199
  f.write(glb_bytes)
200
 
201
+ # Save logs
202
+ images_dict = {
203
+ "original": original_path,
204
+ "transparent": nobg_path,
205
+ "mask": mask_path
206
+ }
207
+ metadata = {
208
+ "type": "generation",
209
+ "has_glb": glb_path is not None,
210
+ "ply_size_bytes": len(ply_bytes),
211
+ "glb_size_bytes": len(glb_bytes) if glb_bytes else 0
212
+ }
213
+ log_dir = save_generation_log(prompt, images_dict, metadata)
214
+
215
+ logger.info(f"βœ“ Generation complete!")
216
+
217
  return json.dumps({
218
  "success": True,
219
  "prompt": prompt,
 
222
  "mask_image": mask_path,
223
  "ply_model": ply_path,
224
  "glb_model": glb_path,
225
+ "log_directory": log_dir,
226
  "message": f"Successfully generated 3D model for: {prompt}"
227
  })
228
 
229
  except Exception as e:
230
+ logger.error(f"βœ— Error: {e}", exc_info=True)
231
  return json.dumps({"error": str(e)})
232
 
233
 
 
242
  Returns:
243
  JSON string with paths to the new edited files
244
  """
245
+ logger.info(f"=== MCP TOOL: edit_3d_model ===")
246
+ logger.info(f"Edit: {edit_prompt}")
247
+
248
  if not client:
249
  if not init_gemini():
250
  return json.dumps({"error": "GEMINI_API_KEY not configured"})
251
 
252
  try:
253
  current_image = Image.open(transparent_image_path)
254
+ logger.info(f"β†’ Loaded image: {current_image.size}")
255
 
256
  image_part = types.Part.from_bytes(
257
  data=image_to_bytes(current_image),
 
259
  )
260
 
261
  full_edit_prompt = f"{edit_prompt}. Keep the background transparent. Maintain image quality and lighting."
262
+ logger.info("β†’ Applying edit...")
263
 
264
  response_edit = client.models.generate_content(
265
  model="gemini-3-pro-image-preview",
 
274
  break
275
 
276
  if edited_image is None:
277
+ logger.error("βœ— Edit failed")
278
  return json.dumps({"error": "Edit failed"})
279
 
280
+ logger.info("βœ“ Edit applied")
281
+
282
  gray = edited_image.convert("L")
283
  ply_bytes, glb_bytes = run_sam3d(edited_image, gray)
284
 
 
300
  with open(glb_path, 'wb') as f:
301
  f.write(glb_bytes)
302
 
303
+ # Save logs
304
+ images_dict = {
305
+ "edited": nobg_path,
306
+ "mask": mask_path
307
+ }
308
+ metadata = {
309
+ "type": "edit",
310
+ "has_glb": glb_path is not None
311
+ }
312
+ log_dir = save_generation_log(edit_prompt, images_dict, metadata)
313
+
314
+ logger.info(f"βœ“ Edit complete!")
315
+
316
  return json.dumps({
317
  "success": True,
318
  "edit_prompt": edit_prompt,
 
320
  "mask_image": mask_path,
321
  "ply_model": ply_path,
322
  "glb_model": glb_path,
323
+ "log_directory": log_dir,
324
  "message": f"Successfully applied edit: {edit_prompt}"
325
  })
326
 
327
  except Exception as e:
328
+ logger.error(f"βœ— Error: {e}", exc_info=True)
329
  return json.dumps({"error": str(e)})
330
 
331
 
 
335
 
336
  def generate_3d_ui(prompt, progress=gr.Progress()):
337
  """UI wrapper with progress updates"""
338
+ logger.info(f"=== NEW GENERATION REQUEST ===")
339
+ logger.info(f"Prompt: {prompt}")
340
+
341
  if not client:
342
  if not init_gemini():
343
  raise gr.Error("GEMINI_API_KEY not set in Space secrets")
 
361
 
362
  if initial_image is None:
363
  raise gr.Error("Image generation failed")
364
+
365
+ logger.info(f"βœ“ Image generated: {initial_image.size}")
366
 
367
  except Exception as e:
368
+ logger.error(f"βœ— Image generation failed: {e}")
369
  raise gr.Error(f"Image generation failed: {e}")
370
 
371
  progress(0.3, desc="Removing background...")
 
390
 
391
  if final_image is None:
392
  raise gr.Error("Background removal failed")
393
+
394
+ logger.info("βœ“ Background removed")
395
 
396
  except Exception as e:
397
+ logger.error(f"βœ— Background removal failed: {e}")
398
  raise gr.Error(f"Background removal failed: {e}")
399
 
400
  progress(0.4, desc="Creating mask...")
 
405
  try:
406
  ply_bytes, glb_bytes = run_sam3d(final_image, gray)
407
  except Exception as e:
408
+ logger.error(f"βœ— SAM-3D failed: {e}")
409
  raise gr.Error(f"SAM-3D failed: {e}")
410
 
411
  progress(0.9, desc="Saving outputs...")
 
430
  with open(glb_path, 'wb') as f:
431
  f.write(glb_bytes)
432
 
433
+ # Save logs
434
+ images_dict = {
435
+ "original": original_path,
436
+ "transparent": nobg_path,
437
+ "mask": mask_path
438
+ }
439
+ metadata = {
440
+ "type": "ui_generation",
441
+ "has_glb": glb_path is not None,
442
+ "ply_size_bytes": len(ply_bytes),
443
+ "glb_size_bytes": len(glb_bytes) if glb_bytes else 0
444
+ }
445
+ save_generation_log(prompt, images_dict, metadata)
446
+
447
  progress(1.0, desc="Done!")
448
+ logger.info(f"βœ“ Generation complete!")
449
 
450
  return (
451
  original_path,
 
461
 
462
  def edit_3d_ui(edit_prompt, current_image, edit_count, progress=gr.Progress()):
463
  """UI wrapper for editing"""
464
+ logger.info(f"=== EDIT REQUEST #{edit_count + 1} ===")
465
+ logger.info(f"Edit: {edit_prompt}")
466
+
467
  if current_image is None:
468
  raise gr.Error("No image to edit. Generate a 3D model first!")
469
 
 
495
 
496
  if edited_image is None:
497
  raise gr.Error("Edit failed")
498
+
499
+ logger.info("βœ“ Edit applied")
500
 
501
  except Exception as e:
502
+ logger.error(f"βœ— Edit failed: {e}")
503
  raise gr.Error(f"Edit failed: {e}")
504
 
505
  progress(0.3, desc="Creating new mask...")
 
510
  try:
511
  ply_bytes, glb_bytes = run_sam3d(edited_image, gray)
512
  except Exception as e:
513
+ logger.error(f"βœ— SAM-3D failed: {e}")
514
  raise gr.Error(f"SAM-3D failed: {e}")
515
 
516
  progress(0.9, desc="Saving outputs...")
 
534
  f.write(glb_bytes)
535
 
536
  new_edit_count = edit_count + 1
537
+
538
+ # Save logs
539
+ images_dict = {
540
+ "edited": nobg_path,
541
+ "mask": mask_path
542
+ }
543
+ metadata = {
544
+ "type": "ui_edit",
545
+ "edit_number": new_edit_count,
546
+ "has_glb": glb_path is not None
547
+ }
548
+ save_generation_log(edit_prompt, images_dict, metadata)
549
+
550
  progress(1.0, desc=f"Edit #{new_edit_count} complete!")
551
+ logger.info(f"βœ“ Edit #{new_edit_count} complete!")
552
 
553
  return (
554
  nobg_path,
 
685
  )
686
 
687
  if __name__ == "__main__":
688
+ logger.info("=== Starting Text-to-3D MCP Server ===")
689
  demo.launch(mcp_server=True)