.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: AI Voice Detection API
3
- emoji: 🎤
4
  colorFrom: blue
5
- colorTo: purple
6
  sdk: docker
7
  pinned: false
8
  license: mit
 
9
  ---
10
 
11
- # AI Voice Detection API
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()