opsiclear-admin commited on
Commit
a604f5d
·
verified ·
1 Parent(s): 02da9ed

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +252 -126
app.py CHANGED
@@ -103,88 +103,92 @@ DEFAULT_STEP = 3
103
 
104
 
105
  css = """
106
- /* ColmapView Dark Theme */
107
  :root {
108
  --bg-void: #0a0a0a;
109
  --bg-primary: #0f0f0f;
110
  --bg-secondary: #161616;
111
  --bg-tertiary: #1e1e1e;
112
- --bg-input: #1a1a1a;
113
- --bg-elevated: #242424;
114
- --bg-hover: #262626;
115
  --text-primary: #e8e8e8;
116
  --text-secondary: #8a8a8a;
117
- --text-muted: #5a5a5a;
118
- --border-subtle: #222222;
119
  --border-color: #2a2a2a;
120
  --border-light: #3a3a3a;
121
  --accent: #b8b8b8;
122
- --accent-hover: #d0d0d0;
123
- --accent-dim: rgba(184, 184, 184, 0.12);
124
- --shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.4), 0 1px 2px rgba(0, 0, 0, 0.3);
125
- --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.5), 0 2px 4px rgba(0, 0, 0, 0.3);
126
- --radius: 0.25rem;
127
- --radius-md: 0.375rem;
128
- --radius-lg: 0.5rem;
129
- --font-sans: 'Roboto', -apple-system, BlinkMacSystemFont, sans-serif;
130
- --font-mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
131
- }
132
-
133
- /* Global Overrides */
134
- .gradio-container { background: var(--bg-primary) !important; color: var(--text-primary) !important; font-family: var(--font-sans) !important; }
135
- .dark { background: var(--bg-primary) !important; }
136
- body { background: var(--bg-void) !important; color-scheme: dark; }
137
-
138
- /* Panels & Blocks */
139
- .block { background: var(--bg-secondary) !important; border: 1px solid var(--border-color) !important; border-radius: var(--radius-lg) !important; }
140
- .panel { background: var(--bg-secondary) !important; }
141
-
142
- /* Inputs */
143
- input, textarea, select { background: var(--bg-input) !important; border: 1px solid var(--border-color) !important; color: var(--text-primary) !important; border-radius: var(--radius) !important; }
144
- input:focus, textarea:focus, select:focus { border-color: var(--accent) !important; outline: none !important; }
145
-
146
- /* Buttons */
147
- .primary { background: var(--accent) !important; color: var(--bg-void) !important; border: none !important; }
148
- .primary:hover { background: var(--accent-hover) !important; }
149
- .secondary { background: var(--bg-hover) !important; color: var(--text-primary) !important; border: 1px solid var(--border-color) !important; }
150
- .secondary:hover { background: var(--bg-tertiary) !important; }
151
- button { transition: all 0.15s ease-out !important; }
152
-
153
- /* Labels & Text */
154
- label, .label-wrap { color: var(--text-secondary) !important; font-size: 0.875rem !important; }
155
- .prose { color: var(--text-primary) !important; }
156
- .prose h2 { color: var(--text-primary) !important; border: none !important; }
157
-
158
- /* Sliders */
159
- input[type="range"] { accent-color: var(--accent) !important; }
160
-
161
- /* Gallery */
162
- .gallery { background: var(--bg-tertiary) !important; border: 1px solid var(--border-color) !important; }
163
 
164
- /* Accordion */
165
- .accordion { background: var(--bg-secondary) !important; border: 1px solid var(--border-color) !important; }
 
 
166
 
167
- /* Scrollbar */
168
- ::-webkit-scrollbar { width: 12px; }
169
- ::-webkit-scrollbar-track { background: var(--bg-secondary); }
170
- ::-webkit-scrollbar-thumb { background: var(--border-light); border-radius: 9999px; }
171
- ::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }
172
-
173
- /* Gradio Overrides */
174
- .stepper-wrapper { padding: 0; }
175
- .stepper-container { padding: 0; align-items: center; }
176
- .step-button { flex-direction: row; }
177
- .step-connector { transform: none; }
178
- .step-number { width: 16px; height: 16px; background: var(--bg-tertiary) !important; border: 1px solid var(--border-color) !important; }
179
- .step-label { position: relative; bottom: 0; color: var(--text-secondary) !important; }
180
- .wrap.center.full { inset: 0; height: 100%; }
181
- .wrap.center.full.translucent { background: var(--bg-secondary); }
182
- .meta-text-center { display: block !important; position: absolute !important; top: unset !important; bottom: 0 !important; right: 0 !important; transform: unset !important; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
 
184
  /* Previewer */
185
  .previewer-container {
186
  position: relative;
187
- font-family: var(--font-sans);
188
  width: 100%;
189
  height: 722px;
190
  margin: 0 auto;
@@ -194,52 +198,168 @@ input[type="range"] { accent-color: var(--accent) !important; }
194
  align-items: center;
195
  justify-content: center;
196
  background: var(--bg-void);
197
- border-radius: var(--radius-lg);
198
  }
 
199
  .previewer-container .tips-icon {
200
- position: absolute; right: 10px; top: 10px; z-index: 10;
201
- border-radius: var(--radius-md); color: var(--bg-void);
202
- background-color: var(--accent); padding: 4px 8px;
203
- user-select: none; font-size: 0.75rem; font-weight: 500;
 
 
 
 
 
204
  }
 
205
  .previewer-container .tips-text {
206
- position: absolute; right: 10px; top: 45px;
207
- color: var(--text-primary); background-color: var(--bg-elevated);
208
- border: 1px solid var(--border-light); border-radius: var(--radius-md);
209
- padding: 8px 12px; text-align: left; max-width: 280px; z-index: 10;
210
- transition: opacity 0.15s ease-out; opacity: 0; user-select: none;
211
- box-shadow: var(--shadow-md);
212
- }
213
- .previewer-container .tips-text p { font-size: 0.75rem; line-height: 1.4; color: var(--text-secondary); margin: 4px 0; }
214
- .tips-icon:hover + .tips-text { opacity: 1; }
215
- .previewer-container .mode-row { width: 100%; display: flex; gap: 8px; justify-content: center; margin-bottom: 16px; flex-wrap: wrap; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
  .previewer-container .mode-btn {
217
- width: 28px; height: 28px; border-radius: 50%; cursor: pointer;
218
- opacity: 0.5; transition: all 0.15s ease-out;
219
- border: 2px solid var(--border-light); object-fit: cover;
220
- }
221
- .previewer-container .mode-btn:hover { opacity: 0.85; transform: scale(1.1); border-color: var(--text-muted); }
222
- .previewer-container .mode-btn.active { opacity: 1; border-color: var(--accent); transform: scale(1.1); }
223
- .previewer-container .display-row { margin-bottom: 16px; min-height: 400px; width: 100%; flex-grow: 1; display: flex; justify-content: center; align-items: center; }
224
- .previewer-container .previewer-main-image { max-width: 100%; max-height: 100%; flex-grow: 1; object-fit: contain; display: none; }
225
- .previewer-container .previewer-main-image.visible { display: block; }
226
- .previewer-container .slider-row { width: 100%; display: flex; flex-direction: column; align-items: center; gap: 8px; padding: 0 16px; }
227
- .previewer-container input[type=range] { -webkit-appearance: none; width: 100%; max-width: 400px; background: transparent; height: 20px; }
228
- .previewer-container input[type=range]::-webkit-slider-runnable-track { width: 100%; height: 4px; cursor: pointer; background: var(--bg-tertiary); border-radius: 9999px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229
  .previewer-container input[type=range]::-webkit-slider-thumb {
230
- height: 12px; width: 12px; border-radius: 50%; background: var(--accent);
231
- cursor: pointer; -webkit-appearance: none; margin-top: -4px;
232
- box-shadow: var(--shadow-sm); transition: transform 0.1s ease-out;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
233
  }
234
- .previewer-container input[type=range]::-webkit-slider-thumb:hover { transform: scale(1.2); background: var(--accent-hover); }
235
- .gradio-container .padded:has(.previewer-container) { padding: 0 !important; }
236
- .gradio-container:has(.previewer-container) [data-testid="block-label"] { position: absolute; top: 0; left: 0; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
237
  """
