{"metadata":{"kernelspec":{"name":"python3","display_name":"Python 3"},"language_info":{"name":"python"},"colab":{"provenance":[]},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":129276,"databundleVersionId":15506988,"isSourceIdPinned":false}],"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# DEPENDENCY INSTALLATION","metadata":{"id":"qi942zt38eU4"}},{"cell_type":"code","source":"!pip install -q \"transformers>=4.35.0\" \"accelerate>=0.24.0\"\n!pip install -q torch torchaudio sentencepiece protobuf pandas\n\nprint(\"✅ Dependencies installed.\")","metadata":{"id":"XZYUVXqz8aZr"},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# IMPORTS  ","metadata":{"id":"mjWxDM0G8iAM"}},{"cell_type":"code","source":"import gc\nimport os\nimport re\nimport time\nimport unicodedata\nimport warnings\nfrom pathlib import Path\nfrom typing import Dict, List, Tuple\n\n\nimport pandas as pd\nimport torch\nimport torchaudio\nfrom tqdm.auto import tqdm\nfrom transformers import WhisperForConditionalGeneration, WhisperProcessor\n\nfrom datetime import datetime\nfrom zoneinfo import ZoneInfo\n\nwarnings.filterwarnings(\"ignore\")\nprint(\"✅ Imports ready.\")","metadata":{"id":"WUncg7XU8grP"},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# CONFIGURATION","metadata":{"id":"RVnP2Ed48nOh"}},{"cell_type":"markdown","source":"- All runtime parameters are defined here. Edit before running.","metadata":{"id":"eCcJPSwT8tNG"}},{"cell_type":"code","source":"# ── Model ─────────────────────────────────────────────────────────────────────\nMERGED_MODEL = \"bitwisemind/sam_15000_clean_text_full_model\"\n\n# ── Paths ─────────────────────────────────────────────────────────────────────\nAUDIO_BASE_PATH = (\n    \"/kaggle/input/competitions/\"\n    \"dl-sprint-4-0-bengali-long-form-speech-recognition/\"\n    \"transcription/transcription/test/audio\"\n)\nOUTPUT_CSV = \"/kaggle/working/transcriptions_optimized8.csv\"\n\n# ── Audio File Selection ──────────────────────────────────────────────────────\n# List specific filenames to process, e.g. [\"test_001.wav\", \"test_002.wav\"].\n# Leave empty to process all audio files found in AUDIO_BASE_PATH.\nAUDIO_FILES: List[str] = []\n\n# ── Chunking & Batching ───────────────────────────────────────────────────────\nCHUNK_LENGTH_S = 25\nOVERLAP_S      = 0\nBATCH_SIZE     = 8\n\n# ── Optimization ──────────────────────────────────────────────────────────────\nUSE_STATIC_CACHE  = True   # Enable static KV cache (faster generation on CUDA)\nUSE_TORCH_COMPILE = True\n\n# ── Device ────────────────────────────────────────────────────────────────────\nDEVICE   = \"cuda\" if torch.cuda.is_available() else \"cpu\"\nUSE_FP16 = DEVICE == \"cuda\"   # fp16 only on GPU\n\nprint(\"✅ Configuration loaded.\")\nprint(f\"   Model        : {MERGED_MODEL}\")\nprint(f\"   Device       : {DEVICE}  |  FP16 : {USE_FP16}\")\nprint(f\"   Chunk / Batch: {CHUNK_LENGTH_S}s / {BATCH_SIZE}\")\nprint(f\"   Static cache : {USE_STATIC_CACHE}  |  torch.compile : {USE_TORCH_COMPILE}\")","metadata":{"id":"5JtCLD758viz"},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# AUDIO UTILITIES","metadata":{"id":"LcdUom2_9H8q"}},{"cell_type":"code","source":"def format_time(seconds: float) -> str:\n    \"\"\"Convert a duration in seconds to a human-readable h/m/s string.\"\"\"\n    h, rem = divmod(int(seconds), 3600)\n    m, s   = divmod(rem, 60)\n    if h > 0:   return f\"{h}h {m}m {s}s\"\n    if m > 0:   return f\"{m}m {s}s\"\n    return f\"{s}s\"\n\n\ndef load_and_resample_audio(audio_path: str) -> Tuple[torch.Tensor, int]:\n    \"\"\"Load an audio file, convert to mono, and resample to 16 kHz.\n\n    Returns:\n        waveform (torch.Tensor): 1-D float32 tensor.\n        sample_rate (int): Always 16000 after resampling.\n    \"\"\"\n    waveform, sr = torchaudio.load(audio_path)\n    waveform = waveform.float()\n\n    # Mix down to mono\n    if waveform.shape[0] > 1:\n        waveform = torch.mean(waveform, dim=0, keepdim=True)\n\n    # Resample to 16 kHz (Whisper requirement)\n    if sr != 16000:\n        waveform = torchaudio.transforms.Resample(sr, 16000)(waveform)\n        sr = 16000\n\n    return waveform.squeeze(), sr\n\n\ndef chunk_audio(\n    audio: torch.Tensor,\n    sample_rate: int,\n    chunk_length_s: int,\n    overlap_s: float,\n) -> List[torch.Tensor]:\n    \"\"\"Split a 1-D audio tensor into fixed-length, optionally overlapping chunks.\n\n    The final chunk is zero-padded to `chunk_length_s` so every chunk has\n    the same length (required for batch processing with Whisper).\n    \"\"\"\n    chunk_samples   = int(chunk_length_s * sample_rate)\n    overlap_samples = int(overlap_s      * sample_rate)\n    stride          = chunk_samples - overlap_samples\n    chunks, start   = [], 0\n\n    while start < len(audio):\n        end   = min(start + chunk_samples, len(audio))\n        chunk = audio[start:end]\n\n        # Pad the last chunk so all chunks share the same length\n        if len(chunk) < chunk_samples:\n            chunk = torch.nn.functional.pad(chunk, (0, chunk_samples - len(chunk)))\n\n        chunks.append(chunk)\n        if end >= len(audio):\n            break\n        start += stride\n\n    return chunks\n\n\ndef merge_transcriptions(transcriptions: List[str]) -> str:\n    \"\"\"Join per-chunk transcriptions into a single string and collapse whitespace.\"\"\"\n    if not transcriptions:\n        return \"\"\n    return \" \".join(\" \".join(transcriptions).split())\n\n\ndef validate_audio_files(\n    audio_files: List[str], base_path: str\n) -> Tuple[List[Path], List[str]]:\n    \"\"\"Check which requested audio files exist on disk.\n\n    Returns:\n        valid_paths  : List of existing Path objects.\n        missing_files: List of filenames not found.\n    \"\"\"\n    base = Path(base_path)\n    valid, missing = [], []\n    for f in audio_files:\n        p = base / f\n        (valid if p.exists() else missing).append(p if p.exists() else f)\n    return valid, missing\n\n\nprint(\"✅ Audio utilities ready.\")","metadata":{"id":"yPOC_7gm9M27"},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# TEXT NORMALIZATION & POST-PROCESSING","metadata":{"id":"GFTKjtWe9POL"}},{"cell_type":"code","source":"# Zero-width Unicode characters frequently introduced by Bengali ASR outputs\n_ZW_PATTERN = re.compile(r\"[\\u200B-\\u200D\\uFEFF]\")\n\n\ndef normalize_bn_text(text: str) -> str:\n    \"\"\"Normalize Bengali text: NFC unicode, remove zero-width chars, collapse spaces.\"\"\"\n    if not text:\n        return \"\"\n    text = unicodedata.normalize(\"NFC\", str(text))\n    text = _ZW_PATTERN.sub(\"\", text)\n    text = text.replace(\"\\u00A0\", \" \")   # Non-breaking space → regular space\n    return \" \".join(text.split())\n\n\ndef clean_transcript(text: str) -> str:\n    \"\"\"Remove ASR hallucination artifacts from a transcript.\n\n    Handles three hallucination patterns common in Whisper outputs:\n      1. Multi-word phrases repeated 3+ times in sequence.\n      2. Single words repeated 3+ times in sequence.\n      3. Character n-grams repeated without spaces (e.g., \"হেহেহেহে\").\n    Also strips '>>' speaker-change markers.\n    \"\"\"\n    if not text or (isinstance(text, float) and pd.isna(text)):\n        return text\n\n    text = str(text).replace(\">>\", \"\")\n\n    # Iteratively collapse repeated phrases, words, and character n-grams\n    for pattern, replacement in [\n        (r\"\\b((?:\\S+\\s+){1,5}\\S+)(?:\\s+\\1){2,}\\b\", r\"\\1\"),   # repeated phrases\n        (r\"\\b(\\S+)(?:\\s+\\1){2,}\\b\",                 r\"\\1\"),   # repeated words\n        (r\"(.{2,}?)\\1{2,}\",                          r\"\\1\"),   # repeated char-grams\n    ]:\n        prev = None\n        while prev != text:\n            prev = text\n            text = re.sub(pattern, replacement, text)\n\n    return re.sub(r\"\\s+\", \" \", text).strip()\n\n\ndef normalize_and_postprocess(text: str) -> str:\n    \"\"\"Apply Unicode normalization then hallucination-removal to a transcript.\"\"\"\n    return clean_transcript(normalize_bn_text(text))\n\n\nprint(\"✅ Text normalization utilities ready.\")","metadata":{"id":"f09W9Xvu9NuR"},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# MODEL LOADING","metadata":{"id":"ADMZmplG9guH"}},{"cell_type":"markdown","source":"- Loads the merged Whisper model directly from HuggingFace Hub.\n- Optionally enables static KV cache and torch.compile.","metadata":{"id":"zlB9e0YR9jdz"}},{"cell_type":"code","source":"torch.cuda.empty_cache(); gc.collect()\n\nprint(f\"📥 Loading processor and model : {MERGED_MODEL}\")\n\nprocessor = WhisperProcessor.from_pretrained(MERGED_MODEL)\n\nmodel = WhisperForConditionalGeneration.from_pretrained(\n    MERGED_MODEL,\n    torch_dtype    = torch.float16 if USE_FP16 else torch.float32,\n    low_cpu_mem_usage = True,\n)\n\nmodel = model.to(DEVICE)\nmodel.eval()\n\n# ── Static KV Cache ───────────────────────────────────────────────────────────\nif USE_STATIC_CACHE:\n    model.generation_config.cache_implementation = \"static\"\n\n# ── torch.compile ─────────────────────────────────────────────────────────────\n# The first inference call will be slow while the model is compiled;\n# subsequent calls benefit from fused CUDA kernels.\nif USE_TORCH_COMPILE and hasattr(torch, \"compile\") and DEVICE == \"cuda\":\n    try:\n        model = torch.compile(model, mode=\"reduce-overhead\")\n        print(\"   torch.compile  : enabled (first call will be slower)\")\n    except Exception as e:\n        print(f\"   torch.compile  : skipped ({e})\")\n\nprint(f\"✅ Model ready  |  device : {DEVICE}  |  dtype : {next(model.parameters()).dtype}\")\n\nif DEVICE == \"cuda\":\n    alloc = torch.cuda.memory_allocated(0) / 1e9\n    resv  = torch.cuda.memory_reserved(0)  / 1e9\n    print(f\"   VRAM — allocated : {alloc:.2f} GB  |  reserved : {resv:.2f} GB\")","metadata":{"id":"TEj64tzf9i-g"},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# TRANSCRIPTION ENGINE","metadata":{"id":"TY2KcA4Q9w9X"}},{"cell_type":"code","source":"def transcribe_chunks_batch(\n    audio_chunks: List[torch.Tensor],\n    processor: WhisperProcessor,\n    model: WhisperForConditionalGeneration,\n    device: str,\n    use_fp16: bool,\n    batch_size: int,\n    use_static_cache: bool,\n) -> List[str]:\n    \"\"\"Run batched inference over a list of fixed-length audio chunks.\n\n    Processes chunks in groups of `batch_size`, accumulates decoded strings,\n    and clears the CUDA cache periodically to prevent memory fragmentation.\n    \"\"\"\n    all_transcriptions = []\n\n    for i in tqdm(range(0, len(audio_chunks), batch_size), desc=\"Batches\", leave=False):\n        batch = audio_chunks[i : i + batch_size]\n        try:\n            inputs = processor(\n                [chunk.numpy() for chunk in batch],\n                sampling_rate = 16000,\n                return_tensors = \"pt\",\n                padding = True,\n            )\n            input_features = inputs.input_features.to(device)\n            if use_fp16:\n                input_features = input_features.half()\n\n            gen_kwargs = {\n                \"max_length\": 448,\n                \"num_beams\":  4,\n                \"do_sample\":  False,\n                \"use_cache\":  True,\n            }\n            if use_static_cache:\n                gen_kwargs[\"cache_implementation\"] = \"static\"\n\n            with torch.no_grad():\n                predicted_ids = model.generate(input_features, **gen_kwargs)\n\n            decoded = processor.batch_decode(predicted_ids, skip_special_tokens=True)\n            all_transcriptions.extend([t.strip() for t in decoded])\n\n            # Periodic CUDA cache flush to reduce fragmentation\n            if device == \"cuda\" and i % (batch_size * 4) == 0:\n                torch.cuda.empty_cache()\n\n        except Exception as e:\n            print(f\"   ⚠️  Batch error at chunk {i}: {e}\")\n            all_transcriptions.extend([\"\"] * len(batch))\n\n    return all_transcriptions\n\n\ndef transcribe_audio_file(\n    audio_path: str,\n    processor: WhisperProcessor,\n    model: WhisperForConditionalGeneration,\n    device: str,\n    use_fp16: bool,\n    chunk_length_s: int,\n    overlap_s: float,\n    batch_size: int,\n    use_static_cache: bool,\n) -> Dict:\n    \"\"\"End-to-end transcription for a single audio file.\n\n    Pipeline: load → chunk → batch inference → merge → normalize & clean.\n\n    Returns a dict with filename, transcript, duration, chunk count,\n    processing time, and the original file path.\n    \"\"\"\n    name       = Path(audio_path).name\n    start_time = time.time()\n\n    # Load & chunk\n    audio, sr = load_and_resample_audio(audio_path)\n    duration  = len(audio) / sr\n    chunks    = chunk_audio(audio, sr, chunk_length_s, overlap_s)\n\n    print(f\"   {name}  |  {format_time(duration)}  |  {len(chunks)} chunks\")\n\n    # Inference\n    raw_transcriptions = transcribe_chunks_batch(\n        chunks, processor, model, device, use_fp16, batch_size, use_static_cache\n    )\n\n    # Merge and clean\n    transcript = normalize_and_postprocess(merge_transcriptions(raw_transcriptions))\n    elapsed    = time.time() - start_time\n\n    print(f\"   ✓ Done in {format_time(elapsed)}  |  speed {duration/elapsed:.2f}× realtime  |  {len(transcript)} chars\")\n\n    return {\n        \"filename\":           Path(audio_path).stem,\n        \"transcript\":         transcript,\n        \"duration_s\":         round(duration, 2),\n        \"num_chunks\":         len(chunks),\n        \"processing_time_s\":  round(elapsed, 2),\n        \"audio_path\":         audio_path,\n    }\n\n\ndef process_multiple_files(\n    audio_files: List[str],\n    base_path: str,\n    output_csv: str,\n    processor: WhisperProcessor,\n    model: WhisperForConditionalGeneration,\n    device: str,\n    use_fp16: bool,\n    chunk_length_s: int,\n    overlap_s: float,\n    batch_size: int,\n    use_static_cache: bool,\n) -> pd.DataFrame:\n    \"\"\"Transcribe a list of audio files and write results to CSV.\n\n    If AUDIO_FILES is empty, discovers all audio files in base_path automatically.\n    Missing files are reported and skipped. Errors per file are caught and logged\n    without stopping the overall pipeline.\n    \"\"\"\n    # ── Resolve file list ──────────────────────────────────────────────────────\n    if not audio_files:\n        # Auto-discover all audio files in the directory\n        audio_files = [\n            p.name for p in Path(base_path).iterdir()\n            if p.suffix.lower() in (\".wav\", \".mp3\", \".flac\", \".ogg\", \".m4a\")\n        ]\n        print(f\"   Auto-discovered {len(audio_files)} audio file(s).\")\n\n    valid_paths, missing = validate_audio_files(audio_files, base_path)\n\n    if missing:\n        print(f\"   ⚠️  {len(missing)} file(s) not found and will be skipped.\")\n\n    if not valid_paths:\n        print(\"❌ No valid audio files to process.\")\n        return pd.DataFrame()\n\n    # ── Process each file ──────────────────────────────────────────────────────\n    results     = []\n    total_start = time.time()\n\n    for i, audio_path in enumerate(valid_paths, 1):\n        print(f\"\\n[{i}/{len(valid_paths)}]\")\n        try:\n            result = transcribe_audio_file(\n                str(audio_path), processor, model, device, use_fp16,\n                chunk_length_s, overlap_s, batch_size, use_static_cache,\n            )\n            results.append(result)\n        except Exception as e:\n            import traceback\n            print(f\"   ❌ Failed: {e}\")\n            traceback.print_exc()\n            results.append({\n                \"filename\":          audio_path.stem,\n                \"transcript\":        f\"[ERROR: {e}]\",\n                \"duration_s\":        0,\n                \"num_chunks\":        0,\n                \"processing_time_s\": 0,\n                \"audio_path\":        str(audio_path),\n            })\n        finally:\n            gc.collect()\n            if device == \"cuda\":\n                torch.cuda.empty_cache()\n\n    # ── Write output CSV ───────────────────────────────────────────────────────\n    df = pd.DataFrame(results)\n    Path(output_csv).parent.mkdir(parents=True, exist_ok=True)\n    df[[\"filename\", \"transcript\"]].to_csv(output_csv, index=False, encoding=\"utf-8-sig\")\n\n    total_elapsed = time.time() - total_start\n    print(f\"\\n✅ {len(results)} file(s) processed in {format_time(total_elapsed)}\")\n    print(f\"   Output → {output_csv}\")\n\n    return df\n\n\nprint(\"✅ Transcription engine ready.\")","metadata":{"id":"JK0WZNzd91Aq"},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# RUN INFERENCE","metadata":{"id":"U-jI2gVE92-B"}},{"cell_type":"code","source":"inference_start = time.time()\nprint(\"=\" * 65)\nprint(\"🚀  INFERENCE STARTED\")\nprint(f\"    {datetime.now(ZoneInfo('Asia/Dhaka')).strftime('%Y-%m-%d %H:%M:%S')}\")\nprint(\"=\" * 65)\n\ndf_results = process_multiple_files(\n    audio_files      = AUDIO_FILES,\n    base_path        = AUDIO_BASE_PATH,\n    output_csv       = OUTPUT_CSV,\n    processor        = processor,\n    model            = model,\n    device           = DEVICE,\n    use_fp16         = USE_FP16,\n    chunk_length_s   = CHUNK_LENGTH_S,\n    overlap_s        = OVERLAP_S,\n    batch_size       = BATCH_SIZE,\n    use_static_cache = USE_STATIC_CACHE,\n)\n\ntotal_time = time.time() - inference_start\nprint(\"\\n\" + \"=\" * 65)\nprint(\"🏁  INFERENCE COMPLETE\")\nprint(f\"    Total time : {format_time(total_time)}\")\nprint(\"=\" * 65)","metadata":{"id":"E5E6FiM9913G"},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# RESULTS PREVIEW","metadata":{"id":"6tKu3yMW-J5c"}},{"cell_type":"code","source":"if not df_results.empty:\n    print(f\"\\n{'FILE':<40} {'CHARS':>6}  PREVIEW\")\n    print(\"─\" * 90)\n    for _, row in df_results.iterrows():\n        preview = str(row[\"transcript\"])[:80].replace(\"\\n\", \" \")\n        print(f\"  {row['filename']:<38} {len(str(row['transcript'])):>6}  {preview}…\")\n    print(f\"\\n✨ Results saved → {OUTPUT_CSV}\")\nelse:\n    print(\"⚠️  No results to display.\")","metadata":{"id":"ABVPAtfV-IkF"},"outputs":[],"execution_count":null}]}