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

feat: auto-analyze + YouTube support in browser extension

Browse files

Extension:
- Auto-analyze: popup detects video and starts analysis immediately (400ms delay)
- Comprehensive video detection: YouTube watch/shorts/youtu.be, Twitter/X,
Instagram, Facebook, generic <video>, og:video meta tags
- YouTube sends URL to /analyze-url backend endpoint
- Direct URLs fetched by background service worker
- Results saved to chrome.storage for popup last-result display

Backend:
- New POST /analyze-url endpoint for browser extension
- yt-dlp integration: downloads YouTube/Twitter/Instagram/TikTok videos
- httpx fallback for direct .mp4/.webm URLs
- Auto-cleanup of temp files
- Added yt-dlp and httpx to requirements.txt

backend/main.py CHANGED
@@ -75,6 +75,71 @@ async def health():
75
  }
76
 
77
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  @app.post("/analyze")
79
  async def analyze_video(file: UploadFile = File(...)):
80
  """
 
75
  }
76
 
77
 
78
+ @app.post("/analyze-url")
79
+ async def analyze_from_url(payload: dict):
80
+ """Download a video from a URL and analyze it. Used by the browser extension."""
81
+ if not authenticator:
82
+ raise HTTPException(status_code=503, detail="Server is still initializing")
83
+
84
+ video_url = payload.get("url", "").strip()
85
+ if not video_url:
86
+ raise HTTPException(status_code=400, detail="No URL provided")
87
+
88
+ tmp_path = UPLOAD_DIR / f"ext_{uuid.uuid4().hex}.mp4"
89
+ downloaded = False
90
+
91
+ try:
92
+ # Try yt-dlp first (YouTube, Twitter, Instagram, TikTok, etc.)
93
+ try:
94
+ import yt_dlp
95
+ ydl_opts = {
96
+ "format": "bestvideo[ext=mp4][height<=720]+bestaudio[ext=m4a]/best[ext=mp4][height<=720]/best",
97
+ "outtmpl": str(tmp_path),
98
+ "quiet": True,
99
+ "no_warnings": True,
100
+ "merge_output_format": "mp4",
101
+ }
102
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl:
103
+ ydl.download([video_url])
104
+ downloaded = tmp_path.exists() and tmp_path.stat().st_size > 1000
105
+ if downloaded:
106
+ logger.info(f"yt-dlp downloaded {tmp_path.stat().st_size // 1024}KB")
107
+ except ImportError:
108
+ logger.info("yt-dlp not installed β€” trying direct HTTP fetch")
109
+ except Exception as e:
110
+ logger.warning(f"yt-dlp failed ({e}) β€” trying direct fetch")
111
+
112
+ # Fallback: direct HTTP fetch for plain .mp4/.webm URLs
113
+ if not downloaded:
114
+ try:
115
+ import httpx
116
+ async with httpx.AsyncClient(timeout=60, follow_redirects=True) as client:
117
+ r = await client.get(video_url, headers={"User-Agent": "Mozilla/5.0"})
118
+ if r.status_code == 200:
119
+ tmp_path.write_bytes(r.content)
120
+ downloaded = tmp_path.exists() and tmp_path.stat().st_size > 1000
121
+ except Exception as e:
122
+ logger.warning(f"Direct fetch failed: {e}")
123
+
124
+ if not downloaded:
125
+ raise HTTPException(
126
+ status_code=400,
127
+ detail="Could not download video. For YouTube, install yt-dlp: pip install yt-dlp"
128
+ )
129
+
130
+ result = authenticator.analyze(str(tmp_path))
131
+ return result
132
+
133
+ except HTTPException:
134
+ raise
135
+ except Exception as e:
136
+ logger.exception(f"analyze-url failed: {e}")
137
+ raise HTTPException(status_code=500, detail=str(e))
138
+ finally:
139
+ if tmp_path.exists():
140
+ tmp_path.unlink()
141
+
142
+
143
  @app.post("/analyze")
144
  async def analyze_video(file: UploadFile = File(...)):
145
  """
backend/requirements.txt CHANGED
@@ -16,3 +16,7 @@ torchaudio
16
  moviepy>=1.0.3
17
  librosa>=0.10.0
