euler314 commited on
Commit
903f458
·
verified ·
1 Parent(s): a1edea0

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +364 -68
app.py CHANGED
@@ -7,62 +7,143 @@ import base64
7
  import sys
8
  import shutil
9
  import re
 
 
10
  from streamlit_ace import st_ace
11
 
 
 
 
 
12
  st.set_page_config(page_title="Blender 3D Viewer", layout="wide")
13
  st.title("🌍 Blender Script → 3D Viewer")
14
 
15
- # Find Blender executable
 
 
 
 
 
16
  def find_blender():
17
- # Common paths to check
18
  paths_to_check = [
19
  "blender", # default PATH
20
  "/usr/bin/blender",
21
- "/opt/blender/blender"
 
 
 
22
  ]
23
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  for path in paths_to_check:
25
  try:
 
26
  result = subprocess.run([path, "--version"],
27
  capture_output=True,
28
  text=True,
29
- timeout=5)
30
  if result.returncode == 0:
31
  st.success(f"Found Blender: {result.stdout.strip()}")
 
32
  return path
33
- except:
 
34
  continue
35
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  return None
37
 
38
- # Preprocess script to ensure it has necessary imports and path handling
39
  def preprocess_script(script_text, tmp_dir):
40
- # Check if 'import os' exists in the script
41
- if 'import os' not in script_text:
42
- script_text = 'import os\n' + script_text
 
 
 
 
 
 
 
 
 
 
 
43
 
44
- # Check if 'import math' exists in the script (commonly needed)
45
- if 'import math' not in script_text and 'math.' in script_text:
46
- script_text = 'import math\n' + script_text
47
 
48
- # Check if script already has file saving code
49
  if 'bpy.ops.wm.save_as_mainfile' not in script_text:
50
- # Add file saving code at the end
51
  script_text += f'''
52
 
53
- # Save the .blend file
54
- output_dir = os.environ.get('BLENDER_OUTPUT_DIR', os.getcwd())
55
- blend_file_path = os.path.join(output_dir, "scene.blend")
56
- bpy.ops.wm.save_as_mainfile(filepath=blend_file_path)
57
- print(f"Saved blend file to: {{blend_file_path}}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  '''
 
59
  else:
60
- # Make sure existing save code uses absolute paths correctly
61
- script_text = re.sub(
62
- r'bpy\.ops\.wm\.save_as_mainfile\s*\(\s*filepath\s*=\s*["\']([^"\']+)["\']',
63
- 'bpy.ops.wm.save_as_mainfile(filepath=os.path.join(os.environ.get("BLENDER_OUTPUT_DIR", os.getcwd()), "scene.blend")',
64
- script_text
65
- )
 
 
 
66
 
67
  return script_text
68
 
@@ -70,13 +151,20 @@ print(f"Saved blend file to: {{blend_file_path}}")
70
  blender_path = find_blender()
71
  if not blender_path:
72
  st.error("❌ Blender not found! The app requires Blender to be installed.")
73
- st.info("Please check the Spaces configuration for proper apt.txt setup.")
 
 
 
 
74
  # Continue anyway to show the interface
75
 
76
  # Hardcoded default texture URLs (with fallbacks)
77
  DEFAULT_TEXTURE_URLS = [
78
  "https://eoimages.gsfc.nasa.gov/images/imagerecords/57000/57730/land_ocean_ice_2048.jpg",
79
- "https://svs.gsfc.nasa.gov/vis/a000000/a002900/a002915/bluemarble-2048.png"
 
 
 
80
  ]
81
 
82
  # Sidebar: Blender script editor with example script
