{"cells":[{"cell_type":"markdown","metadata":{},"source":"# BLACKHAWK DOWN — Inference Notebook## Bengali Speaker Diarization | BUET DL Sprint 4.0 — Problem 2**Team**: BLACKHAWK DOWN (Arafat Rahman, Md. Faisal Sheikh, Md. Naymul Islam, Tahmidul Islam Omi)**Approach**: pyannote.audio 3.1 end-to-end pipeline with Bengali fine-tuned segmentation model**Key components**:- **Segmentation**: `lucius-40/speaker-segmentation-fine-tuned-bengali` (PyanNet fine-tuned on Bengali)- **Embeddings**: WeSpeaker ResNet34-LM (256-d, default)- **Clustering**: Agglomerative Hierarchical Clustering (centroid, threshold=0.70)"},{"cell_type":"code","metadata":{},"source":"import os, json, subprocess, sys, shutil\nfrom pathlib import Path\n\nKAGGLE_DATA = Path(\"/kaggle/input/dl-sprint-4-0-bengali-speaker-diarization-challenge\")\nON_KAGGLE = KAGGLE_DATA.exists()\nif ON_KAGGLE:\n    print(f\"Running on Kaggle -- dataset at {KAGGLE_DATA}\")\nelse:\n    print(\"WARNING: Competition data not found at expected path\")\n    # Try alternative path\n    alt = Path(\"/kaggle/input/competitions/dl-sprint-4-0-bengali-speaker-diarization-challenge\")\n    if alt.exists():\n        KAGGLE_DATA = alt\n        print(f\"Found at alternative path: {KAGGLE_DATA}\")\n\nif not shutil.which(\"ffmpeg\"):\n    !apt-get install -y ffmpeg -qq\n\nNUMPY_VER = subprocess.check_output(\n    [sys.executable, \"-c\", \"import numpy; print(numpy.__version__)\"]\n).decode().strip()\n\n# Install sequence with version pins for compatibility\n!pip install -q \"speechbrain>=1.0,<1.1\" \"numpy=={NUMPY_VER}\"\n!pip install -q huggingface_hub \"numpy=={NUMPY_VER}\"\n!pip install -q pyannote.audio \"numpy=={NUMPY_VER}\"\n!pip install -q --no-deps \"diarizers @ git+https://github.com/huggingface/diarizers.git\" 2>/dev/null || echo \"diarizers install failed\"\n!pip install -q \"datasets[audio]>=2.11.0,<4.0.0\" audiomentations denoiser \"numpy=={NUMPY_VER}\"\n!pip install -q soundfile tqdm \"numpy=={NUMPY_VER}\"\n# Pin transformers for diarizers SegmentationModel compatibility\n!pip install -q \"transformers>=4.40,<4.47\" \"numpy=={NUMPY_VER}\"\n\nimport torch\n_orig_load = torch.load\ndef _patched_load(*a, **kw):\n    kw['weights_only'] = False\n    return _orig_load(*a, **kw)\ntorch.load = _patched_load\nprint(\"torch.load patched\")\n\nimport torchaudio\nif not hasattr(torchaudio, 'list_audio_backends'):\n    torchaudio.list_audio_backends = lambda: [\"soundfile\"]\n\n# Fix for torchaudio>=2.9 which removed AudioMetaData + info() (needed by pyannote.audio 3.x)\nif not hasattr(torchaudio, 'AudioMetaData'):\n    class _AudioMetaData:\n        def __init__(self, sample_rate=0, num_frames=0, num_channels=0,\n                     bits_per_sample=0, encoding=\"\"):\n            self.sample_rate = sample_rate\n            self.num_frames = num_frames\n            self.num_channels = num_channels\n            self.bits_per_sample = bits_per_sample\n            self.encoding = encoding\n    torchaudio.AudioMetaData = _AudioMetaData\n    print(\"Patched torchaudio.AudioMetaData\")\n\nif not hasattr(torchaudio, 'info'):\n    import soundfile as _sf\n    def _torchaudio_info(filepath, backend=None):\n        info = _sf.info(str(filepath))\n        return torchaudio.AudioMetaData(\n            sample_rate=info.samplerate,\n            num_frames=info.frames,\n            num_channels=info.channels,\n            bits_per_sample=16,\n            encoding=info.format,\n        )\n    torchaudio.info = _torchaudio_info\n    print(\"Patched torchaudio.info\")\n\n# Fix: newer huggingface_hub removed use_auth_token param\n# pyannote.audio 3.x still passes it internally, so we patch\nimport functools\nimport huggingface_hub as _hf\ndef _patch_hf(name):\n    orig = getattr(_hf, name, None)\n    if orig is None: return\n    @functools.wraps(orig)\n    def wrapper(*args, **kwargs):\n        if 'use_auth_token' in kwargs:\n            kwargs['token'] = kwargs.pop('use_auth_token')\n        return orig(*args, **kwargs)\n    setattr(_hf, name, wrapper)\nfor _fn in ['hf_hub_download', 'model_info', 'list_repo_files', 'snapshot_download', 'cached_download']:\n    _patch_hf(_fn)\nprint(\"Patched huggingface_hub for use_auth_token compat\")\n\nimport copy, time, warnings, gc\nimport numpy as np\nimport pandas as pd\nimport soundfile as sf\nfrom tqdm.auto import tqdm\n\nwarnings.filterwarnings('ignore')\n\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nN_GPUS = torch.cuda.device_count()\nprint(f\"Device: {DEVICE}, GPUs: {N_GPUS}\")\nfor i in range(N_GPUS):\n    print(f\"  GPU {i}: {torch.cuda.get_device_name(i)}\")\nprint(\"All imports OK\")","outputs":[],"execution_count":null},{"cell_type":"code","metadata":{},"source":"DATA_ROOT = KAGGLE_DATA / \"diarization\" / \"diarization\"\nTEST_AUDIO = DATA_ROOT / \"test\" / \"audio\"\nWORK_DIR = Path(\"/kaggle/working\")\nOUT_PATH = WORK_DIR / \"submission.csv\"\n\n# Debug: show what's available\nprint(\"Data structure:\")\nfor p in [KAGGLE_DATA, DATA_ROOT, TEST_AUDIO]:\n    exists = p.exists()\n    items = sorted(p.iterdir()) if exists else []\n    print(f\"  {p}: exists={exists}, items={len(items)}\")\n    if exists and len(items) <= 20:\n        for c in items:\n            print(f\"    {c.name}\")\n\n# HF auth\nHF_TOKEN = \"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\"\ntry:\n    from kaggle_secrets import UserSecretsClient\n    HF_TOKEN = UserSecretsClient().get_secret(\"HUGGINGFACE_TOKEN\")\n    print(\"HF token from Kaggle secrets\")\nexcept Exception:\n    print(\"HF token from hardcoded fallback\")\n\nfrom huggingface_hub import login\nlogin(token=HF_TOKEN)\n\nTEST_FILES = sorted(TEST_AUDIO.glob(\"*.wav\"))\nprint(f\"\\nTest files: {len(TEST_FILES)}\")\nfor f in TEST_FILES:\n    dur = sf.info(str(f)).duration\n    print(f\"  {f.stem}: {dur:.1f}s\")","outputs":[],"execution_count":null},{"cell_type":"code","metadata":{},"source":"from pyannote.audio import Pipeline as PyannotePipeline\nfrom diarizers import SegmentationModel\n\npipeline = PyannotePipeline.from_pretrained(\n    \"pyannote/speaker-diarization-3.1\", use_auth_token=HF_TOKEN\n)\npipeline.to(DEVICE)\nBASE_PARAMS = pipeline.parameters(instantiated=True)\nprint(f\"[OK] pyannote 3.1 pipeline loaded\")\n\n# Swap in Bengali fine-tuned segmentation\nseg = SegmentationModel().from_pretrained(\n    \"lucius-40/speaker-segmentation-fine-tuned-bengali\"\n)\nbengali_seg = seg.to_pyannote_model()\npipeline._segmentation.model = bengali_seg.to(DEVICE)\ndel seg\ngc.collect()\nprint(f\"[OK] Bengali segmentation model loaded\")\n\n# Set threshold=0.70 (best from v58 evaluation)\nparams = copy.deepcopy(BASE_PARAMS)\nparams[\"clustering\"][\"threshold\"] = 0.70\npipeline.instantiate(params)\nprint(f\"[OK] Pipeline ready: threshold=0.70\")","outputs":[],"execution_count":null},{"cell_type":"code","metadata":{},"source":"print(\"=\" * 60)\nprint(\"RUNNING INFERENCE\")\nprint(\"=\" * 60)\n\ntotal_time, total_dur = 0, 0\nrows = []\n\nfor wav_path in tqdm(TEST_FILES, desc=\"Diarizing\"):\n    stem = wav_path.stem\n    dur = sf.info(str(wav_path)).duration\n\n    t0 = time.time()\n    hyp = pipeline(str(wav_path), min_speakers=8, max_speakers=25)\n    dt = time.time() - t0\n\n    total_time += dt\n    total_dur += dur\n    n_spk = len(hyp.labels())\n\n    # Convert to submission format\n    segments = []\n    label_map, next_id = {}, 1\n    for turn, _, speaker in hyp.itertracks(yield_label=True):\n        if speaker not in label_map:\n            label_map[speaker] = f\"SPEAKER_{next_id}\"\n            next_id += 1\n        segments.append({\n            \"start_time\": round(turn.start, 2),\n            \"end_time\": round(turn.end, 2),\n            \"speaker_id\": label_map[speaker]\n        })\n\n    rows.append({\n        \"filename\": stem,\n        \"diarization\": json.dumps(segments, ensure_ascii=False)\n    })\n    print(f\"  {stem}: {len(segments)} segs, {n_spk} spk, {dt:.1f}s (RTF={dt/dur:.4f})\")\n\nrtf = total_time / total_dur if total_dur > 0 else 0\nprint(f\"\\nTotal: {total_time:.0f}s for {total_dur:.0f}s audio (RTF={rtf:.4f})\")","outputs":[],"execution_count":null},{"cell_type":"code","metadata":{},"source":"sub = pd.DataFrame(rows)\nsub.to_csv(str(OUT_PATH), index=False)\nprint(f\"Saved: {OUT_PATH}\")\n\nassert len(sub) == len(TEST_FILES), f\"Expected {len(TEST_FILES)} rows, got {len(sub)}\"\nfor _, r in sub.iterrows():\n    d = json.loads(r[\"diarization\"])\n    spks = set(x[\"speaker_id\"] for x in d)\n    for seg in d:\n        assert \"start_time\" in seg and \"end_time\" in seg and \"speaker_id\" in seg\n        assert seg[\"end_time\"] > seg[\"start_time\"]\n    print(f\"  {r['filename']}: {len(d)} segments, {len(spks)} speakers\")\nprint(\"\\nSubmission format verified!\")\nprint(\"Done.\")","outputs":[],"execution_count":null}],"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python","version":"3.10.12"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":95498,"databundleId":13365289,"sourceType":"competition","sourceSlug":"dl-sprint-4-0-bengali-speaker-diarization-challenge"}],"isInternetEnabled":true,"language":"python","isGpuEnabled":true}},"nbformat":4,"nbformat_minor":4}