Spaces:
Sleeping
Sleeping
Download backend/app.py from PixelNavigation/Unmask: direct link, hf CLI and curl.
- Browser
- Download file 14.3 kB
-
https://huggingface.co/spaces/PixelNavigation/Unmask/resolve/main/backend/app.py
- Command line
-
hf download hf://spaces/PixelNavigation/Unmask/backend/app.py
-
curl -L -o app.py https://huggingface.co/spaces/PixelNavigation/Unmask/resolve/main/backend/app.py
14.3 kB
| from flask import Flask, request, jsonify, send_from_directory | |
| from flask_cors import CORS | |
| from werkzeug.utils import secure_filename | |
| import os | |
| import sys | |
| import tempfile | |
| import traceback | |
| import logging | |
| import gc | |
| import torch | |
| import numpy as np | |
| import re | |
| from PIL import Image | |
| def _ensure_writable_caches(): | |
| """ | |
| Redirect all model / hub caches to writable tmp directories to avoid | |
| PermissionError inside container (e.g. attempts to write to '/.cache'). | |
| Safe to call multiple times. | |
| """ | |
| tmp_root = os.environ.get("TMPDIR") or tempfile.gettempdir() | |
| base_cache = os.path.join(tmp_root, ".cache") | |
| subdirs = { | |
| "HF_HOME": "huggingface", | |
| "HF_HUB_CACHE": "huggingface", | |
| "TRANSFORMERS_CACHE": "huggingface", | |
| "TORCH_HOME": "torch", | |
| "TIMM_CACHE_DIR": "timm", | |
| "XDG_CACHE_HOME": "", # root of the cache tree | |
| } | |
| for var, sub in subdirs.items(): | |
| path = os.environ.get(var) | |
| if not path: | |
| path = os.path.join(base_cache, sub) if sub else base_cache | |
| os.environ[var] = path | |
| os.makedirs(path, exist_ok=True) | |
| # Quiet albumentations update check noise | |
| os.environ.setdefault("NO_ALBUMENTATIONS_UPDATE", "1") | |
| _ensure_writable_caches() | |
| # Add model directory before importing model utilities | |
| sys.path.append('dfdc_model') | |
| try: | |
| from dfdc_model.kernel_utils import VideoReader, FaceExtractor, confident_strategy, predict_on_video | |
| from dfdc_model.training.zoo.classifiers import DeepFakeClassifier | |
| from enhanced_prediction import enhanced_predict_on_video, enhanced_predict_on_image | |
| except Exception as e: | |
| traceback.print_exc() | |
| with open(os.path.join(tempfile.gettempdir(), "startup_error.log"), "w", encoding="utf-8") as f: | |
| f.write(traceback.format_exc()) | |
| VideoReader = FaceExtractor = confident_strategy = predict_on_video = None | |
| DeepFakeClassifier = None | |
| app = Flask(__name__) | |
| CORS(app) | |
| # Choose a writable upload folder | |
| app.config["UPLOAD_FOLDER"] = os.environ.get("UPLOAD_FOLDER") or os.path.join(tempfile.gettempdir(), "uploads") | |
| os.makedirs(app.config["UPLOAD_FOLDER"], exist_ok=True) | |
| # Choose an upload folder that is writable in container environments. | |
| # Prefer environment override, then /tmp/uploads, then a tempdir fallback. | |
| upload_folder = os.environ.get('UPLOAD_FOLDER', '/tmp/uploads') | |
| try: | |
| os.makedirs(upload_folder, exist_ok=True) | |
| except PermissionError: | |
| # Fallback to system temp directory if the preferred path is not writable | |
| fallback = os.path.join(tempfile.gettempdir(), 'unmask_uploads') | |
| try: | |
| os.makedirs(fallback, exist_ok=True) | |
| upload_folder = fallback | |
| print(f"Warning: preferred upload folder not writable, using fallback: {fallback}") | |
| except Exception as e: | |
| # As a last resort, set upload_folder to tempfile.gettempdir() | |
| upload_folder = tempfile.gettempdir() | |
| print(f"Critical: unable to create upload directories, using system temp: {upload_folder} ({e})") | |
| app.config['UPLOAD_FOLDER'] = upload_folder | |
| def cleanup_memory(): | |
| """Clean up memory and GPU cache""" | |
| gc.collect() | |
| if torch.cuda.is_available(): | |
| torch.cuda.empty_cache() | |
| torch.cuda.synchronize() | |
| def cleanup_file(filepath): | |
| """Clean up uploaded file""" | |
| try: | |
| if os.path.exists(filepath): | |
| os.remove(filepath) | |
| except Exception as e: | |
| print(f"Error removing file {filepath}: {e}") | |
| def smart_prediction_strategy(predictions): | |
| """ | |
| Improved prediction strategy that considers both count and confidence of fake frames | |
| """ | |
| predictions = np.array(predictions) | |
| if len(predictions) == 0: | |
| return 0.5 | |
| # Count frames above different thresholds | |
| fake_frames_50 = np.sum(predictions > 0.5) # Standard threshold | |
| fake_frames_60 = np.sum(predictions > 0.6) # Higher confidence | |
| fake_frames_70 = np.sum(predictions > 0.7) # Very high confidence | |
| total_frames = len(predictions) | |
| fake_ratio = fake_frames_50 / total_frames | |
| print(f"Debug: {fake_frames_50}/{total_frames} frames suspicious ({fake_ratio:.2%})") | |
| print(f"Debug: Mean prediction: {np.mean(predictions):.3f}") | |
| # If majority of frames are suspicious (>50%), classify as fake | |
| if fake_ratio > 0.5: | |
| # Weight the prediction based on how many frames are suspicious | |
| if fake_frames_70 > total_frames * 0.3: # 30%+ very high confidence | |
| return min(0.95, np.mean(predictions[predictions > 0.7])) | |
| elif fake_frames_60 > total_frames * 0.4: # 40%+ high confidence | |
| return min(0.9, np.mean(predictions[predictions > 0.6])) | |
| else: | |
| # Use weighted average favoring suspicious frames | |
| suspicious_mean = np.mean(predictions[predictions > 0.5]) | |
| overall_mean = np.mean(predictions) | |
| return (suspicious_mean * 0.7 + overall_mean * 0.3) | |
| # If significant minority is suspicious (30-50%), be more conservative | |
| elif fake_ratio > 0.3: | |
| return min(0.7, np.mean(predictions)) | |
| # Otherwise use standard mean | |
| else: | |
| return np.mean(predictions) | |
| # Initialize the model and preprocessing pipeline | |
| def load_models(): | |
| models = [] | |
| weights_dir = "dfdc_model/weights" | |
| # Update these model names based on your downloaded weights | |
| model_files = [ | |
| "final_888_DeepFakeClassifier_tf_efficientnet_b7_ns_0_40", | |
| # Add more model files if you have them | |
| ] | |
| device = "cuda" if torch.cuda.is_available() else "cpu" | |
| for model_file in model_files: | |
| model_path = os.path.join(weights_dir, model_file) | |
| if os.path.exists(model_path): | |
| model = DeepFakeClassifier(encoder="tf_efficientnet_b7_ns").to(device) | |
| print(f"Loading state dict {model_path}") | |
| checkpoint = torch.load(model_path, map_location="cpu") | |
| state_dict = checkpoint.get("state_dict", checkpoint) | |
| model.load_state_dict({re.sub("^module.", "", k): v for k, v in state_dict.items()}, strict=True) | |
| model.eval() | |
| del checkpoint | |
| if device == "cuda": | |
| models.append(model.half()) | |
| else: | |
| models.append(model) | |
| return models | |
| # Initialize the preprocessing pipeline | |
| def initialize_preprocessing(): | |
| frames_per_video = 32 | |
| video_reader = VideoReader() | |
| video_read_fn = lambda x: video_reader.read_frames(x, num_frames=frames_per_video) | |
| face_extractor = FaceExtractor(video_read_fn) | |
| input_size = 380 | |
| return face_extractor, input_size, frames_per_video | |
| # Load models and initialize preprocessing | |
| print("Loading models...") | |
| models = [] | |
| try: | |
| models = load_models() | |
| print(f"Loaded {len(models)} models") | |
| except Exception: | |
| tb = traceback.format_exc() | |
| log_path = os.path.join(tempfile.gettempdir(), 'startup_error.log') | |
| try: | |
| with open(log_path, 'w', encoding='utf-8') as f: | |
| f.write('Model loading failed. Traceback:\n') | |
| f.write(tb) | |
| except Exception as write_err: | |
| print(f"Failed writing startup error log: {write_err}") | |
| print(f"Model loading failed; continuing with 0 models. Trace written to {log_path}") | |
| face_extractor, input_size, frames_per_video = initialize_preprocessing() | |
| def predict_on_image(image_path): | |
| """Predict on a single image using face extraction and model inference""" | |
| try: | |
| from dfdc_model.kernel_utils import isotropically_resize_image, put_to_center, normalize_transform | |
| import cv2 | |
| from PIL import Image as PILImage | |
| image = cv2.imread(image_path) | |
| if image is None: | |
| return 0.5 | |
| image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) | |
| img_pil = PILImage.fromarray(image) | |
| img_pil = img_pil.resize(size=[s // 2 for s in img_pil.size]) | |
| device = "cuda" if torch.cuda.is_available() else "cpu" | |
| detector = face_extractor.detector | |
| batch_boxes, probs = detector.detect(img_pil, landmarks=False) | |
| if batch_boxes is None: | |
| return 0.5 | |
| best_idx = 0 | |
| if probs is not None and len(probs) > 1: | |
| best_idx = int(np.argmax(probs)) | |
| bbox = batch_boxes[best_idx] | |
| if bbox is None: | |
| return 0.5 | |
| xmin, ymin, xmax, ymax = [int(b * 2) for b in bbox] | |
| w = xmax - xmin | |
| h = ymax - ymin | |
| p_h = h // 3 | |
| p_w = w // 3 | |
| crop = image[max(ymin - p_h, 0):ymax + p_h, max(xmin - p_w, 0):xmax + p_w] | |
| resized_face = isotropically_resize_image(crop, input_size) | |
| resized_face = put_to_center(resized_face, input_size) | |
| x = torch.tensor(resized_face, device=device).float().unsqueeze(0) | |
| x = x.permute((0, 3, 1, 2)) | |
| x = normalize_transform(x[0] / 255.).unsqueeze(0) | |
| with torch.no_grad(): | |
| preds = [] | |
| for model in models: | |
| if device == "cuda": | |
| try: | |
| y_pred = model(x.half()) | |
| except Exception: | |
| y_pred = model(x) | |
| else: | |
| y_pred = model(x) | |
| y_pred = torch.sigmoid(y_pred.squeeze()) | |
| preds.append(y_pred.cpu().numpy()) | |
| del x, y_pred | |
| cleanup_memory() | |
| return np.mean(preds) | |
| return 0.5 | |
| except Exception as e: | |
| print(f"Error predicting on image: {e}") | |
| cleanup_memory() | |
| return 0.5 | |
| def index(): | |
| # If a static frontend exists (Next.js export), serve it; otherwise return JSON | |
| index_path = os.path.join(app.static_folder or '', 'index.html') | |
| if app.static_folder and os.path.exists(index_path): | |
| return send_from_directory(app.static_folder, 'index.html') | |
| return jsonify({'message': 'Deepfake Detection Flask API is running.'}) | |
| # Serve other static assets (JS/CSS) when using exported Next.js | |
| def static_proxy(path): | |
| if app.static_folder: | |
| file_path = os.path.join(app.static_folder, path) | |
| if os.path.exists(file_path): | |
| return send_from_directory(app.static_folder, path) | |
| return jsonify({'error': 'Not found'}), 404 | |
| def analyze_image(): | |
| if 'file' not in request.files: | |
| return jsonify({'error': 'No file part'}), 400 | |
| file = request.files['file'] | |
| if file.filename == '': | |
| return jsonify({'error': 'No selected file'}), 400 | |
| filename = secure_filename(file.filename) | |
| filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename) | |
| file.save(filepath) | |
| try: | |
| # Use enhanced image prediction with Grad-CAM | |
| result = enhanced_predict_on_image(filepath, models, input_size) | |
| label = 'fake' if result['prediction'] > 0.5 else 'real' | |
| response = { | |
| 'label': label, | |
| 'confidence': float(result['prediction']), | |
| 'details': f'Image analyzed using {len(models)} models with face detection and Grad-CAM', | |
| 'faces_detected': 1, | |
| 'processing_time': 'N/A', | |
| 'gradcam_image': result['gradcam_image'], | |
| 'original_image': result['original_image'], | |
| 'fake_frames': [] # Not applicable for images | |
| } | |
| # Clean up uploaded file and memory | |
| cleanup_file(filepath) | |
| cleanup_memory() | |
| return jsonify(response) | |
| except Exception as e: | |
| cleanup_file(filepath) | |
| cleanup_memory() | |
| return jsonify({'error': str(e)}), 500 | |
| def analyze_video(): | |
| if 'file' not in request.files: | |
| return jsonify({'error': 'No file part'}), 400 | |
| file = request.files['file'] | |
| if file.filename == '': | |
| return jsonify({'error': 'No selected file'}), 400 | |
| filename = secure_filename(file.filename) | |
| filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename) | |
| file.save(filepath) | |
| try: | |
| # Use enhanced video prediction with frame analysis and Grad-CAM | |
| result = enhanced_predict_on_video( | |
| face_extractor=face_extractor, | |
| video_path=filepath, | |
| batch_size=frames_per_video, | |
| input_size=input_size, | |
| models=models, | |
| strategy=smart_prediction_strategy, # Use our improved strategy | |
| apply_compression=False, | |
| return_frame_predictions=True | |
| ) | |
| prediction = result['prediction'] | |
| label = 'fake' if prediction > 0.5 else 'real' | |
| # Calculate some statistics for better details | |
| fake_frames = result.get('fake_frames', []) | |
| total_frames = len(result.get('frame_predictions', [])) | |
| fake_ratio = len(fake_frames) / max(total_frames, 1) * 100 | |
| # Get gradcam images | |
| gradcam_data = result.get('gradcam_images', []) | |
| gradcam_image = None | |
| original_image = None | |
| if gradcam_data and len(gradcam_data) > 0: | |
| if isinstance(gradcam_data[0], dict): | |
| gradcam_image = gradcam_data[0].get('gradcam') | |
| original_image = gradcam_data[0].get('original') | |
| else: | |
| # Fallback for old format | |
| gradcam_image = gradcam_data[0] | |
| response = { | |
| 'label': label, | |
| 'confidence': float(prediction), | |
| 'details': f'Video analyzed: {len(fake_frames)}/{total_frames} frames flagged as suspicious ({fake_ratio:.1f}%). Using smart prediction strategy.', | |
| 'faces_detected': len(result.get('frame_predictions', [])), | |
| 'processing_time': 'N/A', | |
| 'fake_frames': fake_frames, | |
| 'gradcam_image': gradcam_image, | |
| 'original_image': original_image, | |
| 'frame_predictions': result.get('frame_predictions', []) | |
| } | |
| # Clean up uploaded file and memory | |
| cleanup_file(filepath) | |
| cleanup_memory() | |
| return jsonify(response) | |
| except Exception as e: | |
| cleanup_file(filepath) | |
| cleanup_memory() | |
| return jsonify({'error': str(e)}), 500 | |
| if __name__ == '__main__': | |
| app.run(host='0.0.0.0', port=8000) | |