Spaces:
Sleeping
Sleeping
VoiceGuard Bot commited on
Commit ·
1c5dd00
1
Parent(s): f915d06
Deploy optimized VoiceGuard with Abhishtagatya model
Browse files- Dockerfile +8 -3
- README.md +1 -0
- app.py +0 -183
- app/api/routes.py +3 -11
- app/config.py +3 -8
- app/core/audio_processor.py +1 -1
- app/core/detector.py +27 -4
- app/core/elevenlabs_detector.py +0 -270
- app/core/explainer.py +0 -204
- app/core/hybrid_detector.py +0 -504
- app/core/signal_analyzer.py +0 -298
- app/main.py +1 -6
- app/visualization/__init__.py +0 -1
- app/visualization/heatmap.py +0 -220
- requirements.txt +1 -7
Dockerfile
CHANGED
|
@@ -23,6 +23,11 @@ COPY . .
|
|
| 23 |
# Expose port (documentary only, but good practice)
|
| 24 |
EXPOSE 8000
|
| 25 |
|
| 26 |
-
#
|
| 27 |
-
|
| 28 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 76 |
-
print("🔍 Running
|
| 77 |
-
result =
|
| 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 =
|
| 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=
|
| 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 |
-
|
| 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
|
|
|
|
|
|
|
|
|