euler314 commited on
Commit
b482df3
·
verified ·
1 Parent(s): 3795ed5

Upload 3 files

Browse files
Files changed (3) hide show
  1. app.py +342 -0
  2. apt.txt +8 -0
  3. requirements.txt +5 -0
app.py ADDED
@@ -0,0 +1,342 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Updated app.py
2
+ import streamlit as st
3
+ import tempfile
4
+ import subprocess
5
+ import os
6
+ import requests
7
+ import base64
8
+ import sys
9
+ from streamlit_ace import st_ace
10
+ import shutil
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
+ # Check if Blender is available at startup
39
+ blender_path = find_blender()
40
+ if not blender_path:
41
+ st.error("❌ Blender not found! The app requires Blender to be installed.")
42
+ st.info("Please check the Spaces configuration for proper apt.txt setup.")
43
+ # Continue anyway to show the interface
44
+
45
+ # Hardcoded default texture URLs (with fallbacks)
46
+ DEFAULT_TEXTURE_URLS = [
47
+ "https://eoimages.gsfc.nasa.gov/images/imagerecords/57000/57730/land_ocean_ice_2048.jpg",
48
+ "https://svs.gsfc.nasa.gov/vis/a000000/a002900/a002915/bluemarble-2048.png"
49
+ ]
50
+
51
+ # Sidebar: Blender script editor with example script
52
+ default_script = """
53
+ import bpy
54
+ import os
55
+ import math
56
+
57
+ # Clear the scene
58
+ bpy.ops.object.select_all(action='SELECT')
59
+ bpy.ops.object.delete()
60
+
61
+ # Create a UV Sphere for Earth
62
+ bpy.ops.mesh.primitive_uv_sphere_add(radius=1, location=(0, 0, 0))
63
+ earth = bpy.context.active_object
64
+ earth.name = 'Earth'
65
+
66
+ # Apply material to Earth
67
+ mat = bpy.data.materials.new(name="EarthMaterial")
68
+ mat.use_nodes = True
69
+ nodes = mat.node_tree.nodes
70
+ links = mat.node_tree.links
71
+
72
+ # Clear default nodes
73
+ for node in nodes:
74
+ nodes.remove(node)
75
+
76
+ # Create texture node
77
+ texture_node = nodes.new(type='ShaderNodeTexImage')
78
+
79
+ # Get texture path from environment variable if available
80
+ texture_paths = os.environ.get('TEXTURE_PATHS', '').split(',')
81
+ if texture_paths and texture_paths[0]:
82
+ texture_node.image = bpy.data.images.load(texture_paths[0])
83
+
84
+ # Add Principled BSDF node
85
+ principled = nodes.new(type='ShaderNodeBsdfPrincipled')
86
+ principled.inputs['Specular'].default_value = 0.1
87
+ principled.inputs['Roughness'].default_value = 0.8
88
+
89
+ # Add Output node
90
+ output = nodes.new(type='ShaderNodeOutputMaterial')
91
+
92
+ # Link nodes
93
+ links.new(texture_node.outputs['Color'], principled.inputs['Base Color'])
94
+ links.new(principled.outputs['BSDF'], output.inputs['Surface'])
95
+
96
+ # Assign material to Earth
97
+ if earth.data.materials:
98
+ earth.data.materials[0] = mat
99
+ else:
100
+ earth.data.materials.append(mat)
101
+
102
+ # Add a simple animation - rotation
103
+ earth.rotation_euler = (0, 0, 0)
104
+ earth.keyframe_insert(data_path="rotation_euler", frame=1)
105
+
106
+ # Rotate 360 degrees on Z axis
107
+ earth.rotation_euler = (0, 0, math.radians(360))
108
+ earth.keyframe_insert(data_path="rotation_euler", frame=250)
109
+
110
+ # Set animation interpolation to linear
111
+ for fc in earth.animation_data.action.fcurves:
112
+ for kf in fc.keyframe_points:
113
+ kf.interpolation = 'LINEAR'
114
+
115
+ # Setup camera
116
+ bpy.ops.object.camera_add(location=(0, -3, 0))
117
+ camera = bpy.context.active_object
118
+ camera.rotation_euler = (math.radians(90), 0, 0)
119
+ bpy.context.scene.camera = camera
120
+
121
+ # Setup lighting
122
+ bpy.ops.object.light_add(type='SUN', location=(5, -5, 5))
123
+ light = bpy.context.active_object
124
+ light.data.energy = 2.0
125
+
126
+ # Set render settings
127
+ bpy.context.scene.render.engine = 'CYCLES'
128
+ bpy.context.scene.cycles.device = 'CPU'
129
+ bpy.context.scene.render.film_transparent = True
130
+
131
+ # Save the .blend file in the current directory
132
+ blend_file_path = bpy.path.abspath("//scene.blend")
133
+ bpy.ops.wm.save_as_mainfile(filepath=blend_file_path)
134
+ """
135
+
136
+ # Sidebar: Blender script editor
137
+ st.sidebar.header("Blender Python Script")
138
+ script_text = st_ace(
139
+ value=default_script,
140
+ placeholder="Paste your Blender Python script here...",
141
+ language="python",
142
+ theme="monokai",
143
+ key="ace",
144
+ min_lines=20,
145
+ max_lines=100,
146
+ )
147
+
148
+ # Sidebar: texture uploads
149
+ st.sidebar.header("Texture Uploads (JPG/PNG)")
150
+ uploaded_textures = st.sidebar.file_uploader(
151
+ "Upload one or more textures", type=["jpg", "jpeg", "png"], accept_multiple_files=True
152
+ )
153
+
154
+ # Sidebar: custom Python libraries
155
+ st.sidebar.header("Custom Python Libraries")
156
+ custom_packages = st.sidebar.text_area(
157
+ "List pip packages (one per line)",
158
+ height=100
159
+ )
160
+
161
+ # Main action
162
+ if st.sidebar.button("Run & Export GLB"):
163
+ if not blender_path:
164
+ st.error("Cannot proceed: Blender is not available")
165
+ st.stop()
166
+
167
+ if not script_text or not script_text.strip():
168
+ st.error("Please provide a valid Blender Python script.")
169
+ st.stop()
170
+
171
+ with st.spinner("Processing your 3D scene..."):
172
+ # Use a more robust temporary directory approach
173
+ tmp_dir = tempfile.mkdtemp(prefix="blender_app_")
174
+ try:
175
+ # 1) Save user script
176
+ script_path = os.path.join(tmp_dir, "user_script.py")
177
+ with open(script_path, "w") as f:
178
+ f.write(script_text)
179
+
180
+ # 2) Collect textures
181
+ texture_paths = []
182
+ if uploaded_textures:
183
+ for idx, upload in enumerate(uploaded_textures):
184
+ ext = os.path.splitext(upload.name)[1]
185
+ path = os.path.join(tmp_dir, f"texture_{idx}{ext}")
186
+ with open(path, "wb") as tf:
187
+ tf.write(upload.read())
188
+ texture_paths.append(path)
189
+ else:
190
+ # Try multiple default textures if some fail
191
+ for url in DEFAULT_TEXTURE_URLS:
192
+ try:
193
+ r = requests.get(url, timeout=10)
194
+ r.raise_for_status()
195
+ ext = os.path.splitext(url)[-1] or ".jpg"
196
+ path = os.path.join(tmp_dir, f"default{ext}")
197
+ with open(path, "wb") as tf:
198
+ tf.write(r.content)
199
+ texture_paths.append(path)
200
+ # If we got one texture successfully, that's enough
201
+ break
202
+ except Exception as e:
203
+ st.warning(f"Could not download texture {url}: {str(e)}")
204
+
205
+ # Display texture file information
206
+ if texture_paths:
207
+ st.subheader("Texture Files")
208
+ for tp in texture_paths:
209
+ st.code(os.path.basename(tp))
210
+ else:
211
+ st.warning("No textures were loaded. Your 3D model may appear without textures.")
212
+
213
+ # 3) Install custom Python libraries if specified
214
+ if custom_packages and custom_packages.strip():
215
+ pkgs = [l.strip() for l in custom_packages.splitlines() if l.strip()]
216
+ if pkgs:
217
+ st.info(f"Installing: {', '.join(pkgs)}")
218
+ try:
219
+ # Use Python executable from current environment for pip
220
+ pip_cmd = [sys.executable, "-m", "pip", "install", "--user"] + pkgs
221
+ pip_res = subprocess.run(
222
+ pip_cmd,
223
+ check=True,
224
+ capture_output=True,
225
+ text=True,
226
+ timeout=120 # 2 minute timeout
227
+ )
228
+ st.text_area("pip install output", pip_res.stdout + pip_res.stderr, height=100)
229
+ except Exception as e:
230
+ st.warning(f"pip install failed: {str(e)}")
231
+
232
+ # 4) Prepare environment with minimal variables
233
+ env = os.environ.copy()
234
+ env["TEXTURE_PATHS"] = ",".join(texture_paths)
235
+
236
+ # 5) Run Blender to build .blend file
237
+ blend_path = os.path.join(tmp_dir, "scene.blend")
238
+
239
+ with st.status("Running Blender to create scene..."):
240
+ cmd1 = [blender_path, "--background", "--python", script_path]
241
+ try:
242
+ r1 = subprocess.run(
243
+ cmd1,
244
+ cwd=tmp_dir,
245
+ env=env,
246
+ check=True,
247
+ capture_output=True,
248
+ text=True,
249
+ timeout=180 # 3 minute timeout
250
+ )
251
+ st.code(r1.stdout.split("\n")[-10:], language="text") # Show just the last few lines
252
+ except subprocess.TimeoutExpired:
253
+ st.error("Blender process timed out after 3 minutes.")
254
+ st.stop()
255
+ except Exception as e:
256
+ st.error(f"Blender build failed: {str(e)}")
257
+ st.stop()
258
+
259
+ # Check if blend file was created
260
+ if not os.path.exists(blend_path):
261
+ st.error("Blender did not create the expected scene.blend file.")
262
+ st.stop()
263
+
264
+ # 6) Export GLB with animation
265
+ glb_path = os.path.join(tmp_dir, "animation.glb")
266
+
267
+ with st.status("Exporting to GLB format..."):
268
+ expr = (
269
+ "import bpy;"
270
+ "bpy.ops.wm.open_mainfile(filepath=r'" + blend_path + "');"
271
+ "bpy.ops.export_scene.gltf(filepath=r'" + glb_path +
272
+ "', export_format='GLB', export_animations=True)"
273
+ )
274
+
275
+ cmd2 = [blender_path, "--background", "--python-expr", expr]
276
+ try:
277
+ r2 = subprocess.run(
278
+ cmd2,
279
+ cwd=tmp_dir,
280
+ env=env,
281
+ check=True,
282
+ capture_output=True,
283
+ text=True,
284
+ timeout=120 # 2 minute timeout
285
+ )
286
+ except Exception as e:
287
+ st.error(f"GLB export failed: {str(e)}")
288
+ st.stop()
289
+
290
+ # 7) Embed GLB inline if it exists
291
+ if os.path.exists(glb_path):
292
+ with open(glb_path, 'rb') as f:
293
+ data = f.read()
294
+
295
+ b64 = base64.b64encode(data).decode()
296
+
297
+ # Display the 3D model viewer
298
+ st.subheader("3D Model Viewer")
299
+ html = f"""
300
+ <script type="module" src="https://unpkg.com/@google/model-viewer@latest/dist/model-viewer.min.js"></script>
301
+ <model-viewer
302
+ src="data:model/gltf-binary;base64,{b64}"
303
+ alt="3D Model"
304
+ auto-rotate
305
+ camera-controls
306
+ style="width:100%; height:600px;">
307
+ </model-viewer>
308
+ """
309
+ st.components.v1.html(html, height=650)
310
+
311
+ # Add download button
312
+ st.download_button(
313
+ "⬇️ Download GLB File",
314
+ data,
315
+ file_name="animation.glb",
316
+ mime="model/gltf-binary"
317
+ )
318
+ else:
319
+ st.error("GLB file was not generated successfully.")
320
+
321
+ finally:
322
+ # Clean up - remove temporary directory
323
+ try:
324
+ shutil.rmtree(tmp_dir, ignore_errors=True)
325
+ except:
326
+ pass
327
+
328
+ # Add helpful information at the bottom
329
+ st.divider()
330
+ with st.expander("About this app"):
331
+ st.markdown("""
332
+ **Blender 3D Viewer App** lets you:
333
+
334
+ - Write Blender Python scripts directly in your browser
335
+ - Upload custom textures for your 3D models
336
+ - Generate and visualize 3D models with animations
337
+ - Download the results as GLB files for use in other applications
338
+
339
+ The example script creates a rotating Earth with the uploaded texture.
340
+ """)
341
+
342
+ st.info("If the app isn't working, make sure 'blender' is properly installed in the Spaces environment.")
apt.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ blender
2
+ ffmpeg
3
+ libsm6
4
+ libxext6
5
+ libgl1-mesa-glx
6
+ libglu1-mesa
7
+ python3-numpy
8
+ xvfb
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ streamlit>=1.25.0
2
+ requests
3
+ beautifulsoup4
4
+ streamlit_ace
5
+ Pillow