{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":16880,"databundleVersionId":858837},{"sourceType":"datasetVersion","sourceId":5380830,"datasetId":3120670,"databundleVersionId":5454468},{"sourceType":"modelInstanceVersion","sourceId":669664,"databundleVersionId":14729086,"modelInstanceId":507115,"modelId":521857}],"dockerImageVersionId":31287,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nimport numpy as np\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader, WeightedRandomSampler\nfrom torchvision import transforms, models\nfrom sklearn.model_selection import train_test_split,StratifiedKFold\nfrom sklearn.metrics import precision_score, recall_score, confusion_matrix, roc_auc_score,log_loss\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nimport cv2\nfrom tqdm import tqdm\nimport random\nimport copy\nimport librosa\nfrom torch.cuda.amp import autocast, GradScaler\nimport warnings\n\nwarnings.filterwarnings(\"ignore\")\n\n# -------------------------\n# 1. Configuration\n# -------------------------\nCONFIG = {\n    'data_root': '/kaggle/input/datasets/reubensuju/celeb-df-v2',  # Update this path if needed\n    'batch_size': 4,\n    'num_frames': 20,\n    'frame_size': 224,\n    'num_epochs': 10,\n    'learning_rate': 1e-4,\n    'weight_decay': 1e-5,\n    'lstm_hidden_dim': 512,\n    'dropout_rate': 0.5,\n    'scheduler_patience': 3,\n    'scheduler_factor': 0.5,\n    'early_stopping_patience': 5,\n    'model_save_path': './hybrid_model_checkpoints',\n    'num_workers': 2,\n    # Audio Params\n    'audio_sr': 16000,\n    'n_mfcc': 40,\n    'audio_duration': 2.0,  # Duration of audio to analyze per video\n}\n\nos.makedirs(CONFIG['model_save_path'], exist_ok=True)\n\n# -------------------------\n# 2. Utilities\n# -------------------------\ndef set_seed(seed=42):\n    random.seed(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    if torch.cuda.is_available():\n        torch.cuda.manual_seed(seed)\n    torch.backends.cudnn.deterministic = True\n\nset_seed(42)\n\n# -------------------------\n# 3. Data Processing (Video + Audio)\n# -------------------------\ndef extract_frames(video_path, num_frames):\n    cap = cv2.VideoCapture(video_path)\n    if not cap.isOpened():\n        return np.zeros((num_frames, CONFIG['frame_size'], CONFIG['frame_size'], 3), dtype=np.uint8)\n    \n    total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))\n    if total_frames <= 0:\n        cap.release()\n        return np.zeros((num_frames, CONFIG['frame_size'], CONFIG['frame_size'], 3), dtype=np.uint8)\n    \n    indices = np.linspace(0, total_frames - 1, num_frames, dtype=int)\n    frames = []\n    \n    for idx in indices:\n        cap.set(cv2.CAP_PROP_POS_FRAMES, idx)\n        ret, frame = cap.read()\n        if ret:\n            frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)\n            frame = cv2.resize(frame, (CONFIG['frame_size'], CONFIG['frame_size']))\n            frames.append(frame)\n        else:\n            frames.append(np.zeros((CONFIG['frame_size'], CONFIG['frame_size'], 3), dtype=np.uint8))\n            \n    cap.release()\n    return np.array(frames)\n\ndef compute_freq_images(frames):\n    n, h, w, c = frames.shape\n    freq_images = np.zeros((n, h, w, 1), dtype=np.uint8)\n    for i in range(n):\n        img = frames[i].astype(np.float32)\n        gray = 0.299 * img[:, :, 0] + 0.587 * img[:, :, 1] + 0.114 * img[:, :, 2]\n        f = np.fft.fft2(gray)\n        fshift = np.fft.fftshift(f)\n        mag = np.log(np.abs(fshift) + 1e-8)\n        norm = cv2.normalize(mag, None, 0, 255, cv2.NORM_MINMAX)\n        freq_images[i, :, :, 0] = norm.astype(np.uint8)\n    return freq_images\n\ndef extract_audio_features(video_path, target_sr=16000, n_mfcc=40, duration=2.0):\n    try:\n        # Load audio with librosa (only first 'duration' seconds)\n        y, sr = librosa.load(video_path, sr=target_sr, duration=duration, mono=True)\n        \n        # Pad if too short\n        required_len = int(duration * target_sr)\n        if len(y) < required_len:\n            y = np.pad(y, (0, required_len - len(y)))\n        else:\n            y = y[:required_len]\n            \n        # Compute MFCC\n        mfcc = librosa.feature.mfcc(y=y, sr=sr, n_mfcc=n_mfcc)\n        # Transpose to (Time, n_mfcc) -> (Seq_Len, 40)\n        return mfcc.T \n    except Exception as e:\n        # Return zeros if no audio or error\n        # Time steps roughly = (Duration * SR) / Hop_Length. Default hop is 512.\n        # 2.0 * 16000 / 512 ≈ 63 frames. We'll fix size later or pool it.\n        return np.zeros((64, n_mfcc), dtype=np.float32)\n\nclass HybridDataset(Dataset):\n    def __init__(self, video_paths, labels, transform=None):\n        self.video_paths = video_paths\n        self.labels = labels\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.video_paths)\n\n    def __getitem__(self, idx):\n        path = self.video_paths[idx]\n        label = self.labels[idx]\n\n        # 1. Video Frames\n        frames = extract_frames(path, CONFIG['num_frames'])\n        freq_frames = compute_freq_images(frames)\n        \n        # 2. Audio Features\n        mfcc = extract_audio_features(path, CONFIG['audio_sr'], CONFIG['n_mfcc'], CONFIG['audio_duration'])\n        \n        # 3. Transform & Tensorize Video\n        rgb_tensors = []\n        freq_tensors = []\n        \n        if self.transform:\n            for i in range(CONFIG['num_frames']):\n                # RGB\n                t_rgb = self.transform(frames[i])\n                \n                # Freq (Expand to 3 channels for EfficientNet)\n                f_freq_3ch = np.repeat(freq_frames[i], 3, axis=2)\n                t_freq = self.transform(f_freq_3ch)\n                \n                rgb_tensors.append(t_rgb)\n                freq_tensors.append(t_freq)\n        else:\n            # Fallback transform\n            to_tensor = transforms.ToTensor()\n            for i in range(CONFIG['num_frames']):\n                rgb_tensors.append(to_tensor(frames[i]))\n                f_freq_3ch = np.repeat(freq_frames[i], 3, axis=2)\n                freq_tensors.append(to_tensor(f_freq_3ch))\n\n        rgb_tensor = torch.stack(rgb_tensors)   # [Seq, 3, H, W]\n        freq_tensor = torch.stack(freq_tensors) # [Seq, 3, H, W]\n        \n        # Audio Tensor\n        audio_tensor = torch.tensor(mfcc, dtype=torch.float32) # [Audio_Seq, 40]\n        # Ensure fixed size for batching (truncate or pad to e.g., 64 steps)\n        target_audio_len = 64\n        if audio_tensor.shape[0] > target_audio_len:\n            audio_tensor = audio_tensor[:target_audio_len, :]\n        elif audio_tensor.shape[0] < target_audio_len:\n            padding = torch.zeros((target_audio_len - audio_tensor.shape[0], CONFIG['n_mfcc']))\n            audio_tensor = torch.cat([audio_tensor, padding], dim=0)\n\n        return rgb_tensor, freq_tensor, audio_tensor, torch.tensor(label, dtype=torch.float)\n\n# -------------------------\n# 4. Model Components\n# -------------------------\n\n# --- A. Modality Attention (From your instruction) ---\nclass ModalityAttentionPerFrame(nn.Module):\n    def __init__(self, feat_dim, hidden=256, dropout=0.1):\n        super().__init__()\n        self.mlp = nn.Sequential(\n            nn.Linear(feat_dim * 2, hidden),\n            nn.ReLU(inplace=True),\n            nn.Dropout(dropout),\n            nn.Linear(hidden, 2)\n        )\n\n    def forward(self, feat_rgb, feat_fft):\n        x = torch.cat([feat_rgb, feat_fft], dim=-1)\n        logits = self.mlp(x)\n        alpha = F.softmax(logits, dim=-1)\n        a_rgb = alpha[..., 0:1]\n        a_fft = alpha[..., 1:2]\n        fused = a_rgb * feat_rgb + a_fft * feat_fft\n        return fused\n\n# --- B. The Autoencoder Branch (Paper Logic) ---\nclass TransformerBlock(nn.Module):\n    def __init__(self, feature_dim, num_heads=4, dropout=0.1):\n        super().__init__()\n        self.attn = nn.MultiheadAttention(embed_dim=feature_dim, num_heads=num_heads, batch_first=True)\n        self.norm1 = nn.LayerNorm(feature_dim)\n        self.ffn = nn.Sequential(\n            nn.Linear(feature_dim, feature_dim * 2),\n            nn.ReLU(),\n            nn.Dropout(dropout),\n            nn.Linear(feature_dim * 2, feature_dim)\n        )\n        self.norm2 = nn.LayerNorm(feature_dim)\n        self.dropout = nn.Dropout(dropout)\n\n    def forward(self, x):\n        attn_out, _ = self.attn(x, x, x)\n        x = self.norm1(x + self.dropout(attn_out))\n        ffn_out = self.ffn(x)\n        x = self.norm2(x + self.dropout(ffn_out))\n        return x\n\nclass MutualAttentionFusion(nn.Module):\n    def __init__(self, video_dim, audio_dim, fusion_dim=256):\n        super().__init__()\n        self.proj_v = nn.Linear(video_dim, fusion_dim)\n        self.proj_a = nn.Linear(audio_dim, fusion_dim)\n        self.cross_attn_v2a = nn.MultiheadAttention(embed_dim=fusion_dim, num_heads=4, batch_first=True)\n        self.cross_attn_a2v = nn.MultiheadAttention(embed_dim=fusion_dim, num_heads=4, batch_first=True)\n        self.norm_v = nn.LayerNorm(fusion_dim)\n        self.norm_a = nn.LayerNorm(fusion_dim)\n\n    def forward(self, x_v, x_a):\n        feat_v = F.relu(self.proj_v(x_v))\n        feat_a = F.relu(self.proj_a(x_a))\n        attn_v, _ = self.cross_attn_v2a(query=feat_v, key=feat_a, value=feat_a)\n        fused_v = self.norm_v(feat_v + attn_v)\n        attn_a, _ = self.cross_attn_a2v(query=feat_a, key=feat_v, value=feat_v)\n        fused_a = self.norm_a(feat_a + attn_a)\n        return fused_v, fused_a\n\nclass AFMAE_Branch(nn.Module):\n    def __init__(self, video_input_dim=1280, audio_input_dim=40, hidden_dim=256):\n        super().__init__()\n        self.encoder_v = nn.Sequential(nn.Linear(video_input_dim, hidden_dim), TransformerBlock(hidden_dim))\n        self.encoder_a = nn.Sequential(nn.Linear(audio_input_dim, hidden_dim), TransformerBlock(hidden_dim))\n        self.fusion = MutualAttentionFusion(hidden_dim, hidden_dim, hidden_dim)\n        self.decoder_v = nn.Sequential(TransformerBlock(hidden_dim), nn.Linear(hidden_dim, video_input_dim))\n        self.decoder_a = nn.Sequential(TransformerBlock(hidden_dim), nn.Linear(hidden_dim, audio_input_dim))\n\n    def forward(self, video_feats, audio_feats):\n        enc_v = self.encoder_v(video_feats)\n        enc_a = self.encoder_a(audio_feats)\n        latent_v, latent_a = self.fusion(enc_v, enc_a)\n        recon_v = self.decoder_v(latent_v)\n        recon_a = self.decoder_a(latent_a)\n        return latent_v, latent_a, recon_v, recon_a\n\n# --- C. The Original Detector (Refactored) ---\nclass DeepfakeDetectorDual(nn.Module):\n    def __init__(self, config):\n        super().__init__()\n        # Backbone: EfficientNet B0\n        eff = models.efficientnet_b0(weights='DEFAULT')\n        self.feature_extractor = nn.Sequential(eff.features, eff.avgpool)\n        self.feature_dim = 1280\n        \n        # Modality Attention (RGB vs Freq)\n        self.modality_attn = ModalityAttentionPerFrame(self.feature_dim, hidden=256)\n        \n        # BiLSTM\n        self.lstm = nn.LSTM(\n            input_size=self.feature_dim, # Reduced from 2x because of Modality Attention\n            hidden_size=config['lstm_hidden_dim'],\n            num_layers=2,\n            batch_first=True,\n            bidirectional=True,\n            dropout=config['dropout_rate']\n        )\n\n    def get_cnn_features(self, x):\n        # x: [Batch, Seq, 3, H, W]\n        b, seq, c, h, w = x.size()\n        x_in = x.view(b * seq, c, h, w)\n        feat = self.feature_extractor(x_in)\n        feat = feat.view(b, seq, -1) # [B, Seq, 1280]\n        return feat\n\n    def forward_features(self, x_rgb, x_freq):\n        # 1. Extract raw features\n        feat_rgb = self.get_cnn_features(x_rgb)\n        feat_freq = self.get_cnn_features(x_freq)\n        \n        # 2. Fuse Modalities (RGB vs Freq)\n        fused_seq = self.modality_attn(feat_rgb, feat_freq)\n        \n        # 3. Temporal Modeling (LSTM)\n        lstm_out, _ = self.lstm(fused_seq) # [B, Seq, Hidden*2]\n        \n        # 4. Global Temporal Pooling (Last step or Attention)\n        # Simple approach: Average pooling over time for stability\n        lstm_pooled = torch.mean(lstm_out, dim=1) # [B, 1024]\n        \n        return lstm_pooled, fused_seq\n        \nclass GatedFusion(nn.Module):\n    def __init__(self, branch1_dim=1024, branch2_dim=512, bottleneck_dim=128, dropout_rate=0.3):\n        super().__init__()\n        # 1. The Bottleneck: Compress massive vectors to force the network to focus\n        self.proj1 = nn.Sequential(\n            nn.Linear(branch1_dim, bottleneck_dim),\n            nn.ReLU(),\n            nn.Dropout(dropout_rate)\n        )\n        self.proj2 = nn.Sequential(\n            nn.Linear(branch2_dim, bottleneck_dim),\n            nn.ReLU(),\n            nn.Dropout(dropout_rate)\n        )\n        \n        # 2. The Gate Network (now much smaller and harder to overfit)\n        self.gate_net = nn.Sequential(\n            nn.Linear(bottleneck_dim * 2, bottleneck_dim),\n            nn.ReLU(),\n            nn.Dropout(dropout_rate),\n            nn.Linear(bottleneck_dim, 1),\n            nn.Sigmoid()\n        )\n        self.norm = nn.LayerNorm(bottleneck_dim)\n\n    def forward(self, x1, x2):\n        h1 = self.proj1(x1)\n        h2 = self.proj2(x2)\n        \n        combined = torch.cat([h1, h2], dim=1)\n        z = self.gate_net(combined) # Gate value between 0.0 and 1.0\n        \n        fused = z * h1 + (1 - z) * h2\n        return self.norm(fused), z\n\n\nclass BranchAttentionFusion(nn.Module):\n    def __init__(self, branch1_dim=1024, branch2_dim=512, bottleneck_dim=128, dropout_rate=0.3):\n        super().__init__()\n        # 1. The Bottleneck Projections\n        self.proj_q = nn.Sequential(\n            nn.Linear(branch1_dim, bottleneck_dim),\n            nn.ReLU(),\n            nn.Dropout(dropout_rate)\n        )\n        self.proj_kv = nn.Sequential(\n            nn.Linear(branch2_dim, bottleneck_dim),\n            nn.ReLU(),\n            nn.Dropout(dropout_rate)\n        )\n        \n        # 2. Attention Mechanism (Now running on 128-dim instead of 512-dim)\n        self.attn = nn.MultiheadAttention(embed_dim=bottleneck_dim, num_heads=4, batch_first=True, dropout=dropout_rate)\n        self.norm = nn.LayerNorm(bottleneck_dim)\n        \n        # 3. Feed Forward Network (with heavy regularization)\n        self.ffn = nn.Sequential(\n            nn.Linear(bottleneck_dim, bottleneck_dim * 2),\n            nn.ReLU(),\n            nn.Dropout(dropout_rate),\n            nn.Linear(bottleneck_dim * 2, bottleneck_dim),\n            nn.Dropout(dropout_rate)\n        )\n\n    def forward(self, x_lstm, x_ae):\n        q = self.proj_q(x_lstm).unsqueeze(1)    # [Batch, 1, 128]\n        kv = self.proj_kv(x_ae).unsqueeze(1)    # [Batch, 1, 128]\n        \n        attn_out, _ = self.attn(query=q, key=kv, value=kv)\n        fused = attn_out.squeeze(1)             # [Batch, 128]\n        \n        fused = self.norm(fused + q.squeeze(1)) # Add & Norm\n        fused = fused + self.ffn(fused)         # FFN + Residual\n        return fused\n        \n# --- D. The Hybrid Super Model ---\nclass HybridDeepfakeDetector(nn.Module):\n    def __init__(self, config, fusion_type='concat'):\n        super().__init__()\n        self.fusion_type = fusion_type\n        \n        # Branch 1: Supervised BiLSTM\n        self.supervised_branch = DeepfakeDetectorDual(config)\n        \n        # Branch 2: Unsupervised Autoencoder\n        self.ae_branch = AFMAE_Branch(video_input_dim=1280, audio_input_dim=40, hidden_dim=256)\n        \n        # Dimensions\n        lstm_dim = 1024\n        ae_dim = 512 # 256 (Video) + 256 (Audio)\n        \n        # --- FUSION STRATEGIES ---\n        if fusion_type == 'gated':\n            self.fusion_module = GatedFusion(lstm_dim, ae_dim)\n            final_in_dim = 128\n        elif fusion_type == 'attention':\n            self.fusion_module = BranchAttentionFusion(lstm_dim, ae_dim)\n            final_in_dim = 128\n        else: # 'concat' (Default)\n            self.fusion_module = nn.Identity()\n            final_in_dim = lstm_dim + ae_dim\n\n        # Final Classifier\n        self.classifier = nn.Sequential(\n            nn.Linear(final_in_dim, 512),\n            nn.ReLU(),\n            nn.Dropout(0.5),\n            nn.Linear(512, 1)\n        )\n\n    def forward(self, rgb, freq, audio):\n        # 1. Get Features\n        lstm_feat, cnn_fused_seq = self.supervised_branch.forward_features(rgb, freq)\n        latent_v, latent_a, recon_v, recon_a = self.ae_branch(cnn_fused_seq, audio)\n        \n        # Pool AE features\n        latent_v_mean = torch.mean(latent_v, dim=1)\n        latent_a_mean = torch.mean(latent_a, dim=1)\n        ae_feat = torch.cat([latent_v_mean, latent_a_mean], dim=1) # [B, 512]\n        \n        # 2. Apply Fusion\n        if self.fusion_type == 'concat':\n            combined = torch.cat([lstm_feat, ae_feat], dim=1)\n        elif self.fusion_type == 'gated':\n            combined, gate_val = self.fusion_module(lstm_feat, ae_feat)\n        elif self.fusion_type == 'attention':\n            combined = self.fusion_module(lstm_feat, ae_feat)\n            \n        # 3. Classify\n        logits = self.classifier(combined)\n        \n        return logits.squeeze(-1), recon_v, recon_a, cnn_fused_seq\n\n# -------------------------\n# 5. Training Loop\n# -------------------------\ndef train_hybrid(model, train_loader, val_loader, optimizer, device, epochs):\n    scaler = GradScaler()\n    \n    # --- FIX: Class Weights to prevent the 86.37% Mode Collapse ---\n    # Assuming ~86% Fake and ~14% Real, the ratio is roughly 6.0\n    class_weight = torch.tensor([6.0]).to(device)\n    bce_loss = nn.BCEWithLogitsLoss(pos_weight=class_weight)\n    mse_loss = nn.MSELoss()\n    \n    # Variables to store the final epoch's metrics\n    final_val_acc, val_log_loss, val_auc = 0, 0, 0 \n    \n    for epoch in range(epochs):\n        model.train()\n        train_loss = 0\n        train_acc = 0\n        \n        loop = tqdm(train_loader, desc=f\"Epoch {epoch+1}/{epochs}\")\n        \n        for rgb, freq, audio, labels in loop:\n            rgb, freq, audio, labels = rgb.to(device), freq.to(device), audio.to(device), labels.to(device)\n            optimizer.zero_grad()\n            \n            with autocast():\n                logits, rec_v, rec_a, target_v = model(rgb, freq, audio)\n                loss_cls = bce_loss(logits, labels)\n                loss_rec_v = mse_loss(rec_v, target_v.detach())\n                loss_rec_a = mse_loss(rec_a, audio)\n                loss = loss_cls + 0.5 * (loss_rec_v + loss_rec_a)\n            \n            scaler.scale(loss).backward()\n            scaler.step(optimizer)\n            scaler.update()\n            \n            train_loss += loss.item()\n            preds = (torch.sigmoid(logits) > 0.5).float()\n            train_acc += (preds == labels).sum().item()\n            loop.set_postfix(loss=loss.item())\n            \n        print(f\"Epoch {epoch+1} | Train Loss: {train_loss/len(train_loader):.4f} | Acc: {train_acc/len(train_loader.dataset):.4f}\")\n        \n        # --- VALIDATION PHASE ---\n        model.eval()\n        val_acc = 0\n        all_labels = []\n        all_probs = []\n\n        with torch.no_grad():\n            for rgb, freq, audio, labels in val_loader:\n                rgb, freq, audio, labels = rgb.to(device), freq.to(device), audio.to(device), labels.to(device)\n                logits, _, _, _ = model(rgb, freq, audio)\n                probs = torch.sigmoid(logits)\n                preds = (probs > 0.5).float()\n                \n                val_acc += (preds == labels).sum().item()\n                all_labels.extend(labels.cpu().numpy())\n                all_probs.extend(probs.cpu().numpy())\n        \n        final_val_acc = val_acc / len(val_loader.dataset)\n        val_log_loss = log_loss(all_labels, all_probs, eps=1e-15)\n        try:\n            val_auc = roc_auc_score(all_labels, all_probs)\n        except:\n            val_auc = 0.5 \n\n        print(f\"Validation Results - Epoch {epoch+1}: Acc: {final_val_acc:.4f} | Log Loss: {val_log_loss:.4f} | AUC: {val_auc:.4f}\")\n        \n        # Optional: Save checkpoint for each fold if needed\n        torch.save(model.state_dict(), os.path.join(CONFIG['model_save_path'], f\"hybrid_epoch_{epoch+1}.pth\"))\n        \n    # Return the metrics from the very last epoch to average them later\n    return final_val_acc, val_log_loss, val_auc\n\n# -------------------------\n# 6. Main Execution (K-Fold)\n# -------------------------\ndef main():\n    device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n    print(f\"Using device: {device}\")\n    \n    print(\"Scanning dataset...\")\n    try:\n        from glob import glob\n        real = glob(os.path.join(CONFIG['data_root'], 'Celeb-real', '*.mp4'))\n        fake = glob(os.path.join(CONFIG['data_root'], 'Celeb-synthesis', '*.mp4'))\n        \n        # Convert lists to NumPy arrays for easy K-Fold indexing\n        video_paths = np.array(real + fake)\n        labels = np.array([1]*len(real) + [0]*len(fake))\n        print(f\"Total videos loaded: {len(video_paths)}\")\n    except Exception as e:\n        print(f\"Could not load dataset: {e}\")\n        return\n\n    # --- K-Fold Setup ---\n    k_folds = 5\n    skf = StratifiedKFold(n_splits=k_folds, shuffle=True, random_state=42)\n    \n    train_transform = transforms.Compose([\n        transforms.ToPILImage(),\n        transforms.Resize((CONFIG['frame_size'], CONFIG['frame_size'])),\n        transforms.ToTensor(),\n        transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n    ])\n    \n    # Store results to average later\n    fold_accuracies = []\n    fold_aucs = []\n    fold_loglosses = []\n    \n    # --- K-Fold Loop ---\n    for fold, (train_idx, val_idx) in enumerate(skf.split(video_paths, labels)):\n        print(f\"\\n{'='*40}\")\n        print(f\"🚀 STARTING FOLD {fold + 1}/{k_folds}\")\n        print(f\"{'='*40}\")\n        \n        # Split data for this specific fold\n        train_paths_fold = video_paths[train_idx]\n        train_labels_fold = labels[train_idx]\n        val_paths_fold = video_paths[val_idx]\n        val_labels_fold = labels[val_idx]\n        \n        train_ds = HybridDataset(train_paths_fold, train_labels_fold, transform=train_transform)\n        val_ds = HybridDataset(val_paths_fold, val_labels_fold, transform=train_transform)\n        \n        train_loader = DataLoader(train_ds, batch_size=CONFIG['batch_size'], shuffle=True, num_workers=CONFIG['num_workers'])\n        val_loader = DataLoader(val_ds, batch_size=CONFIG['batch_size'], shuffle=False, num_workers=CONFIG['num_workers'])\n        \n        # --- INITIALIZE A FRESH MODEL ---\n        # It is critical to build a new model every fold so it doesn't remember the last fold's data\n        model = HybridDeepfakeDetector(CONFIG, fusion_type='gated').to(device)\n        optimizer = optim.AdamW(model.parameters(), lr=CONFIG['learning_rate'])\n        \n        # Train and collect metrics\n        acc, log_loss_val, auc = train_hybrid(model, train_loader, val_loader, optimizer, device, CONFIG['num_epochs'])\n        \n        fold_accuracies.append(acc)\n        fold_loglosses.append(log_loss_val)\n        fold_aucs.append(auc)\n        \n        print(f\"✅ Fold {fold + 1} Finished -> Acc: {acc:.4f} | AUC: {auc:.4f}\")\n\n    # --- Final Academic Report ---\n    print(f\"\\n{'='*40}\")\n    print(\"🏆 FINAL 5-FOLD STRATIFIED CROSS-VALIDATION RESULTS (GATED FUSION)\")\n    print(f\"{'='*40}\")\n    print(f\"Average Accuracy: {np.mean(fold_accuracies):.4f} (+/- {np.std(fold_accuracies):.4f})\")\n    print(f\"Average AUC:      {np.mean(fold_aucs):.4f} (+/- {np.std(fold_aucs):.4f})\")\n    print(f\"Average Log Loss: {np.mean(fold_loglosses):.4f} (+/- {np.std(fold_loglosses):.4f})\")\n    print(f\"{'='*40}\\n\")\n\nif __name__ == \"__main__\":\n    main()","metadata":{"_cell_guid":"7a3d67a0-725b-474c-aef3-1f21f4db9bd3","_uuid":"cf09951f-65e0-48c2-939c-24aa997d97cf","collapsed":false,"execution":{"iopub.status.busy":"2026-02-19T07:06:00.548254Z","iopub.execute_input":"2026-02-19T07:06:00.548627Z","execution_failed":"2026-02-19T07:06:24.031Z"},"jupyter":{"outputs_hidden":false},"trusted":true},"outputs":[{"name":"stdout","text":"Using device: cuda\nScanning dataset...\nTotal videos loaded: 6229\n\n========================================\n🚀 STARTING FOLD 1/5\n========================================\nDownloading: \"https://download.pytorch.org/models/efficientnet_b0_rwightman-7f5810bc.pth\" to /root/.cache/torch/hub/checkpoints/efficientnet_b0_rwightman-7f5810bc.pth\n","output_type":"stream"},{"name":"stderr","text":"100%|██████████| 20.5M/20.5M [00:00<00:00, 144MB/s]\nEpoch 1/10:   1%|          | 11/1246 [00:27<23:27,  1.14s/it, loss=0.62]  ","output_type":"stream"}],"execution_count":null},{"cell_type":"code","source":"import torch\nimport numpy as np\nimport cv2\nimport librosa\nfrom torchvision import transforms\n\n# --- 1. Configuration ---\n# Ensure these match what you used during training\nTEST_CONFIG = {\n    'num_frames': 20,\n    'frame_size': 224,\n    'audio_sr': 16000,\n    'n_mfcc': 40,\n    'audio_duration': 2.0,\n    'lstm_hidden_dim': 512,\n    'dropout_rate': 0.5,\n    'use_spatial_dropout': True,\n    'use_temporal_dropout': True,\n    'use_attention': True\n}\n\n# --- 2. Load the Best Model ---\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\n# !!! CRITICAL: Set this to match your training experiment !!!\n# Options: 'concat' (Baseline), 'gated', or 'attention'\nFUSION_TYPE = 'concat' \n\n# Initialize the model structure with the correct fusion type\nmodel = HybridDeepfakeDetector(TEST_CONFIG, fusion_type=FUSION_TYPE).to(device)\n\n# Load the specific Epoch 4 weights (Best Model)\ncheckpoint_path = \"/kaggle/working/hybrid_model_checkpoints/hybrid_epoch_4.pth\"\n\nprint(f\"Loading weights from: {checkpoint_path}\")\ntry:\n    ckpt = torch.load(checkpoint_path, map_location=device)\n    # Handle cases where the checkpoint saves just state_dict or the full info dict\n    if \"model_state_dict\" in ckpt:\n        model.load_state_dict(ckpt[\"model_state_dict\"], strict=True)\n    else:\n        model.load_state_dict(ckpt, strict=True)\n    print(\"✅ Model loaded successfully!\")\nexcept FileNotFoundError:\n    print(f\"❌ Error: File not found at {checkpoint_path}. Please check the file path.\")\nexcept Exception as e:\n    print(f\"❌ Error loading model: {e}\")\n\nmodel.eval()\n\n# --- 3. Preprocessing Helper Functions ---\nval_transform = transforms.Compose([\n    transforms.ToPILImage(),\n    transforms.Resize((TEST_CONFIG['frame_size'], TEST_CONFIG['frame_size'])),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=[0.485,0.456,0.406], std=[0.229,0.224,0.225])\n])\n\ndef predict_video(video_path):\n    print(f\"Processing: {video_path}\")\n    \n    # A. Extract Video Frames (RGB)\n    # Ensure you have run the 'extract_frames' cell before this!\n    try:\n        frames = extract_frames(video_path, TEST_CONFIG['num_frames'])\n    except ValueError as e:\n        print(e)\n        return \"ERROR\", 0.5\n\n    # B. Compute Frequency Frames (FFT)\n    freq = compute_freq_images(frames)\n    freq_rgb = np.repeat(freq, 3, axis=3) # Expand to 3 channels\n    \n    # C. Extract Audio Features (MFCC)\n    mfcc = extract_audio_features(video_path, TEST_CONFIG['audio_sr'], TEST_CONFIG['n_mfcc'], TEST_CONFIG['audio_duration'])\n\n    # --- FIX: Force Audio Length to 64 Steps (Same as Training) ---\n    target_audio_len = 64\n    if mfcc.shape[0] > target_audio_len:\n        mfcc = mfcc[:target_audio_len, :]\n    elif mfcc.shape[0] < target_audio_len:\n        padding = np.zeros((target_audio_len - mfcc.shape[0], TEST_CONFIG['n_mfcc']))\n        mfcc = np.concatenate([mfcc, padding], axis=0)\n\n    # D. Transform & Stack\n    rgb_list = []\n    freq_list = []\n    for i in range(TEST_CONFIG['num_frames']):\n        rgb_list.append(val_transform(frames[i].astype(np.uint8)))\n        freq_list.append(val_transform(freq_rgb[i].astype(np.uint8)))\n    \n    # Add Batch Dimension [1, Seq, C, H, W]\n    rgb_tensor = torch.stack(rgb_list).unsqueeze(0).to(device)\n    freq_tensor = torch.stack(freq_list).unsqueeze(0).to(device)\n    \n    # Add Batch Dimension to Audio [1, Seq, MFCC]\n    audio_tensor = torch.tensor(mfcc, dtype=torch.float32).unsqueeze(0).to(device)\n\n    # --- 4. Run Inference ---\n    with torch.no_grad():\n        # The model returns 4 values: (logits, recon_v, recon_a, cnn_feat)\n        # We only need the first one (logits)\n        logits, _, _, _ = model(rgb_tensor, freq_tensor, audio_tensor)\n        \n        # Convert Logits -> Probability (0.0 to 1.0)\n        prob = torch.sigmoid(logits).item()\n\n    # --- 5. Interpret Result ---\n    # In this dataset: 1 = Real, 0 = Fake\n    label = \"REAL\" if prob >= 0.5 else \"FAKE\"\n    confidence = prob if prob >= 0.5 else 1 - prob\n    \n    print(\"-\" * 30)\n    print(f\"Result: {label}\")\n    print(f\"Probability Score (Realness): {prob:.4f}\")\n    print(f\"Confidence: {confidence*100:.2f}%\")\n    print(\"-\" * 30)\n    return label, prob\n\n# --- 6. Run on a Test Video ---\n# Replace this path with the video you want to test\ntest_video_path = \"/kaggle/input/deepfake-detection-challenge/test_videos/bfdopzvxbi.mp4\"\n\npredict_video(test_video_path)","metadata":{"execution":{"iopub.execute_input":"2026-01-25T09:31:22.40209Z","iopub.status.busy":"2026-01-25T09:31:22.401553Z","iopub.status.idle":"2026-01-25T09:31:32.20433Z","shell.execute_reply":"2026-01-25T09:31:32.203745Z","shell.execute_reply.started":"2026-01-25T09:31:22.402067Z"},"trusted":true},"outputs":[{"name":"stdout","output_type":"stream","text":["Loading weights from: /kaggle/working/hybrid_model_checkpoints/hybrid_epoch_4.pth\n","Model loaded successfully!\n","Processing: /kaggle/input/deepfake-detection-challenge/test_videos/bfdopzvxbi.mp4\n","------------------------------\n","Result: FAKE\n","Probability Score (Realness): 0.2724\n","Confidence: 72.76%\n","------------------------------\n"]},{"data":{"text/plain":["('FAKE', 0.2724229693412781)"]},"execution_count":19,"metadata":{},"output_type":"execute_result"}],"execution_count":19}]}