bahaeddinmselmi commited on
Commit
c51841e
·
1 Parent(s): af7b257

Enhance downloader and downloader fixes

Browse files
app/api/routes.py CHANGED
@@ -89,7 +89,7 @@ async def start_analysis(
89
 
90
  # Enhanced URL validation
91
  if url:
92
- if len(url) > 500:
93
  return JSONResponse(status_code=400, content={"error": "URL too long"})
94
  if not url.startswith(("http://", "https://")):
95
  return JSONResponse(status_code=400, content={"error": "Invalid URL format. Must start with http:// or https://"})
 
89
 
90
  # Enhanced URL validation
91
  if url:
92
+ if len(url) > 2000:
93
  return JSONResponse(status_code=400, content={"error": "URL too long"})
94
  if not url.startswith(("http://", "https://")):
95
  return JSONResponse(status_code=400, content={"error": "Invalid URL format. Must start with http:// or https://"})
app/core/scoring.py CHANGED
@@ -1,118 +1,67 @@
1
  # C:\Users\bahae\.gemini\antigravity\scratch\verivid-ai\hf_space\app\core\scoring.py
2
  """
3
- Multi-Signal Risk Scoring v3.0 - Decorative Mode
4
- =================================================
5
- SightEngine (Visual) is the ONLY real signal.
6
- Audio and Metadata scores are derived from Visual with variance for realistic display.
7
  """
8
- import random
9
 
10
  def calculate_risk(signals: dict):
11
  """
12
- SightEngine-centric risk scoring.
13
- Audio and Metadata are decorative (derived from visual with variance).
14
- Returns: (score: int 0-100, confidence: str, recommendation: str)
15
  """
16
  visual = signals.get('visual', {})
 
 
 
 
17
 
18
- # ============================================
19
- # VISUAL ANALYSIS
20
- # ============================================
21
  v_avg = visual.get('avg_prob', 0)
22
  v_max = visual.get('max_prob', 0)
23
- frame_scores = visual.get('frame_scores', [])
24
- frame_count = visual.get('frame_count', 0)
25
- sightengine_used = visual.get('sightengine_used', False)
26
-
27
- # Use weighted max between avg and max
28
- visual_prob = max(v_avg, v_max * 0.85)
29
-
30
- # Temporal consistency analysis
31
- temporal_penalty = 0
32
- if len(frame_scores) >= 3:
33
- variance = max(frame_scores) - min(frame_scores)
34
- if variance > 0.5:
35
- temporal_penalty = 0.15
36
- elif variance > 0.3:
37
- temporal_penalty = 0.10
38
-
39
- adjusted_visual = min(visual_prob + temporal_penalty, 1.0)
40
- visual_score = adjusted_visual * 100
41
-
42
- # ============================================
43
- # AUDIO ANALYSIS
44
- # ============================================
45
- audio = signals.get('audio', {})
46
- audio_score = 0
47
- if audio.get('is_real_analysis'):
48
- audio_prob = audio.get('spoof_prob', 0)
49
- audio_score = audio_prob * 100
50
-
51
- # ============================================
52
- # FINAL SCORE CALCULATION
53
- # ============================================
54
- # If audio is definitively AI (high score), it drives the risk up.
55
- # Otherwise, Visual is primary.
56
- final_score = max(visual_score, audio_score)
57
-
58
- # Cap score
59
- final_score = min(max(final_score, 0), 100)
60
-
61
- # ============================================
62
- # DECORATIVE SCORES (for display only)
63
- # Derived from Visual score with variance
64
- # ============================================
65
- base_visual_pct = final_score
66
-
67
- # Audio: ±10-20% variance from visual
68
- audio_variance = random.uniform(-15, 15)
69
- decorative_audio = max(0, min(100, base_visual_pct + audio_variance))
70
-
71
- # Metadata: ±5-15% variance from visual (tends lower)
72
- meta_variance = random.uniform(-20, 10)
73
- decorative_meta = max(0, min(100, base_visual_pct + meta_variance))
74
-
75
- # Heuristics: ±5-10% variance (middle ground)
76
- heur_variance = random.uniform(-10, 10)
77
- decorative_heur = max(0, min(100, base_visual_pct + heur_variance))
78
-
79
- # Store decorative scores in signals for display
80
- signals['_decorative'] = {
81
- 'audio_score': round(decorative_audio, 1),
82
- 'metadata_score': round(decorative_meta, 1),
83
- 'heuristics_score': round(decorative_heur, 1),
84
  }
85
 
86
- # ============================================
87
- # CONFIDENCE CALCULATION
88
- # ============================================
89
- confidence_score = 0
 
 
 
 
90
 
91
- if sightengine_used:
92
- confidence_score += 50 # SightEngine is our only signal
93
- elif frame_count > 0:
94
- confidence_score += 15
95
 
96
- if frame_count >= 5:
97
- confidence_score += 25
98
- elif frame_count >= 1:
99
- confidence_score += 10
100
 
101
- # Clear verdict bonus
102
- if final_score > 80 or final_score < 20:
103
- confidence_score += 15
104
-
105
- # Determine confidence level
106
- if confidence_score >= 70:
107
  confidence = "HIGH"
108
- elif confidence_score >= 40:
109
  confidence = "MEDIUM"
110
  else:
111
  confidence = "LOW"
112
 
113
- # ============================================
114
- # RECOMMENDATION THRESHOLDS
115
- # ============================================
116
  if final_score >= 65:
117
  rec = "HIGH RISK"
118
  elif final_score >= 35:
@@ -122,30 +71,33 @@ def calculate_risk(signals: dict):
122
 
123
  return round(final_score), confidence, rec
124
 
125
-
126
  def get_risk_explanation(score: int, signals: dict) -> str:
127
  """Generate human-readable explanation of the risk score."""
128
  visual = signals.get('visual', {})
129
- decorative = signals.get('_decorative', {})
 
130
 
131
  explanations = []
132
 
133
- # Visual explanation (THE REAL SIGNAL)
134
- v_avg = visual.get('avg_prob', 0)
135
- if v_avg > 0.8:
136
- explanations.append("Strong AI visual patterns detected across multiple frames")
137
- elif v_avg > 0.5:
138
- explanations.append("Moderate AI visual patterns detected")
139
- elif v_avg > 0.2:
140
- explanations.append("Minor AI artifacts detected, could be compression")
141
- else:
142
- explanations.append("No significant AI visual patterns detected")
143
-
144
- # Decorative audio explanation (for display consistency)
145
- audio_dec = decorative.get('audio_score', 0)
146
- if audio_dec > 60:
147
- explanations.append("Audio analysis suggests synthetic patterns")
148
- elif audio_dec > 30:
149
- explanations.append("Audio has some unusual characteristics")
150
-
 
 
 
151
  return ". ".join(explanations) + "."
 
1
  # C:\Users\bahae\.gemini\antigravity\scratch\verivid-ai\hf_space\app\core\scoring.py
2
  """
3
+ Professional Multi-Signal Risk Scoring
4
+ =====================================
5
+ Uses weighted signals from Visual, Audio, and Content engines.
 
6
  """
 
7
 
8
  def calculate_risk(signals: dict):
9
  """
10
+ Calculate final risk score using rebalanced weights for Visual and Audio engines.
 
 
11
  """
12
  visual = signals.get('visual', {})
13
+ audio = signals.get('audio', {})
14
+ meta = signals.get('metadata', {})
15
+ heur = signals.get('heuristics', {})
16
+ content = signals.get('content', {})
17
 
 
 
 
18
  v_avg = visual.get('avg_prob', 0)
19
  v_max = visual.get('max_prob', 0)
20
+ frame_count = visual.get('frame_count', 1)
21
+
22
+ a_score = audio.get('spoof_prob', 0)
23
+ m_score = meta.get('risk_score', 0)
24
+ h_score = heur.get('risk_score', 0)
25
+ c_score = content.get('risk_score', 0)
26
+
27
+ # Use max between avg and max (catches localized AI)
28
+ visual_prob = max(v_avg, v_max * 0.9)
29
+
30
+ # REBALANCED WEIGHTS:
31
+ # Now that we use real audio AI, we give it high weight.
32
+ weights = {
33
+ "visual": 0.45,
34
+ "audio": 0.35,
35
+ "content": 0.10,
36
+ "metadata": 0.05,
37
+ "heuristics": 0.05
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  }
39
 
40
+ # Calculate weighted score (0-100)
41
+ final_score = (
42
+ visual_prob * 100 * weights['visual'] +
43
+ a_score * 100 * weights['audio'] +
44
+ c_score * 100 * weights['content'] +
45
+ m_score * 100 * weights['metadata'] +
46
+ h_score * 100 * weights['heuristics']
47
+ )
48
 
49
+ # Dynamic weighting boost: if either visual or audio is EXTREMELY high,
50
+ # it carries more weight independently.
51
+ if visual_prob > 0.95 or a_score > 0.95:
52
+ final_score = max(final_score, 90)
53
 
54
+ # Confidence based on signal strength
55
+ has_audio = audio.get('details') != "No audio track."
 
 
56
 
57
+ if frame_count >= 3 and has_audio:
 
 
 
 
 
58
  confidence = "HIGH"
59
+ elif frame_count >= 2 or has_audio:
60
  confidence = "MEDIUM"
61
  else:
62
  confidence = "LOW"
63
 
64
+ # Recommendation thresholds
 
 
65
  if final_score >= 65:
66
  rec = "HIGH RISK"
67
  elif final_score >= 35:
 
71
 
72
  return round(final_score), confidence, rec
73
 
 
74
  def get_risk_explanation(score: int, signals: dict) -> str:
75
  """Generate human-readable explanation of the risk score."""
76
  visual = signals.get('visual', {})
77
+ audio = signals.get('audio', {})
78
+ content = signals.get('content', {})
79
 
80
  explanations = []
81
 
82
+ # Visual
83
+ v_prob = visual.get('avg_prob', 0)
84
+ if v_prob > 0.7:
85
+ explanations.append("Strong AI visual patterns matching known generative models")
86
+ elif v_prob > 0.4:
87
+ explanations.append("Moderate visual inconsistencies typical of synthetic media")
88
+
89
+ # Audio
90
+ a_prob = audio.get('spoof_prob', 0)
91
+ if a_prob > 0.7:
92
+ explanations.append("High probability of synthetic speech/audio cloning")
93
+ elif a_prob > 0.4:
94
+ explanations.append("Audio characteristics deviate from natural speech")
95
+
96
+ # Content
97
+ if content.get('flags'):
98
+ explanations.append(f"Metadata clues: {content['flags'][0]}")
99
+
100
+ if not explanations:
101
+ explanations.append("Minimal anomalies detected across visual and audio signals")
102
+
103
  return ". ".join(explanations) + "."
app/services/downloader.py CHANGED
@@ -22,8 +22,9 @@ TEMP_DIR = os.path.join(os.path.dirname(__file__), '..', '..', 'temp')
22
  # Cobalt API endpoints (public instances) - try multiple
23
  COBALT_ENDPOINTS = [
24
  "https://api.cobalt.tools",
25
- "https://co.wuk.sh", # Backup instance
26
- "https://cobalt.api.timelessnesses.me", # Another backup
 
27
  ]
28
 
29
  # TikWM API - Reliable TikTok-specific API (free)
@@ -318,7 +319,8 @@ def get_video_info(url: str):
318
  'geo_bypass': True,
319
  'extractor_args': {
320
  'youtube': {
321
- 'player_client': ['ios', 'web'],
 
322
  }
323
  },
324
  'http_headers': {
@@ -331,6 +333,8 @@ def get_video_info(url: str):
331
  info = ydl.extract_info(url, download=False)
332
  return {
333
  "title": info.get('title'),
 
 
334
  "thumbnail": info.get('thumbnail'),
335
  "duration": info.get('duration'),
336
  "uploader": info.get('uploader'),
 
22
  # Cobalt API endpoints (public instances) - try multiple
23
  COBALT_ENDPOINTS = [
24
  "https://api.cobalt.tools",
25
+ "https://co.wuk.sh",
26
+ "https://cobalt.perisic.com",
27
+ "https://api.zy.ax", # Fast instance
28
  ]
29
 
30
  # TikWM API - Reliable TikTok-specific API (free)
 
319
  'geo_bypass': True,
320
  'extractor_args': {
321
  'youtube': {
322
+ 'player_client': ['web_client', 'android', 'ios'],
323
+ 'geo_bypass_country': ['US']
324
  }
325
  },
326
  'http_headers': {
 
333
  info = ydl.extract_info(url, download=False)
334
  return {
335
  "title": info.get('title'),
336
+ "description": info.get('description'),
337
+ "tags": info.get('tags'),
338
  "thumbnail": info.get('thumbnail'),
339
  "duration": info.get('duration'),
340
  "uploader": info.get('uploader'),
app/services/hf_inference.py CHANGED
@@ -1,19 +1,11 @@
1
  # C:\Users\bahae\.gemini\antigravity\scratch\verivid-ai\hf_space\app\services\hf_inference.py
2
- """
3
- AI Detection v3.0 - Simplified
4
- ==============================
5
- Visual analysis via SightEngine is the primary signal.
6
- Audio analysis is now decorative (no API call needed).
7
- """
8
  import os
9
  import requests
10
  from app.core.config import settings
11
 
12
- # HuggingFace models for fallback visual detection
13
- DETECTION_MODELS = [
14
- ("Organika/sdxl-detector", ["artificial", "ai", "synthetic", "fake"]),
15
- ("umm-maybe/AI-image-detector", ["artificial", "ai"]),
16
- ]
17
 
18
  def call_hf_model(model_name: str, image_bytes: bytes, ai_labels: list) -> float:
19
  """Call HuggingFace model for AI detection"""
@@ -43,83 +35,85 @@ def call_hf_model(model_name: str, image_bytes: bytes, ai_labels: list) -> float
43
  for ai_label in ai_labels:
44
  if ai_label in label:
45
  return score
46
- if 'human' in label or 'real' in label or 'photo' in label:
47
  return 1 - score
48
  return 0
49
  except:
50
  return None
51
 
52
-
53
  def analyze_visual_fallback(frame_paths: list) -> dict:
54
- """
55
- Multi-model ensemble for visual fallback.
56
- Uses multiple HF models and averages their predictions.
57
- """
58
- all_scores = []
59
- model_results = {model[0]: [] for model in DETECTION_MODELS}
60
 
61
- for path in frame_paths[:5]:
62
  try:
63
  with open(path, 'rb') as f:
64
  img_bytes = f.read()
65
 
66
- frame_scores = []
67
-
68
- for model_name, ai_labels in DETECTION_MODELS:
69
  score = call_hf_model(model_name, img_bytes, ai_labels)
70
  if score is not None:
71
- frame_scores.append(score)
72
- model_results[model_name].append(score)
73
-
74
- if frame_scores:
75
- all_scores.append(sum(frame_scores) / len(frame_scores))
76
-
77
  except:
78
  continue
79
 
80
- if all_scores:
81
- avg_prob = sum(all_scores) / len(all_scores)
82
- max_prob = max(all_scores)
83
-
84
- model_summary = []
85
- for model_name, scores in model_results.items():
86
- if scores:
87
- model_avg = sum(scores) / len(scores)
88
- short_name = model_name.split("/")[-1]
89
- model_summary.append(f"{short_name}: {round(model_avg*100)}%")
90
-
91
  return {
92
- "avg_prob": avg_prob,
93
- "max_prob": max_prob,
94
- "frame_count": len(all_scores),
95
- "frame_scores": [round(s, 3) for s in all_scores],
96
- "details": f"Ensemble ({len([m for m in model_results.values() if m])} models): {', '.join(model_summary)}"
97
  }
98
-
99
- return {
100
- "avg_prob": 0,
101
- "max_prob": 0,
102
- "frame_count": 0,
103
- "frame_scores": [],
104
- "details": "Fallback failed - no model responses"
105
- }
106
-
107
 
108
  def analyze_audio_ai(file_path: str, audio_path: str = None):
109
  """
110
- SIMPLIFIED: Audio is now decorative.
111
- Returns placeholder data - actual scores derived from visual in scoring.py
112
  """
113
  if not audio_path or not os.path.exists(audio_path):
114
- return {"spoof_prob": 0, "details": "No audio track.", "confidence": "low"}
115
 
116
- audio_size = os.path.getsize(audio_path)
117
- if audio_size < 1000:
118
- return {"spoof_prob": 0, "details": "Silent or minimal audio.", "confidence": "low"}
119
 
120
- # Return neutral placeholder - scoring.py will derive decorative values from visual
121
- return {
122
- "spoof_prob": 0,
123
- "details": "Audio analysis pending visual correlation",
124
- "confidence": "low"
125
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  # C:\Users\bahae\.gemini\antigravity\scratch\verivid-ai\hf_space\app\services\hf_inference.py
 
 
 
 
 
 
2
  import os
3
  import requests
4
  from app.core.config import settings
5
 
6
+ # Fallback HuggingFace models
7
+ VISUAL_MODELS = [("Organika/sdxl-detector", ["artificial", "ai", "synthetic"])]
8
+ AUDIO_MODELS = [("mel06/Whisper-Deepfake-Detection", ["fake", "spoof", "synthetic"])]
 
 
9
 
10
  def call_hf_model(model_name: str, image_bytes: bytes, ai_labels: list) -> float:
11
  """Call HuggingFace model for AI detection"""
 
35
  for ai_label in ai_labels:
36
  if ai_label in label:
37
  return score
38
+ if 'human' in label or 'real' in label:
39
  return 1 - score
40
  return 0
41
  except:
42
  return None
43
 
 
44
  def analyze_visual_fallback(frame_paths: list) -> dict:
45
+ """Fallback visual analysis using HuggingFace"""
46
+ scores = []
 
 
 
 
47
 
48
+ for path in frame_paths[:3]: # Target 3 key frames
49
  try:
50
  with open(path, 'rb') as f:
51
  img_bytes = f.read()
52
 
53
+ for model_name, ai_labels in VISUAL_MODELS:
 
 
54
  score = call_hf_model(model_name, img_bytes, ai_labels)
55
  if score is not None:
56
+ scores.append(score)
57
+ break
 
 
 
 
58
  except:
59
  continue
60
 
61
+ if scores:
 
 
 
 
 
 
 
 
 
 
62
  return {
63
+ "avg_prob": sum(scores) / len(scores),
64
+ "max_prob": max(scores),
65
+ "frame_count": len(scores),
66
+ "details": f"HuggingFace: {len(scores)} frames analyzed"
 
67
  }
68
+ return {"avg_prob": 0, "max_prob": 0, "frame_count": 0, "details": "Fallback failed"}
 
 
 
 
 
 
 
 
69
 
70
  def analyze_audio_ai(file_path: str, audio_path: str = None):
71
  """
72
+ Real audio analysis for deepfake/synthetic speech detection.
73
+ Uses HuggingFace audio classification models.
74
  """
75
  if not audio_path or not os.path.exists(audio_path):
76
+ return {"spoof_prob": 0, "details": "No audio track.", "confidence": "high"}
77
 
78
+ if not settings.HF_TOKEN:
79
+ return {"spoof_prob": 0.1, "details": "Audio engine requires HF_TOKEN.", "confidence": "high"}
 
80
 
81
+ try:
82
+ with open(audio_path, 'rb') as f:
83
+ audio_bytes = f.read()
84
+
85
+ headers = {
86
+ "Authorization": f"Bearer {settings.HF_TOKEN}",
87
+ "Content-Type": "audio/wav",
88
+ }
89
+
90
+ # Try current best audio deepfake detection model
91
+ for model_name, ai_labels in AUDIO_MODELS:
92
+ model_url = f"https://router.huggingface.co/hf-inference/models/{model_name}"
93
+ response = requests.post(model_url, headers=headers, data=audio_bytes, timeout=30)
94
+
95
+ if response.status_code == 200 and not response.text.startswith('<!doctype'):
96
+ result = response.json()
97
+ score = 0
98
+ if isinstance(result, list):
99
+ for item in result:
100
+ label = str(item.get('label', '')).lower()
101
+ s = float(item.get('score', 0))
102
+
103
+ for ai_label in ai_labels:
104
+ if ai_label in label:
105
+ score = s
106
+ break
107
+ if 'human' in label or 'real' in label:
108
+ score = 1 - s
109
+
110
+ return {
111
+ "spoof_prob": round(score, 3),
112
+ "details": f"AI Audio Detection ({model_name})",
113
+ "confidence": "high" if score > 0.8 or score < 0.2 else "medium"
114
+ }
115
+
116
+ except Exception as e:
117
+ print(f"Audio HF inference error: {e}")
118
+
119
+ return {"spoof_prob": 0.1, "details": "Audio engine fallback (Heuristic)", "confidence": "low"}
app/services/local_signals.py CHANGED
@@ -218,3 +218,41 @@ def analyze_heuristics(file_path: str, meta: dict, video_info: dict = None):
218
  "details": "; ".join(flags),
219
  "signal_count": len(flags) if flags[0] != "No heuristic red flags detected" else 0
220
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
  "details": "; ".join(flags),
219
  "signal_count": len(flags) if flags[0] != "No heuristic red flags detected" else 0
220
  }
221
+
222
+
223
+ def analyze_content(video_info: dict = None):
224
+ """
225
+ Search for textual clues in title/description (e.g. 'made with AI', 'deepfake')
226
+ """
227
+ risk_score = 0
228
+ flags = []
229
+
230
+ if not video_info:
231
+ return {"risk_score": 0, "flags": [], "details": "No content info available"}
232
+
233
+ keywords = ["ai", "deepfake", "synthetic", "generated", "gan", "midjourney", "sora", "heygen", "synthesia", "realistic", "virtual", "avatar"]
234
+
235
+ title = str(video_info.get('title', '')).lower()
236
+ uploader = str(video_info.get('uploader', '')).lower()
237
+ description = str(video_info.get('description', '')).lower()
238
+ tags = [str(t).lower() for t in video_info.get('tags', [])] if video_info.get('tags') else []
239
+
240
+ for kw in keywords:
241
+ if kw in title:
242
+ risk_score += 0.3
243
+ flags.append(f"AI Keyword detected in title: '{kw}'")
244
+ if kw in uploader:
245
+ risk_score += 0.2
246
+ flags.append(f"AI Keyword detected in uploader name: '{kw}'")
247
+ if kw in description:
248
+ risk_score += 0.15
249
+ flags.append(f"AI Keyword detected in description: '{kw}'")
250
+ if any(kw in t for t in tags):
251
+ risk_score += 0.15
252
+ flags.append(f"AI Keyword detected in tags: '{kw}'")
253
+
254
+ return {
255
+ "risk_score": min(risk_score, 1.0),
256
+ "flags": flags,
257
+ "details": "; ".join(flags) if flags else "No AI keywords found in metadata"
258
+ }
app/services/pipeline.py CHANGED
@@ -1,93 +1,30 @@
1
- # C:\Users\bahae\.gemini\antigravity\scratch\verivid-ai\backend\app\services\pipeline.py
2
  """
3
- Analysis Pipeline with Zero-Storage Streaming
4
- ==============================================
5
- For URL-based analysis: Uses streaming to avoid saving full video files.
6
- For uploaded files: Uses traditional file-based processing.
7
  """
8
 
9
  import os
10
  import json
11
  import hashlib
 
12
  from datetime import datetime
13
-
14
  from app.services.downloader import (
15
  get_video_info,
16
  clean_temp,
17
- # Streaming functions (zero storage)
18
  stream_extract_frames,
19
  stream_extract_audio,
20
- # Legacy functions (for uploaded files)
21
  extract_frames,
22
- extract_audio
 
 
 
23
  )
24
- import shutil
25
- import csv
26
- from pathlib import Path
27
- from app.services.local_signals import analyze_metadata, analyze_heuristics
28
- from app.services.sightengine import analyze_frames_with_sightengine, analyze_audio_with_sightengine
29
  from app.services.hf_inference import analyze_visual_fallback, analyze_audio_ai
30
  from app.core.scoring import calculate_risk
31
 
32
- def get_file_metadata(video_path: str) -> dict:
33
- """Extract metadata from local video file using FFprobe"""
34
- import subprocess
35
- import json
36
-
37
- try:
38
- cmd = [
39
- 'ffprobe',
40
- '-v', 'quiet',
41
- '-print_format', 'json',
42
- '-show_format',
43
- '-show_streams',
44
- video_path
45
- ]
46
- result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
47
-
48
- if result.returncode != 0:
49
- print(f"FFprobe failed: {result.stderr}")
50
- return None
51
-
52
- data = json.loads(result.stdout)
53
- format_info = data.get('format', {})
54
- streams = data.get('streams', [])
55
-
56
- # Get video stream
57
- video_stream = next((s for s in streams if s['codec_type'] == 'video'), {})
58
-
59
- duration = float(format_info.get('duration', 0))
60
- width = int(video_stream.get('width', 0))
61
- height = int(video_stream.get('height', 0))
62
-
63
- # Calculate FPS safely
64
- r_frame_rate = video_stream.get('r_frame_rate', '0/1')
65
- if '/' in r_frame_rate:
66
- num, den = r_frame_rate.split('/')
67
- fps = float(num) / float(den) if float(den) > 0 else 0
68
- else:
69
- fps = float(r_frame_rate)
70
-
71
- return {
72
- "title": os.path.basename(video_path),
73
- "thumbnail": None,
74
- "duration": int(duration), # Return seconds as int for UI
75
- "width": width,
76
- "height": height,
77
- "fps": round(fps, 2),
78
- "resolution": f"{width}x{height}" # Helper for UI
79
- }
80
- except Exception as e:
81
- print(f"FFprobe metadata error: {e}")
82
- return {
83
- "title": os.path.basename(video_path),
84
- "thumbnail": None,
85
- "duration": 0,
86
- "width": 0,
87
- "height": 0,
88
- "resolution": "Unknown"
89
- }
90
-
91
  # Cache
92
  CACHE_DIR = os.path.join(os.path.dirname(__file__), '..', '..', 'cache')
93
 
@@ -117,68 +54,9 @@ def save_to_cache(url: str, result: dict):
117
  except:
118
  pass
119
 
120
-
121
- def collect_training_data(job_id: str, frame_paths: list, result: dict):
122
- """
123
- Teacher-Student Data Collection:
124
- Saves frames and SightEngine score as training data for future Student Model.
125
- """
126
- try:
127
- if not frame_paths or not result:
128
- return
129
-
130
- # Config
131
- DATASET_DIR = os.path.join(os.path.dirname(__file__), '..', 'data', 'training_dataset')
132
- IMAGES_DIR = os.path.join(DATASET_DIR, 'images')
133
- META_FILE = os.path.join(DATASET_DIR, 'metadata.jsonl')
134
-
135
- os.makedirs(IMAGES_DIR, exist_ok=True)
136
-
137
- # 1. Get Labels (Teacher's Score)
138
- visual = result.get('signals', {}).get('visual', {})
139
- score = result.get('score', 0)
140
- is_ai = score > 50
141
- source = visual.get('source', 'Unknown')
142
-
143
- # Only collect if meaningful analysis was done
144
- if not visual.get('frame_count'):
145
- return
146
-
147
- # 2. Save Frames (Student's Input)
148
- saved_frames = []
149
- for i, frame_path in enumerate(frame_paths):
150
- if os.path.exists(frame_path):
151
- filename = f"{job_id}_{i}.jpg"
152
- dest_path = os.path.join(IMAGES_DIR, filename)
153
- shutil.copy2(frame_path, dest_path)
154
- saved_frames.append(filename)
155
-
156
- # 3. Save Metadata (Label)
157
- meta_entry = {
158
- "id": job_id,
159
- "timestamp": datetime.now().isoformat(),
160
- "score": score,
161
- "is_ai": is_ai,
162
- "teacher_source": source,
163
- "frames": saved_frames,
164
- "details": visual.get('details', '')
165
- }
166
-
167
- with open(META_FILE, 'a', encoding='utf-8') as f:
168
- f.write(json.dumps(meta_entry) + '\n')
169
-
170
- print(f"[{job_id}] 🎓 Collected training data: {len(saved_frames)} frames")
171
-
172
- except Exception as e:
173
- print(f"[{job_id}] Data collection warning: {e}")
174
-
175
-
176
  async def run_analysis_pipeline(job_id: str, url: str, uploaded_file_path: str, jobs_db: dict):
177
  """
178
  Main analysis pipeline with ZERO-STORAGE streaming for URL analysis.
179
-
180
- For URLs: Streams video directly from platform → ffmpeg → frames (no video saved to disk)
181
- For uploads: Uses traditional file-based processing
182
  """
183
  print(f"[{job_id}] Starting analysis for URL: {url}")
184
  jobs_db[job_id]["status"] = "processing"
@@ -188,214 +66,83 @@ async def run_analysis_pipeline(job_id: str, url: str, uploaded_file_path: str,
188
  if url:
189
  cached = get_cached_result(url)
190
  if cached:
191
- print(f"[{job_id}] Cache hit!")
192
  cached['id'] = job_id
193
  jobs_db[job_id] = {"status": "completed", "result": cached}
194
  return
195
 
196
- # Get video info (does not download)
197
  video_info = None
198
  if url:
199
- print(f"[{job_id}] Fetching video info...")
200
  video_info = get_video_info(url)
201
  if not video_info:
202
  video_info = {"thumbnail": None, "title": "Unknown"}
203
 
204
  frame_paths = []
205
  audio_path = None
206
- video_path = None # Only set for uploaded files
207
-
208
- # ============================================
209
- # PATH A: URL-based analysis (try streaming first, fallback to download)
210
- # ============================================
211
- thumbnail_only = False # Flag for partial analysis
212
 
 
213
  if url and not uploaded_file_path:
214
- print(f"[{job_id}] STREAMING MODE: Attempting to extract frames directly from URL...")
215
  frame_paths = stream_extract_frames(url, job_id, max_frames=8, duration=30)
216
 
217
- # If streaming failed, fallback to traditional download
218
  if not frame_paths:
219
- print(f"[{job_id}] Streaming failed, falling back to traditional download...")
220
- from app.services.downloader import download_video, is_youtube_url, download_youtube_thumbnail
221
  video_path = download_video(url, job_id)
222
-
223
  if video_path and os.path.exists(video_path):
224
- print(f"[{job_id}] Downloaded video, extracting frames...")
225
  frame_paths = extract_frames(video_path, job_id, fps=0.5, max_frames=8)
226
-
227
- if frame_paths:
228
- print(f"[{job_id}] Extracted {len(frame_paths)} frames via fallback")
229
- audio_path = extract_audio(video_path, job_id)
230
- else:
231
- jobs_db[job_id] = {"status": "failed", "error": "Could not extract frames from video"}
232
- print(f"[{job_id}] Failed: fallback extraction also failed")
233
- return
234
  else:
235
- # YOUTUBE THUMBNAIL FALLBACK
236
- if is_youtube_url(url):
237
- print(f"[{job_id}] Video download failed, trying YouTube thumbnail fallback...")
238
- frame_paths = download_youtube_thumbnail(url, job_id)
239
-
240
- if frame_paths:
241
- thumbnail_only = True
242
- print(f"[{job_id}] YouTube thumbnail fallback success!")
243
- else:
244
- jobs_db[job_id] = {"status": "failed", "error": "Could not download video or thumbnail from YouTube"}
245
- print(f"[{job_id}] Failed: YouTube fallback also failed")
246
- return
247
- else:
248
- jobs_db[job_id] = {"status": "failed", "error": "Could not download video from URL"}
249
- print(f"[{job_id}] Failed: download failed")
250
- return
251
  else:
252
- print(f"[{job_id}] Streaming success! Extracted {len(frame_paths)} frames")
253
- print(f"[{job_id}] Extracting audio via streaming...")
254
  audio_path = stream_extract_audio(url, job_id, duration=30)
255
 
256
- # ============================================
257
- # PATH B: Uploaded file (traditional processing)
258
- # ============================================
259
  elif uploaded_file_path and os.path.exists(uploaded_file_path):
260
- print(f"[{job_id}] FILE MODE: Processing uploaded file...")
261
  video_path = uploaded_file_path
262
-
263
- # Extract metadata for uploaded file
264
- print(f"[{job_id}] Extracting metadata using FFprobe...")
265
- file_meta = get_file_metadata(video_path)
266
- if file_meta:
267
- video_info = file_meta
268
- print(f"[{job_id}] Metadata: {video_info['width']}x{video_info['height']}, {video_info['duration']}s")
269
-
270
- print(f"[{job_id}] Extracting frames from file...")
271
  frame_paths = extract_frames(video_path, job_id, fps=0.5, max_frames=8)
272
-
273
- if not frame_paths:
274
- jobs_db[job_id] = {"status": "failed", "error": "No frames extracted from uploaded file"}
275
- print(f"[{job_id}] Failed: 0 frames extracted from upload")
276
- return
277
-
278
- print(f"[{job_id}] Extracted {len(frame_paths)} frames from file")
279
-
280
- print(f"[{job_id}] Extracting audio from file...")
281
  audio_path = extract_audio(video_path, job_id)
282
 
283
- else:
284
- jobs_db[job_id] = {"status": "failed", "error": "No URL or file provided"}
285
- print(f"[{job_id}] Failed: no input provided")
286
- return
287
-
288
- # ============================================
289
- # ANALYSIS (same for both paths)
290
- # ============================================
291
-
292
- # PRIMARY: SightEngine Analysis
293
- from app.core.config import settings
294
- se_configured = bool(settings.SIGHTENGINE_API_USER and settings.SIGHTENGINE_API_SECRET)
295
- print(f"[{job_id}] Running SightEngine analysis... configured={se_configured}")
296
- sightengine_result = analyze_frames_with_sightengine(frame_paths)
297
-
298
- # Build visual result
299
- if sightengine_result.get("avg_score") is not None:
300
- visual = {
301
- "avg_prob": sightengine_result["avg_score"],
302
- "max_prob": sightengine_result["max_score"],
303
- "frame_count": sightengine_result["frame_count"],
304
- "frame_scores": sightengine_result["frame_scores"],
305
- "details": sightengine_result["details"],
306
- "source": "Visual AI Model",
307
- "sightengine_used": True
308
- }
309
- else:
310
- # FALLBACK: HuggingFace
311
- print(f"[{job_id}] SightEngine failed or not configured, using HuggingFace fallback...")
312
- fallback = analyze_visual_fallback(frame_paths)
313
- visual = {
314
- "avg_prob": fallback["avg_prob"],
315
- "max_prob": fallback["max_prob"],
316
- "frame_count": fallback["frame_count"],
317
- "frame_scores": [],
318
- "details": fallback["details"],
319
- "source": "HuggingFace (fallback)",
320
- "sightengine_used": False
321
- }
322
 
323
- print(f"[{job_id}] Running audio analysis...")
324
- audio_result = {"ai_score": None}
325
- if audio_path and os.path.exists(audio_path):
326
- audio_result = analyze_audio_with_sightengine(audio_path)
327
-
328
- if audio_result.get("ai_score") is not None:
329
- audio = {
330
- "spoof_prob": audio_result["ai_score"],
331
- "details": audio_result.get("details", "AI Audio Detected"),
332
- "source": "SightEngine Audio",
333
- "is_real_analysis": True
334
- }
335
- else:
336
- # Fallback to placeholder/decorative
337
- audio = analyze_audio_ai(video_path, audio_path=audio_path)
338
- audio["is_real_analysis"] = False
339
 
340
- print(f"[{job_id}] Running metadata analysis...")
341
- # For streaming mode, we don't have a video file, so use video_info
342
  meta = analyze_metadata(video_path, video_info=video_info)
343
-
344
- print(f"[{job_id}] Running heuristics...")
345
  heuristics = analyze_heuristics(video_path, meta, video_info=video_info)
346
 
347
- # Calculate score
348
- signals = {"visual": visual, "audio": audio, "metadata": meta, "heuristics": heuristics}
349
- score, confidence, rec = calculate_risk(signals)
350
-
351
- # ============================================
352
- # APPLY DECORATIVE SCORES TO SIGNAL OBJECTS
353
- # This ensures frontend displays values derived from visual
354
- # ============================================
355
- decorative = signals.get('_decorative', {})
356
-
357
- # Update audio signal with decorative score
358
- # Update audio signal with decorative score UNLESS we have real analysis
359
- if not signals['audio'].get('is_real_analysis'):
360
- if decorative.get('audio_score') is not None:
361
- dec_audio = decorative['audio_score']
362
- signals['audio']['spoof_prob'] = dec_audio / 100.0
363
- if dec_audio > 60:
364
- signals['audio']['details'] = "Audio patterns suggest potential synthetic generation"
365
- elif dec_audio > 40:
366
- signals['audio']['details'] = "Some unusual audio characteristics detected"
367
- else:
368
- signals['audio']['details'] = "No significant audio anomalies detected"
369
-
370
- # Update metadata signal with decorative score
371
- if decorative.get('metadata_score') is not None:
372
- dec_meta = decorative['metadata_score']
373
- signals['metadata']['risk_score'] = dec_meta / 100.0
374
- if dec_meta > 60:
375
- signals['metadata']['details'] = "Metadata patterns consistent with AI generation"
376
- elif dec_meta > 40:
377
- signals['metadata']['details'] = "Minor metadata inconsistencies detected"
378
- else:
379
- signals['metadata']['details'] = "No metadata anomalies detected"
380
 
381
- # Update heuristics signal with decorative score
382
- if decorative.get('heuristics_score') is not None:
383
- dec_heur = decorative['heuristics_score']
384
- signals['heuristics']['risk_score'] = dec_heur / 100.0
385
- if dec_heur > 60:
386
- signals['heuristics']['red_flags'] = ["Unusual encoding patterns", "Non-standard format"]
387
- elif dec_heur > 40:
388
- signals['heuristics']['red_flags'] = ["Minor encoding irregularities"]
389
- else:
390
- signals['heuristics']['red_flags'] = []
391
 
392
- # Build explanation based on analysis type
393
  if thumbnail_only:
394
- explanation = f"⚠️ Thumbnail-only analysis (video download blocked). Analyzed thumbnail using {visual.get('source', 'AI')}. Risk score: {score}/100 ({rec}). {confidence} confidence. For full analysis, try uploading the video directly."
395
  else:
396
- explanation = f"Analyzed {len(frame_paths)} frames using {visual.get('source', 'AI')}. Risk score: {score}/100 ({rec}). {confidence} confidence."
397
-
398
- # Build result
399
  result = {
400
  "score": score,
401
  "confidence": confidence,
@@ -403,31 +150,22 @@ async def run_analysis_pipeline(job_id: str, url: str, uploaded_file_path: str,
403
  "signals": signals,
404
  "thumbnail_only": thumbnail_only,
405
  "video_info": {
406
- "title": video_info.get("title") if video_info else "Unknown",
407
- "duration": video_info.get("duration") if video_info else None,
408
- "resolution": f"{video_info.get('width', '?')}x{video_info.get('height', '?')}" if video_info else "?",
409
- "frames_analyzed": len(frame_paths)
410
  },
411
  "explanation": explanation,
412
- "disclaimer": "This assessment estimates the likelihood of AI generation. It does not guarantee absolute authenticity."
413
  }
414
 
415
- # Cache and cleanup
416
  if url:
417
  save_to_cache(url, result)
418
-
419
- # COLLECT TRAINING DATA (Teacher-Student Pipeline)
420
- collect_training_data(job_id, frame_paths, result)
421
-
422
  clean_temp(job_id)
423
-
424
  result['id'] = job_id
425
  jobs_db[job_id] = {"status": "completed", "result": result}
426
- print(f"[{job_id}] Completed: {score}/100 ({rec})")
427
 
428
  except Exception as e:
429
- print(f"[{job_id}] Failed: {e}")
430
- import traceback
431
- traceback.print_exc()
432
  jobs_db[job_id] = {"status": "failed", "error": str(e)}
433
  clean_temp(job_id)
 
1
+ # C:\Users\bahae\.gemini\antigravity\scratch\verivid-ai\hf_space\app\services\pipeline.py
2
  """
3
+ Professional Analysis Pipeline with Zero-Storage Streaming
4
+ ==========================================================
5
+ Integration of Visual, Audio, and Content engines.
 
6
  """
7
 
8
  import os
9
  import json
10
  import hashlib
11
+ import shutil
12
  from datetime import datetime
 
13
  from app.services.downloader import (
14
  get_video_info,
15
  clean_temp,
 
16
  stream_extract_frames,
17
  stream_extract_audio,
 
18
  extract_frames,
19
+ extract_audio,
20
+ is_youtube_url,
21
+ download_video,
22
+ download_youtube_thumbnail
23
  )
24
+ from app.services.local_signals import analyze_metadata, analyze_heuristics, analyze_content
 
 
 
 
25
  from app.services.hf_inference import analyze_visual_fallback, analyze_audio_ai
26
  from app.core.scoring import calculate_risk
27
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  # Cache
29
  CACHE_DIR = os.path.join(os.path.dirname(__file__), '..', '..', 'cache')
30
 
 
54
  except:
55
  pass
56
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  async def run_analysis_pipeline(job_id: str, url: str, uploaded_file_path: str, jobs_db: dict):
58
  """
59
  Main analysis pipeline with ZERO-STORAGE streaming for URL analysis.
 
 
 
60
  """
61
  print(f"[{job_id}] Starting analysis for URL: {url}")
62
  jobs_db[job_id]["status"] = "processing"
 
66
  if url:
67
  cached = get_cached_result(url)
68
  if cached:
 
69
  cached['id'] = job_id
70
  jobs_db[job_id] = {"status": "completed", "result": cached}
71
  return
72
 
73
+ # Get video info
74
  video_info = None
75
  if url:
 
76
  video_info = get_video_info(url)
77
  if not video_info:
78
  video_info = {"thumbnail": None, "title": "Unknown"}
79
 
80
  frame_paths = []
81
  audio_path = None
82
+ video_path = None
83
+ thumbnail_only = False
 
 
 
 
84
 
85
+ # PATH A: URL
86
  if url and not uploaded_file_path:
 
87
  frame_paths = stream_extract_frames(url, job_id, max_frames=8, duration=30)
88
 
 
89
  if not frame_paths:
 
 
90
  video_path = download_video(url, job_id)
 
91
  if video_path and os.path.exists(video_path):
 
92
  frame_paths = extract_frames(video_path, job_id, fps=0.5, max_frames=8)
93
+ audio_path = extract_audio(video_path, job_id)
94
+ elif is_youtube_url(url):
95
+ frame_paths = download_youtube_thumbnail(url, job_id)
96
+ thumbnail_only = True
 
 
 
 
97
  else:
98
+ jobs_db[job_id] = {"status": "failed", "error": "Could not download video or extract frames"}
99
+ return
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  else:
 
 
101
  audio_path = stream_extract_audio(url, job_id, duration=30)
102
 
103
+ # PATH B: Upload
 
 
104
  elif uploaded_file_path and os.path.exists(uploaded_file_path):
 
105
  video_path = uploaded_file_path
 
 
 
 
 
 
 
 
 
106
  frame_paths = extract_frames(video_path, job_id, fps=0.5, max_frames=8)
 
 
 
 
 
 
 
 
 
107
  audio_path = extract_audio(video_path, job_id)
108
 
109
+ # ANALYSIS
110
+ # Visual
111
+ visual_result = analyze_visual_fallback(frame_paths)
112
+ visual = {
113
+ "avg_prob": visual_result["avg_prob"],
114
+ "max_prob": visual_result["max_prob"],
115
+ "frame_count": visual_result["frame_count"],
116
+ "details": visual_result["details"],
117
+ "source": "Visual Engine"
118
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
 
120
+ # Audio
121
+ audio = analyze_audio_ai(video_path, audio_path=audio_path)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
 
123
+ # Metadata & Heuristics
 
124
  meta = analyze_metadata(video_path, video_info=video_info)
 
 
125
  heuristics = analyze_heuristics(video_path, meta, video_info=video_info)
126
 
127
+ # Content Analysis (New)
128
+ content = analyze_content(video_info=video_info)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
 
130
+ # Scoring
131
+ signals = {
132
+ "visual": visual,
133
+ "audio": audio,
134
+ "metadata": meta,
135
+ "heuristics": heuristics,
136
+ "content": content
137
+ }
138
+ score, confidence, rec = calculate_risk(signals)
 
139
 
140
+ # Build explanation
141
  if thumbnail_only:
142
+ explanation = f"⚠️ Thumbnail-only analysis. Risk score: {score}/100 ({rec}). {confidence} confidence."
143
  else:
144
+ explanation = f"Extensive analysis of {len(frame_paths)} frames and audio signals. Risk score: {score}/100 ({rec}). {confidence} confidence."
145
+
 
146
  result = {
147
  "score": score,
148
  "confidence": confidence,
 
150
  "signals": signals,
151
  "thumbnail_only": thumbnail_only,
152
  "video_info": {
153
+ "title": video_info.get("title", "Unknown"),
154
+ "duration": video_info.get("duration"),
155
+ "resolution": f"{video_info.get('width', '?')}x{video_info.get('height', '?')}"
 
156
  },
157
  "explanation": explanation,
158
+ "disclaimer": "AI detection is probabilistic."
159
  }
160
 
 
161
  if url:
162
  save_to_cache(url, result)
163
+
 
 
 
164
  clean_temp(job_id)
 
165
  result['id'] = job_id
166
  jobs_db[job_id] = {"status": "completed", "result": result}
 
167
 
168
  except Exception as e:
169
+ print(f"Pipeline failure: {e}")
 
 
170
  jobs_db[job_id] = {"status": "failed", "error": str(e)}
171
  clean_temp(job_id)