Csaba Bolyos commited on
Commit
8bb0340
·
1 Parent(s): 1c9f2ea

front end overhaul

Browse files
Files changed (6) hide show
  1. README.md +3 -6
  2. app.py +6 -6
  3. backend/mcp_server.py +0 -413
  4. demo/app.py +5 -4
  5. demo/space.py +2 -4
  6. requirements.txt +0 -12
README.md CHANGED
@@ -1,13 +1,10 @@
1
  ---
2
- title: Laban Movement Analysis - Complete Suite
3
  emoji: 🎭
4
- colorFrom: blue
5
- colorTo: green
6
- sdk: gradio
7
- sdk_version: 5.0.0
8
  app_file: app.py
9
  pinned: false
10
- license: mit
11
  tags:
12
  - laban-movement-analysis
13
  - pose-estimation
 
1
  ---
2
+ title: Laban Movement Analysis
3
  emoji: 🎭
4
+ colorFrom: purple
5
+ colorTo: emerald
 
 
6
  app_file: app.py
7
  pinned: false
 
8
  tags:
9
  - laban-movement-analysis
10
  - pose-estimation
app.py CHANGED
@@ -13,12 +13,12 @@ Heavy Beta Version - Under Active Development
13
  """
14
 
15
  import sys
16
- import os
17
  from pathlib import Path
 
18
 
19
  # Import version info
20
  try:
21
- from version import __version__, __author__, get_version_info
22
  print(f"🎭 Laban Movement Analysis v{__version__} by {__author__}")
23
  except ImportError:
24
  __version__ = "not-found"
@@ -36,12 +36,12 @@ try:
36
  demo = create_demo()
37
 
38
  # Configure for Hugging Face Spaces
39
- demo.launch(
40
- show_api=True,
41
- mcp_server=True
42
- )
43
 
44
 
45
  except Exception as e:
46
  print(f"❌ Error launching demo: {e}")
 
 
47
  print("Check the logs above for more details.")
 
13
  """
14
 
15
  import sys
 
16
  from pathlib import Path
17
+ import traceback
18
 
19
  # Import version info
20
  try:
21
+ from version import __version__, __author__
22
  print(f"🎭 Laban Movement Analysis v{__version__} by {__author__}")
23
  except ImportError:
24
  __version__ = "not-found"
 
36
  demo = create_demo()
37
 
38
  # Configure for Hugging Face Spaces
39
+ # Try a simpler launch first for debugging
40
+ demo.launch(server_name='0.0.0.0', server_port=7860, mcp_server=True)
 
 
41
 
42
 
43
  except Exception as e:
44
  print(f"❌ Error launching demo: {e}")
45
+ print("Full traceback below:")
46
+ print(traceback.format_exc())
47
  print("Check the logs above for more details.")
