itssKarthiii commited on
Commit
6b408d7
·
verified ·
1 Parent(s): 3d63d35

Upload 70 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. Dockerfile +70 -0
  2. app/__init__.py +4 -0
  3. app/__pycache__/__init__.cpython-312.pyc +0 -0
  4. app/__pycache__/config.cpython-312.pyc +0 -0
  5. app/__pycache__/main.cpython-312.pyc +0 -0
  6. app/api/__init__.py +9 -0
  7. app/api/__pycache__/__init__.cpython-312.pyc +0 -0
  8. app/api/__pycache__/dependencies.cpython-312.pyc +0 -0
  9. app/api/dependencies.py +143 -0
  10. app/api/middleware/__init__.py +13 -0
  11. app/api/middleware/__pycache__/__init__.cpython-312.pyc +0 -0
  12. app/api/middleware/__pycache__/auth.cpython-312.pyc +0 -0
  13. app/api/middleware/__pycache__/error_handler.cpython-312.pyc +0 -0
  14. app/api/middleware/__pycache__/rate_limiter.cpython-312.pyc +0 -0
  15. app/api/middleware/auth.py +107 -0
  16. app/api/middleware/error_handler.py +130 -0
  17. app/api/middleware/rate_limiter.py +90 -0
  18. app/api/routes/__init__.py +9 -0
  19. app/api/routes/__pycache__/__init__.cpython-312.pyc +0 -0
  20. app/api/routes/__pycache__/federated.cpython-312.pyc +0 -0
  21. app/api/routes/__pycache__/health.cpython-312.pyc +0 -0
  22. app/api/routes/__pycache__/voice_detection.cpython-312.pyc +0 -0
  23. app/api/routes/federated.py +100 -0
  24. app/api/routes/health.py +101 -0
  25. app/api/routes/voice_detection.py +150 -0
  26. app/config.py +124 -0
  27. app/main.py +182 -0
  28. app/ml/__init__.py +11 -0
  29. app/ml/__pycache__/__init__.cpython-312.pyc +0 -0
  30. app/ml/__pycache__/inference.cpython-312.pyc +0 -0
  31. app/ml/__pycache__/model_loader.cpython-312.pyc +0 -0
  32. app/ml/__pycache__/preprocessing.cpython-312.pyc +0 -0
  33. app/ml/inference.py +235 -0
  34. app/ml/model_loader.py +246 -0
  35. app/ml/preprocessing.py +155 -0
  36. app/models/__init__.py +19 -0
  37. app/models/__pycache__/__init__.cpython-312.pyc +0 -0
  38. app/models/__pycache__/enums.cpython-312.pyc +0 -0
  39. app/models/__pycache__/request.cpython-312.pyc +0 -0
  40. app/models/__pycache__/response.cpython-312.pyc +0 -0
  41. app/models/enums.py +57 -0
  42. app/models/request.py +99 -0
  43. app/models/response.py +264 -0
  44. app/services/__init__.py +19 -0
  45. app/services/__pycache__/__init__.cpython-312.pyc +0 -0
  46. app/services/__pycache__/audio_forensics.cpython-312.pyc +0 -0
  47. app/services/__pycache__/audio_processor.cpython-312.pyc +0 -0
  48. app/services/__pycache__/explainability.cpython-312.pyc +0 -0
  49. app/services/__pycache__/federated_learning.cpython-312.pyc +0 -0
  50. app/services/__pycache__/score_calculators.cpython-312.pyc +0 -0
