Deepfake Authenticator commited on
Commit
4c0afe5
·
1 Parent(s): 20dd477

feat: extension v2 — tab capture instead of download

Browse files

- Use chrome.tabCapture API to record live tab stream (no download needed)
- Works on YouTube, Twitter, Netflix, any platform
- MediaRecorder captures 12s of video+audio as WebM
- Background service worker handles tabCapture.getMediaStreamId
- Content script records stream and sends base64 chunks to background
- Background sends blob to /analyze endpoint
- Popup simplified: one big 'Capture & Analyze' button
- Shows recording progress in overlay
- Removed yt-dlp dependency for extension (still available for /analyze-url)

extension/background.js CHANGED
@@ -1,70 +1,37 @@
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 => {
53
- chrome.storage.local.set({ lastResult: { ...result, file: msg.url.split('/').pop().slice(0,40) } });
54
- sendResponse({ ok: true, result });
55
- })
56
- .catch(err => sendResponse({ ok: false, error: err.message }));
57
- return true;
58
- }
59
 
60
- if (msg.type === 'ANALYZE_YOUTUBE') {
61
- // Send YouTube URL directly to backend — backend handles yt-dlp download
62
- analyzeYouTubeUrl(msg.url)
63
- .then(result => {
64
- chrome.storage.local.set({ lastResult: { ...result, file: 'YouTube video' } });
65
- sendResponse({ ok: true, result });
66
- })
67
- .catch(err => sendResponse({ ok: false, error: err.message }));
68
  return true;
69
  }
70
 
@@ -76,81 +43,76 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
76
  return true;
77
  }
78
 
79
- if (msg.type === 'ANALYZE_FILE_BYTES') {
80
- analyzeBase64Video(msg.data, msg.filename, msg.mimeType)
 
81
  .then(result => {
82
- chrome.storage.local.set({ lastResult: { ...result, file: msg.filename } });
 
 
 
 
83
  sendResponse({ ok: true, result });
84
  })
85
- .catch(err => sendResponse({ ok: false, error: err.message }));
 
 
 
 
 
86
  return true;
87
  }
 
 
 
 
 
88
  });
89
 
90
- // ── Analyze a video by URL ────────────────────────────────────────────────────
91
- async function analyzeVideoUrl(videoUrl) {
92
- const response = await fetch(videoUrl);
93
- if (!response.ok) throw new Error(`Failed to fetch video: ${response.status}`);
94
- const blob = await response.blob();
95
- const filename = videoUrl.split('/').pop().split('?')[0] || 'video.mp4';
96
- return sendToBackend(blob, filename);
97
- }
98
 
99
- // ── Analyze YouTube URL via backend ──────────────────────────────────────────
100
- async function analyzeYouTubeUrl(youtubeUrl) {
101
- const res = await fetch(`${API_BASE}/analyze-url`, {
102
- method: 'POST',
103
- headers: { 'Content-Type': 'application/json' },
104
- body: JSON.stringify({ url: youtubeUrl }),
 
 
 
 
105
  });
106
- if (!res.ok) {
107
- const err = await res.json().catch(() => ({}));
108
- throw new Error(err.detail || `Backend error ${res.status}`);
109
- }
110
- return res.json();
111
- }
112
 
113
- // ── Analyze base64 video data ─────────────────────────────────────────────────
114
- async function analyzeBase64Video(base64Data, filename, mimeType) {
115
- const byteString = atob(base64Data);
116
- const bytes = new Uint8Array(byteString.length);
117
- for (let i = 0; i < byteString.length; i++) {
118
- bytes[i] = byteString.charCodeAt(i);
119
- }
120
- const blob = new Blob([bytes], { type: mimeType || 'video/mp4' });
121
- return sendToBackend(blob, filename || 'video.mp4');
122
  }
123
 
124
- // ── Send blob to FastAPI backend ──────────────────────────────────────────────
125
- async function sendToBackend(blob, filename) {
 
 
 
 
 
 
 
 
 
 
 
126
  const fd = new FormData();
127
  fd.append('file', blob, filename);
128
 
129
- const res = await fetch(`${API_BASE}/analyze`, {
130
- method: 'POST',
131
- body: fd,
132
- });
133
-
134
  if (!res.ok) {
135
  const err = await res.json().catch(() => ({}));
136
  throw new Error(err.detail || `Backend error ${res.status}`);
137
  }
138
-
139
  return res.json();
140
  }
141
-
142
- // ── Injected functions (run in page context) ──────────────────────────────────
143
- function triggerPageAnalysis(srcUrl) {
144
- // This runs in the page — calls showAuthrixOverlay which is defined in content.js
145
- if (typeof window.__authrixAnalyze === 'function') {
146
- window.__authrixAnalyze(srcUrl);
147
- } else {
148
- console.warn('[Authrix] Content script not ready');
149
- }
150
- }
151
-
152
- function showAuthrixOverlay(url) {
153
- if (typeof window.__authrixAnalyze === 'function') {
154
- window.__authrixAnalyze(url);
155
- }
156
- }
 
1
  /**
2
+ * Authrix Extension — Background Service Worker v2
3
+ *
4
+ * Strategy: Use chrome.tabCapture to record the tab's live video stream
5
+ * for N seconds, then send the recorded blob to the local FastAPI backend.
6
+ * No video download needed — works on any platform.
7
  */
8
 
9
+ const API_BASE = 'http://localhost:8000';
10
+ const CAPTURE_SEC = 12; // seconds to record (enough for frame sampling)
11
 
12
+ // ── Context menu ──────────────────────────────────────────────────────────────
13
  chrome.runtime.onInstalled.addListener(() => {
14
  chrome.contextMenus.create({
15
+ id: 'authrix-capture',
16
+ title: '🔍 Analyze with Authrix (capture tab)',
17
+ contexts: ['page', 'video', 'frame'],
18
  });
 
 
 
 
 
 
19
  });
20
 
21
+ chrome.contextMenus.onClicked.addListener((info, tab) => {
22
+ if (info.menuItemId === 'authrix-capture' && tab?.id) {
23
+ startCapture(tab.id);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  }
25
  });
26
 
27
+ // ── Message handler ───────────────────────────────────────────────────────────
28
  chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
 
 
 
 
 
 
 
 
 
29
 
