Shujah239 commited on
Commit
31c7755
·
verified ·
1 Parent(s): f5a7a9e

Upload 3 files

Browse files
Files changed (3) hide show
  1. README.md +62 -12
  2. app.py +279 -0
  3. requirements.txt +10 -0
README.md CHANGED
@@ -1,12 +1,62 @@
1
- ---
2
- title: Emotion Detection App Server
3
- emoji: 🦀
4
- colorFrom: gray
5
- colorTo: pink
6
- sdk: docker
7
- pinned: false
8
- license: mit
9
- short_description: an app server to analyze emotions in sound using .wav
10
- ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Audio Emotion Detection API
2
+
3
+ This application provides an API for detecting emotions in audio files using the wav2vec2 model fine-tuned for emotion recognition.
4
+
5
+ ## Features
6
+
7
+ - Upload audio files for emotion analysis
8
+ - List all uploaded recordings
9
+ - Download previously uploaded recordings
10
+ - Analyze existing recordings
11
+ - Delete recordings
12
+
13
+ ## API Endpoints
14
+
15
+ - `GET /health` - Health check endpoint
16
+ - `POST /upload` - Upload and analyze an audio file
17
+ - `GET /recordings` - List all uploaded recordings
18
+ - `GET /recordings/{filename}` - Download a specific recording
19
+ - `GET /analyze/{filename}` - Analyze an existing recording
20
+ - `DELETE /recordings/{filename}` - Delete a recording
21
+
22
+ ## Supported Audio Formats
23
+
24
+ - WAV
25
+ - MP3
26
+ - OGG
27
+ - FLAC
28
+
29
+ ## File Size Limits
30
+
31
+ Maximum file size: 10MB
32
+
33
+ ## Usage Example
34
+
35
+ ```python
36
+ import requests
37
+
38
+ # Upload and analyze an audio file
39
+ with open('your_audio.wav', 'rb') as f:
40
+ files = {'file': f}
41
+ response = requests.post('https://your-space-url.hf.space/upload', files=files)
42
+ print(response.json())
43
+ ```
44
+
45
+ ## Technical Details
46
+
47
+ - Based on FastAPI
48
+ - Uses Hugging Face's wav2vec2-base-superb-er model for emotion recognition
49
+ - Optimized for Hugging Face Spaces deployment
50
+ - Automatic file cleanup to manage storage limits
51
+
52
+ ## Storage Management
53
+
54
+ Files are automatically cleaned up after 24 hours to manage storage limits on Hugging Face Spaces.
55
+
56
+ ## Development
57
+
58
+ To run this API locally:
59
+
60
+ 1. Install dependencies: `pip install -r requirements.txt`
61
+ 2. Run the server: `python app.py`
62
+ 3. Access the Swagger documentation at `http://localhost:7860/docs`
app.py ADDED
@@ -0,0 +1,279 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import shutil
2
+ import logging
3
+ import time
4
+ from pathlib import Path
5
+ from typing import List, Dict, Any, Optional
6
+
7
+ from fastapi import FastAPI, HTTPException, UploadFile, File, BackgroundTasks, Request
8
+ from fastapi.responses import FileResponse
9
+ from fastapi.middleware.cors import CORSMiddleware
10
+ from fastapi.middleware.gzip import GZipMiddleware
11
+ from transformers import pipeline
12
+ import torch
13
+ import uvicorn
14
+
15
+ # Configure logging
16
+ logging.basicConfig(level=logging.INFO)
17
+ logger = logging.getLogger(__name__)
18
+
19
+ # Define uploads directory
20
+ UPLOAD_DIR = Path("uploads")
21
+ MAX_STORAGE_MB = 100 # Maximum storage in MB
22
+ MAX_FILE_AGE_DAYS = 1 # Maximum age of files in days
23
+
24
+ app = FastAPI(
25
+ title="Emotion Detection API",
26
+ description="Audio emotion detection using wav2vec2",
27
+ version="1.0.0",
28
+ )
29
+
30
+ # Add middleware
31
+ app.add_middleware(
32
+ CORSMiddleware,
33
+ allow_origins=["*"],
34
+ allow_credentials=True,
35
+ allow_methods=["*"],
36
+ allow_headers=["*"],
37
+ )
38
+ app.add_middleware(GZipMiddleware, minimum_size=1000)
39
+
40
+ # Preloaded classifier (global)
41
+ classifier = None
42
+
43
+ @app.on_event("startup")
44
+ async def load_model():
45
+ """
46
+ Load the pretrained Wav2Vec2 emotion recognition model at startup
47
+ and ensure the upload directory exists.
48
+ """
49
+ global classifier
50
+ try:
51
+ # Use GPU if available, else CPU
52
+ device = 0 if torch.cuda.is_available() else -1
53
+
54
+ # For Hugging Face Spaces with limited resources, use quantized model if on CPU
55
+ if device == -1:
56
+ logger.info("Loading quantized model for CPU usage")
57
+ classifier = pipeline(
58
+ "audio-classification",
59
+ model="superb/wav2vec2-base-superb-er",
60
+ device=device,
61
+ torch_dtype=torch.float16 # Use half precision
62
+ )
63
+ else:
64
+ classifier = pipeline(
65
+ "audio-classification",
66
+ model="superb/wav2vec2-base-superb-er",
67
+ device=device
68
+ )
69
+
70
+ logger.info("Loaded emotion recognition model (device=%s)",
71
+ "GPU" if device == 0 else "CPU")
72
+ except Exception as e:
73
+ logger.error("Failed to load model: %s", e)
74
+ raise
75
+
76
+ # Ensure the upload directory exists
77
+ try:
78
+ UPLOAD_DIR.mkdir(parents=True, exist_ok=True)
79
+ # Clean up old files at startup
80
+ await cleanup_old_files()
81
+ except Exception as e:
82
+ logger.error("Failed to create upload directory: %s", e)
83
+ raise
84
+
85
+ async def cleanup_old_files():
86
+ """Clean up old files to prevent storage issues on Hugging Face Spaces."""
87
+ try:
88
+ # Remove files older than MAX_FILE_AGE_DAYS
89
+ now = time.time()
90
+ deleted_count = 0
91
+ for file_path in UPLOAD_DIR.iterdir():
92
+ if file_path.is_file():
93
+ file_age_days = (now - file_path.stat().st_mtime) / (60 * 60 * 24)
94
+ if file_age_days > MAX_FILE_AGE_DAYS:
95
+ file_path.unlink()
96
+ deleted_count += 1
97
+
98
+ if deleted_count > 0:
99
+ logger.info(f"Cleaned up {deleted_count} old files")
100
+ except Exception as e:
101
+ logger.error(f"Error during file cleanup: {e}")
102
+
103
+ @app.middleware("http")
104
+ async def add_process_time_header(request: Request, call_next):
105
+ """Add X-Process-Time header to responses."""
106
+ start_time = time.time()
107
+ response = await call_next(request)
108
+ process_time = time.time() - start_time
109
+ response.headers["X-Process-Time"] = str(process_time)
110
+ return response
111
+
112
+ @app.get("/health")
113
+ async def health():
114
+ """Health check endpoint."""
115
+ return {"status": "ok", "model_loaded": classifier is not None}
116
+
117
+ @app.post("/upload")
118
+ async def upload_audio(
119
+ file: UploadFile = File(...),
120
+ background_tasks: BackgroundTasks = None
121
+ ):
122
+ """
123
+ Upload an audio file and analyze emotions.
124
+ Saves the file to the uploads directory and returns model predictions.
125
+ """
126
+ if not classifier:
127
+ raise HTTPException(status_code=503, detail="Model not yet loaded")
128
+
129
+ filename = Path(file.filename).name
130
+ if not filename:
131
+ raise HTTPException(status_code=400, detail="Invalid filename")
132
+
133
+ # Check file extension
134
+ valid_extensions = [".wav", ".mp3", ".ogg", ".flac"]
135
+ if not any(filename.lower().endswith(ext) for ext in valid_extensions):
136
+ raise HTTPException(
137
+ status_code=400,
138
+ detail=f"Invalid file type. Supported types: {', '.join(valid_extensions)}"
139
+ )
140
+
141
+ # Read file contents
142
+ try:
143
+ contents = await file.read()
144
+ except Exception as e:
145
+ logger.error("Error reading file %s: %s", filename, e)
146
+ raise HTTPException(status_code=500, detail=f"Failed to read file: {str(e)}")
147
+ finally:
148
+ await file.close()
149
+
150
+ # Check file size (limit to 10MB for Spaces)
151
+ if len(contents) > 10 * 1024 * 1024:
152
+ raise HTTPException(
153
+ status_code=413,
154
+ detail="File too large. Maximum size is 10MB"
155
+ )
156
+
157
+ # Check available disk space
158
+ try:
159
+ total, used, free = shutil.disk_usage(UPLOAD_DIR)
160
+ free_mb = free / (1024 * 1024)
161
+
162
+ if free_mb < 10: # Keep at least 10MB free
163
+ # Schedule cleanup in background
164
+ if background_tasks:
165
+ background_tasks.add_task(cleanup_old_files)
166
+
167
+ if len(contents) > free:
168
+ logger.error(
169
+ "Insufficient storage: needed %d bytes, free %d bytes",
170
+ len(contents), free
171
+ )
172
+ raise HTTPException(status_code=507, detail="Insufficient storage to save file")
173
+ except Exception as e:
174
+ logger.warning(f"Failed to check disk usage: {e}")
175
+
176
+ # Save file to uploads directory
177
+ file_path = UPLOAD_DIR / filename
178
+ try:
179
+ with open(file_path, "wb") as f:
180
+ f.write(contents)
181
+ logger.info("Saved uploaded file: %s", file_path)
182
+ except Exception as e:
183
+ logger.error("Failed to save file %s: %s", filename, e)
184
+ raise HTTPException(status_code=500, detail=f"Failed to save file: {str(e)}")
185
+
186
+ # Analyze the audio file using the pretrained model pipeline
187
+ try:
188
+ results = classifier(str(file_path))
189
+
190
+ # Schedule cleanup in background
191
+ if background_tasks:
192
+ background_tasks.add_task(cleanup_old_files)
193
+
194
+ return {"filename": filename, "predictions": results}
195
+ except Exception as e:
196
+ logger.error("Model inference failed for %s: %s", filename, e)
197
+ # Try to remove the file if inference fails
198
+ try:
199
+ file_path.unlink(missing_ok=True)
200
+ except Exception:
201
+ pass
202
+ raise HTTPException(status_code=500, detail=f"Emotion detection failed: {str(e)}")
203
+
204
+ @app.get("/recordings")
205
+ async def list_recordings():
206
+ """
207
+ List all uploaded recordings.
208
+ Returns a JSON list of filenames in the uploads directory.
209
+ """
210
+ try:
211
+ files = [f.name for f in UPLOAD_DIR.iterdir() if f.is_file()]
212
+ total, used, free = shutil.disk_usage(UPLOAD_DIR)
213
+ storage_info = {
214
+ "total_mb": total / (1024 * 1024),
215
+ "used_mb": used / (1024 * 1024),
216
+ "free_mb": free / (1024 * 1024)
217
+ }
218
+ return {"recordings": files, "storage": storage_info}
219
+ except Exception as e:
220
+ logger.error("Could not list files: %s", e)
221
+ raise HTTPException(status_code=500, detail=f"Failed to list recordings: {str(e)}")
222
+
223
+ @app.get("/recordings/{filename}")
224
+ async def get_recording(filename: str):
225
+ """
226
+ Stream/download an audio file from the server.
227
+ """
228
+ safe_name = Path(filename).name
229
+ file_path = UPLOAD_DIR / safe_name
230
+ if not file_path.exists() or not file_path.is_file():
231
+ raise HTTPException(status_code=404, detail="Recording not found")
232
+ # Guess MIME type (fallback to octet-stream)
233
+ import mimetypes
234
+ media_type, _ = mimetypes.guess_type(file_path)
235
+ return FileResponse(
236
+ file_path,
237
+ media_type=media_type or "application/octet-stream",
238
+ filename=safe_name
239
+ )
240
+
241
+ @app.get("/analyze/{filename}")
242
+ async def analyze_recording(filename: str):
243
+ """
244
+ Analyze an already-uploaded recording by filename.
245
+ Returns emotion predictions for the given file.
246
+ """
247
+ if not classifier:
248
+ raise HTTPException(status_code=503, detail="Model not yet loaded")
249
+
250
+ safe_name = Path(filename).name
251
+ file_path = UPLOAD_DIR / safe_name
252
+ if not file_path.exists() or not file_path.is_file():
253
+ raise HTTPException(status_code=404, detail="Recording not found")
254
+ try:
255
+ results = classifier(str(file_path))
256
+ except Exception as e:
257
+ logger.error("Model inference failed for %s: %s", filename, e)
258
+ raise HTTPException(status_code=500, detail=f"Emotion detection failed: {str(e)}")
259
+ return {"filename": safe_name, "predictions": results}
260
+
261
+ @app.delete("/recordings/{filename}")
262
+ async def delete_recording(filename: str):
263
+ """
264
+ Delete a recording by filename.
265
+ """
266
+ safe_name = Path(filename).name
267
+ file_path = UPLOAD_DIR / safe_name
268
+ if not file_path.exists() or not file_path.is_file():
269
+ raise HTTPException(status_code=404, detail="Recording not found")
270
+ try:
271
+ file_path.unlink()
272
+ return {"status": "success", "message": f"Deleted {safe_name}"}
273
+ except Exception as e:
274
+ logger.error("Failed to delete file %s: %s", filename, e)
275
+ raise HTTPException(status_code=500, detail=f"Failed to delete file: {str(e)}")
276
+
277
+ if __name__ == "__main__":
278
+ # Bind to 0.0.0.0:7860 for Hugging Face Spaces compatibility
279
+ uvicorn.run(app, host="0.0.0.0", port=7860)
requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi>=0.95.1,<0.96.0
2
+ uvicorn>=0.22.0,<0.23.0
3
+ transformers>=4.28.1,<4.29.0
4
+ torch>=2.0.0,<2.1.0
5
+ librosa>=0.10.0,<0.11.0
6
+ soundfile>=0.12.1,<0.13.0
7
+ python-multipart>=0.0.6,<0.0.7
8
+ numpy>=1.24.3,<1.25.0
9
+ tqdm>=4.65.0,<4.66.0
10
+ pydantic>=1.10.7,<1.11.0