{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[],"dockerImageVersionId":28755,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import sys\nprint(sys.version)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%%writefile preprocessing.py\nimport os\nimport re\nimport csv\nimport sys\nimport argparse\nimport logging\nfrom dataclasses import dataclass, asdict\n\nimport cv2\nimport numpy as np\n\nlogging.basicConfig(level=logging.INFO, format=\"%(asctime)s [%(levelname)s] %(message)s\")\nlogger = logging.getLogger(\"preprocessing\")\n\nFRAMES_PER_CLIP = 16\nGLOBAL_SIZE = (128, 128)\nBOUNDARY_SIZE = (64, 64)\nDILATE_MARGIN_PX = 15\nERODE_MARGIN_PX = 8\nSEED = 42\nnp.random.seed(SEED)\n\nFRAME_NUM_RE = re.compile(r\"_f(\\d+)\\.jpg$\", re.IGNORECASE)\nCASCADE_PATH = cv2.data.haarcascades + \"haarcascade_frontalface_default.xml\"\n\n\n@dataclass\nclass ClipRecord:\n    clip_id: str\n    npz_path: str\n    label: int\n    source_dataset: str\n    num_valid_frames: int\n    missing_face_frac: float\n\n\nclass FaceBoundaryExtractor:\n    def __init__(self):\n        self._detector = cv2.CascadeClassifier(CASCADE_PATH)\n        if self._detector.empty():\n            raise RuntimeError(\"Could not load Haar Cascade from %s\" % CASCADE_PATH)\n\n    def close(self):\n        pass\n\n    def get_face_bbox(self, frame_bgr):\n        gray = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2GRAY)\n        faces = self._detector.detectMultiScale(gray, scaleFactor=1.1, minNeighbors=5, minSize=(30, 30))\n        if len(faces) == 0:\n            return None\n        faces = sorted(faces, key=lambda f: f[2] * f[3], reverse=True)\n        return faces[0]\n\n    def get_face_mask(self, frame_bgr):\n        bbox = self.get_face_bbox(frame_bgr)\n        if bbox is None:\n            return None\n        x, y, w, h = bbox\n        h_img, w_img = frame_bgr.shape[:2]\n        mask = np.zeros((h_img, w_img), dtype=np.uint8)\n        cx, cy = x + w // 2, y + h // 2\n        cv2.ellipse(mask, (cx, cy), (w // 2, h // 2), 0, 0, 360, 255, -1)\n        return mask\n\n    def get_boundary_ring_mask(self, face_mask):\n        dilate_kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (DILATE_MARGIN_PX, DILATE_MARGIN_PX))\n        erode_kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (ERODE_MARGIN_PX, ERODE_MARGIN_PX))\n        dilated = cv2.dilate(face_mask, dilate_kernel, iterations=1)\n        eroded = cv2.erode(face_mask, erode_kernel, iterations=1)\n        return cv2.subtract(dilated, eroded)\n\n    def crop_to_mask_bbox(self, frame, mask, out_size):\n        ys, xs = np.where(mask > 0)\n        h, w = frame.shape[:2]\n        if len(xs) == 0 or len(ys) == 0:\n            cx, cy = w // 2, h // 2\n            half = min(h, w) // 4\n            x0, x1 = max(0, cx - half), min(w, cx + half)\n            y0, y1 = max(0, cy - half), min(h, cy + half)\n        else:\n            pad = 6\n            x0, x1 = max(0, xs.min() - pad), min(w, xs.max() + pad)\n            y0, y1 = max(0, ys.min() - pad), min(h, ys.max() + pad)\n            if x1 <= x0 or y1 <= y0:\n                x0, y0, x1, y1 = 0, 0, w, h\n        crop = frame[y0:y1, x0:x1]\n        if crop.size == 0:\n            crop = frame\n        return cv2.resize(crop, out_size, interpolation=cv2.INTER_AREA)\n\n\ndef sample_frame_indices(total_frames, n=FRAMES_PER_CLIP):\n    if total_frames <= n:\n        return list(range(total_frames)) + [total_frames - 1] * (n - total_frames)\n    return list(np.linspace(0, total_frames - 1, n, dtype=int))\n\n\ndef load_frames_from_video(video_path):\n    cap = cv2.VideoCapture(video_path)\n    if not cap.isOpened():\n        return None\n    total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))\n    if total <= 1:\n        cap.release()\n        return None\n    indices = sample_frame_indices(total, FRAMES_PER_CLIP)\n    frames = []\n    for idx in indices:\n        cap.set(cv2.CAP_PROP_POS_FRAMES, idx)\n        ok, frame = cap.read()\n        frames.append(frame if ok else None)\n    cap.release()\n    if all(f is None for f in frames):\n        return None\n    first_valid = next((f for f in frames if f is not None), None)\n    return [f if f is not None else first_valid for f in frames]\n\n\ndef load_frames_from_folder(clip_location):\n    folder_path, source_id = clip_location.split(\"|\", 1)\n    if not os.path.isdir(folder_path):\n        return None\n    candidates = []\n    prefix = source_id + \"_f\"\n    for fname in os.listdir(folder_path):\n        if not fname.startswith(prefix):\n            continue\n        match = FRAME_NUM_RE.search(fname)\n        if not match:\n            continue\n        frame_num = int(match.group(1))\n        candidates.append((frame_num, os.path.join(folder_path, fname)))\n    if not candidates:\n        return None\n    candidates.sort(key=lambda x: x[0])\n    total = len(candidates)\n    indices = sample_frame_indices(total, FRAMES_PER_CLIP)\n    frames = []\n    for idx in indices:\n        _, fpath = candidates[idx]\n        img = cv2.imread(fpath)\n        frames.append(img)\n    if all(f is None for f in frames):\n        return None\n    first_valid = next((f for f in frames if f is not None), None)\n    return [f if f is not None else first_valid for f in frames]\n\n\ndef extract_clip(clip_type, clip_location, extractor):\n    if clip_type == \"video_file\":\n        frames = load_frames_from_video(clip_location)\n    elif clip_type == \"frame_folder\":\n        frames = load_frames_from_folder(clip_location)\n    else:\n        return None\n    if frames is None:\n        return None\n\n    boundary_crops = []\n    last_valid_ring_crop = None\n    missing_count = 0\n    for frame in frames:\n        face_mask = extractor.get_face_mask(frame)\n        if face_mask is None:\n            missing_count += 1\n            if last_valid_ring_crop is not None:\n                boundary_crops.append(last_valid_ring_crop)\n            else:\n                boundary_crops.append(cv2.resize(frame, BOUNDARY_SIZE, interpolation=cv2.INTER_AREA))\n            continue\n        ring_mask = extractor.get_boundary_ring_mask(face_mask)\n        ring_crop = extractor.crop_to_mask_bbox(frame, ring_mask, BOUNDARY_SIZE)\n        boundary_crops.append(ring_crop)\n        last_valid_ring_crop = ring_crop\n\n    missing_face_frac = missing_count / float(len(frames))\n    num_valid_frames = len(frames) - missing_count\n\n    mid_frame = frames[len(frames) // 2]\n    global_frame = cv2.resize(mid_frame, GLOBAL_SIZE, interpolation=cv2.INTER_AREA)\n    global_frame = cv2.cvtColor(global_frame, cv2.COLOR_BGR2RGB)\n\n    gray_crops = [cv2.cvtColor(c, cv2.COLOR_BGR2GRAY).astype(np.float32) for c in boundary_crops]\n    diff_stack = [np.abs(gray_crops[i] - gray_crops[i - 1]) for i in range(1, len(gray_crops))]\n    boundary_diff_stack = np.stack(diff_stack, axis=0) / 255.0\n\n    return global_frame, boundary_diff_stack, num_valid_frames, missing_face_frac\n\n\ndef process_dataset(video_list_csv, output_dir, index_out_path):\n    if not os.path.isfile(video_list_csv):\n        raise IOError(\"video_list_csv not found: %s\" % video_list_csv)\n    if not os.path.isdir(output_dir):\n        os.makedirs(output_dir)\n    extractor = FaceBoundaryExtractor()\n    records = []\n    with open(video_list_csv, \"r\") as f:\n        rows = list(csv.DictReader(f))\n    logger.info(\"Processing %d clips\" % len(rows))\n    for i, row in enumerate(rows):\n        result = extract_clip(row[\"clip_type\"], row[\"clip_location\"], extractor)\n        if result is None:\n            continue\n        global_frame, boundary_diff_stack, num_valid_frames, missing_face_frac = result\n        npz_path = os.path.join(output_dir, row[\"clip_id\"] + \".npz\")\n        np.savez_compressed(npz_path, global_frame=global_frame,\n                             boundary_diff_stack=boundary_diff_stack, label=int(row[\"label\"]))\n        records.append(ClipRecord(clip_id=row[\"clip_id\"], npz_path=npz_path, label=int(row[\"label\"]),\n                                   source_dataset=row[\"source_dataset\"], num_valid_frames=num_valid_frames,\n                                   missing_face_frac=round(missing_face_frac, 4)))\n        if (i + 1) % 50 == 0:\n            logger.info(\"[%d/%d] processed\" % (i + 1, len(rows)))\n            sys.stdout.flush()\n    extractor.close()\n    fieldnames = [\"clip_id\", \"npz_path\", \"label\", \"source_dataset\", \"num_valid_frames\", \"missing_face_frac\"]\n    with open(index_out_path, \"w\") as f:\n        writer = csv.DictWriter(f, fieldnames=fieldnames)\n        writer.writeheader()\n        for r in records:\n            writer.writerow(asdict(r))\n    logger.info(\"Done. %d/%d clips written\" % (len(records), len(rows)))\n\n\nif __name__ == \"__main__\":\n    parser = argparse.ArgumentParser()\n    parser.add_argument(\"--video_list_csv\", type=str, required=True)\n    parser.add_argument(\"--output_dir\", type=str, required=True)\n    parser.add_argument(\"--index_out\", type=str, default=None)\n    args = parser.parse_args()\n    index_out = args.index_out or os.path.join(args.output_dir, \"index.csv\")\n    process_dataset(args.video_list_csv, args.output_dir, index_out)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%%writefile make_manifest.py\nimport os\nimport re\nimport csv\nimport json\nimport logging\nfrom collections import defaultdict\n\nlogging.basicConfig(level=logging.INFO, format=\"%(asctime)s [%(levelname)s] %(message)s\")\nlogger = logging.getLogger(\"make_manifest\")\n\nDFDC_ROOT = \"/kaggle/input/competitions/deepfake-detection-challenge/train_sample_videos\"\nFFPP_SPLIT_ROOT = \"/kaggle/input/datasets/gradientvoyager/faceforensics-c23-extracted-faces-100k/dataset_processed_split\"\nFFPP_MANIFEST_CSV = os.path.join(FFPP_SPLIT_ROOT, \"dataset_manifest.csv\")\nFFPP_FILENAME_RE = re.compile(r\"^(.+)_f\\d+\\.jpg$\", re.IGNORECASE)\n\nCDF_ROOT = \"/kaggle/input/datasets/reubensuju/celeb-df-v2\"\nCDF_REAL_FOLDERS = set([\"Celeb-real\", \"YouTube-real\"])\nCDF_FAKE_FOLDERS = set([\"Celeb-synthesis\"])\n\nVIDEO_EXTS = set([\".mp4\", \".avi\", \".mov\", \".mkv\"])\n\n\ndef collect_dfdc(root):\n    rows = []\n    metadata_path = os.path.join(root, \"metadata.json\")\n    if not os.path.isfile(metadata_path):\n        logger.warning(\"DFDC metadata.json not found, skipping\")\n        return rows\n    with open(metadata_path, \"r\") as f:\n        metadata = json.load(f)\n    n_real, n_fake = 0, 0\n    for fname, info in metadata.items():\n        label_str = info.get(\"label\", \"\").upper()\n        if label_str == \"REAL\":\n            label = 0\n        elif label_str == \"FAKE\":\n            label = 1\n        else:\n            continue\n        video_path = os.path.join(root, fname)\n        if not os.path.isfile(video_path):\n            continue\n        rows.append({\"clip_id\": \"dfdc_\" + os.path.splitext(fname)[0], \"clip_type\": \"video_file\",\n                      \"clip_location\": video_path, \"label\": label, \"source_dataset\": \"dfdc\"})\n        n_real += (label == 0)\n        n_fake += (label == 1)\n    logger.info(\"DFDC: %d real, %d fake\" % (n_real, n_fake))\n    return rows\n\n\ndef collect_ffpp(split_root, manifest_csv):\n    rows = []\n    if not os.path.isfile(manifest_csv):\n        logger.warning(\"FF++ manifest not found, skipping\")\n        return rows\n    groups = defaultdict(list)\n    with open(manifest_csv, \"r\") as f:\n        reader = csv.DictReader(f)\n        for row in reader:\n            match = FFPP_FILENAME_RE.match(row[\"filename\"])\n            if not match:\n                continue\n            true_video_id = match.group(1)\n            key = (row[\"split\"], row[\"fake_type\"], true_video_id)\n            groups[key].append(row)\n    n_real, n_fake = 0, 0\n    for key, frame_rows in groups.items():\n        split, fake_type, true_video_id = key\n        label = 0 if frame_rows[0][\"label\"].upper() == \"REAL\" else 1\n        folder_path = os.path.join(split_root, split, fake_type)\n        if not os.path.isdir(folder_path):\n            continue\n        rows.append({\"clip_id\": \"ffpp_%s_%s_%s\" % (fake_type, true_video_id, split),\n                      \"clip_type\": \"frame_folder\", \"clip_location\": \"%s|%s\" % (folder_path, true_video_id),\n                      \"label\": label, \"source_dataset\": \"ffpp\"})\n        n_real += (label == 0)\n        n_fake += (label == 1)\n    logger.info(\"FF++: %d real, %d fake\" % (n_real, n_fake))\n    return rows\n\n\ndef collect_cdf(root):\n    rows = []\n    n_real, n_fake = 0, 0\n    for folder_name in sorted(os.listdir(root)):\n        folder_path = os.path.join(root, folder_name)\n        if not os.path.isdir(folder_path):\n            continue\n        if folder_name in CDF_REAL_FOLDERS:\n            label = 0\n        elif folder_name in CDF_FAKE_FOLDERS:\n            label = 1\n        else:\n            continue\n        for fname in os.listdir(folder_path):\n            if os.path.splitext(fname)[1].lower() not in VIDEO_EXTS:\n                continue\n            rows.append({\"clip_id\": \"cdf_%s_%s\" % (folder_name, os.path.splitext(fname)[0]),\n                          \"clip_type\": \"video_file\", \"clip_location\": os.path.join(folder_path, fname),\n                          \"label\": label, \"source_dataset\": \"cdf\"})\n            n_real += (label == 0)\n            n_fake += (label == 1)\n    logger.info(\"Celeb-DF: %d real, %d fake\" % (n_real, n_fake))\n    return rows\n\n\ndef build_manifest(output_csv=\"/kaggle/working/video_list.csv\"):\n    all_rows = []\n    all_rows.extend(collect_dfdc(DFDC_ROOT))\n    all_rows.extend(collect_ffpp(FFPP_SPLIT_ROOT, FFPP_MANIFEST_CSV))\n    all_rows.extend(collect_cdf(CDF_ROOT))\n    if not all_rows:\n        raise RuntimeError(\"No clips collected.\")\n    fieldnames = [\"clip_id\", \"clip_type\", \"clip_location\", \"label\", \"source_dataset\"]\n    with open(output_csv, \"w\") as f:\n        writer = csv.DictWriter(f, fieldnames=fieldnames)\n        writer.writeheader()\n        writer.writerows(all_rows)\n    n_real = sum(1 for r in all_rows if r[\"label\"] == 0)\n    n_fake = sum(1 for r in all_rows if r[\"label\"] == 1)\n    logger.info(\"Manifest written: %d clips (%d real, %d fake)\" % (len(all_rows), n_real, n_fake))\n    return output_csv\n\n\nif __name__ == \"__main__\":\n    build_manifest()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nprint(\"Contents of /kaggle/input:\", os.listdir(\"/kaggle/input\"))","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nprint(\"Contents of /kaggle/input/datasets:\", os.listdir(\"/kaggle/input/datasets\"))","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nprint(\"Contents:\", os.listdir(\"/kaggle/input/datasets/sheharyaaarr\"))","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\npath = \"/kaggle/input/datasets/sheharyaaarr/tbc-net-scripts\"\nprint(\"Contents:\", os.listdir(path))","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import shutil\nimport os\n\nsrc = \"/kaggle/input/datasets/sheharyaaarr/tbc-net-scripts\"\nfor fname in [\"preprocessing.py\", \"make_manifest.py\"]:\n    shutil.copy(os.path.join(src, fname), fname)\n\nprint(\"Copied files:\", os.listdir(\".\"))\n\n# Test that they actually import and work\nimport preprocessing\nimport make_manifest\nprint(\"Both modules imported successfully!\")\nprint(\"FaceBoundaryExtractor available:\", hasattr(preprocessing, \"FaceBoundaryExtractor\"))\nprint(\"build_manifest available:\", hasattr(make_manifest, \"build_manifest\"))","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nprint(\"Competitions:\", os.listdir(\"/kaggle/input/competitions\") if os.path.isdir(\"/kaggle/input/competitions\") else \"none\")\nprint(\"Datasets:\", os.listdir(\"/kaggle/input/datasets\") if os.path.isdir(\"/kaggle/input/datasets\") else \"none\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import make_manifest\nimport importlib\nimportlib.reload(make_manifest)\n\noutput_path = make_manifest.build_manifest(\"/kaggle/working/video_list.csv\")\n\nimport os\nprint(\"File exists:\", os.path.isfile(\"/kaggle/working/video_list.csv\"))\nprint(\"File size (bytes):\", os.path.getsize(\"/kaggle/working/video_list.csv\"))","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\n\nfull_manifest = pd.read_csv(\"/kaggle/working/video_list.csv\")\n\nlarger_sample = full_manifest.groupby(\"source_dataset\", group_keys=False).apply(\n    lambda x: x.sample(min(400, len(x)), random_state=7)\n)\nlarger_sample.to_csv(\"/kaggle/working/video_list_pilot2.csv\", index=False)\nprint(larger_sample[\"source_dataset\"].value_counts())\nprint(\"Total:\", len(larger_sample))","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import importlib\nimport preprocessing\nimportlib.reload(preprocessing)\n\nimport pandas as pd\nimport numpy as np\nimport sys\nimport os\n\nsample = pd.read_csv(\"/kaggle/working/video_list_pilot2.csv\")\noutput_dir = \"/kaggle/working/preprocessed_pilot2\"\nif not os.path.isdir(output_dir):\n    os.makedirs(output_dir)\n\nextractor = preprocessing.FaceBoundaryExtractor()\ntotal = len(sample)\n\nfor i, row in sample.iterrows():\n    clip_id = row[\"clip_id\"]\n    npz_path = os.path.join(output_dir, clip_id + \".npz\")\n\n    if os.path.isfile(npz_path):\n        print(\"\\r[%d/%d] already done: %s\" % (i + 1, total, clip_id), end=\"\")\n        continue\n\n    result = preprocessing.extract_clip(row[\"clip_type\"], row[\"clip_location\"], extractor)\n    if result is None:\n        print(\"\\r[%d/%d] SKIPPED: %s\" % (i + 1, total, clip_id), end=\"\")\n        continue\n\n    global_frame, boundary_diff_stack, num_valid, missing_frac = result\n    np.savez_compressed(npz_path, global_frame=global_frame,\n                         boundary_diff_stack=boundary_diff_stack, label=int(row[\"label\"]))\n\n    print(\"\\r[%d/%d] done: %s\" % (i + 1, total, clip_id), end=\"\")\n    sys.stdout.flush()\n\nextractor.close()\nprint(\"\\nAll done!\")\n\nfiles = os.listdir(output_dir)\nprint(\"Total files in output dir:\", len(files))","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nimport pandas as pd\nimport os\n\noutput_dir = \"/kaggle/working/preprocessed_pilot2\"\nfiles = [f for f in os.listdir(output_dir) if f.endswith(\".npz\")]\n\nrecords = []\nfor fname in files:\n    clip_id = fname[:-4]\n    npz_path = os.path.join(output_dir, fname)\n    data = np.load(npz_path)\n    label = int(data[\"label\"])\n    records.append({\"clip_id\": clip_id, \"npz_path\": npz_path, \"label\": label})\n\nindex_df = pd.DataFrame(records)\nindex_df.to_csv(\"/kaggle/working/preprocessed_pilot2/index.csv\", index=False)\nprint(\"Index written. Total clips:\", len(index_df))\nprint(index_df[\"label\"].value_counts())","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import importlib\nimport pilot_signal_check\nimportlib.reload(pilot_signal_check)\n\nreport = pilot_signal_check.run_pilot_check(\n    index_csv=\"/kaggle/working/preprocessed_pilot2/index.csv\",\n    output_json=\"/kaggle/working/pilot_signal_report_v2.json\"\n)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%%writefile pilot_signal_check.py\nimport json\nimport logging\n\nimport numpy as np\nimport pandas as pd\nfrom scipy import stats\n\nlogging.basicConfig(level=logging.INFO, format=\"%(asctime)s [%(levelname)s] %(message)s\")\nlogger = logging.getLogger(\"pilot_signal_check\")\n\nALPHA = 0.05\nCOHENS_D_GO_THRESHOLD = 0.5\nCOHENS_D_CAUTION_THRESHOLD = 0.2\n\n\ndef cohens_d(a, b):\n    na, nb = len(a), len(b)\n    if na < 2 or nb < 2:\n        return 0.0\n    va, vb = np.var(a, ddof=1), np.var(b, ddof=1)\n    pooled_std = np.sqrt(((na - 1) * va + (nb - 1) * vb) / (na + nb - 2))\n    if pooled_std == 0:\n        return 0.0\n    return (np.mean(a) - np.mean(b)) / pooled_std\n\n\ndef compute_clip_stats(boundary_diff_stack):\n    flat = boundary_diff_stack.reshape(boundary_diff_stack.shape[0], -1)\n    per_frame_mean_abs = flat.mean(axis=1)\n    energy = (flat ** 2).mean(axis=1)\n    return {\n        \"mean_abs_diff\": float(per_frame_mean_abs.mean()),\n        \"temporal_variance\": float(per_frame_mean_abs.var(ddof=1)) if len(per_frame_mean_abs) > 1 else 0.0,\n        \"mean_energy\": float(energy.mean()),\n    }\n\n\ndef run_pilot_check(index_csv, output_json=\"pilot_signal_report.json\"):\n    index_df = pd.read_csv(index_csv)\n    logger.info(\"Loaded index with %d clips\" % len(index_df))\n\n    stat_names = [\"mean_abs_diff\", \"temporal_variance\", \"mean_energy\"]\n    real_stats = {name: [] for name in stat_names}\n    fake_stats = {name: [] for name in stat_names}\n\n    for _, row in index_df.iterrows():\n        data = np.load(row[\"npz_path\"])\n        diff_stack = data[\"boundary_diff_stack\"]\n        clip_stats = compute_clip_stats(diff_stack)\n        target = real_stats if int(row[\"label\"]) == 0 else fake_stats\n        for name in stat_names:\n            target[name].append(clip_stats[name])\n\n    logger.info(\"Real: %d, Fake: %d\" % (len(real_stats[\"mean_abs_diff\"]), len(fake_stats[\"mean_abs_diff\"])))\n\n    results = []\n    for name in stat_names:\n        a = np.array(real_stats[name])\n        b = np.array(fake_stats[name])\n        if len(a) < 2 or len(b) < 2:\n            continue\n        t_stat, p_val = stats.ttest_ind(a, b, equal_var=False)\n        d = cohens_d(a, b)\n        results.append({\n            \"stat_name\": name,\n            \"real_mean\": float(a.mean()), \"real_std\": float(a.std(ddof=1)),\n            \"fake_mean\": float(b.mean()), \"fake_std\": float(b.std(ddof=1)),\n            \"t_statistic\": float(t_stat), \"p_value\": float(p_val),\n            \"cohens_d\": float(d), \"n_real\": len(a), \"n_fake\": len(b),\n        })\n\n    significant_medium = [r for r in results if r[\"p_value\"] < ALPHA and abs(r[\"cohens_d\"]) >= COHENS_D_GO_THRESHOLD]\n    significant_small = [r for r in results if r[\"p_value\"] < ALPHA and abs(r[\"cohens_d\"]) >= COHENS_D_CAUTION_THRESHOLD]\n\n    if significant_medium:\n        names = \", \".join(r[\"stat_name\"] for r in significant_medium)\n        verdict = \"GO\"\n        reason = \"Medium+ effect (|d|>=%.1f), p<%.2f for: %s.\" % (COHENS_D_GO_THRESHOLD, ALPHA, names)\n    elif significant_small:\n        names = \", \".join(r[\"stat_name\"] for r in significant_small)\n        verdict = \"CAUTION\"\n        reason = \"Only small effect for: %s.\" % names\n    else:\n        verdict = \"NO-GO\"\n        reason = \"No significant signal detected.\"\n\n    report = {\"verdict\": verdict, \"verdict_reason\": reason, \"results\": results}\n    with open(output_json, \"w\") as f:\n        json.dump(report, f, indent=2)\n\n    print(\"\\n\" + \"=\" * 70)\n    print(\"PILOT SIGNAL CHECK -- VERDICT: %s\" % verdict)\n    print(\"=\" * 70)\n    print(reason)\n    print(\"\\nPer-statistic results:\")\n    for r in results:\n        sig = \"***\" if r[\"p_value\"] < ALPHA else \"n.s.\"\n        print(\"  %-20s real=%.6f+/-%.6f  fake=%.6f+/-%.6f  t=%.3f  p=%.4f %s  d=%.3f\" % (\n            r[\"stat_name\"], r[\"real_mean\"], r[\"real_std\"], r[\"fake_mean\"], r[\"fake_std\"],\n            r[\"t_statistic\"], r[\"p_value\"], sig, r[\"cohens_d\"]))\n    print(\"=\" * 70)\n\n    return report","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%%writefile model.py\n\"\"\"\nmodel.py\nTBC-Net -- Phase 4: Architecture\n\nADAPTATION NOTE: The plan specifies MobileNetV3-Small (ImageNet-pretrained)\nfor the spatial branch. This environment's torchvision (0.4.1, 2019) predates\nMobileNetV3's introduction to torchvision. A custom lightweight CNN is used\ninstead, matching the same role (global spatial embedding) and staying well\nwithin the <5M total parameter budget. This should be documented as a\nmethodology adaptation, not silently substituted, when writing up the paper.\n\nArchitecture (per Section 4 of the implementation plan):\n  - SpatialBranch: CNN on the 128x128 global frame -> 128-d embedding\n  - TemporalBranch: per-frame CNN on boundary-diff crops (64x64) + GRU\n    pooling across time -> 128-d embedding\n  - Fusion head: concat -> FC(128) -> Dropout(0.3) -> FC(num_classes)\n  - GlobalOnlyAblation / BoundaryOnlyAblation: isolate each branch's\n    contribution, required for reviewer credibility (Section 4)\n\"\"\"\n\nimport torch\nimport torch.nn as nn\n\n\nclass SpatialBranch(nn.Module):\n    \"\"\"CNN on the global 128x128 face frame.\"\"\"\n    def __init__(self, out_dim=128):\n        super(SpatialBranch, self).__init__()\n        self.features = nn.Sequential(\n            nn.Conv2d(3, 16, kernel_size=3, padding=1), nn.BatchNorm2d(16), nn.ReLU(),\n            nn.MaxPool2d(2),  # 128 -> 64\n            nn.Conv2d(16, 32, kernel_size=3, padding=1), nn.BatchNorm2d(32), nn.ReLU(),\n            nn.MaxPool2d(2),  # 64 -> 32\n            nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.BatchNorm2d(64), nn.ReLU(),\n            nn.MaxPool2d(2),  # 32 -> 16\n            nn.Conv2d(64, 64, kernel_size=3, padding=1), nn.BatchNorm2d(64), nn.ReLU(),\n            nn.AdaptiveAvgPool2d(1),  # -> [B, 64, 1, 1]\n        )\n        self.fc = nn.Linear(64, out_dim)\n\n    def forward(self, x):\n        # x: [B, 3, 128, 128]\n        feat = self.features(x)\n        feat = feat.view(feat.size(0), -1)\n        return self.fc(feat)\n\n\nclass TemporalBranch(nn.Module):\n    \"\"\"CNN over each boundary-diff frame, then aggregated across time via GRU\n    (lightweight attention-style pooling, per Section 4 -- 'not a full RNN/TCN'\n    is respected here by using a single-layer GRU rather than a deep RNN).\"\"\"\n    def __init__(self, out_dim=128):\n        super(TemporalBranch, self).__init__()\n        self.frame_cnn = nn.Sequential(\n            nn.Conv2d(1, 16, kernel_size=3, padding=1), nn.BatchNorm2d(16), nn.ReLU(),\n            nn.MaxPool2d(2),  # 64 -> 32\n            nn.Conv2d(16, 32, kernel_size=3, padding=1), nn.BatchNorm2d(32), nn.ReLU(),\n            nn.MaxPool2d(2),  # 32 -> 16\n            nn.Conv2d(32, 32, kernel_size=3, padding=1), nn.BatchNorm2d(32), nn.ReLU(),\n            nn.AdaptiveAvgPool2d(1),  # -> [B*T, 32, 1, 1]\n        )\n        self.gru = nn.GRU(input_size=32, hidden_size=out_dim, batch_first=True)\n\n    def forward(self, x):\n        # x: [B, T, 1, 64, 64]\n        B, T, C, H, W = x.size()\n        x = x.view(B * T, C, H, W)\n        feat = self.frame_cnn(x)                # [B*T, 32, 1, 1]\n        feat = feat.view(B, T, -1)               # [B, T, 32]\n        _, h_n = self.gru(feat)                  # h_n: [1, B, out_dim]\n        return h_n.squeeze(0)                    # [B, out_dim]\n\n\nclass TBCNet(nn.Module):\n    \"\"\"Full model: spatial + temporal branches fused for binary classification.\"\"\"\n    def __init__(self, spatial_dim=128, temporal_dim=128, num_classes=2):\n        super(TBCNet, self).__init__()\n        self.spatial_branch = SpatialBranch(out_dim=spatial_dim)\n        self.temporal_branch = TemporalBranch(out_dim=temporal_dim)\n        self.classifier = nn.Sequential(\n            nn.Linear(spatial_dim + temporal_dim, 128),\n            nn.ReLU(),\n            nn.Dropout(0.3),\n            nn.Linear(128, num_classes),\n        )\n\n    def forward(self, global_frame, boundary_stack):\n        spatial_feat = self.spatial_branch(global_frame)      # [B, spatial_dim]\n        temporal_feat = self.temporal_branch(boundary_stack)  # [B, temporal_dim]\n        combined = torch.cat([spatial_feat, temporal_feat], dim=1)\n        return self.classifier(combined)\n\n\nclass GlobalOnlyAblation(nn.Module):\n    \"\"\"Ablation: spatial branch only, to test whether boundary branch adds value.\"\"\"\n    def __init__(self, spatial_dim=128, num_classes=2):\n        super(GlobalOnlyAblation, self).__init__()\n        self.spatial_branch = SpatialBranch(out_dim=spatial_dim)\n        self.classifier = nn.Sequential(\n            nn.Linear(spatial_dim, 64),\n            nn.ReLU(),\n            nn.Dropout(0.3),\n            nn.Linear(64, num_classes),\n        )\n\n    def forward(self, global_frame, boundary_stack=None):\n        spatial_feat = self.spatial_branch(global_frame)\n        return self.classifier(spatial_feat)\n\n\nclass BoundaryOnlyAblation(nn.Module):\n    \"\"\"Ablation: temporal branch only, to test standalone predictive power\n    of the boundary-jitter signal.\"\"\"\n    def __init__(self, temporal_dim=128, num_classes=2):\n        super(BoundaryOnlyAblation, self).__init__()\n        self.temporal_branch = TemporalBranch(out_dim=temporal_dim)\n        self.classifier = nn.Sequential(\n            nn.Linear(temporal_dim, 64),\n            nn.ReLU(),\n            nn.Dropout(0.3),\n            nn.Linear(64, num_classes),\n        )\n\n    def forward(self, global_frame=None, boundary_stack=None):\n        temporal_feat = self.temporal_branch(boundary_stack)\n        return self.classifier(temporal_feat)\n\n\ndef count_parameters(model):\n    return sum(p.numel() for p in model.parameters())\n\n\nif __name__ == \"__main__\":\n    # Verify param count and tensor shapes before building on top of this\n    # (per plan Section 11 risk mitigation)\n    model = TBCNet()\n    global_only = GlobalOnlyAblation()\n    boundary_only = BoundaryOnlyAblation()\n\n    batch_size = 4\n    dummy_global = torch.randn(batch_size, 3, 128, 128)\n    dummy_boundary = torch.randn(batch_size, 15, 1, 64, 64)  # 16 frames -> 15 diffs\n\n    out_full = model(dummy_global, dummy_boundary)\n    out_global = global_only(dummy_global)\n    out_boundary = boundary_only(boundary_stack=dummy_boundary)\n\n    print(\"TBCNet output shape:\", out_full.size())\n    print(\"GlobalOnlyAblation output shape:\", out_global.size())\n    print(\"BoundaryOnlyAblation output shape:\", out_boundary.size())\n    print(\"\")\n    print(\"TBCNet params:\", count_parameters(model))\n    print(\"GlobalOnlyAblation params:\", count_parameters(global_only))\n    print(\"BoundaryOnlyAblation params:\", count_parameters(boundary_only))\n    print(\"\")\n    target_budget = 5000000\n    ok = count_parameters(model) < target_budget\n    print(\"Under 5M param budget:\", ok)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"Step 1: importing torch...\")\nimport torch\nprint(\"Step 2: torch imported, version:\", torch.__version__)\n\nprint(\"Step 3: importing model...\")\nimport model\nprint(\"Step 4: model module imported\")\n\nprint(\"Step 5: creating TBCNet...\")\nnet = model.TBCNet()\nprint(\"Step 6: TBCNet created successfully\")\n\nprint(\"Step 7: running dummy forward pass...\")\ndummy_global = torch.randn(4, 3, 128, 128)\ndummy_boundary = torch.randn(4, 15, 1, 64, 64)\nout = net(dummy_global, dummy_boundary)\nprint(\"Step 8: forward pass done, output shape:\", out.size())\n\nprint(\"Total params:\", sum(p.numel() for p in net.parameters()))","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"before import\")\nimport torch\nprint(\"after import, version:\", torch.__version__)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"test\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T10:50:31.957775Z","iopub.execute_input":"2026-08-30T10:50:31.958159Z","iopub.status.idle":"2026-08-30T10:50:31.965825Z","shell.execute_reply.started":"2026-08-30T10:50:31.958092Z","shell.execute_reply":"2026-08-30T10:50:31.96416Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nprint(\"Working directory contents:\", os.listdir(\"/kaggle/working\"))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T10:50:42.13287Z","iopub.execute_input":"2026-08-30T10:50:42.133202Z","iopub.status.idle":"2026-08-30T10:50:42.139148Z","shell.execute_reply.started":"2026-08-30T10:50:42.133152Z","shell.execute_reply":"2026-08-30T10:50:42.137939Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import shutil\nimport os\n\nsrc = \"/kaggle/input/datasets/sheharyaaarr/tbc-net-scripts\"\nfor fname in os.listdir(src):\n    shutil.copy(os.path.join(src, fname), fname)\n\nprint(\"Restored files:\", os.listdir(\".\"))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T10:51:12.515492Z","iopub.execute_input":"2026-08-30T10:51:12.515828Z","iopub.status.idle":"2026-08-30T10:51:12.535671Z","shell.execute_reply.started":"2026-08-30T10:51:12.51578Z","shell.execute_reply":"2026-08-30T10:51:12.534578Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%%writefile pilot_signal_check.py\nimport json\nimport logging\n\nimport numpy as np\nimport pandas as pd\nfrom scipy import stats\n\nlogging.basicConfig(level=logging.INFO, format=\"%(asctime)s [%(levelname)s] %(message)s\")\nlogger = logging.getLogger(\"pilot_signal_check\")\n\nALPHA = 0.05\nCOHENS_D_GO_THRESHOLD = 0.5\nCOHENS_D_CAUTION_THRESHOLD = 0.2\n\n\ndef cohens_d(a, b):\n    na, nb = len(a), len(b)\n    if na < 2 or nb < 2:\n        return 0.0\n    va, vb = np.var(a, ddof=1), np.var(b, ddof=1)\n    pooled_std = np.sqrt(((na - 1) * va + (nb - 1) * vb) / (na + nb - 2))\n    if pooled_std == 0:\n        return 0.0\n    return (np.mean(a) - np.mean(b)) / pooled_std\n\n\ndef compute_clip_stats(boundary_diff_stack):\n    flat = boundary_diff_stack.reshape(boundary_diff_stack.shape[0], -1)\n    per_frame_mean_abs = flat.mean(axis=1)\n    energy = (flat ** 2).mean(axis=1)\n    return {\n        \"mean_abs_diff\": float(per_frame_mean_abs.mean()),\n        \"temporal_variance\": float(per_frame_mean_abs.var(ddof=1)) if len(per_frame_mean_abs) > 1 else 0.0,\n        \"mean_energy\": float(energy.mean()),\n    }\n\n\ndef run_pilot_check(index_csv, output_json=\"pilot_signal_report.json\"):\n    index_df = pd.read_csv(index_csv)\n    logger.info(\"Loaded index with %d clips\" % len(index_df))\n\n    stat_names = [\"mean_abs_diff\", \"temporal_variance\", \"mean_energy\"]\n    real_stats = {name: [] for name in stat_names}\n    fake_stats = {name: [] for name in stat_names}\n\n    for _, row in index_df.iterrows():\n        data = np.load(row[\"npz_path\"])\n        diff_stack = data[\"boundary_diff_stack\"]\n        clip_stats = compute_clip_stats(diff_stack)\n        target = real_stats if int(row[\"label\"]) == 0 else fake_stats\n        for name in stat_names:\n            target[name].append(clip_stats[name])\n\n    logger.info(\"Real: %d, Fake: %d\" % (len(real_stats[\"mean_abs_diff\"]), len(fake_stats[\"mean_abs_diff\"])))\n\n    results = []\n    for name in stat_names:\n        a = np.array(real_stats[name])\n        b = np.array(fake_stats[name])\n        if len(a) < 2 or len(b) < 2:\n            continue\n        t_stat, p_val = stats.ttest_ind(a, b, equal_var=False)\n        d = cohens_d(a, b)\n        results.append({\n            \"stat_name\": name,\n            \"real_mean\": float(a.mean()), \"real_std\": float(a.std(ddof=1)),\n            \"fake_mean\": float(b.mean()), \"fake_std\": float(b.std(ddof=1)),\n            \"t_statistic\": float(t_stat), \"p_value\": float(p_val),\n            \"cohens_d\": float(d), \"n_real\": len(a), \"n_fake\": len(b),\n        })\n\n    significant_medium = [r for r in results if r[\"p_value\"] < ALPHA and abs(r[\"cohens_d\"]) >= COHENS_D_GO_THRESHOLD]\n    significant_small = [r for r in results if r[\"p_value\"] < ALPHA and abs(r[\"cohens_d\"]) >= COHENS_D_CAUTION_THRESHOLD]\n\n    if significant_medium:\n        names = \", \".join(r[\"stat_name\"] for r in significant_medium)\n        verdict = \"GO\"\n        reason = \"Medium+ effect (|d|>=%.1f), p<%.2f for: %s.\" % (COHENS_D_GO_THRESHOLD, ALPHA, names)\n    elif significant_small:\n        names = \", \".join(r[\"stat_name\"] for r in significant_small)\n        verdict = \"CAUTION\"\n        reason = \"Only small effect for: %s.\" % names\n    else:\n        verdict = \"NO-GO\"\n        reason = \"No significant signal detected.\"\n\n    report = {\"verdict\": verdict, \"verdict_reason\": reason, \"results\": results}\n    with open(output_json, \"w\") as f:\n        json.dump(report, f, indent=2)\n\n    print(\"\\n\" + \"=\" * 70)\n    print(\"PILOT SIGNAL CHECK -- VERDICT: %s\" % verdict)\n    print(\"=\" * 70)\n    print(reason)\n    print(\"\\nPer-statistic results:\")\n    for r in results:\n        sig = \"***\" if r[\"p_value\"] < ALPHA else \"n.s.\"\n        print(\"  %-20s real=%.6f+/-%.6f  fake=%.6f+/-%.6f  t=%.3f  p=%.4f %s  d=%.3f\" % (\n            r[\"stat_name\"], r[\"real_mean\"], r[\"real_std\"], r[\"fake_mean\"], r[\"fake_std\"],\n            r[\"t_statistic\"], r[\"p_value\"], sig, r[\"cohens_d\"]))\n    print(\"=\" * 70)\n\n    return report","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T10:51:50.754208Z","iopub.execute_input":"2026-08-30T10:51:50.754535Z","iopub.status.idle":"2026-08-30T10:51:50.762968Z","shell.execute_reply.started":"2026-08-30T10:51:50.754486Z","shell.execute_reply":"2026-08-30T10:51:50.761671Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%%writefile model.py\nimport torch\nimport torch.nn as nn\n\n\nclass SpatialBranch(nn.Module):\n    def __init__(self, out_dim=128):\n        super(SpatialBranch, self).__init__()\n        self.features = nn.Sequential(\n            nn.Conv2d(3, 16, kernel_size=3, padding=1), nn.BatchNorm2d(16), nn.ReLU(),\n            nn.MaxPool2d(2),\n            nn.Conv2d(16, 32, kernel_size=3, padding=1), nn.BatchNorm2d(32), nn.ReLU(),\n            nn.MaxPool2d(2),\n            nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.BatchNorm2d(64), nn.ReLU(),\n            nn.MaxPool2d(2),\n            nn.Conv2d(64, 64, kernel_size=3, padding=1), nn.BatchNorm2d(64), nn.ReLU(),\n            nn.AdaptiveAvgPool2d(1),\n        )\n        self.fc = nn.Linear(64, out_dim)\n\n    def forward(self, x):\n        feat = self.features(x)\n        feat = feat.view(feat.size(0), -1)\n        return self.fc(feat)\n\n\nclass TemporalBranch(nn.Module):\n    def __init__(self, out_dim=128):\n        super(TemporalBranch, self).__init__()\n        self.frame_cnn = nn.Sequential(\n            nn.Conv2d(1, 16, kernel_size=3, padding=1), nn.BatchNorm2d(16), nn.ReLU(),\n            nn.MaxPool2d(2),\n            nn.Conv2d(16, 32, kernel_size=3, padding=1), nn.BatchNorm2d(32), nn.ReLU(),\n            nn.MaxPool2d(2),\n            nn.Conv2d(32, 32, kernel_size=3, padding=1), nn.BatchNorm2d(32), nn.ReLU(),\n            nn.AdaptiveAvgPool2d(1),\n        )\n        self.gru = nn.GRU(input_size=32, hidden_size=out_dim, batch_first=True)\n\n    def forward(self, x):\n        B, T, C, H, W = x.size()\n        x = x.view(B * T, C, H, W)\n        feat = self.frame_cnn(x)\n        feat = feat.view(B, T, -1)\n        _, h_n = self.gru(feat)\n        return h_n.squeeze(0)\n\n\nclass TBCNet(nn.Module):\n    def __init__(self, spatial_dim=128, temporal_dim=128, num_classes=2):\n        super(TBCNet, self).__init__()\n        self.spatial_branch = SpatialBranch(out_dim=spatial_dim)\n        self.temporal_branch = TemporalBranch(out_dim=temporal_dim)\n        self.classifier = nn.Sequential(\n            nn.Linear(spatial_dim + temporal_dim, 128),\n            nn.ReLU(),\n            nn.Dropout(0.3),\n            nn.Linear(128, num_classes),\n        )\n\n    def forward(self, global_frame, boundary_stack):\n        spatial_feat = self.spatial_branch(global_frame)\n        temporal_feat = self.temporal_branch(boundary_stack)\n        combined = torch.cat([spatial_feat, temporal_feat], dim=1)\n        return self.classifier(combined)\n\n\nclass GlobalOnlyAblation(nn.Module):\n    def __init__(self, spatial_dim=128, num_classes=2):\n        super(GlobalOnlyAblation, self).__init__()\n        self.spatial_branch = SpatialBranch(out_dim=spatial_dim)\n        self.classifier = nn.Sequential(\n            nn.Linear(spatial_dim, 64),\n            nn.ReLU(),\n            nn.Dropout(0.3),\n            nn.Linear(64, num_classes),\n        )\n\n    def forward(self, global_frame, boundary_stack=None):\n        spatial_feat = self.spatial_branch(global_frame)\n        return self.classifier(spatial_feat)\n\n\nclass BoundaryOnlyAblation(nn.Module):\n    def __init__(self, temporal_dim=128, num_classes=2):\n        super(BoundaryOnlyAblation, self).__init__()\n        self.temporal_branch = TemporalBranch(out_dim=temporal_dim)\n        self.classifier = nn.Sequential(\n            nn.Linear(temporal_dim, 64),\n            nn.ReLU(),\n            nn.Dropout(0.3),\n            nn.Linear(64, num_classes),\n        )\n\n    def forward(self, global_frame=None, boundary_stack=None):\n        temporal_feat = self.temporal_branch(boundary_stack)\n        return self.classifier(temporal_feat)\n\n\ndef count_parameters(model):\n    return sum(p.numel() for p in model.parameters())\n\n\nif __name__ == \"__main__\":\n    model = TBCNet()\n    global_only = GlobalOnlyAblation()\n    boundary_only = BoundaryOnlyAblation()\n\n    batch_size = 4\n    dummy_global = torch.randn(batch_size, 3, 128, 128)\n    dummy_boundary = torch.randn(batch_size, 15, 1, 64, 64)\n\n    out_full = model(dummy_global, dummy_boundary)\n    out_global = global_only(dummy_global)\n    out_boundary = boundary_only(boundary_stack=dummy_boundary)\n\n    print(\"TBCNet output shape:\", out_full.size())\n    print(\"GlobalOnlyAblation output shape:\", out_global.size())\n    print(\"BoundaryOnlyAblation output shape:\", out_boundary.size())\n    print(\"\")\n    print(\"TBCNet params:\", count_parameters(model))\n    print(\"GlobalOnlyAblation params:\", count_parameters(global_only))\n    print(\"BoundaryOnlyAblation params:\", count_parameters(boundary_only))\n    print(\"\")\n    print(\"Under 5M param budget:\", count_parameters(model) < 5000000)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T10:52:07.597928Z","iopub.execute_input":"2026-08-30T10:52:07.59824Z","iopub.status.idle":"2026-08-30T10:52:07.605814Z","shell.execute_reply.started":"2026-08-30T10:52:07.598192Z","shell.execute_reply":"2026-08-30T10:52:07.604937Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import model\nimport importlib\nimportlib.reload(model)\n\nnet = model.TBCNet()\ndummy_global = __import__(\"torch\").randn(4, 3, 128, 128)\ndummy_boundary = __import__(\"torch\").randn(4, 15, 1, 64, 64)\nout = net(dummy_global, dummy_boundary)\nprint(\"Output shape:\", out.size())\nprint(\"Total params:\", model.count_parameters(net))\nprint(\"Under 5M budget:\", model.count_parameters(net) < 5000000)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T10:52:20.055788Z","iopub.execute_input":"2026-08-30T10:52:20.056116Z","iopub.status.idle":"2026-08-30T10:52:21.517766Z","shell.execute_reply.started":"2026-08-30T10:52:20.056067Z","shell.execute_reply":"2026-08-30T10:52:21.516338Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%%writefile dataset.py\nimport numpy as np\nimport pandas as pd\nimport torch\nfrom torch.utils.data import Dataset\n\nSEED = 42\n\n\nclass TBCNetDataset(Dataset):\n    def __init__(self, index_df):\n        self.records = index_df.reset_index(drop=True)\n\n    def __len__(self):\n        return len(self.records)\n\n    def __getitem__(self, idx):\n        row = self.records.iloc[idx]\n        data = np.load(row[\"npz_path\"])\n\n        global_frame = data[\"global_frame\"].astype(np.float32) / 255.0\n        global_frame = np.transpose(global_frame, (2, 0, 1))\n\n        boundary_stack = data[\"boundary_diff_stack\"].astype(np.float32)\n        boundary_stack = np.expand_dims(boundary_stack, axis=1)\n\n        label = int(row[\"label\"])\n\n        return {\n            \"global_frame\": torch.from_numpy(global_frame),\n            \"boundary_stack\": torch.from_numpy(boundary_stack),\n            \"label\": torch.tensor(label, dtype=torch.long),\n            \"clip_id\": row[\"clip_id\"],\n        }\n\n\ndef build_protocol_a_splits(index_csv, val_frac=0.15, test_frac=0.15, seed=SEED):\n    df = pd.read_csv(index_csv)\n    train_parts, val_parts, test_parts = [], [], []\n    group_cols =","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T10:52:53.39251Z","iopub.execute_input":"2026-08-30T10:52:53.392963Z","iopub.status.idle":"2026-08-30T10:52:53.399629Z","shell.execute_reply.started":"2026-08-30T10:52:53.392883Z","shell.execute_reply":"2026-08-30T10:52:53.398626Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%%writefile train.py\nimport json\nimport argparse\n\nimport numpy as np\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import DataLoader\nfrom sklearn.metrics import accuracy_score, precision_recall_fscore_support, roc_auc_score\n\nfrom model import TBCNet, GlobalOnlyAblation, BoundaryOnlyAblation\nfrom dataset import TBCNetDataset, build_protocol_a_splits, build_protocol_b_splits, get_class_weights\n\nSEED = 42\nBATCH_SIZE = 16\nLEARNING_RATE = 0.001\nNUM_EPOCHS = 30\n\ntorch.manual_seed(SEED)\nnp.random.seed(SEED)\n\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\n\ndef build_model(model_type):\n    if model_type == \"full\":\n        return TBCNet()\n    elif model_type == \"global_only\":\n        return GlobalOnlyAblation()\n    elif model_type == \"boundary_only\":\n        return BoundaryOnlyAblation()\n    raise ValueError(\"Unknown model_type: %s\" % model_type)\n\n\ndef run_model(model, model_type, global_frame, boundary_stack):\n    if model_type == \"full\":\n        return model(global_frame, boundary_stack)\n    elif model_type == \"global_only\":\n        return model(global_frame)\n    return model(boundary_stack=boundary_stack)\n\n\ndef evaluate(model, model_type, loader, criterion):\n    model.eval()\n    all_preds, all_labels, all_probs = [], [], []\n    total_loss = 0.0\n    with torch.no_grad():\n        for batch in loader:\n            global_frame = batch[\"global_frame\"].to(DEVICE)\n            boundary_stack = batch[\"boundary_stack\"].to(DEVICE)\n            labels = batch[\"label\"].to(DEVICE)\n            outputs = run_model(model, model_type, global_frame, boundary_stack)\n            loss = criterion(outputs, labels)\n            total_loss += loss.item() * labels.size(0)\n            probs = torch.softmax(outputs, dim=1)[:, 1]\n            preds = torch.argmax(outputs, dim=1)\n            all_preds.extend(preds.cpu().numpy().tolist())\n            all_labels.extend(labels.cpu().numpy().tolist())\n            all_probs.extend(probs.cpu().numpy().tolist())\n    avg_loss = total_loss / len(loader.dataset)\n    acc = accuracy_score(all_labels, all_preds)\n    precision, recall, f1, _ = precision_recall_fscore_support(all_labels, all_preds, labels=[0, 1], zero_division=0)\n    try:\n        auc = roc_auc_score(all_labels, all_probs)\n    except ValueError:\n        auc = float(\"nan\")\n    return {\"loss\": avg_loss, \"accuracy\": acc, \"auc\": auc,\n            \"precision_real\": precision[0], \"recall_real\": recall[0], \"f1_real\": f1[0],\n            \"precision_fake\": precision[1], \"recall_fake\": recall[1], \"f1_fake\": f1[1]}\n\n\ndef print_metrics(prefix, metrics):\n    print(\"%s | loss=%.4f acc=%.4f auc=%.4f | real: P=%.3f R=%.3f F1=%.3f | fake: P=%.3f R=%.3f F1=%.3f\" % (\n        prefix, metrics[\"loss\"], metrics[\"accuracy\"], metrics[\"auc\"],\n        metrics[\"precision_real\"], metrics[\"recall_real\"], metrics[\"f1_real\"],\n        metrics[\"precision_fake\"], metrics[\"recall_fake\"], metrics[\"f1_fake\"]))\n\n\ndef train_one_run(train_df, val_df, test_df, model_type, checkpoint_path, num_epochs=NUM_EPOCHS):\n    class_weights = get_class_weights(train_df).to(DEVICE)\n    train_ds = TBCNetDataset(train_df)\n    val_ds = TBCNetDataset(val_df)\n    test_ds = TBCNetDataset(test_df)\n    train_loader = DataLoader(train_ds, batch_size=BATCH_SIZE, shuffle=True, num_workers=0)\n    val_loader = DataLoader(val_ds, batch_size=BATCH_SIZE, shuffle=False, num_workers=0)\n    test_loader = DataLoader(test_ds, batch_size=BATCH_SIZE, shuffle=False, num_workers=0)\n    model = build_model(model_type).to(DEVICE)\n    criterion = nn.CrossEntropyLoss(weight=class_weights)\n    optimizer = torch.optim.Adam(model.parameters(), lr=LEARNING_RATE)\n    best_val_f1 = -1.0\n    for epoch in range(1, num_epochs + 1):\n        model.train()\n        running_loss = 0.0\n        for batch in train_loader:\n            global_frame = batch[\"global_frame\"].to(DEVICE)\n            boundary_stack = batch[\"boundary_stack\"].to(DEVICE)\n            labels = batch[\"label\"].to(DEVICE)\n            optimizer.zero_grad()\n            outputs = run_model(model, model_type, global_frame, boundary_stack)\n            loss = criterion(outputs, labels)\n            loss.backward()\n            optimizer.step()\n            running_loss += loss.item() * labels.size(0)\n        train_loss = running_loss / len(train_loader.dataset)\n        val_metrics = evaluate(model, model_type, val_loader, criterion)\n        print(\"[%s] Epoch %d/%d | train_loss=%.4f\" % (model_type, epoch, num_epochs, train_loss))\n        print_metrics(\"  val  \", val_metrics)\n        avg_f1 = (val_metrics[\"f1_real\"] + val_metrics[\"f1_fake\"]) / 2.0\n        if avg_f1 > best_val_f1:\n            best_val_f1 = avg_f1\n            torch.save(model.state_dict(), checkpoint_path)\n    model.load_state_dict(torch.load(checkpoint_path))\n    test_metrics = evaluate(model, model_type, test_loader, criterion)\n    print_metrics(\"  TEST \", test_metrics)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T10:53:06.480065Z","iopub.execute_input":"2026-08-30T10:53:06.480425Z","iopub.status.idle":"2026-08-30T10:53:06.487835Z","shell.execute_reply.started":"2026-08-30T10:53:06.480364Z","shell.execute_reply":"2026-08-30T10:53:06.48693Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nprint(\"Files ready to download:\")\nfor f in [\"preprocessing.py\", \"make_manifest.py\", \"pilot_signal_check.py\", \"model.py\", \"dataset.py\", \"train.py\"]:\n    print(\" -\", f, \"| exists:\", os.path.isfile(f))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T10:53:42.685606Z","iopub.execute_input":"2026-08-30T10:53:42.686012Z","iopub.status.idle":"2026-08-30T10:53:42.69553Z","shell.execute_reply.started":"2026-08-30T10:53:42.685938Z","shell.execute_reply":"2026-08-30T10:53:42.693931Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"preprocessed_pilot exists:\", os.path.isdir(\"preprocessed_pilot\"))\nprint(\"preprocessed_pilot2 exists:\", os.path.isdir(\"preprocessed_pilot2\"))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T10:53:55.360045Z","iopub.execute_input":"2026-08-30T10:53:55.360361Z","iopub.status.idle":"2026-08-30T10:53:55.367279Z","shell.execute_reply.started":"2026-08-30T10:53:55.360314Z","shell.execute_reply":"2026-08-30T10:53:55.365712Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nsrc = \"/kaggle/input/datasets/sheharyaaarr/tbc-net-scripts\"\nprint(\"Files in dataset:\", os.listdir(src))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T11:06:59.459268Z","iopub.execute_input":"2026-08-30T11:06:59.459609Z","iopub.status.idle":"2026-08-30T11:06:59.472079Z","shell.execute_reply.started":"2026-08-30T11:06:59.459555Z","shell.execute_reply":"2026-08-30T11:06:59.470839Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import shutil\nimport os\n\nsrc = \"/kaggle/input/datasets/sheharyaaarr/tbc-net-scripts\"\nfor fname in os.listdir(src):\n    shutil.copy(os.path.join(src, fname), fname)\n\nprint(\"Restored files:\", os.listdir(\".\"))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T11:07:25.458468Z","iopub.execute_input":"2026-08-30T11:07:25.458845Z","iopub.status.idle":"2026-08-30T11:07:25.495472Z","shell.execute_reply.started":"2026-08-30T11:07:25.458785Z","shell.execute_reply":"2026-08-30T11:07:25.494409Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import make_manifest\nimport importlib\nimportlib.reload(make_manifest)\n\noutput_path = make_manifest.build_manifest(\"/kaggle/working/video_list.csv\")\n\nimport os\nprint(\"File exists:\", os.path.isfile(\"/kaggle/working/video_list.csv\"))\nprint(\"File size (bytes):\", os.path.getsize(\"/kaggle/working/video_list.csv\"))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T11:07:56.719528Z","iopub.execute_input":"2026-08-30T11:07:56.719903Z","iopub.status.idle":"2026-08-30T11:08:00.235995Z","shell.execute_reply.started":"2026-08-30T11:07:56.719847Z","shell.execute_reply":"2026-08-30T11:08:00.235097Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"Competitions:\", os.listdir(\"/kaggle/input/competitions\") if os.path.isdir(\"/kaggle/input/competitions\") else \"none\")\nprint(\"Datasets:\", os.listdir(\"/kaggle/input/datasets\") if os.path.isdir(\"/kaggle/input/datasets\") else \"none\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T11:08:26.549701Z","iopub.execute_input":"2026-08-30T11:08:26.550104Z","iopub.status.idle":"2026-08-30T11:08:26.556554Z","shell.execute_reply.started":"2026-08-30T11:08:26.550051Z","shell.execute_reply":"2026-08-30T11:08:26.555386Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\n\nfull_manifest = pd.read_csv(\"/kaggle/working/video_list.csv\")\n\nlarger_sample = full_manifest.groupby(\"source_dataset\", group_keys=False).apply(\n    lambda x: x.sample(min(400, len(x)), random_state=7)\n)\nlarger_sample.to_csv(\"/kaggle/working/video_list_pilot2.csv\", index=False)\nprint(larger_sample[\"source_dataset\"].value_counts())\nprint(\"Total:\", len(larger_sample))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T11:09:53.05383Z","iopub.execute_input":"2026-08-30T11:09:53.054297Z","iopub.status.idle":"2026-08-30T11:09:54.653835Z","shell.execute_reply.started":"2026-08-30T11:09:53.054221Z","shell.execute_reply":"2026-08-30T11:09:54.652374Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import importlib\nimport preprocessing\nimportlib.reload(preprocessing)\n\nimport pandas as pd\nimport numpy as np\nimport sys\nimport os\n\nsample = pd.read_csv(\"/kaggle/working/video_list_pilot2.csv\")\noutput_dir = \"/kaggle/working/preprocessed_pilot2\"\nif not os.path.isdir(output_dir):\n    os.makedirs(output_dir)\n\nextractor = preprocessing.FaceBoundaryExtractor()\ntotal = len(sample)\n\nfor i, row in sample.iterrows():\n    clip_id = row[\"clip_id\"]\n    npz_path = os.path.join(output_dir, clip_id + \".npz\")\n\n    if os.path.isfile(npz_path):\n        print(\"\\r[%d/%d] already done: %s\" % (i + 1, total, clip_id), end=\"\")\n        continue\n\n    result = preprocessing.extract_clip(row[\"clip_type\"], row[\"clip_location\"], extractor)\n    if result is None:\n        print(\"\\r[%d/%d] SKIPPED: %s\" % (i + 1, total, clip_id), end=\"\")\n        continue\n\n    global_frame, boundary_diff_stack, num_valid, missing_frac = result\n    np.savez_compressed(npz_path, global_frame=global_frame,\n                         boundary_diff_stack=boundary_diff_stack, label=int(row[\"label\"]))\n\n    print(\"\\r[%d/%d] done: %s\" % (i + 1, total, clip_id), end=\"\")\n    sys.stdout.flush()\n\nextractor.close()\nprint(\"\\nAll done!\")\n\nfiles = os.listdir(output_dir)\nprint(\"Total files in output dir:\", len(files))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T11:10:25.068397Z","iopub.execute_input":"2026-08-30T11:10:25.068774Z","iopub.status.idle":"2026-08-30T13:18:51.136306Z","shell.execute_reply.started":"2026-08-30T11:10:25.0687Z","shell.execute_reply":"2026-08-30T13:18:51.135084Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nimport pandas as pd\nimport os\n\noutput_dir = \"/kaggle/working/preprocessed_pilot2\"\nfiles = [f for f in os.listdir(output_dir) if f.endswith(\".npz\")]\n\nrecords = []\nfor fname in files:\n    clip_id = fname[:-4]\n    npz_path = os.path.join(output_dir, fname)\n    data = np.load(npz_path)\n    label = int(data[\"label\"])\n    records.append({\"clip_id\": clip_id, \"npz_path\": npz_path, \"label\": label})\n\nindex_df = pd.DataFrame(records)\nindex_df.to_csv(os.path.join(output_dir, \"index.csv\"), index=False)\nprint(\"Index written. Total clips:\", len(index_df))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T14:32:07.440845Z","iopub.execute_input":"2026-08-30T14:32:07.441099Z","iopub.status.idle":"2026-08-30T14:32:07.45198Z","shell.execute_reply.started":"2026-08-30T14:32:07.441064Z","shell.execute_reply":"2026-08-30T14:32:07.450832Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import shutil\nshutil.make_archive(\"/kaggle/working/preprocessed_pilot2_backup\", \"zip\", \"/kaggle/working/preprocessed_pilot2\")\n\nimport os\nsize_mb = os.path.getsize(\"/kaggle/working/preprocessed_pilot2_backup.zip\") / (1024*1024)\nprint(\"Backup zip created. Size: %.1f MB\" % size_mb)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T14:32:29.56762Z","iopub.execute_input":"2026-08-30T14:32:29.567841Z","iopub.status.idle":"2026-08-30T14:32:29.578531Z","shell.execute_reply.started":"2026-08-30T14:32:29.567807Z","shell.execute_reply":"2026-08-30T14:32:29.577262Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nprint(\"Current working dir:\", os.getcwd())\nprint(\"Contents of /kaggle/working:\")\nprint(os.listdir(\"/kaggle/working\"))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T14:38:14.805916Z","iopub.execute_input":"2026-08-30T14:38:14.806158Z","iopub.status.idle":"2026-08-30T14:38:14.810482Z","shell.execute_reply.started":"2026-08-30T14:38:14.806125Z","shell.execute_reply":"2026-08-30T14:38:14.809835Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nprint(\"preprocessed_pilot2 exists:\", os.path.isdir(\"/kaggle/working/preprocessed_pilot2\"))\nprint(\"Current directory:\", os.getcwd())\nprint(\"Contents:\", os.listdir(\"/kaggle/working/preprocessed_pilot2\")[:5])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T18:22:23.56375Z","iopub.execute_input":"2026-08-30T18:22:23.564192Z","iopub.status.idle":"2026-08-30T18:22:23.655187Z","shell.execute_reply.started":"2026-08-30T18:22:23.564114Z","shell.execute_reply":"2026-08-30T18:22:23.653777Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nprint(\"Working directory contents:\", os.listdir(\"/kaggle/working\"))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T18:22:56.98578Z","iopub.execute_input":"2026-08-30T18:22:56.986206Z","iopub.status.idle":"2026-08-30T18:22:56.992611Z","shell.execute_reply.started":"2026-08-30T18:22:56.986141Z","shell.execute_reply":"2026-08-30T18:22:56.991738Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import importlib\nimport preprocessing\nimportlib.reload(preprocessing)\n\nimport pandas as pd\nimport numpy as np\nimport sys\nimport os\n\nsample = pd.read_csv(\"/kaggle/working/video_list_pilot2.csv\")\noutput_dir = \"/kaggle/working/preprocessed_pilot2\"\nif not os.path.isdir(output_dir):\n    os.makedirs(output_dir)\n\nextractor = preprocessing.FaceBoundaryExtractor()\ntotal = len(sample)\n\nfor i, row in sample.iterrows():\n    clip_id = row[\"clip_id\"]\n    npz_path = os.path.join(output_dir, clip_id + \".npz\")\n\n    if os.path.isfile(npz_path):\n        continue\n\n    result = preprocessing.extract_clip(row[\"clip_type\"], row[\"clip_location\"], extractor)\n    if result is None:\n        continue\n\n    global_frame, boundary_diff_stack, num_valid, missing_frac = result\n    np.savez_compressed(npz_path, global_frame=global_frame,\n                         boundary_diff_stack=boundary_diff_stack, label=int(row[\"label\"]))\n\n    print(\"\\r[%d/%d] done: %s\" % (i + 1, total, clip_id), end=\"\")\n    sys.stdout.flush()\n\n    # Stop after every 300 clips so we can save progress\n    done_count = len([f for f in os.listdir(output_dir) if f.endswith(\".npz\")])\n    if done_count % 300 == 0 and done_count > 0:\n        print(\"\\n--- Reached %d clips, stopping this batch ---\" % done_count)\n        break\n\nextractor.close()\nfiles = os.listdir(output_dir)\nprint(\"\\nTotal files so far:\", len(files))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T18:26:04.918424Z","iopub.execute_input":"2026-08-30T18:26:04.919083Z","iopub.status.idle":"2026-08-30T18:26:05.70389Z","shell.execute_reply.started":"2026-08-30T18:26:04.918799Z","shell.execute_reply":"2026-08-30T18:26:05.702484Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nprint(\"Working directory contents:\", os.listdir(\"/kaggle/working\"))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T18:26:48.306012Z","iopub.execute_input":"2026-08-30T18:26:48.30636Z","iopub.status.idle":"2026-08-30T18:26:48.312422Z","shell.execute_reply.started":"2026-08-30T18:26:48.306282Z","shell.execute_reply":"2026-08-30T18:26:48.310407Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import shutil\nimport os\n\n# Step 1: Restore scripts from permanent dataset\nsrc = \"/kaggle/input/datasets/sheharyaaarr/tbc-net-scripts\"\nfor fname in os.listdir(src):\n    shutil.copy(os.path.join(src, fname), fname)\nprint(\"Scripts restored:\", os.listdir(\".\"))\n\n# Step 2: Rebuild manifest\nimport make_manifest\nimport importlib\nimportlib.reload(make_manifest)\nmake_manifest.build_manifest(\"/kaggle/working/video_list.csv\")\n\n# Step 3: Rebuild sample (smaller this time - 300 per source instead of 400, to reduce risk)\nimport pandas as pd\nfull_manifest = pd.read_csv(\"/kaggle/working/video_list.csv\")\nsample = full_manifest.groupby(\"source_dataset\", group_keys=False).apply(\n    lambda x: x.sample(min(300, len(x)), random_state=7)\n)\nsample.to_csv(\"/kaggle/working/video_list_pilot3.csv\", index=False)\nprint(\"Sample size:\", len(sample))\n\n# Step 4: Run preprocessing immediately, same cell\nimport preprocessing\nimportlib.reload(preprocessing)\nimport numpy as np\nimport sys\n\noutput_dir = \"/kaggle/working/preprocessed_pilot3\"\nif not os.path.isdir(output_dir):\n    os.makedirs(output_dir)\n\nextractor = preprocessing.FaceBoundaryExtractor()\ntotal = len(sample)\n\nfor i, row in sample.iterrows():\n    clip_id = row[\"clip_id\"]\n    npz_path = os.path.join(output_dir, clip_id + \".npz\")\n    if os.path.isfile(npz_path):\n        continue\n    result = preprocessing.extract_clip(row[\"clip_type\"], row[\"clip_location\"], extractor)\n    if result is None:\n        continue\n    global_frame, boundary_diff_stack, num_valid, missing_frac = result\n    np.savez_compressed(npz_path, global_frame=global_frame,\n                         boundary_diff_stack=boundary_diff_stack, label=int(row[\"label\"]))\n    print(\"\\r[%d/%d] done: %s\" % (i + 1, total, clip_id), end=\"\")\n    sys.stdout.flush()\n\nextractor.close()\nfiles = os.listdir(output_dir)\nprint(\"\\n\\nTOTAL FILES DONE:\", len(files))\n\n# Step 5: Immediately zip for backup\nshutil.make_archive(\"/kaggle/working/preprocessed_pilot3_backup\", \"zip\", output_dir)\nprint(\"Backup zip created:\", os.path.getsize(\"/kaggle/working/preprocessed_pilot3_backup.zip\") / (1024*1024), \"MB\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T18:27:20.153513Z","iopub.execute_input":"2026-08-30T18:27:20.153849Z","iopub.status.idle":"2026-08-30T18:59:09.548743Z","shell.execute_reply.started":"2026-08-30T18:27:20.153792Z","shell.execute_reply":"2026-08-30T18:59:09.547142Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\noutput_dir = \"/kaggle/working/preprocessed_pilot3\"\nif os.path.isdir(output_dir):\n    files = [f for f in os.listdir(output_dir) if f.endswith(\".npz\")]\n    print(\"ACTUAL files on disk:\", len(files))\nelse:\n    print(\"Folder doesn't exist\")\n\nprint(\"Full working dir:\", os.listdir(\"/kaggle/working\"))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T18:59:17.227604Z","iopub.execute_input":"2026-08-30T18:59:17.228088Z","iopub.status.idle":"2026-08-30T18:59:17.236972Z","shell.execute_reply.started":"2026-08-30T18:59:17.228032Z","shell.execute_reply":"2026-08-30T18:59:17.235603Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nimport pandas as pd\nimport os\nimport shutil\n\noutput_dir = \"/kaggle/working/preprocessed_pilot3\"\nfiles = [f for f in os.listdir(output_dir) if f.endswith(\".npz\")]\n\nrecords = []\nfor fname in files:\n    clip_id = fname[:-4]\n    npz_path = os.path.join(output_dir, fname)\n    data = np.load(npz_path)\n    label = int(data[\"label\"])\n    records.append({\"clip_id\": clip_id, \"npz_path\": npz_path, \"label\": label})\n\nindex_df = pd.DataFrame(records)\nindex_df.to_csv(os.path.join(output_dir, \"index.csv\"), index=False)\nprint(\"Index written. Total clips:\", len(index_df))\nprint(index_df[\"label\"].value_counts())\n\nshutil.make_archive(\"/kaggle/working/preprocessed_pilot3_backup\", \"zip\", output_dir)\nsize_mb = os.path.getsize(\"/kaggle/working/preprocessed_pilot3_backup.zip\") / (1024*1024)\nprint(\"Backup zip created. Size: %.1f MB\" % size_mb)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-30T18:59:47.981399Z","iopub.execute_input":"2026-08-30T18:59:47.981727Z","iopub.status.idle":"2026-08-30T18:59:49.925412Z","shell.execute_reply.started":"2026-08-30T18:59:47.981678Z","shell.execute_reply":"2026-08-30T18:59:49.924539Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import importlib\nimport pilot_signal_check\nimportlib.reload(pilot_signal_check)\n\nreport = pilot_signal_check.run_pilot_check(\n    index_csv=\"/kaggle/working/preprocessed_pilot3/index.csv\",\n    output_json=\"/kaggle/working/pilot_signal_report_v3.json\"\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-31T03:28:05.425519Z","iopub.execute_input":"2026-08-31T03:28:05.425858Z","iopub.status.idle":"2026-08-31T03:28:05.500445Z","shell.execute_reply.started":"2026-08-31T03:28:05.425816Z","shell.execute_reply":"2026-08-31T03:28:05.498455Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import shutil\nimport os\n\nsrc = \"/kaggle/input/datasets/sheharyaaarr/tbc-net-scripts\"\nfor fname in os.listdir(src):\n    shutil.copy(os.path.join(src, fname), fname)\nprint(\"Scripts restored:\", os.listdir(\".\"))\n\nprint(\"Competitions:\", os.listdir(\"/kaggle/input/competitions\") if os.path.isdir(\"/kaggle/input/competitions\") else \"none\")\nprint(\"Datasets:\", os.listdir(\"/kaggle/input/datasets\") if os.path.isdir(\"/kaggle/input/datasets\") else \"none\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-31T03:34:29.139565Z","iopub.execute_input":"2026-08-31T03:34:29.139821Z","iopub.status.idle":"2026-08-31T03:34:29.184383Z","shell.execute_reply.started":"2026-08-31T03:34:29.13979Z","shell.execute_reply":"2026-08-31T03:34:29.183111Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import make_manifest\nimport importlib\nimportlib.reload(make_manifest)\n\nmake_manifest.build_manifest(\"/kaggle/working/video_list.csv\")\n\nimport pandas as pd\nfull_manifest = pd.read_csv(\"/kaggle/working/video_list.csv\")\nprint(\"Total available:\", len(full_manifest))\nprint(full_manifest[\"source_dataset\"].value_counts())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-31T03:34:41.080998Z","iopub.execute_input":"2026-08-31T03:34:41.081443Z","iopub.status.idle":"2026-08-31T03:34:43.932837Z","shell.execute_reply.started":"2026-08-31T03:34:41.081374Z","shell.execute_reply":"2026-08-31T03:34:43.931465Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"sample = full_manifest.groupby(\"source_dataset\", group_keys=False).apply(\n    lambda x: x.sample(min(1600, len(x)), random_state=42)\n)\nsample.to_csv(\"/kaggle/working/video_list_train.csv\", index=False)\nprint(\"Training sample size:\", len(sample))\nprint(sample[\"source_dataset\"].value_counts())\nprint(sample[\"label\"].value_counts())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-31T03:35:05.229412Z","iopub.execute_input":"2026-08-31T03:35:05.229657Z","iopub.status.idle":"2026-08-31T03:35:05.837543Z","shell.execute_reply.started":"2026-08-31T03:35:05.229618Z","shell.execute_reply":"2026-08-31T03:35:05.836582Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import importlib\nimport preprocessing\nimportlib.reload(preprocessing)\n\nimport pandas as pd\nimport numpy as np\nimport sys\nimport os\nimport shutil\n\nsample = pd.read_csv(\"/kaggle/working/video_list_train.csv\")\noutput_dir = \"/kaggle/working/preprocessed_train\"\nif not os.path.isdir(output_dir):\n    os.makedirs(output_dir)\n\nextractor = preprocessing.FaceBoundaryExtractor()\ntotal = len(sample)\nCHUNK_SIZE = 300\n\nprocessed_this_run = 0\n\nfor i, row in sample.iterrows():\n    clip_id = row[\"clip_id\"]\n    npz_path = os.path.join(output_dir, clip_id + \".npz\")\n\n    if os.path.isfile(npz_path):\n        continue\n\n    result = preprocessing.extract_clip(row[\"clip_type\"], row[\"clip_location\"], extractor)\n    if result is not None:\n        global_frame, boundary_diff_stack, num_valid, missing_frac = result\n        np.savez_compressed(npz_path, global_frame=global_frame,\n                             boundary_diff_stack=boundary_diff_stack, label=int(row[\"label\"]))\n        processed_this_run += 1\n\n    print(\"\\r[%d/%d] processed_this_run=%d\" % (i + 1, total, processed_this_run), end=\"\")\n    sys.stdout.flush()\n\n    done_count = processed_this_run\n    if done_count > 0 and done_count % CHUNK_SIZE == 0:\n        total_on_disk = len([f for f in os.listdir(output_dir) if f.endswith(\".npz\")])\n        print(\"\\n>>> CHECKPOINT: %d total files on disk. Backing up...\" % total_on_disk)\n        shutil.make_archive(\"/kaggle/working/preprocessed_train_backup\", \"zip\", output_dir)\n        print(\">>> Backup updated.\\n\")\n\nextractor.close()\nfinal_count = len([f for f in os.listdir(output_dir) if f.endswith(\".npz\")])\nprint(\"\\n\\nFINAL: %d files on disk\" % final_count)\n\nshutil.make_archive(\"/kaggle/working/preprocessed_train_backup\", \"zip\", output_dir)\nprint(\"Final backup zip created.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-31T03:35:50.619031Z","iopub.execute_input":"2026-08-31T03:35:50.619262Z","iopub.status.idle":"2026-08-31T05:18:55.204137Z","shell.execute_reply.started":"2026-08-31T03:35:50.619227Z","shell.execute_reply":"2026-08-31T05:18:55.202632Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nimport pandas as pd\nimport os\n\noutput_dir = \"/kaggle/working/preprocessed_train\"\nfiles = [f for f in os.listdir(output_dir) if f.endswith(\".npz\")]\n\nrecords = []\nfor fname in files:\n    clip_id = fname[:-4]\n    npz_path = os.path.join(output_dir, fname)\n    data = np.load(npz_path)\n    label = int(data[\"label\"])\n    \n    # Recover source_dataset from clip_id prefix (needed for Protocol B)\n    if clip_id.startswith(\"dfdc_\"):\n        source = \"dfdc\"\n    elif clip_id.startswith(\"ffpp_\"):\n        source = \"ffpp\"\n    elif clip_id.startswith(\"cdf_\"):\n        source = \"cdf\"\n    else:\n        source = \"unknown\"\n    \n    records.append({\"clip_id\": clip_id, \"npz_path\": npz_path, \"label\": label, \"source_dataset\": source})\n\nindex_df = pd.DataFrame(records)\nindex_df.to_csv(os.path.join(output_dir, \"index.csv\"), index=False)\nprint(\"Index written. Total clips:\", len(index_df))\nprint(index_df[\"label\"].value_counts())\nprint(index_df[\"source_dataset\"].value_counts())","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import shutil\nimport os\n\nsrc = \"/kaggle/input/datasets/sheharyaaarr/tbc-net-scripts\"\nfor fname in os.listdir(src):\n    shutil.copy(os.path.join(src, fname), fname)\nprint(\"Scripts restored:\", os.listdir(\".\"))\n\nimport torch\nprint(\"CUDA available:\", torch.cuda.is_available())\nprint(\"GPU name:\", torch.cuda.get_device_name(0) if torch.cuda.is_available() else \"none\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-31T05:40:03.870537Z","iopub.execute_input":"2026-08-31T05:40:03.871153Z","iopub.status.idle":"2026-08-31T05:40:03.943909Z","shell.execute_reply.started":"2026-08-31T05:40:03.871123Z","shell.execute_reply":"2026-08-31T05:40:03.943314Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"Competitions:\", os.listdir(\"/kaggle/input/competitions\") if os.path.isdir(\"/kaggle/input/competitions\") else \"none\")\nprint(\"Datasets:\", os.listdir(\"/kaggle/input/datasets\") if os.path.isdir(\"/kaggle/input/datasets\") else \"none\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-31T05:40:08.08193Z","iopub.execute_input":"2026-08-31T05:40:08.082354Z","iopub.status.idle":"2026-08-31T05:40:08.088332Z","shell.execute_reply.started":"2026-08-31T05:40:08.082324Z","shell.execute_reply":"2026-08-31T05:40:08.087746Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import zipfile\nimport os\n\nzip_path = \"preprocessed_train_backup.zip\"\nextract_dir = \"/kaggle/working/preprocessed_train\"\n\nif os.path.isfile(zip_path):\n    os.makedirs(extract_dir, exist_ok=True)\n    with zipfile.ZipFile(zip_path, \"r\") as z:\n        z.extractall(extract_dir)\n    files = [f for f in os.listdir(extract_dir) if f.endswith(\".npz\")]\n    print(\"Extracted files:\", len(files))\nelse:\n    print(\"Zip not found yet - please upload preprocessed_train_backup.zip via the file browser's upload button first\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-31T05:57:33.442106Z","iopub.execute_input":"2026-08-31T05:57:33.442526Z","iopub.status.idle":"2026-08-31T05:57:33.448248Z","shell.execute_reply.started":"2026-08-31T05:57:33.442497Z","shell.execute_reply":"2026-08-31T05:57:33.447524Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nprint(os.listdir(\"/kaggle/input\"))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-31T05:54:51.040609Z","iopub.execute_input":"2026-08-31T05:54:51.041235Z","iopub.status.idle":"2026-08-31T05:54:51.045229Z","shell.execute_reply.started":"2026-08-31T05:54:51.041207Z","shell.execute_reply":"2026-08-31T05:54:51.044583Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nfor root_folder in [\"datasets\", \"competitions\"]:\n    path = os.path.join(\"/kaggle/input\", root_folder)\n    print(f\"--- {path} ---\")\n    print(os.listdir(path))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-31T05:55:17.575368Z","iopub.execute_input":"2026-08-31T05:55:17.575882Z","iopub.status.idle":"2026-08-31T05:55:17.580859Z","shell.execute_reply.started":"2026-08-31T05:55:17.575855Z","shell.execute_reply":"2026-08-31T05:55:17.580027Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nfor user in [\"sheharyaaarr\", \"reubensuju\", \"gradientvoyager\"]:\n    path = os.path.join(\"/kaggle/input/datasets\", user)\n    print(f\"--- {path} ---\")\n    print(os.listdir(path))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-31T05:55:51.970584Z","iopub.execute_input":"2026-08-31T05:55:51.971282Z","iopub.status.idle":"2026-08-31T05:55:51.976656Z","shell.execute_reply.started":"2026-08-31T05:55:51.97125Z","shell.execute_reply":"2026-08-31T05:55:51.976106Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\npath = \"/kaggle/input/datasets/sheharyaaarr/preprocessed-train-backup-zip\"\nprint(os.listdir(path))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-31T05:56:21.237275Z","iopub.execute_input":"2026-08-31T05:56:21.23795Z","iopub.status.idle":"2026-08-31T05:56:21.303493Z","shell.execute_reply.started":"2026-08-31T05:56:21.237918Z","shell.execute_reply":"2026-08-31T05:56:21.302883Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import shutil\nimport os\n\nsrc = \"/kaggle/input/datasets/sheharyaaarr/tbc-net-scripts\"\nfor fname in os.listdir(src):\n    shutil.copy(os.path.join(src, fname), fname)\nprint(\"Scripts restored:\", os.listdir(\".\"))\n\nimport torch\nprint(\"CUDA available:\", torch.cuda.is_available())\nprint(\"GPU name:\", torch.cuda.get_device_name(0) if torch.cuda.is_available() else \"none\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-31T09:19:00.010973Z","iopub.execute_input":"2026-08-31T09:19:00.011648Z","iopub.status.idle":"2026-08-31T09:19:03.77156Z","shell.execute_reply.started":"2026-08-31T09:19:00.011617Z","shell.execute_reply":"2026-08-31T09:19:03.770878Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import numpy as np\nimport pandas as pd\nimport os\n\ndata_dir = \"/kaggle/input/datasets/sheharyaaarr/preprocessed-train-backup-zip\"\nfiles = [f for f in os.listdir(data_dir) if f.endswith(\".npz\")]\nprint(\"Total files found:\", len(files))\n\nrecords = []\nfor fname in files:\n    clip_id = fname[:-4]\n    npz_path = os.path.join(data_dir, fname)\n    data = np.load(npz_path)\n    label = int(data[\"label\"])\n    if clip_id.startswith(\"dfdc_\"):\n        source = \"dfdc\"\n    elif clip_id.startswith(\"ffpp_\"):\n        source = \"ffpp\"\n    elif clip_id.startswith(\"cdf_\"):\n        source = \"cdf\"\n    else:\n        source = \"unknown\"\n    records.append({\"clip_id\": clip_id, \"npz_path\": npz_path, \"label\": label, \"source_dataset\": source})\n\nindex_df = pd.DataFrame(records)\nindex_out = \"/kaggle/working/train_index.csv\"\nindex_df.to_csv(index_out, index=False)\nprint(\"Index written:\", index_out)\nprint(index_df[\"label\"].value_counts())\nprint(index_df[\"source_dataset\"].value_counts())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-31T09:22:06.842989Z","iopub.execute_input":"2026-08-31T09:22:06.843903Z","iopub.status.idle":"2026-08-31T09:22:33.425155Z","shell.execute_reply.started":"2026-08-31T09:22:06.843873Z","shell.execute_reply":"2026-08-31T09:22:33.424567Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%%writefile dataset.py\nimport numpy as np\nimport pandas as pd\nimport torch\nfrom torch.utils.data import Dataset\n\nSEED = 42\n\n\nclass TBCNetDataset(Dataset):\n    def __init__(self, index_df):\n        self.records = index_df.reset_index(drop=True)\n\n    def __len__(self):\n        return len(self.records)\n\n    def __getitem__(self, idx):\n        row = self.records.iloc[idx]\n        data = np.load(row[\"npz_path\"])\n\n        global_frame = data[\"global_frame\"].astype(np.float32) / 255.0\n        global_frame = np.transpose(global_frame, (2, 0, 1))\n\n        boundary_stack = data[\"boundary_diff_stack\"].astype(np.float32)\n        boundary_stack = np.expand_dims(boundary_stack, axis=1)\n\n        label = int(row[\"label\"])\n\n        return {\n            \"global_frame\": torch.from_numpy(global_frame),\n            \"boundary_stack\": torch.from_numpy(boundary_stack),\n            \"label\": torch.tensor(label, dtype=torch.long),\n            \"clip_id\": row[\"clip_id\"],\n        }\n\n\ndef build_protocol_a_splits(index_csv, val_frac=0.15,","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-31T09:25:04.613916Z","iopub.execute_input":"2026-08-31T09:25:04.614307Z","iopub.status.idle":"2026-08-31T09:25:04.62019Z","shell.execute_reply.started":"2026-08-31T09:25:04.614278Z","shell.execute_reply":"2026-08-31T09:25:04.619572Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"with open(\"dataset.py\", \"r\") as f:\n    content = f.read()\nprint(\"File length:\", len(content))\nprint(\"Contains 'get_class_weights':\", \"get_class_weights\" in content)\nprint(\"Last 200 characters:\")\nprint(content[-200:])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-31T09:25:25.300271Z","iopub.execute_input":"2026-08-31T09:25:25.300685Z","iopub.status.idle":"2026-08-31T09:25:25.306137Z","shell.execute_reply.started":"2026-08-31T09:25:25.300655Z","shell.execute_reply":"2026-08-31T09:25:25.305484Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"with open(\"dataset.py\", \"r\") as f:\n    content = f.read()\nprint(\"Total length:\", len(content))\nprint(content)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-31T09:26:19.816048Z","iopub.execute_input":"2026-08-31T09:26:19.816538Z","iopub.status.idle":"2026-08-31T09:26:19.821613Z","shell.execute_reply.started":"2026-08-31T09:26:19.816511Z","shell.execute_reply":"2026-08-31T09:26:19.820797Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%%writefile dataset.py\nimport numpy as np\nimport pandas as pd\nimport torch\nfrom torch.utils.data import Dataset\n\nSEED = 42\n\n\nclass TBCNetDataset(Dataset):\n    def __init__(self, index_df):\n        self.records = index_df.reset_index(drop=True)\n\n    def __len__(self):\n        return len(self.records)\n\n    def __getitem__(self, idx):\n        row = self.records.iloc[idx]\n        data = np.load(row[\"npz_path\"])\n        global_frame = data[\"global_frame\"].astype(np.float32) / 255.0\n        global_frame = np.transpose(global_frame, (2, 0, 1))\n        boundary_stack = data[\"boundary_diff_stack\"].astype(np.float32)\n        boundary_stack = np.expand_dims(boundary_stack, axis=1)\n        label = int(row[\"label\"])\n        return {\n            \"global_frame\": torch.from_numpy(global_frame),\n            \"boundary_stack\": torch.from_numpy(boundary_stack),\n            \"label\": torch.tensor(label, dtype=torch.long),\n            \"clip_id\": row[\"clip_id\"],\n        }","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-31T09:26:55.686993Z","iopub.execute_input":"2026-08-31T09:26:55.687805Z","iopub.status.idle":"2026-08-31T09:26:55.693043Z","shell.execute_reply.started":"2026-08-31T09:26:55.687763Z","shell.execute_reply":"2026-08-31T09:26:55.692412Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"with open(\"dataset.py\", \"a\") as f:\n    f.write('''\n\ndef build_protocol_a_splits(index_csv, val_frac=0.15, test_frac=0.15, seed=SEED):\n    df = pd.read_csv(index_csv)\n    train_parts = []\n    val_parts = []\n    test_parts = []\n    if \"source_dataset\" in df.columns:\n        group_cols = [\"source_dataset\", \"label\"]\n    else:\n        group_cols = [\"label\"]\n    for _, group in df.groupby(group_cols):\n        group = group.sample(frac=1.0, random_state=seed).reset_index(drop=True)\n        n = len(group)\n        n_val = int(n * val_frac)\n        n_test = int(n * test_frac)\n        val_parts.append(group.iloc[:n_val])\n        test_parts.append(group.iloc[n_val:n_val + n_test])\n        train_parts.append(group.iloc[n_val + n_test:])\n    train_df = pd.concat(train_parts).reset_index(drop=True)\n    val_df = pd.concat(val_parts).reset_index(drop=True)\n    test_df = pd.concat(test_parts).reset_index(drop=True)\n    return train_df, val_df, test_df\n\n\ndef build_protocol_b_splits(index_csv, held_out_source, val_frac=0.15, seed=SEED):\n    df = pd.read_csv(index_csv)\n    if \"source_dataset\" not in df.columns:\n        raise ValueError(\"index_csv needs a source_dataset column for Protocol B\")\n    train_pool = df[df[\"source_dataset\"] != held_out_source].reset_index(drop=True)\n    test_df = df[df[\"source_dataset\"] == held_out_source].reset_index(drop=True)\n    train_parts = []\n    val_parts = []\n    for _, group in train_pool.groupby(\"label\"):\n        group = group.sample(frac=1.0, random_state=seed).reset_index(drop=True)\n        n_val = int(len(group) * val_frac)\n        val_parts.append(group.iloc[:n_val])\n        train_parts.append(group.iloc[n_val:])\n    train_df = pd.concat(train_parts).reset_index(drop=True)\n    val_df = pd.concat(val_parts).reset_index(drop=True)\n    return train_df, val_df, test_df\n\n\ndef get_class_weights(train_df):\n    counts = train_df[\"label\"].value_counts().sort_index()\n    total = counts.sum()\n    weights = total / (len(counts) * counts)\n    return torch.tensor(weights.values, dtype=torch.float32)\n''')\nprint(\"Appended successfully\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-31T09:27:06.652163Z","iopub.execute_input":"2026-08-31T09:27:06.652871Z","iopub.status.idle":"2026-08-31T09:27:06.660347Z","shell.execute_reply.started":"2026-08-31T09:27:06.65284Z","shell.execute_reply":"2026-08-31T09:27:06.659477Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"with open(\"dataset.py\", \"r\") as f:\n    content = f.read()\nprint(\"Total length:\", len(content))\nprint(\"Contains 'get_class_weights':\", \"get_class_weights\" in content)\n\nimport importlib\nimport dataset\nimportlib.reload(dataset)\nprint(\"Imported successfully. Functions:\", [x for x in dir(dataset) if not x.startswith(\"_\")])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-31T09:27:21.930743Z","iopub.execute_input":"2026-08-31T09:27:21.930993Z","iopub.status.idle":"2026-08-31T09:27:21.938991Z","shell.execute_reply.started":"2026-08-31T09:27:21.930973Z","shell.execute_reply":"2026-08-31T09:27:21.938215Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"with open(\"train.py\", \"r\") as f:\n    content = f.read()\nprint(\"Total length:\", len(content))\nprint(\"Contains 'run_protocol_a':\", \"run_protocol_a\" in content)\nprint(\"Last 300 characters:\")\nprint(content[-300:])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-31T09:31:30.607114Z","iopub.execute_input":"2026-08-31T09:31:30.607577Z","iopub.status.idle":"2026-08-31T09:31:30.613111Z","shell.execute_reply.started":"2026-08-31T09:31:30.607536Z","shell.execute_reply":"2026-08-31T09:31:30.612411Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"with open(\"train.py\", \"a\") as f:\n    f.write('''\n\ndef run_protocol_a(index_csv, model_type=\"full\", num_epochs=NUM_EPOCHS):\n    print(\"=== Protocol A (combined pool) | model=%s ===\" % model_type)\n    train_df, val_df, test_df = build_protocol_a_splits(index_csv)\n    print(\"Train: %d | Val: %d | Test: %d\" % (len(train_df), len(val_df), len(test_df)))\n    checkpoint_path = \"checkpoint_protocol_a_%s.pt\" % model_type\n    return train_one_run(train_df, val_df, test_df, model_type, checkpoint_path, num_epochs)\n\n\ndef run_protocol_b(index_csv, held_out_source, model_type=\"full\", num_epochs=NUM_EPOCHS):\n    print(\"=== Protocol B (leave-%s-out) | model=%s ===\" % (held_out_source, model_type))\n    train_df, val_df, test_df = build_protocol_b_splits(index_csv, held_out_source)\n    print(\"Train: %d | Val: %d | Test(%s): %d\" % (len(train_df), len(val_df), held_out_source, len(test_df)))\n    checkpoint_path = \"checkpoint_protocol_b_%s_%s.pt\" % (held_out_source, model_type)\n    return train_one_run(train_df, val_df, test_df, model_type, checkpoint_path, num_epochs)\n''')\nprint(\"Appended successfully\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-31T09:31:59.526462Z","iopub.execute_input":"2026-08-31T09:31:59.526915Z","iopub.status.idle":"2026-08-31T09:31:59.532258Z","shell.execute_reply.started":"2026-08-31T09:31:59.526887Z","shell.execute_reply":"2026-08-31T09:31:59.531414Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"with open(\"train.py\", \"r\") as f:\n    content = f.read()\nprint(\"Total length:\", len(content))\nprint(\"Contains 'run_protocol_a':\", \"run_protocol_a\" in content)\nprint(\"Contains 'run_protocol_b':\", \"run_protocol_b\" in content)\n\nimport importlib\nimport train\nimportlib.reload(train)\nprint(\"Functions available:\", [x for x in dir(train) if x.startswith(\"run_\") or x.startswith(\"train_\")])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-31T09:32:09.865163Z","iopub.execute_input":"2026-08-31T09:32:09.865775Z","iopub.status.idle":"2026-08-31T09:32:09.875071Z","shell.execute_reply.started":"2026-08-31T09:32:09.865747Z","shell.execute_reply":"2026-08-31T09:32:09.874447Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import importlib\nimport train\nimportlib.reload(train)\n\nresult_full = train.run_protocol_a(\"/kaggle/working/train_index.csv\", model_type=\"full\", num_epochs=30)\nprint(\"\\n\\n=== FULL MODEL FINAL RESULT ===\")\nprint(result_full)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-31T09:36:30.486207Z","iopub.execute_input":"2026-08-31T09:36:30.486477Z","iopub.status.idle":"2026-08-31T09:43:45.141219Z","shell.execute_reply.started":"2026-08-31T09:36:30.486455Z","shell.execute_reply":"2026-08-31T09:43:45.140562Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"with open(\"train.py\", \"r\") as f:\n    content = f.read()\n\n# Find the exact location before run_protocol_a\nmarker = 'print_metrics(\"  TEST \", test_metrics)\\n\\n\\ndef run_protocol_a'\nif marker in content:\n    fixed = content.replace(\n        'print_metrics(\"  TEST \", test_metrics)\\n\\n\\ndef run_protocol_a',\n        'print_metrics(\"  TEST \", test_metrics)\\n    return test_metrics\\n\\n\\ndef run_protocol_a'\n    )\n    with open(\"train.py\", \"w\") as f:\n        f.write(fixed)\n    print(\"Fixed: added missing 'return test_metrics'\")\nelse:\n    print(\"Marker not found - checking manually\")\n    print(content[-500:])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-31T09:44:47.252955Z","iopub.execute_input":"2026-08-31T09:44:47.254033Z","iopub.status.idle":"2026-08-31T09:44:47.261151Z","shell.execute_reply.started":"2026-08-31T09:44:47.253988Z","shell.execute_reply":"2026-08-31T09:44:47.260478Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"with open(\"train.py\", \"r\") as f:\n    content = f.read()\nprint(\"Contains 'return test_metrics':\", \"return test_metrics\" in content)\n\nimport importlib\nimport train\nimportlib.reload(train)\nprint(\"train module reloaded\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-31T09:44:59.186382Z","iopub.execute_input":"2026-08-31T09:44:59.1869Z","iopub.status.idle":"2026-08-31T09:44:59.196654Z","shell.execute_reply.started":"2026-08-31T09:44:59.186872Z","shell.execute_reply":"2026-08-31T09:44:59.195818Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"result_full = {\n    \"loss\": 0.4813,\n    \"accuracy\": 0.7821,\n    \"auc\": 0.7493,\n    \"precision_real\": 0.355,\n    \"recall_real\": 0.459,\n    \"f1_real\": 0.400,\n    \"precision_fake\": 0.892,\n    \"recall_fake\": 0.843,\n    \"f1_fake\": 0.867,\n}\nprint(\"Full model result recorded:\", result_full)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-31T09:45:45.143744Z","iopub.execute_input":"2026-08-31T09:45:45.144012Z","iopub.status.idle":"2026-08-31T09:45:45.149086Z","shell.execute_reply.started":"2026-08-31T09:45:45.143988Z","shell.execute_reply":"2026-08-31T09:45:45.148414Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nprint(\"CUDA available:\", torch.cuda.is_available())\nprint(\"Current device:\", torch.cuda.current_device() if torch.cuda.is_available() else \"CPU\")\nprint(\"Device name:\", torch.cuda.get_device_name(0) if torch.cuda.is_available() else \"N/A\")\n\n# Also check what train.py is actually using\nimport train\nprint(\"train.py's DEVICE setting:\", train.DEVICE)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-31T09:47:03.369668Z","iopub.execute_input":"2026-08-31T09:47:03.370118Z","iopub.status.idle":"2026-08-31T09:47:03.375848Z","shell.execute_reply.started":"2026-08-31T09:47:03.37009Z","shell.execute_reply":"2026-08-31T09:47:03.375233Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"result_full = {\n    \"loss\": 0.4813,\n    \"accuracy\": 0.7821,\n    \"auc\": 0.7493,\n    \"precision_real\": 0.355,\n    \"recall_real\": 0.459,\n    \"f1_real\": 0.400,\n    \"precision_fake\": 0.892,\n    \"recall_fake\": 0.843,\n    \"f1_fake\": 0.867,\n}\nprint(\"Full model result recorded:\", result_full)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-31T09:47:27.884099Z","iopub.execute_input":"2026-08-31T09:47:27.884547Z","iopub.status.idle":"2026-08-31T09:47:27.889518Z","shell.execute_reply.started":"2026-08-31T09:47:27.884478Z","shell.execute_reply":"2026-08-31T09:47:27.888784Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"result_global = train.run_protocol_a(\"/kaggle/working/train_index.csv\", model_type=\"global_only\", num_epochs=30)\nprint(\"\\n\\n=== GLOBAL-ONLY FINAL RESULT ===\")\nprint(result_global)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-31T09:47:56.938567Z","iopub.execute_input":"2026-08-31T09:47:56.939014Z","iopub.status.idle":"2026-08-31T09:53:14.906453Z","shell.execute_reply.started":"2026-08-31T09:47:56.938985Z","shell.execute_reply":"2026-08-31T09:53:14.905783Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import json\n\nall_results = {\n    \"full\": result_full,\n    \"global_only\": result_global,\n    \"boundary_only\": result_boundary,\n}\n\nwith open(\"/kaggle/working/protocol_a_results.json\", \"w\") as f:\n    json.dump(all_results, f, indent=2)\n\nprint(\"=== SUMMARY: Protocol A Results ===\")\nfor model_type, res in all_results.items():\n    print(\"%-15s | acc=%.4f auc=%.4f | fake_f1=%.4f real_f1=%.4f\" % (\n        model_type, res[\"accuracy\"], res[\"auc\"], res[\"f1_fake\"], res[\"f1_real\"]))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-31T10:03:42.49641Z","iopub.execute_input":"2026-08-31T10:03:42.497275Z","iopub.status.idle":"2026-08-31T10:03:42.503985Z","shell.execute_reply.started":"2026-08-31T10:03:42.497231Z","shell.execute_reply":"2026-08-31T10:03:42.503248Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"=\" * 80)\nprint(\"GAP TABLE — Comparison against plan benchmarks\")\nprint(\"=\" * 80)\nprint(\"%-20s | %-15s | %-15s\" % (\"Reference\", \"Same-source\", \"Notes\"))\nprint(\"-\" * 80)\nprint(\"%-20s | %-15s | %s\" % (\"Base paper (3DCNN)\", \"89.6-97.7%%\", \"combined-pool: 64.68%%\"))\nprint(\"%-20s | %-15s | %s\" % (\"TinyDF\", \"93.84%%\", \"DFDC: 68.58%% acc / 74.90%% AUC\"))\nprint(\"%-20s | %.2f%%          | %s\" % (\"TBC-Net (ours, Full)\", result_full[\"accuracy\"]*100, \"Protocol A combined pool\"))\nprint(\"%-20s | %.2f%%          | %s\" % (\"  - GlobalOnly\", result_global[\"accuracy\"]*100, \"ablation\"))\nprint(\"%-20s | %.2f%%          | %s\" % (\"  - BoundaryOnly\", result_boundary[\"accuracy\"]*100, \"ablation\"))\nprint(\"=\" * 80)\nprint(\"\\nDoes boundary branch help? Full vs GlobalOnly accuracy delta: %.2f points\" % \n      ((result_full[\"accuracy\"] - result_global[\"accuracy\"]) * 100))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-31T10:03:46.791266Z","iopub.execute_input":"2026-08-31T10:03:46.791831Z","iopub.status.idle":"2026-08-31T10:03:46.797926Z","shell.execute_reply.started":"2026-08-31T10:03:46.791801Z","shell.execute_reply":"2026-08-31T10:03:46.797054Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"result_boundary = train.run_protocol_a(\"/kaggle/working/train_index.csv\", model_type=\"boundary_only\", num_epochs=30)\nprint(\"\\n\\n=== BOUNDARY-ONLY FINAL RESULT ===\")\nprint(result_boundary)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-31T09:56:14.501348Z","iopub.execute_input":"2026-08-31T09:56:14.50177Z","iopub.status.idle":"2026-08-31T10:03:17.112657Z","shell.execute_reply.started":"2026-08-31T09:56:14.501742Z","shell.execute_reply":"2026-08-31T10:03:17.112012Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import json\n\nall_results = {\n    \"full\": result_full,\n    \"global_only\": result_global,\n    \"boundary_only\": result_boundary,\n}\n\n# Convert numpy types to plain floats for clean JSON\ndef clean(d):\n    return {k: float(v) for k, v in d.items()}\n\nclean_results = {k: clean(v) for k, v in all_results.items()}\n\nwith open(\"/kaggle/working/protocol_a_results.json\", \"w\") as f:\n    json.dump(clean_results, f, indent=2)\n\nprint(\"Saved. Contents:\")\nprint(json.dumps(clean_results, indent=2))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-31T10:07:28.777917Z","iopub.execute_input":"2026-08-31T10:07:28.778331Z","iopub.status.idle":"2026-08-31T10:07:28.785043Z","shell.execute_reply.started":"2026-08-31T10:07:28.778302Z","shell.execute_reply":"2026-08-31T10:07:28.784198Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nprint(\"File exists:\", os.path.isfile(\"/kaggle/working/protocol_a_results.json\"))\nprint(\"Full working directory listing:\", os.listdir(\"/kaggle/working\"))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-31T10:09:26.547185Z","iopub.execute_input":"2026-08-31T10:09:26.547623Z","iopub.status.idle":"2026-08-31T10:09:26.552441Z","shell.execute_reply.started":"2026-08-31T10:09:26.547594Z","shell.execute_reply":"2026-08-31T10:09:26.551892Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"with open(\"/kaggle/working/protocol_a_results.json\", \"r\") as f:\n    print(f.read())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-31T10:10:24.662722Z","iopub.execute_input":"2026-08-31T10:10:24.663442Z","iopub.status.idle":"2026-08-31T10:10:24.668293Z","shell.execute_reply.started":"2026-08-31T10:10:24.663411Z","shell.execute_reply":"2026-08-31T10:10:24.667648Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"with open(\"/kaggle/working/protocol_a_results.json\", \"r\") as f:\n    print(f.read())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-31T10:11:24.434316Z","iopub.execute_input":"2026-08-31T10:11:24.435012Z","iopub.status.idle":"2026-08-31T10:11:24.439245Z","shell.execute_reply.started":"2026-08-31T10:11:24.434973Z","shell.execute_reply":"2026-08-31T10:11:24.43868Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nfor fname in [\"checkpoint_protocol_a_full.pt\", \"checkpoint_protocol_a_global_only.pt\", \"checkpoint_protocol_a_boundary_only.pt\"]:\n    exists = os.path.isfile(fname)\n    size = os.path.getsize(fname) / 1024 if exists else 0\n    print(\"%s | exists: %s | size: %.1f KB\" % (fname, exists, size))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-31T10:16:21.754923Z","iopub.execute_input":"2026-08-31T10:16:21.755342Z","iopub.status.idle":"2026-08-31T10:16:21.760569Z","shell.execute_reply.started":"2026-08-31T10:16:21.755312Z","shell.execute_reply":"2026-08-31T10:16:21.759551Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport shutil\n\nproject_dir = \"/kaggle/working/TBC-Net-Project\"\nos.makedirs(project_dir, exist_ok=True)\n\n# Copy all code files\nfor fname in [\"preprocessing.py\", \"make_manifest.py\", \"pilot_signal_check.py\", \"model.py\", \"dataset.py\", \"train.py\"]:\n    if os.path.isfile(fname):\n        shutil.copy(fname, os.path.join(project_dir, fname))\n\n# Copy results and checkpoints\nfor fname in [\"protocol_a_results.json\", \"checkpoint_protocol_a_full.pt\", \n              \"checkpoint_protocol_a_global_only.pt\", \"checkpoint_protocol_a_boundary_only.pt\",\n              \"train_index.csv\", \"video_list.csv\"]:\n    if os.path.isfile(fname):\n        shutil.copy(fname, os.path.join(project_dir, fname))\n\nprint(\"Files copied:\", os.listdir(project_dir))\n\nshutil.make_archive(\"/kaggle/working/TBC-Net-Complete-Project\", \"zip\", project_dir)\nsize_mb = os.path.getsize(\"/kaggle/working/TBC-Net-Complete-Project.zip\") / (1024*1024)\nprint(\"\\nZip created: TBC-Net-Complete-Project.zip (%.1f MB)\" % size_mb)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-31T10:20:15.919457Z","iopub.execute_input":"2026-08-31T10:20:15.919788Z","iopub.status.idle":"2026-08-31T10:20:16.014182Z","shell.execute_reply.started":"2026-08-31T10:20:15.919762Z","shell.execute_reply":"2026-08-31T10:20:16.013442Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nprint(\"Zip exists:\", os.path.isfile(\"/kaggle/working/TBC-Net-Complete-Project.zip\"))\nprint(\"Size:\", os.path.getsize(\"/kaggle/working/TBC-Net-Complete-Project.zip\") / (1024*1024), \"MB\")\nprint(\"\\nFull working directory:\")\nprint(os.listdir(\"/kaggle/working\"))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-31T10:22:12.837491Z","iopub.execute_input":"2026-08-31T10:22:12.838199Z","iopub.status.idle":"2026-08-31T10:22:12.843152Z","shell.execute_reply.started":"2026-08-31T10:22:12.838171Z","shell.execute_reply":"2026-08-31T10:22:12.842567Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"summary = \"\"\"TBC-Net: Temporal Boundary-Consistency Network\nImplementation Summary\n========================================\n\nPROJECT: Deepfake video detection using a boundary-ring temporal \nconsistency signal, combined with a global spatial branch.\n\nENVIRONMENT CONSTRAINTS AND ADAPTATIONS\n----------------------------------------\nThis implementation was built on a Kaggle notebook environment running\nPython 3.6.6 (an older pinned environment). Two adaptations were made\nto the original plan due to library availability:\n\n1. Face detection: MediaPipe (specified in the original plan) has no\n   Python 3.6 support. OpenCV's Haar Cascade classifier was used instead,\n   with an elliptical mask approximating the face boundary region.\n\n2. Spatial branch backbone: MobileNetV3-Small (specified in the original\n   plan) predates this environment's torchvision version. A custom\n   lightweight CNN was used instead, serving the same architectural role.\n\nThese substitutions are documented here for transparency and should be\nnoted in any write-up of this work.\n\nDATASETS USED\n-------------\n- DFDC (Deepfake Detection Challenge): 400 clips (77 real, 323 fake)\n- FaceForensics++ (C23): 6,417 clips (930 real, 5,487 fake)\n- Celeb-DF v2: 6,529 clips (890 real, 5,639 fake)\nTraining subset used: 3,600 clips (balanced sampling, capped by DFDC's\nsmaller size)\n\nPHASE 3: PILOT SIGNAL CHECK\n----------------------------\nThree independent statistical tests (Welch's t-test + Cohen's d) were\nrun on samples of 450, 1200, and 382 clips, testing whether boundary-ring\nframe-difference statistics differ between real and fake clips.\n\nResult: A small but consistent effect was found (Cohen's d approximately\n0.17-0.37 across metrics and runs), with real clips showing HIGHER\nboundary-diff variance than fake clips -- the reverse of the original\nhypothesis (which predicted fakes would show more temporal jitter).\nVerdict: CAUTION (per the project's own pre-defined GO/CAUTION/NO-GO\nthresholds).\n\nPHASE 4: ARCHITECTURE\n----------------------\nTBC-Net: 178,754 parameters total (well under the 5M budget target).\n- Spatial branch: custom CNN -> 128-d embedding\n- Temporal branch: per-frame CNN + GRU pooling -> 128-d embedding\n- Fusion head: concatenation -> FC(128) -> Dropout(0.3) -> FC(2)\n\nPHASE 5 & 7: TRAINING AND ABLATIONS (Protocol A - combined pool)\n-------------------------------------------------------------------\nTrained for 30 epochs each, Adam optimizer, lr=0.001, batch size 16,\non a Tesla T4 GPU.\n\nModel            | Accuracy | AUC    | Real F1 | Fake F1\n-----------------|----------|--------|---------|--------\nFull (both branches) | 78.21%   | 0.749  | 0.400   | 0.867\nGlobalOnly ablation   | 81.19%   | 0.599  | 0.165   | 0.894\nBoundaryOnly ablation | 71.51%   | 0.718  | 0.390   | 0.814\n\nKEY FINDING: While GlobalOnly shows higher raw accuracy, this is an\nartifact of class imbalance (3,027 fake vs 573 real in training) --\nGlobalOnly's real-class recall is only 0.118, meaning it is largely\njust predicting \"fake.\" AUC is the more reliable metric under this\nimbalance, and by that measure the Full model (0.749) clearly\noutperforms GlobalOnly (0.599), with BoundaryOnly alone (0.718) also\nsubstantially outperforming GlobalOnly. This demonstrates that the\nboundary-temporal branch contributes genuine, measurable discriminative\nvalue beyond what the spatial branch captures alone.\n\nLIMITATIONS AND FUTURE WORK\n-----------------------------\n- Protocol B (leave-one-dataset-out) was not completed in this session\n  due to time constraints; only Protocol A (combined pool) results are\n  reported here.\n- Class imbalance in the training data affected raw accuracy\n  interpretation; balanced accuracy or F1-macro should be reported\n  alongside raw accuracy in future analysis.\n- Sample size (3,600 clips) is modest relative to the full available\n  pool (13,346 clips); a larger training run may improve results.\n- The environment substitutions (OpenCV vs MediaPipe, custom CNN vs\n  MobileNetV3) should be acknowledged as methodology adaptations in\n  any formal write-up, not hidden.\n\"\"\"\n\nwith open(\"/kaggle/working/PROJECT_SUMMARY.txt\", \"w\") as f:\n    f.write(summary)\n\nprint(\"Summary written to PROJECT_SUMMARY.txt\")\nprint(summary)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-31T10:22:56.041293Z","iopub.execute_input":"2026-08-31T10:22:56.041999Z","iopub.status.idle":"2026-08-31T10:22:56.048817Z","shell.execute_reply.started":"2026-08-31T10:22:56.041962Z","shell.execute_reply":"2026-08-31T10:22:56.048061Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"with open(\"PROJECT_SUMMARY.txt\", \"r\") as f:\n    content = f.read()\nprint(content)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-31T10:23:38.915646Z","iopub.execute_input":"2026-08-31T10:23:38.916257Z","iopub.status.idle":"2026-08-31T10:23:38.920904Z","shell.execute_reply.started":"2026-08-31T10:23:38.916228Z","shell.execute_reply":"2026-08-31T10:23:38.920284Z"}},"outputs":[],"execution_count":null}]}