30
+ if (msg.type === 'START_CAPTURE') {
31
+ const tabId = sender.tab?.id || msg.tabId;
32
+ startCapture(tabId)
33
+ .then(() => sendResponse({ ok: true }))
34
+ .catch(e => sendResponse({ ok: false, error: e.message }));
 
 
 
35
  return true;
36
  }
37
 
 
43
  return true;
44
  }
45
 
46
+ if (msg.type === 'SEND_BLOB_CHUNKS') {
47
+ // Receive recorded video chunks from offscreen/content, send to backend
48
+ receiveAndAnalyze(msg.chunks, msg.mimeType, sender.tab?.id)
49
  .then(result => {
50
+ chrome.storage.local.set({ lastResult: { ...result, file: 'Tab capture' } });
51
+ // Send result back to the content script on that tab
52
+ if (sender.tab?.id) {
53
+ chrome.tabs.sendMessage(sender.tab.id, { type: 'ANALYSIS_RESULT', result });
54
+ }
55
  sendResponse({ ok: true, result });
56
  })
57
+ .catch(e => {
58
+ if (sender.tab?.id) {
59
+ chrome.tabs.sendMessage(sender.tab.id, { type: 'ANALYSIS_ERROR', error: e.message });
60
+ }
61
+ sendResponse({ ok: false, error: e.message });
62
+ });
63
  return true;
64
  }
65
+
66
+ if (msg.type === 'ANALYSIS_ERROR_FROM_CONTENT') {
67
+ // Bubble up errors from content script recording
68
+ console.error('[Authrix BG] Content error:', msg.error);
69
+ }
70
  });
71
 
72
+ // ── Start tab capture ─────────────────────────────────────────────────────────
73
+ async function startCapture(tabId) {
74
+ if (!tabId) throw new Error('No tab ID');
 
 
 
 
 
75
 
76
+ // Tell content script to show loading overlay
77
+ await chrome.tabs.sendMessage(tabId, { type: 'SHOW_CAPTURE_OVERLAY' });
78
+
79
+ // Get a MediaStream for the tab using tabCapture
80
+ // tabCapture.getMediaStreamId gives us a stream ID usable in content scripts
81
+ const streamId = await new Promise((resolve, reject) => {
82
+ chrome.tabCapture.getMediaStreamId({ targetTabId: tabId }, (id) => {
83
+ if (chrome.runtime.lastError) reject(new Error(chrome.runtime.lastError.message));
84
+ else resolve(id);
85
+ });
86
  });
 
 
 
 
 
 
87
 
88
+ // Send stream ID to content script — it will record and send back chunks
89
+ await chrome.tabs.sendMessage(tabId, {
90
+ type: 'RECORD_STREAM',
91
+ streamId: streamId,
92
+ durationMs: CAPTURE_SEC * 1000,
93
+ });
 
 
 
94
  }
95
 
