{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","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":[{"sourceId":129276,"databundleVersionId":15506988,"sourceType":"competition"}],"dockerImageVersionId":31260,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"id":"3de6d131","cell_type":"markdown","source":"# DL Sprint 4.0 ASR (Quality-First, Debug-Friendly)\n\nThis notebook is optimized for **accuracy** on this competition:\n- Bangla-specialized Whisper model\n- forced Bengali decoding\n- VAD-based long-form segmentation\n- separate debug cells\n\nRun cells in order.\n","metadata":{}},{"id":"2fe5e964","cell_type":"code","source":"!pip install -q \"transformers==4.39.3\" \"datasets==2.17.1\" \"accelerate==0.28.0\" \"peft==0.10.0\" torchaudio soundfile librosa jiwer tqdm pandas numpy bnunicodenormalizer\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"id":"8a08cbbd","cell_type":"markdown","source":"## 1) Imports and Warning Filters\n","metadata":{}},{"id":"0e198d39","cell_type":"code","source":"import gc\nimport os\nimport re\nimport time\nimport unicodedata\nimport warnings\nfrom pathlib import Path\n\n# Hide noisy deprecation warning from jupyter internals.\nwarnings.filterwarnings(\n    \"ignore\",\n    category=DeprecationWarning,\n    message=r\".*datetime\\.datetime\\.utcnow\\(\\) is deprecated.*\",\n)\n\nos.environ[\"TOKENIZERS_PARALLELISM\"] = \"false\"\n\nimport numpy as np\nimport pandas as pd\nimport soundfile as sf\nimport torch\nfrom tqdm.auto import tqdm\nfrom jiwer import wer\nfrom bnunicodenormalizer import Normalizer\n\nSEED = 42\nnp.random.seed(SEED)\ntorch.manual_seed(SEED)\nif torch.cuda.is_available():\n    torch.cuda.manual_seed_all(SEED)\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"id":"c8e946c1","cell_type":"markdown","source":"## 2) Configuration\n\nIf you have your LoRA adapter uploaded as a Kaggle dataset, set `ADAPTER_DIR`.\n","metadata":{}},{"id":"c775000e","cell_type":"code","source":"\nDEVICE = \"cuda\" if torch.cuda.is_available() else \"cpu\"\nTORCH_DTYPE = torch.float16 if DEVICE == \"cuda\" else torch.float32\n\n# Bangla-specialized base model\nMODEL_ID = os.environ.get(\"MODEL_ID\", \"bengaliAI/tugstugi_bengaliai-regional-asr_whisper-medium\")\n\n# Optional adapter path (if uploaded to Kaggle Input)\n# Example: \"/kaggle/input/whisper-bangla-lora/whisper_bn_lora_balanced\"\nADAPTER_DIR = os.environ.get(\"ADAPTER_DIR\", \"\")\nUSE_ADAPTER = bool(ADAPTER_DIR)\n\n# Primary decoding (warning-safe defaults)\nGEN_KWARGS = {\n    \"num_beams\": int(os.environ.get(\"NUM_BEAMS\", \"5\")),\n    \"do_sample\": os.environ.get(\"DO_SAMPLE\", \"false\").lower() == \"true\",\n    \"no_repeat_ngram_size\": int(os.environ.get(\"NO_REPEAT_NGRAM_SIZE\", \"3\")),\n}\n_tmp_temp = os.environ.get(\"TEMPERATURE\", \"\").strip()\nif _tmp_temp:\n    GEN_KWARGS[\"temperature\"] = float(_tmp_temp)\n\n# Retry decoding for bad chunks (quality-first)\nENABLE_BAD_CHUNK_RETRY = os.environ.get(\"ENABLE_BAD_CHUNK_RETRY\", \"true\").lower() == \"true\"\nRETRY_GEN_KWARGS = {\n    \"num_beams\": int(os.environ.get(\"RETRY_NUM_BEAMS\", \"8\")),\n    \"do_sample\": os.environ.get(\"RETRY_DO_SAMPLE\", \"false\").lower() == \"true\",\n    \"no_repeat_ngram_size\": int(os.environ.get(\"RETRY_NO_REPEAT_NGRAM\", \"4\")),\n}\n_tmp_rtemp = os.environ.get(\"RETRY_TEMPERATURE\", \"\").strip()\nif _tmp_rtemp:\n    RETRY_GEN_KWARGS[\"temperature\"] = float(_tmp_rtemp)\n\nMAX_RETRY_CHUNKS_PER_FILE = int(os.environ.get(\"MAX_RETRY_CHUNKS_PER_FILE\", \"40\"))\nMIN_BN_RATIO_FOR_RETRY = float(os.environ.get(\"MIN_BN_RATIO_FOR_RETRY\", \"0.78\"))\nMAX_REPEAT_RUN_FOR_RETRY = int(os.environ.get(\"MAX_REPEAT_RUN_FOR_RETRY\", \"4\"))\n\n# VAD / segmentation settings\nUSE_VAD = os.environ.get(\"USE_VAD\", \"true\").lower() == \"true\"\nVAD_PARAMS = {\n    \"top_db\": float(os.environ.get(\"VAD_TOP_DB\", \"40\")),\n    \"min_speech_s\": float(os.environ.get(\"VAD_MIN_SPEECH_S\", \"2.0\")),\n    \"min_silence_s\": float(os.environ.get(\"VAD_MIN_SILENCE_S\", \"0.6\")),\n    \"pad_s\": float(os.environ.get(\"VAD_PAD_S\", \"0.1\")),\n    \"max_chunk_s\": float(os.environ.get(\"VAD_MAX_CHUNK_S\", \"30.0\")),\n    \"overlap_s\": float(os.environ.get(\"VAD_OVERLAP_S\", \"1.0\")),\n}\n\nCHUNK_BATCH_SIZE = int(os.environ.get(\"CHUNK_BATCH_SIZE\", \"4\"))\nTARGET_SR = 16000\n\n# Debug controls\nTEST_ONE_ID = os.environ.get(\"TEST_ONE_ID\", \"\")\nQUICK_TEST_MAX_SECONDS = int(os.environ.get(\"QUICK_TEST_MAX_SECONDS\", \"300\"))\nWER_CHECK_SECONDS = int(os.environ.get(\"WER_CHECK_SECONDS\", \"0\"))  # 0 => full file WER\nRUN_FULL_INFERENCE = os.environ.get(\"RUN_FULL_INFERENCE\", \"true\").lower() == \"true\"\n\n# Output\nOUTPUT_CSV = os.environ.get(\"OUTPUT_CSV\", \"/kaggle/working/submission.csv\")\n\nprint(\"DEVICE:\", DEVICE)\nprint(\"MODEL_ID:\", MODEL_ID)\nprint(\"USE_ADAPTER:\", USE_ADAPTER)\nprint(\"USE_VAD:\", USE_VAD)\nprint(\"GEN_KWARGS:\", GEN_KWARGS)\nprint(\"ENABLE_BAD_CHUNK_RETRY:\", ENABLE_BAD_CHUNK_RETRY)\nprint(\"RETRY_GEN_KWARGS:\", RETRY_GEN_KWARGS)\nprint(\"RUN_FULL_INFERENCE:\", RUN_FULL_INFERENCE)\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"id":"b7e6bdb4","cell_type":"markdown","source":"## 3) Paths\n","metadata":{}},{"id":"ecfc6603","cell_type":"code","source":"def find_data_root(base=\"/kaggle/input\"):\n    base = Path(base)\n    if not base.exists():\n        return None\n    for p in base.rglob(\"train/audio\"):\n        root = p.parent.parent\n        if (root / \"train\" / \"annotation\").exists() and (root / \"test\" / \"audio\").exists():\n            return root\n    return None\n\nDATA_ROOT = os.environ.get(\"DATA_ROOT\", \"\")\nif DATA_ROOT:\n    data_root = Path(DATA_ROOT)\nelse:\n    data_root = find_data_root()\n\nif data_root is None:\n    raise FileNotFoundError(\"Could not find data root. Set DATA_ROOT to transcription/transcription path.\")\n\nTRAIN_AUDIO_DIR = data_root / \"train\" / \"audio\"\nTRAIN_ANN_DIR = data_root / \"train\" / \"annotation\"\nTEST_AUDIO_DIR = data_root / \"test\" / \"audio\"\n\nsample_candidates = []\nfor parent in [data_root, data_root.parent, data_root.parent.parent]:\n    if parent.exists():\n        sample_candidates.extend(sorted(parent.glob(\"sample_submission*.csv\")))\nif not sample_candidates:\n    raise FileNotFoundError(\"sample_submission*.csv not found\")\nSAMPLE_SUB_PATH = sample_candidates[0]\n\nprint(\"DATA_ROOT:\", data_root)\nprint(\"SAMPLE_SUB_PATH:\", SAMPLE_SUB_PATH)\nprint(\"Train wav count:\", len(list(TRAIN_AUDIO_DIR.glob(\"*.wav\"))))\nprint(\"Test wav count:\", len(list(TEST_AUDIO_DIR.glob(\"*.wav\"))))\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"id":"4dca6724","cell_type":"markdown","source":"## 4) Load Submission Template\n","metadata":{}},{"id":"bf8cf39c","cell_type":"code","source":"sample_sub = pd.read_csv(SAMPLE_SUB_PATH)\nfile_col = sample_sub.columns[0]\npred_col = sample_sub.columns[1]\n\ntest_ids = sample_sub[file_col].astype(str).tolist()\n\nactual_files = [f for f in os.listdir(TEST_AUDIO_DIR) if f.lower().endswith('.wav')]\nactual_set = set(actual_files)\nstem_to_file = {Path(f).stem: f for f in actual_files}\n\ndef resolve_audio_path(fid: str):\n    if fid in actual_set:\n        return TEST_AUDIO_DIR / fid\n    if (fid + '.wav') in actual_set:\n        return TEST_AUDIO_DIR / (fid + '.wav')\n    if fid in stem_to_file:\n        return TEST_AUDIO_DIR / stem_to_file[fid]\n    return None\n\nprint(\"Submission rows:\", len(test_ids))\nprint(sample_sub.head(2))\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"id":"aa8a723f","cell_type":"markdown","source":"## 5) Text Normalization\n","metadata":{}},{"id":"e6dc4033","cell_type":"code","source":"bn_norm = Normalizer()\nPUNCT_RE = re.compile(r\"\"\"[!\"#$%&'()*+,\\-./:;<=>?@\\[\\]^_`{|}~।॥…—–“”‘’]\"\"\", re.UNICODE)\nZW_RE = re.compile(r\"[​‌‍﻿‎‏]\")\n\nUSE_BNUNICODE_NORMALIZER = os.environ.get(\"USE_BNUNICODE_NORMALIZER\", \"true\").lower() == \"true\"\n\n\ndef normalize_text(text: str) -> str:\n    if not text:\n        return \"\"\n    text = unicodedata.normalize(\"NFC\", text)\n    if USE_BNUNICODE_NORMALIZER:\n        try:\n            out = bn_norm.normalize(text)\n            if isinstance(out, dict) and \"normalized\" in out:\n                text = out[\"normalized\"]\n        except Exception:\n            pass\n    text = ZW_RE.sub(\"\", text)\n    text = PUNCT_RE.sub(\" \", text)\n    text = re.sub(r\"\\s+\", \" \", text).strip()\n    return unicodedata.normalize(\"NFC\", text)\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"id":"4f7dd3b9","cell_type":"markdown","source":"## 6) Audio Utilities\n","metadata":{}},{"id":"15e2c9f1","cell_type":"code","source":"def safe_load_audio(path, target_sr=TARGET_SR):\n    audio, sr = sf.read(str(path), dtype=\"float32\", always_2d=False)\n    if audio.ndim == 2:\n        audio = audio.mean(axis=1)\n    if sr != target_sr:\n        import librosa\n        audio = librosa.resample(audio, orig_sr=sr, target_sr=target_sr).astype(np.float32)\n        sr = target_sr\n    return audio, sr\n\n\ndef vad_split(audio, sr,\n              top_db=40,\n              min_speech_s=2.0,\n              min_silence_s=0.6,\n              pad_s=0.1,\n              max_chunk_s=30.0,\n              overlap_s=1.0,\n              frame_ms=30,\n              hop_ms=10):\n    if audio.ndim > 1:\n        audio = audio.mean(axis=1)\n\n    frame_len = int(sr * frame_ms / 1000)\n    hop = int(sr * hop_ms / 1000)\n    if len(audio) < frame_len:\n        return [(0, len(audio))]\n\n    n = 1 + (len(audio) - frame_len) // hop\n    shape = (n, frame_len)\n    strides = (audio.strides[0] * hop, audio.strides[0])\n    frames = np.lib.stride_tricks.as_strided(audio, shape=shape, strides=strides)\n\n    rms = np.sqrt(np.mean(frames**2, axis=1) + 1e-12)\n    rms_db = 20 * np.log10(rms + 1e-12)\n    thresh = rms_db.max() - top_db\n    speech = rms_db > thresh\n\n    segments = []\n    start = None\n    for i, s in enumerate(speech):\n        if s and start is None:\n            start = i\n        elif not s and start is not None:\n            segments.append((start, i - 1))\n            start = None\n    if start is not None:\n        segments.append((start, len(speech) - 1))\n\n    pad = int(pad_s * sr)\n    seg_samples = []\n    for s, e in segments:\n        st = max(0, s * hop - pad)\n        ed = min(len(audio), e * hop + frame_len + pad)\n        seg_samples.append((st, ed))\n\n    merged = []\n    gap = int(min_silence_s * sr)\n    for st, ed in seg_samples:\n        if not merged:\n            merged.append([st, ed])\n        else:\n            if st - merged[-1][1] <= gap:\n                merged[-1][1] = ed\n            else:\n                merged.append([st, ed])\n\n    min_len = int(min_speech_s * sr)\n    merged = [(st, ed) for st, ed in merged if (ed - st) >= min_len]\n\n    if not merged:\n        return [(0, len(audio))]\n\n    final = []\n    max_len = int(max_chunk_s * sr)\n    ov = int(overlap_s * sr)\n    for st, ed in merged:\n        if ed - st <= max_len:\n            final.append((st, ed))\n        else:\n            cur = st\n            while cur < ed:\n                nxt = min(ed, cur + max_len)\n                final.append((cur, nxt))\n                cur = nxt - ov if (nxt - ov) > cur else nxt\n\n    return final\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"id":"9d2c1c3c","cell_type":"markdown","source":"## 7) Load Model (Base + Optional LoRA Adapter)\n","metadata":{}},{"id":"ed07f3dc","cell_type":"code","source":"\nfrom transformers import AutoProcessor, AutoModelForSpeechSeq2Seq, pipeline, GenerationConfig\nfrom peft import PeftModel\n\nprocessor = AutoProcessor.from_pretrained(MODEL_ID)\nbase_model = AutoModelForSpeechSeq2Seq.from_pretrained(\n    MODEL_ID,\n    torch_dtype=TORCH_DTYPE,\n    low_cpu_mem_usage=True,\n).to(DEVICE)\n\n# Try to refresh generation config (not always available on community checkpoints)\ntry:\n    base_model.generation_config = GenerationConfig.from_pretrained(MODEL_ID)\nexcept Exception:\n    pass\n\n# Force Bengali decoding via decoder prompt IDs\nforced_ids = None\nif hasattr(processor, \"get_decoder_prompt_ids\"):\n    try:\n        forced_ids = processor.get_decoder_prompt_ids(language=\"bn\", task=\"transcribe\")\n    except Exception:\n        forced_ids = None\n\n\ndef apply_forced_ids(m):\n    if forced_ids is None:\n        return\n    if hasattr(m, \"config\"):\n        m.config.forced_decoder_ids = forced_ids\n    if hasattr(m, \"generation_config\") and m.generation_config is not None:\n        m.generation_config.forced_decoder_ids = forced_ids\n\n\napply_forced_ids(base_model)\n\nmodel = base_model\nif USE_ADAPTER:\n    ad = Path(ADAPTER_DIR)\n    if ad.exists():\n        model = PeftModel.from_pretrained(base_model, ADAPTER_DIR)\n        apply_forced_ids(model)\n        if hasattr(model, \"base_model\"):\n            apply_forced_ids(model.base_model)\n            if hasattr(model.base_model, \"model\"):\n                apply_forced_ids(model.base_model.model)\n        print(\"Loaded adapter from:\", ADAPTER_DIR)\n    else:\n        print(\"Adapter path not found, using base model:\", ADAPTER_DIR)\n\nasr_plain = pipeline(\n    \"automatic-speech-recognition\",\n    model=model,\n    tokenizer=processor.tokenizer,\n    feature_extractor=processor.feature_extractor,\n    device=0 if DEVICE == \"cuda\" else -1,\n    torch_dtype=TORCH_DTYPE,\n)\n\nasr_chunk = pipeline(\n    \"automatic-speech-recognition\",\n    model=model,\n    tokenizer=processor.tokenizer,\n    feature_extractor=processor.feature_extractor,\n    device=0 if DEVICE == \"cuda\" else -1,\n    torch_dtype=TORCH_DTYPE,\n    chunk_length_s=30,\n    stride_length_s=5,\n)\n\n# Strip incompatible / warning-prone kwargs\n\ndef make_safe_gen_kwargs(pipe, gen_kwargs):\n    safe = dict(gen_kwargs)\n    gcfg = getattr(pipe.model, \"generation_config\", None)\n    supports_lang_task = bool(gcfg is not None and hasattr(gcfg, \"lang_to_id\"))\n    if not supports_lang_task:\n        safe.pop(\"language\", None)\n        safe.pop(\"task\", None)\n\n    # Avoid warning: temperature is ignored when do_sample=False\n    if not safe.get(\"do_sample\", False):\n        safe.pop(\"temperature\", None)\n        safe.pop(\"top_k\", None)\n        safe.pop(\"top_p\", None)\n\n    return safe\n\nSAFE_GEN_KWARGS = make_safe_gen_kwargs(asr_plain, GEN_KWARGS)\nSAFE_RETRY_GEN_KWARGS = make_safe_gen_kwargs(asr_plain, RETRY_GEN_KWARGS)\n\nprint(\"SAFE_GEN_KWARGS:\", SAFE_GEN_KWARGS)\nprint(\"SAFE_RETRY_GEN_KWARGS:\", SAFE_RETRY_GEN_KWARGS)\nprint(\"Model and pipelines loaded.\")\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"id":"26d392fb","cell_type":"markdown","source":"## 8) Transcription Helpers\n","metadata":{}},{"id":"3331ce98","cell_type":"code","source":"\ndef _sanitize_gen_kwargs(gen_kwargs):\n    safe = dict(gen_kwargs or {})\n    safe.pop(\"language\", None)\n    safe.pop(\"task\", None)\n    if not safe.get(\"do_sample\", False):\n        safe.pop(\"temperature\", None)\n        safe.pop(\"top_k\", None)\n        safe.pop(\"top_p\", None)\n    return safe\n\n\ndef _bn_ratio(text: str) -> float:\n    chars = [c for c in text if not c.isspace()]\n    if not chars:\n        return 0.0\n    bn = sum(\"ঀ\" <= c <= \"৿\" for c in chars)\n    return bn / len(chars)\n\n\ndef _max_repeat_run(tokens):\n    if not tokens:\n        return 0\n    best = 1\n    run = 1\n    for i in range(1, len(tokens)):\n        if tokens[i] == tokens[i - 1]:\n            run += 1\n            best = max(best, run)\n        else:\n            run = 1\n    return best\n\n\ndef _quality_score(text: str) -> float:\n    toks = text.split()\n    bn = _bn_ratio(text)\n    rep = _max_repeat_run(toks)\n    bad = text.count(\"�\")\n    return (2.5 * bn) + (0.01 * len(toks)) - (0.35 * max(rep - 2, 0)) - (0.5 * bad)\n\n\ndef _looks_bad_chunk(text: str) -> bool:\n    if not text or len(text.split()) <= 1:\n        return True\n    if \"�\" in text:\n        return True\n    if _bn_ratio(text) < MIN_BN_RATIO_FOR_RETRY:\n        return True\n    if _max_repeat_run(text.split()) >= MAX_REPEAT_RUN_FOR_RETRY:\n        return True\n    return False\n\n\ndef _pipe_call(pipe, inp, gk, batch_size=None):\n    safe_gk = _sanitize_gen_kwargs(gk)\n\n    try:\n        if batch_size is None:\n            return pipe(inp, generate_kwargs=safe_gk)\n        return pipe(inp, batch_size=batch_size, generate_kwargs=safe_gk)\n    except ValueError as e:\n        msg = str(e)\n        # Compatibility fallback for old Whisper generation configs.\n        if \"generation config is outdated\" in msg or \"language argument\" in msg:\n            safe_gk.pop(\"language\", None)\n            safe_gk.pop(\"task\", None)\n            if batch_size is None:\n                return pipe(inp, generate_kwargs=safe_gk)\n            return pipe(inp, batch_size=batch_size, generate_kwargs=safe_gk)\n        raise\n\n\ndef transcribe_with_vad_batched(audio, sr, asr_pipe, gen_kwargs, vad_params=None):\n    if vad_params is None:\n        vad_params = {}\n    chunks = vad_split(audio, sr, **vad_params)\n    if not chunks:\n        return \"\"\n\n    # Permanent fix: pass plain numpy arrays (already 16k), not dicts.\n    inputs = [audio[st:ed].astype(np.float32, copy=False) for st, ed in chunks]\n\n    out = _pipe_call(asr_pipe, inputs, gen_kwargs, batch_size=CHUNK_BATCH_SIZE)\n\n    if isinstance(out, dict):\n        texts = [out.get(\"text\", \"\")]\n    else:\n        texts = [o.get(\"text\", \"\") for o in out]\n\n    if ENABLE_BAD_CHUNK_RETRY and texts:\n        bad_ids = [i for i, t in enumerate(texts) if _looks_bad_chunk(t)]\n        if bad_ids:\n            retry_ids = bad_ids[:MAX_RETRY_CHUNKS_PER_FILE]\n            for idx in retry_ids:\n                retry_out = _pipe_call(asr_pipe, inputs[idx], SAFE_RETRY_GEN_KWARGS, batch_size=None)\n                retry_text = retry_out.get(\"text\", \"\") if isinstance(retry_out, dict) else str(retry_out)\n                if _quality_score(retry_text) > _quality_score(texts[idx]):\n                    texts[idx] = retry_text\n\n    return normalize_text(\" \".join(texts))\n\n\ndef transcribe_file(audio_path, max_seconds=None, use_vad=USE_VAD):\n    audio, sr = safe_load_audio(audio_path)\n    if max_seconds is not None:\n        audio = audio[: int(max_seconds * sr)]\n\n    gk = globals().get(\"SAFE_GEN_KWARGS\", globals().get(\"GEN_KWARGS\", {}))\n\n    if use_vad:\n        pred = transcribe_with_vad_batched(audio, sr, asr_plain, gk, VAD_PARAMS)\n    else:\n        # Permanent fix: use raw array input instead of dict input.\n        out = _pipe_call(asr_chunk, audio.astype(np.float32, copy=False), gk, batch_size=None)\n        pred = normalize_text(out[\"text\"] if isinstance(out, dict) else out)\n\n    del audio\n    gc.collect()\n    if torch.cuda.is_available():\n        torch.cuda.empty_cache()\n    return pred\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"id":"2169fdb0","cell_type":"markdown","source":"## 8b) Runtime Hotfix (If Same Error Persists)\n\nRun this cell once to override stale functions in current kernel.\n","metadata":{}},{"id":"8142103b","cell_type":"code","source":"\n# Emergency runtime hotfix for stale kernel state.\n# If same error appears, run this cell and re-run WER cell.\n\nGEN_KWARGS = {k: v for k, v in dict(globals().get(\"GEN_KWARGS\", {})).items() if k not in {\"language\", \"task\"}}\nRETRY_GEN_KWARGS = {k: v for k, v in dict(globals().get(\"RETRY_GEN_KWARGS\", GEN_KWARGS)).items() if k not in {\"language\", \"task\"}}\nSAFE_GEN_KWARGS = {k: v for k, v in dict(globals().get(\"SAFE_GEN_KWARGS\", GEN_KWARGS)).items() if k not in {\"language\", \"task\"}}\nSAFE_RETRY_GEN_KWARGS = {k: v for k, v in dict(globals().get(\"SAFE_RETRY_GEN_KWARGS\", RETRY_GEN_KWARGS)).items() if k not in {\"language\", \"task\"}}\n\nfor _d in (GEN_KWARGS, RETRY_GEN_KWARGS, SAFE_GEN_KWARGS, SAFE_RETRY_GEN_KWARGS):\n    if not _d.get(\"do_sample\", False):\n        _d.pop(\"temperature\", None)\n        _d.pop(\"top_k\", None)\n        _d.pop(\"top_p\", None)\n\n\ndef _hotfix_pipe_call(pipe, inp, gk, batch_size=None):\n    gk = {k: v for k, v in dict(gk or {}).items() if k not in {\"language\", \"task\"}}\n    if batch_size is None:\n        return pipe(inp, generate_kwargs=gk)\n    return pipe(inp, batch_size=batch_size, generate_kwargs=gk)\n\n\ndef transcribe_with_vad_batched(audio, sr, asr_pipe, gen_kwargs, vad_params=None):\n    if vad_params is None:\n        vad_params = {}\n    chunks = vad_split(audio, sr, **vad_params)\n    if not chunks:\n        return \"\"\n\n    # Use numpy arrays only (no dict format dependency).\n    inputs = [audio[st:ed].astype(np.float32, copy=False) for st, ed in chunks]\n    out = _hotfix_pipe_call(asr_pipe, inputs, gen_kwargs, batch_size=CHUNK_BATCH_SIZE)\n\n    if isinstance(out, dict):\n        texts = [out.get(\"text\", \"\")]\n    else:\n        texts = [o.get(\"text\", \"\") for o in out]\n\n    if ENABLE_BAD_CHUNK_RETRY and texts:\n        bad_ids = [i for i, t in enumerate(texts) if _looks_bad_chunk(t)]\n        for idx in bad_ids[:MAX_RETRY_CHUNKS_PER_FILE]:\n            retry_out = _hotfix_pipe_call(asr_pipe, inputs[idx], SAFE_RETRY_GEN_KWARGS, batch_size=None)\n            retry_text = retry_out.get(\"text\", \"\") if isinstance(retry_out, dict) else str(retry_out)\n            if _quality_score(retry_text) > _quality_score(texts[idx]):\n                texts[idx] = retry_text\n\n    return normalize_text(\" \".join(texts))\n\n\ndef transcribe_file(audio_path, max_seconds=None, use_vad=USE_VAD):\n    audio, sr = safe_load_audio(audio_path)\n    if max_seconds is not None:\n        audio = audio[: int(max_seconds * sr)]\n\n    gk = dict(globals().get(\"SAFE_GEN_KWARGS\", GEN_KWARGS))\n    gk = {k: v for k, v in gk.items() if k not in {\"language\", \"task\"}}\n\n    if use_vad:\n        pred = transcribe_with_vad_batched(audio, sr, asr_plain, gk, VAD_PARAMS)\n    else:\n        out = _hotfix_pipe_call(asr_chunk, audio.astype(np.float32, copy=False), gk)\n        pred = normalize_text(out[\"text\"] if isinstance(out, dict) else out)\n\n    del audio\n    gc.collect()\n    if torch.cuda.is_available():\n        torch.cuda.empty_cache()\n    return pred\n\nprint(\"Hotfix loaded (array-only path). SAFE_GEN_KWARGS:\", SAFE_GEN_KWARGS)\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"id":"709451f1","cell_type":"markdown","source":"## 9) Train WER Probe\n\n- `WER_CHECK_SECONDS = 0` gives real full-file WER (slow but valid).\n- `WER_CHECK_SECONDS > 0` is only a quick transcript sanity probe.\n","metadata":{}},{"id":"624e9c65","cell_type":"code","source":"\"\"\"\ntrain_audio_files = sorted(TRAIN_AUDIO_DIR.glob(\"*.wav\"))\nif not train_audio_files:\n    print(\"No train files found.\")\nelse:\n    train_path = train_audio_files[0]\n    ann_path = TRAIN_ANN_DIR / f\"{train_path.stem}.txt\"\n\n    ref = ann_path.read_text(encoding=\"utf-8\").strip() if ann_path.exists() else \"\"\n\n    max_s = None if WER_CHECK_SECONDS == 0 else WER_CHECK_SECONDS\n    t0 = time.time()\n    pred = transcribe_file(train_path, max_seconds=max_s, use_vad=USE_VAD)\n    elapsed = time.time() - t0\n\n    ref_n = normalize_text(ref)\n    pred_n = normalize_text(pred)\n\n    info = sf.info(str(train_path))\n    used_dur = info.frames / info.samplerate if max_s is None else min(max_s, info.frames / info.samplerate)\n\n    print(\"Train file:\", train_path.name)\n    print(f\"Audio seconds used: {used_dur:.1f}\")\n    print(f\"Time: {elapsed:.1f}s\")\n    print(\"REF:\", ref_n[:800])\n    print(\"PRED:\", pred_n[:800])\n\n    if WER_CHECK_SECONDS == 0 and ref_n:\n        print(\"WER:\", wer(ref_n, pred_n))\n    else:\n        print(\"WER skipped for clipped probe.\")\n\"\"\"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"id":"7d306a4a","cell_type":"markdown","source":"## 10) Auto Config Sweep (Train File)\n\nRuns a small quality-focused sweep on one train file and applies the best config globally.\n\nDefault uses full-file WER for valid comparison.\n","metadata":{}},{"id":"9b1ef4f8","cell_type":"code","source":"\n# Sweep settings\nSWEEP_TARGET_FILE = os.environ.get(\"SWEEP_TARGET_FILE\", \"train_001.wav\")\nSWEEP_USE_FULL_FILE = os.environ.get(\"SWEEP_USE_FULL_FILE\", \"true\").lower() == \"true\"\nSWEEP_MAX_SECONDS = int(os.environ.get(\"SWEEP_MAX_SECONDS\", \"1200\"))\n\n# Candidate configs (quality-first)\nCANDIDATE_CONFIGS = [\n    {\n        \"name\": \"baseline_q\",\n        \"gen\": {\"num_beams\": 5, \"do_sample\": False, \"no_repeat_ngram_size\": 3},\n        \"retry\": {\"num_beams\": 8, \"do_sample\": False, \"no_repeat_ngram_size\": 4},\n        \"vad\": {\"top_db\": 40, \"min_speech_s\": 2.0, \"min_silence_s\": 0.6, \"pad_s\": 0.1, \"max_chunk_s\": 30.0, \"overlap_s\": 1.0},\n        \"chunk_batch_size\": 4,\n        \"enable_retry\": True,\n        \"max_retry_chunks\": 40,\n        \"min_bn_ratio\": 0.78,\n        \"max_repeat_run\": 4,\n    },\n    {\n        \"name\": \"strict_vad\",\n        \"gen\": {\"num_beams\": 6, \"do_sample\": False, \"no_repeat_ngram_size\": 3},\n        \"retry\": {\"num_beams\": 10, \"do_sample\": False, \"no_repeat_ngram_size\": 4},\n        \"vad\": {\"top_db\": 38, \"min_speech_s\": 1.8, \"min_silence_s\": 0.5, \"pad_s\": 0.12, \"max_chunk_s\": 26.0, \"overlap_s\": 1.0},\n        \"chunk_batch_size\": 2,\n        \"enable_retry\": True,\n        \"max_retry_chunks\": 80,\n        \"min_bn_ratio\": 0.80,\n        \"max_repeat_run\": 4,\n    },\n    {\n        \"name\": \"ultra_quality\",\n        \"gen\": {\"num_beams\": 7, \"do_sample\": False, \"no_repeat_ngram_size\": 4},\n        \"retry\": {\"num_beams\": 12, \"do_sample\": False, \"no_repeat_ngram_size\": 4},\n        \"vad\": {\"top_db\": 37, \"min_speech_s\": 1.6, \"min_silence_s\": 0.45, \"pad_s\": 0.12, \"max_chunk_s\": 24.0, \"overlap_s\": 1.0},\n        \"chunk_batch_size\": 1,\n        \"enable_retry\": True,\n        \"max_retry_chunks\": 120,\n        \"min_bn_ratio\": 0.82,\n        \"max_repeat_run\": 4,\n    },\n]\n\n# Resolve target train file\nif SWEEP_TARGET_FILE.endswith('.wav'):\n    target_path = TRAIN_AUDIO_DIR / SWEEP_TARGET_FILE\nelse:\n    target_path = TRAIN_AUDIO_DIR / f\"{SWEEP_TARGET_FILE}.wav\"\nif not target_path.exists():\n    train_audio_files = sorted(TRAIN_AUDIO_DIR.glob(\"*.wav\"))\n    if not train_audio_files:\n        raise FileNotFoundError(\"No train files found for sweep.\")\n    target_path = train_audio_files[0]\n\nann_path = TRAIN_ANN_DIR / f\"{target_path.stem}.txt\"\nif not ann_path.exists():\n    raise FileNotFoundError(f\"Missing annotation for sweep file: {target_path.name}\")\n\nref = ann_path.read_text(encoding=\"utf-8\").strip()\nref_n = normalize_text(ref)\n\ninfo = sf.info(str(target_path))\ntotal_dur = info.frames / info.samplerate\nmax_s = None if SWEEP_USE_FULL_FILE else min(SWEEP_MAX_SECONDS, int(total_dur))\n\nprint(\"Sweep file:\", target_path.name)\nprint(\"Total duration (s):\", round(total_dur, 1))\nprint(\"Using full file:\", SWEEP_USE_FULL_FILE)\nif not SWEEP_USE_FULL_FILE:\n    print(\"Using first seconds:\", max_s)\n\nrows = []\n\nfor cfg in CANDIDATE_CONFIGS:\n    GEN_KWARGS = dict(cfg[\"gen\"])\n    RETRY_GEN_KWARGS = dict(cfg[\"retry\"])\n    SAFE_GEN_KWARGS = make_safe_gen_kwargs(asr_plain, GEN_KWARGS)\n    SAFE_RETRY_GEN_KWARGS = make_safe_gen_kwargs(asr_plain, RETRY_GEN_KWARGS)\n\n    VAD_PARAMS = dict(cfg[\"vad\"])\n    CHUNK_BATCH_SIZE = int(cfg[\"chunk_batch_size\"])\n    ENABLE_BAD_CHUNK_RETRY = bool(cfg[\"enable_retry\"])\n    MAX_RETRY_CHUNKS_PER_FILE = int(cfg[\"max_retry_chunks\"])\n    MIN_BN_RATIO_FOR_RETRY = float(cfg[\"min_bn_ratio\"])\n    MAX_REPEAT_RUN_FOR_RETRY = int(cfg[\"max_repeat_run\"])\n\n    t0 = time.time()\n    pred = transcribe_file(target_path, max_seconds=max_s, use_vad=USE_VAD)\n    elapsed = time.time() - t0\n\n    pred_n = normalize_text(pred)\n\n    if SWEEP_USE_FULL_FILE:\n        w = wer(ref_n, pred_n)\n        score_key = w\n    else:\n        w = None\n        score_key = -_quality_score(pred_n)\n\n    row = {\n        \"name\": cfg[\"name\"],\n        \"wer\": w,\n        \"seconds\": elapsed,\n        \"rtf\": elapsed / max((total_dur if max_s is None else max_s), 1e-6),\n        \"score_key\": score_key,\n        \"gen\": cfg[\"gen\"],\n        \"retry\": cfg[\"retry\"],\n        \"vad\": cfg[\"vad\"],\n        \"chunk_batch_size\": cfg[\"chunk_batch_size\"],\n        \"enable_retry\": cfg[\"enable_retry\"],\n        \"max_retry_chunks\": cfg[\"max_retry_chunks\"],\n        \"min_bn_ratio\": cfg[\"min_bn_ratio\"],\n        \"max_repeat_run\": cfg[\"max_repeat_run\"],\n    }\n    rows.append(row)\n\n    print(f\"[{cfg['name']}] time={elapsed:.1f}s rtf={row['rtf']:.4f}\", end=\"\")\n    if w is not None:\n        print(f\" wer={w:.6f}\")\n    else:\n        print(\" wer=SKIPPED(clipped)\")\n\nres_df = pd.DataFrame(rows)\nif SWEEP_USE_FULL_FILE:\n    res_df = res_df.sort_values([\"wer\", \"seconds\"], ascending=[True, True]).reset_index(drop=True)\nelse:\n    res_df = res_df.sort_values([\"score_key\", \"seconds\"], ascending=[True, True]).reset_index(drop=True)\n\nbest_row = res_df.iloc[0].to_dict()\nprint(\"\")\nprint(\"Best config:\", best_row[\"name\"])\n\n# Apply best globally for later cells\nGEN_KWARGS = dict(best_row[\"gen\"])\nRETRY_GEN_KWARGS = dict(best_row[\"retry\"])\nSAFE_GEN_KWARGS = make_safe_gen_kwargs(asr_plain, GEN_KWARGS)\nSAFE_RETRY_GEN_KWARGS = make_safe_gen_kwargs(asr_plain, RETRY_GEN_KWARGS)\nVAD_PARAMS = dict(best_row[\"vad\"])\nCHUNK_BATCH_SIZE = int(best_row[\"chunk_batch_size\"])\nENABLE_BAD_CHUNK_RETRY = bool(best_row[\"enable_retry\"])\nMAX_RETRY_CHUNKS_PER_FILE = int(best_row[\"max_retry_chunks\"])\nMIN_BN_RATIO_FOR_RETRY = float(best_row[\"min_bn_ratio\"])\nMAX_REPEAT_RUN_FOR_RETRY = int(best_row[\"max_repeat_run\"])\n\nprint(\"Applied best config globally.\")\nprint(\"GEN_KWARGS:\", GEN_KWARGS)\nprint(\"RETRY_GEN_KWARGS:\", RETRY_GEN_KWARGS)\nprint(\"VAD_PARAMS:\", VAD_PARAMS)\nprint(\"CHUNK_BATCH_SIZE:\", CHUNK_BATCH_SIZE)\n\nres_df[[\"name\", \"wer\", \"seconds\", \"rtf\", \"chunk_batch_size\", \"max_retry_chunks\"]]\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"id":"d7ecd083","cell_type":"markdown","source":"## 11) Mandatory Single Test-File Inference\n","metadata":{}},{"id":"044fe0fd","cell_type":"code","source":"\"\"\"\none_id = TEST_ONE_ID if TEST_ONE_ID else test_ids[0]\none_path = resolve_audio_path(one_id)\nif one_path is None:\n    raise FileNotFoundError(f\"Could not resolve: {one_id}\")\n\nprint(\"Testing one file:\", one_id, \"->\", one_path.name)\nt0 = time.time()\none_pred = transcribe_file(one_path, max_seconds=QUICK_TEST_MAX_SECONDS, use_vad=USE_VAD)\nelapsed = time.time() - t0\n\nprint(f\"Audio seconds used: {QUICK_TEST_MAX_SECONDS:.1f}\")\nprint(f\"Inference seconds: {elapsed:.1f}\")\nprint(f\"Approx RTF: {elapsed / max(QUICK_TEST_MAX_SECONDS, 1e-6):.4f}\")\nprint(\"Prediction preview:\")\nprint(one_pred[:1200])\n\"\"\"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"id":"7efb3cb6","cell_type":"markdown","source":"## 12) Full Inference + Submission\n\nSet `RUN_FULL_INFERENCE=True` in config before running this cell.\n","metadata":{}},{"id":"f911eb74","cell_type":"code","source":"if not RUN_FULL_INFERENCE:\n    raise SystemExit(\"RUN_FULL_INFERENCE=False. Set it True after validating single-file output.\")\n\npreds = []\nrtfs = []\n\nfor i, fid in enumerate(tqdm(test_ids, desc=\"Transcribing\"), start=1):\n    audio_path = resolve_audio_path(fid)\n    if audio_path is None:\n        preds.append(\"\")\n        continue\n\n    info = sf.info(str(audio_path))\n    dur = info.frames / info.samplerate\n\n    t0 = time.time()\n    pred = transcribe_file(audio_path, max_seconds=None, use_vad=USE_VAD)\n    t = time.time() - t0\n\n    preds.append(pred)\n    rtfs.append(t / max(dur, 1e-3))\n\n    if i % 3 == 0:\n        gc.collect()\n        if torch.cuda.is_available():\n            torch.cuda.empty_cache()\n\nsubmission = pd.DataFrame({file_col: test_ids, pred_col: preds})\nsubmission[pred_col] = submission[pred_col].fillna(\"\").astype(str).map(lambda x: unicodedata.normalize(\"NFC\", x))\n\nsubmission.to_csv(OUTPUT_CSV, index=False)\nprint(\"Saved:\", OUTPUT_CSV)\nprint(\"Filenames match sample:\", submission[file_col].tolist() == sample_sub[file_col].tolist())\nif rtfs:\n    print(\"Mean RTF:\", float(np.mean(rtfs)))\nsubmission.head()\n","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}