{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":129276,"databundleVersionId":15506988},{"sourceType":"datasetVersion","sourceId":6707460,"datasetId":3865741,"databundleVersionId":6791840}],"dockerImageVersionId":31260,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"## **Install Required Inference Dependencies**\n\nInstall `demucs` along with `transformers`, `torchaudio`, `soundfile`, and `librosa` to support speech separation and audio processing for inference.","metadata":{}},{"cell_type":"code","source":"!pip install -q demucs","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!pip -q install transformers torchaudio soundfile librosa","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-11T14:56:57.1303Z","iopub.execute_input":"2026-02-11T14:56:57.13051Z","iopub.status.idle":"2026-02-11T14:57:00.472586Z","shell.execute_reply.started":"2026-02-11T14:56:57.130484Z","shell.execute_reply":"2026-02-11T14:57:00.471793Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### **Necessary imports**","metadata":{}},{"cell_type":"code","source":"import os\nimport glob\nimport shutil\nimport pathlib\nimport subprocess\nimport torch\nimport torchaudio\nimport librosa\nimport pandas as pd\nimport soundfile as sf\nfrom tqdm.auto import tqdm\nfrom transformers import WhisperProcessor, WhisperForConditionalGeneration","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-11T14:57:00.474504Z","iopub.execute_input":"2026-02-11T14:57:00.474768Z","iopub.status.idle":"2026-02-11T14:57:27.858489Z","shell.execute_reply.started":"2026-02-11T14:57:00.474739Z","shell.execute_reply":"2026-02-11T14:57:27.857883Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## **Set Up Test Paths and Working Directories**\n\nThis cell defines the test audio directory, creates necessary working folders for processing and outputs, and collects the list of test `.wav` files for transcription.","metadata":{}},{"cell_type":"code","source":"# Test directory\ntest_audio_dir = \"/kaggle/input/competitions/dl-sprint-4-0-bengali-long-form-speech-recognition/transcription/transcription/test/audio\"\n\n# Working folders\nwork_audio_dir = \"/kaggle/working/audio_full\"\ndemucs_out_dir = \"/kaggle/working/separated_full\"\nout_txt_dir = \"/kaggle/working/test_transcripts\"\n\nos.makedirs(work_audio_dir, exist_ok=True)\nos.makedirs(demucs_out_dir, exist_ok=True)\nos.makedirs(out_txt_dir, exist_ok=True)\n\nwav_files = sorted(glob.glob(os.path.join(test_audio_dir, \"*.wav\")))\nprint(\"Number of test files:\", len(wav_files))\nprint(\"Example:\", wav_files[0] if wav_files else \"None\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-11T14:57:27.859315Z","iopub.execute_input":"2026-02-11T14:57:27.859888Z","iopub.status.idle":"2026-02-11T14:57:27.882522Z","shell.execute_reply.started":"2026-02-11T14:57:27.859845Z","shell.execute_reply":"2026-02-11T14:57:27.881812Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## **Load Silero Voice Activity Detection Model**\n\nThis cell loads the Silero VAD model via Torch Hub and retrieves the speech timestamp utility function for detecting speech segments in audio files.","metadata":{}},{"cell_type":"code","source":"vad_model, utils = torch.hub.load(\n    repo_or_dir=\"snakers4/silero-vad\",\n    model=\"silero_vad\",\n    trust_repo=True\n)\n\n(get_speech_timestamps, _, _, _, _) = utils\nprint(\"Silero VAD loaded\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-11T14:57:27.883458Z","iopub.execute_input":"2026-02-11T14:57:27.883768Z","iopub.status.idle":"2026-02-11T14:57:29.27197Z","shell.execute_reply.started":"2026-02-11T14:57:27.883728Z","shell.execute_reply":"2026-02-11T14:57:29.271308Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## **Load Local Whisper Model for ASR**\n\nThis cell locates the Whisper model files, loads the processor and model from the detected path, moves the model to the appropriate device, and prepares it for transcription.","metadata":{}},{"cell_type":"code","source":"base = \"/kaggle/input/datasets/tugstugi/bengali-ai-asr-submission/bengali-whisper-medium\"\n\n# Auto-detect HF model folder\ncandidates = []\nfor root, _, files in os.walk(base):\n    if \"config.json\" in files and (\"pytorch_model.bin\" in files or \"model.safetensors\" in files):\n        candidates.append(root)\n\nif not candidates:\n    raise FileNotFoundError(\"Whisper model not found\")\n\nmodel_path = candidates[0]\nprint(\"Using model:\", model_path)\n\ndevice = \"cuda\" if torch.cuda.is_available() else \"cpu\"\nprint(\"Device:\", device)\n\nprocessor = WhisperProcessor.from_pretrained(model_path)\nmodel = WhisperForConditionalGeneration.from_pretrained(model_path).to(device)\nmodel.eval()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-11T14:57:29.272864Z","iopub.execute_input":"2026-02-11T14:57:29.273132Z","iopub.status.idle":"2026-02-11T14:57:46.748516Z","shell.execute_reply.started":"2026-02-11T14:57:29.273105Z","shell.execute_reply":"2026-02-11T14:57:46.747868Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## **Utilities for VAD-Based Chunking**\n\nThis cell defines helper functions to group VAD speech segments into ~20s windows without cutting mid-speech, and to extract audio chunks with optional padding for more stable ASR decoding.","metadata":{}},{"cell_type":"code","source":"def build_vad_windows(segments, target_len_s=20.0, max_gap_s=5.0):\n    \"\"\"\n    Builds timeline windows:\n    - Target ~20s\n    - Never cut inside a segment\n    - Do NOT merge if silence gap > max_gap_s\n    \"\"\"\n    windows = []\n    i, n = 0, len(segments)\n\n    while i < n:\n        win_start = segments[i][0]\n        win_end = segments[i][1]\n        i += 1\n\n        while i < n:\n            next_start, next_end = segments[i]\n            gap = next_start - win_end\n\n            # Stop if silence too large\n            if gap > max_gap_s:\n                break\n\n            win_end = next_end\n            i += 1\n\n            if (win_end - win_start) >= target_len_s:\n                break\n\n        windows.append((win_start, win_end))\n\n    return windows\n\n\ndef slice_with_padding(wav_1ch, sr, start_s, end_s, pad_s=2.0):\n    start_i = max(0, int(round(start_s * sr)))\n    end_i = min(wav_1ch.shape[-1], int(round(end_s * sr)))\n    chunk = wav_1ch[:, start_i:end_i]\n\n    pad_len = int(round(pad_s * sr))\n    pad = torch.zeros((1, pad_len), dtype=chunk.dtype)\n\n    return torch.cat([pad, chunk, pad], dim=1)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## **Demucs Vocal Separation Function**\n\nThis cell defines a function that runs Demucs (`htdemucs`) to extract the vocal stem from an input audio file and returns the path to the separated vocals for downstream processing.","metadata":{}},{"cell_type":"code","source":"TARGET_SR = 16000\n\ndef demucs_two_stem_vocals(in_wav_path):\n    cmd = [\n        \"python\", \"-m\", \"demucs\",\n        \"-n\", \"htdemucs\",\n        \"--two-stems\", \"vocals\",\n        \"-o\", demucs_out_dir,\n        in_wav_path\n    ]\n    subprocess.run(cmd, check=True)\n\n    track_name = pathlib.Path(in_wav_path).stem\n    vocals_path = f\"{demucs_out_dir}/htdemucs/{track_name}/vocals.wav\"\n    return vocals_path","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-11T14:57:46.749466Z","iopub.execute_input":"2026-02-11T14:57:46.749761Z","iopub.status.idle":"2026-02-11T14:57:46.75745Z","shell.execute_reply.started":"2026-02-11T14:57:46.749718Z","shell.execute_reply":"2026-02-11T14:57:46.756834Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## **Run Full ASR Pipeline and Generate Transcripts**\n\nThis cell processes each test audio file by performing Demucs vocal separation, applying VAD-based chunking, transcribing segments with Whisper, saving individual transcript files, and compiling the final submission dataframe.","metadata":{}},{"cell_type":"code","source":"all_results = {}\ncnt = 0\nfor in_wav in tqdm(wav_files, desc=\"Processing Files\"):\n    fname = pathlib.Path(in_wav).name\n    stem = pathlib.Path(in_wav).stem\n\n\n    # # Demucs 2-stem\n    # vocals_path = demucs_two_stem_vocals(in_wav)\n\n    # # Load vocals\n    # wav, sr = torchaudio.load(vocals_path)\n\n    \n    # Load original audio directly\n    wav, sr = torchaudio.load(in_wav)\n    wav = wav.mean(dim=0, keepdim=True)\n\n    if sr != TARGET_SR:\n        wav = torchaudio.functional.resample(wav, sr, TARGET_SR)\n    sr = TARGET_SR\n\n    audio_1d = wav.squeeze(0).cpu()\n\n    # VAD\n    speech_ts = get_speech_timestamps(audio_1d, vad_model, sampling_rate=sr)\n    segments = [(s[\"start\"]/sr, s[\"end\"]/sr) for s in speech_ts]\n\n    if not segments:\n        transcript = \"\"\n    else:\n        # Build windows with gap protection\n        windows = build_vad_windows(\n            segments,\n            target_len_s=20.0,\n            max_gap_s=5.0\n        )\n\n        parts = []\n\n        for ws, we in tqdm(windows, desc=f\"Windows {fname}\", leave=False):\n            chunk = slice_with_padding(wav, sr, ws, we, pad_s=1.0)\n            audio_np = chunk.squeeze(0).cpu().numpy()\n\n            inputs = processor(audio_np, sampling_rate=sr, return_tensors=\"pt\")\n            feats = inputs.input_features.to(device)\n\n            with torch.no_grad():\n                pred_ids = model.generate(\n                    feats,\n                    max_new_tokens=256,\n                )\n\n            text = processor.batch_decode(pred_ids, skip_special_tokens=True)[0].strip()\n            if text:\n                parts.append(text)\n\n        transcript = \" \".join(parts)  \n\n    # Save individual txt\n    out_path = os.path.join(out_txt_dir, f\"{stem}.txt\")\n    with open(out_path, \"w\", encoding=\"utf-8\") as f:\n        f.write(transcript)\n\n    # Store for submission\n    all_results[stem] = transcript\n\n    # Cleanup Demucs output\n    # track_folder = os.path.join(demucs_out_dir, \"htdemucs\", stem)\n    # if os.path.exists(track_folder):\n    #     shutil.rmtree(track_folder)\n    # cnt = cnt+1\n    # print(f\"{cnt} files transcribed\")\n\nprint(\"Done. Individual transcripts saved.\")\n\n\n# ----------------------------------\n# Create submission CSV\n# ----------------------------------\nsubmission_df = pd.DataFrame({\n    \"filename\": list(all_results.keys()),\n    \"transcript\": list(all_results.values())\n})\n\nsubmission_df = submission_df.sort_values(\"filename\").reset_index(drop=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-11T15:10:50.920172Z","iopub.execute_input":"2026-02-11T15:10:50.920723Z","iopub.status.idle":"2026-02-11T15:14:46.667778Z","shell.execute_reply.started":"2026-02-11T15:10:50.920695Z","shell.execute_reply":"2026-02-11T15:14:46.666718Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## **Normalize Transcripts and Save Submission**\n\nThis cell applies Unicode normalization and whitespace cleanup to Bengali transcripts, removes hidden zero-width characters, and saves the final sorted submission file as `submission.csv`.","metadata":{}},{"cell_type":"code","source":"import re\nimport unicodedata\n\nZW = r\"[\\u200B-\\u200D\\uFEFF]\"  # zero-width chars + BOM\n\ndef normalize_bn_text(s: str) -> str:\n    if s is None:\n        return \"\"\n    s = str(s)\n    s = unicodedata.normalize(\"NFC\", s)   # Unicode normalize\n    s = re.sub(ZW, \"\", s)                 # remove zero-width chars\n    s = s.replace(\"\\u00A0\", \" \")          # replace non-breaking space\n    s = \" \".join(s.split())               # collapse extra whitespace\n    return s\n\nsubmission_df[\"transcript\"] = submission_df[\"transcript\"].apply(normalize_bn_text)\n\nsubmission_df = submission_df.sort_values(\"filename\").reset_index(drop=True)\n\nsubmission_path = \"submission.csv\"\nsubmission_df.to_csv(submission_path, index=False, encoding=\"utf-8\")\n\nprint(\"Submission saved to:\", submission_path)","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}