caggianov commited on
Commit
ba8d2b8
·
1 Parent(s): 5c2f432

update requirements

Browse files
Files changed (6) hide show
  1. Dockerfile +0 -13
  2. app.py +186 -170
  3. pyproject.toml +0 -32
  4. python-version +0 -1
  5. requirements.txt +8 -0
  6. uv.lock +0 -0
Dockerfile DELETED
@@ -1,13 +0,0 @@
1
- FROM python:3.10-slim
2
-
3
- # Install uv
4
- RUN pip install --no-cache-dir uv
5
-
6
- WORKDIR /app
7
- COPY pyproject.toml .
8
- COPY . .
9
-
10
- # Install deps from pyproject.toml
11
- RUN uv sync --frozen
12
-
13
- CMD ["uv", "run", "python", "app.py"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app.py CHANGED
@@ -22,14 +22,13 @@ import numpy as np
22
  import pandas as pd
23
  import plotly.graph_objs as go
24
  import spaces
25
- import torch
26
  from embed4d import model3d_viewer
27
- from metrabs_pytorch.scripts.run_video import run_metrabs_video
28
  from myo_tools.mjs.marker.marker_api import get_marker_names
29
  from myo_tools.utils.file_ops.dataframe_utils import from_array_to_dataframe
30
  from myo_tools.utils.file_ops.io_utils import from_qpos_to_joint_angles
31
  from myo_tools.utils.mocap_ops.mocap_utils import rotate_mocap_ydown_to_zup
32
  from myosdk import Client
 
33
 
34
  PLOT_CONFIG = {
35
  "plot_bgcolor": "#0f172a",
@@ -40,7 +39,16 @@ PLOT_CONFIG = {
40
  }
41
 
42
 
43
- DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
 
 
 
 
 
 
 
 
 
44
  UPLOAD_GLB = os.path.join(os.path.dirname(__file__), "./info_files/upload_files.glb")
45
  ERROR_GLB = os.path.join(os.path.dirname(__file__), "./info_files/error.glb")
46
  PROCESSING_GLB = os.path.join(os.path.dirname(__file__), "./info_files/processing.glb")
@@ -167,6 +175,7 @@ def run_retargeting_c3d(api_key, c3d_files, markerset_file):
167
  gr.update(visible=False),
168
  gr.update(visible=True, value=model3d_viewer(ERROR_GLB)),
169
  )
 
170
 
171
  try:
172
  # Initialize client
@@ -180,22 +189,11 @@ def run_retargeting_c3d(api_key, c3d_files, markerset_file):
180
  status.append(
181
  f"🔹 MyoSDK client initialized in { time.time() - init_time:.2f} seconds"
182
  )
183
- init_time = time.time()
184
- # Upload markerset
185
- status.append("🔹 Uploading markerset file...")
186
- yield "\n".join(status), None, None, gr.update(value=[]), gr.update(
187
- visible=False
188
- ), gr.update(visible=True, value=model3d_viewer(PROCESSING_GLB)),
189
- mk_asset = client.assets.upload_file(markerset_file.name)
190
 
191
- status.append(
192
- f"🔹 Markerset file uploaded in {time.time() - init_time:.2f} seconds"
 
193
  )
194
- yield "\n".join(status), None, None, gr.update(value=[]), gr.update(
195
- visible=False
196
- ), gr.update(visible=True, value=model3d_viewer(PROCESSING_GLB)),
197
-
198
- mk_id = mk_asset["asset_id"]
199
 
200
  # Process each C3D file
201
  total_files = len(c3d_files)
@@ -209,55 +207,47 @@ def run_retargeting_c3d(api_key, c3d_files, markerset_file):
209
  ), gr.update(visible=True, value=model3d_viewer(PROCESSING_GLB)),
210
 
211
  init_time = time.time()
212
- tracker_asset = client.assets.upload_file(f)
213
- status.append(
214
- f"\t🔹 C3D file uploaded in {time.time() - init_time:.2f} seconds"
215
- )
216
  yield "\n".join(status), None, None, gr.update(value=[]), gr.update(
217
  visible=False
218
  ), gr.update(visible=True, value=model3d_viewer(PROCESSING_GLB)),