96
+ // ── Receive chunks and analyze ────────────────────────────────────────────────
97
+ async function receiveAndAnalyze(base64Chunks, mimeType, tabId) {
98
+ // Reconstruct blob from base64 chunks
99
+ const byteArrays = base64Chunks.map(chunk => {
100
+ const binary = atob(chunk);
101
+ const bytes = new Uint8Array(binary.length);
102
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
103
+ return bytes;
104
+ });
105
+
106
+ const blob = new Blob(byteArrays, { type: mimeType || 'video/webm' });
107
+ const filename = `capture_${Date.now()}.webm`;
108
+
109
  const fd = new FormData();
110
  fd.append('file', blob, filename);
111
 
112
+ const res = await fetch(`${API_BASE}/analyze`, { method: 'POST', body: fd });
 
 
 
 
113
  if (!res.ok) {
114
  const err = await res.json().catch(() => ({}));
115
  throw new Error(err.detail || `Backend error ${res.status}`);
116
  }
 
117
  return res.json();
118
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
extension/content.js CHANGED
@@ -1,42 +1,113 @@
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');
@@ -51,11 +122,7 @@ function showAnalysisOverlay(videoUrl) {
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>
@@ -63,17 +130,17 @@ function showAnalysisOverlay(videoUrl) {
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>
@@ -89,12 +156,12 @@ function showAnalysisOverlay(videoUrl) {
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>
@@ -106,177 +173,126 @@ function showAnalysisOverlay(videoUrl) {
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) throw new Error('SERVER_OFFLINE');
126
-
127
- const isYouTube = videoUrl.includes('youtube.com') || videoUrl.includes('youtu.be');
128
-
129
- let response;
130
- if (isYouTube) {
131
- // For YouTube, send the URL to backend's /analyze-url endpoint
132
- response = await chrome.runtime.sendMessage({
133
- type: 'ANALYZE_YOUTUBE',
134
- url: videoUrl,
135
- });
136
- } else {
137
- response = await chrome.runtime.sendMessage({
138
- type: 'ANALYZE_URL',
139
- url: videoUrl,
140
- });
141
- }
142
 
143
- stopStepAnimation();
144
- if (!response.ok) throw new Error(response.error || 'Analysis failed');
145
- renderResult(response.result, videoUrl);
146
- } catch (err) {
147
- stopStepAnimation();
148
- showError(err.message, videoUrl);
149
- }
150
  }
151
 
152
- // ── Render result ─────────────────────────────────────────────────────────────
153
- function renderResult(data, videoUrl) {
 
154
  const isFake = data.result === 'FAKE';
155
  const conf = data.confidence || 0;
156
  const color = isFake ? '#ff4466' : '#00ff9c';
157
 
158
- // Badge
159
  const badge = document.getElementById('authrix-badge');
160
- badge.textContent = isFake ? '⚠' : '✓';
161
- badge.style.color = color;
162
- badge.style.borderColor = color;
163
- badge.style.boxShadow = `0 0 16px ${color}44`;
 
 
164
 
165
- // Verdict
166
  const vt = document.getElementById('authrix-verdict-text');
167
- vt.textContent = isFake ? 'DEEPFAKE DETECTED' : 'AUTHENTIC VIDEO';
168
- vt.style.color = color;
169
 
170
- // Confidence
171
- document.getElementById('authrix-conf').textContent = conf + '%';
172
- document.getElementById('authrix-conf').style.color = color;
173
 
174
- // Bar
175
  const bar = document.getElementById('authrix-conf-bar');
176
- bar.style.background = isFake
177
- ? 'linear-gradient(90deg,#880022,#ff4466)'
178
- : 'linear-gradient(90deg,#006633,#00ff9c)';
179
- bar.style.boxShadow = `0 0 8px ${color}66`;
180
- setTimeout(() => { bar.style.width = conf + '%'; }, 80);
 
 
181
 
182
- // Details (first 3)
183
  const dl = document.getElementById('authrix-details');
184
- dl.innerHTML = (data.details || []).slice(0, 3).map(d =>
185
- `<div class="authrix-detail" style="border-left-color:${color};">${escHtml(d)}</div>`
186
- ).join('');
 
 
187
 
188
- // Audio
189
  const audioRow = document.getElementById('authrix-audio-row');
190
- if (data.audio?.available) {
191
  const isAI = data.audio.result === 'AI_VOICE';
192
  const isMismatch = data.audio.result === 'AV_MISMATCH';
193
  const aColor = (isAI || isMismatch) ? '#ff4466' : '#00ff9c';
194
- document.getElementById('authrix-audio-icon').textContent =
195
- (isAI || isMismatch) ? '🤖' : '🎙️';
196
- document.getElementById('authrix-audio-label').textContent =
197
- isMismatch ? 'AV Mismatch — face-swap detected' :
198
- isAI ? `AI Voice (${data.audio.confidence}%)` :
199
- `Human Voice (${data.audio.confidence}%)`;
200
- document.getElementById('authrix-audio-label').style.color = aColor;
 
 
 
201
  audioRow.style.display = 'flex';
202
  }
203
 
204
- // Actions
205
- document.getElementById('authrix-reanalyze').onclick = () => startAnalysis(videoUrl);
206
- document.getElementById('authrix-open-app').onclick = () => window.open('http://localhost:8000', '_blank');
207
-
208
  showState('result');
209
  }
210
 
211
- // ── Show error ────────────────────────────────────────────────────────────────
212
- function showError(message, videoUrl) {
213
- const isOffline = message === 'SERVER_OFFLINE';
214
- document.getElementById('authrix-error-msg').textContent =
215
- isOffline ? 'Authrix server is not running' : message;
216
- document.getElementById('authrix-error-hint').textContent =
217
- isOffline
218
- ? 'Start the server: cd backend && python -m uvicorn main:app --port 8000'
219
- : 'The video may be DRM-protected or require authentication.';
220
- document.getElementById('authrix-retry').onclick = () => startAnalysis(videoUrl);
221
  showState('error');
222
  }
223
 
224
- // ── State management ──────────────────────────────────────────────────────────
225
- function showState(state) {
226
- document.getElementById('authrix-loading').style.display = state === 'loading' ? 'flex' : 'none';
227
- document.getElementById('authrix-result').style.display = state === 'result' ? 'block' : 'none';
228
- document.getElementById('authrix-error').style.display = state === 'error' ? 'flex' : 'none';
 
 
 
 
229
  }
230
 
231
- // ── Step animation ────────────────────────────────────────────────────────────
232
- let _stepTimers = [];
233
- const _stepDelays = [0, 2000, 5000, 9000];
234
- const _stepMsgs = [
235
- 'Downloading video frames...',
236
- 'Detecting facial regions...',
237
- 'Running ViT ensemble inference...',
238
- 'Compiling authenticity report...',
239
- ];
240
-
241
- function animateSteps() {
242
- _stepTimers = [];
243
- _stepDelays.forEach((delay, i) => {
244
- const t = setTimeout(() => {
245
- document.querySelectorAll('.authrix-step').forEach((el, j) => {
246
- el.classList.toggle('active', j === i);
247
- el.classList.toggle('done', j < i);
248
- });
249
- document.getElementById('authrix-loading-text').textContent = _stepMsgs[i];
250
- }, delay);
251
- _stepTimers.push(t);
252
  });
253
  }
254
 
255
- function stopStepAnimation() {
256
- _stepTimers.forEach(clearTimeout);
257
- _stepTimers = [];
258
- }
259
-
260
- // ── Toast notification ────────────────────────────────────────────────────────
261
- function showToast(msg, type = 'info') {
262
- const t = document.createElement('div');
263
- t.className = 'authrix-toast authrix-toast-' + type;
264
- t.textContent = msg;
265
- document.body.appendChild(t);
266
- setTimeout(() => t.remove(), 3500);
267
- }
268
-
269
- // ── Utilities ─────────────────────────────────────────────────────────────────
270
- function truncateUrl(url) {
271
- try {
272
- const u = new URL(url);
273
- const path = u.pathname.split('/').pop() || u.hostname;
274
- return u.hostname + '/…/' + path.slice(0, 30);
275
- } catch {
276
- return url.slice(0, 50);
277
- }
278
- }
279
-
280
  function escHtml(s) {
281
  return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
282
  }
 
1
  /**
2
+ * Authrix Extension — Content Script v2
3
+ *
4
+ * Handles:
5
+ * 1. Overlay UI (loading, result, error)
6
+ * 2. Tab stream recording via MediaRecorder (using streamId from background)
7
+ * 3. Sending recorded chunks back to background for analysis
8
  */
9
 
10
+ // ── Message listener from background ─────────────────────────────────────────
11
+ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
12
+ if (msg.type === 'SHOW_CAPTURE_OVERLAY') {
13
+ showOverlay();
14
+ sendResponse({ ok: true });
15
+ }
16
+
17
+ if (msg.type === 'RECORD_STREAM') {
18
+ recordStream(msg.streamId, msg.durationMs);
19
+ sendResponse({ ok: true });
20
+ }
21
+
22
+ if (msg.type === 'ANALYSIS_RESULT') {
23
+ renderResult(msg.result);
24
  }
 
 
25
 
26
+ if (msg.type === 'ANALYSIS_ERROR') {
27
+ showError(msg.error);
 
 
 
 
 
 
28
  }
29
+ });
30
 
31
+ // ── Global trigger (from background context menu) ─────────────────────────────
32
+ window.__authrixCapture = function() {
33
+ chrome.runtime.sendMessage({ type: 'START_CAPTURE' });
34
+ };
35
 