238
 
239
 
240
  head = """
241
  <script>
242
  function refreshView(mode, step) {
 
243
  const allImgs = document.querySelectorAll('.previewer-main-image');
244
  for (let i = 0; i < allImgs.length; i++) {
245
  const img = allImgs[i];
@@ -251,18 +371,36 @@ head = """
251
  break;
252
  }
253
  }
 
 
254
  allImgs.forEach(img => img.classList.remove('visible'));
 
 
255
  const targetId = 'view-m' + mode + '-s' + step;
256
  const targetImg = document.getElementById(targetId);
257
- if (targetImg) { targetImg.classList.add('visible'); }
 
 
 
 
 
 
258
  const allBtns = document.querySelectorAll('.mode-btn');
259
  allBtns.forEach((btn, idx) => {
260
  if (idx === mode) btn.classList.add('active');
261
  else btn.classList.remove('active');
262
  });
263
  }
264
- function selectMode(mode) { refreshView(mode, -1); }
265
- function onSliderChange(val) { refreshView(-1, parseInt(val)); }
 
 
 
 
 
 
 
 
266
  </script>
267
  """
268
 
@@ -368,7 +506,7 @@ def get_seed(randomize_seed: bool, seed: int) -> int:
368
 
369
 
370
  def prepare_examples() -> List[List[str]]:
371
- """Prepare examples as lists of image paths (not concatenated)."""
372
  example_dir = "assets/example_multi_image"
373
  if not os.path.exists(example_dir):
374
  return []
@@ -377,7 +515,7 @@ def prepare_examples() -> List[List[str]]:
377
  examples = []
378
  for case in sorted(cases):
379
  case_images = []
380
- for i in range(1, 10): # Support up to 9 images per example
381
  img_path = f'{example_dir}/{case}_{i}.png'
382
  if os.path.exists(img_path):
383
  case_images.append(img_path)
@@ -497,24 +635,13 @@ def image_to_3d(
497
  torch.cuda.empty_cache()
498
 
499
  # --- HTML Construction ---
500
- def encode_preview_image(args):
501
- m_idx, s_idx, render_key = args
502
- img_base64 = image_to_base64(Image.fromarray(render_images[render_key][s_idx]))
503
- return (m_idx, s_idx, img_base64)
504
-
505
- encode_tasks = [(m_idx, s_idx, mode['render_key']) for m_idx, mode in enumerate(MODES) for s_idx in range(STEPS)]
506
-
507
- with ThreadPoolExecutor(max_workers=8) as executor:
508
- encoded_results = list(executor.map(encode_preview_image, encode_tasks))
509
-
510
- encoded_map = {(m, s): b64 for m, s, b64 in encoded_results}
511
  images_html = ""
512
  for m_idx, mode in enumerate(MODES):
513
  for s_idx in range(STEPS):
514
  unique_id = f"view-m{m_idx}-s{s_idx}"
515
  is_visible = (m_idx == DEFAULT_MODE and s_idx == DEFAULT_STEP)
516
  vis_class = "visible" if is_visible else ""
517
- img_base64 = encoded_map[(m_idx, s_idx)]
518
  images_html += f'<img id="{unique_id}" class="previewer-main-image {vis_class}" src="{img_base64}" loading="eager">'
519
 
520
  btns_html = ""
@@ -581,22 +708,22 @@ def extract_glb(
581
 
582
  with gr.Blocks(delete_cache=(600, 600)) as demo:
583
  gr.Markdown("""
