MySafeCode commited on
Commit
06a5ef5
·
verified ·
1 Parent(s): 5c0d072

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +95 -163
app.py CHANGED
@@ -3,10 +3,10 @@ import numpy as np
3
  import trimesh
4
  import tempfile
5
  import os
6
- from pathlib import Path
7
- import logging
8
- from typing import Tuple, Optional
9
  import struct
 
 
 
10
 
11
  # Configure logging
12
  logging.basicConfig(level=logging.INFO)
@@ -14,101 +14,82 @@ logger = logging.getLogger(__name__)
14
 
15
  class VoxParser:
16
  """Parse MagicaVoxel .vox files"""
17
- def __init__(self, file_data):
18
- self.file_data = file_data
19
- self.voxels = []
20
- self.palette = []
21
- self.size = {}
22
- self.offset = 0
23
 
24
  def parse(self) -> dict:
25
  """Parse the .vox file and extract voxel data"""
26
  try:
27
- # Handle both bytes and file objects
28
- if hasattr(self.file_data, 'read'):
29
- data = self.file_data.read()
30
- else:
31
- data = self.file_data
32
 
33
- self.buffer = data
34
- self.view = memoryview(data)
35
- self.offset = 0
36
 
37
  # Read header
38
- header = self.read_string(4)
 
39
  if header != 'VOX ':
40
  raise ValueError("Invalid VOX file header")
41
 
42
- version = self.read_int32()
 
 
 
 
 
43
 
44
  # Parse chunks
45
- while self.offset < len(data):
46
- chunk_id = self.read_string(4)
47
- chunk_size = self.read_int32()
48
- child_size = self.read_int32()
 
 
 
49
 
50
  if chunk_id == 'SIZE':
51
- self.parse_size(chunk_size)
 
 
 
 
 
52
  elif chunk_id == 'XYZI':
53
- self.parse_voxels(chunk_size)
 
 
 
 
 
 
 
 
 
 
 
 
54
  elif chunk_id == 'RGBA':
55
- self.parse_palette(chunk_size)
 
 
 
 
56
  else:
57
- self.offset += chunk_size
58
 
59
- # Safety check
60
- if self.offset >= len(data):
61
  break
62
-
63
  return {
64
- 'voxels': self.voxels,
65
- 'palette': self.palette or self._default_palette(),
66
- 'size': self.size
67
  }
68
  except Exception as e:
69
- logger.error(f"Error parsing VOX file: {e}")
70
- raise
71
-
72
- def parse_size(self, size: int):
73
- """Parse size chunk"""
74
- if self.offset + 12 <= len(self.buffer):
75
- self.size = {
76
- 'x': self.read_int32(),
77
- 'y': self.read_int32(),
78
- 'z': self.read_int32()
79
- }
80
-
81
- def parse_voxels(self, size: int):
82
- """Parse voxel data"""
83
- if self.offset + 4 <= len(self.buffer):
84
- num_voxels = self.read_int32()
85
-
86
- for i in range(num_voxels):
87
- if self.offset + 4 <= len(self.buffer):
88
- x, y, z, color_index = struct.unpack('BBBB', self.buffer[self.offset:self.offset+4])
89
- self.voxels.append({
90
- 'x': x,
91
- 'y': y,
92
- 'z': z,
93
- 'color_index': color_index
94
- })
95
- self.offset += 4
96
-
97
- def parse_palette(self, size: int):
98
- """Parse palette data"""
99
- for i in range(256):
100
- if self.offset + 4 <= len(self.buffer):
101
- r, g, b, a = struct.unpack('BBBB', self.buffer[self.offset:self.offset+4])
102
- self.palette.append({
103
- 'r': r,
104
- 'g': g,
105
- 'b': b,
106
- 'a': a
107
- })
108
- self.offset += 4
109
 
110
  def _default_palette(self) -> list:
111
- """Default MagicaVoxel palette if none found"""
112
  colors = []
113
  for i in range(256):
114
  intensity = i / 255.0
@@ -120,41 +101,17 @@ class VoxParser:
120
  })
121
  return colors
122
 
123
- def read_string(self, length: int) -> str:
124
- """Read string from buffer"""
125
- if self.offset + length <= len(self.buffer):
126
- result = self.buffer[self.offset:self.offset+length].tobytes().decode('ascii', errors='ignore')
127
- self.offset += length
128
- return result
129
- return ""
130
-
131
- def read_int32(self) -> int:
132
- """Read 32-bit integer"""
133
- if self.offset + 4 <= len(self.buffer):
134
- result = struct.unpack('<I', self.buffer[self.offset:self.offset+4])[0]
135
- self.offset += 4
136
- return result
137
- return 0
138
-
139
  class VoxToGlbConverter:
140
  """Convert MagicaVoxel files to GLB format"""
141
 
142
  def __init__(self):
143
  self.voxel_size = 1.0
144
 
145
- def vox_to_glb(self, vox_file) -> Tuple[str, str]:
146
- """
147
- Convert .vox file to .glb file
148
-
149
- Args:
150
- vox_file: Gradio file object
151
-
152
- Returns:
153
- Tuple of (glb_file_path, status_message)
154
- """
155
  try:
156
- # Parse the .vox file directly from the file object
157
- parser = VoxParser(vox_file)
158
  voxel_data = parser.parse()
159
 
160
  if not voxel_data['voxels']:
@@ -178,7 +135,7 @@ class VoxToGlbConverter:
178
  voxels = voxel_data['voxels']
179
  palette = voxel_data['palette']
180
 
181
- # Group voxels by color for better performance
182
  color_groups = {}
183
  for voxel in voxels:
184
  color_idx = voxel['color_index']
@@ -186,122 +143,97 @@ class VoxToGlbConverter:
186
  color_groups[color_idx] = []
187
  color_groups[color_idx].append(voxel)
188
 
189
- # Create meshes for each color group
190
  meshes = []
191
 
192
  for color_idx, voxels in color_groups.items():
193
  color = palette[color_idx] if color_idx < len(palette) else {'r': 255, 'g': 255, 'b': 255, 'a': 255}
194
 
195
- # Create instanced cubes for this color
196
  cube = trimesh.creation.box(extents=[self.voxel_size, self.voxel_size, self.voxel_size])
197
 
198
  for voxel in voxels:
199
- # Position the cube
200
  translation = trimesh.transformations.translation_matrix([
201
  voxel['x'] * self.voxel_size,
202
- voxel['z'] * self.voxel_size, # Swap Y and Z for proper orientation
203
  voxel['y'] * self.voxel_size
204
  ])
205
 
206
  transformed_cube = cube.copy()
207
  transformed_cube.apply_transform(translation)
208
 
209
- # Set vertex colors
210
  vertex_colors = np.tile([color['r']/255, color['g']/255, color['b']/255, color['a']/255],
211
  (len(transformed_cube.vertices), 1))
212
  transformed_cube.visual.vertex_colors = vertex_colors
213
 
214
  meshes.append(transformed_cube)
215
 
216
- # Combine all meshes
217
  if meshes:
218
  combined = trimesh.util.concatenate(meshes)
219
  return combined
220
  else:
221
- # Fallback: create a simple cube
222
  return trimesh.creation.box(extents=[self.voxel_size, self.voxel_size, self.voxel_size])
223
 
224
  def process_vox_file(vox_file) -> Tuple[str, str]:
225
- """Process uploaded .vox file and convert to .glb"""
226
  if vox_file is None:
227
  return "", "Please upload a .vox file"
228
 
229
  try:
230
  converter = VoxToGlbConverter()
231
- glb_path, message = converter.vox_to_glb(vox_file)
232
- return glb_path, message
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
233
  except Exception as e:
234
  return "", f"Error: {str(e)}"
235
 
236
  def create_gradio_interface():
237
- """Create the Gradio interface"""
238
-
239
  with gr.Blocks(title="VOX to GLB Converter", theme=gr.themes.Soft()) as app:
240
  gr.Markdown("""
241
  # 🧊 MagicaVoxel to GLB Converter
242
-
243
- Convert your MagicaVoxel `.vox` files to `.glb` format for preview and use in 3D applications.
244
-
245
- **Features:**
246
- - ✅ Preserves voxel colors and structure
247
- - ✅ Optimized for 3D preview
248
- - ✅ Handles legacy VOX formats (2019-2020)
249
- - ✅ Downloads as GLB file
250
  """)
251
 
252
  with gr.Row():
253
  with gr.Column():
254
- vox_input = gr.File(
255
- label="Upload VOX File",
256
- file_types=[".vox"],
257
- file_count="single"
258
- )
259
-
260
  convert_btn = gr.Button("Convert to GLB", variant="primary")
261
-
262
- status_output = gr.Textbox(
263
- label="Status",
264
- interactive=False,
265
- placeholder="Ready to convert..."
266
- )
267
 
268
  with gr.Column():
269
- glb_output = gr.File(
270
- label="Download GLB File",
271
- file_types=[".glb"],
272
- interactive=False
273
- )
274
-
275
- preview_info = gr.Markdown("""
276
- **Preview Info:**
277
- - Download the GLB file
278
- - Drag into any 3D viewer
279
- - Compatible with Three.js, Babylon.js, Unity, Blender
280
- """)
281
 