@@ -256,6 +344,34 @@ bpy.context.scene.cycles.device = 'CPU'
256
  print('Solar system animation setup complete.')"""
257
  }
258
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
259
  # Add example selector
260
  selected_example = st.sidebar.selectbox(
261
  "Load example script:",
@@ -287,6 +403,19 @@ custom_packages = st.sidebar.text_area(
287
  height=100
288
  )
289
 
 
 
 
 
 
 
 
 
 
 
 
 
 
290
  # Main action
291
  if st.sidebar.button("Run & Export GLB"):
292
  if not blender_path:
@@ -300,12 +429,15 @@ if st.sidebar.button("Run & Export GLB"):
300
  with st.spinner("Processing your 3D scene..."):
301
  # Use a more robust temporary directory approach
302
  tmp_dir = tempfile.mkdtemp(prefix="blender_app_")
 
 
303
  try:
304
  # 1) Preprocess and save user script
305
  processed_script = preprocess_script(script_text, tmp_dir)
306
  script_path = os.path.join(tmp_dir, "user_script.py")
307
  with open(script_path, "w") as f:
308
  f.write(processed_script)
 
309
 
310
  # 2) Collect textures
311
  texture_paths = []
@@ -316,20 +448,23 @@ if st.sidebar.button("Run & Export GLB"):
316
  with open(path, "wb") as tf:
317
  tf.write(upload.read())
318
  texture_paths.append(path)
 
319
  else:
320
  # Try multiple default textures if some fail
321
  for url in DEFAULT_TEXTURE_URLS:
322
  try:
323
- r = requests.get(url, timeout=10)
324
  r.raise_for_status()
325
  ext = os.path.splitext(url)[-1] or ".jpg"
326
  path = os.path.join(tmp_dir, f"default{ext}")
327
  with open(path, "wb") as tf:
328
  tf.write(r.content)
329
  texture_paths.append(path)
 
330
  # If we got one texture successfully, that's enough
331
  break
332
  except Exception as e:
 
333
  st.warning(f"Could not download texture {url}: {str(e)}")
334
 
335
  # Display texture file information
@@ -337,14 +472,17 @@ if st.sidebar.button("Run & Export GLB"):
337
  st.subheader("Texture Files")
338
  for tp in texture_paths:
339
  st.code(os.path.basename(tp))
 
340
  else:
341
  st.warning("No textures were loaded. Your 3D model may appear without textures.")
 
342
 
343
  # 3) Install custom Python libraries if specified
344
  if custom_packages and custom_packages.strip():
345
  pkgs = [l.strip() for l in custom_packages.splitlines() if l.strip()]
346
  if pkgs:
347
  st.info(f"Installing: {', '.join(pkgs)}")
 
348
  try:
349
  # Use Python executable from current environment for pip
350
  pip_cmd = [sys.executable, "-m", "pip", "install", "--user"] + pkgs
@@ -353,52 +491,123 @@ if st.sidebar.button("Run & Export GLB"):
353
  check=True,
354
  capture_output=True,
355
  text=True,
356
- timeout=120 # 2 minute timeout
357
  )
358
  st.text_area("pip install output", pip_res.stdout + pip_res.stderr, height=100)
 
359
  except Exception as e:
360
  st.warning(f"pip install failed: {str(e)}")
 
361
 
362
  # 4) Prepare environment with necessary variables
363
  env = os.environ.copy()
364
  env["TEXTURE_PATHS"] = ",".join(texture_paths)
365
  env["BLENDER_OUTPUT_DIR"] = tmp_dir # Pass the tmp_dir to the Blender script
 
366
 
367
- # 5) Run Blender to build .blend file
368
  blend_path = os.path.join(tmp_dir, "scene.blend")
369
 
370
- with st.status("Running Blender to create scene...") as status:
371
- cmd1 = [blender_path, "--background", "--python", script_path]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
372
  try:
373
- r1 = subprocess.run(
374
- cmd1,
 
375
  cwd=tmp_dir,
376
  env=env,
377
  check=True,
378
  capture_output=True,
379
  text=True,
380
- timeout=180 # 3 minute timeout
381
  )
382
- status.update(label="Blender scene created successfully", state="complete")
383
- st.text_area("Blender output", r1.stdout + r1.stderr, height=150)
384
- except subprocess.TimeoutExpired:
385
- st.error("Blender process timed out after 3 minutes.")
386
- st.stop()
387
- except subprocess.CalledProcessError as e:
388
- st.error(f"Blender build failed with error code {e.returncode}")
389
- st.text_area("Error details", e.stdout + e.stderr, height=150)
390
- st.stop()
 
391
  except Exception as e:
392
- st.error(f"Blender build failed: {str(e)}")
 
393
  st.stop()
394
-
395
- # Check if blend file was created
396
- if not os.path.exists(blend_path):
397
- st.error("Blender did not create the expected scene.blend file.")
398
- st.info("This might be due to an issue with the script or Blender configuration.")
399
- st.stop()
400
 
401
- # 6) Export GLB with animation
402
  glb_path = os.path.join(tmp_dir, "animation.glb")
403
 
404
  with st.status("Exporting to GLB format...") as status:
@@ -408,19 +617,57 @@ if st.sidebar.button("Run & Export GLB"):
408
  f.write(f"""
409
  import bpy
410
  import os
 
411
 
