Deepfake Authenticator commited on
Commit
31c9c07
Β·
1 Parent(s): bb8ac90

feat: audio deepfake detection (AI voice analysis)

Browse files

Backend:
- New AudioAuthenticator with 4-agent pipeline
- AudioExtractorAgent: extracts mono 16kHz audio via moviepy
- AudioAnalysisAgent: librosa heuristics (pitch variance, MFCC delta,
spectral flatness, ZCR consistency, silence/breath ratio)
- AudioDecisionAgent: Wav2Vec2 model (Bisher/wav2vec2_ASV, labels: fake/real)
- AudioReportAgent: ensemble 65% model + 35% heuristics
- Combined visual+audio verdict in ReportGeneratorAgent
- Audio analysis runs after visual, both signals inform final verdict

Frontend:
- New Voice Authenticity Analysis card in results
- Shows AI VOICE DETECTED / HUMAN VOICE badge
- Wav2Vec2 model score + heuristic score side by side
- Voice insights with colored left border
- Signal features: pitch std, MFCC delta, spectral flatness, silence ratio

backend/audio_detector.py ADDED
@@ -0,0 +1,423 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Deepfake Authenticator β€” Audio Analysis Agent
3
+ Detects AI-generated / synthetic voices from video audio tracks.
4
+
5
+ Pipeline:
6
+ 1. AudioExtractorAgent β€” extracts audio from video via moviepy
7
+ 2. AudioAnalysisAgent β€” librosa heuristics (MFCC, pitch, spectral)
8
+ 3. AudioDecisionAgent β€” Wav2Vec2 model (Bisher/wav2vec2_ASV_deepfake_audio_detection)
9
+ 4. AudioReportAgent β€” builds structured result
10
+ """
11
+
12
+ import os
13
+ import tempfile
14
+ import logging
15
+ import numpy as np
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ # ─────────────────────────────────────────────
20
+ # Agent 1: Audio Extractor
21
+ # Pulls audio track from video file
22
+ # ─────────────────────────────────────────────
23
+ class AudioExtractorAgent:
24
+ TARGET_SR = 16000 # Wav2Vec2 expects 16kHz
25
+
26
+ def extract(self, video_path: str) -> tuple[np.ndarray | None, int]:
27
+ """
28
+ Extract mono 16kHz audio from video.
29
+ Returns (waveform_array, sample_rate) or (None, 0) if no audio.
30
+ """
31
+ try:
32
+ from moviepy import VideoFileClip
33
+ except ImportError:
34
+ try:
35
+ from moviepy.editor import VideoFileClip
36
+ except ImportError:
37
+ logger.warning("moviepy not installed β€” audio analysis skipped")
38
+ return None, 0
39
+
40
+ tmp_wav = None
41
+ try:
42
+ clip = VideoFileClip(video_path)
43
+ if clip.audio is None:
44
+ logger.info("Video has no audio track")
45
+ clip.close()
46
+ return None, 0
47
+
48
+ # Write to temp WAV
49
+ tmp_wav = tempfile.mktemp(suffix=".wav")
50
+ clip.audio.write_audiofile(
51
+ tmp_wav,
52
+ fps=self.TARGET_SR,
53
+ nbytes=2,
54
+ codec="pcm_s16le",
55
+ logger=None,
56
+ )
57
+ clip.close()
58
+
59
+ # Load with soundfile for clean numpy array
60
+ import soundfile as sf
61
+ waveform, sr = sf.read(tmp_wav, dtype="float32")
62
+
63
+ # Convert stereo β†’ mono
64
+ if waveform.ndim > 1:
65
+ waveform = waveform.mean(axis=1)
66
+
67
+ # Resample if needed
68
+ if sr != self.TARGET_SR:
69
+ import torchaudio
70
+ import torch
71
+ t = torch.from_numpy(waveform).unsqueeze(0)
72
+ resampler = torchaudio.transforms.Resample(sr, self.TARGET_SR)
73
+ waveform = resampler(t).squeeze(0).numpy()
74
+ sr = self.TARGET_SR
75
+
76
+ logger.info(f"Audio extracted: {len(waveform)/sr:.1f}s @ {sr}Hz")
77
+ return waveform, sr
78
+
79
+ except Exception as e:
80
+ logger.warning(f"Audio extraction failed: {e}")
81
+ return None, 0
82
+ finally:
83
+ if tmp_wav and os.path.exists(tmp_wav):
84
+ os.unlink(tmp_wav)
85
+
86
+
87
+ # ─────────────────────────────────────────────
88
+ # Agent 2: Audio Heuristic Analyzer
89
+ # Librosa-based feature analysis
90
+ # ─────────────────────────────────────────────
91
+ class AudioAnalysisAgent:
92
+ """
93
+ Detects AI voice artifacts using signal processing:
94
+ - Pitch variance (AI voices are unnaturally consistent)
95
+ - MFCC delta variance (AI lacks natural micro-variations)
96
+ - Spectral flatness (AI voices have unusual spectral distribution)
97
+ - Zero-crossing rate (synthetic voices differ in ZCR patterns)
98
+ - Silence/breath ratio (AI voices often lack natural breath sounds)
99
+ """
100
+
101
+ def analyze(self, waveform: np.ndarray, sr: int) -> dict:
102
+ try:
103
+ import librosa
104
+ except ImportError:
105
+ logger.warning("librosa not installed β€” heuristic audio analysis skipped")
106
+ return {"heuristic_fake_prob": 0.5, "features": {}, "available": False}
107
+
108
+ scores = []
109
+ features = {}
110
+
111
+ # ── 1. Pitch variance ─────────────────────────────────────────
112
+ # AI voices have unnaturally stable pitch (low variance = suspicious)
113
+ try:
114
+ f0, voiced_flag, _ = librosa.pyin(
115
+ waveform, fmin=50, fmax=500, sr=sr
116
+ )
117
+ voiced_f0 = f0[voiced_flag & ~np.isnan(f0)]
118
+ if len(voiced_f0) > 10:
119
+ pitch_std = float(np.std(voiced_f0))
120
+ features["pitch_std_hz"] = round(pitch_std, 2)
121
+ # Real human speech: std typically 20-80 Hz
122
+ # AI voices: often < 10 Hz (too stable)
123
+ if pitch_std < 8:
124
+ scores.append(0.80) # Very suspicious
125
+ elif pitch_std < 15:
126
+ scores.append(0.65)
127
+ elif pitch_std < 25:
128
+ scores.append(0.45)
129
+ else:
130
+ scores.append(0.25) # Natural variation
131
+ else:
132
+ scores.append(0.50)
133
+ except Exception as e:
134
+ logger.debug(f"Pitch analysis failed: {e}")
135
+ scores.append(0.50)
136
+
137
+ # ── 2. MFCC delta variance ────────────────────────────────────
138
+ # AI voices lack natural micro-variations in articulation
139
+ try:
140
+ mfcc = librosa.feature.mfcc(y=waveform, sr=sr, n_mfcc=13)
141
+ delta = librosa.feature.delta(mfcc)
142
+ delta_var = float(np.mean(np.var(delta, axis=1)))
143
+ features["mfcc_delta_var"] = round(delta_var, 4)
144
+ # Low delta variance β†’ unnaturally smooth transitions
145
+ if delta_var < 0.5:
146
+ scores.append(0.75)
147
+ elif delta_var < 1.5:
148
+ scores.append(0.55)
149
+ elif delta_var < 4.0:
150
+ scores.append(0.35)
151
+ else:
152
+ scores.append(0.20)
153
+ except Exception as e:
154
+ logger.debug(f"MFCC analysis failed: {e}")
155
+ scores.append(0.50)
156
+
157
+ # ── 3. Spectral flatness ──────────────────────────────────────
158
+ # AI voices often have unusual spectral distribution
159
+ try:
160
+ flatness = librosa.feature.spectral_flatness(y=waveform)
161
+ mean_flatness = float(np.mean(flatness))
162
+ features["spectral_flatness"] = round(mean_flatness, 4)
163
+ # Very low flatness = tonal (could be AI), very high = noisy
164
+ if mean_flatness < 0.001:
165
+ scores.append(0.65)
166
+ elif mean_flatness < 0.005:
167
+ scores.append(0.45)
168
+ else:
169
+ scores.append(0.30)
170
+ except Exception as e:
171
+ logger.debug(f"Spectral flatness failed: {e}")
172
+ scores.append(0.50)
173
+
174
+ # ── 4. Zero-crossing rate consistency ────────────────────────
175
+ # AI voices have unnaturally consistent ZCR
176
+ try:
177
+ zcr = librosa.feature.zero_crossing_rate(waveform)
178
+ zcr_std = float(np.std(zcr))
179
+ features["zcr_std"] = round(zcr_std, 4)
180
+ if zcr_std < 0.02:
181
+ scores.append(0.65) # Too consistent
182
+ elif zcr_std < 0.05:
183
+ scores.append(0.40)
184
+ else:
185
+ scores.append(0.25)
186
+ except Exception as e:
187
+ logger.debug(f"ZCR analysis failed: {e}")
188
+ scores.append(0.50)
189
+
190
+ # ── 5. Silence/breath detection ───────────────────────────────
191
+ # Real speech has natural pauses and breath sounds
192
+ # AI voices often have perfectly clean silence or no breaths
193
+ try:
194
+ rms = librosa.feature.rms(y=waveform)[0]
195
+ silence_ratio = float(np.mean(rms < 0.01))
196
+ features["silence_ratio"] = round(silence_ratio, 3)
197
+ # Very low silence ratio = no natural pauses (suspicious)
198
+ # Very high = mostly silent (not useful)
199
+ if silence_ratio < 0.05:
200
+ scores.append(0.60) # No natural pauses
201
+ elif 0.05 <= silence_ratio <= 0.35:
202
+ scores.append(0.25) # Natural speech rhythm
203
+ else:
204
+ scores.append(0.45)
205
+ except Exception as e:
206
+ logger.debug(f"Silence analysis failed: {e}")
207
+ scores.append(0.50)
208
+
209
+ heuristic_prob = float(np.mean(scores)) if scores else 0.5
210
+ logger.info(f"Audio heuristics: {features} β†’ fake_prob={heuristic_prob:.3f}")
211
+
212
+ return {
213
+ "heuristic_fake_prob": round(heuristic_prob, 4),
214
+ "features": features,
215
+ "available": True,
216
+ }
217
+
218
+
219
+ # ─────────────────────────────────────────────
220
+ # Agent 3: Audio Decision Agent
221
+ # Wav2Vec2 model for AI voice detection
222
+ # ─────────────────────────────────────────────
223
+ class AudioDecisionAgent:
224
+ MODEL_ID = "Bisher/wav2vec2_ASV_deepfake_audio_detection"
225
+ CHUNK_SEC = 10 # Process in 10-second chunks (model limit)
226
+ TARGET_SR = 16000
227
+
228
+ def __init__(self):
229
+ self.model = None
230
+ self.processor = None
231
+ self.fake_idx = 0 # label 0 = 'fake'
232
+ self.available = False
233
+ self._load()
234
+
235
+ def _load(self):
236
+ try:
237
+ from transformers import (
238
+ Wav2Vec2ForSequenceClassification,
239
+ Wav2Vec2Processor,
240
+ )
241
+ logger.info(f"Loading audio model: {self.MODEL_ID}")
242
+ self.processor = Wav2Vec2Processor.from_pretrained(self.MODEL_ID)
243
+ self.model = Wav2Vec2ForSequenceClassification.from_pretrained(self.MODEL_ID)
244
+ self.model.eval()
245
+
246
+ # Confirm fake label index
247
+ for idx, lbl in self.model.config.id2label.items():
248
+ if lbl.lower() == "fake":
249
+ self.fake_idx = idx
250
+ break
251
+
252
+ self.available = True
253
+ logger.info(f"Audio model loaded β€” fake_idx={self.fake_idx}, labels={self.model.config.id2label}")
254
+ except Exception as e:
255
+ logger.warning(f"Audio model unavailable: {e}")
256
+ self.available = False
257
+
258
+ def predict(self, waveform: np.ndarray, sr: int) -> float:
259
+ """
260
+ Run Wav2Vec2 on audio in chunks, return mean fake probability.
261
+ """
262
+ if not self.available:
263
+ return 0.5
264
+
265
+ import torch
266
+
267
+ chunk_size = self.CHUNK_SEC * sr
268
+ chunks = [
269
+ waveform[i : i + chunk_size]
270
+ for i in range(0, len(waveform), chunk_size)
271
+ if len(waveform[i : i + chunk_size]) > sr // 2 # skip < 0.5s chunks
272
+ ]
273
+
274
+ if not chunks:
275
+ return 0.5
276
+
277
+ fake_probs = []
278
+ for chunk in chunks:
279
+ try:
280
+ inputs = self.processor(
281
+ chunk,
282
+ sampling_rate=self.TARGET_SR,
283
+ return_tensors="pt",
284
+ padding=True,
285
+ )
286
+ with torch.no_grad():
287
+ logits = self.model(**inputs).logits
288
+ probs = torch.softmax(logits, dim=-1)[0]
289
+ fake_probs.append(probs[self.fake_idx].item())
290
+ except Exception as e:
291
+ logger.warning(f"Audio chunk inference failed: {e}")
292
+
293
+ if not fake_probs:
294
+ return 0.5
295
+
296
+ result = float(np.mean(fake_probs))
297
+ logger.info(f"Audio model: {len(fake_probs)} chunks β†’ fake_prob={result:.3f}")
298
+ return result
299
+
300
+
301
+ # ─────────────────────────────────────────────
302
+ # Agent 4: Audio Report Agent
303
+ # Builds structured audio result
304
+ # ─────────────────────────────────────────────
305
+ class AudioReportAgent:
306
+ FAKE_THRESHOLD = 0.60
307
+
308
+ def generate(
309
+ self,
310
+ model_prob: float,
311
+ heuristic: dict,
312
+ has_audio: bool,
313
+ ) -> dict:
314
+ if not has_audio:
315
+ return {
316
+ "available": False,
317
+ "result": "NO_AUDIO",
318
+ "confidence": 0,
319
+ "fake_probability": 0,
320
+ "details": ["No audio track found in video"],
321
+ }
322
+
323
+ heur_prob = heuristic.get("heuristic_fake_prob", 0.5)
324
+ features = heuristic.get("features", {})
325
+
326
+ # Ensemble: 65% model + 35% heuristics
327
+ if heuristic.get("available", False):
328
+ combined = model_prob * 0.65 + heur_prob * 0.35
329
+ else:
330
+ combined = model_prob
331
+
332
+ combined = float(np.clip(combined, 0.0, 1.0))
333
+ is_fake = combined >= self.FAKE_THRESHOLD
334
+ confidence = round(combined * 100, 1)
335
+
336
+ details = self._build_details(combined, is_fake, features, model_prob, heur_prob)
337
+
338
+ return {
339
+ "available": True,
340
+ "result": "AI_VOICE" if is_fake else "HUMAN_VOICE",
341
+ "confidence": confidence,
342
+ "fake_probability": round(combined, 4),
343
+ "model_score": round(model_prob * 100, 1),
344
+ "heuristic_score": round(heur_prob * 100, 1),
345
+ "details": details,
346
+ "features": features,
347
+ }
348
+
349
+ def _build_details(
350
+ self,
351
+ prob: float,
352
+ is_fake: bool,
353
+ features: dict,
354
+ model_prob: float,
355
+ heur_prob: float,
356
+ ) -> list[str]:
357
+ details = []
358
+
359
+ if is_fake:
360
+ if prob > 0.85:
361
+ details.append("High-confidence AI-generated voice detected")
362
+ elif prob > 0.70:
363
+ details.append("Strong synthetic voice characteristics identified")
364
+ else:
365
+ details.append("AI voice patterns detected β€” likely TTS or voice cloning")
366
+
367
+ pitch_std = features.get("pitch_std_hz")
368
+ if pitch_std is not None and pitch_std < 15:
369
+ details.append(f"Unnaturally stable pitch (Οƒ={pitch_std}Hz) β€” human speech typically varies 20-80Hz")
370
+
371
+ delta_var = features.get("mfcc_delta_var")
372
+ if delta_var is not None and delta_var < 1.5:
373
+ details.append("Insufficient micro-variation in articulation β€” characteristic of TTS synthesis")
374
+
375
+ silence = features.get("silence_ratio")
376
+ if silence is not None and silence < 0.05:
377
+ details.append("No natural breath pauses detected β€” AI voices lack organic speech rhythm")
378
+
379
+ details.append(f"Wav2Vec2 model confidence: {model_prob*100:.1f}% synthetic")
380
+ else:
381
+ if prob < 0.25:
382
+ details.append("Strong indicators of authentic human voice")
383
+ else:
384
+ details.append("Voice characteristics consistent with natural human speech")
385
+
386
+ pitch_std = features.get("pitch_std_hz")
387
+ if pitch_std is not None and pitch_std >= 20:
388
+ details.append(f"Natural pitch variation detected (Οƒ={pitch_std}Hz)")
389
+
390
+ silence = features.get("silence_ratio")
391
+ if silence is not None and 0.05 <= silence <= 0.35:
392
+ details.append("Natural speech rhythm with organic pauses and breath sounds")
393
+
394
+ details.append(f"Wav2Vec2 model confidence: {(1-model_prob)*100:.1f}% human")
395
+
396
+ return details
397
+
398
+
399
+ # ─────────────────────────────────────────────
400
+ # Orchestrator
401
+ # ─────────────────────────────────────────────
402
+ class AudioAuthenticator:
403
+ def __init__(self):
404
+ self.extractor = AudioExtractorAgent()
405
+ self.analyzer = AudioAnalysisAgent()
406
+ self.decision = AudioDecisionAgent()
407
+ self.reporter = AudioReportAgent()
408
+
409
+ def analyze(self, video_path: str) -> dict:
410
+ # Step 1: Extract audio
411
+ waveform, sr = self.extractor.extract(video_path)
412
+
413
+ if waveform is None or len(waveform) == 0:
414
+ return self.reporter.generate(0.5, {}, has_audio=False)
415
+
416
+ # Step 2: Heuristic analysis
417
+ heuristic = self.analyzer.analyze(waveform, sr)
418
+
419
+ # Step 3: Model prediction
420
+ model_prob = self.decision.predict(waveform, sr)
421
+
422
+ # Step 4: Report
423
+ return self.reporter.generate(model_prob, heuristic, has_audio=True)
backend/detector.py CHANGED
@@ -425,34 +425,53 @@ class ReportGeneratorAgent:
425
  # Base threshold β€” adjusted adaptively per video
426
  BASE_THRESHOLD = 0.58
427
 
428
- def generate(self, analysis: dict, metadata: dict) -> dict:
429
  prob = analysis["overall_fake_probability"]
430
  consistency = analysis.get("consistency", 0.5)
431
  coverage = analysis.get("face_coverage", 0.5)
432
 
433
- # ── Adaptive threshold ────────────────────────────────────────
434
- # Lower threshold when:
435
- # - High consistency (many frames agree it's fake) β†’ easier to flag
436
- # - High face coverage (face visible throughout) β†’ more reliable signal
437
- # Raise threshold when:
438
- # - Low consistency (only a few frames look fake) β†’ likely false positive
439
- # - Low coverage (face rarely visible) β†’ unreliable signal
440
  threshold = self.BASE_THRESHOLD
441
  if consistency >= 0.70 and coverage >= 0.50:
442
- threshold -= 0.06 # 0.52 β€” confident signal, lower bar
443
  elif consistency >= 0.55:
444
- threshold -= 0.03 # 0.55
445
  elif consistency < 0.35:
446
- threshold += 0.07 # 0.65 β€” inconsistent, raise bar
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
447
 
448
- is_fake = prob >= threshold
449
  calibrated = self._calibrate(prob)
450
  confidence = round(calibrated * 100, 1)
451
  result = "FAKE" if is_fake else "REAL"
452
 
453
  logger.info(
454
- f"Decision: prob={prob:.3f} threshold={threshold:.3f} "
455
- f"consistency={consistency:.2f} coverage={coverage:.2f} β†’ {result}"
456
  )
457
 
458
  details = self._build_details(analysis, metadata, prob, is_fake, threshold)
@@ -558,18 +577,33 @@ class ReportGeneratorAgent:
558
  # ─────────────────────────────────────────────
559
  class DeepfakeAuthenticator:
560
  def __init__(self):
561
- self.frame_agent = FrameAnalyzerAgent(sample_rate=10)
562
- self.face_agent = FaceDetectorAgent(min_detection_confidence=0.5)
563
  self.decision_agent = DecisionAgent()
564
  self.report_agent = ReportGeneratorAgent()
565
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
566
  def analyze(self, video_path: str) -> dict:
 
567
  start = time.time()
568
  logger.info(f"Starting analysis: {video_path}")
569
 
570
  # Step 1: Extract frames
571
  metadata = self.frame_agent.get_video_metadata(video_path)
572
- frames = self.frame_agent.extract_frames(video_path, max_frames=40)
573
 
574
  if not frames:
575
  return {
@@ -578,22 +612,34 @@ class DeepfakeAuthenticator:
578
  "details": ["Could not extract frames from video"],
579
  "frame_timeline": [],
580
  "metadata": metadata,
 
581
  }
582
 
583
- # Step 2: Detect faces in each frame
584
  face_crops_per_frame = [
585
  self.face_agent.detect_and_crop_faces(frame) for frame in frames
586
  ]
587
 
588
- # Step 3: Run decision analysis
589
  analysis = self.decision_agent.analyze_frames(frames, face_crops_per_frame)
590
 
591
- # Step 4: Generate report
592
- report = self.report_agent.generate(analysis, metadata)
 
 
 
 
 
 
 
 
 
593
  report["processing_time_sec"] = round(time.time() - start, 2)
 
594
 
595
  logger.info(
596
  f"Analysis complete: {report['result']} ({report['confidence']}%) "
 
597
  f"in {report['processing_time_sec']}s"
598
  )
599
  return report
 
425
  # Base threshold β€” adjusted adaptively per video
426
  BASE_THRESHOLD = 0.58
427
 
428
+ def generate(self, analysis: dict, metadata: dict, audio: dict | None = None) -> dict:
429
  prob = analysis["overall_fake_probability"]
430
  consistency = analysis.get("consistency", 0.5)
431
  coverage = analysis.get("face_coverage", 0.5)
432
 
433
+ # ── Adaptive visual threshold ─────────────────────────────────
 
 
 
 
 
 
434
  threshold = self.BASE_THRESHOLD
435
  if consistency >= 0.70 and coverage >= 0.50:
436
+ threshold -= 0.06
437
  elif consistency >= 0.55:
438
+ threshold -= 0.03
439
  elif consistency < 0.35:
440
+ threshold += 0.07
441
+
442
+ visual_fake = prob >= threshold
443
+
444
+ # ── Combine with audio signal ─────────────────────────────────
445
+ audio_fake = False
446
+ audio_prob = 0.0
447
+ if audio and audio.get("available"):
448
+ audio_prob = audio.get("fake_probability", 0.0)
449
+ audio_fake = audio.get("result") == "AI_VOICE"
450
+
451
+ # Final verdict: visual is primary, audio can upgrade OR downgrade
452
+ if audio and audio.get("available"):
453
+ # Both agree β†’ high confidence
454
+ if visual_fake and audio_fake:
455
+ is_fake = True
456
+ elif not visual_fake and not audio_fake:
457
+ is_fake = False
458
+ # Disagreement β†’ visual wins but audio nudges the score
459
+ elif visual_fake and not audio_fake:
460
+ # Audio says real β€” only keep FAKE if visual is strong
461
+ is_fake = prob >= (threshold + 0.05)
462
+ else:
463
+ # Visual says real but audio says AI β€” flag as suspicious
464
+ is_fake = audio_prob >= 0.75 # only override if audio is very confident
465
+ else:
466
+ is_fake = visual_fake
467
 
 
468
  calibrated = self._calibrate(prob)
469
  confidence = round(calibrated * 100, 1)
470
  result = "FAKE" if is_fake else "REAL"
471
 
472
  logger.info(
473
+ f"Decision: visual_prob={prob:.3f} threshold={threshold:.3f} "
474
+ f"visual_fake={visual_fake} audio_fake={audio_fake} β†’ {result}"
475
  )
476
 
477
  details = self._build_details(analysis, metadata, prob, is_fake, threshold)
 
577
  # ─────────────────────────────────────────────
578
  class DeepfakeAuthenticator:
579
  def __init__(self):
580
+ self.frame_agent = FrameAnalyzerAgent(sample_rate=10)
581
+ self.face_agent = FaceDetectorAgent(min_detection_confidence=0.5)
582
  self.decision_agent = DecisionAgent()
583
  self.report_agent = ReportGeneratorAgent()
584
 
585
+ # Audio analysis (lazy import to avoid blocking startup)
586
+ self._audio = None
587
+
588
+ def _get_audio(self):
589
+ if self._audio is None:
590
+ try:
591
+ from audio_detector import AudioAuthenticator
592
+ self._audio = AudioAuthenticator()
593
+ logger.info("AudioAuthenticator initialized")
594
+ except Exception as e:
595
+ logger.warning(f"AudioAuthenticator unavailable: {e}")
596
+ self._audio = False
597
+ return self._audio if self._audio else None
598
+
599
  def analyze(self, video_path: str) -> dict:
600
+ import time
601
  start = time.time()
602
  logger.info(f"Starting analysis: {video_path}")
603
 
604
  # Step 1: Extract frames
605
  metadata = self.frame_agent.get_video_metadata(video_path)
606
+ frames = self.frame_agent.extract_frames(video_path, max_frames=40)
607
 
608
  if not frames:
609
  return {
 
612
  "details": ["Could not extract frames from video"],
613
  "frame_timeline": [],
614
  "metadata": metadata,
615
+ "audio": {"available": False, "result": "NO_AUDIO", "confidence": 0, "details": []},
616
  }
617
 
618
+ # Step 2: Detect faces
619
  face_crops_per_frame = [
620
  self.face_agent.detect_and_crop_faces(frame) for frame in frames
621
  ]
622
 
623
+ # Step 3: Visual decision
624
  analysis = self.decision_agent.analyze_frames(frames, face_crops_per_frame)
625
 
626
+ # Step 4: Audio analysis (parallel-ish β€” runs after visual)
627
+ audio_result = {"available": False, "result": "NO_AUDIO", "confidence": 0, "details": []}
628
+ audio_agent = self._get_audio()
629
+ if audio_agent:
630
+ try:
631
+ audio_result = audio_agent.analyze(video_path)
632
+ except Exception as e:
633
+ logger.warning(f"Audio analysis failed: {e}")
634
+
635
+ # Step 5: Generate report (visual + audio combined)
636
+ report = self.report_agent.generate(analysis, metadata, audio_result)
637
  report["processing_time_sec"] = round(time.time() - start, 2)
638
+ report["audio"] = audio_result
639
 
640
  logger.info(
641
  f"Analysis complete: {report['result']} ({report['confidence']}%) "
642
+ f"audio={audio_result.get('result','N/A')} "
643
  f"in {report['processing_time_sec']}s"
644
  )
645
  return report
backend/main.py CHANGED
@@ -135,7 +135,7 @@ async def analyze_video(file: UploadFile = File(...)):
135
 
136
 
137
  # ── Serve frontend ────────────────────────────
138
- frontend_path = Path(__file__).parent.parent / "frontend"
139
 
140
  if frontend_path.exists():
141
  @app.get("/")
 
135
 
136
 
137
  # ── Serve frontend ────────────────────────────
138
+ frontend_path = Path(__file__).parent.parent / "frontend-vanilla"
139
 
140
  if frontend_path.exists():
141
  @app.get("/")
backend/requirements.txt CHANGED
@@ -6,6 +6,13 @@ mediapipe==0.10.14
6
  numpy==1.26.4
7
  Pillow==10.3.0
8
 
9
- # HuggingFace deepfake model
10
  transformers>=4.41.0
11
  torch>=2.3.0
 
 
 
 
 
 
 
 
6
  numpy==1.26.4
7
  Pillow==10.3.0
8
 
9
+ # HuggingFace models
10
  transformers>=4.41.0
11
  torch>=2.3.0
12
+ torchvision
13
+ torchaudio
14
+
15
+ # Audio analysis
16
+ moviepy>=1.0.3
17
+ librosa>=0.10.0
18
+ soundfile>=0.12.1
frontend-vanilla/index.html ADDED
@@ -0,0 +1,377 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ο»Ώ<!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8"/>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1.0"/>
6
+ <title>Deepfake Authenticator</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet"/>
9
+ <style>
10
+ :root{--g:#00ff88;--r:#ff3355;--w:#ffaa00;--bg:#050508;--card:rgba(255,255,255,0.04);--border:rgba(0,255,136,0.18);--muted:#5a6a7a;}
11
+ *{box-sizing:border-box;margin:0;padding:0;}
12
+ body{background:var(--bg);font-family:'Space Grotesk',sans-serif;color:#e2e8f0;min-height:100vh;overflow-x:hidden;}
13
+ code,pre,.mono{font-family:'JetBrains Mono',monospace;}
14
+ ::-webkit-scrollbar{width:5px;} ::-webkit-scrollbar-track{background:var(--bg);} ::-webkit-scrollbar-thumb{background:rgba(0,255,136,0.25);border-radius:4px;}
15
+ .hidden{display:none!important;}
16
+ </style>
17
+ </head>
18
+ <body>
19
+
20
+ <style>
21
+ /* ── Particle Canvas ── */
22
+ #particles{position:fixed;inset:0;z-index:0;pointer-events:none;}
23
+ /* ── Grid overlay ── */
24
+ .grid-bg{position:fixed;inset:0;z-index:0;pointer-events:none;
25
+ background-image:linear-gradient(rgba(0,255,136,0.03) 1px,transparent 1px),linear-gradient(90deg,rgba(0,255,136,0.03) 1px,transparent 1px);
26
+ background-size:60px 60px;}
27
+ /* ── Glass ── */
28
+ .glass{background:var(--card);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);border:1px solid var(--border);border-radius:16px;box-shadow:0 8px 40px rgba(0,0,0,0.5);}
29
+ .glass-sm{background:rgba(255,255,255,0.03);backdrop-filter:blur(12px);border:1px solid rgba(255,255,255,0.07);border-radius:12px;}
30
+ /* ── Glow borders ── */
31
+ .glow-green{box-shadow:0 0 20px rgba(0,255,136,0.25),inset 0 0 20px rgba(0,255,136,0.04);}
32
+ .glow-red{box-shadow:0 0 20px rgba(255,51,85,0.25),inset 0 0 20px rgba(255,51,85,0.04);}
33
+ /* ── Header ── */
34
+ header{position:sticky;top:0;z-index:50;background:rgba(5,5,8,0.85);backdrop-filter:blur(24px);border-bottom:1px solid rgba(0,255,136,0.1);}
35
+ /* ── Eye SVG animation ── */
36
+ @keyframes scanPulse{0%,100%{opacity:1;transform:scale(1);}50%{opacity:0.5;transform:scale(1.15);}}
37
+ @keyframes eyeScan{0%{stroke-dashoffset:100;}100%{stroke-dashoffset:0;}}
38
+ .eye-scan{animation:scanPulse 2.5s ease-in-out infinite;}
39
+ /* ── Status dot ── */
40
+ @keyframes statusBlink{0%,100%{opacity:1;box-shadow:0 0 6px var(--g);}50%{opacity:0.4;box-shadow:none;}}
41
+ .status-dot{width:8px;height:8px;border-radius:50%;background:var(--g);animation:statusBlink 2s ease-in-out infinite;}
42
+ /* ── Drop zone border animation ── */
43
+ @keyframes borderDash{to{stroke-dashoffset:-20;}}
44
+ #dropZone{border:2px dashed rgba(0,255,136,0.25);border-radius:16px;cursor:pointer;min-height:200px;display:flex;align-items:center;justify-content:center;transition:all 0.35s ease;position:relative;overflow:hidden;}
45
+ #dropZone:hover,#dropZone.drag-over{border-color:var(--g);background:rgba(0,255,136,0.04);box-shadow:0 0 40px rgba(0,255,136,0.2),inset 0 0 30px rgba(0,255,136,0.06);transform:scale(1.01);}
46
+ /* ── Orbit ring ── */
47
+ @keyframes orbitRing{from{transform:rotate(0deg);}to{transform:rotate(360deg);}}
48
+ .orbit{animation:orbitRing 3s linear infinite;transform-origin:center;}
49
+ /* ── Analyze button ── */
50
+ #analyzeBtn{width:100%;padding:16px;border-radius:12px;font-family:'Space Grotesk',sans-serif;font-weight:700;font-size:14px;letter-spacing:0.12em;text-transform:uppercase;border:1px solid rgba(255,255,255,0.1);background:rgba(255,255,255,0.05);color:var(--muted);cursor:not-allowed;transition:all 0.3s ease;position:relative;overflow:hidden;}
51
+ #analyzeBtn.active{background:linear-gradient(135deg,#00ff88,#00cc66);color:#050508;border-color:transparent;cursor:pointer;box-shadow:0 0 30px rgba(0,255,136,0.4);}
52
+ #analyzeBtn.active:hover{transform:translateY(-2px) scale(1.01);box-shadow:0 0 50px rgba(0,255,136,0.6);}
53
+ #analyzeBtn.active::after{content:'';position:absolute;top:0;left:-100%;width:60%;height:100%;background:linear-gradient(90deg,transparent,rgba(255,255,255,0.25),transparent);animation:shimmer 2s ease infinite;}
54
+ @keyframes shimmer{to{left:150%;}}
55
+ #analyzeBtn:disabled{opacity:0.45;cursor:not-allowed;transform:none!important;}
56
+ </style>
57
+
58
+ <style>
59
+ /* ── Loading ── */
60
+ @keyframes radarSpin{from{transform:rotate(0deg);}to{transform:rotate(360deg);}}
61
+ @keyframes radarPulse{0%,100%{opacity:0.8;transform:scale(1);}50%{opacity:0.3;transform:scale(1.05);}}
62
+ .radar-ring{border-radius:50%;border:1px solid rgba(0,255,136,0.2);position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);}
63
+ .radar-sweep{position:absolute;top:50%;left:50%;width:50%;height:2px;transform-origin:left center;background:linear-gradient(90deg,transparent,var(--g));animation:radarSpin 2s linear infinite;}
64
+ /* ── Agent cards ── */
65
+ .agent-card{transition:all 0.4s ease;border:1px solid rgba(255,255,255,0.06);border-radius:12px;padding:12px 16px;background:rgba(255,255,255,0.02);}
66
+ .agent-card.active{border-color:var(--g);background:rgba(0,255,136,0.06);box-shadow:0 0 20px rgba(0,255,136,0.15);}
67
+ .agent-card.active .agent-dot{background:var(--g);box-shadow:0 0 8px var(--g);}
68
+ .agent-dot{width:8px;height:8px;border-radius:50%;background:rgba(255,255,255,0.15);transition:all 0.4s ease;flex-shrink:0;}
69
+ /* ── Progress bar ── */
70
+ .prog-track{height:4px;background:rgba(255,255,255,0.06);border-radius:99px;overflow:hidden;}
71
+ .prog-fill{height:100%;border-radius:99px;background:linear-gradient(90deg,#00aaff,var(--g));box-shadow:0 0 12px var(--g);transition:width 0.5s ease;}
72
+ /* ── Verdict ── */
73
+ @keyframes glitch{0%,100%{text-shadow:0 0 10px var(--r);}20%{text-shadow:-2px 0 #ff0,2px 0 #0ff,0 0 20px var(--r);}40%{text-shadow:2px 0 #ff0,-2px 0 #0ff,0 0 10px var(--r);}60%{text-shadow:0 0 30px var(--r);}}
74
+ .glitch-text{animation:glitch 3s ease-in-out infinite;}
75
+ @keyframes fadeUp{from{opacity:0;transform:translateY(24px);}to{opacity:1;transform:none;}}
76
+ .fade-up{animation:fadeUp 0.6s cubic-bezier(0.16,1,0.3,1) both;}
77
+ @keyframes slideInLeft{from{opacity:0;transform:translateX(-20px);}to{opacity:1;transform:none;}}
78
+ /* ── Verdict badge ── */
79
+ .verdict-badge{width:110px;height:110px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:2.8rem;transition:all 0.5s;border:2px solid var(--border);}
80
+ @keyframes pulseFake{0%,100%{box-shadow:0 0 20px rgba(255,51,85,0.3);}50%{box-shadow:0 0 50px rgba(255,51,85,0.7);}}
81
+ @keyframes pulseReal{0%,100%{box-shadow:0 0 20px rgba(0,255,136,0.3);}50%{box-shadow:0 0 50px rgba(0,255,136,0.7);}}
82
+ .verdict-badge.fake{border-color:var(--r);animation:pulseFake 2s infinite;}
83
+ .verdict-badge.real{border-color:var(--g);animation:pulseReal 2s infinite;}
84
+ /* ── Confidence arc (CSS semicircle gauge) ── */
85
+ .gauge-wrap{position:relative;width:200px;height:110px;overflow:hidden;margin:0 auto;}
86
+ .gauge-bg{width:200px;height:200px;border-radius:50%;border:12px solid rgba(255,255,255,0.06);position:absolute;top:0;left:0;clip-path:inset(0 0 50% 0);}
87
+ .gauge-fill{width:200px;height:200px;border-radius:50%;border:12px solid transparent;position:absolute;top:0;left:0;clip-path:inset(0 0 50% 0);transition:transform 1.2s cubic-bezier(0.22,1,0.36,1);}
88
+ .gauge-fill.fake{border-color:var(--r);box-shadow:0 0 20px rgba(255,51,85,0.5);}
89
+ .gauge-fill.real{border-color:var(--g);box-shadow:0 0 20px rgba(0,255,136,0.5);}
90
+ /* ── Conf bar ── */
91
+ .conf-track{height:10px;background:rgba(255,255,255,0.05);border-radius:99px;overflow:hidden;border:1px solid rgba(255,255,255,0.05);}
92
+ .conf-fill{height:100%;border-radius:99px;transition:width 1.2s cubic-bezier(0.22,1,0.36,1);}
93
+ .conf-fill.fake{background:linear-gradient(90deg,#880022,var(--r));box-shadow:0 0 15px rgba(255,51,85,0.6);}
94
+ .conf-fill.real{background:linear-gradient(90deg,#00aaff,var(--g));box-shadow:0 0 15px rgba(0,255,136,0.6);}
95
+ /* ── Risk needle ── */
96
+ #riskNeedle{display:inline-block;padding:4px 14px;border-radius:99px;font-size:11px;font-weight:700;letter-spacing:0.15em;border:1px solid;transition:all 0.5s;}
97
+ /* ── Insight bullets ── */
98
+ .insight-item{display:flex;align-items:flex-start;gap:12px;padding:12px 16px;border-radius:10px;background:rgba(255,255,255,0.02);border:1px solid rgba(255,255,255,0.05);font-size:13px;line-height:1.6;color:#a0aec0;animation:slideInLeft 0.4s ease both;}
99
+ .insight-item:hover{background:rgba(255,255,255,0.04);}
100
+ .insight-dot{flex-shrink:0;width:8px;height:8px;border-radius:50%;margin-top:6px;}
101
+ /* ── Timeline bars ── */
102
+ .bar-wrap{display:flex;flex-direction:column;align-items:center;gap:4px;flex:1;min-width:0;position:relative;}
103
+ .bar-outer{width:100%;background:rgba(255,255,255,0.05);border-radius:4px 4px 0 0;overflow:hidden;position:relative;}
104
+ .bar-inner{width:100%;border-radius:4px 4px 0 0;transition:height 0.8s cubic-bezier(0.22,1,0.36,1);}
105
+ .bar-wrap:hover .bar-tooltip{opacity:1;transform:translateY(-4px);}
106
+ .bar-tooltip{position:absolute;bottom:calc(100% + 6px);left:50%;transform:translateX(-50%);background:rgba(10,10,20,0.95);border:1px solid rgba(255,255,255,0.1);border-radius:6px;padding:4px 8px;font-size:10px;white-space:nowrap;pointer-events:none;opacity:0;transition:all 0.2s ease;z-index:10;font-family:'JetBrains Mono',monospace;}
107
+ /* ── Meta grid ── */
108
+ .meta-row{display:flex;justify-content:space-between;align-items:center;padding:8px 0;border-bottom:1px solid rgba(255,255,255,0.05);}
109
+ .meta-row:last-child{border-bottom:none;}
110
+ /* ── Error ── */
111
+ #errorSection{border-color:rgba(255,51,85,0.3)!important;background:rgba(255,51,85,0.04)!important;}
112
+ /* ── Stat pills ── */
113
+ .stat-pill{display:inline-flex;align-items:center;gap:8px;padding:8px 18px;border-radius:99px;background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.08);font-size:12px;font-weight:600;letter-spacing:0.05em;color:#a0aec0;}
114
+ .stat-pill .pip{width:6px;height:6px;border-radius:50%;background:var(--g);box-shadow:0 0 6px var(--g);}
115
+ /* ── Section label ── */
116
+ .sec-label{font-size:10px;font-weight:700;letter-spacing:0.2em;text-transform:uppercase;color:#00aaff;display:flex;align-items:center;gap:8px;margin-bottom:14px;}
117
+ .sec-label::before{content:'';display:inline-block;width:3px;height:14px;background:#00aaff;border-radius:2px;box-shadow:0 0 8px #00aaff;}
118
+ /* ── Model badge ── */
119
+ #modelBadge{display:inline-flex;align-items:center;gap:8px;padding:6px 14px;border-radius:99px;background:rgba(0,255,136,0.08);border:1px solid rgba(0,255,136,0.25);font-size:11px;font-weight:600;letter-spacing:0.1em;color:var(--g);}
120
+ </style>
121
+
122
+
123
+ <!-- Particle canvas + grid -->
124
+ <canvas id="particles"></canvas>
125
+ <div class="grid-bg"></div>
126
+
127
+ <!-- ══ HEADER ══ -->
128
+ <header>
129
+ <div style="max-width:1100px;margin:0 auto;padding:0 24px;height:64px;display:flex;align-items:center;justify-content:space-between;">
130
+ <div style="display:flex;align-items:center;gap:14px;">
131
+ <!-- Animated eye logo -->
132
+ <div class="eye-scan" style="width:36px;height:36px;display:flex;align-items:center;justify-content:center;">
133
+ <svg width="36" height="36" viewBox="0 0 36 36" fill="none">
134
+ <ellipse cx="18" cy="18" rx="14" ry="9" stroke="#00ff88" stroke-width="1.5" opacity="0.9"/>
135
+ <circle cx="18" cy="18" r="5" fill="#00ff88" opacity="0.9"/>
136
+ <circle cx="18" cy="18" r="2.5" fill="#050508"/>
137
+ <path d="M4 18 Q18 4 32 18" stroke="#00ff88" stroke-width="0.8" stroke-dasharray="4 3" opacity="0.4">
138
+ <animateTransform attributeName="transform" type="rotate" from="0 18 18" to="360 18 18" dur="8s" repeatCount="indefinite"/>
139
+ </path>
140
+ </svg>
141
+ </div>
142
+ <div>
143
+ <div style="font-size:13px;font-weight:700;letter-spacing:0.18em;color:#fff;">DEEPFAKE AUTHENTICATOR</div>
144
+ <div style="font-size:9px;letter-spacing:0.25em;color:var(--muted);font-family:'JetBrains Mono',monospace;">AI-POWERED VIDEO FORENSICS</div>
145
+ </div>
146
+ </div>
147
+ <div id="modelBadge">
148
+ <div class="status-dot"></div>
149
+ <span>ViT ENSEMBLE</span>
150
+ </div>
151
+ </div>
152
+ </header>
153
+
154
+ <!-- ══ MAIN ══ -->
155
+ <div style="position:relative;z-index:1;max-width:860px;margin:0 auto;padding:48px 24px 80px;">
156
+
157
+ <!-- Hero -->
158
+ <div class="fade-up" style="text-align:center;margin-bottom:48px;animation-delay:0.1s;">
159
+ <h1 style="font-size:clamp(2rem,5vw,3.5rem);font-weight:700;line-height:1.1;margin-bottom:16px;background:linear-gradient(135deg,#fff 30%,var(--g));-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;">Is This Video Real?</h1>
160
+ <p style="color:var(--muted);font-size:15px;max-width:480px;margin:0 auto 28px;line-height:1.7;">Advanced AI ensemble detects facial manipulation with 99%+ accuracy using dual Vision Transformer models.</p>
161
+ <div style="display:flex;flex-wrap:wrap;gap:10px;justify-content:center;">
162
+ <div class="stat-pill"><span class="pip"></span>2 ViT Models</div>
163
+ <div class="stat-pill"><span class="pip"></span>40 Frame Analysis</div>
164
+ <div class="stat-pill"><span class="pip"></span>&lt; 15s Detection</div>
165
+ </div>
166
+ </div>
167
+
168
+ <!-- ══ UPLOAD SECTION ══ -->
169
+ <section id="uploadSection" class="glass fade-up" style="padding:32px;margin-bottom:24px;animation-delay:0.2s;">
170
+ <div id="dropZone">
171
+ <!-- Upload prompt -->
172
+ <div id="uploadPrompt" style="text-align:center;padding:40px 24px;">
173
+ <div style="position:relative;width:80px;height:80px;margin:0 auto 20px;">
174
+ <div style="position:absolute;inset:0;border-radius:50%;border:1px dashed rgba(0,255,136,0.3);" class="orbit"></div>
175
+ <div style="position:absolute;inset:8px;border-radius:50%;border:1px solid rgba(0,255,136,0.15);"></div>
176
+ <div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;">
177
+ <svg width="32" height="32" fill="none" stroke="#00ff88" stroke-width="1.5" viewBox="0 0 24 24" opacity="0.8">
178
+ <path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5"/>
179
+ </svg>
180
+ </div>
181
+ </div>
182
+ <h3 style="font-size:17px;font-weight:600;color:#fff;margin-bottom:8px;">Drop Video for Forensic Analysis</h3>
183
+ <p style="font-size:13px;color:var(--muted);">Drag &amp; drop or click to browse</p>
184
+ <p style="font-size:11px;color:var(--muted);margin-top:12px;opacity:0.6;font-family:'JetBrains Mono',monospace;">MP4 Β· AVI Β· MOV Β· MKV Β· WebM β€” Max 100MB</p>
185
+ </div>
186
+ <!-- File chosen state -->
187
+ <div id="fileChosen" class="hidden" style="width:100%;padding:28px 32px;display:flex;align-items:center;gap:20px;">
188
+ <div style="width:56px;height:56px;border-radius:12px;background:rgba(0,255,136,0.1);border:1px solid rgba(0,255,136,0.3);display:flex;align-items:center;justify-content:center;flex-shrink:0;box-shadow:0 0 20px rgba(0,255,136,0.15);">
189
+ <svg width="28" height="28" fill="none" stroke="#00ff88" stroke-width="1.5" viewBox="0 0 24 24">
190
+ <path stroke-linecap="round" stroke-linejoin="round" d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.348a1.125 1.125 0 010 1.971l-11.54 6.347a1.125 1.125 0 01-1.667-.985V5.653z"/>
191
+ </svg>
192
+ </div>
193
+ <div style="flex:1;min-width:0;">
194
+ <p id="chosenName" style="font-size:14px;font-weight:600;color:#fff;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;"></p>
195
+ <p id="chosenSize" style="font-size:12px;color:var(--muted);margin-top:4px;font-family:'JetBrains Mono',monospace;"></p>
196
+ </div>
197
+ <button id="clearBtn" style="padding:8px;color:var(--muted);background:none;border:none;cursor:pointer;border-radius:8px;transition:all 0.2s;" onmouseover="this.style.color='#ff3355';this.style.background='rgba(255,51,85,0.1)'" onmouseout="this.style.color='var(--muted)';this.style.background='none'">
198
+ <svg width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg>
199
+ </button>
200
+ </div>
201
+ </div>
202
+ <input type="file" id="fileInput" accept="video/*" style="display:none;"/>
203
+ <div style="margin-top:20px;">
204
+ <button id="analyzeBtn" disabled>
205
+ <span id="analyzeBtnText">Analyze Video</span>
206
+ </button>
207
+ </div>
208
+ </section>
209
+
210
+ <!-- ══ LOADING SECTION ══ -->
211
+ <section id="loadingSection" class="hidden glass fade-up" style="padding:48px 32px;text-align:center;margin-bottom:24px;">
212
+ <!-- Radar -->
213
+ <div style="position:relative;width:140px;height:140px;margin:0 auto 36px;">
214
+ <div class="radar-ring" style="width:140px;height:140px;"></div>
215
+ <div class="radar-ring" style="width:100px;height:100px;"></div>
216
+ <div class="radar-ring" style="width:60px;height:60px;"></div>
217
+ <div class="radar-sweep" style="width:70px;top:50%;left:50%;transform-origin:left center;"></div>
218
+ <div style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:12px;height:12px;border-radius:50%;background:var(--g);box-shadow:0 0 15px var(--g);"></div>
219
+ </div>
220
+ <!-- Status -->
221
+ <p id="loadingStatus" style="font-size:11px;font-weight:700;letter-spacing:0.2em;color:#00aaff;text-transform:uppercase;margin-bottom:20px;font-family:'JetBrains Mono',monospace;">Initializing sequence...</p>
222
+ <!-- Progress -->
223
+ <div style="max-width:400px;margin:0 auto 32px;">
224
+ <div class="prog-track">
225
+ <div id="progressBar" style="width:0%;" class="prog-fill"></div>
226
+ </div>
227
+ </div>
228
+ <!-- Agent cards -->
229
+ <div style="display:grid;grid-template-columns:repeat(2,1fr);gap:12px;max-width:500px;margin:0 auto;">
230
+ <div id="ag0" class="agent-card" style="display:flex;align-items:center;gap:10px;">
231
+ <div class="agent-dot"></div>
232
+ <div style="text-align:left;">
233
+ <div style="font-size:11px;font-weight:600;color:#fff;letter-spacing:0.05em;">Frame Extractor</div>
234
+ <div style="font-size:10px;color:var(--muted);font-family:'JetBrains Mono',monospace;">Sampling keyframes</div>
235
+ </div>
236
+ </div>
237
+ <div id="ag1" class="agent-card" style="display:flex;align-items:center;gap:10px;">
238
+ <div class="agent-dot"></div>
239
+ <div style="text-align:left;">
240
+ <div style="font-size:11px;font-weight:600;color:#fff;letter-spacing:0.05em;">Face Detector</div>
241
+ <div style="font-size:10px;color:var(--muted);font-family:'JetBrains Mono',monospace;">Isolating regions</div>
242
+ </div>
243
+ </div>
244
+ <div id="ag2" class="agent-card" style="display:flex;align-items:center;gap:10px;">
245
+ <div class="agent-dot"></div>
246
+ <div style="text-align:left;">
247
+ <div style="font-size:11px;font-weight:600;color:#fff;letter-spacing:0.05em;">ViT Ensemble</div>
248
+ <div style="font-size:10px;color:var(--muted);font-family:'JetBrains Mono',monospace;">Neural inference</div>
249
+ </div>
250
+ </div>
251
+ <div id="ag3" class="agent-card" style="display:flex;align-items:center;gap:10px;">
252
+ <div class="agent-dot"></div>
253
+ <div style="text-align:left;">
254
+ <div style="font-size:11px;font-weight:600;color:#fff;letter-spacing:0.05em;">Report Builder</div>
255
+ <div style="font-size:10px;color:var(--muted);font-family:'JetBrains Mono',monospace;">Compiling results</div>
256
+ </div>
257
+ </div>
258
+ </div>
259
+ </section>
260
+
261
+ <!-- ══ RESULT SECTION ══ -->
262
+ <section id="resultSection" class="hidden fade-up" style="display:flex;flex-direction:column;gap:20px;">
263
+
264
+ <!-- Verdict Card -->
265
+ <div id="verdictCard" class="glass" style="padding:40px;display:flex;flex-wrap:wrap;align-items:center;gap:36px;">
266
+ <!-- Badge + label -->
267
+ <div style="display:flex;flex-direction:column;align-items:center;flex-shrink:0;">
268
+ <div id="verdictBadge" class="verdict-badge" style="margin-bottom:14px;background:rgba(5,5,8,0.8);">
269
+ <span id="verdictEmoji" style="font-size:2.5rem;"></span>
270
+ </div>
271
+ <div id="verdictLabel" style="font-size:28px;font-weight:700;letter-spacing:0.15em;text-transform:uppercase;"></div>
272
+ <div style="font-size:9px;color:var(--muted);letter-spacing:0.25em;margin-top:4px;font-family:'JetBrains Mono',monospace;">VERDICT</div>
273
+ </div>
274
+ <!-- Confidence + risk -->
275
+ <div style="flex:1;min-width:220px;">
276
+ <div style="display:flex;justify-content:space-between;align-items:baseline;margin-bottom:10px;">
277
+ <span style="font-size:10px;color:var(--muted);letter-spacing:0.15em;text-transform:uppercase;">Confidence Score</span>
278
+ <span id="confValue" style="font-size:36px;font-weight:700;font-family:'JetBrains Mono',monospace;"></span>
279
+ </div>
280
+ <div class="conf-track" style="margin-bottom:20px;">
281
+ <div id="confBar" class="conf-fill" style="width:0%;"></div>
282
+ </div>
283
+ <div class="glass-sm" style="padding:14px 18px;display:flex;align-items:center;justify-content:space-between;">
284
+ <span style="font-size:10px;color:var(--muted);letter-spacing:0.15em;text-transform:uppercase;">Risk Assessment</span>
285
+ <span id="riskNeedle"></span>
286
+ </div>
287
+ <div style="margin-top:12px;text-align:right;">
288
+ <span id="riskLabel" style="font-size:11px;color:var(--muted);font-family:'JetBrains Mono',monospace;"></span>
289
+ </div>
290
+ </div>
291
+ </div>
292
+
293
+ <!-- Insights + Meta -->
294
+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;">
295
+ <div class="glass" style="padding:24px;">
296
+ <div class="sec-label">Analysis Insights</div>
297
+ <div id="detailsList" style="display:flex;flex-direction:column;gap:10px;"></div>
298
+ </div>
299
+ <div class="glass" style="padding:24px;display:flex;flex-direction:column;justify-content:space-between;">
300
+ <div>
301
+ <div class="sec-label">Video Metadata</div>
302
+ <div id="metaGrid"></div>
303
+ </div>
304
+ <div style="margin-top:16px;padding-top:14px;border-top:1px solid rgba(255,255,255,0.05);display:flex;align-items:center;gap:8px;">
305
+ <div style="width:6px;height:6px;border-radius:50%;background:var(--g);box-shadow:0 0 6px var(--g);"></div>
306
+ <span style="font-size:10px;color:var(--muted);letter-spacing:0.15em;text-transform:uppercase;font-family:'JetBrains Mono',monospace;">Models: ViT Ensemble</span>
307
+ </div>
308
+ </div>
309
+ </div>
310
+
311
+ <!-- Frame Timeline -->
312
+ <div class="glass" style="padding:24px;">
313
+ <div class="sec-label">Frame Analysis Timeline</div>
314
+ <div id="timelineChart" style="display:flex;align-items:flex-end;gap:3px;height:80px;position:relative;padding-bottom:20px;"></div>
315
+ <div style="display:flex;justify-content:space-between;margin-top:8px;">
316
+ <span style="font-size:10px;color:var(--muted);font-family:'JetBrains Mono',monospace;">Frame 0</span>
317
+ <span style="font-size:10px;color:var(--muted);font-family:'JetBrains Mono',monospace;">Frame 40</span>
318
+ </div>
319
+ </div>
320
+
321
+ <!-- Reset -->
322
+ <div style="text-align:center;margin-top:8px;">
323
+ <button onclick="resetAll()" style="padding:12px 28px;font-size:11px;font-weight:700;letter-spacing:0.15em;text-transform:uppercase;color:var(--muted);background:none;border:1px solid rgba(255,255,255,0.1);border-radius:10px;cursor:pointer;transition:all 0.3s;font-family:'Space Grotesk',sans-serif;" onmouseover="this.style.color='var(--g)';this.style.borderColor='rgba(0,255,136,0.4)';this.style.background='rgba(0,255,136,0.05)'" onmouseout="this.style.color='var(--muted)';this.style.borderColor='rgba(255,255,255,0.1)';this.style.background='none'">
324
+ ↻ &nbsp;Analyze Another Video
325
+ </button>
326
+ </div>
327
+ </section>
328
+
329
+ <!-- ══ ERROR SECTION ══ -->
330
+ <section id="errorSection" class="hidden glass fade-up" style="padding:28px 32px;margin-bottom:24px;">
331
+ <div style="display:flex;align-items:center;gap:12px;margin-bottom:10px;">
332
+ <svg width="22" height="22" fill="none" stroke="#ff3355" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg>
333
+ <span style="font-size:12px;font-weight:700;letter-spacing:0.2em;color:#ff3355;text-transform:uppercase;font-family:'JetBrains Mono',monospace;">Analysis Failed</span>
334
+ </div>
335
+ <p id="errorMsg" style="font-size:13px;color:var(--muted);margin-left:34px;line-height:1.6;"></p>
336
+ <div style="margin-left:34px;margin-top:16px;">
337
+ <button onclick="resetAll()" style="font-size:11px;color:rgba(255,255,255,0.4);background:none;border:none;cursor:pointer;letter-spacing:0.15em;text-transform:uppercase;font-family:'Space Grotesk',sans-serif;transition:color 0.2s;" onmouseover="this.style.color='#fff'" onmouseout="this.style.color='rgba(255,255,255,0.4)'">↻ Try Again</button>
338
+ </div>
339
+ </section>
340
+
341
+ </div><!-- /main -->
342
+
343
+ <footer style="position:relative;z-index:1;text-align:center;padding:24px;font-size:10px;color:rgba(90,106,122,0.5);letter-spacing:0.2em;text-transform:uppercase;font-family:'JetBrains Mono',monospace;">
344
+ Deepfake Authenticator &nbsp;Β·&nbsp; Secure Local Inference &nbsp;Β·&nbsp; ViT Ensemble v2
345
+ </footer>
346
+
347
+ <script>
348
+ // ── Particle background ──────────────────────────────────────────────────────
349
+ (function(){
350
+ const c=document.getElementById('particles');
351
+ const ctx=c.getContext('2d');
352
+ let W,H,pts=[];
353
+ function resize(){W=c.width=window.innerWidth;H=c.height=window.innerHeight;}
354
+ resize();window.addEventListener('resize',resize);
355
+ for(let i=0;i<60;i++)pts.push({x:Math.random()*W,y:Math.random()*H,vx:(Math.random()-0.5)*0.3,vy:(Math.random()-0.5)*0.3,r:Math.random()*1.5+0.5,a:Math.random()});
356
+ function draw(){
357
+ ctx.clearRect(0,0,W,H);
358
+ pts.forEach(p=>{
359
+ p.x+=p.vx;p.y+=p.vy;
360
+ if(p.x<0||p.x>W)p.vx*=-1;
361
+ if(p.y<0||p.y>H)p.vy*=-1;
362
+ ctx.beginPath();ctx.arc(p.x,p.y,p.r,0,Math.PI*2);
363
+ ctx.fillStyle=`rgba(0,255,136,${0.15+p.a*0.2})`;ctx.fill();
364
+ });
365
+ // draw connections
366
+ for(let i=0;i<pts.length;i++)for(let j=i+1;j<pts.length;j++){
367
+ const dx=pts[i].x-pts[j].x,dy=pts[i].y-pts[j].y,d=Math.sqrt(dx*dx+dy*dy);
368
+ if(d<120){ctx.beginPath();ctx.moveTo(pts[i].x,pts[i].y);ctx.lineTo(pts[j].x,pts[j].y);ctx.strokeStyle=`rgba(0,255,136,${0.06*(1-d/120)})`;ctx.lineWidth=0.5;ctx.stroke();}
369
+ }
370
+ requestAnimationFrame(draw);
371
+ }
372
+ draw();
373
+ })();
374
+ </script>
375
+ <script src="script.js"></script>
376
+ </body>
377
+ </html>
frontend-vanilla/script.js ADDED
@@ -0,0 +1,354 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Deepfake Authenticator β€” Frontend Logic (Cinematic Dark UI)
3
+ */
4
+
5
+ const API_BASE = (window.location.protocol === 'file:')
6
+ ? 'http://localhost:8000'
7
+ : window.location.origin;
8
+
9
+ let selectedFile = null;
10
+
11
+ // ── Boot ──────────────────────────────────────
12
+ window.addEventListener('load', () => {
13
+ initUpload();
14
+ });
15
+
16
+ // ── Upload wiring ─────────────────────────────
17
+ function initUpload() {
18
+ const zone = document.getElementById('dropZone');
19
+ const input = document.getElementById('fileInput');
20
+ const clear = document.getElementById('clearBtn');
21
+ const btn = document.getElementById('analyzeBtn');
22
+
23
+ zone.addEventListener('click', e => {
24
+ if (!e.target.closest('#clearBtn')) input.click();
25
+ });
26
+
27
+ input.addEventListener('change', () => {
28
+ if (input.files?.[0]) applyFile(input.files[0]);
29
+ });
30
+
31
+ clear.addEventListener('click', e => {
32
+ e.stopPropagation();
33
+ clearFile();
34
+ });
35
+
36
+ zone.addEventListener('dragover', e => {
37
+ e.preventDefault(); e.stopPropagation();
38
+ zone.classList.add('drag-over');
39
+ });
40
+
41
+ zone.addEventListener('dragleave', e => {
42
+ e.preventDefault();
43
+ zone.classList.remove('drag-over');
44
+ });
45
+
46
+ zone.addEventListener('drop', e => {
47
+ e.preventDefault(); e.stopPropagation();
48
+ zone.classList.remove('drag-over');
49
+ const f = e.dataTransfer.files[0];
50
+ if (f?.type.startsWith('video/')) applyFile(f);
51
+ else showError('Please drop a valid video file (MP4, AVI, MOV, MKV, WebM).');
52
+ });
53
+
54
+ btn.addEventListener('click', analyzeVideo);
55
+ }
56
+
57
+ function applyFile(file) {
58
+ selectedFile = file;
59
+ document.getElementById('uploadPrompt').classList.add('hidden');
60
+ const fc = document.getElementById('fileChosen');
61
+ fc.classList.remove('hidden');
62
+ document.getElementById('chosenName').textContent = file.name;
63
+ document.getElementById('chosenSize').textContent = fmtBytes(file.size);
64
+ const btn = document.getElementById('analyzeBtn');
65
+ btn.disabled = false;
66
+ btn.classList.add('active');
67
+ }
68
+
69
+ function clearFile() {
70
+ selectedFile = null;
71
+ document.getElementById('fileInput').value = '';
72
+ document.getElementById('fileChosen').classList.add('hidden');
73
+ document.getElementById('uploadPrompt').classList.remove('hidden');
74
+ const btn = document.getElementById('analyzeBtn');
75
+ btn.disabled = true;
76
+ btn.classList.remove('active');
77
+ }
78
+
79
+ function resetAll() {
80
+ clearFile();
81
+ ['loadingSection', 'resultSection', 'errorSection'].forEach(hide);
82
+ show('uploadSection');
83
+ }
84
+
85
+ // ── Analyze ───────────────────────────────────
86
+ async function analyzeVideo() {
87
+ if (!selectedFile) return;
88
+
89
+ hide('uploadSection');
90
+ show('loadingSection');
91
+ ['resultSection', 'errorSection'].forEach(hide);
92
+
93
+ startAgentAnim();
94
+
95
+ const fd = new FormData();
96
+ fd.append('file', selectedFile);
97
+
98
+ try {
99
+ const res = await fetch(`${API_BASE}/analyze`, { method: 'POST', body: fd });
100
+ if (!res.ok) {
101
+ const e = await res.json().catch(() => ({}));
102
+ throw new Error(e.detail || `Server error ${res.status}`);
103
+ }
104
+ const data = await res.json();
105
+ renderResult(data);
106
+ } catch (err) {
107
+ showError(err.message || 'Connection to analysis engine failed.');
108
+ } finally {
109
+ hide('loadingSection');
110
+ stopAgentAnim();
111
+ }
112
+ }
113
+
114
+ // ── Loading Animation ─────────────────────────
115
+ let _simTimer = null;
116
+ let _agentTimer = null;
117
+
118
+ function startAgentAnim() {
119
+ const statusEl = document.getElementById('loadingStatus');
120
+ const progBar = document.getElementById('progressBar');
121
+
122
+ const phases = [
123
+ { p: 12, msg: 'Extracting keyframes...', ag: 0 },
124
+ { p: 35, msg: 'Isolating facial regions...', ag: 1 },
125
+ { p: 65, msg: 'Running ViT neural inference...', ag: 2 },
126
+ { p: 85, msg: 'Cross-referencing metadata...', ag: 2 },
127
+ { p: 95, msg: 'Compiling authenticity report...', ag: 3 },
128
+ ];
129
+
130
+ // Reset agents
131
+ [0,1,2,3].forEach(i => {
132
+ const card = document.getElementById('ag' + i);
133
+ if (card) card.classList.remove('active');
134
+ });
135
+
136
+ statusEl.textContent = 'Initializing sequence...';
137
+ progBar.style.width = '0%';
138
+
139
+ let idx = 0;
140
+ _simTimer = setInterval(() => {
141
+ if (idx < phases.length) {
142
+ const ph = phases[idx];
143
+ statusEl.textContent = ph.msg;
144
+ progBar.style.width = ph.p + '%';
145
+ const card = document.getElementById('ag' + ph.ag);
146
+ if (card) card.classList.add('active');
147
+ idx++;
148
+ }
149
+ }, 1100);
150
+ }
151
+
152
+ function stopAgentAnim() {
153
+ if (_simTimer) { clearInterval(_simTimer); _simTimer = null; }
154
+ document.getElementById('progressBar').style.width = '100%';
155
+ [0,1,2,3].forEach(i => {
156
+ const card = document.getElementById('ag' + i);
157
+ if (card) card.classList.add('active');
158
+ });
159
+ }
160
+
161
+ // ── Render Result ───────────���─────────────────
162
+ function renderResult(data) {
163
+ const isFake = data.result === 'FAKE';
164
+ const pct = data.confidence;
165
+
166
+ // Verdict card border glow
167
+ const vc = document.getElementById('verdictCard');
168
+ vc.style.borderColor = isFake ? 'rgba(255,51,85,0.5)' : 'rgba(0,255,136,0.5)';
169
+ vc.style.boxShadow = isFake
170
+ ? '0 0 50px rgba(255,51,85,0.15), inset 0 0 30px rgba(255,51,85,0.05)'
171
+ : '0 0 50px rgba(0,255,136,0.15), inset 0 0 30px rgba(0,255,136,0.05)';
172
+
173
+ // Badge
174
+ const badge = document.getElementById('verdictBadge');
175
+ badge.className = 'verdict-badge ' + (isFake ? 'fake' : 'real');
176
+ badge.style.background = isFake ? 'rgba(255,51,85,0.08)' : 'rgba(0,255,136,0.08)';
177
+
178
+ // Emoji
179
+ document.getElementById('verdictEmoji').textContent = isFake ? '⚠' : 'βœ“';
180
+
181
+ // Label
182
+ const lbl = document.getElementById('verdictLabel');
183
+ lbl.textContent = isFake ? 'DEEPFAKE' : 'AUTHENTIC';
184
+ lbl.style.color = isFake ? '#ff3355' : '#00ff88';
185
+ lbl.style.textShadow = isFake
186
+ ? '0 0 20px rgba(255,51,85,0.7)'
187
+ : '0 0 20px rgba(0,255,136,0.7)';
188
+ if (isFake) lbl.classList.add('glitch-text');
189
+ else lbl.classList.remove('glitch-text');
190
+
191
+ // Confidence value
192
+ const cv = document.getElementById('confValue');
193
+ cv.textContent = pct + '%';
194
+ cv.style.color = isFake ? '#ff3355' : '#00ff88';
195
+
196
+ // Confidence bar
197
+ const bar = document.getElementById('confBar');
198
+ bar.className = 'conf-fill ' + (isFake ? 'fake' : 'real');
199
+ setTimeout(() => { bar.style.width = pct + '%'; }, 80);
200
+
201
+ // Risk needle
202
+ const needle = document.getElementById('riskNeedle');
203
+ const riskLbl = document.getElementById('riskLabel');
204
+ if (pct < 35) {
205
+ needle.textContent = 'LOW RISK';
206
+ needle.style.color = '#00ff88';
207
+ needle.style.borderColor = 'rgba(0,255,136,0.35)';
208
+ needle.style.background = 'rgba(0,255,136,0.08)';
209
+ if (riskLbl) riskLbl.textContent = 'Minimal manipulation indicators detected';
210
+ } else if (pct < 65) {
211
+ needle.textContent = 'MEDIUM RISK';
212
+ needle.style.color = '#ffaa00';
213
+ needle.style.borderColor = 'rgba(255,170,0,0.35)';
214
+ needle.style.background = 'rgba(255,170,0,0.08)';
215
+ if (riskLbl) riskLbl.textContent = 'Moderate anomalies detected β€” review advised';
216
+ } else {
217
+ needle.textContent = 'CRITICAL RISK';
218
+ needle.style.color = '#ff3355';
219
+ needle.style.borderColor = 'rgba(255,51,85,0.35)';
220
+ needle.style.background = 'rgba(255,51,85,0.08)';
221
+ if (riskLbl) riskLbl.textContent = 'High-confidence manipulation signatures found';
222
+ }
223
+
224
+ // Insights
225
+ const dl = document.getElementById('detailsList');
226
+ dl.innerHTML = '';
227
+ const details = data.details || ['Analysis completed successfully.'];
228
+ const dotColor = isFake ? '#ff3355' : '#00ff88';
229
+ const dotGlow = isFake ? 'rgba(255,51,85,0.6)' : 'rgba(0,255,136,0.6)';
230
+ details.forEach((txt, i) => {
231
+ const div = document.createElement('div');
232
+ div.className = 'insight-item';
233
+ div.style.animationDelay = (i * 0.08) + 's';
234
+ div.style.borderLeftColor = dotColor;
235
+ div.style.borderLeft = `2px solid ${dotColor}`;
236
+ div.innerHTML = `<span class="insight-dot" style="background:${dotColor};box-shadow:0 0 8px ${dotGlow};"></span><span>${esc(txt)}</span>`;
237
+ dl.appendChild(div);
238
+ });
239
+
240
+ // Metadata
241
+ const meta = data.metadata || {};
242
+ const mg = document.getElementById('metaGrid');
243
+ mg.innerHTML = '';
244
+ const metaItems = [
245
+ ['Frames Analyzed', meta.frames_analyzed ?? 'β€”'],
246
+ ['Duration', meta.video_duration_sec ? meta.video_duration_sec + 's' : 'β€”'],
247
+ ['FPS', meta.video_fps ?? 'β€”'],
248
+ ['Resolution', meta.resolution ?? 'β€”'],
249
+ ['Processing Time', data.processing_time_sec ? data.processing_time_sec + 's' : 'β€”'],
250
+ ];
251
+ metaItems.forEach(([k, v]) => {
252
+ const row = document.createElement('div');
253
+ row.className = 'meta-row';
254
+ row.innerHTML = `<span style="font-size:11px;color:var(--muted);letter-spacing:0.08em;">${k}</span><span style="font-size:13px;font-weight:600;color:#fff;font-family:'JetBrains Mono',monospace;">${v}</span>`;
255
+ mg.appendChild(row);
256
+ });
257
+
258
+ // Frame timeline
259
+ renderTimeline(data, isFake);
260
+
261
+ show('resultSection');
262
+ }
263
+
264
+ function renderTimeline(data, isFake) {
265
+ const chart = document.getElementById('timelineChart');
266
+ if (!chart) return;
267
+ chart.innerHTML = '';
268
+
269
+ const frames = data.frame_scores || [];
270
+ if (!frames.length) {
271
+ chart.innerHTML = '<span style="font-size:11px;color:var(--muted);font-family:\'JetBrains Mono\',monospace;margin:auto;">No per-frame data available</span>';
272
+ return;
273
+ }
274
+
275
+ const maxH = 60; // px
276
+ const barColor = isFake ? '#ff3355' : '#00ff88';
277
+ const barGlow = isFake ? 'rgba(255,51,85,0.5)' : 'rgba(0,255,136,0.5)';
278
+
279
+ frames.forEach((score, i) => {
280
+ const pct = Math.round(score * 100);
281
+ const h = Math.max(4, Math.round((score) * maxH));
282
+
283
+ const wrap = document.createElement('div');
284
+ wrap.className = 'bar-wrap';
285
+ wrap.style.height = maxH + 'px';
286
+
287
+ const outer = document.createElement('div');
288
+ outer.className = 'bar-outer';
289
+ outer.style.height = maxH + 'px';
290
+
291
+ const inner = document.createElement('div');
292
+ inner.className = 'bar-inner';
293
+ inner.style.height = '0px';
294
+ inner.style.background = score > 0.5
295
+ ? `linear-gradient(to top, ${barColor}, rgba(255,255,255,0.3))`
296
+ : 'rgba(255,255,255,0.12)';
297
+ if (score > 0.5) inner.style.boxShadow = `0 0 8px ${barGlow}`;
298
+
299
+ outer.appendChild(inner);
300
+
301
+ const tip = document.createElement('div');
302
+ tip.className = 'bar-tooltip';
303
+ tip.textContent = `F${i+1}: ${pct}%`;
304
+
305
+ wrap.appendChild(outer);
306
+ wrap.appendChild(tip);
307
+ chart.appendChild(wrap);
308
+
309
+ // Animate in
310
+ setTimeout(() => { inner.style.height = h + 'px'; }, 50 + i * 20);
311
+ });
312
+
313
+ // Threshold line at 50%
314
+ const line = document.createElement('div');
315
+ line.style.cssText = `position:absolute;left:0;right:0;bottom:${maxH*0.5}px;height:1px;background:rgba(255,170,0,0.4);pointer-events:none;`;
316
+ const lineLbl = document.createElement('span');
317
+ lineLbl.style.cssText = 'position:absolute;right:4px;top:-14px;font-size:9px;color:#ffaa00;font-family:\'JetBrains Mono\',monospace;letter-spacing:0.1em;';
318
+ lineLbl.textContent = '50%';
319
+ line.appendChild(lineLbl);
320
+ chart.appendChild(line);
321
+ }
322
+
323
+ // ── Helpers ───────────────────────────────────
324
+ function show(id) {
325
+ const el = document.getElementById(id);
326
+ if (!el) return;
327
+ el.classList.remove('hidden');
328
+ if (el.style.display === 'none') el.style.display = '';
329
+ }
330
+
331
+ function hide(id) {
332
+ const el = document.getElementById(id);
333
+ if (el) el.classList.add('hidden');
334
+ }
335
+
336
+ function fmtBytes(b) {
337
+ if (b < 1024) return b + ' B';
338
+ if (b < 1048576) return (b / 1024).toFixed(1) + ' KB';
339
+ return (b / 1048576).toFixed(1) + ' MB';
340
+ }
341
+
342
+ function esc(s) {
343
+ return String(s)
344
+ .replace(/&/g, '&amp;')
345
+ .replace(/</g, '&lt;')
346
+ .replace(/>/g, '&gt;');
347
+ }
348
+
349
+ function showError(msg) {
350
+ hide('uploadSection');
351
+ hide('loadingSection');
352
+ document.getElementById('errorMsg').textContent = msg;
353
+ show('errorSection');
354
+ }
frontend/.gitignore ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ *.local
14
+
15
+ # Editor directories and files
16
+ .vscode/*
17
+ !.vscode/extensions.json
18
+ .idea
19
+ .DS_Store
20
+ *.suo
21
+ *.ntvs*
22
+ *.njsproj
23
+ *.sln
24
+ *.sw?
frontend/README.md ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # React + Vite
2
+
3
+ This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4
+
5
+ Currently, two official plugins are available:
6
+
7
+ - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
8
+ - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
9
+
10
+ ## React Compiler
11
+
12
+ The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
13
+
14
+ ## Expanding the ESLint configuration
15
+
16
+ If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
frontend/eslint.config.js ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import reactHooks from 'eslint-plugin-react-hooks'
4
+ import reactRefresh from 'eslint-plugin-react-refresh'
5
+ import { defineConfig, globalIgnores } from 'eslint/config'
6
+
7
+ export default defineConfig([
8
+ globalIgnores(['dist']),
9
+ {
10
+ files: ['**/*.{js,jsx}'],
11
+ extends: [
12
+ js.configs.recommended,
13
+ reactHooks.configs.flat.recommended,
14
+ reactRefresh.configs.vite,
15
+ ],
16
+ languageOptions: {
17
+ globals: globals.browser,
18
+ parserOptions: { ecmaFeatures: { jsx: true } },
19
+ },
20
+ },
21
+ ])
frontend/index.html CHANGED
@@ -318,6 +318,42 @@ header{position:sticky;top:0;z-index:50;background:rgba(5,5,8,0.85);backdrop-fil
318
  </div>
319
  </div>
320
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
321
  <!-- Reset -->
322
  <div style="text-align:center;margin-top:8px;">
323
  <button onclick="resetAll()" style="padding:12px 28px;font-size:11px;font-weight:700;letter-spacing:0.15em;text-transform:uppercase;color:var(--muted);background:none;border:1px solid rgba(255,255,255,0.1);border-radius:10px;cursor:pointer;transition:all 0.3s;font-family:'Space Grotesk',sans-serif;" onmouseover="this.style.color='var(--g)';this.style.borderColor='rgba(0,255,136,0.4)';this.style.background='rgba(0,255,136,0.05)'" onmouseout="this.style.color='var(--muted)';this.style.borderColor='rgba(255,255,255,0.1)';this.style.background='none'">
 
318
  </div>
319
  </div>
320
 
321
+ <!-- ══ AUDIO ANALYSIS CARD ══ -->
322
+ <div id="audioSection" class="hidden glass fade-up" style="padding:28px;">
323
+ <div class="sec-label" style="margin-bottom:18px;">
324
+ <svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" style="flex-shrink:0;"><path stroke-linecap="round" stroke-linejoin="round" d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z"/></svg>
325
+ VOICE AUTHENTICITY ANALYSIS
326
+ </div>
327
+ <div style="display:flex;flex-wrap:wrap;align-items:center;gap:20px;margin-bottom:20px;">
328
+ <div id="audioBadge" style="padding:8px 18px;border-radius:99px;font-size:12px;font-weight:700;letter-spacing:0.1em;border:1px solid;transition:all .4s;"></div>
329
+ <div style="flex:1;min-width:160px;">
330
+ <div class="conf-track">
331
+ <div id="audioBar" class="conf-fill" style="width:0%;transition:width 1.2s cubic-bezier(0.22,1,0.36,1);"></div>
332
+ </div>
333
+ </div>
334
+ </div>
335
+ <div style="display:flex;gap:16px;margin-bottom:20px;flex-wrap:wrap;">
336
+ <div class="glass-sm" style="padding:12px 18px;flex:1;min-width:120px;text-align:center;">
337
+ <div style="font-size:10px;color:var(--muted);letter-spacing:.1em;margin-bottom:4px;">WAV2VEC2 MODEL</div>
338
+ <div id="audioModelScore" style="font-size:22px;font-weight:700;font-family:'JetBrains Mono',monospace;color:#fff;"></div>
339
+ </div>
340
+ <div class="glass-sm" style="padding:12px 18px;flex:1;min-width:120px;text-align:center;">
341
+ <div style="font-size:10px;color:var(--muted);letter-spacing:.1em;margin-bottom:4px;">SIGNAL HEURISTICS</div>
342
+ <div id="audioHeurScore" style="font-size:22px;font-weight:700;font-family:'JetBrains Mono',monospace;color:#fff;"></div>
343
+ </div>
344
+ </div>
345
+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;">
346
+ <div>
347
+ <div style="font-size:10px;color:var(--muted);letter-spacing:.12em;margin-bottom:10px;">VOICE INSIGHTS</div>
348
+ <div id="audioDetailsList" style="display:flex;flex-direction:column;gap:8px;"></div>
349
+ </div>
350
+ <div>
351
+ <div style="font-size:10px;color:var(--muted);letter-spacing:.12em;margin-bottom:10px;">SIGNAL FEATURES</div>
352
+ <div id="audioFeatGrid"></div>
353
+ </div>
354
+ </div>
355
+ </div>
356
+
357
  <!-- Reset -->
358
  <div style="text-align:center;margin-top:8px;">
359
  <button onclick="resetAll()" style="padding:12px 28px;font-size:11px;font-weight:700;letter-spacing:0.15em;text-transform:uppercase;color:var(--muted);background:none;border:1px solid rgba(255,255,255,0.1);border-radius:10px;cursor:pointer;transition:all 0.3s;font-family:'Space Grotesk',sans-serif;" onmouseover="this.style.color='var(--g)';this.style.borderColor='rgba(0,255,136,0.4)';this.style.background='rgba(0,255,136,0.05)'" onmouseout="this.style.color='var(--muted)';this.style.borderColor='rgba(255,255,255,0.1)';this.style.background='none'">
frontend/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
frontend/package.json ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "frontend",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "lint": "eslint .",
10
+ "preview": "vite preview"
11
+ },
12
+ "dependencies": {
13
+ "@tailwindcss/vite": "^4.2.4",
14
+ "autoprefixer": "^10.5.0",
15
+ "postcss": "^8.5.10",
16
+ "react": "^19.2.5",
17
+ "react-dom": "^19.2.5",
18
+ "styled-components": "^6.4.1",
19
+ "tailwindcss": "^4.2.4"
20
+ },
21
+ "devDependencies": {
22
+ "@eslint/js": "^10.0.1",
23
+ "@types/react": "^19.2.14",
24
+ "@types/react-dom": "^19.2.3",
25
+ "@vitejs/plugin-react": "^6.0.1",
26
+ "eslint": "^10.2.1",
27
+ "eslint-plugin-react-hooks": "^7.1.1",
28
+ "eslint-plugin-react-refresh": "^0.5.2",
29
+ "globals": "^17.5.0",
30
+ "vite": "^8.0.10"
31
+ }
32
+ }
frontend/public/favicon.svg ADDED
frontend/public/icons.svg ADDED
frontend/script.js CHANGED
@@ -258,6 +258,9 @@ function renderResult(data) {
258
  // Frame timeline
259
  renderTimeline(data, isFake);
260
 
 
 
 
261
  show('resultSection');
262
  }
263
 
@@ -352,3 +355,79 @@ function showError(msg) {
352
  document.getElementById('errorMsg').textContent = msg;
353
  show('errorSection');
354
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
  // Frame timeline
259
  renderTimeline(data, isFake);
260
 
261
+ // Audio result
262
+ renderAudio(data.audio || null);
263
+
264
  show('resultSection');
265
  }
266
 
 
355
  document.getElementById('errorMsg').textContent = msg;
356
  show('errorSection');
357
  }
358
+
359
+ // ── Audio Result ──────────────────────────────
360
+ function renderAudio(audio) {
361
+ const section = document.getElementById('audioSection');
362
+ if (!section) return;
363
+
364
+ if (!audio || !audio.available) {
365
+ section.classList.add('hidden');
366
+ return;
367
+ }
368
+
369
+ section.classList.remove('hidden');
370
+
371
+ const isAI = audio.result === 'AI_VOICE';
372
+ const pct = audio.confidence;
373
+ const color = isAI ? '#ff3355' : '#00ff88';
374
+ const colorDim = isAI ? 'rgba(255,51,85,0.15)' : 'rgba(0,255,136,0.15)';
375
+
376
+ // Header badge
377
+ const badge = document.getElementById('audioBadge');
378
+ if (badge) {
379
+ badge.textContent = isAI ? 'πŸ€– AI VOICE DETECTED' : 'πŸŽ™οΈ HUMAN VOICE';
380
+ badge.style.color = color;
381
+ badge.style.borderColor = color + '55';
382
+ badge.style.background = colorDim;
383
+ }
384
+
385
+ // Scores
386
+ const modelScore = document.getElementById('audioModelScore');
387
+ const heurScore = document.getElementById('audioHeurScore');
388
+ if (modelScore) modelScore.textContent = audio.model_score + '%';
389
+ if (heurScore) heurScore.textContent = audio.heuristic_score + '%';
390
+
391
+ // Bar
392
+ const bar = document.getElementById('audioBar');
393
+ if (bar) {
394
+ bar.style.background = isAI
395
+ ? 'linear-gradient(90deg,#880022,#ff3355)'
396
+ : 'linear-gradient(90deg,#00aaff,#00ff88)';
397
+ bar.style.boxShadow = `0 0 12px ${color}66`;
398
+ setTimeout(() => { bar.style.width = pct + '%'; }, 80);
399
+ }
400
+
401
+ // Details
402
+ const dl = document.getElementById('audioDetailsList');
403
+ if (dl) {
404
+ dl.innerHTML = '';
405
+ (audio.details || []).forEach((txt, i) => {
406
+ const div = document.createElement('div');
407
+ div.className = 'insight-item';
408
+ div.style.animationDelay = (i * 0.07) + 's';
409
+ div.style.borderLeft = `2px solid ${color}`;
410
+ div.innerHTML = `<span class="insight-dot" style="background:${color};box-shadow:0 0 6px ${color}88;"></span><span>${esc(txt)}</span>`;
411
+ dl.appendChild(div);
412
+ });
413
+ }
414
+
415
+ // Features
416
+ const feat = audio.features || {};
417
+ const featGrid = document.getElementById('audioFeatGrid');
418
+ if (featGrid) {
419
+ featGrid.innerHTML = '';
420
+ const items = [
421
+ ['Pitch Std Dev', feat.pitch_std_hz != null ? feat.pitch_std_hz + ' Hz' : 'β€”'],
422
+ ['MFCC Ξ” Variance', feat.mfcc_delta_var != null ? feat.mfcc_delta_var : 'β€”'],
423
+ ['Spectral Flatness', feat.spectral_flatness != null ? feat.spectral_flatness : 'β€”'],
424
+ ['Silence Ratio', feat.silence_ratio != null ? (feat.silence_ratio * 100).toFixed(1) + '%' : 'β€”'],
425
+ ];
426
+ items.forEach(([k, v]) => {
427
+ const row = document.createElement('div');
428
+ row.className = 'meta-row';
429
+ row.innerHTML = `<span style="font-size:11px;color:var(--muted);">${k}</span><span style="font-size:12px;font-weight:600;color:#fff;font-family:'JetBrains Mono',monospace;">${v}</span>`;
430
+ featGrid.appendChild(row);
431
+ });
432
+ }
433
+ }
frontend/src/App.css ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .counter {
2
+ font-size: 16px;
3
+ padding: 5px 10px;
4
+ border-radius: 5px;
5
+ color: var(--accent);
6
+ background: var(--accent-bg);
7
+ border: 2px solid transparent;
8
+ transition: border-color 0.3s;
9
+ margin-bottom: 24px;
10
+
11
+ &:hover {
12
+ border-color: var(--accent-border);
13
+ }
14
+ &:focus-visible {
15
+ outline: 2px solid var(--accent);
16
+ outline-offset: 2px;
17
+ }
18
+ }
19
+
20
+ .hero {
21
+ position: relative;
22
+
23
+ .base,
24
+ .framework,
25
+ .vite {
26
+ inset-inline: 0;
27
+ margin: 0 auto;
28
+ }
29
+
30
+ .base {
31
+ width: 170px;
32
+ position: relative;
33
+ z-index: 0;
34
+ }
35
+
36
+ .framework,
37
+ .vite {
38
+ position: absolute;
39
+ }
40
+
41
+ .framework {
42
+ z-index: 1;
43
+ top: 34px;
44
+ height: 28px;
45
+ transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
46
+ scale(1.4);
47
+ }
48
+
49
+ .vite {
50
+ z-index: 0;
51
+ top: 107px;
52
+ height: 26px;
53
+ width: auto;
54
+ transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
55
+ scale(0.8);
56
+ }
57
+ }
58
+
59
+ #center {
60
+ display: flex;
61
+ flex-direction: column;
62
+ gap: 25px;
63
+ place-content: center;
64
+ place-items: center;
65
+ flex-grow: 1;
66
+
67
+ @media (max-width: 1024px) {
68
+ padding: 32px 20px 24px;
69
+ gap: 18px;
70
+ }
71
+ }
72
+
73
+ #next-steps {
74
+ display: flex;
75
+ border-top: 1px solid var(--border);
76
+ text-align: left;
77
+
78
+ & > div {
79
+ flex: 1 1 0;
80
+ padding: 32px;
81
+ @media (max-width: 1024px) {
82
+ padding: 24px 20px;
83
+ }
84
+ }
85
+
86
+ .icon {
87
+ margin-bottom: 16px;
88
+ width: 22px;
89
+ height: 22px;
90
+ }
91
+
92
+ @media (max-width: 1024px) {
93
+ flex-direction: column;
94
+ text-align: center;
95
+ }
96
+ }
97
+
98
+ #docs {
99
+ border-right: 1px solid var(--border);
100
+
101
+ @media (max-width: 1024px) {
102
+ border-right: none;
103
+ border-bottom: 1px solid var(--border);
104
+ }
105
+ }
106
+
107
+ #next-steps ul {
108
+ list-style: none;
109
+ padding: 0;
110
+ display: flex;
111
+ gap: 8px;
112
+ margin: 32px 0 0;
113
+
114
+ .logo {
115
+ height: 18px;
116
+ }
117
+
118
+ a {
119
+ color: var(--text-h);
120
+ font-size: 16px;
121
+ border-radius: 6px;
122
+ background: var(--social-bg);
123
+ display: flex;
124
+ padding: 6px 12px;
125
+ align-items: center;
126
+ gap: 8px;
127
+ text-decoration: none;
128
+ transition: box-shadow 0.3s;
129
+
130
+ &:hover {
131
+ box-shadow: var(--shadow);
132
+ }
133
+ .button-icon {
134
+ height: 18px;
135
+ width: 18px;
136
+ }
137
+ }
138
+
139
+ @media (max-width: 1024px) {
140
+ margin-top: 20px;
141
+ flex-wrap: wrap;
142
+ justify-content: center;
143
+
144
+ li {
145
+ flex: 1 1 calc(50% - 8px);
146
+ }
147
+
148
+ a {
149
+ width: 100%;
150
+ justify-content: center;
151
+ box-sizing: border-box;
152
+ }
153
+ }
154
+ }
155
+
156
+ #spacer {
157
+ height: 88px;
158
+ border-top: 1px solid var(--border);
159
+ @media (max-width: 1024px) {
160
+ height: 48px;
161
+ }
162
+ }
163
+
164
+ .ticks {
165
+ position: relative;
166
+ width: 100%;
167
+
168
+ &::before,
169
+ &::after {
170
+ content: '';
171
+ position: absolute;
172
+ top: -4.5px;
173
+ border: 5px solid transparent;
174
+ }
175
+
176
+ &::before {
177
+ left: 0;
178
+ border-left-color: var(--border);
179
+ }
180
+ &::after {
181
+ right: 0;
182
+ border-right-color: var(--border);
183
+ }
184
+ }
frontend/src/App.jsx ADDED
@@ -0,0 +1,225 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useRef } from 'react';
2
+ import Loader from './components/Loader';
3
+ const API_BASE = 'http://localhost:8000';
4
+
5
+ function App() {
6
+ const [file, setFile] = useState(null);
7
+ const [status, setStatus] = useState('idle'); // 'idle', 'analyzing', 'result', 'error'
8
+ const [resultData, setResultData] = useState(null);
9
+ const [errorMsg, setErrorMsg] = useState('');
10
+
11
+ const fileInputRef = useRef(null);
12
+
13
+ const handleDrop = (e) => {
14
+ e.preventDefault();
15
+ const droppedFile = e.dataTransfer.files[0];
16
+ if (droppedFile && droppedFile.type.startsWith('video/')) {
17
+ setFile(droppedFile);
18
+ } else {
19
+ setErrorMsg('Please drop a valid video file (MP4, AVI, MOV, MKV, WebM).');
20
+ setStatus('error');
21
+ }
22
+ };
23
+
24
+ const handleFileChange = (e) => {
25
+ if (e.target.files && e.target.files[0]) {
26
+ setFile(e.target.files[0]);
27
+ }
28
+ };
29
+
30
+ const handleAnalyze = async () => {
31
+ if (!file) return;
32
+ setStatus('analyzing');
33
+
34
+ const formData = new FormData();
35
+ formData.append('file', file);
36
+
37
+ try {
38
+ const res = await fetch(`${API_BASE}/analyze`, { method: 'POST', body: formData });
39
+ if (!res.ok) {
40
+ const e = await res.json().catch(() => ({}));
41
+ throw new Error(e.detail || `Server error ${res.status}`);
42
+ }
43
+ const data = await res.json();
44
+ setResultData(data);
45
+ setStatus('result');
46
+ } catch (err) {
47
+ setErrorMsg(err.message || 'Connection to analysis engine failed.');
48
+ setStatus('error');
49
+ }
50
+ };
51
+
52
+ const resetAll = () => {
53
+ setFile(null);
54
+ setResultData(null);
55
+ setErrorMsg('');
56
+ setStatus('idle');
57
+ };
58
+
59
+ const formatBytes = (bytes) => {
60
+ if (bytes < 1024) return bytes + ' B';
61
+ if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
62
+ return (bytes / 1048576).toFixed(1) + ' MB';
63
+ };
64
+
65
+ return (
66
+ <>
67
+ <div className="scanner"></div>
68
+
69
+ <div className="max-w-5xl mx-auto px-6 py-12 min-h-screen flex flex-col relative z-10">
70
+
71
+ {/* Header */}
72
+ <header className="text-center mb-12 fade-up" style={{ animationDelay: '0.1s' }}>
73
+ <div className="inline-flex items-center gap-2 px-4 py-1.5 rounded-full border border-[rgba(0,255,156,0.3)] bg-[rgba(0,255,156,0.05)] mb-6 shadow-[0_0_15px_rgba(0,255,156,0.1)]">
74
+ <div className="w-2 h-2 rounded-full bg-[#00ff9c] shadow-[0_0_8px_#00ff9c] animate-pulse"></div>
75
+ <span className="text-[10px] font-semibold tracking-[0.2em] text-[#00ff9c] uppercase">AI & Machine Learning</span>
76
+ </div>
77
+ <h1 className="text-4xl md:text-5xl font-bold tracking-tight text-white mb-2">Deepfake <span className="text-glow">Authenticator</span></h1>
78
+ <p className="text-[#849ca3] text-sm md:text-base font-light tracking-wide">Advanced video forensics and digital truth verification.</p>
79
+ </header>
80
+
81
+ {/* Main Content */}
82
+ <main className="flex-1 flex flex-col items-center gap-8 w-full">
83
+
84
+ {status === 'idle' && (
85
+ <section className="w-full max-w-3xl glass p-8 fade-up" style={{ animationDelay: '0.2s' }}>
86
+ <div
87
+ className="glass-inner rounded-xl border-2 border-dashed border-[rgba(0,255,156,0.15)] hover:border-[#00ff9c] transition-all cursor-pointer min-h-[200px] flex items-center justify-center relative overflow-hidden"
88
+ onDragOver={(e) => e.preventDefault()}
89
+ onDrop={handleDrop}
90
+ onClick={() => fileInputRef.current?.click()}
91
+ >
92
+ {!file ? (
93
+ <div className="text-center p-8">
94
+ <div className="w-16 h-16 mx-auto mb-4 rounded-full border border-[rgba(0,255,156,0.2)] flex items-center justify-center bg-[rgba(0,255,156,0.02)] shadow-[0_0_20px_rgba(0,255,156,0.05)]">
95
+ <svg className="w-8 h-8 text-[#00ff9c] opacity-70" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24">
96
+ <path strokeLinecap="round" strokeLinejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" />
97
+ </svg>
98
+ </div>
99
+ <h3 className="text-lg font-medium text-white mb-1">Upload Video for Analysis</h3>
100
+ <p className="text-sm text-[#849ca3]">Drag & drop or click to browse</p>
101
+ <p className="text-[11px] text-[#849ca3] mt-4 opacity-60">MP4, AVI, MOV, MKV, WebM β€” Max 100MB</p>
102
+ </div>
103
+ ) : (
104
+ <div className="w-full p-8 flex items-center gap-6" onClick={(e) => e.stopPropagation()}>
105
+ <div className="w-14 h-14 rounded-lg bg-[rgba(0,255,156,0.1)] border border-[rgba(0,255,156,0.3)] flex items-center justify-center flex-shrink-0 shadow-[0_0_15px_rgba(0,255,156,0.15)]">
106
+ <svg className="w-7 h-7 text-[#00ff9c]" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24">
107
+ <path strokeLinecap="round" strokeLinejoin="round" d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.348a1.125 1.125 0 010 1.971l-11.54 6.347a1.125 1.125 0 01-1.667-.985V5.653z" />
108
+ </svg>
109
+ </div>
110
+ <div className="flex-1 min-w-0">
111
+ <p className="text-sm font-medium text-white truncate">{file.name}</p>
112
+ <p className="text-xs text-[#849ca3] mt-1">{formatBytes(file.size)}</p>
113
+ </div>
114
+ <button onClick={() => setFile(null)} className="p-2 text-[#849ca3] hover:text-[#ff4444] transition-colors rounded-lg hover:bg-[rgba(255,68,68,0.1)]">
115
+ <svg className="w-5 h-5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
116
+ <path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
117
+ </svg>
118
+ </button>
119
+ </div>
120
+ )}
121
+ </div>
122
+ <input type="file" ref={fileInputRef} onChange={handleFileChange} accept="video/*" className="hidden" />
123
+
124
+ <div className="mt-8 text-center">
125
+ <button
126
+ onClick={handleAnalyze}
127
+ disabled={!file}
128
+ className={`border border-[rgba(0,255,156,0.15)] rounded-lg px-10 py-3 font-semibold text-sm tracking-widest uppercase transition-all ${
129
+ file
130
+ ? 'bg-[#00ff9c] text-[#040906] shadow-[0_0_20px_rgba(0,255,156,0.4)] hover:bg-[#00cc7d] hover:-translate-y-1'
131
+ : 'text-[#849ca3] opacity-50 cursor-not-allowed'
132
+ }`}
133
+ >
134
+ Analyze Video
135
+ </button>
136
+ </div>
137
+ </section>
138
+ )}
139
+
140
+ {status === 'analyzing' && (
141
+ <section className="fade-up w-full flex justify-center py-20">
142
+ <Loader />
143
+ </section>
144
+ )}
145
+
146
+ {status === 'result' && resultData && (
147
+ <section className="w-full flex flex-col gap-10 fade-up">
148
+
149
+ <div className="flex flex-col md:flex-row items-stretch justify-center gap-6 w-full max-w-3xl mx-auto">
150
+ <div className={`flex-1 glass p-8 text-center border-t-4 flex flex-col justify-center ${resultData.result === 'FAKE' ? 'border-t-[#ff4444] shadow-[0_-5px_20px_rgba(255,68,68,0.15)]' : 'border-t-[#00ff9c] shadow-[0_-5px_20px_rgba(0,255,156,0.15)]'}`}>
151
+ <p className="text-[#849ca3] text-xs font-semibold tracking-[0.2em] uppercase mb-2">Verdict</p>
152
+ <h2 className={`text-4xl font-bold tracking-widest ${resultData.result === 'FAKE' ? 'text-[#ff4444] drop-shadow-[0_0_10px_rgba(255,68,68,0.5)]' : 'text-[#00ff9c] drop-shadow-[0_0_10px_rgba(0,255,156,0.5)]'}`}>
153
+ {resultData.result}
154
+ </h2>
155
+ <div className="mt-4 inline-block bg-[rgba(255,255,255,0.05)] px-4 py-1.5 rounded-full border border-[rgba(255,255,255,0.1)]">
156
+ <span className="text-[#849ca3] text-xs uppercase tracking-wider mr-2">Confidence:</span>
157
+ <span className="text-white font-bold">{resultData.confidence}%</span>
158
+ </div>
159
+ </div>
160
+
161
+ <div className="flex-1 glass p-8 text-center border-t-4 border-t-[#00e5ff] shadow-[0_-5px_20px_rgba(0,229,255,0.15)] flex flex-col justify-center">
162
+ <p className="text-[#849ca3] text-xs font-semibold tracking-[0.2em] uppercase mb-2">Metrics</p>
163
+ <div className="flex flex-col gap-3 mt-2">
164
+ <div className="flex justify-between items-center bg-[rgba(255,255,255,0.03)] p-3 rounded border border-[rgba(255,255,255,0.05)]">
165
+ <span className="text-xs text-[#849ca3] uppercase tracking-wider">Frames</span>
166
+ <span className="text-white font-mono font-medium">{resultData.metadata?.frames_analyzed || 0}</span>
167
+ </div>
168
+ <div className="flex justify-between items-center bg-[rgba(255,255,255,0.03)] p-3 rounded border border-[rgba(255,255,255,0.05)]">
169
+ <span className="text-xs text-[#849ca3] uppercase tracking-wider">Time</span>
170
+ <span className="text-[#00e5ff] font-mono font-medium">{resultData.processing_time_sec || 0}s</span>
171
+ </div>
172
+ </div>
173
+ </div>
174
+ </div>
175
+
176
+ <div className="w-full max-w-3xl mx-auto glass p-8 mt-4">
177
+ <h3 className="text-xs font-semibold tracking-[0.15em] text-[#00e5ff] uppercase flex items-center gap-2 mb-4">
178
+ <div className="w-1 h-3 rounded-sm bg-[#00e5ff] shadow-[0_0_8px_#00e5ff]"></div>
179
+ Analysis Insights
180
+ </h3>
181
+ <div className="flex flex-col gap-3">
182
+ {(resultData.details || ['Analysis completed successfully.']).map((txt, i) => (
183
+ <div key={i} className="flex items-start gap-3 p-3 rounded-lg bg-[rgba(255,255,255,0.02)] border border-[rgba(255,255,255,0.05)]">
184
+ <span className={`flex-shrink-0 w-2 h-2 rounded-full mt-1.5 ${resultData.result === 'FAKE' ? 'bg-[#ff4444] shadow-[0_0_8px_rgba(255,68,68,0.6)]' : 'bg-[#00ff9c] shadow-[0_0_8px_rgba(0,255,156,0.6)]'}`}></span>
185
+ <span className="text-sm text-[#a0aec0]">{txt}</span>
186
+ </div>
187
+ ))}
188
+ </div>
189
+ </div>
190
+
191
+ <div className="text-center mt-4">
192
+ <button onClick={resetAll} className="px-6 py-3 text-xs font-semibold tracking-widest uppercase text-[#849ca3] border border-[#849ca3]/30 rounded-lg hover:text-[#00ff9c] hover:border-[#00ff9c]/50 hover:bg-[#00ff9c]/5 transition-all">
193
+ <span className="mr-2">↻</span> Analyze Another Video
194
+ </button>
195
+ </div>
196
+ </section>
197
+ )}
198
+
199
+ {status === 'error' && (
200
+ <section className="glass p-6 border-[#ff4444]/30 bg-[#ff4444]/5 fade-up w-full max-w-3xl">
201
+ <div className="flex items-center gap-3 mb-2">
202
+ <svg className="w-6 h-6 text-[#ff4444]" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>
203
+ <span className="text-sm font-bold tracking-widest text-[#ff4444] uppercase">Analysis Failed</span>
204
+ </div>
205
+ <p className="text-sm text-[#849ca3] ml-9">{errorMsg}</p>
206
+ <div className="ml-9 mt-4">
207
+ <button onClick={resetAll} className="text-xs text-white/50 hover:text-white uppercase tracking-wider transition-colors">
208
+ ↻ Try Again
209
+ </button>
210
+ </div>
211
+ </section>
212
+ )}
213
+
214
+ </main>
215
+
216
+ <footer className="mt-16 text-center text-[10px] text-[#849ca3]/50 tracking-[0.2em] uppercase">
217
+ Deepfake Authenticator β€’ Secure Cybernetic Analysis β€’ Local Inference
218
+ </footer>
219
+
220
+ </div>
221
+ </>
222
+ );
223
+ }
224
+
225
+ export default App;
frontend/src/assets/hero.png ADDED
frontend/src/assets/react.svg ADDED
frontend/src/assets/vite.svg ADDED
frontend/src/components/CyberCard.jsx ADDED
@@ -0,0 +1,364 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import styled from 'styled-components';
3
+
4
+ const CyberCard = ({ title, subtitle, highlight, hoverText, isFake }) => {
5
+ return (
6
+ <StyledWrapper $isFake={isFake}>
7
+ <div className="container noselect">
8
+ <div className="canvas">
9
+ {Array.from({ length: 25 }).map((_, i) => (
10
+ <div key={i} className={`tracker tr-${i + 1}`} />
11
+ ))}
12
+ <div id="card">
13
+ <div className="card-content">
14
+ <div className="card-glare" />
15
+ <div className="cyber-lines">
16
+ <span /><span /><span /><span />
17
+ </div>
18
+ <p id="prompt">{hoverText || "HOVER ME"}</p>
19
+ <div className="title" dangerouslySetInnerHTML={{ __html: title || "CYBER<br />CARD" }} />
20
+ <div className="glowing-elements">
21
+ <div className="glow-1" />
22
+ <div className="glow-2" />
23
+ <div className="glow-3" />
24
+ </div>
25
+ <div className="subtitle">
26
+ <span>{subtitle || "INTERACTIVE"}</span>
27
+ <span className="highlight">{highlight || "3D EFFECT"}</span>
28
+ </div>
29
+ <div className="card-particles">
30
+ <span /><span /><span /> <span /><span /><span />
31
+ </div>
32
+ <div className="corner-elements">
33
+ <span /><span /><span /><span />
34
+ </div>
35
+ <div className="scan-line" />
36
+ </div>
37
+ </div>
38
+ </div>
39
+ </div>
40
+ </StyledWrapper>
41
+ );
42
+ }
43
+
44
+ const StyledWrapper = styled.div`
45
+ .container {
46
+ position: relative;
47
+ width: 260px;
48
+ height: 340px;
49
+ transition: 200ms;
50
+ margin: 0 auto;
51
+ }
52
+
53
+ .container:active {
54
+ width: 250px;
55
+ height: 330px;
56
+ }
57
+
58
+ #card {
59
+ position: absolute;
60
+ inset: 0;
61
+ z-index: 0;
62
+ display: flex;
63
+ justify-content: center;
64
+ align-items: center;
65
+ border-radius: 20px;
66
+ transition: 700ms;
67
+ background: linear-gradient(45deg, #1a1a1a, #262626);
68
+ border: 2px solid ${props => props.$isFake ? 'rgba(255, 68, 68, 0.4)' : 'rgba(0, 255, 170, 0.4)'};
69
+ overflow: hidden;
70
+ box-shadow:
71
+ 0 0 20px ${props => props.$isFake ? 'rgba(255, 68, 68, 0.3)' : 'rgba(0, 255, 170, 0.3)'},
72
+ inset 0 0 20px rgba(0, 0, 0, 0.2);
73
+ }
74
+
75
+ .card-content {
76
+ position: relative;
77
+ width: 100%;
78
+ height: 100%;
79
+ }
80
+
81
+ #prompt {
82
+ bottom: 120px;
83
+ left: 50%;
84
+ transform: translateX(-50%);
85
+ z-index: 20;
86
+ font-size: 16px;
87
+ font-weight: 600;
88
+ letter-spacing: 2px;
89
+ transition: 300ms ease-in-out;
90
+ position: absolute;
91
+ text-align: center;
92
+ color: rgba(255, 255, 255, 0.7);
93
+ text-shadow: 0 0 10px rgba(255, 255, 255, 0.3);
94
+ }
95
+
96
+ .title {
97
+ opacity: 0;
98
+ transition: 300ms ease-in-out;
99
+ position: absolute;
100
+ font-size: 42px;
101
+ font-weight: 800;
102
+ letter-spacing: 6px;
103
+ text-align: center;
104
+ width: 100%;
105
+ padding-top: 40px;
106
+ background: ${props => props.$isFake ? 'linear-gradient(45deg, #ff4444, #ff8888)' : 'linear-gradient(45deg, #00ffaa, #00a2ff)'};
107
+ -webkit-background-clip: text;
108
+ -webkit-text-fill-color: transparent;
109
+ filter: drop-shadow(0 0 15px ${props => props.$isFake ? 'rgba(255, 68, 68, 0.3)' : 'rgba(0, 255, 170, 0.3)'});
110
+ text-shadow:
111
+ 0 0 10px ${props => props.$isFake ? 'rgba(255, 68, 68, 0.5)' : 'rgba(92, 103, 255, 0.5)'},
112
+ 0 0 20px ${props => props.$isFake ? 'rgba(255, 68, 68, 0.3)' : 'rgba(92, 103, 255, 0.3)'};
113
+ }
114
+
115
+ .subtitle {
116
+ position: absolute;
117
+ bottom: 40px;
118
+ width: 100%;
119
+ text-align: center;
120
+ font-size: 14px;
121
+ letter-spacing: 2px;
122
+ transform: translateY(30px);
123
+ color: rgba(255, 255, 255, 0.6);
124
+ }
125
+
126
+ .highlight {
127
+ color: ${props => props.$isFake ? '#ff4444' : '#00ffaa'};
128
+ margin-left: 5px;
129
+ background: ${props => props.$isFake ? 'linear-gradient(90deg, #ff4444, #ff8888)' : 'linear-gradient(90deg, #5c67ff, #ad51ff)'};
130
+ -webkit-background-clip: text;
131
+ -webkit-text-fill-color: transparent;
132
+ font-weight: bold;
133
+ }
134
+
135
+ .glowing-elements {
136
+ position: absolute;
137
+ inset: 0;
138
+ pointer-events: none;
139
+ }
140
+
141
+ .glow-1,
142
+ .glow-2,
143
+ .glow-3 {
144
+ position: absolute;
145
+ width: 120px;
146
+ height: 120px;
147
+ border-radius: 50%;
148
+ background: radial-gradient(
149
+ circle at center,
150
+ ${props => props.$isFake ? 'rgba(255, 68, 68, 0.3)' : 'rgba(0, 255, 170, 0.3)'} 0%,
151
+ transparent 70%
152
+ );
153
+ filter: blur(15px);
154
+ opacity: 0;
155
+ transition: opacity 0.3s ease;
156
+ }
157
+
158
+ .glow-1 { top: -20px; left: -20px; }
159
+ .glow-2 { top: 50%; right: -30px; transform: translateY(-50%); }
160
+ .glow-3 { bottom: -20px; left: 30%; }
161
+
162
+ .card-particles span {
163
+ position: absolute;
164
+ width: 4px;
165
+ height: 4px;
166
+ background: ${props => props.$isFake ? '#ff4444' : '#00ffaa'};
167
+ border-radius: 50%;
168
+ opacity: 0;
169
+ transition: opacity 0.3s ease;
170
+ }
171
+
172
+ /* Hover effects */
173
+ .tracker:hover ~ #card .title {
174
+ opacity: 1;
175
+ transform: translateY(-10px);
176
+ }
177
+
178
+ .tracker:hover ~ #card .glowing-elements div {
179
+ opacity: 1;
180
+ }
181
+
182
+ .tracker:hover ~ #card .card-particles span {
183
+ animation: particleFloat 2s infinite;
184
+ }
185
+
186
+ @keyframes particleFloat {
187
+ 0% { transform: translate(0, 0); opacity: 0; }
188
+ 50% { opacity: 1; }
189
+ 100% { transform: translate(calc(var(--x, 0) * 30px), calc(var(--y, 0) * 30px)); opacity: 0; }
190
+ }
191
+
192
+ /* Particle positions */
193
+ .card-particles span:nth-child(1) { --x: 1; --y: -1; top: 40%; left: 20%; }
194
+ .card-particles span:nth-child(2) { --x: -1; --y: -1; top: 60%; right: 20%; }
195
+ .card-particles span:nth-child(3) { --x: 0.5; --y: 1; top: 20%; left: 40%; }
196
+ .card-particles span:nth-child(4) { --x: -0.5; --y: 1; top: 80%; right: 40%; }
197
+ .card-particles span:nth-child(5) { --x: 1; --y: 0.5; top: 30%; left: 60%; }
198
+ .card-particles span:nth-child(6) { --x: -1; --y: 0.5; top: 70%; right: 60%; }
199
+
200
+ #card::before {
201
+ content: "";
202
+ background: radial-gradient(
203
+ circle at center,
204
+ ${props => props.$isFake ? 'rgba(255, 68, 68, 0.1)' : 'rgba(0, 255, 170, 0.1)'} 0%,
205
+ ${props => props.$isFake ? 'rgba(255, 136, 136, 0.05)' : 'rgba(0, 162, 255, 0.05)'} 50%,
206
+ transparent 100%
207
+ );
208
+ filter: blur(20px);
209
+ opacity: 0;
210
+ width: 150%;
211
+ height: 150%;
212
+ position: absolute;
213
+ left: 50%;
214
+ top: 50%;
215
+ transform: translate(-50%, -50%);
216
+ transition: opacity 0.3s ease;
217
+ }
218
+
219
+ .tracker:hover ~ #card::before { opacity: 1; }
220
+
221
+ .tracker {
222
+ position: absolute;
223
+ z-index: 200;
224
+ width: 100%;
225
+ height: 100%;
226
+ }
227
+
228
+ .tracker:hover { cursor: pointer; }
229
+ .tracker:hover ~ #card #prompt { opacity: 0; }
230
+ .tracker:hover ~ #card { transition: 300ms; filter: brightness(1.1); }
231
+ .container:hover #card::before { transition: 200ms; content: ""; opacity: 80%; }
232
+
233
+ .canvas {
234
+ perspective: 800px;
235
+ inset: 0;
236
+ z-index: 200;
237
+ position: absolute;
238
+ display: grid;
239
+ grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
240
+ grid-template-rows: 1fr 1fr 1fr 1fr 1fr;
241
+ gap: 0px 0px;
242
+ grid-template-areas:
243
+ "tr-1 tr-2 tr-3 tr-4 tr-5"
244
+ "tr-6 tr-7 tr-8 tr-9 tr-10"
245
+ "tr-11 tr-12 tr-13 tr-14 tr-15"
246
+ "tr-16 tr-17 tr-18 tr-19 tr-20"
247
+ "tr-21 tr-22 tr-23 tr-24 tr-25";
248
+ }
249
+
250
+ .tr-1 { grid-area: tr-1; } .tr-2 { grid-area: tr-2; } .tr-3 { grid-area: tr-3; } .tr-4 { grid-area: tr-4; } .tr-5 { grid-area: tr-5; }
251
+ .tr-6 { grid-area: tr-6; } .tr-7 { grid-area: tr-7; } .tr-8 { grid-area: tr-8; } .tr-9 { grid-area: tr-9; } .tr-10 { grid-area: tr-10; }
252
+ .tr-11 { grid-area: tr-11; } .tr-12 { grid-area: tr-12; } .tr-13 { grid-area: tr-13; } .tr-14 { grid-area: tr-14; } .tr-15 { grid-area: tr-15; }
253
+ .tr-16 { grid-area: tr-16; } .tr-17 { grid-area: tr-17; } .tr-18 { grid-area: tr-18; } .tr-19 { grid-area: tr-19; } .tr-20 { grid-area: tr-20; }
254
+ .tr-21 { grid-area: tr-21; } .tr-22 { grid-area: tr-22; } .tr-23 { grid-area: tr-23; } .tr-24 { grid-area: tr-24; } .tr-25 { grid-area: tr-25; }
255
+
256
+ .tr-1:hover ~ #card { transition: 125ms ease-in-out; transform: rotateX(20deg) rotateY(-10deg) rotateZ(0deg); }
257
+ .tr-2:hover ~ #card { transition: 125ms ease-in-out; transform: rotateX(20deg) rotateY(-5deg) rotateZ(0deg); }
258
+ .tr-3:hover ~ #card { transition: 125ms ease-in-out; transform: rotateX(20deg) rotateY(0deg) rotateZ(0deg); }
259
+ .tr-4:hover ~ #card { transition: 125ms ease-in-out; transform: rotateX(20deg) rotateY(5deg) rotateZ(0deg); }
260
+ .tr-5:hover ~ #card { transition: 125ms ease-in-out; transform: rotateX(20deg) rotateY(10deg) rotateZ(0deg); }
261
+ .tr-6:hover ~ #card { transition: 125ms ease-in-out; transform: rotateX(10deg) rotateY(-10deg) rotateZ(0deg); }
262
+ .tr-7:hover ~ #card { transition: 125ms ease-in-out; transform: rotateX(10deg) rotateY(-5deg) rotateZ(0deg); }
263
+ .tr-8:hover ~ #card { transition: 125ms ease-in-out; transform: rotateX(10deg) rotateY(0deg) rotateZ(0deg); }
264
+ .tr-9:hover ~ #card { transition: 125ms ease-in-out; transform: rotateX(10deg) rotateY(5deg) rotateZ(0deg); }
265
+ .tr-10:hover ~ #card { transition: 125ms ease-in-out; transform: rotateX(10deg) rotateY(10deg) rotateZ(0deg); }
266
+ .tr-11:hover ~ #card { transition: 125ms ease-in-out; transform: rotateX(0deg) rotateY(-10deg) rotateZ(0deg); }
267
+ .tr-12:hover ~ #card { transition: 125ms ease-in-out; transform: rotateX(0deg) rotateY(-5deg) rotateZ(0deg); }
268
+ .tr-13:hover ~ #card { transition: 125ms ease-in-out; transform: rotateX(0deg) rotateY(0deg) rotateZ(0deg); }
269
+ .tr-14:hover ~ #card { transition: 125ms ease-in-out; transform: rotateX(0deg) rotateY(5deg) rotateZ(0deg); }
270
+ .tr-15:hover ~ #card { transition: 125ms ease-in-out; transform: rotateX(0deg) rotateY(10deg) rotateZ(0deg); }
271
+ .tr-16:hover ~ #card { transition: 125ms ease-in-out; transform: rotateX(-10deg) rotateY(-10deg) rotateZ(0deg); }
272
+ .tr-17:hover ~ #card { transition: 125ms ease-in-out; transform: rotateX(-10deg) rotateY(-5deg) rotateZ(0deg); }
273
+ .tr-18:hover ~ #card { transition: 125ms ease-in-out; transform: rotateX(-10deg) rotateY(0deg) rotateZ(0deg); }
274
+ .tr-19:hover ~ #card { transition: 125ms ease-in-out; transform: rotateX(-10deg) rotateY(5deg) rotateZ(0deg); }
275
+ .tr-20:hover ~ #card { transition: 125ms ease-in-out; transform: rotateX(-10deg) rotateY(10deg) rotateZ(0deg); }
276
+ .tr-21:hover ~ #card { transition: 125ms ease-in-out; transform: rotateX(-20deg) rotateY(-10deg) rotateZ(0deg); }
277
+ .tr-22:hover ~ #card { transition: 125ms ease-in-out; transform: rotateX(-20deg) rotateY(-5deg) rotateZ(0deg); }
278
+ .tr-23:hover ~ #card { transition: 125ms ease-in-out; transform: rotateX(-20deg) rotateY(0deg) rotateZ(0deg); }
279
+ .tr-24:hover ~ #card { transition: 125ms ease-in-out; transform: rotateX(-20deg) rotateY(5deg) rotateZ(0deg); }
280
+ .tr-25:hover ~ #card { transition: 125ms ease-in-out; transform: rotateX(-20deg) rotateY(10deg) rotateZ(0deg); }
281
+
282
+ .noselect {
283
+ -webkit-touch-callout: none;
284
+ -webkit-user-select: none;
285
+ -moz-user-select: none;
286
+ -ms-user-select: none;
287
+ user-select: none;
288
+ }
289
+
290
+ .card-glare {
291
+ position: absolute;
292
+ inset: 0;
293
+ background: linear-gradient(
294
+ 125deg,
295
+ rgba(255, 255, 255, 0) 0%,
296
+ rgba(255, 255, 255, 0.05) 45%,
297
+ rgba(255, 255, 255, 0.1) 50%,
298
+ rgba(255, 255, 255, 0.05) 55%,
299
+ rgba(255, 255, 255, 0) 100%
300
+ );
301
+ opacity: 0;
302
+ transition: opacity 300ms;
303
+ }
304
+
305
+ .cyber-lines span {
306
+ position: absolute;
307
+ background: linear-gradient(
308
+ 90deg,
309
+ transparent,
310
+ ${props => props.$isFake ? 'rgba(255, 68, 68, 0.4)' : 'rgba(92, 103, 255, 0.2)'},
311
+ transparent
312
+ );
313
+ }
314
+
315
+ .cyber-lines span:nth-child(1) { top: 20%; left: 0; width: 100%; height: 1px; transform: scaleX(0); transform-origin: left; animation: lineGrow 3s linear infinite; }
316
+ .cyber-lines span:nth-child(2) { top: 40%; right: 0; width: 100%; height: 1px; transform: scaleX(0); transform-origin: right; animation: lineGrow 3s linear infinite 1s; }
317
+ .cyber-lines span:nth-child(3) { top: 60%; left: 0; width: 100%; height: 1px; transform: scaleX(0); transform-origin: left; animation: lineGrow 3s linear infinite 2s; }
318
+ .cyber-lines span:nth-child(4) { top: 80%; right: 0; width: 100%; height: 1px; transform: scaleX(0); transform-origin: right; animation: lineGrow 3s linear infinite 1.5s; }
319
+
320
+ .corner-elements span {
321
+ position: absolute;
322
+ width: 15px;
323
+ height: 15px;
324
+ border: 2px solid ${props => props.$isFake ? 'rgba(255, 68, 68, 0.5)' : 'rgba(92, 103, 255, 0.3)'};
325
+ transition: all 0.3s ease;
326
+ }
327
+
328
+ .corner-elements span:nth-child(1) { top: 10px; left: 10px; border-right: 0; border-bottom: 0; }
329
+ .corner-elements span:nth-child(2) { top: 10px; right: 10px; border-left: 0; border-bottom: 0; }
330
+ .corner-elements span:nth-child(3) { bottom: 10px; left: 10px; border-right: 0; border-top: 0; }
331
+ .corner-elements span:nth-child(4) { bottom: 10px; right: 10px; border-left: 0; border-top: 0; }
332
+
333
+ .scan-line {
334
+ position: absolute;
335
+ inset: 0;
336
+ background: linear-gradient(
337
+ to bottom,
338
+ transparent,
339
+ ${props => props.$isFake ? 'rgba(255, 68, 68, 0.2)' : 'rgba(92, 103, 255, 0.1)'},
340
+ transparent
341
+ );
342
+ transform: translateY(-100%);
343
+ animation: scanMove 2s linear infinite;
344
+ }
345
+
346
+ @keyframes lineGrow {
347
+ 0% { transform: scaleX(0); opacity: 0; }
348
+ 50% { transform: scaleX(1); opacity: 1; }
349
+ 100% { transform: scaleX(0); opacity: 0; }
350
+ }
351
+
352
+ @keyframes scanMove {
353
+ 0% { transform: translateY(-100%); }
354
+ 100% { transform: translateY(100%); }
355
+ }
356
+
357
+ #card:hover .card-glare { opacity: 1; }
358
+ #card:hover .corner-elements span {
359
+ border-color: ${props => props.$isFake ? 'rgba(255, 68, 68, 0.8)' : 'rgba(92, 103, 255, 0.8)'};
360
+ box-shadow: 0 0 10px ${props => props.$isFake ? 'rgba(255, 68, 68, 0.5)' : 'rgba(92, 103, 255, 0.5)'};
361
+ }
362
+ `;
363
+
364
+ export default CyberCard;
frontend/src/components/Loader.jsx ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import styled from 'styled-components';
3
+
4
+ const Loader = () => {
5
+ return (
6
+ <StyledWrapper>
7
+ <div className="liquid-loader">
8
+ <div className="loading-text">
9
+ Analyzing<span className="dot">.</span><span className="dot">.</span><span className="dot">.</span>
10
+ </div>
11
+ <div className="loader-track">
12
+ <div className="liquid-fill" />
13
+ </div>
14
+ </div>
15
+ </StyledWrapper>
16
+ );
17
+ }
18
+
19
+ const StyledWrapper = styled.div`
20
+ .liquid-loader {
21
+ display: flex;
22
+ flex-direction: column;
23
+ align-items: center;
24
+ gap: 15px;
25
+ padding: 20px;
26
+ font-family: system-ui, sans-serif;
27
+ }
28
+
29
+ .loader-track {
30
+ position: relative;
31
+ width: 250px;
32
+ height: 32px;
33
+ background: linear-gradient(135deg, #2a2a2a, #1a1a1a);
34
+ border-radius: 16px;
35
+ overflow: hidden;
36
+ box-shadow:
37
+ inset 0 2px 4px rgba(0, 0, 0, 0.6),
38
+ 0 1px 3px rgba(255, 255, 255, 0.1);
39
+ }
40
+
41
+ .liquid-fill {
42
+ position: absolute;
43
+ top: 2px;
44
+ left: 2px;
45
+ height: calc(100% - 4px);
46
+ background: linear-gradient(90deg, #00ffaa, #00a2ff, #5c67ff, #00ffaa);
47
+ border-radius: 14px;
48
+ animation:
49
+ fillProgress 8s ease-out forwards,
50
+ colorShift 3s linear infinite;
51
+ box-shadow:
52
+ 0 0 12px rgba(0, 255, 170, 0.4),
53
+ inset 0 1px 2px rgba(255, 255, 255, 0.2);
54
+ }
55
+
56
+ .loading-text {
57
+ color: white;
58
+ font-size: 18px;
59
+ font-weight: 600;
60
+ letter-spacing: 2px;
61
+ text-transform: uppercase;
62
+ animation: textGlow 1s ease-in-out infinite;
63
+ }
64
+
65
+ .dot {
66
+ margin-left: 3px;
67
+ animation: blink 1.5s infinite;
68
+ }
69
+ .dot:nth-of-type(1) { animation-delay: 0s; }
70
+ .dot:nth-of-type(2) { animation-delay: 0.3s; }
71
+ .dot:nth-of-type(3) { animation-delay: 0.6s; }
72
+
73
+ @keyframes fillProgress {
74
+ 0% { width: 4px; }
75
+ 25% { width: 35%; }
76
+ 50% { width: 65%; }
77
+ 75% { width: 85%; }
78
+ 100% { width: calc(100% - 4px); }
79
+ }
80
+
81
+ @keyframes colorShift {
82
+ 0% { filter: hue-rotate(0deg) brightness(1); }
83
+ 100% { filter: hue-rotate(360deg) brightness(1); }
84
+ }
85
+
86
+ @keyframes textGlow {
87
+ 0%, 100% { opacity: 0.7; text-shadow: 0 0 8px rgba(0, 255, 170, 0.3); }
88
+ 50% { opacity: 1; text-shadow: 0 0 16px rgba(0, 255, 170, 0.6); }
89
+ }
90
+
91
+ @keyframes blink {
92
+ 0%, 50% { opacity: 1; }
93
+ 51%, 100% { opacity: 0; }
94
+ }
95
+ `;
96
+
97
+ export default Loader;
frontend/src/index.css ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import "tailwindcss";
2
+
3
+ :root {
4
+ --primary: #00ff9c;
5
+ --primary-dim: #00cc7d;
6
+ --secondary: #00e5ff; /* soft cyan */
7
+ --red: #ff4444;
8
+ --red-dim: #cc2222;
9
+ --bg-dark: #040906;
10
+ --bg-gradient: radial-gradient(circle at top center, #001a11 0%, #040906 100%);
11
+ --card-bg: rgba(10, 20, 15, 0.6);
12
+ --card-bg-inner: rgba(15, 25, 20, 0.8);
13
+ --border: rgba(0, 255, 156, 0.15);
14
+ --border-hover: rgba(0, 255, 156, 0.4);
15
+ --text: #e2e8f0;
16
+ --muted: #849ca3;
17
+ }
18
+
19
+ body {
20
+ background: var(--bg-dark);
21
+ background-image: var(--bg-gradient);
22
+ font-family: 'Inter', sans-serif;
23
+ color: var(--text);
24
+ min-height: 100vh;
25
+ overflow-x: hidden;
26
+ letter-spacing: 0.02em;
27
+ }
28
+
29
+ /* ── Scrollbar ── */
30
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
31
+ ::-webkit-scrollbar-track { background: var(--bg-dark); }
32
+ ::-webkit-scrollbar-thumb { background: rgba(0, 255, 156, 0.3); border-radius: 6px; }
33
+
34
+ /* ── Scanner Background ── */
35
+ .scanner {
36
+ position: fixed; inset: 0; pointer-events: none; z-index: 100;
37
+ background: linear-gradient(to bottom,
38
+ transparent 0%, transparent 49.9%,
39
+ rgba(0, 255, 156, 0.05) 50%, transparent 50.1%);
40
+ background-size: 100% 4px;
41
+ animation: scanMove 8s linear infinite;
42
+ }
43
+ @keyframes scanMove {
44
+ 0% { background-position: 0 0; }
45
+ 100% { background-position: 0 100vh; }
46
+ }
47
+
48
+ .text-glow {
49
+ color: var(--primary);
50
+ text-shadow: 0 0 10px rgba(0, 255, 156, 0.6);
51
+ }
52
+
53
+ .fade-up { animation: fadeUp 0.6s cubic-bezier(0.16, 1, 0.3, 1) both; }
54
+ @keyframes fadeUp {
55
+ from { opacity: 0; transform: translateY(20px); }
56
+ to { opacity: 1; transform: none; }
57
+ }
frontend/src/main.jsx ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react'
2
+ import ReactDOM from 'react-dom/client'
3
+ import App from './App.jsx'
4
+ import './index.css'
5
+
6
+ ReactDOM.createRoot(document.getElementById('root')).render(
7
+ <React.StrictMode>
8
+ <App />
9
+ </React.StrictMode>,
10
+ )
frontend/vite.config.js ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+ import tailwindcss from '@tailwindcss/vite'
4
+
5
+ // https://vite.dev/config/
6
+ export default defineConfig({
7
+ plugins: [react(), tailwindcss()],
8
+ })