584
- ## Multi-View Image to 3D with [TRELLIS.2](https://microsoft.github.io/TRELLIS.2)
585
- Upload one or more images of an object and click Generate to create a 3D asset.
586
- Multiple views from different angles will produce better results.
 
587
  """)
588
 
589
  with gr.Row():
590
  with gr.Column(scale=1, min_width=360):
591
  image_prompt = gr.Gallery(
592
- label="Input Images (upload 1 or more views)",
593
  format="png",
594
  type="pil",
595
  height=400,
596
  columns=3,
597
  object_fit="contain"
598
  )
599
- gr.Markdown("*Upload multiple views of the same object for better 3D reconstruction.*")
600
 
601
  resolution = gr.Radio(["512", "1024", "1536"], label="Resolution", value="1024")
602
  seed = gr.Slider(0, MAX_SEED, label="Seed", value=0, step=1)
@@ -604,7 +731,7 @@ with gr.Blocks(delete_cache=(600, 600)) as demo:
604
  decimation_target = gr.Slider(100000, 500000, label="Decimation Target", value=300000, step=10000)
605
  texture_size = gr.Slider(1024, 4096, label="Texture Size", value=2048, step=1024)
606
 
607
- generate_btn = gr.Button("Generate", variant="primary")
608
 
609
  with gr.Accordion(label="Advanced Settings", open=False):
610
  gr.Markdown("Stage 1: Sparse Structure Generation")
@@ -637,11 +764,10 @@ with gr.Blocks(delete_cache=(600, 600)) as demo:
637
  download_btn = gr.DownloadButton(label="Download GLB")
638
  gr.Markdown("*GLB extraction may take 30+ seconds.*")
639
 
640
- with gr.Column(scale=1, min_width=200):
641
- gr.Markdown("### Examples")
642
- # Create example buttons that load images into gallery
643
  example_data = prepare_examples()
644
- for i, example_paths in enumerate(example_data[:12]): # Limit to 12 examples
645
  case_name = os.path.basename(example_paths[0]).split('_')[0]
646
  btn = gr.Button(f"{case_name} ({len(example_paths)} views)", size="sm")
647
  btn.click(
 
103
 
104
 
105
  css = """
