{"metadata":{"kernelspec":{"name":"python3","display_name":"Python 3","language":"python"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"colab":{"machine_shape":"hm","gpuType":"T4"},"accelerator":"GPU","kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":129271,"databundleVersionId":15506307,"sourceType":"competition"}],"dockerImageVersionId":31260,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"## Local Inference on GPU \nModel page: https://huggingface.co/pyannote/speaker-diarization-3.1\n\n⚠️ If the generated code snippets do not work, please open an issue on either the [model repo](https://huggingface.co/pyannote/speaker-diarization-3.1)\n\t\t\tand/or on [huggingface.js](https://github.com/huggingface/huggingface.js/blob/main/packages/tasks/src/model-libraries-snippets.ts) 🙏","metadata":{}},{"cell_type":"markdown","source":"The model you are trying to use is gated. Please make sure you have access to it by visiting the model page.To run inference, either set HF_TOKEN in your environment variables/ Secrets or run the following cell to login. 🤗","metadata":{}},{"cell_type":"code","source":"from huggingface_hub import login\nlogin(new_session=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-14T16:28:45.284222Z","iopub.execute_input":"2026-02-14T16:28:45.284441Z","iopub.status.idle":"2026-02-14T16:28:45.619498Z","shell.execute_reply.started":"2026-02-14T16:28:45.284419Z","shell.execute_reply":"2026-02-14T16:28:45.618694Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!pip install pyannote.audio==3.3.2 demucs torchaudio librosa soundfile pandas==2.2.2 --quiet","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-14T16:28:45.621377Z","iopub.execute_input":"2026-02-14T16:28:45.62169Z","iopub.status.idle":"2026-02-14T16:29:13.437767Z","shell.execute_reply.started":"2026-02-14T16:28:45.621658Z","shell.execute_reply":"2026-02-14T16:29:13.43701Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os, subprocess, json, shutil, glob as globmod\nimport librosa, soundfile as sf\nimport torch, pandas as pd, torchaudio\nfrom pyannote.audio import Pipeline\n\n# --- CONFIGURE THESE ---\nHF_TOKEN = \"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\"  # https://huggingface.co/settings/tokens\n\n# Auto-detect dataset path on Kaggle\nKAGGLE_INPUT = \"/kaggle/input\"\n# Search for test audio wav files anywhere under the input directory\ntest_wavs = globmod.glob(os.path.join(KAGGLE_INPUT, \"**\", \"test\", \"audio\", \"*.wav\"), recursive=True)\nif not test_wavs:\n    # Maybe audio files are directly under test/\n    test_wavs = globmod.glob(os.path.join(KAGGLE_INPUT, \"**\", \"test\", \"*.wav\"), recursive=True)\n\nassert len(test_wavs) > 0, (\n    \"No test wav files found! Run this in a cell to debug:\\n\"\n    \"  !find /kaggle/input -name '*.wav' | head -20\"\n)\n\nTEST_DIR = os.path.dirname(test_wavs[0])\n\n# Also find training data paths\ntrain_wavs = globmod.glob(os.path.join(KAGGLE_INPUT, \"**\", \"train\", \"audio\", \"*.wav\"), recursive=True)\nTRAIN_AUDIO_DIR = os.path.dirname(train_wavs[0]) if train_wavs else None\ntrain_csvs = globmod.glob(os.path.join(KAGGLE_INPUT, \"**\", \"train\", \"annotation\", \"*.csv\"), recursive=True)\nTRAIN_ANNOT_DIR = os.path.dirname(train_csvs[0]) if train_csvs else None\n\nprint(f\"TEST_DIR:        {TEST_DIR}\")\nprint(f\"TRAIN_AUDIO_DIR: {TRAIN_AUDIO_DIR}\")\nprint(f\"TRAIN_ANNOT_DIR: {TRAIN_ANNOT_DIR}\")\n\n# Working directories\nDEMUCS_DIR = \"demucs_cleaned\"\nRESAMPLED_DIR = \"resampled_16k\"\nos.makedirs(DEMUCS_DIR, exist_ok=True)\nos.makedirs(RESAMPLED_DIR, exist_ok=True)\n\ndevice = \"cuda\" if torch.cuda.is_available() else \"cpu\"\nprint(f\"\\nUsing device: {device}\")\nprint(f\"Test files:   {len([f for f in os.listdir(TEST_DIR) if f.endswith('.wav')])}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-14T16:30:23.131503Z","iopub.execute_input":"2026-02-14T16:30:23.132464Z","iopub.status.idle":"2026-02-14T16:30:23.177906Z","shell.execute_reply.started":"2026-02-14T16:30:23.132432Z","shell.execute_reply":"2026-02-14T16:30:23.177154Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Resample RAW audio files (not demucs-cleaned) to 16kHz\nfor wav in sorted(os.listdir(TEST_DIR)):  # Changed from DEMUCS_DIR to TEST_DIR\n    if not wav.endswith(\".wav\"):\n        continue\n    out = os.path.join(RESAMPLED_DIR, wav)\n    if os.path.exists(out):\n        print(f\"  Skipping {wav} (already resampled)\")\n        continue\n    inp = os.path.join(TEST_DIR, wav)  # Changed from DEMUCS_DIR to TEST_DIR\n    print(\"Resampling:\", wav)\n    y, sr = librosa.load(inp, sr=16000, mono=True)\n    sf.write(out, y, 16000)\nprint(\"\\nDone: all files resampled to 16 kHz.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-14T16:30:28.36228Z","iopub.execute_input":"2026-02-14T16:30:28.362597Z","iopub.status.idle":"2026-02-14T16:30:57.627881Z","shell.execute_reply.started":"2026-02-14T16:30:28.36257Z","shell.execute_reply":"2026-02-14T16:30:57.627138Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\"\"\"\nFix for PyTorch 2.6+ compatibility with pyannote.audio\n\nWHY THIS ERROR HAPPENS:\n======================\n1. PyTorch 2.6 changed the default behavior of torch.load():\n   - OLD (PyTorch < 2.6): weights_only=False (allows arbitrary Python objects)\n   - NEW (PyTorch 2.6+): weights_only=True (only loads tensors for security)\n\n2. pyannote.audio was built before this change and uses pickle to save:\n   - Custom Python classes (Specifications, Problem, etc.)\n   - Model architectures\n   - Configuration objects\n   \n3. When weights_only=True, PyTorch rejects these custom classes for security reasons\n   (they could contain malicious code)\n\n4. The error tells you which classes are blocked each time you hit a new one\n\nTHE SOLUTION:\n=============\nTwo approaches:\nA) Add ALL required classes to safe_globals (whack-a-mole, tedious)\nB) Disable weights_only entirely (simpler, but requires trusting the source)\n\nThis script uses approach B because:\n- pyannote is from HuggingFace (trusted source)\n- We'd need to add 10+ classes otherwise\n- It's the official recommendation in the error message for trusted sources\n\"\"\"\n\nimport torch\nimport torch.serialization\n\n# Method 1: The \"nuclear option\" - disable the security check entirely\n# This is safe for pyannote because it's from a trusted source (HuggingFace)\nimport warnings\n\n# Suppress the deprecation warning about torchaudio backends\nwarnings.filterwarnings('ignore', message='.*torchaudio._backend.*')\n\nprint(\"Setting up PyTorch compatibility patch for pyannote.audio...\")\n\n# Save original torch.load before any modifications\nif not hasattr(torch, '_original_load_saved'):\n    torch._original_load_saved = torch.load\n    \n    def patched_torch_load(*args, **kwargs):\n        \"\"\"\n        Patched torch.load that forces weights_only=False\n        This is needed for pyannote.audio compatibility with PyTorch 2.6+\n        \"\"\"\n        # FORCE weights_only=False (don't use setdefault, explicitly override)\n        kwargs['weights_only'] = False\n        return torch._original_load_saved(*args, **kwargs)\n    \n    torch.load = patched_torch_load\n    print(\"✓ torch.load patched successfully\")\nelse:\n    print(\"✓ torch.load already patched\")\n\n\n# Method 2 (alternative/backup): If the above doesn't work, uncomment this:\n# This adds specific classes to the safe list instead of disabling the check\n\"\"\"\nfrom pyannote.audio.core.task import Specifications, Problem\nfrom pyannote.audio.core.model import Model\n\ntorch.serialization.add_safe_globals([\n    torch.torch_version.TorchVersion,\n    Specifications,\n    Problem,\n    # Add more as needed if you get more UnpicklingError messages\n])\nprint(\"✓ Safe globals added\")\n\"\"\"\n\nprint(\"\\nNow you can load your pipeline:\")\nprint(\"=\" * 60)\nprint(\"pipeline = Pipeline.from_pretrained(\")\nprint(\"    'pyannote/speaker-diarization-3.1',\")\nprint(\"    use_auth_token=HF_TOKEN,\")\nprint(\")\")\nprint(\"=\" * 60)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-14T16:31:03.027624Z","iopub.execute_input":"2026-02-14T16:31:03.027944Z","iopub.status.idle":"2026-02-14T16:31:03.035813Z","shell.execute_reply.started":"2026-02-14T16:31:03.027917Z","shell.execute_reply":"2026-02-14T16:31:03.035139Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!pip install git+https://github.com/huggingface/diarizers.git --quiet","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-14T16:36:46.405224Z","iopub.execute_input":"2026-02-14T16:36:46.405518Z","iopub.status.idle":"2026-02-14T16:37:16.808845Z","shell.execute_reply.started":"2026-02-14T16:36:46.405491Z","shell.execute_reply":"2026-02-14T16:37:16.808061Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from pyannote.audio import Pipeline\nfrom diarizers import SegmentationModel\nimport torch\n\n# Load the base diarization pipeline\npipeline = Pipeline.from_pretrained(\n    \"pyannote/speaker-diarization-3.1\",\n    use_auth_token=HF_TOKEN,\n)\n\n# Load the Hindi fine-tuned segmentation model\nprint(\"Loading Hindi fine-tuned segmentation model...\")\nsegmentation_model = SegmentationModel().from_pretrained(\n    'Samyak29/speaker-segmentation-fine-tuned-hindi'\n)\n\n# Convert to pyannote format\nfine_tuned_model = segmentation_model.to_pyannote_model()\n\n# NUCLEAR OPTION: Recursively move EVERYTHING to GPU\ndef move_all_to_device(module, device):\n    \"\"\"Recursively move all parameters, buffers, and submodules to device\"\"\"\n    module.to(device)\n    for child in module.children():\n        move_all_to_device(child, device)\n    # Explicitly move buffers\n    for name, buffer in module.named_buffers(recurse=False):\n        if buffer is not None:\n            buffer.data = buffer.data.to(device)\n    return module\n\nfine_tuned_model = move_all_to_device(fine_tuned_model, torch.device(device))\n\n# Now replace the segmentation component\npipeline._segmentation.model = fine_tuned_model\npipeline.to(torch.device(device))\n\nprint(f\"✓ Pipeline loaded with Hindi fine-tuned segmentation model on {device}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-14T16:43:58.312157Z","iopub.execute_input":"2026-02-14T16:43:58.312866Z","iopub.status.idle":"2026-02-14T16:44:00.217067Z","shell.execute_reply.started":"2026-02-14T16:43:58.312831Z","shell.execute_reply":"2026-02-14T16:44:00.216444Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"MIN_SEGMENT_DURATION = 0.3   # drop segments shorter than this (seconds)\nMERGE_GAP = 2.0              # merge same-speaker segments closer than this\n\n\ndef postprocess_segments(segments):\n    \"\"\"Remove micro-segments and merge consecutive same-speaker segments.\"\"\"\n    if not segments:\n        return segments\n\n    # 1. Remove very short segments (noise / artifacts)\n    filtered = [\n        s for s in segments\n        if s[\"end_time\"] - s[\"start_time\"] >= MIN_SEGMENT_DURATION\n    ]\n    if not filtered:\n        return segments  # keep originals if all got filtered\n\n    # 2. Sort by start time\n    filtered.sort(key=lambda s: s[\"start_time\"])\n\n    # 3. Merge consecutive segments from the same speaker if gap is small\n    merged = [filtered[0]]\n    for seg in filtered[1:]:\n        prev = merged[-1]\n        if (\n            seg[\"speaker_id\"] == prev[\"speaker_id\"]\n            and seg[\"start_time\"] - prev[\"end_time\"] <= MERGE_GAP\n        ):\n            prev[\"end_time\"] = seg[\"end_time\"]\n        else:\n            merged.append(seg)\n\n    return merged\nprint(f\"Post-processing: drop < {MIN_SEGMENT_DURATION}s, merge gap < {MERGE_GAP}s\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-14T16:48:59.047907Z","iopub.execute_input":"2026-02-14T16:48:59.048725Z","iopub.status.idle":"2026-02-14T16:48:59.055419Z","shell.execute_reply.started":"2026-02-14T16:48:59.048693Z","shell.execute_reply":"2026-02-14T16:48:59.054826Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"results = []\n\nfor wav in sorted(os.listdir(TEST_DIR)):\n    if not wav.endswith(\".wav\"):\n        continue\n\n    file_id = wav.replace(\".wav\", \"\")\n\n    # >>> FIX: use the cleaned + resampled audio <\n    cleaned_path = os.path.join(RESAMPLED_DIR, wav)\n    if not os.path.exists(cleaned_path):\n        print(f\"  WARNING: {wav} not in resampled dir, using raw audio\")\n        cleaned_path = os.path.join(TEST_DIR, wav)\n\n    print(f\"Diarizing: {file_id}\")\n    waveform, sr = torchaudio.load(cleaned_path)\n    \n    # FIX: Move waveform to the same device as the model\n    waveform = waveform.to(device)\n    \n    diarization = pipeline({\"waveform\": waveform, \"sample_rate\": sr})\n\n    # Extract segments using the correct API\n    segments = []\n    for turn, _, speaker in diarization.itertracks(yield_label=True):\n        segments.append({\n            \"start_time\": round(turn.start, 2),\n            \"end_time\":   round(turn.end, 2),\n            \"speaker_id\": speaker,\n        })\n\n    # Post-process\n    segments = postprocess_segments(segments)\n\n    print(f\"  -> {len(segments)} segments\")\n    results.append({\n        \"filename\":    file_id,\n        \"diarization\": json.dumps(segments),\n    })\nprint(f\"\\nDone: processed {len(results)} files.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-14T16:49:00.85435Z","iopub.execute_input":"2026-02-14T16:49:00.855039Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"df = pd.DataFrame(results)\ndf.to_csv(\"submission.csv\", index=False)\n\nprint(\"submission.csv saved\")\nprint(df.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-14T16:29:44.789282Z","iopub.status.idle":"2026-02-14T16:29:44.789708Z","shell.execute_reply.started":"2026-02-14T16:29:44.78949Z","shell.execute_reply":"2026-02-14T16:29:44.789519Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Save to current directory with absolute path\noutput_path = os.path.abspath(\"submission.csv\")\ndf.to_csv(output_path, index=False)\nprint(f\"submission.csv saved to: {output_path}\")\nprint(df.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-14T16:29:44.790998Z","iopub.status.idle":"2026-02-14T16:29:44.791429Z","shell.execute_reply.started":"2026-02-14T16:29:44.791212Z","shell.execute_reply":"2026-02-14T16:29:44.79124Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Save to Kaggle's working directory\noutput_path = \"/kaggle/working/submission.csv\"\ndf.to_csv(output_path, index=False)\nprint(f\"submission.csv saved to: {output_path}\")\nprint(df.head())\n\n# Verify it exists\nif os.path.exists(output_path):\n    print(f\"✓ File exists! Size: {os.path.getsize(output_path)} bytes\")\nelse:\n    print(\"✗ File not found!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-14T16:29:44.792423Z","iopub.status.idle":"2026-02-14T16:29:44.792698Z","shell.execute_reply.started":"2026-02-14T16:29:44.792573Z","shell.execute_reply":"2026-02-14T16:29:44.792591Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import json\nimport pandas as pd\n\ndf = pd.read_csv(\"submission.csv\")\nfor i, row in df.iterrows():\n    json.loads(row[\"diarization\"])\nprint(\"All JSON valid ✅\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-14T16:29:44.793895Z","iopub.status.idle":"2026-02-14T16:29:44.794295Z","shell.execute_reply.started":"2026-02-14T16:29:44.794099Z","shell.execute_reply":"2026-02-14T16:29:44.794128Z"}},"outputs":[],"execution_count":null}]}