Vamsi Suhas Sadhu commited on
Commit
5b0a188
·
0 Parent(s):

Initial commit - MotionGPT without large models

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +10 -0
  2. Dockerfile +31 -0
  3. README.md +14 -0
  4. app.py +1121 -0
  5. assets/2025-10-18-17_10_3968067.gif +3 -0
  6. assets/2025-10-18-17_10_3968067.mp4 +3 -0
  7. assets/2025-10-18-17_10_3968067.npy +3 -0
  8. assets/2025-10-18-17_18_3068067.gif +3 -0
  9. assets/2025-10-18-17_18_3068067.mp4 +3 -0
  10. assets/2025-10-18-17_18_3068067.npy +3 -0
  11. assets/2025-10-18-17_22_1444086.npy +3 -0
  12. assets/2025-10-18-17_23_5868067.gif +3 -0
  13. assets/2025-10-18-17_23_5868067.mp4 +3 -0
  14. assets/2025-10-18-17_23_5868067.npy +3 -0
  15. assets/2025-10-18-17_26_2068067.gif +3 -0
  16. assets/2025-10-18-17_26_2068067.mp4 +3 -0
  17. assets/2025-10-18-17_26_2068067.npy +3 -0
  18. assets/2025-10-18-17_26_4344086.gif +3 -0
  19. assets/2025-10-18-17_26_4344086.mp4 +3 -0
  20. assets/2025-10-18-17_26_4344086.npy +3 -0
  21. assets/2025-10-18-17_28_0770620.gif +3 -0
  22. assets/2025-10-18-17_28_0770620.mp4 +3 -0
  23. assets/2025-10-18-17_28_0770620.npy +3 -0
  24. assets/2025-10-18-17_28_4799460.gif +3 -0
  25. assets/2025-10-18-17_28_4799460.mp4 +3 -0
  26. assets/2025-10-18-17_28_4799460.npy +3 -0
  27. assets/2025-10-18-17_38_3668067.gif +3 -0
  28. assets/2025-10-18-17_38_3668067.mp4 +3 -0
  29. assets/2025-10-18-17_38_3668067.npy +3 -0
  30. assets/2025-10-18-17_46_1768067.gif +3 -0
  31. assets/2025-10-18-17_46_1768067.mp4 +3 -0
  32. assets/2025-10-18-17_46_1768067.npy +3 -0
  33. assets/2025-10-18-17_46_3844086.gif +3 -0
  34. assets/2025-10-18-17_46_3844086.mp4 +3 -0
  35. assets/2025-10-18-17_46_3844086.npy +3 -0
  36. assets/2025-10-18-17_49_2670620.gif +3 -0
  37. assets/2025-10-18-17_49_2670620.mp4 +3 -0
  38. assets/2025-10-18-17_49_2670620.npy +3 -0
  39. assets/2025-10-18-17_49_4299460.gif +3 -0
  40. assets/2025-10-18-17_49_4299460.mp4 +3 -0
  41. assets/2025-10-18-17_49_4299460.npy +3 -0
  42. assets/2025-10-18-17_49_5592584.gif +3 -0
  43. assets/2025-10-18-17_49_5592584.mp4 +3 -0
  44. assets/2025-10-18-17_49_5592584.npy +3 -0
  45. assets/2025-10-18-17_50_1042399.gif +3 -0
  46. assets/2025-10-18-17_50_1042399.mp4 +3 -0
  47. assets/2025-10-18-17_50_1042399.npy +3 -0
  48. assets/2025-10-18-17_50_4765985.gif +3 -0
  49. assets/2025-10-18-17_50_4765985.mp4 +3 -0
  50. assets/2025-10-18-17_50_4765985.npy +3 -0