36
+ // ── Record stream ─────────────────────────────────────────────────────────────
37
+ async function recordStream(streamId, durationMs) {
38
+ try {
39
+ updateLoadingText('Connecting to video stream...');
40
+
41
+ // Get the MediaStream from the stream ID
42
+ const stream = await navigator.mediaDevices.getUserMedia({
43
+ video: { mandatory: { chromeMediaSource: 'tab', chromeMediaSourceId: streamId } },
44
+ audio: { mandatory: { chromeMediaSource: 'tab', chromeMediaSourceId: streamId } },
45
+ });
46
+
47
+ updateLoadingText('Recording video stream...');
48
+ activateStep(0);
49
+
50
+ // Pick best supported format
51
+ const mimeType = getSupportedMimeType();
52
+ const recorder = new MediaRecorder(stream, { mimeType, videoBitsPerSecond: 2_000_000 });
53
+ const chunks = [];
54
+
55
+ recorder.ondataavailable = e => { if (e.data.size > 0) chunks.push(e.data); };
56
+
57
+ recorder.onstop = async () => {
58
+ stream.getTracks().forEach(t => t.stop());
59
+ activateStep(1);
60
+ updateLoadingText('Processing captured frames...');
61
+
62
+ try {
63
+ // Convert chunks to base64 for message passing
64
+ const blob = new Blob(chunks, { type: mimeType });
65
+ const base64 = await blobToBase64(blob);
66
+
67
+ activateStep(2);
68
+ updateLoadingText('Running AI analysis...');
69
+
70
+ // Send to background for backend submission
71
+ chrome.runtime.sendMessage({
72
+ type: 'SEND_BLOB_CHUNKS',
73
+ chunks: [base64], // single base64 string of full blob
74
+ mimeType: mimeType,
75
+ });
76
+ } catch (err) {
77
+ showError('Failed to process recording: ' + err.message);
78
+ chrome.runtime.sendMessage({ type: 'ANALYSIS_ERROR_FROM_CONTENT', error: err.message });
79
+ }
80
+ };
81
+
82
+ recorder.onerror = e => {
83
+ showError('Recording error: ' + e.error?.message);
84
+ };
85
+
86
+ // Record for durationMs then stop
87
+ recorder.start(1000); // collect data every 1s
88
+ setTimeout(() => {
89
+ if (recorder.state === 'recording') recorder.stop();
90
+ }, durationMs);
91
+
92
+ // Update progress during recording
93
+ const startTime = Date.now();
94
+ const progressInterval = setInterval(() => {
95
+ if (recorder.state !== 'recording') { clearInterval(progressInterval); return; }
96
+ const elapsed = (Date.now() - startTime) / 1000;
97
+ const total = durationMs / 1000;
98
+ const pct = Math.min(99, Math.round((elapsed / total) * 60));
99
+ updateLoadingText(`Recording: ${elapsed.toFixed(0)}s / ${total}s (${pct}% captured)`);
100
+ }, 500);
101
 
102
+ } catch (err) {
103
+ showError(err.message.includes('Permission')
104
+ ? 'Tab capture permission denied. Try reloading the page.'
105
+ : 'Stream capture failed: ' + err.message);
106
+ }
107
  }
108
 
109
+ // ── Overlay UI ────────────────────────────────────────────────────────────────
110
+ function showOverlay() {
 
111
  document.getElementById('authrix-overlay')?.remove();
112
 
113
  const overlay = document.createElement('div');
 
122
  <button id="authrix-close">✕</button>
123
  </div>
124
 
125
+ <!-- Loading -->
 
 
 
 
126
  <div id="authrix-loading">
127
  <div id="authrix-radar">
128
  <div class="authrix-ring r1"></div>
 
130
  <div class="authrix-ring r3"></div>
131
  <div id="authrix-radar-dot"></div>
132
  </div>
133
+ <div id="authrix-loading-text">Initializing capture...</div>
134
  <div id="authrix-steps">
135
+ <div class="authrix-step" id="as0">🎬 Recording tab stream</div>
136
+ <div class="authrix-step" id="as1">🖼️ Extracting frames</div>
137
  <div class="authrix-step" id="as2">🧠 Running AI models</div>
138
  <div class="authrix-step" id="as3">📊 Generating report</div>
139
  </div>
140
+ <div id="authrix-note">Recording ~12 seconds of video for analysis</div>
141
  </div>
142
 
143
+ <!-- Result -->
144
  <div id="authrix-result" style="display:none;">
145
  <div id="authrix-verdict-row">
146
  <div id="authrix-badge"></div>
 
156
  <span id="authrix-audio-label"></span>
157
  </div>
158
  <div id="authrix-actions">
159
+ <button id="authrix-reanalyze">↺ Capture Again</button>
160
  <button id="authrix-open-app">Open Authrix App ↗</button>
161
  </div>
162
  </div>
163
 
164
+ <!-- Error -->
165
  <div id="authrix-error" style="display:none;">
166
  <div id="authrix-error-icon">⚠</div>
167
  <div id="authrix-error-msg"></div>
 
173
 
174
  document.body.appendChild(overlay);
175
 
 
176
  document.getElementById('authrix-close').onclick = () => overlay.remove();
177
  overlay.addEventListener('click', e => { if (e.target === overlay) overlay.remove(); });
178
+ document.getElementById('authrix-open-app').onclick = () =>
179
+ window.open('http://localhost:8000', '_blank');
180
+ document.getElementById('authrix-reanalyze').onclick = () =>
181
+ chrome.runtime.sendMessage({ type: 'START_CAPTURE' });
182
+ document.getElementById('authrix-retry').onclick = () =>
183
+ chrome.runtime.sendMessage({ type: 'START_CAPTURE' });
184
  }
185
 
186
+ function updateLoadingText(text) {
187
+ const el = document.getElementById('authrix-loading-text');
188
+ if (el) el.textContent = text;
189
+ }
190
 