219
- init_time = time.time()
220
- job = client.jobs.start_retarget(
221
- tracker_asset_id=tracker_asset["asset_id"],
222
- markerset_asset_id=mk_id,
223
- export_glb=True,
224
- )
225
 
226
- status.append(
227
- f"\t🔹 Retargeting job started in {time.time() - init_time:.2f} seconds ... Processing may take a few seconds depending on the file size."
 
 
 
228
  )
229
- yield "\n".join(status), None, None, gr.update(value=[]), gr.update(
230
- visible=False
231
- ), gr.update(visible=True, value=model3d_viewer(PROCESSING_GLB)),
232
- init_time = time.time()
233
- result = client.jobs.wait(job["job_id"])
234
 
235
  status.append(
236
  f"\t🔹 Retargeting job completed in {time.time() - init_time:.2f} seconds"
237
  )
 
 
 
 
238
  yield "\n".join(status), None, None, gr.update(value=[]), gr.update(
239
  visible=False
240
  ), gr.update(visible=True, value=model3d_viewer(PROCESSING_GLB)),
241
- assert (
242
- result["status"] == "SUCCEEDED"
243
- ), f"Failed retarget for {os.path.basename(f)}"
244
 
245
  status.append(f"\t✅ Retargeting completed for {os.path.basename(f)}")
246
  base = os.path.splitext(os.path.basename(f))[0]
247
  out_path = os.path.join(tempfile.gettempdir(), base + ".parquet")
248
- output_glb_path = os.path.join(
249
- tempfile.gettempdir(), base + f"_{int(time.time())}_" + ".glb"
250
- )
251
-
252
- client.assets.download(
253
- result["output"]["retarget_output_asset_ids"]["qpos"], out_path
254
- )
255
 
 
 
256
  output_files.append(out_path)
257
- client.assets.download(
258
- result["output"]["retarget_output_asset_ids"]["motion"], output_glb_path
259
- )
260
- output_glb_files.append(output_glb_path)
 
 
 
 
 
 
261
 