282
- # Event handlers
283
  convert_btn.click(
284
  fn=process_vox_file,
285
  inputs=[vox_input],
286
  outputs=[glb_output, status_output]
287
  )
288
-
289
- gr.Markdown("### 📁 Example Usage")
290
- gr.Markdown("""
291
- **How to use:**
292
- 1. Click "Upload VOX File" and select your `.vox` file
293
- 2. Click "Convert to GLB"
294
- 3. Download the converted `.glb` file
295
- 4. Preview in any 3D viewer or use in your projects
296
- """)
297
 
298
  return app
299
 
300
  if __name__ == "__main__":
301
  app = create_gradio_interface()
302
- app.launch(
303
- server_name="0.0.0.0",
304
- server_port=7860,
305
- share=True,
306
- show_error=True
307
- )
 
3
  import trimesh
4
  import tempfile
5
  import os
 
 
 
6
  import struct
7
+ import logging
8
+ from pathlib import Path
9
+ from typing import Tuple
10
 
11
  # Configure logging
12
  logging.basicConfig(level=logging.INFO)
 
14
 
15
  class VoxParser:
16
  """Parse MagicaVoxel .vox files"""
17
+
18
+ def __init__(self, file_path):
19
+ self.file_path = file_path
 
 
 
20
 
21
  def parse(self) -> dict:
22
  """Parse the .vox file and extract voxel data"""
23
  try:
24
+ with open(self.file_path, 'rb') as f:
25
+ data = f.read()
 
 
 
26
 
27
+ offset = 0
 
 
28
 
29
  # Read header
30
+ header = data[offset:offset+4].decode('ascii', errors='ignore')
31
+ offset += 4
32
  if header != 'VOX ':
33
  raise ValueError("Invalid VOX file header")
34
 
35
+ version = struct.unpack('<I', data[offset:offset+4])[0]
36
+ offset += 4
37
+
38
+ voxels = []
39
+ palette = []
40
+ size = {}
41
 
42
  # Parse chunks
43
+ while offset < len(data):
44
+ chunk_id = data[offset:offset+4].decode('ascii', errors='ignore')
45
+ offset += 4
46
+ chunk_size = struct.unpack('<I', data[offset:offset+4])[0]
47
+ offset += 4
48
+ child_size = struct.unpack('<I', data[offset:offset+4])[0]
49
+ offset += 4
50
 
51
  if chunk_id == 'SIZE':
52
+ size = {
53
+ 'x': struct.unpack('<I', data[offset:offset+4])[0],
54
+ 'y': struct.unpack('<I', data[offset+4:offset+8])[0],
55
+ 'z': struct.unpack('<I', data[offset+8:offset+12])[0]
56
+ }
57
+ offset += chunk_size
58
  elif chunk_id == 'XYZI':
59
+ num_voxels = struct.unpack('<I', data[offset:offset+4])[0]
60
+ offset += 4
61
+
62
+ for i in range(num_voxels):
63
+ if offset + 4 <= len(data):
64
+ x, y, z, color_index = struct.unpack('BBBB', data[offset:offset+4])
65
+ voxels.append({
66
+ 'x': x,
67
+ 'y': y,
68
+ 'z': z,
69
+ 'color_index': color_index
70
+ })
71
+ offset += 4
72
  elif chunk_id == 'RGBA':
73
+ for i in range(256):
74
+ if offset + 4 <= len(data):
75
+ r, g, b, a = struct.unpack('BBBB', data[offset:offset+4])
76
+ palette.append({'r': r, 'g': g, 'b': b, 'a': a})
77
+ offset += 4
78
  else:
79
+ offset += chunk_size
80
 
81
+ if offset >= len(data):
 
82
  break
83
+
84
  return {
85
+ 'voxels': voxels,
86
+ 'palette': palette or self._default_palette(),
87
+ 'size': size
88
  }
89
  except Exception as e:
90
+ raise ValueError(f"Error parsing VOX file: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
 
92
  def _default_palette(self) -> list:
 
93
  colors = []
94
  for i in range(256):
95
  intensity = i / 255.0
 
101
  })
102
  return colors
103
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  class VoxToGlbConverter:
105
  """Convert MagicaVoxel files to GLB format"""
106
 
107
  def __init__(self):
108
  self.voxel_size = 1.0
109
 
110
+ def vox_to_glb(self, vox_file_path: str) -> Tuple[str, str]:
111
+ """Convert .vox file to .glb file"""
 
 
 
 
 
 
 
 
112
  try:
113
+ # Parse the .vox file
114
+ parser = VoxParser(vox_file_path)
115
  voxel_data = parser.parse()