191
+ function activateStep(idx) {
192
+ document.querySelectorAll('.authrix-step').forEach((el, i) => {
193
+ el.classList.toggle('active', i === idx);
194
+ el.classList.toggle('done', i < idx);
195
+ });
196
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
197
 
198
+ function showState(state) {
199
+ const loading = document.getElementById('authrix-loading');
200
+ const result = document.getElementById('authrix-result');
201
+ const error = document.getElementById('authrix-error');
202
+ if (loading) loading.style.display = state === 'loading' ? 'flex' : 'none';
203
+ if (result) result.style.display = state === 'result' ? 'block' : 'none';
204
+ if (error) error.style.display = state === 'error' ? 'flex' : 'none';
205
  }
206
 
207
+ function renderResult(data) {
208
+ activateStep(3);
209
+
210
  const isFake = data.result === 'FAKE';
211
  const conf = data.confidence || 0;
212
  const color = isFake ? '#ff4466' : '#00ff9c';
213
 
 
214
  const badge = document.getElementById('authrix-badge');
215
+ if (badge) {
216
+ badge.textContent = isFake ? '⚠' : '✓';
217
+ badge.style.color = color;
218
+ badge.style.borderColor = color;
219
+ badge.style.boxShadow = `0 0 16px ${color}44`;
220
+ }
221
 
 
222
  const vt = document.getElementById('authrix-verdict-text');
223
+ if (vt) { vt.textContent = isFake ? 'DEEPFAKE DETECTED' : 'AUTHENTIC VIDEO'; vt.style.color = color; }
 
224
 
225
+ const cv = document.getElementById('authrix-conf');
226
+ if (cv) { cv.textContent = conf + '%'; cv.style.color = color; }
 
227
 
 
228
  const bar = document.getElementById('authrix-conf-bar');
229
+ if (bar) {
230
+ bar.style.background = isFake
231
+ ? 'linear-gradient(90deg,#880022,#ff4466)'
232
+ : 'linear-gradient(90deg,#006633,#00ff9c)';
233
+ bar.style.boxShadow = `0 0 8px ${color}66`;
234
+ setTimeout(() => { bar.style.width = conf + '%'; }, 80);
235
+ }
236
 
 
237
  const dl = document.getElementById('authrix-details');
238
+ if (dl) {
239
+ dl.innerHTML = (data.details || []).slice(0, 3).map(d =>
240
+ `<div class="authrix-detail" style="border-left-color:${color};">${escHtml(d)}</div>`
241
+ ).join('');
242
+ }
243
 
 
244
  const audioRow = document.getElementById('authrix-audio-row');
245
+ if (audioRow && data.audio?.available) {
246
  const isAI = data.audio.result === 'AI_VOICE';
247
  const isMismatch = data.audio.result === 'AV_MISMATCH';
248
  const aColor = (isAI || isMismatch) ? '#ff4466' : '#00ff9c';
249
+ const audioIcon = document.getElementById('authrix-audio-icon');
250
+ const audioLabel = document.getElementById('authrix-audio-label');
251
+ if (audioIcon) audioIcon.textContent = (isAI || isMismatch) ? '🤖' : '🎙️';
252
+ if (audioLabel) {
253
+ audioLabel.textContent = isMismatch
254
+ ? 'AV Mismatch — face-swap detected'
255
+ : isAI ? `AI Voice (${data.audio.confidence}%)`
256
+ : `Human Voice (${data.audio.confidence}%)`;
257
+ audioLabel.style.color = aColor;
258
+ }
259
  audioRow.style.display = 'flex';
260
  }
261
 
 
 
 
 
262
  showState('result');
263
  }
264
 
265
+ function showError(message) {
266
+ const isOffline = message?.includes('fetch') || message?.includes('Failed to fetch');
267
+ const errMsg = document.getElementById('authrix-error-msg');
268
+ const errHint = document.getElementById('authrix-error-hint');
269
+ if (errMsg) errMsg.textContent = isOffline ? 'Authrix server is not running' : (message || 'Unknown error');
270
+ if (errHint) errHint.textContent = isOffline
271
+ ? 'Run: cd backend && python -m uvicorn main:app --port 8000'
272
+ : 'Make sure the video is playing before capturing.';
 
 
273
  showState('error');
274
  }
275
 
276
+ // ── Utilities ─────────────────────────────────────────────────────────────────
277
+ function getSupportedMimeType() {
278
+ const types = [
279
+ 'video/webm;codecs=vp9,opus',
280
+ 'video/webm;codecs=vp8,opus',
281
+ 'video/webm',
282
+ 'video/mp4',
283
+ ];
284
+ return types.find(t => MediaRecorder.isTypeSupported(t)) || 'video/webm';
285
  }
286
 
287
+ function blobToBase64(blob) {
288
+ return new Promise((resolve, reject) => {
289
+ const reader = new FileReader();
290
+ reader.onload = () => resolve(reader.result.split(',')[1]);
291
+ reader.onerror = reject;
292
+ reader.readAsDataURL(blob);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
293
  });
294
  }
295
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
296
  function escHtml(s) {
297
  return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
298
  }
extension/manifest.json CHANGED
@@ -1,20 +1,20 @@
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": {
@@ -31,7 +31,7 @@
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",
 
1
  {
2
  "manifest_version": 3,
3
  "name": "Authrix — Deepfake Detector",
4
+ "version": "2.0.0",
5
+ "description": "Detect deepfake videos on any webpage. Captures tab stream directly no download needed. Works on YouTube, Twitter, any platform.",
6
 
7
  "permissions": [
8
  "contextMenus",
9
  "activeTab",
10
  "scripting",
11
  "storage",
12
+ "tabCapture",
13
+ "tabs"
14
  ],
15
 
16
  "host_permissions": [
17
+ "http://localhost:8000/*"
 
18
  ],
19
 
20
  "background": {
 
31
 
32
  "action": {
33
  "default_popup": "popup.html",
34
+ "default_title": "Authrix Analyze this video",
35
  "default_icon": {
36
  "16": "icons/icon16.png",
37
  "48": "icons/icon48.png",
extension/popup.html CHANGED
@@ -2,19 +2,17 @@
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);
@@ -32,8 +30,8 @@
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;
@@ -46,105 +44,81 @@
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);
@@ -152,8 +126,7 @@
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; }
@@ -161,7 +134,6 @@
161
  </head>
162
  <body>
163
 
164
- <!-- Header -->
165
  <div class="header">
166
  <div class="logo">
167
  <div class="logo-dot"></div>
@@ -170,40 +142,33 @@
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>
@@ -216,7 +181,6 @@
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>
 
2
  <html lang="en">
3
  <head>
4
  <meta charset="UTF-8"/>
 
5
  <title>Authrix</title>
6
  <style>
7
  * { box-sizing: border-box; margin: 0; padding: 0; }
8
  body {
9
+ width: 320px;
10
  background: #0c150f;
11
  color: #dae5da;
12
  font-family: 'Segoe UI', system-ui, sans-serif;
13
  font-size: 13px;
14
  }
15
 
 
16
  .header {
17
  padding: 14px 16px 12px;
18
  border-bottom: 1px solid rgba(0,255,156,0.12);
 
30
  animation: pulse 2s ease-in-out infinite;
31
  }
32
  @keyframes pulse {
33
+ 0%,100% { opacity:1; box-shadow:0 0 8px #00ff9c; }
34
+ 50% { opacity:0.4; box-shadow:none; }
35
  }
36
  .status-badge {
37
  font-size: 9px; font-weight: 700; letter-spacing: 0.1em;
 
44
  color: #ff4466; background: rgba(255,68,102,0.08);
45
  }
46
 
 
47
  .body { padding: 16px; display: flex; flex-direction: column; gap: 12px; }
48
 
49
+ /* Page info */
50
+ .page-card {
 
 
 
 
 
51
  background: rgba(255,255,255,0.03);
52
  border: 1px solid rgba(255,255,255,0.07);
53
+ border-radius: 8px; padding: 10px 12px;
54
  display: flex; align-items: center; gap: 10px;
55
  }
56
+ .page-icon { font-size: 18px; flex-shrink: 0; }
57
+ .page-title {
 
58
  font-size: 11px; font-weight: 600; color: #dae5da;
59
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
60
  }
61
+ .page-sub { font-size: 10px; color: rgba(255,255,255,0.3); margin-top: 2px; }
62
 
63
+ /* Main capture button */
64
+ .btn-capture {
65
+ width: 100%; padding: 14px;
66
  background: linear-gradient(135deg, #00ff9c, #00cc6a);
67
+ color: #002110; border: none; border-radius: 10px;
68
+ font-size: 13px; font-weight: 700; letter-spacing: 0.08em;
69
  cursor: pointer; transition: all 0.2s;
70
+ box-shadow: 0 0 24px rgba(0,255,156,0.3);
71
+ display: flex; align-items: center; justify-content: center; gap: 8px;
 
 
 
72
  }
73
+ .btn-capture:hover {
74
+ transform: translateY(-2px);
75
+ box-shadow: 0 0 36px rgba(0,255,156,0.5);
 
 
76
  }
77
+ .btn-capture:active { transform: scale(0.98); }
78
 
79
+ /* How it works */
80
+ .how-it-works {
81
+ background: rgba(255,255,255,0.02);
82
+ border: 1px solid rgba(255,255,255,0.05);
83
+ border-radius: 8px; padding: 10px 12px;
84
  }
85
+ .how-label {
86
+ font-size: 9px; font-weight: 700; letter-spacing: 0.15em;
87
+ color: rgba(255,255,255,0.25); text-transform: uppercase; margin-bottom: 8px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  }
89
+ .how-step {
 
 
 
90
  display: flex; align-items: center; gap: 8px;
91
+ font-size: 10px; color: rgba(255,255,255,0.4);
92
+ padding: 3px 0;
93
  }
94
+ .how-step-num {
95
+ width: 16px; height: 16px; border-radius: 50%;
96
+ background: rgba(0,255,156,0.1); border: 1px solid rgba(0,255,156,0.2);
97
+ display: flex; align-items: center; justify-content: center;
98
+ font-size: 9px; font-weight: 700; color: #00ff9c; flex-shrink: 0;
99
  }
100
 
101
  /* Last result */
102
  #last-result {
103
  background: rgba(255,255,255,0.03);
104
  border: 1px solid rgba(255,255,255,0.07);
105
+ border-radius: 8px; padding: 10px 12px;
106
  display: none;
107
  }
108
+ .result-label {
109
+ font-size: 9px; font-weight: 700; letter-spacing: 0.15em;
110
+ color: rgba(255,255,255,0.25); text-transform: uppercase; margin-bottom: 6px;
 
 
111
  }
112
+ .result-row { display: flex; align-items: center; justify-content: space-between; }
113
+ .result-verdict { font-size: 13px; font-weight: 700; letter-spacing: 0.06em; }
114
+ .result-conf { font-size: 16px; font-weight: 700; font-family: monospace; }
115
  .result-bar-wrap {
116
+ height: 3px; background: rgba(255,255,255,0.06);
117
+ border-radius: 99px; overflow: hidden; margin: 6px 0;
118
  }
119
  .result-bar { height: 100%; border-radius: 99px; transition: width 0.8s ease; }
120
+ .result-file { font-size: 9px; color: rgba(255,255,255,0.2); font-family: monospace; }
121
 
 
122
  .footer {
123
  padding: 10px 16px;
124
  border-top: 1px solid rgba(255,255,255,0.05);
 
126
  }
127
  .footer-link {
128
  font-size: 10px; color: rgba(0,255,156,0.5);
129
+ text-decoration: none; letter-spacing: 0.06em; transition: color 0.2s;
 
130
  }
131
  .footer-link:hover { color: #00ff9c; }
132
  .model-info { font-size: 9px; color: rgba(255,255,255,0.2); letter-spacing: 0.06em; }
 
134
  </head>
135
  <body>
136
 
 
137
  <div class="header">
138
  <div class="logo">
139
  <div class="logo-dot"></div>
 
142
  <div id="statusBadge" class="status-badge offline">OFFLINE</div>
143
  </div>
144
 
 
145
  <div class="body">
146
 
147
+ <!-- Current page -->
148
+ <div class="page-card">
149
+ <div class="page-icon">🎬</div>
150
+ <div style="flex:1;min-width:0;">
151
+ <div class="page-title" id="pageTitle">Loading...</div>
152
+ <div class="page-sub" id="pageSub">Detecting page</div>
 
 
 
153
  </div>
154
  </div>
155
 
156
+ <!-- Main button -->
157
+ <button class="btn-capture" id="btnCapture">
158
+ <span>⬤</span> Capture &amp; Analyze Video
159
  </button>
160
 
161
+ <!-- How it works -->
162
+ <div class="how-it-works">
163
+ <div class="how-label">How it works</div>
164
+ <div class="how-step"><div class="how-step-num">1</div>Records 12s of the playing video</div>
165
+ <div class="how-step"><div class="how-step-num">2</div>Sends to local AI for analysis</div>
166
+ <div class="how-step"><div class="how-step-num">3</div>Shows FAKE / REAL verdict</div>
 
 
 
 
167
  </div>
168
 
169
  <!-- Last result -->
170
  <div id="last-result">
171
+ <div class="result-label">Last Result</div>
172
  <div class="result-row">
173
  <div id="lastVerdict" class="result-verdict">—</div>
174
  <div id="lastConf" class="result-conf">—</div>
 
181
 
182
  </div>
183
 
 
184
  <div class="footer">
185
  <a class="footer-link" href="http://localhost:8000" target="_blank">Open Full App ↗</a>
186
  <div class="model-info" id="modelInfo">VIT ENSEMBLE</div>
extension/popup.js CHANGED
@@ -1,27 +1,18 @@
1
  /**
2
- * Authrix Extension — Popup Script
3
- * Auto-detects and auto-analyzes videos on the current page.
4
  */
5
 
6
  const API_BASE = 'http://localhost:8000';
7
 
8
- // ── Init ──────────────────────────────────────────────────────────────────────
9
  document.addEventListener('DOMContentLoaded', async () => {
10
- const [healthOk, found] = await Promise.all([checkHealth(), detectPageVideo()]);
11
  loadLastResult();
12
  wireButtons();
13
-
14
- // AUTO-ANALYZE: if server is online and a video was found, start immediately
15
- if (healthOk && found) {
16
- const btn = document.getElementById('btnAnalyzePage');
17
- if (!btn.disabled) {
18
- // Small delay so user sees the popup before it closes
19
- setTimeout(() => btn.click(), 400);
20
- }
21
- }
22
  });
23
 
24
- // ── Health check — returns true if ready ─────────────────────────────────────
25
  async function checkHealth() {
26
  const badge = document.getElementById('statusBadge');
27
  const modelInfo = document.getElementById('modelInfo');
@@ -34,7 +25,7 @@ async function checkHealth() {
34
  modelInfo.textContent = (d.model || 'VIT ENSEMBLE').toUpperCase().slice(0, 22);
35
  return true;
36
  }
37
- badge.textContent = 'LOADING';
38
  return false;
39
  } catch {
40
  badge.textContent = 'OFFLINE';
@@ -43,163 +34,35 @@ async function checkHealth() {
43
  }
44
  }
45
 
46
- // ── Detect video on current tab — returns true if found ──────────────────────
47
- async function detectPageVideo() {
48
- const btn = document.getElementById('btnAnalyzePage');
49
  const title = document.getElementById('pageTitle');
50
  const sub = document.getElementById('pageSub');
51
-
52
  try {
53
  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
54
- if (!tab?.id) return false;
55
-
56
- const results = await chrome.scripting.executeScript({
57
- target: { tabId: tab.id },
58
- func: detectVideosOnPage,
59
- });
60
-
61
- const found = results?.[0]?.result;
62
- if (found?.url) {
63
- title.textContent = found.label || found.title || 'Video detected';
64
- sub.textContent = truncateUrl(found.url);
65
- btn.disabled = false;
66
- btn.dataset.url = found.url;
67
- return true;
68
- } else {
69
- title.textContent = 'No video detected';
70
- sub.textContent = 'Try right-clicking a video element';
71
- btn.disabled = true;
72
- return false;
73
  }
74
  } catch {
75
- title.textContent = 'Cannot access this page';
76
- sub.textContent = 'Extension pages are restricted';
77
- return false;
78
- }
79
- }
80
-
81
- // ── Runs in page context — comprehensive video detection ─────────────────────
82
- function detectVideosOnPage() {
83
- const url = location.href;
84
- const host = location.hostname;
85
-
86
- // ── YouTube ──────────────────────────────────────────────────────────────
87
- // youtube.com/watch?v=... or youtu.be/...
88
- const ytWatch = url.match(/[?&]v=([a-zA-Z0-9_-]{11})/);
89
- const ytShort = url.match(/youtu\.be\/([a-zA-Z0-9_-]{11})/);
90
- const ytShorts = url.match(/youtube\.com\/shorts\/([a-zA-Z0-9_-]{11})/);
91
- const ytId = (ytWatch?.[1] || ytShort?.[1] || ytShorts?.[1]);
92
- if (ytId) {
93
- return {
94
- url: `https://www.youtube.com/watch?v=${ytId}`,
95
- label: '▶ YouTube: ' + (document.title.replace(' - YouTube','').slice(0,40)),
96
- title: document.title,
97
- type: 'youtube',
98
- };
99
- }
100
-
101
- // ── Twitter / X ───────────────────────────────────────────────────────────
102
- if (host.includes('twitter.com') || host.includes('x.com')) {
103
- const twitterVid = document.querySelector('video[src*="video.twimg.com"]');
104
- if (twitterVid?.src) {
105
- return { url: twitterVid.src, label: '▶ Twitter/X video', title: document.title, type: 'direct' };
106
- }
107
- // Try blob → find the highest quality source
108
- const allVids = Array.from(document.querySelectorAll('video'));
109
- for (const v of allVids) {
110
- const sources = Array.from(v.querySelectorAll('source'));
111
- for (const s of sources) {
112
- if (s.src && s.src.startsWith('http')) {
113
- return { url: s.src, label: '▶ Twitter/X video', title: document.title, type: 'direct' };
114
- }
115
- }
116
- }
117
- }
118
-
119
- // ── Instagram ─────────────────────────────────────────────────────────────
120
- if (host.includes('instagram.com')) {
121
- const igVid = document.querySelector('video');
122
- if (igVid?.src && igVid.src.startsWith('http')) {
123
- return { url: igVid.src, label: '▶ Instagram video', title: document.title, type: 'direct' };
124
- }
125
- }
126
-
127
- // ── Facebook ──────────────────────────────────────────────────────────────
128
- if (host.includes('facebook.com') || host.includes('fb.watch')) {
129
- const fbVid = document.querySelector('video[src*="fbcdn"]');
130
- if (fbVid?.src) {
131
- return { url: fbVid.src, label: '▶ Facebook video', title: document.title, type: 'direct' };
132
- }
133
- }
134
-
135
- // ── Generic <video> with direct HTTP src ─────────────────────────────────
136
- const videos = Array.from(document.querySelectorAll('video'));
137
- for (const v of videos) {
138
- // Direct src
139
- if (v.src && v.src.startsWith('http') && !v.src.startsWith('blob:')) {
140
- return { url: v.src, label: '▶ Video on page', title: document.title, type: 'direct' };
141
- }
142
- // <source> children
143
- const sources = Array.from(v.querySelectorAll('source'));
144
- for (const s of sources) {
145
- if (s.src && s.src.startsWith('http')) {
146
- return { url: s.src, label: '▶ Video on page', title: document.title, type: 'direct' };
147
- }
148
- }
149
  }
150
-
151
- // ── OG / meta video tag ───────────────────────────────────────────────────
152
- const ogVideo = document.querySelector('meta[property="og:video"], meta[property="og:video:url"]');
153
- if (ogVideo?.content) {
154
- return { url: ogVideo.content, label: '▶ Embedded video', title: document.title, type: 'meta' };
155
- }
156
-
157
- return null;
158
  }
159
 
160
  // ── Wire buttons ──────────────────────────────────────────────────────────────
161
  function wireButtons() {
162
- document.getElementById('btnAnalyzePage').addEventListener('click', async () => {
163
- const url = document.getElementById('btnAnalyzePage').dataset.url;
164
- if (url) await injectAndAnalyze(url);
165
- });
166
-
167
- document.getElementById('btnGo').addEventListener('click', async () => {
168
- const url = document.getElementById('urlInput').value.trim();
169
- if (url) await injectAndAnalyze(url);
170
- });
171
-
172
- document.getElementById('urlInput').addEventListener('keydown', async (e) => {
173
- if (e.key === 'Enter') {
174
- const url = e.target.value.trim();
175
- if (url) await injectAndAnalyze(url);
176
- }
177
- });
178
- }
179
-
180
- // ── Inject overlay and trigger analysis ──────────────────────────────────────
181
- async function injectAndAnalyze(url) {
182
- try {
183
  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
184
  if (!tab?.id) return;
185
 
186
- await chrome.scripting.executeScript({
187
- target: { tabId: tab.id },
188
- func: (videoUrl) => {
189
- if (typeof window.__authrixAnalyze === 'function') {
190
- window.__authrixAnalyze(videoUrl);
191
- }
192
- },
193
- args: [url],
194
- });
195
-
196
  window.close(); // close popup so overlay is visible
197
- } catch (err) {
198
- console.error('[Authrix popup]', err);
199
- }
200
  }
201
 
202
- // ── Load last result from storage ────────────────────────────────────────────
203
  function loadLastResult() {
204
  chrome.storage.local.get('lastResult', ({ lastResult }) => {
205
  if (!lastResult) return;
@@ -213,7 +76,7 @@ function loadLastResult() {
213
  document.getElementById('lastVerdict').style.color = color;
214
  document.getElementById('lastConf').textContent = lastResult.confidence + '%';
215
  document.getElementById('lastConf').style.color = color;
216
- document.getElementById('lastFile').textContent = lastResult.file || '';
217
 
218
  const bar = document.getElementById('lastBar');
219
  bar.style.background = isFake
@@ -222,12 +85,3 @@ function loadLastResult() {
222
  setTimeout(() => { bar.style.width = lastResult.confidence + '%'; }, 80);
223
  });
224
  }
225
-
226
- function truncateUrl(url) {
227
- try {
228
- const u = new URL(url);
229
- return u.hostname + u.pathname.slice(0, 28);
230
- } catch {
231
- return url.slice(0, 45);
232
- }
233
- }
 
1
  /**
2
+ * Authrix Extension — Popup Script v2
3
+ * Simple popup: check health, show status, trigger capture on click.
4
  */
5
 
6
  const API_BASE = 'http://localhost:8000';
7
 
 
8
  document.addEventListener('DOMContentLoaded', async () => {
9
+ await checkHealth();
10
  loadLastResult();
11
  wireButtons();
12
+ updatePageInfo();
 
 
 
 
 
 
 
 
13
  });
14
 
15
+ // ── Health check ──────────────────────────────────────────────────────────────
16
  async function checkHealth() {
17
  const badge = document.getElementById('statusBadge');
18
  const modelInfo = document.getElementById('modelInfo');
 
25
  modelInfo.textContent = (d.model || 'VIT ENSEMBLE').toUpperCase().slice(0, 22);
26
  return true;
27
  }
28
+ badge.textContent = 'LOADING...';
29
  return false;
30
  } catch {
31
  badge.textContent = 'OFFLINE';
 
34
  }
35
  }
36
 
37
+ // ── Update page info ──────────────────────────────────────────────────────────
38
+ async function updatePageInfo() {
 
39
  const title = document.getElementById('pageTitle');
40
  const sub = document.getElementById('pageSub');
 
41
  try {
42
  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
43
+ if (tab?.title) {
44
+ title.textContent = tab.title.slice(0, 45);
45
+ sub.textContent = new URL(tab.url).hostname;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  }
47
  } catch {
48
+ title.textContent = 'Current page';
49
+ sub.textContent = 'Ready to capture';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  }
 
 
 
 
 
 
 
 
51
  }
52
 
53
  // ── Wire buttons ──────────────────────────────────────────────────────────────
54
  function wireButtons() {
55
+ document.getElementById('btnCapture').addEventListener('click', async () => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
57
  if (!tab?.id) return;
58
 
59
+ // Trigger capture from background (needs to be initiated from background for tabCapture)
60
+ await chrome.runtime.sendMessage({ type: 'START_CAPTURE', tabId: tab.id });
 
 
 
 
 
 
 
 
61
  window.close(); // close popup so overlay is visible
62
+ });
 
 
63
  }
64
 
65
+ // ── Load last result ──────────────────────────────────────────────────────────
66
  function loadLastResult() {
67
  chrome.storage.local.get('lastResult', ({ lastResult }) => {
68
  if (!lastResult) return;
 
76
  document.getElementById('lastVerdict').style.color = color;
77
  document.getElementById('lastConf').textContent = lastResult.confidence + '%';
78
  document.getElementById('lastConf').style.color = color;
79
+ document.getElementById('lastFile').textContent = lastResult.file || 'Tab capture';
80
 
81
  const bar = document.getElementById('lastBar');
82
  bar.style.background = isFake
 
85
  setTimeout(() => { bar.style.width = lastResult.confidence + '%'; }, 80);
86
  });
87
  }