18
  soundfile>=0.12.1
 
 
 
 
 
16
  moviepy>=1.0.3
17
  librosa>=0.10.0
18
  soundfile>=0.12.1
19
+
20
+ # Browser extension support
21
+ yt-dlp>=2024.1.0
22
+ httpx>=0.27.0
extension/background.js CHANGED
@@ -49,9 +49,23 @@ chrome.contextMenus.onClicked.addListener(async (info, tab) => {
49
  chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
50
  if (msg.type === 'ANALYZE_URL') {
51
  analyzeVideoUrl(msg.url)
52
- .then(result => sendResponse({ ok: true, result }))
53
- .catch(err => sendResponse({ ok: false, error: err.message }));
54
- return true; // keep channel open for async
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  }
56
 
57
  if (msg.type === 'CHECK_HEALTH') {
@@ -63,26 +77,39 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
63
  }
64
 
65
  if (msg.type === 'ANALYZE_FILE_BYTES') {
66
- // Receive base64 video bytes from content script, send to backend
67
  analyzeBase64Video(msg.data, msg.filename, msg.mimeType)
68
- .then(result => sendResponse({ ok: true, result }))
69
- .catch(err => sendResponse({ ok: false, error: err.message }));
 
 
 
70
  return true;
71
  }
72
  });
73
 
74
  // ── Analyze a video by URL ────────────────────────────────────────────────────
75
  async function analyzeVideoUrl(videoUrl) {
76
- // First fetch the video as a blob
77
  const response = await fetch(videoUrl);
78
  if (!response.ok) throw new Error(`Failed to fetch video: ${response.status}`);
79
-
80
  const blob = await response.blob();
81
  const filename = videoUrl.split('/').pop().split('?')[0] || 'video.mp4';
82
-
83
  return sendToBackend(blob, filename);
84
  }
