Prathamesh Sarjerao Vaidya commited on
Commit
922e083
·
1 Parent(s): 6a40a61

made changes to fasten hf spaces start

Browse files
.gitattributes CHANGED
@@ -1,9 +1,10 @@
1
- \# filter=lfs diff=lfs merge=lfs -text
2
  *.mp3 filter=lfs diff=lfs merge=lfs -text
3
  *.ogg filter=lfs diff=lfs merge=lfs -text
4
  *.wav filter=lfs diff=lfs merge=lfs -text
5
  *.flac filter=lfs diff=lfs merge=lfs -text
6
  *.m4a filter=lfs diff=lfs merge=lfs -text
 
7
  # Image files
8
  *.png filter=lfs diff=lfs merge=lfs -text
9
  *.jpg filter=lfs diff=lfs merge=lfs -text
@@ -12,14 +13,3 @@
12
  *.bmp filter=lfs diff=lfs merge=lfs -text
13
  *.webp filter=lfs diff=lfs merge=lfs -text
14
  *.tiff filter=lfs diff=lfs merge=lfs -text
15
- file filter=lfs diff=lfs merge=lfs -text
16
- types filter=lfs diff=lfs merge=lfs -text
17
- needed) filter=lfs diff=lfs merge=lfs -text
18
- Track filter=lfs diff=lfs merge=lfs -text
19
- PNG filter=lfs diff=lfs merge=lfs -text
20
- files filter=lfs diff=lfs merge=lfs -text
21
- can filter=lfs diff=lfs merge=lfs -text
22
- more filter=lfs diff=lfs merge=lfs -text
23
- as filter=lfs diff=lfs merge=lfs -text
24
- (you filter=lfs diff=lfs merge=lfs -text
25
- add filter=lfs diff=lfs merge=lfs -text
 
1
+ # Audio files
2
  *.mp3 filter=lfs diff=lfs merge=lfs -text
3
  *.ogg filter=lfs diff=lfs merge=lfs -text
4
  *.wav filter=lfs diff=lfs merge=lfs -text
5
  *.flac filter=lfs diff=lfs merge=lfs -text
6
  *.m4a filter=lfs diff=lfs merge=lfs -text
7
+
8
  # Image files
9
  *.png filter=lfs diff=lfs merge=lfs -text
10
  *.jpg filter=lfs diff=lfs merge=lfs -text
 
13
  *.bmp filter=lfs diff=lfs merge=lfs -text
14
  *.webp filter=lfs diff=lfs merge=lfs -text
15
  *.tiff filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
Dockerfile CHANGED
@@ -1,5 +1,5 @@
1
  # Multi-stage Dockerfile for Dance Movement Analyzer
2
- # Optimized for production deployment
3
 
4
  # Stage 1: Base image with dependencies
5
  FROM python:3.10-slim as base
@@ -8,10 +8,11 @@ FROM python:3.10-slim as base
8
  ENV PYTHONUNBUFFERED=1 \
9
  PYTHONDONTWRITEBYTECODE=1 \
10
  PIP_NO_CACHE_DIR=1 \
11
- PIP_DISABLE_PIP_VERSION_CHECK=1
 
12
 
13
  # Install system dependencies
14
- RUN apt-get update && apt-get install -y \
15
  libgl1 \
16
  libglib2.0-0 \
17
  libsm6 \
@@ -19,6 +20,7 @@ RUN apt-get update && apt-get install -y \
19
  libxrender-dev \
20
  libgomp1 \
21
  ffmpeg \
 
22
  && rm -rf /var/lib/apt/lists/*
23
 
24
  # Create app directory
@@ -28,11 +30,14 @@ WORKDIR /app
28
  FROM base as dependencies
29
 
30
  # Copy requirements first for better caching
31
- COPY requirements.txt .
32
 
33
  # Install Python dependencies
34
  RUN pip install --no-cache-dir -r requirements.txt
35
 
 
 
 
36
  # Stage 3: Production image
37
  FROM base as production
38
 
@@ -49,6 +54,12 @@ COPY backend/app /app/app
49
  # Copy frontend files
50
  COPY frontend /app/frontend
51
 
 
 
 
 
 
 
52
  # Set permissions
53
  RUN chmod -R 755 /app
54
 
@@ -58,12 +69,16 @@ RUN useradd -m -u 1000 appuser && \
58
 
59
  USER appuser
60
 
61
- # Expose port
62
  EXPOSE 8000
 
63
 
64
- # Health check
65
- HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
 
 
66
  CMD python -c "import requests; requests.get('http://localhost:8000/health')" || exit 1
 
67
 
68
- # Run the application
69
- CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
 
1
  # Multi-stage Dockerfile for Dance Movement Analyzer
2
+ # Optimized for Hugging Face Spaces
3
 
4
  # Stage 1: Base image with dependencies
5
  FROM python:3.10-slim as base
 
8
  ENV PYTHONUNBUFFERED=1 \
9
  PYTHONDONTWRITEBYTECODE=1 \
10
  PIP_NO_CACHE_DIR=1 \
11
+ PIP_DISABLE_PIP_VERSION_CHECK=1 \
12
+ DEBIAN_FRONTEND=noninteractive
13
 
14
  # Install system dependencies
15
+ RUN apt-get update && apt-get install -y --no-install-recommends \
16
  libgl1 \
17
  libglib2.0-0 \
18
  libsm6 \
 
20
  libxrender-dev \
21
  libgomp1 \
22
  ffmpeg \
23
+ curl \
24
  && rm -rf /var/lib/apt/lists/*
25
 
26
  # Create app directory
 
30
  FROM base as dependencies
31
 
32
  # Copy requirements first for better caching
33
+ COPY backend/requirements.txt .
34
 
35
  # Install Python dependencies
36
  RUN pip install --no-cache-dir -r requirements.txt
37
 
38
+ # Pre-download MediaPipe models to speed up first run
39
+ RUN python -c "import mediapipe as mp; pose = mp.solutions.pose.Pose(); pose.close()" || true
40
+
41
  # Stage 3: Production image
42
  FROM base as production
43
 
 
54
  # Copy frontend files
55
  COPY frontend /app/frontend
56
 
57
+ # Copy startup script
58
+ COPY startup.sh /app/startup.sh
59
+
60
+ # Make startup script executable
61
+ RUN chmod +x /app/startup.sh
62
+
63
  # Set permissions
64
  RUN chmod -R 755 /app
65
 
 
69
 
70
  USER appuser
71
 
72
+ # Expose port (7860 for Hugging Face, 8000 for local)
73
  EXPOSE 8000
74
+ EXPOSE 7860
75
 
76
+ # ⬇️ OPTION B: HEALTHCHECK GOES HERE ⬇️
77
+ # Health check with 5 minute startup period (for model loading)
78
+ # This allows the container up to 5 minutes to start before health checks begin
79
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=5m --retries=3 \
80
  CMD python -c "import requests; requests.get('http://localhost:8000/health')" || exit 1
81
+ # ⬆️ HEALTHCHECK ENDS HERE ⬆️
82
 
83
+ # Run the application using startup script
84
+ CMD ["/app/startup.sh"]
backend/app/__init__.py CHANGED
@@ -4,7 +4,7 @@ Provides pose detection, movement classification, and rhythm analysis
4
  """
5
 
6
  __version__ = "1.0.0"
7
- __author__ = "Your Name"
8
 
9
  from .config import Config
10
  from .pose_analyzer import PoseAnalyzer, PoseKeypoints
 
4
  """
5
 
6
  __version__ = "1.0.0"
7
+ __author__ = "Prathamesh Vaidya"
8
 
9
  from .config import Config
10
  from .pose_analyzer import PoseAnalyzer, PoseKeypoints
backend/app/config.py CHANGED
@@ -54,7 +54,8 @@ class Config:
54
  # ==================== MediaPipe Configuration ====================
55
  # Model complexity: 0 (Lite), 1 (Full), 2 (Heavy)
56
  MEDIAPIPE_MODEL_COMPLEXITY: int = int(
57
- os.getenv("MEDIAPIPE_MODEL_COMPLEXITY", 1)
 
58
  )
59
 
60
  # Confidence thresholds
 
54
  # ==================== MediaPipe Configuration ====================
55
  # Model complexity: 0 (Lite), 1 (Full), 2 (Heavy)
56
  MEDIAPIPE_MODEL_COMPLEXITY: int = int(
57
+ # os.getenv("MEDIAPIPE_MODEL_COMPLEXITY", 1)
58
+ os.getenv("MEDIAPIPE_MODEL_COMPLEXITY", 0)
59
  )
60
 
61
  # Confidence thresholds
backend/app/main.py CHANGED
@@ -1,12 +1,12 @@
1
  """
2
- FastAPI Application - Phase 3
3
- REST API and WebSocket endpoints for Dance Movement Analysis
4
  """
5
 
6
  from fastapi import FastAPI, File, UploadFile, WebSocket, WebSocketDisconnect, HTTPException, Request
7
  from fastapi.responses import FileResponse, JSONResponse, HTMLResponse, StreamingResponse
8
  from fastapi.middleware.cors import CORSMiddleware
9
  from fastapi.staticfiles import StaticFiles
 
10
  from pathlib import Path
11
  import asyncio
12
  import json
@@ -29,33 +29,57 @@ logging.basicConfig(
29
  )
30
  logger = logging.getLogger(__name__)
31
 
32
- # Initialize FastAPI app
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  app = FastAPI(
34
  title="Dance Movement Analysis API",
35
  description="AI-powered dance movement analysis with pose detection and classification",
36
  version="1.0.0",
37
  docs_url="/api/docs",
38
- redoc_url="/api/redoc"
 
39
  )
40
 
41
  # CORS Configuration
42
  app.add_middleware(
43
  CORSMiddleware,
44
- allow_origins=["*"], # In production, specify exact origins
45
  allow_credentials=True,
46
  allow_methods=["*"],
47
  allow_headers=["*"],
48
  )
49
 
50
- # Initialize folders
51
- Config.initialize_folders()
52
-
53
- # Mount static files for frontend
54
  static_path = Path(__file__).parent.parent.parent / "frontend"
55
  if static_path.exists():
56
  app.mount("/static", StaticFiles(directory=str(static_path)), name="static")
57
 
58
- # Setup templates and static files
59
  templates = Jinja2Templates(directory=static_path)
60
 
61
  # Active WebSocket connections
@@ -64,32 +88,19 @@ active_connections: Dict[str, WebSocket] = {}
64
  # Processing sessions
65
  processing_sessions: Dict[str, Dict[str, Any]] = {}
66
 
67
- '''
68
  def convert_to_native_bool(obj):
69
- if isinstance(obj, np.bool_): # Check for numpy boolean
70
- return bool(obj)
71
- elif isinstance(obj, dict): # If it's a dictionary, convert its values
72
- return {k: convert_to_native_bool(v) for k, v in obj.items()}
73
- elif isinstance(obj, list): # If it's a list, convert each item
74
- return [convert_to_native_bool(item) for item in obj]
75
- else:
76
- return obj
77
- '''
78
-
79
- def convert_to_native_bool(obj):
80
- """Recursively convert numpy.bool_ and nested structures to native Python types."""
81
  if isinstance(obj, np.bool_):
82
  return bool(obj)
83
  elif isinstance(obj, (np.integer, np.floating)):
84
- return obj.item() # Convert numpy numbers to Python int/float
85
  elif isinstance(obj, dict):
86
  return {k: convert_to_native_bool(v) for k, v in obj.items()}
87
  elif isinstance(obj, (list, tuple)):
88
  return [convert_to_native_bool(v) for v in obj]
89
  else:
90
  return obj
91
-
92
-
93
  class ConnectionManager:
94
  """Manages WebSocket connections for real-time updates"""
95
 
@@ -126,23 +137,8 @@ class ConnectionManager:
126
  for session_id in disconnected:
127
  self.disconnect(session_id)
128
 
129
-
130
  manager = ConnectionManager()
131
 
132
-
133
- def progress_callback_factory(session_id: str):
134
- """Create a progress callback function for a specific session"""
135
-
136
- async def callback(progress: float, message: str):
137
- await manager.send_message(session_id, {
138
- "type": "progress",
139
- "progress": progress,
140
- "message": message,
141
- "timestamp": datetime.now().isoformat()
142
- })
143
-
144
- return callback
145
-
146
  @app.get("/", response_class=HTMLResponse)
147
  async def home(request: Request):
148
  """Home page."""
@@ -150,11 +146,12 @@ async def home(request: Request):
150
 
151
  @app.get("/info")
152
  async def root():
153
- """Root endpoint - serves frontend or API info"""
154
  return {
155
  "name": "Dance Movement Analysis API",
156
  "version": "1.0.0",
157
  "status": "online",
 
158
  "endpoints": {
159
  "upload": "/api/upload",
160
  "analyze": "/api/analyze/{session_id}",
@@ -164,32 +161,24 @@ async def root():
164
  }
165
  }
166
 
167
-
168
  @app.get("/health")
169
  async def health_check():
170
  """Health check endpoint"""
171
  return {
172
  "status": "healthy",
 
173
  "timestamp": datetime.now().isoformat(),
174
  "active_sessions": len(processing_sessions),
175
- "active_connections": len(active_connections)
176
  }
177
 
178
-
179
  @app.post("/api/upload")
180
  async def upload_video(file: UploadFile = File(...)):
181
- """
182
- Upload a video file for processing
183
-
184
- Returns:
185
- JSON with session_id and file info
186
- """
187
-
188
  from typing import List
189
  allowed_extensions: List[str] = [".mp4", ".avi", ".mov", ".mkv", ".webm"]
190
 
191
  try:
192
- # Generate unique session ID
193
  session_id = str(uuid.uuid4())
194
 
195
  # Validate file
@@ -203,10 +192,9 @@ async def upload_video(file: UploadFile = File(...)):
203
  with open(upload_path, "wb") as buffer:
204
  shutil.copyfileobj(file.file, buffer)
205
 
206
- # Get video info
207
- processor = VideoProcessor()
208
  video_info = processor.load_video(upload_path)
209
- print("get video info")
210
 
211
  # Store session info
212
  processing_sessions[session_id] = {
 
1
  """
2
+ FastAPI Application with Optimized Startup for Hugging Face Spaces
 
3
  """
4
 
5
  from fastapi import FastAPI, File, UploadFile, WebSocket, WebSocketDisconnect, HTTPException, Request
6
  from fastapi.responses import FileResponse, JSONResponse, HTMLResponse, StreamingResponse
7
  from fastapi.middleware.cors import CORSMiddleware
8
  from fastapi.staticfiles import StaticFiles
9
+ from contextlib import asynccontextmanager
10
  from pathlib import Path
11
  import asyncio
12
  import json
 
29
  )
30
  logger = logging.getLogger(__name__)
31
 
32
+ # Global processor instance (initialized on startup)
33
+ global_processor: Optional[VideoProcessor] = None
34
+
35
+ @asynccontextmanager
36
+ async def lifespan(app: FastAPI):
37
+ """Lifespan event handler for startup and shutdown"""
38
+ global global_processor
39
+
40
+ # Startup
41
+ logger.info("🚀 Starting Dance Movement Analyzer...")
42
+
43
+ # Initialize folders
44
+ Config.initialize_folders()
45
+ logger.info("✅ Folders initialized")
46
+
47
+ # Pre-initialize VideoProcessor to load MediaPipe models
48
+ logger.info("📦 Loading MediaPipe models...")
49
+ global_processor = VideoProcessor()
50
+ logger.info("✅ MediaPipe models loaded successfully")
51
+
52
+ logger.info("🎉 Application startup complete!")
53
+
54
+ yield
55
+
56
+ # Shutdown (cleanup if needed)
57
+ logger.info("👋 Shutting down application...")
58
+
59
+ # Initialize FastAPI app with lifespan
60
  app = FastAPI(
61
  title="Dance Movement Analysis API",
62
  description="AI-powered dance movement analysis with pose detection and classification",
63
  version="1.0.0",
64
  docs_url="/api/docs",
65
+ redoc_url="/api/redoc",
66
+ lifespan=lifespan
67
  )
68
 
69
  # CORS Configuration
70
  app.add_middleware(
71
  CORSMiddleware,
72
+ allow_origins=["*"],
73
  allow_credentials=True,
74
  allow_methods=["*"],
75
  allow_headers=["*"],
76
  )
77
 
78
+ # Mount static files
 
 
 
79
  static_path = Path(__file__).parent.parent.parent / "frontend"
80
  if static_path.exists():
81
  app.mount("/static", StaticFiles(directory=str(static_path)), name="static")
82
 
 
83
  templates = Jinja2Templates(directory=static_path)
84
 
85
  # Active WebSocket connections
 
88
  # Processing sessions
89
  processing_sessions: Dict[str, Dict[str, Any]] = {}
90
 
 
91
  def convert_to_native_bool(obj):
92
+ """Recursively convert numpy types to native Python types."""
 
 
 
 
 
 
 
 
 
 
 
93
  if isinstance(obj, np.bool_):
94
  return bool(obj)
95
  elif isinstance(obj, (np.integer, np.floating)):
96
+ return obj.item()
97
  elif isinstance(obj, dict):
98
  return {k: convert_to_native_bool(v) for k, v in obj.items()}
99
  elif isinstance(obj, (list, tuple)):
100
  return [convert_to_native_bool(v) for v in obj]
101
  else:
102
  return obj
103
+
 
104
  class ConnectionManager:
105
  """Manages WebSocket connections for real-time updates"""
106
 
 
137
  for session_id in disconnected:
138
  self.disconnect(session_id)
139
 
 
140
  manager = ConnectionManager()
141
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
  @app.get("/", response_class=HTMLResponse)
143
  async def home(request: Request):
144
  """Home page."""
 
146
 
147
  @app.get("/info")
148
  async def root():
149
+ """Root endpoint - serves API info"""
150
  return {
151
  "name": "Dance Movement Analysis API",
152
  "version": "1.0.0",
153
  "status": "online",
154
+ "models_loaded": global_processor is not None,
155
  "endpoints": {
156
  "upload": "/api/upload",
157
  "analyze": "/api/analyze/{session_id}",
 
161
  }
162
  }
163
 
 
164
  @app.get("/health")
165
  async def health_check():
166
  """Health check endpoint"""
167
  return {
168
  "status": "healthy",
169
+ "models_loaded": global_processor is not None,
170
  "timestamp": datetime.now().isoformat(),
171
  "active_sessions": len(processing_sessions),
172
+ "active_connections": len(manager.active_connections)
173
  }
174
 
 
175
  @app.post("/api/upload")
176
  async def upload_video(file: UploadFile = File(...)):
177
+ """Upload a video file for processing"""
 
 
 
 
 
 
178
  from typing import List
179
  allowed_extensions: List[str] = [".mp4", ".avi", ".mov", ".mkv", ".webm"]
180
 
181
  try:
 
182
  session_id = str(uuid.uuid4())
183
 
184
  # Validate file
 
192
  with open(upload_path, "wb") as buffer:
193
  shutil.copyfileobj(file.file, buffer)
194
 
195
+ # Use pre-initialized processor
196
+ processor = global_processor or VideoProcessor()
197
  video_info = processor.load_video(upload_path)
 
198
 
199
  # Store session info
200
  processing_sessions[session_id] = {
spaces.yaml CHANGED
@@ -5,4 +5,6 @@ colorTo: indigo
5
  sdk: docker
6
  app_port: 8000
7
  pinned: false
8
- short_description: AI-powered tool for real-time dance movement analysis.
 
 
 
5
  sdk: docker
6
  app_port: 8000
7
  pinned: false
8
+ short_description: AI-powered tool for real-time dance movement analysis.
9
+ startup_duration_timeout: 10m
10
+ python_version: "3.10"
startup.sh ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ echo "🚀 Starting Dance Movement Analyzer..."
5
+
6
+ # Pre-warm MediaPipe models
7
+ echo "📦 Pre-loading MediaPipe models..."
8
+ python3 -c "
9
+ import mediapipe as mp
10
+ import logging
11
+ logging.basicConfig(level=logging.INFO)
12
+ logger = logging.getLogger(__name__)
13
+
14
+ try:
15
+ logger.info('Loading Pose model...')
16
+ pose = mp.solutions.pose.Pose(
17
+ static_image_mode=False,
18
+ model_complexity=1,
19
+ min_detection_confidence=0.5,
20
+ min_tracking_confidence=0.5
21
+ )
22
+ pose.close()
23
+ logger.info('✅ Models loaded successfully')
24
+ except Exception as e:
25
+ logger.error(f'❌ Error loading models: {e}')
26
+ # Continue anyway - models will load on first use
27
+ " || echo "⚠️ Model pre-loading failed, will load on first use"
28
+
29
+ # Detect port (Hugging Face uses 7860, local uses 8000)
30
+ PORT=${PORT:-8000}
31
+
32
+ # Start the application
33
+ echo "🎬 Starting Uvicorn server on port $PORT..."
34
+ exec uvicorn app.main:app --host 0.0.0.0 --port $PORT --workers 1