Shujah239 commited on
Commit
f0029de
·
verified ·
1 Parent(s): f7f4d0f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +272 -279
app.py CHANGED
@@ -1,279 +1,272 @@
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)
 
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
+ @app.get("/")
16
+ async def root():
17
+ return {"message": "Audio Emotion Detection API", "status": "running"}
18
+
19
+ # Configure logging
20
+ logging.basicConfig(level=logging.INFO)
21
+ logger = logging.getLogger(__name__)
22
+
23
+ # Define uploads directory
24
+ UPLOAD_DIR = Path("uploads")
25
+ MAX_STORAGE_MB = 100 # Maximum storage in MB
26
+ MAX_FILE_AGE_DAYS = 1 # Maximum age of files in days
27
+
28
+ app = FastAPI(
29
+ title="Emotion Detection API",
30
+ description="Audio emotion detection using wav2vec2",
31
+ version="1.0.0",
32
+ )
33
+
34
+ # Add middleware
35
+ app.add_middleware(
36
+ CORSMiddleware,
37
+ allow_origins=["*"],
38
+ allow_credentials=True,
39
+ allow_methods=["*"],
40
+ allow_headers=["*"],
41
+ )
42
+ app.add_middleware(GZipMiddleware, minimum_size=1000)
43
+
44
+ # Preloaded classifier (global)
45
+ classifier = None
46
+
47
+ @app.on_event("startup")
48
+ async def load_model():
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
+ # Don't raise the error - let the app start even if model fails
75
+ # We'll handle this in the endpoints
76
+
77
+ async def cleanup_old_files():
78
+ """Clean up old files to prevent storage issues on Hugging Face Spaces."""
79
+ try:
80
+ # Remove files older than MAX_FILE_AGE_DAYS
81
+ now = time.time()
82
+ deleted_count = 0
83
+ for file_path in UPLOAD_DIR.iterdir():
84
+ if file_path.is_file():
85
+ file_age_days = (now - file_path.stat().st_mtime) / (60 * 60 * 24)
86
+ if file_age_days > MAX_FILE_AGE_DAYS:
87
+ file_path.unlink()
88
+ deleted_count += 1
89
+
90
+ if deleted_count > 0:
91
+ logger.info(f"Cleaned up {deleted_count} old files")
92
+ except Exception as e:
93
+ logger.error(f"Error during file cleanup: {e}")
94
+
95
+ @app.middleware("http")
96
+ async def add_process_time_header(request: Request, call_next):
97
+ """Add X-Process-Time header to responses."""
98
+ start_time = time.time()
99
+ response = await call_next(request)
100
+ process_time = time.time() - start_time
101
+ response.headers["X-Process-Time"] = str(process_time)
102
+ return response
103
+
104
+ @app.get("/health")
105
+ async def health():
106
+ """Health check endpoint."""
107
+ return {"status": "ok", "model_loaded": classifier is not None}
108
+
109
+ @app.post("/upload")
110
+ async def upload_audio(
111
+ file: UploadFile = File(...),
112
+ background_tasks: BackgroundTasks = None
113
+ ):
114
+ """
115
+ Upload an audio file and analyze emotions.
116
+ Saves the file to the uploads directory and returns model predictions.
117
+ """
118
+ if not classifier:
119
+ raise HTTPException(status_code=503, detail="Model not yet loaded")
120
+
121
+ filename = Path(file.filename).name
122
+ if not filename:
123
+ raise HTTPException(status_code=400, detail="Invalid filename")
124
+
125
+ # Check file extension
126
+ valid_extensions = [".wav", ".mp3", ".ogg", ".flac"]
127
+ if not any(filename.lower().endswith(ext) for ext in valid_extensions):
128
+ raise HTTPException(
129
+ status_code=400,
130
+ detail=f"Invalid file type. Supported types: {', '.join(valid_extensions)}"
131
+ )
132
+
133
+ # Read file contents
134
+ try:
135
+ contents = await file.read()
136
+ except Exception as e:
137
+ logger.error("Error reading file %s: %s", filename, e)
138
+ raise HTTPException(status_code=500, detail=f"Failed to read file: {str(e)}")
139
+ finally:
140
+ await file.close()
141
+
142
+ # Check file size (limit to 10MB for Spaces)
143
+ if len(contents) > 10 * 1024 * 1024:
144
+ raise HTTPException(
145
+ status_code=413,
146
+ detail="File too large. Maximum size is 10MB"
147
+ )
148
+
149
+ # Check available disk space
150
+ try:
151
+ total, used, free = shutil.disk_usage(UPLOAD_DIR)
152
+ free_mb = free / (1024 * 1024)
153
+
154
+ if free_mb < 10: # Keep at least 10MB free
155
+ # Schedule cleanup in background
156
+ if background_tasks:
157
+ background_tasks.add_task(cleanup_old_files)
158
+
159
+ if len(contents) > free:
160
+ logger.error(
161
+ "Insufficient storage: needed %d bytes, free %d bytes",
162
+ len(contents), free
163
+ )
164
+ raise HTTPException(status_code=507, detail="Insufficient storage to save file")
165
+ except Exception as e:
166
+ logger.warning(f"Failed to check disk usage: {e}")
167
+
168
+ # Save file to uploads directory
169
+ file_path = UPLOAD_DIR / filename
170
+ try:
171
+ with open(file_path, "wb") as f:
172
+ f.write(contents)
173
+ logger.info("Saved uploaded file: %s", file_path)
174
+ except Exception as e:
175
+ logger.error("Failed to save file %s: %s", filename, e)
176
+ raise HTTPException(status_code=500, detail=f"Failed to save file: {str(e)}")
177
+
178
+ # Analyze the audio file using the pretrained model pipeline
179
+ try:
180
+ results = classifier(str(file_path))
181
+
182
+ # Schedule cleanup in background
183
+ if background_tasks:
184
+ background_tasks.add_task(cleanup_old_files)
185
+
186
+ return {"filename": filename, "predictions": results}
187
+ except Exception as e:
188
+ logger.error("Model inference failed for %s: %s", filename, e)
189
+ # Try to remove the file if inference fails
190
+ try:
191
+ file_path.unlink(missing_ok=True)
192
+ except Exception:
193
+ pass
194
+ raise HTTPException(status_code=500, detail=f"Emotion detection failed: {str(e)}")
195
+
196
+ @app.get("/recordings")
197
+ async def list_recordings():
198
+ """
199
+ List all uploaded recordings.
200
+ Returns a JSON list of filenames in the uploads directory.
201
+ """
202
+ try:
203
+ files = [f.name for f in UPLOAD_DIR.iterdir() if f.is_file()]
204
+ total, used, free = shutil.disk_usage(UPLOAD_DIR)
205
+ storage_info = {
206
+ "total_mb": total / (1024 * 1024),
207
+ "used_mb": used / (1024 * 1024),
208
+ "free_mb": free / (1024 * 1024)
209
+ }
210
+ return {"recordings": files, "storage": storage_info}
211
+ except Exception as e:
212
+ logger.error("Could not list files: %s", e)
213
+ raise HTTPException(status_code=500, detail=f"Failed to list recordings: {str(e)}")
214
+
215
+ @app.get("/recordings/{filename}")
216
+ async def get_recording(filename: str):
217
+ """
218
+ Stream/download an audio file from the server.
219
+ """
220
+ safe_name = Path(filename).name
221
+ file_path = UPLOAD_DIR / safe_name
222
+ if not file_path.exists() or not file_path.is_file():
223
+ raise HTTPException(status_code=404, detail="Recording not found")
224
+ # Guess MIME type (fallback to octet-stream)
225
+ import mimetypes
226
+ media_type, _ = mimetypes.guess_type(file_path)
227
+ return FileResponse(
228
+ file_path,
229
+ media_type=media_type or "application/octet-stream",
230
+ filename=safe_name
231
+ )
232
+
233
+ @app.get("/analyze/{filename}")
234
+ async def analyze_recording(filename: str):
235
+ """
236
+ Analyze an already-uploaded recording by filename.
237
+ Returns emotion predictions for the given file.
238
+ """
239
+ if not classifier:
240
+ raise HTTPException(status_code=503, detail="Model not yet loaded")
241
+
242
+ safe_name = Path(filename).name
243
+ file_path = UPLOAD_DIR / safe_name
244
+ if not file_path.exists() or not file_path.is_file():
245
+ raise HTTPException(status_code=404, detail="Recording not found")
246
+ try:
247
+ results = classifier(str(file_path))
248
+ except Exception as e:
249
+ logger.error("Model inference failed for %s: %s", filename, e)
250
+ raise HTTPException(status_code=500, detail=f"Emotion detection failed: {str(e)}")
251
+ return {"filename": safe_name, "predictions": results}
252
+
253
+ @app.delete("/recordings/{filename}")
254
+ async def delete_recording(filename: str):
255
+ """
256
+ Delete a recording by filename.
257
+ """
258
+ safe_name = Path(filename).name
259
+ file_path = UPLOAD_DIR / safe_name
260
+ if not file_path.exists() or not file_path.is_file():
261
+ raise HTTPException(status_code=404, detail="Recording not found")
262
+ try:
263
+ file_path.unlink()
264
+ return {"status": "success", "message": f"Deleted {safe_name}"}
265
+ except Exception as e:
266
+ logger.error("Failed to delete file %s: %s", filename, e)
267
+ raise HTTPException(status_code=500, detail=f"Failed to delete file: {str(e)}")
268
+
269
+ if __name__ == "__main__":
270
+ # Bind to 0.0.0.0:7860 for Hugging Face Spaces compatibility
271
+ import uvicorn
272
+ uvicorn.run(app, host="0.0.0.0", port=7860)