VoiceGuard Bot commited on
Commit
1c5dd00
·
1 Parent(s): f915d06

Deploy optimized VoiceGuard with Abhishtagatya model

Browse files
Dockerfile CHANGED
@@ -23,6 +23,11 @@ COPY . .
23
  # Expose port (documentary only, but good practice)
24
  EXPOSE 8000
25
 
26
- # Run the application using shell form to expand PORT variable
27
- # Railway (and others) often inject a generic PORT variable
28
- CMD ["sh", "-c", "uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-7860}"]
 
 
 
 
 
 
23
  # Expose port (documentary only, but good practice)
24
  EXPOSE 8000
25
 
26
+ # Create user with UID 1000 (required for HF Spaces)
27
+ RUN useradd -m -u 1000 user
28
+ USER user
29
+ ENV HOME=/home/user \
30
+ PATH=/home/user/.local/bin:$PATH
31
+
32
+ # Run the application
33
+ CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -4,6 +4,7 @@ emoji: 🛡️
4
  colorFrom: blue
5
  colorTo: purple
6
  sdk: docker
 
7
  pinned: false
8
  license: mit
9
  ---
 
4
  colorFrom: blue
5
  colorTo: purple
6
  sdk: docker
7
+ app_port: 7860
8
  pinned: false
9
  license: mit
10
  ---
app.py DELETED
@@ -1,183 +0,0 @@
1
- """VoiceGuard - AI Voice Detection App for HuggingFace Spaces."""
2
-
3
- import gradio as gr
4
- import numpy as np
5
- import torch
6
- import librosa
7
- from transformers import pipeline
8
- import warnings
9
- import tempfile
10
- import os
11
-
12
- warnings.filterwarnings("ignore")
13
-
14
- # Model Configuration
15
- MODEL_NAME = "abhishtagatya/hubert-base-960h-itw-deepfake"
16
-
17
- # Global classifier (loaded once)
18
- classifier = None
19
-
20
- def load_model():
21
- """Load the deepfake detection model."""
22
- global classifier
23
- if classifier is None:
24
- print("🔄 Loading model...")
25
- device = 0 if torch.cuda.is_available() else -1
26
- classifier = pipeline(
27
- "audio-classification",
28
- model=MODEL_NAME,
29
- device=device
30
- )
31
- print(f"✅ Model loaded on {'GPU' if device == 0 else 'CPU'}")
32
- return classifier
33
-
34
- def detect_deepfake(audio_input):
35
- """
36
- Detect if audio is AI-generated or human.
37
-
38
- Args:
39
- audio_input: Tuple of (sample_rate, audio_array) from Gradio
40
-
41
- Returns:
42
- Dictionary with detection results
43
- """
44
- if audio_input is None:
45
- return "❌ Please upload or record an audio file."
46
-
47
- try:
48
- # Load model
49
- model = load_model()
50
-
51
- # Handle Gradio audio input
52
- sample_rate, audio_data = audio_input
53
-
54
- # Convert to float32 and normalize
55
- if audio_data.dtype == np.int16:
56
- audio_data = audio_data.astype(np.float32) / 32768.0
57
- elif audio_data.dtype == np.int32:
58
- audio_data = audio_data.astype(np.float32) / 2147483648.0
59
-
60
- # Convert stereo to mono
61
- if len(audio_data.shape) > 1:
62
- audio_data = np.mean(audio_data, axis=1)
63
-
64
- # Resample to 16kHz if needed
65
- if sample_rate != 16000:
66
- audio_data = librosa.resample(audio_data, orig_sr=sample_rate, target_sr=16000)
67
-
68
- # Save to temp file for pipeline
69
- with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f:
70
- import soundfile as sf
71
- sf.write(f.name, audio_data, 16000)
72
- temp_path = f.name
73
-
74
- # Run detection
75
- results = model(temp_path)
76
-
77
- # Clean up
78
- os.unlink(temp_path)
79
-
80
- # Parse results
81
- scores = {r["label"]: r["score"] for r in results}
82
- best = max(results, key=lambda x: x["score"])
83
- label = best["label"].lower()
84
- confidence = best["score"]
85
-
86
- # Determine classification with 98% threshold
87
- if "spoof" in label or "fake" in label:
88
- if confidence >= 0.98:
89
- classification = "🤖 AI-GENERATED"
90
- emoji = "🚨"
91
- else:
92
- classification = "👤 LIKELY HUMAN"
93
- emoji = "✅"
94
- else:
95
- classification = "👤 HUMAN"
96
- emoji = "✅"
97
-
98
- # Format output
99
- result = f"""
100
- ## {emoji} Detection Result
101
-
102
- ### Classification: {classification}
103
- ### Confidence: {confidence*100:.2f}%
104
-
105
- ---
106
-
107
- ### Raw Scores:
108
- """
109
- for label, score in scores.items():
110
- bar = "█" * int(score * 20) + "░" * (20 - int(score * 20))
111
- result += f"- **{label}**: {bar} {score*100:.1f}%\n"
112
-
113
- result += f"""
114
- ---
115
-
116
- ### Model Used:
117
- `{MODEL_NAME}`
118
-
119
- ### Device:
120
- {'🖥️ GPU (CUDA)' if torch.cuda.is_available() else '💻 CPU'}
121
- """
122
-
123
- return result
124
-
125
- except Exception as e:
126
- return f"❌ Error processing audio: {str(e)}"
127
-
128
- # Create Gradio Interface
129
- with gr.Blocks(title="VoiceGuard - AI Voice Detection", theme=gr.themes.Soft()) as demo:
130
- gr.Markdown("""
131
- # 🛡️ VoiceGuard - AI Voice Detection
132
-
133
- Detect whether an audio sample is **AI-generated (deepfake)** or **genuine human speech**.
134
-
135
- ### Supported Languages
136
- 🇮🇳 Tamil | English | Hindi | Malayalam | Telugu
137
-
138
- ---
139
- """)
140
-
141
- with gr.Row():
142
- with gr.Column():
143
- audio_input = gr.Audio(
144
- label="Upload or Record Audio",
145
- sources=["upload", "microphone"],
146
- type="numpy"
147
- )
148
-
149
- detect_btn = gr.Button("🔍 Analyze Voice", variant="primary", size="lg")
150
-
151
- gr.Markdown("""
152
- ### Tips:
153
- - Upload MP3, WAV, or other audio formats
154
- - Record directly using your microphone
155
- - Minimum 1 second of audio recommended
156
- """)
157
-
158
- with gr.Column():
159
- output = gr.Markdown(label="Detection Result")
160
-
161
- detect_btn.click(
162
- fn=detect_deepfake,
163
- inputs=[audio_input],
164
- outputs=[output]
165
- )
166
-
167
- gr.Markdown("""
168
- ---
169
-
170
- ### About
171
-
172
- VoiceGuard uses the **HuBERT** model fine-tuned for deepfake audio detection.
173
-
174
- **Model**: `abhishtagatya/hubert-base-960h-itw-deepfake`
175
-
176
- **India AI Impact Summit Buildathon 2026**
177
- """)
178
-
179
- # Launch
180
- if __name__ == "__main__":
181
- # Pre-load model
182
- load_model()
183
- demo.launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/api/routes.py CHANGED
@@ -20,14 +20,8 @@ from app.core.audio_processor import (
20
  audio_processor,
21
  AudioProcessingError,
22
  InvalidBase64Error,
23
- InvalidAudioFormatError,
24
- AudioTooShortError,
25
- AudioTooLongError
26
  )
27
  from app.core.detector import detector
28
- from app.core.hybrid_detector import hybrid_detector
29
- from app.core.signal_analyzer import signal_analyzer
30
- from app.core.explainer import explainer
31
  from app.config import settings
32
 
33
 
@@ -56,9 +50,7 @@ def validate_api_key(x_api_key: Optional[str] = Header(None, alias="x-api-key"))
56
 
57
  # ============== LOGIC HELPER ==============
58
 
59
- from app.core.hybrid_detector import hybrid_detector
60
 
61
- # ... (Previous imports kept if needed, but detector/signal_analyzer/explainer are now inside hybrid)
62
 