412
- # Load the blend file
413
- bpy.ops.wm.open_mainfile(filepath=r'{blend_path}')
414
-
415
- # Export to GLB
416
- bpy.ops.export_scene.gltf(
417
- filepath=r'{glb_path}',
418
- export_format='GLB',
419
- export_animations=True
420
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
421
  """)
422
 
423
  cmd2 = [blender_path, "--background", "--python", export_script]
 
 
424
  try:
425
  r2 = subprocess.run(
426
  cmd2,
@@ -429,12 +676,14 @@ bpy.ops.export_scene.gltf(
429
  check=True,
430
  capture_output=True,
431
  text=True,
432
- timeout=120 # 2 minute timeout
433
  )
434
  status.update(label="GLB export completed successfully", state="complete")
435
  st.text_area("Export output", r2.stdout + r2.stderr, height=100)
 
436
  except Exception as e:
437
  st.error(f"GLB export failed: {str(e)}")
 
438
  st.stop()
439
 
440
  # 7) Embed GLB inline if it exists
@@ -444,10 +693,12 @@ bpy.ops.export_scene.gltf(
444
 
445
  file_size = len(data) / (1024 * 1024) # Size in MB
446
  st.success(f"Successfully created GLB file ({file_size:.1f} MB)")
 
447
 
448
  # Check if file isn't too large for embedding
449
  if file_size > 50:
450
  st.warning("The GLB file is quite large. The viewer might be slow to load.")
 
451
 
452
  b64 = base64.b64encode(data).decode()
453
 
@@ -476,15 +727,24 @@ bpy.ops.export_scene.gltf(
476
  )
477
  else:
478
  st.error("GLB file was not generated successfully.")
 
 
479
  if os.path.exists(blend_path):
480
  st.info("The Blender file was created, but the GLB export failed.")
 
481
 
482
  finally:
483
- # Clean up - remove temporary directory
484
- try:
485
- shutil.rmtree(tmp_dir, ignore_errors=True)
486
- except Exception as e:
487
- st.warning(f"Failed to clean up temporary files: {str(e)}")
 
 
 
 
 
 
488
 
489
  # Add helpful information at the bottom
490
  st.divider()
@@ -497,8 +757,11 @@ with st.expander("About this app"):
497
  - Generate and visualize 3D models with animations
498
  - Download the results as GLB files for use in other applications
499
 
500
- The app automatically adds necessary imports (`import os`) and file saving code to your scripts,
501
  so you can focus on creating 3D content without worrying about environment details.
 
 
 
502
  """)
503
 
504
  with st.expander("Script Tips"):
@@ -525,4 +788,37 @@ with st.expander("Script Tips"):
525
  obj.rotation_euler = (0, 0, math.radians(360))
526
  obj.keyframe_insert(data_path="rotation_euler", frame=250)
