{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.10.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":4143520,"sourceType":"datasetVersion","datasetId":2447262},{"sourceId":6707460,"sourceType":"datasetVersion","datasetId":3865741},{"sourceId":14729470,"sourceType":"datasetVersion","datasetId":9412227}],"dockerImageVersionId":30528,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"\"\"\"\nDL Sprint 4.0 — Mofa(v2.3)_inference.py\n========================================\n\nCHANGES from v2.1:\n1. ✅ num_beams=5 (was 1) — beam search reduces misspellings significantly\n2. ✅ no_repeat_ngram_size=0 (was 4) — stops garbled output from forced diversity\n3. ✅ condition_on_prev_tokens=True (was False) — cross-chunk coherence\n4. ✅ batch_size=1 (was 4) — required for sequential conditioning\n5. ✅ Silero VAD still active — safe to use condition_on_prev=True since noise is removed\n6. ✅ Post-processing: consecutive-only repetition handling (no global word cap)\n\nResearch basis:\n- Beam search (num_beams=5): Whisper paper default, reduces substitution errors\n- no_repeat_ngram_size=0: Avoids forcing wrong tokens when audio genuinely repeats\n- condition_on_prev_tokens=True: Maintains context across 30s chunks\n- VAD makes condition_on_prev=True safe (no music/noise to trigger loops)\n\"\"\"\n\n# ============================================================================\n# SETUP - PART 1: Install dependencies FIRST\n# ============================================================================\n\nimport subprocess\nimport sys\n\nprint(\"📦 Installing dependencies...\")\nsubprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\",\n                \"transformers>=4.30.0\", \"accelerate\"], check=True)\nsubprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\",\n                \"jiwer\", \"librosa\", \"soundfile\"], check=True)\nsubprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\",\n                \"torch\", \"torchaudio\"], check=True)\nprint(\"✓ Dependencies ready\\n\")\n\n# ============================================================================\n# SETUP - PART 2: Import everything AFTER installation\n# ============================================================================\n\nimport os\nimport re\nimport csv\nimport time\nimport json\nimport glob\nimport warnings\nimport numpy as np\nfrom pathlib import Path\nfrom typing import List, Tuple, Dict\nfrom collections import Counter\n\nimport torch\nimport librosa\nimport soundfile as sf\nfrom transformers import pipeline\nfrom jiwer import wer as compute_wer, cer as compute_cer\n\nwarnings.filterwarnings(\"ignore\")\nimport logging\nlogging.getLogger(\"transformers\").setLevel(logging.ERROR)\n\n# ============================================================================\n# CONFIGURATION\n# ============================================================================\n\nTEST_MODE = False   # Smoke test with 2 files\nDEBUG_MODE = True\nVALIDATION_SAMPLES = 0  # Train samples for WER validation\n\nPATHS = {\n    # Model (base or merged) - CORRECTED PATH\n    \"model\": \"/kaggle/input/bengali-ai-asr-submission/bengali-whisper-medium\",\n    \n    # Competition data (these look correct already)\n    \"test_audio\": \"/kaggle/input/datasets/mofazzal874/dl-sprint-4-0-bengali-long-form-speech-recognition/dl-sprint-4-0-bengali-long-form-speech-recognition/transcription/transcription/test/audio\",\n    \"train_audio\": \"/kaggle/input/datasets/mofazzal874/dl-sprint-4-0-bengali-long-form-speech-recognition/dl-sprint-4-0-bengali-long-form-speech-recognition/transcription/transcription/train/audio\",\n    \"train_annotation\": \"/kaggle/input/datasets/mofazzal874/dl-sprint-4-0-bengali-long-form-speech-recognition/dl-sprint-4-0-bengali-long-form-speech-recognition/transcription/transcription/train/annotation\",\n    \n    # Outputs\n    \"submission\": \"/kaggle/working/submission.csv\",\n    \"validation_report\": \"/kaggle/working/validation_report.txt\",\n    \"vad_cache\": \"/kaggle/working/vad_cache\",\n}\n# ── Silero VAD Config ─────────────────────────────────────────────────────\nVAD_CONFIG = {\n    \"threshold\": 0.5,\n    \"min_speech_duration_ms\": 250,\n    \"max_speech_duration_s\": 30,\n    \"min_silence_duration_ms\": 100,\n    \"speech_pad_ms\": 30,\n    \"min_volume_db\": -40,\n}\n\n# ── Whisper Inference Config (v2.3 — accuracy-focused) ────────────────────\nINFERENCE_CONFIG = {\n    \"chunk_length_s\": 30,\n    \"max_length\": 448,\n    \"num_beams\": 5,          # ← CHANGED: beam search for fewer misspellings\n    \"batch_size\": 1,         # ← CHANGED: required for condition_on_prev=True\n    \n    \"no_repeat_ngram_size\": 0,   # ← CHANGED: disabled — was causing garbled output\n    \"compression_ratio_threshold\": 2.4,\n    \"logprob_threshold\": -1.0,\n    \"no_speech_threshold\": 0.6,\n    \n    # ← CHANGED: True for cross-chunk coherence (safe with VAD)\n    \"condition_on_prev_tokens\": True,\n}\n\n# ── Post-processing (v2.3 — lighter, consecutive-only) ────────────────────\nPOST_PROCESS_CONFIG = {\n    \"max_word_repetitions\": 3,   # Max CONSECUTIVE repeats of same word\n    \"max_phrase_repetitions\": 2,\n    \"ngram_sizes\": [2, 3, 4, 5],\n    \"leading_hallucination_window\": 50,\n    \"leading_hallucination_threshold\": 0.5,\n}\n\n\n# ============================================================================\n# SILERO VAD SETUP\n# ============================================================================\n\ndef load_silero_vad():\n    \"\"\"Load Silero VAD model.\"\"\"\n    print(\"\\n🎤 Loading Silero VAD...\")\n    \n    try:\n        torch.set_num_threads(1)\n        model, utils = torch.hub.load(\n            repo_or_dir='snakers4/silero-vad',\n            model='silero_vad',\n            force_reload=False,\n            onnx=False,\n        )\n        \n        (get_speech_timestamps, _, read_audio, _, _) = utils\n        \n        print(\"✓ Silero VAD loaded\")\n        print(f\"  Config: threshold={VAD_CONFIG['threshold']}, \"\n              f\"min_speech={VAD_CONFIG['min_speech_duration_ms']}ms, \"\n              f\"min_volume={VAD_CONFIG['min_volume_db']}dB\")\n        \n        return model, get_speech_timestamps, read_audio\n    \n    except Exception as e:\n        print(f\"⚠️  Failed to load Silero VAD: {e}\")\n        print(\"  Continuing WITHOUT VAD (expect hallucinations!)\")\n        return None, None, None\n\n\ndef compute_volume_db(audio: np.ndarray) -> float:\n    \"\"\"Compute RMS volume in dB.\"\"\"\n    rms = np.sqrt(np.mean(audio ** 2))\n    if rms == 0:\n        return -100\n    return 20 * np.log10(rms)\n\n\ndef apply_vad(\n    audio_path: str,\n    vad_model,\n    get_speech_timestamps_fn,\n    read_audio_fn,\n) -> List[Tuple[float, float]]:\n    \"\"\"Apply Silero VAD to detect speech segments.\"\"\"\n    if vad_model is None:\n        duration = librosa.get_duration(path=audio_path)\n        return [(0.0, duration)]\n    \n    try:\n        wav = read_audio_fn(audio_path, sampling_rate=16000)\n        \n        speech_timestamps = get_speech_timestamps_fn(\n            wav,\n            vad_model,\n            threshold=VAD_CONFIG['threshold'],\n            min_speech_duration_ms=VAD_CONFIG['min_speech_duration_ms'],\n            max_speech_duration_s=VAD_CONFIG['max_speech_duration_s'],\n            min_silence_duration_ms=VAD_CONFIG['min_silence_duration_ms'],\n            speech_pad_ms=VAD_CONFIG['speech_pad_ms'],\n            return_seconds=True,\n        )\n        \n        if not speech_timestamps:\n            if DEBUG_MODE:\n                print(f\"    ⚠️  VAD: No speech detected\")\n            return []\n        \n        # Filter by volume\n        audio_full, sr = librosa.load(audio_path, sr=16000)\n        filtered_segments = []\n        \n        for seg in speech_timestamps:\n            start_sec, end_sec = seg['start'], seg['end']\n            start_sample = int(start_sec * sr)\n            end_sample = int(end_sec * sr)\n            \n            segment_audio = audio_full[start_sample:end_sample]\n            volume_db = compute_volume_db(segment_audio)\n            \n            if volume_db >= VAD_CONFIG['min_volume_db']:\n                filtered_segments.append((start_sec, end_sec))\n            elif DEBUG_MODE:\n                print(f\"      Filtered low-volume: {start_sec:.1f}-{end_sec:.1f}s ({volume_db:.1f}dB)\")\n        \n        if DEBUG_MODE and filtered_segments:\n            total_speech = sum(e - s for s, e in filtered_segments)\n            total_duration = librosa.get_duration(path=audio_path)\n            print(f\"    🎤 VAD: {len(filtered_segments)} segments, \"\n                  f\"{total_speech:.1f}s/{total_duration:.1f}s speech ({total_speech/total_duration*100:.0f}%)\")\n        \n        return filtered_segments\n    \n    except Exception as e:\n        if DEBUG_MODE:\n            print(f\"    ⚠️  VAD failed: {e}\")\n        duration = librosa.get_duration(path=audio_path)\n        return [(0.0, duration)]\n\n\ndef extract_speech_audio(\n    audio_path: str,\n    speech_segments: List[Tuple[float, float]],\n    output_path: str = None,\n) -> str:\n    \"\"\"Extract only speech segments from audio file.\"\"\"\n    if not speech_segments:\n        silence = np.zeros(16000, dtype=np.float32)\n        if output_path is None:\n            output_path = audio_path.replace('.wav', '_vad.wav').replace('.mp3', '_vad.wav')\n        sf.write(output_path, silence, 16000)\n        return output_path\n    \n    audio, sr = librosa.load(audio_path, sr=16000)\n    \n    speech_audio = []\n    for start, end in speech_segments:\n        start_sample = int(start * sr)\n        end_sample = int(end * sr)\n        speech_audio.append(audio[start_sample:end_sample])\n    \n    speech_audio = np.concatenate(speech_audio) if speech_audio else np.zeros(sr, dtype=np.float32)\n    \n    if output_path is None:\n        output_path = audio_path.replace('.wav', '_vad.wav').replace('.mp3', '_vad.wav')\n    \n    sf.write(output_path, speech_audio, sr)\n    return output_path\n\n\n# ============================================================================\n# POST-PROCESSING (v2.3 — consecutive-only, no global word cap)\n# ============================================================================\n\ndef normalize_bengali(text: str) -> str:\n    \"\"\"Normalize Bengali text.\"\"\"\n    if not text:\n        return \"\"\n    text = re.sub(r'\\s+', ' ', text)\n    return text.strip()\n\n\ndef fix_word_repetition(text: str, max_consecutive: int = 3) -> str:\n    \"\"\"Collapse CONSECUTIVE runs of the same word to max_consecutive.\n    \n    IMPORTANT: Only targets consecutive repetitions like 'মানুষ মানুষ মানুষ মানুষ'.\n    Does NOT limit total word frequency — common words like আমি, তার, না\n    naturally appear dozens of times in long audio and must be kept.\n    \"\"\"\n    if not text:\n        return \"\"\n    words = text.split()\n    if not words:\n        return \"\"\n    \n    result = []\n    i = 0\n    while i < len(words):\n        j = i + 1\n        while j < len(words) and words[j] == words[i]:\n            j += 1\n        run_len = j - i\n        \n        keep = min(run_len, max_consecutive)\n        result.extend([words[i]] * keep)\n        \n        if DEBUG_MODE and run_len > max_consecutive:\n            print(f\"      Collapsed '{words[i]}' x{run_len} → x{keep}\")\n        \n        i = j\n    \n    return \" \".join(result)\n\n\ndef fix_ngram_repetition(text: str, max_repeats: int = 2, ngram_sizes: List[int] = None) -> str:\n    \"\"\"Remove consecutively repeated n-gram phrases.\"\"\"\n    if not text:\n        return \"\"\n    if ngram_sizes is None:\n        ngram_sizes = POST_PROCESS_CONFIG[\"ngram_sizes\"]\n    \n    words = text.split()\n    \n    for n in sorted(ngram_sizes, reverse=True):\n        if len(words) < n * 2:\n            continue\n        \n        new_words = []\n        i = 0\n        while i < len(words):\n            if i + n <= len(words):\n                ngram = tuple(words[i:i + n])\n                repeat_count = 1\n                j = i + n\n                while j + n <= len(words) and tuple(words[j:j + n]) == ngram:\n                    repeat_count += 1\n                    j += n\n                \n                if repeat_count > max_repeats:\n                    for _ in range(max_repeats):\n                        new_words.extend(list(ngram))\n                    i = j\n                else:\n                    new_words.append(words[i])\n                    i += 1\n            else:\n                new_words.append(words[i])\n                i += 1\n        words = new_words\n    \n    return \" \".join(words)\n\n\ndef trim_leading_hallucination(text: str, window: int = 50, threshold: float = 0.5) -> str:\n    \"\"\"Trim hallucinated text at beginning.\"\"\"\n    if not text:\n        return \"\"\n    \n    words = text.split()\n    if len(words) <= 10:\n        return text\n    \n    check_len = min(window, len(words))\n    head = words[:check_len]\n    unique_count = len(set(head))\n    repetition_ratio = 1.0 - (unique_count / check_len)\n    \n    if repetition_ratio < threshold:\n        return text\n    \n    scan_window = 15\n    for start_idx in range(0, min(len(words) - scan_window, window)):\n        segment = words[start_idx:start_idx + scan_window]\n        seg_unique = len(set(segment))\n        seg_ratio = 1.0 - (seg_unique / scan_window)\n        \n        if seg_ratio < 0.35:\n            trimmed = \" \".join(words[start_idx:])\n            if DEBUG_MODE:\n                print(f\"    🔪 Trimmed {start_idx} leading words (hallucination)\")\n            return trimmed\n    \n    return text\n\n\ndef trim_mid_hallucination(text: str) -> str:\n    \"\"\"Remove hallucinated bursts in middle of text.\"\"\"\n    if not text:\n        return \"\"\n    \n    words = text.split()\n    result = []\n    i = 0\n    \n    while i < len(words):\n        j = i + 1\n        while j < len(words) and words[j] == words[i]:\n            j += 1\n        \n        run_length = j - i\n        \n        if run_length >= 4:\n            result.extend([words[i]] * min(2, run_length))\n            if DEBUG_MODE and run_length >= 6:\n                print(f\"    🔪 Mid: collapsed '{words[i]}' x{run_length} → x2\")\n        else:\n            result.extend(words[i:j])\n        i = j\n    \n    return \" \".join(result)\n\n\ndef full_post_process(text: str, file_id: str = \"\") -> str:\n    \"\"\"Full v2.3 post-processing pipeline.\"\"\"\n    if not text:\n        return \"\"\n    \n    original_word_count = len(text.split())\n    \n    # Pipeline\n    text = normalize_bengali(text)\n    text = trim_leading_hallucination(text, POST_PROCESS_CONFIG[\"leading_hallucination_window\"], POST_PROCESS_CONFIG[\"leading_hallucination_threshold\"])\n    text = trim_mid_hallucination(text)\n    text = fix_ngram_repetition(text, POST_PROCESS_CONFIG[\"max_phrase_repetitions\"], POST_PROCESS_CONFIG[\"ngram_sizes\"])\n    text = fix_word_repetition(text, POST_PROCESS_CONFIG[\"max_word_repetitions\"])\n    text = normalize_bengali(text)\n    \n    final_word_count = len(text.split()) if text else 0\n    removed = original_word_count - final_word_count\n    \n    if DEBUG_MODE and removed > 0:\n        print(f\"    📝 Post: {original_word_count} → {final_word_count} words (-{removed})\")\n    \n    return text\n\n\n# ============================================================================\n# HELPERS\n# ============================================================================\n\ndef find_audio_files(test_dir: str) -> List[str]:\n    \"\"\"Find all audio files.\"\"\"\n    files = []\n    for ext in ['*.mp3', '*.wav', '*.flac']:\n        files.extend(glob.glob(os.path.join(test_dir, ext)))\n    files.sort()\n    \n    if TEST_MODE:\n        files = files[:2]\n        print(f\"⚠️  TEST_MODE: Processing only {len(files)} files\")\n    \n    return files\n\n\ndef load_model():\n    \"\"\"Load Whisper model.\"\"\"\n    print(\"\\n🚀 Loading Whisper model...\")\n    print(f\"📁 {PATHS['model']}\")\n    \n    if not os.path.exists(PATHS[\"model\"]):\n        raise FileNotFoundError(f\"Model not found: {PATHS['model']}\")\n    \n    num_gpus = torch.cuda.device_count() if torch.cuda.is_available() else 0\n    use_gpus = min(num_gpus, 2)\n    \n    pipelines = []\n    for gpu_id in range(max(1, use_gpus)):\n        device_val = gpu_id if torch.cuda.is_available() else -1\n        \n        asr_pipe = pipeline(\n            task=\"automatic-speech-recognition\",\n            model=PATHS[\"model\"],\n            tokenizer=PATHS[\"model\"],\n            chunk_length_s=INFERENCE_CONFIG['chunk_length_s'],\n            device=device_val,\n            batch_size=INFERENCE_CONFIG['batch_size'],\n            ignore_warning=True,\n        )\n        \n        asr_pipe.model.config.forced_decoder_ids = \\\n            asr_pipe.tokenizer.get_decoder_prompt_ids(language=\"bn\", task=\"transcribe\")\n        \n        # ── FIX: Patch missing generation config attributes ────────────────\n        gen_cfg = asr_pipe.model.generation_config\n        \n        # Patch no_timestamps_token_id\n        if not hasattr(gen_cfg, \"no_timestamps_token_id\") or gen_cfg.no_timestamps_token_id is None:\n            try:\n                no_ts_id = asr_pipe.tokenizer.convert_tokens_to_ids(\"<|notimestamps|>\")\n                if no_ts_id is not None and no_ts_id != asr_pipe.tokenizer.unk_token_id:\n                    gen_cfg.no_timestamps_token_id = no_ts_id\n                    print(f\"  🔧 Patched no_timestamps_token_id = {no_ts_id}\")\n                else:\n                    raise ValueError(\"Token not in vocab\")\n            except Exception:\n                gen_cfg.no_timestamps_token_id = 50363\n                print(f\"  🔧 Patched no_timestamps_token_id = 50363 (default)\")\n        # ───────────────────────────────────────────────────────────────────\n        \n        pipelines.append(asr_pipe)\n    \n    print(\"✓ Model loaded\")\n    print(f\"⚙️  Config: chunk={INFERENCE_CONFIG['chunk_length_s']}s, \"\n          f\"beams={INFERENCE_CONFIG['num_beams']}, \"\n          f\"no_repeat_ngram={INFERENCE_CONFIG['no_repeat_ngram_size']}, \"\n          f\"condition_on_prev={INFERENCE_CONFIG['condition_on_prev_tokens']}\")\n    \n    return pipelines\n\n\ndef _build_generate_kwargs() -> dict:\n    \"\"\"Build generate_kwargs for Whisper.\"\"\"\n    kwargs = {\n        \"max_length\": INFERENCE_CONFIG[\"max_length\"],\n        \"num_beams\": INFERENCE_CONFIG[\"num_beams\"],\n        \"temperature\": 0.0,\n    }\n    \n    # Only add no_repeat_ngram_size if > 0 (0 means disabled)\n    if INFERENCE_CONFIG[\"no_repeat_ngram_size\"] > 0:\n        kwargs[\"no_repeat_ngram_size\"] = INFERENCE_CONFIG[\"no_repeat_ngram_size\"]\n    \n    return kwargs\n\n\n# ============================================================================\n# VALIDATION\n# ============================================================================\n\ndef validate_model(asr_pipeline, vad_model, get_speech_timestamps_fn, read_audio_fn):\n    \"\"\"Validate on train samples with ground truth.\"\"\"\n    print(\"\\n\" + \"=\" * 70)\n    print(\"📊  VALIDATION (v2.3 — beam search + sequential)\")\n    print(\"=\" * 70)\n    \n    train_audio_dir = PATHS.get(\"train_audio\", \"\")\n    train_annot_dir = PATHS.get(\"train_annotation\", \"\")\n    \n    if not os.path.exists(train_audio_dir) or not os.path.exists(train_annot_dir):\n        print(\"⚠️  No train data — skipping validation\")\n        return None\n    \n    audio_files = sorted([f for f in glob.glob(os.path.join(train_audio_dir, \"*\"))\n                         if Path(f).suffix.lower() in {'.wav', '.mp3', '.flac'}])\n    \n    paired = []\n    for af in audio_files:\n        stem = Path(af).stem\n        annot_path = os.path.join(train_annot_dir, stem + '.txt')\n        if os.path.exists(annot_path):\n            paired.append((af, annot_path))\n    \n    if not paired:\n        print(\"⚠️  No pairs found\")\n        return None\n    \n    val_pairs = paired[:min(VALIDATION_SAMPLES, len(paired))]\n    print(f\"  Using {len(val_pairs)} samples\\n\")\n    \n    all_refs = []\n    all_preds = []\n    \n    os.makedirs(PATHS[\"vad_cache\"], exist_ok=True)\n    \n    for i, (audio_path, annot_path) in enumerate(val_pairs):\n        file_id = Path(audio_path).stem\n        print(f\"  [{i+1}/{len(val_pairs)}] 🎧 {file_id}\")\n        \n        with open(annot_path, 'r', encoding='utf-8') as f:\n            reference = f.read().strip()\n        \n        if not reference:\n            continue\n        \n        # Apply VAD\n        speech_segs = apply_vad(audio_path, vad_model, get_speech_timestamps_fn, read_audio_fn)\n        vad_audio_path = os.path.join(PATHS[\"vad_cache\"], f\"{file_id}_vad.wav\")\n        vad_audio_path = extract_speech_audio(audio_path, speech_segs, vad_audio_path)\n        \n        # Transcribe\n        t_start = time.time()\n        result = asr_pipeline(vad_audio_path, generate_kwargs=_build_generate_kwargs())\n        t_elapsed = time.time() - t_start\n        \n        predicted = result['text'].strip()\n        predicted = full_post_process(predicted, file_id)\n        \n        wer = compute_wer(reference, predicted) if predicted else 1.0\n        cer = compute_cer(reference, predicted) if predicted else 1.0\n        \n        all_refs.append(reference)\n        all_preds.append(predicted)\n        \n        audio_dur = librosa.get_duration(path=audio_path)\n        rtf = t_elapsed / audio_dur if audio_dur > 0 else 0\n        \n        print(f\"    📏 Audio: {audio_dur:.1f}s\")\n        print(f\"    ⏱️  Time: {t_elapsed:.1f}s (RTF={rtf:.3f})\")\n        print(f\"    📝 REF: {reference[:100]}...\")\n        print(f\"    🤖 HYP: {predicted[:100]}...\")\n        print(f\"    🎯 WER={wer:.2%}  CER={cer:.2%}\\n\")\n    \n    if not all_preds:\n        return None\n    \n    overall_wer = compute_wer(all_refs, all_preds)\n    overall_cer = compute_cer(all_refs, all_preds)\n    \n    print(\"=\" * 50)\n    print(f\"📈 Overall WER: {overall_wer:.2%}\")\n    print(f\"📈 Overall CER: {overall_cer:.2%}\")\n    print(\"=\" * 50)\n    \n    return {'wer': overall_wer, 'cer': overall_cer}\n\n\n# ============================================================================\n# INFERENCE\n# ============================================================================\n\ndef transcribe_files(pipelines, files: List[str], vad_model, get_speech_timestamps_fn, read_audio_fn) -> List[Tuple[str, str]]:\n    \"\"\"Transcribe all files with VAD preprocessing.\"\"\"\n    print(f\"\\n{'=' * 70}\")\n    print(f\"🔄 TRANSCRIBING {len(files)} FILES (v2.3 — beam search)\")\n    print(f\"{'=' * 70}\\n\")\n    \n    os.makedirs(PATHS[\"vad_cache\"], exist_ok=True)\n    \n    predictions = []\n    \n    for idx, file_path in enumerate(files):\n        file_id = Path(file_path).stem\n        \n        try:\n            # Step 1: Apply VAD\n            speech_segs = apply_vad(file_path, vad_model, get_speech_timestamps_fn, read_audio_fn)\n            vad_audio_path = os.path.join(PATHS[\"vad_cache\"], f\"{file_id}_vad.wav\")\n            vad_audio_path = extract_speech_audio(file_path, speech_segs, vad_audio_path)\n            \n            # Step 2: Transcribe\n            t_start = time.time()\n            result = pipelines[0](vad_audio_path, generate_kwargs=_build_generate_kwargs())\n            t_elapsed = time.time() - t_start\n            \n            # Step 3: Post-process\n            text = result['text'].strip()\n            text = full_post_process(text, file_id)\n            \n            predictions.append((file_id, text))\n            \n            if DEBUG_MODE:\n                print(f\"\\n  [{idx+1}/{len(files)}] 📄 {file_id} ({t_elapsed:.0f}s)\")\n                print(f\"  📝 {text[:120]}...\")\n        \n        except Exception as e:\n            print(f\"\\n  ❌ Error: {file_id} — {e}\")\n            import traceback\n            traceback.print_exc()\n            predictions.append((file_id, \"\"))\n    \n    return predictions\n\n\n# ============================================================================\n# SUBMISSION\n# ============================================================================\n\ndef save_submission(predictions: List[Tuple[str, str]]):\n    \"\"\"Save submission.csv.\"\"\"\n    print(f\"\\n💾 Creating submission...\")\n    \n    with open(PATHS[\"submission\"], 'w', encoding='utf-8', newline='') as f:\n        writer = csv.writer(f)\n        writer.writerow(['filename', 'transcript'])\n        for file_id, text in predictions:\n            writer.writerow([file_id, text])\n    \n    print(f\"✓ Saved: {PATHS['submission']} ({len(predictions)} entries)\")\n    \n    print(f\"\\n📋 Samples:\")\n    for file_id, text in predictions[:3]:\n        print(f\"  {file_id}: {text[:80]}...\")\n\n\n# ============================================================================\n# MAIN\n# ============================================================================\n\ndef main():\n    \"\"\"Main v2.3 pipeline.\"\"\"\n    print(\"\\n\" + \"=\" * 70)\n    print(\"🎙️  DL SPRINT 4.0 — Mofa(v2.3) INFERENCE\")\n    print(\"=\" * 70)\n    print(\"  🆕 v2.3 Changes from v2.1:\")\n    print(\"    ✅ num_beams=5 (was 1) — fewer misspellings\")\n    print(\"    ✅ no_repeat_ngram_size=0 (was 4) — no garbled output\")\n    print(\"    ✅ condition_on_prev_tokens=True (was False) — coherence\")\n    print(\"    ✅ batch_size=1 (required for sequential mode)\")\n    print(\"    ✅ Silero VAD still active\")\n    print(\"    ✅ Consecutive-only repetition handling\")\n    print()\n    \n    try:\n        # Load VAD\n        vad_model, get_speech_timestamps_fn, read_audio_fn = load_silero_vad()\n        \n        # Load Whisper\n        pipelines = load_model()\n        \n        # Validate\n        val_results = validate_model(pipelines[0], vad_model, get_speech_timestamps_fn, read_audio_fn)\n        if val_results:\n            print(f\"\\n  → Validation: WER={val_results['wer']:.2%}  CER={val_results['cer']:.2%}\")\n        \n        # Transcribe test files\n        files = find_audio_files(PATHS[\"test_audio\"])\n        predictions = transcribe_files(pipelines, files, vad_model, get_speech_timestamps_fn, read_audio_fn)\n        \n        # Save\n        save_submission(predictions)\n        \n        # Cleanup\n        del pipelines\n        import gc\n        gc.collect()\n        torch.cuda.empty_cache()\n        \n        print(\"\\n\" + \"=\" * 70)\n        print(\"✅ INFERENCE COMPLETE (v2.3)\")\n        print(\"=\" * 70)\n        print(f\"  📁 Submission: {PATHS['submission']}\")\n        if val_results:\n            print(f\"  📈 Val WER: {val_results['wer']:.2%}\")\n        \n        if TEST_MODE:\n            print(\"\\n⚠️  TEST_MODE — Set to False for full run\")\n        else:\n            print(\"\\n✅ Ready to submit! 🚀\")\n    \n    except Exception as e:\n        print(f\"\\n❌ FAILED: {e}\")\n        import traceback\n        traceback.print_exc()\n        raise\n\n\nif __name__ == \"__main__\":\n    main()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-11T17:31:02.667264Z","iopub.execute_input":"2026-02-11T17:31:02.668078Z","execution_failed":"2026-02-11T17:33:19.048Z"}},"outputs":[],"execution_count":null}]}