106
+ /* ColmapView Dark Theme - Color Overrides */
107
  :root {
108
  --bg-void: #0a0a0a;
109
  --bg-primary: #0f0f0f;
110
  --bg-secondary: #161616;
111
  --bg-tertiary: #1e1e1e;
 
 
 
112
  --text-primary: #e8e8e8;
113
  --text-secondary: #8a8a8a;
 
 
114
  --border-color: #2a2a2a;
115
  --border-light: #3a3a3a;
116
  --accent: #b8b8b8;
117
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
 
119
+ body, .gradio-container, .dark {
120
+ background: var(--bg-primary) !important;
121
+ color: var(--text-primary) !important;
122
+ }
123
 
124
+ .block {
125
+ background: var(--bg-secondary) !important;
126
+ border-color: var(--border-color) !important;
127
+ }
128
+
129
+ input, textarea, select {
130
+ background: var(--bg-tertiary) !important;
131
+ border-color: var(--border-color) !important;
132
+ color: var(--text-primary) !important;
133
+ }
134
+
135
+ label, .label-wrap {
136
+ color: var(--text-secondary) !important;
137
+ }
138
+
139
+ /* Overwrite Gradio Default Style */
140
+ .stepper-wrapper {
141
+ padding: 0;
142
+ }
143
+
144
+ .stepper-container {
145
+ padding: 0;
146
+ align-items: center;
147
+ }
148
+
149
+ .step-button {
150
+ flex-direction: row;
151
+ }
152
+
153
+ .step-connector {
154
+ transform: none;
155
+ }
156
+
157
+ .step-number {
158
+ width: 16px;
159
+ height: 16px;
160
+ background: var(--bg-tertiary) !important;
161
+ border-color: var(--border-color) !important;
162
+ }
163
+
164
+ .step-label {
165
+ position: relative;
166
+ bottom: 0;
167
+ color: var(--text-secondary) !important;
168
+ }
169
+
170
+ .wrap.center.full {
171
+ inset: 0;
172
+ height: 100%;
173
+ }
174
+
175
+ .wrap.center.full.translucent {
176
+ background: var(--bg-secondary);
177
+ }
178
+
179
+ .meta-text-center {
180
+ display: block !important;
181
+ position: absolute !important;
182
+ top: unset !important;
183
+ bottom: 0 !important;
184
+ right: 0 !important;
185
+ transform: unset !important;
186
+ }
187
 
188
  /* Previewer */
189
  .previewer-container {
190
  position: relative;
191
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
192
  width: 100%;
193
  height: 722px;
194
  margin: 0 auto;
 
198
  align-items: center;
199
  justify-content: center;
200
  background: var(--bg-void);
 
201
  }
202
+
203
  .previewer-container .tips-icon {
204
+ position: absolute;
205
+ right: 10px;
206
+ top: 10px;
207
+ z-index: 10;
208
+ border-radius: 10px;
209
+ color: var(--bg-void);
210
+ background-color: var(--accent);
211
+ padding: 3px 6px;
212
+ user-select: none;
213
  }
214
+
215
  .previewer-container .tips-text {
216
+ position: absolute;
217
+ right: 10px;
218
+ top: 50px;
219
+ color: var(--text-primary);
220
+ background-color: var(--bg-tertiary);
221
+ border: 1px solid var(--border-light);
222
+ border-radius: 10px;
223
+ padding: 6px;
224
+ text-align: left;
225
+ max-width: 300px;
226
+ z-index: 10;
227
+ transition: all 0.3s;
228
+ opacity: 0%;
229
+ user-select: none;
230
+ }
231
+
232
+ .previewer-container .tips-text p {
233
+ font-size: 14px;
234
+ line-height: 1.2;
235
+ color: var(--text-secondary);
236
+ }
237
+
238
+ .tips-icon:hover + .tips-text {
239
+ display: block;
240
+ opacity: 100%;
241
+ }
242
+
243
+ /* Row 1: Display Modes */
244
+ .previewer-container .mode-row {
245
+ width: 100%;
246
+ display: flex;
247
+ gap: 8px;
248
+ justify-content: center;
249
+ margin-bottom: 20px;
250
+ flex-wrap: wrap;
251
+ }
252
  .previewer-container .mode-btn {
253
+ width: 24px;
254
+ height: 24px;
255
+ border-radius: 50%;
256
+ cursor: pointer;
257
+ opacity: 0.5;
258
+ transition: all 0.2s;
259
+ border: 2px solid var(--border-light);
260
+ object-fit: cover;
261
+ }
262
+ .previewer-container .mode-btn:hover { opacity: 0.9; transform: scale(1.1); }
263
+ .previewer-container .mode-btn.active {
264
+ opacity: 1;
265
+ border-color: var(--accent);
266
+ transform: scale(1.1);
267
+ }
268
+
269
+ /* Row 2: Display Image */
270
+ .previewer-container .display-row {
271
+ margin-bottom: 20px;
272
+ min-height: 400px;
273
+ width: 100%;
274
+ flex-grow: 1;
275
+ display: flex;
276
+ justify-content: center;
277
+ align-items: center;
278
+ }
279
+ .previewer-container .previewer-main-image {
280
+ max-width: 100%;
281
+ max-height: 100%;
282
+ flex-grow: 1;
283
+ object-fit: contain;
284
+ display: none;
285
+ }
286
+ .previewer-container .previewer-main-image.visible {
287
+ display: block;
288
+ }
289
+
290
+ /* Row 3: Custom HTML Slider */
291
+ .previewer-container .slider-row {
292
+ width: 100%;
293
+ display: flex;
294
+ flex-direction: column;
295
+ align-items: center;
296
+ gap: 10px;
297
+ padding: 0 10px;
298
+ }
299
+
300
+ .previewer-container input[type=range] {
301
+ -webkit-appearance: none;
302
+ width: 100%;
303
+ max-width: 400px;
304
+ background: transparent;
305
+ }
306
+ .previewer-container input[type=range]::-webkit-slider-runnable-track {
307
+ width: 100%;
308
+ height: 8px;
309
+ cursor: pointer;
310
+ background: var(--bg-tertiary);
311
+ border-radius: 5px;
312
+ }
313
  .previewer-container input[type=range]::-webkit-slider-thumb {
314
+ height: 20px;
315
+ width: 20px;
316
+ border-radius: 50%;
317
+ background: var(--accent);
318
+ cursor: pointer;
319
+ -webkit-appearance: none;
320
+ margin-top: -6px;
321
+ box-shadow: 0 2px 5px rgba(0,0,0,0.3);
322
+ transition: transform 0.1s;
323
+ }
324
+ .previewer-container input[type=range]::-webkit-slider-thumb:hover {
325
+ transform: scale(1.2);
326
+ }
327
+
328
+ /* Overwrite Previewer Block Style */
329
+ .gradio-container .padded:has(.previewer-container) {
330
+ padding: 0 !important;
331
  }
332
+
333
+ .gradio-container:has(.previewer-container) [data-testid="block-label"] {
334
+ position: absolute;
335
+ top: 0;
336
+ left: 0;
337
+ }
338
+
339
+ /* Gallery styling */
340
+ .gallery {
341
+ background: var(--bg-tertiary) !important;
342
+ border-color: var(--border-color) !important;
343
+ }
344
+
345
+ /* Accordion */
346
+ .accordion {
347
+ background: var(--bg-secondary) !important;
348
+ border-color: var(--border-color) !important;
349
+ }
350
+
351
+ /* Scrollbar */
352
+ ::-webkit-scrollbar { width: 10px; }
353
+ ::-webkit-scrollbar-track { background: var(--bg-secondary); }
354
+ ::-webkit-scrollbar-thumb { background: var(--border-light); border-radius: 5px; }
355
+ ::-webkit-scrollbar-thumb:hover { background: var(--text-secondary); }
356
  """
357
 
358
 
359
  head = """
360
  <script>
361
  function refreshView(mode, step) {
362
+ // 1. Find current mode and step
363
  const allImgs = document.querySelectorAll('.previewer-main-image');
364
  for (let i = 0; i < allImgs.length; i++) {
365
  const img = allImgs[i];
 
371
  break;
372
  }
373
  }
374
+
375
+ // 2. Hide ALL images
376
  allImgs.forEach(img => img.classList.remove('visible'));
377
+
378
+ // 3. Construct the specific ID for the current state
379
  const targetId = 'view-m' + mode + '-s' + step;
380
  const targetImg = document.getElementById(targetId);
381
+
382
+ // 4. Show ONLY the target
383
+ if (targetImg) {
384
+ targetImg.classList.add('visible');
385
+ }
386
+
387
+ // 5. Update Button Highlights
388
  const allBtns = document.querySelectorAll('.mode-btn');
389
  allBtns.forEach((btn, idx) => {
390
  if (idx === mode) btn.classList.add('active');
391
  else btn.classList.remove('active');
392
  });
393
  }