backend/mcp_server.py DELETED
@@ -1,413 +0,0 @@
1
- """
2
- MCP (Model Context Protocol) Server for Laban Movement Analysis
3
- Provides tools for video movement analysis accessible to AI agents
4
- """
5
-
6
- import asyncio
7
- import json
8
- import os
9
- import tempfile
10
- from datetime import datetime
11
- from pathlib import Path
12
- from typing import Any, Dict, List, Optional, Tuple
13
- from urllib.parse import urlparse
14
- import aiofiles
15
- import httpx
16
-
17
- from mcp.server import Server
18
- from mcp.server.stdio import stdio_server
19
- from mcp.types import (
20
- Tool,
21
- TextContent,
22
- ImageContent,
23
- EmbeddedResource,
24
- ToolParameterType,
25
- ToolResponse,
26
- ToolResult,
27
- ToolError
28
- )
29
-
30
- # Add parent directory to path for imports
31
- import sys
32
- sys.path.insert(0, str(Path(__file__).parent))
33
-
34
- from gradio_labanmovementanalysis import LabanMovementAnalysis
35
-
36
-
37
- class LabanMCPServer:
38
- """MCP Server for Laban Movement Analysis"""
39
-
40
- def __init__(self):
41
- self.server = Server("laban-movement-analysis")
42
- self.analyzer = LabanMovementAnalysis()
43
- self.analysis_cache = {}
44
- self.temp_dir = tempfile.mkdtemp(prefix="laban_mcp_")
45
-
46
- # Register tools
47
- self._register_tools()
48
-
49
- def _register_tools(self):
50
- """Register all available tools"""
51
-
52
- @self.server.tool()
53
- async def analyze_video(
54
- video_path: str,
55
- model: str = "mediapipe",
56
- enable_visualization: bool = False,
57
- include_keypoints: bool = False
58
- ) -> ToolResult:
59
- """
60
- Analyze movement in a video file using Laban Movement Analysis.
61
-
62
- Args:
63
- video_path: Path or URL to video file
64
- model: Pose estimation model ('mediapipe', 'movenet', 'yolo')
65
- enable_visualization: Generate annotated video output
66
- include_keypoints: Include raw keypoint data in JSON
67
-
68
- Returns:
69
- Movement analysis results and optional visualization
70
- """
71
- try:
72
- # Handle URL vs local path
73
- if video_path.startswith(('http://', 'https://')):
74
- video_path = await self._download_video(video_path)
75
-
76
- # Process video
77
- json_output, viz_video = await asyncio.to_thread(
78
- self.analyzer.process_video,
79
- video_path,
80
- model=model,
81
- enable_visualization=enable_visualization,
82
- include_keypoints=include_keypoints
83
- )
84
-
85
- # Store in cache
86
- analysis_id = f"{Path(video_path).stem}_{datetime.now().isoformat()}"
87
- self.analysis_cache[analysis_id] = {
88
- "json_output": json_output,
89
- "viz_video": viz_video,
90
- "timestamp": datetime.now().isoformat()
91
- }
92
-
93
- # Format response
94
- response_data = {
95
- "analysis_id": analysis_id,
96
- "analysis": json_output,
97
- "visualization_path": viz_video if viz_video else None
98
- }
99
-
100
- return ToolResult(
101
- success=True,
102
- content=[TextContent(text=json.dumps(response_data, indent=2))]
103
- )
104
-
105
- except Exception as e:
106
- return ToolResult(
107
- success=False,
108
- error=ToolError(message=f"Analysis failed: {str(e)}")
109
- )
110
-
111
- @self.server.tool()
112
- async def get_analysis_summary(
113
- analysis_id: str
114
- ) -> ToolResult:
115
- """
116
- Get a human-readable summary of a previous analysis.
117
-
118
- Args:
119
- analysis_id: ID of the analysis to summarize
120
-
121
- Returns:
122
- Summary of movement analysis
123
- """
124
- try:
125
- if analysis_id not in self.analysis_cache:
126
- return ToolResult(
127
- success=False,
128
- error=ToolError(message=f"Analysis ID '{analysis_id}' not found")
129
- )
130
-
131
- analysis_data = self.analysis_cache[analysis_id]["json_output"]
132
-
133
- # Extract key information
134
- summary = self._generate_summary(analysis_data)
135
-
136
- return ToolResult(
137
- success=True,
138
- content=[TextContent(text=summary)]
139
- )
140
-
141
- except Exception as e:
142
- return ToolResult(
143
- success=False,
144
- error=ToolError(message=f"Summary generation failed: {str(e)}")
145
- )
146
-
147
- @self.server.tool()
148
- async def list_available_models() -> ToolResult:
149
- """
150
- List available pose estimation models with their characteristics.
151
-
152
- Returns:
153
- Information about available models
154
- """
155
- models_info = {
156
- "mediapipe": {
157
- "name": "MediaPipe Pose",
158
- "keypoints": 33,
159
- "dimensions": "3D",
160
- "optimization": "CPU",
161
- "best_for": "Single person, detailed analysis",
162
- "speed": "Fast"
163
- },
164
- "movenet": {
165
- "name": "MoveNet",
166
- "keypoints": 17,
167
- "dimensions": "2D",
168
- "optimization": "Mobile/Edge",
169
- "best_for": "Real-time applications, mobile devices",
170
- "speed": "Very Fast"
171
- },
172
- "yolo": {
173
- "name": "YOLO Pose",
174
- "keypoints": 17,
175
- "dimensions": "2D",
176
- "optimization": "GPU",
177
- "best_for": "Multi-person detection",
178
- "speed": "Fast (with GPU)"
179
- }
180
- }
181
-
182
- return ToolResult(
183
- success=True,
184
- content=[TextContent(text=json.dumps(models_info, indent=2))]
185
- )
186
-
187
- @self.server.tool()
188
- async def batch_analyze(
189
- video_paths: List[str],
190
- model: str = "mediapipe",
191
- parallel: bool = True
192
- ) -> ToolResult:
193
- """
194
- Analyze multiple videos in batch.
195
-
196
- Args:
197
- video_paths: List of video paths or URLs
198
- model: Pose estimation model to use
199
- parallel: Process videos in parallel
200
-
201
- Returns:
202
- Batch analysis results
203
- """
204
- try:
205
- results = {}
206
-
207
- if parallel:
208
- # Process in parallel
209
- tasks = []
210
- for path in video_paths:
211
- task = self._analyze_single_video(path, model)
212
- tasks.append(task)
213
-
214
- analyses = await asyncio.gather(*tasks)
215
-
216
- for path, analysis in zip(video_paths, analyses):
217
- results[path] = analysis
218
- else:
219
- # Process sequentially
220
- for path in video_paths:
221
- results[path] = await self._analyze_single_video(path, model)
222
-
223
- return ToolResult(
224
- success=True,
225
- content=[TextContent(text=json.dumps(results, indent=2))]
226
- )
227
-
228
- except Exception as e:
229
- return ToolResult(
230
- success=False,
231
- error=ToolError(message=f"Batch analysis failed: {str(e)}")
232
- )
233
-
234
- @self.server.tool()
235
- async def compare_movements(
236
- analysis_id1: str,
237
- analysis_id2: str
238
- ) -> ToolResult:
239
- """
240
- Compare movement patterns between two analyzed videos.
241
-
242
- Args:
243
- analysis_id1: First analysis ID
244
- analysis_id2: Second analysis ID
245
-
246
- Returns:
247
- Comparison of movement metrics
248
- """
249
- try:
250
- if analysis_id1 not in self.analysis_cache:
251
- return ToolResult(
252
- success=False,
253
- error=ToolError(message=f"Analysis ID '{analysis_id1}' not found")
254
- )
255
-
256
- if analysis_id2 not in self.analysis_cache:
257
- return ToolResult(
258
- success=False,
259
- error=ToolError(message=f"Analysis ID '{analysis_id2}' not found")
260
- )
261
-
262
- # Get analyses
263
- analysis1 = self.analysis_cache[analysis_id1]["json_output"]
264
- analysis2 = self.analysis_cache[analysis_id2]["json_output"]
265
-
266
- # Compare metrics
267
- comparison = self._compare_analyses(analysis1, analysis2)
268
-
269
- return ToolResult(
270
- success=True,
271
- content=[TextContent(text=json.dumps(comparison, indent=2))]
272
- )
273
-
274
- except Exception as e:
275
- return ToolResult(
276
- success=False,
277
- error=ToolError(message=f"Comparison failed: {str(e)}")
278
- )
279
-
280
- async def _download_video(self, url: str) -> str:
281
- """Download video from URL to temporary file"""
282
- async with httpx.AsyncClient() as client:
283
- response = await client.get(url)
284
- response.raise_for_status()
285
-
286
- # Save to temp file
287
- filename = Path(urlparse(url).path).name or "video.mp4"
288
- temp_path = os.path.join(self.temp_dir, filename)
289
-
290
- async with aiofiles.open(temp_path, 'wb') as f:
291
- await f.write(response.content)
292
-
293
- return temp_path
294
-
295
- async def _analyze_single_video(self, path: str, model: str) -> Dict[str, Any]:
296
- """Analyze a single video"""
297
- try:
298
- if path.startswith(('http://', 'https://')):
299
- path = await self._download_video(path)
300
-
301
- json_output, _ = await asyncio.to_thread(
302
- self.analyzer.process_video,
303
- path,
304
- model=model,
305
- enable_visualization=False
306
- )
307
-
308
- return {
309
- "status": "success",
310
- "analysis": json_output
311
- }
312
- except Exception as e:
313
- return {
314
- "status": "error",
315
- "error": str(e)
316
- }
317
-
318
- def _generate_summary(self, analysis_data: Dict[str, Any]) -> str:
319
- """Generate human-readable summary from analysis data"""
320
- summary_parts = []
321
-
322
- # Video info
323
- video_info = analysis_data.get("video_info", {})
324
- summary_parts.append(f"Video Analysis Summary")
325
- summary_parts.append(f"Duration: {video_info.get('duration_seconds', 0):.1f} seconds")
326
- summary_parts.append(f"Resolution: {video_info.get('width', 0)}x{video_info.get('height', 0)}")
327
- summary_parts.append("")
328
-
329
- # Movement summary
330
- movement_summary = analysis_data.get("movement_analysis", {}).get("summary", {})
331
-
332
- # Direction analysis
333
- direction_data = movement_summary.get("direction", {})
334
- dominant_direction = direction_data.get("dominant", "unknown")
335
- summary_parts.append(f"Dominant Movement Direction: {dominant_direction}")
336
-
337
- # Intensity analysis
338
- intensity_data = movement_summary.get("intensity", {})
339
- dominant_intensity = intensity_data.get("dominant", "unknown")
340
- summary_parts.append(f"Movement Intensity: {dominant_intensity}")
341
-
342
- # Speed analysis
343
- speed_data = movement_summary.get("speed", {})
344
- dominant_speed = speed_data.get("dominant", "unknown")
345
- summary_parts.append(f"Movement Speed: {dominant_speed}")
346
-
347
- # Segments
348
- segments = movement_summary.get("movement_segments", [])
349
- if segments:
350
- summary_parts.append(f"\nMovement Segments: {len(segments)}")
351
- for i, segment in enumerate(segments[:3]): # Show first 3
352
- start_time = segment.get("start_time", 0)
353
- end_time = segment.get("end_time", 0)
354
- movement_type = segment.get("movement_type", "unknown")
355
- summary_parts.append(f" Segment {i+1}: {movement_type} ({start_time:.1f}s - {end_time:.1f}s)")
356
-
357
- return "\n".join(summary_parts)
358
-
359
- def _compare_analyses(self, analysis1: Dict, analysis2: Dict) -> Dict[str, Any]:
360
- """Compare two movement analyses"""
361
- comparison = {
362
- "video1_info": analysis1.get("video_info", {}),
363
- "video2_info": analysis2.get("video_info", {}),
364
- "metric_comparison": {}
365
- }
366
-
367
- # Compare summaries
368
- summary1 = analysis1.get("movement_analysis", {}).get("summary", {})
369
- summary2 = analysis2.get("movement_analysis", {}).get("summary", {})
370
-
371
- # Compare directions
372
- dir1 = summary1.get("direction", {})
373
- dir2 = summary2.get("direction", {})
374
- comparison["metric_comparison"]["direction"] = {
375
- "video1_dominant": dir1.get("dominant", "unknown"),
376
- "video2_dominant": dir2.get("dominant", "unknown"),
377
- "match": dir1.get("dominant") == dir2.get("dominant")
378
- }
379
-
380
- # Compare intensity
381
- int1 = summary1.get("intensity", {})
382
- int2 = summary2.get("intensity", {})
383
- comparison["metric_comparison"]["intensity"] = {
384
- "video1_dominant": int1.get("dominant", "unknown"),
385
- "video2_dominant": int2.get("dominant", "unknown"),
386
- "match": int1.get("dominant") == int2.get("dominant")
387
- }
388
-
389
- # Compare speed
390
- speed1 = summary1.get("speed", {})
391
- speed2 = summary2.get("speed", {})
392
- comparison["metric_comparison"]["speed"] = {
393
- "video1_dominant": speed1.get("dominant", "unknown"),
394
- "video2_dominant": speed2.get("dominant", "unknown"),
395
- "match": speed1.get("dominant") == speed2.get("dominant")
396
- }
397
-
398
- return comparison
399
-
400
- async def run(self):
401
- """Run the MCP server"""
402
- async with stdio_server() as (read_stream, write_stream):
403
- await self.server.run(read_stream, write_stream)
404
-
405
-
406
- async def main():
407
- """Main entry point"""
408
- server = LabanMCPServer()
409
- await server.run()
410
-
411
-
412
- if __name__ == "__main__":
413
- asyncio.run(main())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
demo/app.py CHANGED
@@ -65,8 +65,9 @@ def create_demo() -> gr.Blocks:
65
  """
66
  )
67
  return demo
68
-
69
  if __name__ == "__main__":
70
- print("🚀 Starting Laban Movement Analysis...")
71
- demo = create_demo()
72
-
 
 
65
  """