Dockerfile ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # =============================================================================
2
+ # VoiceAuth API Dockerfile
3
+ # =============================================================================
4
+ # Multi-stage build for optimized production image
5
+
6
+ # -----------------------------------------------------------------------------
7
+ # Stage 1: Builder
8
+ # -----------------------------------------------------------------------------
9
+ FROM python:3.11-slim AS builder
10
+
11
+ # Install build dependencies
12
+ RUN apt-get update && apt-get install -y --no-install-recommends \
13
+ build-essential \
14
+ && rm -rf /var/lib/apt/lists/*
15
+
16
+ # Create virtual environment
17
+ RUN python -m venv /opt/venv
18
+ ENV PATH="/opt/venv/bin:$PATH"
19
+
20
+ # Install Python dependencies
21
+ COPY requirements.txt .
22
+ RUN pip install --no-cache-dir --upgrade pip && \
23
+ pip install --no-cache-dir -r requirements.txt
24
+
25
+ # -----------------------------------------------------------------------------
26
+ # Stage 2: Production
27
+ # -----------------------------------------------------------------------------
28
+ FROM python:3.11-slim AS production
29
+
30
+ # Install runtime dependencies
31
+ RUN apt-get update && apt-get install -y --no-install-recommends \
32
+ ffmpeg \
33
+ libsndfile1 \
34
+ curl \
35
+ && rm -rf /var/lib/apt/lists/*
36
+
37
+ # Create non-root user
38
+ RUN useradd --create-home --uid 1000 appuser
39
+
40
+ # Copy virtual environment from builder
41
+ COPY --from=builder /opt/venv /opt/venv
42
+ ENV PATH="/opt/venv/bin:$PATH"
43
+
44
+ # Set working directory
45
+ WORKDIR /app
46
+
47
+ # Copy application code
48
+ COPY --chown=appuser:appuser . .
49
+
50
+ # Create directories for models and logs
51
+ RUN mkdir -p /app/models /app/logs && \
52
+ chown -R appuser:appuser /app
53
+
54
+ # Switch to non-root user
55
+ USER appuser
56
+
57
+ # Environment variables
58
+ ENV PYTHONUNBUFFERED=1 \
59
+ PYTHONDONTWRITEBYTECODE=1 \
60
+ PORT=7860
61
+
62
+ # Expose port
63
+ EXPOSE 7860
64
+
65
+ # Health check
66
+ HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
67
+ CMD curl -f http://localhost:7860/api/health || exit 1
68
+
69
+ # Run application
70
+ CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"]
app/__init__.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ """VoiceAuth - AI Voice Detection API."""
2
+
3
+ __version__ = "1.0.0"
4
+ __author__ = "VoiceAuth Team"
app/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (277 Bytes). View file
 
app/__pycache__/config.cpython-312.pyc ADDED
Binary file (5 kB). View file
 
app/__pycache__/main.cpython-312.pyc ADDED
Binary file (5.14 kB). View file
 
app/api/__init__.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ """API package."""
2
+
3
+ from app.api.routes import health
4
+ from app.api.routes import voice_detection
5
+
6
+ __all__ = [
7
+ "health",
8
+ "voice_detection",
9
+ ]
app/api/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (324 Bytes). View file
 
app/api/__pycache__/dependencies.cpython-312.pyc ADDED
Binary file (4.31 kB). View file
 
app/api/dependencies.py ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ FastAPI dependency injection.
3
+
4
+ Provides dependencies for route handlers.
5
+ """
6
+
7
+ from typing import Annotated
8
+
9
+ from fastapi import Depends
10
+ from fastapi import Header
11
+ from fastapi import HTTPException
12
+ from fastapi import Request
13
+ from fastapi import status
14
+
15
+ from app.config import Settings
16
+ from app.config import get_settings
17
+ from app.ml.model_loader import ModelLoader
18
+ from app.services.voice_detector import VoiceDetector
19
+ from app.utils.logger import get_logger
20
+
21
+ logger = get_logger(__name__)
22
+
23
+
24
+ def get_model_loader(request: Request) -> ModelLoader:
25
+ """
26
+ Get ModelLoader from app state.
27
+
28
+ Args:
29
+ request: FastAPI request object
30
+
31
+ Returns:
32
+ ModelLoader instance from app state
33
+ """
34
+ if hasattr(request.app.state, "model_loader"):
35
+ return request.app.state.model_loader
36
+ return ModelLoader()
37
+
38
+
39
+ def get_voice_detector(
40
+ model_loader: Annotated[ModelLoader, Depends(get_model_loader)],
41
+ ) -> VoiceDetector:
42
+ """
43
+ Get VoiceDetector instance.
44
+
45
+ Args:
46
+ model_loader: ModelLoader from dependency
47
+
48
+ Returns:
49
+ Configured VoiceDetector instance
50
+ """
51
+ return VoiceDetector(model_loader=model_loader)
52
+
53
+
54
+ async def validate_api_key(
55
+ x_api_key: Annotated[
56
+ str | None,
57
+ Header(
58
+ alias="x-api-key",
59
+ description="API key for authentication",
60
+ ),
61
+ ] = None,
62
+ settings: Annotated[Settings, Depends(get_settings)] = None, # type: ignore
63
+ ) -> str:
64
+ """
65
+ Validate API key from request header.
66
+
67
+ Args:
68
+ x_api_key: API key from x-api-key header
69
+ settings: Application settings
70
+
71
+ Returns:
72
+ Validated API key
73
+
74
+ Raises:
75
+ HTTPException: 401 if API key is missing or invalid
76
+ """
77
+ if settings is None:
78
+ settings = get_settings()
79
+
80
+ if not x_api_key:
81
+ logger.warning("Request without API key")
82
+ raise HTTPException(
83
+ status_code=status.HTTP_401_UNAUTHORIZED,
84
+ detail="API key is required. Provide it in the x-api-key header.",
85
+ headers={"WWW-Authenticate": "ApiKey"},
86
+ )
87
+
88
+ # Get valid API keys
89
+ valid_keys = settings.api_keys_list
90
+
91
+ if not valid_keys:
92
+ logger.error("No API keys configured on server")
93
+ raise HTTPException(
94
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
95
+ detail="Server configuration error",
96
+ )
97
+
98
+ # Constant-time comparison to prevent timing attacks
99
+ key_valid = False
100
+ for valid_key in valid_keys:
101
+ if _constant_time_compare(x_api_key, valid_key):
102
+ key_valid = True
103
+ break
104
+
105
+ if not key_valid:
106
+ logger.warning(
107
+ "Invalid API key attempt",
108
+ key_prefix=x_api_key[:8] + "..." if len(x_api_key) > 8 else "***",
109
+ )
110
+ raise HTTPException(
111
+ status_code=status.HTTP_401_UNAUTHORIZED,
112
+ detail="Invalid API key",
113
+ headers={"WWW-Authenticate": "ApiKey"},
114
+ )
115
+
116
+ return x_api_key
117
+
118
+
119
+ def _constant_time_compare(val1: str, val2: str) -> bool:
120
+ """
121
+ Constant-time string comparison to prevent timing attacks.
122
+
123
+ Args:
124
+ val1: First string
125
+ val2: Second string
126
+
127
+ Returns:
128
+ True if strings are equal
129
+ """
130
+ if len(val1) != len(val2):
131
+ return False
132
+
133
+ result = 0
134
+ for x, y in zip(val1, val2):
135
+ result |= ord(x) ^ ord(y)
136
+
137
+ return result == 0
138
+
139
+
140
+ # Type aliases for cleaner route signatures
141
+ ValidatedApiKey = Annotated[str, Depends(validate_api_key)]
142
+ VoiceDetectorDep = Annotated[VoiceDetector, Depends(get_voice_detector)]
143
+ SettingsDep = Annotated[Settings, Depends(get_settings)]
app/api/middleware/__init__.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Middleware package."""
2
+
3
+ from app.api.middleware.auth import APIKeyMiddleware
4
+ from app.api.middleware.error_handler import setup_exception_handlers
5
+ from app.api.middleware.rate_limiter import get_limiter
6
+ from app.api.middleware.rate_limiter import limiter
7
+
8
+ __all__ = [
9
+ "APIKeyMiddleware",
10
+ "setup_exception_handlers",
11
+ "limiter",
12
+ "get_limiter",
13
+ ]
app/api/middleware/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (507 Bytes). View file
 
app/api/middleware/__pycache__/auth.cpython-312.pyc ADDED
Binary file (3.44 kB). View file
 
app/api/middleware/__pycache__/error_handler.cpython-312.pyc ADDED
Binary file (4.05 kB). View file
 
app/api/middleware/__pycache__/rate_limiter.cpython-312.pyc ADDED
Binary file (2.46 kB). View file
 
app/api/middleware/auth.py ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ API Key authentication middleware.
3
+
4
+ Provides middleware for API key validation.
5
+ """
6
+
7
+ from starlette.middleware.base import BaseHTTPMiddleware
8
+ from starlette.requests import Request
9
+ from starlette.responses import JSONResponse
10
+
11
+ from app.config import get_settings
12
+ from app.utils.logger import get_logger
13
+
14
+ logger = get_logger(__name__)
15
+
16
+
17
+ class APIKeyMiddleware(BaseHTTPMiddleware):
18
+ """
19
+ Middleware for API key authentication.
20
+
21
+ This middleware checks for valid API keys on protected endpoints.
22
+ Public endpoints (health, docs) are excluded from authentication.
23
+ """
24
+
25
+ # Endpoints that don't require authentication
26
+ PUBLIC_PATHS: set[str] = {
27
+ "/api/health",
28
+ "/api/ready",
29
+ "/api/languages",
30
+ "/api/",
31
+ "/docs",
32
+ "/redoc",
33
+ "/openapi.json",
34
+ "/",
35
+ }
36
+
37
+ async def dispatch(self, request: Request, call_next):
38
+ """
39
+ Process request and validate API key for protected endpoints.
40
+
41
+ Args:
42
+ request: Incoming request
43
+ call_next: Next middleware/handler
44
+
45
+ Returns:
46
+ Response from next handler or 401 error
47
+ """
48
+ # Skip authentication for public paths
49
+ path = request.url.path.rstrip("/") or "/"
50
+
51
+ # Check if path is public
52
+ is_public = path in self.PUBLIC_PATHS or any(
53
+ path.startswith(public.rstrip("/")) for public in self.PUBLIC_PATHS if public != "/"
54
+ )
55
+
56
+ if is_public:
57
+ return await call_next(request)
58
+
59
+ # Get API key from header
60
+ api_key = request.headers.get("x-api-key")
61
+
62
+ if not api_key:
63
+ logger.warning(
64
+ "Request without API key",
65
+ path=path,
66
+ method=request.method,
67
+ )
68
+ return JSONResponse(
69
+ status_code=401,
70
+ content={
71
+ "status": "error",
72
+ "message": "API key is required",
73
+ },
74
+ headers={"WWW-Authenticate": "ApiKey"},
75
+ )
76
+
77
+ # Validate API key
78
+ settings = get_settings()
79
+ valid_keys = settings.api_keys_list
80
+
81
+ if not valid_keys:
82
+ logger.error("No API keys configured")
83
+ return JSONResponse(
84
+ status_code=500,
85
+ content={
86
+ "status": "error",
87
+ "message": "Server configuration error",
88
+ },
89
+ )
90
+
91
+ if api_key not in valid_keys:
92
+ logger.warning(
93
+ "Invalid API key",
94
+ path=path,
95
+ key_prefix=api_key[:8] + "..." if len(api_key) > 8 else "***",
96
+ )
97
+ return JSONResponse(
98
+ status_code=401,
99
+ content={
100
+ "status": "error",
101
+ "message": "Invalid API key",
102
+ },
103
+ headers={"WWW-Authenticate": "ApiKey"},
104
+ )
105
+
106
+ # Continue to next handler
107
+ return await call_next(request)
app/api/middleware/error_handler.py ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Global error handling for FastAPI application.
3
+
4
+ Provides exception handlers for consistent error responses.
5
+ """
6
+
7
+ from fastapi import FastAPI
8
+ from fastapi import HTTPException
9
+ from fastapi import Request
10
+ from fastapi.exceptions import RequestValidationError
11
+ from starlette.responses import JSONResponse
12
+
13
+ from app.utils.logger import get_logger
14
+
15
+ logger = get_logger(__name__)
16
+
17
+
18
+ async def validation_exception_handler(
19
+ request: Request,
20
+ exc: RequestValidationError,
21
+ ) -> JSONResponse:
22
+ """
23
+ Handle Pydantic validation errors.
24
+
25
+ Formats validation errors into a consistent response format.
26
+
27
+ Args:
28
+ request: Incoming request
29
+ exc: Validation exception
30
+
31
+ Returns:
32
+ JSON response with error details
33
+ """
34
+ errors = exc.errors()
35
+
36
+ # Format error messages
37
+ error_messages = []
38
+ for error in errors:
39
+ loc = " -> ".join(str(x) for x in error.get("loc", []))
40
+ msg = error.get("msg", "Validation error")
41
+ error_messages.append(f"{loc}: {msg}")
42
+
43
+ logger.warning(
44
+ "Validation error",
45
+ path=request.url.path,
46
+ errors=error_messages,
47
+ )
48
+
49
+ return JSONResponse(
50
+ status_code=422,
51
+ content={
52
+ "status": "error",
53
+ "message": "Validation error",
54
+ "details": {
55
+ "errors": error_messages,
56
+ },
57
+ },
58
+ )
59
+
60
+
61
+ async def http_exception_handler(
62
+ request: Request,
63
+ exc: HTTPException,
64
+ ) -> JSONResponse:
65
+ """
66
+ Handle HTTP exceptions.
67
+
68
+ Formats HTTP exceptions into a consistent response format.
69
+
70
+ Args:
71
+ request: Incoming request
72
+ exc: HTTP exception
73
+
74
+ Returns:
75
+ JSON response with error details
76
+ """
77
+ return JSONResponse(
78
+ status_code=exc.status_code,
79
+ content={
80
+ "status": "error",
81
+ "message": exc.detail,
82
+ },
83
+ headers=exc.headers,
84
+ )
85
+
86
+
87
+ async def general_exception_handler(
88
+ request: Request,
89
+ exc: Exception,
90
+ ) -> JSONResponse:
91
+ """
92
+ Handle unexpected exceptions.
93
+
94
+ Logs the exception and returns a generic error response.
95
+
96
+ Args:
97
+ request: Incoming request
98
+ exc: Unexpected exception
99
+
100
+ Returns:
101
+ JSON response with generic error message
102
+ """
103
+ logger.exception(
104
+ "Unhandled exception",
105
+ path=request.url.path,
106
+ method=request.method,
107
+ error=str(exc),
108
+ )
109
+
110
+ return JSONResponse(
111
+ status_code=500,
112
+ content={
113
+ "status": "error",
114
+ "message": "Internal server error",
115
+ },
116
+ )
117
+
118
+
119
+ def setup_exception_handlers(app: FastAPI) -> None:
120
+ """
121
+ Register all exception handlers with the FastAPI app.
122
+
123
+ Args:
124
+ app: FastAPI application instance
125
+ """
126
+ app.add_exception_handler(RequestValidationError, validation_exception_handler)
127
+ app.add_exception_handler(HTTPException, http_exception_handler)
128
+ app.add_exception_handler(Exception, general_exception_handler)
129
+
130
+ logger.debug("Exception handlers registered")
app/api/middleware/rate_limiter.py ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Rate limiting middleware using SlowAPI.
3
+
4
+ Provides request rate limiting per API key.
5
+ """
6
+
7
+ from slowapi import Limiter
8
+ from slowapi.util import get_remote_address
9
+ from starlette.requests import Request
10
+
11
+ from app.config import get_settings
12
+ from app.utils.logger import get_logger
13
+
14
+ logger = get_logger(__name__)
15
+
16
+
17
+ def get_api_key_or_ip(request: Request) -> str:
18
+ """
19
+ Extract rate limit key from request.
20
+
21
+ Uses API key if present, otherwise falls back to IP address.
22
+
23
+ Args:
24
+ request: Incoming request
25
+
26
+ Returns:
27
+ Rate limit key (API key or IP)
28
+ """
29
+ api_key = request.headers.get("x-api-key")
30
+
31
+ if api_key:
32
+ # Use API key for per-key rate limiting
33
+ return f"key:{api_key}"
34
+
35
+ # Fall back to IP address
36
+ return f"ip:{get_remote_address(request)}"
37
+
38
+
39
+ def get_limiter() -> Limiter:
40
+ """
41
+ Create and configure rate limiter.
42
+
43
+ Returns:
44
+ Configured Limiter instance
45
+ """
46
+ settings = get_settings()
47
+
48
+ # Build default limit string
49
+ default_limit = f"{settings.RATE_LIMIT_REQUESTS}/minute"
50
+
51
+ return Limiter(
52
+ key_func=get_api_key_or_ip,
53
+ default_limits=[default_limit],
54
+ # Note: Redis storage will be configured in main.py if available
55
+ )
56
+
57
+
58
+ # Global limiter instance
59
+ limiter = get_limiter()
60
+
61
+
62
+ def rate_limit_exceeded_handler(request: Request, exc: Exception):
63
+ """
64
+ Handle rate limit exceeded errors.
65
+
66
+ Args:
67
+ request: Request that exceeded the limit
68
+ exc: Rate limit exception
69
+
70
+ Returns:
71
+ JSON response with 429 status
72
+ """
73
+ from starlette.responses import JSONResponse
74
+
75
+ logger.warning(
76
+ "Rate limit exceeded",
77
+ path=request.url.path,
78
+ client=get_api_key_or_ip(request),
79
+ )
80
+
81
+ return JSONResponse(
82
+ status_code=429,
83
+ content={
84
+ "status": "error",
85
+ "message": "Rate limit exceeded. Please try again later.",
86
+ },
87
+ headers={
88
+ "Retry-After": "60",
89
+ },
90
+ )
app/api/routes/__init__.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ """Routes package."""
2
+
3
+ from app.api.routes import health
4
+ from app.api.routes import voice_detection
5
+
6
+ __all__ = [
7
+ "health",
8
+ "voice_detection",
9
+ ]
app/api/routes/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (334 Bytes). View file
 
app/api/routes/__pycache__/federated.cpython-312.pyc ADDED
Binary file (4 kB). View file
 
app/api/routes/__pycache__/health.cpython-312.pyc ADDED
Binary file (3.18 kB). View file
 
app/api/routes/__pycache__/voice_detection.cpython-312.pyc ADDED
Binary file (5.12 kB). View file
 
app/api/routes/federated.py ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Federated Learning API endpoints.
3
+
4
+ Provides endpoints for FL operations:
5
+ - Client registration
6
+ - Contribution submission
7
+ - Federation status
8
+ """
9
+
10
+ from fastapi import APIRouter
11
+ from fastapi import HTTPException
12
+ from fastapi import status
13
+ from pydantic import BaseModel
14
+ from pydantic import Field
15
+
16
+ from app.api.dependencies import ValidatedApiKey
17
+ from app.services.federated_learning import fl_manager
18
+
19
+ router = APIRouter()
20
+
21
+
22
+ class ClientRegistrationRequest(BaseModel):
23
+ """Request to register as FL client."""
24
+
25
+ client_id: str = Field(..., min_length=3, max_length=64)
26
+ organization: str | None = Field(None, max_length=128)
27
+
28
+
29
+ class ContributionRequest(BaseModel):
30
+ """Request to submit a training contribution."""
31
+
32
+ client_id: str = Field(..., min_length=3, max_length=64)
33
+ gradient_hash: str = Field(..., min_length=16, max_length=128)
34
+ samples_trained: int = Field(..., ge=1, le=100000)
35
+ local_accuracy: float = Field(..., ge=0.0, le=1.0)
36
+
37
+
38
+ @router.post(
39
+ "/federated/register",
40
+ summary="Register as Federated Client",
41
+ description="Register as a federated learning participant.",
42
+ )
43
+ async def register_client(
44
+ request: ClientRegistrationRequest,
45
+ api_key: ValidatedApiKey,
46
+ ) -> dict:
47
+ """Register a new federated learning client."""
48
+ client = fl_manager.register_client(
49
+ client_id=request.client_id,
50
+ organization=request.organization,
51
+ )
52
+
53
+ return {
54
+ "status": "registered",
55
+ "client_id": client.client_id,
56
+ "organization": client.organization,
57
+ "registered_at": client.registered_at,
58
+ }
59
+
60
+
61
+ @router.post(
62
+ "/federated/contribute",
63
+ summary="Submit Training Contribution",
64
+ description="Submit model gradients from local training.",
65
+ )
66
+ async def submit_contribution(
67
+ request: ContributionRequest,
68
+ api_key: ValidatedApiKey,
69
+ ) -> dict:
70
+ """Submit a training contribution."""
71
+ result = fl_manager.submit_contribution(
72
+ client_id=request.client_id,
73
+ gradient_hash=request.gradient_hash,
74
+ samples_trained=request.samples_trained,
75
+ local_accuracy=request.local_accuracy,
76
+ )
77
+
78
+ return result
79
+
80
+
81
+ @router.get(
82
+ "/federated/status",
83
+ summary="Federation Status",
84
+ description="Get current federated learning status.",
85
+ )
86
+ async def federation_status() -> dict:
87
+ """Get federation status."""
88
+ return fl_manager.get_federation_status()
89
+
90
+
91
+ @router.post(
92
+ "/federated/aggregate",
93
+ summary="Trigger Aggregation",
94
+ description="Trigger federated model aggregation (admin only).",
95
+ )
96
+ async def trigger_aggregation(
97
+ api_key: ValidatedApiKey,
98
+ ) -> dict:
99
+ """Trigger model aggregation."""
100
+ return fl_manager.simulate_aggregation()
app/api/routes/health.py ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Health check endpoints.
3
+
4
+ Provides health, readiness, and information endpoints.
5
+ """
6
+
7
+ from fastapi import APIRouter
8
+
9
+ from app.api.dependencies import VoiceDetectorDep
10
+ from app.models.enums import SupportedLanguage
11
+ from app.models.response import HealthResponse
12
+ from app.models.response import LanguagesResponse
13
+
14
+ router = APIRouter()
15
+
16
+
17
+ @router.get(
18
+ "/health",
19
+ response_model=HealthResponse,
20
+ summary="Health Check",
21
+ description="Check the health status of the API and ML model.",
22
+ )
23
+ async def health_check(
24
+ voice_detector: VoiceDetectorDep,
25
+ ) -> HealthResponse:
26
+ """
27
+ Get health status of the API.
28
+
29
+ Returns model loading status, device info, and supported languages.
30
+ """
31
+ health = voice_detector.health_check()
32
+
33
+ return HealthResponse(
34
+ status=health["status"],
35
+ version=health["version"],
36
+ model_loaded=health["model_loaded"],
37
+ model_name=health.get("model_name"),
38
+ device=health.get("device"),
39
+ supported_languages=health["supported_languages"],
40
+ )
41
+
42
+
43
+ @router.get(
44
+ "/ready",
45
+ summary="Readiness Check",
46
+ description="Check if the API is ready to accept requests.",
47
+ )
48
+ async def readiness_check(
49
+ voice_detector: VoiceDetectorDep,
50
+ ) -> dict:
51
+ """
52
+ Check if API is ready to accept requests.
53
+
54
+ Returns ready status based on model availability.
55
+ """
56
+ health = voice_detector.health_check()
57
+
58
+ if health["model_loaded"]:
59
+ return {"status": "ready", "message": "API is ready to accept requests"}
60
+ else:
61
+ return {"status": "not_ready", "message": "Model is still loading"}
62
+
63
+
64
+ @router.get(
65
+ "/languages",
66
+ response_model=LanguagesResponse,
67
+ summary="Supported Languages",
68
+ description="Get the list of supported languages for voice detection.",
69
+ )
70
+ async def supported_languages() -> LanguagesResponse:
71
+ """
72
+ Get list of supported languages.
73
+
74
+ Returns all languages supported by the voice detection API.
75
+ """
76
+ languages = SupportedLanguage.values()
77
+
78
+ return LanguagesResponse(
79
+ languages=languages,
80
+ count=len(languages),
81
+ )
82
+
83
+
84
+ @router.get(
85
+ "/",
86
+ summary="API Info",
87
+ description="Get basic API information.",
88
+ )
89
+ async def api_info() -> dict:
90
+ """
91
+ Get basic API information.
92
+
93
+ Returns API name, version, and documentation links.
94
+ """
95
+ return {
96
+ "name": "VoiceAuth API",
97
+ "description": "AI-Generated Voice Detection API",
98
+ "version": "1.0.0",
99
+ "documentation": "/docs",
100
+ "supported_languages": SupportedLanguage.values(),
101
+ }
app/api/routes/voice_detection.py ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Voice detection API endpoint.
3
+
4
+ Main endpoint for detecting AI-generated vs human voice.
5
+ """
6
+
7
+ from fastapi import APIRouter
8
+ from fastapi import HTTPException
9
+ from fastapi import status
10
+
11
+ from app.api.dependencies import ValidatedApiKey
12
+ from app.api.dependencies import VoiceDetectorDep
13
+ from app.models.request import VoiceDetectionRequest
14
+ from app.models.response import ErrorResponse
15
+ from app.models.response import VoiceDetectionResponse
16
+ from app.utils.exceptions import AudioDecodeError
17
+ from app.utils.exceptions import AudioDurationError
18
+ from app.utils.exceptions import AudioFormatError
19
+ from app.utils.exceptions import AudioProcessingError
20
+ from app.utils.exceptions import InferenceError
21
+ from app.utils.exceptions import ModelNotLoadedError
22
+ from app.utils.logger import get_logger
23
+
24
+ logger = get_logger(__name__)
25
+
26
+ router = APIRouter()
27
+
28
+
29
+ @router.post(
30
+ "/voice-detection",
31
+ response_model=VoiceDetectionResponse,
32
+ response_model_include={"status", "language", "classification", "confidenceScore", "explanation"},
33
+ responses={
34
+ 200: {
35
+ "description": "Successful voice detection",
36
+ "model": VoiceDetectionResponse,
37
+ },
38
+ 400: {
39
+ "description": "Invalid audio data",
40
+ "model": ErrorResponse,
41
+ },
42
+ 401: {
43
+ "description": "Invalid or missing API key",
44
+ "model": ErrorResponse,
45
+ },
46
+ 422: {
47
+ "description": "Validation error",
48
+ "model": ErrorResponse,
49
+ },
50
+ 429: {
51
+ "description": "Rate limit exceeded",
52
+ "model": ErrorResponse,
53
+ },
54
+ 500: {
55
+ "description": "Internal server error",
56
+ "model": ErrorResponse,
57
+ },
58
+ 503: {
59
+ "description": "Model not loaded",
60
+ "model": ErrorResponse,
61
+ },
62
+ },
63
+ summary="Detect AI-Generated Voice",
64
+ description="""
65
+ Analyze a voice sample to determine if it's AI-generated or spoken by a human.
66
+
67
+ **Supported Languages:** Tamil, English, Hindi, Malayalam, Telugu
68
+
69
+ **Input Requirements:**
70
+ - Audio must be Base64-encoded MP3
71
+ - Duration: 0.5s to 30s
72
+ - One audio sample per request
73
+
74
+ **Response:**
75
+ - Classification: AI_GENERATED or HUMAN
76
+ - Confidence score: 0.0 to 1.0
77
+ - Human-readable explanation
78
+ """,
79
+ )
80
+ async def detect_voice(
81
+ request: VoiceDetectionRequest,
82
+ voice_detector: VoiceDetectorDep,
83
+ api_key: ValidatedApiKey,
84
+ ) -> VoiceDetectionResponse:
85
+ """
86
+ Detect whether a voice sample is AI-generated or human.
87
+
88
+ Args:
89
+ request: Voice detection request with audio data
90
+ voice_detector: VoiceDetector service dependency
91
+ api_key: Validated API key from header
92
+
93
+ Returns:
94
+ VoiceDetectionResponse with classification result
95
+ """
96
+ try:
97
+ result = await voice_detector.detect(
98
+ audio_base64=request.audioBase64,
99
+ language=request.language,
100
+ )
101
+ return result
102
+
103
+ except AudioDecodeError as e:
104
+ logger.warning("Audio decode error", error=str(e))
105
+ raise HTTPException(
106
+ status_code=status.HTTP_400_BAD_REQUEST,
107
+ detail=f"Failed to decode audio: {e.message}",
108
+ ) from e
109
+
110
+ except AudioFormatError as e:
111
+ logger.warning("Audio format error", error=str(e))
112
+ raise HTTPException(
113
+ status_code=status.HTTP_400_BAD_REQUEST,
114
+ detail=f"Invalid audio format: {e.message}",
115
+ ) from e
116
+
117
+ except AudioDurationError as e:
118
+ logger.warning("Audio duration error", error=str(e), details=e.details)
119
+ raise HTTPException(
120
+ status_code=status.HTTP_400_BAD_REQUEST,
121
+ detail=e.message,
122
+ ) from e
123
+
124
+ except AudioProcessingError as e:
125
+ logger.error("Audio processing error", error=str(e))
126
+ raise HTTPException(
127
+ status_code=status.HTTP_400_BAD_REQUEST,
128
+ detail=f"Audio processing failed: {e.message}",
129
+ ) from e
130
+
131
+ except ModelNotLoadedError as e:
132
+ logger.error("Model not loaded", error=str(e))
133
+ raise HTTPException(
134
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
135
+ detail="Voice detection model is not available. Please try again later.",
136
+ ) from e
137
+
138
+ except InferenceError as e:
139
+ logger.error("Inference error", error=str(e))
140
+ raise HTTPException(
141
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
142
+ detail="Voice analysis failed. Please try again.",
143
+ ) from e
144
+
145
+ except Exception as e:
146
+ logger.exception("Unexpected error in voice detection")
147
+ raise HTTPException(
148
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
149
+ detail="Internal server error",
150
+ ) from e
app/config.py ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Application configuration using Pydantic Settings.
3
+
4
+ Loads configuration from environment variables and .env file.
5
+ """
6
+
7
+ from functools import lru_cache
8
+ from typing import Literal
9
+
10
+ import torch
11
+ from pydantic import Field
12
+ from pydantic import field_validator
13
+ from pydantic_settings import BaseSettings
14
+ from pydantic_settings import SettingsConfigDict
15
+
16
+
17
+ class Settings(BaseSettings):
18
+ """Application settings loaded from environment variables."""
19
+
20
+ model_config = SettingsConfigDict(
21
+ env_file=".env",
22
+ env_file_encoding="utf-8",
23
+ case_sensitive=False,
24
+ extra="ignore",
25
+ )
26
+
27
+ # -------------------------------------------------------------------------
28
+ # Application Settings
29
+ # -------------------------------------------------------------------------
30
+ APP_NAME: str = "VoiceAuth API"
31
+ APP_VERSION: str = "1.0.0"
32
+ DEBUG: bool = False
33
+ HOST: str = "0.0.0.0"
34
+ PORT: int = 8000
35
+
36
+ # -------------------------------------------------------------------------
37
+ # Security Settings
38
+ # -------------------------------------------------------------------------
39
+ API_KEYS: str = Field(
40
+ default="",
41
+ description="Comma-separated list of valid API keys",
42
+ )
43
+ CORS_ORIGINS: str = Field(
44
+ default="http://localhost:3000,http://localhost:8000",
45
+ description="Comma-separated list of allowed CORS origins",
46
+ )
47
+ RATE_LIMIT_REQUESTS: int = Field(default=100, ge=1)
48
+ RATE_LIMIT_PERIOD: int = Field(default=60, ge=1, description="Period in seconds")
49
+
50
+ # -------------------------------------------------------------------------
51
+ # ML Model Settings
52
+ # -------------------------------------------------------------------------
53
+ MODEL_NAME: str = "facebook/wav2vec2-base"
54
+ MODEL_PATH: str = ""
55
+ DEVICE: str = "auto"
56
+ MAX_AUDIO_DURATION: float = Field(default=30.0, ge=1.0)
57
+ MIN_AUDIO_DURATION: float = Field(default=0.5, ge=0.1)
58
+ SAMPLE_RATE: int = Field(default=16000, ge=8000, le=48000)
59
+
60
+ # -------------------------------------------------------------------------
61
+ # Redis Settings
62
+ # -------------------------------------------------------------------------
63
+ REDIS_URL: str = "redis://localhost:6379"
64
+ REDIS_DB: int = 0
65
+
66
+ # -------------------------------------------------------------------------
67
+ # Logging Settings
68
+ # -------------------------------------------------------------------------
69
+ LOG_LEVEL: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO"
70
+ LOG_FORMAT: Literal["json", "console"] = "json"
71
+
72
+ # -------------------------------------------------------------------------
73
+ # Computed Properties
74
+ # -------------------------------------------------------------------------
75
+ @property
76
+ def api_keys_list(self) -> list[str]:
77
+ """Parse comma-separated API keys into a list."""
78
+ if not self.API_KEYS:
79
+ return []
80
+ return [key.strip() for key in self.API_KEYS.split(",") if key.strip()]
81
+
82
+ @property
83
+ def cors_origins_list(self) -> list[str]:
84
+ """Parse comma-separated CORS origins into a list."""
85
+ if not self.CORS_ORIGINS:
86
+ return []
87
+ return [origin.strip() for origin in self.CORS_ORIGINS.split(",") if origin.strip()]
88
+
89
+ @property
90
+ def torch_device(self) -> str:
91
+ """Determine the appropriate torch device."""
92
+ if self.DEVICE == "auto":
93
+ return "cuda" if torch.cuda.is_available() else "cpu"
94
+ return self.DEVICE
95
+
96
+ @property
97
+ def model_identifier(self) -> str:
98
+ """Get the model path or name to load."""
99
+ return self.MODEL_PATH if self.MODEL_PATH else self.MODEL_NAME
100
+
101
+ # -------------------------------------------------------------------------
102
+ # Validators
103
+ # -------------------------------------------------------------------------
104
+ @field_validator("DEVICE")
105
+ @classmethod
106
+ def validate_device(cls, v: str) -> str:
107
+ """Validate device configuration."""
108
+ valid_devices = {"auto", "cpu", "cuda", "mps"}
109
+ # Allow cuda:N format
110
+ if v.startswith("cuda:"):
111
+ return v
112
+ if v not in valid_devices:
113
+ raise ValueError(f"Device must be one of {valid_devices} or 'cuda:N' format")
114
+ return v
115
+
116
+
117
+ @lru_cache
118
+ def get_settings() -> Settings:
119
+ """
120
+ Get cached settings instance.
121
+
122
+ Uses lru_cache to ensure settings are only loaded once.
123
+ """
124
+ return Settings()
app/main.py ADDED
@@ -0,0 +1,182 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ VoiceAuth API - Main Application Entry Point.
3
+
4
+ FastAPI application for AI-generated voice detection.
5
+ """
6
+
7
+ from contextlib import asynccontextmanager
8
+
9
+ from fastapi import FastAPI
10
+ from fastapi.middleware.cors import CORSMiddleware
11
+ from slowapi import _rate_limit_exceeded_handler
12
+ from slowapi.errors import RateLimitExceeded
13
+
14
+ from app.api.middleware.error_handler import setup_exception_handlers
15
+ from app.api.middleware.rate_limiter import limiter
16
+ from app.api.routes import health
17
+ from app.api.routes import voice_detection
18
+ from app.config import get_settings
19
+ from app.ml.model_loader import ModelLoader
20
+ from app.utils.logger import get_logger
21
+ from app.utils.logger import setup_logging
22
+
23
+ # Initialize logging first
24
+ setup_logging()
25
+ logger = get_logger(__name__)
26
+
27
+
28
+ @asynccontextmanager
29
+ async def lifespan(app: FastAPI):
30
+ """
31
+ Application lifespan manager.
32
+
33
+ Handles startup and shutdown events:
34
+ - Startup: Load ML model, run warmup
35
+ - Shutdown: Unload model, cleanup
36
+ """
37
+ # =========================================================================
38
+ # STARTUP
39
+ # =========================================================================
40
+ logger.info("Starting VoiceAuth API...")
41
+
42
+ # Initialize model loader
43
+ model_loader = ModelLoader()
44
+ app.state.model_loader = model_loader
45
+
46
+ # Load ML model
47
+ logger.info("Loading ML model...")
48
+ try:
49
+ await model_loader.load_model_async()
50
+ logger.info("ML model loaded successfully")
51
+
52
+ # Run warmup inference
53
+ logger.info("Running model warmup...")
54
+ model_loader.warmup()
55
+ logger.info("Model warmup complete")
56
+
57
+ except Exception as e:
58
+ logger.error("Failed to load ML model", error=str(e))
59
+ # Continue without model for health checks
60
+ # Actual detection will fail with proper error
61
+
62
+ logger.info("VoiceAuth API is ready!")
63
+
64
+ yield
65
+
66
+ # =========================================================================
67
+ # SHUTDOWN
68
+ # =========================================================================
69
+ logger.info("Shutting down VoiceAuth API...")
70
+
71
+ # Unload model
72
+ if hasattr(app.state, "model_loader"):
73
+ app.state.model_loader.unload_model()
74
+
75
+ logger.info("Shutdown complete")
76
+
77
+
78
+ def create_app() -> FastAPI:
79
+ """
80
+ Create and configure the FastAPI application.
81
+
82
+ Returns:
83
+ Configured FastAPI application
84
+ """
85
+ settings = get_settings()
86
+
87
+ # Create FastAPI app
88
+ app = FastAPI(
89
+ title=settings.APP_NAME,
90
+ version=settings.APP_VERSION,
91
+ description="""
92
+ # VoiceAuth - AI Voice Detection API
93
+
94
+ Detect whether a voice sample is **AI-generated** or **human-spoken** across 5 languages.
95
+
96
+ ## Supported Languages
97
+ - Tamil
98
+ - English
99
+ - Hindi
100
+ - Malayalam
101
+ - Telugu
102
+
103
+ ## Authentication
104
+ All detection requests require an API key in the `x-api-key` header.
105
+
106
+ ## Rate Limiting
107
+ Default: 100 requests per minute per API key.
108
+ """,
109
+ lifespan=lifespan,
110
+ docs_url="/docs",
111
+ redoc_url="/redoc",
112
+ openapi_url="/openapi.json",
113
+ )
114
+
115
+ # =========================================================================
116
+ # MIDDLEWARE
117
+ # =========================================================================
118
+
119
+ # CORS
120
+ app.add_middleware(
121
+ CORSMiddleware,
122
+ allow_origins=settings.cors_origins_list or ["*"],
123
+ allow_credentials=True,
124
+ allow_methods=["*"],
125
+ allow_headers=["*"],
126
+ )
127
+
128
+ # Rate limiting
129
+ app.state.limiter = limiter
130
+ app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
131
+
132
+ # =========================================================================
133
+ # EXCEPTION HANDLERS
134
+ # =========================================================================
135
+ setup_exception_handlers(app)
136
+
137
+ # =========================================================================
138
+ # ROUTES
139
+ # =========================================================================
140
+ app.include_router(
141
+ health.router,
142
+ prefix="/api",
143
+ tags=["Health"],
144
+ )
145
+ app.include_router(
146
+ voice_detection.router,
147
+ prefix="/api",
148
+ tags=["Voice Detection"],
149
+ )
150
+
151
+ # Federated Learning routes (Phase 2)
152
+ from app.api.routes import federated
153
+ app.include_router(
154
+ federated.router,
155
+ prefix="/api",
156
+ tags=["Federated Learning"],
157
+ )
158
+
159
+ return app
160
+
161
+
162
+ # Create application instance
163
+ app = create_app()
164
+
165
+
166
+ def main() -> None:
167
+ """Run the application using uvicorn."""
168
+ import uvicorn
169
+
170
+ settings = get_settings()
171
+
172
+ uvicorn.run(
173
+ "app.main:app",
174
+ host=settings.HOST,
175
+ port=settings.PORT,
176
+ reload=settings.DEBUG,
177
+ log_level=settings.LOG_LEVEL.lower(),
178
+ )
179
+
180
+
181
+ if __name__ == "__main__":
182
+ main()
app/ml/__init__.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Machine Learning pipeline package."""
2
+
3
+ from app.ml.inference import InferenceEngine
4
+ from app.ml.model_loader import ModelLoader
5
+ from app.ml.preprocessing import AudioPreprocessor
6
+
7
+ __all__ = [
8
+ "ModelLoader",
9
+ "InferenceEngine",
10
+ "AudioPreprocessor",
11
+ ]
app/ml/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (435 Bytes). View file
 
app/ml/__pycache__/inference.cpython-312.pyc ADDED
Binary file (9.4 kB). View file
 
app/ml/__pycache__/model_loader.cpython-312.pyc ADDED
Binary file (9.92 kB). View file
 
app/ml/__pycache__/preprocessing.cpython-312.pyc ADDED
Binary file (5.69 kB). View file
 
app/ml/inference.py ADDED
@@ -0,0 +1,235 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Inference engine for voice classification.
3
+
4
+ Handles model inference and result processing.
5
+ """
6
+
7
+ import time
8
+ from typing import TYPE_CHECKING
9
+
10
+ import torch
11
+ import torch.nn.functional as F
12
+
13
+ from app.models.enums import Classification
14
+ from app.utils.constants import ID_TO_LABEL
15
+ from app.utils.exceptions import InferenceError
16
+ from app.utils.logger import get_logger
17
+
18
+ if TYPE_CHECKING:
19
+ from transformers import Wav2Vec2ForSequenceClassification
20
+ from transformers import Wav2Vec2Processor
21
+
22
+ logger = get_logger(__name__)
23
+
24
+
25
+ class InferenceEngine:
26
+ """
27
+ Inference engine for Wav2Vec2 voice classification.
28
+
29
+ Handles running model inference and converting outputs
30
+ to classification results.
31
+ """
32
+
33
+ def __init__(
34
+ self,
35
+ model: "Wav2Vec2ForSequenceClassification",
36
+ processor: "Wav2Vec2Processor",
37
+ device: str = "cpu",
38
+ ) -> None:
39
+ """
40
+ Initialize InferenceEngine.
41
+
42
+ Args:
43
+ model: Loaded Wav2Vec2ForSequenceClassification model
44
+ processor: Wav2Vec2Processor for preprocessing
45
+ device: Device to run inference on
46
+ """
47
+ self.model = model
48
+ self.processor = processor
49
+ self.device = device
50
+
51
+ def predict(
52
+ self,
53
+ input_tensors: dict[str, torch.Tensor],
54
+ ) -> tuple[Classification, float]:
55
+ """
56
+ Run inference and return classification result.
57
+
58
+ Args:
59
+ input_tensors: Preprocessed input tensors with input_values
60
+
61
+ Returns:
62
+ Tuple of (Classification, confidence_score)
63
+
64
+ Raises:
65
+ InferenceError: If inference fails
66
+ """
67
+ try:
68
+ start_time = time.perf_counter()
69
+
70
+ # Ensure model is in eval mode
71
+ self.model.eval()
72
+
73
+ # Run inference without gradient computation
74
+ with torch.no_grad():
75
+ outputs = self.model(**input_tensors)
76
+
77
+ # Get logits
78
+ logits = outputs.logits
79
+
80
+ # Apply softmax to get probabilities
81
+ probabilities = F.softmax(logits, dim=-1)
82
+
83
+ # Get predicted class and confidence
84
+ confidence, predicted_class = torch.max(probabilities, dim=-1)
85
+
86
+ # Convert to Python types
87
+ predicted_class_id = predicted_class.item()
88
+ confidence_score = confidence.item()
89
+
90
+ # Get label from model's config or fallback to our mapping
91
+ if hasattr(self.model.config, 'id2label') and self.model.config.id2label:
92
+ model_label = self.model.config.id2label.get(predicted_class_id, "HUMAN")
93
+ # Convert pretrained model labels to standard format
94
+ model_label_lower = str(model_label).lower()
95
+
96
+ ai_keywords = ["fake", "spoof", "synthetic", "ai", "deepfake", "generated"]
97
+ is_ai = any(keyword in model_label_lower for keyword in ai_keywords)
98
+
99
+ if is_ai:
100
+ label = "AI_GENERATED"
101
+ else:
102
+ label = "HUMAN"
103
+ else:
104
+ label = ID_TO_LABEL.get(predicted_class_id, "HUMAN")
105
+
106
+ classification = Classification(label)
107
+
108
+ inference_time_ms = (time.perf_counter() - start_time) * 1000
109
+
110
+ logger.info(
111
+ "Inference complete",
112
+ classification=classification.value,
113
+ confidence=round(confidence_score, 4),
114
+ inference_time_ms=round(inference_time_ms, 2),
115
+ )
116
+
117
+ return classification, confidence_score
118
+
119
+ except Exception as e:
120
+ logger.error("Inference failed", error=str(e))
121
+ raise InferenceError(
122
+ f"Model inference failed: {e}",
123
+ details={"error": str(e)},
124
+ ) from e
125
+
126
+ def predict_with_probabilities(
127
+ self,
128
+ input_tensors: dict[str, torch.Tensor],
129
+ ) -> dict:
130
+ """
131
+ Run inference and return full probability distribution.
132
+
133
+ Args:
134
+ input_tensors: Preprocessed input tensors
135
+
136
+ Returns:
137
+ Dictionary with classification, confidence, and all probabilities
138
+ """
139
+ try:
140
+ self.model.eval()
141
+
142
+ with torch.no_grad():
143
+ outputs = self.model(**input_tensors)
144
+
145
+ logits = outputs.logits
146
+ probabilities = F.softmax(logits, dim=-1)
147
+
148
+ # Get all probabilities
149
+ probs = probabilities.squeeze().cpu().numpy()
150
+
151
+ # Get predicted class
152
+ confidence, predicted_class = torch.max(probabilities, dim=-1)
153
+ predicted_class_id = predicted_class.item()
154
+ label = ID_TO_LABEL.get(predicted_class_id, "HUMAN")
155
+
156
+ return {
157
+ "classification": Classification(label),
158
+ "confidence": float(confidence.item()),
159
+ "probabilities": {
160
+ "HUMAN": float(probs[0]) if len(probs) > 0 else 0.0,
161
+ "AI_GENERATED": float(probs[1]) if len(probs) > 1 else 0.0,
162
+ },
163
+ }
164
+
165
+ except Exception as e:
166
+ logger.error("Inference with probabilities failed", error=str(e))
167
+ raise InferenceError(
168
+ f"Model inference failed: {e}",
169
+ details={"error": str(e)},
170
+ ) from e
171
+
172
+ def batch_predict(
173
+ self,
174
+ input_tensors: dict[str, torch.Tensor],
175
+ ) -> list[tuple[Classification, float]]:
176
+ """
177
+ Run batch inference.
178
+
179
+ Args:
180
+ input_tensors: Batched preprocessed input tensors
181
+
182
+ Returns:
183
+ List of (Classification, confidence) tuples
184
+ """
185
+ try:
186
+ self.model.eval()
187
+
188
+ with torch.no_grad():
189
+ outputs = self.model(**input_tensors)
190
+
191
+ logits = outputs.logits
192
+ probabilities = F.softmax(logits, dim=-1)
193
+
194
+ results = []
195
+ for i in range(probabilities.shape[0]):
196
+ confidence, predicted_class = torch.max(probabilities[i], dim=-1)
197
+ predicted_class_id = predicted_class.item()
198
+ label = ID_TO_LABEL.get(predicted_class_id, "HUMAN")
199
+ results.append((Classification(label), float(confidence.item())))
200
+
201
+ return results
202
+
203
+ except Exception as e:
204
+ logger.error("Batch inference failed", error=str(e))
205
+ raise InferenceError(
206
+ f"Batch inference failed: {e}",
207
+ details={"error": str(e)},
208
+ ) from e
209
+
210
+ def get_hidden_states(
211
+ self,
212
+ input_tensors: dict[str, torch.Tensor],
213
+ ) -> torch.Tensor:
214
+ """
215
+ Extract hidden states for explainability.
216
+
217
+ Args:
218
+ input_tensors: Preprocessed input tensors
219
+
220
+ Returns:
221
+ Hidden state tensor from last layer
222
+ """
223
+ self.model.eval()
224
+
225
+ with torch.no_grad():
226
+ outputs = self.model(
227
+ **input_tensors,
228
+ output_hidden_states=True,
229
+ )
230
+
231
+ # Return last hidden state
232
+ if hasattr(outputs, "hidden_states") and outputs.hidden_states:
233
+ return outputs.hidden_states[-1]
234
+
235
+ return torch.tensor([])
app/ml/model_loader.py ADDED
@@ -0,0 +1,246 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Model loader for Wav2Vec2 voice classification model.
3
+
4
+ Handles loading, caching, and management of the ML model.
5
+ """
6
+
7
+ import gc
8
+ import threading
9
+ from typing import TYPE_CHECKING
10
+
11
+ import torch
12
+ from transformers import Wav2Vec2ForSequenceClassification
13
+ from transformers import Wav2Vec2Processor
14
+
15
+ from app.config import get_settings
16
+ from app.utils.constants import ID_TO_LABEL
17
+ from app.utils.constants import LABEL_TO_ID
18
+ from app.utils.exceptions import ModelNotLoadedError
19
+ from app.utils.logger import get_logger
20
+
21
+ if TYPE_CHECKING:
22
+ from transformers import PreTrainedModel
23
+
24
+ logger = get_logger(__name__)
25
+
26
+
27
+ class ModelLoader:
28
+ """
29
+ Singleton model loader for Wav2Vec2 classification model.
30
+
31
+ Handles lazy loading, caching, and memory management of the ML model.
32
+ Thread-safe for production use.
33
+ """
34
+
35
+ _instance: "ModelLoader | None" = None
36
+ _lock: threading.Lock = threading.Lock()
37
+
38
+ def __new__(cls) -> "ModelLoader":
39
+ """Ensure only one instance exists (Singleton pattern)."""
40
+ if cls._instance is None:
41
+ with cls._lock:
42
+ if cls._instance is None:
43
+ cls._instance = super().__new__(cls)
44
+ cls._instance._initialized = False
45
+ return cls._instance
46
+
47
+ def __init__(self) -> None:
48
+ """Initialize ModelLoader if not already initialized."""
49
+ if getattr(self, "_initialized", False):
50
+ return
51
+
52
+ self.settings = get_settings()
53
+ self.model: Wav2Vec2ForSequenceClassification | None = None
54
+ self.processor: Wav2Vec2Processor | None = None
55
+ self.device: str = self.settings.torch_device
56
+ self._model_lock = threading.Lock()
57
+ self._initialized = True
58
+
59
+ logger.info(
60
+ "ModelLoader initialized",
61
+ device=self.device,
62
+ model_identifier=self.settings.model_identifier,
63
+ )
64
+
65
+ @property
66
+ def is_loaded(self) -> bool:
67
+ """Check if model is loaded and ready for inference."""
68
+ return self.model is not None and self.processor is not None
69
+
70
+ def load_model(self) -> None:
71
+ """
72
+ Load the Wav2Vec2 model and processor.
73
+
74
+ Thread-safe loading with proper error handling.
75
+
76
+ Raises:
77
+ Exception: If model loading fails
78
+ """
79
+ with self._model_lock:
80
+ if self.is_loaded:
81
+ logger.debug("Model already loaded, skipping")
82
+ return
83
+
84
+ model_identifier = self.settings.model_identifier
85
+
86
+ logger.info("Loading Wav2Vec2 model", model=model_identifier, device=self.device)
87
+
88
+ try:
89
+ # Load processor - try model first, fallback to base wav2vec2
90
+ try:
91
+ self.processor = Wav2Vec2Processor.from_pretrained(
92
+ model_identifier,
93
+ trust_remote_code=False,
94
+ )
95
+ except Exception:
96
+ # Fine-tuned models often don't have processor, use base
97
+ logger.info("Using base wav2vec2 processor")
98
+ self.processor = Wav2Vec2Processor.from_pretrained(
99
+ "facebook/wav2vec2-base",
100
+ trust_remote_code=False,
101
+ )
102
+
103
+ # Load model with classification head
104
+ # For pretrained deepfake models, use their existing configuration
105
+ self.model = Wav2Vec2ForSequenceClassification.from_pretrained(
106
+ model_identifier,
107
+ trust_remote_code=False,
108
+ ignore_mismatched_sizes=True, # Allow different classifier sizes
109
+ )
110
+
111
+ # Move model to device
112
+ self.model = self.model.to(self.device)
113
+
114
+ # Set to evaluation mode
115
+ self.model.eval()
116
+
117
+ # Log memory usage
118
+ if self.device.startswith("cuda"):
119
+ memory_allocated = torch.cuda.memory_allocated() / (1024**3)
120
+ logger.info(
121
+ "Model loaded successfully",
122
+ device=self.device,
123
+ gpu_memory_gb=round(memory_allocated, 2),
124
+ )
125
+ else:
126
+ logger.info("Model loaded successfully", device=self.device)
127
+
128
+ except Exception as e:
129
+ self.model = None
130
+ self.processor = None
131
+ logger.error("Failed to load model", error=str(e))
132
+ raise
133
+
134
+ async def load_model_async(self) -> None:
135
+ """
136
+ Async wrapper for model loading.
137
+
138
+ Useful for FastAPI lifespan context.
139
+ """
140
+ # Run in thread pool to avoid blocking
141
+ import asyncio
142
+
143
+ loop = asyncio.get_event_loop()
144
+ await loop.run_in_executor(None, self.load_model)
145
+
146
+ def get_model(self) -> tuple[Wav2Vec2ForSequenceClassification, Wav2Vec2Processor]:
147
+ """
148
+ Get the loaded model and processor.
149
+
150
+ Returns:
151
+ Tuple of (model, processor)
152
+
153
+ Raises:
154
+ ModelNotLoadedError: If model is not loaded
155
+ """
156
+ if not self.is_loaded:
157
+ raise ModelNotLoadedError(
158
+ "Model not loaded. Call load_model() first.",
159
+ details={"model_identifier": self.settings.model_identifier},
160
+ )
161
+
162
+ return self.model, self.processor # type: ignore
163
+
164
+ def unload_model(self) -> None:
165
+ """
166
+ Unload model and free memory.
167
+
168
+ Useful for memory management in constrained environments.
169
+ """
170
+ with self._model_lock:
171
+ if self.model is not None:
172
+ del self.model
173
+ self.model = None
174
+
175
+ if self.processor is not None:
176
+ del self.processor
177
+ self.processor = None
178
+
179
+ # Force garbage collection
180
+ gc.collect()
181
+
182
+ # Clear CUDA cache if using GPU
183
+ if torch.cuda.is_available():
184
+ torch.cuda.empty_cache()
185
+
186
+ logger.info("Model unloaded, memory freed")
187
+
188
+ def warmup(self) -> None:
189
+ """
190
+ Run a warmup inference to initialize CUDA kernels.
191
+
192
+ This reduces latency on the first real inference.
193
+ """
194
+ if not self.is_loaded:
195
+ logger.warning("Cannot warmup - model not loaded")
196
+ return
197
+
198
+ logger.info("Running model warmup...")
199
+
200
+ try:
201
+ # Create dummy input
202
+ dummy_audio = torch.randn(1, 16000) # 1 second of audio
203
+
204
+ model, processor = self.get_model()
205
+
206
+ # Preprocess dummy audio
207
+ inputs = processor(
208
+ dummy_audio.squeeze().numpy(),
209
+ sampling_rate=16000,
210
+ return_tensors="pt",
211
+ padding=True,
212
+ )
213
+
214
+ inputs = {k: v.to(self.device) for k, v in inputs.items()}
215
+
216
+ # Run warmup inference
217
+ with torch.no_grad():
218
+ _ = model(**inputs)
219
+
220
+ logger.info("Model warmup complete")
221
+
222
+ except Exception as e:
223
+ logger.warning("Warmup failed (non-critical)", error=str(e))
224
+
225
+ def health_check(self) -> dict:
226
+ """
227
+ Get model health status.
228
+
229
+ Returns:
230
+ Dictionary with health information
231
+ """
232
+ status = {
233
+ "model_loaded": self.is_loaded,
234
+ "device": self.device,
235
+ "model_identifier": self.settings.model_identifier,
236
+ }
237
+
238
+ if self.device.startswith("cuda") and torch.cuda.is_available():
239
+ status["gpu_memory_allocated_gb"] = round(
240
+ torch.cuda.memory_allocated() / (1024**3), 2
241
+ )
242
+ status["gpu_memory_reserved_gb"] = round(
243
+ torch.cuda.memory_reserved() / (1024**3), 2
244
+ )
245
+
246
+ return status
app/ml/preprocessing.py ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Audio preprocessing for Wav2Vec2 model.
3
+
4
+ Handles conversion from audio arrays to model input tensors.
5
+ """
6
+
7
+ import numpy as np
8
+ import torch
9
+ from transformers import Wav2Vec2Processor
10
+
11
+ from app.utils.constants import TARGET_SAMPLE_RATE
12
+ from app.utils.logger import get_logger
13
+
14
+ logger = get_logger(__name__)
15
+
16
+
17
+ class AudioPreprocessor:
18
+ """
19
+ Preprocessor for preparing audio data for Wav2Vec2 model.
20
+
21
+ Converts numpy audio arrays into the tensor format expected
22
+ by the Wav2Vec2ForSequenceClassification model.
23
+ """
24
+
25
+ def __init__(
26
+ self,
27
+ processor: Wav2Vec2Processor,
28
+ device: str = "cpu",
29
+ ) -> None:
30
+ """
31
+ Initialize AudioPreprocessor.
32
+
33
+ Args:
34
+ processor: Wav2Vec2Processor instance
35
+ device: Target device for tensors (cpu/cuda)
36
+ """
37
+ self.processor = processor
38
+ self.device = device
39
+ self.sample_rate = TARGET_SAMPLE_RATE
40
+
41
+ def validate_input(self, audio_array: np.ndarray) -> bool:
42
+ """
43
+ Validate audio array for processing.
44
+
45
+ Args:
46
+ audio_array: Input audio array
47
+
48
+ Returns:
49
+ True if valid
50
+
51
+ Raises:
52
+ ValueError: If validation fails
53
+ """
54
+ if not isinstance(audio_array, np.ndarray):
55
+ raise ValueError(f"Expected numpy array, got {type(audio_array)}")
56
+
57
+ if audio_array.ndim != 1:
58
+ raise ValueError(f"Expected 1D array, got {audio_array.ndim}D")
59
+
60
+ if len(audio_array) == 0:
61
+ raise ValueError("Audio array is empty")
62
+
63
+ if np.isnan(audio_array).any():
64
+ raise ValueError("Audio array contains NaN values")
65
+
66
+ if np.isinf(audio_array).any():
67
+ raise ValueError("Audio array contains infinite values")
68
+
69
+ return True
70
+
71
+ def preprocess(
72
+ self,
73
+ audio_array: np.ndarray,
74
+ return_attention_mask: bool = True,
75
+ ) -> dict[str, torch.Tensor]:
76
+ """
77
+ Preprocess audio array for model inference.
78
+
79
+ Args:
80
+ audio_array: 1D numpy array of audio samples (16kHz, normalized)
81
+ return_attention_mask: Whether to return attention mask
82
+
83
+ Returns:
84
+ Dictionary with input_values and optionally attention_mask
85
+ """
86
+ # Validate input
87
+ self.validate_input(audio_array)
88
+
89
+ # Ensure float32
90
+ audio_array = audio_array.astype(np.float32)
91
+
92
+ # Process through Wav2Vec2Processor
93
+ inputs = self.processor(
94
+ audio_array,
95
+ sampling_rate=self.sample_rate,
96
+ return_tensors="pt",
97
+ padding=True,
98
+ return_attention_mask=return_attention_mask,
99
+ )
100
+
101
+ # Move to target device
102
+ inputs = {key: value.to(self.device) for key, value in inputs.items()}
103
+
104
+ logger.debug(
105
+ "Audio preprocessed for model",
106
+ input_length=inputs["input_values"].shape[-1],
107
+ device=self.device,
108
+ )
109
+
110
+ return inputs
111
+
112
+ def preprocess_batch(
113
+ self,
114
+ audio_arrays: list[np.ndarray],
115
+ return_attention_mask: bool = True,
116
+ ) -> dict[str, torch.Tensor]:
117
+ """
118
+ Preprocess a batch of audio arrays.
119
+
120
+ Args:
121
+ audio_arrays: List of 1D numpy arrays
122
+ return_attention_mask: Whether to return attention mask
123
+
124
+ Returns:
125
+ Dictionary with batched input_values and optionally attention_mask
126
+ """
127
+ # Validate all inputs
128
+ for i, audio in enumerate(audio_arrays):
129
+ try:
130
+ self.validate_input(audio)
131
+ except ValueError as e:
132
+ raise ValueError(f"Invalid audio at index {i}: {e}") from e
133
+
134
+ # Ensure float32
135
+ audio_arrays = [audio.astype(np.float32) for audio in audio_arrays]
136
+
137
+ # Process batch through Wav2Vec2Processor
138
+ inputs = self.processor(
139
+ audio_arrays,
140
+ sampling_rate=self.sample_rate,
141
+ return_tensors="pt",
142
+ padding=True,
143
+ return_attention_mask=return_attention_mask,
144
+ )
145
+
146
+ # Move to target device
147
+ inputs = {key: value.to(self.device) for key, value in inputs.items()}
148
+
149
+ logger.debug(
150
+ "Batch preprocessed for model",
151
+ batch_size=len(audio_arrays),
152
+ device=self.device,
153
+ )
154
+
155
+ return inputs
app/models/__init__.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Pydantic models package."""
2
+
3
+ from app.models.enums import AudioFormat
4
+ from app.models.enums import Classification
5
+ from app.models.enums import SupportedLanguage
6
+ from app.models.request import VoiceDetectionRequest
7
+ from app.models.response import ErrorResponse
8
+ from app.models.response import HealthResponse
9
+ from app.models.response import VoiceDetectionResponse
10
+
11
+ __all__ = [
12
+ "SupportedLanguage",
13
+ "Classification",
14
+ "AudioFormat",
15
+ "VoiceDetectionRequest",
16
+ "VoiceDetectionResponse",
17
+ "ErrorResponse",
18
+ "HealthResponse",
19
+ ]
app/models/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (612 Bytes). View file
 
app/models/__pycache__/enums.cpython-312.pyc ADDED
Binary file (2.39 kB). View file
 
app/models/__pycache__/request.cpython-312.pyc ADDED
Binary file (3.49 kB). View file
 
app/models/__pycache__/response.cpython-312.pyc ADDED
Binary file (7.04 kB). View file
 
app/models/enums.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Enumeration types for VoiceAuth API.
3
+
4
+ Defines supported languages, classification results, and audio formats.
5
+ """
6
+
7
+ from enum import Enum
8
+
9
+
10
+ class SupportedLanguage(str, Enum):
11
+ """
12
+ Supported languages for voice detection.
13
+
14
+ The API supports these 5 Indian languages for AI voice detection.
15
+ """
16
+
17
+ TAMIL = "Tamil"
18
+ ENGLISH = "English"
19
+ HINDI = "Hindi"
20
+ MALAYALAM = "Malayalam"
21
+ TELUGU = "Telugu"
22
+
23
+ @classmethod
24
+ def values(cls) -> list[str]:
25
+ """Get all language values as a list."""
26
+ return [lang.value for lang in cls]
27
+
28
+
29
+ class Classification(str, Enum):
30
+ """
31
+ Voice classification result.
32
+
33
+ Indicates whether the detected voice is AI-generated or human.
34
+ """
35
+
36
+ AI_GENERATED = "AI_GENERATED"
37
+ HUMAN = "HUMAN"
38
+
39
+ @property
40
+ def is_synthetic(self) -> bool:
41
+ """Check if classification indicates synthetic voice."""
42
+ return self == Classification.AI_GENERATED
43
+
44
+
45
+ class AudioFormat(str, Enum):
46
+ """
47
+ Supported audio input formats.
48
+
49
+ Currently only MP3 is supported as per competition requirements.
50
+ """
51
+
52
+ MP3 = "mp3"
53
+
54
+ @classmethod
55
+ def values(cls) -> list[str]:
56
+ """Get all format values as a list."""
57
+ return [fmt.value for fmt in cls]
app/models/request.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Request models for VoiceAuth API.
3
+
4
+ Defines Pydantic models for API request validation.
5
+ """
6
+
7
+ import base64
8
+ import re
9
+ from typing import Annotated
10
+
11
+ from pydantic import BaseModel
12
+ from pydantic import ConfigDict
13
+ from pydantic import Field
14
+ from pydantic import field_validator
15
+
16
+ from app.models.enums import AudioFormat
17
+ from app.models.enums import SupportedLanguage
18
+
19
+
20
+ class VoiceDetectionRequest(BaseModel):
21
+ """
22
+ Request model for voice detection endpoint.
23
+
24
+ Accepts Base64-encoded MP3 audio in one of 5 supported languages.
25
+ """
26
+
27
+ model_config = ConfigDict(
28
+ json_schema_extra={
29
+ "example": {
30
+ "language": "Tamil",
31
+ "audioFormat": "mp3",
32
+ "audioBase64": "SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU2LjM2LjEwMAAAAAAA...",
33
+ }
34
+ }
35
+ )
36
+
37
+ language: Annotated[
38
+ SupportedLanguage,
39
+ Field(
40
+ description="Language of the audio content. Must be one of: Tamil, English, Hindi, Malayalam, Telugu"
41
+ ),
42
+ ]
43
+
44
+ audioFormat: Annotated[
45
+ AudioFormat,
46
+ Field(
47
+ default=AudioFormat.MP3,
48
+ description="Format of the audio file. Currently only 'mp3' is supported",
49
+ ),
50
+ ] = AudioFormat.MP3
51
+
52
+ audioBase64: Annotated[
53
+ str,
54
+ Field(
55
+ min_length=100,
56
+ description="Base64-encoded MP3 audio data. Minimum 100 characters for valid audio",
57
+ ),
58
+ ]
59
+
60
+ @field_validator("audioBase64")
61
+ @classmethod
62
+ def validate_base64(cls, v: str) -> str:
63
+ """
64
+ Validate that the string is valid Base64.
65
+
66
+ Args:
67
+ v: The base64 string to validate
68
+
69
+ Returns:
70
+ The validated base64 string
71
+
72
+ Raises:
73
+ ValueError: If the string is not valid base64
74
+ """
75
+ # Remove any whitespace
76
+ v = v.strip()
77
+
78
+ # Check for valid base64 characters
79
+ base64_pattern = re.compile(r"^[A-Za-z0-9+/]*={0,2}$")
80
+ if not base64_pattern.match(v):
81
+ raise ValueError("Invalid Base64 encoding: contains invalid characters")
82
+
83
+ # Try to decode to verify it's valid base64
84
+ try:
85
+ # Add padding if needed
86
+ padding = 4 - len(v) % 4
87
+ if padding != 4:
88
+ v += "=" * padding
89
+
90
+ decoded = base64.b64decode(v)
91
+ if len(decoded) < 100:
92
+ raise ValueError("Decoded audio data is too small to be a valid MP3 file")
93
+
94
+ except Exception as e:
95
+ if "Invalid Base64" in str(e) or "too small" in str(e):
96
+ raise
97
+ raise ValueError(f"Invalid Base64 encoding: {e}") from e
98
+
99
+ return v.rstrip("=") + "=" * (4 - len(v.rstrip("=")) % 4) if len(v.rstrip("=")) % 4 else v
app/models/response.py ADDED
@@ -0,0 +1,264 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Response models for VoiceAuth API.
3
+
4
+ Defines Pydantic models for API responses.
5
+
6
+ PHASE 1 ENHANCED: Includes Risk Score, Quality Score, Temporal Analysis.
7
+ """
8
+
9
+ from typing import Annotated
10
+ from typing import Any
11
+ from typing import Literal
12
+ from typing import Optional
13
+
14
+ from pydantic import BaseModel
15
+ from pydantic import ConfigDict
16
+ from pydantic import Field
17
+
18
+ from app.models.enums import Classification
19
+
20
+
21
+ class VoiceDetectionResponse(BaseModel):
22
+ """
23
+ Successful voice detection response.
24
+
25
+ Contains classification result, confidence score, explanation,
26
+ and comprehensive analysis data.
27
+
28
+ PHASE 1 FEATURES:
29
+ - deepfakeRiskScore: Business-friendly risk rating
30
+ - audioQuality: Input quality assessment
31
+ - temporalAnalysis: Breathing, pauses, rhythm analysis
32
+ - audioForensics: Spectral and energy analysis
33
+ - performanceMetrics: Processing time breakdown
34
+ """
35
+
36
+ model_config = ConfigDict(
37
+ json_schema_extra={
38
+ "example": {
39
+ "status": "success",
40
+ "language": "Tamil",
41
+ "classification": "AI_GENERATED",
42
+ "confidenceScore": 0.91,
43
+ "explanation": "Strong evidence of AI-generated speech: absence of natural breathing sounds and mechanically consistent pause patterns detected",
44
+ "deepfakeRiskScore": {
45
+ "score": 87,
46
+ "level": "HIGH",
47
+ "recommendation": "Manual review required before approval",
48
+ },
49
+ "audioQuality": {
50
+ "score": 85,
51
+ "rating": "GOOD",
52
+ "reliability": "High confidence in detection results",
53
+ },
54
+ "temporalAnalysis": {
55
+ "breathingDetected": False,
56
+ "breathingNaturalness": 0.0,
57
+ "pauseMechanicalScore": 0.78,
58
+ "rhythmConsistency": 0.85,
59
+ "anomalyScore": 0.72,
60
+ "verdict": "HIGH_ANOMALY",
61
+ },
62
+ "audioForensics": {
63
+ "spectralCentroid": 1523.45,
64
+ "pitchStability": 0.89,
65
+ "jitter": 0.0021,
66
+ "energyConsistency": 0.92,
67
+ "silenceRatio": 0.08,
68
+ "aiLikelihood": 0.76,
69
+ },
70
+ "performanceMetrics": {
71
+ "audioProcessingMs": 45.23,
72
+ "forensicsAnalysisMs": 12.87,
73
+ "temporalAnalysisMs": 8.45,
74
+ "modelInferenceMs": 127.45,
75
+ "totalProcessingMs": 193.00,
76
+ },
77
+ }
78
+ }
79
+ )
80
+
81
+ status: Annotated[
82
+ Literal["success"],
83
+ Field(description="Response status, always 'success' for successful detections"),
84
+ ] = "success"
85
+
86
+ language: Annotated[
87
+ str,
88
+ Field(description="Language of the analyzed audio"),
89
+ ]
90
+
91
+ classification: Annotated[
92
+ Classification,
93
+ Field(description="Classification result: AI_GENERATED or HUMAN"),
94
+ ]
95
+
96
+ confidenceScore: Annotated[
97
+ float,
98
+ Field(
99
+ ge=0.0,
100
+ le=1.0,
101
+ description="Calibrated confidence score between 0.0 and 1.0",
102
+ ),
103
+ ]
104
+
105
+ explanation: Annotated[
106
+ str,
107
+ Field(
108
+ max_length=250,
109
+ description="Human-readable explanation based on comprehensive analysis",
110
+ ),
111
+ ]
112
+
113
+ # NEW: Deepfake Risk Score
114
+ deepfakeRiskScore: Annotated[
115
+ Optional[dict[str, Any]],
116
+ Field(
117
+ default=None,
118
+ description="Business-friendly risk score (0-100) with level and recommendation",
119
+ ),
120
+ ] = None
121
+
122
+ # NEW: Audio Quality Score
123
+ audioQuality: Annotated[
124
+ Optional[dict[str, Any]],
125
+ Field(
126
+ default=None,
127
+ description="Input audio quality assessment affecting detection reliability",
128
+ ),
129
+ ] = None
130
+
131
+ # NEW: Temporal Analysis
132
+ temporalAnalysis: Annotated[
133
+ Optional[dict[str, Any]],
134
+ Field(
135
+ default=None,
136
+ description="Temporal anomaly analysis (breathing, pauses, rhythm)",
137
+ ),
138
+ ] = None
139
+
140
+ # Audio Forensics
141
+ audioForensics: Annotated[
142
+ Optional[dict[str, float]],
143
+ Field(
144
+ default=None,
145
+ description="Detailed audio forensics analysis metrics",
146
+ ),
147
+ ] = None
148
+
149
+ # Performance Metrics
150
+ performanceMetrics: Annotated[
151
+ Optional[dict[str, float]],
152
+ Field(
153
+ default=None,
154
+ description="Performance timing breakdown",
155
+ ),
156
+ ] = None
157
+
158
+
159
+ class ErrorResponse(BaseModel):
160
+ """
161
+ Error response model.
162
+
163
+ Returned when the API encounters an error.
164
+ """
165
+
166
+ model_config = ConfigDict(
167
+ json_schema_extra={
168
+ "example": {
169
+ "status": "error",
170
+ "message": "Invalid API key or malformed request",
171
+ }
172
+ }
173
+ )
174
+
175
+ status: Annotated[
176
+ Literal["error"],
177
+ Field(description="Response status, always 'error' for error responses"),
178
+ ] = "error"
179
+
180
+ message: Annotated[
181
+ str,
182
+ Field(description="Human-readable error message"),
183
+ ]
184
+
185
+ details: Annotated[
186
+ Optional[dict[str, Any]],
187
+ Field(default=None, description="Additional error details if available"),
188
+ ] = None
189
+
190
+
191
+ class HealthResponse(BaseModel):
192
+ """
193
+ Health check response model.
194
+
195
+ Returned by health check endpoints.
196
+ """
197
+
198
+ model_config = ConfigDict(
199
+ json_schema_extra={
200
+ "example": {
201
+ "status": "healthy",
202
+ "version": "1.0.0",
203
+ "model_loaded": True,
204
+ "model_name": "facebook/wav2vec2-base",
205
+ "device": "cuda",
206
+ "supported_languages": ["Tamil", "English", "Hindi", "Malayalam", "Telugu"],
207
+ "features": [
208
+ "audio_forensics",
209
+ "temporal_anomaly_detection",
210
+ "deepfake_risk_score",
211
+ "audio_quality_score",
212
+ ],
213
+ }
214
+ }
215
+ )
216
+
217
+ status: Annotated[
218
+ str,
219
+ Field(description="Health status: 'healthy' or 'unhealthy'"),
220
+ ]
221
+
222
+ version: Annotated[
223
+ str,
224
+ Field(description="API version"),
225
+ ]
226
+
227
+ model_loaded: Annotated[
228
+ bool,
229
+ Field(description="Whether the ML model is loaded and ready"),
230
+ ]
231
+
232
+ model_name: Annotated[
233
+ Optional[str],
234
+ Field(default=None, description="Name of the loaded model"),
235
+ ] = None
236
+
237
+ device: Annotated[
238
+ Optional[str],
239
+ Field(default=None, description="Device used for inference (cpu/cuda)"),
240
+ ] = None
241
+
242
+ supported_languages: Annotated[
243
+ list[str],
244
+ Field(description="List of supported languages"),
245
+ ]
246
+
247
+ features: Annotated[
248
+ Optional[list[str]],
249
+ Field(default=None, description="List of enabled features"),
250
+ ] = None
251
+
252
+
253
+ class LanguagesResponse(BaseModel):
254
+ """Response model for supported languages endpoint."""
255
+
256
+ languages: Annotated[
257
+ list[str],
258
+ Field(description="List of supported language names"),
259
+ ]
260
+
261
+ count: Annotated[
262
+ int,
263
+ Field(description="Number of supported languages"),
264
+ ]
app/services/__init__.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Services package."""
2
+
3
+ from app.services.audio_forensics import AudioForensicsAnalyzer
4
+ from app.services.audio_processor import AudioProcessor
5
+ from app.services.explainability import ExplainabilityService
6
+ from app.services.score_calculators import AudioQualityScorer
7
+ from app.services.score_calculators import RiskScoreCalculator
8
+ from app.services.temporal_detector import TemporalAnomalyDetector
9
+ from app.services.voice_detector import VoiceDetector
10
+
11
+ __all__ = [
12
+ "AudioProcessor",
13
+ "AudioForensicsAnalyzer",
14
+ "ExplainabilityService",
15
+ "TemporalAnomalyDetector",
16
+ "RiskScoreCalculator",
17
+ "AudioQualityScorer",
18
+ "VoiceDetector",
19
+ ]
app/services/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (748 Bytes). View file
 
app/services/__pycache__/audio_forensics.cpython-312.pyc ADDED
Binary file (13.8 kB). View file
 
app/services/__pycache__/audio_processor.cpython-312.pyc ADDED
Binary file (10.7 kB). View file
 
app/services/__pycache__/explainability.cpython-312.pyc ADDED
Binary file (6.76 kB). View file
 
app/services/__pycache__/federated_learning.cpython-312.pyc ADDED
Binary file (10.1 kB). View file
 
app/services/__pycache__/score_calculators.cpython-312.pyc ADDED
Binary file (7.86 kB). View file