394
+
395
+ // --- Action: Switch Mode ---
396
+ function selectMode(mode) {
397
+ refreshView(mode, -1);
398
+ }
399
+
400
+ // --- Action: Slider Change ---
401
+ function onSliderChange(val) {
402
+ refreshView(-1, parseInt(val));
403
+ }
404
  </script>
405
  """
406
 
 
506
 
507
 
508
  def prepare_examples() -> List[List[str]]:
509
+ """Prepare multi-image examples as lists of image paths."""
510
  example_dir = "assets/example_multi_image"
511
  if not os.path.exists(example_dir):
512
  return []
 
515
  examples = []
516
  for case in sorted(cases):
517
  case_images = []
518
+ for i in range(1, 10):
519
  img_path = f'{example_dir}/{case}_{i}.png'
520
  if os.path.exists(img_path):
521
  case_images.append(img_path)
 
635
  torch.cuda.empty_cache()
636
 
637
  # --- HTML Construction ---
 
 
 
 
 
 
 
 
 
 
 
638
  images_html = ""
639
  for m_idx, mode in enumerate(MODES):
640
  for s_idx in range(STEPS):
641
  unique_id = f"view-m{m_idx}-s{s_idx}"
642
  is_visible = (m_idx == DEFAULT_MODE and s_idx == DEFAULT_STEP)
643
  vis_class = "visible" if is_visible else ""
644
+ img_base64 = image_to_base64(Image.fromarray(render_images[mode['render_key']][s_idx]))
645
  images_html += f'<img id="{unique_id}" class="previewer-main-image {vis_class}" src="{img_base64}" loading="eager">'
646
 
647
  btns_html = ""
 
708
 
709
  with gr.Blocks(delete_cache=(600, 600)) as demo:
710
  gr.Markdown("""