527
  ```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
528
  """)
 
7
  import sys
8
  import shutil
9
  import re
10
+ import logging
11
+ import platform
12
  from streamlit_ace import st_ace
13
 
14
+ # Set up logging
15
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
16
+ logger = logging.getLogger('blender_app')
17
+
18
  st.set_page_config(page_title="Blender 3D Viewer", layout="wide")
19
  st.title("🌍 Blender Script → 3D Viewer")
20
 
21
+ # Detect environment
22
+ IS_HUGGINGFACE = os.environ.get('SPACE_ID') is not None
23
+ if IS_HUGGINGFACE:
24
+ st.info("Running in Hugging Face Spaces environment")
25
+
26
+ # Find Blender executable with enhanced detection for Hugging Face
27
  def find_blender():
28
+ # Common paths to check, expanded for Hugging Face environment
29
  paths_to_check = [
30
  "blender", # default PATH
31
  "/usr/bin/blender",
32
+ "/opt/blender/blender",
33
+ "/app/bin/blender", # Common in Docker containers
34
+ "/usr/local/bin/blender",
35
+ os.path.join(os.path.expanduser("~"), "blender/blender")
36
  ]
37
 
38
+ # On HF, Blender might be installed in a custom location
39
+ if IS_HUGGINGFACE:
40
+ # Check if Blender was installed via apt.txt
41
+ hf_paths = [
42
+ "/opt/conda/bin/blender",
43
+ "/home/user/blender/blender"
44
+ ]
45
+ paths_to_check = hf_paths + paths_to_check
46
+
47
+ # On Windows systems
48
+ if platform.system() == "Windows":
49
+ program_files = os.environ.get("ProgramFiles", "C:\\Program Files")
50
+ paths_to_check.append(os.path.join(program_files, "Blender Foundation", "Blender", "blender.exe"))
51
+
52
  for path in paths_to_check:
53
  try:
54
+ logger.info(f"Checking Blender at: {path}")
55
  result = subprocess.run([path, "--version"],
56
  capture_output=True,
57
  text=True,
58
+ timeout=10)
59
  if result.returncode == 0:
60
  st.success(f"Found Blender: {result.stdout.strip()}")
61
+ logger.info(f"Blender found at {path}: {result.stdout.strip()}")
62
  return path
63
+ except Exception as e:
64
+ logger.debug(f"Failed to find Blender at {path}: {str(e)}")
65
  continue
66
 
67
+ # If not found, try using the 'which' command on Unix-like systems
68
+ if platform.system() != "Windows":
69
+ try:
70
+ result = subprocess.run(["which", "blender"],
71
+ capture_output=True,
72
+ text=True,
73
+ timeout=5)
74
+ if result.returncode == 0 and result.stdout.strip():
75
+ path = result.stdout.strip()
76
+ st.success(f"Found Blender using 'which': {path}")
77
+ logger.info(f"Blender found using 'which': {path}")
78
+ return path
79
+ except Exception as e:
80
+ logger.debug(f"Failed to use 'which' to find Blender: {str(e)}")
81
+
82
  return None
83
 
84
+ # Enhanced script preprocessor to ensure reliable .blend file saving
85
  def preprocess_script(script_text, tmp_dir):
86
+ logger.info("Preprocessing script...")
87
+
88
+ # Check if necessary imports exist in the script
89
+ required_imports = {
90
+ 'import bpy': 'import bpy',
91
+ 'import os': 'import os',
92
+ 'import math': 'import math' if 'math.' in script_text else None,
93
+ 'import sys': 'import sys'
94
+ }
95
+
96
+ for import_check, import_statement in required_imports.items():
97
+ if import_statement and import_check not in script_text:
98
+ script_text = import_statement + '\n' + script_text
99
+ logger.info(f"Added {import_statement} to script")
100
 
101
+ # Ensure absolute paths for file operations in Hugging Face environment
102
+ absolute_tmp_dir = os.path.abspath(tmp_dir)
103
+ logger.info(f"Using absolute temp directory: {absolute_tmp_dir}")
104
 
105
+ # More robust file saving code with error handling
106
  if 'bpy.ops.wm.save_as_mainfile' not in script_text:
107
+ # Add enhanced file saving code at the end with detailed error handling
108
  script_text += f'''
109
 
110
+ # Save the .blend file with error handling
111
+ try:
112
+ # Ensure the output directory exists
113
+ output_dir = os.environ.get('BLENDER_OUTPUT_DIR', '{absolute_tmp_dir}')
114
+ os.makedirs(output_dir, exist_ok=True)
115
+
116
+ # Use absolute path for the blend file
117
+ blend_file_path = os.path.abspath(os.path.join(output_dir, "scene.blend"))
118
+
119
+ # Print debug info for troubleshooting
120
+ print(f"Attempting to save blend file to: {{blend_file_path}}")
121
+ print(f"Current working directory: {{os.getcwd()}}")
122
+
123
+ # Save the file with absolute path - using both filepath and check_existing parameters
124
+ bpy.ops.wm.save_as_mainfile(filepath=blend_file_path, check_existing=False)
125
+
126
+ # Verify the file was saved
127
+ if os.path.exists(blend_file_path):
128
+ print(f"Successfully saved blend file to: {{blend_file_path}}")
129
+ else:
130
+ print(f"ERROR: Failed to save blend file - file does not exist after save operation")
131
+ except Exception as e:
132
+ print(f"ERROR saving .blend file: {{str(e)}}")
133
+ import traceback
134
+ traceback.print_exc()
135
  '''
136
+ logger.info("Added enhanced file saving code to script")
137
  else:
138
+ # Make sure existing save code uses absolute paths correctly and has error handling
139
+ save_pattern = r'bpy\.ops\.wm\.save_as_mainfile\s*\(\s*filepath\s*=\s*["\']([^"\']+)["\']'
140
+ if re.search(save_pattern, script_text):
141
+ script_text = re.sub(
142
+ save_pattern,
143
+ f'bpy.ops.wm.save_as_mainfile(filepath=os.path.abspath(os.path.join(os.environ.get("BLENDER_OUTPUT_DIR", "{absolute_tmp_dir}"), "scene.blend")), check_existing=False',
144
+ script_text
145
+ )
146
+ logger.info("Modified existing save code to use absolute paths")
147
 
148
  return script_text
149
 
 
151
  blender_path = find_blender()
152
  if not blender_path:
153
  st.error("❌ Blender not found! The app requires Blender to be installed.")
154
+ if IS_HUGGINGFACE:
155
+ st.info("For Hugging Face Spaces, make sure to include 'blender' in your apt.txt file to install Blender.")
156
+ st.code("blender", language="text")
157
+ else:
158
+ st.info("Please install Blender and make sure it's in your PATH.")
159
  # Continue anyway to show the interface
160
 
161
  # Hardcoded default texture URLs (with fallbacks)
162
  DEFAULT_TEXTURE_URLS = [
163
  "https://eoimages.gsfc.nasa.gov/images/imagerecords/57000/57730/land_ocean_ice_2048.jpg",
164
+ "https://svs.gsfc.nasa.gov/vis/a000000/a002900/a002915/bluemarble-2048.png",
165
+ # Additional fallbacks
166
+ "https://upload.wikimedia.org/wikipedia/commons/thumb/c/cd/Blue_Marble_2007_East.jpg/600px-Blue_Marble_2007_East.jpg",
167
+ "https://www.solarsystemscope.com/textures/download/2k_earth_daymap.jpg"
168
  ]
169
 
170
  # Sidebar: Blender script editor with example script
 
344
  print('Solar system animation setup complete.')"""
345
  }
346
 
347
+ # Add a simple script example that's lightweight for testing
348
+ script_examples["Simple Cube (Test)"] = """import bpy
349
+
350
+ # Clear the scene
351
+ bpy.ops.object.select_all(action='SELECT')
352
+ bpy.ops.object.delete()
353
+
354
+ # Create a cube
355
+ bpy.ops.mesh.primitive_cube_add(size=1, location=(0, 0, 0))
356
+ cube = bpy.context.active_object
357
+ cube.name = 'TestCube'
358
+
359
+ # Add animation
360
+ cube.rotation_euler = (0, 0, 0)
361
+ cube.keyframe_insert(data_path="rotation_euler", frame=1)
362
+ cube.rotation_euler = (0, math.radians(360), 0)
363
+ cube.keyframe_insert(data_path="rotation_euler", frame=100)
364
+
365
+ # Setup camera
366
+ bpy.ops.object.camera_add(location=(0, -3, 0))
367
+ camera = bpy.context.active_object
368
+ camera.rotation_euler = (math.radians(90), 0, 0)
369
+ bpy.context.scene.camera = camera
370
+
371
+ # Setup lighting
372
+ bpy.ops.object.light_add(type='SUN', location=(5, -5, 5))
373
+ """
374
+
375
  # Add example selector
376
  selected_example = st.sidebar.selectbox(
377
  "Load example script:",
 
403
  height=100
404
  )
405
 
406
+ # Add options specific to troubleshooting in Hugging Face
407
+ if IS_HUGGINGFACE:
408
+ st.sidebar.header("Hugging Face Options")
409
+ debug_mode = st.sidebar.checkbox("Enable Debug Mode", value=False)
410
+ if debug_mode:
411
+ log_level = st.sidebar.selectbox(
412
+ "Log Level",
413
+ options=["INFO", "DEBUG"],
414
+ index=0
415
+ )
416
+ if log_level == "DEBUG":
417
+ logger.setLevel(logging.DEBUG)
418
+
419
  # Main action
420
  if st.sidebar.button("Run & Export GLB"):
421
  if not blender_path:
 
429
  with st.spinner("Processing your 3D scene..."):
430
  # Use a more robust temporary directory approach
431
  tmp_dir = tempfile.mkdtemp(prefix="blender_app_")
432
+ logger.info(f"Created temporary directory: {tmp_dir}")
433
+
434
  try:
435
  # 1) Preprocess and save user script
436
  processed_script = preprocess_script(script_text, tmp_dir)
437
  script_path = os.path.join(tmp_dir, "user_script.py")
438
  with open(script_path, "w") as f:
439
  f.write(processed_script)
440
+ logger.info(f"Saved processed script to: {script_path}")
441
 
442
  # 2) Collect textures
443
  texture_paths = []
 
448
  with open(path, "wb") as tf:
449
  tf.write(upload.read())
450
  texture_paths.append(path)
451
+ logger.info(f"Saved {len(texture_paths)} uploaded textures")
452
  else:
453
  # Try multiple default textures if some fail
454
  for url in DEFAULT_TEXTURE_URLS:
455
  try:
456
+ r = requests.get(url, timeout=15)
457
  r.raise_for_status()
458
  ext = os.path.splitext(url)[-1] or ".jpg"
459
  path = os.path.join(tmp_dir, f"default{ext}")
460
  with open(path, "wb") as tf:
461
  tf.write(r.content)
462
  texture_paths.append(path)
463
+ logger.info(f"Downloaded texture from {url}")
464
  # If we got one texture successfully, that's enough
465
  break
466
  except Exception as e:
467
+ logger.warning(f"Could not download texture {url}: {str(e)}")
468
  st.warning(f"Could not download texture {url}: {str(e)}")
469
 
470
  # Display texture file information
 
472
  st.subheader("Texture Files")
473
  for tp in texture_paths:
474
  st.code(os.path.basename(tp))
475
+ logger.info(f"Using texture: {tp}")
476
  else:
477
  st.warning("No textures were loaded. Your 3D model may appear without textures.")
478
+ logger.warning("No textures were loaded")
479
 
480
  # 3) Install custom Python libraries if specified
481
  if custom_packages and custom_packages.strip():
482
  pkgs = [l.strip() for l in custom_packages.splitlines() if l.strip()]
483
  if pkgs:
484
  st.info(f"Installing: {', '.join(pkgs)}")
485
+ logger.info(f"Installing packages: {', '.join(pkgs)}")
486
  try:
487
  # Use Python executable from current environment for pip
488
  pip_cmd = [sys.executable, "-m", "pip", "install", "--user"] + pkgs
 
491
  check=True,
492
  capture_output=True,
493
  text=True,
494
+ timeout=180 # 3 minute timeout
495
  )
496
  st.text_area("pip install output", pip_res.stdout + pip_res.stderr, height=100)
497
+ logger.info(f"Pip install result: {pip_res.returncode}")
498
  except Exception as e:
499
  st.warning(f"pip install failed: {str(e)}")
500
+ logger.error(f"pip install failed: {str(e)}")
501
 
502
  # 4) Prepare environment with necessary variables
503
  env = os.environ.copy()
504
  env["TEXTURE_PATHS"] = ",".join(texture_paths)
505
  env["BLENDER_OUTPUT_DIR"] = tmp_dir # Pass the tmp_dir to the Blender script
506
+ logger.info(f"Set environment variable BLENDER_OUTPUT_DIR={tmp_dir}")
507
 
508
+ # 5) Run Blender to build .blend file with enhanced error handling and retry
509
  blend_path = os.path.join(tmp_dir, "scene.blend")
510
 
511
+ max_retries = 3
512
+ for attempt in range(1, max_retries + 1):
513
+ with st.status(f"Running Blender to create scene (attempt {attempt}/{max_retries})...") as status:
514
+ cmd1 = [blender_path, "--background", "--python", script_path]
515
+ logger.info(f"Running Blender command: {' '.join(cmd1)}")
516
+
517
+ try:
518
+ r1 = subprocess.run(
519
+ cmd1,
520
+ cwd=tmp_dir,
521
+ env=env,
522
+ check=True,
523
+ capture_output=True,
524
+ text=True,
525
+ timeout=300 # 5 minute timeout
526
+ )
527
+ status.update(label=f"Blender scene created successfully (attempt {attempt})", state="complete")
528
+ st.text_area("Blender output", r1.stdout + r1.stderr, height=150)
529
+ logger.info("Blender script execution completed successfully")
530
+ break
531
+ except subprocess.TimeoutExpired:
532
+ st.error(f"Blender process timed out after 5 minutes (attempt {attempt}/{max_retries}).")
533
+ logger.error(f"Blender timeout on attempt {attempt}")
534
+ if attempt == max_retries:
535
+ st.stop()
536
+ except subprocess.CalledProcessError as e:
537
+ st.error(f"Blender build failed with error code {e.returncode} (attempt {attempt}/{max_retries})")
538
+ st.text_area("Error details", e.stdout + e.stderr, height=150)
539
+ logger.error(f"Blender error on attempt {attempt}: {e.returncode}")
540
+ logger.debug(f"Blender stderr: {e.stderr}")
541
+
542
+ # If it's the last attempt, stop execution
543
+ if attempt == max_retries:
544
+ st.stop()
545
+ except Exception as e:
546
+ st.error(f"Blender build failed: {str(e)} (attempt {attempt}/{max_retries})")
547
+ logger.error(f"Unexpected error on attempt {attempt}: {str(e)}")
548
+ if attempt == max_retries:
549
+ st.stop()
550
+
551
+ # Check if blend file was created with better diagnostics
552
+ if not os.path.exists(blend_path):
553
+ st.error("Blender did not create the expected scene.blend file.")
554
+ logger.error(f"Blend file not found at {blend_path}")
555
+
556
+ # Check directory contents for troubleshooting
557
+ dir_contents = os.listdir(tmp_dir)
558
+ logger.info(f"Temporary directory contents: {dir_contents}")
559
+ st.info(f"Temporary directory contents: {', '.join(dir_contents)}")
560
+
561
+ # Try an alternative approach by writing a minimal script just to save a file
562
+ st.warning("Attempting alternative approach to create .blend file...")
563
+ logger.info("Trying alternative approach with minimal script")
564
+
565
+ minimal_script_path = os.path.join(tmp_dir, "minimal_save.py")
566
+ with open(minimal_script_path, "w") as f:
567
+ f.write(f"""
568
+ import bpy
569
+ import os
570
+
571
+ # Make sure the directory exists
572
+ os.makedirs(r'{tmp_dir}', exist_ok=True)
573
+
574
+ # Create a simple cube
575
+ bpy.ops.mesh.primitive_cube_add(size=1, location=(0, 0, 0))
576
+
577
+ # Save the file
578
+ blend_path = r'{blend_path}'
579
+ print(f"Saving to {{blend_path}}")
580
+ bpy.ops.wm.save_as_mainfile(filepath=blend_path, check_existing=False)
581
+ print(f"Save operation completed, checking if file exists: {{os.path.exists(blend_path)}}")
582
+ """)
583
+
584
  try:
585
+ cmd_min = [blender_path, "--background", "--python", minimal_script_path]
586
+ r_min = subprocess.run(
587
+ cmd_min,
588
  cwd=tmp_dir,
589
  env=env,
590
  check=True,
591
  capture_output=True,
592
  text=True,
593
+ timeout=120
594
  )
595
+ st.text_area("Minimal script output", r_min.stdout + r_min.stderr, height=150)
596
+ logger.info("Minimal script execution completed")
597
+
598
+ if os.path.exists(blend_path):
599
+ st.success("Alternative approach created a .blend file successfully!")
600
+ logger.info("Alternative approach succeeded")
601
+ else:
602
+ st.error("Alternative approach also failed to create .blend file.")
603
+ logger.error("Alternative approach failed")
604
+ st.stop()
605
  except Exception as e:
606
+ st.error(f"Alternative approach failed: {str(e)}")
607
+ logger.error(f"Alternative approach error: {str(e)}")
608
  st.stop()
 
 
 
 
 
 
609
 
610
+ # 6) Export GLB with animation - enhanced error handling
611
  glb_path = os.path.join(tmp_dir, "animation.glb")
612
 
613
  with st.status("Exporting to GLB format...") as status:
 
617
  f.write(f"""
618
  import bpy
619
  import os
620
+ import sys
621
 
622
+ # Print debug info
623
+ print(f"Python version: {{sys.version}}")
624
+ print(f"Blender version: {{bpy.app.version_string}}")
625
+ print(f"Attempting to load blend file: {blend_path}")
626
+ print(f"Current working directory: {{os.getcwd()}}")
627
+ print(f"File exists check: {{os.path.exists(r'{blend_path}')}}")
628
+
629
+ try:
630
+ # Load the blend file
631
+ bpy.ops.wm.open_mainfile(filepath=r'{blend_path}')
632
+ print("Blend file loaded successfully")
633
+
634
+ # Export to GLB
635
+ glb_path = r'{glb_path}'
636
+ print(f"Exporting to GLB: {{glb_path}}")
637
+
638
+ # Check if glTF export is available
639
+ if hasattr(bpy.ops.export_scene, 'gltf'):
640
+ bpy.ops.export_scene.gltf(
641
+ filepath=glb_path,
642
+ export_format='GLB',
643
+ export_animations=True
644
+ )
645
+ print(f"GLB export completed, file exists: {{os.path.exists(glb_path)}}")
646
+ else:
647
+ print("ERROR: glTF export operator not available. Check if the add-on is enabled.")
648
+ # Try to enable the addon
649
+ bpy.ops.preferences.addon_enable(module='io_scene_gltf2')
650
+ print("Attempted to enable glTF add-on")
651
+
652
+ # Try export again
653
+ if hasattr(bpy.ops.export_scene, 'gltf'):
654
+ bpy.ops.export_scene.gltf(
655
+ filepath=glb_path,
656
+ export_format='GLB',
657
+ export_animations=True
658
+ )
659
+ print(f"GLB export completed after enabling add-on")
660
+ else:
661
+ print("ERROR: Could not enable glTF export add-on")
662
+ except Exception as e:
663
+ print(f"ERROR during export: {{str(e)}}")
664
+ import traceback
665
+ traceback.print_exc()
666
  """)
667
 
668
  cmd2 = [blender_path, "--background", "--python", export_script]
669
+ logger.info(f"Running GLB export command: {' '.join(cmd2)}")
670
+
671
  try:
672
  r2 = subprocess.run(
673
  cmd2,
 
676
  check=True,
677
  capture_output=True,
678
  text=True,
679
+ timeout=180 # 3 minute timeout
680
  )
681
  status.update(label="GLB export completed successfully", state="complete")
682
  st.text_area("Export output", r2.stdout + r2.stderr, height=100)
683
+ logger.info("GLB export completed successfully")
684
  except Exception as e:
685
  st.error(f"GLB export failed: {str(e)}")
686
+ logger.error(f"GLB export error: {str(e)}")
687
  st.stop()
688
 
689
  # 7) Embed GLB inline if it exists
 
693
 
694
  file_size = len(data) / (1024 * 1024) # Size in MB
695
  st.success(f"Successfully created GLB file ({file_size:.1f} MB)")
696
+ logger.info(f"Created GLB file, size: {file_size:.1f} MB")
697
 
698
  # Check if file isn't too large for embedding
699
  if file_size > 50:
700
  st.warning("The GLB file is quite large. The viewer might be slow to load.")
701
+ logger.warning(f"Large GLB file: {file_size:.1f} MB")
702
 
703
  b64 = base64.b64encode(data).decode()
704
 
 
727
  )
728
  else:
729
  st.error("GLB file was not generated successfully.")
730
+ logger.error(f"GLB file not found at {glb_path}")
731
+
732
  if os.path.exists(blend_path):
733
  st.info("The Blender file was created, but the GLB export failed.")
734
+ logger.info("Blend file exists but GLB export failed")
735
 
736
  finally:
737
+ # Clean up - remove temporary directory if not in debug mode
738
+ if IS_HUGGINGFACE and debug_mode:
739
+ st.info(f"Debug mode: Temporary files kept at {tmp_dir}")
740
+ logger.info(f"Keeping temp files for debugging: {tmp_dir}")
741
+ else:
742
+ try:
743
+ shutil.rmtree(tmp_dir, ignore_errors=True)
744
+ logger.info(f"Cleaned up temporary directory: {tmp_dir}")
745
+ except Exception as e:
746
+ st.warning(f"Failed to clean up temporary files: {str(e)}")
747
+ logger.error(f"Cleanup error: {str(e)}")
748
 
749
  # Add helpful information at the bottom
750
  st.divider()
 
757
  - Generate and visualize 3D models with animations
758
  - Download the results as GLB files for use in other applications
759
 
760
+ The app automatically adds necessary imports and file saving code to your scripts,
761
  so you can focus on creating 3D content without worrying about environment details.
762
+
763
+ Running on Hugging Face Spaces: This app requires Blender to be installed. Make sure
764
+ your Space includes 'blender' in the apt.txt file.
765
  """)
766
 
767
  with st.expander("Script Tips"):
 
788
  obj.rotation_euler = (0, 0, math.radians(360))
789
  obj.keyframe_insert(data_path="rotation_euler", frame=250)
790
  ```