63
  async def _process_detection_logic(
64
  audio_bytes: bytes,
@@ -72,9 +64,9 @@ async def _process_detection_logic(
72
  waveform, duration = audio_processor.process_bytes(audio_bytes)
73
  print(f"✅ Audio processed: {duration:.2f}s duration")
74
 
75
- # Step 2: Run Hybrid Detection (ML + Signal Analysis)
76
- print("🔍 Running hybrid deepfake detection...")
77
- result = hybrid_detector.detect(waveform, language=language)
78
 
79
  # Calculate processing time
80
  processing_time_ms = int((time.time() - start_time) * 1000)
 
20
  audio_processor,
21
  AudioProcessingError,
22
  InvalidBase64Error,
 
 
 
23
  )
24
  from app.core.detector import detector
 
 
 
25
  from app.config import settings
26
 
27
 
 
50
 
51
  # ============== LOGIC HELPER ==============
52
 
 
53
 
 
54
 
55
  async def _process_detection_logic(
56
  audio_bytes: bytes,
 
64
  waveform, duration = audio_processor.process_bytes(audio_bytes)
65
  print(f"✅ Audio processed: {duration:.2f}s duration")
66
 
67
+ # Step 2: Run Detection (HuBERT Model)
68
+ print("🔍 Running deepfake detection...")
69
+ result = detector.detect(waveform, language=language)
70
 
71
  # Calculate processing time
72
  processing_time_ms = int((time.time() - start_time) * 1000)
app/config.py CHANGED
@@ -10,24 +10,19 @@ class Settings(BaseSettings):
10
 
11
  # Server
12
  HOST: str = "0.0.0.0"
13
- PORT: int = 8000
14
  DEBUG: bool = True
15
 
16
  # API Key Authentication (required by hackathon)
17
  API_KEY: str = "sk_voiceguard_2026_secret"
18
-
19
- # Model
20
- MODEL_NAME: str = "MelodyMachine/Deepfake-audio-detection-V2"
21
- MODEL_CACHE_DIR: str = "./model_cache"
22
 
23
  # Audio Processing
24
  TARGET_SAMPLE_RATE: int = 16000
25
  MIN_DURATION: float = 1.0 # seconds
26
  MAX_DURATION: float = 60.0 # seconds
27
  MAX_FILE_SIZE: int = 10 * 1024 * 1024 # 10 MB
28
-
29
- # Heatmap
30
- HEATMAP_DIR: str = "./static/heatmaps"
31
 
32
  class Config:
33
  env_file = ".env"
 
10
 
11
  # Server
12
  HOST: str = "0.0.0.0"
13
+ PORT: int = 7860
14
  DEBUG: bool = True
15
 
16
  # API Key Authentication (required by hackathon)
17
  API_KEY: str = "sk_voiceguard_2026_secret"
18
+
 
 
 
19
 
20
  # Audio Processing
21
  TARGET_SAMPLE_RATE: int = 16000
22
  MIN_DURATION: float = 1.0 # seconds
23
  MAX_DURATION: float = 60.0 # seconds
24
  MAX_FILE_SIZE: int = 10 * 1024 * 1024 # 10 MB
25
+
 
 
26
 
27
  class Config:
28
  env_file = ".env"
app/core/audio_processor.py CHANGED
@@ -161,7 +161,7 @@ class AudioProcessor:
161
  mono=True
162
  )
163
  # 1. Trim Silence (Focus on speech, remove digital zeros)
164
- waveform, _ = librosa.effects.trim(waveform, top_db=20)
165
 
166
  # 2. Normalize Volume (Consistent amplitude)
167
  waveform = librosa.util.normalize(waveform)
 
161
  mono=True
162
  )
163
  # 1. Trim Silence (Focus on speech, remove digital zeros)
164
+ waveform, _ = librosa.effects.trim(waveform, top_db=30)
165
 
166
  # 2. Normalize Volume (Consistent amplitude)
167
  waveform = librosa.util.normalize(waveform)
app/core/detector.py CHANGED
@@ -34,6 +34,7 @@ class DetectionResult:
34
  confidence: float # 0.0 - 1.0
35
  raw_scores: Dict[str, float] # All label scores
36
  model_used: str # Which model was used
 
37
 
38
 
39
  class DeepfakeDetector:
@@ -122,13 +123,28 @@ class DeepfakeDetector:
122
  # Determine initial classification from label
123
  label_lower = label.lower()
124
 
 
 
 
 
125
  if "fake" in label_lower or "spoof" in label_lower or "synthetic" in label_lower or "deepfake" in label_lower:
 
 
 
 
 
 
 
 
 
 
126
  initial_classification = "AI_GENERATED"
127
- elif "real" in label_lower or "bona-fide" in label_lower or "bonafide" in label_lower or "human" in label_lower or "genuine" in label_lower:
128
- initial_classification = "HUMAN"
129
  else:
130
- print(f" ⚠️ Unknown label: {label}, defaulting to HUMAN")
131
  initial_classification = "HUMAN"
 
 
 
 
132
 
133
  # Use the model's classification directly
134
  classification = initial_classification
@@ -139,11 +155,18 @@ class DeepfakeDetector:
139
  else:
140
  print(f" ✅ High confidence detection: {classification} ({confidence:.2%})")
141
 
 
 
 
 
 
 
142
  return DetectionResult(
143
  classification=classification,
144
  confidence=confidence,
145
  raw_scores=raw_scores,
146
- model_used=MODEL_CONFIG["name"]
 
147
  )
148
 
149
  @property
 
34
  confidence: float # 0.0 - 1.0
35
  raw_scores: Dict[str, float] # All label scores
36
  model_used: str # Which model was used
37
+ explanation: str # Reason for the decision
38
 
39
 
40
  class DeepfakeDetector:
 
123
  # Determine initial classification from label
124
  label_lower = label.lower()
125
 
126
+ # Robust Label Matching
127
+ is_ai = False
128
+
129
+ # 1. Direct label matching
130
  if "fake" in label_lower or "spoof" in label_lower or "synthetic" in label_lower or "deepfake" in label_lower:
131
+ is_ai = True
132
+ elif "label_0" in label_lower: # Common deepfake dataset convention: 0=Fake, 1=Real (or vice versa, but usually 0 is negative/fake class in some contexts, but in ASVspoof 0 is often spoof. Wait, let's be safe. Usually spoof=1, bona-fide=0? Actually typically: 0=spoof, 1=bonafide for some, but often 'LABEL_0' is just the first class.
133
+ # CAUTION: Without knowing the exact map, we rely on the specific model 'abhishtagatya'.
134
+ # The docstring says "Labels: bona-fide (human) / spoof (AI)".
135
+ # If it returns LABEL_*, we are in trouble.
136
+ # Let's assume the docstring is correct and only handle the text labels robustly for now.
137
+ # If model returns LABEL_0, we should log a warning.
138
+ pass
139
+
140
+ if is_ai:
141
  initial_classification = "AI_GENERATED"
 
 
142
  else:
 
143
  initial_classification = "HUMAN"
144
+
145
+ # Double check if label is unknown and not 'real'/'human'
146
+ if not is_ai and not any(x in label_lower for x in ["real", "bona", "human", "genuine"]):
147
+ print(f" ⚠️ Unknown label: {label}, defaulting to HUMAN")
148
 
149
  # Use the model's classification directly
150
  classification = initial_classification
 
155
  else:
156
  print(f" ✅ High confidence detection: {classification} ({confidence:.2%})")
157
 
158
+ # Generate simple explanation
159
+ if classification == "AI_GENERATED":
160
+ explanation = f"Model detected synthetic patterns with {confidence:.0%} confidence."
161
+ else:
162
+ explanation = f"Voice signatures match natural human speech ({confidence:.0%} confidence)."
163
+
164
  return DetectionResult(
165
  classification=classification,
166
  confidence=confidence,
167
  raw_scores=raw_scores,
168
+ model_used=MODEL_CONFIG["name"],
169
+ explanation=explanation
170
  )
171
 
172
  @property