66
  )
67
  return demo
68
+
69
  if __name__ == "__main__":
70
+ demo = create_demo()
71
+ demo.launch(server_name="0.0.0.0",
72
+ server_port=int(os.getenv("PORT", 7860)),
73
+ mcp_server=True)
demo/space.py CHANGED
@@ -1,5 +1,4 @@
1
  import gradio as gr
2
- from app import demo as app
3
  import os
4
 
5
  _docs = {'LabanMovementAnalysis': {'description': 'Gradio component for video-based pose analysis with Laban Movement Analysis metrics.', 'members': {'__init__': {'default_model': {'type': 'str', 'default': '"mediapipe"', 'description': 'Default pose estimation model ("mediapipe", "movenet", "yolo")'}, 'enable_visualization': {'type': 'bool', 'default': 'True', 'description': 'Whether to generate visualization video by default'}, 'include_keypoints': {'type': 'bool', 'default': 'False', 'description': 'Whether to include raw keypoints in JSON output'}, 'enable_webrtc': {'type': 'bool', 'default': 'False', 'description': 'Whether to enable WebRTC real-time analysis'}, 'label': {'type': 'typing.Optional[str][str, None]', 'default': 'None', 'description': 'Component label'}, 'every': {'type': 'typing.Optional[float][float, None]', 'default': 'None', 'description': None}, 'show_label': {'type': 'typing.Optional[bool][bool, None]', 'default': 'None', 'description': None}, 'container': {'type': 'bool', 'default': 'True', 'description': None}, 'scale': {'type': 'typing.Optional[int][int, None]', 'default': 'None', 'description': None}, 'min_width': {'type': 'int', 'default': '160', 'description': None}, 'interactive': {'type': 'typing.Optional[bool][bool, None]', 'default': 'None', 'description': None}, 'visible': {'type': 'bool', 'default': 'True', 'description': None}, 'elem_id': {'type': 'typing.Optional[str][str, None]', 'default': 'None', 'description': None}, 'elem_classes': {'type': 'typing.Optional[typing.List[str]][\n typing.List[str][str], None\n]', 'default': 'None', 'description': None}, 'render': {'type': 'bool', 'default': 'True', 'description': None}}, 'postprocess': {'value': {'type': 'typing.Any', 'description': 'Analysis results'}}, 'preprocess': {'return': {'type': 'typing.Dict[str, typing.Any][str, typing.Any]', 'description': 'Processed data for analysis'}, 'value': None}}, 'events': {}}, '__meta__': {'additional_interfaces': {}, 'user_fn_refs': {'LabanMovementAnalysis': []}}}
