Spaces:
Sleeping
Sleeping
File size: 8,396 Bytes
96fd859 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 | """Audio analysis endpoint with file validation and mock response."""
import uuid
import os
import tempfile
import random
from datetime import datetime, timezone
from fastapi import APIRouter, UploadFile, File, Request, status, HTTPException
from fastapi.responses import JSONResponse
import logging
import soundfile as sf
from app.state import model_info
from app.services.detector import EnsembleDetector
from app.services.forensics import ForensicsAnalyzer
logger = logging.getLogger(__name__)
from app.config import settings
from app.models import AnalysisResponse, ErrorResponse
from app.exceptions import (
FileTooLargeError,
InvalidFileTypeError,
AudioTooLongError,
AudioProcessingError,
)
router = APIRouter(tags=["Analysis"])
ALLOWED_EXTENSIONS = {".wav", ".mp3", ".m4a"}
ALLOWED_CONTENT_TYPES = {
"audio/wav",
"audio/x-wav",
"audio/wave",
"audio/mpeg",
"audio/mp3",
"audio/m4a",
"audio/mp4",
"audio/x-m4a",
"application/octet-stream", # fallback for some clients
}
MAX_FILE_SIZE_BYTES = settings.max_file_size_mb * 1024 * 1024
def _get_file_extension(filename: str | None) -> str:
"""Extract and normalize file extension."""
if not filename:
return ""
return os.path.splitext(filename)[1].lower()
def _validate_file_type(filename: str | None, content_type: str | None) -> None:
"""Validate that the uploaded file is wav or mp3."""
ext = _get_file_extension(filename)
if ext not in ALLOWED_EXTENSIONS:
raise InvalidFileTypeError(
file_type=ext or "unknown",
allowed_types=sorted(ALLOWED_EXTENSIONS),
)
def _validate_file_size(file_size: int) -> None:
"""Validate that the file does not exceed the size limit."""
if file_size > MAX_FILE_SIZE_BYTES:
raise FileTooLargeError(
file_size_mb=file_size / (1024 * 1024),
max_size_mb=settings.max_file_size_mb,
)
def _get_audio_duration(file_path: str, extension: str) -> float:
"""Get audio duration in seconds."""
try:
import mutagen
audio = mutagen.File(file_path)
if audio is not None and audio.info is not None:
return audio.info.length # mutagen returns seconds
except Exception:
pass
try:
import librosa
return librosa.get_duration(path=file_path)
except Exception as e:
raise AudioProcessingError(
reason=f"Could not read audio duration. Ensure it is a valid {extension} file. ({str(e)})"
)
def _validate_duration(duration_seconds: float) -> None:
"""Validate that the audio duration does not exceed the limit."""
if duration_seconds > settings.max_duration_seconds:
raise AudioTooLongError(
duration_seconds=duration_seconds,
max_seconds=settings.max_duration_seconds,
)
def _generate_prediction(
analysis_id: str,
filename: str,
file_size_bytes: int,
processed_data: dict,
detection_result,
forensics_data: dict,
timeline_segments: list
) -> dict:
"""Generate final response using the real DL detector results."""
explanation = "Analysis completed successfully."
return {
"id": analysis_id,
"filename": filename,
"duration_seconds": round(processed_data["duration"], 2),
"file_size_bytes": file_size_bytes,
"prediction": detection_result.prediction,
"confidence": round(detection_result.confidence, 4),
"human_probability": round(detection_result.human_probability, 4),
"ai_probability": round(detection_result.ai_probability, 4),
"forensics": forensics_data,
"timeline": timeline_segments,
"sample_rate": processed_data["sample_rate"],
"channels": processed_data["channels"],
"peak_amplitude": processed_data["peak_amplitude"],
"waveform": processed_data["waveform"],
"spectrogram_image": processed_data["spectrogram_image"],
"processed_audio_path": processed_data["processed_audio_path"],
}
@router.post(
"/analyze",
response_model=AnalysisResponse,
status_code=status.HTTP_200_OK,
responses={
413: {"model": ErrorResponse, "description": "File too large"},
415: {"model": ErrorResponse, "description": "Unsupported file type"},
422: {"model": ErrorResponse, "description": "Audio too long or unprocessable"},
500: {"model": ErrorResponse, "description": "Internal server error"},
},
summary="Analyze audio for deepfake detection",
description=(
"Upload a WAV or MP3 audio file for AI-powered deepfake detection analysis. "
"Maximum file size: 30 MB. Maximum duration: 5 minutes."
),
)
async def analyze_audio(file: UploadFile = File(..., description="Audio file (.wav or .mp3)")):
"""
Analyze an uploaded audio file for deepfake detection.
"""
if model_info.status != "ready":
raise HTTPException(
status_code=503,
detail=f"DL model is not ready. Status: {model_info.status}"
)
from app.utils.audio import AudioProcessor
analysis_id = str(uuid.uuid4())
_validate_file_type(file.filename, file.content_type)
try:
content = await file.read()
except Exception:
raise AudioProcessingError(reason="Failed to read uploaded file.")
file_size = len(content)
_validate_file_size(file_size)
ext = _get_file_extension(file.filename)
tmp_path = None
processed_data = None
try:
with tempfile.NamedTemporaryFile(delete=False, suffix=ext) as tmp:
tmp.write(content)
tmp_path = tmp.name
duration = _get_audio_duration(tmp_path, ext)
_validate_duration(duration)
processor = AudioProcessor()
cache_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "cache"))
processed_data = processor.process(tmp_path, cache_dir, analysis_id)
except (FileTooLargeError, InvalidFileTypeError, AudioTooLongError, AudioProcessingError):
raise # Re-raise known validation errors
except Exception as e:
error_msg = str(e)
if not error_msg or "NoBackendError" in str(type(e)):
error_msg = "The audio file format is not supported or the file is corrupted. (Missing decoding backend)"
raise AudioProcessingError(reason=error_msg)
finally:
if tmp_path and os.path.exists(tmp_path):
try:
os.unlink(tmp_path)
except OSError:
pass
if not processed_data:
raise AudioProcessingError(reason="Failed to process audio features.")
try:
waveform, sr = sf.read(processed_data["processed_audio_path"], dtype="float32")
from app.services.detector import EnsembleDetector
detector = EnsembleDetector(
model_info.gary_model, model_info.gary_feature_extractor,
model_info.bisher_model, model_info.bisher_feature_extractor,
sample_rate=16000
)
detection_result = detector.analyze(waveform, sample_rate=sr)
timeline_segments = detector.analyze_timeline(waveform, sample_rate=sr)
forensics_data = ForensicsAnalyzer.analyze(waveform, sr, ai_probability=detection_result.ai_probability)
logger.info(
f"Analysis complete - ID: {analysis_id} | "
f"Duration: {processed_data['duration']:.2f}s | "
f"Inference Time: {detection_result.inference_time_ms:.2f}ms | "
f"Prediction: {detection_result.prediction} | "
f"Confidence: {detection_result.confidence:.4f} | "
f"AI Prob: {detection_result.ai_probability:.4f} | "
f"Human Prob: {detection_result.human_probability:.4f}"
)
except Exception as e:
logger.error(f"Detection failed: {str(e)}")
raise AudioProcessingError(reason=f"Model inference failed: {str(e)}")
result = _generate_prediction(
analysis_id=analysis_id,
filename=file.filename or "unknown",
file_size_bytes=file_size,
processed_data=processed_data,
detection_result=detection_result,
forensics_data=forensics_data,
timeline_segments=timeline_segments
)
return result
|