Spaces:
Running
Running
Anish commited on
Commit ·
0dab55c
1
Parent(s): 13b0169
[Feature Added] > Video Pipeline. Have to update the confidence and ai_analysis param for images, where they won't appear.
Browse files- backend/ai_detector.db +0 -0
- backend/app/ai/video/frame_detector.py +2 -2
- backend/app/api/file_routes.py +4 -0
- backend/app/models/file_model.py +3 -1
- backend/app/security/video_validator.py +22 -11
- backend/app/services/video_processor.py +86 -0
- backend/app/utils/file_handler.py +2 -0
- backend/app/worker/tasks.py +8 -1
backend/ai_detector.db
ADDED
|
File without changes
|
backend/app/ai/video/frame_detector.py
CHANGED
|
@@ -12,8 +12,8 @@ class FrameDetector:
|
|
| 12 |
|
| 13 |
try:
|
| 14 |
model_id = "prithivMLmods/Deep-Fake-Detector-Model"
|
| 15 |
-
self.processor =
|
| 16 |
-
self.model =
|
| 17 |
self.model = self.model.to(self.device)
|
| 18 |
self.model.eval()
|
| 19 |
|
|
|
|
| 12 |
|
| 13 |
try:
|
| 14 |
model_id = "prithivMLmods/Deep-Fake-Detector-Model"
|
| 15 |
+
self.processor = AutoImageProcessor.from_pretrained(model_id)
|
| 16 |
+
self.model = AutoModelForImageClassification.from_pretrained(model_id)
|
| 17 |
self.model = self.model.to(self.device)
|
| 18 |
self.model.eval()
|
| 19 |
|
backend/app/api/file_routes.py
CHANGED
|
@@ -31,6 +31,8 @@ def list_user_files(
|
|
| 31 |
"size": f.filesize,
|
| 32 |
"status": f.status,
|
| 33 |
"result": f.result,
|
|
|
|
|
|
|
| 34 |
"uploaded_at": f.created_at
|
| 35 |
}
|
| 36 |
for f in files
|
|
@@ -57,6 +59,8 @@ def get_file(
|
|
| 57 |
"size": file.filesize,
|
| 58 |
"status": file.status,
|
| 59 |
"result": file.result,
|
|
|
|
|
|
|
| 60 |
"uploaded_at": file.created_at
|
| 61 |
}
|
| 62 |
|
|
|
|
| 31 |
"size": f.filesize,
|
| 32 |
"status": f.status,
|
| 33 |
"result": f.result,
|
| 34 |
+
"confidence": f.confidence,
|
| 35 |
+
"ai_explanation": f.ai_explanation,
|
| 36 |
"uploaded_at": f.created_at
|
| 37 |
}
|
| 38 |
for f in files
|
|
|
|
| 59 |
"size": file.filesize,
|
| 60 |
"status": file.status,
|
| 61 |
"result": file.result,
|
| 62 |
+
"confidence": file.confidence,
|
| 63 |
+
"ai_explanation": file.ai_explanation,
|
| 64 |
"uploaded_at": file.created_at
|
| 65 |
}
|
| 66 |
|
backend/app/models/file_model.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey
|
| 2 |
from datetime import datetime, UTC
|
| 3 |
from app.db.database import Base
|
| 4 |
|
|
@@ -17,5 +17,7 @@ class File(Base):
|
|
| 17 |
|
| 18 |
status = Column(String, default="uploaded")
|
| 19 |
result = Column(String, nullable=True)
|
|
|
|
|
|
|
| 20 |
|
| 21 |
model_version_used = Column(String, nullable=True)
|
|
|
|
| 1 |
+
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Float
|
| 2 |
from datetime import datetime, UTC
|
| 3 |
from app.db.database import Base
|
| 4 |
|
|
|
|
| 17 |
|
| 18 |
status = Column(String, default="uploaded")
|
| 19 |
result = Column(String, nullable=True)
|
| 20 |
+
confidence = Column(Float, nullable=True)
|
| 21 |
+
ai_explanation = Column(String, nullable=True)
|
| 22 |
|
| 23 |
model_version_used = Column(String, nullable=True)
|
backend/app/security/video_validator.py
CHANGED
|
@@ -29,19 +29,23 @@ class VideoValidator:
|
|
| 29 |
|
| 30 |
bytes_read = 0
|
| 31 |
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
|
| 41 |
-
|
| 42 |
file.file.seek(0)
|
| 43 |
-
|
| 44 |
-
cap = cv2.VideoCapture(temp_video.name)
|
| 45 |
|
| 46 |
if not cap.isOpened():
|
| 47 |
logger.warning("Security Alert: OpenCV failed to open the video container. File may be corrupted or disguised.")
|
|
@@ -55,6 +59,13 @@ class VideoValidator:
|
|
| 55 |
logger.warning("Security Alert: OpenCV opened container but failed to extract any visual frames.")
|
| 56 |
return False, "Video contains no readable visual frames."
|
| 57 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
return True, "Valid"
|
| 59 |
|
| 60 |
except Exception as e:
|
|
|
|
| 29 |
|
| 30 |
bytes_read = 0
|
| 31 |
|
| 32 |
+
temp_path = None
|
| 33 |
+
try:
|
| 34 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix=ext) as temp_video:
|
| 35 |
+
temp_path = temp_video.name
|
| 36 |
+
while chunk := file.file.read(1024 * 1024): # Read 1 MB at a time
|
| 37 |
+
bytes_read += len(chunk)
|
| 38 |
+
if bytes_read > cls.MAX_FILE_BYTES:
|
| 39 |
+
logger.warning(f"Security Alert: Blocked upload exceeding {cls.MAX_FILE_BYTES} bytes")
|
| 40 |
+
return False, "File size exceeds the maximum allowed limit"
|
| 41 |
+
|
| 42 |
+
temp_video.write(chunk)
|
| 43 |
+
|
| 44 |
+
temp_video.flush()
|
| 45 |
|
| 46 |
+
# File is safely closed by Python now, so OpenCV can gain read-access on Windows
|
| 47 |
file.file.seek(0)
|
| 48 |
+
cap = cv2.VideoCapture(temp_path)
|
|
|
|
| 49 |
|
| 50 |
if not cap.isOpened():
|
| 51 |
logger.warning("Security Alert: OpenCV failed to open the video container. File may be corrupted or disguised.")
|
|
|
|
| 59 |
logger.warning("Security Alert: OpenCV opened container but failed to extract any visual frames.")
|
| 60 |
return False, "Video contains no readable visual frames."
|
| 61 |
|
| 62 |
+
finally:
|
| 63 |
+
if temp_path and os.path.exists(temp_path):
|
| 64 |
+
try:
|
| 65 |
+
os.remove(temp_path)
|
| 66 |
+
except:
|
| 67 |
+
pass
|
| 68 |
+
|
| 69 |
return True, "Valid"
|
| 70 |
|
| 71 |
except Exception as e:
|
backend/app/services/video_processor.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import time
|
| 2 |
+
import logging
|
| 3 |
+
import os
|
| 4 |
+
from sqlalchemy.orm import Session
|
| 5 |
+
from app.models.file_model import File
|
| 6 |
+
from app.ai.video.frame_extractor import extract_frames
|
| 7 |
+
from app.ai.video.frame_detector import analyze_frame
|
| 8 |
+
from app.ai.video.motion_detector import compute_motion_anomaly
|
| 9 |
+
from app.ai.video.noise_entropy_detector import compute_noise_entropy_anomaly
|
| 10 |
+
from app.ai.video.metadata_analyzer import analyze_metadata
|
| 11 |
+
from app.ai.video.aggregator import aggregate_scores
|
| 12 |
+
|
| 13 |
+
logger = logging.getLogger(__name__)
|
| 14 |
+
|
| 15 |
+
def process_video_pipeline(file_id: int, file_path: str, db: Session):
|
| 16 |
+
start_time = time.time()
|
| 17 |
+
logger.info(f"Starting Video Pipeline for file_id: {file_id}")
|
| 18 |
+
|
| 19 |
+
try:
|
| 20 |
+
db_file = db.query(File).filter(File.id == file_id).first()
|
| 21 |
+
if not db_file:
|
| 22 |
+
logger.error(f"File ID {file_id} not found in DB.")
|
| 23 |
+
return
|
| 24 |
+
|
| 25 |
+
md_score, md_dict = analyze_metadata(file_path)
|
| 26 |
+
|
| 27 |
+
frame_scores = []
|
| 28 |
+
motion_scores = []
|
| 29 |
+
noise_scores = []
|
| 30 |
+
|
| 31 |
+
previous_frame = None
|
| 32 |
+
|
| 33 |
+
for frame in extract_frames(file_path, sample_rate=15, max_frames=50):
|
| 34 |
+
|
| 35 |
+
# Module A: ViT Image Forensic Analysis
|
| 36 |
+
f_score = analyze_frame(frame)
|
| 37 |
+
frame_scores.append(f_score)
|
| 38 |
+
|
| 39 |
+
# Module B: Microscopic Silicon Noise Analysis
|
| 40 |
+
n_score = compute_noise_entropy_anomaly(frame)
|
| 41 |
+
if n_score is not None:
|
| 42 |
+
noise_scores.append(n_score)
|
| 43 |
+
|
| 44 |
+
# Module C: Farneback Optical Flow
|
| 45 |
+
if previous_frame is not None:
|
| 46 |
+
m_score = compute_motion_anomaly(previous_frame, frame)
|
| 47 |
+
|
| 48 |
+
if m_score is not None:
|
| 49 |
+
motion_scores.append(m_score)
|
| 50 |
+
|
| 51 |
+
previous_frame = frame
|
| 52 |
+
|
| 53 |
+
avg_motion = sum(motion_scores)/len(motion_scores) if motion_scores else None
|
| 54 |
+
avg_noise = sum(noise_scores)/len(noise_scores) if noise_scores else None
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
final_verdict = aggregate_scores(
|
| 58 |
+
frame_scores=frame_scores,
|
| 59 |
+
motion_score=avg_motion,
|
| 60 |
+
noise_score=avg_noise,
|
| 61 |
+
metadata_score=md_score
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
db_file.status = "Completed"
|
| 65 |
+
db_file.result = f"{final_verdict['label']} ({final_verdict['probability']*100:.1f}%)"
|
| 66 |
+
db_file.confidence = final_verdict['probability']
|
| 67 |
+
db_file.ai_explanation = final_verdict['explanation']
|
| 68 |
+
|
| 69 |
+
db.commit()
|
| 70 |
+
|
| 71 |
+
elapsed = time.time() - start_time
|
| 72 |
+
logger.info(f"Successfully processed video {file_id} in {elapsed:.2f} seconds.")
|
| 73 |
+
logger.info(f"Verdict: {db_file.result}")
|
| 74 |
+
|
| 75 |
+
except Exception as e:
|
| 76 |
+
logger.error(f"FATAL Pipeline Crash for file_id {file_id}: {str(e)}")
|
| 77 |
+
|
| 78 |
+
try:
|
| 79 |
+
db_file = db.query(File).filter(File.id == file_id).first()
|
| 80 |
+
|
| 81 |
+
if db_file:
|
| 82 |
+
db_file.status = "Failed"
|
| 83 |
+
db_file.result = str(e)
|
| 84 |
+
db.commit()
|
| 85 |
+
except:
|
| 86 |
+
pass
|
backend/app/utils/file_handler.py
CHANGED
|
@@ -26,8 +26,10 @@ def validate_file(file: UploadFile):
|
|
| 26 |
if not is_valid:
|
| 27 |
raise HTTPException(status_code=400, detail=validation_reason)
|
| 28 |
|
|
|
|
| 29 |
return file.size or 0
|
| 30 |
|
|
|
|
| 31 |
file_bytes = file.file.read()
|
| 32 |
size = len(file_bytes)
|
| 33 |
|
|
|
|
| 26 |
if not is_valid:
|
| 27 |
raise HTTPException(status_code=400, detail=validation_reason)
|
| 28 |
|
| 29 |
+
file.file.seek(0) # CRITICAL: Reset the pointer after Video validation reads it!
|
| 30 |
return file.size or 0
|
| 31 |
|
| 32 |
+
file.file.seek(0)
|
| 33 |
file_bytes = file.file.read()
|
| 34 |
size = len(file_bytes)
|
| 35 |
|
backend/app/worker/tasks.py
CHANGED
|
@@ -41,7 +41,14 @@ def process_file_task(self, file_id: int):
|
|
| 41 |
|
| 42 |
logger.info(f"Starting Celery worker process for file_id: {file_id}")
|
| 43 |
try:
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
|
| 46 |
job.status = "COMPLETED"
|
| 47 |
job.finished_at = datetime.now(UTC)
|
|
|
|
| 41 |
|
| 42 |
logger.info(f"Starting Celery worker process for file_id: {file_id}")
|
| 43 |
try:
|
| 44 |
+
from app.models.file_model import File
|
| 45 |
+
db_file = db.query(File).filter(File.id == file_id).first()
|
| 46 |
+
|
| 47 |
+
if db_file and db_file.filetype and db_file.filetype.startswith('video/'):
|
| 48 |
+
from app.services.video_processor import process_video_pipeline
|
| 49 |
+
process_video_pipeline(file_id, db_file.filepath, db)
|
| 50 |
+
else:
|
| 51 |
+
process_file(file_id, db)
|
| 52 |
|
| 53 |
job.status = "COMPLETED"
|
| 54 |
job.finished_at = datetime.now(UTC)
|