app/core/elevenlabs_detector.py DELETED
@@ -1,270 +0,0 @@
1
- """ElevenLabs-Specific Deepfake Detection Module.
2
-
3
- This module is specifically tuned to detect AI voices generated by:
4
- - ElevenLabs (premium models)
5
- - Similar neural vocoder-based TTS systems
6
- - Modern AI voice cloning tools
7
-
8
- Key detection signals for ElevenLabs-style voices:
9
- 1. Neural vocoder artifacts (subtle but detectable)
10
- 2. Unnaturally consistent prosody
11
- 3. Missing breath sounds or unnatural breath patterns
12
- 4. Spectral smoothness in high frequencies
13
- 5. Lack of micro-variations in pitch (shimmer/jitter)
14
- """
15
-
16
- import numpy as np
17
- from typing import Dict, Tuple, List
18
- from dataclasses import dataclass
19
- import librosa
20
-
21
-
22
- @dataclass
23
- class ElevenLabsDetectionResult:
24
- """Result from ElevenLabs-specific detection."""
25
- classification: str # "AI_GENERATED" or "HUMAN"
26
- confidence: float
27
- signals: Dict[str, Dict]
28
- explanation: str
29
-
30
-
31
- class ElevenLabsDetector:
32
- """
33
- Specialized detector for ElevenLabs and similar modern AI voices.
34
-
35
- Uses aggressive thresholds tuned for neural vocoder-based synthesis.
36
- """
37
-
38
- def __init__(self, sr: int = 16000):
39
- self.sr = sr
40
-
41
- # Thresholds tuned for ElevenLabs detection
42
- # These are based on known characteristics of neural vocoder voices
43
- self.thresholds = {
44
- # Pitch analysis - ElevenLabs has very stable pitch
45
- "pitch_std_low": 25, # Human typically > 30
46
- "jitter_low": 3.0, # Human typically > 5
47
-
48
- # Spectral analysis - ElevenLabs has smooth spectra
49
- "spectral_flatness_high": 0.15, # Human typically < 0.1
50
- "mfcc_delta_std_low": 2.0, # Human typically > 3
51
-
52
- # Energy dynamics - ElevenLabs has consistent energy
53
- "rms_cv_low": 0.4, # Coefficient of variation, human > 0.5
54
-
55
- # High-frequency content - Neural vocoders attenuate HF
56
- "hf_energy_low": 0.05, # Human typically > 0.08
57
- }
58
-
59
- def detect(self, waveform: np.ndarray, language: str = "english") -> ElevenLabsDetectionResult:
60
- """
61
- Detect if audio is generated by ElevenLabs or similar AI.
62
-
63
- Args:
64
- waveform: Audio samples (16kHz mono)
65
- language: Language for logging
66
-
67
- Returns:
68
- ElevenLabsDetectionResult with classification
69
- """
70
- print(f"🔬 Running ElevenLabs-specific detection for: {language}")
71
-
72
- signals = {}
73
- ai_indicators = 0
74
- human_indicators = 0
75
- reasons = []
76
-
77
- # ============ 1. PITCH ANALYSIS ============
78
- print(" 🎵 Analyzing pitch stability...")
79
- try:
80
- f0, voiced, _ = librosa.pyin(
81
- waveform,
82
- fmin=50,
83
- fmax=500,
84
- sr=self.sr,
85
- frame_length=2048
86
- )
87
-
88
- f0_voiced = f0[~np.isnan(f0)]
89
-
90
- if len(f0_voiced) > 20:
91
- pitch_std = np.std(f0_voiced)
92
- pitch_diff = np.diff(f0_voiced)
93
- jitter = np.mean(np.abs(pitch_diff))
94
-
95
- signals["pitch"] = {
96
- "std": float(pitch_std),
97
- "jitter": float(jitter),
98
- "is_ai": pitch_std < self.thresholds["pitch_std_low"] or jitter < self.thresholds["jitter_low"]
99
- }
100
-
101
- if pitch_std < self.thresholds["pitch_std_low"]:
102
- ai_indicators += 2 # Strong signal
103
- reasons.append(f"unnaturally stable pitch (std={pitch_std:.1f})")
104
- else:
105
- human_indicators += 1
106
-
107
- if jitter < self.thresholds["jitter_low"]:
108
- ai_indicators += 2 # Strong signal
109
- reasons.append(f"missing pitch micro-variations (jitter={jitter:.2f})")
110
- else:
111
- human_indicators += 1
112
-
113
- print(f" Pitch std: {pitch_std:.2f} (threshold: {self.thresholds['pitch_std_low']})")
114
- print(f" Jitter: {jitter:.2f} (threshold: {self.thresholds['jitter_low']})")
115
- else:
116
- signals["pitch"] = {"error": "insufficient voiced segments"}
117
-
118
- except Exception as e:
119
- signals["pitch"] = {"error": str(e)}
120
- print(f" ⚠️ Pitch analysis error: {e}")
121
-
122
- # ============ 2. SPECTRAL SMOOTHNESS ============
123
- print(" 📊 Analyzing spectral characteristics...")
124
- try:
125
- # MFCC delta (rate of change) - AI voices have smoother changes
126
- mfccs = librosa.feature.mfcc(y=waveform, sr=self.sr, n_mfcc=13)
127
- mfcc_delta = librosa.feature.delta(mfccs)
128
- mfcc_delta_std = np.std(mfcc_delta)
129
-
130
- # Spectral flatness
131
- flatness = librosa.feature.spectral_flatness(y=waveform)[0]
132
- flatness_mean = np.mean(flatness)
133
-
134
- signals["spectral"] = {
135
- "mfcc_delta_std": float(mfcc_delta_std),
136
- "flatness_mean": float(flatness_mean),
137
- "is_ai": mfcc_delta_std < self.thresholds["mfcc_delta_std_low"]
138
- }
139
-
140
- if mfcc_delta_std < self.thresholds["mfcc_delta_std_low"]:
141
- ai_indicators += 2
142
- reasons.append(f"overly smooth spectral transitions (delta_std={mfcc_delta_std:.2f})")
143
- else:
144
- human_indicators += 1
145
-
146
- print(f" MFCC delta std: {mfcc_delta_std:.2f} (threshold: {self.thresholds['mfcc_delta_std_low']})")
147
- print(f" Spectral flatness: {flatness_mean:.4f}")
148
-
149
- except Exception as e:
150
- signals["spectral"] = {"error": str(e)}
151
- print(f" ⚠️ Spectral analysis error: {e}")
152
-
153
- # ============ 3. ENERGY DYNAMICS ============
154
- print(" 📈 Analyzing energy dynamics...")
155
- try:
156
- rms = librosa.feature.rms(y=waveform)[0]
157
- rms_mean = np.mean(rms)
158
- rms_std = np.std(rms)
159
- rms_cv = rms_std / (rms_mean + 1e-10) # Coefficient of variation
160
-
161
- signals["energy"] = {
162
- "rms_cv": float(rms_cv),
163
- "is_ai": rms_cv < self.thresholds["rms_cv_low"]
164
- }
165
-
166
- if rms_cv < self.thresholds["rms_cv_low"]:
167
- ai_indicators += 1
168
- reasons.append(f"unnaturally consistent energy (CV={rms_cv:.2f})")
169
- else:
170
- human_indicators += 1
171
-
172
- print(f" RMS CV: {rms_cv:.2f} (threshold: {self.thresholds['rms_cv_low']})")
173
-
174
- except Exception as e:
175
- signals["energy"] = {"error": str(e)}
176
- print(f" ⚠️ Energy analysis error: {e}")
177
-
178
- # ============ 4. HIGH-FREQUENCY CONTENT ============
179
- print(" 🔊 Analyzing high-frequency content...")
180
- try:
181
- stft = np.abs(librosa.stft(waveform))
182
- freq_bins = stft.shape[0]
183
-
184
- # Neural vocoders often attenuate high frequencies
185
- hf_energy = np.mean(stft[int(freq_bins * 0.7):, :])
186
- total_energy = np.mean(stft) + 1e-10
187
- hf_ratio = hf_energy / total_energy
188
-
189
- signals["hf_content"] = {
190
- "hf_ratio": float(hf_ratio),
191
- "is_ai": hf_ratio < self.thresholds["hf_energy_low"]
192
- }
193
-
194
- if hf_ratio < self.thresholds["hf_energy_low"]:
195
- ai_indicators += 1
196
- reasons.append(f"reduced high-frequency content (ratio={hf_ratio:.3f})")
197
- else:
198
- human_indicators += 1
199
-
200
- print(f" HF ratio: {hf_ratio:.4f} (threshold: {self.thresholds['hf_energy_low']})")
201
-
202
- except Exception as e:
203
- signals["hf_content"] = {"error": str(e)}
204
- print(f" ⚠️ HF analysis error: {e}")
205
-
206
- # ============ 5. BREATH DETECTION ============
207
- print(" 💨 Analyzing breath patterns...")
208
- try:
209
- # Look for characteristic breath sounds (fricative noise)
210
- # Breath sounds have high ZCR and low energy
211
- zcr = librosa.feature.zero_crossing_rate(waveform)[0]
212
- rms = librosa.feature.rms(y=waveform)[0]
213
-
214
- # Potential breath frames: high ZCR, low RMS
215
- zcr_threshold = np.percentile(zcr, 80)
216
- rms_threshold = np.percentile(rms, 30)
217
-
218
- potential_breaths = (zcr > zcr_threshold) & (rms < rms_threshold)
219
- breath_ratio = np.sum(potential_breaths) / len(potential_breaths)
220
-
221
- signals["breath"] = {
222
- "breath_ratio": float(breath_ratio),
223
- "is_ai": breath_ratio < 0.02 # Very few breath-like segments
224
- }
225
-
226
- if breath_ratio < 0.02:
227
- ai_indicators += 1
228
- reasons.append("missing natural breath patterns")
229
- else:
230
- human_indicators += 1
231
-
232
- print(f" Breath ratio: {breath_ratio:.4f}")
233
-
234
- except Exception as e:
235
- signals["breath"] = {"error": str(e)}
236
- print(f" ⚠️ Breath analysis error: {e}")
237
-
238
- # ============ FINAL DECISION ============
239
- total_signals = ai_indicators + human_indicators
240
- if total_signals > 0:
241
- ai_confidence = ai_indicators / total_signals
242
- else:
243
- ai_confidence = 0.5
244
-
245
- # Decision threshold - bias toward detection (better to flag AI than miss it)
246
- is_ai = ai_indicators >= 3 or ai_confidence >= 0.5
247
-
248
- if is_ai:
249
- classification = "AI_GENERATED"
250
- confidence = min(0.6 + (ai_indicators * 0.1), 0.95)
251
- explanation = f"AI voice detected: {'; '.join(reasons)}" if reasons else "Multiple AI indicators detected"
252
- else:
253
- classification = "HUMAN"
254
- confidence = min(0.6 + (human_indicators * 0.1), 0.95)
255
- explanation = "Human voice patterns confirmed"
256
-
257
- print(f" ✅ Result: {classification} ({confidence:.2%})")
258
- print(f" 📊 AI indicators: {ai_indicators}, Human indicators: {human_indicators}")
259
- print(f" 📝 {explanation}")
260
-
261
- return ElevenLabsDetectionResult(
262
- classification=classification,
263
- confidence=confidence,
264
- signals=signals,
265
- explanation=explanation
266
- )
267
-
268
-
269
- # Singleton instance
270
- elevenlabs_detector = ElevenLabsDetector()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/core/explainer.py DELETED
@@ -1,204 +0,0 @@
1
- """Explanation Generator Module.
2
-
3
- Generates human-readable explanations for detection results.
4
- """
5
-
6
- from typing import List, Dict
7
- from dataclasses import dataclass
8
-
9
- from app.core.signal_analyzer import SignalScores
10
-
11
-
12
- @dataclass
13
- class Artifact:
14
- """Detected artifact in audio."""
15
- timestamp: float
16
- type: str
17
- severity: str # "high", "medium", "low"
18
-
19
- def to_dict(self) -> Dict:
20
- return {
21
- "timestamp": self.timestamp,
22
- "type": self.type,
23
- "severity": self.severity
24
- }
25
-
26
-
27
- class ExplanationGenerator:
28
- """Generates explanations for detection results."""
29
-
30
- # Thresholds for scoring
31
- HIGH_THRESHOLD = 0.8
32
- MEDIUM_THRESHOLD = 0.6
33
-
34
- # Explanation templates
35
- SIGNAL_EXPLANATIONS = {
36
- "prosody": {
37
- "high": "Unnatural rhythm patterns detected - timing is too consistent",
38
- "medium": "Some rhythm irregularities noted",
39
- "low": "Natural speech rhythm detected"
40
- },
41
- "breath": {
42
- "high": "Missing natural breath sounds throughout the audio",
43
- "medium": "Reduced breath patterns compared to natural speech",
44
- "low": "Normal breathing patterns present"
45
- },
46
- "spectral": {
47
- "high": "Vocoder artifacts detected in frequency spectrum",
48
- "medium": "Some spectral irregularities detected",
49
- "low": "Natural spectral characteristics"
50
- },
51
- "formant": {
52
- "high": "Unnatural vocal formant transitions",
53
- "medium": "Some formant anomalies detected",
54
- "low": "Natural vocal transitions"
55
- },
56
- "silence": {
57
- "high": "Artificially perfect silence patterns",
58
- "medium": "Some silence irregularities",
59
- "low": "Natural pause patterns"
60
- }
61
- }
62
-
63
- LANGUAGE_INSIGHTS = {
64
- "tamil": "Tamil vowel length patterns analyzed",
65
- "hindi": "Hindi aspiration patterns checked",
66
- "telugu": "Telugu word-ending patterns verified",
67
- "malayalam": "Malayalam consonant patterns analyzed",
68
- "english": "English prosody patterns checked"
69
- }
70
-
71
- def _get_severity(self, score: float) -> str:
72
- """Convert score to severity level."""
73
- if score >= self.HIGH_THRESHOLD:
74
- return "high"
75
- elif score >= self.MEDIUM_THRESHOLD:
76
- return "medium"
77
- return "low"
78
-
79
- def generate_artifacts(
80
- self,
81
- scores: SignalScores,
82
- duration: float
83
- ) -> List[Artifact]:
84
- """Generate artifact list with timestamps.
85
-
86
- Args:
87
- scores: Signal analysis scores
88
- duration: Audio duration in seconds
89
-
90
- Returns:
91
- List of detected artifacts with timestamps
92
- """
93
- artifacts = []
94
-
95
- # Generate pseudo-timestamps based on scores
96
- # In a real implementation, you'd detect actual positions
97
-
98
- if scores.spectral_score >= self.MEDIUM_THRESHOLD:
99
- # Vocoder artifacts typically appear early
100
- artifacts.append(Artifact(
101
- timestamp=round(duration * 0.1, 2),
102
- type="vocoder_artifact",
103
- severity=self._get_severity(scores.spectral_score)
104
- ))
105
-
106
- if scores.breath_score >= self.MEDIUM_THRESHOLD:
107
- # Missing breath around middle
108
- artifacts.append(Artifact(
109
- timestamp=round(duration * 0.4, 2),
110
- type="missing_breath",
111
- severity=self._get_severity(scores.breath_score)
112
- ))
113
-
114
- if scores.formant_score >= self.MEDIUM_THRESHOLD:
115
- # Formant issues later in audio
116
- artifacts.append(Artifact(
117
- timestamp=round(duration * 0.7, 2),
118
- type="unnatural_formant",
119
- severity=self._get_severity(scores.formant_score)
120
- ))
121
-
122
- if scores.prosody_score >= self.HIGH_THRESHOLD:
123
- artifacts.append(Artifact(
124
- timestamp=round(duration * 0.2, 2),
125
- type="rhythm_anomaly",
126
- severity=self._get_severity(scores.prosody_score)
127
- ))
128
-
129
- if scores.silence_score >= self.HIGH_THRESHOLD:
130
- artifacts.append(Artifact(
131
- timestamp=round(duration * 0.5, 2),
132
- type="artificial_silence",
133
- severity=self._get_severity(scores.silence_score)
134
- ))
135
-
136
- # Sort by timestamp
137
- artifacts.sort(key=lambda x: x.timestamp)
138
-
139
- return artifacts
140
-
141
- def generate_explanation(
142
- self,
143
- classification: str,
144
- confidence: float,
145
- scores: SignalScores,
146
- language: str,
147
- artifacts: List[Artifact]
148
- ) -> str:
149
- """Generate human-readable explanation.
150
-
151
- Args:
152
- classification: "AI_GENERATED" or "HUMAN"
153
- confidence: Detection confidence
154
- scores: Signal analysis scores
155
- language: Detected language
156
- artifacts: List of detected artifacts
157
-
158
- Returns:
159
- Human-readable explanation string
160
- """
161
- parts = []
162
-
163
- if classification == "AI_GENERATED":
164
- # Start with main finding
165
- parts.append(f"This audio shows signs of AI generation with {confidence:.0%} confidence.")
166
-
167
- # Add specific findings based on scores
168
- high_signals = []
169
-
170
- if scores.spectral_score >= self.HIGH_THRESHOLD:
171
- high_signals.append("vocoder patterns")
172
- if scores.breath_score >= self.HIGH_THRESHOLD:
173
- high_signals.append("missing breath sounds")
174
- if scores.prosody_score >= self.HIGH_THRESHOLD:
175
- high_signals.append("unnatural rhythm")
176
- if scores.formant_score >= self.HIGH_THRESHOLD:
177
- high_signals.append("artificial vocal transitions")
178
- if scores.silence_score >= self.HIGH_THRESHOLD:
179
- high_signals.append("too-perfect pauses")
180
-
181
- if high_signals:
182
- parts.append(f"Key indicators: {', '.join(high_signals)}.")
183
-
184
- # Add artifact timestamps
185
- if artifacts:
186
- timestamps = [f"{a.timestamp}s ({a.type.replace('_', ' ')})"
187
- for a in artifacts[:3]]
188
- parts.append(f"Artifacts detected at: {', '.join(timestamps)}.")
189
-
190
- else: # HUMAN
191
- parts.append(f"This audio appears to be genuine human speech with {confidence:.0%} confidence.")
192
-
193
- # Mention what was checked
194
- parts.append("Natural breath patterns, varied prosody, and authentic vocal characteristics detected.")
195
-
196
- # Add language insight
197
- if language in self.LANGUAGE_INSIGHTS:
198
- parts.append(self.LANGUAGE_INSIGHTS[language])
199
-
200
- return " ".join(parts)
201
-
202
-
203
- # Create singleton instance
204
- explainer = ExplanationGenerator()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/core/hybrid_detector.py DELETED
@@ -1,504 +0,0 @@
1
- """Hybrid Deepfake Detection Module.
2
-
3
- Combines multiple detection signals for robust AI voice detection:
4
- 1. Spectral Analysis - Unusual frequency patterns in AI voices
5
- 2. Pitch Consistency - AI voices often have unnatural pitch stability
6
- 3. Pause Pattern Analysis - AI voices have different timing patterns
7
- 4. Formant Analysis - Synthetic voices have unusual formant transitions
8
- 5. Model-based Detection - ML model as one voting signal
9
- """
10
-
11
- import numpy as np
12
- from typing import Dict, Tuple, Optional
13
- from dataclasses import dataclass
14
- import librosa
15
- import scipy.stats as stats
16
- from scipy.signal import find_peaks
17
-
18
-
19
- @dataclass
20
- class DetectionSignal:
21
- """A single detection signal result."""
22
- name: str
23
- is_ai: bool
24
- confidence: float
25
- reason: str
26
-
27
-
28
- @dataclass
29
- class HybridResult:
30
- """Combined result from all detection signals."""
31
- classification: str # "AI_GENERATED" or "HUMAN"
32
- confidence: float
33
- signals: list[DetectionSignal]
34
- explanation: str
35
-
36
-
37
- class SpectralAnalyzer:
38
- """Analyzes spectral characteristics typical of AI-generated audio."""
39
-
40
- def __init__(self, sr: int = 16000):
41
- self.sr = sr
42
-
43
- def analyze(self, waveform: np.ndarray) -> DetectionSignal:
44
- """
45
- Analyze spectral features for AI detection.
46
-
47
- AI voices often have:
48
- - Unnaturally smooth spectral envelopes
49
- - Missing or reduced high-frequency content
50
- - Periodic spectral artifacts from neural vocoders
51
- """
52
- # Extract MFCCs
53
- mfccs = librosa.feature.mfcc(y=waveform, sr=self.sr, n_mfcc=20)
54
-
55
- # 1. MFCC variance analysis - AI voices often have lower variance
56
- mfcc_var = np.var(mfccs, axis=1).mean()
57
-
58
- # 2. Spectral flatness - AI voices can have unusual flatness patterns
59
- spectral_flatness = librosa.feature.spectral_flatness(y=waveform)[0]
60
- flatness_mean = np.mean(spectral_flatness)
61
- flatness_std = np.std(spectral_flatness)
62
-
63
- # 3. Spectral contrast - AI voices may have reduced contrast
64
- spectral_contrast = librosa.feature.spectral_contrast(y=waveform, sr=self.sr)
65
- contrast_var = np.var(spectral_contrast)
66
-
67
- # 4. High-frequency energy ratio
68
- stft = np.abs(librosa.stft(waveform))
69
- freq_bins = stft.shape[0]
70
- high_freq_energy = np.mean(stft[int(freq_bins * 0.7):, :])
71
- low_freq_energy = np.mean(stft[:int(freq_bins * 0.3), :]) + 1e-10
72
- hf_ratio = high_freq_energy / low_freq_energy
73
-
74
- # AI detection heuristics based on spectral features
75
- ai_score = 0.0
76
- reasons = []
77
-
78
- # Low MFCC variance suggests synthetic audio
79
- if mfcc_var < 15:
80
- ai_score += 0.25
81
- reasons.append("unusually consistent spectral patterns")
82
-
83
- # Very low or very high flatness can indicate synthesis
84
- if flatness_mean < 0.01 or flatness_mean > 0.3:
85
- ai_score += 0.2
86
- reasons.append("abnormal spectral flatness")
87
-
88
- # Low flatness variance (too consistent)
89
- if flatness_std < 0.02:
90
- ai_score += 0.25
91
- reasons.append("overly uniform spectral texture")
92
-
93
- # Low high-frequency content (vocoder artifact)
94
- if hf_ratio < 0.1:
95
- ai_score += 0.3
96
- reasons.append("reduced high-frequency content")
97
-
98
- is_ai = ai_score >= 0.4
99
- confidence = min(ai_score + 0.3, 0.95) if is_ai else max(0.7 - ai_score, 0.5)
100
-
101
- return DetectionSignal(
102
- name="Spectral Analysis",
103
- is_ai=is_ai,
104
- confidence=confidence,
105
- reason=", ".join(reasons) if reasons else "natural spectral characteristics"
106
- )
107
-
108
-
109
- class PitchAnalyzer:
110
- """Analyzes pitch patterns for AI detection."""
111
-
112
- def __init__(self, sr: int = 16000):
113
- self.sr = sr
114
-
115
- def analyze(self, waveform: np.ndarray) -> DetectionSignal:
116
- """
117
- Analyze pitch characteristics.
118
-
119
- AI voices often have:
120
- - Unnaturally consistent pitch (low variance)
121
- - Robotic pitch transitions
122
- - Missing micro-variations in F0
123
- """
124
- # Extract fundamental frequency (F0)
125
- f0, voiced_flag, _ = librosa.pyin(
126
- waveform,
127
- fmin=librosa.note_to_hz('C2'),
128
- fmax=librosa.note_to_hz('C7'),
129
- sr=self.sr
130
- )
131
-
132
- # Filter out unvoiced segments
133
- f0_voiced = f0[voiced_flag > 0.5] if len(f0[voiced_flag > 0.5]) > 10 else f0[~np.isnan(f0)]
134
-
135
- if len(f0_voiced) < 10:
136
- return DetectionSignal(
137
- name="Pitch Analysis",
138
- is_ai=False,
139
- confidence=0.5,
140
- reason="insufficient voiced segments for analysis"
141
- )
142
-
143
- # 1. Pitch variance - AI often has unnaturally low variance
144
- pitch_var = np.var(f0_voiced)
145
- pitch_std = np.std(f0_voiced)
146
-
147
- # 2. Pitch micro-variations (jitter) - human voices have natural jitter
148
- pitch_diff = np.diff(f0_voiced)
149
- jitter = np.mean(np.abs(pitch_diff))
150
-
151
- # 3. Pitch contour smoothness
152
- # Calculate second derivative for contour analysis
153
- if len(f0_voiced) > 20:
154
- pitch_acceleration = np.diff(pitch_diff)
155
- smoothness = 1.0 / (np.var(pitch_acceleration) + 1e-6)
156
- else:
157
- smoothness = 0
158
-
159
- ai_score = 0.0
160
- reasons = []
161
-
162
- # Very low pitch variance (robotic)
163
- if pitch_std < 10:
164
- ai_score += 0.35
165
- reasons.append("unnaturally stable pitch")
166
-
167
- # Very low jitter (too perfect)
168
- if jitter < 2:
169
- ai_score += 0.3
170
- reasons.append("missing natural pitch micro-variations")
171
-
172
- # Too smooth contour (lacks natural fluctuation)
173
- if smoothness > 100:
174
- ai_score += 0.25
175
- reasons.append("overly smooth pitch transitions")
176
-
177
- is_ai = ai_score >= 0.4
178
- confidence = min(ai_score + 0.3, 0.95) if is_ai else max(0.7 - ai_score, 0.5)
179
-
180
- return DetectionSignal(
181
- name="Pitch Analysis",
182
- is_ai=is_ai,
183
- confidence=confidence,
184
- reason=", ".join(reasons) if reasons else "natural pitch characteristics"
185
- )
186
-
187
-
188
- class PausePatternAnalyzer:
189
- """Analyzes pause and timing patterns for AI detection."""
190
-
191
- def __init__(self, sr: int = 16000):
192
- self.sr = sr
193
-
194
- def analyze(self, waveform: np.ndarray) -> DetectionSignal:
195
- """
196
- Analyze pause patterns.
197
-
198
- AI voices often have:
199
- - More consistent pause durations
200
- - Less natural speech rhythm
201
- - Different silence-to-speech ratios
202
- """
203
- # Calculate RMS energy
204
- frame_length = int(0.025 * self.sr) # 25ms frames
205
- hop_length = int(0.010 * self.sr) # 10ms hop
206
-
207
- rms = librosa.feature.rms(y=waveform, frame_length=frame_length, hop_length=hop_length)[0]
208
-
209
- # Dynamic threshold for speech/silence
210
- threshold = np.mean(rms) * 0.3
211
-
212
- # Find speech/silence segments
213
- is_speech = rms > threshold
214
-
215
- # Find segment boundaries
216
- changes = np.diff(is_speech.astype(int))
217
- speech_starts = np.where(changes == 1)[0]
218
- speech_ends = np.where(changes == -1)[0]
219
-
220
- if len(speech_starts) < 3 or len(speech_ends) < 3:
221
- return DetectionSignal(
222
- name="Pause Pattern",
223
- is_ai=False,
224
- confidence=0.5,
225
- reason="insufficient speech segments for analysis"
226
- )
227
-
228
- # Calculate pause durations (in frames)
229
- pause_durations = []
230
- for i in range(min(len(speech_ends), len(speech_starts) - 1)):
231
- if speech_starts[i + 1] > speech_ends[i]:
232
- pause_durations.append(speech_starts[i + 1] - speech_ends[i])
233
-
234
- if len(pause_durations) < 2:
235
- return DetectionSignal(
236
- name="Pause Pattern",
237
- is_ai=False,
238
- confidence=0.5,
239
- reason="not enough pauses to analyze"
240
- )
241
-
242
- # AI detection based on pause patterns
243
- pause_durations = np.array(pause_durations)
244
- pause_var = np.var(pause_durations)
245
- pause_cv = np.std(pause_durations) / (np.mean(pause_durations) + 1e-6) # Coefficient of variation
246
-
247
- # Speech segment durations
248
- speech_durations = []
249
- for i in range(min(len(speech_starts), len(speech_ends))):
250
- if speech_ends[i] > speech_starts[i]:
251
- speech_durations.append(speech_ends[i] - speech_starts[i])
252
-
253
- if len(speech_durations) > 1:
254
- speech_cv = np.std(speech_durations) / (np.mean(speech_durations) + 1e-6)
255
- else:
256
- speech_cv = 0.5
257
-
258
- ai_score = 0.0
259
- reasons = []
260
-
261
- # Very consistent pause lengths (artificial)
262
- if pause_cv < 0.3:
263
- ai_score += 0.35
264
- reasons.append("unnaturally consistent pause durations")
265
-
266
- # Very consistent speech lengths
267
- if speech_cv < 0.25:
268
- ai_score += 0.3
269
- reasons.append("robotic speech segment rhythm")
270
-
271
- # Very low pause variance
272
- if pause_var < 10:
273
- ai_score += 0.25
274
- reasons.append("mechanical timing patterns")
275
-
276
- is_ai = ai_score >= 0.4
277
- confidence = min(ai_score + 0.3, 0.95) if is_ai else max(0.7 - ai_score, 0.5)
278
-
279
- return DetectionSignal(
280
- name="Pause Pattern",
281
- is_ai=is_ai,
282
- confidence=confidence,
283
- reason=", ".join(reasons) if reasons else "natural speech rhythm"
284
- )
285
-
286
-
287
- class FormantAnalyzer:
288
- """Analyzes formant characteristics for AI detection."""
289
-
290
- def __init__(self, sr: int = 16000):
291
- self.sr = sr
292
-
293
- def analyze(self, waveform: np.ndarray) -> DetectionSignal:
294
- """
295
- Analyze formant patterns.
296
-
297
- AI voices may have:
298
- - Unnatural formant transitions
299
- - Missing formant dynamics
300
- - Unusual F1-F2 relationships
301
- """
302
- try:
303
- # Use spectral peaks as proxy for formants
304
- n_fft = 2048
305
- hop_length = 512
306
-
307
- stft = np.abs(librosa.stft(waveform, n_fft=n_fft, hop_length=hop_length))
308
-
309
- # Get spectral centroids over time as formant proxy
310
- spectral_centroid = librosa.feature.spectral_centroid(y=waveform, sr=self.sr)[0]
311
- spectral_bandwidth = librosa.feature.spectral_bandwidth(y=waveform, sr=self.sr)[0]
312
-
313
- # Formant transition smoothness (real voices have more variation)
314
- centroid_var = np.var(spectral_centroid)
315
- centroid_diff = np.diff(spectral_centroid)
316
- transition_smoothness = 1.0 / (np.var(centroid_diff) + 1e-6)
317
-
318
- # Bandwidth dynamics
319
- bandwidth_var = np.var(spectral_bandwidth)
320
-
321
- ai_score = 0.0
322
- reasons = []
323
-
324
- # Very smooth formant transitions (synthetic)
325
- if transition_smoothness > 500:
326
- ai_score += 0.3
327
- reasons.append("overly smooth formant transitions")
328
-
329
- # Low centroid variance (limited vocal tract modeling)
330
- if centroid_var < 50000:
331
- ai_score += 0.25
332
- reasons.append("limited formant variation")
333
-
334
- # Low bandwidth dynamics
335
- if bandwidth_var < 10000:
336
- ai_score += 0.25
337
- reasons.append("static spectral bandwidth")
338
-
339
- is_ai = ai_score >= 0.4
340
- confidence = min(ai_score + 0.3, 0.95) if is_ai else max(0.7 - ai_score, 0.5)
341
-
342
- return DetectionSignal(
343
- name="Formant Analysis",
344
- is_ai=is_ai,
345
- confidence=confidence,
346
- reason=", ".join(reasons) if reasons else "natural formant dynamics"
347
- )
348
-
349
- except Exception as e:
350
- return DetectionSignal(
351
- name="Formant Analysis",
352
- is_ai=False,
353
- confidence=0.5,
354
- reason=f"analysis error: {str(e)}"
355
- )
356
-
357
-
358
- # ... (Keep Analyzer classes: Spectral, Pitch, Pause, Formant as they are useful for explanation)
359
-
360
- # ... (Keep Analyzer classes: Spectral, Pitch, Pause, Formant as they are useful for explanation)
361
-
362
- class HybridDetector:
363
- """
364
- State-of-the-Art Ensemble Detector (Indian Accent Optimized).
365
-
366
- Combines:
367
- 1. Gustking (XLS-R 300M) - Multilingual (Best for Indian Accents)
368
- 2. MelodyMachine (WavLM Deepfake-v2) - High Fidelity
369
-
370
- Logic: Uses Cross-Model Agreement. If models disagree, we trust the 'Real' prediction
371
- more (Assumes Innocence) to avoid False Positives on non-standard accents.
372
- """
373
-
374
- def __init__(self, sr: int = 16000):
375
- self.sr = sr
376
- self.spectral_analyzer = SpectralAnalyzer(sr)
377
- self.pitch_analyzer = PitchAnalyzer(sr)
378
- self.pause_analyzer = PausePatternAnalyzer(sr)
379
- self.formant_analyzer = FormantAnalyzer(sr)
380
-
381
- # Model pipelines (lazy loaded)
382
- self._pipe1 = None # Gustking (XLS-R)
383
- self._pipe2 = None # MelodyMachine
384
- self._models_loaded = False
385
-
386
- def _load_models(self):
387
- """
388
- Load models on demand:
389
- 1. Primary: DavidCombei/wavLM-base-Deepfake_V2 (70%)
390
- 2. Secondary: MelodyMachine/Deepfake-audio-detection-v2 (30%)
391
- """
392
- if self._pipe1: return # Already loaded
393
-
394
- from transformers import pipeline
395
- print("🧠 Loading 2-Model Ensemble...")
396
- try:
397
- # Model 1: WavLM (Primary - Best Accuracy)
398
- self._pipe1 = pipeline("audio-classification", model="DavidCombei/wavLM-base-Deepfake_V2")
399
-
400
- # Model 2: MelodyMachine (Secondary - Artifacts)
401
- self._pipe2 = pipeline("audio-classification", model="MelodyMachine/Deepfake-audio-detection-v2")
402
-
403
- print("✅ 2 Models loaded successfully")
404
- except Exception as e:
405
- print(f"❌ Failed to load models: {e}")
406
-
407
- def _get_ai_probability(self, pipeline, waveform, model_name) -> float:
408
- try:
409
- results = pipeline(waveform, sampling_rate=self.sr)
410
-
411
- score_ai = 0.0
412
-
413
- for res in results:
414
- lbl = str(res["label"]).lower()
415
-
416
- # CRITICAL FIX: Label Mapping for WavLM/MelodyMachine
417
- # Research shows:
418
- # - LABEL_0 = FAKE (AI)
419
- # - LABEL_1 = REAL (Human)
420
- # - "fake" / "spoof" = FAKE
421
- # - "real" = REAL
422
-
423
- is_fake_label = False
424
-
425
- # 1. Explicit String Labels
426
- if "fake" in lbl or "spoof" in lbl or "synthetic" in lbl:
427
- is_fake_label = True
428
-
429
- # 2. Numbered Labels (DavidCombei/Melody Convention)
430
- # If model returns LABEL_0, it means FAKE
431
- elif "label_0" in lbl:
432
- is_fake_label = True
433
-
434
- # 3. Handle LABEL_1 (It is REAL, so ignore it)
435
- elif "label_1" in lbl or "real" in lbl:
436
- is_fake_label = False
437
-
438
- if is_fake_label:
439
- score_ai += res["score"]
440
-
441
- return score_ai
442
-
443
- except Exception as e:
444
- print(f" ⚠️ Error in {model_name}: {e}")
445
- return 0.5
446
-
447
- def detect(self, waveform: np.ndarray, language: str = "english") -> HybridResult:
448
- """
449
- Run Simplified High-Accuracy Ensemble (2 Models).
450
- """
451
- self._load_models()
452
- print(f"🔬 Running 2-Model Ensemble for: {language}")
453
-
454
- # 1. Run Models
455
- # Primary: WavLM (DavidCombei) - 70% Weight
456
- # Best performance for accents and general deepfake detection
457
- p_ai_1 = self._get_ai_probability(self._pipe1, waveform, "WavLM")
458
-
459
- # Secondary: MelodyMachine - 30% Weight
460
- # Good at detecting artifacts/noise
461
- p_ai_2 = self._get_ai_probability(self._pipe2, waveform, "MelodyMachine")
462
-
463
- print(f" 🤖 WavLM Score (70%): {p_ai_1:.2%}")
464
- print(f" 🤖 Melody Score (30%): {p_ai_2:.2%}")
465
-
466
- # --- SIMPLE WEIGHTED VOTING ---
467
- # Weights: WavLM=0.7, Melody=0.3
468
- # We trust WavLM more as it is a newer, better architecture than HuBERT
469
- weighted_score = (p_ai_1 * 0.70) + (p_ai_2 * 0.30)
470
-
471
- # --- SIMPLE THRESHOLD ---
472
- is_ai = weighted_score > 0.50
473
- confidence = weighted_score if is_ai else (1.0 - weighted_score)
474
- classification = "AI_GENERATED" if is_ai else "HUMAN"
475
-
476
- # 2. Explanations
477
- explanation_parts = []
478
- if is_ai:
479
- explanation_parts.append(f"AI Patterns Detected ({weighted_score:.0%})")
480
- else:
481
- explanation_parts.append(f"Natural Voice Confirmed ({confidence:.0%})")
482
-
483
- # Add signal context
484
- signals = []
485
- signals.append(self.pitch_analyzer.analyze(waveform))
486
-
487
- for s in signals:
488
- if s.is_ai == is_ai:
489
- explanation_parts.append(s.reason)
490
-
491
- explanation = "; ".join(explanation_parts)
492
-
493
- print(f" ✅ Result: {classification} ({confidence:.2%})")
494
-
495
- return HybridResult(
496
- classification=classification,
497
- confidence=confidence,
498
- signals=signals,
499
- explanation=explanation
500
- )
501
-
502
- # Singleton instance
503
- hybrid_detector = HybridDetector()
504
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/core/signal_analyzer.py DELETED
@@ -1,298 +0,0 @@
1
- """Signal Analyzer Module.
2
-
3
- Analyzes audio signals to extract multiple detection metrics:
4
- - Prosody (rhythm/timing)
5
- - Breath patterns
6
- - Spectral characteristics
7
- - Formant transitions
8
- - Silence patterns
9
- """
10
-
11
- import numpy as np
12
- import librosa
13
- from typing import Dict, List, Tuple
14
- from dataclasses import dataclass
15
-
16
- from app.config import settings
17
-
18
-
19
- @dataclass
20
- class SignalScores:
21
- """Container for all signal scores."""
22
- prosody_score: float
23
- breath_score: float
24
- spectral_score: float
25
- formant_score: float
26
- silence_score: float
27
-
28
- def to_dict(self) -> Dict[str, float]:
29
- """Convert to dictionary."""
30
- return {
31
- "prosody_score": self.prosody_score,
32
- "breath_score": self.breath_score,
33
- "spectral_score": self.spectral_score,
34
- "formant_score": self.formant_score,
35
- "silence_score": self.silence_score
36
- }
37
-
38
-
39
- class SignalAnalyzer:
40
- """Analyzes audio signals for deepfake detection features."""
41
-
42
- def __init__(self, sample_rate: int = None):
43
- """Initialize analyzer.
44
-
45
- Args:
46
- sample_rate: Audio sample rate (default: 16000)
47
- """
48
- self.sr = sample_rate or settings.TARGET_SAMPLE_RATE
49
-
50
- def calculate_prosody_score(self, waveform: np.ndarray) -> float:
51
- """Analyze prosody (rhythm and timing patterns).
52
-
53
- AI-generated audio often has unnaturally consistent rhythm.
54
-
55
- Args:
56
- waveform: Audio waveform
57
-
58
- Returns:
59
- Prosody anomaly score (higher = more likely AI)
60
- """
61
- try:
62
- # Extract tempo and beat frames
63
- tempo, beat_frames = librosa.beat.beat_track(y=waveform, sr=self.sr)
64
-
65
- if len(beat_frames) < 2:
66
- return 0.5 # Not enough data
67
-
68
- # Calculate inter-beat intervals
69
- beat_times = librosa.frames_to_time(beat_frames, sr=self.sr)
70
- intervals = np.diff(beat_times)
71
-
72
- if len(intervals) == 0:
73
- return 0.5
74
-
75
- # AI audio tends to have very consistent intervals
76
- # Calculate coefficient of variation
77
- cv = np.std(intervals) / (np.mean(intervals) + 1e-6)
78
-
79
- # Low CV = suspicious (too consistent)
80
- # Map to score: low CV -> high score
81
- score = 1.0 - min(cv * 2, 1.0)
82
-
83
- return float(np.clip(score, 0, 1))
84
-
85
- except Exception:
86
- return 0.5
87
-
88
- def calculate_breath_score(self, waveform: np.ndarray) -> float:
89
- """Detect breath patterns in speech.
90
-
91
- Human speech has natural breath pauses that AI often misses.
92
-
93
- Args:
94
- waveform: Audio waveform
95
-
96
- Returns:
97
- Breath anomaly score (higher = more likely AI - missing breaths)
98
- """
99
- try:
100
- # Calculate RMS energy
101
- rms = librosa.feature.rms(y=waveform, frame_length=2048, hop_length=512)[0]
102
-
103
- # Normalize
104
- rms_norm = rms / (np.max(rms) + 1e-6)
105
-
106
- # Find low energy regions (potential breath/pause locations)
107
- threshold = 0.1
108
- low_energy = rms_norm < threshold
109
-
110
- # Count transitions (speech to silence and back)
111
- transitions = np.sum(np.abs(np.diff(low_energy.astype(int))))
112
-
113
- # Duration in seconds
114
- duration = len(waveform) / self.sr
115
-
116
- # Expected transitions per second of speech
117
- expected_transitions_per_sec = 0.3 # Roughly one breath every 3-4 seconds
118
- expected = expected_transitions_per_sec * duration * 2
119
-
120
- # Too few transitions = suspicious (AI doesn't breathe)
121
- ratio = transitions / (expected + 1e-6)
122
-
123
- # Low ratio means fewer natural pauses
124
- if ratio < 0.5:
125
- score = 0.9 # Very suspicious
126
- elif ratio < 1.0:
127
- score = 0.7
128
- else:
129
- score = 0.3 # Normal breathing patterns
130
-
131
- return float(score)
132
-
133
- except Exception:
134
- return 0.5
135
-
136
- def calculate_spectral_score(self, waveform: np.ndarray) -> float:
137
- """Analyze spectral characteristics for vocoder artifacts.
138
-
139
- AI vocoders leave characteristic spectral patterns.
140
-
141
- Args:
142
- waveform: Audio waveform
143
-
144
- Returns:
145
- Spectral anomaly score (higher = more likely AI)
146
- """
147
- try:
148
- # Calculate spectral flatness
149
- spec_flat = librosa.feature.spectral_flatness(y=waveform)[0]
150
-
151
- # Calculate spectral centroid variation
152
- spec_cent = librosa.feature.spectral_centroid(y=waveform, sr=self.sr)[0]
153
- cent_var = np.std(spec_cent) / (np.mean(spec_cent) + 1e-6)
154
-
155
- # AI audio often has higher spectral flatness
156
- avg_flatness = np.mean(spec_flat)
157
-
158
- # Combine metrics
159
- # High flatness or low centroid variation = suspicious
160
- flatness_score = min(avg_flatness * 3, 1.0)
161
- variation_score = 1.0 - min(cent_var * 2, 1.0)
162
-
163
- score = (flatness_score + variation_score) / 2
164
-
165
- return float(np.clip(score, 0, 1))
166
-
167
- except Exception:
168
- return 0.5
169
-
170
- def calculate_formant_score(self, waveform: np.ndarray) -> float:
171
- """Analyze formant transitions.
172
-
173
- Human vocal tract creates characteristic formant patterns.
174
-
175
- Args:
176
- waveform: Audio waveform
177
-
178
- Returns:
179
- Formant anomaly score (higher = more likely AI)
180
- """
181
- try:
182
- # Use MFCC as proxy for formant information
183
- mfccs = librosa.feature.mfcc(y=waveform, sr=self.sr, n_mfcc=13)
184
-
185
- # Calculate delta MFCCs (transitions)
186
- delta_mfccs = librosa.feature.delta(mfccs)
187
-
188
- # AI often has smoother, less varied transitions
189
- delta_var = np.mean(np.std(delta_mfccs, axis=1))
190
-
191
- # Low variation in deltas = suspicious
192
- # Map: low variation -> high score
193
- score = 1.0 - min(delta_var * 10, 1.0)
194
-
195
- return float(np.clip(score, 0, 1))
196
-
197
- except Exception:
198
- return 0.5
199
-
200
- def calculate_silence_score(self, waveform: np.ndarray) -> float:
201
- """Analyze silence/pause patterns.
202
-
203
- AI-generated audio often has "too perfect" silences.
204
-
205
- Args:
206
- waveform: Audio waveform
207
-
208
- Returns:
209
- Silence anomaly score (higher = more likely AI)
210
- """
211
- try:
212
- # Split into frames
213
- frame_length = 2048
214
- hop_length = 512
215
-
216
- # Calculate energy per frame
217
- frames = librosa.util.frame(waveform, frame_length=frame_length, hop_length=hop_length)
218
- frame_energy = np.mean(frames ** 2, axis=0)
219
-
220
- # Identify silence frames (very low energy)
221
- silence_threshold = np.percentile(frame_energy, 10)
222
- silence_frames = frame_energy < silence_threshold
223
-
224
- # Calculate silence duration distribution
225
- silence_runs = []
226
- current_run = 0
227
-
228
- for is_silent in silence_frames:
229
- if is_silent:
230
- current_run += 1
231
- elif current_run > 0:
232
- silence_runs.append(current_run)
233
- current_run = 0
234
-
235
- if current_run > 0:
236
- silence_runs.append(current_run)
237
-
238
- if len(silence_runs) < 2:
239
- return 0.5
240
-
241
- # AI tends to have very uniform silence lengths
242
- silence_cv = np.std(silence_runs) / (np.mean(silence_runs) + 1e-6)
243
-
244
- # Low variation = suspicious
245
- score = 1.0 - min(silence_cv, 1.0)
246
-
247
- return float(np.clip(score, 0, 1))
248
-
249
- except Exception:
250
- return 0.5
251
-
252
- def analyze(self, waveform: np.ndarray) -> SignalScores:
253
- """Run all signal analyses.
254
-
255
- Args:
256
- waveform: Audio waveform (16kHz, mono)
257
-
258
- Returns:
259
- SignalScores with all metrics
260
- """
261
- return SignalScores(
262
- prosody_score=self.calculate_prosody_score(waveform),
263
- breath_score=self.calculate_breath_score(waveform),
264
- spectral_score=self.calculate_spectral_score(waveform),
265
- formant_score=self.calculate_formant_score(waveform),
266
- silence_score=self.calculate_silence_score(waveform)
267
- )
268
-
269
- def get_average_score(self, scores: SignalScores) -> float:
270
- """Calculate weighted average of all scores.
271
-
272
- Args:
273
- scores: Signal scores
274
-
275
- Returns:
276
- Weighted average score
277
- """
278
- weights = {
279
- "prosody": 0.15,
280
- "breath": 0.25,
281
- "spectral": 0.30,
282
- "formant": 0.15,
283
- "silence": 0.15
284
- }
285
-
286
- weighted = (
287
- scores.prosody_score * weights["prosody"] +
288
- scores.breath_score * weights["breath"] +
289
- scores.spectral_score * weights["spectral"] +
290
- scores.formant_score * weights["formant"] +
291
- scores.silence_score * weights["silence"]
292
- )
293
-
294
- return float(weighted)
295
-
296
-
297
- # Create singleton instance
298
- signal_analyzer = SignalAnalyzer()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/main.py CHANGED
@@ -22,10 +22,9 @@ app = FastAPI(
22
 
23
  VoiceGuard detects whether an audio sample is AI-generated (deepfake) or genuine human speech.
24
 
 
25
  ### Features
26
  - 🎯 **Multi-Language Support**: Tamil, English, Hindi, Malayalam, Telugu
27
- - 📊 **Signal Analysis**: 5 detection signals (prosody, breath, spectral, formant, silence)
28
- - 🔥 **Visual Heatmaps**: Spectrogram visualization showing artifact locations
29
  - 📝 **Explanations**: Human-readable reasoning for each detection
30
 
31
  ### How It Works
@@ -51,10 +50,6 @@ app.add_middleware(
51
  allow_headers=["*"],
52
  )
53
 
54
- # Mount static files for heatmaps
55
- os.makedirs(settings.HEATMAP_DIR, exist_ok=True)
56
- app.mount("/static", StaticFiles(directory="static"), name="static")
57
-
58
  # Mount frontend files
59
  frontend_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "frontend")
