Khalil09 commited on
Commit
4e41382
Β·
verified Β·
1 Parent(s): 870643b

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +536 -0
app.py ADDED
@@ -0,0 +1,536 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import numpy as np
3
+ import librosa
4
+ import matplotlib
5
+ matplotlib.use('Agg')
6
+ import matplotlib.pyplot as plt
7
+ import matplotlib.patches as mpatches
8
+ from matplotlib.gridspec import GridSpec
9
+ import io
10
+ from PIL import Image
11
+ import warnings
12
+ warnings.filterwarnings('ignore')
13
+
14
+ # ── colour palette ──────────────────────────────────────────────────────────
15
+ BG = "#0a0a0f"
16
+ PANEL = "#12121a"
17
+ ACCENT = "#00f5c4"
18
+ ACCENT2 = "#7b61ff"
19
+ DANGER = "#ff4d6d"
20
+ WARNING = "#ffb347"
21
+ TEXT = "#e8e8f0"
22
+ SUBTEXT = "#6b6b8a"
23
+
24
+ # ═══════════════════════════════════════════════════════════════════════════
25
+ # FEATURE EXTRACTION
26
+ # ═══════════════════════════════════════════════════════════════════════════
27
+
28
+ def extract_features(audio_path):
29
+ """Extract comprehensive audio features for deepfake detection."""
30
+ y, sr = librosa.load(audio_path, sr=None, duration=30)
31
+
32
+ feats = {}
33
+
34
+ # --- spectral features ---
35
+ feats["spectral_centroid"] = float(np.mean(librosa.feature.spectral_centroid(y=y, sr=sr)))
36
+ feats["spectral_bandwidth"] = float(np.mean(librosa.feature.spectral_bandwidth(y=y, sr=sr)))
37
+ feats["spectral_rolloff"] = float(np.mean(librosa.feature.spectral_rolloff(y=y, sr=sr)))
38
+ feats["spectral_flatness"] = float(np.mean(librosa.feature.spectral_flatness(y=y)))
39
+ feats["zero_crossing_rate"] = float(np.mean(librosa.feature.zero_crossing_rate(y)))
40
+
41
+ # --- MFCCs ---
42
+ mfccs = librosa.feature.mfcc(y=y, sr=sr, n_mfcc=13)
43
+ for i, m in enumerate(mfccs):
44
+ feats[f"mfcc_{i+1}_mean"] = float(np.mean(m))
45
+ feats[f"mfcc_{i+1}_std"] = float(np.std(m))
46
+
47
+ # --- rhythm ---
48
+ tempo, _ = librosa.beat.beat_track(y=y, sr=sr)
49
+ feats["tempo"] = float(tempo)
50
+
51
+ # --- energy / dynamics ---
52
+ rms = librosa.feature.rms(y=y)[0]
53
+ feats["rms_mean"] = float(np.mean(rms))
54
+ feats["rms_std"] = float(np.std(rms))
55
+ feats["rms_range"] = float(np.max(rms) - np.min(rms))
56
+
57
+ # --- pitch / harmonics ---
58
+ f0, _, _ = librosa.pyin(y, fmin=librosa.note_to_hz('C2'),
59
+ fmax=librosa.note_to_hz('C7'))
60
+ f0_clean = f0[~np.isnan(f0)] if f0 is not None else np.array([])
61
+ feats["f0_mean"] = float(np.mean(f0_clean)) if len(f0_clean) > 0 else 0.0
62
+ feats["f0_std"] = float(np.std(f0_clean)) if len(f0_clean) > 0 else 0.0
63
+ feats["f0_voiced"] = float(len(f0_clean) / max(len(f0), 1)) if f0 is not None else 0.0
64
+
65
+ # --- harmonics / noise ratio proxy ---
66
+ harm, perc = librosa.effects.hpss(y)
67
+ feats["harmonic_ratio"] = float(np.mean(np.abs(harm)) / (np.mean(np.abs(perc)) + 1e-8))
68
+
69
+ # --- chroma ---
70
+ chroma = librosa.feature.chroma_stft(y=y, sr=sr)
71
+ feats["chroma_mean"] = float(np.mean(chroma))
72
+ feats["chroma_std"] = float(np.std(chroma))
73
+
74
+ return feats, y, sr
75
+
76
+
77
+ # ═══════════════════════════════════════════════════════════════════════════
78
+ # HEURISTIC DETECTION ENGINE
79
+ # ═══════════════════════════════════════════════════════════════════════════
80
+
81
+ def compute_ai_probability(feats):
82
+ """
83
+ Rule-based heuristic that returns (overall_score, sub_scores_dict).
84
+ Score range: 0.0 (fully human) β†’ 1.0 (fully AI).
85
+ """
86
+ scores = {}
87
+
88
+ # 1. Spectral flatness β€” AI tends to be more spectrally uniform
89
+ sf = feats["spectral_flatness"]
90
+ scores["Spectral Uniformity"] = min(sf * 18, 1.0)
91
+
92
+ # 2. F0 stability β€” AI pitch is often unnaturally steady
93
+ f0_std = feats["f0_std"]
94
+ voiced = feats["f0_voiced"]
95
+ if voiced > 0.3:
96
+ stability = max(0, 1 - (f0_std / 35))
97
+ else:
98
+ stability = 0.4
99
+ scores["Pitch Stability"] = float(np.clip(stability, 0, 1))
100
+
101
+ # 3. Harmonic-to-noise ratio β€” AI voices are often too clean
102
+ hr = feats["harmonic_ratio"]
103
+ scores["Harmonic Perfection"] = float(np.clip((hr - 1.5) / 8, 0, 1))
104
+
105
+ # 4. Dynamic range β€” AI tends to have compressed dynamics
106
+ rms_range = feats["rms_range"]
107
+ rms_mean = feats["rms_mean"]
108
+ dyn_norm = rms_range / (rms_mean + 1e-8)
109
+ scores["Dynamics Compression"] = float(np.clip(1 - (dyn_norm / 3.5), 0, 1))
110
+
111
+ # 5. MFCC variance β€” AI speech has less natural MFCC fluctuation
112
+ mfcc_stds = [feats[f"mfcc_{i}_std"] for i in range(1, 14)]
113
+ avg_mfcc_std = np.mean(mfcc_stds)
114
+ scores["MFCC Naturalness"] = float(np.clip(1 - (avg_mfcc_std / 22), 0, 1))
115
+
116
+ # 6. Zero crossing regularity β€” AI shows less randomness
117
+ zcr = feats["zero_crossing_rate"]
118
+ scores["Waveform Regularity"] = float(np.clip((0.15 - zcr) / 0.12 + 0.3, 0, 1))
119
+
120
+ # 7. Spectral bandwidth β€” AI voices often occupy a narrower band
121
+ bw = feats["spectral_bandwidth"]
122
+ scores["Bandwidth Compression"] = float(np.clip(1 - (bw / 3200), 0, 1))
123
+
124
+ # weighted average
125
+ weights = {
126
+ "Spectral Uniformity": 0.18,
127
+ "Pitch Stability": 0.22,
128
+ "Harmonic Perfection": 0.15,
129
+ "Dynamics Compression": 0.15,
130
+ "MFCC Naturalness": 0.15,
131
+ "Waveform Regularity": 0.08,
132
+ "Bandwidth Compression": 0.07,
133
+ }
134
+ overall = sum(scores[k] * weights[k] for k in scores)
135
+ overall = float(np.clip(overall, 0.0, 1.0))
136
+
137
+ return overall, scores
138
+
139
+
140
+ # ═══════════════════════════════════════════════════════════════════════════
141
+ # VISUALISATION
142
+ # ═══════════════════════════════════════════════════════════════════════════
143
+
144
+ def style_fig(fig):
145
+ fig.patch.set_facecolor(BG)
146
+
147
+
148
+ def plot_waveform(y, sr):
149
+ fig, ax = plt.subplots(figsize=(9, 2.2))
150
+ style_fig(fig)
151
+ ax.set_facecolor(PANEL)
152
+ t = np.linspace(0, len(y)/sr, len(y))
153
+ ax.fill_between(t, y, alpha=0.7, color=ACCENT)
154
+ ax.plot(t, y, color=ACCENT, lw=0.4, alpha=0.9)
155
+ ax.set_xlabel("Time (s)", color=SUBTEXT, fontsize=8)
156
+ ax.set_ylabel("Amplitude", color=SUBTEXT, fontsize=8)
157
+ ax.set_title("Waveform", color=TEXT, fontsize=10, fontweight='bold', pad=8)
158
+ ax.tick_params(colors=SUBTEXT, labelsize=7)
159
+ for sp in ax.spines.values(): sp.set_edgecolor(SUBTEXT); sp.set_alpha(0.3)
160
+ fig.tight_layout(pad=0.8)
161
+ return fig
162
+
163
+
164
+ def plot_spectrogram(y, sr):
165
+ fig, ax = plt.subplots(figsize=(9, 2.8))
166
+ style_fig(fig)
167
+ ax.set_facecolor(PANEL)
168
+ D = librosa.amplitude_to_db(np.abs(librosa.stft(y)), ref=np.max)
169
+ img = librosa.display.specshow(D, sr=sr, x_axis='time', y_axis='hz',
170
+ ax=ax, cmap='inferno')
171
+ plt.colorbar(img, ax=ax, format='%+2.0f dB', label='dB').ax.yaxis.label.set_color(SUBTEXT)
172
+ ax.set_title("Spectrogram", color=TEXT, fontsize=10, fontweight='bold', pad=8)
173
+ ax.tick_params(colors=SUBTEXT, labelsize=7)
174
+ ax.set_xlabel("Time (s)", color=SUBTEXT, fontsize=8)
175
+ ax.set_ylabel("Frequency (Hz)", color=SUBTEXT, fontsize=8)
176
+ for sp in ax.spines.values(): sp.set_edgecolor(SUBTEXT); sp.set_alpha(0.3)
177
+ fig.tight_layout(pad=0.8)
178
+ return fig
179
+
180
+
181
+ def plot_mfcc(y, sr):
182
+ fig, ax = plt.subplots(figsize=(9, 2.8))
183
+ style_fig(fig)
184
+ ax.set_facecolor(PANEL)
185
+ mfccs = librosa.feature.mfcc(y=y, sr=sr, n_mfcc=13)
186
+ img = librosa.display.specshow(mfccs, x_axis='time', ax=ax, cmap='coolwarm')
187
+ plt.colorbar(img, ax=ax).ax.yaxis.label.set_color(SUBTEXT)
188
+ ax.set_title("MFCC Heatmap", color=TEXT, fontsize=10, fontweight='bold', pad=8)
189
+ ax.tick_params(colors=SUBTEXT, labelsize=7)
190
+ ax.set_xlabel("Time (s)", color=SUBTEXT, fontsize=8)
191
+ ax.set_ylabel("MFCC Coefficient", color=SUBTEXT, fontsize=8)
192
+ for sp in ax.spines.values(): sp.set_edgecolor(SUBTEXT); sp.set_alpha(0.3)
193
+ fig.tight_layout(pad=0.8)
194
+ return fig
195
+
196
+
197
+ def plot_radar(sub_scores):
198
+ labels = list(sub_scores.keys())
199
+ vals = [sub_scores[k] for k in labels]
200
+ N = len(labels)
201
+
202
+ angles = np.linspace(0, 2*np.pi, N, endpoint=False).tolist()
203
+ vals_c = vals + [vals[0]]
204
+ ang_c = angles + [angles[0]]
205
+
206
+ fig, ax = plt.subplots(figsize=(5, 5), subplot_kw=dict(polar=True))
207
+ style_fig(fig)
208
+ ax.set_facecolor(PANEL)
209
+ ax.plot(ang_c, vals_c, color=ACCENT2, lw=2)
210
+ ax.fill(ang_c, vals_c, color=ACCENT2, alpha=0.25)
211
+ ax.set_xticks(angles)
212
+ ax.set_xticklabels(labels, size=7.5, color=TEXT)
213
+ ax.set_yticks([0.25, 0.5, 0.75, 1.0])
214
+ ax.set_yticklabels(["25%","50%","75%","100%"], size=6, color=SUBTEXT)
215
+ ax.set_ylim(0, 1)
216
+ ax.tick_params(colors=SUBTEXT)
217
+ ax.spines['polar'].set_color(SUBTEXT); ax.spines['polar'].set_alpha(0.3)
218
+ ax.set_title("AI Indicator Radar", color=TEXT, fontsize=10, fontweight='bold', pad=18)
219
+ ax.grid(color=SUBTEXT, alpha=0.2)
220
+ fig.tight_layout()
221
+ return fig
222
+
223
+
224
+ def plot_bar(sub_scores):
225
+ labels = list(sub_scores.keys())
226
+ vals = [v * 100 for v in sub_scores.values()]
227
+
228
+ fig, ax = plt.subplots(figsize=(7, 4))
229
+ style_fig(fig)
230
+ ax.set_facecolor(PANEL)
231
+
232
+ colors = [DANGER if v > 65 else WARNING if v > 40 else ACCENT for v in vals]
233
+ bars = ax.barh(labels, vals, color=colors, height=0.55, edgecolor='none')
234
+
235
+ for bar, val in zip(bars, vals):
236
+ ax.text(min(val + 1.5, 97), bar.get_y() + bar.get_height()/2,
237
+ f"{val:.0f}%", va='center', ha='left', color=TEXT, fontsize=8)
238
+
239
+ ax.set_xlim(0, 105)
240
+ ax.set_xlabel("AI Likelihood (%)", color=SUBTEXT, fontsize=8)
241
+ ax.set_title("Sub-Indicator Breakdown", color=TEXT, fontsize=10, fontweight='bold', pad=8)
242
+ ax.tick_params(colors=SUBTEXT, labelsize=8)
243
+ for sp in ax.spines.values(): sp.set_edgecolor(SUBTEXT); sp.set_alpha(0.3)
244
+
245
+ patches = [
246
+ mpatches.Patch(color=DANGER, label='High AI signal (>65%)'),
247
+ mpatches.Patch(color=WARNING, label='Moderate (40–65%)'),
248
+ mpatches.Patch(color=ACCENT, label='Low AI signal (<40%)'),
249
+ ]
250
+ ax.legend(handles=patches, loc='lower right', fontsize=7,
251
+ facecolor=PANEL, edgecolor=SUBTEXT, labelcolor=TEXT)
252
+ fig.tight_layout(pad=0.8)
253
+ return fig
254
+
255
+
256
+ def plot_gauge(score):
257
+ """Semicircular gauge."""
258
+ fig, ax = plt.subplots(figsize=(5, 3), subplot_kw=dict(aspect='equal'))
259
+ style_fig(fig)
260
+ ax.set_facecolor(BG)
261
+ ax.axis('off')
262
+
263
+ # background arc
264
+ theta_bg = np.linspace(np.pi, 0, 200)
265
+ ax.plot(np.cos(theta_bg), np.sin(theta_bg), color=PANEL, lw=28, solid_capstyle='round')
266
+
267
+ # filled arc
268
+ end_angle = np.pi - score * np.pi
269
+ theta_fill = np.linspace(np.pi, end_angle, 200)
270
+ fill_color = DANGER if score > 0.65 else WARNING if score > 0.4 else ACCENT
271
+ ax.plot(np.cos(theta_fill), np.sin(theta_fill), color=fill_color, lw=28, solid_capstyle='round')
272
+
273
+ # needle
274
+ needle_angle = np.pi - score * np.pi
275
+ ax.annotate("", xy=(0.72*np.cos(needle_angle), 0.72*np.sin(needle_angle)),
276
+ xytext=(0, 0),
277
+ arrowprops=dict(arrowstyle='->', color=TEXT, lw=2.5))
278
+ ax.add_patch(plt.Circle((0, 0), 0.06, color=TEXT, zorder=5))
279
+
280
+ # labels
281
+ ax.text(0, 0.28, f"{score*100:.1f}%", ha='center', va='center',
282
+ color=fill_color, fontsize=26, fontweight='bold')
283
+ ax.text(0, 0.05, "AI Generated", ha='center', va='center', color=SUBTEXT, fontsize=9)
284
+ ax.text(-1.05, -0.12, "HUMAN", color=ACCENT, fontsize=8, fontweight='bold')
285
+ ax.text( 0.75, -0.12, "AI", color=DANGER, fontsize=8, fontweight='bold')
286
+
287
+ ax.set_xlim(-1.2, 1.2)
288
+ ax.set_ylim(-0.3, 1.15)
289
+ ax.set_title("Detection Gauge", color=TEXT, fontsize=10, fontweight='bold', pad=4)
290
+ fig.tight_layout()
291
+ return fig
292
+
293
+
294
+ def fig_to_pil(fig):
295
+ buf = io.BytesIO()
296
+ fig.savefig(buf, format='png', dpi=130, bbox_inches='tight',
297
+ facecolor=fig.get_facecolor())
298
+ buf.seek(0)
299
+ img = Image.open(buf).copy()
300
+ plt.close(fig)
301
+ return img
302
+
303
+
304
+ # ═══════════════════════════════════════════════════════════════════════════
305
+ # VERDICT TEXT
306
+ # ═══════════════════════════════════════════════════════════════════════════
307
+
308
+ def verdict_text(score, sub_scores):
309
+ pct = score * 100
310
+ if pct >= 75:
311
+ label, emoji = "HIGHLY LIKELY AI GENERATED", "πŸ”΄"
312
+ desc = (f"This audio shows strong AI synthesis signatures across multiple "
313
+ f"dimensions. The model detected {pct:.1f}% AI likelihood β€” "
314
+ f"consistent with TTS or voice cloning technology.")
315
+ elif pct >= 50:
316
+ label, emoji = "LIKELY AI GENERATED", "🟠"
317
+ desc = (f"Several AI indicators are elevated. The audio has a {pct:.1f}% "
318
+ f"probability of being synthesised, though some human elements remain.")
319
+ elif pct >= 30:
320
+ label, emoji = "AMBIGUOUS / MIXED", "🟑"
321
+ desc = (f"The signal is mixed ({pct:.1f}%). This could be lightly processed "
322
+ f"human speech, or a very natural-sounding AI voice. Manual review advised.")
323
+ else:
324
+ label, emoji = "LIKELY HUMAN", "🟒"
325
+ desc = (f"Low AI likelihood detected ({pct:.1f}%). The audio shows natural "
326
+ f"variance and organic imperfections consistent with genuine human speech.")
327
+
328
+ top_indicator = max(sub_scores, key=sub_scores.get)
329
+ top_val = sub_scores[top_indicator] * 100
330
+
331
+ detail = (
332
+ f"\n\n**Key Finding:** The strongest AI signal came from **{top_indicator}** "
333
+ f"({top_val:.1f}%), "
334
+ + ("indicating unusually stable, machine-like characteristics in that dimension."
335
+ if top_val > 60 else
336
+ "though it remains within ambiguous territory.")
337
+ )
338
+
339
+ lines = [f"## {emoji} {label}", "", desc + detail, "", "---", "",
340
+ "**Sub-Indicator Summary:**"]
341
+ for k, v in sub_scores.items():
342
+ bar_filled = int(v * 20)
343
+ bar = "β–ˆ" * bar_filled + "β–‘" * (20 - bar_filled)
344
+ lines.append(f"`{k:<26}` `{bar}` **{v*100:.1f}%**")
345
+
346
+ return "\n".join(lines)
347
+
348
+
349
+ # ═══════════════════════════════════════════════════════════════════════════
350
+ # MAIN PIPELINE
351
+ # ═══════════════════════════════════════════════════════════════════════════
352
+
353
+ def analyse_audio(audio_path):
354
+ if audio_path is None:
355
+ return (None,) * 6 + ("⚠️ Please upload an audio file first.",)
356
+
357
+ try:
358
+ feats, y, sr = extract_features(audio_path)
359
+ score, sub = compute_ai_probability(feats)
360
+
361
+ gauge = fig_to_pil(plot_gauge(score))
362
+ radar = fig_to_pil(plot_radar(sub))
363
+ bar = fig_to_pil(plot_bar(sub))
364
+ wave = fig_to_pil(plot_waveform(y, sr))
365
+ spec = fig_to_pil(plot_spectrogram(y, sr))
366
+ mfcc_img = fig_to_pil(plot_mfcc(y, sr))
367
+ verdict = verdict_text(score, sub)
368
+
369
+ return gauge, radar, bar, wave, spec, mfcc_img, verdict
370
+
371
+ except Exception as e:
372
+ err = f"❌ Analysis failed: {str(e)}\n\nPlease ensure your file is a valid audio format (WAV, MP3, FLAC, OGG)."
373
+ return (None,) * 6 + (err,)
374
+
375
+
376
+ # ═══════════════════════════════════════════════════════════════════════════
377
+ # GRADIO UI
378
+ # ═══════════════════════════════════════════════════════════════════════════
379
+
380
+ CSS = f"""
381
+ @import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;600;700&family=JetBrains+Mono:wght@400;700&display=swap');
382
+
383
+ body, .gradio-container {{
384
+ background: {BG} !important;
385
+ font-family: 'Space Grotesk', sans-serif !important;
386
+ color: {TEXT} !important;
387
+ }}
388
+
389
+ /* Hero header */
390
+ .hero {{
391
+ background: linear-gradient(135deg, #0d0d18 0%, #12122a 50%, #0a0f1a 100%);
392
+ border: 1px solid #1e1e3a;
393
+ border-radius: 20px;
394
+ padding: 40px 32px 32px;
395
+ margin-bottom: 24px;
396
+ position: relative;
397
+ overflow: hidden;
398
+ text-align: center;
399
+ }}
400
+ .hero::before {{
401
+ content: '';
402
+ position: absolute; inset: 0;
403
+ background: radial-gradient(ellipse at 30% 50%, rgba(0,245,196,0.07) 0%, transparent 60%),
404
+ radial-gradient(ellipse at 70% 50%, rgba(123,97,255,0.07) 0%, transparent 60%);
405
+ pointer-events: none;
406
+ }}
407
+ .hero h1 {{
408
+ font-size: 2.6rem; font-weight: 700; margin: 0 0 8px;
409
+ background: linear-gradient(90deg, {ACCENT}, {ACCENT2});
410
+ -webkit-background-clip: text; -webkit-text-fill-color: transparent;
411
+ letter-spacing: -0.02em;
412
+ }}
413
+ .hero p {{ color: {SUBTEXT}; font-size: 1rem; margin: 0; }}
414
+ .hero .badge {{
415
+ display: inline-block; background: rgba(0,245,196,0.1);
416
+ border: 1px solid rgba(0,245,196,0.3); border-radius: 20px;
417
+ padding: 4px 14px; font-size: 0.78rem; color: {ACCENT};
418
+ font-family: 'JetBrains Mono', monospace; margin-bottom: 16px;
419
+ }}
420
+
421
+ /* Upload zone */
422
+ .upload-zone {{
423
+ border: 2px dashed rgba(0,245,196,0.3) !important;
424
+ border-radius: 16px !important;
425
+ background: rgba(18,18,26,0.8) !important;
426
+ transition: border-color 0.3s;
427
+ }}
428
+ .upload-zone:hover {{ border-color: {ACCENT} !important; }}
429
+
430
+ /* Buttons */
431
+ button.primary {{
432
+ background: linear-gradient(135deg, {ACCENT}, {ACCENT2}) !important;
433
+ color: #000 !important; font-weight: 700 !important;
434
+ border-radius: 12px !important; border: none !important;
435
+ font-size: 1rem !important; padding: 12px 32px !important;
436
+ transition: opacity 0.2s, transform 0.15s !important;
437
+ }}
438
+ button.primary:hover {{ opacity: 0.9 !important; transform: translateY(-1px) !important; }}
439
+
440
+ /* Tabs */
441
+ .tab-nav button {{ color: {SUBTEXT} !important; border-radius: 8px !important; }}
442
+ .tab-nav button.selected {{ color: {ACCENT} !important; border-bottom: 2px solid {ACCENT} !important; }}
443
+
444
+ /* Markdown */
445
+ .prose {{ background: {PANEL}; border-radius: 12px; padding: 20px; border: 1px solid #1e1e3a; }}
446
+
447
+ /* Section labels */
448
+ .section-label {{
449
+ font-size: 0.72rem; letter-spacing: 0.12em; text-transform: uppercase;
450
+ color: {SUBTEXT}; font-family: 'JetBrains Mono', monospace; margin-bottom: 8px;
451
+ }}
452
+ """
453
+
454
+ HEADER_HTML = f"""
455
+ <div class="hero">
456
+ <div class="badge">v2.1 Β· Acoustic Analysis Engine</div>
457
+ <h1>⚑ DeepFake Audio Detector</h1>
458
+ <p>Upload any audio clip β€” we'll expose AI synthesis fingerprints with multi-dimensional acoustic analysis</p>
459
+ </div>
460
+ """
461
+
462
+ HOW_IT_WORKS = """
463
+ ### πŸ”¬ How the Detection Works
464
+
465
+ This tool analyses **7 acoustic dimensions** known to differ between human and AI-synthesised speech:
466
+
467
+ | Indicator | What it measures |
468
+ |---|---|
469
+ | **Spectral Uniformity** | AI voices have unnaturally flat spectral energy |
470
+ | **Pitch Stability** | Human pitch wanders naturally; AI pitch is robotic-smooth |
471
+ | **Harmonic Perfection** | AI voices are too clean β€” humans have breath noise & imperfections |
472
+ | **Dynamics Compression** | Human speech has wide loudness variation; AI is compressed |
473
+ | **MFCC Naturalness** | Mel-cepstral coefficients reveal synthesis artefacts |
474
+ | **Waveform Regularity** | AI waveforms lack micro-jitter present in human recordings |
475
+ | **Bandwidth Compression** | TTS systems often clip high-frequency content |
476
+
477
+ > ⚠️ **Disclaimer:** This is a heuristic-based acoustic analyser β€” not a certified forensic tool. Low-quality recordings or heavy post-processing can affect results.
478
+ """
479
+
480
+ def build_app():
481
+ with gr.Blocks(css=CSS, title="DeepFake Audio Detector") as demo:
482
+
483
+ gr.HTML(HEADER_HTML)
484
+
485
+ with gr.Row():
486
+ with gr.Column(scale=1):
487
+ gr.HTML('<div class="section-label">πŸ“ Upload Audio</div>')
488
+ audio_input = gr.Audio(
489
+ label="Drop your audio file here",
490
+ type="filepath",
491
+ elem_classes=["upload-zone"]
492
+ )
493
+ analyse_btn = gr.Button("πŸ” Analyse Audio", variant="primary", size="lg")
494
+ gr.HTML('<div style="height:12px"></div>')
495
+ gr.Markdown(HOW_IT_WORKS)
496
+
497
+ with gr.Column(scale=2):
498
+ gr.HTML('<div class="section-label">πŸ“Š Detection Results</div>')
499
+
500
+ with gr.Tabs():
501
+ with gr.Tab("🎯 Verdict"):
502
+ gauge_out = gr.Image(label="Detection Gauge", show_label=False)
503
+ verdict_md = gr.Markdown(elem_classes=["prose"])
504
+
505
+ with gr.Tab("πŸ“‘ Radar & Bars"):
506
+ with gr.Row():
507
+ radar_out = gr.Image(label="Radar Chart", show_label=False)
508
+ bar_out = gr.Image(label="Bar Chart", show_label=False)
509
+
510
+ with gr.Tab("πŸ”Š Waveform"):
511
+ wave_out = gr.Image(label="Waveform", show_label=False)
512
+
513
+ with gr.Tab("🌈 Spectrogram"):
514
+ spec_out = gr.Image(label="Spectrogram", show_label=False)
515
+
516
+ with gr.Tab("🧬 MFCC"):
517
+ mfcc_out = gr.Image(label="MFCC Heatmap", show_label=False)
518
+
519
+ analyse_btn.click(
520
+ fn=analyse_audio,
521
+ inputs=[audio_input],
522
+ outputs=[gauge_out, radar_out, bar_out, wave_out, spec_out, mfcc_out, verdict_md]
523
+ )
524
+
525
+ gr.HTML(f"""
526
+ <div style="text-align:center; padding:20px 0 8px; color:{SUBTEXT}; font-size:0.8rem; font-family:'JetBrains Mono',monospace;">
527
+ Built with Gradio Β· Librosa Β· Matplotlib &nbsp;|&nbsp; Heuristic acoustic analysis
528
+ </div>
529
+ """)
530
+
531
+ return demo
532
+
533
+
534
+ if __name__ == "__main__":
535
+ app = build_app()
536
+ app.launch(share=False)