aaronmphilip commited on
Commit
12740fd
·
verified ·
1 Parent(s): 5c8af73

Deploy DHVANI inference backend

Browse files
Files changed (5) hide show
  1. Dockerfile +17 -0
  2. README.md +14 -5
  3. analyzer.py +135 -0
  4. app.py +72 -0
  5. requirements.txt +9 -0
Dockerfile ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ RUN apt-get update && apt-get install -y --no-install-recommends \
6
+ libsndfile1 ffmpeg \
7
+ && rm -rf /var/lib/apt/lists/*
8
+
9
+ COPY requirements.txt .
10
+ RUN pip install --no-cache-dir -r requirements.txt
11
+
12
+ COPY . .
13
+
14
+ ENV DHVANI_MODEL_ID=garystafford/wav2vec2-deepfake-voice-detector
15
+
16
+ EXPOSE 7860
17
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,10 +1,19 @@
1
  ---
2
- title: Dhvani
3
- emoji: 🏃
4
- colorFrom: pink
5
- colorTo: blue
6
  sdk: docker
 
7
  pinned: false
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
1
  ---
2
+ title: DHVANI Inference
3
+ emoji: 🛡️
4
+ colorFrom: blue
5
+ colorTo: indigo
6
  sdk: docker
7
+ app_port: 7860
8
  pinned: false
9
+ license: mit
10
  ---
11
 
12
+ # DHVANI Inference Backend
13
+
14
+ Free CPU inference for the DHVANI voice authenticity checker.
15
+ Used by the Vercel frontend via `DHVANI_BACKEND_URL`.
16
+
17
+ Endpoints:
18
+ - `GET /health`
19
+ - `POST /analyze` (multipart field: `audio`)
analyzer.py ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """DHVANI audio authenticity analyzer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+
9
+ import librosa
10
+ import numpy as np
11
+ import torch
12
+ from transformers import AutoFeatureExtractor, AutoModelForAudioClassification
13
+
14
+
15
+ MODEL_ID = os.getenv("DHVANI_MODEL_ID", "garystafford/wav2vec2-deepfake-voice-detector")
16
+ SAMPLE_RATE = 16_000
17
+ MAX_DURATION_SEC = 30.0
18
+
19
+
20
+ @dataclass
21
+ class AnalysisResult:
22
+ synthetic_probability: float
23
+ authentic_probability: float
24
+ verdict: str
25
+ verdict_en: str
26
+ hindi_message: str
27
+ model_id: str
28
+ duration_sec: float
29
+
30
+
31
+ class VoiceAnalyzer:
32
+ def __init__(self) -> None:
33
+ self._model = None
34
+ self._extractor = None
35
+ self._device = "cuda" if torch.cuda.is_available() else "cpu"
36
+
37
+ def _ensure_loaded(self) -> None:
38
+ if self._model is not None:
39
+ return
40
+ self._extractor = AutoFeatureExtractor.from_pretrained(MODEL_ID)
41
+ self._model = AutoModelForAudioClassification.from_pretrained(MODEL_ID)
42
+ self._model.to(self._device)
43
+ self._model.eval()
44
+
45
+ def analyze_file(self, audio_path: str | Path) -> AnalysisResult:
46
+ self._ensure_loaded()
47
+ waveform, _ = librosa.load(str(audio_path), sr=SAMPLE_RATE, mono=True)
48
+ duration_sec = len(waveform) / SAMPLE_RATE
49
+ max_samples = int(MAX_DURATION_SEC * SAMPLE_RATE)
50
+ if len(waveform) > max_samples:
51
+ waveform = waveform[:max_samples]
52
+
53
+ inputs = self._extractor(
54
+ waveform,
55
+ sampling_rate=SAMPLE_RATE,
56
+ return_tensors="pt",
57
+ padding=True,
58
+ )
59
+ inputs = {key: value.to(self._device) for key, value in inputs.items()}
60
+
61
+ with torch.no_grad():
62
+ logits = self._model(**inputs).logits
63
+ probs = torch.softmax(logits, dim=-1).cpu().numpy()[0]
64
+
65
+ id2label = self._model.config.id2label
66
+ label_probs = {id2label[i].lower(): float(probs[i]) for i in range(len(probs))}
67
+
68
+ fake_prob = self._pick_probability(
69
+ label_probs,
70
+ keys=("fake", "spoof", "synthetic", "deepfake", "bonafide_neg", "label_1", "1"),
71
+ )
72
+ real_prob = self._pick_probability(
73
+ label_probs,
74
+ keys=("real", "bonafide", "genuine", "authentic", "label_0", "0"),
75
+ )
76
+
77
+ if fake_prob is None and real_prob is None:
78
+ fake_prob = float(np.max(probs))
79
+ real_prob = 1.0 - fake_prob
80
+ elif fake_prob is None:
81
+ fake_prob = max(0.0, 1.0 - real_prob)
82
+ elif real_prob is None:
83
+ real_prob = max(0.0, 1.0 - fake_prob)
84
+
85
+ verdict, verdict_en, hindi_message = self._verdict(fake_prob)
86
+
87
+ return AnalysisResult(
88
+ synthetic_probability=round(fake_prob * 100, 1),
89
+ authentic_probability=round(real_prob * 100, 1),
90
+ verdict=verdict,
91
+ verdict_en=verdict_en,
92
+ hindi_message=hindi_message,
93
+ model_id=MODEL_ID,
94
+ duration_sec=round(duration_sec, 2),
95
+ )
96
+
97
+ @staticmethod
98
+ def _pick_probability(label_probs: dict[str, float], keys: tuple[str, ...]) -> float | None:
99
+ for key in keys:
100
+ if key in label_probs:
101
+ return label_probs[key]
102
+ for label, prob in label_probs.items():
103
+ if any(key in label for key in keys):
104
+ return prob
105
+ return None
106
+
107
+ @staticmethod
108
+ def _verdict(fake_prob: float) -> tuple[str, str, str]:
109
+ pct = int(round(fake_prob * 100))
110
+ if fake_prob >= 0.75:
111
+ return (
112
+ "LIKELY_FAKE",
113
+ f"Likely AI-generated voice ({pct}% synthetic probability).",
114
+ (
115
+ f"⚠️ {pct}% synthetic probability. Yeh awaaz AI-cloned lag rahi hai. "
116
+ "Order verify karein. Turant action mat lein."
117
+ ),
118
+ )
119
+ if fake_prob >= 0.45:
120
+ return (
121
+ "SUSPICIOUS",
122
+ f"Suspicious voice pattern ({pct}% synthetic probability).",
123
+ (
124
+ f"⚠️ {pct}% synthetic probability. Sandeh janak awaaz. "
125
+ "Doosre channel se confirm karein."
126
+ ),
127
+ )
128
+ return (
129
+ "LIKELY_AUTHENTIC",
130
+ f"Likely authentic voice ({pct}% synthetic probability).",
131
+ (
132
+ f"✅ Kam synthetic signs ({pct}%). Phir bhi critical order ho to "
133
+ "dusre channel se verify karein."
134
+ ),
135
+ )
app.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """DHVANI inference API for Hugging Face Spaces."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import tempfile
6
+ import uuid
7
+ from pathlib import Path
8
+
9
+ from fastapi import FastAPI, File, HTTPException, UploadFile
10
+ from fastapi.middleware.cors import CORSMiddleware
11
+
12
+ from analyzer import VoiceAnalyzer
13
+
14
+ ALLOWED = {".wav", ".mp3", ".m4a", ".ogg", ".flac", ".webm"}
15
+ MAX_BYTES = 20 * 1024 * 1024
16
+
17
+ app = FastAPI(title="DHVANI Inference", version="0.1.0")
18
+ app.add_middleware(
19
+ CORSMiddleware,
20
+ allow_origins=["*"],
21
+ allow_methods=["*"],
22
+ allow_headers=["*"],
23
+ )
24
+
25
+ analyzer = VoiceAnalyzer()
26
+
27
+
28
+ @app.get("/")
29
+ @app.get("/health")
30
+ def health():
31
+ return {
32
+ "status": "ok",
33
+ "product": "DHVANI",
34
+ "mode": "hf-space",
35
+ "version": "0.1.0",
36
+ }
37
+
38
+
39
+ @app.post("/analyze")
40
+ async def analyze(audio: UploadFile = File(...)):
41
+ if not audio.filename:
42
+ raise HTTPException(status_code=400, detail="Empty filename.")
43
+
44
+ suffix = Path(audio.filename).suffix.lower()
45
+ if suffix and suffix not in ALLOWED:
46
+ raise HTTPException(status_code=400, detail=f"Unsupported file type: {suffix}")
47
+
48
+ data = await audio.read()
49
+ if not data:
50
+ raise HTTPException(status_code=400, detail="No audio file provided.")
51
+ if len(data) > MAX_BYTES:
52
+ raise HTTPException(status_code=400, detail="File exceeds 20 MB limit.")
53
+
54
+ saved_path = Path(tempfile.gettempdir()) / f"{uuid.uuid4().hex}{suffix or '.wav'}"
55
+ saved_path.write_bytes(data)
56
+
57
+ try:
58
+ result = analyzer.analyze_file(saved_path)
59
+ return {
60
+ "synthetic_probability": result.synthetic_probability,
61
+ "authentic_probability": result.authentic_probability,
62
+ "verdict": result.verdict,
63
+ "verdict_en": result.verdict_en,
64
+ "hindi_message": result.hindi_message,
65
+ "model_id": result.model_id,
66
+ "duration_sec": result.duration_sec,
67
+ }
68
+ except Exception as exc: # noqa: BLE001
69
+ raise HTTPException(status_code=500, detail=f"Analysis failed: {exc}") from exc
70
+ finally:
71
+ if saved_path.exists():
72
+ saved_path.unlink()
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ fastapi>=0.110.0
2
+ uvicorn[standard]>=0.27.0
3
+ python-multipart>=0.0.9
4
+ torch>=2.2.0
5
+ torchaudio>=2.2.0
6
+ transformers>=4.40.0
7
+ librosa>=0.10.0
8
+ soundfile>=0.12.0
9
+ numpy>=1.26.0