feat: audio deepfake detection (AI voice analysis)
Browse filesBackend:
- 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 +423 -0
- backend/detector.py +67 -21
- backend/main.py +1 -1
- backend/requirements.txt +8 -1
- frontend-vanilla/index.html +377 -0
- frontend-vanilla/script.js +354 -0
- frontend/.gitignore +24 -0
- frontend/README.md +16 -0
- frontend/eslint.config.js +21 -0
- frontend/index.html +36 -0
- frontend/package-lock.json +0 -0
- frontend/package.json +32 -0
- frontend/public/favicon.svg +1 -0
- frontend/public/icons.svg +24 -0
- frontend/script.js +79 -0
- frontend/src/App.css +184 -0
- frontend/src/App.jsx +225 -0
- frontend/src/assets/hero.png +0 -0
- frontend/src/assets/react.svg +1 -0
- frontend/src/assets/vite.svg +1 -0
- frontend/src/components/CyberCard.jsx +364 -0
- frontend/src/components/Loader.jsx +97 -0
- frontend/src/index.css +57 -0
- frontend/src/main.jsx +10 -0
- frontend/vite.config.js +8 -0
|
@@ -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)
|
|
@@ -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
|
| 443 |
elif consistency >= 0.55:
|
| 444 |
-
threshold -= 0.03
|
| 445 |
elif consistency < 0.35:
|
| 446 |
-
threshold += 0.07
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|
| 455 |
-
f"
|
| 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
|
| 562 |
-
self.face_agent
|
| 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
|
| 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
|
| 584 |
face_crops_per_frame = [
|
| 585 |
self.face_agent.detect_and_crop_faces(frame) for frame in frames
|
| 586 |
]
|
| 587 |
|
| 588 |
-
# Step 3:
|
| 589 |
analysis = self.decision_agent.analyze_frames(frames, face_crops_per_frame)
|
| 590 |
|
| 591 |
-
# Step 4:
|
| 592 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
@@ -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("/")
|
|
@@ -6,6 +6,13 @@ mediapipe==0.10.14
|
|
| 6 |
numpy==1.26.4
|
| 7 |
Pillow==10.3.0
|
| 8 |
|
| 9 |
-
# HuggingFace
|
| 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
|
|
@@ -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>< 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 & 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 |
+
β» 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 Β· Secure Local Inference Β· 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>
|
|
@@ -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, '&')
|
| 345 |
+
.replace(/</g, '<')
|
| 346 |
+
.replace(/>/g, '>');
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
function showError(msg) {
|
| 350 |
+
hide('uploadSection');
|
| 351 |
+
hide('loadingSection');
|
| 352 |
+
document.getElementById('errorMsg').textContent = msg;
|
| 353 |
+
show('errorSection');
|
| 354 |
+
}
|
|
@@ -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?
|
|
@@ -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.
|
|
@@ -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 |
+
])
|
|
@@ -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'">
|
|
The diff for this file is too large to render.
See raw diff
|
|
|
|
@@ -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 |
+
}
|
|
|
|
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -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;
|
|
|
|
|
|
|
@@ -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;
|
|
@@ -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;
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
)
|
|
@@ -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 |
+
})
|