Spaces:
Sleeping
Sleeping
entire
#1
by bakshia - opened
- .gitattributes +35 -0
- .gitignore +0 -32
- Dockerfile +0 -22
- README.md +5 -20
- app/__init__.py +0 -3
- app/api/__init__.py +0 -3
- app/api/routes.py +0 -276
- app/api/schemas.py +0 -185
- app/audio_utils.py +0 -76
- app/config.py +0 -67
- app/main.py +0 -107
- app/models/__init__.py +0 -17
- app/models/ensemble_detector.py +0 -310
- app/models/personaplex_detector.py +0 -282
- app/models/spectrogram_cnn.py +0 -349
- app/models/wav2vec_detector.py +0 -338
- build.sh +0 -5
- deployment.md +0 -71
- render.yaml +0 -13
- requirements.txt +0 -24
- run.py +0 -40
- test_api.py +0 -149
- verify_tester_config.py +0 -37
.gitattributes
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
+
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
+
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
+
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
+
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
.gitignore
DELETED
|
@@ -1,32 +0,0 @@
|
|
| 1 |
-
# Python
|
| 2 |
-
__pycache__/
|
| 3 |
-
*.py[cod]
|
| 4 |
-
*$py.class
|
| 5 |
-
|
| 6 |
-
# Environments
|
| 7 |
-
.env
|
| 8 |
-
.venv
|
| 9 |
-
env/
|
| 10 |
-
venv/
|
| 11 |
-
ENV/
|
| 12 |
-
env.bak/
|
| 13 |
-
venv.bak/
|
| 14 |
-
|
| 15 |
-
# IDEs
|
| 16 |
-
.vscode/
|
| 17 |
-
.idea/
|
| 18 |
-
|
| 19 |
-
# OS
|
| 20 |
-
.DS_Store
|
| 21 |
-
Thumbs.db
|
| 22 |
-
|
| 23 |
-
# App specific
|
| 24 |
-
*.mp3
|
| 25 |
-
*.wav
|
| 26 |
-
*.log
|
| 27 |
-
|
| 28 |
-
# Sensitive
|
| 29 |
-
config.yml
|
| 30 |
-
*.json
|
| 31 |
-
credentials-file.json
|
| 32 |
-
/weights/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Dockerfile
DELETED
|
@@ -1,22 +0,0 @@
|
|
| 1 |
-
FROM python:3.11-slim
|
| 2 |
-
|
| 3 |
-
WORKDIR /app
|
| 4 |
-
|
| 5 |
-
# Install system dependencies for audio processing
|
| 6 |
-
RUN apt-get update && apt-get install -y \
|
| 7 |
-
ffmpeg \
|
| 8 |
-
libsndfile1 \
|
| 9 |
-
&& rm -rf /var/lib/apt/lists/*
|
| 10 |
-
|
| 11 |
-
# Copy requirements first for caching
|
| 12 |
-
COPY requirements.txt .
|
| 13 |
-
RUN pip install --no-cache-dir -r requirements.txt
|
| 14 |
-
|
| 15 |
-
# Copy application code
|
| 16 |
-
COPY . .
|
| 17 |
-
|
| 18 |
-
# Expose port 7860 (HuggingFace Spaces default)
|
| 19 |
-
EXPOSE 7860
|
| 20 |
-
|
| 21 |
-
# Run the application
|
| 22 |
-
CMD ["python", "run.py", "--host", "0.0.0.0", "--port", "7860"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
README.md
CHANGED
|
@@ -1,27 +1,12 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
colorFrom: blue
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
license: mit
|
|
|
|
| 9 |
---
|
| 10 |
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
Detect AI-generated voices using advanced acoustic analysis and neural network patterns.
|
| 14 |
-
|
| 15 |
-
## API Endpoints
|
| 16 |
-
|
| 17 |
-
- `POST /api/v1/detect` - Detect if audio is AI-generated
|
| 18 |
-
- `GET /api/v1/health` - Health check
|
| 19 |
-
- `GET /docs` - API documentation
|
| 20 |
-
|
| 21 |
-
## Usage
|
| 22 |
-
|
| 23 |
-
```bash
|
| 24 |
-
curl -X POST "https://YOUR-SPACE.hf.space/api/v1/detect" \
|
| 25 |
-
-H "Content-Type: application/json" \
|
| 26 |
-
-d '{"audioUrl": "https://example.com/audio.mp3"}'
|
| 27 |
-
```
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Voice Detection Api
|
| 3 |
+
emoji: 📊
|
| 4 |
colorFrom: blue
|
| 5 |
+
colorTo: blue
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
license: mit
|
| 9 |
+
short_description: api for buildathon
|
| 10 |
---
|
| 11 |
|
| 12 |
+
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/__init__.py
DELETED
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
AI Voice Detection System - App Package
|
| 3 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
app/api/__init__.py
DELETED
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
API Package
|
| 3 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
app/api/routes.py
DELETED
|
@@ -1,276 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
API Routes for Voice Detection
|
| 3 |
-
|
| 4 |
-
Defines all API endpoints for the voice detection service.
|
| 5 |
-
"""
|
| 6 |
-
import base64
|
| 7 |
-
import tempfile
|
| 8 |
-
import os
|
| 9 |
-
from typing import Optional
|
| 10 |
-
from fastapi import APIRouter, HTTPException, status, BackgroundTasks, Security, Request
|
| 11 |
-
from fastapi.security import APIKeyHeader
|
| 12 |
-
import httpx
|
| 13 |
-
from pydantic import ValidationError
|
| 14 |
-
|
| 15 |
-
from .schemas import (
|
| 16 |
-
DetectRequest,
|
| 17 |
-
ExternalTesterRequest,
|
| 18 |
-
DetectResponse,
|
| 19 |
-
HealthResponse,
|
| 20 |
-
ErrorResponse,
|
| 21 |
-
LanguageCode
|
| 22 |
-
)
|
| 23 |
-
from ..models.ensemble_detector import EnsembleVoiceDetector, create_detector
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
_detector: Optional[EnsembleVoiceDetector] = None
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
def get_detector(language: str = "en") -> EnsembleVoiceDetector:
|
| 30 |
-
"""Get or create detector instance"""
|
| 31 |
-
global _detector
|
| 32 |
-
if _detector is None:
|
| 33 |
-
_detector = create_detector(language=language)
|
| 34 |
-
return _detector
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
router = APIRouter(prefix="/api/v1", tags=["Voice Detection"])
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
@router.post(
|
| 41 |
-
"/detect",
|
| 42 |
-
response_model=DetectResponse,
|
| 43 |
-
# ... (responses dict)
|
| 44 |
-
summary="Detect AI-Generated Voice",
|
| 45 |
-
description="..."
|
| 46 |
-
)
|
| 47 |
-
async def detect_voice(
|
| 48 |
-
request: DetectRequest,
|
| 49 |
-
api_key: str = Security(APIKeyHeader(name="X-API-Key", auto_error=False))
|
| 50 |
-
) -> DetectResponse:
|
| 51 |
-
"""
|
| 52 |
-
Detect if the provided audio is AI-generated or human.
|
| 53 |
-
"""
|
| 54 |
-
# Optional API Key Validation
|
| 55 |
-
# We allow the key to be missing for public testing/hackathon judges.
|
| 56 |
-
if api_key:
|
| 57 |
-
# In a real app, validate against DB/Env
|
| 58 |
-
pass
|
| 59 |
-
|
| 60 |
-
temp_path = None
|
| 61 |
-
|
| 62 |
-
try:
|
| 63 |
-
audio_bytes = None
|
| 64 |
-
|
| 65 |
-
if request.audio_url:
|
| 66 |
-
# Download from URL
|
| 67 |
-
async with httpx.AsyncClient() as client:
|
| 68 |
-
try:
|
| 69 |
-
resp = await client.get(request.audio_url, follow_redirects=True, timeout=30.0)
|
| 70 |
-
resp.raise_for_status()
|
| 71 |
-
audio_bytes = resp.content
|
| 72 |
-
except Exception as e:
|
| 73 |
-
raise HTTPException(
|
| 74 |
-
status_code=status.HTTP_400_BAD_REQUEST,
|
| 75 |
-
detail=f"Failed to download audio from URL: {str(e)}"
|
| 76 |
-
)
|
| 77 |
-
elif request.audio_base64:
|
| 78 |
-
# Decode Base64
|
| 79 |
-
try:
|
| 80 |
-
audio_bytes = base64.b64decode(request.audio_base64)
|
| 81 |
-
except Exception as e:
|
| 82 |
-
raise HTTPException(
|
| 83 |
-
status_code=status.HTTP_400_BAD_REQUEST,
|
| 84 |
-
detail=f"Invalid Base64 encoding: {str(e)}"
|
| 85 |
-
)
|
| 86 |
-
else:
|
| 87 |
-
raise HTTPException(
|
| 88 |
-
status_code=status.HTTP_400_BAD_REQUEST,
|
| 89 |
-
detail="Either 'audio_url' or 'audio_base64' must be provided."
|
| 90 |
-
)
|
| 91 |
-
|
| 92 |
-
if len(audio_bytes) > 10 * 1024 * 1024:
|
| 93 |
-
raise HTTPException(
|
| 94 |
-
status_code=status.HTTP_400_BAD_REQUEST,
|
| 95 |
-
detail="Audio file too large. Maximum size is 10MB."
|
| 96 |
-
)
|
| 97 |
-
|
| 98 |
-
if len(audio_bytes) < 1000:
|
| 99 |
-
raise HTTPException(
|
| 100 |
-
status_code=status.HTTP_400_BAD_REQUEST,
|
| 101 |
-
detail="Audio file too small. Minimum duration is 1 second."
|
| 102 |
-
)
|
| 103 |
-
|
| 104 |
-
# Write audio to temporary file
|
| 105 |
-
with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as f:
|
| 106 |
-
f.write(audio_bytes)
|
| 107 |
-
temp_path = f.name
|
| 108 |
-
|
| 109 |
-
# Get language from request or default to English
|
| 110 |
-
language = request.language_hint.value if request.language_hint else "en"
|
| 111 |
-
|
| 112 |
-
detector = get_detector(language=language)
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
result = detector.detect(temp_path)
|
| 116 |
-
|
| 117 |
-
return DetectResponse(
|
| 118 |
-
classification=result["classification"],
|
| 119 |
-
confidence=result["confidence"],
|
| 120 |
-
explanation=result["explanation"]["key_indicators"]
|
| 121 |
-
)
|
| 122 |
-
|
| 123 |
-
except HTTPException:
|
| 124 |
-
raise
|
| 125 |
-
except Exception as e:
|
| 126 |
-
raise HTTPException(
|
| 127 |
-
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 128 |
-
detail=f"Detection failed: {str(e)}"
|
| 129 |
-
)
|
| 130 |
-
finally:
|
| 131 |
-
# Cleanup temp file
|
| 132 |
-
if temp_path and os.path.exists(temp_path):
|
| 133 |
-
try:
|
| 134 |
-
os.unlink(temp_path)
|
| 135 |
-
except OSError:
|
| 136 |
-
pass
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
@router.post(
|
| 140 |
-
"/detect-external",
|
| 141 |
-
response_model=DetectResponse,
|
| 142 |
-
summary="Detect AI-Generated Voice (External Tester Compatible)",
|
| 143 |
-
description="Alternative endpoint compatible with external testing tools"
|
| 144 |
-
)
|
| 145 |
-
async def detect_voice_external(
|
| 146 |
-
request: ExternalTesterRequest
|
| 147 |
-
) -> DetectResponse:
|
| 148 |
-
"""
|
| 149 |
-
Detect if the provided audio is AI-generated or human.
|
| 150 |
-
Compatible with external endpoint testers.
|
| 151 |
-
"""
|
| 152 |
-
temp_path = None
|
| 153 |
-
|
| 154 |
-
try:
|
| 155 |
-
# Convert external tester format to internal format
|
| 156 |
-
audio_bytes = base64.b64decode(request.Audio_Base64_Format)
|
| 157 |
-
|
| 158 |
-
if len(audio_bytes) > 10 * 1024 * 1024:
|
| 159 |
-
raise HTTPException(
|
| 160 |
-
status_code=status.HTTP_400_BAD_REQUEST,
|
| 161 |
-
detail="Audio file too large. Maximum size is 10MB."
|
| 162 |
-
)
|
| 163 |
-
|
| 164 |
-
if len(audio_bytes) < 1000:
|
| 165 |
-
raise HTTPException(
|
| 166 |
-
status_code=status.HTTP_400_BAD_REQUEST,
|
| 167 |
-
detail="Audio file too small. Minimum duration is 1 second."
|
| 168 |
-
)
|
| 169 |
-
|
| 170 |
-
# Write audio to temporary file
|
| 171 |
-
with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as f:
|
| 172 |
-
f.write(audio_bytes)
|
| 173 |
-
temp_path = f.name
|
| 174 |
-
|
| 175 |
-
# Get language from request
|
| 176 |
-
language = request.Language.lower()
|
| 177 |
-
|
| 178 |
-
detector = get_detector(language=language)
|
| 179 |
-
|
| 180 |
-
result = detector.detect(temp_path)
|
| 181 |
-
|
| 182 |
-
result["language_detected"] = language
|
| 183 |
-
|
| 184 |
-
return DetectResponse(**result)
|
| 185 |
-
|
| 186 |
-
except HTTPException:
|
| 187 |
-
raise
|
| 188 |
-
except Exception as e:
|
| 189 |
-
raise HTTPException(
|
| 190 |
-
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 191 |
-
detail=f"Detection failed: {str(e)}"
|
| 192 |
-
)
|
| 193 |
-
finally:
|
| 194 |
-
# Cleanup temp file
|
| 195 |
-
if temp_path and os.path.exists(temp_path):
|
| 196 |
-
try:
|
| 197 |
-
os.unlink(temp_path)
|
| 198 |
-
except OSError:
|
| 199 |
-
pass
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
@router.get(
|
| 203 |
-
"/health",
|
| 204 |
-
response_model=HealthResponse,
|
| 205 |
-
summary="Health Check",
|
| 206 |
-
description="Check the health status of the voice detection service"
|
| 207 |
-
)
|
| 208 |
-
async def health_check() -> HealthResponse:
|
| 209 |
-
"""
|
| 210 |
-
Check if the service is healthy and all models are loaded.
|
| 211 |
-
"""
|
| 212 |
-
import torch
|
| 213 |
-
|
| 214 |
-
detector = get_detector()
|
| 215 |
-
|
| 216 |
-
return HealthResponse(
|
| 217 |
-
status="healthy",
|
| 218 |
-
version="1.0.0",
|
| 219 |
-
models_loaded={
|
| 220 |
-
"wav2vec": hasattr(detector, 'wav2vec_detector'),
|
| 221 |
-
"spectrogram_cnn": hasattr(detector, 'spectrogram_detector'),
|
| 222 |
-
"personaplex": hasattr(detector, 'personaplex_detector')
|
| 223 |
-
},
|
| 224 |
-
device="cuda" if torch.cuda.is_available() else "cpu"
|
| 225 |
-
)
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
@router.get(
|
| 229 |
-
"/languages",
|
| 230 |
-
summary="Supported Languages",
|
| 231 |
-
description="Get list of supported languages for voice detection"
|
| 232 |
-
)
|
| 233 |
-
async def get_languages():
|
| 234 |
-
"""Get supported languages"""
|
| 235 |
-
return {
|
| 236 |
-
"languages": [
|
| 237 |
-
{"code": "ta", "name": "Tamil"},
|
| 238 |
-
{"code": "en", "name": "English"},
|
| 239 |
-
{"code": "hi", "name": "Hindi"},
|
| 240 |
-
{"code": "ml", "name": "Malayalam"},
|
| 241 |
-
{"code": "te", "name": "Telugu"}
|
| 242 |
-
]
|
| 243 |
-
}
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
@router.get(
|
| 247 |
-
"/tools",
|
| 248 |
-
summary="Detectable AI Tools",
|
| 249 |
-
description="Get list of AI voice synthesis tools that can be detected"
|
| 250 |
-
)
|
| 251 |
-
async def get_detectable_tools():
|
| 252 |
-
"""Get list of detectable AI tools"""
|
| 253 |
-
return {
|
| 254 |
-
"tools": [
|
| 255 |
-
{
|
| 256 |
-
"id": "nvidia_personaplex",
|
| 257 |
-
"name": "NVIDIA PersonaPlex/Riva",
|
| 258 |
-
"description": "NVIDIA's conversational AI voice synthesis"
|
| 259 |
-
},
|
| 260 |
-
{
|
| 261 |
-
"id": "elevenlabs",
|
| 262 |
-
"name": "ElevenLabs",
|
| 263 |
-
"description": "ElevenLabs voice cloning and synthesis"
|
| 264 |
-
},
|
| 265 |
-
{
|
| 266 |
-
"id": "azure_neural",
|
| 267 |
-
"name": "Microsoft Azure Neural TTS",
|
| 268 |
-
"description": "Azure Cognitive Services Neural TTS"
|
| 269 |
-
},
|
| 270 |
-
{
|
| 271 |
-
"id": "google_wavenet",
|
| 272 |
-
"name": "Google WaveNet/Neural2",
|
| 273 |
-
"description": "Google Cloud Text-to-Speech"
|
| 274 |
-
}
|
| 275 |
-
]
|
| 276 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/api/schemas.py
DELETED
|
@@ -1,185 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Pydantic Schemas for API Request/Response Validation
|
| 3 |
-
"""
|
| 4 |
-
from pydantic import BaseModel, Field
|
| 5 |
-
from typing import Dict, List, Optional, Any
|
| 6 |
-
from enum import Enum
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
class LanguageCode(str, Enum):
|
| 10 |
-
"""Supported language codes"""
|
| 11 |
-
TAMIL = "ta"
|
| 12 |
-
ENGLISH = "en"
|
| 13 |
-
HINDI = "hi"
|
| 14 |
-
MALAYALAM = "ml"
|
| 15 |
-
TELUGU = "te"
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
class ClassificationType(str, Enum):
|
| 19 |
-
"""Voice classification types"""
|
| 20 |
-
AI_GENERATED = "ai_generated"
|
| 21 |
-
HUMAN = "human"
|
| 22 |
-
UNKNOWN = "unknown"
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
class ConfidenceLevel(str, Enum):
|
| 26 |
-
"""Confidence level descriptors"""
|
| 27 |
-
HIGH = "high"
|
| 28 |
-
MEDIUM = "medium"
|
| 29 |
-
LOW = "low"
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
# ============== Request Schemas ==============
|
| 33 |
-
|
| 34 |
-
class DetectRequest(BaseModel):
|
| 35 |
-
"""Request schema for voice detection endpoint"""
|
| 36 |
-
audio_base64: Optional[str] = Field(
|
| 37 |
-
default=None,
|
| 38 |
-
description="Base64-encoded MP3 audio data (mutually exclusive with audio_url)",
|
| 39 |
-
min_length=100,
|
| 40 |
-
examples=["SGVsbG8gV29ybGQ..."],
|
| 41 |
-
alias="audioBase64"
|
| 42 |
-
)
|
| 43 |
-
audio_url: Optional[str] = Field(
|
| 44 |
-
default=None,
|
| 45 |
-
description="URL to audio file (MP3, WAV, etc.) (mutually exclusive with audio_base64)",
|
| 46 |
-
alias="audioUrl"
|
| 47 |
-
)
|
| 48 |
-
language_hint: Optional[LanguageCode] = Field(
|
| 49 |
-
default=None,
|
| 50 |
-
description="Optional hint for expected language (improves accuracy)"
|
| 51 |
-
)
|
| 52 |
-
|
| 53 |
-
class Config:
|
| 54 |
-
populate_by_name = True
|
| 55 |
-
json_schema_extra = {
|
| 56 |
-
"example": {
|
| 57 |
-
"audio_url": "https://example.com/sample.mp3",
|
| 58 |
-
"language_hint": "en"
|
| 59 |
-
}
|
| 60 |
-
}
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
class ExternalTesterRequest(BaseModel):
|
| 64 |
-
"""Request schema compatible with external endpoint tester"""
|
| 65 |
-
Language: str = Field(
|
| 66 |
-
...,
|
| 67 |
-
description="Language code (en, ta, hi, ml, te)",
|
| 68 |
-
examples=["en"]
|
| 69 |
-
)
|
| 70 |
-
Audio_Format: str = Field(
|
| 71 |
-
default="mp3",
|
| 72 |
-
description="Audio format (mp3, wav, etc.)",
|
| 73 |
-
alias="Audio Format"
|
| 74 |
-
)
|
| 75 |
-
Audio_Base64_Format: str = Field(
|
| 76 |
-
...,
|
| 77 |
-
description="Base64-encoded audio data",
|
| 78 |
-
alias="Audio Base64 Format"
|
| 79 |
-
)
|
| 80 |
-
|
| 81 |
-
class Config:
|
| 82 |
-
populate_by_name = True
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
# ============== Response Schemas ==============
|
| 87 |
-
|
| 88 |
-
class TechnicalDetails(BaseModel):
|
| 89 |
-
"""Technical analysis details"""
|
| 90 |
-
spectral_artifacts: List[str] = Field(default_factory=list)
|
| 91 |
-
temporal_patterns: List[str] = Field(default_factory=list)
|
| 92 |
-
synthesis_markers: List[str] = Field(default_factory=list)
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
class ExplanationResponse(BaseModel):
|
| 96 |
-
"""Detailed explanation of detection result"""
|
| 97 |
-
summary: str = Field(
|
| 98 |
-
...,
|
| 99 |
-
description="Human-readable summary of the detection"
|
| 100 |
-
)
|
| 101 |
-
confidence_level: ConfidenceLevel = Field(
|
| 102 |
-
...,
|
| 103 |
-
description="Confidence level (high/medium/low)"
|
| 104 |
-
)
|
| 105 |
-
technical_details: TechnicalDetails = Field(
|
| 106 |
-
...,
|
| 107 |
-
description="Technical analysis breakdown"
|
| 108 |
-
)
|
| 109 |
-
key_indicators: List[str] = Field(
|
| 110 |
-
...,
|
| 111 |
-
description="Top indicators that led to the classification"
|
| 112 |
-
)
|
| 113 |
-
model_contributions: Dict[str, float] = Field(
|
| 114 |
-
...,
|
| 115 |
-
description="Weight contribution of each detection model"
|
| 116 |
-
)
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
class ComponentResult(BaseModel):
|
| 120 |
-
"""Result from an individual detection component"""
|
| 121 |
-
classification: Optional[str] = None
|
| 122 |
-
confidence: Optional[float] = None
|
| 123 |
-
ai_probability: Optional[float] = None
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
class DetectedTool(BaseModel):
|
| 127 |
-
"""Information about detected AI voice tool"""
|
| 128 |
-
tool_id: str
|
| 129 |
-
tool_name: str
|
| 130 |
-
confidence: float
|
| 131 |
-
reasons: List[str]
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
class DetailedAnalysis(BaseModel):
|
| 135 |
-
"""Detailed analysis from all detection components"""
|
| 136 |
-
wav2vec_indicators: List[str] = Field(default_factory=list)
|
| 137 |
-
spectrogram_indicators: List[str] = Field(default_factory=list)
|
| 138 |
-
personaplex_indicators: List[str] = Field(default_factory=list)
|
| 139 |
-
detected_tools: List[DetectedTool] = Field(default_factory=list)
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
class DetectResponse(BaseModel):
|
| 143 |
-
"""Response schema for voice detection endpoint"""
|
| 144 |
-
classification: ClassificationType = Field(
|
| 145 |
-
...,
|
| 146 |
-
description="Classification result: ai_generated or human"
|
| 147 |
-
)
|
| 148 |
-
confidence: float = Field(
|
| 149 |
-
...,
|
| 150 |
-
ge=0.0,
|
| 151 |
-
le=1.0,
|
| 152 |
-
description="Confidence score between 0 and 1"
|
| 153 |
-
)
|
| 154 |
-
explanation: List[str] = Field(
|
| 155 |
-
...,
|
| 156 |
-
description="List of key indicators explaining the classification"
|
| 157 |
-
)
|
| 158 |
-
|
| 159 |
-
class Config:
|
| 160 |
-
json_schema_extra = {
|
| 161 |
-
"example": {
|
| 162 |
-
"classification": "ai_generated",
|
| 163 |
-
"confidence": 0.85,
|
| 164 |
-
"explanation": [
|
| 165 |
-
"Unusually smooth spectral distribution typical of neural vocoders",
|
| 166 |
-
"Low temporal variation suggesting synthetic generation",
|
| 167 |
-
"Periodic patterns suggesting neural vocoder"
|
| 168 |
-
]
|
| 169 |
-
}
|
| 170 |
-
}
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
class HealthResponse(BaseModel):
|
| 174 |
-
"""Health check response"""
|
| 175 |
-
status: str = "healthy"
|
| 176 |
-
version: str
|
| 177 |
-
models_loaded: Dict[str, bool]
|
| 178 |
-
device: str
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
class ErrorResponse(BaseModel):
|
| 182 |
-
"""Error response schema"""
|
| 183 |
-
error: str
|
| 184 |
-
detail: Optional[str] = None
|
| 185 |
-
code: str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/audio_utils.py
DELETED
|
@@ -1,76 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Audio Utilities - Shared audio loading functions with MP3 support
|
| 3 |
-
"""
|
| 4 |
-
import os
|
| 5 |
-
from typing import Tuple
|
| 6 |
-
import numpy as np
|
| 7 |
-
import soundfile as sf
|
| 8 |
-
from scipy import signal
|
| 9 |
-
import logging
|
| 10 |
-
|
| 11 |
-
logger = logging.getLogger(__name__)
|
| 12 |
-
|
| 13 |
-
try:
|
| 14 |
-
import torch
|
| 15 |
-
TORCH_AVAILABLE = True
|
| 16 |
-
except ImportError:
|
| 17 |
-
TORCH_AVAILABLE = False
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
def load_audio(audio_path: str, target_sr: int = 16000) -> Tuple[np.ndarray, int]:
|
| 21 |
-
"""
|
| 22 |
-
Load audio file with MP3 support using soundfile.
|
| 23 |
-
Returns: Tuple of (audio_array, sample_rate)
|
| 24 |
-
"""
|
| 25 |
-
samples, sr = sf.read(audio_path, dtype='float32')
|
| 26 |
-
|
| 27 |
-
if len(samples.shape) > 1:
|
| 28 |
-
samples = samples.mean(axis=1)
|
| 29 |
-
|
| 30 |
-
if sr != target_sr:
|
| 31 |
-
samples = resample_audio(samples, sr, target_sr)
|
| 32 |
-
|
| 33 |
-
return samples, target_sr
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
def resample_audio(samples: np.ndarray, orig_sr: int, target_sr: int) -> np.ndarray:
|
| 37 |
-
"""Resample audio using scipy"""
|
| 38 |
-
if orig_sr == target_sr:
|
| 39 |
-
return samples
|
| 40 |
-
|
| 41 |
-
duration = len(samples) / orig_sr
|
| 42 |
-
new_length = int(duration * target_sr)
|
| 43 |
-
|
| 44 |
-
resampled = signal.resample(samples, new_length)
|
| 45 |
-
|
| 46 |
-
return resampled.astype(np.float32)
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
def load_audio_torch(audio_path: str, target_sr: int = 16000) -> "torch.Tensor":
|
| 50 |
-
"""Load audio and return as torch tensor"""
|
| 51 |
-
samples, sr = load_audio(audio_path, target_sr)
|
| 52 |
-
if TORCH_AVAILABLE:
|
| 53 |
-
return torch.from_numpy(samples).float()
|
| 54 |
-
else:
|
| 55 |
-
raise ImportError("PyTorch is required for load_audio_torch")
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
def extract_advanced_features(audio_path: str, sample_rate: int = 16000) -> dict:
|
| 59 |
-
"""Extract advanced features using librosa (Flux, MFCC)"""
|
| 60 |
-
import librosa
|
| 61 |
-
try:
|
| 62 |
-
# Load short segment for speed (max 10s)
|
| 63 |
-
y, sr = librosa.load(audio_path, duration=10, sr=sample_rate)
|
| 64 |
-
|
| 65 |
-
# Spectral Flux (Change in spectrum over time)
|
| 66 |
-
onset_env = librosa.onset.onset_strength(y=y, sr=sr)
|
| 67 |
-
flux = float(np.mean(onset_env))
|
| 68 |
-
|
| 69 |
-
# MFCC Variance (Timbre complexity)
|
| 70 |
-
mfcc = librosa.feature.mfcc(y=y, sr=sr, n_mfcc=13)
|
| 71 |
-
mfcc_var = float(np.mean(np.var(mfcc, axis=1)))
|
| 72 |
-
|
| 73 |
-
return {"spectral_flux": flux, "mfcc_variance": mfcc_var}
|
| 74 |
-
except Exception as e:
|
| 75 |
-
logger.error(f"Error extracting advanced features: {e}")
|
| 76 |
-
return {"spectral_flux": 0.0, "mfcc_variance": 0.0}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/config.py
DELETED
|
@@ -1,67 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
AI Voice Detection System Configuration
|
| 3 |
-
"""
|
| 4 |
-
import os
|
| 5 |
-
from pathlib import Path
|
| 6 |
-
|
| 7 |
-
# Base paths
|
| 8 |
-
BASE_DIR = Path(__file__).parent.parent
|
| 9 |
-
WEIGHTS_DIR = BASE_DIR / "weights"
|
| 10 |
-
CACHE_DIR = BASE_DIR / ".cache"
|
| 11 |
-
|
| 12 |
-
# Create directories
|
| 13 |
-
WEIGHTS_DIR.mkdir(exist_ok=True)
|
| 14 |
-
CACHE_DIR.mkdir(exist_ok=True)
|
| 15 |
-
|
| 16 |
-
# Model configurations
|
| 17 |
-
class ModelConfig:
|
| 18 |
-
# IndicWav2Vec - Best for Indian languages
|
| 19 |
-
INDICWAV2VEC_MODEL = "ai4bharat/indicwav2vec-hindi"
|
| 20 |
-
|
| 21 |
-
# Multilingual Wav2Vec2 as fallback
|
| 22 |
-
XLSR_MODEL = "facebook/wav2vec2-large-xlsr-53"
|
| 23 |
-
|
| 24 |
-
# Language detection
|
| 25 |
-
LANG_ID_MODEL = "speechbrain/lang-id-voxlingua107"
|
| 26 |
-
|
| 27 |
-
# Audio settings
|
| 28 |
-
SAMPLE_RATE = 16000
|
| 29 |
-
MAX_AUDIO_LENGTH = 60 # seconds
|
| 30 |
-
MIN_AUDIO_LENGTH = 1 # seconds
|
| 31 |
-
|
| 32 |
-
# Detection thresholds
|
| 33 |
-
CONFIDENCE_THRESHOLD = 0.7
|
| 34 |
-
ENSEMBLE_WEIGHTS = {
|
| 35 |
-
"wav2vec": 0.5,
|
| 36 |
-
"spectrogram_cnn": 0.3,
|
| 37 |
-
"acoustic_rules": 0.2
|
| 38 |
-
}
|
| 39 |
-
|
| 40 |
-
# Supported languages
|
| 41 |
-
SUPPORTED_LANGUAGES = {
|
| 42 |
-
"ta": "Tamil",
|
| 43 |
-
"en": "English",
|
| 44 |
-
"hi": "Hindi",
|
| 45 |
-
"ml": "Malayalam",
|
| 46 |
-
"te": "Telugu"
|
| 47 |
-
}
|
| 48 |
-
|
| 49 |
-
# AI Tools signatures for detection
|
| 50 |
-
AI_TOOL_SIGNATURES = {
|
| 51 |
-
"nvidia_personaplex": {
|
| 52 |
-
"description": "NVIDIA PersonaPlex/Riva TTS",
|
| 53 |
-
"markers": ["hifi_gan_vocoder", "low_latency_synthesis"]
|
| 54 |
-
},
|
| 55 |
-
"elevenlabs": {
|
| 56 |
-
"description": "ElevenLabs Voice Synthesis",
|
| 57 |
-
"markers": ["multilingual_v2", "voice_cloning"]
|
| 58 |
-
},
|
| 59 |
-
"azure_tts": {
|
| 60 |
-
"description": "Microsoft Azure TTS",
|
| 61 |
-
"markers": ["neural_voice", "ssml_prosody"]
|
| 62 |
-
},
|
| 63 |
-
"google_tts": {
|
| 64 |
-
"description": "Google Cloud TTS",
|
| 65 |
-
"markers": ["wavenet", "neural2"]
|
| 66 |
-
}
|
| 67 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/main.py
DELETED
|
@@ -1,107 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
AI Voice Detection API - Main Application
|
| 3 |
-
|
| 4 |
-
FastAPI application for detecting AI-generated voices across
|
| 5 |
-
multiple Indian languages with NVIDIA PersonaPlex detection.
|
| 6 |
-
"""
|
| 7 |
-
from fastapi import FastAPI, Request
|
| 8 |
-
from fastapi.middleware.cors import CORSMiddleware
|
| 9 |
-
from fastapi.responses import JSONResponse
|
| 10 |
-
import time
|
| 11 |
-
import logging
|
| 12 |
-
|
| 13 |
-
from .api.routes import router as api_router
|
| 14 |
-
|
| 15 |
-
logging.basicConfig(
|
| 16 |
-
level=logging.INFO,
|
| 17 |
-
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
| 18 |
-
)
|
| 19 |
-
logger = logging.getLogger(__name__)
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
app = FastAPI(
|
| 23 |
-
title="AI Voice Detection API",
|
| 24 |
-
description="API for detecting AI-generated voices using NVIDIA PersonaPlex signature matching and acoustic analysis.",
|
| 25 |
-
version="1.0.0",
|
| 26 |
-
docs_url="/docs",
|
| 27 |
-
redoc_url="/redoc",
|
| 28 |
-
contact={
|
| 29 |
-
"name": "AI Voice Detection Team",
|
| 30 |
-
"email": "contact@example.com"
|
| 31 |
-
},
|
| 32 |
-
license_info={
|
| 33 |
-
"name": "MIT License"
|
| 34 |
-
}
|
| 35 |
-
)
|
| 36 |
-
|
| 37 |
-
app.add_middleware(
|
| 38 |
-
CORSMiddleware,
|
| 39 |
-
allow_origins=["*"],
|
| 40 |
-
allow_credentials=True,
|
| 41 |
-
allow_methods=["*"],
|
| 42 |
-
allow_headers=["*"],
|
| 43 |
-
)
|
| 44 |
-
|
| 45 |
-
# Increase max request body size (50MB for audio files)
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
@app.get("/")
|
| 50 |
-
async def root():
|
| 51 |
-
"""Root endpoint for health checks"""
|
| 52 |
-
return {"status": "ok", "message": "AI Voice Detection API"}
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
@app.middleware("http")
|
| 56 |
-
async def add_timing_header(request: Request, call_next):
|
| 57 |
-
"""Add response timing header"""
|
| 58 |
-
start_time = time.time()
|
| 59 |
-
response = await call_next(request)
|
| 60 |
-
process_time = time.time() - start_time
|
| 61 |
-
response.headers["X-Process-Time"] = f"{process_time:.4f}"
|
| 62 |
-
return response
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
@app.exception_handler(Exception)
|
| 66 |
-
async def global_exception_handler(request: Request, exc: Exception):
|
| 67 |
-
"""Handle unexpected exceptions"""
|
| 68 |
-
logger.error(f"Unexpected error: {exc}", exc_info=True)
|
| 69 |
-
return JSONResponse(
|
| 70 |
-
status_code=500,
|
| 71 |
-
content={
|
| 72 |
-
"error": "Internal server error",
|
| 73 |
-
"detail": str(exc),
|
| 74 |
-
"code": "INTERNAL_ERROR"
|
| 75 |
-
}
|
| 76 |
-
)
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
app.include_router(api_router)
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
@app.get("/", tags=["Root"])
|
| 83 |
-
async def root():
|
| 84 |
-
"""
|
| 85 |
-
Root endpoint with API information.
|
| 86 |
-
"""
|
| 87 |
-
return {
|
| 88 |
-
"name": "AI Voice Detection API",
|
| 89 |
-
"version": "1.0.0",
|
| 90 |
-
"description": "Detect AI-generated voices in multiple languages",
|
| 91 |
-
"docs": "/docs",
|
| 92 |
-
"health": "/api/v1/health",
|
| 93 |
-
"detect": "/api/v1/detect"
|
| 94 |
-
}
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
@app.on_event("startup")
|
| 98 |
-
async def startup_event():
|
| 99 |
-
"""Initialize models on startup"""
|
| 100 |
-
logger.info("Starting AI Voice Detection API...")
|
| 101 |
-
logger.info("Models will be loaded on first request (lazy loading)")
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
@app.on_event("shutdown")
|
| 105 |
-
async def shutdown_event():
|
| 106 |
-
"""Cleanup on shutdown"""
|
| 107 |
-
logger.info("Shutting down AI Voice Detection API...")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/models/__init__.py
DELETED
|
@@ -1,17 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Models Package - AI Voice Detection Models
|
| 3 |
-
"""
|
| 4 |
-
from .wav2vec_detector import Wav2VecDetector, IndicWav2VecDetector
|
| 5 |
-
from .spectrogram_cnn import SpectrogramDetector, SpectrogramCNN
|
| 6 |
-
from .personaplex_detector import PersonaPlexDetector
|
| 7 |
-
from .ensemble_detector import EnsembleVoiceDetector, create_detector
|
| 8 |
-
|
| 9 |
-
__all__ = [
|
| 10 |
-
"Wav2VecDetector",
|
| 11 |
-
"IndicWav2VecDetector",
|
| 12 |
-
"SpectrogramDetector",
|
| 13 |
-
"SpectrogramCNN",
|
| 14 |
-
"PersonaPlexDetector",
|
| 15 |
-
"EnsembleVoiceDetector",
|
| 16 |
-
"create_detector"
|
| 17 |
-
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/models/ensemble_detector.py
DELETED
|
@@ -1,310 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Ensemble Voice Detector
|
| 3 |
-
|
| 4 |
-
Combines multiple detection methods (Wav2Vec2, Spectrogram CNN, PersonaPlex)
|
| 5 |
-
with weighted fusion for robust AI voice detection.
|
| 6 |
-
"""
|
| 7 |
-
import torch
|
| 8 |
-
import numpy as np
|
| 9 |
-
from typing import Dict, List, Optional, Any
|
| 10 |
-
from concurrent.futures import ThreadPoolExecutor
|
| 11 |
-
import traceback
|
| 12 |
-
import logging
|
| 13 |
-
|
| 14 |
-
logger = logging.getLogger(__name__)
|
| 15 |
-
|
| 16 |
-
from .wav2vec_detector import Wav2VecDetector, IndicWav2VecDetector
|
| 17 |
-
from .spectrogram_cnn import SpectrogramDetector
|
| 18 |
-
from .personaplex_detector import PersonaPlexDetector
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
class EnsembleVoiceDetector:
|
| 22 |
-
"""
|
| 23 |
-
Ensemble detector that combines multiple AI voice detection methods.
|
| 24 |
-
|
| 25 |
-
Components:
|
| 26 |
-
1. Wav2Vec2/IndicWav2Vec - Deep acoustic pattern analysis
|
| 27 |
-
2. Spectrogram CNN - Visual pattern detection in mel spectrograms
|
| 28 |
-
3. PersonaPlex Detector - AI tool signature matching
|
| 29 |
-
|
| 30 |
-
Uses weighted fusion to combine predictions for robust detection.
|
| 31 |
-
"""
|
| 32 |
-
|
| 33 |
-
DEFAULT_WEIGHTS = {
|
| 34 |
-
"wav2vec": 0.45, # Primary - best for language-specific patterns
|
| 35 |
-
"spectrogram": 0.35, # Secondary - catches vocoder artifacts
|
| 36 |
-
"personaplex": 0.20 # Tertiary - identifies specific tools
|
| 37 |
-
}
|
| 38 |
-
|
| 39 |
-
def __init__(
|
| 40 |
-
self,
|
| 41 |
-
language: str = "en",
|
| 42 |
-
device: str = None,
|
| 43 |
-
weights: Dict[str, float] = None,
|
| 44 |
-
enable_parallel: bool = True
|
| 45 |
-
):
|
| 46 |
-
"""
|
| 47 |
-
Initialize ensemble detector.
|
| 48 |
-
|
| 49 |
-
Args:
|
| 50 |
-
language: Primary language code (en, hi, ta, te, ml)
|
| 51 |
-
device: Torch device (cuda/cpu)
|
| 52 |
-
weights: Custom weights for ensemble fusion
|
| 53 |
-
enable_parallel: Enable parallel inference for speed
|
| 54 |
-
"""
|
| 55 |
-
self.device = device or ("cuda" if torch.cuda.is_available() else "cpu")
|
| 56 |
-
self.language = language
|
| 57 |
-
self.weights = weights or self.DEFAULT_WEIGHTS
|
| 58 |
-
self.enable_parallel = enable_parallel
|
| 59 |
-
|
| 60 |
-
logger.info(f"[EnsembleDetector] Initializing with language={language}, device={self.device}")
|
| 61 |
-
|
| 62 |
-
self._init_detectors()
|
| 63 |
-
|
| 64 |
-
def _init_detectors(self):
|
| 65 |
-
"""Initialize all component detectors"""
|
| 66 |
-
logger.info("[EnsembleDetector] Loading Wav2Vec detector...")
|
| 67 |
-
try:
|
| 68 |
-
self.wav2vec_detector = IndicWav2VecDetector(
|
| 69 |
-
language=self.language,
|
| 70 |
-
device=self.device
|
| 71 |
-
)
|
| 72 |
-
except Exception as e:
|
| 73 |
-
logger.warning(f"[EnsembleDetector] Warning: IndicWav2Vec failed, using base model: {e}")
|
| 74 |
-
self.wav2vec_detector = Wav2VecDetector(
|
| 75 |
-
model_name="facebook/wav2vec2-base",
|
| 76 |
-
device=self.device
|
| 77 |
-
)
|
| 78 |
-
|
| 79 |
-
logger.info("[EnsembleDetector] Loading Spectrogram detector...")
|
| 80 |
-
self.spectrogram_detector = SpectrogramDetector(device=self.device)
|
| 81 |
-
|
| 82 |
-
logger.info("[EnsembleDetector] Loading PersonaPlex detector...")
|
| 83 |
-
self.personaplex_detector = PersonaPlexDetector()
|
| 84 |
-
|
| 85 |
-
logger.info("[EnsembleDetector] All detectors loaded successfully!")
|
| 86 |
-
|
| 87 |
-
def _run_detector(self, detector_name: str, audio_path: str) -> Dict:
|
| 88 |
-
"""Run a single detector with error handling"""
|
| 89 |
-
try:
|
| 90 |
-
if detector_name == "wav2vec":
|
| 91 |
-
return self.wav2vec_detector.detect(audio_path)
|
| 92 |
-
elif detector_name == "spectrogram":
|
| 93 |
-
return self.spectrogram_detector.detect(audio_path)
|
| 94 |
-
elif detector_name == "personaplex":
|
| 95 |
-
return self.personaplex_detector.detect(audio_path)
|
| 96 |
-
else:
|
| 97 |
-
raise ValueError(f"Unknown detector: {detector_name}")
|
| 98 |
-
except Exception as e:
|
| 99 |
-
logger.error(f"[EnsembleDetector] Error in {detector_name}: {e}")
|
| 100 |
-
traceback.print_exc()
|
| 101 |
-
return {
|
| 102 |
-
"classification": "unknown",
|
| 103 |
-
"confidence": 0.0,
|
| 104 |
-
"model_scores": {"ai_probability": 0.5, "human_probability": 0.5},
|
| 105 |
-
"error": str(e)
|
| 106 |
-
}
|
| 107 |
-
|
| 108 |
-
def detect(self, audio_path: str) -> Dict:
|
| 109 |
-
"""
|
| 110 |
-
Perform ensemble detection on audio file.
|
| 111 |
-
|
| 112 |
-
Args:
|
| 113 |
-
audio_path: Path to audio file
|
| 114 |
-
|
| 115 |
-
Returns:
|
| 116 |
-
Comprehensive detection result with ensemble fusion
|
| 117 |
-
"""
|
| 118 |
-
results = {}
|
| 119 |
-
|
| 120 |
-
# Run detectors (parallel or sequential)
|
| 121 |
-
if self.enable_parallel:
|
| 122 |
-
with ThreadPoolExecutor(max_workers=3) as executor:
|
| 123 |
-
futures = {
|
| 124 |
-
name: executor.submit(self._run_detector, name, audio_path)
|
| 125 |
-
for name in ["wav2vec", "spectrogram", "personaplex"]
|
| 126 |
-
}
|
| 127 |
-
results = {name: future.result() for name, future in futures.items()}
|
| 128 |
-
else:
|
| 129 |
-
for name in ["wav2vec", "spectrogram", "personaplex"]:
|
| 130 |
-
results[name] = self._run_detector(name, audio_path)
|
| 131 |
-
|
| 132 |
-
ensemble_result = self._fuse_results(results)
|
| 133 |
-
|
| 134 |
-
ensemble_result["component_results"] = {
|
| 135 |
-
name: {
|
| 136 |
-
"classification": r.get("classification"),
|
| 137 |
-
"confidence": r.get("confidence"),
|
| 138 |
-
"ai_probability": r.get("model_scores", {}).get("ai_probability")
|
| 139 |
-
}
|
| 140 |
-
for name, r in results.items()
|
| 141 |
-
}
|
| 142 |
-
|
| 143 |
-
ensemble_result["detailed_analysis"] = {
|
| 144 |
-
"wav2vec_indicators": results["wav2vec"].get("indicators", []),
|
| 145 |
-
"spectrogram_indicators": results["spectrogram"].get("indicators", []),
|
| 146 |
-
"personaplex_indicators": results["personaplex"].get("indicators", []),
|
| 147 |
-
"detected_tools": results["personaplex"].get("detected_tools", [])
|
| 148 |
-
}
|
| 149 |
-
|
| 150 |
-
return ensemble_result
|
| 151 |
-
|
| 152 |
-
def _fuse_results(self, results: Dict[str, Dict]) -> Dict:
|
| 153 |
-
"""
|
| 154 |
-
Fuse results from multiple detectors using weighted voting.
|
| 155 |
-
|
| 156 |
-
Uses AI probability from each detector weighted by confidence.
|
| 157 |
-
"""
|
| 158 |
-
weighted_ai_prob = 0.0
|
| 159 |
-
total_weight = 0.0
|
| 160 |
-
all_indicators = []
|
| 161 |
-
|
| 162 |
-
for detector_name, result in results.items():
|
| 163 |
-
if "error" in result:
|
| 164 |
-
continue
|
| 165 |
-
|
| 166 |
-
weight = self.weights.get(detector_name, 0.33)
|
| 167 |
-
ai_prob = result.get("model_scores", {}).get("ai_probability", 0.5)
|
| 168 |
-
confidence = result.get("confidence", 0.5)
|
| 169 |
-
|
| 170 |
-
# Weight by both assigned weight and detector confidence
|
| 171 |
-
effective_weight = weight * confidence
|
| 172 |
-
weighted_ai_prob += ai_prob * effective_weight
|
| 173 |
-
total_weight += effective_weight
|
| 174 |
-
|
| 175 |
-
# Collect indicators
|
| 176 |
-
all_indicators.extend(result.get("indicators", []))
|
| 177 |
-
|
| 178 |
-
# Normalize
|
| 179 |
-
if total_weight > 0:
|
| 180 |
-
final_ai_prob = weighted_ai_prob / total_weight
|
| 181 |
-
else:
|
| 182 |
-
final_ai_prob = 0.5
|
| 183 |
-
|
| 184 |
-
# Determine classification
|
| 185 |
-
is_ai = final_ai_prob >= 0.5
|
| 186 |
-
|
| 187 |
-
# Confidence is the probability of the winning class
|
| 188 |
-
base_confidence = final_ai_prob if is_ai else (1.0 - final_ai_prob)
|
| 189 |
-
|
| 190 |
-
# Boost confidence if detectors agree
|
| 191 |
-
agreement = self._calculate_agreement(results)
|
| 192 |
-
if agreement > 0.8:
|
| 193 |
-
# Boost towards 1.0
|
| 194 |
-
confidence = base_confidence + (1.0 - base_confidence) * 0.2
|
| 195 |
-
else:
|
| 196 |
-
confidence = base_confidence
|
| 197 |
-
|
| 198 |
-
# Select top indicators
|
| 199 |
-
unique_indicators = list(dict.fromkeys(all_indicators))[:5]
|
| 200 |
-
|
| 201 |
-
# Determine detected AI tool
|
| 202 |
-
detected_tools = results.get("personaplex", {}).get("detected_tools", [])
|
| 203 |
-
primary_tool = detected_tools[0]["tool_name"] if detected_tools else None
|
| 204 |
-
|
| 205 |
-
return {
|
| 206 |
-
"classification": "ai_generated" if is_ai else "human",
|
| 207 |
-
"confidence": round(confidence, 4),
|
| 208 |
-
"ai_probability": round(final_ai_prob, 4),
|
| 209 |
-
"human_probability": round(1 - final_ai_prob, 4),
|
| 210 |
-
"ai_tool_detected": primary_tool,
|
| 211 |
-
"detector_agreement": round(agreement, 4),
|
| 212 |
-
"indicators": unique_indicators,
|
| 213 |
-
"explanation": self._generate_explanation(
|
| 214 |
-
is_ai, confidence, unique_indicators, primary_tool, results
|
| 215 |
-
)
|
| 216 |
-
}
|
| 217 |
-
|
| 218 |
-
def _calculate_agreement(self, results: Dict[str, Dict]) -> float:
|
| 219 |
-
"""Calculate agreement between detectors (0-1)"""
|
| 220 |
-
classifications = []
|
| 221 |
-
|
| 222 |
-
for result in results.values():
|
| 223 |
-
if "error" not in result:
|
| 224 |
-
classifications.append(result.get("classification") == "ai_generated")
|
| 225 |
-
|
| 226 |
-
if not classifications:
|
| 227 |
-
return 0.0
|
| 228 |
-
|
| 229 |
-
# Agreement is proportion of detectors that agree with majority
|
| 230 |
-
ai_count = sum(classifications)
|
| 231 |
-
human_count = len(classifications) - ai_count
|
| 232 |
-
majority_count = max(ai_count, human_count)
|
| 233 |
-
|
| 234 |
-
return majority_count / len(classifications)
|
| 235 |
-
|
| 236 |
-
def _generate_explanation(
|
| 237 |
-
self,
|
| 238 |
-
is_ai: bool,
|
| 239 |
-
confidence: float,
|
| 240 |
-
indicators: List[str],
|
| 241 |
-
tool_detected: Optional[str],
|
| 242 |
-
results: Dict
|
| 243 |
-
) -> Dict:
|
| 244 |
-
"""Generate comprehensive explanation for the detection"""
|
| 245 |
-
|
| 246 |
-
# Determine confidence level
|
| 247 |
-
if confidence >= 0.8:
|
| 248 |
-
confidence_level = "high"
|
| 249 |
-
summary_prefix = "Strong evidence"
|
| 250 |
-
elif confidence >= 0.6:
|
| 251 |
-
confidence_level = "medium"
|
| 252 |
-
summary_prefix = "Moderate evidence"
|
| 253 |
-
else:
|
| 254 |
-
confidence_level = "low"
|
| 255 |
-
summary_prefix = "Weak evidence"
|
| 256 |
-
|
| 257 |
-
if is_ai:
|
| 258 |
-
summary = f"{summary_prefix} of AI-generated voice detected"
|
| 259 |
-
if tool_detected:
|
| 260 |
-
summary += f" (likely {tool_detected})"
|
| 261 |
-
else:
|
| 262 |
-
summary = f"{summary_prefix} suggests this is authentic human speech"
|
| 263 |
-
|
| 264 |
-
# Technical details
|
| 265 |
-
technical_details = {
|
| 266 |
-
"spectral_artifacts": [],
|
| 267 |
-
"temporal_patterns": [],
|
| 268 |
-
"synthesis_markers": []
|
| 269 |
-
}
|
| 270 |
-
|
| 271 |
-
# Categorize indicators
|
| 272 |
-
for ind in indicators:
|
| 273 |
-
ind_lower = ind.lower()
|
| 274 |
-
if any(kw in ind_lower for kw in ["spectral", "frequency", "band", "harmonic"]):
|
| 275 |
-
technical_details["spectral_artifacts"].append(ind)
|
| 276 |
-
elif any(kw in ind_lower for kw in ["temporal", "time", "variation", "smooth"]):
|
| 277 |
-
technical_details["temporal_patterns"].append(ind)
|
| 278 |
-
elif any(kw in ind_lower for kw in ["vocoder", "synthesis", "signature", "neural"]):
|
| 279 |
-
technical_details["synthesis_markers"].append(ind)
|
| 280 |
-
else:
|
| 281 |
-
# Default to synthesis markers for AI indicators
|
| 282 |
-
if is_ai:
|
| 283 |
-
technical_details["synthesis_markers"].append(ind)
|
| 284 |
-
else:
|
| 285 |
-
technical_details["temporal_patterns"].append(ind)
|
| 286 |
-
|
| 287 |
-
return {
|
| 288 |
-
"summary": summary,
|
| 289 |
-
"confidence_level": confidence_level,
|
| 290 |
-
"technical_details": technical_details,
|
| 291 |
-
"key_indicators": indicators[:3],
|
| 292 |
-
"model_contributions": {
|
| 293 |
-
name: round(self.weights[name], 2)
|
| 294 |
-
for name in self.weights
|
| 295 |
-
}
|
| 296 |
-
}
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
def create_detector(language: str = "en", device: str = None) -> EnsembleVoiceDetector:
|
| 300 |
-
"""
|
| 301 |
-
Factory function to create an ensemble detector.
|
| 302 |
-
|
| 303 |
-
Args:
|
| 304 |
-
language: Primary language code (en, hi, ta, te, ml)
|
| 305 |
-
device: Torch device (cuda/cpu/auto)
|
| 306 |
-
|
| 307 |
-
Returns:
|
| 308 |
-
Configured EnsembleVoiceDetector instance
|
| 309 |
-
"""
|
| 310 |
-
return EnsembleVoiceDetector(language=language, device=device)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/models/personaplex_detector.py
DELETED
|
@@ -1,282 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
NVIDIA PersonaPlex and AI Tool Signature Detection
|
| 3 |
-
|
| 4 |
-
Detects specific AI voice synthesis tools by analyzing their unique
|
| 5 |
-
acoustic signatures and vocoder fingerprints.
|
| 6 |
-
"""
|
| 7 |
-
import torch
|
| 8 |
-
import numpy as np
|
| 9 |
-
import torchaudio
|
| 10 |
-
import torchaudio.transforms as T
|
| 11 |
-
from typing import Dict, List, Optional, Tuple
|
| 12 |
-
from scipy import signal
|
| 13 |
-
from scipy.fft import fft, fftfreq
|
| 14 |
-
|
| 15 |
-
# Import audio utilities for MP3 support
|
| 16 |
-
from ..audio_utils import load_audio
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
class PersonaPlexDetector:
|
| 20 |
-
"""
|
| 21 |
-
Specialized detector for NVIDIA PersonaPlex and other AI voice synthesis tools.
|
| 22 |
-
|
| 23 |
-
NVIDIA PersonaPlex uses HiFi-GAN vocoder which has characteristic patterns:
|
| 24 |
-
- Specific frequency artifacts in 6-8kHz range
|
| 25 |
-
- Phase correlation patterns from real-time synthesis
|
| 26 |
-
- Low-latency inference artifacts
|
| 27 |
-
"""
|
| 28 |
-
|
| 29 |
-
def __init__(self, sample_rate: int = 16000):
|
| 30 |
-
self.sample_rate = sample_rate
|
| 31 |
-
self.resampler_cache = {}
|
| 32 |
-
|
| 33 |
-
# Known AI tool signatures
|
| 34 |
-
self.signatures = {
|
| 35 |
-
"nvidia_personaplex": {
|
| 36 |
-
"name": "NVIDIA PersonaPlex/Riva",
|
| 37 |
-
"vocoder": "HiFi-GAN",
|
| 38 |
-
"frequency_artifacts": (6000, 8000), # Hz range
|
| 39 |
-
"phase_coherence_threshold": 0.85,
|
| 40 |
-
"spectral_tilt_range": (-2, 0),
|
| 41 |
-
},
|
| 42 |
-
"elevenlabs": {
|
| 43 |
-
"name": "ElevenLabs",
|
| 44 |
-
"vocoder": "Proprietary Neural",
|
| 45 |
-
"frequency_artifacts": (7000, 10000),
|
| 46 |
-
"phase_coherence_threshold": 0.80,
|
| 47 |
-
"spectral_tilt_range": (-3, -1),
|
| 48 |
-
},
|
| 49 |
-
"azure_neural": {
|
| 50 |
-
"name": "Microsoft Azure Neural TTS",
|
| 51 |
-
"vocoder": "Neural Vocoder",
|
| 52 |
-
"frequency_artifacts": (5000, 7000),
|
| 53 |
-
"phase_coherence_threshold": 0.82,
|
| 54 |
-
"spectral_tilt_range": (-2.5, -0.5),
|
| 55 |
-
},
|
| 56 |
-
"google_wavenet": {
|
| 57 |
-
"name": "Google WaveNet/Neural2",
|
| 58 |
-
"vocoder": "WaveNet",
|
| 59 |
-
"frequency_artifacts": (6500, 9000),
|
| 60 |
-
"phase_coherence_threshold": 0.78,
|
| 61 |
-
"spectral_tilt_range": (-2, 0.5),
|
| 62 |
-
}
|
| 63 |
-
}
|
| 64 |
-
|
| 65 |
-
def _load_audio(self, audio_path: str) -> np.ndarray:
|
| 66 |
-
"""Load audio file and return numpy array with MP3 support"""
|
| 67 |
-
# Use audio_utils which supports MP3 via soundfile
|
| 68 |
-
samples, sr = load_audio(audio_path, target_sr=self.sample_rate)
|
| 69 |
-
return samples
|
| 70 |
-
|
| 71 |
-
def _compute_stft(self, audio: np.ndarray) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
| 72 |
-
"""Compute Short-Time Fourier Transform"""
|
| 73 |
-
nperseg = 1024
|
| 74 |
-
noverlap = nperseg // 2
|
| 75 |
-
|
| 76 |
-
frequencies, times, Zxx = signal.stft(
|
| 77 |
-
audio,
|
| 78 |
-
fs=self.sample_rate,
|
| 79 |
-
nperseg=nperseg,
|
| 80 |
-
noverlap=noverlap
|
| 81 |
-
)
|
| 82 |
-
|
| 83 |
-
return frequencies, times, Zxx
|
| 84 |
-
|
| 85 |
-
def _analyze_frequency_artifacts(self, frequencies: np.ndarray, Zxx: np.ndarray) -> Dict:
|
| 86 |
-
"""Analyze frequency-domain artifacts typical of neural vocoders"""
|
| 87 |
-
magnitude = np.abs(Zxx)
|
| 88 |
-
phase = np.angle(Zxx)
|
| 89 |
-
|
| 90 |
-
analysis = {}
|
| 91 |
-
|
| 92 |
-
# Analyze different frequency bands
|
| 93 |
-
for band_name, (low_hz, high_hz) in [
|
| 94 |
-
("low", (0, 2000)),
|
| 95 |
-
("mid", (2000, 5000)),
|
| 96 |
-
("high", (5000, 8000)),
|
| 97 |
-
("very_high", (8000, self.sample_rate // 2))
|
| 98 |
-
]:
|
| 99 |
-
mask = (frequencies >= low_hz) & (frequencies < high_hz)
|
| 100 |
-
if mask.any():
|
| 101 |
-
band_mag = magnitude[mask, :]
|
| 102 |
-
analysis[f"{band_name}_band_energy"] = float(np.mean(band_mag))
|
| 103 |
-
analysis[f"{band_name}_band_std"] = float(np.std(band_mag))
|
| 104 |
-
|
| 105 |
-
# Check for vocoder-specific artifacts in 6-8kHz range
|
| 106 |
-
vocoder_mask = (frequencies >= 6000) & (frequencies <= 8000)
|
| 107 |
-
if vocoder_mask.any():
|
| 108 |
-
vocoder_band = magnitude[vocoder_mask, :]
|
| 109 |
-
|
| 110 |
-
# Neural vocoders often show unnaturally consistent energy in this band
|
| 111 |
-
analysis["vocoder_band_consistency"] = float(
|
| 112 |
-
1.0 - (np.std(vocoder_band.mean(axis=0)) / (np.mean(vocoder_band) + 1e-8))
|
| 113 |
-
)
|
| 114 |
-
|
| 115 |
-
# Check for periodic patterns (grid artifacts)
|
| 116 |
-
fft_of_band = np.abs(fft(vocoder_band.mean(axis=0)))
|
| 117 |
-
analysis["vocoder_periodicity"] = float(
|
| 118 |
-
np.max(fft_of_band[1:20]) / (np.mean(fft_of_band) + 1e-8)
|
| 119 |
-
)
|
| 120 |
-
else:
|
| 121 |
-
analysis["vocoder_band_consistency"] = 0.0
|
| 122 |
-
analysis["vocoder_periodicity"] = 0.0
|
| 123 |
-
|
| 124 |
-
return analysis
|
| 125 |
-
|
| 126 |
-
def _analyze_phase_coherence(self, Zxx: np.ndarray) -> Dict:
|
| 127 |
-
"""
|
| 128 |
-
Analyze phase coherence patterns.
|
| 129 |
-
AI-generated audio often shows higher phase coherence due to deterministic synthesis.
|
| 130 |
-
"""
|
| 131 |
-
phase = np.angle(Zxx)
|
| 132 |
-
|
| 133 |
-
# Compute phase difference between adjacent frames
|
| 134 |
-
phase_diff = np.diff(phase, axis=1)
|
| 135 |
-
|
| 136 |
-
# Unwrap phase differences
|
| 137 |
-
phase_diff = np.mod(phase_diff + np.pi, 2 * np.pi) - np.pi
|
| 138 |
-
|
| 139 |
-
analysis = {
|
| 140 |
-
"phase_coherence": float(1.0 - np.std(phase_diff)),
|
| 141 |
-
"phase_consistency": float(np.mean(np.abs(phase_diff) < 0.5)),
|
| 142 |
-
"phase_periodicity": float(
|
| 143 |
-
np.corrcoef(phase_diff[:, :-1].flatten(), phase_diff[:, 1:].flatten())[0, 1]
|
| 144 |
-
if phase_diff.shape[1] > 1 else 0
|
| 145 |
-
)
|
| 146 |
-
}
|
| 147 |
-
|
| 148 |
-
return analysis
|
| 149 |
-
|
| 150 |
-
def _compute_spectral_tilt(self, frequencies: np.ndarray, magnitude: np.ndarray) -> float:
|
| 151 |
-
"""
|
| 152 |
-
Compute spectral tilt (slope of spectral envelope).
|
| 153 |
-
AI voices often have different spectral tilt than natural voices.
|
| 154 |
-
"""
|
| 155 |
-
# Use frequencies up to 8kHz
|
| 156 |
-
mask = frequencies <= 8000
|
| 157 |
-
freqs = frequencies[mask]
|
| 158 |
-
mags = magnitude[mask, :].mean(axis=1)
|
| 159 |
-
|
| 160 |
-
if len(freqs) < 2:
|
| 161 |
-
return 0.0
|
| 162 |
-
|
| 163 |
-
# Log-log regression for spectral tilt
|
| 164 |
-
log_freqs = np.log(freqs + 1)
|
| 165 |
-
log_mags = np.log(mags + 1e-8)
|
| 166 |
-
|
| 167 |
-
# Simple linear regression
|
| 168 |
-
slope = np.polyfit(log_freqs, log_mags, 1)[0]
|
| 169 |
-
|
| 170 |
-
return float(slope)
|
| 171 |
-
|
| 172 |
-
def _match_signatures(self, freq_analysis: Dict, phase_analysis: Dict, spectral_tilt: float) -> List[Dict]:
|
| 173 |
-
"""Match analysis results against known AI tool signatures"""
|
| 174 |
-
matches = []
|
| 175 |
-
|
| 176 |
-
for tool_id, sig in self.signatures.items():
|
| 177 |
-
score = 0.0
|
| 178 |
-
reasons = []
|
| 179 |
-
|
| 180 |
-
# Check phase coherence
|
| 181 |
-
if phase_analysis["phase_coherence"] >= sig["phase_coherence_threshold"]:
|
| 182 |
-
score += 0.3
|
| 183 |
-
reasons.append(f"Phase coherence matches {sig['vocoder']} pattern")
|
| 184 |
-
|
| 185 |
-
# Check spectral tilt
|
| 186 |
-
tilt_low, tilt_high = sig["spectral_tilt_range"]
|
| 187 |
-
if tilt_low <= spectral_tilt <= tilt_high:
|
| 188 |
-
score += 0.25
|
| 189 |
-
reasons.append("Spectral tilt consistent with synthesis")
|
| 190 |
-
|
| 191 |
-
# Check vocoder band artifacts
|
| 192 |
-
if freq_analysis["vocoder_band_consistency"] > 0.7:
|
| 193 |
-
score += 0.25
|
| 194 |
-
reasons.append("Vocoder frequency artifacts detected")
|
| 195 |
-
|
| 196 |
-
# Check periodicity (neural vocoder grid)
|
| 197 |
-
if freq_analysis["vocoder_periodicity"] > 1.5:
|
| 198 |
-
score += 0.2
|
| 199 |
-
reasons.append("Periodic synthesis patterns found")
|
| 200 |
-
|
| 201 |
-
if score > 0.4: # Threshold for reporting
|
| 202 |
-
matches.append({
|
| 203 |
-
"tool_id": tool_id,
|
| 204 |
-
"tool_name": sig["name"],
|
| 205 |
-
"confidence": score,
|
| 206 |
-
"reasons": reasons
|
| 207 |
-
})
|
| 208 |
-
|
| 209 |
-
# Sort by confidence
|
| 210 |
-
matches.sort(key=lambda x: x["confidence"], reverse=True)
|
| 211 |
-
|
| 212 |
-
return matches
|
| 213 |
-
|
| 214 |
-
def detect(self, audio_path: str) -> Dict:
|
| 215 |
-
"""
|
| 216 |
-
Detect AI voice synthesis tool signatures.
|
| 217 |
-
|
| 218 |
-
Args:
|
| 219 |
-
audio_path: Path to audio file
|
| 220 |
-
|
| 221 |
-
Returns:
|
| 222 |
-
Dictionary with detection results including matched tools
|
| 223 |
-
"""
|
| 224 |
-
audio = self._load_audio(audio_path)
|
| 225 |
-
|
| 226 |
-
frequencies, times, Zxx = self._compute_stft(audio)
|
| 227 |
-
magnitude = np.abs(Zxx)
|
| 228 |
-
|
| 229 |
-
freq_analysis = self._analyze_frequency_artifacts(frequencies, Zxx)
|
| 230 |
-
|
| 231 |
-
phase_analysis = self._analyze_phase_coherence(Zxx)
|
| 232 |
-
|
| 233 |
-
spectral_tilt = self._compute_spectral_tilt(frequencies, magnitude)
|
| 234 |
-
|
| 235 |
-
matched_tools = self._match_signatures(freq_analysis, phase_analysis, spectral_tilt)
|
| 236 |
-
|
| 237 |
-
is_ai = len(matched_tools) > 0 and matched_tools[0]["confidence"] > 0.5
|
| 238 |
-
|
| 239 |
-
ai_probability = max([m["confidence"] for m in matched_tools]) if matched_tools else 0.2
|
| 240 |
-
|
| 241 |
-
result = {
|
| 242 |
-
"classification": "ai_generated" if is_ai else "human",
|
| 243 |
-
"confidence": ai_probability if is_ai else (1 - ai_probability),
|
| 244 |
-
"model_scores": {
|
| 245 |
-
"ai_probability": ai_probability,
|
| 246 |
-
"human_probability": 1 - ai_probability
|
| 247 |
-
},
|
| 248 |
-
"detected_tools": matched_tools[:3] if matched_tools else [],
|
| 249 |
-
"primary_tool": matched_tools[0]["tool_name"] if matched_tools else None,
|
| 250 |
-
"frequency_analysis": freq_analysis,
|
| 251 |
-
"phase_analysis": phase_analysis,
|
| 252 |
-
"spectral_tilt": spectral_tilt,
|
| 253 |
-
"indicators": self._generate_indicators(is_ai, matched_tools, freq_analysis, phase_analysis)
|
| 254 |
-
}
|
| 255 |
-
|
| 256 |
-
return result
|
| 257 |
-
|
| 258 |
-
def _generate_indicators(
|
| 259 |
-
self,
|
| 260 |
-
is_ai: bool,
|
| 261 |
-
matched_tools: List[Dict],
|
| 262 |
-
freq_analysis: Dict,
|
| 263 |
-
phase_analysis: Dict
|
| 264 |
-
) -> List[str]:
|
| 265 |
-
"""Generate human-readable indicators"""
|
| 266 |
-
indicators = []
|
| 267 |
-
|
| 268 |
-
if is_ai and matched_tools:
|
| 269 |
-
top_match = matched_tools[0]
|
| 270 |
-
indicators.append(f"Signature matches {top_match['tool_name']} ({top_match['confidence']:.0%} confidence)")
|
| 271 |
-
indicators.extend(top_match["reasons"][:2])
|
| 272 |
-
elif is_ai:
|
| 273 |
-
if phase_analysis["phase_coherence"] > 0.7:
|
| 274 |
-
indicators.append("Unusually high phase coherence suggesting deterministic synthesis")
|
| 275 |
-
if freq_analysis["vocoder_band_consistency"] > 0.6:
|
| 276 |
-
indicators.append("Vocoder artifacts detected in 6-8kHz band")
|
| 277 |
-
else:
|
| 278 |
-
indicators.append("No known AI synthesis tool signatures detected")
|
| 279 |
-
if phase_analysis["phase_coherence"] < 0.6:
|
| 280 |
-
indicators.append("Phase patterns consistent with natural recording")
|
| 281 |
-
|
| 282 |
-
return indicators
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/models/spectrogram_cnn.py
DELETED
|
@@ -1,349 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Mel Spectrogram CNN Classifier for Voice Deepfake Detection
|
| 3 |
-
|
| 4 |
-
Uses CNN to analyze mel spectrograms for visual artifacts
|
| 5 |
-
indicative of AI-generated speech (vocoder patterns, unnatural harmonics).
|
| 6 |
-
"""
|
| 7 |
-
import torch
|
| 8 |
-
import torch.nn as nn
|
| 9 |
-
import torch.nn.functional as F
|
| 10 |
-
import numpy as np
|
| 11 |
-
import torchaudio
|
| 12 |
-
import torchaudio.transforms as T
|
| 13 |
-
from typing import Dict, Tuple, Optional
|
| 14 |
-
import logging
|
| 15 |
-
|
| 16 |
-
logger = logging.getLogger(__name__)
|
| 17 |
-
|
| 18 |
-
# Import audio utilities for MP3 support
|
| 19 |
-
from ..audio_utils import load_audio_torch, extract_advanced_features
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
class SpectrogramCNN(nn.Module):
|
| 23 |
-
"""
|
| 24 |
-
CNN architecture for analyzing mel spectrograms.
|
| 25 |
-
Inspired by ResNet but optimized for audio deepfake detection.
|
| 26 |
-
"""
|
| 27 |
-
|
| 28 |
-
def __init__(self, num_classes: int = 2):
|
| 29 |
-
super().__init__()
|
| 30 |
-
|
| 31 |
-
# Initial convolution
|
| 32 |
-
self.conv1 = nn.Sequential(
|
| 33 |
-
nn.Conv2d(1, 32, kernel_size=7, stride=2, padding=3),
|
| 34 |
-
nn.BatchNorm2d(32),
|
| 35 |
-
nn.ReLU(),
|
| 36 |
-
nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
|
| 37 |
-
)
|
| 38 |
-
|
| 39 |
-
# Residual-style blocks
|
| 40 |
-
self.block1 = self._make_block(32, 64)
|
| 41 |
-
self.block2 = self._make_block(64, 128)
|
| 42 |
-
self.block3 = self._make_block(128, 256)
|
| 43 |
-
|
| 44 |
-
# Attention mechanism for focusing on relevant frequency bands
|
| 45 |
-
self.attention = nn.Sequential(
|
| 46 |
-
nn.AdaptiveAvgPool2d(1),
|
| 47 |
-
nn.Flatten(),
|
| 48 |
-
nn.Linear(256, 64),
|
| 49 |
-
nn.ReLU(),
|
| 50 |
-
nn.Linear(64, 256),
|
| 51 |
-
nn.Sigmoid()
|
| 52 |
-
)
|
| 53 |
-
|
| 54 |
-
# Global pooling and classifier
|
| 55 |
-
self.global_pool = nn.AdaptiveAvgPool2d(1)
|
| 56 |
-
self.classifier = nn.Sequential(
|
| 57 |
-
nn.Flatten(),
|
| 58 |
-
nn.Dropout(0.3),
|
| 59 |
-
nn.Linear(256, 128),
|
| 60 |
-
nn.ReLU(),
|
| 61 |
-
nn.Dropout(0.2),
|
| 62 |
-
nn.Linear(128, num_classes)
|
| 63 |
-
)
|
| 64 |
-
|
| 65 |
-
self._initialize_weights()
|
| 66 |
-
|
| 67 |
-
def _make_block(self, in_channels: int, out_channels: int) -> nn.Module:
|
| 68 |
-
"""Create a convolutional block with skip connection"""
|
| 69 |
-
return nn.Sequential(
|
| 70 |
-
nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=2, padding=1),
|
| 71 |
-
nn.BatchNorm2d(out_channels),
|
| 72 |
-
nn.ReLU(),
|
| 73 |
-
nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1),
|
| 74 |
-
nn.BatchNorm2d(out_channels),
|
| 75 |
-
nn.ReLU()
|
| 76 |
-
)
|
| 77 |
-
|
| 78 |
-
def _initialize_weights(self):
|
| 79 |
-
"""Initialize weights with Xavier uniform"""
|
| 80 |
-
for module in self.modules():
|
| 81 |
-
if isinstance(module, nn.Conv2d):
|
| 82 |
-
nn.init.kaiming_normal_(module.weight, mode='fan_out', nonlinearity='relu')
|
| 83 |
-
elif isinstance(module, nn.BatchNorm2d):
|
| 84 |
-
nn.init.constant_(module.weight, 1)
|
| 85 |
-
nn.init.constant_(module.bias, 0)
|
| 86 |
-
elif isinstance(module, nn.Linear):
|
| 87 |
-
nn.init.xavier_uniform_(module.weight)
|
| 88 |
-
if module.bias is not None:
|
| 89 |
-
nn.init.zeros_(module.bias)
|
| 90 |
-
|
| 91 |
-
def forward(self, x: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
|
| 92 |
-
"""
|
| 93 |
-
Forward pass returning logits and attention weights.
|
| 94 |
-
|
| 95 |
-
Args:
|
| 96 |
-
x: Input mel spectrogram [B, 1, H, W]
|
| 97 |
-
|
| 98 |
-
Returns:
|
| 99 |
-
logits: Classification logits [B, 2]
|
| 100 |
-
attention_weights: Frequency attention weights [B, 256]
|
| 101 |
-
"""
|
| 102 |
-
# Feature extraction
|
| 103 |
-
x = self.conv1(x)
|
| 104 |
-
x = self.block1(x)
|
| 105 |
-
x = self.block2(x)
|
| 106 |
-
x = self.block3(x)
|
| 107 |
-
|
| 108 |
-
# Attention
|
| 109 |
-
attn = self.attention(x)
|
| 110 |
-
x = x * attn.unsqueeze(-1).unsqueeze(-1)
|
| 111 |
-
|
| 112 |
-
# Classification
|
| 113 |
-
x = self.global_pool(x)
|
| 114 |
-
logits = self.classifier(x)
|
| 115 |
-
|
| 116 |
-
return logits, attn
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
class SpectrogramDetector:
|
| 120 |
-
"""
|
| 121 |
-
Mel Spectrogram-based detector for AI-generated voice detection.
|
| 122 |
-
|
| 123 |
-
Converts audio to mel spectrograms and uses CNN to detect
|
| 124 |
-
visual patterns indicative of neural vocoders.
|
| 125 |
-
"""
|
| 126 |
-
|
| 127 |
-
def __init__(self, device: str = None):
|
| 128 |
-
self.device = device or ("cuda" if torch.cuda.is_available() else "cpu")
|
| 129 |
-
|
| 130 |
-
logger.info(f"[SpectrogramDetector] Using device: {self.device}")
|
| 131 |
-
|
| 132 |
-
# Initialize CNN model
|
| 133 |
-
self.model = SpectrogramCNN(num_classes=2)
|
| 134 |
-
self.model.to(self.device)
|
| 135 |
-
self.model.eval()
|
| 136 |
-
|
| 137 |
-
# Mel spectrogram parameters
|
| 138 |
-
self.sample_rate = 16000
|
| 139 |
-
self.n_mels = 128
|
| 140 |
-
self.n_fft = 1024
|
| 141 |
-
self.hop_length = 256
|
| 142 |
-
self.target_length = 128 # Fixed width for CNN input
|
| 143 |
-
|
| 144 |
-
# Mel transform
|
| 145 |
-
self.mel_transform = T.MelSpectrogram(
|
| 146 |
-
sample_rate=self.sample_rate,
|
| 147 |
-
n_fft=self.n_fft,
|
| 148 |
-
hop_length=self.hop_length,
|
| 149 |
-
n_mels=self.n_mels
|
| 150 |
-
)
|
| 151 |
-
|
| 152 |
-
self.resampler_cache = {}
|
| 153 |
-
|
| 154 |
-
def _load_audio(self, audio_path: str) -> torch.Tensor:
|
| 155 |
-
"""Load and resample audio with MP3 support"""
|
| 156 |
-
# Use audio_utils which supports MP3 via soundfile
|
| 157 |
-
waveform = load_audio_torch(audio_path, target_sr=self.sample_rate)
|
| 158 |
-
return waveform.unsqueeze(0) # Add channel dim
|
| 159 |
-
|
| 160 |
-
def _create_mel_spectrogram(self, waveform: torch.Tensor) -> torch.Tensor:
|
| 161 |
-
"""Convert waveform to normalized mel spectrogram"""
|
| 162 |
-
# Compute mel spectrogram
|
| 163 |
-
mel_spec = self.mel_transform(waveform)
|
| 164 |
-
|
| 165 |
-
# Convert to log scale (dB)
|
| 166 |
-
mel_spec = torch.log(mel_spec + 1e-9)
|
| 167 |
-
|
| 168 |
-
# Normalize
|
| 169 |
-
mel_spec = (mel_spec - mel_spec.mean()) / (mel_spec.std() + 1e-8)
|
| 170 |
-
|
| 171 |
-
# Resize to fixed width
|
| 172 |
-
if mel_spec.shape[-1] != self.target_length:
|
| 173 |
-
mel_spec = F.interpolate(
|
| 174 |
-
mel_spec.unsqueeze(0),
|
| 175 |
-
size=(self.n_mels, self.target_length),
|
| 176 |
-
mode='bilinear',
|
| 177 |
-
align_corners=False
|
| 178 |
-
).squeeze(0)
|
| 179 |
-
|
| 180 |
-
return mel_spec
|
| 181 |
-
|
| 182 |
-
def _analyze_spectrogram(self, mel_spec: torch.Tensor) -> Dict:
|
| 183 |
-
"""Analyze spectrogram for AI-typical patterns"""
|
| 184 |
-
spec = mel_spec.squeeze().numpy()
|
| 185 |
-
|
| 186 |
-
analysis = {}
|
| 187 |
-
|
| 188 |
-
# Check for unnaturally smooth regions
|
| 189 |
-
gradient = np.gradient(spec, axis=1)
|
| 190 |
-
analysis["temporal_smoothness"] = float(1.0 / (np.std(gradient) + 1e-8))
|
| 191 |
-
|
| 192 |
-
# Check frequency band energy distribution
|
| 193 |
-
low_band = spec[:32, :].mean()
|
| 194 |
-
mid_band = spec[32:96, :].mean()
|
| 195 |
-
high_band = spec[96:, :].mean()
|
| 196 |
-
|
| 197 |
-
analysis["low_band_energy"] = float(low_band)
|
| 198 |
-
analysis["mid_band_energy"] = float(mid_band)
|
| 199 |
-
analysis["high_band_energy"] = float(high_band)
|
| 200 |
-
|
| 201 |
-
# Check for vocoder grid patterns (common in neural TTS)
|
| 202 |
-
fft_spec = np.abs(np.fft.fft2(spec))
|
| 203 |
-
analysis["periodicity_score"] = float(fft_spec[1:10, 1:10].mean() / fft_spec.mean())
|
| 204 |
-
|
| 205 |
-
# Harmonic-to-noise ratio approximation
|
| 206 |
-
sorted_spec = np.sort(spec.flatten())[::-1]
|
| 207 |
-
top_10_pct = sorted_spec[:int(len(sorted_spec) * 0.1)].mean()
|
| 208 |
-
bottom_50_pct = sorted_spec[int(len(sorted_spec) * 0.5):].mean()
|
| 209 |
-
analysis["hnr_approx"] = float(top_10_pct / (bottom_50_pct + 1e-8))
|
| 210 |
-
|
| 211 |
-
return analysis
|
| 212 |
-
|
| 213 |
-
def detect(self, audio_path: str) -> Dict:
|
| 214 |
-
"""
|
| 215 |
-
Detect if audio is AI-generated using spectrogram analysis.
|
| 216 |
-
|
| 217 |
-
Args:
|
| 218 |
-
audio_path: Path to audio file
|
| 219 |
-
|
| 220 |
-
Returns:
|
| 221 |
-
Dictionary with detection results
|
| 222 |
-
"""
|
| 223 |
-
waveform = self._load_audio(audio_path)
|
| 224 |
-
mel_spec = self._create_mel_spectrogram(waveform)
|
| 225 |
-
|
| 226 |
-
spec_analysis = self._analyze_spectrogram(mel_spec)
|
| 227 |
-
|
| 228 |
-
spec_analysis['energy_cv'] = self._compute_energy_cv(waveform)
|
| 229 |
-
|
| 230 |
-
adv_features = extract_advanced_features(audio_path, self.sample_rate)
|
| 231 |
-
spec_analysis.update(adv_features)
|
| 232 |
-
|
| 233 |
-
ai_score = self._compute_ai_score_from_spectrogram(spec_analysis)
|
| 234 |
-
|
| 235 |
-
ai_score = max(0.0, min(1.0, ai_score))
|
| 236 |
-
|
| 237 |
-
is_ai = ai_score >= 0.5
|
| 238 |
-
confidence = abs(ai_score - 0.5) * 2
|
| 239 |
-
|
| 240 |
-
result = {
|
| 241 |
-
"classification": "ai_generated" if is_ai else "human",
|
| 242 |
-
"confidence": confidence,
|
| 243 |
-
"model_scores": {
|
| 244 |
-
"ai_probability": ai_score,
|
| 245 |
-
"human_probability": 1 - ai_score
|
| 246 |
-
},
|
| 247 |
-
"spectrogram_analysis": spec_analysis,
|
| 248 |
-
"frequency_attention": [],
|
| 249 |
-
"indicators": self._generate_indicators(1 if is_ai else 0, spec_analysis)
|
| 250 |
-
}
|
| 251 |
-
|
| 252 |
-
return result
|
| 253 |
-
|
| 254 |
-
def _compute_energy_cv(self, waveform: torch.Tensor) -> float:
|
| 255 |
-
"""Compute Coefficient of Variation of energy"""
|
| 256 |
-
if waveform.dim() > 1:
|
| 257 |
-
waveform = waveform.squeeze()
|
| 258 |
-
|
| 259 |
-
chunk_size = len(waveform) // 10
|
| 260 |
-
if chunk_size == 0: return 0.0
|
| 261 |
-
|
| 262 |
-
energies = []
|
| 263 |
-
for i in range(10):
|
| 264 |
-
chunk = waveform[i*chunk_size:(i+1)*chunk_size]
|
| 265 |
-
energies.append(float(torch.sqrt(torch.mean(chunk ** 2))))
|
| 266 |
-
|
| 267 |
-
energy_std = np.std(energies) if energies else 0
|
| 268 |
-
energy_mean = np.mean(energies) if energies else 1
|
| 269 |
-
return float(energy_std / (energy_mean + 1e-8))
|
| 270 |
-
|
| 271 |
-
def _compute_ai_score_from_spectrogram(self, analysis: Dict) -> float:
|
| 272 |
-
"""
|
| 273 |
-
Compute AI probability from spectrogram analysis.
|
| 274 |
-
|
| 275 |
-
AI-generated voices typically have:
|
| 276 |
-
- Higher temporal smoothness (consistent spectrum)
|
| 277 |
-
- Low energy variation (consistent volume, less natural pausing)
|
| 278 |
-
- Specific band energy distributions
|
| 279 |
-
"""
|
| 280 |
-
score = 0.5 # Start neutral
|
| 281 |
-
|
| 282 |
-
# 1. Energy CV - STRONGEST INDICATOR (prioritize this)
|
| 283 |
-
# High variation (>0.5) is very typical of human speech (pauses/breathing)
|
| 284 |
-
# Low variation (<0.2) is typical of AI
|
| 285 |
-
energy_cv = analysis.get("energy_cv", 0.25)
|
| 286 |
-
if energy_cv > 0.7:
|
| 287 |
-
score -= 0.30 # Very strong human signal
|
| 288 |
-
elif energy_cv > 0.5:
|
| 289 |
-
score -= 0.20 # Strong human signal
|
| 290 |
-
elif energy_cv > 0.35:
|
| 291 |
-
score -= 0.10 # Moderate human signal
|
| 292 |
-
elif energy_cv < 0.2:
|
| 293 |
-
score += 0.10 # Consistent energy = AI like
|
| 294 |
-
|
| 295 |
-
# 2. Advanced Features
|
| 296 |
-
flux = analysis.get("spectral_flux", 0)
|
| 297 |
-
mfcc_var = analysis.get("mfcc_variance", 0)
|
| 298 |
-
|
| 299 |
-
# Flux Heuristic - widened threshold
|
| 300 |
-
if flux > 2.4:
|
| 301 |
-
score += 0.20 # Strong AI signal
|
| 302 |
-
elif flux > 2.0:
|
| 303 |
-
score += 0.10 # Moderate AI signal
|
| 304 |
-
elif flux < 1.8 and flux > 0.1:
|
| 305 |
-
score -= 0.10 # Natural human transitions
|
| 306 |
-
|
| 307 |
-
# MFCC Var Heuristic
|
| 308 |
-
if mfcc_var > 1900:
|
| 309 |
-
score -= 0.20 # High complexity = human
|
| 310 |
-
|
| 311 |
-
# 3. Temporal smoothness (de-prioritized - often misleading)
|
| 312 |
-
# Only use extreme values
|
| 313 |
-
smoothness = analysis.get("temporal_smoothness", 1.0)
|
| 314 |
-
if smoothness > 5.0:
|
| 315 |
-
score += 0.10 # Very unnaturally smooth
|
| 316 |
-
|
| 317 |
-
# Additional features removed as unreliable for edge cases
|
| 318 |
-
# (periodicity, high_band, hnr can cause false positives on clean recordings)
|
| 319 |
-
|
| 320 |
-
return score
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
def _generate_indicators(self, pred_class: int, analysis: Dict) -> list:
|
| 324 |
-
"""Generate human-readable indicators"""
|
| 325 |
-
indicators = []
|
| 326 |
-
|
| 327 |
-
if pred_class == 1: # AI detected
|
| 328 |
-
if analysis["temporal_smoothness"] > 5:
|
| 329 |
-
indicators.append("Unnaturally smooth spectrogram transitions")
|
| 330 |
-
|
| 331 |
-
if analysis["periodicity_score"] > 2:
|
| 332 |
-
indicators.append("Periodic patterns suggesting neural vocoder")
|
| 333 |
-
|
| 334 |
-
if analysis["high_band_energy"] < -2:
|
| 335 |
-
indicators.append("Reduced high-frequency content typical of TTS")
|
| 336 |
-
|
| 337 |
-
if not indicators:
|
| 338 |
-
indicators.append("Spectrogram patterns consistent with AI synthesis")
|
| 339 |
-
else: # Human
|
| 340 |
-
if analysis["hnr_approx"] > 10:
|
| 341 |
-
indicators.append("Strong harmonic structure of natural voice")
|
| 342 |
-
|
| 343 |
-
if analysis["temporal_smoothness"] < 3:
|
| 344 |
-
indicators.append("Natural variation in spectral features")
|
| 345 |
-
|
| 346 |
-
if not indicators:
|
| 347 |
-
indicators.append("Spectrogram shows natural speech characteristics")
|
| 348 |
-
|
| 349 |
-
return indicators
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/models/wav2vec_detector.py
DELETED
|
@@ -1,338 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Wav2Vec2-based Voice Detector for Indian Languages
|
| 3 |
-
|
| 4 |
-
Uses IndicWav2Vec/XLSR models to detect AI-generated speech by analyzing
|
| 5 |
-
deep acoustic representations learned from raw waveforms.
|
| 6 |
-
"""
|
| 7 |
-
import torch
|
| 8 |
-
import torch.nn as nn
|
| 9 |
-
import numpy as np
|
| 10 |
-
from typing import Dict, Tuple, Optional
|
| 11 |
-
from transformers import AutoModel, AutoProcessor
|
| 12 |
-
import torchaudio
|
| 13 |
-
import torchaudio.transforms as T
|
| 14 |
-
import librosa
|
| 15 |
-
import numpy as np
|
| 16 |
-
import logging
|
| 17 |
-
|
| 18 |
-
logger = logging.getLogger(__name__)
|
| 19 |
-
|
| 20 |
-
# Import audio utilities for MP3 support
|
| 21 |
-
from ..audio_utils import load_audio_torch, extract_advanced_features
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
class Wav2VecClassificationHead(nn.Module):
|
| 25 |
-
"""Classification head for deepfake detection on top of Wav2Vec2"""
|
| 26 |
-
|
| 27 |
-
def __init__(self, hidden_size: int = 768, num_classes: int = 2):
|
| 28 |
-
super().__init__()
|
| 29 |
-
self.classifier = nn.Sequential(
|
| 30 |
-
nn.Linear(hidden_size, 512),
|
| 31 |
-
nn.ReLU(),
|
| 32 |
-
nn.Dropout(0.3),
|
| 33 |
-
nn.Linear(512, 256),
|
| 34 |
-
nn.ReLU(),
|
| 35 |
-
nn.Dropout(0.2),
|
| 36 |
-
nn.Linear(256, num_classes)
|
| 37 |
-
)
|
| 38 |
-
|
| 39 |
-
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
|
| 40 |
-
# Pool over time dimension (mean pooling)
|
| 41 |
-
pooled = hidden_states.mean(dim=1)
|
| 42 |
-
return self.classifier(pooled)
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
class Wav2VecDetector:
|
| 46 |
-
"""
|
| 47 |
-
Wav2Vec2-based detector for AI-generated voice detection.
|
| 48 |
-
|
| 49 |
-
Uses pretrained Wav2Vec2 models (IndicWav2Vec for Indian languages)
|
| 50 |
-
with a fine-tuned classification head for deepfake detection.
|
| 51 |
-
"""
|
| 52 |
-
|
| 53 |
-
def __init__(self, model_name: str = "facebook/wav2vec2-base", device: str = None):
|
| 54 |
-
self.device = device or ("cuda" if torch.cuda.is_available() else "cpu")
|
| 55 |
-
self.model_name = model_name
|
| 56 |
-
|
| 57 |
-
logger.info(f"[Wav2VecDetector] Loading model: {model_name}")
|
| 58 |
-
logger.info(f"[Wav2VecDetector] Using device: {self.device}")
|
| 59 |
-
|
| 60 |
-
# Load pretrained Wav2Vec2 model
|
| 61 |
-
self.processor = AutoProcessor.from_pretrained(model_name)
|
| 62 |
-
self.wav2vec = AutoModel.from_pretrained(model_name)
|
| 63 |
-
self.wav2vec.to(self.device)
|
| 64 |
-
self.wav2vec.eval()
|
| 65 |
-
|
| 66 |
-
# Initialize classification head
|
| 67 |
-
hidden_size = getattr(self.wav2vec.config, 'hidden_size', 768)
|
| 68 |
-
self.classifier = Wav2VecClassificationHead(hidden_size=hidden_size)
|
| 69 |
-
self.classifier.to(self.device)
|
| 70 |
-
|
| 71 |
-
# Initialize with pretrained-like weights for demo
|
| 72 |
-
# In production, load fine-tuned weights
|
| 73 |
-
self._initialize_classifier_weights()
|
| 74 |
-
|
| 75 |
-
self.sample_rate = 16000
|
| 76 |
-
self.resampler_cache = {}
|
| 77 |
-
|
| 78 |
-
def _initialize_classifier_weights(self):
|
| 79 |
-
"""Initialize classifier with reasonable default weights"""
|
| 80 |
-
for module in self.classifier.modules():
|
| 81 |
-
if isinstance(module, nn.Linear):
|
| 82 |
-
nn.init.xavier_uniform_(module.weight)
|
| 83 |
-
if module.bias is not None:
|
| 84 |
-
nn.init.zeros_(module.bias)
|
| 85 |
-
|
| 86 |
-
def _load_audio(self, audio_path: str) -> torch.Tensor:
|
| 87 |
-
"""Load and preprocess audio file with MP3 support"""
|
| 88 |
-
# Use audio_utils which supports MP3 via soundfile
|
| 89 |
-
waveform = load_audio_torch(audio_path, target_sr=self.sample_rate)
|
| 90 |
-
return waveform
|
| 91 |
-
|
| 92 |
-
def _extract_features(self, waveform: torch.Tensor) -> Dict[str, np.ndarray]:
|
| 93 |
-
"""Extract acoustic features for analysis"""
|
| 94 |
-
features = {}
|
| 95 |
-
|
| 96 |
-
# Compute energy
|
| 97 |
-
features["energy"] = float(torch.sqrt(torch.mean(waveform ** 2)))
|
| 98 |
-
|
| 99 |
-
# Compute zero crossing rate
|
| 100 |
-
signs = torch.sign(waveform)
|
| 101 |
-
sign_changes = torch.abs(signs[1:] - signs[:-1])
|
| 102 |
-
features["zero_crossing_rate"] = float(sign_changes.mean())
|
| 103 |
-
|
| 104 |
-
# Compute spectral features using STFT
|
| 105 |
-
n_fft = 1024
|
| 106 |
-
hop_length = 256
|
| 107 |
-
|
| 108 |
-
stft = torch.stft(
|
| 109 |
-
waveform,
|
| 110 |
-
n_fft=n_fft,
|
| 111 |
-
hop_length=hop_length,
|
| 112 |
-
return_complex=True
|
| 113 |
-
)
|
| 114 |
-
magnitude = torch.abs(stft)
|
| 115 |
-
|
| 116 |
-
# Spectral centroid (simplified)
|
| 117 |
-
freqs = torch.linspace(0, self.sample_rate / 2, magnitude.shape[0])
|
| 118 |
-
centroid = (freqs.unsqueeze(1) * magnitude).sum(dim=0) / (magnitude.sum(dim=0) + 1e-8)
|
| 119 |
-
features["spectral_centroid_mean"] = float(centroid.mean())
|
| 120 |
-
features["spectral_centroid_std"] = float(centroid.std())
|
| 121 |
-
|
| 122 |
-
# Spectral flatness (measure of noise-like vs tonal)
|
| 123 |
-
geometric_mean = torch.exp(torch.log(magnitude + 1e-8).mean(dim=0))
|
| 124 |
-
arithmetic_mean = magnitude.mean(dim=0)
|
| 125 |
-
flatness = geometric_mean / (arithmetic_mean + 1e-8)
|
| 126 |
-
features["spectral_flatness"] = float(flatness.mean())
|
| 127 |
-
|
| 128 |
-
return features
|
| 129 |
-
|
| 130 |
-
def detect(self, audio_path: str) -> Dict:
|
| 131 |
-
"""
|
| 132 |
-
Detect if audio is AI-generated or human.
|
| 133 |
-
|
| 134 |
-
Args:
|
| 135 |
-
audio_path: Path to audio file
|
| 136 |
-
|
| 137 |
-
Returns:
|
| 138 |
-
Dictionary with detection results
|
| 139 |
-
"""
|
| 140 |
-
waveform = self._load_audio(audio_path)
|
| 141 |
-
|
| 142 |
-
acoustic_features = self._extract_features(waveform)
|
| 143 |
-
|
| 144 |
-
ai_score = self._compute_ai_score_from_acoustics(acoustic_features, waveform, audio_path)
|
| 145 |
-
|
| 146 |
-
with torch.no_grad():
|
| 147 |
-
inputs = self.processor(
|
| 148 |
-
waveform.numpy(),
|
| 149 |
-
sampling_rate=self.sample_rate,
|
| 150 |
-
return_tensors="pt",
|
| 151 |
-
padding=True
|
| 152 |
-
)
|
| 153 |
-
inputs = {k: v.to(self.device) for k, v in inputs.items()}
|
| 154 |
-
|
| 155 |
-
# Get hidden states from Wav2Vec2
|
| 156 |
-
outputs = self.wav2vec(**inputs)
|
| 157 |
-
hidden_states = outputs.last_hidden_state
|
| 158 |
-
|
| 159 |
-
# Analyze hidden state statistics
|
| 160 |
-
hidden_stats = self._analyze_hidden_states(hidden_states)
|
| 161 |
-
|
| 162 |
-
# Add hidden state analysis to AI score
|
| 163 |
-
ai_score = self._adjust_score_with_hidden_states(ai_score, hidden_stats)
|
| 164 |
-
|
| 165 |
-
# Clamp to 0-1
|
| 166 |
-
ai_score = max(0.0, min(1.0, ai_score))
|
| 167 |
-
|
| 168 |
-
is_ai = ai_score >= 0.5
|
| 169 |
-
confidence = abs(ai_score - 0.5) * 2 # Scale distance from threshold
|
| 170 |
-
|
| 171 |
-
result = {
|
| 172 |
-
"classification": "ai_generated" if is_ai else "human",
|
| 173 |
-
"confidence": confidence,
|
| 174 |
-
"model_scores": {
|
| 175 |
-
"ai_probability": ai_score,
|
| 176 |
-
"human_probability": 1 - ai_score
|
| 177 |
-
},
|
| 178 |
-
"acoustic_features": acoustic_features,
|
| 179 |
-
"hidden_state_analysis": hidden_stats,
|
| 180 |
-
"indicators": self._generate_indicators(1 if is_ai else 0, acoustic_features, hidden_stats)
|
| 181 |
-
}
|
| 182 |
-
|
| 183 |
-
return result
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
def _compute_ai_score_from_acoustics(self, features: Dict, waveform: torch.Tensor, audio_path: str = None) -> float:
|
| 187 |
-
"""
|
| 188 |
-
Compute AI probability score using acoustic heuristics.
|
| 189 |
-
|
| 190 |
-
Prioritized features:
|
| 191 |
-
1. Energy CV - strongest human indicator (pauses/breathing)
|
| 192 |
-
2. MFCC Variance - timbral complexity
|
| 193 |
-
3. Spectral Flux - vocoder artifacts
|
| 194 |
-
4. Flatness - synthesis noise
|
| 195 |
-
"""
|
| 196 |
-
score = 0.5 # Start neutral
|
| 197 |
-
|
| 198 |
-
# 1. Energy CV - STRONGEST INDICATOR (compute first)
|
| 199 |
-
chunk_size = len(waveform) // 10
|
| 200 |
-
energy_cv = 0.25
|
| 201 |
-
if chunk_size > 0:
|
| 202 |
-
energies = []
|
| 203 |
-
for i in range(10):
|
| 204 |
-
chunk = waveform[i*chunk_size:(i+1)*chunk_size]
|
| 205 |
-
energies.append(float(torch.sqrt(torch.mean(chunk ** 2))))
|
| 206 |
-
energy_cv = np.std(energies) / (np.mean(energies) + 1e-8)
|
| 207 |
-
|
| 208 |
-
if energy_cv > 0.7:
|
| 209 |
-
score -= 0.30 # Very strong human signal
|
| 210 |
-
elif energy_cv > 0.5:
|
| 211 |
-
score -= 0.20 # Strong human signal
|
| 212 |
-
elif energy_cv > 0.35:
|
| 213 |
-
score -= 0.10 # Moderate human signal
|
| 214 |
-
elif energy_cv < 0.2:
|
| 215 |
-
score += 0.10 # Consistent AI
|
| 216 |
-
|
| 217 |
-
# 2. Advanced Features (Librosa)
|
| 218 |
-
adv_features = {"spectral_flux": 0, "mfcc_variance": 0}
|
| 219 |
-
if audio_path:
|
| 220 |
-
adv_features = extract_advanced_features(audio_path, self.sample_rate)
|
| 221 |
-
|
| 222 |
-
flux = adv_features["spectral_flux"]
|
| 223 |
-
mfcc_var = adv_features["mfcc_variance"]
|
| 224 |
-
|
| 225 |
-
# MFCC Variance Heuristic
|
| 226 |
-
if mfcc_var > 1900:
|
| 227 |
-
score -= 0.25 # High complexity -> Human
|
| 228 |
-
|
| 229 |
-
# Spectral Flux Heuristic - widened thresholds
|
| 230 |
-
if flux > 2.4:
|
| 231 |
-
score += 0.20 # Strong AI
|
| 232 |
-
elif flux > 2.0:
|
| 233 |
-
score += 0.10 # Moderate AI
|
| 234 |
-
elif flux < 1.8 and flux > 0.1:
|
| 235 |
-
score -= 0.10 # Human transitions
|
| 236 |
-
|
| 237 |
-
# 3. Spectral flatness (secondary)
|
| 238 |
-
flatness = features.get("spectral_flatness", 0.25)
|
| 239 |
-
if flatness > 0.38:
|
| 240 |
-
score += 0.12 # High noise = AI
|
| 241 |
-
elif flatness < 0.22:
|
| 242 |
-
score -= 0.08 # Clean harmonic = Human
|
| 243 |
-
|
| 244 |
-
return score
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
def _adjust_score_with_hidden_states(self, score: float, hidden_stats: Dict) -> float:
|
| 248 |
-
"""Adjust AI score based on Wav2Vec2 hidden state analysis"""
|
| 249 |
-
|
| 250 |
-
# Temporal variance: Lower variance often indicates synthetic speech
|
| 251 |
-
temp_var = hidden_stats.get("temporal_variance", 0.1)
|
| 252 |
-
if temp_var < 0.05:
|
| 253 |
-
score += 0.08
|
| 254 |
-
elif temp_var > 0.2:
|
| 255 |
-
score -= 0.08
|
| 256 |
-
|
| 257 |
-
# Activation sparsity: AI voices may have different sparsity patterns
|
| 258 |
-
sparsity = hidden_stats.get("activation_sparsity", 0.5)
|
| 259 |
-
if sparsity > 0.7 or sparsity < 0.2:
|
| 260 |
-
score += 0.05 # Unusual sparsity pattern
|
| 261 |
-
|
| 262 |
-
return score
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
def _analyze_hidden_states(self, hidden_states: torch.Tensor) -> Dict:
|
| 266 |
-
"""Analyze hidden state patterns for explainability"""
|
| 267 |
-
hs = hidden_states.squeeze(0) # Remove batch dim
|
| 268 |
-
|
| 269 |
-
stats = {
|
| 270 |
-
"temporal_variance": float(hs.var(dim=0).mean()),
|
| 271 |
-
"feature_variance": float(hs.var(dim=1).mean()),
|
| 272 |
-
"activation_sparsity": float((hs.abs() < 0.1).float().mean()),
|
| 273 |
-
"mean_activation": float(hs.mean()),
|
| 274 |
-
"max_activation": float(hs.max()),
|
| 275 |
-
}
|
| 276 |
-
|
| 277 |
-
return stats
|
| 278 |
-
|
| 279 |
-
def _generate_indicators(
|
| 280 |
-
self,
|
| 281 |
-
pred_class: int,
|
| 282 |
-
acoustic: Dict,
|
| 283 |
-
hidden_stats: Dict
|
| 284 |
-
) -> list:
|
| 285 |
-
"""Generate human-readable indicators for the detection"""
|
| 286 |
-
indicators = []
|
| 287 |
-
|
| 288 |
-
if pred_class == 1: # AI detected
|
| 289 |
-
# Check for AI-typical patterns
|
| 290 |
-
if acoustic["spectral_flatness"] > 0.3:
|
| 291 |
-
indicators.append("Unusually smooth spectral distribution typical of neural vocoders")
|
| 292 |
-
|
| 293 |
-
if hidden_stats["temporal_variance"] < 0.1:
|
| 294 |
-
indicators.append("Low temporal variation suggesting synthetic generation")
|
| 295 |
-
|
| 296 |
-
if acoustic["spectral_centroid_std"] < 500:
|
| 297 |
-
indicators.append("Consistent spectral characteristics unlike natural speech variation")
|
| 298 |
-
|
| 299 |
-
if not indicators:
|
| 300 |
-
indicators.append("Deep acoustic patterns suggest synthetic generation")
|
| 301 |
-
else: # Human detected
|
| 302 |
-
if acoustic["spectral_flatness"] < 0.2:
|
| 303 |
-
indicators.append("Natural harmonic structure consistent with human voice")
|
| 304 |
-
|
| 305 |
-
if hidden_stats["temporal_variance"] > 0.15:
|
| 306 |
-
indicators.append("High temporal variation typical of natural speech")
|
| 307 |
-
|
| 308 |
-
if not indicators:
|
| 309 |
-
indicators.append("Acoustic patterns consistent with natural human speech")
|
| 310 |
-
|
| 311 |
-
return indicators
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
class IndicWav2VecDetector(Wav2VecDetector):
|
| 315 |
-
"""
|
| 316 |
-
Specialized detector using IndicWav2Vec for Indian languages.
|
| 317 |
-
Inherits from Wav2VecDetector with Indian language optimizations.
|
| 318 |
-
"""
|
| 319 |
-
|
| 320 |
-
INDIC_MODELS = {
|
| 321 |
-
"hi": "ai4bharat/indicwav2vec-hindi",
|
| 322 |
-
"ta": "ai4bharat/indicwav2vec_v1_tamil",
|
| 323 |
-
"te": "ai4bharat/indicwav2vec_v1_telugu",
|
| 324 |
-
"ml": "ai4bharat/indicwav2vec_v1_malayalam",
|
| 325 |
-
"en": "facebook/wav2vec2-base" # English fallback
|
| 326 |
-
}
|
| 327 |
-
|
| 328 |
-
def __init__(self, language: str = "hi", device: str = None):
|
| 329 |
-
# Select model based on language
|
| 330 |
-
model_name = self.INDIC_MODELS.get(language, "facebook/wav2vec2-base")
|
| 331 |
-
|
| 332 |
-
try:
|
| 333 |
-
super().__init__(model_name=model_name, device=device)
|
| 334 |
-
self.language = language
|
| 335 |
-
except Exception as e:
|
| 336 |
-
print(f"[IndicWav2VecDetector] Failed to load {model_name}, falling back to base model")
|
| 337 |
-
super().__init__(model_name="facebook/wav2vec2-base", device=device)
|
| 338 |
-
self.language = "en"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
build.sh
DELETED
|
@@ -1,5 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env bash
|
| 2 |
-
# exit on error
|
| 3 |
-
set -o errexit
|
| 4 |
-
pip install --upgrade pip
|
| 5 |
-
pip install -r requirements.txt
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
deployment.md
DELETED
|
@@ -1,71 +0,0 @@
|
|
| 1 |
-
# 🌍 Deployment Guide: Exposing Your API
|
| 2 |
-
|
| 3 |
-
Since you have the code running locally on your machine, the fastest way to make it accessible to the world (and link it to your domain) is using a **Tunnel**.
|
| 4 |
-
|
| 5 |
-
## Option A: Cloudflare Tunnel (Recommended for Custom Domains)
|
| 6 |
-
This is free, secure, and allows you to use your own domain (e.g., `api.yourdomain.com`).
|
| 7 |
-
|
| 8 |
-
### 1. Install Cloudflare Tunnel (`cloudflared`)
|
| 9 |
-
Run this in PowerShell to download the verified Windows executable:
|
| 10 |
-
```powershell
|
| 11 |
-
# Create a folder for tools
|
| 12 |
-
mkdir c:\tools
|
| 13 |
-
cd c:\tools
|
| 14 |
-
|
| 15 |
-
# Download cloudflared
|
| 16 |
-
Invoke-WebRequest -Uri https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-windows-amd64.exe -OutFile cloudflared.exe
|
| 17 |
-
```
|
| 18 |
-
|
| 19 |
-
### 2. Login to Cloudflare
|
| 20 |
-
This connects your machine to your Cloudflare account (where your domain is managed).
|
| 21 |
-
```powershell
|
| 22 |
-
.\cloudflared.exe tunnel login
|
| 23 |
-
```
|
| 24 |
-
* A browser window will open. Select the domain you want to use.
|
| 25 |
-
|
| 26 |
-
### 3. Create a Tunnel
|
| 27 |
-
```powershell
|
| 28 |
-
.\cloudflared.exe tunnel create voice-api
|
| 29 |
-
```
|
| 30 |
-
* Copy the **Tunnel ID** (a long UUID like `d4c3b2a1-...`) from the output.
|
| 31 |
-
|
| 32 |
-
### 4. Route Your Domain to Localhost
|
| 33 |
-
Replace `<UUID>` with your Tunnel ID and `api.yourdomain.com` with your desired domain.
|
| 34 |
-
```powershell
|
| 35 |
-
# 1. Configure the tunnel target
|
| 36 |
-
# Create a config.yml file
|
| 37 |
-
echo "url: http://localhost:8000" > config.yml
|
| 38 |
-
echo "tunnel: <UUID>" >> config.yml
|
| 39 |
-
echo "credentials-file: C:\Users\baksh\.cloudflared\<UUID>.json" >> config.yml
|
| 40 |
-
|
| 41 |
-
# 2. Assign the domain DNS
|
| 42 |
-
.\cloudflared.exe tunnel route dns voice-api api.yourdomain.com
|
| 43 |
-
```
|
| 44 |
-
|
| 45 |
-
### 5. Run it!
|
| 46 |
-
```powershell
|
| 47 |
-
.\cloudflared.exe tunnel run voice-api
|
| 48 |
-
```
|
| 49 |
-
**Success!** Your local server (`localhost:8000`) is now live at `https://api.yourdomain.com`.
|
| 50 |
-
|
| 51 |
-
---
|
| 52 |
-
|
| 53 |
-
## Option B: Ngrok (Fastest, Random URL)
|
| 54 |
-
If you don't use Cloudflare for DNS, or just want a quick link:
|
| 55 |
-
|
| 56 |
-
1. **Download Ngrok**: [https://ngrok.com/download](https://ngrok.com/download)
|
| 57 |
-
2. **Run**:
|
| 58 |
-
```powershell
|
| 59 |
-
ngrok http 8000
|
| 60 |
-
```
|
| 61 |
-
3. **Result**: It will give you a URL like `https://a1b2-c3d4.ngrok-free.app`. Use this in the endpoint tester.
|
| 62 |
-
|
| 63 |
-
---
|
| 64 |
-
|
| 65 |
-
## 🔒 Important: Update Test Scripts
|
| 66 |
-
Once you have your public URL, test it using the verify script:
|
| 67 |
-
|
| 68 |
-
```python
|
| 69 |
-
# Update verify_tester_config.py
|
| 70 |
-
API_URL = "https://api.yourdomain.com/api/v1/detect"
|
| 71 |
-
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
render.yaml
DELETED
|
@@ -1,13 +0,0 @@
|
|
| 1 |
-
services:
|
| 2 |
-
- type: web
|
| 3 |
-
name: voice-detection-api
|
| 4 |
-
env: python
|
| 5 |
-
plan: free # Change to 'starter' for always-on service
|
| 6 |
-
buildCommand: pip install -r requirements.txt
|
| 7 |
-
startCommand: python run.py --host 0.0.0.0 --port $PORT
|
| 8 |
-
envVars:
|
| 9 |
-
- key: PYTHON_VERSION
|
| 10 |
-
value: 3.11.0
|
| 11 |
-
- key: PORT
|
| 12 |
-
generateValue: true # Render will auto-assign
|
| 13 |
-
healthCheckPath: /api/v1/health
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
requirements.txt
DELETED
|
@@ -1,24 +0,0 @@
|
|
| 1 |
-
# FastAPI
|
| 2 |
-
fastapi>=0.109.0
|
| 3 |
-
uvicorn[standard]>=0.27.0
|
| 4 |
-
pydantic>=2.5.3
|
| 5 |
-
python-multipart>=0.0.6
|
| 6 |
-
|
| 7 |
-
# Audio Processing
|
| 8 |
-
librosa>=0.10.1
|
| 9 |
-
soundfile>=0.12.1
|
| 10 |
-
pydub>=0.25.1
|
| 11 |
-
|
| 12 |
-
# ML/DL - Core (use available versions)
|
| 13 |
-
torch>=2.5.0
|
| 14 |
-
torchaudio>=2.5.0
|
| 15 |
-
transformers>=4.36.2
|
| 16 |
-
|
| 17 |
-
# IndicWav2Vec and SpeechBrain
|
| 18 |
-
speechbrain>=1.0.0
|
| 19 |
-
|
| 20 |
-
# Utilities
|
| 21 |
-
numpy>=1.26.0
|
| 22 |
-
scipy>=1.12.0
|
| 23 |
-
scikit-learn>=1.4.0
|
| 24 |
-
httpx>=0.26.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
run.py
DELETED
|
@@ -1,40 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Production Server Entry Point
|
| 3 |
-
|
| 4 |
-
Run with: python run.py
|
| 5 |
-
"""
|
| 6 |
-
import uvicorn
|
| 7 |
-
import argparse
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
def main():
|
| 11 |
-
parser = argparse.ArgumentParser(description="AI Voice Detection API Server")
|
| 12 |
-
parser.add_argument("--host", default="0.0.0.0", help="Host to bind to")
|
| 13 |
-
parser.add_argument("--port", type=int, default=8001, help="Port to bind to")
|
| 14 |
-
parser.add_argument("--reload", action="store_true", help="Enable auto-reload")
|
| 15 |
-
parser.add_argument("--workers", type=int, default=1, help="Number of workers")
|
| 16 |
-
|
| 17 |
-
args = parser.parse_args()
|
| 18 |
-
|
| 19 |
-
print(f"""
|
| 20 |
-
╔══════════════════════════════════════════════════════════════╗
|
| 21 |
-
║ AI Voice Detection API Server ║
|
| 22 |
-
╠══════════════════════════════════════════════════════════════╣
|
| 23 |
-
║ 🚀 Starting server... ║
|
| 24 |
-
║ 📍 URL: http://{args.host}:{args.port} ║
|
| 25 |
-
║ 📚 Docs: http://{args.host}:{args.port}/docs ║
|
| 26 |
-
║ 🔍 API: http://{args.host}:{args.port}/api/v1/detect ║
|
| 27 |
-
╚══════════════════════════════════════════════════════════════╝
|
| 28 |
-
""")
|
| 29 |
-
|
| 30 |
-
uvicorn.run(
|
| 31 |
-
"app.main:app",
|
| 32 |
-
host=args.host,
|
| 33 |
-
port=args.port,
|
| 34 |
-
reload=args.reload,
|
| 35 |
-
workers=args.workers if not args.reload else 1
|
| 36 |
-
)
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
if __name__ == "__main__":
|
| 40 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
test_api.py
DELETED
|
@@ -1,149 +0,0 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Test script for AI Voice Detection API
|
| 3 |
-
|
| 4 |
-
Usage:
|
| 5 |
-
python test_api.py <path_to_audio.mp3>
|
| 6 |
-
python test_api.py --generate-sample # Generate test sample
|
| 7 |
-
"""
|
| 8 |
-
import base64
|
| 9 |
-
import requests
|
| 10 |
-
import json
|
| 11 |
-
import sys
|
| 12 |
-
import os
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
API_URL = "http://localhost:8001"
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
def encode_audio(file_path: str) -> str:
|
| 19 |
-
"""Encode audio file to base64"""
|
| 20 |
-
with open(file_path, 'rb') as f:
|
| 21 |
-
return base64.b64encode(f.read()).decode('utf-8')
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
def test_detection(audio_path: str, language: str = "en"):
|
| 25 |
-
"""Test the detection endpoint"""
|
| 26 |
-
print(f"\n{'='*60}")
|
| 27 |
-
print(f"Testing AI Voice Detection")
|
| 28 |
-
print(f"{'='*60}")
|
| 29 |
-
print(f"Audio file: {audio_path}")
|
| 30 |
-
print(f"Language hint: {language}")
|
| 31 |
-
|
| 32 |
-
# Encode audio
|
| 33 |
-
print("\n[1/3] Encoding audio to Base64...")
|
| 34 |
-
audio_base64 = encode_audio(audio_path)
|
| 35 |
-
print(f" Encoded size: {len(audio_base64)} characters")
|
| 36 |
-
|
| 37 |
-
# Prepare request
|
| 38 |
-
payload = {
|
| 39 |
-
"audio_base64": audio_base64,
|
| 40 |
-
"language_hint": language
|
| 41 |
-
}
|
| 42 |
-
|
| 43 |
-
# Send request
|
| 44 |
-
print("\n[2/3] Sending request to API...")
|
| 45 |
-
try:
|
| 46 |
-
response = requests.post(
|
| 47 |
-
f"{API_URL}/api/v1/detect",
|
| 48 |
-
json=payload,
|
| 49 |
-
timeout=120 # Detection can take time
|
| 50 |
-
)
|
| 51 |
-
|
| 52 |
-
process_time = response.headers.get('X-Process-Time', 'N/A')
|
| 53 |
-
print(f" Response time: {process_time}s")
|
| 54 |
-
print(f" Status code: {response.status_code}")
|
| 55 |
-
|
| 56 |
-
except requests.exceptions.ConnectionError:
|
| 57 |
-
print("\n❌ ERROR: Could not connect to API server")
|
| 58 |
-
print(" Make sure the server is running: python run.py --reload")
|
| 59 |
-
return
|
| 60 |
-
except Exception as e:
|
| 61 |
-
print(f"\n❌ ERROR: {e}")
|
| 62 |
-
return
|
| 63 |
-
|
| 64 |
-
# Parse response
|
| 65 |
-
print("\n[3/3] Detection Result:")
|
| 66 |
-
print(f"{'='*60}")
|
| 67 |
-
|
| 68 |
-
if response.status_code == 200:
|
| 69 |
-
result = response.json()
|
| 70 |
-
|
| 71 |
-
# Classification
|
| 72 |
-
classification = result['classification']
|
| 73 |
-
confidence = result['confidence']
|
| 74 |
-
emoji = "🤖" if classification == "ai_generated" else "👤"
|
| 75 |
-
|
| 76 |
-
print(f"\nClassification: {classification.upper()}")
|
| 77 |
-
print(f"Confidence: {confidence:.1%}")
|
| 78 |
-
|
| 79 |
-
# Explanation
|
| 80 |
-
explanation = result['explanation']
|
| 81 |
-
print("\nExplanation:")
|
| 82 |
-
for indicator in explanation.get('key_indicators', []):
|
| 83 |
-
print(f" • {indicator}")
|
| 84 |
-
|
| 85 |
-
else:
|
| 86 |
-
print(f"\n❌ Error: {response.status_code}")
|
| 87 |
-
print(response.json())
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
def test_health():
|
| 91 |
-
"""Test the health endpoint"""
|
| 92 |
-
print("\n[Health Check]")
|
| 93 |
-
try:
|
| 94 |
-
response = requests.get(f"{API_URL}/api/v1/health")
|
| 95 |
-
print(f"Status: {response.json()}")
|
| 96 |
-
except Exception as e:
|
| 97 |
-
print(f"Error: {e}")
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
def create_test_sample():
|
| 101 |
-
"""Create a simple test audio sample using basic sine waves"""
|
| 102 |
-
import numpy as np
|
| 103 |
-
from scipy.io import wavfile
|
| 104 |
-
|
| 105 |
-
print("\n[Creating test audio sample]")
|
| 106 |
-
|
| 107 |
-
# Generate 3 seconds of audio
|
| 108 |
-
sample_rate = 16000
|
| 109 |
-
duration = 3
|
| 110 |
-
t = np.linspace(0, duration, sample_rate * duration)
|
| 111 |
-
|
| 112 |
-
# Simple speech-like signal (not real speech, just for testing)
|
| 113 |
-
signal = np.sin(2 * np.pi * 200 * t) # Fundamental
|
| 114 |
-
signal += 0.5 * np.sin(2 * np.pi * 400 * t) # Harmonic
|
| 115 |
-
signal += 0.3 * np.sin(2 * np.pi * 600 * t) # Harmonic
|
| 116 |
-
signal += 0.1 * np.random.randn(len(t)) # Noise
|
| 117 |
-
|
| 118 |
-
# Normalize
|
| 119 |
-
signal = signal / np.max(np.abs(signal)) * 0.8
|
| 120 |
-
signal = (signal * 32767).astype(np.int16)
|
| 121 |
-
|
| 122 |
-
# Save as WAV (API will handle conversion)
|
| 123 |
-
output_path = "test_sample.wav"
|
| 124 |
-
wavfile.write(output_path, sample_rate, signal)
|
| 125 |
-
print(f"Created: {output_path}")
|
| 126 |
-
|
| 127 |
-
return output_path
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
if __name__ == "__main__":
|
| 131 |
-
if len(sys.argv) < 2:
|
| 132 |
-
print("Usage: python test_api.py <audio_file.mp3>")
|
| 133 |
-
print(" python test_api.py --health")
|
| 134 |
-
print(" python test_api.py --generate-sample")
|
| 135 |
-
sys.exit(1)
|
| 136 |
-
|
| 137 |
-
if sys.argv[1] == "--health":
|
| 138 |
-
test_health()
|
| 139 |
-
elif sys.argv[1] == "--generate-sample":
|
| 140 |
-
sample_path = create_test_sample()
|
| 141 |
-
test_detection(sample_path)
|
| 142 |
-
else:
|
| 143 |
-
audio_path = sys.argv[1]
|
| 144 |
-
if not os.path.exists(audio_path):
|
| 145 |
-
print(f"Error: File not found: {audio_path}")
|
| 146 |
-
sys.exit(1)
|
| 147 |
-
|
| 148 |
-
language = sys.argv[2] if len(sys.argv) > 2 else "en"
|
| 149 |
-
test_detection(audio_path, language)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
verify_tester_config.py
DELETED
|
@@ -1,37 +0,0 @@
|
|
| 1 |
-
import requests
|
| 2 |
-
import json
|
| 3 |
-
import sys
|
| 4 |
-
|
| 5 |
-
API_URL = "http://localhost:8000/api/v1/detect"
|
| 6 |
-
SAMPLE_AUDIO_URL = "https://www2.cs.uic.edu/~i101/SoundFiles/CantinaBand3.wav" # Public domain sample
|
| 7 |
-
|
| 8 |
-
def test_url_detection():
|
| 9 |
-
print(f"\nTesting URL Detection with: {SAMPLE_AUDIO_URL}")
|
| 10 |
-
|
| 11 |
-
payload = {
|
| 12 |
-
"audio_url": SAMPLE_AUDIO_URL,
|
| 13 |
-
"language_hint": "en"
|
| 14 |
-
}
|
| 15 |
-
|
| 16 |
-
headers = {
|
| 17 |
-
"X-API-Key": "hackathon_secret_key"
|
| 18 |
-
}
|
| 19 |
-
|
| 20 |
-
try:
|
| 21 |
-
response = requests.post(API_URL, json=payload, headers=headers)
|
| 22 |
-
|
| 23 |
-
if response.status_code == 200:
|
| 24 |
-
print("✅ URL Detection Success!")
|
| 25 |
-
result = response.json()
|
| 26 |
-
print(f" Classification: {result['classification']}")
|
| 27 |
-
print(f" Confidence: {result['confidence']:.2%}")
|
| 28 |
-
else:
|
| 29 |
-
print(f"❌ Failed: {response.status_code}")
|
| 30 |
-
print(response.text)
|
| 31 |
-
|
| 32 |
-
except Exception as e:
|
| 33 |
-
print(f"❌ Error: {str(e)}")
|
| 34 |
-
|
| 35 |
-
if __name__ == "__main__":
|
| 36 |
-
print("=== Verifying Hackathon Tester Configuration ===")
|
| 37 |
-
test_url_detection()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|