.gitattributes ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ *.tar filter=lfs diff=lfs merge=lfs -text
2
+ *.pkl filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
5
+ *.h5 filter=lfs diff=lfs merge=lfs -text
6
+ *.npz filter=lfs diff=lfs merge=lfs -text
7
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
8
+ *.mp4 filter=lfs diff=lfs merge=lfs -text
9
+ *.gif filter=lfs diff=lfs merge=lfs -text
10
+ *.npy filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Custom Dockerfile for MotionGPT with OSMesa support for slow mode rendering
2
+ FROM python:3.10-slim
3
+
4
+ # Install system dependencies including OSMesa for headless OpenGL rendering
5
+ RUN apt-get update && apt-get install -y \
6
+ libosmesa6-dev \
7
+ libgl1-mesa-glx \
8
+ libglib2.0-0 \
9
+ libsm6 \
10
+ libxext6 \
11
+ libxrender-dev \
12
+ libgomp1 \
13
+ && rm -rf /var/lib/apt/lists/*
14
+
15
+ # Set working directory
16
+ WORKDIR /app
17
+
18
+ # Copy requirements and install Python packages
19
+ COPY requirements.txt .
20
+ RUN pip install --no-cache-dir -r requirements.txt
21
+
22
+ # Copy application files
23
+ COPY . .
24
+
25
+ # Expose port
26
+ EXPOSE 7860
27
+
28
+ # Run the application
29
+ CMD ["python", "app.py"]
30
+
31
+
README.md ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: MotionGPT
3
+ emoji: 🕺
4
+ colorFrom: blue
5
+ colorTo: purple
6
+ sdk: gradio
7
+ sdk_version: 5.49.1
8
+ app_file: app.py
9
+ pinned: false
10
+ ---
11
+
12
+ # MotionGPT
13
+
14
+ MotionGPT: Unified Motion-Language Model for Human Motion Generation and Understanding
app.py ADDED
@@ -0,0 +1,1121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # CRITICAL: Set up OSMesa and PyOpenGL BEFORE any OpenGL imports
2
+ # This must be done at the very top of the file, before any imports
3
+ import os
4
+ import sys
5
+ import subprocess
6
+
7
+ if os.getenv("SPACE_ID") is not None: # HuggingFace Spaces
8
+ print("🔧 Setting up OSMesa for HuggingFace Spaces...")
9
+ # Set OSMesa platform BEFORE any OpenGL/pyglet imports
10
+ os.environ['PYOPENGL_PLATFORM'] = 'osmesa'
11
+ # Prevent pyglet from trying to use GLX (X11)
12
+ os.environ['PYGLET_HIDE_WINDOW'] = '1'
13
+ # Disable display (headless)
14
+ os.environ['DISPLAY'] = ''
15
+
16
+ # Uninstall PyOpenGL-accelerate (incompatible with OSMesa)
17
+ try:
18
+ subprocess.check_call([
19
+ sys.executable, "-m", "pip", "uninstall", "-y", "PyOpenGL-accelerate"
20
+ ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
21
+ except:
22
+ pass
23
+
24
+ # Reinstall PyOpenGL to ensure it has GL_HALF_FLOAT and OSMesa support
25
+ try:
26
+ subprocess.check_call([
27
+ sys.executable, "-m", "pip", "uninstall", "-y", "PyOpenGL"
28
+ ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
29
+ subprocess.check_call([
30
+ sys.executable, "-m", "pip", "install", "--no-cache-dir", "PyOpenGL>=3.1.6"
31
+ ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
32
+ print("✅ PyOpenGL reinstalled with OSMesa support")
33
+ except Exception as e:
34
+ print(f"⚠️ Could not reinstall PyOpenGL: {e}")
35
+
36
+ # Patch GL_HALF_FLOAT if missing (must be done before any OpenGL imports)
37
+ def patch_gl_half_float():
38
+ try:
39
+ from OpenGL.raw.GL import _types
40
+ if not hasattr(_types, 'GL_HALF_FLOAT'):
41
+ _types.GL_HALF_FLOAT = 0x140B # GL_HALF_FLOAT constant value
42
+ print("✅ Patched GL_HALF_FLOAT constant")
43
+ except:
44
+ pass
45
+
46
+ # Register patch to run before OpenGL is imported
47
+ import atexit
48
+ atexit.register(patch_gl_half_float)
49
+
50
+ # Also patch immediately if OpenGL is already imported somehow
51
+ if 'OpenGL' in sys.modules:
52
+ patch_gl_half_float()
53
+
54
+ import imageio
55
+ # Temporary workaround for Gradio import issue with huggingface_hub
56
+ try:
57
+ import gradio as gr
58
+ except ImportError as e:
59
+ if "HfFolder" in str(e):
60
+ print("⚠️ Gradio import error due to huggingface_hub version mismatch.")
61
+ print(" Attempting workaround...")
62
+ # Try to patch huggingface_hub before importing gradio
63
+ try:
64
+ import huggingface_hub
65
+ # Create a dummy HfFolder class if it doesn't exist
66
+ if not hasattr(huggingface_hub, 'HfFolder'):
67
+ class HfFolder:
68
+ @staticmethod
69
+ def save_token(token):
70
+ pass
71
+ @staticmethod
72
+ def get_token():
73
+ return None
74
+ huggingface_hub.HfFolder = HfFolder
75
+ import gradio as gr
76
+ print("✅ Gradio imported with workaround")
77
+ except Exception as patch_error:
78
+ print(f"❌ Workaround failed: {patch_error}")
79
+ print(" Please run: pip install 'huggingface_hub<0.20.0'")
80
+ raise
81
+ else:
82
+ raise
83
+
84
+ # Patch Gradio to handle API schema generation errors
85
+ def patch_gradio_api_error():
86
+ """Patch Gradio's schema parser to handle boolean additionalProperties"""
87
+ try:
88
+ import gradio_client.utils as gradio_client_utils
89
+
90
+ # Patch the get_type function that's causing the error
91
+ if hasattr(gradio_client_utils, 'get_type'):
92
+ original_get_type = gradio_client_utils.get_type
93
+
94
+ def safe_get_type(schema):
95
+ # Handle case where schema is a boolean (True/False)
96
+ if isinstance(schema, bool):
97
+ return "bool"
98
+ # Handle case where schema is not a dict
99
+ if not isinstance(schema, dict):
100
+ return "unknown"
101
+ # Call original function for normal cases
102
+ return original_get_type(schema)
103
+
104
+ gradio_client_utils.get_type = safe_get_type
105
+
106
+ # Also patch _json_schema_to_python_type to handle boolean additionalProperties
107
+ if hasattr(gradio_client_utils, '_json_schema_to_python_type'):
108
+ original_json_schema_to_python_type = gradio_client_utils._json_schema_to_python_type
109
+
110
+ def safe_json_schema_to_python_type(schema, defs=None):
111
+ # Handle boolean additionalProperties
112
+ if isinstance(schema, bool):
113
+ return "bool"
114
+ if isinstance(schema, dict) and 'additionalProperties' in schema:
115
+ if isinstance(schema['additionalProperties'], bool):
116
+ # If additionalProperties is True/False, treat as dict/object
117
+ return "dict" if schema['additionalProperties'] else "dict"
118
+ try:
119
+ return original_json_schema_to_python_type(schema, defs)
120
+ except (TypeError, AttributeError) as e:
121
+ if "bool" in str(e) or "not iterable" in str(e) or "const" in str(e):
122
+ # Return a safe default
123
+ return "dict"
124
+ raise
125
+
126
+ gradio_client_utils._json_schema_to_python_type = safe_json_schema_to_python_type
127
+
128
+ print("✅ Patched Gradio schema parser")
129
+ else:
130
+ # Fallback: patch at Blocks level
131
+ import gradio.blocks as gradio_blocks
132
+ if hasattr(gradio_blocks, 'Blocks'):
133
+ original_get_api_info = gradio_blocks.Blocks.get_api_info
134
+
135
+ def safe_get_api_info(self):
136
+ try:
137
+ return original_get_api_info(self)
138
+ except (TypeError, AttributeError) as e:
139
+ if "bool" in str(e) or "not iterable" in str(e) or "const" in str(e):
140
+ print("⚠️ API schema generation error caught, returning empty API info")
141
+ return {}
142
+ raise
143
+
144
+ gradio_blocks.Blocks.get_api_info = safe_get_api_info
145
+ print("✅ Patched Gradio Blocks.get_api_info (fallback)")
146
+ except Exception as e:
147
+ print(f"⚠️ Could not patch Gradio API: {e}")
148
+ import traceback
149
+ traceback.print_exc()
150
+
151
+ patch_gradio_api_error()
152
+ import random
153
+ import torch
154
+ import time
155
+ import threading
156
+ import cv2
157
+ import os
158
+ import shutil
159
+ import subprocess
160
+ import sys
161
+ import numpy as np
162
+
163
+ import pytorch_lightning as pl
164
+ from moviepy import VideoFileClip
165
+ from pathlib import Path
166
+
167
+ # Fix chumpy compatibility with NumPy 1.23+ (MUST be before chumpy import)
168
+ # Patch at module level for 'from numpy import bool' to work
169
+ # Always set these attributes (they may not exist in newer numpy versions)
170
+ import numpy
171
+ numpy.bool = numpy.bool_
172
+ numpy.int = numpy.int_
173
+ numpy.float = numpy.float_
174
+ numpy.complex = numpy.complex_
175
+ numpy.object = numpy.object_
176
+ numpy.unicode = numpy.str_
177
+ numpy.str = numpy.str_
178
+ # Also patch np alias for consistency
179
+ np.bool = np.bool_
180
+ np.int = np.int_
181
+ np.float = np.float_
182
+ np.complex = np.complex_
183
+ np.object = np.object_
184
+ np.unicode = np.str_
185
+ np.str = np.str_
186
+
187
+ # Install and import chumpy (REQUIRED for SMPL rendering - slow mode)
188
+ chumpy = None
189
+ try:
190
+ import chumpy
191
+ print("✅ chumpy imported successfully")
192
+ except (ImportError, AttributeError, ModuleNotFoundError) as e:
193
+ print("📦 chumpy not found. Installing chumpy (REQUIRED for slow mode/SMPL rendering)...")
194
+ try:
195
+ # Install with verbose output to see any errors
196
+ subprocess.check_call([sys.executable, "-m", "pip", "install", "--no-build-isolation", "chumpy"])
197
+ # Re-import after installation
198
+ import importlib
199
+ if 'chumpy' in sys.modules:
200
+ del sys.modules['chumpy']
201
+ import chumpy
202
+ print("✅ chumpy installed and imported successfully")
203
+ except Exception as install_error:
204
+ print(f"❌ CRITICAL: Failed to install/import chumpy: {install_error}")
205
+ print(" Slow mode (SMPL rendering) will NOT work without chumpy.")
206
+ raise RuntimeError(f"chumpy is required for slow mode but failed to install/import: {install_error}")
207
+
208
+ from mGPT.data.build_data import build_data
209
+ from mGPT.models.build_model import build_model
210
+ from mGPT.config import parse_args
211
+ from scipy.spatial.transform import Rotation as RRR
212
+ import mGPT.render.matplot.plot_3d_global as plot_3d
213
+ from mGPT.render.pyrender.hybrik_loc2rot import HybrIKJointsToRotmat
214
+ # Import SMPLRender (REQUIRED for slow mode)
215
+ if chumpy is None:
216
+ raise RuntimeError("chumpy must be imported before SMPLRender")
217
+
218
+ # Patch GL_HALF_FLOAT before importing pyrender (which imports OpenGL)
219
+ if os.getenv("SPACE_ID") is not None:
220
+ try:
221
+ # Import OpenGL types and patch if needed
222
+ from OpenGL.raw.GL import _types
223
+ if not hasattr(_types, 'GL_HALF_FLOAT'):
224
+ _types.GL_HALF_FLOAT = 0x140B
225
+ print("✅ Patched GL_HALF_FLOAT before pyrender import")
226
+ except:
227
+ pass
228
+
229
+ try:
230
+ from mGPT.render.pyrender.smpl_render import SMPLRender
231
+ print("✅ SMPLRender imported successfully")
232
+ except Exception as e:
233
+ print(f"❌ CRITICAL: Could not import SMPLRender: {e}")
234
+ raise RuntimeError(f"SMPLRender is required for slow mode but failed to import: {e}")
235
+ from transformers import WhisperProcessor, WhisperForConditionalGeneration
236
+ import librosa
237
+
238
+ # OpenGL platform is set at the top of the file (line 5) for HuggingFace Spaces
239
+ # For local environments, it will use the default (EGL or GLX)
240
+
241
+ # Download models from HuggingFace Hub if not present locally
242
+ def download_model_if_needed(repo_id, local_path, repo_type="model"):
243
+ """Download model from HuggingFace Hub if local path doesn't exist"""
244
+ if os.path.exists(local_path):
245
+ return
246
+
247
+ print(f"📥 Downloading {repo_id} to {local_path}...")
248
+ try:
249
+ from huggingface_hub import snapshot_download
250
+ hf_username = os.getenv("HF_USERNAME", "vsadhu1")
251
+ full_repo_id = repo_id if "/" in repo_id else f"{hf_username}/{repo_id}"
252
+
253
+ # For checkpoint file, download to parent directory
254
+ if local_path.endswith(".tar"):
255
+ target_dir = Path(local_path).parent
256
+ target_dir.mkdir(parents=True, exist_ok=True)
257
+ # Download to temp location first
258
+ temp_dir = snapshot_download(
259
+ repo_id=full_repo_id,
260
+ repo_type=repo_type,
261
+ local_dir=str(target_dir / "temp"),
262
+ local_dir_use_symlinks=False
263
+ )
264
+ # Find the .tar file and move it
265
+ for file in Path(temp_dir).rglob("*.tar"):
266
+ shutil.move(str(file), local_path)
267
+ print(f" ✅ Downloaded checkpoint to {local_path}")
268
+ shutil.rmtree(Path(temp_dir).parent / "temp", ignore_errors=True)
269
+ return
270
+ else:
271
+ # For directories, download directly to the target path
272
+ target_path = Path(local_path).resolve() # Get absolute path
273
+ target_path.mkdir(parents=True, exist_ok=True)
274
+ snapshot_download(
275
+ repo_id=full_repo_id,
276
+ repo_type=repo_type,
277
+ local_dir=str(target_path),
278
+ )
279
+ print(f" ✅ Downloaded to {target_path}")
280
+ except Exception as e:
281
+ print(f" ⚠️ Failed to download {repo_id}: {e}")
282
+ print(f" Please ensure models are available or upload them first")
283
+
284
+ # Download models if needed (for HuggingFace Spaces deployment)
285
+ is_hf_space = os.getenv("SPACE_ID") is not None
286
+ if is_hf_space:
287
+ # Uninstall PyOpenGL-accelerate if present (incompatible with OSMesa)
288
+ # This should be handled by packages.txt installing OSMesa, but ensure accelerate is not installed
289
+ try:
290
+ subprocess.check_call([
291
+ sys.executable, "-m", "pip", "uninstall", "-y", "PyOpenGL-accelerate"
292
+ ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
293
+ except:
294
+ pass # Not installed, that's fine
295
+
296
+ # PyOpenGL setup is already done at the top of the file
297
+ # Just ensure GL_HALF_FLOAT is patched if needed
298
+ try:
299
+ from OpenGL.raw.GL import _types
300
+ if not hasattr(_types, 'GL_HALF_FLOAT'):
301
+ _types.GL_HALF_FLOAT = 0x140B
302
+ print("✅ Patched GL_HALF_FLOAT constant")
303
+ except:
304
+ pass
305
+
306
+ hf_username = os.getenv("HF_USERNAME", "vsadhu1")
307
+ print("🌐 HuggingFace Spaces detected - downloading models...")
308
+
309
+ # Download checkpoint
310
+ download_model_if_needed(
311
+ f"{hf_username}/MotionGPT-checkpoint",
312
+ "checkpoints/MotionGPT-base/motiongpt_s3_h3d.tar"
313
+ )
314
+
315
+ # Download T5 model
316
+ download_model_if_needed(
317
+ f"{hf_username}/MotionGPT-t5-base",
318
+ "deps/flan-t5-base"
319
+ )
320
+
321
+ # Download Whisper model
322
+ download_model_if_needed(
323
+ f"{hf_username}/MotionGPT-whisper-large-v2",
324
+ "deps/whisper-large-v2"
325
+ )
326
+
327
+ # Download SMPL models
328
+ download_model_if_needed(
329
+ f"{hf_username}/MotionGPT-smpl-models",
330
+ "deps/smpl_models"
331
+ )
332
+
333
+ # Load model
334
+ cfg = parse_args(phase="webui") # parse config file
335
+
336
+ # Validate slow mode dependencies
337
+ def validate_slow_mode():
338
+ """Validate that all dependencies for slow mode (SMPL rendering) are available"""
339
+ errors = []
340
+
341
+ if chumpy is None:
342
+ errors.append("❌ chumpy is not imported")
343
+ else:
344
+ print("✅ chumpy is available")
345
+
346
+ if SMPLRender is None:
347
+ errors.append("❌ SMPLRender is not imported")
348
+ else:
349
+ print("✅ SMPLRender is available")
350
+
351
+ smpl_model_path = cfg.RENDER.SMPL_MODEL_PATH
352
+ # Check if path exists, also check parent directory (for hf_space/)
353
+ app_dir = Path(__file__).parent.absolute()
354
+ if not os.path.exists(smpl_model_path):
355
+ # Try parent directory
356
+ clean_path = smpl_model_path[2:] if smpl_model_path.startswith('./') else smpl_model_path
357
+ parent_path = (app_dir.parent / clean_path).resolve()
358
+ if parent_path.exists():
359
+ print(f"✅ SMPL model path exists (in parent): {parent_path}")
360
+ else:
361
+ errors.append(f"❌ SMPL model path does not exist: {smpl_model_path}")
362
+ errors.append(f" Absolute path: {os.path.abspath(smpl_model_path)}")
363
+ errors.append(f" Parent path: {parent_path}")
364
+ else:
365
+ print(f"✅ SMPL model path exists: {smpl_model_path}")
366
+
367
+ if errors:
368
+ print("\n⚠️ SLOW MODE VALIDATION FAILED:")
369
+ for error in errors:
370
+ print(f" {error}")
371
+ print("\n Slow mode will fail with clear error messages when attempted.")
372
+ print(" Fast mode will continue to work normally.\n")
373
+ else:
374
+ print("✅ All slow mode dependencies validated successfully\n")
375
+
376
+ validate_slow_mode()
377
+
378
+ # Fix relative paths in config to absolute paths (required for transformers)
379
+ # Use app.py's directory as base for resolving relative paths
380
+ app_dir = Path(__file__).parent.absolute()
381
+ print(f"🔍 App directory: {app_dir}")
382
+
383
+ if hasattr(cfg, 'model') and hasattr(cfg.model, 'params') and hasattr(cfg.model.params, 'lm'):
384
+ # lm is a DictConfig with 'target' and 'params' keys
385
+ if hasattr(cfg.model.params.lm, 'params') and hasattr(cfg.model.params.lm.params, 'model_path'):
386
+ model_path = cfg.model.params.lm.params.model_path
387
+ print(f"🔍 Original model_path: {model_path}")
388
+ # If it's a relative path (starts with ./) or local path (not a HF repo ID format)
389
+ # Resolve to absolute path using app.py's directory as base
390
+ if model_path.startswith('./') or (not os.path.isabs(model_path) and '/' in model_path and not model_path.count('/') == 1 and not model_path.startswith('google/') and not model_path.startswith('openai/')):
391
+ # Remove ./ prefix if present
392
+ clean_path = model_path[2:] if model_path.startswith('./') else model_path
393
+ abs_path = (app_dir / clean_path).resolve()
394
+ print(f"🔍 Checking: {abs_path} (exists: {abs_path.exists()})")
395
+ # Update if the path exists (local file)
396
+ if abs_path.exists():
397
+ # Direct assignment works with OmegaConf DictConfig
398
+ cfg.model.params.lm.params.model_path = str(abs_path)
399
+ print(f"📝 Resolved model_path: {model_path} -> {abs_path}")
400
+ else:
401
+ # Try parent directory (in case running from hf_space/)
402
+ parent_abs_path = (app_dir.parent / clean_path).resolve()
403
+ print(f"🔍 Checking parent: {parent_abs_path} (exists: {parent_abs_path.exists()})")
404
+ if parent_abs_path.exists():
405
+ # Direct assignment works with OmegaConf DictConfig
406
+ cfg.model.params.lm.params.model_path = str(parent_abs_path)
407
+ print(f"📝 Resolved model_path: {model_path} -> {parent_abs_path} (from parent directory)")
408
+ else:
409
+ print(f"⚠️ Model path {model_path} not found at {abs_path} or {parent_abs_path}. Keeping original path.")
410
+ else:
411
+ print(f"⚠️ Model path {model_path} doesn't match relative path pattern. Skipping resolution.")
412
+
413
+ # Fix whisper_path similarly
414
+ if hasattr(cfg, 'model') and hasattr(cfg.model, 'whisper_path'):
415
+ whisper_path = cfg.model.whisper_path
416
+ # Check if it's a relative path (not absolute, contains /, and not a HF repo ID like google/flan-t5-base)
417
+ # HF repo IDs have exactly 1 / and don't start with ./ or common local prefixes
418
+ is_local_path = (not os.path.isabs(whisper_path) and '/' in whisper_path and
419
+ (whisper_path.startswith('./') or
420
+ whisper_path.startswith('deps/') or
421
+ whisper_path.count('/') > 1 or
422
+ (whisper_path.count('/') == 1 and not whisper_path.startswith('google/') and not whisper_path.startswith('openai/'))))
423
+ if is_local_path:
424
+ clean_path = whisper_path[2:] if whisper_path.startswith('./') else whisper_path
425
+ abs_path = (app_dir / clean_path).resolve()
426
+ if abs_path.exists():
427
+ cfg.model.whisper_path = str(abs_path)
428
+ print(f"📝 Resolved whisper_path: {whisper_path} -> {abs_path}")
429
+ else:
430
+ parent_abs_path = (app_dir.parent / clean_path).resolve()
431
+ if parent_abs_path.exists():
432
+ cfg.model.whisper_path = str(parent_abs_path)
433
+ print(f"📝 Resolved whisper_path: {whisper_path} -> {parent_abs_path} (from parent directory)")
434
+ else:
435
+ print(f"⚠️ Whisper path {whisper_path} not found at {abs_path} or {parent_abs_path}. Keeping original path.")
436
+
437
+ # Fix checkpoint path similarly
438
+ if hasattr(cfg, 'TEST') and hasattr(cfg.TEST, 'CHECKPOINTS'):
439
+ checkpoint_path = cfg.TEST.CHECKPOINTS
440
+ if checkpoint_path and (checkpoint_path.startswith('./') or (not os.path.isabs(checkpoint_path) and '/' in checkpoint_path)):
441
+ clean_path = checkpoint_path[2:] if checkpoint_path.startswith('./') else checkpoint_path
442
+ abs_path = (app_dir / clean_path).resolve()
443
+ if abs_path.exists():
444
+ cfg.TEST.CHECKPOINTS = str(abs_path)
445
+ print(f"📝 Resolved checkpoint_path: {checkpoint_path} -> {abs_path}")
446
+ else:
447
+ parent_abs_path = (app_dir.parent / clean_path).resolve()
448
+ if parent_abs_path.exists():
449
+ cfg.TEST.CHECKPOINTS = str(parent_abs_path)
450
+ print(f"📝 Resolved checkpoint_path: {checkpoint_path} -> {parent_abs_path} (from parent directory)")
451
+
452
+ # Fix SMPL model path similarly
453
+ if hasattr(cfg, 'RENDER') and hasattr(cfg.RENDER, 'SMPL_MODEL_PATH'):
454
+ smpl_path = cfg.RENDER.SMPL_MODEL_PATH
455
+ if smpl_path and (smpl_path.startswith('./') or (not os.path.isabs(smpl_path) and '/' in smpl_path)):
456
+ clean_path = smpl_path[2:] if smpl_path.startswith('./') else smpl_path
457
+ abs_path = (app_dir / clean_path).resolve()
458
+ if abs_path.exists():
459
+ cfg.RENDER.SMPL_MODEL_PATH = str(abs_path)
460
+ print(f"📝 Resolved SMPL_MODEL_PATH: {smpl_path} -> {abs_path}")
461
+ else:
462
+ parent_abs_path = (app_dir.parent / clean_path).resolve()
463
+ if parent_abs_path.exists():
464
+ cfg.RENDER.SMPL_MODEL_PATH = str(parent_abs_path)
465
+ print(f"📝 Resolved SMPL_MODEL_PATH: {smpl_path} -> {parent_abs_path} (from parent directory)")
466
+
467
+ cfg.FOLDER = 'cache'
468
+ output_dir = Path("assets")
469
+ output_dir.mkdir(parents=True, exist_ok=True)
470
+ pl.seed_everything(cfg.SEED_VALUE)
471
+ if cfg.ACCELERATOR == "gpu":
472
+ device = torch.device("cuda")
473
+ else:
474
+ device = torch.device("cpu")
475
+ datamodule = build_data(cfg, phase="test")
476
+ model = build_model(cfg, datamodule)
477
+ state_dict = torch.load(cfg.TEST.CHECKPOINTS, map_location="cpu")["state_dict"]
478
+ model.load_state_dict(state_dict)
479
+ model.to(device)
480
+
481
+ audio_processor = WhisperProcessor.from_pretrained(cfg.model.whisper_path)
482
+ audio_model = WhisperForConditionalGeneration.from_pretrained(cfg.model.whisper_path).to(device)
483
+ forced_decoder_ids = audio_processor.get_decoder_prompt_ids(language="zh", task="translate")
484
+ forced_decoder_ids_zh = audio_processor.get_decoder_prompt_ids(language="zh", task="translate")
485
+ forced_decoder_ids_en = audio_processor.get_decoder_prompt_ids(language="en", task="translate")
486
+
487
+ def ensure_absolute_video_path(video_path: str) -> str:
488
+ """Convert a relative video path to an absolute path for Gradio uploads."""
489
+ if isinstance(video_path, str) and not os.path.isabs(video_path):
490
+ base_dir = os.path.dirname(os.path.abspath(__file__))
491
+ video_path = os.path.join(base_dir, video_path)
492
+ return video_path
493
+
494
+
495
+ def create_bot_message(content):
496
+ """Create an assistant message for Gradio's messages format."""
497
+ return {"role": "assistant", "content": content}
498
+
499
+
500
+ def create_user_message(content):
501
+ """Create a user message for Gradio's messages format."""
502
+ return {"role": "user", "content": content}
503
+
504
+ def create_video_message(video_path):
505
+ """Create a video message for Gradio 5.x chatbot"""
506
+ abs_path = ensure_absolute_video_path(video_path)
507
+ return create_bot_message({"path": abs_path, "mime_type": "video/mp4"})
508
+
509
+ def create_example_video(video_path):
510
+ """Create a video reference for examples"""
511
+ return create_video_message(video_path)
512
+
513
+ def create_download_links(video_path, motion_path, video_fname, motion_fname):
514
+ """Create download links for video and motion files"""
515
+ import os
516
+ # Get absolute paths for downloads
517
+ abs_video_path = os.path.abspath(video_path)
518
+ abs_motion_path = os.path.abspath(motion_path)
519
+
520
+ text = f"""**Generated Files:**
521
+ - **Video:** `{video_fname}` → saved to `{video_path}`
522
+ - **Motion Data:** `{motion_fname}` → saved to `{motion_path}`
523
+
524
+ **To download:** Right-click on the video above and select "Save video as..." or access files directly from the paths shown above."""
525
+ return create_bot_message(text)
526
+
527
+
528
+ def motion_token_to_string(motion_token, lengths, codebook_size=512):
529
+ motion_string = []
530
+ for i in range(motion_token.shape[0]):
531
+ motion_i = motion_token[i].cpu(
532
+ ) if motion_token.device.type == 'cuda' else motion_token[i]
533
+ motion_list = motion_i.tolist()[:lengths[i]]
534
+ motion_string.append(
535
+ (f'<motion_id_{codebook_size}>' +
536
+ ''.join([f'<motion_id_{int(i)}>' for i in motion_list]) +
537
+ f'<motion_id_{codebook_size + 1}>'))
538
+ return motion_string
539
+
540
+
541
+ def render_motion(data, feats, method='fast'):
542
+ fname = time.strftime("%Y-%m-%d-%H_%M_%S", time.localtime(
543
+ time.time())) + str(np.random.randint(10000, 99999))
544
+ video_fname = fname + '.mp4'
545
+ feats_fname = fname + '.npy'
546
+ output_npy_path = os.path.join(output_dir, feats_fname)
547
+ output_mp4_path = os.path.join(output_dir, video_fname)
548
+ np.save(output_npy_path, feats)
549
+
550
+ if method == 'slow':
551
+ # Validate slow mode dependencies
552
+ if SMPLRender is None:
553
+ raise RuntimeError("SMPLRender is not available. Cannot use slow mode.")
554
+
555
+ smpl_model_path = cfg.RENDER.SMPL_MODEL_PATH
556
+ if not os.path.exists(smpl_model_path):
557
+ raise FileNotFoundError(
558
+ f"SMPL model path does not exist: {smpl_model_path}\n"
559
+ f"Slow mode requires SMPL models to be downloaded. "
560
+ f"Expected path: {os.path.abspath(smpl_model_path)}"
561
+ )
562
+
563
+ # Perform slow mode rendering (SMPL)
564
+ if len(data.shape) == 4:
565
+ data = data[0]
566
+ data = data - data[0, 0]
567
+ pose_generator = HybrIKJointsToRotmat()
568
+ pose = pose_generator(data)
569
+ pose = np.concatenate([
570
+ pose,
571
+ np.stack([np.stack([np.eye(3)] * pose.shape[0], 0)] * 2, 1)
572
+ ], 1)
573
+ shape = [768, 768]
574
+ # Force CPU for SMPL rendering to avoid CUDA compatibility issues
575
+ # (PyTorch may not support older GPUs like V100 with CUDA 7.0)
576
+ original_cuda_visible = os.environ.get('CUDA_VISIBLE_DEVICES', None)
577
+ os.environ['CUDA_VISIBLE_DEVICES'] = '' # Hide CUDA to force CPU
578
+
579
+ # Try OSMesa for headless environments if EGL fails
580
+ original_pyopengl = os.environ.get('PYOPENGL_PLATFORM', None)
581
+
582
+ try:
583
+ render = SMPLRender(smpl_model_path)
584
+ # Ensure it's using CPU (in case CUDA_VISIBLE_DEVICES didn't work)
585
+ if render.device.type == 'cuda':
586
+ print("⚠️ Renderer is using CUDA, forcing to CPU for compatibility...")
587
+ render.device = torch.device("cpu")
588
+ render.smpl = render.smpl.cpu()
589
+ except (ImportError, OSError) as e:
590
+ if "EGL" in str(e) or "egl" in str(e).lower():
591
+ # EGL failed, try OSMesa (software rendering for headless)
592
+ print("⚠️ EGL not available, trying OSMesa (software rendering)...")
593
+ os.environ['PYOPENGL_PLATFORM'] = 'osmesa'
594
+ try:
595
+ render = SMPLRender(smpl_model_path)
596
+ if render.device.type == 'cuda':
597
+ render.device = torch.device("cpu")
598
+ render.smpl = render.smpl.cpu()
599
+ except Exception as osmesa_error:
600
+ print(f"❌ OSMesa also failed: {osmesa_error}")
601
+ raise RuntimeError(
602
+ "Slow mode (SMPL rendering) requires OpenGL/EGL or OSMesa. "
603
+ "Neither is available in this environment. "
604
+ "Please use fast mode instead, or install OpenGL libraries."
605
+ )
606
+ else:
607
+ raise
608
+ finally:
609
+ # Restore original settings
610
+ if original_cuda_visible is not None:
611
+ os.environ['CUDA_VISIBLE_DEVICES'] = original_cuda_visible
612
+ else:
613
+ os.environ.pop('CUDA_VISIBLE_DEVICES', None)
614
+ if original_pyopengl is not None:
615
+ os.environ['PYOPENGL_PLATFORM'] = original_pyopengl
616
+
617
+ r = RRR.from_rotvec(np.array([np.pi, 0.0, 0.0]))
618
+ pose[:, 0] = np.matmul(r.as_matrix().reshape(1, 3, 3), pose[:, 0])
619
+ vid = []
620
+ aroot = data[[0], 0]
621
+ aroot[:, 1] = -aroot[:, 1]
622
+ params = dict(pred_shape=np.zeros([1, 10]),
623
+ pred_root=aroot,
624
+ pred_pose=pose)
625
+ try:
626
+ render.init_renderer([shape[0], shape[1], 3], params)
627
+ except (ImportError, OSError, RuntimeError, AttributeError) as e:
628
+ error_str = str(e)
629
+ if any(x in error_str for x in ["EGL", "egl", "OpenGL", "OSMesa", "osmesa", "GLXPlatform"]):
630
+ # OpenGL/EGL/OSMesa error - try to fix by reinstalling/reinitializing
631
+ if is_hf_space:
632
+ # In HuggingFace Spaces, OSMesa should be installed via packages.txt
633
+ # If we get here, it means OSMesa is not properly installed
634
+ raise RuntimeError(
635
+ "Slow mode (SMPL rendering) requires OSMesa libraries. "
636
+ "Please ensure packages.txt includes: libosmesa6-dev libgl1 libglx-mesa0. "
637
+ f"Error: {error_str}"
638
+ )
639
+ else:
640
+ raise RuntimeError(
641
+ f"Slow mode (SMPL rendering) failed: {error_str}. "
642
+ "Please check that OpenGL/EGL libraries are installed."
643
+ )
644
+ else:
645
+ raise
646
+
647
+ for i in range(data.shape[0]):
648
+ try:
649
+ renderImg = render.render(i)
650
+ vid.append(renderImg)
651
+ except (TypeError, AttributeError) as render_error:
652
+ # PyOpenGL-accelerate causes TypeError during rendering
653
+ if "NoneType" in str(render_error) or "zeros()" in str(render_error):
654
+ print(f"⚠️ Rendering error (PyOpenGL-accelerate): {render_error}")
655
+ print(" Uninstalling PyOpenGL-accelerate and retrying...")
656
+ subprocess.check_call([
657
+ sys.executable, "-m", "pip", "uninstall", "-y", "PyOpenGL-accelerate"
658
+ ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
659
+ # Clear module cache
660
+ modules_to_clear = [m for m in sys.modules.keys() if 'OpenGL' in m or 'pyrender' in m]
661
+ for m in modules_to_clear:
662
+ del sys.modules[m]
663
+ # Recreate renderer
664
+ render = SMPLRender(smpl_model_path)
665
+ if render.device.type == 'cuda':
666
+ render.device = torch.device("cpu")
667
+ render.smpl = render.smpl.cpu()
668
+ render.init_renderer([shape[0], shape[1], 3], params)
669
+ # Retry rendering
670
+ renderImg = render.render(i)
671
+ vid.append(renderImg)
672
+ else:
673
+ raise
674
+
675
+ out = np.stack(vid, axis=0)
676
+ output_gif_path = output_mp4_path[:-4] + '.gif'
677
+ imageio.mimwrite(output_gif_path, out, duration=50)
678
+ out_video = VideoFileClip(output_gif_path)
679
+ out_video.write_videofile(output_mp4_path)
680
+ del out, render
681
+
682
+ elif method == 'fast':
683
+ output_gif_path = output_mp4_path[:-4] + '.gif'
684
+ if len(data.shape) == 3:
685
+ data = data[None]
686
+ if isinstance(data, torch.Tensor):
687
+ data = data.cpu().numpy()
688
+ pose_vis = plot_3d.draw_to_batch(data, [''], [output_gif_path])
689
+ out_video = VideoFileClip(output_gif_path)
690
+ out_video.write_videofile(output_mp4_path)
691
+ del pose_vis
692
+ else:
693
+ raise ValueError(f"Unknown rendering method: {method}. Must be 'slow' or 'fast'.")
694
+
695
+ return output_mp4_path, video_fname, output_npy_path, feats_fname
696
+
697
+
698
+ def load_motion(motion_uploaded, method):
699
+ file = motion_uploaded['file']
700
+
701
+ feats = torch.tensor(np.load(file), device=model.device)
702
+ if len(feats.shape) == 2:
703
+ feats = feats[None]
704
+ # feats = model.datamodule.normalize(feats)
705
+
706
+ # Motion tokens
707
+ motion_lengths = feats.shape[0]
708
+ motion_token, _ = model.vae.encode(feats)
709
+
710
+ motion_token_string = model.lm.motion_token_to_string(
711
+ motion_token, [motion_token.shape[1]])[0]
712
+ motion_token_length = motion_token.shape[1]
713
+
714
+ # Motion rendered
715
+ joints = model.datamodule.feats2joints(feats.cpu()).cpu().numpy()
716
+ output_mp4_path, video_fname, output_npy_path, joints_fname = render_motion(
717
+ joints,
718
+ feats.to('cpu').numpy(), method)
719
+
720
+ motion_uploaded.update({
721
+ "feats": feats,
722
+ "joints": joints,
723
+ "motion_video": output_mp4_path,
724
+ "motion_video_fname": video_fname,
725
+ "motion_joints": output_npy_path,
726
+ "motion_joints_fname": joints_fname,
727
+ "motion_lengths": motion_lengths,
728
+ "motion_token": motion_token,
729
+ "motion_token_string": motion_token_string,
730
+ "motion_token_length": motion_token_length,
731
+ })
732
+
733
+ return motion_uploaded
734
+
735
+
736
+ def add_text(history, text, motion_uploaded, data_stored, method):
737
+ data_stored = data_stored + [{'user_input': text}]
738
+
739
+ history = history + [create_user_message(text)]
740
+ if 'file' in motion_uploaded.keys():
741
+ motion_uploaded = load_motion(motion_uploaded, method)
742
+ output_mp4_path = motion_uploaded['motion_video']
743
+ video_fname = motion_uploaded['motion_video_fname']
744
+ output_npy_path = motion_uploaded['motion_joints']
745
+ joints_fname = motion_uploaded['motion_joints_fname']
746
+
747
+ # Add video using Gradio 5.x messages format
748
+ video_msg = create_video_message(output_mp4_path)
749
+ history = history + [video_msg]
750
+
751
+ return history, gr.update(value="",
752
+ interactive=False), motion_uploaded, data_stored
753
+
754
+
755
+ def add_audio(history, audio_path, data_stored, language='en'):
756
+ audio, sampling_rate = librosa.load(audio_path, sr=16000)
757
+ input_features = audio_processor(
758
+ audio, sampling_rate, return_tensors="pt"
759
+ ).input_features # whisper training sampling rate, do not modify
760
+ input_features = torch.Tensor(input_features).to(device)
761
+
762
+ if language == 'English':
763
+ forced_decoder_ids = forced_decoder_ids_en
764
+ else:
765
+ forced_decoder_ids = forced_decoder_ids_zh
766
+ predicted_ids = audio_model.generate(input_features,
767
+ forced_decoder_ids=forced_decoder_ids)
768
+ text_input = audio_processor.batch_decode(predicted_ids,
769
+ skip_special_tokens=True)
770
+ text_input = str(text_input).strip('[]"')
771
+ data_stored = data_stored + [{'user_input': text_input}]
772
+ gr.update(value=data_stored, interactive=False)
773
+ history = history + [create_user_message(text_input)]
774
+
775
+ return history, data_stored
776
+
777
+
778
+ def add_file(history, file, txt, motion_uploaded):
779
+ motion_uploaded['file'] = file.name
780
+ txt = txt.replace(" <Motion_Placeholder>", "") + " <Motion_Placeholder>"
781
+ return history, gr.update(value=txt, interactive=True), motion_uploaded
782
+
783
+
784
+ def bot(history, motion_uploaded, data_stored, method):
785
+
786
+ motion_length, motion_token_string = motion_uploaded[
787
+ "motion_lengths"], motion_uploaded["motion_token_string"]
788
+
789
+ input = data_stored[-1]['user_input']
790
+ prompt = model.lm.placeholder_fulfill(input, motion_length,
791
+ motion_token_string, "")
792
+ data_stored[-1]['model_input'] = prompt
793
+ batch = {
794
+ "length": [motion_length],
795
+ "text": [prompt],
796
+ }
797
+
798
+ outputs = model(batch, task="t2m")
799
+ out_feats = outputs["feats"][0]
800
+ out_lengths = outputs["length"][0]
801
+ out_joints = outputs["joints"][:out_lengths].detach().cpu().numpy()
802
+ out_texts = outputs["texts"][0]
803
+ output_mp4_path, video_fname, output_npy_path, joints_fname = render_motion(
804
+ out_joints,
805
+ out_feats.to('cpu').numpy(), method)
806
+
807
+ motion_uploaded = {
808
+ "feats": None,
809
+ "joints": None,
810
+ "motion_video": None,
811
+ "motion_lengths": 0,
812
+ "motion_token": None,
813
+ "motion_token_string": '',
814
+ "motion_token_length": 0,
815
+ }
816
+
817
+ data_stored[-1]['model_output'] = {
818
+ "feats": out_feats,
819
+ "joints": out_joints,
820
+ "length": out_lengths,
821
+ "texts": out_texts,
822
+ "motion_video": output_mp4_path,
823
+ "motion_video_fname": video_fname,
824
+ "motion_joints": output_npy_path,
825
+ "motion_joints_fname": joints_fname,
826
+ }
827
+
828
+ if '<Motion_Placeholder>' == out_texts:
829
+ response = f"Generated motion video: {video_fname}"
830
+ is_motion_generation = True
831
+ elif '<Motion_Placeholder>' in out_texts:
832
+ response = f"{out_texts.split('<Motion_Placeholder>')[0]} Generated motion video: {video_fname} {out_texts.split('<Motion_Placeholder>')[1]}"
833
+ is_motion_generation = True
834
+ else:
835
+ # This is motion-to-text task, only show text description
836
+ response = f"{out_texts}"
837
+ is_motion_generation = False
838
+
839
+ # Add bot response - animate text character by character
840
+ bot_response_msg = create_bot_message("")
841
+ history = history + [bot_response_msg]
842
+
843
+ for character in response:
844
+ history[-1]["content"] += character
845
+ time.sleep(0.02)
846
+ yield history, motion_uploaded, data_stored
847
+
848
+ # Add video to chat only for text-to-motion tasks (not motion-to-text)
849
+ if is_motion_generation:
850
+ video_msg = create_video_message(output_mp4_path)
851
+ history = history + [video_msg]
852
+ yield history, motion_uploaded, data_stored
853
+
854
+
855
+ def bot_example(history, responses):
856
+ """Append example responses to chatbot history (messages format)"""
857
+ # Ensure both are lists
858
+ if not isinstance(history, list):
859
+ history = []
860
+ if not isinstance(responses, list):
861
+ responses = [responses]
862
+ # Concatenate and return (messages format)
863
+ return history + responses
864
+
865
+
866
+ with open("assets/css/custom.css", "r", encoding="utf-8") as f:
867
+ customCSS = f.read()
868
+
869
+ with gr.Blocks(css=customCSS) as demo:
870
+
871
+ # Examples - converted to messages format
872
+ chat_instruct = gr.State([
873
+ create_bot_message("Hi, I'm MotionGPT! I can generate realistic human motion from text, or generate text from motion."),
874
+ create_bot_message("You can chat with me in pure text like generating human motion following your descriptions."),
875
+ create_bot_message("After generation, you can click the button in the top right of generation human motion result to download the human motion video or feature stored in .npy format."),
876
+ create_bot_message("With the human motion feature file downloaded or got from dataset, you are able to ask me to translate it!"),
877
+ create_bot_message("Of courser, you can also purely chat with me and let me give you human motion in text, here are some examples!"),
878
+ create_bot_message("We provide two motion visulization methods. The default fast method is skeleton line ploting which is like the examples below:"),
879
+ create_example_video("assets/videos/example0_fast.mp4"),
880
+ create_bot_message("And the slow method is SMPL model rendering which is more realistic but slower."),
881
+ create_example_video("assets/videos/example0.mp4"),
882
+ create_bot_message("If you want to get the video in our paper and website like below, you can refer to the scirpt in our [github repo](https://github.com/OpenMotionLab/MotionGPT#-visualization)."),
883
+ create_example_video("assets/videos/example0_blender.mp4"),
884
+ create_bot_message("Follow the examples and try yourself!"),
885
+ ])
886
+ chat_instruct_sum = gr.State([create_bot_message('''Hi, I'm MotionGPT! I can generate realistic human motion from text, or generate text from motion.
887
+
888
+ 1. You can chat with me in pure text like generating human motion following your descriptions.
889
+ 2. After generation, you can click the button in the top right of generation human motion result to download the human motion video or feature stored in .npy format.
890
+ 3. With the human motion feature file downloaded or got from dataset, you are able to ask me to translate it!
891
+ 4. Of course, you can also purely chat with me and let me give you human motion in text, here are some examples!
892
+ ''')] + chat_instruct.value[-7:])
893
+
894
+ t2m_examples = gr.State([
895
+ create_bot_message("You can chat with me in pure text, following are some examples of text-to-motion generation!"),
896
+ create_user_message("A person is walking forwards, but stumbles and steps back, then carries on forward."),
897
+ create_example_video("assets/videos/example0.mp4"),
898
+ create_user_message("Generate a man aggressively kicks an object to the left using his right foot."),
899
+ create_example_video("assets/videos/example1.mp4"),
900
+ create_user_message("Generate a person lowers their arms, gets onto all fours, and crawls."),
901
+ create_example_video("assets/videos/example2.mp4"),
902
+ create_user_message("Show me the video of a person bends over and picks things up with both hands individually, then walks forward."),
903
+ create_example_video("assets/videos/example3.mp4"),
904
+ create_user_message("Imagine a person is practing balancing on one leg."),
905
+ create_example_video("assets/videos/example5.mp4"),
906
+ create_user_message("Show me a person walks forward, stops, turns directly to their right, then walks forward again."),
907
+ create_example_video("assets/videos/example6.mp4"),
908
+ create_user_message("I saw a person sits on the ledge of something then gets off and walks away."),
909
+ create_example_video("assets/videos/example7.mp4"),
910
+ create_user_message("Show me a person is crouched down and walking around sneakily."),
911
+ create_example_video("assets/videos/example8.mp4"),
912
+ ])
913
+
914
+ m2t_examples = gr.State([
915
+ create_bot_message("With the human motion feature file downloaded or got from dataset, you are able to ask me to translate it, here are some examples!"),
916
+ create_user_message("Please explain the movement shown in <Motion_Placeholder> using natural language."),
917
+ create_example_video("assets/videos/example0.mp4"),
918
+ create_bot_message("The person was pushed but didn't fall down"),
919
+ create_user_message("What kind of action is being represented in <Motion_Placeholder>? Explain it in text."),
920
+ create_example_video("assets/videos/example4.mp4"),
921
+ create_bot_message("The figure has its hands curled at jaw level, steps onto its left foot and raises right leg with bent knee to kick forward and return to starting stance."),
922
+ create_user_message("Provide a summary of the motion demonstrated in <Motion_Placeholder> using words."),
923
+ create_example_video("assets/videos/example2.mp4"),
924
+ create_bot_message("A person who is standing with his arms up and away from his sides bends over, gets down on his hands and then his knees and crawls forward."),
925
+ create_user_message("Generate text for <Motion_Placeholder>:"),
926
+ create_example_video("assets/videos/example5.mp4"),
927
+ create_bot_message("The man tries to stand in a yoga tree pose and looses his balance."),
928
+ create_user_message("Provide a summary of the motion depicted in <Motion_Placeholder> using language."),
929
+ create_example_video("assets/videos/example6.mp4"),
930
+ create_bot_message("Person walks up some steps then leeps to the other side and goes up a few more steps and jumps dow"),
931
+ create_user_message("Describe the motion represented by <Motion_Placeholder> in plain English."),
932
+ create_example_video("assets/videos/example7.mp4"),
933
+ create_bot_message("Person sits down, then stands up and walks forward. then the turns around 180 degrees and walks the opposite direction"),
934
+ create_user_message("Provide a description of the action in <Motion_Placeholder> using words."),
935
+ create_example_video("assets/videos/example8.mp4"),
936
+ create_bot_message("This man is bent forward and walks slowly around."),
937
+ ])
938
+
939
+ t2t_examples = gr.State([
940
+ create_bot_message("Of course, you can also purely chat with me and let me give you human motion in text, here are some examples!"),
941
+ create_user_message('Depict a motion as like you have seen it.'),
942
+ create_bot_message("A person slowly walked forward in rigth direction while making the circle"),
943
+ create_user_message('Random say something about describing a human motion.'),
944
+ create_bot_message("A man throws punches using his right hand."),
945
+ create_user_message('Describe the motion of someone as you will.'),
946
+ create_bot_message("Person is moving left to right in a dancing stance swaying hips, moving feet left to right with arms held out"),
947
+ create_user_message('Come up with a human motion caption.'),
948
+ create_bot_message("A person is walking in a counter counterclockwise motion."),
949
+ create_user_message('Write a sentence about how someone might dance.'),
950
+ create_bot_message("A person with his hands down by his sides reaches down for something with his right hand, uses the object to make a stirring motion, then places the item back down."),
951
+ create_user_message('Depict a motion as like you have seen it.'),
952
+ create_bot_message("A person is walking forward a few feet, then turns around, walks back, and continues walking.")
953
+ ])
954
+
955
+ # Convert messages to Gradio 4.0.0 format (list of tuples)
956
+ def convert_to_tuples(messages):
957
+ """Convert list of messages to list of tuples for Gradio 4.0.0"""
958
+ result = []
959
+ i = 0
960
+ while i < len(messages):
961
+ msg = messages[i]
962
+ if isinstance(msg, tuple):
963
+ # Already a tuple
964
+ result.append(msg)
965
+ i += 1
966
+ elif isinstance(msg, dict):
967
+ # Old format - skip (will be handled by new format)
968
+ i += 1
969
+ else:
970
+ # String message - check if it's user or bot
971
+ # For now, treat as bot message and pair with None user
972
+ result.append((None, msg))
973
+ i += 1
974
+ return result
975
+
976
+ # Combine examples and convert to tuple format for Gradio
977
+ # Handle videos based on Gradio version (4.0.0 doesn't support dict format)
978
+ Init_chatbot = (
979
+ chat_instruct.value[:1]
980
+ + t2m_examples.value[:3]
981
+ + m2t_examples.value[:3]
982
+ + t2t_examples.value[:2]
983
+ + chat_instruct.value[-7:]
984
+ )
985
+
986
+ # Variables
987
+ motion_uploaded = gr.State({
988
+ "feats": None,
989
+ "joints": None,
990
+ "motion_video": None,
991
+ "motion_lengths": 0,
992
+ "motion_token": None,
993
+ "motion_token_string": '',
994
+ "motion_token_length": 0,
995
+ })
996
+ data_stored = gr.State([])
997
+
998
+ gr.Markdown("# MotionGPT")
999
+
1000
+ chatbot = gr.Chatbot(Init_chatbot,
1001
+ elem_id="mGPT",
1002
+ height=600,
1003
+ label="MotionGPT",
1004
+ type="messages",
1005
+ avatar_images=(None, "assets/images/avatar_bot.jpg"),
1006
+ show_copy_button=True)
1007
+
1008
+ with gr.Row():
1009
+ with gr.Column(scale=6):
1010
+ with gr.Row():
1011
+ txt = gr.Textbox(
1012
+ label="Text",
1013
+ show_label=False,
1014
+ elem_id="textbox",
1015
+ placeholder=
1016
+ "Enter text and press ENTER or speak to input. You can also upload motion.",
1017
+ container=False)
1018
+
1019
+ with gr.Row():
1020
+ aud = gr.Audio(sources=["microphone"],
1021
+ label="Speak input",
1022
+ type='filepath')
1023
+ btn = gr.UploadButton("📁 Upload motion",
1024
+ elem_id="upload",
1025
+ file_types=["file"])
1026
+ # regen = gr.Button("🔄 Regenerate", elem_id="regen")
1027
+ clear = gr.ClearButton([txt, chatbot, aud], value='🗑️ Clear')
1028
+
1029
+ with gr.Row():
1030
+ gr.Markdown('''
1031
+ ### You can get more examples (pre-generated for faster response) by clicking the buttons below:
1032
+ ''')
1033
+
1034
+ with gr.Row():
1035
+ instruct_eg = gr.Button("Instructions", elem_id="instruct")
1036
+ t2m_eg = gr.Button("Text-to-Motion", elem_id="t2m")
1037
+ m2t_eg = gr.Button("Motion-to-Text", elem_id="m2t")
1038
+ t2t_eg = gr.Button("Random description", elem_id="t2t")
1039
+
1040
+ with gr.Column(scale=1, min_width=150):
1041
+ method = gr.Dropdown(["slow", "fast"],
1042
+ label="Visualization method",
1043
+ interactive=True,
1044
+ elem_id="method",
1045
+ value="fast")
1046
+
1047
+ language = gr.Dropdown(["English", "中文"],
1048
+ label="Speech language",
1049
+ interactive=True,
1050
+ elem_id="language",
1051
+ value="English")
1052
+
1053
+ txt_msg = txt.submit(
1054
+ add_text, [chatbot, txt, motion_uploaded, data_stored, method],
1055
+ [chatbot, txt, motion_uploaded, data_stored],
1056
+ queue=False).then(bot, [chatbot, motion_uploaded, data_stored, method],
1057
+ [chatbot, motion_uploaded, data_stored])
1058
+
1059
+ txt_msg.then(lambda: gr.update(interactive=True), None, [txt], queue=False)
1060
+
1061
+ file_msg = btn.upload(add_file, [chatbot, btn, txt, motion_uploaded],
1062
+ [chatbot, txt, motion_uploaded],
1063
+ queue=False)
1064
+ aud_msg = aud.stop_recording(
1065
+ add_audio, [chatbot, aud, data_stored, language],
1066
+ [chatbot, data_stored],
1067
+ queue=False).then(bot, [chatbot, motion_uploaded, data_stored, method],
1068
+ [chatbot, motion_uploaded, data_stored])
1069
+ # regen_msg = regen.click(bot,
1070
+ # [chatbot, motion_uploaded, data_stored, method],
1071
+ # [chatbot, motion_uploaded, data_stored],
1072
+ # queue=False)
1073
+
1074
+ instruct_msg = instruct_eg.click(bot_example, [chatbot, chat_instruct_sum],
1075
+ [chatbot],
1076
+ queue=False)
1077
+ t2m_eg_msg = t2m_eg.click(bot_example, [chatbot, t2m_examples], [chatbot],
1078
+ queue=False)
1079
+ m2t_eg_msg = m2t_eg.click(bot_example, [chatbot, m2t_examples], [chatbot],
1080
+ queue=False)
1081
+ t2t_eg_msg = t2t_eg.click(bot_example, [chatbot, t2t_examples], [chatbot],
1082
+ queue=False)
1083
+
1084
+ chatbot.change(scroll_to_output=True)
1085
+
1086
+ demo.queue()
1087
+
1088
+ # Disable API docs to avoid schema generation error (TypeError in gradio_client)
1089
+ try:
1090
+ demo.api_open = False
1091
+ except:
1092
+ pass
1093
+
1094
+ if __name__ == "__main__":
1095
+ # Detect HuggingFace Spaces environment
1096
+ is_hf_space = os.getenv("SPACE_ID") is not None
1097
+
1098
+ if is_hf_space:
1099
+ # HuggingFace Spaces - use default settings
1100
+ demo.launch(server_name="0.0.0.0", server_port=7860, share=False)
1101
+ else:
1102
+ # Local deployment with ngrok
1103
+ try:
1104
+ from pyngrok import ngrok
1105
+
1106
+ SERVER_PORT = 7860
1107
+
1108
+ def start_ngrok():
1109
+ time.sleep(2)
1110
+ tunnel = ngrok.connect(SERVER_PORT)
1111
+ print(f"\n🌐 Public URL: {tunnel.public_url}")
1112
+ print("🔗 Share this URL to access your MotionGPT app from anywhere!")
1113
+
1114
+ ngrok_thread = threading.Thread(target=start_ngrok)
1115
+ ngrok_thread.daemon = True
1116
+ ngrok_thread.start()
1117
+
1118
+ demo.launch(server_name="0.0.0.0", server_port=SERVER_PORT, share=True, debug=True)
1119
+ except ImportError:
1120
+ # Fallback to Gradio share if ngrok not available
1121
+ demo.launch(server_name="0.0.0.0", server_port=7860, share=True, debug=True)
assets/2025-10-18-17_10_3968067.gif ADDED

Git LFS Details

  • SHA256: c91b69ca6c0442eb7e3e3c65026424ac425e002317b6b23cec5c73ac34e6f38a
  • Pointer size: 131 Bytes
  • Size of remote file: 875 kB
assets/2025-10-18-17_10_3968067.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:05c3632c3c24ce10216fe6863493aa7ef5680a6fb4507b5bd16c48e6d67b700e
3
+ size 154914
assets/2025-10-18-17_10_3968067.npy ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4e7c6941ca3caddefb60f8994802441fe8cbd3bbff9ff8b4724e194f05252169
3
+ size 206320
assets/2025-10-18-17_18_3068067.gif ADDED

Git LFS Details

  • SHA256: 55c74b96ec8a29081402070f7f296d6e8becba4e6f4f9b26ba6c933c289caf99
  • Pointer size: 131 Bytes
  • Size of remote file: 179 kB
assets/2025-10-18-17_18_3068067.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4a384edbc8bc4e95ad7ce3dd28858cb4bd050c35f494554759640c51a3846527
3
+ size 22061
assets/2025-10-18-17_18_3068067.npy ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4784c4c5f6db8f529dd1f4bbe21e3ee8faaba212ba9ec6734bc61badd9f5b6ae
3
+ size 59040
assets/2025-10-18-17_22_1444086.npy ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7d299ca5c8f5216c42ec5badd083afdf6252fd851d2bbddeb54101528cae8666
3
+ size 206320
assets/2025-10-18-17_23_5868067.gif ADDED

Git LFS Details

  • SHA256: 55c74b96ec8a29081402070f7f296d6e8becba4e6f4f9b26ba6c933c289caf99
  • Pointer size: 131 Bytes
  • Size of remote file: 179 kB
assets/2025-10-18-17_23_5868067.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4a384edbc8bc4e95ad7ce3dd28858cb4bd050c35f494554759640c51a3846527
3
+ size 22061
assets/2025-10-18-17_23_5868067.npy ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4784c4c5f6db8f529dd1f4bbe21e3ee8faaba212ba9ec6734bc61badd9f5b6ae
3
+ size 59040
assets/2025-10-18-17_26_2068067.gif ADDED

Git LFS Details

  • SHA256: 55c74b96ec8a29081402070f7f296d6e8becba4e6f4f9b26ba6c933c289caf99
  • Pointer size: 131 Bytes
  • Size of remote file: 179 kB
assets/2025-10-18-17_26_2068067.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4a384edbc8bc4e95ad7ce3dd28858cb4bd050c35f494554759640c51a3846527
3
+ size 22061
assets/2025-10-18-17_26_2068067.npy ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4784c4c5f6db8f529dd1f4bbe21e3ee8faaba212ba9ec6734bc61badd9f5b6ae
3
+ size 59040
assets/2025-10-18-17_26_4344086.gif ADDED

Git LFS Details

  • SHA256: 5357eb0503f9b0c9885597aa22e7cbaa1c70f74a6f77f992bee822b47e4fd617
  • Pointer size: 131 Bytes
  • Size of remote file: 192 kB
assets/2025-10-18-17_26_4344086.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:74fe9e8807c1fcae5627814abdcd0f40f0600169b1c1d4e064d269dd369f5183
3
+ size 44827
assets/2025-10-18-17_26_4344086.npy ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1ef436003716cacf0007c0098359776e3db47dad69f957c316da5d20d2a145b6
3
+ size 54832
assets/2025-10-18-17_28_0770620.gif ADDED

Git LFS Details

  • SHA256: 8a837e9881b16dba8b9b6fccb84feb7cca6182b0d5f8ddd56fc1fe63a30103e9
  • Pointer size: 131 Bytes
  • Size of remote file: 260 kB
assets/2025-10-18-17_28_0770620.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4a369be7458bc3e40a4e6219cd953b6258a8adbed6a96eb0c62fe0942ef3730b
3
+ size 51986
assets/2025-10-18-17_28_0770620.npy ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2e2665a0fde8bc84979d24cfcabf0191d12982e66c92e0c2376ee1822d02d4c7
3
+ size 71664
assets/2025-10-18-17_28_4799460.gif ADDED

Git LFS Details

  • SHA256: 8a837e9881b16dba8b9b6fccb84feb7cca6182b0d5f8ddd56fc1fe63a30103e9
  • Pointer size: 131 Bytes
  • Size of remote file: 260 kB
assets/2025-10-18-17_28_4799460.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4a369be7458bc3e40a4e6219cd953b6258a8adbed6a96eb0c62fe0942ef3730b
3
+ size 51986
assets/2025-10-18-17_28_4799460.npy ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2e2665a0fde8bc84979d24cfcabf0191d12982e66c92e0c2376ee1822d02d4c7
3
+ size 71664
assets/2025-10-18-17_38_3668067.gif ADDED

Git LFS Details

  • SHA256: 55c74b96ec8a29081402070f7f296d6e8becba4e6f4f9b26ba6c933c289caf99
  • Pointer size: 131 Bytes
  • Size of remote file: 179 kB
assets/2025-10-18-17_38_3668067.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4a384edbc8bc4e95ad7ce3dd28858cb4bd050c35f494554759640c51a3846527
3
+ size 22061
assets/2025-10-18-17_38_3668067.npy ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4784c4c5f6db8f529dd1f4bbe21e3ee8faaba212ba9ec6734bc61badd9f5b6ae
3
+ size 59040
assets/2025-10-18-17_46_1768067.gif ADDED

Git LFS Details

  • SHA256: 55c74b96ec8a29081402070f7f296d6e8becba4e6f4f9b26ba6c933c289caf99
  • Pointer size: 131 Bytes
  • Size of remote file: 179 kB
assets/2025-10-18-17_46_1768067.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4a384edbc8bc4e95ad7ce3dd28858cb4bd050c35f494554759640c51a3846527
3
+ size 22061
assets/2025-10-18-17_46_1768067.npy ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4784c4c5f6db8f529dd1f4bbe21e3ee8faaba212ba9ec6734bc61badd9f5b6ae
3
+ size 59040
assets/2025-10-18-17_46_3844086.gif ADDED

Git LFS Details

  • SHA256: d59a04be6117e7d4cffd203f37e3bb93ed03570f6b6ac58f5e63ae0581b20b0f
  • Pointer size: 130 Bytes
  • Size of remote file: 13.9 kB
assets/2025-10-18-17_46_3844086.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:703847d1a26f58e89b5e08a1793674400cb1fad7e2e8ff6164cb76a0549a6fbd
3
+ size 4848
assets/2025-10-18-17_46_3844086.npy ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:fb54dd3a35900c051f356dbc2824c03baa9b6e9fe34b3247e9c35fe0ba6db663
3
+ size 4336
assets/2025-10-18-17_49_2670620.gif ADDED

Git LFS Details

  • SHA256: c91b69ca6c0442eb7e3e3c65026424ac425e002317b6b23cec5c73ac34e6f38a
  • Pointer size: 131 Bytes
  • Size of remote file: 875 kB
assets/2025-10-18-17_49_2670620.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:05c3632c3c24ce10216fe6863493aa7ef5680a6fb4507b5bd16c48e6d67b700e
3
+ size 154914
assets/2025-10-18-17_49_2670620.npy ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:fe9a2f83e048fc5e612033b88ccd0798fb61d6abd38c5b3dd7b6539dacab091e
3
+ size 206320
assets/2025-10-18-17_49_4299460.gif ADDED

Git LFS Details

  • SHA256: d59a04be6117e7d4cffd203f37e3bb93ed03570f6b6ac58f5e63ae0581b20b0f
  • Pointer size: 130 Bytes
  • Size of remote file: 13.9 kB
assets/2025-10-18-17_49_4299460.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:703847d1a26f58e89b5e08a1793674400cb1fad7e2e8ff6164cb76a0549a6fbd
3
+ size 4848
assets/2025-10-18-17_49_4299460.npy ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:fb54dd3a35900c051f356dbc2824c03baa9b6e9fe34b3247e9c35fe0ba6db663
3
+ size 4336
assets/2025-10-18-17_49_5592584.gif ADDED

Git LFS Details

  • SHA256: c91b69ca6c0442eb7e3e3c65026424ac425e002317b6b23cec5c73ac34e6f38a
  • Pointer size: 131 Bytes
  • Size of remote file: 875 kB
assets/2025-10-18-17_49_5592584.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:05c3632c3c24ce10216fe6863493aa7ef5680a6fb4507b5bd16c48e6d67b700e
3
+ size 154914
assets/2025-10-18-17_49_5592584.npy ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:fe9a2f83e048fc5e612033b88ccd0798fb61d6abd38c5b3dd7b6539dacab091e
3
+ size 206320
assets/2025-10-18-17_50_1042399.gif ADDED

Git LFS Details

  • SHA256: d59a04be6117e7d4cffd203f37e3bb93ed03570f6b6ac58f5e63ae0581b20b0f
  • Pointer size: 130 Bytes
  • Size of remote file: 13.9 kB
assets/2025-10-18-17_50_1042399.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:703847d1a26f58e89b5e08a1793674400cb1fad7e2e8ff6164cb76a0549a6fbd
3
+ size 4848
assets/2025-10-18-17_50_1042399.npy ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:fb54dd3a35900c051f356dbc2824c03baa9b6e9fe34b3247e9c35fe0ba6db663
3
+ size 4336
assets/2025-10-18-17_50_4765985.gif ADDED

Git LFS Details

  • SHA256: c91b69ca6c0442eb7e3e3c65026424ac425e002317b6b23cec5c73ac34e6f38a
  • Pointer size: 131 Bytes
  • Size of remote file: 875 kB
assets/2025-10-18-17_50_4765985.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:05c3632c3c24ce10216fe6863493aa7ef5680a6fb4507b5bd16c48e6d67b700e
3
+ size 154914
assets/2025-10-18-17_50_4765985.npy ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:fe9a2f83e048fc5e612033b88ccd0798fb61d6abd38c5b3dd7b6539dacab091e
3
+ size 206320