85
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  // ── Analyze base64 video data ─────────────────────────────────────────────────
87
  async function analyzeBase64Video(base64Data, filename, mimeType) {
88
  const byteString = atob(base64Data);
 
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
 
71
  if (msg.type === 'CHECK_HEALTH') {
 
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);
extension/content.js CHANGED
@@ -122,20 +122,26 @@ async function startAnalysis(videoUrl) {
122
  try {
123
  // Check if server is running first
124
  const health = await fetch('http://localhost:8000/health').catch(() => null);
125
- if (!health?.ok) {
126
- throw new Error('SERVER_OFFLINE');
127
- }
128
 
129
- // Send URL to background for fetching + analysis
130
- const response = await chrome.runtime.sendMessage({
131
- type: 'ANALYZE_URL',
132
- url: videoUrl,
133
- });
134
 
135
- stopStepAnimation();
 
 
 
 
 
 
 
 
 
 
 
 
136
 
 
137
  if (!response.ok) throw new Error(response.error || 'Analysis failed');
138
-
139
  renderResult(response.result, videoUrl);
140
  } catch (err) {
141
  stopStepAnimation();
 
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();
extension/popup.js CHANGED
@@ -1,20 +1,29 @@
1
  /**
2
  * Authrix Extension β€” Popup Script
 
3
  */
4
 
5
  const API_BASE = 'http://localhost:8000';
6
 
7
  // ── Init ──────────────────────────────────────────────────────────────────────
8
  document.addEventListener('DOMContentLoaded', async () => {
9
- await checkHealth();
10
- await detectPageVideo();
11
  loadLastResult();
12
  wireButtons();
 
 
 
 
 
 
 
 
 
13
  });
14
 
15
- // ── Health check ──────────────────────────────────────────────────────────────
16
  async function checkHealth() {
17
- const badge = document.getElementById('statusBadge');
18
  const modelInfo = document.getElementById('modelInfo');
19
  try {
20
  const res = await fetch(`${API_BASE}/health`);
@@ -23,16 +32,18 @@ async function checkHealth() {
23
  badge.textContent = 'ONLINE';
24
  badge.classList.remove('offline');
25
  modelInfo.textContent = (d.model || 'VIT ENSEMBLE').toUpperCase().slice(0, 22);
26
- } else {
27
- badge.textContent = 'LOADING';
28
  }
 
 
29
  } catch {
30
  badge.textContent = 'OFFLINE';
31
  badge.classList.add('offline');
 
32
  }
33
  }
34
 
35
- // ── Detect video on current tab ───────────────────────────────────────────────
36
  async function detectPageVideo() {
37
  const btn = document.getElementById('btnAnalyzePage');
38
  const title = document.getElementById('pageTitle');
@@ -40,7 +51,7 @@ async function detectPageVideo() {
40
 
41
  try {
42
  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
43
- if (!tab?.id) return;
44
 
45
  const results = await chrome.scripting.executeScript({
46
  target: { tabId: tab.id },
@@ -49,54 +60,113 @@ async function detectPageVideo() {
49
 
50
  const found = results?.[0]?.result;
51
  if (found?.url) {
52
- title.textContent = found.title || 'Video detected';
53
  sub.textContent = truncateUrl(found.url);
54
- btn.disabled = false;
55
- btn.dataset.url = found.url;
 
56
  } else {
57
  title.textContent = 'No video detected';
58
  sub.textContent = 'Try right-clicking a video element';
59
- btn.disabled = true;
 
60
  }
61
  } catch {
62
  title.textContent = 'Cannot access this page';
63
  sub.textContent = 'Extension pages are restricted';
 
64
  }
65
  }
66
 
67
- // ── Runs in page context ──────────────────────────────────────────────────────
68
  function detectVideosOnPage() {
69
- // Check <video> elements
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  const videos = Array.from(document.querySelectorAll('video'));
71
  for (const v of videos) {
72
- const src = v.src || v.querySelector('source')?.src;
73
- if (src && src.startsWith('http')) {
74
- return { url: src, title: document.title };
 
 
 
 
 
 
 
75
  }
76
  }
77
- // YouTube
78
- const ytMatch = location.href.match(/[?&]v=([^&]+)/);
79
- if (ytMatch) {
80
- return { url: location.href, title: document.title };
 
81
  }
 
82
  return null;
83
  }
84
 
85
  // ── Wire buttons ──────────────────────────────────────────────────────────────
86
  function wireButtons() {
87
- // Analyze page video
88
  document.getElementById('btnAnalyzePage').addEventListener('click', async () => {
89
- const btn = document.getElementById('btnAnalyzePage');
90
- const url = btn.dataset.url;
91
- if (!url) return;
92
- await injectAndAnalyze(url);
93
  });
94
 
95
- // Manual URL
96
  document.getElementById('btnGo').addEventListener('click', async () => {
97
  const url = document.getElementById('urlInput').value.trim();
98
- if (!url) return;
99
- await injectAndAnalyze(url);
100
  });
101
 
102
  document.getElementById('urlInput').addEventListener('keydown', async (e) => {
@@ -107,7 +177,7 @@ function wireButtons() {
107
  });
108
  }
109
 
110
- // ── Inject overlay into active tab and trigger analysis ───────────────────────
111
  async function injectAndAnalyze(url) {
112
  try {
113
  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
@@ -123,8 +193,7 @@ async function injectAndAnalyze(url) {
123
  args: [url],
124
  });
125
 
126
- // Close popup so user can see the overlay
127
- window.close();
128
  } catch (err) {
129
  console.error('[Authrix popup]', err);
130
  }
@@ -154,12 +223,11 @@ function loadLastResult() {
154
  });
155
  }
156
 
157
- // ── Utilities ────────────────────────────────────────────────────��────────────
158
  function truncateUrl(url) {
159
  try {
160
  const u = new URL(url);
161
- return u.hostname + u.pathname.slice(0, 25);
162
  } catch {
163
- return url.slice(0, 40);
164
  }
165
  }
 
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');
28
  try {
29
  const res = await fetch(`${API_BASE}/health`);
 
32
  badge.textContent = 'ONLINE';
33
  badge.classList.remove('offline');
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';
41
  badge.classList.add('offline');
42
+ return false;
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');
 
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 },
 
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) => {
 
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 });
 
193
  args: [url],
194
  });
195
 
196
+ window.close(); // close popup so overlay is visible
 
197
  } catch (err) {
198
  console.error('[Authrix popup]', err);
199
  }
 
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
  }