juzer09 commited on
Commit
fed4526
·
verified ·
1 Parent(s): b2948e7

Update app.py

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