{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.11","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":12229612,"sourceType":"datasetVersion","datasetId":7705414},{"sourceId":12237020,"sourceType":"datasetVersion","datasetId":7710204},{"sourceId":12237566,"sourceType":"datasetVersion","datasetId":7710483},{"sourceId":12238094,"sourceType":"datasetVersion","datasetId":7701697},{"sourceId":12240992,"sourceType":"datasetVersion","datasetId":7711330},{"sourceId":12241550,"sourceType":"datasetVersion","datasetId":7712318},{"sourceId":12321109,"sourceType":"datasetVersion","datasetId":7764365}],"dockerImageVersionId":31041,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"<h1>[팀떡볶이에다가순대]가중 앙상블 기반 다중 딥페이크 탐지기 결합 기법 연구</h1>\n\n","metadata":{}},{"cell_type":"markdown","source":"[<h2>*** 레포트 링크***</h2>](https://drive.google.com/file/d/1APz-oFmuFSB1vYZGaaMRLsdoHV0glZyN/view?usp=drive_link)","metadata":{}},{"cell_type":"markdown","source":"<h4>가중 앙상블 접근의 필요성</h4>\n딥페이크 탐지를 위한 다양한 도메인(RGB, 주파수, 잠재 공간 등)의 특징을 단일 모델에 통합하는 접근은 구조적 복잡성과 해석의 불확실성으로 인해 한계가 존재한다. 특히 Attention 기반 통합 방식은 시각적 해석은 가능하지만, 정렬의 일관성 부족과 구조적 결합의 불안정성으로 실질적인 성능 향상에 제약이 따른다. 본 연구는 이러한 문제를 극복하기 위해 각 모델이 산출한 위조 확률을 기반으로 하는 가중 앙상블 방식으로 전환하였다. 이를 통해 개별 모델의 판단을 수치화하여 통합함으로써, 해석 가능성과 예측 안정성을 동시에 확보하였다.","metadata":{}},{"cell_type":"markdown","source":"\n<div align=\"center\"><h4>**제안 기법: 가중 앙상블 기반 탐지기 결합**</h4></div>\n\n| 모델명       | 특징 도메인                       | 설명                                           |\n| --------- | ---------------------------- | -------------------------------------------- |\n| GenConViT | RGB, Attention, Latent Space | ConvNeXt + Swin + VAE 기반 하이브리드 모델            |\n| FreqNet   | 주파수(DCT) 기반                  | DCT 기반 CNN으로 고주파 위조 신호 탐지                    |\n| CaFft     | RGB + 주파수                    | RGB 패치에 Cross Attention, Radial Histogram 융합 |\n","metadata":{}},{"cell_type":"markdown","source":"Adaptive Weighted Ensemble\n* 단순 평균이 아닌 softmax 기반 학습 가능한 가중치 적용\n\n* 검증 데이터를 활용해 각 모델의 성능 기여도 반영\n\n* 해석 가능하고 신뢰성 높은 판단 제공","metadata":{}},{"cell_type":"markdown","source":"Reperence\n<br>\n[1] D. W. Deressa et al., “GenConViT: Deepfake Video Detection Using Generative\nConvolutional Vision Transformer,” Applied Sciences, vol. 15, no. 12, p. 6622, 2025. doi:\n10.3390/app15126622.\n<br>\n[2] C. Tan et al., “Frequency-Aware Deepfake Detection: Improving Generalizability\nthrough Frequency Space Domain Learning,” in Proc. AAAI Conf. Artif. Intell., vol. 38, no.\n5, pp. 5052–5060, 2024. doi: 10.1609/aaai.v38i5.28310.\n<br>\n[3] Y. Meng et al., “Deepfake Detection Based on Multi-scale RGB-Frequency Feature\nFusion,” in Proc. 2nd Int. Conf. Intelligent Perception and Computer Vision (CIPCV), pp.\n46–50, 2024. doi: 10.1109/cipcv61763.2024.00018.\n<br>\n[4] L. Li et al., “Face X-ray for More General Face Forgery Detection,” in Proc.\nIEEE/CVF Conf. Comput. Vis. Pattern Recognit. (CVPR), pp. 5000–5009, 2020. doi:\n10.1109/cvpr42600.2020.00505.\n<br>\n[5] A. Jadhav, “Deepfake Detection Using Deep Learning,” GitHub repository, [Online].\nAvailable: https://github.com/abhijithjadhav/Deepfake_detection_using_deep_learning.\n[Accessed: Jun. 20, 2025].\n<br>\n[6] M. Salman et al., “Aware-Net: Adaptive Weighted Averaging for Robust Ensemble\nNetwork in Deepfake Detection,” IET Conf. Proc., vol. 2025, no. 3, pp. 526–533, 2025. doi:\n10.1049/icp.2025.1162","metadata":{}},{"cell_type":"code","source":"!pip install --no-index --find-links=/kaggle/input/wheels2/wheels/wheels torch==2.0.1 torchvision==0.15.2 timm==0.6.5\n!pip install --no-index --find-links=/kaggle/input/wheels2/wheels/wheels facenet-pytorch==2.5.2\n!pip install --no-index --find-links=/kaggle/input/wheels2/wheels/wheels scipy scikit-learn numpy Pillow opencv-python albumentations==1.3.0\n!pip install --no-index --find-links=/kaggle/input/wheels2/wheels/wheels decord==0.6.0\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-30T12:23:41.408960Z","iopub.execute_input":"2025-06-30T12:23:41.409213Z","iopub.status.idle":"2025-06-30T12:25:22.008950Z","shell.execute_reply.started":"2025-06-30T12:23:41.409185Z","shell.execute_reply":"2025-06-30T12:25:22.007598Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!pip install /kaggle/input/wheels2/face_recognition_models-0.3.0-py2.py3-none-any.whl","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-30T12:25:22.010109Z","iopub.execute_input":"2025-06-30T12:25:22.010948Z","iopub.status.idle":"2025-06-30T12:25:27.441756Z","shell.execute_reply.started":"2025-06-30T12:25:22.010913Z","shell.execute_reply":"2025-06-30T12:25:27.441063Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!pip install /kaggle/input/wheels2/face_recognition-1.4.0-py2.py3-none-any.whl","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-30T12:25:27.443905Z","iopub.execute_input":"2025-06-30T12:25:27.444575Z","iopub.status.idle":"2025-06-30T12:25:30.433718Z","shell.execute_reply.started":"2025-06-30T12:25:27.444548Z","shell.execute_reply":"2025-06-30T12:25:30.432819Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 비디오 경로 지정 시 src 부분만 상위 디렉토리로 지정(하위 순회하면서 복사)\n\n# cliper비디오 경로 지정\ncliper_src = \"/kaggle/input/kcc-data/Cilper\"\ncliper_dst = \"/kaggle/working/Cliper\"\n# metaforensics 비디오 경로 지정\nmetaforensics_src = \"/kaggle/input/kcc-data/본선) MetaForensics\"\nmetaforensics_dst = \"/kaggle/working/metaforensics\"\n# 교수님제주도에서는차긁으시면안됩니다 비디오 경로 지정\njejuCar_src = \"/kaggle/input/kcc-data/본선) 교수님제주도에서는차긁으시면안됩니다\"\njejuCar_dst = \"/kaggle/working/jejuCar\"","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-30T12:25:30.434782Z","iopub.execute_input":"2025-06-30T12:25:30.435026Z","iopub.status.idle":"2025-06-30T12:25:30.439418Z","shell.execute_reply.started":"2025-06-30T12:25:30.434988Z","shell.execute_reply":"2025-06-30T12:25:30.438706Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import subprocess\nfrom pathlib import Path\nfrom tqdm import tqdm\nimport hashlib\n\ndef copy_videos_to_directory(source_dir, destination_dir):\n    \"\"\"\n    소스 디렉토리의 모든 하위 디렉토리에서 비디오 파일을 찾아 대상 디렉토리에 복사\n    (복사에는 rsync 커맨드를 사용하고 MD5 해시로 무결성 검증)\n    \"\"\"\n    source_path = Path(source_dir)\n    dest_path = Path(destination_dir)\n    video_extensions = {\".mp4\", \".avi\", \".mov\", \".mkv\", \".wmv\", \".flv\", \".m4v\", \".webm\"}\n\n    dest_path.mkdir(parents=True, exist_ok=True)\n\n    print(f\"소스 디렉토리: {source_path}\")\n    print(f\"대상 디렉토리: {dest_path}\")\n    print(\"비디오 파일 탐색 중...\")\n\n    video_files = []\n    for ext in video_extensions:\n        video_files.extend(source_path.rglob(f\"*{ext}\"))\n        video_files.extend(source_path.rglob(f\"*{ext.upper()}\"))\n    video_files = list(set(video_files))\n\n    if not video_files:\n        print(\"❌ 비디오 파일을 찾을 수 없습니다.\")\n        return\n\n    print(f\"발견된 비디오 파일: {len(video_files)}개\")\n\n    copied_files = []\n    failed_files = []\n\n    def file_md5(file_path):\n        \"\"\"파일의 MD5 해시값을 계산\"\"\"\n        hash_md5 = hashlib.md5()\n        with open(file_path, \"rb\") as f:\n            for chunk in iter(lambda: f.read(4096), b\"\"):\n                hash_md5.update(chunk)\n        return hash_md5.hexdigest()\n\n    def verify_file_integrity(src_path, dst_path):\n        \"\"\"파일 크기와 MD5 해시로 무결성 검증\"\"\"\n        # 파일 크기 비교\n        if src_path.stat().st_size != dst_path.stat().st_size:\n            return False, \"파일 크기 불일치\"\n        \n        # MD5 해시 비교\n        src_md5 = file_md5(src_path)\n        dst_md5 = file_md5(dst_path)\n        \n        if src_md5 != dst_md5:\n            return False, f\"MD5 해시 불일치 (원본: {src_md5[:8]}..., 복사본: {dst_md5[:8]}...)\"\n        \n        return True, \"무결성 검증 통과\"\n\n    for video_path in tqdm(video_files, desc=\"비디오 복사 및 검증 중\"):\n        try:\n            dest_file_path = dest_path / video_path.name\n\n            # 파일명 중복 처리\n            if dest_file_path.exists():\n                base_name = video_path.stem\n                extension = video_path.suffix\n                counter = 1\n                while dest_file_path.exists():\n                    new_name = f\"{base_name}_{counter}{extension}\"\n                    dest_file_path = dest_path / new_name\n                    counter += 1\n\n            # rsync로 복사 (체크섬 옵션 추가)\n            result = subprocess.run(\n                [\"rsync\", \"-a\", \"--checksum\", str(video_path), str(dest_file_path)],\n                stdout=subprocess.PIPE,\n                stderr=subprocess.PIPE\n            )\n\n            if result.returncode == 0:\n                # 복사 후 무결성 검증\n                is_valid, message = verify_file_integrity(video_path, dest_file_path)\n                \n                if is_valid:\n                    copied_files.append(dest_file_path)\n                else:\n                    # 검증 실패 시 복사된 파일 삭제\n                    if dest_file_path.exists():\n                        dest_file_path.unlink()\n                    raise RuntimeError(f\"무결성 검증 실패: {message}\")\n            else:\n                raise RuntimeError(result.stderr.decode().strip())\n\n        except Exception as e:\n            print(f\"복사 실패 - {video_path.name}: {e}\")\n            failed_files.append(video_path)\n            # 실패한 파일이 있다면 정리\n            if dest_file_path.exists():\n                try:\n                    dest_file_path.unlink()\n                except:\n                    pass\n\n    print(f\"\\n✅ 복사 완료!\")\n    print(f\"성공: {len(copied_files)}개\")\n    print(f\"실패: {len(failed_files)}개\")\n\n    if failed_files:\n        print(\"\\n실패한 파일들:\")\n        for failed in failed_files:\n            print(f\"  - {failed.name}\")\n\n    return copied_files\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-30T12:25:30.440234Z","iopub.execute_input":"2025-06-30T12:25:30.440439Z","iopub.status.idle":"2025-06-30T12:25:30.467240Z","shell.execute_reply.started":"2025-06-30T12:25:30.440416Z","shell.execute_reply":"2025-06-30T12:25:30.466776Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"copy_videos_to_directory(cliper_src, cliper_dst)\ncopy_videos_to_directory(metaforensics_src, metaforensics_dst)\ncopy_videos_to_directory(jejuCar_src, jejuCar_dst)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-30T12:25:30.468020Z","iopub.execute_input":"2025-06-30T12:25:30.468244Z","iopub.status.idle":"2025-06-30T12:26:00.839691Z","shell.execute_reply.started":"2025-06-30T12:25:30.468223Z","shell.execute_reply":"2025-06-30T12:26:00.839156Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<h3>GenConViT 모델 추론</h3>","metadata":{}},{"cell_type":"code","source":"import os\nimport subprocess\nimport pandas as pd\nfrom tqdm import tqdm\nimport numpy as np\nimport logging\nimport re\nimport json\n# ------------------- 설정 -------------------\nGENCONVIT_PROJECT_ROOT = '/kaggle/input/genconvit8/GenConViT'\nGENCONVIT_PREDICTION_SCRIPT = os.path.join(GENCONVIT_PROJECT_ROOT, 'prediction.py')\nGENCONVIT_DATASET = 'other'\nGENCONVIT_FRAMES = '10'\nOUTPUT_CSV_FILE = '/kaggle/working/genconvit_predictions.csv'\n\nGENCONVIT_OUTPUT_REGEX = r\"Prediction: (\\d+\\.?\\d*)\"\n\n# ------------------- 로깅 -------------------\nlogging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')\n\n# ------------------- 유틸리티 함수 -------------------\ndef get_video_list(video_dir):\n    return [\n        (os.path.join(video_dir, fname), fname)\n        for fname in sorted(os.listdir(video_dir))\n        if fname.lower().endswith(('.mp4', '.mov', '.avi')) and os.path.isfile(os.path.join(video_dir, fname))\n    ]\n\ndef run_genconvit_prediction(VIDEOS_TO_PROCESS_DIR):\n    command = [\n        'python', GENCONVIT_PREDICTION_SCRIPT,\n        '--p', VIDEOS_TO_PROCESS_DIR,\n        '--d', GENCONVIT_DATASET,\n        '--f', GENCONVIT_FRAMES\n    ]\n    logging.info(f\"GenConViT 실행: {' '.join(command)}\")\n    subprocess.run(command, cwd=GENCONVIT_PROJECT_ROOT,  check=True)\n    with open(\"GenConViT_result.json\", \"r\") as f:\n        prediction_dict = json.load(f)\n\n    # 이름과 확률 리스트 추출\n    video_names = prediction_dict[\"video\"][\"name\"]\n    pred_probs = prediction_dict[\"video\"][\"pred\"]\n\n    # 결과 딕셔너리 생성\n    GenConViT_results = {name: prob for name, prob in zip(video_names, pred_probs)}\n    return GenConViT_results","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-30T12:26:00.840566Z","iopub.execute_input":"2025-06-30T12:26:00.841088Z","iopub.status.idle":"2025-06-30T12:26:01.117452Z","shell.execute_reply.started":"2025-06-30T12:26:00.841068Z","shell.execute_reply":"2025-06-30T12:26:01.116916Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<h3>CaFfa 모델 추론</h3>","metadata":{}},{"cell_type":"code","source":"import argparse, torch, numpy as np, cv2, csv, random, json\nfrom pathlib import Path\nfrom tqdm import tqdm\nfrom facenet_pytorch import MTCNN\nfrom PIL import Image\nimport timm\nimport torch.nn as nn\nfrom safetensors.torch import load_file\n\nNUM_FRAMES = 16\nIMG_SIZE = 224\n\n# ----- 모델 구조 -----\nclass CaFftDetector(nn.Module):\n    def __init__(self):\n        super().__init__()\n        self.vit = timm.create_model(\"vit_base_patch16_224\", pretrained=False, num_classes=0, global_pool=\"\")\n        self.vit.load_state_dict(torch.load(\"/kaggle/input/weights/vit_base_patch16_224.pth\"))\n        feat_dim = self.vit.num_features\n        self.kv_proj = nn.Linear(1, 256, bias=False)\n        self.ca = nn.MultiheadAttention(feat_dim, 8, kdim=256, vdim=256, batch_first=True)\n        self.hist_fc = nn.Sequential(nn.Linear(256, 128), nn.ReLU())\n        self.head = nn.Sequential(nn.Linear(feat_dim + 128, 512), nn.ReLU(), nn.Linear(512, 1))\n\n    @torch.no_grad()\n    def _rgb_tok(self, imgs):\n        return self.vit.forward_features(imgs)\n\n    def forward(self, imgs, fft_mat, hist):\n        rgb = self._rgb_tok(imgs)\n        kv = torch.nn.functional.avg_pool2d(fft_mat.unsqueeze(1), 4)\n        kv = kv.flatten(2).transpose(1, 2)\n        kv = self.kv_proj(kv)\n        fused, _ = self.ca(rgb, kv, kv)\n        cls = fused[:, 0]\n        hist = self.hist_fc(hist)\n        return self.head(torch.cat([cls, hist], dim=-1)).squeeze(-1)\n\n# ----- 얼굴 영역 정사각형 크롭 -----\ndef square_crop(img, bbox, margin=0.2):\n    h, w = img.shape[:2]\n    x1, y1, x2, y2 = bbox\n    cx, cy = (x1 + x2) / 2, (y1 + y2) / 2\n    side = max(x2 - x1, y2 - y1) * (1 + margin)\n    x1, y1 = int(cx - side / 2), int(cy - side / 2)\n    x2, y2 = int(cx + side / 2), int(cy + side / 2)\n    x1, y1 = max(0, x1), max(0, y1)\n    x2, y2 = min(w, x2), min(h, y2)\n    if x2 - x1 < 2 or y2 - y1 < 2:\n        return None\n    return img[y1:y2, x1:x2]\n\n# ----- FFT + 히스토그램 계산 -----\ndef calc_fft_hist(gray):\n    gray = cv2.resize(gray, (64, 64), cv2.INTER_AREA).astype(np.float32)\n    gray = (gray - gray.mean()) / (gray.std() + 1e-6)\n    power = np.log(np.abs(np.fft.fftshift(np.fft.fft2(gray))) ** 2 + 1e-8)\n    yy, xx = np.mgrid[:64, :64] - 32\n    r = np.round(np.sqrt(xx ** 2 + yy ** 2)).astype(np.int32)\n    hist = np.bincount(r.ravel(), power.ravel(), minlength=256).astype(np.float32)\n    hist /= (hist.max() + 1e-6)\n    return power, hist\n\n# ----- 프레임에서 얼굴 추출 (20번 랜덤시도 후 복제) -----\ndef get_face_stack_from_video(vpath, mtcnn, size=224, margin=0.2, n_frames=16, max_trials=20):\n    cap = cv2.VideoCapture(str(vpath))\n    total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))\n    if total < 1:\n        cap.release()\n        return None\n\n    all_idx = list(range(total))\n    random.shuffle(all_idx)\n    tried = 0\n    faces = []\n\n    while tried < max_trials and tried < len(all_idx):\n        idx = all_idx[tried]\n        tried += 1\n\n        cap.set(cv2.CAP_PROP_POS_FRAMES, idx)\n        ok, frame = cap.read()\n        if not ok:\n            continue\n\n        rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)\n        boxes, _ = mtcnn.detect(rgb)\n        if boxes is not None and len(boxes) > 0:\n            crop = square_crop(rgb, boxes[0], margin)\n            if crop is not None:\n                face = cv2.resize(crop.astype(np.uint8), (size, size), cv2.INTER_AREA)\n                faces = [face] * n_frames\n                break\n\n    cap.release()\n    return faces if faces else None\n\n# ----- 메인 추론 파이프라인 -----\ndef infer_videos_in_directory(video_dir, device=\"cuda\"):\n    video_dir = Path(video_dir)\n    exts = {'.mp4', '.avi', '.mov', '.mkv'}\n    video_files = [p for p in video_dir.glob('**/*') if p.suffix.lower() in exts]\n    print(f\"Found {len(video_files)} video files.\")\n\n    device = torch.device(device if torch.cuda.is_available() else \"cpu\")\n    print(\"Using device:\", device)\n\n    mtcnn = MTCNN(keep_all=False, device=device, thresholds=[0.7, 0.8, 0.8])\n    model = CaFftDetector().to(device).eval()\n    model.load_state_dict(torch.load(\"/kaggle/input/weights/ff_dfd_test.pth\", map_location=device))\n    to_tensor = lambda x: torch.from_numpy(x).permute(2, 0, 1).float() / 255.\n    results = []\n\n    for vpath in tqdm(video_files, desc=\"Infer\"):\n        fname = vpath.name\n        faces = get_face_stack_from_video(vpath, mtcnn, size=IMG_SIZE, margin=0.2, n_frames=NUM_FRAMES)\n        if faces is None:\n            print(f\"Skip {fname} (no face found)\")\n            results.append((fname, 0.5))\n            continue\n\n        imgs, ffts, hists = [], [], []\n        for face in faces:\n            img_t = to_tensor(face)\n            imgs.append(img_t)\n            gray = cv2.cvtColor(face, cv2.COLOR_RGB2GRAY)\n            fft, hist = calc_fft_hist(gray)\n            ffts.append(torch.from_numpy(fft).float())\n            hists.append(torch.from_numpy(hist).float())\n\n        imgs  = torch.stack(imgs).to(device)\n        ffts  = torch.stack(ffts).to(device)\n        hists = torch.stack(hists).to(device)\n\n        with torch.no_grad():\n            logits = model(imgs, ffts, hists)\n            probs = torch.sigmoid(logits).cpu().numpy()\n            fake_prob = float(np.mean(probs))\n\n        results.append((fname, fake_prob))\n\n    return {video_id: prob for video_id, prob in results}\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-30T12:26:01.118296Z","iopub.execute_input":"2025-06-30T12:26:01.118828Z","iopub.status.idle":"2025-06-30T12:26:08.180158Z","shell.execute_reply.started":"2025-06-30T12:26:01.118801Z","shell.execute_reply":"2025-06-30T12:26:08.179630Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<h3>FreqNet 모델 추론</h3>","metadata":{}},{"cell_type":"code","source":"import os\nimport subprocess\nimport re\nimport pandas as pd\nfrom tqdm import tqdm\nimport numpy as np\n\n# ------------------- 설정 -------------------\nFREQNET_PROJECT_ROOT = '/kaggle/input/freqnet-deepfakedetection/FreqNet-DeepfakeDetection-main'\nFREQNET_PREDICTION_SCRIPT = '/kaggle/input/test-single-video/test_single_video.py'\nFREQNET_MODEL_WEIGHTS = os.path.join(FREQNET_PROJECT_ROOT, '4-classes-freqnet-v2.pth')\nOUTPUT_CSV_FILE = '/kaggle/working/FreqNet_result.csv'\n\n# ------------------- 유틸리티 함수 -------------------\ndef get_video_files_from_dir(video_dir):\n    video_file_paths = []\n    if os.path.isdir(video_dir):\n        for filename in sorted(os.listdir(video_dir)):\n            if filename.lower().endswith(('.mp4', '.avi', '.mov', '.mkv')):\n                video_path = os.path.join(video_dir, filename)\n                video_file_paths.append((video_path, filename))\n    else:\n        print(f\"Error: 비디오 디렉토리 '{video_dir}'를 찾을 수 없습니다.\")\n\n    if not video_file_paths:\n        print(f\"Error: {video_dir} 경로에서 비디오 파일을 찾을 수 없습니다.\")\n    return video_file_paths\n\n\ndef run_freqnet_prediction(script_path, model_weights_path, video_file_path, script_root_dir):\n    command = [\n        'python', script_path,\n        '--model_path', model_weights_path,\n        '--video_path', video_file_path\n    ]\n    env = os.environ.copy()\n    env['PYTHONPATH'] = FREQNET_PROJECT_ROOT\n\n    try:\n        process = subprocess.run(command, capture_output=True, text=True, check=True, cwd=script_root_dir, env=env)\n        stdout = process.stdout\n        match = re.search(r\"Predicted score \\(closer to 1 = fake\\): (\\d+\\.?\\d*)\", stdout)\n        if match:\n            return float(match.group(1))\n        else:\n            print(f\"Warning: 점수 파싱 실패 → {video_file_path}\")\n            print(f\"FreqNet Output:\\n{stdout.strip()}\")\n            return None\n    except subprocess.CalledProcessError as e:\n        print(f\"Error: FreqNet 실행 실패 → {video_file_path}\")\n        print(f\"stderr:\\n{e.stderr.strip()}\")\n        return None\n    except Exception as e:\n        print(f\"Unexpected error → {video_file_path}: {e}\")\n        return None\n\n# ------------------- 실행 -------------------\n\ndef freqnet_predictions(video_dir):\n    # 비디오 파일 목록 불러오기\n    video_files = get_video_files_from_dir(video_dir)\n    if not video_files:\n        print(\"처리할 비디오 없음. 종료.\")\n        exit()\n    \n    print(f\"{len(video_files)}개의 비디오 예측 시작\")\n    \n    # 예측 수행\n    prediction_results = []\n    for video_path, video_filename in tqdm(video_files, desc=\"Predicting with FreqNet\"):\n        fake_prob = run_freqnet_prediction(\n            FREQNET_PREDICTION_SCRIPT,\n            FREQNET_MODEL_WEIGHTS,\n            video_path,\n            FREQNET_PROJECT_ROOT\n        )\n        prediction_results.append({\n            'video_id': video_filename,\n            'prob': fake_prob if fake_prob is not None else 0.5\n        })\n    \n    return {entry['video_id']: entry['prob'] for entry in prediction_results}\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-30T12:26:08.182094Z","iopub.execute_input":"2025-06-30T12:26:08.182426Z","iopub.status.idle":"2025-06-30T12:26:08.191286Z","shell.execute_reply.started":"2025-06-30T12:26:08.182409Z","shell.execute_reply":"2025-06-30T12:26:08.190715Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<h3>Aware-Net 기반 가중 앙상블 실행(가중치는 사전학습 값으로 고정)</h3>","metadata":{}},{"cell_type":"code","source":"import os\nimport pandas as pd\n\ndef get_prediction(video_dir, output_file_name = None):\n    GenConViT_results = run_genconvit_prediction(video_dir)\n    CaFft_results = infer_videos_in_directory(video_dir)\n    FreqNet_results = freqnet_predictions(video_dir)\n\n    video_ids = [ f for f in os.listdir(video_dir) if f.lower().endswith(('.mp4', '.avi', '.mov', '.mkv')) ]\n    \n    weights = {\n        \"GenConViT\": 0.971,\n        \"FreqNet\": 0.0163,\n        \"CaFft\": 0.127\n    }\n    \n    submission = []\n    for vid in video_ids:\n        genconvit_prob = GenConViT_results.get(vid, 0.5)\n        freqnet_prob   = FreqNet_results.get(vid, 0.5)\n        cafft_prob     = CaFft_results.get(vid, 0.5)\n    \n        final_prob = (\n            weights[\"GenConViT\"] * genconvit_prob +\n            weights[\"FreqNet\"]   * freqnet_prob   +\n            weights[\"CaFft\"]     * cafft_prob\n        )\n    \n        final_label = int(final_prob >= 0.5)\n    \n        submission.append({\n            \"ID\": vid,\n            \"label\": final_label\n        })\n    \n    df = pd.DataFrame(submission)\n\n    if(output_file_name == None):\n        print(\"결과 DataFrame 반환\")\n        return df\n    \n    else:\n        df.to_csv(output_file_name, index=False)\n        print(f\"✅ {output_file_name} 생성 완료\")\n    ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-30T12:26:08.191889Z","iopub.execute_input":"2025-06-30T12:26:08.192087Z","iopub.status.idle":"2025-06-30T12:26:08.211713Z","shell.execute_reply.started":"2025-06-30T12:26:08.192073Z","shell.execute_reply":"2025-06-30T12:26:08.211111Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"get_prediction(\"/kaggle/working/metaforensics\", \"submission1.csv\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-30T12:26:08.212420Z","iopub.execute_input":"2025-06-30T12:26:08.212652Z","iopub.status.idle":"2025-06-30T12:36:31.759113Z","shell.execute_reply.started":"2025-06-30T12:26:08.212611Z","shell.execute_reply":"2025-06-30T12:36:31.758443Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"get_prediction(\"/kaggle/working/Cliper\",\"submission2.csv\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-30T12:36:31.759839Z","iopub.execute_input":"2025-06-30T12:36:31.760092Z","iopub.status.idle":"2025-06-30T12:45:26.356463Z","shell.execute_reply.started":"2025-06-30T12:36:31.760064Z","shell.execute_reply":"2025-06-30T12:45:26.355807Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"get_prediction(\"/kaggle/working/jejuCar\", \"submission3.csv\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-30T12:45:26.357347Z","iopub.execute_input":"2025-06-30T12:45:26.358102Z","iopub.status.idle":"2025-06-30T12:54:14.959812Z","shell.execute_reply.started":"2025-06-30T12:45:26.358083Z","shell.execute_reply":"2025-06-30T12:54:14.959181Z"}},"outputs":[],"execution_count":null}]}