711
+ ## Multi-View Image to 3D Asset with [TRELLIS.2](https://microsoft.github.io/TRELLIS.2)
712
+ * Upload one or more images and click Generate to create a 3D asset.
713
+ * Multiple views from different angles will produce better results.
714
+ * Click Extract GLB to export and download the generated GLB file.
715
  """)
716
 
717
  with gr.Row():
718
  with gr.Column(scale=1, min_width=360):
719
  image_prompt = gr.Gallery(
720
+ label="Input Images",
721
  format="png",
722
  type="pil",
723
  height=400,
724
  columns=3,
725
  object_fit="contain"
726
  )
 
727
 
728
  resolution = gr.Radio(["512", "1024", "1536"], label="Resolution", value="1024")
729
  seed = gr.Slider(0, MAX_SEED, label="Seed", value=0, step=1)
 
731
  decimation_target = gr.Slider(100000, 500000, label="Decimation Target", value=300000, step=10000)
732
  texture_size = gr.Slider(1024, 4096, label="Texture Size", value=2048, step=1024)
733
 
734
+ generate_btn = gr.Button("Generate")
735
 
736
  with gr.Accordion(label="Advanced Settings", open=False):
737
  gr.Markdown("Stage 1: Sparse Structure Generation")
 
764
  download_btn = gr.DownloadButton(label="Download GLB")
765
  gr.Markdown("*GLB extraction may take 30+ seconds.*")
766
 
767
+ with gr.Column(scale=1, min_width=172):
768
+ gr.Markdown("### Multi-View Examples")
 
769
  example_data = prepare_examples()
770
+ for i, example_paths in enumerate(example_data[:12]):
771
  case_name = os.path.basename(example_paths[0]).split('_')[0]
772
  btn = gr.Button(f"{case_name} ({len(example_paths)} views)", size="sm")
773
  btn.click(