{"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":[{"sourceId":129271,"databundleVersionId":15506307,"sourceType":"competition"}],"dockerImageVersionId":31260,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"!pip install --upgrade --upgrade-strategy only-if-needed soundfile torchaudio speechbrain tqdm silero-vad\n!pip install --upgrade --upgrade-strategy only-if-needed git+https://github.com/wenet-e2e/wespeaker.git\n!pip install --upgrade --upgrade-strategy only-if-needed onnxruntime-gpu\n!pip install silero-vad\n","metadata":{"_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","execution":{"iopub.status.busy":"2026-02-10T22:22:35.894039Z","iopub.execute_input":"2026-02-10T22:22:35.894906Z","iopub.status.idle":"2026-02-10T22:30:12.49818Z","shell.execute_reply.started":"2026-02-10T22:22:35.894873Z","shell.execute_reply":"2026-02-10T22:30:12.497392Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import json\nfrom pathlib import Path\n\nimport numpy as np\nimport pandas as pd\nimport soundfile as sf\nimport torch\nimport torchaudio\nfrom tqdm.auto import tqdm\n\nfrom sklearn.cluster import AgglomerativeClustering\nfrom sklearn.metrics import silhouette_score\nfrom sklearn.preprocessing import normalize\n\nimport matplotlib.pyplot as plt","metadata":{"execution":{"iopub.status.busy":"2026-02-10T22:30:12.500134Z","iopub.execute_input":"2026-02-10T22:30:12.50064Z","iopub.status.idle":"2026-02-10T22:30:12.505128Z","shell.execute_reply.started":"2026-02-10T22:30:12.500612Z","shell.execute_reply":"2026-02-10T22:30:12.504376Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Configuration: Choose embedding model\nUSE_RESNET293 = True  # Set to False to use ECAPA-TDNN\nDEVICE = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n\n# Define paths\nAUDIO_DIR = Path(\"/kaggle/input/dl-sprint-4-0-bengali-speaker-diarization-challenge/diarization/diarization/test/audio\")    \nOUT_PATH = \"/kaggle/working/submission.csv\"  # Update this path\n\nprint(f\"Device: {DEVICE}\")\nprint(f\"Using ResNet-293: {USE_RESNET293}\")","metadata":{"execution":{"iopub.status.busy":"2026-02-10T22:30:12.506193Z","iopub.execute_input":"2026-02-10T22:30:12.506458Z","iopub.status.idle":"2026-02-10T22:30:12.5238Z","shell.execute_reply.started":"2026-02-10T22:30:12.506437Z","shell.execute_reply":"2026-02-10T22:30:12.523024Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Download ResNet-293-LM model (for Kaggle/Linux environments)\nimport os\n\nif USE_RESNET293:\n    os.makedirs(\"/kaggle/working/resnet293_LM\", exist_ok=True)\n    \n    !wget -P /kaggle/working/resnet293_LM \\\n        https://huggingface.co/Wespeaker/wespeaker-voxceleb-resnet293-LM/resolve/main/voxceleb_resnet293_LM.onnx\n    \n    !wget -P /kaggle/working/resnet293_LM \\\n        https://huggingface.co/Wespeaker/wespeaker-voxceleb-resnet293-LM/resolve/main/config.yaml\n    \n    print(\"ResNet-293-LM model files downloaded\")","metadata":{"execution":{"iopub.status.busy":"2026-02-10T22:30:12.525275Z","iopub.execute_input":"2026-02-10T22:30:12.525776Z","iopub.status.idle":"2026-02-10T22:30:13.597208Z","shell.execute_reply.started":"2026-02-10T22:30:12.525744Z","shell.execute_reply":"2026-02-10T22:30:13.596319Z"},"trusted":true},"outputs":[],"execution_count":null},{"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-10T22:30:13.598442Z","iopub.execute_input":"2026-02-10T22:30:13.598765Z","iopub.status.idle":"2026-02-10T22:30:13.60969Z","shell.execute_reply.started":"2026-02-10T22:30:13.598728Z","shell.execute_reply":"2026-02-10T22:30:13.608992Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def load_audio_mono_16k(path: str | Path, target_sr: int = 16000) -> np.ndarray:\n    \"\"\"Load audio file as mono float32 at target_sr.\"\"\"\n    wav, sr = sf.read(str(path))\n    if wav.ndim == 2:\n        wav = wav.mean(axis=1)\n    wav = wav.astype(np.float32)\n\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).cpu().numpy().astype(np.float32)\n    return wav","metadata":{"execution":{"iopub.status.busy":"2026-02-10T22:30:13.610493Z","iopub.execute_input":"2026-02-10T22:30:13.610788Z","iopub.status.idle":"2026-02-10T22:30:13.616003Z","shell.execute_reply.started":"2026-02-10T22:30:13.610766Z","shell.execute_reply":"2026-02-10T22:30:13.615347Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"USE_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()","metadata":{"execution":{"iopub.status.busy":"2026-02-10T22:30:13.616875Z","iopub.execute_input":"2026-02-10T22:30:13.617208Z","iopub.status.idle":"2026-02-10T22:30:13.766945Z","shell.execute_reply.started":"2026-02-10T22:30:13.617179Z","shell.execute_reply":"2026-02-10T22:30:13.76638Z"},"trusted":true},"outputs":[],"execution_count":null},{"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]","metadata":{"execution":{"iopub.status.busy":"2026-02-10T22:30:13.767773Z","iopub.execute_input":"2026-02-10T22:30:13.76805Z","iopub.status.idle":"2026-02-10T22:30:13.774039Z","shell.execute_reply.started":"2026-02-10T22:30:13.768028Z","shell.execute_reply":"2026-02-10T22:30:13.773497Z"},"trusted":true},"outputs":[],"execution_count":null},{"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\nif 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}\")","metadata":{"execution":{"iopub.status.busy":"2026-02-10T22:30:13.774783Z","iopub.execute_input":"2026-02-10T22:30:13.775044Z","iopub.status.idle":"2026-02-10T22:31:22.602951Z","shell.execute_reply.started":"2026-02-10T22:30:13.775024Z","shell.execute_reply":"2026-02-10T22:31:22.602348Z"},"trusted":true},"outputs":[],"execution_count":null},{"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-10T22:31:22.605023Z","iopub.execute_input":"2026-02-10T22:31:22.605252Z","iopub.status.idle":"2026-02-10T22:31:22.610983Z","shell.execute_reply.started":"2026-02-10T22:31:22.60523Z","shell.execute_reply":"2026-02-10T22:31:22.610372Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Load speaker embedding model based on configuration\nif USE_RESNET293:\n    import onnxruntime as ort\n    \n    onnx_path = \"/kaggle/working/resnet293_LM/voxceleb_resnet293_LM.onnx\"\n    \n    resnet_session = ort.InferenceSession(\n        onnx_path,\n        providers=[\"CUDAExecutionProvider\", \"CPUExecutionProvider\"]\n    )\n    \n    print(\"ResNet-293-LM ONNX model loaded\")\nelse:\n    from speechbrain.pretrained import EncoderClassifier\n\n    spk_model = EncoderClassifier.from_hparams(\n        source=\"speechbrain/spkrec-ecapa-voxceleb\",\n        run_opts={\"device\": DEVICE},\n    )\n    print(\"ECAPA-TDNN model loaded\")","metadata":{"execution":{"iopub.status.busy":"2026-02-10T22:31:22.611811Z","iopub.execute_input":"2026-02-10T22:31:22.612076Z","iopub.status.idle":"2026-02-10T22:31:24.047026Z","shell.execute_reply.started":"2026-02-10T22:31:22.612054Z","shell.execute_reply":"2026-02-10T22:31:24.046239Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"@torch.inference_mode()\ndef ecapa_embedding(wav16k: np.ndarray) -> np.ndarray:\n    \"\"\"\n    wav16k: mono float32, 16kHz\n    returns: (D,) numpy embedding using ECAPA-TDNN\n    \"\"\"\n    x = torch.from_numpy(wav16k).unsqueeze(0).to(DEVICE)\n    emb = spk_model.encode_batch(x)\n    return emb.squeeze().detach().cpu().numpy().astype(np.float32)\n\n\n@torch.inference_mode()\ndef resnet293_embedding(wav16k: np.ndarray) -> np.ndarray:\n    \"\"\"\n    wav16k: mono float32, 16kHz\n    returns: (D,) numpy embedding using ResNet-293-LM\n    \"\"\"\n    # Compute 80-dimensional mel-filterbank features\n    wav_tensor = torch.from_numpy(wav16k).unsqueeze(0)  # (1, time)\n    \n    # Create mel-spectrogram: 80 mel bins\n    mel_transform = torchaudio.transforms.MelSpectrogram(\n        sample_rate=16000,\n        n_fft=400,\n        win_length=400,\n        hop_length=160,\n        n_mels=80,\n    )\n    \n    mel_spec = mel_transform(wav_tensor)  # (1, 80, time_frames)\n    \n    # Log scale\n    mel_spec = torch.log(mel_spec + 1e-6)\n    \n    # Transpose to (1, time_frames, 80) as expected by ResNet-293\n    mel_spec = mel_spec.transpose(1, 2)  # (1, time_frames, 80)\n    \n    # Convert to numpy\n    feats = mel_spec.cpu().numpy().astype(np.float32)\n    \n    # Run ONNX inference\n    ort_inputs = {resnet_session.get_inputs()[0].name: feats}\n    embedding = resnet_session.run(None, ort_inputs)[0]\n    \n    return embedding.squeeze().astype(np.float32)\n\n\n# Select embedding function based on configuration\nif USE_RESNET293:\n    embedding_fn = resnet293_embedding\n    print(\"Using ResNet-293-LM for embeddings\")\nelse:\n    embedding_fn = ecapa_embedding\n    print(\"Using ECAPA-TDNN for embeddings\")","metadata":{"execution":{"iopub.status.busy":"2026-02-10T22:31:24.048045Z","iopub.execute_input":"2026-02-10T22:31:24.048276Z","iopub.status.idle":"2026-02-10T22:31:24.055722Z","shell.execute_reply.started":"2026-02-10T22:31:24.048253Z","shell.execute_reply":"2026-02-10T22:31:24.055039Z"},"trusted":true},"outputs":[],"execution_count":null},{"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-10T22:31:24.056453Z","iopub.execute_input":"2026-02-10T22:31:24.056696Z","iopub.status.idle":"2026-02-10T22:31:24.06933Z","shell.execute_reply.started":"2026-02-10T22:31:24.056664Z","shell.execute_reply":"2026-02-10T22:31:24.068835Z"},"trusted":true},"outputs":[],"execution_count":null},{"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-10T22:31:24.070088Z","iopub.execute_input":"2026-02-10T22:31:24.070261Z","iopub.status.idle":"2026-02-10T22:31:24.084725Z","shell.execute_reply.started":"2026-02-10T22:31:24.070243Z","shell.execute_reply":"2026-02-10T22:31:24.084128Z"},"trusted":true},"outputs":[],"execution_count":null},{"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()","metadata":{"execution":{"iopub.status.busy":"2026-02-10T22:31:24.085553Z","iopub.execute_input":"2026-02-10T22:31:24.085912Z","iopub.status.idle":"2026-02-10T22:31:24.096256Z","shell.execute_reply.started":"2026-02-10T22:31:24.085882Z","shell.execute_reply":"2026-02-10T22:31:24.095614Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ==================== TRAINING SETUP ====================\n# Load training data\nTRAIN_AUDIO_DIR = Path(\"/kaggle/input/dl-sprint-4-0-bengali-speaker-diarization-challenge/diarization/diarization/train/audio\")\nTRAIN_ANNOTATION_DIR = Path(\"/kaggle/input/dl-sprint-4-0-bengali-speaker-diarization-challenge/diarization/diarization/train/annotation\")\n\ntrain_wav_paths = sorted(TRAIN_AUDIO_DIR.glob(\"*.wav\"))\nprint(train_wav_paths)\nprint(f\"Found {len(train_wav_paths)} training audio files\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-10T22:31:24.097127Z","iopub.execute_input":"2026-02-10T22:31:24.097381Z","iopub.status.idle":"2026-02-10T22:31:24.116072Z","shell.execute_reply.started":"2026-02-10T22:31:24.09736Z","shell.execute_reply":"2026-02-10T22:31:24.115582Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# # Load annotation files\n# import json\n\n# def load_annotation(json_path):\n#     \"\"\"Load annotation JSON file\"\"\"\n#     with open(json_path, 'r') as f:\n#         return json.load(f)\n\n# annotations = {}\n# for json_file in TRAIN_ANNOTATION_DIR.glob(\"*.json\"):\n#     stem = json_file.stem\n#     annotations[stem] = load_annotation(json_file)\n\n# print(f\"Loaded {len(annotations)} annotation files\")\n# print(f\"Sample annotation keys: {list(annotations.keys())[:3]}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-10T22:31:24.116834Z","iopub.execute_input":"2026-02-10T22:31:24.117075Z","iopub.status.idle":"2026-02-10T22:31:24.120792Z","shell.execute_reply.started":"2026-02-10T22:31:24.117055Z","shell.execute_reply":"2026-02-10T22:31:24.120055Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\n\ndef load_annotation_csv(csv_path):\n    df = pd.read_csv(csv_path)\n\n    # Normalize column names\n    df.columns = [c.strip().lower() for c in df.columns]\n\n    # Handle \"speaker id\" with space\n    if \"speaker id\" in df.columns:\n        df = df.rename(columns={\"speaker id\": \"speaker_id\"})\n\n    required = {\"start_time\", \"end_time\", \"speaker_id\"}\n    missing = required - set(df.columns)\n    if missing:\n        raise ValueError(\n            f\"{csv_path.name} missing columns: {missing}. \"\n            f\"Found: {list(df.columns)}\"\n        )\n\n    # Type safety\n    df[\"start_time\"] = pd.to_numeric(df[\"start_time\"], errors=\"coerce\")\n    df[\"end_time\"]   = pd.to_numeric(df[\"end_time\"], errors=\"coerce\")\n\n    # Clean rows\n    df = df.dropna(subset=[\"start_time\", \"end_time\", \"speaker_id\"])\n    df = df[df[\"end_time\"] > df[\"start_time\"]]\n    df = df.sort_values(\"start_time\")\n\n    return df[[\"start_time\", \"end_time\", \"speaker_id\"]].to_dict(\"records\")\n\nannotations = {}\nfor csv_file in TRAIN_ANNOTATION_DIR.glob(\"*.csv\"):\n    stem = csv_file.stem\n    annotations[stem] = load_annotation_csv(csv_file)\n\nprint(f\"Loaded {len(annotations)} annotation files\")\nprint(f\"Sample annotation keys: {list(annotations.keys())[:3]}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-10T22:57:40.17286Z","iopub.execute_input":"2026-02-10T22:57:40.173548Z","iopub.status.idle":"2026-02-10T22:57:40.247308Z","shell.execute_reply.started":"2026-02-10T22:57:40.173518Z","shell.execute_reply":"2026-02-10T22:57:40.246751Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Extract embeddings and labels from training data\nfrom sklearn.preprocessing import LabelEncoder\n\ntrain_embeddings = []\ntrain_labels = []\ntrain_speaker_map = {}  # Map speaker names to speaker IDs\n\nfor wav_path in tqdm(train_wav_paths, desc=\"Extracting training embeddings\"):\n    stem = wav_path.stem\n    print(\"wav path is \",wav_path)\n    \n    \n    if stem not in annotations:\n        print(f\"Warning: No annotation for {stem}\")\n        continue\n    \n    # Load audio\n    wav = load_audio_mono_16k(wav_path, 16000)\n\n    print(\"shape:\", wav.shape, \"dtype:\", wav.dtype)\n    print(\"min/max:\", float(np.min(wav)), float(np.max(wav)))\n    print(\"mean:\", float(np.mean(wav)))\n    print(\"nonzero count:\", int(np.count_nonzero(wav)))\n    print(\"max abs:\", float(np.max(np.abs(wav))))\n    # print(\"wav is \", wav)\n    \n    # Get VAD segments\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    annotation = annotations[stem]\n    \n    # For each window, get the corresponding speaker label from annotation\n    for ws, we in windows:\n        s = int(ws * 16000)\n        e = int(we * 16000)\n        chunk = wav[s:e]\n        \n        if len(chunk) < int(0.4 * 16000):  # Skip too-short windows\n            continue\n        \n        # Find which speaker this window belongs to\n        window_mid = (ws + we) / 2  # Use midpoint of window\n        \n        speaker_id = None\n        for turn in annotation:\n            if turn['start_time'] <= window_mid <= turn['end_time']:\n                speaker_id = turn['speaker_id']\n                break\n        \n        if speaker_id is None:\n            continue  # Window doesn't have a speaker label\n        \n        # Extract embedding\n        emb = embedding_fn(chunk)\n        train_embeddings.append(emb)\n        train_labels.append(speaker_id)\n        \n        # Track speaker mapping\n        if speaker_id not in train_speaker_map:\n            train_speaker_map[speaker_id] = len(train_speaker_map)\n\n# Convert to arrays\nX_train = np.vstack(train_embeddings) if train_embeddings else np.array([])\nprint(\"X train is \", X_train)\ntrain_labels_raw = np.array(train_labels)\n\n# Encode labels\nlabel_encoder = LabelEncoder()\ny_train = label_encoder.fit_transform(train_labels_raw)\n\nprint(f\"Extracted {len(X_train)} training embeddings\")\nprint(f\"Number of unique speakers: {len(label_encoder.classes_)}\")\nprint(f\"Class distribution: {np.bincount(y_train)}\")\nprint(f\"Embedding dimension: {X_train.shape[1]}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-10T23:28:36.129385Z","iopub.status.idle":"2026-02-10T23:28:36.129688Z","shell.execute_reply.started":"2026-02-10T23:28:36.129526Z","shell.execute_reply":"2026-02-10T23:28:36.129542Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Train outer layer (classification head) on top of frozen embeddings\nfrom sklearn.linear_model import LogisticRegression\nfrom sklearn.metrics import accuracy_score, f1_score\n\n# Option 1: Logistic Regression (simple, fast)\nprint(\"Training Logistic Regression classifier...\")\nclassifier = LogisticRegression(\n    max_iter=1000,\n    solver='lbfgs',\n    multi_class='multinomial',\n    random_state=42,\n    n_jobs=-1\n)\n\nclassifier.fit(X_train, y_train)\n\n# Evaluate on training data\ny_pred = classifier.predict(X_train)\ntrain_acc = accuracy_score(y_train, y_pred)\ntrain_f1 = f1_score(y_train, y_pred, average='weighted', zero_division=0)\n\nprint(f\"Training Accuracy: {train_acc:.4f}\")\nprint(f\"Training F1 Score: {train_f1:.4f}\")\nprint(f\"Classifier trained successfully!\")\n\n# Save the trained classifier\nimport pickle\nclassifier_path = \"/kaggle/working/speaker_classifier.pkl\"\nwith open(classifier_path, 'wb') as f:\n    pickle.dump({\n        'classifier': classifier,\n        'label_encoder': label_encoder,\n        'embedding_model': 'ResNet-293-LM' if USE_RESNET293 else 'ECAPA-TDNN'\n    }, f)\nprint(f\"Classifier saved to {classifier_path}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-10T23:07:24.55352Z","iopub.status.idle":"2026-02-10T23:07:24.553829Z","shell.execute_reply.started":"2026-02-10T23:07:24.55369Z","shell.execute_reply":"2026-02-10T23:07:24.553706Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Train with PyTorch Neural Network (more powerful)\nimport torch.nn as nn\nfrom torch.utils.data import TensorDataset, DataLoader\nfrom torch.optim import Adam\n\nclass SpeakerClassifier(nn.Module):\n    def __init__(self, input_dim, num_classes, hidden_dim=256):\n        super().__init__()\n        self.fc1 = nn.Linear(input_dim, hidden_dim)\n        self.dropout1 = nn.Dropout(0.3)\n        self.fc2 = nn.Linear(hidden_dim, hidden_dim // 2)\n        self.dropout2 = nn.Dropout(0.3)\n        self.fc3 = nn.Linear(hidden_dim // 2, num_classes)\n        self.relu = nn.ReLU()\n    \n    def forward(self, x):\n        x = self.relu(self.fc1(x))\n        x = self.dropout1(x)\n        x = self.relu(self.fc2(x))\n        x = self.dropout2(x)\n        x = self.fc3(x)\n        return x\n\n# Prepare data for PyTorch\nX_train_torch = torch.from_numpy(X_train).float()\ny_train_torch = torch.from_numpy(y_train).long()\n\ndataset = TensorDataset(X_train_torch, y_train_torch)\ntrain_loader = DataLoader(dataset, batch_size=32, shuffle=True)\n\n# Initialize model\ninput_dim = X_train.shape[1]\nnum_classes = len(label_encoder.classes_)\nnn_classifier = SpeakerClassifier(input_dim, num_classes).to(DEVICE)\n\n# Training setup\ncriterion = nn.CrossEntropyLoss()\noptimizer = Adam(nn_classifier.parameters(), lr=0.001)\nepochs = 50\n\nprint(f\"Training Neural Network classifier...\")\nprint(f\"Input dimension: {input_dim}, Output classes: {num_classes}\")\n\nlosses = []\nfor epoch in range(epochs):\n    total_loss = 0\n    for batch_x, batch_y in train_loader:\n        batch_x, batch_y = batch_x.to(DEVICE), batch_y.to(DEVICE)\n        \n        optimizer.zero_grad()\n        outputs = nn_classifier(batch_x)\n        loss = criterion(outputs, batch_y)\n        loss.backward()\n        optimizer.step()\n        \n        total_loss += loss.item()\n    \n    avg_loss = total_loss / len(train_loader)\n    losses.append(avg_loss)\n    \n    if (epoch + 1) % 10 == 0:\n        print(f\"Epoch {epoch + 1}/{epochs}, Loss: {avg_loss:.4f}\")\n\n# Evaluate\nnn_classifier.eval()\nwith torch.no_grad():\n    outputs = nn_classifier(X_train_torch.to(DEVICE))\n    _, predicted = torch.max(outputs, 1)\n    nn_train_acc = accuracy_score(y_train, predicted.cpu().numpy())\n    nn_train_f1 = f1_score(y_train, predicted.cpu().numpy(), average='weighted', zero_division=0)\n\nprint(f\"NN Training Accuracy: {nn_train_acc:.4f}\")\nprint(f\"NN Training F1 Score: {nn_train_f1:.4f}\")\n\n# Save the neural network classifier\nnn_classifier_path = \"/kaggle/working/speaker_classifier_nn.pt\"\ntorch.save({\n    'model_state_dict': nn_classifier.state_dict(),\n    'label_encoder': label_encoder,\n    'input_dim': input_dim,\n    'num_classes': num_classes,\n    'embedding_model': 'ResNet-293-LM' if USE_RESNET293 else 'ECAPA-TDNN'\n}, nn_classifier_path)\nprint(f\"Neural network classifier saved to {nn_classifier_path}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-10T23:07:24.555164Z","iopub.status.idle":"2026-02-10T23:07:24.555518Z","shell.execute_reply.started":"2026-02-10T23:07:24.555338Z","shell.execute_reply":"2026-02-10T23:07:24.55536Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Plot training loss\nplt.figure(figsize=(10, 4))\nplt.plot(losses, linewidth=2)\nplt.xlabel('Epoch')\nplt.ylabel('Loss')\nplt.title('Neural Network Training Loss')\nplt.grid(True)\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-10T23:07:24.556935Z","iopub.status.idle":"2026-02-10T23:07:24.557231Z","shell.execute_reply.started":"2026-02-10T23:07:24.557087Z","shell.execute_reply":"2026-02-10T23:07:24.557104Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# --- One-file debug run: compute embs + labels for visualization ---\ntest_path = wav_paths[0]\n\nwav = load_audio_mono_16k(test_path, 16000)\nspeech_segs = silero_vad_speech_segments(wav, sr=16000)\nwindows = make_windows(speech_segs, win_sec=1.5, hop_sec=0.75)\n\nembs = []\nvalid_windows = []\nfor 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(embedding_fn(chunk))\n    valid_windows.append((ws, we))\n\n# Dynamically determine embedding dimension from first embedding\nemb_dim = embs[0].shape[0] if len(embs) > 0 else (256 if USE_RESNET293 else 192)\nembs = np.vstack(embs) if len(embs) else np.empty((0, emb_dim), dtype=np.float32)\n\nn_spk = estimate_num_speakers(embs, max_k=6) if len(embs) else 1\nlabels = cluster_embeddings(embs, n_speakers=n_spk) if len(embs) else np.array([], dtype=int)\n\nlen(embs), len(labels), n_spk","metadata":{"execution":{"iopub.status.busy":"2026-02-10T23:07:24.558259Z","iopub.status.idle":"2026-02-10T23:07:24.558512Z","shell.execute_reply.started":"2026-02-10T23:07:24.558389Z","shell.execute_reply":"2026-02-10T23:07:24.558408Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model_name = \"ResNet-293-LM\" if USE_RESNET293 else \"ECAPA-TDNN\"\nplot_embedding_pca(embs, labels, title=f\"{model_name} embeddings clustered\")","metadata":{"execution":{"iopub.status.busy":"2026-02-10T12:38:29.987727Z","iopub.status.idle":"2026-02-10T12:38:29.988091Z","shell.execute_reply.started":"2026-02-10T12:38:29.987911Z","shell.execute_reply":"2026-02-10T12:38:29.987933Z"},"trusted":true},"outputs":[],"execution_count":null},{"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-10T12:38:29.989386Z","iopub.status.idle":"2026-02-10T12:38:29.989641Z","shell.execute_reply.started":"2026-02-10T12:38:29.989529Z","shell.execute_reply":"2026-02-10T12:38:29.989543Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def diarize_file(\n    audio_path: str | Path,\n    # --- ML VAD knobs ---\n    vad_threshold: float = 0.5,\n    padding_ms: int = 300,\n    min_silence_ms: int = 200,\n    min_speech_ms: int = 300,\n    # --- windowing knobs ---\n    win_sec: float = 1.5,\n    hop_sec: float = 0.75,\n    # --- clustering knobs ---\n    max_speakers: int = 6,\n):\n    \"\"\"\n    Returns list[dict] with keys: start_time, end_time, speaker_id\n    \"\"\"\n    wav = load_audio_mono_16k(audio_path, 16000)\n\n    speech_segs = silero_vad_speech_segments(\n        wav,\n        sr=16000,\n        vad_threshold=vad_threshold,\n        padding_ms=padding_ms,\n        min_silence_ms=min_silence_ms,\n        min_speech_ms=min_speech_ms,\n    )\n    if len(speech_segs) == 0:\n        return []\n\n    windows = make_windows(speech_segs, win_sec=win_sec, hop_sec=hop_sec)\n\n    # Embeddings per window\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):  # drop too-short\n            continue\n        embs.append(embedding_fn(chunk))\n        valid_windows.append((ws, we))\n\n    if len(embs) == 0:\n        return []\n\n    embs = np.vstack(embs)\n\n    # Speaker count + clustering\n    n_spk = estimate_num_speakers(embs, max_k=max_speakers)\n    labels = cluster_embeddings(embs, n_speakers=n_spk)\n\n    turns = windows_to_turns(valid_windows, labels)\n\n    # Map cluster labels to SPEAKER_1.. in order of first appearance\n    label_map = {}\n    next_id = 1\n    out = []\n    for s, e, lab in turns:\n        if lab not in label_map:\n            label_map[lab] = f\"SPEAKER_{next_id}\"\n            next_id += 1\n        out.append({\n            \"start_time\": round(s, 2),\n            \"end_time\": round(e, 2),\n            \"speaker_id\": label_map[lab],\n        })\n\n    return out","metadata":{"execution":{"iopub.status.busy":"2026-02-10T12:38:29.990675Z","iopub.status.idle":"2026-02-10T12:38:29.990961Z","shell.execute_reply.started":"2026-02-10T12:38:29.990837Z","shell.execute_reply":"2026-02-10T12:38:29.990857Z"},"trusted":true},"outputs":[],"execution_count":null},{"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\nif 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-10T12:38:29.992052Z","iopub.status.idle":"2026-02-10T12:38:29.99228Z","shell.execute_reply.started":"2026-02-10T12:38:29.992174Z","shell.execute_reply":"2026-02-10T12:38:29.992187Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"pred_rows = []\nfor stem, wav_path in tqdm(list(zip(stems, wav_paths)), total=len(wav_paths), desc=\"Diarizing\"):\n    diar = diarize_file(wav_path)\n    pred_rows.append({\n        \"filename\": stem,\n        \"diarization\": json.dumps(diar, ensure_ascii=False),\n    })\n\nsub_df = pd.DataFrame(pred_rows)\nsub_df.to_csv(OUT_PATH, index=False)\n\nOUT_PATH, sub_df.head()","metadata":{"execution":{"iopub.status.busy":"2026-02-10T12:38:29.992992Z","iopub.status.idle":"2026-02-10T12:38:29.993323Z","shell.execute_reply.started":"2026-02-10T12:38:29.993171Z","shell.execute_reply":"2026-02-10T12:38:29.993193Z"},"trusted":true},"outputs":[],"execution_count":null}]}