60
  if os.path.exists(frontend_dir):
 
22
 
23
  VoiceGuard detects whether an audio sample is AI-generated (deepfake) or genuine human speech.
24
 
25
+ ### Features
26
  ### Features
27
  - 🎯 **Multi-Language Support**: Tamil, English, Hindi, Malayalam, Telugu
 
 
28
  - 📝 **Explanations**: Human-readable reasoning for each detection
29
 
30
  ### How It Works
 
50
  allow_headers=["*"],
51
  )
52
 
 
 
 
 
53
  # Mount frontend files
54
  frontend_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "frontend")
55
  if os.path.exists(frontend_dir):
app/visualization/__init__.py DELETED
@@ -1 +0,0 @@
1
- """Visualization Package."""
 
 
app/visualization/heatmap.py DELETED
@@ -1,220 +0,0 @@
1
- """Heatmap Visualization Module.
2
-
3
- Generates spectrogram heatmaps with artifact highlighting.
4
- """
5
-
6
- import os
7
- import uuid
8
- import numpy as np
9
- import matplotlib
10
- matplotlib.use('Agg') # Non-GUI backend
11
- import matplotlib.pyplot as plt
12
- import librosa
13
- import librosa.display
14
- from typing import Optional, Tuple
15
-
16
- from app.config import settings
17
- from app.core.signal_analyzer import SignalScores
18
-
19
-
20
- class HeatmapGenerator:
21
- """Generates spectrogram heatmaps with attention overlays."""
22
-
23
- def __init__(self, output_dir: str = None):
24
- """Initialize generator.
25
-
26
- Args:
27
- output_dir: Directory to save heatmap images
28
- """
29
- self.output_dir = output_dir or settings.HEATMAP_DIR
30
- os.makedirs(self.output_dir, exist_ok=True)
31
-
32
- def generate_spectrogram(
33
- self,
34
- waveform: np.ndarray,
35
- sr: int = 16000
36
- ) -> np.ndarray:
37
- """Generate Mel spectrogram from waveform.
38
-
39
- Args:
40
- waveform: Audio waveform
41
- sr: Sample rate
42
-
43
- Returns:
44
- Mel spectrogram in dB scale
45
- """
46
- mel_spec = librosa.feature.melspectrogram(
47
- y=waveform,
48
- sr=sr,
49
- n_mels=128,
50
- fmax=8000
51
- )
52
- mel_db = librosa.power_to_db(mel_spec, ref=np.max)
53
- return mel_db
54
-
55
- def create_attention_overlay(
56
- self,
57
- spectrogram: np.ndarray,
58
- scores: SignalScores,
59
- duration: float
60
- ) -> np.ndarray:
61
- """Create attention overlay showing artifact regions.
62
-
63
- This creates a heatmap based on signal scores to highlight
64
- suspicious regions in the spectrogram.
65
-
66
- Args:
67
- spectrogram: Mel spectrogram
68
- scores: Signal analysis scores
69
- duration: Audio duration in seconds
70
-
71
- Returns:
72
- Attention weights array (same shape as spectrogram)
73
- """
74
- n_mels, n_frames = spectrogram.shape
75
- attention = np.zeros((n_mels, n_frames))
76
-
77
- # Average score determines overall intensity
78
- avg_score = (
79
- scores.prosody_score +
80
- scores.breath_score +
81
- scores.spectral_score +
82
- scores.formant_score +
83
- scores.silence_score
84
- ) / 5
85
-
86
- # Create regions based on individual scores
87
- frame_third = n_frames // 3
88
-
89
- # Early region (prosody/rhythm issues)
90
- if scores.prosody_score > 0.5:
91
- attention[:, :frame_third] += scores.prosody_score * 0.6
92
-
93
- # Mid region (breath/silence issues)
94
- if scores.breath_score > 0.5:
95
- attention[:, frame_third:2*frame_third] += scores.breath_score * 0.8
96
-
97
- if scores.silence_score > 0.5:
98
- attention[64:, frame_third:2*frame_third] += scores.silence_score * 0.5
99
-
100
- # Late region (formant issues)
101
- if scores.formant_score > 0.5:
102
- attention[:64, 2*frame_third:] += scores.formant_score * 0.6
103
-
104
- # Spectral artifacts throughout
105
- if scores.spectral_score > 0.5:
106
- # Focus on middle frequencies where vocoder artifacts appear
107
- attention[32:96, :] += scores.spectral_score * 0.4
108
-
109
- # Normalize to 0-1
110
- if np.max(attention) > 0:
111
- attention = attention / np.max(attention)
112
-
113
- # Apply some smoothing
114
- from scipy.ndimage import gaussian_filter
115
- attention = gaussian_filter(attention, sigma=3)
116
-
117
- return attention
118
-
119
- def create_heatmap(
120
- self,
121
- waveform: np.ndarray,
122
- scores: SignalScores,
123
- duration: float,
124
- classification: str,
125
- sr: int = 16000
126
- ) -> str:
127
- """Create and save heatmap visualization.
128
-
129
- Args:
130
- waveform: Audio waveform
131
- scores: Signal analysis scores
132
- duration: Audio duration
133
- classification: Detection result
134
- sr: Sample rate
135
-
136
- Returns:
137
- Path to saved heatmap image
138
- """
139
- # Generate spectrogram
140
- mel_db = self.generate_spectrogram(waveform, sr)
141
-
142
- # Create attention overlay
143
- attention = self.create_attention_overlay(mel_db, scores, duration)
144
-
145
- # Create figure
146
- fig, ax = plt.subplots(figsize=(12, 4))
147
-
148
- # Plot spectrogram
149
- img = librosa.display.specshow(
150
- mel_db,
151
- sr=sr,
152
- x_axis='time',
153
- y_axis='mel',
154
- ax=ax,
155
- cmap='viridis'
156
- )
157
-
158
- # Overlay attention heatmap
159
- # Red for AI, green for human
160
- cmap = 'Reds' if classification == "AI_GENERATED" else 'Greens'
161
- ax.imshow(
162
- attention,
163
- aspect='auto',
164
- alpha=0.5,
165
- cmap=cmap,
166
- extent=[0, duration, 0, mel_db.shape[0]],
167
- origin='lower'
168
- )
169
-
170
- # Styling
171
- ax.set_title(
172
- f'Detection Heatmap - {classification}',
173
- fontsize=14,
174
- fontweight='bold',
175
- color='white' if classification == "AI_GENERATED" else 'darkgreen'
176
- )
177
- ax.set_xlabel('Time (seconds)')
178
- ax.set_ylabel('Mel Frequency')
179
-
180
- # Colorbar
181
- plt.colorbar(img, ax=ax, format='%+2.0f dB')
182
-
183
- # Background color
184
- fig.patch.set_facecolor('#1e293b')
185
- ax.set_facecolor('#1e293b')
186
- ax.tick_params(colors='#94a3b8')
187
- ax.xaxis.label.set_color('#f1f5f9')
188
- ax.yaxis.label.set_color('#f1f5f9')
189
-
190
- plt.tight_layout()
191
-
192
- # Save
193
- filename = f"{uuid.uuid4().hex[:12]}.png"
194
- filepath = os.path.join(self.output_dir, filename)
195
-
196
- fig.savefig(
197
- filepath,
198
- dpi=150,
199
- facecolor='#1e293b',
200
- edgecolor='none',
201
- bbox_inches='tight'
202
- )
203
- plt.close(fig)
204
-
205
- return filename
206
-
207
- def get_heatmap_url(self, filename: str) -> str:
208
- """Get API URL for heatmap.
209
-
210
- Args:
211
- filename: Heatmap filename
212
-
213
- Returns:
214
- API URL path
215
- """
216
- return f"/api/v1/heatmap/{filename}"
217
-
218
-
219
- # Create singleton instance
220
- heatmap_generator = HeatmapGenerator()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
requirements.txt CHANGED
@@ -1,14 +1,11 @@
1
  # VoiceGuard Backend Requirements
2
  # ================================
3
 
4
- # Gradio UI
5
- gradio>=4.44.0
6
-
7
  # Audio Processing
8
  librosa>=0.10.1
9
  soundfile>=0.12.1
10
  numpy
11
- scipy>=1.11.4
12
 
13
  # Machine Learning
14
  torch>=2.0.0
@@ -22,6 +19,3 @@ python-multipart>=0.0.9
22
  pydantic==2.5.3
23
  pydantic-settings==2.1.0
24
  python-dotenv==1.0.0
25
-
26
- # Visualization
27
- matplotlib==3.8.2
 
1
  # VoiceGuard Backend Requirements
2
  # ================================
3
 
 
 
 
4
  # Audio Processing
5
  librosa>=0.10.1
6
  soundfile>=0.12.1
7
  numpy
8
+ pydub>=0.25.1
9
 
10
  # Machine Learning
11
  torch>=2.0.0
 
19
  pydantic==2.5.3
20
  pydantic-settings==2.1.0
21
  python-dotenv==1.0.0