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

Upload 2 files

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