Deepfake Authenticator commited on
Commit
0f27de7
Β·
1 Parent(s): bd97ad3

feat: Chrome/Edge browser extension

Browse files

- Manifest V3 extension with context menu, toolbar popup, content script
- Right-click any video β†’ Analyze with Authrix
- Toolbar popup: auto-detects page videos, manual URL input, last result
- Content script overlay: radar loading animation, verdict display,
confidence bar, insight cards, audio result, re-analyze button
- Background service worker: fetches video URL, sends to localhost:8000
- AV_MISMATCH shown as DEEPFAKE in overlay
- Error state with server offline detection and instructions
- Icons generated (16/48/128px)

extension/README.md ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Authrix Browser Extension
2
+
3
+ Detect deepfake videos on any webpage without downloading them.
4
+
5
+ ## Install (Chrome / Edge / Brave)
6
+
7
+ 1. Open `chrome://extensions`
8
+ 2. Enable **Developer Mode** (top right toggle)
9
+ 3. Click **Load unpacked**
10
+ 4. Select this `extension/` folder
11
+ 5. The Authrix icon appears in your toolbar
12
+
13
+ ## Requirements
14
+
15
+ The Authrix backend must be running locally:
16
+ ```bash
17
+ cd backend
18
+ python -m uvicorn main:app --host 0.0.0.0 --port 8000
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ ### Method 1 β€” Right-click any video
24
+ Right-click on a video element on any webpage β†’ **"πŸ” Analyze with Authrix"**
25
+
26
+ ### Method 2 β€” Toolbar popup
27
+ Click the Authrix icon in the toolbar:
28
+ - Auto-detects videos on the current page
29
+ - Paste any direct video URL manually
30
+ - Shows last analysis result
31
+
32
+ ### Method 3 β€” Right-click a link
33
+ Right-click any link to a video file β†’ **"πŸ” Analyze video URL with Authrix"**
34
+
35
+ ## How it works
36
+
37
+ 1. Extension captures the video URL from the page
38
+ 2. Background service worker fetches the video and sends it to `localhost:8000/analyze`
39
+ 3. FastAPI backend runs the full 5-agent pipeline (visual + audio)
40
+ 4. Result overlay appears on the page with verdict, confidence, and insights
41
+
42
+ ## Limitations
43
+
44
+ - **DRM-protected videos** (Netflix, Disney+) cannot be analyzed
45
+ - **YouTube** β€” the extension detects the page URL but YouTube's video streams require yt-dlp; direct URL analysis works for non-DRM videos
46
+ - Backend must be running on `localhost:8000`
47
+ - Large videos (>100MB) may time out
extension/background.js ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Authrix Extension β€” Background Service Worker
3
+ * Handles context menu, coordinates analysis requests
4
+ */
5
+
6
+ const API_BASE = 'http://localhost:8000';
7
+
8
+ // ── Create context menu on install ────────────────────────────────────────────
9
+ chrome.runtime.onInstalled.addListener(() => {
10
+ chrome.contextMenus.create({
11
+ id: 'authrix-analyze',
12
+ title: 'πŸ” Analyze with Authrix',
13
+ contexts: ['video', 'link', 'page'],
14
+ });
15
+ chrome.contextMenus.create({
16
+ id: 'authrix-analyze-url',
17
+ title: 'πŸ” Analyze video URL with Authrix',
18
+ contexts: ['link'],
19
+ });
20
+ console.log('[Authrix] Extension installed, context menu created');
21
+ });
22
+
23
+ // ── Context menu click ────────────────────────────────────────────────────────
24
+ chrome.contextMenus.onClicked.addListener(async (info, tab) => {
25
+ if (!tab?.id) return;
26
+
27
+ if (info.menuItemId === 'authrix-analyze') {
28
+ // Inject content script to find and analyze the video on the page
29
+ chrome.scripting.executeScript({
30
+ target: { tabId: tab.id },
31
+ func: triggerPageAnalysis,
32
+ args: [info.srcUrl || info.pageUrl || null],
33
+ });
34
+ }
35
+
36
+ if (info.menuItemId === 'authrix-analyze-url') {
37
+ const url = info.linkUrl || info.srcUrl;
38
+ if (url) {
39
+ chrome.scripting.executeScript({
40
+ target: { tabId: tab.id },
41
+ func: showAuthrixOverlay,
42
+ args: [url],
43
+ });
44
+ }
45
+ }
46
+ });
47
+
48
+ // ── Message handler (from content script / popup) ─────────────────────────────
49
+ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
50
+ if (msg.type === 'ANALYZE_URL') {
51
+ analyzeVideoUrl(msg.url)
52
+ .then(result => sendResponse({ ok: true, result }))
53
+ .catch(err => sendResponse({ ok: false, error: err.message }));
54
+ return true; // keep channel open for async
55
+ }
56
+
57
+ if (msg.type === 'CHECK_HEALTH') {
58
+ fetch(`${API_BASE}/health`)
59
+ .then(r => r.json())
60
+ .then(d => sendResponse({ ok: true, data: d }))
61
+ .catch(() => sendResponse({ ok: false }));
62
+ return true;
63
+ }
64
+
65
+ if (msg.type === 'ANALYZE_FILE_BYTES') {
66
+ // Receive base64 video bytes from content script, send to backend
67
+ analyzeBase64Video(msg.data, msg.filename, msg.mimeType)
68
+ .then(result => sendResponse({ ok: true, result }))
69
+ .catch(err => sendResponse({ ok: false, error: err.message }));
70
+ return true;
71
+ }
72
+ });
73
+
74
+ // ── Analyze a video by URL ────────────────────────────────────────────────────
75
+ async function analyzeVideoUrl(videoUrl) {
76
+ // First fetch the video as a blob
77
+ const response = await fetch(videoUrl);
78
+ if (!response.ok) throw new Error(`Failed to fetch video: ${response.status}`);
79
+
80
+ const blob = await response.blob();
81
+ const filename = videoUrl.split('/').pop().split('?')[0] || 'video.mp4';
82
+
83
+ return sendToBackend(blob, filename);
84
+ }
85
+
86
+ // ── Analyze base64 video data ─────────────────────────────────────────────────
87
+ async function analyzeBase64Video(base64Data, filename, mimeType) {
88
+ const byteString = atob(base64Data);
89
+ const bytes = new Uint8Array(byteString.length);
90
+ for (let i = 0; i < byteString.length; i++) {
91
+ bytes[i] = byteString.charCodeAt(i);
92
+ }
93
+ const blob = new Blob([bytes], { type: mimeType || 'video/mp4' });
94
+ return sendToBackend(blob, filename || 'video.mp4');
95
+ }
96
+
97
+ // ── Send blob to FastAPI backend ──────────────────────────────────────────────
98
+ async function sendToBackend(blob, filename) {
99
+ const fd = new FormData();
100
+ fd.append('file', blob, filename);
101
+
102
+ const res = await fetch(`${API_BASE}/analyze`, {
103
+ method: 'POST',
104
+ body: fd,
105
+ });
106
+
107
+ if (!res.ok) {
108
+ const err = await res.json().catch(() => ({}));
109
+ throw new Error(err.detail || `Backend error ${res.status}`);
110
+ }
111
+
112
+ return res.json();
113
+ }
114
+
115
+ // ── Injected functions (run in page context) ──────────────────────────────────
116
+ function triggerPageAnalysis(srcUrl) {
117
+ // This runs in the page β€” calls showAuthrixOverlay which is defined in content.js
118
+ if (typeof window.__authrixAnalyze === 'function') {
119
+ window.__authrixAnalyze(srcUrl);
120
+ } else {
121
+ console.warn('[Authrix] Content script not ready');
122
+ }
123
+ }
124
+
125
+ function showAuthrixOverlay(url) {
126
+ if (typeof window.__authrixAnalyze === 'function') {
127
+ window.__authrixAnalyze(url);
128
+ }
129
+ }
extension/content.js ADDED
@@ -0,0 +1,276 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Authrix Extension β€” Content Script
3
+ * Injected into every page. Handles overlay UI and video detection.
4
+ */
5
+
6
+ // ── Register global trigger ───────────────────────────────────────────────────
7
+ window.__authrixAnalyze = function(hintUrl) {
8
+ const url = hintUrl || findBestVideoUrl();
9
+ if (!url) {
10
+ showToast('⚠ No video found on this page', 'warn');
11
+ return;
12
+ }
13
+ showAnalysisOverlay(url);
14
+ };
15
+
16
+ // ── Find best video URL on the page ──────────────────────────────────────────
17
+ function findBestVideoUrl() {
18
+ // 1. <video> element with src
19
+ const videos = Array.from(document.querySelectorAll('video'));
20
+ for (const v of videos) {
21
+ if (v.src && v.src.startsWith('http')) return v.src;
22
+ const src = v.querySelector('source');
23
+ if (src?.src) return src.src;
24
+ }
25
+
26
+ // 2. YouTube β€” extract video ID and use yt-dlp-style URL
27
+ const ytMatch = location.href.match(/[?&]v=([^&]+)/);
28
+ if (ytMatch) return `https://www.youtube.com/watch?v=${ytMatch[1]}`;
29
+
30
+ // 3. Twitter/X video
31
+ const twitterVideo = document.querySelector('video[src*="video.twimg.com"]');
32
+ if (twitterVideo?.src) return twitterVideo.src;
33
+
34
+ return null;
35
+ }
36
+
37
+ // ── Main overlay ──────────────────────────────────────────────────────────────
38
+ function showAnalysisOverlay(videoUrl) {
39
+ // Remove existing overlay
40
+ document.getElementById('authrix-overlay')?.remove();
41
+
42
+ const overlay = document.createElement('div');
43
+ overlay.id = 'authrix-overlay';
44
+ overlay.innerHTML = `
45
+ <div id="authrix-panel">
46
+ <div id="authrix-header">
47
+ <div id="authrix-logo">
48
+ <span id="authrix-logo-icon">β—‰</span>
49
+ <span>AUTHRIX AI</span>
50
+ </div>
51
+ <button id="authrix-close">βœ•</button>
52
+ </div>
53
+
54
+ <div id="authrix-url-bar">
55
+ <span id="authrix-url-text">${truncateUrl(videoUrl)}</span>
56
+ </div>
57
+
58
+ <!-- Loading state -->
59
+ <div id="authrix-loading">
60
+ <div id="authrix-radar">
61
+ <div class="authrix-ring r1"></div>
62
+ <div class="authrix-ring r2"></div>
63
+ <div class="authrix-ring r3"></div>
64
+ <div id="authrix-radar-dot"></div>
65
+ </div>
66
+ <div id="authrix-loading-text">Initializing analysis...</div>
67
+ <div id="authrix-steps">
68
+ <div class="authrix-step" id="as0">🎬 Extracting frames</div>
69
+ <div class="authrix-step" id="as1">πŸ‘€ Detecting faces</div>
70
+ <div class="authrix-step" id="as2">🧠 Running AI models</div>
71
+ <div class="authrix-step" id="as3">πŸ“Š Generating report</div>
72
+ </div>
73
+ <div id="authrix-note">⚠ Large videos may take 15-30s</div>
74
+ </div>
75
+
76
+ <!-- Result state -->
77
+ <div id="authrix-result" style="display:none;">
78
+ <div id="authrix-verdict-row">
79
+ <div id="authrix-badge"></div>
80
+ <div id="authrix-verdict-text"></div>
81
+ <div id="authrix-conf"></div>
82
+ </div>
83
+ <div id="authrix-conf-bar-wrap">
84
+ <div id="authrix-conf-bar"></div>
85
+ </div>
86
+ <div id="authrix-details"></div>
87
+ <div id="authrix-audio-row" style="display:none;">
88
+ <span id="authrix-audio-icon">πŸŽ™οΈ</span>
89
+ <span id="authrix-audio-label"></span>
90
+ </div>
91
+ <div id="authrix-actions">
92
+ <button id="authrix-reanalyze">β†Ί Re-analyze</button>
93
+ <button id="authrix-open-app">Open Authrix App β†—</button>
94
+ </div>
95
+ </div>
96
+
97
+ <!-- Error state -->
98
+ <div id="authrix-error" style="display:none;">
99
+ <div id="authrix-error-icon">⚠</div>
100
+ <div id="authrix-error-msg"></div>
101
+ <div id="authrix-error-hint"></div>
102
+ <button id="authrix-retry">β†Ί Retry</button>
103
+ </div>
104
+ </div>
105
+ `;
106
+
107
+ document.body.appendChild(overlay);
108
+
109
+ // Wire close
110
+ document.getElementById('authrix-close').onclick = () => overlay.remove();
111
+ overlay.addEventListener('click', e => { if (e.target === overlay) overlay.remove(); });
112
+
113
+ // Start analysis
114
+ startAnalysis(videoUrl);
115
+ }
116
+
117
+ // ── Run analysis ──────────────────────────────────────────────────────────────
118
+ async function startAnalysis(videoUrl) {
119
+ showState('loading');
120
+ animateSteps();
121
+
122
+ try {
123
+ // Check if server is running first
124
+ const health = await fetch('http://localhost:8000/health').catch(() => null);
125
+ if (!health?.ok) {
126
+ throw new Error('SERVER_OFFLINE');
127
+ }
128
+
129
+ // Send URL to background for fetching + analysis
130
+ const response = await chrome.runtime.sendMessage({
131
+ type: 'ANALYZE_URL',
132
+ url: videoUrl,
133
+ });
134
+
135
+ stopStepAnimation();
136
+
137
+ if (!response.ok) throw new Error(response.error || 'Analysis failed');
138
+
139
+ renderResult(response.result, videoUrl);
140
+ } catch (err) {
141
+ stopStepAnimation();
142
+ showError(err.message, videoUrl);
143
+ }
144
+ }
145
+
146
+ // ── Render result ─────────────────────────────────────────────────────────────
147
+ function renderResult(data, videoUrl) {
148
+ const isFake = data.result === 'FAKE';
149
+ const conf = data.confidence || 0;
150
+ const color = isFake ? '#ff4466' : '#00ff9c';
151
+
152
+ // Badge
153
+ const badge = document.getElementById('authrix-badge');
154
+ badge.textContent = isFake ? '⚠' : 'βœ“';
155
+ badge.style.color = color;
156
+ badge.style.borderColor = color;
157
+ badge.style.boxShadow = `0 0 16px ${color}44`;
158
+
159
+ // Verdict
160
+ const vt = document.getElementById('authrix-verdict-text');
161
+ vt.textContent = isFake ? 'DEEPFAKE DETECTED' : 'AUTHENTIC VIDEO';
162
+ vt.style.color = color;
163
+
164
+ // Confidence
165
+ document.getElementById('authrix-conf').textContent = conf + '%';
166
+ document.getElementById('authrix-conf').style.color = color;
167
+
168
+ // Bar
169
+ const bar = document.getElementById('authrix-conf-bar');
170
+ bar.style.background = isFake
171
+ ? 'linear-gradient(90deg,#880022,#ff4466)'
172
+ : 'linear-gradient(90deg,#006633,#00ff9c)';
173
+ bar.style.boxShadow = `0 0 8px ${color}66`;
174
+ setTimeout(() => { bar.style.width = conf + '%'; }, 80);
175
+
176
+ // Details (first 3)
177
+ const dl = document.getElementById('authrix-details');
178
+ dl.innerHTML = (data.details || []).slice(0, 3).map(d =>
179
+ `<div class="authrix-detail" style="border-left-color:${color};">${escHtml(d)}</div>`
180
+ ).join('');
181
+
182
+ // Audio
183
+ const audioRow = document.getElementById('authrix-audio-row');
184
+ if (data.audio?.available) {
185
+ const isAI = data.audio.result === 'AI_VOICE';
186
+ const isMismatch = data.audio.result === 'AV_MISMATCH';
187
+ const aColor = (isAI || isMismatch) ? '#ff4466' : '#00ff9c';
188
+ document.getElementById('authrix-audio-icon').textContent =
189
+ (isAI || isMismatch) ? 'πŸ€–' : 'πŸŽ™οΈ';
190
+ document.getElementById('authrix-audio-label').textContent =
191
+ isMismatch ? 'AV Mismatch β€” face-swap detected' :
192
+ isAI ? `AI Voice (${data.audio.confidence}%)` :
193
+ `Human Voice (${data.audio.confidence}%)`;
194
+ document.getElementById('authrix-audio-label').style.color = aColor;
195
+ audioRow.style.display = 'flex';
196
+ }
197
+
198
+ // Actions
199
+ document.getElementById('authrix-reanalyze').onclick = () => startAnalysis(videoUrl);
200
+ document.getElementById('authrix-open-app').onclick = () => window.open('http://localhost:8000', '_blank');
201
+
202
+ showState('result');
203
+ }
204
+
205
+ // ── Show error ────────────────────────────────────────────────────────────────
206
+ function showError(message, videoUrl) {
207
+ const isOffline = message === 'SERVER_OFFLINE';
208
+ document.getElementById('authrix-error-msg').textContent =
209
+ isOffline ? 'Authrix server is not running' : message;
210
+ document.getElementById('authrix-error-hint').textContent =
211
+ isOffline
212
+ ? 'Start the server: cd backend && python -m uvicorn main:app --port 8000'
213
+ : 'The video may be DRM-protected or require authentication.';
214
+ document.getElementById('authrix-retry').onclick = () => startAnalysis(videoUrl);
215
+ showState('error');
216
+ }
217
+
218
+ // ── State management ──────────────────────────────────────────────────────────
219
+ function showState(state) {
220
+ document.getElementById('authrix-loading').style.display = state === 'loading' ? 'flex' : 'none';
221
+ document.getElementById('authrix-result').style.display = state === 'result' ? 'block' : 'none';
222
+ document.getElementById('authrix-error').style.display = state === 'error' ? 'flex' : 'none';
223
+ }
224
+
225
+ // ── Step animation ────────────────────────────────────────────────────────────
226
+ let _stepTimers = [];
227
+ const _stepDelays = [0, 2000, 5000, 9000];
228
+ const _stepMsgs = [
229
+ 'Downloading video frames...',
230
+ 'Detecting facial regions...',
231
+ 'Running ViT ensemble inference...',
232
+ 'Compiling authenticity report...',
233
+ ];
234
+
235
+ function animateSteps() {
236
+ _stepTimers = [];
237
+ _stepDelays.forEach((delay, i) => {
238
+ const t = setTimeout(() => {
239
+ document.querySelectorAll('.authrix-step').forEach((el, j) => {
240
+ el.classList.toggle('active', j === i);
241
+ el.classList.toggle('done', j < i);
242
+ });
243
+ document.getElementById('authrix-loading-text').textContent = _stepMsgs[i];
244
+ }, delay);
245
+ _stepTimers.push(t);
246
+ });
247
+ }
248
+
249
+ function stopStepAnimation() {
250
+ _stepTimers.forEach(clearTimeout);
251
+ _stepTimers = [];
252
+ }
253
+
254
+ // ── Toast notification ────────────────────────────────────────────────────────
255
+ function showToast(msg, type = 'info') {
256
+ const t = document.createElement('div');
257
+ t.className = 'authrix-toast authrix-toast-' + type;
258
+ t.textContent = msg;
259
+ document.body.appendChild(t);
260
+ setTimeout(() => t.remove(), 3500);
261
+ }
262
+
263
+ // ── Utilities ─────────────────────────────────────────────────────────────────
264
+ function truncateUrl(url) {
265
+ try {
266
+ const u = new URL(url);
267
+ const path = u.pathname.split('/').pop() || u.hostname;
268
+ return u.hostname + '/…/' + path.slice(0, 30);
269
+ } catch {
270
+ return url.slice(0, 50);
271
+ }
272
+ }
273
+
274
+ function escHtml(s) {
275
+ return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
276
+ }
extension/generate_icons.html ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <!-- Run this file in a browser to generate icons, then save the PNGs -->
3
+ <html><body>
4
+ <canvas id="c16" width="16" height="16"></canvas>
5
+ <canvas id="c48" width="48" height="48"></canvas>
6
+ <canvas id="c128" width="128" height="128"></canvas>
7
+ <script>
8
+ function drawIcon(canvas) {
9
+ const ctx = canvas.getContext('2d');
10
+ const s = canvas.width;
11
+ const cx = s/2, cy = s/2, r = s*0.42;
12
+
13
+ // Background
14
+ ctx.fillStyle = '#0c150f';
15
+ ctx.beginPath();
16
+ ctx.roundRect(0, 0, s, s, s*0.18);
17
+ ctx.fill();
18
+
19
+ // Outer ring
20
+ ctx.strokeStyle = 'rgba(0,255,156,0.4)';
21
+ ctx.lineWidth = s*0.04;
22
+ ctx.beginPath();
23
+ ctx.arc(cx, cy, r, 0, Math.PI*2);
24
+ ctx.stroke();
25
+
26
+ // Inner dot
27
+ ctx.fillStyle = '#00ff9c';
28
+ ctx.shadowColor = '#00ff9c';
29
+ ctx.shadowBlur = s*0.15;
30
+ ctx.beginPath();
31
+ ctx.arc(cx, cy, r*0.35, 0, Math.PI*2);
32
+ ctx.fill();
33
+
34
+ // Cross-hair lines
35
+ ctx.strokeStyle = 'rgba(0,255,156,0.6)';
36
+ ctx.lineWidth = s*0.03;
37
+ ctx.shadowBlur = 0;
38
+ ctx.beginPath();
39
+ ctx.moveTo(cx - r*0.7, cy); ctx.lineTo(cx - r*0.45, cy);
40
+ ctx.moveTo(cx + r*0.45, cy); ctx.lineTo(cx + r*0.7, cy);
41
+ ctx.moveTo(cx, cy - r*0.7); ctx.lineTo(cx, cy - r*0.45);
42
+ ctx.moveTo(cx, cy + r*0.45); ctx.lineTo(cx, cy + r*0.7);
43
+ ctx.stroke();
44
+ }
45
+
46
+ ['c16','c48','c128'].forEach(id => {
47
+ const c = document.getElementById(id);
48
+ drawIcon(c);
49
+ const link = document.createElement('a');
50
+ link.download = `icon${c.width}.png`;
51
+ link.href = c.toDataURL();
52
+ link.textContent = `Download icon${c.width}.png`;
53
+ link.style.display = 'block';
54
+ link.style.margin = '8px';
55
+ document.body.appendChild(link);
56
+ });
57
+ </script>
58
+ </body></html>
extension/manifest.json ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "manifest_version": 3,
3
+ "name": "Authrix β€” Deepfake Detector",
4
+ "version": "1.0.0",
5
+ "description": "Detect deepfake videos on any webpage using AI. Right-click any video or use the toolbar popup.",
6
+
7
+ "permissions": [
8
+ "contextMenus",
9
+ "activeTab",
10
+ "scripting",
11
+ "storage",
12
+ "notifications"
13
+ ],
14
+
15
+ "host_permissions": [
16
+ "http://localhost:8000/*",
17
+ "<all_urls>"
18
+ ],
19
+
20
+ "background": {
21
+ "service_worker": "background.js"
22
+ },
23
+
24
+ "content_scripts": [
25
+ {
26
+ "matches": ["<all_urls>"],
27
+ "js": ["content.js"],
28
+ "css": ["overlay.css"]
29
+ }
30
+ ],
31
+
32
+ "action": {
33
+ "default_popup": "popup.html",
34
+ "default_title": "Authrix Deepfake Detector",
35
+ "default_icon": {
36
+ "16": "icons/icon16.png",
37
+ "48": "icons/icon48.png",
38
+ "128": "icons/icon128.png"
39
+ }
40
+ },
41
+
42
+ "icons": {
43
+ "16": "icons/icon16.png",
44
+ "48": "icons/icon48.png",
45
+ "128": "icons/icon128.png"
46
+ }
47
+ }
extension/overlay.css ADDED
@@ -0,0 +1,347 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Authrix Extension β€” Overlay Styles */
2
+
3
+ #authrix-overlay {
4
+ position: fixed;
5
+ inset: 0;
6
+ z-index: 2147483647;
7
+ background: rgba(0, 0, 0, 0.65);
8
+ backdrop-filter: blur(4px);
9
+ display: flex;
10
+ align-items: center;
11
+ justify-content: center;
12
+ font-family: 'Segoe UI', system-ui, sans-serif;
13
+ animation: authrix-fade-in 0.2s ease;
14
+ }
15
+
16
+ @keyframes authrix-fade-in {
17
+ from { opacity: 0; }
18
+ to { opacity: 1; }
19
+ }
20
+
21
+ #authrix-panel {
22
+ background: #0c150f;
23
+ border: 1px solid rgba(0, 255, 156, 0.25);
24
+ border-radius: 14px;
25
+ width: 420px;
26
+ max-width: calc(100vw - 32px);
27
+ box-shadow: 0 0 60px rgba(0, 255, 156, 0.12), 0 24px 48px rgba(0,0,0,0.6);
28
+ overflow: hidden;
29
+ animation: authrix-slide-up 0.25s ease;
30
+ }
31
+
32
+ @keyframes authrix-slide-up {
33
+ from { opacity: 0; transform: translateY(16px); }
34
+ to { opacity: 1; transform: none; }
35
+ }
36
+
37
+ /* ── Header ── */
38
+ #authrix-header {
39
+ display: flex;
40
+ align-items: center;
41
+ justify-content: space-between;
42
+ padding: 14px 18px;
43
+ border-bottom: 1px solid rgba(0, 255, 156, 0.1);
44
+ background: rgba(0, 255, 156, 0.04);
45
+ }
46
+
47
+ #authrix-logo {
48
+ display: flex;
49
+ align-items: center;
50
+ gap: 8px;
51
+ font-size: 13px;
52
+ font-weight: 700;
53
+ letter-spacing: 0.12em;
54
+ color: #00ff9c;
55
+ text-shadow: 0 0 10px rgba(0,255,156,0.5);
56
+ }
57
+
58
+ #authrix-logo-icon {
59
+ font-size: 18px;
60
+ animation: authrix-pulse 2s ease-in-out infinite;
61
+ }
62
+
63
+ @keyframes authrix-pulse {
64
+ 0%,100% { opacity: 1; text-shadow: 0 0 8px #00ff9c; }
65
+ 50% { opacity: 0.5; text-shadow: none; }
66
+ }
67
+
68
+ #authrix-close {
69
+ background: none;
70
+ border: none;
71
+ color: rgba(255,255,255,0.3);
72
+ font-size: 16px;
73
+ cursor: pointer;
74
+ padding: 4px 8px;
75
+ border-radius: 4px;
76
+ transition: color 0.2s, background 0.2s;
77
+ line-height: 1;
78
+ }
79
+ #authrix-close:hover { color: #ff4466; background: rgba(255,68,102,0.1); }
80
+
81
+ /* ── URL bar ── */
82
+ #authrix-url-bar {
83
+ padding: 8px 18px;
84
+ background: rgba(255,255,255,0.03);
85
+ border-bottom: 1px solid rgba(255,255,255,0.05);
86
+ }
87
+ #authrix-url-text {
88
+ font-size: 10px;
89
+ color: rgba(255,255,255,0.3);
90
+ font-family: monospace;
91
+ letter-spacing: 0.03em;
92
+ }
93
+
94
+ /* ── Loading ── */
95
+ #authrix-loading {
96
+ display: flex;
97
+ flex-direction: column;
98
+ align-items: center;
99
+ padding: 28px 20px 24px;
100
+ gap: 16px;
101
+ }
102
+
103
+ #authrix-radar {
104
+ position: relative;
105
+ width: 80px;
106
+ height: 80px;
107
+ }
108
+
109
+ .authrix-ring {
110
+ position: absolute;
111
+ border-radius: 50%;
112
+ border: 1px solid rgba(0,255,156,0.2);
113
+ top: 50%; left: 50%;
114
+ transform: translate(-50%, -50%);
115
+ }
116
+ .r1 { width: 80px; height: 80px; }
117
+ .r2 { width: 56px; height: 56px; border-color: rgba(0,255,156,0.3); }
118
+ .r3 { width: 32px; height: 32px; border-color: rgba(0,255,156,0.5); }
119
+
120
+ #authrix-radar-dot {
121
+ position: absolute;
122
+ top: 50%; left: 50%;
123
+ transform: translate(-50%, -50%);
124
+ width: 10px; height: 10px;
125
+ border-radius: 50%;
126
+ background: #00ff9c;
127
+ box-shadow: 0 0 12px #00ff9c;
128
+ animation: authrix-radar-spin 2s linear infinite;
129
+ }
130
+
131
+ @keyframes authrix-radar-spin {
132
+ 0% { box-shadow: 0 0 12px #00ff9c; transform: translate(-50%,-50%) scale(1); }
133
+ 50% { box-shadow: 0 0 24px #00ff9c; transform: translate(-50%,-50%) scale(1.3); }
134
+ 100% { box-shadow: 0 0 12px #00ff9c; transform: translate(-50%,-50%) scale(1); }
135
+ }
136
+
137
+ #authrix-loading-text {
138
+ font-size: 12px;
139
+ color: #00ff9c;
140
+ font-weight: 600;
141
+ letter-spacing: 0.08em;
142
+ text-align: center;
143
+ }
144
+
145
+ #authrix-steps {
146
+ display: flex;
147
+ flex-direction: column;
148
+ gap: 6px;
149
+ width: 100%;
150
+ }
151
+
152
+ .authrix-step {
153
+ font-size: 11px;
154
+ color: rgba(255,255,255,0.2);
155
+ padding: 6px 12px;
156
+ border-radius: 6px;
157
+ border: 1px solid transparent;
158
+ transition: all 0.3s ease;
159
+ letter-spacing: 0.03em;
160
+ }
161
+ .authrix-step.active {
162
+ color: #00ff9c;
163
+ background: rgba(0,255,156,0.06);
164
+ border-color: rgba(0,255,156,0.2);
165
+ }
166
+ .authrix-step.done {
167
+ color: rgba(0,255,156,0.4);
168
+ }
169
+
170
+ #authrix-note {
171
+ font-size: 10px;
172
+ color: rgba(255,255,255,0.2);
173
+ text-align: center;
174
+ }
175
+
176
+ /* ── Result ── */
177
+ #authrix-result {
178
+ padding: 20px 18px;
179
+ }
180
+
181
+ #authrix-verdict-row {
182
+ display: flex;
183
+ align-items: center;
184
+ gap: 12px;
185
+ margin-bottom: 14px;
186
+ }
187
+
188
+ #authrix-badge {
189
+ width: 44px; height: 44px;
190
+ border-radius: 50%;
191
+ border: 2px solid;
192
+ display: flex; align-items: center; justify-content: center;
193
+ font-size: 20px;
194
+ flex-shrink: 0;
195
+ }
196
+
197
+ #authrix-verdict-text {
198
+ font-size: 18px;
199
+ font-weight: 700;
200
+ letter-spacing: 0.08em;
201
+ flex: 1;
202
+ }
203
+
204
+ #authrix-conf {
205
+ font-size: 22px;
206
+ font-weight: 700;
207
+ font-family: monospace;
208
+ }
209
+
210
+ #authrix-conf-bar-wrap {
211
+ height: 6px;
212
+ background: rgba(255,255,255,0.06);
213
+ border-radius: 99px;
214
+ overflow: hidden;
215
+ margin-bottom: 14px;
216
+ }
217
+
218
+ #authrix-conf-bar {
219
+ height: 100%;
220
+ border-radius: 99px;
221
+ width: 0%;
222
+ transition: width 1s cubic-bezier(0.22,1,0.36,1);
223
+ }
224
+
225
+ #authrix-details {
226
+ display: flex;
227
+ flex-direction: column;
228
+ gap: 6px;
229
+ margin-bottom: 12px;
230
+ }
231
+
232
+ .authrix-detail {
233
+ font-size: 11px;
234
+ color: rgba(255,255,255,0.55);
235
+ padding: 7px 10px;
236
+ background: rgba(255,255,255,0.03);
237
+ border-left: 2px solid;
238
+ border-radius: 0 6px 6px 0;
239
+ line-height: 1.5;
240
+ }
241
+
242
+ #authrix-audio-row {
243
+ display: flex;
244
+ align-items: center;
245
+ gap: 8px;
246
+ padding: 8px 10px;
247
+ background: rgba(255,255,255,0.03);
248
+ border-radius: 6px;
249
+ margin-bottom: 14px;
250
+ font-size: 11px;
251
+ border: 1px solid rgba(255,255,255,0.06);
252
+ }
253
+
254
+ #authrix-actions {
255
+ display: flex;
256
+ gap: 8px;
257
+ }
258
+
259
+ #authrix-actions button {
260
+ flex: 1;
261
+ padding: 9px;
262
+ border-radius: 7px;
263
+ font-size: 11px;
264
+ font-weight: 600;
265
+ letter-spacing: 0.06em;
266
+ cursor: pointer;
267
+ transition: all 0.2s;
268
+ font-family: inherit;
269
+ }
270
+
271
+ #authrix-reanalyze {
272
+ background: none;
273
+ border: 1px solid rgba(255,255,255,0.12);
274
+ color: rgba(255,255,255,0.5);
275
+ }
276
+ #authrix-reanalyze:hover {
277
+ border-color: rgba(0,255,156,0.4);
278
+ color: #00ff9c;
279
+ background: rgba(0,255,156,0.06);
280
+ }
281
+
282
+ #authrix-open-app {
283
+ background: rgba(0,255,156,0.1);
284
+ border: 1px solid rgba(0,255,156,0.3);
285
+ color: #00ff9c;
286
+ }
287
+ #authrix-open-app:hover {
288
+ background: rgba(0,255,156,0.18);
289
+ box-shadow: 0 0 16px rgba(0,255,156,0.2);
290
+ }
291
+
292
+ /* ── Error ── */
293
+ #authrix-error {
294
+ flex-direction: column;
295
+ align-items: center;
296
+ padding: 28px 20px 24px;
297
+ gap: 10px;
298
+ text-align: center;
299
+ }
300
+
301
+ #authrix-error-icon { font-size: 32px; }
302
+
303
+ #authrix-error-msg {
304
+ font-size: 13px;
305
+ font-weight: 600;
306
+ color: #ff4466;
307
+ }
308
+
309
+ #authrix-error-hint {
310
+ font-size: 11px;
311
+ color: rgba(255,255,255,0.35);
312
+ line-height: 1.5;
313
+ max-width: 300px;
314
+ font-family: monospace;
315
+ }
316
+
317
+ #authrix-retry {
318
+ background: rgba(255,68,102,0.1);
319
+ border: 1px solid rgba(255,68,102,0.3);
320
+ color: #ff4466;
321
+ padding: 8px 20px;
322
+ border-radius: 7px;
323
+ font-size: 11px;
324
+ font-weight: 600;
325
+ cursor: pointer;
326
+ transition: all 0.2s;
327
+ font-family: inherit;
328
+ margin-top: 4px;
329
+ }
330
+ #authrix-retry:hover { background: rgba(255,68,102,0.2); }
331
+
332
+ /* ── Toast ── */
333
+ .authrix-toast {
334
+ position: fixed;
335
+ bottom: 24px;
336
+ right: 24px;
337
+ z-index: 2147483647;
338
+ padding: 10px 16px;
339
+ border-radius: 8px;
340
+ font-size: 12px;
341
+ font-weight: 600;
342
+ animation: authrix-fade-in 0.2s ease;
343
+ pointer-events: none;
344
+ }
345
+ .authrix-toast-info { background: #0c150f; border: 1px solid rgba(0,255,156,0.3); color: #00ff9c; }
346
+ .authrix-toast-warn { background: #1a0f00; border: 1px solid rgba(255,170,0,0.3); color: #ffaa00; }
347
+ .authrix-toast-error { background: #1a0005; border: 1px solid rgba(255,68,102,0.3); color: #ff4466; }
extension/popup.html ADDED
@@ -0,0 +1,227 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>Authrix</title>
7
+ <style>
8
+ * { box-sizing: border-box; margin: 0; padding: 0; }
9
+ body {
10
+ width: 340px;
11
+ background: #0c150f;
12
+ color: #dae5da;
13
+ font-family: 'Segoe UI', system-ui, sans-serif;
14
+ font-size: 13px;
15
+ }
16
+
17
+ /* Header */
18
+ .header {
19
+ padding: 14px 16px 12px;
20
+ border-bottom: 1px solid rgba(0,255,156,0.12);
21
+ background: rgba(0,255,156,0.04);
22
+ display: flex; align-items: center; justify-content: space-between;
23
+ }
24
+ .logo {
25
+ display: flex; align-items: center; gap: 8px;
26
+ font-size: 14px; font-weight: 700; letter-spacing: 0.12em;
27
+ color: #00ff9c; text-shadow: 0 0 10px rgba(0,255,156,0.4);
28
+ }
29
+ .logo-dot {
30
+ width: 8px; height: 8px; border-radius: 50%;
31
+ background: #00ff9c; box-shadow: 0 0 8px #00ff9c;
32
+ animation: pulse 2s ease-in-out infinite;
33
+ }
34
+ @keyframes pulse {
35
+ 0%,100% { opacity:1; box-shadow: 0 0 8px #00ff9c; }
36
+ 50% { opacity:0.4; box-shadow: none; }
37
+ }
38
+ .status-badge {
39
+ font-size: 9px; font-weight: 700; letter-spacing: 0.1em;
40
+ padding: 3px 8px; border-radius: 99px;
41
+ border: 1px solid rgba(0,255,156,0.3);
42
+ color: #00ff9c; background: rgba(0,255,156,0.08);
43
+ }
44
+ .status-badge.offline {
45
+ border-color: rgba(255,68,102,0.3);
46
+ color: #ff4466; background: rgba(255,68,102,0.08);
47
+ }
48
+
49
+ /* Body */
50
+ .body { padding: 16px; display: flex; flex-direction: column; gap: 12px; }
51
+
52
+ /* Current page section */
53
+ .section-label {
54
+ font-size: 9px; font-weight: 700; letter-spacing: 0.15em;
55
+ color: rgba(255,255,255,0.3); text-transform: uppercase; margin-bottom: 8px;
56
+ }
57
+
58
+ .page-video-card {
59
+ background: rgba(255,255,255,0.03);
60
+ border: 1px solid rgba(255,255,255,0.07);
61
+ border-radius: 8px; padding: 12px;
62
+ display: flex; align-items: center; gap: 10px;
63
+ }
64
+ .page-video-icon { font-size: 20px; flex-shrink: 0; }
65
+ .page-video-info { flex: 1; min-width: 0; }
66
+ .page-video-title {
67
+ font-size: 11px; font-weight: 600; color: #dae5da;
68
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
69
+ }
70
+ .page-video-sub { font-size: 10px; color: rgba(255,255,255,0.3); margin-top: 2px; }
71
+
72
+ .btn-analyze-page {
73
+ width: 100%; padding: 11px;
74
+ background: linear-gradient(135deg, #00ff9c, #00cc6a);
75
+ color: #002110; border: none; border-radius: 8px;
76
+ font-size: 12px; font-weight: 700; letter-spacing: 0.1em;
77
+ cursor: pointer; transition: all 0.2s;
78
+ box-shadow: 0 0 20px rgba(0,255,156,0.25);
79
+ }
80
+ .btn-analyze-page:hover {
81
+ transform: translateY(-1px);
82
+ box-shadow: 0 0 30px rgba(0,255,156,0.4);
83
+ }
84
+ .btn-analyze-page:disabled {
85
+ background: rgba(255,255,255,0.08);
86
+ color: rgba(255,255,255,0.2);
87
+ cursor: not-allowed; transform: none;
88
+ box-shadow: none;
89
+ }
90
+
91
+ /* URL input */
92
+ .url-input-wrap {
93
+ display: flex; gap: 6px;
94
+ }
95
+ .url-input {
96
+ flex: 1; padding: 9px 12px;
97
+ background: rgba(255,255,255,0.05);
98
+ border: 1px solid rgba(255,255,255,0.1);
99
+ border-radius: 7px; color: #dae5da;
100
+ font-size: 11px; font-family: monospace;
101
+ outline: none; transition: border-color 0.2s;
102
+ }
103
+ .url-input:focus { border-color: rgba(0,255,156,0.4); }
104
+ .url-input::placeholder { color: rgba(255,255,255,0.2); }
105
+
106
+ .btn-go {
107
+ padding: 9px 14px;
108
+ background: rgba(0,255,156,0.1);
109
+ border: 1px solid rgba(0,255,156,0.3);
110
+ color: #00ff9c; border-radius: 7px;
111
+ font-size: 11px; font-weight: 700;
112
+ cursor: pointer; transition: all 0.2s; white-space: nowrap;
113
+ }
114
+ .btn-go:hover { background: rgba(0,255,156,0.18); }
115
+
116
+ /* Divider */
117
+ .divider {
118
+ display: flex; align-items: center; gap: 8px;
119
+ color: rgba(255,255,255,0.15); font-size: 10px;
120
+ }
121
+ .divider::before, .divider::after {
122
+ content: ''; flex: 1; height: 1px;
123
+ background: rgba(255,255,255,0.08);
124
+ }
125
+
126
+ /* Last result */
127
+ #last-result {
128
+ background: rgba(255,255,255,0.03);
129
+ border: 1px solid rgba(255,255,255,0.07);
130
+ border-radius: 8px; padding: 12px;
131
+ display: none;
132
+ }
133
+ .result-row {
134
+ display: flex; align-items: center; justify-content: space-between;
135
+ }
136
+ .result-verdict {
137
+ font-size: 14px; font-weight: 700; letter-spacing: 0.08em;
138
+ }
139
+ .result-conf { font-size: 18px; font-weight: 700; font-family: monospace; }
140
+ .result-bar-wrap {
141
+ height: 4px; background: rgba(255,255,255,0.06);
142
+ border-radius: 99px; overflow: hidden; margin: 8px 0;
143
+ }
144
+ .result-bar { height: 100%; border-radius: 99px; transition: width 0.8s ease; }
145
+ .result-file { font-size: 10px; color: rgba(255,255,255,0.25); font-family: monospace; }
146
+
147
+ /* Footer */
148
+ .footer {
149
+ padding: 10px 16px;
150
+ border-top: 1px solid rgba(255,255,255,0.05);
151
+ display: flex; align-items: center; justify-content: space-between;
152
+ }
153
+ .footer-link {
154
+ font-size: 10px; color: rgba(0,255,156,0.5);
155
+ text-decoration: none; letter-spacing: 0.08em;
156
+ transition: color 0.2s;
157
+ }
158
+ .footer-link:hover { color: #00ff9c; }
159
+ .model-info { font-size: 9px; color: rgba(255,255,255,0.2); letter-spacing: 0.06em; }
160
+ </style>
161
+ </head>
162
+ <body>
163
+
164
+ <!-- Header -->
165
+ <div class="header">
166
+ <div class="logo">
167
+ <div class="logo-dot"></div>
168
+ AUTHRIX AI
169
+ </div>
170
+ <div id="statusBadge" class="status-badge offline">OFFLINE</div>
171
+ </div>
172
+
173
+ <!-- Body -->
174
+ <div class="body">
175
+
176
+ <!-- Analyze current page -->
177
+ <div>
178
+ <div class="section-label">Current Page</div>
179
+ <div class="page-video-card">
180
+ <div class="page-video-icon">🎬</div>
181
+ <div class="page-video-info">
182
+ <div class="page-video-title" id="pageTitle">Detecting videos...</div>
183
+ <div class="page-video-sub" id="pageSub">Scanning page</div>
184
+ </div>
185
+ </div>
186
+ </div>
187
+
188
+ <button class="btn-analyze-page" id="btnAnalyzePage" disabled>
189
+ β–Ά &nbsp;Analyze Video on This Page
190
+ </button>
191
+
192
+ <div class="divider">OR</div>
193
+
194
+ <!-- Manual URL -->
195
+ <div>
196
+ <div class="section-label">Paste Video URL</div>
197
+ <div class="url-input-wrap">
198
+ <input class="url-input" id="urlInput" type="url"
199
+ placeholder="https://example.com/video.mp4"/>
200
+ <button class="btn-go" id="btnGo">Analyze</button>
201
+ </div>
202
+ </div>
203
+
204
+ <!-- Last result -->
205
+ <div id="last-result">
206
+ <div class="section-label" style="margin-bottom:8px;">Last Result</div>
207
+ <div class="result-row">
208
+ <div id="lastVerdict" class="result-verdict">β€”</div>
209
+ <div id="lastConf" class="result-conf">β€”</div>
210
+ </div>
211
+ <div class="result-bar-wrap">
212
+ <div id="lastBar" class="result-bar" style="width:0%"></div>
213
+ </div>
214
+ <div id="lastFile" class="result-file">β€”</div>
215
+ </div>
216
+
217
+ </div>
218
+
219
+ <!-- Footer -->
220
+ <div class="footer">
221
+ <a class="footer-link" href="http://localhost:8000" target="_blank">Open Full App β†—</a>
222
+ <div class="model-info" id="modelInfo">VIT ENSEMBLE</div>
223
+ </div>
224
+
225
+ <script src="popup.js"></script>
226
+ </body>
227
+ </html>
extension/popup.js ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Authrix Extension β€” Popup Script
3
+ */
4
+
5
+ const API_BASE = 'http://localhost:8000';
6
+
7
+ // ── Init ──────────────────────────────────────────────────────────────────────
8
+ document.addEventListener('DOMContentLoaded', async () => {
9
+ await checkHealth();
10
+ await detectPageVideo();
11
+ loadLastResult();
12
+ wireButtons();
13
+ });
14
+
15
+ // ── Health check ──────────────────────────────────────────────────────────────
16
+ async function checkHealth() {
17
+ const badge = document.getElementById('statusBadge');
18
+ const modelInfo = document.getElementById('modelInfo');
19
+ try {
20
+ const res = await fetch(`${API_BASE}/health`);
21
+ const d = await res.json();
22
+ if (d.ready) {
23
+ badge.textContent = 'ONLINE';
24
+ badge.classList.remove('offline');
25
+ modelInfo.textContent = (d.model || 'VIT ENSEMBLE').toUpperCase().slice(0, 22);
26
+ } else {
27
+ badge.textContent = 'LOADING';
28
+ }
29
+ } catch {
30
+ badge.textContent = 'OFFLINE';
31
+ badge.classList.add('offline');
32
+ }
33
+ }
34
+
35
+ // ── Detect video on current tab ───────────────────────────────────────────────
36
+ async function detectPageVideo() {
37
+ const btn = document.getElementById('btnAnalyzePage');
38
+ const title = document.getElementById('pageTitle');
39
+ const sub = document.getElementById('pageSub');
40
+
41
+ try {
42
+ const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
43
+ if (!tab?.id) return;
44
+
45
+ const results = await chrome.scripting.executeScript({
46
+ target: { tabId: tab.id },
47
+ func: detectVideosOnPage,
48
+ });
49
+
50
+ const found = results?.[0]?.result;
51
+ if (found?.url) {
52
+ title.textContent = found.title || 'Video detected';
53
+ sub.textContent = truncateUrl(found.url);
54
+ btn.disabled = false;
55
+ btn.dataset.url = found.url;
56
+ } else {
57
+ title.textContent = 'No video detected';
58
+ sub.textContent = 'Try right-clicking a video element';
59
+ btn.disabled = true;
60
+ }
61
+ } catch {
62
+ title.textContent = 'Cannot access this page';
63
+ sub.textContent = 'Extension pages are restricted';
64
+ }
65
+ }
66
+
67
+ // ── Runs in page context ──────────────────────────────────────────────────────
68
+ function detectVideosOnPage() {
69
+ // Check <video> elements
70
+ const videos = Array.from(document.querySelectorAll('video'));
71
+ for (const v of videos) {
72
+ const src = v.src || v.querySelector('source')?.src;
73
+ if (src && src.startsWith('http')) {
74
+ return { url: src, title: document.title };
75
+ }
76
+ }
77
+ // YouTube
78
+ const ytMatch = location.href.match(/[?&]v=([^&]+)/);
79
+ if (ytMatch) {
80
+ return { url: location.href, title: document.title };
81
+ }
82
+ return null;
83
+ }
84
+
85
+ // ── Wire buttons ──────────────────────────────────────────────────────────────
86
+ function wireButtons() {
87
+ // Analyze page video
88
+ document.getElementById('btnAnalyzePage').addEventListener('click', async () => {
89
+ const btn = document.getElementById('btnAnalyzePage');
90
+ const url = btn.dataset.url;
91
+ if (!url) return;
92
+ await injectAndAnalyze(url);
93
+ });
94
+
95
+ // Manual URL
96
+ document.getElementById('btnGo').addEventListener('click', async () => {
97
+ const url = document.getElementById('urlInput').value.trim();
98
+ if (!url) return;
99
+ await injectAndAnalyze(url);
100
+ });
101
+
102
+ document.getElementById('urlInput').addEventListener('keydown', async (e) => {
103
+ if (e.key === 'Enter') {
104
+ const url = e.target.value.trim();
105
+ if (url) await injectAndAnalyze(url);
106
+ }
107
+ });
108
+ }
109
+
110
+ // ── Inject overlay into active tab and trigger analysis ───────────────────────
111
+ async function injectAndAnalyze(url) {
112
+ try {
113
+ const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
114
+ if (!tab?.id) return;
115
+
116
+ await chrome.scripting.executeScript({
117
+ target: { tabId: tab.id },
118
+ func: (videoUrl) => {
119
+ if (typeof window.__authrixAnalyze === 'function') {
120
+ window.__authrixAnalyze(videoUrl);
121
+ }
122
+ },
123
+ args: [url],
124
+ });
125
+
126
+ // Close popup so user can see the overlay
127
+ window.close();
128
+ } catch (err) {
129
+ console.error('[Authrix popup]', err);
130
+ }
131
+ }
132
+
133
+ // ── Load last result from storage ────────────────────────────────────────────
134
+ function loadLastResult() {
135
+ chrome.storage.local.get('lastResult', ({ lastResult }) => {
136
+ if (!lastResult) return;
137
+ const el = document.getElementById('last-result');
138
+ el.style.display = 'block';
139
+
140
+ const isFake = lastResult.result === 'FAKE';
141
+ const color = isFake ? '#ff4466' : '#00ff9c';
142
+
143
+ document.getElementById('lastVerdict').textContent = lastResult.result;
144
+ document.getElementById('lastVerdict').style.color = color;
145
+ document.getElementById('lastConf').textContent = lastResult.confidence + '%';
146
+ document.getElementById('lastConf').style.color = color;
147
+ document.getElementById('lastFile').textContent = lastResult.file || '';
148
+
149
+ const bar = document.getElementById('lastBar');
150
+ bar.style.background = isFake
151
+ ? 'linear-gradient(90deg,#880022,#ff4466)'
152
+ : 'linear-gradient(90deg,#006633,#00ff9c)';
153
+ setTimeout(() => { bar.style.width = lastResult.confidence + '%'; }, 80);
154
+ });
155
+ }
156
+
157
+ // ── Utilities ─────────────────────────────────────────────────────────────────
158
+ function truncateUrl(url) {
159
+ try {
160
+ const u = new URL(url);
161
+ return u.hostname + u.pathname.slice(0, 25);
162
+ } catch {
163
+ return url.slice(0, 40);
164
+ }
165
+ }