791
+
792
+ 4. **Troubleshooting in Hugging Face**: If you're having issues with the app in Hugging Face Spaces:
793
+
794
+ - Enable Debug Mode in the sidebar
795
+ - Check the error messages in the app output
796
+ - Make sure your apt.txt file includes 'blender'
797
+ - Try the "Simple Cube (Test)" example first to verify Blender is working
798
+ """)
799
+
800
+ # Add a section at the bottom for Hugging Face Spaces setup
801
+ with st.expander("Hugging Face Spaces Setup"):
802
+ st.markdown("""
803
+ ### Setting up this app on Hugging Face Spaces
804
+
805
+ To ensure this app works correctly on Hugging Face:
806
+
807
+ 1. **Create an apt.txt file** in your repository with:
808
+ ```
809
+ blender
810
+ ```
811
+
812
+ 2. **Create a requirements.txt file** with:
813
+ ```
814
+ streamlit
815
+ streamlit-ace
816
+ requests
817
+ ```
818
+
819
+ 3. **Set the Space SDK** to "Streamlit" in the Space settings
820
+
821
+ 4. **Set resource allocation** to at least 2 CPU + 4GB RAM for better performance
822
+
823
+ Troubleshooting: If you encounter "Blender not found" errors, check your apt.txt file and make sure Blender is being installed correctly.
824
  """)