juzer09 commited on
Commit
d7836e8
Β·
verified Β·
1 Parent(s): fd166a6

Upload 2 files

Browse files
Files changed (2) hide show
  1. Dockerfile +1 -1
  2. app.py +466 -466
Dockerfile CHANGED
@@ -1,4 +1,4 @@
1
- FROM python:3.9-slim
2
 
3
  # Set working directory
4
  WORKDIR /app
 
1
+ FROM python:3.10-slim
2
 
3
  # Set working directory
4
  WORKDIR /app
app.py CHANGED
@@ -1,467 +1,467 @@
1
- #!/usr/bin/env python3
2
- """
3
- Madverse Music API
4
- AI Music Detection Service
5
- """
6
-
7
- from fastapi import FastAPI, HTTPException, BackgroundTasks, Header, Depends
8
- from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
9
- from pydantic import BaseModel, HttpUrl
10
- import torch
11
- import librosa
12
- import tempfile
13
- import os
14
- import requests
15
- from pathlib import Path
16
- import time
17
- from typing import Optional, Annotated, List
18
- import uvicorn
19
- import asyncio
20
- from contextlib import asynccontextmanager
21
- import socket
22
-
23
- # Global model variable
24
- model = None
25
-
26
- @asynccontextmanager
27
- async def lifespan(app: FastAPI):
28
- """Application lifespan management"""
29
- # Startup
30
- global model
31
- try:
32
- from sonics import HFAudioClassifier
33
- print("πŸ”„ Loading Madverse Music AI model...")
34
- model = HFAudioClassifier.from_pretrained("awsaf49/sonics-spectttra-alpha-120s")
35
- model.eval()
36
- print("βœ… Model loaded successfully!")
37
- except Exception as e:
38
- print(f"❌ Failed to load model: {e}")
39
- raise
40
-
41
- yield
42
-
43
- # Shutdown
44
- print("πŸ”„ Shutting down...")
45
-
46
- # Initialize FastAPI app with lifespan
47
- app = FastAPI(
48
- title="Madverse Music API",
49
- description="AI-powered music detection API to identify AI-generated vs human-created music",
50
- version="1.0.0",
51
- docs_url="/",
52
- redoc_url="/docs",
53
- lifespan=lifespan
54
- )
55
-
56
- # API Key Configuration
57
- API_KEY = os.getenv("MADVERSE_API_KEY", "madverse-music-api-key-2024") # Default key for demo
58
-
59
- async def verify_api_key(x_api_key: Annotated[str | None, Header()] = None):
60
- """Verify API key from header"""
61
- if x_api_key is None:
62
- raise HTTPException(
63
- status_code=401,
64
- detail="Missing API key. Please provide a valid X-API-Key header."
65
- )
66
- if x_api_key != API_KEY:
67
- raise HTTPException(
68
- status_code=401,
69
- detail="Invalid API key. Please provide a valid X-API-Key header."
70
- )
71
- return x_api_key
72
-
73
- class MusicAnalysisRequest(BaseModel):
74
- urls: List[HttpUrl]
75
-
76
- def check_api_key_first(request: MusicAnalysisRequest, x_api_key: Annotated[str | None, Header()] = None):
77
- """Check API key before processing request"""
78
- if x_api_key is None:
79
- raise HTTPException(
80
- status_code=401,
81
- detail="Missing API key. Please provide a valid X-API-Key header."
82
- )
83
- if x_api_key != API_KEY:
84
- raise HTTPException(
85
- status_code=401,
86
- detail="Invalid API key. Please provide a valid X-API-Key header."
87
- )
88
- return request
89
-
90
- class FileAnalysisResult(BaseModel):
91
- url: str
92
- success: bool
93
- classification: Optional[str] = None # "Real" or "Fake"
94
- confidence: Optional[float] = None # 0.0 to 1.0
95
- probability: Optional[float] = None # Raw sigmoid probability
96
- raw_score: Optional[float] = None # Raw model output
97
- duration: Optional[float] = None # Audio duration in seconds
98
- message: str
99
- processing_time: Optional[float] = None
100
- error: Optional[str] = None
101
-
102
- class MusicAnalysisResponse(BaseModel):
103
- success: bool
104
- total_files: int
105
- successful_analyses: int
106
- failed_analyses: int
107
- results: List[FileAnalysisResult]
108
- total_processing_time: float
109
- message: str
110
-
111
- class ErrorResponse(BaseModel):
112
- success: bool
113
- error: str
114
- message: str
115
-
116
- def cleanup_file(file_path: str):
117
- """Background task to cleanup temporary files"""
118
- try:
119
- if os.path.exists(file_path):
120
- os.unlink(file_path)
121
- except:
122
- pass
123
-
124
- def download_audio(url: str, max_size_mb: int = 100) -> str:
125
- """Download audio file from URL with size validation"""
126
- try:
127
- # Check if URL is accessible
128
- response = requests.head(str(url), timeout=10)
129
-
130
- # Check content size
131
- content_length = response.headers.get('Content-Length')
132
- if content_length and int(content_length) > max_size_mb * 1024 * 1024:
133
- raise HTTPException(
134
- status_code=413,
135
- detail=f"File too large. Maximum size: {max_size_mb}MB"
136
- )
137
-
138
- # Download file
139
- response = requests.get(str(url), timeout=30, stream=True)
140
- response.raise_for_status()
141
-
142
- # Create temporary file
143
- with tempfile.NamedTemporaryFile(delete=False, suffix='.tmp') as tmp_file:
144
- downloaded_size = 0
145
- for chunk in response.iter_content(chunk_size=8192):
146
- downloaded_size += len(chunk)
147
- if downloaded_size > max_size_mb * 1024 * 1024:
148
- os.unlink(tmp_file.name)
149
- raise HTTPException(
150
- status_code=413,
151
- detail=f"File too large. Maximum size: {max_size_mb}MB"
152
- )
153
- tmp_file.write(chunk)
154
-
155
- return tmp_file.name
156
-
157
- except requests.exceptions.RequestException as e:
158
- raise HTTPException(
159
- status_code=400,
160
- detail=f"Failed to download audio: {str(e)}"
161
- )
162
- except Exception as e:
163
- raise HTTPException(
164
- status_code=500,
165
- detail=f"Error downloading file: {str(e)}"
166
- )
167
-
168
- def classify_audio(file_path: str) -> dict:
169
- """Classify audio file using the AI model"""
170
- try:
171
- # Load audio (model uses 16kHz sample rate)
172
- audio, sr = librosa.load(file_path, sr=16000)
173
-
174
- # Convert to tensor and add batch dimension
175
- audio_tensor = torch.FloatTensor(audio).unsqueeze(0)
176
-
177
- # Get prediction
178
- with torch.no_grad():
179
- output = model(audio_tensor)
180
-
181
- # Convert logit to probability using sigmoid
182
- prob = torch.sigmoid(output).item()
183
-
184
- # Classify: prob < 0.5 = Real, prob >= 0.5 = Fake
185
- if prob < 0.5:
186
- classification = "Real"
187
- confidence = (1 - prob) * 2 # Convert to 0-1 scale
188
- else:
189
- classification = "Fake"
190
- confidence = (prob - 0.5) * 2 # Convert to 0-1 scale
191
-
192
- return {
193
- "classification": classification,
194
- "confidence": min(confidence, 1.0), # Cap at 1.0
195
- "probability": prob,
196
- "raw_score": output.item(),
197
- "duration": len(audio) / sr
198
- }
199
-
200
- except Exception as e:
201
- raise HTTPException(
202
- status_code=500,
203
- detail=f"Error analyzing audio: {str(e)}"
204
- )
205
-
206
- async def process_single_url(url: str) -> FileAnalysisResult:
207
- """Process a single URL and return result"""
208
- start_time = time.time()
209
-
210
- try:
211
- # Download audio file
212
- temp_file = download_audio(url)
213
-
214
- # Classify audio
215
- result = classify_audio(temp_file)
216
-
217
- # Calculate processing time
218
- processing_time = time.time() - start_time
219
-
220
- # Cleanup file in background
221
- try:
222
- os.unlink(temp_file)
223
- except:
224
- pass
225
-
226
- # Prepare response
227
- emoji = "🎀" if result["classification"] == "Real" else "πŸ€–"
228
- message = f'{emoji} Detected as {result["classification"].lower()} music'
229
-
230
- return FileAnalysisResult(
231
- url=str(url),
232
- success=True,
233
- classification=result["classification"],
234
- confidence=result["confidence"],
235
- probability=result["probability"],
236
- raw_score=result["raw_score"],
237
- duration=result["duration"],
238
- message=message,
239
- processing_time=processing_time
240
- )
241
-
242
- except Exception as e:
243
- processing_time = time.time() - start_time
244
- error_msg = str(e)
245
-
246
- return FileAnalysisResult(
247
- url=str(url),
248
- success=False,
249
- message=f"❌ Failed to process: {error_msg}",
250
- processing_time=processing_time,
251
- error=error_msg
252
- )
253
-
254
- @app.post("/analyze", response_model=MusicAnalysisResponse)
255
- async def analyze_music(
256
- request: MusicAnalysisRequest = Depends(check_api_key_first)
257
- ):
258
- """
259
- Analyze music from URL(s) to detect if it's AI-generated or human-created
260
-
261
- - **urls**: Array of direct URLs to audio files (MP3, WAV, FLAC, M4A, OGG)
262
- - Returns classification results for each file
263
- - Processes files concurrently for better performance when multiple URLs provided
264
- """
265
- start_time = time.time()
266
-
267
- if not model:
268
- raise HTTPException(
269
- status_code=503,
270
- detail="Model not loaded. Please try again later."
271
- )
272
-
273
- if len(request.urls) > 50: # Limit processing
274
- raise HTTPException(
275
- status_code=400,
276
- detail="Too many URLs. Maximum 50 files per request."
277
- )
278
-
279
- if len(request.urls) == 0:
280
- raise HTTPException(
281
- status_code=400,
282
- detail="At least one URL is required."
283
- )
284
-
285
- try:
286
- # Process all URLs concurrently with limited concurrency
287
- semaphore = asyncio.Semaphore(5) # Limit to 5 concurrent downloads
288
-
289
- async def process_with_semaphore(url):
290
- async with semaphore:
291
- return await process_single_url(str(url))
292
-
293
- # Create tasks for all URLs
294
- tasks = [process_with_semaphore(url) for url in request.urls]
295
-
296
- # Wait for all tasks to complete
297
- results = await asyncio.gather(*tasks, return_exceptions=True)
298
-
299
- # Process results and handle any exceptions
300
- processed_results = []
301
- successful_count = 0
302
- failed_count = 0
303
-
304
- for i, result in enumerate(results):
305
- if isinstance(result, Exception):
306
- # Handle exception case
307
- processed_results.append(FileAnalysisResult(
308
- url=str(request.urls[i]),
309
- success=False,
310
- message=f"❌ Processing failed: {str(result)}",
311
- error=str(result)
312
- ))
313
- failed_count += 1
314
- else:
315
- processed_results.append(result)
316
- if result.success:
317
- successful_count += 1
318
- else:
319
- failed_count += 1
320
-
321
- # Calculate total processing time
322
- total_processing_time = time.time() - start_time
323
-
324
- # Prepare summary message
325
- total_files = len(request.urls)
326
- if total_files == 1:
327
- # Single file message
328
- if successful_count == 1:
329
- message = processed_results[0].message
330
- else:
331
- message = processed_results[0].message
332
- else:
333
- # Multiple files message
334
- if successful_count == total_files:
335
- message = f"βœ… Successfully analyzed all {total_files} files"
336
- elif successful_count > 0:
337
- message = f"⚠️ Analyzed {successful_count}/{total_files} files successfully"
338
- else:
339
- message = f"❌ Failed to analyze any files"
340
-
341
- return MusicAnalysisResponse(
342
- success=successful_count > 0,
343
- total_files=total_files,
344
- successful_analyses=successful_count,
345
- failed_analyses=failed_count,
346
- results=processed_results,
347
- total_processing_time=total_processing_time,
348
- message=message
349
- )
350
-
351
- except Exception as e:
352
- raise HTTPException(
353
- status_code=500,
354
- detail=f"Internal server error during processing: {str(e)}"
355
- )
356
-
357
- @app.get("/health")
358
- async def health_check():
359
- """Health check endpoint"""
360
- return {
361
- "status": "healthy",
362
- "model_loaded": model is not None,
363
- "service": "Madverse Music API"
364
- }
365
-
366
- @app.get("/info")
367
- async def get_info():
368
- """Get API information"""
369
- return {
370
- "name": "Madverse Music API",
371
- "version": "1.0.0",
372
- "description": "AI-powered music detection to identify AI-generated vs human-created music",
373
- "model": "SpecTTTra-Ξ± (120s)",
374
- "accuracy": {
375
- "f1_score": 0.97,
376
- "sensitivity": 0.96,
377
- "specificity": 0.99
378
- },
379
- "supported_formats": ["MP3", "WAV", "FLAC", "M4A", "OGG"],
380
- "max_file_size": "100MB",
381
- "max_duration": "120 seconds",
382
- "authentication": {
383
- "required": True,
384
- "type": "API Key",
385
- "header": "X-API-Key",
386
- "example": "X-API-Key: your-api-key-here"
387
- },
388
- "usage": {
389
- "curl_example": "curl -X POST 'http://localhost:8000/analyze' -H 'X-API-Key: your-api-key' -H 'Content-Type: application/json' -d '{\"url\":\"https://example.com/song.mp3\"}'"
390
- }
391
- }
392
-
393
- def find_available_port(start_port: int = 8000, max_attempts: int = 10) -> int:
394
- """Find an available port starting from start_port"""
395
- import random
396
- import time
397
-
398
- # Add some randomization to avoid race conditions
399
- time.sleep(random.uniform(0.1, 0.5))
400
-
401
- for port in range(start_port, start_port + max_attempts):
402
- try:
403
- # Try to bind to the port with proper error handling
404
- with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
405
- s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
406
- s.bind(('0.0.0.0', port))
407
- s.listen(1)
408
- print(f"βœ… Port {port} is available")
409
- return port
410
- except OSError as e:
411
- print(f"❌ Port {port} is busy: {e}")
412
- continue
413
-
414
- # If no port found, raise an exception
415
- raise RuntimeError(f"No available port found in range {start_port}-{start_port + max_attempts - 1}")
416
-
417
- if __name__ == "__main__":
418
- try:
419
- # Check if we're in a Hugging Face environment
420
- is_hf_space = os.getenv('SPACE_ID') is not None
421
- hf_port = os.getenv('PORT') # HF Spaces sets this
422
-
423
- if is_hf_space and hf_port:
424
- # Use HF Spaces assigned port
425
- port = int(hf_port)
426
- print(f"πŸ€— Running in Hugging Face Spaces on port {port}")
427
- elif is_hf_space:
428
- print("πŸ€— Running in Hugging Face Spaces environment")
429
- # Use standard HF Spaces port
430
- port = 7860
431
- else:
432
- # Find an available port for local development
433
- port = find_available_port(8000, 10)
434
-
435
- print(f"πŸš€ Starting server on port {port}")
436
-
437
- # For HF Spaces, don't use the retry logic as it might cause issues
438
- if is_hf_space:
439
- uvicorn.run(app, host="0.0.0.0", port=port)
440
- else:
441
- # Add retry logic for local development
442
- max_retries = 3
443
- for attempt in range(max_retries):
444
- try:
445
- uvicorn.run(app, host="0.0.0.0", port=port)
446
- break # If successful, break out of retry loop
447
- except OSError as e:
448
- if "Address already in use" in str(e) and attempt < max_retries - 1:
449
- print(f"⚠️ Port {port} became busy, trying next port...")
450
- port = find_available_port(port + 1, 10)
451
- print(f"πŸ”„ Retrying on port {port}")
452
- else:
453
- raise
454
-
455
- except RuntimeError as e:
456
- print(f"❌ {e}")
457
- print("πŸ’‘ Suggestions:")
458
- print(" 1. Wait a moment and try again (another instance might be shutting down)")
459
- print(" 2. Manually specify a different port:")
460
- print(" uvicorn app:app --host 0.0.0.0 --port 8001")
461
- print(" 3. Check for running processes: ps aux | grep python")
462
- except KeyboardInterrupt:
463
- print("\nπŸ›‘ Server stopped by user")
464
- except Exception as e:
465
- print(f"❌ Failed to start server: {e}")
466
- import traceback
467
  traceback.print_exc()
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Madverse Music API
4
+ AI Music Detection Service
5
+ """
6
+
7
+ from fastapi import FastAPI, HTTPException, BackgroundTasks, Header, Depends
8
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
9
+ from pydantic import BaseModel, HttpUrl
10
+ import torch
11
+ import librosa
12
+ import tempfile
13
+ import os
14
+ import requests
15
+ from pathlib import Path
16
+ import time
17
+ from typing import Optional, Annotated, List, Union
18
+ import uvicorn
19
+ import asyncio
20
+ from contextlib import asynccontextmanager
21
+ import socket
22
+
23
+ # Global model variable
24
+ model = None
25
+
26
+ @asynccontextmanager
27
+ async def lifespan(app: FastAPI):
28
+ """Application lifespan management"""
29
+ # Startup
30
+ global model
31
+ try:
32
+ from sonics import HFAudioClassifier
33
+ print("πŸ”„ Loading Madverse Music AI model...")
34
+ model = HFAudioClassifier.from_pretrained("awsaf49/sonics-spectttra-alpha-120s")
35
+ model.eval()
36
+ print("βœ… Model loaded successfully!")
37
+ except Exception as e:
38
+ print(f"❌ Failed to load model: {e}")
39
+ raise
40
+
41
+ yield
42
+
43
+ # Shutdown
44
+ print("πŸ”„ Shutting down...")
45
+
46
+ # Initialize FastAPI app with lifespan
47
+ app = FastAPI(
48
+ title="Madverse Music API",
49
+ description="AI-powered music detection API to identify AI-generated vs human-created music",
50
+ version="1.0.0",
51
+ docs_url="/",
52
+ redoc_url="/docs",
53
+ lifespan=lifespan
54
+ )
55
+
56
+ # API Key Configuration
57
+ API_KEY = os.getenv("MADVERSE_API_KEY", "madverse-music-api-key-2024") # Default key for demo
58
+
59
+ async def verify_api_key(x_api_key: Annotated[Union[str, None], Header()] = None):
60
+ """Verify API key from header"""
61
+ if x_api_key is None:
62
+ raise HTTPException(
63
+ status_code=401,
64
+ detail="Missing API key. Please provide a valid X-API-Key header."
65
+ )
66
+ if x_api_key != API_KEY:
67
+ raise HTTPException(
68
+ status_code=401,
69
+ detail="Invalid API key. Please provide a valid X-API-Key header."
70
+ )
71
+ return x_api_key
72
+
73
+ class MusicAnalysisRequest(BaseModel):
74
+ urls: List[HttpUrl]
75
+
76
+ def check_api_key_first(request: MusicAnalysisRequest, x_api_key: Annotated[Union[str, None], Header()] = None):
77
+ """Check API key before processing request"""
78
+ if x_api_key is None:
79
+ raise HTTPException(
80
+ status_code=401,
81
+ detail="Missing API key. Please provide a valid X-API-Key header."
82
+ )
83
+ if x_api_key != API_KEY:
84
+ raise HTTPException(
85
+ status_code=401,
86
+ detail="Invalid API key. Please provide a valid X-API-Key header."
87
+ )
88
+ return request
89
+
90
+ class FileAnalysisResult(BaseModel):
91
+ url: str
92
+ success: bool
93
+ classification: Optional[str] = None # "Real" or "Fake"
94
+ confidence: Optional[float] = None # 0.0 to 1.0
95
+ probability: Optional[float] = None # Raw sigmoid probability
96
+ raw_score: Optional[float] = None # Raw model output
97
+ duration: Optional[float] = None # Audio duration in seconds
98
+ message: str
99
+ processing_time: Optional[float] = None
100
+ error: Optional[str] = None
101
+
102
+ class MusicAnalysisResponse(BaseModel):
103
+ success: bool
104
+ total_files: int
105
+ successful_analyses: int
106
+ failed_analyses: int
107
+ results: List[FileAnalysisResult]
108
+ total_processing_time: float
109
+ message: str
110
+
111
+ class ErrorResponse(BaseModel):
112
+ success: bool
113
+ error: str
114
+ message: str
115
+
116
+ def cleanup_file(file_path: str):
117
+ """Background task to cleanup temporary files"""
118
+ try:
119
+ if os.path.exists(file_path):
120
+ os.unlink(file_path)
121
+ except:
122
+ pass
123
+
124
+ def download_audio(url: str, max_size_mb: int = 100) -> str:
125
+ """Download audio file from URL with size validation"""
126
+ try:
127
+ # Check if URL is accessible
128
+ response = requests.head(str(url), timeout=10)
129
+
130
+ # Check content size
131
+ content_length = response.headers.get('Content-Length')
132
+ if content_length and int(content_length) > max_size_mb * 1024 * 1024:
133
+ raise HTTPException(
134
+ status_code=413,
135
+ detail=f"File too large. Maximum size: {max_size_mb}MB"
136
+ )
137
+
138
+ # Download file
139
+ response = requests.get(str(url), timeout=30, stream=True)
140
+ response.raise_for_status()
141
+
142
+ # Create temporary file
143
+ with tempfile.NamedTemporaryFile(delete=False, suffix='.tmp') as tmp_file:
144
+ downloaded_size = 0
145
+ for chunk in response.iter_content(chunk_size=8192):
146
+ downloaded_size += len(chunk)
147
+ if downloaded_size > max_size_mb * 1024 * 1024:
148
+ os.unlink(tmp_file.name)
149
+ raise HTTPException(
150
+ status_code=413,
151
+ detail=f"File too large. Maximum size: {max_size_mb}MB"
152
+ )
153
+ tmp_file.write(chunk)
154
+
155
+ return tmp_file.name
156
+
157
+ except requests.exceptions.RequestException as e:
158
+ raise HTTPException(
159
+ status_code=400,
160
+ detail=f"Failed to download audio: {str(e)}"
161
+ )
162
+ except Exception as e:
163
+ raise HTTPException(
164
+ status_code=500,
165
+ detail=f"Error downloading file: {str(e)}"
166
+ )
167
+
168
+ def classify_audio(file_path: str) -> dict:
169
+ """Classify audio file using the AI model"""
170
+ try:
171
+ # Load audio (model uses 16kHz sample rate)
172
+ audio, sr = librosa.load(file_path, sr=16000)
173
+
174
+ # Convert to tensor and add batch dimension
175
+ audio_tensor = torch.FloatTensor(audio).unsqueeze(0)
176
+
177
+ # Get prediction
178
+ with torch.no_grad():
179
+ output = model(audio_tensor)
180
+
181
+ # Convert logit to probability using sigmoid
182
+ prob = torch.sigmoid(output).item()
183
+
184
+ # Classify: prob < 0.5 = Real, prob >= 0.5 = Fake
185
+ if prob < 0.5:
186
+ classification = "Real"
187
+ confidence = (1 - prob) * 2 # Convert to 0-1 scale
188
+ else:
189
+ classification = "Fake"
190
+ confidence = (prob - 0.5) * 2 # Convert to 0-1 scale
191
+
192
+ return {
193
+ "classification": classification,
194
+ "confidence": min(confidence, 1.0), # Cap at 1.0
195
+ "probability": prob,
196
+ "raw_score": output.item(),
197
+ "duration": len(audio) / sr
198
+ }
199
+
200
+ except Exception as e:
201
+ raise HTTPException(
202
+ status_code=500,
203
+ detail=f"Error analyzing audio: {str(e)}"
204
+ )
205
+
206
+ async def process_single_url(url: str) -> FileAnalysisResult:
207
+ """Process a single URL and return result"""
208
+ start_time = time.time()
209
+
210
+ try:
211
+ # Download audio file
212
+ temp_file = download_audio(url)
213
+
214
+ # Classify audio
215
+ result = classify_audio(temp_file)
216
+
217
+ # Calculate processing time
218
+ processing_time = time.time() - start_time
219
+
220
+ # Cleanup file in background
221
+ try:
222
+ os.unlink(temp_file)
223
+ except:
224
+ pass
225
+
226
+ # Prepare response
227
+ emoji = "🎀" if result["classification"] == "Real" else "πŸ€–"
228
+ message = f'{emoji} Detected as {result["classification"].lower()} music'
229
+
230
+ return FileAnalysisResult(
231
+ url=str(url),
232
+ success=True,
233
+ classification=result["classification"],
234
+ confidence=result["confidence"],
235
+ probability=result["probability"],
236
+ raw_score=result["raw_score"],
237
+ duration=result["duration"],
238
+ message=message,
239
+ processing_time=processing_time
240
+ )
241
+
242
+ except Exception as e:
243
+ processing_time = time.time() - start_time
244
+ error_msg = str(e)
245
+
246
+ return FileAnalysisResult(
247
+ url=str(url),
248
+ success=False,
249
+ message=f"❌ Failed to process: {error_msg}",
250
+ processing_time=processing_time,
251
+ error=error_msg
252
+ )
253
+
254
+ @app.post("/analyze", response_model=MusicAnalysisResponse)
255
+ async def analyze_music(
256
+ request: MusicAnalysisRequest = Depends(check_api_key_first)
257
+ ):
258
+ """
259
+ Analyze music from URL(s) to detect if it's AI-generated or human-created
260
+
261
+ - **urls**: Array of direct URLs to audio files (MP3, WAV, FLAC, M4A, OGG)
262
+ - Returns classification results for each file
263
+ - Processes files concurrently for better performance when multiple URLs provided
264
+ """
265
+ start_time = time.time()
266
+
267
+ if not model:
268
+ raise HTTPException(
269
+ status_code=503,
270
+ detail="Model not loaded. Please try again later."
271
+ )
272
+
273
+ if len(request.urls) > 50: # Limit processing
274
+ raise HTTPException(
275
+ status_code=400,
276
+ detail="Too many URLs. Maximum 50 files per request."
277
+ )
278
+
279
+ if len(request.urls) == 0:
280
+ raise HTTPException(
281
+ status_code=400,
282
+ detail="At least one URL is required."
283
+ )
284
+
285
+ try:
286
+ # Process all URLs concurrently with limited concurrency
287
+ semaphore = asyncio.Semaphore(5) # Limit to 5 concurrent downloads
288
+
289
+ async def process_with_semaphore(url):
290
+ async with semaphore:
291
+ return await process_single_url(str(url))
292
+
293
+ # Create tasks for all URLs
294
+ tasks = [process_with_semaphore(url) for url in request.urls]
295
+
296
+ # Wait for all tasks to complete
297
+ results = await asyncio.gather(*tasks, return_exceptions=True)
298
+
299
+ # Process results and handle any exceptions
300
+ processed_results = []
301
+ successful_count = 0
302
+ failed_count = 0
303
+
304
+ for i, result in enumerate(results):
305
+ if isinstance(result, Exception):
306
+ # Handle exception case
307
+ processed_results.append(FileAnalysisResult(
308
+ url=str(request.urls[i]),
309
+ success=False,
310
+ message=f"❌ Processing failed: {str(result)}",
311
+ error=str(result)
312
+ ))
313
+ failed_count += 1
314
+ else:
315
+ processed_results.append(result)
316
+ if result.success:
317
+ successful_count += 1
318
+ else:
319
+ failed_count += 1
320
+
321
+ # Calculate total processing time
322
+ total_processing_time = time.time() - start_time
323
+
324
+ # Prepare summary message
325
+ total_files = len(request.urls)
326
+ if total_files == 1:
327
+ # Single file message
328
+ if successful_count == 1:
329
+ message = processed_results[0].message
330
+ else:
331
+ message = processed_results[0].message
332
+ else:
333
+ # Multiple files message
334
+ if successful_count == total_files:
335
+ message = f"βœ… Successfully analyzed all {total_files} files"
336
+ elif successful_count > 0:
337
+ message = f"⚠️ Analyzed {successful_count}/{total_files} files successfully"
338
+ else:
339
+ message = f"❌ Failed to analyze any files"
340
+
341
+ return MusicAnalysisResponse(
342
+ success=successful_count > 0,
343
+ total_files=total_files,
344
+ successful_analyses=successful_count,
345
+ failed_analyses=failed_count,
346
+ results=processed_results,
347
+ total_processing_time=total_processing_time,
348
+ message=message
349
+ )
350
+
351
+ except Exception as e:
352
+ raise HTTPException(
353
+ status_code=500,
354
+ detail=f"Internal server error during processing: {str(e)}"
355
+ )
356
+
357
+ @app.get("/health")
358
+ async def health_check():
359
+ """Health check endpoint"""
360
+ return {
361
+ "status": "healthy",
362
+ "model_loaded": model is not None,
363
+ "service": "Madverse Music API"
364
+ }
365
+
366
+ @app.get("/info")
367
+ async def get_info():
368
+ """Get API information"""
369
+ return {
370
+ "name": "Madverse Music API",
371
+ "version": "1.0.0",
372
+ "description": "AI-powered music detection to identify AI-generated vs human-created music",
373
+ "model": "SpecTTTra-Ξ± (120s)",
374
+ "accuracy": {
375
+ "f1_score": 0.97,
376
+ "sensitivity": 0.96,
377
+ "specificity": 0.99
378
+ },
379
+ "supported_formats": ["MP3", "WAV", "FLAC", "M4A", "OGG"],
380
+ "max_file_size": "100MB",
381
+ "max_duration": "120 seconds",
382
+ "authentication": {
383
+ "required": True,
384
+ "type": "API Key",
385
+ "header": "X-API-Key",
386
+ "example": "X-API-Key: your-api-key-here"
387
+ },
388
+ "usage": {
389
+ "curl_example": "curl -X POST 'http://localhost:8000/analyze' -H 'X-API-Key: your-api-key' -H 'Content-Type: application/json' -d '{\"url\":\"https://example.com/song.mp3\"}'"
390
+ }
391
+ }
392
+
393
+ def find_available_port(start_port: int = 8000, max_attempts: int = 10) -> int:
394
+ """Find an available port starting from start_port"""
395
+ import random
396
+ import time
397
+
398
+ # Add some randomization to avoid race conditions
399
+ time.sleep(random.uniform(0.1, 0.5))
400
+
401
+ for port in range(start_port, start_port + max_attempts):
402
+ try:
403
+ # Try to bind to the port with proper error handling
404
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
405
+ s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
406
+ s.bind(('0.0.0.0', port))
407
+ s.listen(1)
408
+ print(f"βœ… Port {port} is available")
409
+ return port
410
+ except OSError as e:
411
+ print(f"❌ Port {port} is busy: {e}")
412
+ continue
413
+
414
+ # If no port found, raise an exception
415
+ raise RuntimeError(f"No available port found in range {start_port}-{start_port + max_attempts - 1}")
416
+
417
+ if __name__ == "__main__":
418
+ try:
419
+ # Check if we're in a Hugging Face environment
420
+ is_hf_space = os.getenv('SPACE_ID') is not None
421
+ hf_port = os.getenv('PORT') # HF Spaces sets this
422
+
423
+ if is_hf_space and hf_port:
424
+ # Use HF Spaces assigned port
425
+ port = int(hf_port)
426
+ print(f"πŸ€— Running in Hugging Face Spaces on port {port}")
427
+ elif is_hf_space:
428
+ print("πŸ€— Running in Hugging Face Spaces environment")
429
+ # Use standard HF Spaces port
430
+ port = 7860
431
+ else:
432
+ # Find an available port for local development
433
+ port = find_available_port(8000, 10)
434
+
435
+ print(f"πŸš€ Starting server on port {port}")
436
+
437
+ # For HF Spaces, don't use the retry logic as it might cause issues
438
+ if is_hf_space:
439
+ uvicorn.run(app, host="0.0.0.0", port=port)
440
+ else:
441
+ # Add retry logic for local development
442
+ max_retries = 3
443
+ for attempt in range(max_retries):
444
+ try:
445
+ uvicorn.run(app, host="0.0.0.0", port=port)
446
+ break # If successful, break out of retry loop
447
+ except OSError as e:
448
+ if "Address already in use" in str(e) and attempt < max_retries - 1:
449
+ print(f"⚠️ Port {port} became busy, trying next port...")
450
+ port = find_available_port(port + 1, 10)
451
+ print(f"πŸ”„ Retrying on port {port}")
452
+ else:
453
+ raise
454
+
455
+ except RuntimeError as e:
456
+ print(f"❌ {e}")
457
+ print("πŸ’‘ Suggestions:")
458
+ print(" 1. Wait a moment and try again (another instance might be shutting down)")
459
+ print(" 2. Manually specify a different port:")
460
+ print(" uvicorn app:app --host 0.0.0.0 --port 8001")
461
+ print(" 3. Check for running processes: ps aux | grep python")
462
+ except KeyboardInterrupt:
463
+ print("\nπŸ›‘ Server stopped by user")
464
+ except Exception as e:
465
+ print(f"❌ Failed to start server: {e}")
466
+ import traceback
467
  traceback.print_exc()