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 +72 -110
- extension/content.js +188 -172
- extension/manifest.json +6 -6
- extension/popup.html +63 -99
- extension/popup.js +19 -165
extension/background.js
CHANGED
|
@@ -1,70 +1,37 @@
|
|
| 1 |
/**
|
| 2 |
-
* Authrix Extension — Background Service Worker
|
| 3 |
-
*
|
|
|
|
|
|
|
|
|
|
| 4 |
*/
|
| 5 |
|
| 6 |
-
const API_BASE
|
|
|
|
| 7 |
|
| 8 |
-
// ──
|
| 9 |
chrome.runtime.onInstalled.addListener(() => {
|
| 10 |
chrome.contextMenus.create({
|
| 11 |
-
id:
|
| 12 |
-
title:
|
| 13 |
-
contexts: ['
|
| 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 |
-
|
| 24 |
-
|
| 25 |
-
|
| 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
|
| 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 === '
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
.then(
|
| 64 |
-
|
| 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 === '
|
| 80 |
-
|
|
|
|
| 81 |
.then(result => {
|
| 82 |
-
chrome.storage.local.set({ lastResult: { ...result, file:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
sendResponse({ ok: true, result });
|
| 84 |
})
|
| 85 |
-
.catch(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
return true;
|
| 87 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
});
|
| 89 |
|
| 90 |
-
// ──
|
| 91 |
-
async function
|
| 92 |
-
|
| 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 |
-
//
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
//
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
}
|
| 120 |
-
const blob = new Blob([bytes], { type: mimeType || 'video/mp4' });
|
| 121 |
-
return sendToBackend(blob, filename || 'video.mp4');
|
| 122 |
}
|
| 123 |
|
| 124 |
-
// ──
|
| 125 |
-
async function
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
*
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
*/
|
| 5 |
|
| 6 |
-
// ──
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
}
|
| 13 |
-
showAnalysisOverlay(url);
|
| 14 |
-
};
|
| 15 |
|
| 16 |
-
|
| 17 |
-
|
| 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 |
-
|
| 27 |
-
|
| 28 |
-
|
|
|
|
| 29 |
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
}
|
| 36 |
|
| 37 |
-
// ──
|
| 38 |
-
function
|
| 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 |
-
<
|
| 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
|
| 67 |
<div id="authrix-steps">
|
| 68 |
-
<div class="authrix-step" id="as0">🎬
|
| 69 |
-
<div class="authrix-step" id="as1">
|
| 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">
|
| 74 |
</div>
|
| 75 |
|
| 76 |
-
<!-- Result
|
| 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">↺
|
| 93 |
<button id="authrix-open-app">Open Authrix App ↗</button>
|
| 94 |
</div>
|
| 95 |
</div>
|
| 96 |
|
| 97 |
-
<!-- Error
|
| 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 |
-
|
| 114 |
-
|
|
|
|
|
|
|
|
|
|
| 115 |
}
|
| 116 |
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 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 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
}
|
| 151 |
|
| 152 |
-
|
| 153 |
-
|
|
|
|
| 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
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
|
|
|
|
|
|
| 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 |
-
|
| 171 |
-
|
| 172 |
-
document.getElementById('authrix-conf').style.color = color;
|
| 173 |
|
| 174 |
-
// Bar
|
| 175 |
const bar = document.getElementById('authrix-conf-bar');
|
| 176 |
-
bar
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
|
|
|
|
|
|
| 181 |
|
| 182 |
-
// Details (first 3)
|
| 183 |
const dl = document.getElementById('authrix-details');
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
|
|
|
|
|
|
| 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')
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 212 |
-
|
| 213 |
-
const
|
| 214 |
-
document.getElementById('authrix-error-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
: 'The video may be DRM-protected or require authentication.';
|
| 220 |
-
document.getElementById('authrix-retry').onclick = () => startAnalysis(videoUrl);
|
| 221 |
showState('error');
|
| 222 |
}
|
| 223 |
|
| 224 |
-
// ──
|
| 225 |
-
function
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 229 |
}
|
| 230 |
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
const
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 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,'&').replace(/</g,'<').replace(/>/g,'>');
|
| 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,'&').replace(/</g,'<').replace(/>/g,'>');
|
| 298 |
}
|
extension/manifest.json
CHANGED
|
@@ -1,20 +1,20 @@
|
|
| 1 |
{
|
| 2 |
"manifest_version": 3,
|
| 3 |
"name": "Authrix — Deepfake Detector",
|
| 4 |
-
"version": "
|
| 5 |
-
"description": "Detect deepfake videos on any webpage
|
| 6 |
|
| 7 |
"permissions": [
|
| 8 |
"contextMenus",
|
| 9 |
"activeTab",
|
| 10 |
"scripting",
|
| 11 |
"storage",
|
| 12 |
-
"
|
|
|
|
| 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
|
| 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:
|
| 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:
|
| 36 |
-
50% { opacity:0.4; box-shadow:
|
| 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 |
-
/*
|
| 53 |
-
.
|
| 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-
|
| 65 |
-
.page-
|
| 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-
|
| 71 |
|
| 72 |
-
|
| 73 |
-
|
|
|
|
| 74 |
background: linear-gradient(135deg, #00ff9c, #00cc6a);
|
| 75 |
-
color: #002110; border: none; border-radius:
|
| 76 |
-
font-size:
|
| 77 |
cursor: pointer; transition: all 0.2s;
|
| 78 |
-
box-shadow: 0 0
|
| 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-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
cursor: not-allowed; transform: none;
|
| 88 |
-
box-shadow: none;
|
| 89 |
}
|
|
|
|
| 90 |
|
| 91 |
-
/*
|
| 92 |
-
.
|
| 93 |
-
|
|
|
|
|
|
|
| 94 |
}
|
| 95 |
-
.
|
| 96 |
-
|
| 97 |
-
|
| 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 |
-
.
|
| 115 |
-
|
| 116 |
-
/* Divider */
|
| 117 |
-
.divider {
|
| 118 |
display: flex; align-items: center; gap: 8px;
|
| 119 |
-
color: rgba(255,255,255,0.
|
|
|
|
| 120 |
}
|
| 121 |
-
.
|
| 122 |
-
|
| 123 |
-
background: rgba(
|
|
|
|
|
|
|
| 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-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
.result-verdict {
|
| 137 |
-
font-size: 14px; font-weight: 700; letter-spacing: 0.08em;
|
| 138 |
}
|
| 139 |
-
.result-
|
|
|
|
|
|
|
| 140 |
.result-bar-wrap {
|
| 141 |
-
height:
|
| 142 |
-
border-radius: 99px; overflow: hidden; margin:
|
| 143 |
}
|
| 144 |
.result-bar { height: 100%; border-radius: 99px; transition: width 0.8s ease; }
|
| 145 |
-
.result-file { font-size:
|
| 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.
|
| 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 |
-
<!--
|
| 177 |
-
<div>
|
| 178 |
-
<div class="
|
| 179 |
-
<div
|
| 180 |
-
<div class="page-
|
| 181 |
-
<div class="page-
|
| 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 |
-
<
|
| 189 |
-
|
|
|
|
| 190 |
</button>
|
| 191 |
|
| 192 |
-
<
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
<div class="
|
| 197 |
-
<div class="
|
| 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="
|
| 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 & 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 |
-
*
|
| 4 |
*/
|
| 5 |
|
| 6 |
const API_BASE = 'http://localhost:8000';
|
| 7 |
|
| 8 |
-
// ── Init ──────────────────────────────────────────────────────────────────────
|
| 9 |
document.addEventListener('DOMContentLoaded', async () => {
|
| 10 |
-
|
| 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
|
| 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 |
-
// ──
|
| 47 |
-
async function
|
| 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 (
|
| 55 |
-
|
| 56 |
-
|
| 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 = '
|
| 76 |
-
sub.textContent = '
|
| 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('
|
| 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 |
-
|
| 187 |
-
|
| 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 |
-
}
|
| 198 |
-
console.error('[Authrix popup]', err);
|
| 199 |
-
}
|
| 200 |
}
|
| 201 |
|
| 202 |
-
// ── Load last result
|
| 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 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|