116
 
117
  if not voxel_data['voxels']:
 
135
  voxels = voxel_data['voxels']
136
  palette = voxel_data['palette']
137
 
138
+ # Group voxels by color
139
  color_groups = {}
140
  for voxel in voxels:
141
  color_idx = voxel['color_index']
 
143
  color_groups[color_idx] = []
144
  color_groups[color_idx].append(voxel)
145
 
 
146
  meshes = []
147
 
148
  for color_idx, voxels in color_groups.items():
149
  color = palette[color_idx] if color_idx < len(palette) else {'r': 255, 'g': 255, 'b': 255, 'a': 255}
150
 
 
151
  cube = trimesh.creation.box(extents=[self.voxel_size, self.voxel_size, self.voxel_size])
152
 
153
  for voxel in voxels:
 
154
  translation = trimesh.transformations.translation_matrix([
155
  voxel['x'] * self.voxel_size,
156
+ voxel['z'] * self.voxel_size,
157
  voxel['y'] * self.voxel_size
158
  ])
159
 
160
  transformed_cube = cube.copy()
161
  transformed_cube.apply_transform(translation)
162
 
 
163
  vertex_colors = np.tile([color['r']/255, color['g']/255, color['b']/255, color['a']/255],
164
  (len(transformed_cube.vertices), 1))
165
  transformed_cube.visual.vertex_colors = vertex_colors
166
 
167
  meshes.append(transformed_cube)
168
 
 
169
  if meshes:
170
  combined = trimesh.util.concatenate(meshes)
171
  return combined
172
  else:
 
173
  return trimesh.creation.box(extents=[self.voxel_size, self.voxel_size, self.voxel_size])
174
 
175
  def process_vox_file(vox_file) -> Tuple[str, str]:
176
+ """Process uploaded .vox file and convert to .glb - FIXED for Error 21"""
177
  if vox_file is None:
178
  return "", "Please upload a .vox file"
179
 
180
  try:
181
  converter = VoxToGlbConverter()
182
+
183
+ # PROPER FIX FOR ERROR 21:
184
+ # Create temp file and copy the actual uploaded file
185
+ with tempfile.NamedTemporaryFile(delete=False, suffix='.vox') as tmp_file:
186
+ # Get the actual file path from Gradio's file object
187
+ if hasattr(vox_file, 'name'):
188
+ real_file_path = vox_file.name
189
+
190
+ # Verify it's a file, not a directory
191
+ if os.path.isfile(real_file_path):
192
+ # Copy the uploaded file to temp location
193
+ import shutil
194
+ shutil.copy(real_file_path, tmp_file.name)
195
+ temp_vox_path = tmp_file.name
196
+ else:
197
+ return "", "Could not access uploaded file - may be a directory"
198
+ else:
199
+ return "", "Invalid file format"
200
+
201
+ # Convert using the temp file path
202
+ glb_path, message = converter.vox_to_glb(temp_vox_path)
203
+
204
+ # Clean up temp file
205
+ if os.path.exists(temp_vox_path):
206
+ os.unlink(temp_vox_path)
207
+
208
+ return glb_path, message
209
+
210
  except Exception as e:
211
  return "", f"Error: {str(e)}"
212
 
213
  def create_gradio_interface():
 
 
214
  with gr.Blocks(title="VOX to GLB Converter", theme=gr.themes.Soft()) as app:
215
  gr.Markdown("""
216
  # 🧊 MagicaVoxel to GLB Converter
217
+ Convert your MagicaVoxel `.vox` files to `.glb` format
 
 
 
 
 
 
 
218
  """)
219
 
220
  with gr.Row():
221
  with gr.Column():
222
+ vox_input = gr.File(label="Upload VOX File", file_types=[".vox"], file_count="single")
 
 
 
 
 
223
  convert_btn = gr.Button("Convert to GLB", variant="primary")
224
+ status_output = gr.Textbox(label="Status", interactive=False, placeholder="Ready...")
 
 
 
 
 
225
 
226
  with gr.Column():
227
+ glb_output = gr.File(label="Download GLB File", file_types=[".glb"], interactive=False)
 
 
 
 
 
 
 
 
 
 
 
228
 
 
229
  convert_btn.click(
230
  fn=process_vox_file,
231
  inputs=[vox_input],
232
  outputs=[glb_output, status_output]
233
  )
 
 
 
 
 
 
 
 
 
234
 
235
  return app
236
 
237
  if __name__ == "__main__":
238
  app = create_gradio_interface()
239
+ app.launch(server_name="0.0.0.0", server_port=7860, share=True, show_error=True)