@@ -19,9 +18,8 @@ with gr.Blocks(
19
 
20
  A Gradio 5 component for video movement analysis using Laban Movement Analysis (LMA) with MCP support for AI agents
21
  """, elem_classes=["md-custom"], header_links=True)
22
- app.render()
23
  gr.Markdown(
24
- """
25
  ## Installation
26
 
27
  ```bash
@@ -105,7 +103,7 @@ if __name__ == "__main__":
105
 
106
 
107
  ```
108
- """, elem_classes=["md-custom"], header_links=True)
109
 
110
 
111
  gr.Markdown("""
 
1
  import gradio as gr
 
2
  import os
3
 
4
  _docs = {'LabanMovementAnalysis': {'description': 'Gradio component for video-based pose analysis with Laban Movement Analysis metrics.', 'members': {'__init__': {'default_model': {'type': 'str', 'default': '"mediapipe"', 'description': 'Default pose estimation model ("mediapipe", "movenet", "yolo")'}, 'enable_visualization': {'type': 'bool', 'default': 'True', 'description': 'Whether to generate visualization video by default'}, 'include_keypoints': {'type': 'bool', 'default': 'False', 'description': 'Whether to include raw keypoints in JSON output'}, 'enable_webrtc': {'type': 'bool', 'default': 'False', 'description': 'Whether to enable WebRTC real-time analysis'}, 'label': {'type': 'typing.Optional[str][str, None]', 'default': 'None', 'description': 'Component label'}, 'every': {'type': 'typing.Optional[float][float, None]', 'default': 'None', 'description': None}, 'show_label': {'type': 'typing.Optional[bool][bool, None]', 'default': 'None', 'description': None}, 'container': {'type': 'bool', 'default': 'True', 'description': None}, 'scale': {'type': 'typing.Optional[int][int, None]', 'default': 'None', 'description': None}, 'min_width': {'type': 'int', 'default': '160', 'description': None}, 'interactive': {'type': 'typing.Optional[bool][bool, None]', 'default': 'None', 'description': None}, 'visible': {'type': 'bool', 'default': 'True', 'description': None}, 'elem_id': {'type': 'typing.Optional[str][str, None]', 'default': 'None', 'description': None}, 'elem_classes': {'type': 'typing.Optional[typing.List[str]][\n typing.List[str][str], None\n]', 'default': 'None', 'description': None}, 'render': {'type': 'bool', 'default': 'True', 'description': None}}, 'postprocess': {'value': {'type': 'typing.Any', 'description': 'Analysis results'}}, 'preprocess': {'return': {'type': 'typing.Dict[str, typing.Any][str, typing.Any]', 'description': 'Processed data for analysis'}, 'value': None}}, 'events': {}}, '__meta__': {'additional_interfaces': {}, 'user_fn_refs': {'LabanMovementAnalysis': []}}}
 
18
 
19
  A Gradio 5 component for video movement analysis using Laban Movement Analysis (LMA) with MCP support for AI agents
20
  """, elem_classes=["md-custom"], header_links=True)
 
21
  gr.Markdown(
22
+ '''
23
  ## Installation
24
 
25
  ```bash
 
103
 
104
 
105
  ```
106
+ ''', elem_classes=["md-custom"], header_links=True)
107
 
108
 
109
  gr.Markdown("""
requirements.txt DELETED
@@ -1,12 +0,0 @@
1
- # Laban Movement Analysis - Complete Suite
2
- # Created by: Csaba Bolyós (BladeSzaSza)
3
- # Heavy Beta Version
4
-
5
- # Core Gradio and UI (Updated to latest stable version)
6
- gradio[mcp]>=5.23.2
7
- mcp>=1.9.0
8
-
9
- # Computer Vision and Pose Estimation
10
- opencv-python>=4.8.0
11
- mediapipe>=0.10.21
12
- ultralytics>=8.0.0