{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"codemirror_mode":{"name":"ipython","version":3},"file_extension":".py","mimetype":"text/x-python","name":"python","nbconvert_exporter":"python","pygments_lexer":"ipython3","version":"3.12.12"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":129271,"databundleVersionId":15506307}],"dockerImageVersionId":31287,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Bengali Long-form Speaker Diarization — Starter Notebook\n\n**Audio → ML-based Voice Activity Detection (Silero) → windows → ECAPA embeddings → clustering → speaker turns → CSV output**\n\n\n","metadata":{}},{"cell_type":"markdown","source":"## 1) Dependencies install","metadata":{}},{"cell_type":"markdown","source":"\n# !pip uninstall -y scipy\n# !pip install -q pyannote.audio==3.1.1 speechbrain silero-vad soundfile\n# !pip install -q --upgrade \"scipy>=1.13,<1.15\"\n\n# %% [code]\n\n!pip install -q --no-cache-dir pyannote.audio==3.3.1 speechbrain silero-vad\n\n!pip uninstall -y -q numpy pandas scipy torchcodec torch torchaudio torchvision\n!pip install -q numpy==2.0.2 pandas scipy torchcodec==0.8.0 torch==2.8.0+cu126 torchaudio==2.8.0+cu126 torchvision==0.23.0+cu126 --extra-index-url https://download.pytorch.org/whl/cu126\n\nprint(\"SUCCESS: Tools installed without breaking the foundation.\")\n","metadata":{"execution":{"iopub.status.busy":"2026-02-22T00:32:19.389505Z","iopub.execute_input":"2026-02-22T00:32:19.389761Z","iopub.status.idle":"2026-02-22T00:34:41.250566Z","shell.execute_reply.started":"2026-02-22T00:32:19.389731Z","shell.execute_reply":"2026-02-22T00:34:41.249688Z"}}},{"cell_type":"code","source":"# %% [code]\n# 1. Install pyannote 4.x (REQUIRED for the community-1 model)\n!pip install -q --no-cache-dir \"pyannote.audio>=4.0.0\" speechbrain silero-vad\n\n# 2. Lock safe versions for data processing, but LEAVE PyTorch alone!\n# (Force-upgrading torch on Kaggle triggers the DeadKernel error)\n!pip install -q numpy==2.0.2 pandas scipy\n\nprint(\"SUCCESS: Tools installed without breaking the foundation.\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-04T10:24:56.887499Z","iopub.execute_input":"2026-03-04T10:24:56.887706Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!pip install huggingface-hub==0.36.0 transformers==4.57.1","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-22T00:34:41.252866Z","iopub.execute_input":"2026-02-22T00:34:41.25342Z","iopub.status.idle":"2026-02-22T00:34:58.437333Z","shell.execute_reply.started":"2026-02-22T00:34:41.253386Z","shell.execute_reply":"2026-02-22T00:34:58.436457Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!pip freeze","metadata":{"execution":{"iopub.status.busy":"2026-02-22T00:34:58.438684Z","iopub.execute_input":"2026-02-22T00:34:58.439113Z","iopub.status.idle":"2026-02-22T00:35:01.710213Z","shell.execute_reply.started":"2026-02-22T00:34:58.439075Z","shell.execute_reply":"2026-02-22T00:35:01.709491Z"},"trusted":true,"collapsed":true,"jupyter":{"outputs_hidden":true}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# !pip install --upgrade torchaudio","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-22T00:35:01.71214Z","iopub.execute_input":"2026-02-22T00:35:01.712397Z","iopub.status.idle":"2026-02-22T00:35:01.716516Z","shell.execute_reply.started":"2026-02-22T00:35:01.712371Z","shell.execute_reply":"2026-02-22T00:35:01.715954Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 2) Import setup","metadata":{}},{"cell_type":"code","source":"\n# =========================\n# 1) Standard Libraries\n# =========================\nimport os\nimport sys\nimport gc\nimport json\nimport time\nimport pathlib\nfrom pathlib import Path\nimport warnings\n\n# =========================\n# 2) NumPy FIRST (do not downgrade)\n# =========================\nimport numpy as np\n\n# --- NumPy 2.x compatibility patch (ONLY what is needed) ---\nif not hasattr(np, \"NaN\"):\n    np.NaN = np.nan\nif not hasattr(np, 'NAN'): np.NAN = np.nan\nif not hasattr(np, 'NaN'): np.NaN = np.nan\nif not hasattr(np, 'infty'): np.infty = np.inf\n\nimport pandas as pd\nfrom tqdm.auto import tqdm\nimport matplotlib.pyplot as plt\n\n# =========================\n# 3) SciPy / Audio stack\n# =========================\nimport scipy\nimport scipy.signal as signal\nimport librosa\nimport soundfile as sf\nfrom IPython.display import Audio\n\n# =========================\n# 4) Torch backend\n# =========================\nimport torch\nimport torchaudio\nimport torch.nn.functional as F\n\n# =========================\n# 5) Speaker models\n# =========================\nfrom speechbrain.inference.speaker import EncoderClassifier\nfrom pyannote.audio import Model, Inference\nfrom pyannote.audio.pipelines import VoiceActivityDetection\nfrom pyannote.core import Segment, Annotation\n\n# =========================\n# 6) Clustering & Metrics\n# =========================\nfrom sklearn.cluster import AgglomerativeClustering, SpectralClustering\nfrom sklearn.metrics import silhouette_score\nfrom sklearn.preprocessing import normalize, StandardScaler\n\n# =========================\n# 7) Environment + Multi-GPU Setup\n# =========================\nwarnings.filterwarnings(\"ignore\")\n\nNUM_GPUS = torch.cuda.device_count() if torch.cuda.is_available() else 0\nDEVICES = [f\"cuda:{i}\" for i in range(NUM_GPUS)] if NUM_GPUS > 0 else [\"cpu\"]\ndevice = torch.device(DEVICES[0])  # primary device (backward compat)\n\nprint(f\"Number of GPUs available: {NUM_GPUS}\")\nfor i in range(NUM_GPUS):\n    print(f\"  GPU {i}: {torch.cuda.get_device_name(i)}\")\nif NUM_GPUS == 0:\n    print(\"No GPU found, using CPU\")\n\n# =========================\n# 8) Sanity checks\n# =========================\nimport pyannote.core\nprint(f\"NumPy version     : {np.__version__}\")\nprint(f\"SciPy version     : {scipy.__version__}\")\nprint(f\"Pyannote core     : {pyannote.core.__version__}\")\nprint(\"SUCCESS: All libraries loaded correctly.\")\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 3) Paths + Device configuration","metadata":{}},{"cell_type":"code","source":"\n# 3) Paths + Device configuration + Hugging Face Login\nimport os\nimport torch\nfrom pathlib import Path\nfrom huggingface_hub import login\n\n# === FILE PATH (Preserving exact Starter Notebook path) ===\nAUDIO_DIR = Path(\"/kaggle/input/competitions/dl-sprint-4-0-bengali-speaker-diarization-challenge/diarization/diarization/test/audio\")\nOUT_PATH = Path(\"submission.csv\")\n\n# === Device Configuration (Multi-GPU) ===\nDEVICE = DEVICES[0]  # Primary device (backward compat)\nprint(f\"Primary device: {DEVICE}\")\nprint(f\"All devices: {DEVICES}\")\nfor i in range(NUM_GPUS):\n    print(f\"  GPU {i}: {torch.cuda.get_device_name(i)}\")\n\n# === Hugging Face Authentication ===\ntry:\n    from kaggle_secrets import UserSecretsClient\n    HF_TOKEN = UserSecretsClient().get_secret(\"HF_TOKEN\")\n    print(\"HF_TOKEN found in Kaggle Secrets.\")\nexcept Exception:\n    HF_TOKEN = \"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\" \n\nif HF_TOKEN and HF_TOKEN != \"INSERT_TOKEN_HERE\":\n    try:\n        login(token=HF_TOKEN)\n        print(\"✅ Hugging Face Login Successful. High-accuracy models enabled.\")\n    except Exception as e:\n        print(f\"⚠️ Login failed: {e}\")\n        print(\"Fallback: Notebook will run using basic Starter models (lower accuracy).\")\nelse:\n    print(\"⚠️ No HF_TOKEN provided.\")\n    print(\"Fallback: Notebook will run using basic Starter models (lower accuracy).\")\n","metadata":{"execution":{"iopub.status.busy":"2026-02-22T00:37:16.69518Z","iopub.execute_input":"2026-02-22T00:37:16.696006Z","iopub.status.idle":"2026-02-22T00:37:16.873771Z","shell.execute_reply.started":"2026-02-22T00:37:16.695977Z","shell.execute_reply":"2026-02-22T00:37:16.872896Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 4) Collect files (and stems)","metadata":{}},{"cell_type":"code","source":"wav_paths = sorted(AUDIO_DIR.glob(\"*.wav\"))\nstems = [p.stem for p in wav_paths]\n\nlen(wav_paths), (wav_paths[0] if wav_paths else None)","metadata":{"execution":{"iopub.status.busy":"2026-02-22T00:37:22.366653Z","iopub.execute_input":"2026-02-22T00:37:22.367468Z","iopub.status.idle":"2026-02-22T00:37:22.382489Z","shell.execute_reply.started":"2026-02-22T00:37:22.367439Z","shell.execute_reply":"2026-02-22T00:37:22.381858Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 5) Audio loader (mono float32 @ 16 kHz)\nReads an audio file, converts it to **mono** `float32`, and **resamples** to **16 kHz** (if needed) for consistent downstream VAD and embedding extraction.\n","metadata":{}},{"cell_type":"code","source":"import numpy as np\nimport soundfile as sf\nimport torch\nimport torchaudio\n\ndef load_audio_mono_16k(path: str, target_sr: int = 16000) -> np.ndarray:\n    \"\"\"\n    Zero-destructive audio loading. No tanh, no noise, no pre-emphasis.\n    \"\"\"\n    print(\"just raw\")\n    wav, sr = sf.read(str(path), always_2d=True)\n    \n    # Smart downmix: pick the channel with the most energy instead of blindly averaging\n    if wav.shape[1] > 1:\n        energy = np.sum(wav**2, axis=0)\n        wav = wav[:, np.argmax(energy)]\n    else:\n        wav = wav.squeeze()\n\n    wav = wav.astype(np.float32)\n    wav = np.nan_to_num(wav, nan=0.0)\n\n    if sr != target_sr:\n        wav_t = torch.from_numpy(wav).unsqueeze(0)\n        resampler = torchaudio.transforms.Resample(orig_freq=sr, new_freq=target_sr)\n        wav = resampler(wav_t).squeeze(0).numpy()\n\n    # Simple RMS Peak Normalization (No clipping/tanh)\n    peak = np.max(np.abs(wav))\n    if peak > 0:\n        wav = wav / peak * 0.95 \n\n    return wav.astype(np.float32)","metadata":{"execution":{"iopub.status.busy":"2026-02-22T00:37:24.442071Z","iopub.execute_input":"2026-02-22T00:37:24.442758Z","iopub.status.idle":"2026-02-22T00:37:24.449443Z","shell.execute_reply.started":"2026-02-22T00:37:24.442731Z","shell.execute_reply":"2026-02-22T00:37:24.448525Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# import numpy as np\n# import soundfile as sf\n# import torch\n# import torchaudio\n# import scipy.signal\n# from pathlib import Path\n\n# def load_audio_mono_16k(\n#     path: str | Path,\n#     target_sr: int = 16000,\n# ) -> np.ndarray:\n#     \"\"\"\n#     Enhanced loader for Pyannote/ECAPA pipelines.\n#     Optimizes for Voice Activity Detection (VAD) and Embedding stability.\n#     \"\"\"\n#     print(\"using overengi\")\n#     # 1. Faster/Robust Loading\n#     # Use sf.blocks if files are massive, but for standard diarization:\n#     wav, sr = sf.read(str(path), always_2d=True) # Always 2D for consistent indexing\n\n#     # 2. Intelligent Downmixing (Center-focused)\n#     # If stereo, check for Phase Cancellation or just take the loudest channel?\n#     # Better: Use RMS-based channel selection or simple mean.\n#     if wav.shape[1] > 1:\n#         # Check if channels are identical or inverted\n#         wav = np.mean(wav, axis=1)\n#     else:\n#         wav = wav.squeeze()\n\n#     wav = wav.astype(np.float32)\n#     wav = np.nan_to_num(wav, nan=0.0, posinf=0.0, neginf=0.0)\n\n#     # 3. Resample FIRST (Before non-linear processing)\n#     # Doing this early ensures the bandpass and tanh operate on the target SR.\n#     if sr != target_sr:\n#         wav_t = torch.from_numpy(wav).unsqueeze(0)\n#         resampler = torchaudio.transforms.Resample(\n#             orig_freq=sr,\n#             new_freq=target_sr,\n#             lowpass_filter_width=128, # Higher quality for competition\n#             rolloff=0.99,\n#             resampling_method=\"kaiser_window\", # Often superior to Hann for aliasing\n#         )\n#         wav = resampler(wav_t).squeeze(0).numpy()\n#         sr = target_sr\n\n#     # 4. Pre-emphasis Filter (CRITICAL for Pyannote/Speech models)\n#     # Pyannote/SincNet models often benefit from high-frequency boosting \n#     # to highlight phonetic features (consonants).\n#     # Formula: y[t] = x[t] - 0.97 * x[t-1]\n#     wav = np.append(wav[0], wav[1:] - 0.97 * wav[:-1])\n\n#     # 5. Adaptive Gain Control / LUFS Normalization\n#     # Pyannote can be sensitive to quiet segments. Simple RMS is okay, \n#     # but Peak Normalization + Tanh is better for preventing clipping.\n#     # We use a standard -1dB peak norm.\n#     peak = np.max(np.abs(wav))\n#     if peak > 0:\n#         wav = wav / peak * 0.95 \n\n#     # 6. Noise Floor Dithering (The \"Secret Sauce\")\n#     # In competitions, perfectly silent blocks can cause NaN gradients \n#     # or \"mathematical artifacts\" in VAD models. Add a tiny bit of white noise.\n#     noise = np.random.normal(0, 1e-5, wav.shape).astype(np.float32)\n#     wav = wav + noise\n\n#     # 7. Final DC Offset & Soft Limiter\n#     wav = wav - np.mean(wav)\n#     wav = np.tanh(wav) # Your tanh is good; it acts as a natural compressor\n\n#     return wav.astype(np.float32)","metadata":{"execution":{"iopub.status.busy":"2026-02-22T00:35:16.701817Z","iopub.execute_input":"2026-02-22T00:35:16.702115Z","iopub.status.idle":"2026-02-22T00:35:16.719062Z","shell.execute_reply.started":"2026-02-22T00:35:16.702088Z","shell.execute_reply":"2026-02-22T00:35:16.718554Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# # %% [code]\n# # REPLACES CELL 6: Competition-Grade Diarization Audio Loader\n\n# import numpy as np\n# import soundfile as sf\n# import torch\n# import torchaudio\n# import scipy.signal\n# from pathlib import Path\n\n\n# def load_audio_mono_16k(\n#     path: str | Path,\n#     target_sr: int = 16000,\n# ) -> np.ndarray:\n#     \"\"\"\n#     High-fidelity audio loader optimized for speaker diarization.\n#     Preserves speaker identity while suppressing background noise.\n#     \"\"\"\n#     print(\"gpt opt\")\n#     # ---------------------------------------------------------\n#     # 1. Load audio (float32, mono)\n#     # ---------------------------------------------------------\n#     wav, sr = sf.read(str(path), always_2d=False)\n\n#     if wav.ndim > 1:\n#         wav = np.mean(wav, axis=1)\n\n#     wav = wav.astype(np.float32)\n\n#     # Remove NaNs / infs (VERY IMPORTANT for Kaggle test audio)\n#     wav = np.nan_to_num(wav, nan=0.0, posinf=0.0, neginf=0.0)\n\n#     # ---------------------------------------------------------\n#     # 2. DC Offset Removal (improves embedding stability)\n#     # ---------------------------------------------------------\n#     wav = wav - np.mean(wav)\n\n#     # ---------------------------------------------------------\n#     # 3. Gentle Speech-Preserving Band-Pass\n#     #   (do NOT kill speaker formants)\n#     # ---------------------------------------------------------\n#     # Human speech + speaker cues ≈ 70Hz – 8.5kHz\n#     sos = scipy.signal.butter(\n#         N=4,\n#         Wn=[70, min(8500, sr // 2 - 100)],\n#         btype=\"bandpass\",\n#         fs=sr,\n#         output=\"sos\",\n#     )\n#     wav = scipy.signal.sosfilt(sos, wav)\n\n#     # ---------------------------------------------------------\n#     # 4. RMS-Based Normalization (speaker-safe)\n#     # ---------------------------------------------------------\n#     rms = np.sqrt(np.mean(wav**2) + 1e-8)\n\n#     # Target RMS ≈ -20 dBFS (good for ECAPA / pyannote)\n#     target_rms = 0.1\n\n#     if rms > 0:\n#         wav = wav * (target_rms / rms)\n\n#     # Soft clipping (NOT hard clip)\n#     wav = np.tanh(wav)\n\n#     # ---------------------------------------------------------\n#     # 5. Resample with anti-aliasing\n#     # ---------------------------------------------------------\n#     if sr != target_sr:\n#         wav_t = torch.from_numpy(wav).unsqueeze(0)\n\n#         resampler = torchaudio.transforms.Resample(\n#             orig_freq=sr,\n#             new_freq=target_sr,\n#             lowpass_filter_width=64,\n#             rolloff=0.95,\n#             resampling_method=\"sinc_interp_hann\",\n#         )\n\n#         wav = resampler(wav_t).squeeze(0).numpy()\n\n#     # ---------------------------------------------------------\n#     # 6. Final safety clamp\n#     # ---------------------------------------------------------\n#     wav = np.clip(wav, -1.0, 1.0).astype(np.float32)\n\n#     return wav\n","metadata":{"execution":{"iopub.status.busy":"2026-02-22T00:35:16.71975Z","iopub.execute_input":"2026-02-22T00:35:16.720024Z","iopub.status.idle":"2026-02-22T00:35:16.743117Z","shell.execute_reply.started":"2026-02-22T00:35:16.719995Z","shell.execute_reply":"2026-02-22T00:35:16.742287Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# # %% [code]\n# # REPLACES CELL 6: Absolute Perfect Audio Loader\n# import scipy.signal\n\n# def load_audio_mono_16k(path: str | Path, target_sr: int = 16000) -> np.ndarray:\n#     \"\"\"\n#     Isolates vocal range (60-7000Hz) and normalizes for dramatic drama audio.\n#     \"\"\"\n#     wav, sr = sf.read(str(path))\n#     if wav.ndim > 1:\n#         wav = wav.mean(axis=1)\n        \n#     # 1. Band-Pass Filter: Removes BGM rumble and hiss\n#     sos = scipy.signal.butter(5, [50, 7000], 'bandpass', fs=sr, output='sos')\n#     wav = scipy.signal.sosfilt(sos, wav)\n#     print(hello)\n\n#     # 2. Peak-Aware Normalization: Prevents clipping during shouting\n#     p95 = np.percentile(np.abs(wav), 95)\n#     if p95 > 0:\n#         wav = wav / p95 * 0.5 \n#     wav = np.clip(wav, -1.0, 1.0).astype(np.float32)\n\n#     # 3. Resample for Pyannote/SpeechBrain\n#     if sr != target_sr:\n#         x = torch.from_numpy(wav).unsqueeze(0)\n#         resampler = torchaudio.transforms.Resample(orig_freq=sr, new_freq=target_sr)\n#         wav = resampler(x).squeeze(0).numpy()\n        \n#     return wav","metadata":{"execution":{"iopub.status.busy":"2026-02-22T00:35:16.745368Z","iopub.execute_input":"2026-02-22T00:35:16.745835Z","iopub.status.idle":"2026-02-22T00:35:16.766842Z","shell.execute_reply.started":"2026-02-22T00:35:16.745807Z","shell.execute_reply":"2026-02-22T00:35:16.766098Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 6) Load ML-based Voice Activity Detection(VAD) model (Silero)\nLoads the **Silero neural VAD** using the official `silero-vad` package (with an optional `torch.hub` fallback), supports **Torch/ONNX** variants, and places the VAD model on **CPU** for stable inference across environments.\n","metadata":{}},{"cell_type":"code","source":"\nUSE_PIP = True   # set False to use torch.hub instead\nUSE_ONNX = False # set True to use ONNX model\nVAD_DEVICE=\"cpu\" # silero VAD is light-weight,okay with cpu \n\nif USE_PIP:\n    from silero_vad import (\n        load_silero_vad,\n        get_speech_timestamps,\n        read_audio,\n        save_audio,\n        VADIterator,\n        collect_chunks,\n    )\n    silero_model = load_silero_vad(onnx=USE_ONNX)\nelse:\n    silero_model, utils = torch.hub.load(\n        repo_or_dir=\"snakers4/silero-vad\",\n        model=\"silero_vad\",\n        trust_repo=True,\n        onnx=USE_ONNX,\n    )\n    (get_speech_timestamps, save_audio, read_audio, VADIterator, collect_chunks) = utils\n\nsilero_model.to(VAD_DEVICE)\nsilero_model.eval()\n","metadata":{"execution":{"iopub.status.busy":"2026-02-22T00:37:32.176535Z","iopub.execute_input":"2026-02-22T00:37:32.177305Z","iopub.status.idle":"2026-02-22T00:37:32.264522Z","shell.execute_reply.started":"2026-02-22T00:37:32.177275Z","shell.execute_reply":"2026-02-22T00:37:32.263951Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 7) Neural VAD → speech segments (Silero)\nRuns **Silero VAD** on the full **16 kHz mono waveform** to extract speech regions as `(start_sec, end_sec)`. The main knobs are `vad_threshold` (strictness), `padding_ms` (context around speech), and `min_speech_ms` / `min_silence_ms` (smoothing), with a final merge step to handle overlaps.\n","metadata":{}},{"cell_type":"code","source":"def silero_vad_speech_segments(\n    wav16k: np.ndarray,\n    sr: int = 16000,\n    vad_threshold: float = 0.5,\n    padding_ms: int = 300,\n    min_silence_ms: int = 200,\n    min_speech_ms: int = 300,\n):\n    \"\"\"\n    Returns list of (start_sec, end_sec) speech segments using Silero neural VAD.\n    Runs VAD on the full waveform (more robust than streaming chunk iteration).\n    \"\"\"\n    assert sr == 16000, \"Silero VAD expects 16kHz audio\"\n\n    x = torch.from_numpy(wav16k).float().to(VAD_DEVICE)  # Silero utils typically expect CPU tensor\n\n    ts = get_speech_timestamps(\n        x,\n        silero_model,\n        sampling_rate=sr,\n        threshold=vad_threshold,\n        min_speech_duration_ms=min_speech_ms,\n        min_silence_duration_ms=min_silence_ms,\n        speech_pad_ms=padding_ms,\n    )\n\n\n    segs = [(t[\"start\"] / sr, t[\"end\"] / sr) for t in ts]\n\n    # Defensive merge in case of overlaps\n    merged = []\n    for s, e in sorted(segs):\n        if not merged or s > merged[-1][1]:\n            merged.append([s, e])\n        else:\n            merged[-1][1] = max(merged[-1][1], e)\n\n    return [(float(s), float(e)) for s, e in merged]\n","metadata":{"execution":{"iopub.status.busy":"2026-02-22T00:37:35.051641Z","iopub.execute_input":"2026-02-22T00:37:35.05194Z","iopub.status.idle":"2026-02-22T00:37:35.058339Z","shell.execute_reply.started":"2026-02-22T00:37:35.051915Z","shell.execute_reply":"2026-02-22T00:37:35.057606Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 7.1) Visual check: waveform + VAD speech segments\nOverlay detected speech regions on the waveform to quickly spot missed speech or false alarms.\n","metadata":{}},{"cell_type":"code","source":"# def plot_waveform_with_segments(wav, sr, segments, title=\"Waveform + VAD\"):\n#     t = np.arange(len(wav)) / sr\n#     plt.figure(figsize=(12, 3))\n#     plt.plot(t, wav, linewidth=0.7)\n#     for s, e in segments:\n#         plt.axvspan(s, e, alpha=0.25)\n#     plt.xlabel(\"Time (s)\")\n#     plt.title(title)\n#     plt.grid(True, axis=\"x\")\n#     plt.show()\n\n# # Example on one file\n# if wav_paths:\n#     wav = load_audio_mono_16k(wav_paths[0], 16000)\n#     speech_segs = silero_vad_speech_segments(wav, sr=16000)\n#     plot_waveform_with_segments(wav, 16000, speech_segs, title=f\"VAD: {wav_paths[0].name}\")\n","metadata":{"execution":{"iopub.status.busy":"2026-02-22T00:35:16.89067Z","iopub.execute_input":"2026-02-22T00:35:16.891064Z","iopub.status.idle":"2026-02-22T00:35:16.906136Z","shell.execute_reply.started":"2026-02-22T00:35:16.891043Z","shell.execute_reply":"2026-02-22T00:35:16.905556Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 8) Sliding windows inside speech segments\nCreates overlapping windows `(ws, we)` to extract speaker embeddings.\n","metadata":{}},{"cell_type":"code","source":"def make_windows(segments, win_sec=1.5, hop_sec=0.75):\n    \"\"\"Create sliding windows (ws,we) inside each speech segment.\"\"\"\n    windows = []\n    for s, e in segments:\n        dur = e - s\n        if dur <= win_sec:\n            windows.append((s, e))\n            continue\n        t = s\n        while t + win_sec <= e + 1e-6:\n            windows.append((t, t + win_sec))\n            t += hop_sec\n        if windows and windows[-1][1] < e:\n            windows.append((max(s, e - win_sec), e))\n\n    # de-dup (rounding is only for identity keys)\n    out, seen = [], set()\n    for ws, we in windows:\n        key = (round(ws, 3), round(we, 3))\n        if key not in seen:\n            out.append((float(ws), float(we)))\n            seen.add(key)\n    return out","metadata":{"execution":{"iopub.status.busy":"2026-02-22T00:37:39.297338Z","iopub.execute_input":"2026-02-22T00:37:39.297845Z","iopub.status.idle":"2026-02-22T00:37:39.303349Z","shell.execute_reply.started":"2026-02-22T00:37:39.297817Z","shell.execute_reply":"2026-02-22T00:37:39.30268Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 9) Load speaker embedding model (SpeechBrain ECAPA-TDNN)\nLoads a pretrained **ECAPA-TDNN speaker recognition model** (`spkrec-ecapa-voxceleb`) to convert speech audio into fixed-dimensional speaker embeddings for clustering.\n","metadata":{}},{"cell_type":"code","source":"# %% [code]\n# 9) Load Speaker Models on ALL GPUs (Parallel-Ready)\n\nimport os\nimport torch\nfrom pyannote.audio import Pipeline\nfrom speechbrain.inference.speaker import EncoderClassifier\n\n# Disable strict weights_only check\nos.environ[\"TORCH_FORCE_NO_WEIGHTS_ONLY_LOAD\"] = \"1\"\n\ntry:\n    from pyannote.audio.core.task import Specifications, Problem, Resolution\n    torch.serialization.add_safe_globals([Specifications, Problem, Resolution])\nexcept Exception:\n    pass\n\ntry:\n    torch.serialization.add_safe_globals([torch.torch_version.TorchVersion])\nexcept Exception:\n    pass\n\n# === Per-GPU Model Storage ===\nspk_models = {}          # gpu_idx -> SpeechBrain ECAPA model\ndiarization_pipelines = {}  # gpu_idx -> Pyannote pipeline (or None)\n\n# --- Step 1: Load Community Pipeline ONCE to verify ---\nprint(\"Verifying Pyannote Community Pipeline access...\")\ntry:\n    base_pipeline = Pipeline.from_pretrained(\n        \"pyannote/speaker-diarization-community-1\",\n        use_auth_token=HF_TOKEN\n    )\n    print(\"  ✅ Base pyannote community pipeline verified.\")\nexcept Exception as e:\n    print(f\"⚠️ Could not load pyannote artifacts: {e}\")\n    base_pipeline = None\n\n# --- Step 2: Load models on EACH GPU ---\nfor gpu_idx, dev in enumerate(DEVICES):\n    print(f\"\\n{'='*50}\")\n    print(f\"  Loading models on GPU {gpu_idx} ({dev})\")\n    print(f\"{'='*50}\")\n\n    # --- A. SpeechBrain ECAPA (one per GPU fallback) ---\n    spk_models[gpu_idx] = EncoderClassifier.from_hparams(\n        source=\"speechbrain/spkrec-ecapa-voxceleb\",\n        run_opts={\"device\": dev},\n        savedir=f\"/tmp/speechbrain_ecapa_gpu{gpu_idx}\",\n    )\n    print(f\"  ✅ SpeechBrain ECAPA loaded on {dev}\")\n\n    # --- B. Pyannote Pipeline (one per GPU) ---\n    try:\n        if base_pipeline is None:\n            raise RuntimeError(\"Base pipeline not available\")\n\n        # Create a fresh pipeline instance for this GPU to avoid thread locks\n        gpu_pipeline = Pipeline.from_pretrained(\n            \"pyannote/speaker-diarization-community-1\",\n            use_auth_token=HF_TOKEN\n        )\n\n        # Optional: You can still override the community defaults here if your Optuna hunt found better params\n        # gpu_pipeline.instantiate({\n        #     \"segmentation\": {\n        #         \"threshold\": 0.4742,\n        #         \"min_duration_off\": 0.0,\n        #     },\n        #     \"clustering\": {\n        #         \"method\": \"centroid\",\n        #         \"min_cluster_size\": 20,\n        #         \"threshold\": 0.7133,\n        #     }\n        # })\n\n        gpu_pipeline.to(torch.device(dev))\n        diarization_pipelines[gpu_idx] = gpu_pipeline\n        print(f\"  ✅ Pyannote community pipeline loaded on {dev}\")\n\n    except Exception as e:\n        print(f\"  ⚠️ Pyannote pipeline failed for GPU {gpu_idx}: {e}\")\n        diarization_pipelines[gpu_idx] = None\n\n# --- Backward compatibility aliases (GPU 0) ---\nspk_model = spk_models[0]\ndiarization_pipeline = diarization_pipelines.get(0)\n\nprint(f\"\\n{'='*50}\")\nprint(f\"✅ Models loaded on {len(DEVICES)} device(s): {DEVICES}\")\nprint(f\"   Pyannote pipelines active: {sum(1 for v in diarization_pipelines.values() if v is not None)}/{len(DEVICES)}\")\nprint(f\"{'='*50}\")\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"\n# %% [code]\n# 9) Load Speaker Models on ALL GPUs (Parallel-Ready)\n\nimport os\nimport copy\nimport torch\n\n# Disable strict weights_only check\nos.environ[\"TORCH_FORCE_NO_WEIGHTS_ONLY_LOAD\"] = \"1\"\n\ntry:\n    from pyannote.audio.core.task import Specifications, Problem, Resolution\n    torch.serialization.add_safe_globals([Specifications, Problem, Resolution])\nexcept Exception:\n    pass\n\ntry:\n    torch.serialization.add_safe_globals([torch.torch_version.TorchVersion])\nexcept Exception:\n    pass\n\nimport torch\nfrom pyannote.audio import Pipeline\nfrom pyannote.audio.pipelines import SpeakerDiarization\nfrom speechbrain.inference.speaker import EncoderClassifier\n\n# === Per-GPU Model Storage ===\nspk_models = {}          # gpu_idx -> SpeechBrain ECAPA model\ndiarization_pipelines = {}  # gpu_idx -> Pyannote pipeline (or None)\n\n# --- Step 1: Download shared artifacts ONCE ---\nprint(\"Downloading shared model artifacts...\")\n\nfrom huggingface_hub import hf_hub_download\nfrom pyannote.audio import Model\n\ncheckpoint_path = None\nbase_pipeline = None\n\ntry:\n    checkpoint_path = hf_hub_download(\n        repo_id=\"ashque12/diarizeback\",\n        filename=\"checkpoint_epoch_9.ckpt\",\n        use_auth_token=HF_TOKEN\n    )\n    print(f\"  Bengali fine-tuned checkpoint: {checkpoint_path}\")\n    \n    base_pipeline = Pipeline.from_pretrained(\n        \"pyannote/speaker-diarization-3.1\",\n        use_auth_token=HF_TOKEN\n    )\n    print(\"  Base pyannote pipeline downloaded.\")\nexcept Exception as e:\n    print(f\"⚠️ Could not download pyannote artifacts: {e}\")\n\n# --- Step 2: Load models on EACH GPU ---\nfor gpu_idx, dev in enumerate(DEVICES):\n    print(f\"\\n{'='*50}\")\n    print(f\"  Loading models on GPU {gpu_idx} ({dev})\")\n    print(f\"{'='*50}\")\n\n    # --- A. SpeechBrain ECAPA (one per GPU) ---\n    spk_models[gpu_idx] = EncoderClassifier.from_hparams(\n        source=\"speechbrain/spkrec-ecapa-voxceleb\",\n        run_opts={\"device\": dev},\n        savedir=f\"/tmp/speechbrain_ecapa_gpu{gpu_idx}\",\n    )\n    print(f\"  ✅ SpeechBrain ECAPA loaded on {dev}\")\n\n    # --- B. Pyannote Pipeline (one per GPU) ---\n    try:\n        if checkpoint_path is None or base_pipeline is None:\n            raise RuntimeError(\"Base pipeline not available\")\n\n        # Load a fresh segmentation model for this GPU\n        tuned_seg_model = Model.from_pretrained(\n            checkpoint_path,\n            strict=False\n        )\n\n        # Create a new pipeline instance for this GPU\n        gpu_pipeline = SpeakerDiarization(\n            segmentation=tuned_seg_model,\n            embedding=base_pipeline.embedding,\n            clustering=base_pipeline.klustering,\n        )\n\n        gpu_pipeline.instantiate({\n            \"segmentation\": {\n                \"threshold\": 0.4742,\n                \"min_duration_off\": 0.0,\n            },\n            \"clustering\": {\n                \"method\": \"centroid\",\n                \"min_cluster_size\": 20,\n                \"threshold\": 0.7133,\n            }\n        })\n\n        gpu_pipeline.to(torch.device(dev))\n        diarization_pipelines[gpu_idx] = gpu_pipeline\n        print(f\"  ✅ Pyannote pipeline loaded on {dev}\")\n\n    except Exception as e:\n        print(f\"  ⚠️ Pyannote pipeline failed for GPU {gpu_idx}: {e}\")\n        diarization_pipelines[gpu_idx] = None\n\n# --- Backward compatibility aliases (GPU 0) ---\nspk_model = spk_models[0]\ndiarization_pipeline = diarization_pipelines.get(0)\n\nprint(f\"\\n{'='*50}\")\nprint(f\"✅ Models loaded on {len(DEVICES)} device(s): {DEVICES}\")\nprint(f\"   Pyannote pipelines active: {sum(1 for v in diarization_pipelines.values() if v is not None)}/{len(DEVICES)}\")\nprint(f\"{'='*50}\")\n","metadata":{"execution":{"iopub.status.busy":"2026-02-22T00:37:42.209782Z","iopub.execute_input":"2026-02-22T00:37:42.210186Z","iopub.status.idle":"2026-02-22T00:37:43.49856Z","shell.execute_reply.started":"2026-02-22T00:37:42.210159Z","shell.execute_reply":"2026-02-22T00:37:43.497848Z"}}},{"cell_type":"markdown","source":"## 10) Speaker embedding extraction (ECAPA-TDNN)\nComputes a **fixed-dimensional speaker embedding** from a **mono 16 kHz** waveform using the pretrained ECAPA model in `torch.inference_mode()` (no gradients) and returns a `(D,)` `float32` NumPy vector for clustering.\n","metadata":{}},{"cell_type":"code","source":"\n@torch.inference_mode()\ndef ecapa_embedding(wav16k: np.ndarray, model=None, device=None) -> np.ndarray:\n    \"\"\"\n    wav16k: mono float32, 16kHz\n    model: SpeechBrain EncoderClassifier (defaults to GPU 0 model)\n    device: target device string (defaults to primary DEVICE)\n    returns: (D,) numpy embedding\n    \"\"\"\n    if model is None:\n        model = spk_models[0]\n    if device is None:\n        device = DEVICES[0]\n    x = torch.from_numpy(wav16k).unsqueeze(0).to(device)\n    emb = model.encode_batch(x)\n    return emb.squeeze().detach().cpu().numpy().astype(np.float32)\n","metadata":{"execution":{"iopub.status.busy":"2026-02-22T00:35:20.578853Z","iopub.execute_input":"2026-02-22T00:35:20.579247Z","iopub.status.idle":"2026-02-22T00:35:20.583821Z","shell.execute_reply.started":"2026-02-22T00:35:20.57922Z","shell.execute_reply":"2026-02-22T00:35:20.583321Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 11) Estimate number of speakers (silhouette on cosine distance)\nTries `k = 2..max_k` using agglomerative clustering on **L2-normalized embeddings** and selects the `k` with the best **silhouette score** (cosine metric) as a rough speaker-count estimate.\n","metadata":{}},{"cell_type":"code","source":"def estimate_num_speakers(embs: np.ndarray, max_k: int = 6) -> int:\n    \"\"\"Rough speaker count estimation using silhouette score on cosine distance.\"\"\"\n    n = len(embs)\n    if n < 3:\n        return 1\n    max_k = min(max_k, n)\n    if max_k <= 1:\n        return 1\n\n    X = normalize(embs)\n\n    best_k = 1\n    best_score = -1.0\n    for k in range(2, max_k + 1):\n        try:\n            labels = AgglomerativeClustering(\n                n_clusters=k, metric=\"cosine\", linkage=\"average\"\n            ).fit_predict(X)\n            if len(set(labels)) < 2:\n                continue\n            score = silhouette_score(X, labels, metric=\"cosine\")\n            if score > best_score:\n                best_score = score\n                best_k = k\n        except Exception:\n            continue\n\n    return best_k","metadata":{"execution":{"iopub.status.busy":"2026-02-22T00:35:20.584688Z","iopub.execute_input":"2026-02-22T00:35:20.584955Z","iopub.status.idle":"2026-02-22T00:35:20.613449Z","shell.execute_reply.started":"2026-02-22T00:35:20.584928Z","shell.execute_reply":"2026-02-22T00:35:20.612825Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 12) Cluster embeddings into speaker labels\nClusters the (normalized) window-level speaker embeddings into `n_speakers` groups using **agglomerative clustering** with **cosine distance** and **average linkage**, returning an integer label per window.\n","metadata":{}},{"cell_type":"code","source":"def cluster_embeddings(embs: np.ndarray, n_speakers: int) -> np.ndarray:\n    if len(embs) == 0:\n        return np.array([], dtype=int)\n    if n_speakers <= 1 or len(embs) < 2:\n        return np.zeros(len(embs), dtype=int)\n    X = normalize(embs)\n    labels = AgglomerativeClustering(\n        n_clusters=n_speakers, metric=\"cosine\", linkage=\"average\"\n    ).fit_predict(X)\n    return labels.astype(int)","metadata":{"execution":{"iopub.status.busy":"2026-02-22T00:35:20.614424Z","iopub.execute_input":"2026-02-22T00:35:20.614757Z","iopub.status.idle":"2026-02-22T00:35:20.629798Z","shell.execute_reply.started":"2026-02-22T00:35:20.61473Z","shell.execute_reply":"2026-02-22T00:35:20.629249Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 12.1) Visual check: embedding clusters (PCA to 2D)\nProjects speaker embeddings to 2D to see whether predicted speakers form separable clusters.\n","metadata":{}},{"cell_type":"code","source":"from sklearn.decomposition import PCA\n\ndef plot_embedding_pca(embs, labels, title=\"Embedding PCA\"):\n    if len(embs) < 2:\n        print(\"Not enough embeddings to plot.\")\n        return\n    X = normalize(embs)\n    Z = PCA(n_components=2).fit_transform(X)\n\n    plt.figure(figsize=(6, 5))\n    plt.scatter(Z[:, 0], Z[:, 1], c=labels, s=18)\n    plt.title(title)\n    plt.xlabel(\"PC1\")\n    plt.ylabel(\"PC2\")\n    plt.grid(True)\n    plt.show()\n\n","metadata":{"execution":{"iopub.status.busy":"2026-02-22T00:35:20.630816Z","iopub.execute_input":"2026-02-22T00:35:20.631064Z","iopub.status.idle":"2026-02-22T00:35:20.64548Z","shell.execute_reply.started":"2026-02-22T00:35:20.631046Z","shell.execute_reply":"2026-02-22T00:35:20.644927Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- One-file debug run: compute embs + labels for visualization ---\n# test_path = wav_paths[0]\n\n# wav = load_audio_mono_16k(test_path, 16000)\n# speech_segs = silero_vad_speech_segments(wav, sr=16000)\n# windows = make_windows(speech_segs, win_sec=1.5, hop_sec=0.75)\n\n# embs = []\n# valid_windows = []\n# for ws, we in windows:\n#     s = int(ws * 16000)\n#     e = int(we * 16000)\n#     chunk = wav[s:e]\n#     if len(chunk) < int(0.4 * 16000):\n#         continue\n#     embs.append(ecapa_embedding(chunk))\n#     valid_windows.append((ws, we))\n\n# embs = np.vstack(embs) if len(embs) else np.empty((0, 192), dtype=np.float32)  # 192 is common for ECAPA\n\n# n_spk = estimate_num_speakers(embs, max_k=6) if len(embs) else 1\n# labels = cluster_embeddings(embs, n_speakers=n_spk) if len(embs) else np.array([], dtype=int)\n\n# len(embs), len(labels), n_spk\n","metadata":{"execution":{"iopub.status.busy":"2026-02-22T00:35:20.646367Z","iopub.execute_input":"2026-02-22T00:35:20.646756Z","iopub.status.idle":"2026-02-22T00:35:20.66422Z","shell.execute_reply.started":"2026-02-22T00:35:20.646728Z","shell.execute_reply":"2026-02-22T00:35:20.663643Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# plot_embedding_pca(embs, labels, title=\"ECAPA embeddings clustered\")\n","metadata":{"execution":{"iopub.status.busy":"2026-02-22T00:35:20.667313Z","iopub.execute_input":"2026-02-22T00:35:20.667549Z","iopub.status.idle":"2026-02-22T00:35:20.678663Z","shell.execute_reply.started":"2026-02-22T00:35:20.667532Z","shell.execute_reply":"2026-02-22T00:35:20.67806Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 13) Convert labeled windows into speaker turns\nMerges adjacent/overlapping windows that share the same speaker label into continuous **speaker turns** `(start_sec, end_sec, speaker_label)`, filtering very short turns and optionally bridging small same-speaker gaps.\n","metadata":{}},{"cell_type":"code","source":"def windows_to_turns(windows, labels, min_turn_sec=0.2):\n    \"\"\"Merge adjacent/overlapping same-speaker windows into turns.\"\"\"\n    if not windows:\n        return []\n\n    items = sorted(zip(windows, labels), key=lambda x: x[0][0])\n    cur_s, cur_e = items[0][0]\n    cur_lab = int(items[0][1])\n\n    turns = []\n    for (ws, we), lab in items[1:]:\n        lab = int(lab)\n        if lab == cur_lab and ws <= cur_e + 1e-3:\n            cur_e = max(cur_e, we)\n        else:\n            if cur_e - cur_s >= min_turn_sec:\n                turns.append((cur_s, cur_e, cur_lab))\n            cur_s, cur_e, cur_lab = ws, we, lab\n\n    if cur_e - cur_s >= min_turn_sec:\n        turns.append((cur_s, cur_e, cur_lab))\n\n    # Merge small gaps for same speaker\n    merged = []\n    for s, e, lab in turns:\n        if not merged:\n            merged.append([s, e, lab])\n        else:\n            ps, pe, pl = merged[-1]\n            if lab == pl and s <= pe + 0.25:\n                merged[-1][1] = max(pe, e)\n            else:\n                merged.append([s, e, lab])\n\n    return [(float(s), float(e), int(lab)) for s, e, lab in merged]","metadata":{"execution":{"iopub.status.busy":"2026-02-22T00:35:20.679654Z","iopub.execute_input":"2026-02-22T00:35:20.680143Z","iopub.status.idle":"2026-02-22T00:35:20.694275Z","shell.execute_reply.started":"2026-02-22T00:35:20.680123Z","shell.execute_reply":"2026-02-22T00:35:20.693637Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 14) End-to-end diarization pipeline (ML VAD → embeddings → clustering → turns)\nRuns the full diarization workflow on one audio file: **load + resample to 16 kHz**, detect **speech segments** using Silero VAD, create **overlapping windows**, extract **ECAPA speaker embeddings**, estimate **speaker count**, **cluster** windows, merge them into **speaker turns**, and output a list of `{start_time, end_time, speaker_id}` labeled as `SPEAKER_1..N`.\n","metadata":{}},{"cell_type":"code","source":"# %% [code]\n# NEW COMPONENT: Heuristic Segment Merging\nfrom pyannote.core import Annotation, Segment\n\ndef merge_close_segments(annotation: Annotation, gap_threshold: float = 0.1) -> Annotation:\n    \"\"\"\n    Failsafe logic: stitches fragmented speaker turns separated by small gaps.\n    Complexity: O(N log N).\n    \"\"\"\n    new_annotation = Annotation()\n    # Sort chronologically to bridge inter-syllabic gaps\n    tracks = sorted(list(annotation.itertracks(yield_label=True)), key=lambda x: x[0].start)\n    \n    if not tracks: return new_annotation\n        \n    current_segment, _, current_label = tracks[0]\n    \n    for next_segment, _, next_label in tracks[1:]:\n        gap = next_segment.start - current_segment.end\n        # Condition: Same speaker + small gap (stutter block range)\n        if (current_label == next_label) and (gap < gap_threshold):\n            current_segment = Segment(current_segment.start, max(current_segment.end, next_segment.end))\n        else:\n            new_annotation[current_segment] = current_label\n            current_segment, current_label = next_segment, next_label\n            \n    new_annotation[current_segment] = current_label\n    return new_annotation","metadata":{"execution":{"iopub.status.busy":"2026-02-22T00:35:20.695141Z","iopub.execute_input":"2026-02-22T00:35:20.695411Z","iopub.status.idle":"2026-02-22T00:35:20.715054Z","shell.execute_reply.started":"2026-02-22T00:35:20.695386Z","shell.execute_reply":"2026-02-22T00:35:20.714483Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# def refine_annotation(annotation: Annotation, gap_threshold: float = 0.6, min_duration: float = 0.8) -> Annotation:\n#     # 1. Merge same-speaker gaps (Your current logic)\n#     merged = merge_close_segments(annotation, gap_threshold)\n    \n#     # 2. Filter out \"flickers\"\n#     final_annotation = Annotation()\n#     for segment, track, label in merged.itertracks(yield_label=True):\n#         if segment.duration >= min_duration:\n#             final_annotation[segment, track] = label\n            \n#     return final_annotation\n\n\ndef refine_annotation(annotation: Annotation, gap_threshold: float = 0.6, min_duration: float = 0.8) -> Annotation:\n    # Step 1: Standardize by merging same-speaker segments first\n    # This prevents the loop from seeing two segments of the same speaker as different entities\n    merged = merge_close_segments(annotation, gap_threshold)\n    \n    final_annotation = Annotation()\n    tracks = list(merged.itertracks(yield_label=True))\n    \n    if not tracks:\n        return final_annotation\n\n    # Initialize with the first segment, but only if it's long enough\n    # If the very first segment of the file is a flicker, we skip it\n    idx = 0\n    while idx < len(tracks) and tracks[idx][0].duration < min_duration:\n        idx += 1\n    \n    if idx >= len(tracks):\n        return final_annotation # Entire file was flickers\n\n    current_seg, _, current_label = tracks[idx]\n\n    for i in range(idx + 1, len(tracks)):\n        next_seg, _, next_label = tracks[i]\n        gap = next_seg.start - current_seg.end\n\n        if next_seg.duration < min_duration:\n            if gap < gap_threshold:\n                # STITCH: Segment is short AND close -> absorb it\n                current_seg = Segment(current_seg.start, next_seg.end)\n            else:\n                # REMOVE: Segment is short but gap is too large -> ignore it (delete)\n                continue\n        else:\n            # COMMIT: Segment is long enough -> save current and move to next\n            final_annotation[current_seg] = current_label\n            current_seg, current_label = next_seg, next_label\n\n    # Save the final processed segment\n    final_annotation[current_seg] = current_label\n    return final_annotation","metadata":{"execution":{"iopub.status.busy":"2026-02-22T00:35:20.715795Z","iopub.execute_input":"2026-02-22T00:35:20.716267Z","iopub.status.idle":"2026-02-22T00:35:20.734857Z","shell.execute_reply.started":"2026-02-22T00:35:20.716247Z","shell.execute_reply":"2026-02-22T00:35:20.734273Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# %% [code]\n# Optimized Pyannote 3.1 for Bengali Drama (Multi-GPU with SpeechBrain fallback)\n\ndef diarize_file(\n    audio_path: str | Path,\n    gpu_id: int = 0,\n    merge_threshold: float = 0.6,\n    min_duration: float = 0.8\n):\n    \"\"\"\n    Primary: Pyannote 3.1 pipeline (with Bengali fine-tuned segmentation).\n    Fallback: SpeechBrain ECAPA + Silero VAD + Agglomerative Clustering.\n    \n    gpu_id: which GPU to use (0 or 1 for dual T4 setup)\n    \"\"\"\n    # Select models for this GPU\n    dev = DEVICES[gpu_id] if gpu_id < len(DEVICES) else DEVICES[0]\n    pipeline = diarization_pipelines.get(gpu_id)\n    model = spk_models.get(gpu_id, spk_models[0])\n\n    # ── STRATEGY A: Pyannote Pipeline ──\n    if pipeline is not None:\n        try:\n            waveform = load_audio_mono_16k(audio_path)\n            waveform_tensor = torch.from_numpy(waveform).unsqueeze(0)\n\n            diarization = pipeline({\n                \"waveform\": waveform_tensor,\n                \"sample_rate\": 16000\n            })\n\n            found_spk = len(diarization.labels())\n            print(f\"  [GPU {gpu_id}] ✅ Pyannote: {found_spk} speakers in {Path(audio_path).name}\")\n\n            refined = refine_annotation(diarization, gap_threshold=merge_threshold, min_duration=min_duration)\n            \n            found_spk2 = len(refined.labels())\n            print(f\"  [GPU {gpu_id}] ✅ After refinement: {found_spk2} speakers\")\n\n            refined = reindex_labels(refined)\n\n            out = []\n            for turn, _, speaker in refined.itertracks(yield_label=True):\n                out.append({\n                    \"start_time\": round(turn.start, 2),\n                    \"end_time\": round(turn.end, 2),\n                    \"speaker_id\": f\"SPEAKER_{speaker}\",\n                })\n            return out\n\n        except Exception as e:\n            print(f\"  [GPU {gpu_id}] ⚠️ Pyannote error on {Path(audio_path).name}: {e}\")\n            print(f\"  [GPU {gpu_id}] → Falling back to SpeechBrain pipeline.\")\n\n    # ── STRATEGY B: SpeechBrain ECAPA fallback ──\n    try:\n        wav = load_audio_mono_16k(audio_path)\n        speech_segs = silero_vad_speech_segments(wav, sr=16000)\n        windows = make_windows(speech_segs, win_sec=1.5, hop_sec=0.75)\n\n        if not windows:\n            return []\n\n        embs, valid_wins = [], []\n        for ws, we in windows:\n            s, e = int(ws * 16000), int(we * 16000)\n            chunk = wav[s:e]\n            if len(chunk) < int(0.4 * 16000):\n                continue\n            embs.append(ecapa_embedding(chunk, model=model, device=dev))\n            valid_wins.append((ws, we))\n\n        if not embs:\n            return []\n\n        embs = np.vstack(embs)\n        n_spk = estimate_num_speakers(embs, max_k=6)\n        labels = cluster_embeddings(embs, n_speakers=n_spk)\n        turns = windows_to_turns(valid_wins, labels)\n\n        return [\n            {\n                \"start_time\": round(s, 2),\n                \"end_time\": round(e, 2),\n                \"speaker_id\": f\"SPEAKER_{lab}\",\n            }\n            for s, e, lab in turns\n        ]\n\n    except Exception as e2:\n        print(f\"  [GPU {gpu_id}] ⚠️ SpeechBrain fallback also failed on {Path(audio_path).name}: {e2}\")\n        return []\n","metadata":{"execution":{"iopub.status.busy":"2026-02-22T00:35:20.73565Z","iopub.execute_input":"2026-02-22T00:35:20.735871Z","iopub.status.idle":"2026-02-22T00:35:20.756301Z","shell.execute_reply.started":"2026-02-22T00:35:20.735842Z","shell.execute_reply":"2026-02-22T00:35:20.755751Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from pyannote.core import Annotation\n\ndef reindex_labels(annotation: Annotation) -> Annotation:\n    \"\"\"\n    Maps speaker labels to a clean sequential order (1, 2, 3...) \n    based on their first appearance.\n    \"\"\"\n    new_annotation = Annotation()\n    label_map = {}\n    next_id = 1\n    \n    # We must iterate chronologically to map IDs in order of appearance\n    for segment, track, old_label in annotation.itertracks(yield_label=True):\n        # Convert old_label to string or int as needed for consistency\n        old_label = str(old_label)\n        \n        # If we haven't seen this original ID before, assign it the next available number\n        if old_label not in label_map:\n            label_map[old_label] = str(next_id)\n            next_id += 1\n        \n        # Apply the mapping\n        new_annotation[segment, track] = label_map[old_label]\n        \n    return new_annotation","metadata":{"execution":{"iopub.status.busy":"2026-02-22T00:35:20.757041Z","iopub.execute_input":"2026-02-22T00:35:20.757284Z","iopub.status.idle":"2026-02-22T00:35:20.776608Z","shell.execute_reply.started":"2026-02-22T00:35:20.757255Z","shell.execute_reply":"2026-02-22T00:35:20.776037Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 14.1) Visual check: diarization timeline (speaker turns)\nPlots the final speaker turns over time to verify speaker switches and boundary smoothness.","metadata":{}},{"cell_type":"code","source":"# def plot_diarization_turns(turns, title=\"Diarization timeline\"):\n#     if not turns:\n#         print(\"No turns to plot.\")\n#         return\n#     speakers = sorted({t[\"speaker_id\"] for t in turns})\n#     spk_to_y = {s: i for i, s in enumerate(speakers)}\n\n#     plt.figure(figsize=(12, 1 + 0.45 * len(speakers)))\n#     for t in turns:\n#         y = spk_to_y[t[\"speaker_id\"]]\n#         plt.plot([t[\"start_time\"], t[\"end_time\"]], [y, y], linewidth=6)\n#     plt.yticks(range(len(speakers)), speakers)\n#     plt.xlabel(\"Time (s)\")\n#     plt.title(title)\n#     plt.grid(True, axis=\"x\")\n#     plt.show()\n\n# if wav_paths:\n#     diar = diarize_file(wav_paths[0])\n#     plot_diarization_turns(diar, title=f\"Diarization: {wav_paths[0].name}\")\n","metadata":{"execution":{"iopub.status.busy":"2026-02-22T00:35:20.777641Z","iopub.execute_input":"2026-02-22T00:35:20.777865Z","iopub.status.idle":"2026-02-22T00:35:20.800658Z","shell.execute_reply.started":"2026-02-22T00:35:20.777844Z","shell.execute_reply":"2026-02-22T00:35:20.800137Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### (Archived) Weighted Voting Ensemble — disabled\nThe previous \"Weighted Voting Ensemble\" `diarize_file` was removed because it called undefined functions (`align_annotation`, `weighted_consensus`) and caused errors. The active pipeline is in the cell above.","metadata":{}},{"cell_type":"code","source":"# import os\n# os.remove(\"/kaggle/working/diarization_checkpoint.jsonl\") if os.path.exists(\"/kaggle/working/diarization_checkpoint.jsonl\") else None\n# os.remove(\"/kaggle/working/diarization_partial_backup.csv\") if os.path.exists(\"/kaggle/working/diarization_partial_backup.csv\") else None\n# print(\"Checkpoints cleared. Ready for a fresh, high-score run!\")","metadata":{"execution":{"iopub.status.busy":"2026-02-22T00:35:20.801563Z","iopub.execute_input":"2026-02-22T00:35:20.801852Z","iopub.status.idle":"2026-02-22T00:35:20.815208Z","shell.execute_reply.started":"2026-02-22T00:35:20.801803Z","shell.execute_reply":"2026-02-22T00:35:20.814634Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# %% [code]\n# PARALLEL Batch Processing across 2 GPUs\nimport gc\nimport json\nimport threading\nfrom concurrent.futures import ThreadPoolExecutor, as_completed\n\n# Clear GPU cache on all devices\nfor i in range(NUM_GPUS):\n    with torch.cuda.device(i):\n        torch.cuda.empty_cache()\n\ndef process_files_on_gpu(gpu_id, file_list):\n    \"\"\"Worker function: processes a list of (stem, path) on a specific GPU.\"\"\"\n    results = []\n    for stem, wav_path in tqdm(file_list, desc=f\"GPU {gpu_id}\", position=gpu_id):\n        gc.collect()\n        if gpu_id < NUM_GPUS:\n            with torch.cuda.device(gpu_id):\n                torch.cuda.empty_cache()\n\n        diar = diarize_file(wav_path, gpu_id=gpu_id, merge_threshold=0.3, min_duration=1.0)\n\n        results.append({\n            \"filename\": stem,\n            \"diarization\": json.dumps(diar, ensure_ascii=False),\n        })\n    return results\n\n# --- Split files evenly across GPUs (round-robin) ---\nall_files = list(zip(stems, wav_paths))\nnum_workers = max(1, len(DEVICES))  # number of GPUs (or 1 for CPU)\n\nsplits = [[] for _ in range(num_workers)]\nfor idx, item in enumerate(all_files):\n    splits[idx % num_workers].append(item)\n\nprint(f\"Total files: {len(all_files)}\")\nfor i, s in enumerate(splits):\n    print(f\"  GPU {i}: {len(s)} files\")\n\n# --- Run in parallel ---\nall_results = []\n\nif num_workers == 1:\n    # Single GPU / CPU: run sequentially\n    all_results = process_files_on_gpu(0, all_files)\nelse:\n    # Multi-GPU: run workers in parallel threads\n    with ThreadPoolExecutor(max_workers=num_workers) as executor:\n        futures = {\n            executor.submit(process_files_on_gpu, gpu_id, split): gpu_id\n            for gpu_id, split in enumerate(splits)\n        }\n        for future in as_completed(futures):\n            gpu_id = futures[future]\n            try:\n                results = future.result()\n                all_results.extend(results)\n                print(f\"✅ GPU {gpu_id} finished: {len(results)} files processed\")\n            except Exception as e:\n                print(f\"❌ GPU {gpu_id} failed: {e}\")\n\n# --- Sort results by filename to match expected order ---\nall_results.sort(key=lambda x: x[\"filename\"])\n\n# --- Final CSV Export ---\nsub_df = pd.DataFrame(all_results)\nsub_df.to_csv(OUT_PATH, index=False)\nprint(f\"\\n✅ Submission created: {OUT_PATH} ({len(sub_df)} files)\")\nsub_df.head()\n","metadata":{"execution":{"iopub.status.busy":"2026-02-22T00:35:59.651042Z","iopub.execute_input":"2026-02-22T00:35:59.651705Z","iopub.status.idle":"2026-02-22T00:35:59.662083Z","shell.execute_reply.started":"2026-02-22T00:35:59.651678Z","shell.execute_reply":"2026-02-22T00:35:59.661256Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## tune the parameters","metadata":{}},{"cell_type":"code","source":"# # %% [code]\n# # --- LOCAL EVALUATION & PARAMETER TUNING STATION ---\n# import pandas as pd\n# from pyannote.core import Annotation, Segment\n# from pyannote.metrics.diarization import DiarizationErrorRate\n\n# def time_str_to_sec(time_str):\n#     h, m, s = time_str.split(':')\n#     return int(h) * 3600 + int(m) * 60 + float(s)\n\n# def evaluate_parameters(audio_name, merge_gap, min_turn, hp_cutoff, noise_reduce, onset=0.3, offset=0.3, num_speakers=2):\n#     base_dir = \"/kaggle/input/competitions/dl-sprint-4-0-bengali-speaker-diarization-challenge/diarization/diarization/train\"\n#     audio_path = f\"{base_dir}/audio/{audio_name}.wav\"\n#     csv_path = f\"{base_dir}/annotation/{audio_name}.csv\"\n    \n#     # Load Ground Truth\n#     truth_df = pd.read_csv(csv_path)\n#     reference = Annotation()\n#     for _, row in truth_df.iterrows():\n#         start = time_str_to_sec(row['start_time'])\n#         end = time_str_to_sec(row['end_time'])\n#         spk_id = str(row['speaker_id']).replace(\"SPEAKER_\", \"\").replace(\"speaker_\", \"\").strip()\n#         reference[Segment(start, end)] = spk_id\n\n#     # Run Prediction with Custom Parameters\n#     predictions = diarize_file(\n#         audio_path,\n#         merge_threshold=merge_gap,\n#         min_duration=min_turn,\n#         # hp_cutoff=hp_cutoff,\n#         # noise_reduce=noise_reduce,\n#         # onset=onset,\n#         # offset=offset,\n#         # num_speakers=num_speakers\n#     )\n    \n#     # Format Prediction\n#     hypothesis = Annotation()\n#     for seg in predictions:\n#         start = float(seg[\"start_time\"])\n#         end = float(seg[\"end_time\"])\n#         spk = str(seg[\"speaker_id\"]).replace(\"SPEAKER_\", \"\").strip()\n#         # Fallback mapping if Pyannote outputs 00/01 instead of 1/2\n#         if spk == \"00\": spk = \"1\"\n#         elif spk == \"01\": spk = \"2\"\n#         elif spk == \"02\": spk = \"3\"\n#         elif spk == \"03\": spk = \"4\"\n#         elif spk == \"04\": spk = \"5\"\n#         elif spk == \"05\": spk = \"6\"\n#         elif spk == \"06\": spk = \"7\"\n#         elif spk == \"07\": spk = \"8\"\n#         elif spk == \"08\": spk = \"9\"\n#         elif spk == \"09\": spk = \"10\"\n#         hypothesis[Segment(start, end)] = spk\n\n\n\n    \n#     output_dir = \"/kaggle/working/eval_debug\"\n#     os.makedirs(output_dir, exist_ok=True)\n    \n#     ref_rttm = os.path.join(output_dir, f\"{audio_name}_reference.rttm\")\n#     hyp_rttm = os.path.join(output_dir, f\"{audio_name}_hypothesis.rttm\")\n    \n#     with open(ref_rttm, \"w\") as f:\n#         reference.write_rttm(f)\n        \n#     with open(hyp_rttm, \"w\") as f:\n#         hypothesis.write_rttm(f)\n        \n#     print(f\"DEBUG: Files saved to {output_dir}\")\n#     # ----------------------------------------\n\n\n    \n#     # Calculate Score\n#     metric = DiarizationErrorRate()\n#     der = metric(reference, hypothesis, detailed=True)\n    \n#     total_der = der['diarization error rate']\n#     score = max(0.0, 100 * (1 - total_der))\n    \n#     print(f\"Gap: {merge_gap}s | Min_Turn: {min_turn}s | HP_Cut: {hp_cutoff}Hz | NR: {noise_reduce} | ONSET: {onset} | OFFSET: {offset}\")\n#     print(f\" -> FA: {der['false alarm']/der['total']:.1%} | Miss: {der['missed detection']/der['total']:.1%} | Conf: {der['confusion']/der['total']:.1%}\")\n#     print(f\" -> DER: {total_der:.1%} | KAGGLE SCORE: {score:.5f}\\n\")\n    \n#     return score, total_der\n\n# # ========================================================\n# # 🚀 TWEAK THESE TO CRUSH THE 0.20 DER BARRIER 🚀\n# # ========================================================\n# print(\"Starting Parameter Search on train_004...\\n\")\n\n\n\n# configs_to_test = [\n#     # (merge_gap, min_turn, hp_cutoff, noise_reduce, onset, offset, min_spk, max_spk)\n#     (0.3, 1.0, 0.0, 0.0, 0.0, 0.0,), # HIGH SKEPTICISM: High onset, high min_turn\n#     (0.2, 1.0, 0.0, 0.0, 0.0, 0.0,), # HIGH SKEPTICISM: High onset, high min_turn\n#     (0.3, 0.9, 0.0, 0.0, 0.0, 0.0,), # HIGH SKEPTICISM: High onset, high min_turn\n#     (0.3, 1.0, 0.0, 0.0, 0.0, 0.0,), # HIGH SKEPTICISM: High onset, high min_turn\n# ]\n\n# best_score = 0\n# for config in configs_to_test:\n#     score, der = evaluate_parameters(\"train_011\", *config)\n    \n#     if score > best_score:\n#         best_score = score\n\n# print(f\"🏆 Best Score Found: {best_score:.2f}\")","metadata":{"execution":{"execution_failed":"2026-02-22T00:35:25.558Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# !ls -R /kaggle","metadata":{"execution":{"execution_failed":"2026-02-22T00:35:25.558Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# import os\n# import optuna\n# import pandas as pd\n# import torch\n# from tqdm.auto import tqdm\n# from pyannote.core import Annotation, Segment\n# from pyannote.metrics.diarization import DiarizationErrorRate\n\n# # --- 1. Helper to load Ground Truth ---\n# def time_str_to_sec(time_str):\n#     h, m, s = time_str.split(':')\n#     return int(h) * 3600 + int(m) * 60 + float(s)\n\n# def load_ground_truth(csv_path):\n#     truth_df = pd.read_csv(csv_path)\n#     reference = Annotation()\n#     for _, row in truth_df.iterrows():\n#         start = time_str_to_sec(row['start_time'])\n#         end = time_str_to_sec(row['end_time'])\n#         spk_id = str(row['speaker_id']).replace(\"SPEAKER_\", \"\").replace(\"speaker_\", \"\").strip()\n#         reference[Segment(start, end)] = spk_id\n#     return reference\n\n# VALIDATION_FILES = [\"train_004\", \"train_009\", \"train_011\"] \n# BASE_DIR = \"/kaggle/input/competitions/dl-sprint-4-0-bengali-speaker-diarization-challenge/diarization/diarization/train\"\n\n# def print_best_so_far(study):\n#     try:\n#         best = study.best_trial\n#     except Exception:\n#         return\n\n#     if best is None or best.value is None:\n#         return\n\n#     params = best.params\n\n#     print(\n#         f\"🏆 BEST SO FAR | \"\n#         f\"DER={float(best.value)*100:.2f}% | \"\n#         f\"seg={float(params.get('segmentation_threshold', 0)):.4f}, \"\n#         f\"clust={float(params.get('clustering_threshold', 0)):.4f}, \"\n#         f\"min_cluster={int(params.get('min_cluster_size', 0))}\"\n#     )\n\n\n# # --- 2. Optuna Objective Function ---\n# def objective(trial):\n#     trial_dir = f\"/kaggle/working/trial_{trial.number}\"\n#     os.makedirs(trial_dir, exist_ok=True)\n#     # Suggest hyperparameter values\n#     seg_threshold = trial.suggest_float(\"segmentation_threshold\", 0.3, 0.7)\n#     clust_threshold = trial.suggest_float(\"clustering_threshold\", 0.5, 0.9)\n#     min_cluster_size = trial.suggest_int(\"min_cluster_size\", 10, 30)\n    \n#     print(f\"\\n{'='*20} [Trial {trial.number}] {'='*20}\")\n#     print(f\"PARAMS: Seg_Thresh={seg_threshold:.4f} | Clust_Thresh={clust_threshold:.4f} | Min_Cluster={min_cluster_size}\")\n    \n#     diarization_pipeline.instantiate({\n#         \"segmentation\": {\n#             \"threshold\": seg_threshold,\n#             \"min_duration_off\": 0.0,\n#         },\n#         \"clustering\": {\n#             \"method\": \"centroid\",\n#             \"min_cluster_size\": min_cluster_size,\n#             \"threshold\": clust_threshold,\n#         }\n#     })\n    \n#     # We use 'metric' for global aggregation and 'file_metric' for individual file logging\n#     global_metric = DiarizationErrorRate()\n    \n#     for file_id in tqdm(VALIDATION_FILES, desc=f\"Trial {trial.number}\", leave=False):\n#         audio_path = f\"{BASE_DIR}/audio/{file_id}.wav\"\n#         csv_path = f\"{BASE_DIR}/annotation/{file_id}.csv\"\n        \n#         reference = load_ground_truth(csv_path)\n#         waveform = load_audio_mono_16k(audio_path)\n#         waveform_tensor = torch.from_numpy(waveform).unsqueeze(0)\n        \n#         try:\n#             hypothesis = diarization_pipeline({\n#                 \"waveform\": waveform_tensor,\n#                 \"sample_rate\": 16000\n#             })\n#         except Exception as e:\n#             print(f\"❌ Failed on {file_id}: {e}\")\n#             return float(\"inf\")\n\n\n#         # 2. SAVE RTTM FILES\n#         ref_path = os.path.join(trial_dir, f\"{file_id}_reference.rttm\")\n#         hyp_path = os.path.join(trial_dir, f\"{file_id}_hypothesis.rttm\")\n        \n#         with open(ref_path, \"w\") as f:\n#             reference.write_rttm(f)\n#         with open(hyp_path, \"w\") as f:\n#             hypothesis.write_rttm(f)\n        \n#         # Calculate individual file metric for logging\n#         file_metric = DiarizationErrorRate()\n#         file_der = file_metric(reference, hypothesis)\n\n#         # Save a CSV report for this specific file in this trial\n#         report_df = file_metric.report(display=False)\n#         report_df.to_csv(os.path.join(trial_dir, f\"{file_id}_metrics.csv\"))\n        \n#         # Extract components for this specific file\n#         # file_metric reports are dictionaries of percentages\n#         components = file_metric.compute_components(reference, hypothesis)\n\n#         total = sum(components.values()) + 1e-8\n\n#         f_miss = components[\"missed detection\"] / total\n#         f_fa   = components[\"false alarm\"] / total\n#         f_conf = components[\"confusion\"] / total\n\n        \n#         print(f\"  > File {file_id}: DER={file_der*100:.2f}% | Miss={f_miss*100:.2f}% | FA={f_fa*100:.2f}% | Conf={f_conf*100:.2f}%\")\n#         print(\"🔥 GLOBAL BEST \")\n#         print_best_so_far(trial.study)\n#         # Accumulate into the global metric\n#         global_metric(reference, hypothesis)\n#         break\n    \n#     # Calculate global totals\n#     # total_report = global_metric.report(display=False).iloc[-1]\n#     # global_der = total_report['diarization error rate']\n#     final_report = global_metric.report(display=False)\n#     global_der = float(final_report.iloc[-1][\"diarization error rate\"])\n\n#     # 4. SAVE SUMMARY FOR THE WHOLE TRIAL\n#     final_summary = global_metric.report(display=False)\n#     final_summary.to_csv(os.path.join(trial_dir, \"TRIAL_SUMMARY.csv\"))\n    \n#     print(f\"--- TRIAL SUMMARY ---\")\n#     print(f\"GLOBAL DER: {global_der*100:.2f}%\")\n    \n#     return global_der\n\n# # --- 3. Execute the Hunt ---\n# print(\"🚀 Starting Grandmaster Optuna Hunt with File-Level Logging...\")\n\n# study = optuna.create_study(\n#     study_name=\"pyannote_bengali_tuning\", \n#     direction=\"minimize\",\n#     sampler=optuna.samplers.TPESampler(seed=42)\n# )\n\n# n_trials = 30\n# pbar = tqdm(total=n_trials, desc=\"Optimization Progress\")\n\n# def callback(study, trial):\n#     pbar.update(1)\n#     pbar.set_description(f\"Best Global DER: {study.best_value*100:.2f}%\")\n\n# try:\n#     study.optimize(objective, n_trials=n_trials, callbacks=[callback])\n# finally:\n#     pbar.close()\n\n# print(\"\\n\" + \"=\"*60)\n# print(f\"🏆 FINAL OPTIMIZATION COMPLETE\")\n# print(f\"Best Global DER achieved: {study.best_value * 100:.2f}%\")\n# print(\"-\" * 60)\n# print(\"🔥 USE THESE PARAMETERS IN YOUR SUBMISSION:\")\n# for key, value in study.best_params.items():\n#     print(f\"    {key.upper()}: {value:.6f}\")\n# print(\"=\"*60)","metadata":{"execution":{"execution_failed":"2026-02-22T00:35:25.558Z"},"jupyter":{"source_hidden":true},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# import os\n# import optuna\n# import pandas as pd\n# import torch\n# from tqdm.auto import tqdm\n# from pyannote.core import Annotation, Segment\n# from pyannote.metrics.diarization import DiarizationErrorRate\n\n# # --- 1. Helper to load Ground Truth ---\n# def time_str_to_sec(time_str):\n#     h, m, s = time_str.split(':')\n#     return int(h) * 3600 + int(m) * 60 + float(s)\n\n# def load_ground_truth(csv_path):\n#     truth_df = pd.read_csv(csv_path)\n#     reference = Annotation()\n#     for _, row in truth_df.iterrows():\n#         start = time_str_to_sec(row['start_time'])\n#         end = time_str_to_sec(row['end_time'])\n#         spk_id = str(row['speaker_id']).replace(\"SPEAKER_\", \"\").replace(\"speaker_\", \"\").strip()\n#         reference[Segment(start, end)] = spk_id\n#     return reference\n\n# VALIDATION_FILES = [\"train_004\", \"train_009\", \"train_011\"] \n# BASE_DIR = \"/kaggle/input/competitions/dl-sprint-4-0-bengali-speaker-diarization-challenge/diarization/diarization/train\"\n\n# def print_best_so_far(study):\n#     try:\n#         best = study.best_trial\n#     except Exception:\n#         return\n\n#     if best is None or best.value is None:\n#         return\n\n#     params = best.params\n\n#     print(\n#         f\" BEST SO FAR | \"\n#         f\"DER={float(best.value)*100:.2f}% | \"\n#         f\"seg={float(params.get('segmentation_threshold', 0)):.4f}, \"\n#         f\"clust={float(params.get('clustering_threshold', 0)):.4f}, \"\n#         f\"min_cluster={int(params.get('min_cluster_size', 0))}\"\n#     )\n\n\n# # --- 2. Optuna Objective Function ---\n# def objective(trial):\n#     trial_dir = f\"/kaggle/working/trial_{trial.number}\"\n#     os.makedirs(trial_dir, exist_ok=True)\n    \n#     # Suggest hyperparameter values\n#     seg_threshold = trial.suggest_float(\"segmentation_threshold\", 0.3, 0.7)\n#     clust_threshold = trial.suggest_float(\"clustering_threshold\", 0.5, 0.9)\n#     min_cluster_size = trial.suggest_int(\"min_cluster_size\", 10, 30)\n    \n#     print(f\"\\n{'='*20} [Trial {trial.number}] {'='*20}\")\n#     print(f\"PARAMS: Seg_Thresh={seg_threshold:.4f} | Clust_Thresh={clust_threshold:.4f} | Min_Cluster={min_cluster_size}\")\n    \n#     # Instantiate the pipeline with new params\n#     # NOTE: 'diarization_pipeline' must be defined in a previous cell\n#     diarization_pipeline.instantiate({\n#         \"segmentation\": {\n#             \"threshold\": seg_threshold,\n#             \"min_duration_off\": 0.0,\n#         },\n#         \"clustering\": {\n#             \"method\": \"centroid\",\n#             \"min_cluster_size\": min_cluster_size,\n#             \"threshold\": clust_threshold,\n#         }\n#     })\n    \n#     # Global metric accumulator\n#     global_metric = DiarizationErrorRate()\n    \n#     for file_id in tqdm(VALIDATION_FILES, desc=f\"Trial {trial.number}\", leave=False):\n#         audio_path = f\"{BASE_DIR}/audio/{file_id}.wav\"\n#         csv_path = f\"{BASE_DIR}/annotation/{file_id}.csv\"\n        \n#         reference = load_ground_truth(csv_path)\n#         # Load audio (assuming load_audio_mono_16k is defined globally)\n#         waveform = load_audio_mono_16k(audio_path)\n#         waveform_tensor = torch.from_numpy(waveform).unsqueeze(0)\n        \n#         try:\n#             hypothesis = diarization_pipeline({\n#                 \"waveform\": waveform_tensor,\n#                 \"sample_rate\": 16000\n#             })\n#         except Exception as e:\n#             print(f\" Failed on {file_id}: {e}\")\n#             return float(\"inf\")\n\n#         # Accumulate into the global metric immediately\n#         # This updates the global stats with the current file\n#         global_metric(reference, hypothesis)\n\n#         # --- Logging per file ---\n#         # Calculate individual file metric just for display/logging\n#         file_metric = DiarizationErrorRate()\n#         file_der = file_metric(reference, hypothesis)\n        \n#         # Save RTTM files\n#         ref_path = os.path.join(trial_dir, f\"{file_id}_reference.rttm\")\n#         hyp_path = os.path.join(trial_dir, f\"{file_id}_hypothesis.rttm\")\n        \n#         with open(ref_path, \"w\") as f:\n#             reference.write_rttm(f)\n#         with open(hyp_path, \"w\") as f:\n#             hypothesis.write_rttm(f)\n            \n#         # Optional: Save per-file CSV\n#         # report_df = file_metric.report(display=False)\n#         # report_df.to_csv(os.path.join(trial_dir, f\"{file_id}_metrics.csv\"))\n        \n#         # Simple logging\n#         components = file_metric.compute_components(reference, hypothesis)\n#         total = sum(components.values()) + 1e-8\n#         f_miss = components[\"missed detection\"] / total\n#         f_fa   = components[\"false alarm\"] / total\n#         f_conf = components[\"confusion\"] / total\n        \n#         print(f\"  > File {file_id}: DER={file_der*100:.2f}% | Miss={f_miss*100:.2f}% | FA={f_fa*100:.2f}% | Conf={f_conf*100:.2f}%\")\n        \n#         # REMOVED: 'break' statement was here, causing the loop to exit after 1 file!\n\n#     # --- Final Calculation ---\n#     # FIX: Use abs() on the metric object to get the aggregated DER directly.\n#     # This avoids the \"float() argument must be... not 'Series'\" error.\n#     global_der = abs(global_metric)\n\n#     # 4. SAVE SUMMARY FOR THE WHOLE TRIAL\n#     final_summary = global_metric.report(display=False)\n#     final_summary.to_csv(os.path.join(trial_dir, \"TRIAL_SUMMARY.csv\"))\n    \n#     print(f\"--- TRIAL SUMMARY ---\")\n#     print(f\"GLOBAL DER: {global_der*100:.2f}%\")\n#     print(\" GLOBAL BEST SO FAR: \")\n#     print_best_so_far(trial.study)\n    \n#     return global_der\n\n# # --- 3. Execute the Hunt ---\n# print(\" Starting Grandmaster Optuna Hunt with File-Level Logging...\")\n\n# study = optuna.create_study(\n#     study_name=\"pyannote_bengali_tuning\", \n#     direction=\"minimize\",\n#     sampler=optuna.samplers.TPESampler(seed=42)\n# )\n\n# n_trials = 30\n# pbar = tqdm(total=n_trials, desc=\"Optimization Progress\")\n\n# def callback(study, trial):\n#     pbar.update(1)\n#     if study.best_value is not None:\n#         pbar.set_description(f\"Best Global DER: {study.best_value*100:.2f}%\")\n\n# try:\n#     study.optimize(objective, n_trials=n_trials, callbacks=[callback])\n# finally:\n#     pbar.close()\n\n# print(\"\\n\" + \"=\"*60)\n# print(f\" FINAL OPTIMIZATION COMPLETE\")\n# print(f\"Best Global DER achieved: {study.best_value * 100:.2f}%\")\n# print(\"-\" * 60)\n# print(\" USE THESE PARAMETERS IN YOUR SUBMISSION:\")\n# for key, value in study.best_params.items():\n#     print(f\"    {key.upper()}: {value:.6f}\")\n# print(\"=\"*60)","metadata":{"execution":{"execution_failed":"2026-02-22T00:35:25.558Z"},"jupyter":{"source_hidden":true},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# import os\n# import optuna\n# import pandas as pd\n# import torch\n# from tqdm.auto import tqdm\n# from pyannote.core import Annotation, Segment\n# from pyannote.metrics.diarization import DiarizationErrorRate\n\n# # --- 1. Helper to load Ground Truth ---\n# def time_str_to_sec(time_str):\n#     h, m, s = time_str.split(':')\n#     return int(h) * 3600 + int(m) * 60 + float(s)\n\n# def load_ground_truth(csv_path):\n#     truth_df = pd.read_csv(csv_path)\n#     reference = Annotation()\n#     for _, row in truth_df.iterrows():\n#         start = time_str_to_sec(row['start_time'])\n#         end = time_str_to_sec(row['end_time'])\n#         spk_id = str(row['speaker_id']).replace(\"SPEAKER_\", \"\").replace(\"speaker_\", \"\").strip()\n#         reference[Segment(start, end)] = spk_id\n#     return reference\n\n# VALIDATION_FILES = [\"train_004\", \"train_009\", \"train_011\"] \n# BASE_DIR = \"/kaggle/input/competitions/dl-sprint-4-0-bengali-speaker-diarization-challenge/diarization/diarization/train\"\n\n# def print_best_so_far(study):\n#     try:\n#         best = study.best_trial\n#     except Exception:\n#         return\n\n#     if best is None or best.value is None:\n#         return\n\n#     params = best.params\n\n#     print(\n#         f\" BEST SO FAR | \"\n#         f\"DER={float(best.value)*100:.2f}% | \"\n#         f\"seg={float(params.get('segmentation_threshold', 0)):.4f}, \"\n#         f\"clust={float(params.get('clustering_threshold', 0)):.4f}, \"\n#         f\"min_cluster={int(params.get('min_cluster_size', 0))}\"\n#     )\n\n\n# # --- 2. Optuna Objective Function ---\n# def objective(trial):\n#     trial_dir = f\"/kaggle/working/trial_{trial.number}\"\n#     os.makedirs(trial_dir, exist_ok=True)\n    \n#     # Suggest hyperparameter values\n#     seg_threshold = trial.suggest_float(\"segmentation_threshold\", 0.3, 0.7)\n#     clust_threshold = trial.suggest_float(\"clustering_threshold\", 0.5, 0.9)\n#     min_cluster_size = trial.suggest_int(\"min_cluster_size\", 10, 30)\n    \n#     print(f\"\\n{'='*20} [Trial {trial.number}] {'='*20}\")\n#     print(f\"PARAMS: Seg_Thresh={seg_threshold:.4f} | Clust_Thresh={clust_threshold:.4f} | Min_Cluster={min_cluster_size}\")\n    \n#     # Instantiate the pipeline with new params\n#     # NOTE: 'diarization_pipeline' must be defined in a previous cell\n#     diarization_pipeline.instantiate({\n#         \"segmentation\": {\n#             \"threshold\": seg_threshold,\n#             \"min_duration_off\": 0.0,\n#         },\n#         \"clustering\": {\n#             \"method\": \"centroid\",\n#             \"min_cluster_size\": min_cluster_size,\n#             \"threshold\": clust_threshold,\n#         }\n#     })\n    \n#     # Global metric accumulator\n#     global_metric = DiarizationErrorRate()\n    \n#     for file_id in tqdm(VALIDATION_FILES, desc=f\"Trial {trial.number}\", leave=False):\n#         audio_path = f\"{BASE_DIR}/audio/{file_id}.wav\"\n#         csv_path = f\"{BASE_DIR}/annotation/{file_id}.csv\"\n        \n#         reference = load_ground_truth(csv_path)\n#         # Load audio (assuming load_audio_mono_16k is defined globally)\n#         waveform = load_audio_mono_16k(audio_path)\n#         waveform_tensor = torch.from_numpy(waveform).unsqueeze(0)\n        \n#         try:\n#             hypothesis = diarization_pipeline({\n#                 \"waveform\": waveform_tensor,\n#                 \"sample_rate\": 16000\n#             })\n#         except Exception as e:\n#             print(f\" Failed on {file_id}: {e}\")\n#             return float(\"inf\")\n\n#         # Accumulate into the global metric immediately\n#         # This updates the global stats with the current file\n#         global_metric(reference, hypothesis)\n\n#         # --- Logging per file ---\n#         # Calculate individual file metric just for display/logging\n#         file_metric = DiarizationErrorRate()\n#         file_der = file_metric(reference, hypothesis)\n        \n#         # Save RTTM files\n#         ref_path = os.path.join(trial_dir, f\"{file_id}_reference.rttm\")\n#         hyp_path = os.path.join(trial_dir, f\"{file_id}_hypothesis.rttm\")\n        \n#         with open(ref_path, \"w\") as f:\n#             reference.write_rttm(f)\n#         with open(hyp_path, \"w\") as f:\n#             hypothesis.write_rttm(f)\n            \n#         # Optional: Save per-file CSV\n#         report_df = file_metric.report(display=False)\n#         report_df.to_csv(os.path.join(trial_dir, f\"{file_id}_metrics.csv\"))\n        \n#         # Simple logging\n#         components = file_metric.compute_components(reference, hypothesis)\n#         total = sum(components.values()) + 1e-8\n#         f_miss = components[\"missed detection\"] / total\n#         f_fa   = components[\"false alarm\"] / total\n#         f_conf = components[\"confusion\"] / total\n        \n#         print(f\"  > File {file_id}: DER={file_der*100:.2f}% | Miss={f_miss*100:.2f}% | FA={f_fa*100:.2f}% | Conf={f_conf*100:.2f}%\")\n#         # break\n#         # REMOVED: 'break' statement was here, causing the loop to exit after 1 file!\n\n#     # --- Final Calculation ---\n#     # FIX: Use abs() on the metric object to get the aggregated DER directly.\n#     # This avoids the \"float() argument must be... not 'Series'\" error.\n#     global_der = abs(global_metric)\n\n#     # 4. SAVE SUMMARY FOR THE WHOLE TRIAL\n#     final_summary = global_metric.report(display=False)\n#     final_summary.to_csv(os.path.join(trial_dir, \"TRIAL_SUMMARY.csv\"))\n    \n#     print(f\"--- TRIAL SUMMARY ---\")\n#     print(f\"GLOBAL DER: {global_der*100:.2f}%\")\n#     print(\" GLOBAL BEST SO FAR: \")\n#     print_best_so_far(trial.study)\n    \n#     return global_der\n\n# # --- 3. Execute the Hunt ---\n# print(\" Starting Grandmaster Optuna Hunt with File-Level Logging...\")\n\n# study = optuna.create_study(\n#     study_name=\"pyannote_bengali_tuning\", \n#     direction=\"minimize\",\n#     sampler=optuna.samplers.TPESampler(seed=42)\n# )\n\n# n_trials = 30\n# pbar = tqdm(total=n_trials, desc=\"Optimization Progress\")\n\n# def callback(study, trial):\n#     pbar.update(1)\n#     if study.best_value is not None:\n#         pbar.set_description(f\"Best Global DER: {study.best_value*100:.2f}%\")\n\n# try:\n#     study.optimize(objective, n_trials=n_trials, callbacks=[callback])\n# finally:\n#     pbar.close()\n\n# print(\"\\n\" + \"=\"*60)\n# print(f\" FINAL OPTIMIZATION COMPLETE\")\n# print(f\"Best Global DER achieved: {study.best_value * 100:.2f}%\")\n# print(\"-\" * 60)\n# print(\" USE THESE PARAMETERS IN YOUR SUBMISSION:\")\n# for key, value in study.best_params.items():\n#     print(f\"    {key.upper()}: {value:.6f}\")\n# print(\"=\"*60)","metadata":{"execution":{"execution_failed":"2026-02-22T00:35:25.558Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## for train_009\n\n✅ Pyannote outputted 15 speakers.\n✅ Pyannote outputted 15 speakers.\nDEBUG: Files saved to /kaggle/working/eval_debug\nGap: 0.6s | Min_Turn: 0.8s | HP_Cut: 80.0Hz | NR: 0.0 | ONSET: 0.35 | OFFSET: 0.35\n -> FA: 18.0% | Miss: 4.8% | Conf: 15.2%\n -> DER: 37.9% | KAGGLE SCORE: 62.08274\n\n \n✅ Pyannote outputted 15 speakers.\n✅ Pyannote outputted 15 speakers.\nDEBUG: Files saved to /kaggle/working/eval_debug\nGap: 0.4s | Min_Turn: 0.8s | HP_Cut: 80.0Hz | NR: 0.0 | ONSET: 0.35 | OFFSET: 0.35\n -> FA: 17.2% | Miss: 5.4% | Conf: 14.9%\n -> DER: 37.6% | KAGGLE SCORE: 62.44990\n\n✅ Pyannote outputted 15 speakers.\n✅ Pyannote outputted 15 speakers.\nDEBUG: Files saved to /kaggle/working/eval_debug\nGap: 0.5s | Min_Turn: 0.8s | HP_Cut: 50.0Hz | NR: 0.0 | ONSET: 0.45 | OFFSET: 0.45\n -> FA: 17.3% | Miss: 5.3% | Conf: 14.9%\n -> DER: 37.6% | KAGGLE SCORE: 62.40918\n\n✅ Pyannote outputted 15 speakers.\n✅ Pyannote outputted 15 speakers.\nDEBUG: Files saved to /kaggle/working/eval_debug\nGap: 0.3s | Min_Turn: 0.9s | HP_Cut: 35.0Hz | NR: 0.0 | ONSET: 0.3 | OFFSET: 0.3\n -> FA: 17.0% | Miss: 5.6% | Conf: 14.8%\n -> DER: 37.4% | KAGGLE SCORE: 62.55591\n\n\n\n\n ## for train_011\n\n ✅ Pyannote outputted 13 speakers.\n✅ Pyannote outputted 13 speakers.\nDEBUG: Files saved to /kaggle/working/eval_debug\nGap: 0.3s | Min_Turn: 0.9s | HP_Cut: 0.0Hz | NR: 0.0 | ONSET: 0.0 | OFFSET: 0.0\n -> FA: 7.9% | Miss: 4.4% | Conf: 15.5%\n -> DER: 27.7% | KAGGLE SCORE: 72.28285\n\n✅ Pyannote outputted 13 speakers.\n✅ Pyannote outputted 13 speakers.\nDEBUG: Files saved to /kaggle/working/eval_debug\nGap: 0.3s | Min_Turn: 0.8s | HP_Cut: 0.0Hz | NR: 0.0 | ONSET: 0.0 | OFFSET: 0.0\n -> FA: 7.9% | Miss: 4.4% | Conf: 15.5%\n -> DER: 27.7% | KAGGLE SCORE: 72.27392\n\n✅ Pyannote outputted 13 speakers.\n✅ Pyannote outputted 13 speakers.\nDEBUG: Files saved to /kaggle/working/eval_debug\nGap: 0.3s | Min_Turn: 1.0s | HP_Cut: 0.0Hz | NR: 0.0 | ONSET: 0.0 | OFFSET: 0.0\n -> FA: 7.9% | Miss: 4.4% | Conf: 15.5%\n -> DER: 27.7% | KAGGLE SCORE: 72.29344\n\n✅ Pyannote outputted 13 speakers.\n✅ Pyannote outputted 13 speakers.\nDEBUG: Files saved to /kaggle/working/eval_debug\nGap: 0.4s | Min_Turn: 0.9s | HP_Cut: 0.0Hz | NR: 0.0 | ONSET: 0.0 | OFFSET: 0.0\n -> FA: 8.0% | Miss: 4.3% | Conf: 15.5%\n -> DER: 27.8% | KAGGLE SCORE: 72.20753\n\n✅ Pyannote outputted 13 speakers.\n✅ Pyannote outputted 13 speakers.\nDEBUG: Files saved to /kaggle/working/eval_debug\nGap: 0.4s | Min_Turn: 0.8s | HP_Cut: 0.0Hz | NR: 0.0 | ONSET: 0.0 | OFFSET: 0.0\n -> FA: 8.0% | Miss: 4.3% | Conf: 15.6%\n -> DER: 27.8% | KAGGLE SCORE: 72.19861\n\n✅ Pyannote outputted 13 speakers.\n✅ Pyannote outputted 13 speakers.\nDEBUG: Files saved to /kaggle/working/eval_debug\nGap: 0.4s | Min_Turn: 1.0s | HP_Cut: 0.0Hz | NR: 0.0 | ONSET: 0.0 | OFFSET: 0.0\n -> FA: 8.0% | Miss: 4.3% | Conf: 15.5%\n -> DER: 27.8% | KAGGLE SCORE: 72.21813\n\n✅ Pyannote outputted 13 speakers.\n✅ Pyannote outputted 13 speakers.\nDEBUG: Files saved to /kaggle/working/eval_debug\nGap: 0.2s | Min_Turn: 0.9s | HP_Cut: 0.0Hz | NR: 0.0 | ONSET: 0.0 | OFFSET: 0.0\n -> FA: 7.8% | Miss: 4.5% | Conf: 15.4%\n -> DER: 27.7% | KAGGLE SCORE: 72.29930\n\n✅ Pyannote outputted 13 speakers.\n✅ Pyannote outputted 13 speakers.\nDEBUG: Files saved to /kaggle/working/eval_debug\nGap: 0.2s | Min_Turn: 0.8s | HP_Cut: 0.0Hz | NR: 0.0 | ONSET: 0.0 | OFFSET: 0.0\n -> FA: 7.8% | Miss: 4.5% | Conf: 15.4%\n -> DER: 27.7% | KAGGLE SCORE: 72.29038\n\n✅ Pyannote outputted 13 speakers.\n✅ Pyannote outputted 13 speakers.\nDEBUG: Files saved to /kaggle/working/eval_debug\nGap: 0.2s | Min_Turn: 1.0s | HP_Cut: 0.0Hz | NR: 0.0 | ONSET: 0.0 | OFFSET: 0.0\n -> FA: 7.8% | Miss: 4.5% | Conf: 15.4%\n -> DER: 27.7% | KAGGLE SCORE: 72.30990\n\n\n### audio loader improvement 1\nStarting Parameter Search on train_011...\n\n✅ Pyannote outputted 14 speakers.\n✅ Pyannote outputted 14 speakers.\nDEBUG: Files saved to /kaggle/working/eval_debug\nGap: 0.3s | Min_Turn: 1.0s | HP_Cut: 0.0Hz | NR: 0.0 | ONSET: 0.0 | OFFSET: 0.0\n -> FA: 8.1% | Miss: 3.9% | Conf: 15.4%\n -> DER: 27.4% | KAGGLE SCORE: 72.61590\n\n✅ Pyannote outputted 14 speakers.\n✅ Pyannote outputted 14 speakers.\nDEBUG: Files saved to /kaggle/working/eval_debug\nGap: 0.2s | Min_Turn: 1.0s | HP_Cut: 0.0Hz | NR: 0.0 | ONSET: 0.0 | OFFSET: 0.0\n -> FA: 7.9% | Miss: 4.0% | Conf: 15.4%\n -> DER: 27.3% | KAGGLE SCORE: 72.69372\n\n\n\n\n \n\n\n\n ## for train_004\n\n ✅ Pyannote outputted 10 speakers.\n✅ Pyannote outputted 10 speakers.\nDEBUG: Files saved to /kaggle/working/eval_debug\nGap: 0.2s | Min_Turn: 0.9s | HP_Cut: 0.0Hz | NR: 0.0 | ONSET: 0.0 | OFFSET: 0.0\n -> FA: 2.7% | Miss: 0.8% | Conf: 2.8%\n -> DER: 6.3% | KAGGLE SCORE: 93.71659\n\n✅ Pyannote outputted 10 speakers.\n✅ Pyannote outputted 10 speakers.\nDEBUG: Files saved to /kaggle/working/eval_debug\nGap: 0.2s | Min_Turn: 1.0s | HP_Cut: 0.0Hz | NR: 0.0 | ONSET: 0.0 | OFFSET: 0.0\n -> FA: 2.7% | Miss: 0.8% | Conf: 2.8%\n -> DER: 6.3% | KAGGLE SCORE: 93.71659\n\n✅ Pyannote outputted 10 speakers.\n✅ Pyannote outputted 10 speakers.\nDEBUG: Files saved to /kaggle/working/eval_debug\nGap: 0.3s | Min_Turn: 0.9s | HP_Cut: 0.0Hz | NR: 0.0 | ONSET: 0.0 | OFFSET: 0.0\n -> FA: 2.7% | Miss: 0.7% | Conf: 2.8%\n -> DER: 6.3% | KAGGLE SCORE: 93.72606\n\n✅ Pyannote outputted 10 speakers.\n✅ Pyannote outputted 10 speakers.\nDEBUG: Files saved to /kaggle/working/eval_debug\nGap: 0.3s | Min_Turn: 1.0s | HP_Cut: 0.0Hz | NR: 0.0 | ONSET: 0.0 | OFFSET: 0.0\n -> FA: 2.7% | Miss: 0.7% | Conf: 2.8%\n -> DER: 6.3% | KAGGLE SCORE: 93.72606\n\n### overengineered load\n\nStarting Parameter Search on train_004...\n\nusing overengi\n✅ Pyannote outputted 11 speakers.\n✅ Pyannote outputted 11 speakers.\nDEBUG: Files saved to /kaggle/working/eval_debug\nGap: 0.3s | Min_Turn: 1.0s | HP_Cut: 0.0Hz | NR: 0.0 | ONSET: 0.0 | OFFSET: 0.0\n -> FA: 2.8% | Miss: 0.7% | Conf: 2.9%\n -> DER: 6.4% | KAGGLE SCORE: 93.55709\n\n### gpt opt\nStarting Parameter Search on train_004...\n\n✅ Pyannote outputted 9 speakers.\n✅ Pyannote outputted 9 speakers.\nDEBUG: Files saved to /kaggle/working/eval_debug\nGap: 0.3s | Min_Turn: 1.0s | HP_Cut: 0.0Hz | NR: 0.0 | ONSET: 0.0 | OFFSET: 0.0\n -> FA: 2.7% | Miss: 0.7% | Conf: 2.7%\n -> DER: 6.2% | KAGGLE SCORE: 93.84204\n\n✅ Pyannote outputted 9 speakers.\n✅ Pyannote outputted 9 speakers.\nDEBUG: Files saved to /kaggle/working/eval_debug\nGap: 0.2s | Min_Turn: 1.0s | HP_Cut: 0.0Hz | NR: 0.0 | ONSET: 0.0 | OFFSET: 0.0\n -> FA: 2.7% | Miss: 0.7% | Conf: 2.7%\n -> DER: 6.1% | KAGGLE SCORE: 93.88479\n\n✅ Pyannote outputted 9 speakers.\n✅ Pyannote outputted 9 speakers.\nDEBUG: Files saved to /kaggle/working/eval_debug\nGap: 0.3s | Min_Turn: 0.9s | HP_Cut: 0.0Hz | NR: 0.0 | ONSET: 0.0 | OFFSET: 0.0\n -> FA: 2.7% | Miss: 0.7% | Conf: 2.8%\n -> DER: 6.2% | KAGGLE SCORE: 93.82898\n\n✅ Pyannote outputted 9 speakers.\n✅ Pyannote outputted 9 speakers.\nDEBUG: Files saved to /kaggle/working/eval_debug\nGap: 0.3s | Min_Turn: 1.0s | HP_Cut: 0.0Hz | NR: 0.0 | ONSET: 0.0 | OFFSET: 0.0\n -> FA: 2.7% | Miss: 0.7% | Conf: 2.7%\n -> DER: 6.2% | KAGGLE SCORE: 93.84204\n\n🏆 Best Score Found: 93.88\n ","metadata":{}}]}