{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.11","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":31011,"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\nfrom sklearn.model_selection import 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/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    # Loss functions\n    bce_loss = nn.BCEWithLogitsLoss()\n    mse_loss = nn.MSELoss()\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            \n            optimizer.zero_grad()\n            \n            with autocast():\n                logits, rec_v, rec_a, target_v = model(rgb, freq, audio)\n                \n                # A. Classification Loss (Supervised)\n                loss_cls = bce_loss(logits, labels)\n                \n                # B. Reconstruction Loss (Unsupervised)\n                # We want the AE to reconstruct the valid features\n                loss_rec_v = mse_loss(rec_v, target_v.detach()) # Detach target to stop grad flow back to backbone\n                loss_rec_a = mse_loss(rec_a, audio)\n                \n                # Total Loss\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            \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\n        # ... inside train_hybrid function ...\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                \n                # Forward pass\n                logits, _, _, _ = model(rgb, freq, audio)\n                \n                # Calculate probabilities (0.0 to 1.0)\n                probs = torch.sigmoid(logits)\n                preds = (probs > 0.5).float()\n                \n                # Accumulate stats\n                val_acc += (preds == labels).sum().item()\n                all_labels.extend(labels.cpu().numpy())\n                all_probs.extend(probs.cpu().numpy())\n        \n        # --- CALCULATE METRICS ---\n        final_val_acc = val_acc / len(val_loader.dataset)\n        \n        # 1. Log Loss (The new metric)\n        # We use a small eps (epsilon) to prevent log(0) errors if prob is exactly 0 or 1\n        val_log_loss = log_loss(all_labels, all_probs, eps=1e-15)\n        \n        # 2. AUC (Area Under Curve)\n        try:\n            val_auc = roc_auc_score(all_labels, all_probs)\n        except:\n            val_auc = 0.5 # Handle edge case with single class batch\n\n        print(f\"Validation Results - Epoch {epoch+1}:\")\n        print(f\"  Accuracy: {final_val_acc:.4f}\")\n        print(f\"  Log Loss: {val_log_loss:.4f}  <-- Lower is better\")\n        print(f\"  AUC:      {val_auc:.4f}       <-- Higher is better\")\n        \n        # Save Checkpoint (You can now choose to save based on best Log Loss if you want!)\n        torch.save(model.state_dict(), os.path.join(CONFIG['model_save_path'], f\"hybrid_epoch_{epoch+1}.pth\"))\n\n# -------------------------\n# 6. Main Execution (FIXED)\n# -------------------------\ndef main():\n    device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n    print(f\"Using device: {device}\")\n\n    data_root = CONFIG['data_root']\n\n    if not os.path.exists(data_root):\n        raise ValueError(f\"Dataset path not found: {data_root}\")\n\n    video_paths = []\n    labels = []\n\n    # Expected Celeb-DF-v2 structure:\n    # Celeb-real\n    # Celeb-synthesis\n    # YouTube-real\n\n    for root, dirs, files in os.walk(data_root):\n        for file in files:\n            if file.lower().endswith((\".mp4\", \".avi\", \".mov\")):\n                full_path = os.path.join(root, file)\n\n                # Label logic:\n                # 0 = Real\n                # 1 = Fake\n                root_lower = root.lower()\n\n                if \"synthesis\" in root_lower:\n                    label = 1\n                elif \"real\" in root_lower:\n                    label = 0\n                else:\n                    continue  # Skip unknown folders\n\n                video_paths.append(full_path)\n                labels.append(label)\n    if len(video_paths) == 0:\n        raise ValueError(\"No video files found. Check dataset path.\")\n\n    print(f\"Total videos found: {len(video_paths)}\")\n    print(f\"Real videos: {labels.count(0)}\")\n    print(f\"Fake videos: {labels.count(1)}\")\n    # -------------------------\n    # Train / Validation Split\n    # -------------------------\n    train_paths, val_paths, train_labels, val_labels = train_test_split(\n        video_paths,\n        labels,\n        test_size=0.2,\n        random_state=42,\n        stratify=labels\n    )\n    print(f\"Train size: {len(train_paths)}\")\n    print(f\"Validation size: {len(val_paths)}\")\n    # -------------------------\n    # Transforms\n    # -------------------------\n    train_transform = transforms.Compose([\n        transforms.ToPILImage(),\n        transforms.Resize((CONFIG['frame_size'], CONFIG['frame_size'])),\n        transforms.ToTensor(),\n        transforms.Normalize(\n            mean=[0.485, 0.456, 0.406],\n            std=[0.229, 0.224, 0.225]\n        )\n    ])\n    train_ds = HybridDataset(train_paths, train_labels, transform=train_transform)\n    val_ds = HybridDataset(val_paths, val_labels, transform=train_transform)\n    train_loader = DataLoader(\n        train_ds,\n        batch_size=CONFIG['batch_size'],\n        shuffle=True,\n        num_workers=CONFIG['num_workers'],\n        pin_memory=True\n    )\n    val_loader = DataLoader(\n        val_ds,\n        batch_size=CONFIG['batch_size'],\n        shuffle=False,\n        num_workers=CONFIG['num_workers'],\n        pin_memory=True\n    )\n    # -------------------------\n    # Initialize Model\n    # -------------------------\n    model = HybridDeepfakeDetector(CONFIG, fusion_type='gated').to(device)\n    optimizer = optim.AdamW(\n        model.parameters(),\n        lr=CONFIG['learning_rate'],\n        weight_decay=CONFIG['weight_decay']\n    )\n    # -------------------------\n    # Train\n    # -------------------------\n    train_hybrid(\n        model,\n        train_loader,\n        val_loader,\n        optimizer,\n        device,\n        CONFIG['num_epochs']\n    )\n\nif __name__ == \"__main__\":\n    main()\n","metadata":{"_uuid":"cf09951f-65e0-48c2-939c-24aa997d97cf","_cell_guid":"7a3d67a0-725b-474c-aef3-1f21f4db9bd3","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-02-19T10:26:50.141916Z","iopub.execute_input":"2026-02-19T10:26:50.142203Z","iopub.status.idle":"2026-02-19T10:27:13.047727Z","shell.execute_reply.started":"2026-02-19T10:26:50.142178Z","shell.execute_reply":"2026-02-19T10:27:13.046702Z"}},"outputs":[],"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":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-25T09:31:22.401553Z","iopub.execute_input":"2026-01-25T09:31:22.402090Z","iopub.status.idle":"2026-01-25T09:31:32.204330Z","shell.execute_reply.started":"2026-01-25T09:31:22.402067Z","shell.execute_reply":"2026-01-25T09:31:32.203745Z"}},"outputs":[],"execution_count":null}]}