262
  assert os.path.exists(
263
  output_files[0]
@@ -265,27 +255,26 @@ def run_retargeting_c3d(api_key, c3d_files, markerset_file):
265
 
266
  # Load angles from first output file
267
  status.append("🔹 Loading animation and angle data...")
268
-
269
- assert os.path.getsize(output_glb_files[0]) > 0
270
- if hasattr(os, "sync"):
271
- os.sync()
272
- else:
273
- # Windows doesn't have os.sync
274
- pass
275
- time.sleep(0.5) # allow filesystem flush
276
  yield "\n".join(status), None, None, gr.update(
277
  interactive=True, visible=True
278
  ), gr.update(visible=True), gr.update(
279
- visible=True, value=model3d_viewer(output_glb_files[0])
 
280
  )
 
281
  assert os.path.exists(out_path)
282
  df = from_qpos_to_joint_angles(output_files[0])
283
 
284
  angle_list = list(df.columns[1:])
285
  initial_value = [angle_list[0]] if angle_list else []
286
- initial_plot = update_plot(df, initial_value)
287
  status.append("✅ Complete!")
288
  df_state_path = os.path.abspath(output_files[0])
 
 
 
289
  yield (
290
  "\n".join(status),
291
  gr.update(value=output_files, visible=True),
@@ -293,13 +282,38 @@ def run_retargeting_c3d(api_key, c3d_files, markerset_file):
293
  gr.update(
294
  choices=angle_list, value=initial_value, visible=True, interactive=True
295
  ),
296
- gr.update(value=initial_plot, visible=True),
297
- gr.update(visible=True, value=model3d_viewer(output_glb_files[0])),
 
 
 
 
 
 
 
298
  )
299
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
300
  except Exception as e:
301
  yield (
302
- f"❌ {e}",
303
  None,
304
  None,
305
  gr.update(visible=False),
@@ -365,8 +379,26 @@ def run_retargeting_video(
365
  visible=False
366
  ), video_path, gr.update(visible=True, value=model3d_viewer(PROCESSING_GLB)),
367
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
368
  results = list(
369
- run_metrabs_video(video_path=video_path, device=DEVICE, visualize=False)
370
  )
371
  markers = rotate_mocap_ydown_to_zup(
372
  np.array([res["poses3d"] for res in results]).squeeze()
@@ -410,30 +442,8 @@ def run_retargeting_video(
410
  status.append(
411
  f"🔹 MyoSDK client initialized in { time.time() - init_time:.2f} seconds"
412
  )
413
- init_time = time.time()
414
- # Upload markerset
415
- status.append("🔹 Uploading markerset file...")
416
- yield "\n".join(status), None, None, gr.update(visible=False), gr.update(
417
- visible=False,
418
- ), video_with_keypoints, gr.update(
419
- visible=True, value=model3d_viewer(PROCESSING_GLB)
420
- ),
421
-
422
- markerset_file_name = "markersets/movi_metrabs_markerset.xml"
423
-
424
- mk_asset = client.assets.upload_file(markerset_file_name)
425
-
426
- status.append(
427
- f"🔹 Markerset file uploaded in {time.time() - init_time:.2f} seconds"
428
- )
429
-
430
- yield "\n".join(status), None, None, gr.update(visible=False), gr.update(
431
- visible=False
432
- ), video_with_keypoints, gr.update(
433
- visible=True, value=model3d_viewer(PROCESSING_GLB)
434
- ),
435
- init_time = time.time()
436
 
 
437
  marker_names = get_marker_names(markerset_file_name)
438
 
439
  from_array_to_dataframe(
@@ -445,82 +455,77 @@ def run_retargeting_video(
445
 
446
  fn_parquet = os.path.join(tempfile.gettempdir(), "video_trackers.parquet")
447
  from_array_to_dataframe(markers, marker_names, fps, fn_parquet)
448
- tracker_asset = client.assets.upload_file(fn_parquet)
449
 
450
  print("fn_parquet: ", fn_parquet)
451
 
452
  init_time = time.time()
453
- job = client.jobs.start_retarget(
454
- tracker_asset_id=tracker_asset["asset_id"],
455
- markerset_asset_id=mk_asset["asset_id"],
456
- export_glb=True,
457
- )
458
-
459
- status.append(
460
- f"\t🔹 Retargeting job started in {time.time() - init_time:.2f} seconds ... Processing may take a few seconds depending on the video length."
461
- )
462
  yield "\n".join(status), None, None, gr.update(value=[]), gr.update(
463
  visible=False
464
  ), video_with_keypoints, gr.update(
465
  visible=True, value=model3d_viewer(PROCESSING_GLB)
466
  ),
467
- init_time = time.time()
468
- result = client.jobs.wait(job["job_id"])
 
 
 
 
 
 
469
 
470
  status.append(
471
  f"\t🔹 Retargeting job completed in {time.time() - init_time:.2f} seconds"
472
  )
 
 
 
 
473
  yield "\n".join(status), None, None, gr.update(visible=False), gr.update(
474
  visible=False
475
  ), video_with_keypoints, gr.update(
476
  visible=True, value=model3d_viewer(PROCESSING_GLB)
477
  ),
478
 
479
- print("STATUS: ", result["status"])
480
- assert (
481
- result["status"] == "SUCCEEDED"
482
- ), f"Failed retarget for {os.path.basename(video_path)}"
483
-
484
  base = os.path.splitext(os.path.basename(video_path))[0]
485
  out_path = os.path.join(tempfile.gettempdir(), base + ".parquet")
486
- output_glb_path = os.path.join(
487
- tempfile.gettempdir(), base + f"_{int(time.time())}_" + ".glb"
488
- )
489
- client.assets.download(
490
- result["output"]["retarget_output_asset_ids"]["qpos"], out_path
491
- )
492
 
493
  assert os.path.exists(
494
  out_path
495
  ), f"Failed to download retargeted data for {os.path.basename(video_path)}"
496
 
497
- client.assets.download(
498
- result["output"]["retarget_output_asset_ids"]["motion"], output_glb_path
499
- )
 
 
 
 
500
 
501
- assert os.path.getsize(output_glb_path) > 0
502
- if hasattr(os, "sync"):
503
- os.sync()
504
- else:
505
- # Windows doesn't have os.sync
506
- pass
507
- time.sleep(0.5) # allow filesystem flush
508
- print("output_glb_path: ", output_glb_path, os.path.getsize(output_glb_path))
509
  # Load angles from first output file
510
  status.append("🔹 Loading animation and angle data...")
511
  yield "\n".join(status), None, None, gr.update(visible=False), gr.update(
512
  visible=True
513
  ), video_with_keypoints, gr.update(
514
- visible=True, value=model3d_viewer(output_glb_path)
 
 
 
515
  )
 
516
  assert os.path.exists(out_path)
517
  df = from_qpos_to_joint_angles(out_path)
518
 
519
  angle_list = list(df.columns[1:])
520
  initial_value = [angle_list[0]] if angle_list else []
521
- initial_plot = update_plot(df, initial_value)
522
  status.append("✅ Complete!")
523
  df_state_path = os.path.abspath(out_path)
 
 
 
524
  yield (
525
  "\n".join(status),
526
  gr.update(value=out_path, visible=True),
@@ -528,12 +533,51 @@ def run_retargeting_video(
528
  gr.update(
529
  choices=angle_list, value=initial_value, visible=True, interactive=True
530
  ),
531
- gr.update(value=initial_plot, visible=True),
532
  video_with_keypoints,
533
- gr.update(visible=True, value=model3d_viewer(output_glb_path)),
 
 
 
 
 
 
 
534
  )
535
  return
536
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
537
  except Exception as e:
538
  # Use video_path if defined, otherwise use video_file or None
539
  error_video = (
@@ -542,7 +586,7 @@ def run_retargeting_video(
542
  else (video_file if video_file else None)
543
  )
544
  yield (
545
- "\n".join(status + ["\n❌ Error: " + str(e)]),
546
  None,
547
  None,
548
  gr.update(visible=False),
@@ -558,27 +602,24 @@ def run_retargeting_video(
558
 
559
 
560
  def update_plot(parquet_path, joints):
561
- """Safe Linux/macOS plotting from parquet or DataFrame."""
562
- # Check joints first to avoid unnecessary processing
563
- if not joints:
564
- return go.Figure()
 
565
 
566
- # Check if parquet_path is valid
567
- if parquet_path is None:
568
  return go.Figure()
569
 
570
- if isinstance(parquet_path, pd.DataFrame):
571
- df = parquet_path
572
- else:
573
- # Nothing loaded yet - check if it's an empty string
574
- if parquet_path == "":
575
- return go.Figure()
576
-
577
- # File might not exist yet (race-safe)
578
- if not os.path.exists(parquet_path):
579
  return go.Figure()
 
580
 
581
- df = pd.read_parquet(parquet_path)
 
 
 
582
 
583
  if df is None or df.empty:
584
  return go.Figure()
@@ -717,7 +758,7 @@ with gr.Blocks(css=CSS) as app:
717
  )
718
  plot_area = gr.Plot(
719
  label="Joint Angle Visualization - Interactive plot showing the selected joint angles over time. Use the legend to toggle individual joints on/off.",
720
- visible=False,
721
  )
722
  status_box = gr.Textbox(
723
  label="Processing Status",
@@ -726,38 +767,13 @@ with gr.Blocks(css=CSS) as app:
726
  )
727
  gr.Markdown(f"MyoSDK v{myosdk.__version__}")
728
 
729
- def update_plot_wrapper(parquet_path, joints):
730
- """Wrapper to ensure plot is visible and updated on Linux."""
731
- if not joints:
732
- return gr.update(value=go.Figure(), visible=False)
733
-
734
- if parquet_path is None:
735
- return gr.update(value=go.Figure(), visible=False)
736
-
737
- # Check if file exists and convert to absolute path for consistency
738
- if isinstance(parquet_path, str):
739
- if not os.path.exists(parquet_path):
740
- return gr.update(value=go.Figure(), visible=False)
741
- parquet_path = os.path.abspath(parquet_path)
742
-
743
- try:
744
- fig = update_plot(parquet_path, joints)
745
- return gr.update(value=fig, visible=True)
746
- except Exception:
747
- return gr.update(value=go.Figure(), visible=False)
748
-
749
  joint_dropdown.change(
750
- fn=update_plot_wrapper,
751
- inputs=[df_state, joint_dropdown],
752
- outputs=[plot_area],
753
- show_progress=False,
754
- )
755
-
756
- joint_dropdown.input(
757
- fn=update_plot_wrapper,
758
  inputs=[df_state, joint_dropdown],
759
- outputs=[plot_area],
760
  show_progress=False,
 
761
  )
762
  run_btn_c3d.click(
763
  fn=run_retargeting_c3d,
 
22
  import pandas as pd
23
  import plotly.graph_objs as go
24
  import spaces
 
25
  from embed4d import model3d_viewer
 
26
  from myo_tools.mjs.marker.marker_api import get_marker_names
27
  from myo_tools.utils.file_ops.dataframe_utils import from_array_to_dataframe
28
  from myo_tools.utils.file_ops.io_utils import from_qpos_to_joint_angles
29
  from myo_tools.utils.mocap_ops.mocap_utils import rotate_mocap_ydown_to_zup
30
  from myosdk import Client
31
+ from myosdk.exceptions import ApiError, InsufficientCreditsError
32
 
33
  PLOT_CONFIG = {
34
  "plot_bgcolor": "#0f172a",
 
39
  }
40
 
41
 
42
+ def get_device():
43
+ """Lazy import torch and get device, falling back to CPU if CUDA unavailable."""
44
+ try:
45
+ import torch
46
+
47
+ return "cuda" if torch.cuda.is_available() else "cpu"
48
+ except ImportError:
49
+ return "cpu"
50
+
51
+
52
  UPLOAD_GLB = os.path.join(os.path.dirname(__file__), "./info_files/upload_files.glb")
53
  ERROR_GLB = os.path.join(os.path.dirname(__file__), "./info_files/error.glb")
54
  PROCESSING_GLB = os.path.join(os.path.dirname(__file__), "./info_files/processing.glb")
 
175
  gr.update(visible=False),
176
  gr.update(visible=True, value=model3d_viewer(ERROR_GLB)),
177
  )
178
+ return
179
 
180
  try:
181
  # Initialize client
 
189
  status.append(
190
  f"🔹 MyoSDK client initialized in { time.time() - init_time:.2f} seconds"
191
  )
 
 
 
 
 
 
 
192
 
193
+ # Get markerset file path
194
+ markerset_path = (
195
+ markerset_file.name if hasattr(markerset_file, "name") else markerset_file
196
  )
 
 
 
 
 
197
 
198
  # Process each C3D file
199
  total_files = len(c3d_files)
 
207
  ), gr.update(visible=True, value=model3d_viewer(PROCESSING_GLB)),
208
 
209
  init_time = time.time()
210
+ # Use the new simplified API - it handles file uploads automatically
211
+ status.append("🔹 Starting retargeting job...")
 
 
212
  yield "\n".join(status), None, None, gr.update(value=[]), gr.update(
213
  visible=False
214
  ), gr.update(visible=True, value=model3d_viewer(PROCESSING_GLB)),
 
 
 
 
 
 
215
 
216
+ result = client.create_retarget_job(
217
+ tracker=f,
218
+ markerset=markerset_path,
219
+ export_glb=True,
220
+ stream_status=True, # Stream status updates automatically
221
  )
 
 
 
 
 
222
 
223
  status.append(
224
  f"\t🔹 Retargeting job completed in {time.time() - init_time:.2f} seconds"
225
  )
226
+ if result.processing_time_seconds is not None:
227
+ status.append(
228
+ f"\t Processing time: {result.processing_time_seconds:.2f}s"
229
+ )
230
  yield "\n".join(status), None, None, gr.update(value=[]), gr.update(
231
  visible=False
232
  ), gr.update(visible=True, value=model3d_viewer(PROCESSING_GLB)),
 
 
 
233
 
234
  status.append(f"\t✅ Retargeting completed for {os.path.basename(f)}")
235
  base = os.path.splitext(os.path.basename(f))[0]
236
  out_path = os.path.join(tempfile.gettempdir(), base + ".parquet")
 
 
 
 
 
 
 
237
 
238
+ # Download qpos file
239
+ result.download_qpos(out_path)
240
  output_files.append(out_path)
241
+
242
+ # Download GLB file for rendering (model3d_viewer needs local file path)
243
+ if result.output.motion:
244
+ output_glb_path = os.path.join(
245
+ tempfile.gettempdir(), base + f"_{int(time.time())}_" + ".glb"
246
+ )
247
+ result.output.motion.download(output_glb_path)
248
+ output_glb_files.append(output_glb_path)
249
+ else:
250
+ output_glb_files.append(None)
251
 
252
  assert os.path.exists(
253
  output_files[0]
 
255
 
256
  # Load angles from first output file
257
  status.append("🔹 Loading animation and angle data...")
258
+ first_glb = (
259
+ output_glb_files[0] if output_glb_files and output_glb_files[0] else None
260
+ )
 
 
 
 
 
261
  yield "\n".join(status), None, None, gr.update(
262
  interactive=True, visible=True
263
  ), gr.update(visible=True), gr.update(
264
+ visible=True,
265
+ value=model3d_viewer(first_glb) if first_glb else model3d_viewer(ERROR_GLB),
266
  )
267
+
268
  assert os.path.exists(out_path)
269
  df = from_qpos_to_joint_angles(output_files[0])
270
 
271
  angle_list = list(df.columns[1:])
272
  initial_value = [angle_list[0]] if angle_list else []
 
273
  status.append("✅ Complete!")
274
  df_state_path = os.path.abspath(output_files[0])
275
+
276
+ # Build an initial plot for the first joint so users see something immediately.
277
+ initial_plot = update_plot(df_state_path, initial_value)
278
  yield (
279
  "\n".join(status),
280
  gr.update(value=output_files, visible=True),
 
282
  gr.update(
283
  choices=angle_list, value=initial_value, visible=True, interactive=True
284
  ),
285
+ initial_plot,
286
+ gr.update(
287
+ visible=True,
288
+ value=(
289
+ model3d_viewer(first_glb)
290
+ if first_glb
291
+ else model3d_viewer(ERROR_GLB)
292
+ ),
293
+ ),
294
  )
295
 
296
+ except InsufficientCreditsError as e:
297
+ yield (
298
+ f"❌ Insufficient credits: {e}",
299
+ None,
300
+ None,
301
+ gr.update(visible=False),
302
+ gr.update(visible=False),
303
+ gr.update(visible=True, value=model3d_viewer(ERROR_GLB)),
304
+ )
305
+ except ApiError as e:
306
+ yield (
307
+ f"❌ API error: {e}",
308
+ None,
309
+ None,
310
+ gr.update(visible=False),
311
+ gr.update(visible=False),
312
+ gr.update(visible=True, value=model3d_viewer(ERROR_GLB)),
313
+ )
314
  except Exception as e:
315
  yield (
316
+ f"❌ Unexpected error: {e}",
317
  None,
318
  None,
319
  gr.update(visible=False),
 
379
  visible=False
380
  ), video_path, gr.update(visible=True, value=model3d_viewer(PROCESSING_GLB)),
381
 
382
+ # Lazy import torch and metrabs for video processing
383
+ try:
384
+ import torch
385
+ from metrabs_pytorch.scripts.run_video import run_metrabs_video
386
+
387
+ device = get_device()
388
+ except (ImportError, Exception) as e:
389
+ yield (
390
+ f"❌ Error: Failed to import PyTorch/Metrabs: {e}. Please check your PyTorch installation.",
391
+ None,
392
+ None,
393
+ gr.update(visible=False),
394
+ gr.update(visible=False),
395
+ video_path,
396
+ gr.update(visible=True, value=model3d_viewer(ERROR_GLB)),
397
+ )
398
+ return
399
+
400
  results = list(
401
+ run_metrabs_video(video_path=video_path, device=device, visualize=False)
402
  )
403
  markers = rotate_mocap_ydown_to_zup(
404
  np.array([res["poses3d"] for res in results]).squeeze()
 
442
  status.append(
443
  f"🔹 MyoSDK client initialized in { time.time() - init_time:.2f} seconds"
444
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
445
 
446
+ markerset_file_name = "./markersets/movi_metrabs_markerset.xml"
447
  marker_names = get_marker_names(markerset_file_name)
448
 
449
  from_array_to_dataframe(
 
455
 
456
  fn_parquet = os.path.join(tempfile.gettempdir(), "video_trackers.parquet")
457
  from_array_to_dataframe(markers, marker_names, fps, fn_parquet)
 
458
 
459
  print("fn_parquet: ", fn_parquet)
460
 
461
  init_time = time.time()
462
+ status.append("🔹 Starting retargeting job...")
 
 
 
 
 
 
 
 
463
  yield "\n".join(status), None, None, gr.update(value=[]), gr.update(
464
  visible=False
465
  ), video_with_keypoints, gr.update(
466
  visible=True, value=model3d_viewer(PROCESSING_GLB)
467
  ),
468
+
469
+ # Use the new simplified API - it handles file uploads automatically
470
+ result = client.create_retarget_job(
471
+ tracker=fn_parquet,
472
+ markerset=markerset_file_name,
473
+ export_glb=True,
474
+ stream_status=True, # Stream status updates automatically
475
+ )
476
 
477
  status.append(
478
  f"\t🔹 Retargeting job completed in {time.time() - init_time:.2f} seconds"
479
  )
480
+ if result.processing_time_seconds is not None:
481
+ status.append(
482
+ f"\t Processing time: {result.processing_time_seconds:.2f}s"
483
+ )
484
  yield "\n".join(status), None, None, gr.update(visible=False), gr.update(
485
  visible=False
486
  ), video_with_keypoints, gr.update(
487
  visible=True, value=model3d_viewer(PROCESSING_GLB)
488
  ),
489
 
 
 
 
 
 
490
  base = os.path.splitext(os.path.basename(video_path))[0]
491
  out_path = os.path.join(tempfile.gettempdir(), base + ".parquet")
492
+
493
+ # Download qpos file
494
+ result.download_qpos(out_path)
 
 
 
495
 
496
  assert os.path.exists(
497
  out_path
498
  ), f"Failed to download retargeted data for {os.path.basename(video_path)}"
499
 
500
+ # Download GLB file for rendering (model3d_viewer needs local file path)
501
+ glb_path = None
502
+ if result.output.motion:
503
+ glb_path = os.path.join(
504
+ tempfile.gettempdir(), base + f"_{int(time.time())}_" + ".glb"
505
+ )
506
+ result.output.motion.download(glb_path)
507
 
 
 
 
 
 
 
 
 
508
  # Load angles from first output file
509
  status.append("🔹 Loading animation and angle data...")
510
  yield "\n".join(status), None, None, gr.update(visible=False), gr.update(
511
  visible=True
512
  ), video_with_keypoints, gr.update(
513
+ visible=True,
514
+ value=(
515
+ model3d_viewer(glb_path) if glb_path else model3d_viewer(PROCESSING_GLB)
516
+ ),
517
  )
518
+
519
  assert os.path.exists(out_path)
520
  df = from_qpos_to_joint_angles(out_path)
521
 
522
  angle_list = list(df.columns[1:])
523
  initial_value = [angle_list[0]] if angle_list else []
 
524
  status.append("✅ Complete!")
525
  df_state_path = os.path.abspath(out_path)
526
+
527
+ # Build an initial plot for the first joint so users see something immediately.
528
+ initial_plot = update_plot(df_state_path, initial_value)
529
  yield (
530
  "\n".join(status),
531
  gr.update(value=out_path, visible=True),
 
533
  gr.update(
534
  choices=angle_list, value=initial_value, visible=True, interactive=True
535
  ),
536
+ initial_plot,
537
  video_with_keypoints,
538
+ gr.update(
539
+ visible=True,
540
+ value=(
541
+ model3d_viewer(glb_path)
542
+ if glb_path
543
+ else model3d_viewer(PROCESSING_GLB)
544
+ ),
545
+ ),
546
  )
547
  return
548
 
549
+ except InsufficientCreditsError as e:
550
+ # Use video_path if defined, otherwise use video_file or None
551
+ error_video = (
552
+ video_path
553
+ if "video_path" in locals()
554
+ else (video_file if video_file else None)
555
+ )
556
+ yield (
557
+ "\n".join(status + [f"\n❌ Insufficient credits: {e}"]),
558
+ None,
559
+ None,
560
+ gr.update(visible=False),
561
+ gr.update(visible=False),
562
+ error_video,
563
+ gr.update(visible=True, value=model3d_viewer(ERROR_GLB)),
564
+ )
565
+ except ApiError as e:
566
+ # Use video_path if defined, otherwise use video_file or None
567
+ error_video = (
568
+ video_path
569
+ if "video_path" in locals()
570
+ else (video_file if video_file else None)
571
+ )
572
+ yield (
573
+ "\n".join(status + [f"\n❌ API error: {e}"]),
574
+ None,
575
+ None,
576
+ gr.update(visible=False),
577
+ gr.update(visible=False),
578
+ error_video,
579
+ gr.update(visible=True, value=model3d_viewer(ERROR_GLB)),
580
+ )
581
  except Exception as e:
582
  # Use video_path if defined, otherwise use video_file or None
583
  error_video = (
 
586
  else (video_file if video_file else None)
587
  )
588
  yield (
589
+ "\n".join(status + [f"\n❌ Unexpected error: {e}"]),
590
  None,
591
  None,
592
  gr.update(visible=False),
 
602
 
603
 
604
  def update_plot(parquet_path, joints):
605
+ """Build a Plotly figure from a parquet file path and selected joints."""
606
+ # Normalize parquet_path (can come from gr.State as str or list)
607
+ path = parquet_path
608
+ if isinstance(path, list):
609
+ path = path[0] if path else None
610
 
611
+ if not joints or path is None or path == "":
 
612
  return go.Figure()
613
 
614
+ if isinstance(path, str):
615
+ if not os.path.exists(path):
 
 
 
 
 
 
 
616
  return go.Figure()
617
+ path = os.path.abspath(path)
618
 
619
+ if isinstance(path, pd.DataFrame):
620
+ df = path
621
+ else:
622
+ df = pd.read_parquet(path)
623
 
624
  if df is None or df.empty:
625
  return go.Figure()
 
758
  )
759
  plot_area = gr.Plot(
760
  label="Joint Angle Visualization - Interactive plot showing the selected joint angles over time. Use the legend to toggle individual joints on/off.",
761
+ visible=True,
762
  )
763
  status_box = gr.Textbox(
764
  label="Processing Status",
 
767
  )
768
  gr.Markdown(f"MyoSDK v{myosdk.__version__}")
769
 
770
+ # When the user changes the joint selection, recompute and update the plot.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
771
  joint_dropdown.change(
772
+ fn=update_plot,
 
 
 
 
 
 
 
773
  inputs=[df_state, joint_dropdown],
774
+ outputs=plot_area,
775
  show_progress=False,
776
+ queue=False,
777
  )
778
  run_btn_c3d.click(
779
  fn=run_retargeting_c3d,
pyproject.toml DELETED
@@ -1,32 +0,0 @@
1
- [project]
2
- name = "myosdk-app"
3
- version = "0.1.0"
4
- description = "Add your description here"
5
- readme = "README.md"
6
- requires-python = ">=3.10"
7
- dependencies = [
8
- "embed4d>=0.0.5",
9
- "gradio>6",
10
- "myo-tools>=0.1.1",
11
- "numpy<2.0",
12
- "plotly>=6.5.2",
13
- "spaces>=0.47.0",
14
- "torch==2.5.1",
15
- "torchaudio==2.5.1",
16
- "torchvision==0.20.1",
17
- "vtk==9.4.2",
18
- "mayavi==4.8.3",
19
- "metrabs-pytorch",
20
- "myosdk>=0.2.0",
21
- ]
22
-
23
- [tool.uv]
24
- no-build-isolation-package = ["mayavi"]
25
-
26
- [[tool.uv.dependency-metadata]]
27
- name = "mayavi"
28
- version = "4.8.3"
29
- requires-dist = ["numpy"]
30
-
31
- [tool.uv.sources]
32
- metrabs-pytorch = { git = "https://github.com/Vittorio-Caggiano/metrabs.git" }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
python-version DELETED
@@ -1 +0,0 @@
1
- 3.10
 
 
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ gradio>6
2
+ plotly
3
+ myosdk
4
+ myo_tools
5
+ git+https://github.com/Vittorio-Caggiano/metrabs.git@claude/fix-mayavi-gradio-tZHmu
6
+ spaces
7
+ embed4d
8
+ opencv-python-headless
uv.lock DELETED
The diff for this file is too large to render. See raw diff