Output:
Exit code: 0
Wall time: 0 seconds
Output:
            prompt_payload = self._model_cls.get_generation_prompt(
                audio=wav,
                stt_config=self._stt_config,
                model_config=self._model_config,
                language=lang,
                task_type="transcribe",
                request_prompt=prompt_text,
                to_language=None,
            )

        max_tokens = self._fixed_max_tokens
        if max_tokens is None:
            duration_sec = float(wav.shape[0]) / float(sr) if sr else 0.0
            max_tokens = max(32, int(duration_sec * self._tokens_per_second))
            max_tokens = min(max_tokens, self._max_tokens_cap)

        sampling_params = self._sampling_params_cls(
            temperature=0.0,
            top_p=1.0,
            max_tokens=max_tokens,
            stop_token_ids=self._stop_token_ids,
        )

        outputs = self._llm.generate([prompt_payload], sampling_params)
        text = ""
        if outputs and outputs[0].outputs:
            text = outputs[0].outputs[0].text
        if prompt_text and mode != "proofread":
            text = self._strip_prompt_output(text)

        return ASRResult(
            model=self.name,
            text=str(text).strip(),
            language=lang,
            language_probability=None,
            segments=[],
            meta={"dtype": self.dtype, "tensor_parallel_size": self.tensor_parallel_size},
        )

def _to_iso3(lang: str | None) -> str | None:
    """Convert language code to ISO-639-3 format."""
    if not lang:
        return None
    code = lang.strip().lower()
    if not code:
        return None
    code = code.replace("-", "_")
    if "_" in code:
        code = code.split("_", 1)[0]
    if len(code) == 3:
        return code
    if len(code) == 2:
        return _WHISPER_TO_ISO3.get(code, None)
    return code

class MMSASR:
    """
    MMS ASR backend using CTC decoding (facebook/mms-1b-all).
    Provides architectural diversity from Whisper's encoder-decoder approach.
    """

    def __init__(
        self,
        model_id: str = "facebook/mms-1b-all",
        device: str | None = None,
    ) -> None:
        try:
            import torch
            from transformers import AutoModelForCTC, AutoProcessor
        except Exception as e:  # pragma: no cover
            raise RuntimeError(
                "MMSASR requires torch + transformers. Install with: pip install -e '.[torch]'"
            ) from e

        if device is None:
            device = "cuda" if torch.cuda.is_available() else "cpu"

        self.model_id = model_id
        self.device = device
        self._torch = torch
        self._processor = AutoProcessor.from_pretrained(model_id)
        self._model = AutoModelForCTC.from_pretrained(model_id).to(device)
        self._model.eval()

        # MMS models require set_target_lang for language-specific vocab.
        self._target_lang: str | None = None

    @property
    def name(self) -> str:
        return f"mms-ctc:{self.model_id.split('/')[-1]}"

    def close(self) -> None:
        """Best-effort release of GPU memory."""
        model = getattr(self, "_model", None)
        if model is not None:
            try:
                del self._model
            except Exception:
                pass
        proc = getattr(self, "_processor", None)
        if proc is not None:
            try:
                del self._processor
            except Exception:
                pass

        try:
            gc.collect()
        except Exception:
            pass

        try:
            if self.device == "cuda" and hasattr(self, "_torch"):
                self._torch.cuda.empty_cache()
        except Exception:
            pass

    def _maybe_set_target_lang(self, lang: str | None) -> None:
        """Set target language for MMS multilingual tokenizer."""
        tokenizer = getattr(self._processor, "tokenizer", None)
        set_lang = getattr(tokenizer, "set_target_lang", None) if tokenizer is not None else None
        if not callable(set_lang):
            return

        iso3 = _to_iso3(lang)
        if not iso3:
            return
        if self._target_lang == iso3:
            return
        try:
            set_lang(iso3)
            self._target_lang = iso3
        except Exception:
            # Language not supported by model, continue without setting.
            pass

    def transcribe(
        self,
        audio_path: Path,
        language: str | None = None,
    ) -> ASRResult:
        from maya_transcribe.audio import decode_pcm16

        wav, sr = decode_pcm16(audio_path, sample_rate=16000, mono=True)
        if wav.size == 0:
            return ASRResult(
                model=self.name,
                text="",
                language=language,
                language_probability=None,
                segments=[],
                meta={"device": self.device},
            )

        # Set target language for multilingual vocab.
        self._maybe_set_target_lang(language)

        inputs = self._processor(wav, sampling_rate=sr, return_tensors="pt")
        input_values = inputs.get("input_values")
        if input_values is None:
            input_values = inputs.get("input_features")
        if input_values is None:
            raise RuntimeError("MMS processor did not return input_values or input_features")
        input_values = input_values.to(self.device)

        with self._torch.no_grad():
            logits = self._model(input_values).logits
            predicted_ids = self._torch.argmax(logits, dim=-1)

        text = self._processor.batch_decode(predicted_ids)[0]

        return ASRResult(
            model=self.name,
            text=text.strip(),
            language=self._target_lang or language,
            language_probability=None,
            segments=[],
            meta={"device": self.device, "target_lang": self._target_lang},
        )

class MMSASRRouter:
    """
    Router for MMS ASR with language-specific model loading.
    MMS-1B-all supports 1000+ languages with a single model but benefits from
    language-specific tokenizer configuration.
    """

    def __init__(
        self,
        *,
        model_id: str = "facebook/mms-1b-all",
        device: str | None = None,
    ) -> None:
        self.model_id = model_id
        self.device = device
        self._backend: MMSASR | None = None

    def transcribe(
        self,
        audio_path: Path,
        *,
        language: str | None = None,
    ) -> ASRResult | None:
        if self._backend is None:
            self._backend = MMSASR(model_id=self.model_id, device=self.device)
        return self._backend.transcribe(audio_path=audio_path, language=language)

class HFIndicConformerASR:
    """
    HF Indic Conformer backend (ai4bharat/indic-conformer-600m-multilingual).
    Uses the model's custom forward for CTC/RNNT decoding.
    """

    def __init__(
        self,
        model_id: str = "ai4bharat/indic-conformer-600m-multilingual",
        decode_mode: str = "rnnt",
        device: str | None = None,
    ) -> None:
        try:
            import torch
            from transformers import AutoModel
        except Exception as e:  # pragma: no cover
            raise RuntimeError(
                "HFIndicConformerASR requires transformers + torch. Install with: pip install -e '.[torch]'"
            ) from e

        if device is None:
            device = "cuda" if torch.cuda.is_available() else "cpu"

        self.model_id = model_id
        self.decode_mode = decode_mode.strip().lower() if decode_mode else "rnnt"
        if self.decode_mode not in {"ctc", "rnnt"}:
            self.decode_mode = "rnnt"
        self.device = device
        self._torch = torch

        self._model = AutoModel.from_pretrained(model_id, trust_remote_code=True)
        self._model = self._model.to(device)
        self._model.eval()

    @property
    def name(self) -> str:
        return f"hf-indicconformer:{self.model_id}"

    def transcribe(
        self,
        audio_path: Path,
        language: str | None = None,
    ) -> ASRResult:
        from maya_transcribe.audio import decode_pcm16

        wav, _ = decode_pcm16(audio_path, sample_rate=16000, mono=True)
        if wav.size == 0:
            return ASRResult(
                model=self.name,
                text="",
                language=language,
                language_probability=None,
                segments=[],
                meta={"device": self.device, "decode_mode": self.decode_mode},
            )

        lang = to_whisper_lang(language) or (language.strip().lower() if language else None)
        wav_tensor = self._torch.from_numpy(wav).unsqueeze(0).to(self.device)

        with self._torch.inference_mode():
            output = self._model(wav_tensor, lang, self.decode_mode)

        text = ""
        if isinstance(output, str):
            text = output
        elif isinstance(output, (list, tuple)) and output:
            first = output[0]
            text = first if isinstance(first, str) else str(first)
        elif output is not None:
            text = getattr(output, "text", None) or str(output)

        return ASRResult(
            model=self.name,
            text=str(text).strip(),
            language=language,
            language_probability=None,
            segments=[],
            meta={"device": self.device, "decode_mode": self.decode_mode},
        )

class IndicConformerASR:
    """
    IndicConformer ASR backend using NVIDIA NeMo.
    Provides Conformer architecture diversity from Whisper's encoder-decoder.
    Uses ai4bharat/indicconformer_stt_hi_hybrid_rnnt_large or similar checkpoints.
    """

    def __init__(
        self,
        model_name: str = "ai4bharat/indicconformer_stt_hi_hybrid_rnnt_large",
        device: str | None = None,
    ) -> None:
        try:
            import torch
            import nemo.collections.asr as nemo_asr
        except Exception as e:  # pragma: no cover
            raise RuntimeError(
                "IndicConformerASR requires nemo_toolkit. Install with: pip install nemo_toolkit[asr]"
            ) from e

        if device is None:
            device = "cuda" if torch.cuda.is_available() else "cpu"

        self.model_name = model_name
        self.device = device
        self._torch = torch

        # Load NeMo ASR model from HuggingFace or local path.
        self._model = nemo_asr.models.ASRModel.from_pretrained(model_name=model_name)
        if device == "cuda":
            self._model = self._model.cuda()
        self._model.eval()

    @property
    def name(self) -> str:
        return f"indicconformer:{self.model_name.split('/')[-1]}"

    def close(self) -> None:
        """Best-effort release of GPU memory."""
        model = getattr(self, "_model", None)
        if model is not None:
            try:
                del self._model
            except Exception:
                pass
        try:
            gc.collect()
        except Exception:
            pass
        try:
            if self.device == "cuda" and hasattr(self, "_torch"):
                self._torch.cuda.empty_cache()
        except Exception:
            pass

    def transcribe(
        self,
        audio_path: Path,
        language: str | None = None,
    ) -> ASRResult:
        # NeMo models transcribe directly from file path.
        try:
            texts = self._model.transcribe([str(audio_path)])
            text = texts[0] if texts else ""
        except Exception:
            text = ""

        return ASRResult(
            model=self.name,


Summary: auto

## Skills
A skill is a set of local instructions to follow that is stored in a `SKILL.md` file. Below is the list of skills that can be used. Each entry includes a name, description, and file path so you can open the source for full instructions when using a specific skill.
### Available skills
- skill-creator: Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Codex's capabilities with specialized knowledge, workflows, or tool integrations. (file: /home/ubuntu/.codex/skills/.system/skill-creator/SKILL.md)
- skill-installer: Install Codex skills into $CODEX_HOME/skills from a curated list or a GitHub repo path. Use when a user asks to list installable skills, install a curated skill, or install a skill from another repo (including private repos). (file: /home/ubuntu/.codex/skills/.system/skill-installer/SKILL.md)
### How to use skills
- Discovery: The list above is the skills available in this session (name + description + file path). Skill bodies live on disk at the listed paths.
- Trigger rules: If the user names a skill (with `$SkillName` or plain text) OR the task clearly matches a skill's description shown above, you must use that skill for that turn. Multiple mentions mean use them all. Do not carry skills across turns unless re-mentioned.
- Missing/blocked: If a named skill isn't in the list or the path can't be read, say so briefly and continue with the best fallback.
- How to use a skill (progressive disclosure):
  1) After deciding to use a skill, open its `SKILL.md`. Read only enough to follow the workflow.
  2) If `SKILL.md` points to extra folders such as `references/`, load only the specific files needed for the request; don't bulk-load everything.
  3) If `scripts/` exist, prefer running or patching them instead of retyping large code blocks.
  4) If `assets/` or templates exist, reuse them instead of recreating from scratch.
- Coordination and sequencing:
  - If multiple skills apply, choose the minimal set that covers the request and state the order you'll use them.
  - Announce which skill(s) you're using and why (one short line). If you skip an obvious skill, say why.
- Context hygiene:
  - Keep context small: summarize long sections instead of pasting them; only load extra files when needed.
  - Avoid deep reference-chasing: prefer opening only files directly linked from `SKILL.md` unless you're blocked.
  - When variants exist (frameworks, providers, domains), pick only the relevant reference file(s) and note that choice.
- Safety and fallback: If a skill can't be applied cleanly (missing files, unclear instructions), state the issue, pick the next-best approach, and continue.


**Planning ASR decoding and guard improvements**

I'm outlining changes to add decoding constraints and a new max_tokens formula to the ASR component and to implement a guard/fallback approach where a stable ASR anchors results and an LLM (Voxtral or Gemma) proofreads edits, accepting them only if edits are small and scripts match; this requires pipeline updates to optionally enable this mode, especially for Voxtral which supports proofread mode.


**Clarifying proofread approach and results update policy**

I confirmed Gemma lacks a dedicated proofread mode but can use a prompt with the anchor transcript plus audio to mimic it; Voxtral has explicit proofread support. The re-run on Hindi bench needs adding guard logic with edit distance and length ratio checks in the pipeline and updating Hindi results text or adding a small table only if improvements occur, respecting the user's instructions.


sed -n '1,240p' maya_transcribe/pipeline.py


Output:
Exit code: 0
Wall time: 0 seconds
Output:
from __future__ import annotations

import json
import time
from dataclasses import asdict, replace
from pathlib import Path
from typing import Any

from rich.progress import track

from maya_transcribe.asr import (
    ASRResult,
    FasterWhisperASR,
    HFIndicConformerASR,
    IndicConformerASRRouter,
    MMSASR,
    TransformersWhisperASR,
    TransformersWhisperASRRouter,
    VllmTranscriptionASR,
)
from maya_transcribe.audio import compute_qc, ffprobe, normalize_audio
from maya_transcribe.ids import segment_identity
from maya_transcribe.language import LANG_CODE_TO_NAME
from maya_transcribe.lid import LIDResult, MMSLID
from maya_transcribe.rover import rover_combine
from maya_transcribe.textnorm import normalize_text
from maya_transcribe.verify import verify_segment

def _iter_audio_files(segments_dir: Path) -> list[Path]:
    audio_files: list[Path] = []
    for ext in ("*.wav", "*.mp3", "*.m4a", "*.flac", "*.ogg", "*.opus"):
        audio_files.extend(sorted(segments_dir.rglob(ext)))
    return sorted(set(audio_files))

def _asr_summary(result: ASRResult) -> dict[str, Any]:
    avg_logprobs = [s.avg_logprob for s in result.segments if s.avg_logprob is not None]
    no_speech_probs = [s.no_speech_prob for s in result.segments if s.no_speech_prob is not None]
    compression_ratios = [s.compression_ratio for s in result.segments if s.compression_ratio is not None]

    def mean(xs: list[float]) -> float | None:
        return (sum(xs) / len(xs)) if xs else None

    return {
        "language": result.language,
        "language_probability": result.language_probability,
        "avg_logprob_mean": mean([float(x) for x in avg_logprobs]),
        "no_speech_prob_mean": mean([float(x) for x in no_speech_probs]),
        "compression_ratio_mean": mean([float(x) for x in compression_ratios]),
    }

def _infer_lang_from_relpath(relpath: str) -> str | None:
    parts = [p.strip().lower() for p in (relpath or "").split("/") if p.strip()]
    for p in parts[:-1]:
        if p in LANG_CODE_TO_NAME:
            return p
    return None

DEFAULT_VLLM_PROOFREAD_PROMPT = (
    "Correct transcription errors using the audio. Keep the same language/script. "
    "Do not add new content. Output only the corrected transcript.\n\nTranscript:\n{transcript}"
)

def run_pipeline(
    *,
    segments_dir: Path,
    out_dir: Path,
    lid_model_id: str = "facebook/mms-lid-256",
    whisper_model: str = "large-v3",
    skip_whisper: bool = False,
    vllm_asr_model: str | None = None,
    vllm_gpu_memory_utilization: float | None = None,
    vllm_max_model_len: int | None = None,
    vllm_transcribe_prompt: str | None = None,
    vllm_proofread: bool = False,
    vllm_proofread_prompt: str | None = None,
    extra_transformers_whisper_models: list[str] | None = None,
    transformers_whisper_model_map: dict[str, str] | None = None,
    transformers_whisper_default_model: str | None = None,
    # MMS ASR for CTC-based diversity (per master.md plan).
    mms_asr_model: str | None = None,
    # IndicConformer for Conformer architecture diversity (per master.md plan).
    indicconformer_model_map: dict[str, str] | None = None,
    indicconformer_default_model: str | None = None,
    hf_indicconformer_model: str | None = None,
    hf_indicconformer_decode: str = "rnnt",
    gemini_model: str | None = None,
    gemini_prompt_path: Path | None = None,
    gemini_max_output_tokens: int = 512,
    gemini_hard_only: bool = True,
    ctc_model: str | None = None,
    ctc_model_map: dict[str, str] | None = None,
    require_lid_backend: bool = True,
    trim_silence: bool = True,
    silence_threshold_db: float = -40.0,
    min_silence_sec: float = 0.15,
    limit: int | None = None,
    thresholds: dict[str, Any] | None = None,
) -> Path:
    """
    End-to-end run: preprocess -> LID -> ASR ensemble -> normalize -> ROVER -> verify.
    Writes `out_dir/results.jsonl` and returns that path.
    """
    thresholds = thresholds or {}
    if (ctc_model or ctc_model_map) and "require_ctc" not in thresholds:
        thresholds["require_ctc"] = True
    if (ctc_model or ctc_model_map) and "min_ctc_margin" not in thresholds:
        thresholds["min_ctc_margin"] = 0.05
    out_dir.mkdir(parents=True, exist_ok=True)

    audio_files = _iter_audio_files(segments_dir)
    if not audio_files:
        raise RuntimeError(f"No audio files found under: {segments_dir}")
    if limit is not None:
        audio_files = audio_files[: max(0, int(limit))]

    # Instantiate models once.
    lid_backend: MMSLID | None
    try:
        lid_backend = MMSLID(model_id=lid_model_id)
    except Exception as e:
        if require_lid_backend:
            raise RuntimeError(
                "Failed to initialize MMS-LID. Install torch/transformers deps (pip install -e '.[torch]') "
                "or run with require_lid_backend=False."
            ) from e
        lid_backend = None

    whisper_backend: FasterWhisperASR | None = None
    if not skip_whisper and whisper_model:
        whisper_backend = FasterWhisperASR(model_size_or_path=whisper_model)

    extra_transformers_whisper_models = extra_transformers_whisper_models or []
    transformers_backends: list[TransformersWhisperASR] = []
    for model_id in extra_transformers_whisper_models:
        transformers_backends.append(TransformersWhisperASR(model_id=model_id))

    transformers_router = None
    if transformers_whisper_model_map:
        transformers_router = TransformersWhisperASRRouter(
            model_map=transformers_whisper_model_map, default_model=transformers_whisper_default_model
        )

    # MMS ASR backend for CTC-based diversity (per master.md: "Optional diversity: MMS ASR").
    mms_backend: MMSASR | None = None
    if mms_asr_model:
        try:
            mms_backend = MMSASR(model_id=mms_asr_model)
        except Exception:
            pass  # MMS optional; continue if unavailable.

    # IndicConformer router for architecture diversity (per master.md: "IndicConformer-600M multilingual").
    indicconformer_router: IndicConformerASRRouter | None = None
    if indicconformer_model_map or indicconformer_default_model:
        try:
            indicconformer_router = IndicConformerASRRouter(
                model_map=indicconformer_model_map,
                default_model=indicconformer_default_model,
            )
        except Exception:
            pass  # IndicConformer optional; continue if NeMo unavailable.

    hf_indicconformer_backend: HFIndicConformerASR | None = None
    if hf_indicconformer_model:
        try:
            hf_indicconformer_backend = HFIndicConformerASR(
                model_id=hf_indicconformer_model,
                decode_mode=hf_indicconformer_decode,
            )
        except Exception:
            pass  # HF IndicConformer optional; continue if unavailable.

    ctc_scorer = None
    rank_by_ctc = None
    if ctc_model or ctc_model_map:
        from maya_transcribe.ctc_fit import CTCFitScorer, rank_candidates_by_ctc

        ctc_scorer = CTCFitScorer(model_map=ctc_model_map, default_model=ctc_model)
        rank_by_ctc = rank_candidates_by_ctc

    gemini_backend = None
    if gemini_model:
        try:
            from maya_transcribe.gemini_asr import GeminiASR, GeminiConfig, load_prompt_template

            prompt = load_prompt_template(gemini_prompt_path or Path("Prompt.md"))
            gemini_backend = GeminiASR(
                config=GeminiConfig(model=gemini_model, max_output_tokens=gemini_max_output_tokens),
                prompt_template=prompt,
            )
        except Exception as e:
            raise RuntimeError("Failed to initialize Gemini backend (check GEMINI_KEY and google-genai).") from e

    vllm_backend: VllmTranscriptionASR | None = None
    if vllm_asr_model:
        try:
            vllm_backend = VllmTranscriptionASR(
                model_id=vllm_asr_model,
                gpu_memory_utilization=vllm_gpu_memory_utilization,
                max_model_len=vllm_max_model_len,
            )
        except Exception as e:
            raise RuntimeError("Failed to initialize vLLM ASR backend.") from e

    default_whisper_run_sampling = bool(whisper_backend is not None) and not bool(
        transformers_backends or transformers_router or gemini_backend is not None
    )

    results_path = out_dir / "results.jsonl"
    summary_path = out_dir / "summary.json"

    counts = {"accept": 0, "human": 0, "reject": 0, "error": 0}
    timings_total: list[float] = []

    with results_path.open("w", encoding="utf-8") as f:
        for src in track(audio_files, description="Transcribing"):
            ident = segment_identity(src, root=segments_dir)
            segment_id = ident.segment_id
            t0 = time.perf_counter()

            try:
                # 1) Preprocess
                t_pre0 = time.perf_counter()
                src_info = ffprobe(src)
                wav_path = out_dir / "audio" / f"{segment_id}.wav"
                normalize_audio(
                    src,
                    wav_path,
                    trim_silence=trim_silence,
                    silence_threshold_db=silence_threshold_db,
                    min_silence_sec=min_silence_sec,
                )
                info = ffprobe(wav_path)
                qc = compute_qc(wav_path)
                t_pre1 = time.perf_counter()


Summary: auto


sed -n '240,520p' maya_transcribe/pipeline.py


Output:
Exit code: 0
Wall time: 0 seconds
Output:

                # 2) LID
                t_lid0 = time.perf_counter()
                lid: LIDResult | None = None
                if lid_backend is not None:
                    lid = lid_backend.predict(wav_path, top_k=5)
                t_lid1 = time.perf_counter()

                # Route language hint (only if confident).
                route_lang = None
                route_lang_source = None
                prefer_path = bool(thresholds.get("route_lang_prefer_path", False))
                if prefer_path and bool(thresholds.get("route_lang_from_path", False)):
                    inferred = _infer_lang_from_relpath(ident.relpath)
                    if inferred:
                        route_lang = inferred
                        route_lang_source = "path"

                if route_lang is None and lid and lid.predictions:
                    top1 = lid.predictions[0]
                    top2p = lid.predictions[1].prob if len(lid.predictions) > 1 else 0.0
                    if float(top1.prob) >= float(thresholds.get("lid_min_prob", 0.90)) and (
                        float(top1.prob) - float(top2p)
                    ) >= float(thresholds.get("lid_min_margin", 0.20)):
                        route_lang = top1.code
                        route_lang_source = "lid"

                if route_lang is None and bool(thresholds.get("route_lang_from_path", False)):
                    inferred = _infer_lang_from_relpath(ident.relpath)
                    if inferred:
                        route_lang = inferred
                        route_lang_source = "path"

                # 3) ASR ensemble
                t_asr0 = time.perf_counter()
                asr_results: dict[str, ASRResult] = {}
                asr_errors: dict[str, str] = {}
                primary_asr_key = None
                det = None
                router_key: str | None = None

                # Whisper: deterministic + a small sampling diversity pass.
                if whisper_backend is not None:
                    try:
                        det = whisper_backend.transcribe(
                            wav_path, language=route_lang, beam_size=5, best_of=5, temperature=0.0
                        )
                        primary_asr_key = f"{det.model}|det"
                        asr_results[primary_asr_key] = det
                    except Exception as e:
                        asr_errors[f"{whisper_backend.name}|det"] = f"{type(e).__name__}:{e}"

                run_sampling = bool(thresholds.get("whisper_run_sampling", default_whisper_run_sampling))
                if run_sampling and whisper_backend is not None:
                    try:
                        samp = whisper_backend.transcribe(
                            wav_path, language=route_lang, beam_size=5, best_of=10, temperature=0.2
                        )
                        asr_results[f"{samp.model}|samp_t0.2"] = samp
                    except Exception as e:
                        asr_errors[f"{whisper_backend.name}|samp_t0.2"] = f"{type(e).__name__}:{e}"

                for tf_backend in transformers_backends:
                    try:
                        res = tf_backend.transcribe(wav_path, language=route_lang, num_beams=5, max_new_tokens=256)
                        asr_results[res.model] = res
                    except Exception as e:
                        asr_errors[tf_backend.name] = f"{type(e).__name__}:{e}"

                if transformers_router is not None:
                    try:
                        routed = transformers_router.transcribe(
                            wav_path,
                            language=route_lang,
                            num_beams=5,
                            max_new_tokens=256,
                        )
                        if routed is not None:
                            asr_results[routed.model] = routed
                            router_key = routed.model
                    except Exception as e:
                        asr_errors[f"transformers_router:{route_lang or 'none'}"] = f"{type(e).__name__}:{e}"

                if vllm_backend is not None:
                    try:
                        vllm_res = vllm_backend.transcribe(
                            wav_path, language=route_lang, prompt=vllm_transcribe_prompt
                        )
                    except Exception as e:
                        asr_errors[vllm_backend.name] = f"{type(e).__name__}:{e}"
                    else:
                        if vllm_proofread and vllm_backend.supports_proofread:
                            prompt_tpl = vllm_proofread_prompt or DEFAULT_VLLM_PROOFREAD_PROMPT
                            proof_prompt = prompt_tpl.format(transcript=vllm_res.text)
                            try:
                                proof_res = vllm_backend.transcribe(
                                    wav_path, language=route_lang, prompt=proof_prompt, mode="proofread"
                                )
                                raw_key = f"{vllm_res.model}|raw"
                                proof_key = f"{vllm_res.model}|proofread"
                                asr_results[raw_key] = vllm_res
                                asr_results[proof_key] = replace(proof_res, model=proof_key)
                                if primary_asr_key is None:
                                    primary_asr_key = proof_key
                            except Exception as e:
                                asr_errors[f"{vllm_backend.name}|proofread"] = f"{type(e).__name__}:{e}"
                                asr_results[vllm_res.model] = vllm_res
                        else:
                            asr_results[vllm_res.model] = vllm_res

                # MMS ASR for CTC-based diversity (per master.md plan).
                if mms_backend is not None:
                    try:
                        mms_res = mms_backend.transcribe(wav_path, language=route_lang)
                        asr_results[mms_res.model] = mms_res
                    except Exception as e:
                        asr_errors[f"mms-ctc:{route_lang or 'none'}"] = f"{type(e).__name__}:{e}"

                # IndicConformer for Conformer architecture diversity (per master.md plan).
                if indicconformer_router is not None:
                    try:
                        conformer_res = indicconformer_router.transcribe(wav_path, language=route_lang)
                        if conformer_res is not None:
                            asr_results[conformer_res.model] = conformer_res
                    except Exception as e:
                        asr_errors[f"indicconformer:{route_lang or 'none'}"] = f"{type(e).__name__}:{e}"

                if hf_indicconformer_backend is not None:
                    try:
                        hf_res = hf_indicconformer_backend.transcribe(wav_path, language=route_lang)
                        asr_results[hf_res.model] = hf_res
                    except Exception as e:
                        asr_errors[f"hf-indicconformer:{route_lang or 'none'}"] = f"{type(e).__name__}:{e}"

                if primary_asr_key is None and asr_results:
                    primary_asr_key = sorted(asr_results.keys())[0]
                if not asr_results:
                    raise RuntimeError(f"no_asr_hypotheses:{asr_errors or 'unknown'}")

                # Gemini: optional extra hypothesis (prefer running only on "hard" segments).
                if gemini_backend is not None:
                    run_gemini = True
                    if gemini_hard_only:
                        base_for_heur = det or (next(iter(asr_results.values())) if asr_results else None)
                        det_sum = _asr_summary(base_for_heur) if base_for_heur is not None else {}
                        avg_lp = det_sum.get("avg_logprob_mean")
                        comp = det_sum.get("compression_ratio_mean")
                        no_speech = det_sum.get("no_speech_prob_mean")
                        run_gemini = bool(
                            route_lang is None
                            or (no_speech is not None and float(no_speech) > 0.5)
                            or (avg_lp is not None and float(avg_lp) < float(thresholds.get("min_avg_logprob_mean", -1.0)))
                            or (
                                comp is not None
                                and float(comp) > float(thresholds.get("max_compression_ratio_mean", 2.4))
                            )
                        )

                    if run_gemini:
                        try:
                            g = gemini_backend.transcribe(wav_path, language=route_lang)
                            asr_results[g.model] = g
                        except Exception:
                            pass

                t_asr1 = time.perf_counter()

                ensemble_models = sorted({v.model for v in asr_results.values()})
                ensemble_backends = sorted({v.model.split(":", 1)[0] for v in asr_results.values()})
                ensemble_warnings: list[str] = []
                if len(ensemble_models) < 2:
                    ensemble_warnings.append("low_model_diversity")

                # 4) Normalize hypotheses
                t_norm0 = time.perf_counter()
                norm_hyps: dict[str, str] = {}
                for k, res in asr_results.items():
                    lang_hint = res.language or route_lang
                    norm_hyps[k] = normalize_text(res.text, lang=lang_hint)
                t_norm1 = time.perf_counter()

                # 5) ROVER
                t_rover0 = time.perf_counter()
                rover_hyps = dict(norm_hyps)
                rover_filtered_out: list[str] = []
                if bool(thresholds.get("filter_hypotheses_by_script", False)) and route_lang:
                    from maya_transcribe.language import dominant_script, expected_script

                    expected = expected_script(route_lang)
                    if expected:
                        kept: dict[str, str] = {}
                        for k, res in asr_results.items():
                            script = dominant_script(res.text)
                            if script is None or script == expected:
                                kept[k] = norm_hyps.get(k, "")
                            else:
                                rover_filtered_out.append(k)

                        kept = {k: v for k, v in kept.items() if v.strip()}
                        if kept:
                            rover_hyps = kept
                        else:
                            rover_filtered_out = []

                include_sampling_opt = thresholds.get("rover_include_sampling")
                if include_sampling_opt is None:
                    include_sampling = len(ensemble_backends) < 2
                else:
                    include_sampling = bool(include_sampling_opt)

                if not include_sampling:
                    dropped = [k for k in rover_hyps.keys() if k.endswith("|samp_t0.2")]
                    for k in dropped:
                        rover_hyps.pop(k, None)
                    rover_filtered_out.extend(dropped)

                prefer_router_opt = thresholds.get("rover_prefer_router")
                prefer_router = bool(prefer_router_opt) if prefer_router_opt is not None else (router_key is not None)
                if prefer_router and router_key and router_key in rover_hyps:
                    rover_hyps = {
                        router_key: rover_hyps[router_key],
                        **{k: v for k, v in rover_hyps.items() if k != router_key},
                    }

                if primary_asr_key is not None and primary_asr_key not in rover_hyps:
                    primary_asr_key = sorted(rover_hyps.keys())[0] if rover_hyps else None

                rover = rover_combine(rover_hyps)
                t_rover1 = time.perf_counter()

                # 5.5) Audio-grounded fit scoring (optional): choose best candidate by CTC loss.
                t_ctc0 = time.perf_counter()
                ctc_info = None
                final_text = rover.text
                final_source = "rover"
                if router_key and router_key in rover_hyps and bool(thresholds.get("final_prefer_router", False)):
                    final_text = rover_hyps[router_key]
                    final_source = f"asr:{router_key}"
                elif bool(thresholds.get("final_prefer_transformers", False)):
                    tf_keys = [k for k in rover_hyps.keys() if k.startswith("transformers:")]
                    if tf_keys:
                        preferred = tf_keys[0]
                        final_text = rover_hyps[preferred]
                        final_source = f"asr:{preferred}"
                if ctc_scorer is not None and rank_by_ctc is not None:
                    candidates = {"rover": rover.text, **rover_hyps}
                    ctc_info = rank_by_ctc(ctc_scorer, wav_path, candidates=candidates, lang=route_lang)
                    best_key = ctc_info.get("best_key") if isinstance(ctc_info, dict) else None
                    if best_key and best_key in candidates:
                        final_text = candidates[best_key]
                        final_source = "rover" if best_key == "rover" else f"asr:{best_key}"
                t_ctc1 = time.perf_counter()

                # 6) Verify
                t_ver0 = time.perf_counter()
                asr_summaries = {k: _asr_summary(v) for k, v in asr_results.items()}
                verification = verify_segment(
                    qc=asdict(qc),
                    lid=lid,
                    final_text=final_text,
                    rover={
                        "text": rover.text,
                        "disagreement_rate": rover.disagreement_rate,
                        "skeleton_key": rover.skeleton_key,
                    },
                    asr_summaries=asr_summaries,
                    primary_asr_key=primary_asr_key,
                    ctc=ctc_info,
                    thresholds=thresholds,
                )
                t_ver1 = time.perf_counter()

                record = {
                    "schema_version": 1,
                    "segment_id": segment_id,
                    "base_id": ident.base_id,
                    "relpath": ident.relpath,
                    "uid": ident.uid,
                    "src_path": str(src),
                    "wav_path": str(wav_path),
                    "src_ffprobe": asdict(src_info),


sed -n '520,980p' maya_transcribe/pipeline.py


Output:
Exit code: 0
Wall time: 0 seconds
Output:
                    "src_ffprobe": asdict(src_info),
                    "ffprobe": asdict(info),
                    "qc": asdict(qc),
                    "lid": (
                        None
                        if lid is None
                        else {
                            "model": lid.model,
                            "predictions": [{"code": p.code, "prob": p.prob} for p in lid.predictions],
                        }
                    ),
                    "route": {"language_hint": route_lang, "language_hint_source": route_lang_source},
                    "asr": {
                        k: {
                            "model": v.model,
                            "text": v.text,
                            "language": v.language,
                            "language_probability": v.language_probability,
                            "segments": [asdict(s) for s in v.segments],
                            "meta": v.meta,
                        }
                        for k, v in asr_results.items()
                    },
                    "asr_summary": asr_summaries,
                    "asr_errors": asr_errors,
                    "ensemble": {
                        "unique_models": ensemble_models,
                        "unique_backends": ensemble_backends,
                        "warnings": ensemble_warnings,
                    },
                    "normalized": norm_hyps,
                    "rover": {
                        "text": rover.text,
                        "skeleton_key": rover.skeleton_key,
                        "disagreement_rate": rover.disagreement_rate,
                        "inputs": sorted(rover_hyps.keys()),
                        "filtered_out": sorted(set(rover_filtered_out)),
                        "disagreements": [
                            {"kind": d.kind, "index": d.index, "chosen": d.chosen, "options": d.options}
                            for d in rover.disagreements
                        ],
                    },
                    "ctc": ctc_info,
                    "final": {"text": final_text, "source": final_source},
                    "verification": {
                        "decision": verification.decision,
                        "reasons": verification.reasons,
                        "scores": verification.scores,
                    },
                    "config": {
                        "trim_silence": trim_silence,
                        "silence_threshold_db": silence_threshold_db,
                        "min_silence_sec": min_silence_sec,
                        "gemini_model": gemini_model,
                        "gemini_hard_only": gemini_hard_only,
                        "ctc_model": ctc_model,
                        "mms_asr_model": mms_asr_model,
                        "indicconformer_default_model": indicconformer_default_model,
                        "hf_indicconformer_model": hf_indicconformer_model,
                        "hf_indicconformer_decode": hf_indicconformer_decode,
                        "vllm_asr_model": vllm_asr_model,
                        "vllm_gpu_memory_utilization": vllm_gpu_memory_utilization,
                        "vllm_max_model_len": vllm_max_model_len,
                        "vllm_transcribe_prompt": vllm_transcribe_prompt,
                        "vllm_proofread": vllm_proofread,
                        "vllm_proofread_prompt": vllm_proofread_prompt,
                        "skip_whisper": skip_whisper,
                    },
                    "timing": {
                        "preprocess_sec": t_pre1 - t_pre0,
                        "lid_sec": t_lid1 - t_lid0,
                        "asr_sec": t_asr1 - t_asr0,
                        "normalize_sec": t_norm1 - t_norm0,
                        "rover_sec": t_rover1 - t_rover0,
                        "ctc_sec": t_ctc1 - t_ctc0,
                        "verify_sec": t_ver1 - t_ver0,
                        "total_sec": time.perf_counter() - t0,
                    },
                }

                f.write(json.dumps(record, ensure_ascii=False) + "\n")
                decision = verification.decision
                counts[decision] = counts.get(decision, 0) + 1
                timings_total.append(record["timing"]["total_sec"])

            except Exception as e:  # pragma: no cover
                counts["error"] += 1
                f.write(
                    json.dumps(
                        {"segment_id": segment_id, "src_path": str(src), "error": str(e)}, ensure_ascii=False
                    )
                    + "\n"
                )

    summary = {
        "segments_total": len(audio_files),
        "counts": counts,
        "avg_total_sec": (sum(timings_total) / len(timings_total)) if timings_total else None,
        "p50_total_sec": _percentile(timings_total, 0.50),
        "p90_total_sec": _percentile(timings_total, 0.90),
    }
    summary_path.write_text(json.dumps(summary, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
    return results_path

def _percentile(xs: list[float], q: float) -> float | None:
    if not xs:
        return None
    xs_sorted = sorted(xs)
    idx = int(round((len(xs_sorted) - 1) * q))
    idx = max(0, min(len(xs_sorted) - 1, idx))
    return float(xs_sorted[idx])


**Planning anchor-based transcript guarding**

I'm considering running a stable ASR (like whisper) first as an anchor, then using the LLM as a proofreader with edit distance checks to decide whether to accept LLM output or keep the anchor. Next, I'll integrate this guard and fallback logic into the pipeline around vllm results, possibly by feeding the anchor transcript as input to vllm and marking final selections accordingly.


**Defining anchor proofread flow and guard rules**

The existing vllm proofread flow uses the same model twice, so I'll change it to accept an anchor transcript from another ASR (like whisper) when a new option enables anchor-based proofread. I'll also implement post-generation guards based on script correctness and length ratio relative to the anchor, using script detection functions from the language module.


sed -n '1,260p' maya_transcribe/language.py


Output:
Exit code: 0
Wall time: 0 seconds
Output:
from __future__ import annotations

import unicodedata

ISO3_TO_WHISPER_LANG: dict[str, str] = {
    "eng": "en",
    "hin": "hi",
    "mar": "mr",
    "guj": "gu",
    "pan": "pa",
    "tam": "ta",
    "tel": "te",
    "kan": "kn",
    "mal": "ml",
    "ben": "bn",
    "asm": "as",
    "urd": "ur",
    "san": "sa",
    "ori": "or",
    "ory": "or",
}

# Human-friendly names and scripts (for prompts/UI).
LANG_CODE_TO_NAME: dict[str, str] = {
    "eng": "English",
    "en": "English",
    "hin": "Hindi",
    "hi": "Hindi",
    "mar": "Marathi",
    "mr": "Marathi",
    "guj": "Gujarati",
    "gu": "Gujarati",
    "pan": "Punjabi",
    "pa": "Punjabi",
    "tam": "Tamil",
    "ta": "Tamil",
    "tel": "Telugu",
    "te": "Telugu",
    "kan": "Kannada",
    "kn": "Kannada",
    "mal": "Malayalam",
    "ml": "Malayalam",
    "ben": "Bengali",
    "bn": "Bengali",
    "asm": "Assamese",
    "as": "Assamese",
    "ori": "Odia",
    "ory": "Odia",
    "or": "Odia",
}

LANG_NAME_TO_SCRIPT: dict[str, str] = {
    "English": "Latin",
    "Hindi": "Devanagari",
    "Marathi": "Devanagari",
    "Gujarati": "Gujarati",
    "Punjabi": "Gurmukhi",
    "Tamil": "Tamil",
    "Telugu": "Telugu",
    "Kannada": "Kannada",
    "Malayalam": "Malayalam",
    "Bengali": "Bengali",
    "Assamese": "Assamese",
    "Odia": "Odia",
}

# Coarse script expectations for filtering/normalization heuristics.
# Assamese uses the Bengali Unicode block.
LANG_CODE_TO_SCRIPT: dict[str, str] = {
    "en": "Latin",
    "eng": "Latin",
    "hi": "Devanagari",
    "hin": "Devanagari",
    "mr": "Devanagari",
    "mar": "Devanagari",
    "gu": "Gujarati",
    "guj": "Gujarati",
    "pa": "Gurmukhi",
    "pan": "Gurmukhi",
    "ta": "Tamil",
    "tam": "Tamil",
    "te": "Telugu",
    "tel": "Telugu",
    "kn": "Kannada",
    "kan": "Kannada",
    "ml": "Malayalam",
    "mal": "Malayalam",
    "bn": "Bengali",
    "ben": "Bengali",
    "as": "Bengali",
    "asm": "Bengali",
    "or": "Oriya",
    "ori": "Oriya",
    "ory": "Oriya",
    "ur": "Arabic",
    "urd": "Arabic",
}

def expected_script(lang_code: str | None) -> str | None:
    if not lang_code:
        return None
    return LANG_CODE_TO_SCRIPT.get(lang_code.strip().lower())

def dominant_script(text: str, *, min_letters: int = 4) -> str | None:
    """
    Heuristic: return the dominant script bucket based on Unicode block ranges.
    Returns None if there's not enough signal (e.g., mostly digits/punctuation).
    """
    if not text:
        return None

    counts: dict[str, int] = {}
    total_letters = 0

    def bump(script: str) -> None:
        nonlocal total_letters
        counts[script] = counts.get(script, 0) + 1
        total_letters += 1

    for ch in text:
        if ch.isspace():
            continue
        cat = unicodedata.category(ch)
        if not cat.startswith("L"):
            continue

        o = ord(ch)
        if 0x0900 <= o <= 0x097F:
            bump("Devanagari")
        elif 0x0980 <= o <= 0x09FF:
            bump("Bengali")
        elif 0x0A00 <= o <= 0x0A7F:
            bump("Gurmukhi")
        elif 0x0A80 <= o <= 0x0AFF:
            bump("Gujarati")
        elif 0x0B00 <= o <= 0x0B7F:
            bump("Oriya")
        elif 0x0B80 <= o <= 0x0BFF:
            bump("Tamil")
        elif 0x0C00 <= o <= 0x0C7F:
            bump("Telugu")
        elif 0x0C80 <= o <= 0x0CFF:
            bump("Kannada")
        elif 0x0D00 <= o <= 0x0D7F:
            bump("Malayalam")
        else:
            # Rough Latin bucket for English/code-switch.
            if ("LATIN" in unicodedata.name(ch, "")) or ("A" <= ch <= "z"):
                bump("Latin")

    if total_letters < int(min_letters):
        return None
    return max(counts.items(), key=lambda kv: kv[1])[0] if counts else None

def to_whisper_lang(lang_code: str | None) -> str | None:
    if not lang_code:
        return None
    lang_code = lang_code.strip().lower()
    if len(lang_code) == 2:
        return lang_code
    return ISO3_TO_WHISPER_LANG.get(lang_code, None)

def to_language_name(lang_code: str | None) -> str | None:
    if not lang_code:
        return None
    lang_code = lang_code.strip().lower()
    return LANG_CODE_TO_NAME.get(lang_code)

def to_prompt_language_hint(lang_code: str | None) -> str:
    """
    Return a human-friendly language hint string for prompting audio-native models.
    """
    if not lang_code:
        return "Unknown (auto-detect primary spoken language)"
    lang_code_norm = lang_code.strip().lower()
    name = LANG_CODE_TO_NAME.get(lang_code_norm)
    if not name:
        return f"{lang_code_norm} (auto-detect primary spoken language)"
    script = LANG_NAME_TO_SCRIPT.get(name)
    if script:
        return f"{name} ({script} script)"
    return name


**Planning script ratio implementation**

I'm deciding to implement script ratio calculation as a helper in the pipeline for minimal changes but considering placing it in language.py for reuse; I'll also enhance the VLLM ASR transcription to pass decoding parameters like top_k cautiously after verifying SamplingParams support.


python3 - <<'PY'
import inspect
from vllm import SamplingParams
sig = inspect.signature(SamplingParams)
print(sig)
PY


Output:
Exit code: 134
Wall time: 4.2 seconds
Output:
/home/ubuntu/.local/lib/python3.10/site-packages/torch/cuda/__init__.py:63: FutureWarning: The pynvml package is deprecated. Please install nvidia-ml-py instead. If you did not install pynvml directly, please report this to the maintainers of the package that installed pynvml for you.
  import pynvml  # type: ignore[import]
terminate called after throwing an instance of 'std::bad_alloc'
  what():  std::bad_alloc
/bin/bash: line 6: 2918058 Aborted                 (core dumped) python3 - <<'PY'
import inspect
from vllm import SamplingParams
sig = inspect.signature(SamplingParams)
print(sig)
PY


**Avoiding direct import for inspection**

Importing vllm risks high memory usage and crashes, so I'll avoid it by reading the sampling_params.py file directly from its known install path to inspect its contents safely.


sed -n '1,200p' /home/ubuntu/.local/lib/python3.10/site-packages/vllm/sampling_params.py


Output:
Exit code: 0
Wall time: 0 seconds
Output:
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Sampling parameters for text generation."""

import copy
from dataclasses import field
from enum import Enum, IntEnum
from functools import cached_property
from typing import Annotated, Any

import msgspec
from pydantic.dataclasses import dataclass

from vllm.logger import init_logger
from vllm.logits_process import LogitsProcessor
from vllm.tokenizers import TokenizerLike
from vllm.v1.serial_utils import PydanticMsgspecMixin

logger = init_logger(__name__)

_SAMPLING_EPS = 1e-5
_MAX_TEMP = 1e-2

class SamplingType(IntEnum):
    GREEDY = 0
    RANDOM = 1
    RANDOM_SEED = 2

# maybe make msgspec?
@dataclass
class StructuredOutputsParams:
    # One of these fields will be used to build a logit processor.
    json: str | dict | None = None
    regex: str | None = None
    choice: list[str] | None = None
    grammar: str | None = None
    json_object: bool | None = None
    # These are other options that can be set.
    disable_fallback: bool = False
    disable_any_whitespace: bool = False
    disable_additional_properties: bool = False
    whitespace_pattern: str | None = None
    structural_tag: str | None = None

    _backend: str | None = field(default=None, init=False)
    """CAUTION: Should only be set by Processor._validate_structured_output"""
    _backend_was_auto: bool = field(default=False, init=False)
    """CAUTION: Should only be set by Processor._validate_structured_output"""

    def __post_init__(self):
        """Validate that some fields are mutually exclusive."""
        count = sum(
            [
                self.json is not None,
                self.regex is not None,
                self.choice is not None,
                self.grammar is not None,
                self.json_object is not None,
                self.structural_tag is not None,
            ]
        )
        if count > 1:
            raise ValueError(
                "You can only use one kind of structured outputs constraint "
                f"but multiple are specified: {self.__dict__}"
            )

    def all_constraints_none(self) -> bool:
        """
        Returns True if all structured-output constraint fields are None.
        """
        return all(
            getattr(self, field) is None
            for field in (
                "json",
                "regex",
                "choice",
                "grammar",
                "json_object",
                "structural_tag",
            )
        )

    def all_non_structural_tag_constraints_none(self) -> bool:
        """
        Returns True if all structured-output constraint fields are None.
        """
        return all(
            getattr(self, field) is None
            for field in (
                "json",
                "regex",
                "choice",
                "grammar",
                "json_object",
            )
        )

class RequestOutputKind(Enum):
    # Return entire output so far in every RequestOutput
    CUMULATIVE = 0
    # Return only deltas in each RequestOutput
    DELTA = 1
    # Do not return intermediate RequestOutput
    FINAL_ONLY = 2

class SamplingParams(
    PydanticMsgspecMixin,
    msgspec.Struct,
    omit_defaults=True,  # type: ignore[call-arg]
    # required for @cached_property.
    dict=True,
):  # type: ignore[call-arg]
    """Sampling parameters for text generation.

    Overall, we follow the sampling parameters from the OpenAI text completion
    API (https://platform.openai.com/docs/api-reference/completions/create).
    In addition, we support beam search, which is not supported by OpenAI.
    """

    n: int = 1
    """Number of outputs to return for the given prompt request.

    NOTE:
        `AsyncLLM` streams outputs by default. When `n > 1`, all `n` outputs
        are generated and streamed cumulatively per request. To see all `n`
        outputs upon completion, use `output_kind=RequestOutputKind.FINAL_ONLY`
        in `SamplingParams`."""
    presence_penalty: float = 0.0
    """Penalizes new tokens based on whether they appear in the generated text
    so far. Values > 0 encourage the model to use new tokens, while values < 0
    encourage the model to repeat tokens."""
    frequency_penalty: float = 0.0
    """Penalizes new tokens based on their frequency in the generated text so
    far. Values > 0 encourage the model to use new tokens, while values < 0
    encourage the model to repeat tokens."""
    repetition_penalty: float = 1.0
    """Penalizes new tokens based on whether they appear in the prompt and the
    generated text so far. Values > 1 encourage the model to use new tokens,
    while values < 1 encourage the model to repeat tokens."""
    temperature: float = 1.0
    """Controls the randomness of the sampling. Lower values make the model
    more deterministic, while higher values make the model more random. Zero
    means greedy sampling."""
    top_p: float = 1.0
    """Controls the cumulative probability of the top tokens to consider. Must
    be in (0, 1]. Set to 1 to consider all tokens."""
    top_k: int = 0
    """Controls the number of top tokens to consider. Set to 0 (or -1) to
    consider all tokens."""
    min_p: float = 0.0
    """Represents the minimum probability for a token to be considered,
    relative to the probability of the most likely token. Must be in [0, 1].
    Set to 0 to disable this."""
    seed: int | None = None
    """Random seed to use for the generation."""
    stop: str | list[str] | None = None
    """String(s) that stop the generation when they are generated. The returned
    output will not contain the stop strings."""
    stop_token_ids: list[int] | None = None
    """Token IDs that stop the generation when they are generated. The returned
    output will contain the stop tokens unless the stop tokens are special
    tokens."""
    ignore_eos: bool = False
    """Whether to ignore the EOS token and continue generating
    tokens after the EOS token is generated."""
    max_tokens: int | None = 16
    """Maximum number of tokens to generate per output sequence."""
    min_tokens: int = 0
    """Minimum number of tokens to generate per output sequence before EOS or
    `stop_token_ids` can be generated"""
    logprobs: int | None = None
    """Number of log probabilities to return per output token. When set to
    `None`, no probability is returned. If set to a non-`None` value, the
    result includes the log probabilities of the specified number of most
    likely tokens, as well as the chosen tokens. Note that the implementation
    follows the OpenAI API: The API will always return the log probability of
    the sampled token, so there may be up to `logprobs+1` elements in the
    response. When set to -1, return all `vocab_size` log probabilities."""
    prompt_logprobs: int | None = None
    """Number of log probabilities to return per prompt token.
    When set to -1, return all `vocab_size` log probabilities."""
    flat_logprobs: bool = False
    """Whether to return logprobs in flatten format (i.e. FlatLogprob)
    for better performance.
    NOTE: GC costs of FlatLogprobs is significantly smaller than
    list[dict[int, Logprob]]. After enabled, PromptLogprobs and
    SampleLogprobs would populated as FlatLogprobs."""
    # NOTE: This parameter is only exposed at the engine level for now.
    # It is not exposed in the OpenAI API server, as the OpenAI API does
    # not support returning only a list of token IDs.
    detokenize: bool = True
    """Whether to detokenize the output."""
    skip_special_tokens: bool = True
    """Whether to skip special tokens in the output."""
    spaces_between_special_tokens: bool = True


**Searching for no_repeat_ngram_size usage**


grep -n "no_repeat" -n /home/ubuntu/.local/lib/python3.10/site-packages/vllm/sampling_params.py


Output:
Exit code: 1
Wall time: 0 seconds
Output:


**Adjusting for vLLM constraints**

No support for no_repeat_ngram_size means adapting to only officially supported parameters like top_k and repetition_penalty; I'll implement those and set a strict default prompt for vLLM usage, supplying it via CLI or updating the pipeline to use it by default.


**Planning strict prompt, decode constraints, and fallback guard**

I'll implement a new strict default prompt including output-only instructions and safe-failure tokens, hardcode decoding constraints in vLLM transcription (temperature 0, top_p 1, top_k 1, repetition_penalty 1.08), adjust max tokens formula to use 5 tokens/sec plus offset capped at 512, and add a post-generation guard with fallback to an anchor transcript—all configurable via thresholds to enable safe, incremental rollout without affecting other use cases.


grep -n "thresholds" -n maya_transcribe/cli.py


Output:
Exit code: 0
Wall time: 0 seconds
Output:
258:    thresholds_json: Path | None = typer.Option(None, help="Optional JSON file of threshold overrides (verify + routing)."),
291:    thresholds: dict[str, object] = {}
292:    if thresholds_json is not None:
293:        loaded = json.loads(thresholds_json.read_text(encoding="utf-8"))
295:            raise typer.BadParameter("thresholds_json must be a JSON object")
296:        thresholds = loaded
298:        thresholds["require_ctc"] = require_ctc
299:        thresholds["min_ctc_margin"] = float(min_ctc_margin)
330:        thresholds=thresholds,
442:    thresholds_json: Path | None = typer.Option(None, help="Optional JSON file of threshold overrides (verify + routing)."),
486:    thresholds: dict[str, object] = {"route_lang_from_path": True, "route_lang_prefer_path": True}
487:    if thresholds_json is not None:
488:        loaded = json.loads(thresholds_json.read_text(encoding="utf-8"))
490:            raise typer.BadParameter("thresholds_json must be a JSON object")
491:        thresholds.update(loaded)
493:        thresholds["require_ctc"] = require_ctc
494:        thresholds["min_ctc_margin"] = float(min_ctc_margin)
522:        thresholds=thresholds,


sed -n '220,360p' maya_transcribe/cli.py


Output:
Exit code: 0
Wall time: 0 seconds
Output:
    vllm_transcribe_prompt_file: Path | None = typer.Option(None, help="Text prompt file for vLLM transcription."),
    vllm_transcribe_prompt: str | None = typer.Option(None, help="Inline prompt text for vLLM transcription."),
    vllm_proofread: bool = typer.Option(False, help="Run vLLM proofread pass when supported (audio + transcript)."),
    vllm_proofread_prompt: str | None = typer.Option(None, help="Override the proofread prompt template."),
    lid_model_id: str = typer.Option("facebook/mms-lid-256", help="HuggingFace MMS-LID model id."),
    require_lid_backend: bool = typer.Option(True, help="Fail if MMS-LID backend can't initialize."),
    transformers_whisper_model: list[str] = typer.Option(
        [], help="Extra whisper-family HF models to run via transformers (repeatable)."
    ),
    transformers_whisper_model_map: Path | None = typer.Option(
        None, help="JSON map {lang_code: hf_model_id} for per-language Whisper-family HF models (transformers)."
    ),
    # MMS ASR for CTC-based model diversity (per master.md plan).
    mms_asr_model: str | None = typer.Option(
        None, help="Optional MMS ASR model id (e.g., facebook/mms-1b-all) for CTC-based diversity."
    ),
    # IndicConformer for Conformer architecture diversity (per master.md plan).
    indicconformer_model_map: Path | None = typer.Option(
        None, help="JSON map {lang_code: nemo_model_name} for per-language IndicConformer models."
    ),
    indicconformer_default_model: str | None = typer.Option(
        None, help="Default IndicConformer NeMo model name for architecture diversity."
    ),
    hf_indicconformer_model: str | None = typer.Option(
        None, help="HF IndicConformer model id (e.g., ai4bharat/indic-conformer-600m-multilingual)."
    ),
    hf_indicconformer_decode: str = typer.Option("rnnt", help="HF IndicConformer decode mode (rnnt or ctc)."),
    gemini_model: str | None = typer.Option(None, help="Optional Gemini model name (adds Gemini as a hypothesis)."),
    gemini_prompt_file: Path | None = typer.Option(Path("Prompt.md"), help="Prompt file for Gemini."),
    gemini_hard_only: bool = typer.Option(True, help="Only call Gemini on 'hard' segments."),
    ctc_model: str | None = typer.Option(None, help="Optional HF CTC model id for audio-grounded fit scoring."),
    ctc_model_map: Path | None = typer.Option(None, help="JSON map {lang_code: hf_model_id} for CTC scoring."),
    require_ctc: bool = typer.Option(True, help="When CTC is enabled, require a CTC score to accept."),
    min_ctc_margin: float = typer.Option(0.05, help="Min (second-best - best) CTC loss margin to auto-accept."),
    trim_silence: bool = typer.Option(True, help="Trim leading/trailing silence (ffmpeg silenceremove)."),
    silence_threshold_db: float = typer.Option(-40.0, help="Silence threshold in dB for trimming."),
    min_silence_sec: float = typer.Option(0.15, help="Min silence duration for trimming."),
    limit: int | None = typer.Option(None, help="Process only the first N segments."),
    thresholds_json: Path | None = typer.Option(None, help="Optional JSON file of threshold overrides (verify + routing)."),
) -> None:
    """
    End-to-end pipeline: preprocess -> LID -> ASR ensemble -> ROVER -> verify.
    """
    from maya_transcribe.pipeline import run_pipeline

    vllm_transcribe_prompt_text = vllm_transcribe_prompt
    if vllm_transcribe_prompt_file is not None:
        vllm_transcribe_prompt_text = vllm_transcribe_prompt_file.read_text(encoding="utf-8")

    ctc_map = None
    if ctc_model_map is not None:
        ctc_map = json.loads(ctc_model_map.read_text(encoding="utf-8"))
        if not isinstance(ctc_map, dict):
            raise typer.BadParameter("ctc_model_map must be a JSON object {lang_code: model_id}")

    tf_map = None
    if transformers_whisper_model_map is not None:
        tf_map = json.loads(transformers_whisper_model_map.read_text(encoding="utf-8"))
        if not isinstance(tf_map, dict):
            raise typer.BadParameter("transformers_whisper_model_map must be a JSON object {lang_code: model_id}")

    ic_map = None
    if indicconformer_model_map is not None:
        ic_map = json.loads(indicconformer_model_map.read_text(encoding="utf-8"))
        if not isinstance(ic_map, dict):
            raise typer.BadParameter("indicconformer_model_map must be a JSON object {lang_code: nemo_model_name}")

    vllm_transcribe_prompt_text = vllm_transcribe_prompt
    if vllm_transcribe_prompt_file is not None:
        vllm_transcribe_prompt_text = vllm_transcribe_prompt_file.read_text(encoding="utf-8")

    thresholds: dict[str, object] = {}
    if thresholds_json is not None:
        loaded = json.loads(thresholds_json.read_text(encoding="utf-8"))
        if not isinstance(loaded, dict):
            raise typer.BadParameter("thresholds_json must be a JSON object")
        thresholds = loaded
    if ctc_model or ctc_map:
        thresholds["require_ctc"] = require_ctc
        thresholds["min_ctc_margin"] = float(min_ctc_margin)

    results_path = run_pipeline(
        segments_dir=segments_dir,
        out_dir=out,
        lid_model_id=lid_model_id,
        require_lid_backend=require_lid_backend,
        whisper_model=whisper_model,
        skip_whisper=skip_whisper,
        vllm_asr_model=vllm_asr_model,
        vllm_gpu_memory_utilization=vllm_gpu_memory_utilization,
        vllm_max_model_len=vllm_max_model_len,
        vllm_transcribe_prompt=vllm_transcribe_prompt_text,
        vllm_proofread=vllm_proofread,
        vllm_proofread_prompt=vllm_proofread_prompt,
        extra_transformers_whisper_models=transformers_whisper_model,
        transformers_whisper_model_map=tf_map,
        mms_asr_model=mms_asr_model,
        indicconformer_model_map=ic_map,
        indicconformer_default_model=indicconformer_default_model,
        hf_indicconformer_model=hf_indicconformer_model,
        hf_indicconformer_decode=hf_indicconformer_decode,
        gemini_model=gemini_model,
        gemini_prompt_path=gemini_prompt_file,
        gemini_hard_only=gemini_hard_only,
        ctc_model=ctc_model,
        ctc_model_map=ctc_map,
        trim_silence=trim_silence,
        silence_threshold_db=silence_threshold_db,
        min_silence_sec=min_silence_sec,
        limit=limit,
        thresholds=thresholds,
    )
    typer.echo(f"Wrote: {results_path}")
    typer.echo(f"Wrote: {out / 'summary.json'}")

@app.command()
def eval(
    results: Path = typer.Option(..., exists=True, dir_okay=False, help="Pipeline results JSONL (outputs/run/results.jsonl)."),
    refs: Path = typer.Option(..., exists=True, dir_okay=False, help="Reference transcripts JSONL."),
    out: Path | None = typer.Option(None, help="Write summary JSON to this path."),
    hypothesis: str = typer.Option('final', help='Hypothesis to evaluate: "final", "rover", or "asr:<key>".'),
    oracle: bool = typer.Option(True, help="Compute oracle-best and selection-gap stats across all candidates."),
    language_source: str = typer.Option("route", help="Use language from: 'route' (LID) or 'refs' (refs.jsonl)."),
    decision: list[str] = typer.Option([], help="Filter to verification decision(s): accept/human/reject (repeatable)."),
) -> None:
    """
    Compute WER/CER for a hypothesis against a reference JSONL keyed by `segment_id`.
    """
    from maya_transcribe.eval import evaluate

    summary = evaluate(
        results_path=results,
        refs_path=refs,
        out_path=out,
        hypothesis=hypothesis,
        oracle=oracle,
        language_source=language_source,
        decision_filter=decision or None,
    )
    typer.echo(json.dumps(summary, ensure_ascii=False, indent=2))


**Planning gating and anchor integration**

I'm outlining new threshold keys for gating and anchor-based proofreading in the pipeline, using a stable ASR (whisper deterministic) as anchor. The pipeline will run whisper first (no skip), then LLM proofreader gated by edit distance and script ratio thresholds to decide whether to keep anchor or LLM output.


**Designing anchor-based proofread with gating and strict prompts**

I'm detailing the pipeline flow to run LLM proofread using an anchor ASR transcript, gating LLM output on script ratio, length ratio, and edit distance versus anchor, with fallback to anchor if guard fails. This includes new threshold keys for flexible control, helper functions for script ratio and WER checks, and new strict prompt templates enforcing verbatim transcription with safe failure tokens. The plan also updates vLLM backend calls to support proofread mode or prompt-based proofread for different backends.


**Finalizing guard logic with anchor fallback and strict prompt integration**

I'm defining precise pipeline steps to compute guard metrics—script ratio, length ratio, and WER—using new helper functions and language module updates. The guard will conditionally override final output to anchor or proofread LLM transcript based on thresholds, controlled by new config keys, with detailed tracking in asr_errors and record. This plan includes creating strict prompt templates and a new thresholds JSON for controlled re-runs.


Output:
Exit code: 0
Wall time: 0 seconds
Output:
total 12
drwxrwxr-x  2 ubuntu ubuntu 4096 Jan 15 18:49 .
drwxrwxr-x 13 ubuntu ubuntu 4096 Jan 15 22:22 ..
-rw-rw-r--  1 ubuntu ubuntu  102 Jan 15 18:49 bench_v2_100_thresholds.json


Summary: auto


**Defining strict prompt constants**

Planning to add separate strict prompt constants for transcribe and proofread modes to avoid affecting prior results, applying them conditionally based on threshold flags in the pipeline.


**Designing strict prompts and guard logic**

Planning to implement strict transcribe and proofread prompts set via thresholds, incorporate language placeholders, and add decoding constraints in VllmTranscriptionASR. Designing a guard function to validate LLM outputs against anchor text using script ratio, length ratio, and WER, with configurable thresholds. This guard will conditionally override final outputs in the pipeline based on acceptance criteria, integrating with anchor-based proofread and skip-raw-transcribe options.


**Preparing script ratio updates and planning evaluation**

Planning to implement a character-based script detection helper and update dominant script logic to use it for consistent script ratio checks. Also preparing evaluation runs with guarded outputs to compare WER improvements and update results only if improved. Noted apply_patch failures for new files and considering alternative file edit methods.


Input:
*** Begin Patch
*** Update File: maya_transcribe/language.py
@@
 def dominant_script(text: str, *, min_letters: int = 4) -> str | None:
*** End Patch


Output: execution error: Io(Os { code: 2, kind: NotFound, message: "No such file or directory" })


**Switching from patch to python edit**

apply_patch fails due to file path issues, so I'll edit the target file directly via Python, reading and modifying its contents to insert new functions and update existing ones.


**Planning targeted function replacements**

I'm preparing to insert two new helper functions and replace the existing dominant_script function using Python string operations, locating function boundaries by markers for precise, minimal edits.


python3 - <<'PY'
from pathlib import Path

path = Path('maya_transcribe/language.py')
text = path.read_text(encoding='utf-8')

start = text.find('def dominant_script(')
end = text.find('\n\ndef to_whisper_lang(')
if start == -1 or end == -1:
    raise SystemExit('Could not locate dominant_script block')

prefix = text[:start]
suffix = text[end:]

new_block = '''def _char_script(ch: str) -> str | None:
    o = ord(ch)
    if 0x0900 <= o <= 0x097F:
        return "Devanagari"
    if 0x0980 <= o <= 0x09FF:
        return "Bengali"
    if 0x0A00 <= o <= 0x0A7F:
        return "Gurmukhi"
    if 0x0A80 <= o <= 0x0AFF:
        return "Gujarati"
    if 0x0B00 <= o <= 0x0B7F:
        return "Oriya"
    if 0x0B80 <= o <= 0x0BFF:
        return "Tamil"
    if 0x0C00 <= o <= 0x0C7F:
        return "Telugu"
    if 0x0C80 <= o <= 0x0CFF:
        return "Kannada"
    if 0x0D00 <= o <= 0x0D7F:
        return "Malayalam"
    if ("LATIN" in unicodedata.name(ch, "")) or ("A" <= ch <= "z"):
        return "Latin"
    return None

def dominant_script(text: str, *, min_letters: int = 4) -> str | None:
    """
    Heuristic: return the dominant script bucket based on Unicode block ranges.
    Returns None if there's not enough signal (e.g., mostly digits/punctuation).
    """
    if not text:
        return None

    counts: dict[str, int] = {}
    total_letters = 0

    for ch in text:
        if ch.isspace():
            continue
        cat = unicodedata.category(ch)
        if not cat.startswith("L"):
            continue

        script = _char_script(ch)
        if script is None:
            continue
        counts[script] = counts.get(script, 0) + 1
        total_letters += 1

    if total_letters < int(min_letters):
        return None
    return max(counts.items(), key=lambda kv: kv[1])[0] if counts else None

def script_ratio(text: str, expected_script: str | None, *, min_letters: int = 4) -> float | None:
    if not text or not expected_script:
        return None

    total_letters = 0
    expected_letters = 0

    for ch in text:
        if ch.isspace():
            continue
        cat = unicodedata.category(ch)
        if not cat.startswith("L"):
            continue

        script = _char_script(ch)
        if script is None:
            continue
        total_letters += 1
        if script == expected_script:
            expected_letters += 1

    if total_letters < int(min_letters):
        return None
    return expected_letters / float(total_letters) if total_letters else None
'''

path.write_text(prefix + new_block + suffix, encoding='utf-8')
PY


Output:
Exit code: 0
Wall time: 0 seconds
Output:


**Planning pipeline updates**

I'm preparing to add new imports for script checks, constants for strict prompts, and helper functions for word counting and output guarding, carefully integrating with existing imports and ensuring text normalization compatibility.


**Designing LLM output guard and VLLM improvements**

I'm defining a detailed guard function that compares anchor and candidate texts on script ratio, length ratio, and WER, recording accept/reject reasons and metrics for inspection. I'll integrate strict prompts and guard logic into the pipeline's VLLM processing, handling anchor text and keys carefully and adding guard info to output records. I'm also planning enhancements to the VllmTranscriptionASR class with refined token limit calculations, sampling parameters including repetition penalty and top_k, and updated defaults to control generation behavior.


python3 - <<'PY'
from pathlib import Path
path = Path('maya_transcribe/asr.py')
text = path.read_text(encoding='utf-8')

text = text.replace(
    '        self._tokens_per_second = 12\n        self._max_tokens_cap = min(1024, self._max_model_len)\n',
    '        self._tokens_per_second = 5\n        self._max_tokens_bias = 16\n        self._max_tokens_cap = min(512, self._max_model_len)\n        self._repetition_penalty = 1.08\n        self._top_k = 1\n',
)

old_block = '''        if max_tokens is None:\n            duration_sec = float(wav.shape[0]) / float(sr) if sr else 0.0\n            max_tokens = max(32, int(duration_sec * self._tokens_per_second))\n            max_tokens = min(max_tokens, self._max_tokens_cap)\n\n        sampling_params = self._sampling_params_cls(\n            temperature=0.0,\n            top_p=1.0,\n            max_tokens=max_tokens,\n            stop_token_ids=self._stop_token_ids,\n        )\n'''
new_block = '''        if max_tokens is None:\n            duration_sec = float(wav.shape[0]) / float(sr) if sr else 0.0\n            max_tokens = int(duration_sec * self._tokens_per_second) + int(self._max_tokens_bias)\n            max_tokens = max(32, max_tokens)\n            max_tokens = min(max_tokens, self._max_tokens_cap)\n\n        sampling_params = self._sampling_params_cls(\n            temperature=0.0,\n            top_p=1.0,\n            top_k=self._top_k,\n            repetition_penalty=self._repetition_penalty,\n            max_tokens=max_tokens,\n            stop_token_ids=self._stop_token_ids,\n        )\n'''
if old_block not in text:
    raise SystemExit('Could not find sampling block to replace')
text = text.replace(old_block, new_block)

path.write_text(text, encoding='utf-8')
PY


Output:
Exit code: 0
Wall time: 0 seconds
Output:


**Preparing prompt constants and helpers**

New strict transcription and proofreading prompt constants will be added near existing defaults, plus helper functions for word counting and output guarding will be inserted near the language inference function; preparing to update the pipeline to include these changes.


**Updating import statements**

The import line will be expanded with new language utility functions plus a WER function from jiwer, modifying the top of the file accordingly.


python3 - <<'PY'
from pathlib import Path
path = Path('maya_transcribe/pipeline.py')
text = path.read_text(encoding='utf-8')

if 'from jiwer import wer as jiwer_wer' not in text:
    text = text.replace('from rich.progress import track\n\n', 'from rich.progress import track\n\nfrom jiwer import wer as jiwer_wer\n\n')

text = text.replace('from maya_transcribe.language import LANG_CODE_TO_NAME\n',
                    'from maya_transcribe.language import LANG_CODE_TO_NAME, expected_script, script_ratio\n')

path.write_text(text, encoding='utf-8')
PY


Output:
Exit code: 0
Wall time: 0 seconds
Output:


**Preparing to insert constants and helpers**

I'm planning to add new constants and a helper function near the existing default prompt constant to support upcoming changes.


python3 - <<'PY'
from pathlib import Path
path = Path('maya_transcribe/pipeline.py')
text = path.read_text(encoding='utf-8')

marker = 'DEFAULT_VLLM_PROOFREAD_PROMPT = (\n'
idx = text.find(marker)
if idx == -1:
    raise SystemExit('DEFAULT_VLLM_PROOFREAD_PROMPT not found')

# Find end of that constant (after the closing parenthesis and blank line)
end_idx = text.find('\n\n', idx)
if end_idx == -1:
    raise SystemExit('Could not find end of DEFAULT_VLLM_PROOFREAD_PROMPT block')

insert_block = '''DEFAULT_VLLM_PROOFREAD_PROMPT = (
    "Correct transcription errors using the audio. Keep the same language/script. "
    "Do not add new content. Output only the corrected transcript.\n\nTranscript:\n{transcript}"
)

STRICT_VLLM_TRANSCRIBE_PROMPT = (
    "You are an ASR system. Transcribe the audio verbatim in <language> script.\n"
    "Rules:\n"
    "- Do not translate, summarize, or paraphrase.\n"
    "- Do not add or omit words.\n"
    "- Preserve numbers and casing as spoken.\n"
    "- If unsure, output [inaudible] for that word.\n"
    "Output only the transcript."
)

STRICT_VLLM_PROOFREAD_PROMPT = (
    "Correct transcription errors using the audio. Keep the same language/script.\n"
    "Rules:\n"
    "- Do not translate, summarize, or paraphrase.\n"
    "- Do not add or omit words.\n"
    "- Preserve numbers and casing as spoken.\n"
    "- If unsure, output [inaudible] for that word.\n"
    "Output only the corrected transcript.\n\n"
    "Transcript:\n{transcript}"
)
'''

# Replace the old DEFAULT_VLLM_PROOFREAD_PROMPT block with insert_block
old_block_end = text.find(')\n\n', idx)
if old_block_end == -1:
    raise SystemExit('Could not locate old DEFAULT_VLLM_PROOFREAD_PROMPT block end')
old_block_end += len(')\n\n')
text = text[:idx] + insert_block + text[old_block_end:]

path.write_text(text, encoding='utf-8')
PY


Output:
Exit code: 0
Wall time: 0 seconds
Output:


**Verifying prompt constant replacement**

I'll check the surrounding code to confirm if the new constant block replaced the old one cleanly or if any duplication occurred.


python3 - <<'PY'
from pathlib import Path
text = Path('maya_transcribe/pipeline.py').read_text(encoding='utf-8')
start = text.index('DEFAULT_VLLM_PROOFREAD_PROMPT')
print(text[start:start+600])
PY


Output:
Exit code: 0
Wall time: 0 seconds
Output:
DEFAULT_VLLM_PROOFREAD_PROMPT = (
    "Correct transcription errors using the audio. Keep the same language/script. "
    "Do not add new content. Output only the corrected transcript.

Transcript:
{transcript}"
)

STRICT_VLLM_TRANSCRIBE_PROMPT = (
    "You are an ASR system. Transcribe the audio verbatim in <language> script.
"
    "Rules:
"
    "- Do not translate, summarize, or paraphrase.
"
    "- Do not add or omit words.
"
    "- Preserve numbers and casing as spoken.
"
    "- If unsure, output [inaudible] for that word.
"
    "Output only the transcript."
)

STRICT_VLLM_PROOFREAD_PROMPT


Summary: auto


**Clarifying newline escaping in inserted prompt**

The observed newlines in the inserted prompt come from the way `\n` escapes were used inside triple-quoted Python strings, causing them to be interpreted as actual newlines instead of literal `\n` sequences in the final inserted text.


**Inspecting inserted string validity in file**

I'm verifying whether actual newline characters inside quoted strings in the inserted block break Python syntax by inspecting the file lines to confirm if the insertion remains valid Python code.


sed -n '60,120p' maya_transcribe/pipeline.py


Output:
Exit code: 0
Wall time: 0 seconds
Output:
            return p
    return None

DEFAULT_VLLM_PROOFREAD_PROMPT = (
    "Correct transcription errors using the audio. Keep the same language/script. "
    "Do not add new content. Output only the corrected transcript.

Transcript:
{transcript}"
)

STRICT_VLLM_TRANSCRIBE_PROMPT = (
    "You are an ASR system. Transcribe the audio verbatim in <language> script.
"
    "Rules:
"
    "- Do not translate, summarize, or paraphrase.
"
    "- Do not add or omit words.
"
    "- Preserve numbers and casing as spoken.
"
    "- If unsure, output [inaudible] for that word.
"
    "Output only the transcript."
)

STRICT_VLLM_PROOFREAD_PROMPT = (
    "Correct transcription errors using the audio. Keep the same language/script.
"
    "Rules:
"
    "- Do not translate, summarize, or paraphrase.
"
    "- Do not add or omit words.
"
    "- Preserve numbers and casing as spoken.
"
    "- If unsure, output [inaudible] for that word.
"
    "Output only the corrected transcript.

"
    "Transcript:
{transcript}"
)

def run_pipeline(
    *,
    segments_dir: Path,
    out_dir: Path,
    lid_model_id: str = "facebook/mms-lid-256",
    whisper_model: str = "large-v3",
    skip_whisper: bool = False,
    vllm_asr_model: str | None = None,
    vllm_gpu_memory_utilization: float | None = None,
    vllm_max_model_len: int | None = None,
    vllm_transcribe_prompt: str | None = None,
    vllm_proofread: bool = False,
    vllm_proofread_prompt: str | None = None,


**Fixing multiline string literals**

I'm correcting broken string literals by replacing actual newlines inside quotes with proper `\n` escapes and reformatting the constants as properly escaped multiline strings using parentheses to ensure valid Python syntax.


**Finalizing prompt block replacement**

I'm preparing to replace the broken prompt constants block from the proofread prompt start up to the `def run_pipeline` line with the corrected, properly escaped multiline strings plus the `def run_pipeline` line to fix syntax.


python3 - <<'PY'
from pathlib import Path
path = Path('maya_transcribe/pipeline.py')
text = path.read_text(encoding='utf-8')
start = text.find('DEFAULT_VLLM_PROOFREAD_PROMPT')
end = text.find('def run_pipeline(')
if start == -1 or end == -1:
    raise SystemExit('Prompt block or run_pipeline not found')

prompt_block = '''DEFAULT_VLLM_PROOFREAD_PROMPT = (
    "Correct transcription errors using the audio. Keep the same language/script. "
    "Do not add new content. Output only the corrected transcript.\n\n"
    "Transcript:\n{transcript}"
)

STRICT_VLLM_TRANSCRIBE_PROMPT = (
    "You are an ASR system. Transcribe the audio verbatim in <language> script.\n"
    "Rules:\n"
    "- Do not translate, summarize, or paraphrase.\n"
    "- Do not add or omit words.\n"
    "- Preserve numbers and casing as spoken.\n"
    "- If unsure, output [inaudible] for that word.\n"
    "Output only the transcript."
)

STRICT_VLLM_PROOFREAD_PROMPT = (
    "Correct transcription errors using the audio. Keep the same language/script.\n"
    "Rules:\n"
    "- Do not translate, summarize, or paraphrase.\n"
    "- Do not add or omit words.\n"
    "- Preserve numbers and casing as spoken.\n"
    "- If unsure, output [inaudible] for that word.\n"
    "Output only the corrected transcript.\n\n"
    "Transcript:\n{transcript}"
)

'''

text = text[:start] + prompt_block + text[end:]
path.write_text(text, encoding='utf-8')
PY


Output:
Exit code: 0
Wall time: 0 seconds
Output:


sed -n '60,140p' maya_transcribe/pipeline.py


Output:
Exit code: 0
Wall time: 0 seconds
Output:
            return p
    return None

DEFAULT_VLLM_PROOFREAD_PROMPT = (
    "Correct transcription errors using the audio. Keep the same language/script. "
    "Do not add new content. Output only the corrected transcript.

"
    "Transcript:
{transcript}"
)

STRICT_VLLM_TRANSCRIBE_PROMPT = (
    "You are an ASR system. Transcribe the audio verbatim in <language> script.
"
    "Rules:
"
    "- Do not translate, summarize, or paraphrase.
"
    "- Do not add or omit words.
"
    "- Preserve numbers and casing as spoken.
"
    "- If unsure, output [inaudible] for that word.
"
    "Output only the transcript."
)

STRICT_VLLM_PROOFREAD_PROMPT = (
    "Correct transcription errors using the audio. Keep the same language/script.
"
    "Rules:
"
    "- Do not translate, summarize, or paraphrase.
"
    "- Do not add or omit words.
"
    "- Preserve numbers and casing as spoken.
"
    "- If unsure, output [inaudible] for that word.
"
    "Output only the corrected transcript.

"
    "Transcript:
{transcript}"
)

def run_pipeline(
    *,
    segments_dir: Path,
    out_dir: Path,
    lid_model_id: str = "facebook/mms-lid-256",
    whisper_model: str = "large-v3",
    skip_whisper: bool = False,
    vllm_asr_model: str | None = None,
    vllm_gpu_memory_utilization: float | None = None,
    vllm_max_model_len: int | None = None,
    vllm_transcribe_prompt: str | None = None,
    vllm_proofread: bool = False,
    vllm_proofread_prompt: str | None = None,
    extra_transformers_whisper_models: list[str] | None = None,
    transformers_whisper_model_map: dict[str, str] | None = None,
    transformers_whisper_default_model: str | None = None,
    # MMS ASR for CTC-based diversity (per master.md plan).
    mms_asr_model: str | None = None,
    # IndicConformer for Conformer architecture diversity (per master.md plan).
    indicconformer_model_map: dict[str, str] | None = None,
    indicconformer_default_model: str | None = None,
    hf_indicconformer_model: str | None = None,
    hf_indicconformer_decode: str = "rnnt",
    gemini_model: str | None = None,
    gemini_prompt_path: Path | None = None,
    gemini_max_output_tokens: int = 512,
    gemini_hard_only: bool = True,
    ctc_model: str | None = None,
    ctc_model_map: dict[str, str] | None = None,
    require_lid_backend: bool = True,
    trim_silence: bool = True,
    silence_threshold_db: float = -40.0,


**Diagnosing newline escape issue**

The inserted string uses `\\n` in a triple-quoted literal but still results in actual newlines in output, indicating the backslashes aren't escaping properly through the string layers; next, I'll verify the string literal escaping and how it's written to file to preserve `\n` visibly.


**Investigating literal newline characters**

The `sed` output shows actual newlines where `\n` should be, so the file likely contains literal newline characters instead of escaped sequences; I’ll print the lines’ repr in Python to confirm their exact content.


python3 - <<'PY'
from pathlib import Path
text = Path('maya_transcribe/pipeline.py').read_text(encoding='utf-8')
# extract prompt block lines
start = text.find('DEFAULT_VLLM_PROOFREAD_PROMPT')
end = text.find('def run_pipeline')
block = text[start:end]
print(repr(block.splitlines()[1]))
print(repr(block.splitlines()[2]))
print(repr(block.splitlines()[3]))
PY


Output:
Exit code: 0
Wall time: 0 seconds
Output:
'    "Correct transcription errors using the audio. Keep the same language/script. "'
'    "Do not add new content. Output only the corrected transcript.'
''


**Identifying missing string closure due to newline in prompt block**


**Diagnosing escape sequence error causing string literal break**

The prompt block used `\\n` which was interpreted as actual newlines when written, breaking string literals; replacing `\\n` with `\\\\n` or reconstructing the block with explicit escaped backslashes will preserve literal `\n` in the file and prevent missing closing quotes.


python3 - <<'PY'
from pathlib import Path
path = Path('maya_transcribe/pipeline.py')
text = path.read_text(encoding='utf-8')
start = text.find('DEFAULT_VLLM_PROOFREAD_PROMPT')
end = text.find('def run_pipeline(')
if start == -1 or end == -1:
    raise SystemExit('Prompt block or run_pipeline not found')

lines = [
    'DEFAULT_VLLM_PROOFREAD_PROMPT = (',
    '    "Correct transcription errors using the audio. Keep the same language/script. "',
    '    "Do not add new content. Output only the corrected transcript.\\n\\n"',
    '    "Transcript:\\n{transcript}"',
    ')',
    '',
    'STRICT_VLLM_TRANSCRIBE_PROMPT = (',
    '    "You are an ASR system. Transcribe the audio verbatim in <language> script.\\n"',
    '    "Rules:\\n"',
    '    "- Do not translate, summarize, or paraphrase.\\n"',
    '    "- Do not add or omit words.\\n"',
    '    "- Preserve numbers and casing as spoken.\\n"',
    '    "- If unsure, output [inaudible] for that word.\\n"',
    '    "Output only the transcript."',
    ')',
    '',
    'STRICT_VLLM_PROOFREAD_PROMPT = (',
    '    "Correct transcription errors using the audio. Keep the same language/script.\\n"',
    '    "Rules:\\n"',
    '    "- Do not translate, summarize, or paraphrase.\\n"',
    '    "- Do not add or omit words.\\n"',
    '    "- Preserve numbers and casing as spoken.\\n"',
    '    "- If unsure, output [inaudible] for that word.\\n"',
    '    "Output only the corrected transcript.\\n\\n"',
    '    "Transcript:\\n{transcript}"',
    ')',
    '',
    '',
]

prompt_block = "\n".join(lines)
text = text[:start] + prompt_block + text[end:]
path.write_text(text, encoding='utf-8')
PY


Output:
Exit code: 0
Wall time: 0 seconds
Output:


Output:
Exit code: 0
Wall time: 0 seconds
Output:
            return p
    return None

DEFAULT_VLLM_PROOFREAD_PROMPT = (
    "Correct transcription errors using the audio. Keep the same language/script. "
    "Do not add new content. Output only the corrected transcript.\n\n"
    "Transcript:\n{transcript}"
)

STRICT_VLLM_TRANSCRIBE_PROMPT = (
    "You are an ASR system. Transcribe the audio verbatim in <language> script.\n"
    "Rules:\n"
    "- Do not translate, summarize, or paraphrase.\n"
    "- Do not add or omit words.\n"
    "- Preserve numbers and casing as spoken.\n"
    "- If unsure, output [inaudible] for that word.\n"
    "Output only the transcript."
)

STRICT_VLLM_PROOFREAD_PROMPT = (
    "Correct transcription errors using the audio. Keep the same language/script.\n"
    "Rules:\n"
    "- Do not translate, summarize, or paraphrase.\n"
    "- Do not add or omit words.\n"
    "- Preserve numbers and casing as spoken.\n"
    "- If unsure, output [inaudible] for that word.\n"
    "Output only the corrected transcript.\n\n"
    "Transcript:\n{transcript}"
)

def run_pipeline(
    *,
    segments_dir: Path,
    out_dir: Path,
    lid_model_id: str = "facebook/mms-lid-256",
    whisper_model: str = "large-v3",
    skip_whisper: bool = False,
    vllm_asr_model: str | None = None,
    vllm_gpu_memory_utilization: float | None = None,
    vllm_max_model_len: int | None = None,
    vllm_transcribe_prompt: str | None = None,
    vllm_proofread: bool = False,
    vllm_proofread_prompt: str | None = None,
    extra_transformers_whisper_models: list[str] | None = None,
    transformers_whisper_model_map: dict[str, str] | None = None,
    transformers_whisper_default_model: str | None = None,
    # MMS ASR for CTC-based diversity (per master.md plan).
    mms_asr_model: str | None = None,
    # IndicConformer for Conformer architecture diversity (per master.md plan).
    indicconformer_model_map: dict[str, str] | None = None,
    indicconformer_default_model: str | None = None,
    hf_indicconformer_model: str | None = None,
    hf_indicconformer_decode: str = "rnnt",
    gemini_model: str | None = None,
    gemini_prompt_path: Path | None = None,
    gemini_max_output_tokens: int = 512,
    gemini_hard_only: bool = True,
    ctc_model: str | None = None,
    ctc_model_map: dict[str, str] | None = None,
    require_lid_backend: bool = True,
    trim_silence: bool = True,
    silence_threshold_db: float = -40.0,
    min_silence_sec: float = 0.15,
    limit: int | None = None,
    thresholds: dict[str, Any] | None = None,
) -> Path:
    """
    End-to-end run: preprocess -> LID -> ASR ensemble -> normalize -> ROVER -> verify.
    Writes `out_dir/results.jsonl` and returns that path.
    """
    thresholds = thresholds or {}
    if (ctc_model or ctc_model_map) and "require_ctc" not in thresholds:
        thresholds["require_ctc"] = True
    if (ctc_model or ctc_model_map) and "min_ctc_margin" not in thresholds:
        thresholds["min_ctc_margin"] = 0.05
    out_dir.mkdir(parents=True, exist_ok=True)

    audio_files = _iter_audio_files(segments_dir)
    if not audio_files:
        raise RuntimeError(f"No audio files found under: {segments_dir}")


**Inserting helper functions for output guarding**

I'm preparing a new code block with `_word_count` and `_guard_llm_output` functions to insert after `_infer_lang_from_relpath` but before the prompt constants, ensuring correct imports are referenced and handling empty anchor text safely.


**Finalizing guard function with WER and imports**

I'm detailing `_guard_llm_output` to handle empty anchor text safely, compute WER conditionally with fallback, and add `SequenceMatcher` import locally inside the function to avoid top-level import conflicts.


python3 - <<'PY'
from pathlib import Path
path = Path('maya_transcribe/pipeline.py')
text = path.read_text(encoding='utf-8')

marker = 'def _infer_lang_from_relpath(relpath: str) -> str | None:\n'
idx = text.find(marker)
if idx == -1:
    raise SystemExit('infer_lang_from_relpath not found')
# find end of function
end_idx = text.find('\n\n', idx)
if end_idx == -1:
    raise SystemExit('Could not find end of infer_lang_from_relpath')
end_idx += 2

insert = '''def _word_count(text: str, lang: str | None) -> int:
    norm = normalize_text(text or "", lang=lang)
    if not norm.strip():
        return 0
    return len(norm.split())

def _guard_llm_output(
    *,
    anchor_text: str,
    candidate_text: str,
    lang: str | None,
    thresholds: dict[str, Any],
) -> dict[str, Any]:
    info: dict[str, Any] = {
        "accept": True,
        "reason": None,
        "expected_script": None,
        "script_ratio": None,
        "length_ratio": None,
        "anchor_wer": None,
        "anchor_word_count": None,
        "candidate_word_count": None,
    }

    candidate_text = str(candidate_text or "")
    anchor_text = str(anchor_text or "")
    if not candidate_text.strip():
        info["accept"] = False
        info["reason"] = "empty"
        return info

    expected = expected_script(lang)
    info["expected_script"] = expected
    min_letters = int(thresholds.get("vllm_guard_min_letters", 4))
    if expected:
        ratio = script_ratio(candidate_text, expected, min_letters=min_letters)
        info["script_ratio"] = ratio
        min_ratio = float(thresholds.get("vllm_guard_script_ratio_min", 0.6))
        if ratio is not None and ratio < min_ratio:
            info["accept"] = False
            info["reason"] = "script_ratio"
            return info

    anchor_norm = normalize_text(anchor_text, lang=lang)
    cand_norm = normalize_text(candidate_text, lang=lang)
    anchor_words = _word_count(anchor_norm, lang)
    cand_words = _word_count(cand_norm, lang)
    info["anchor_word_count"] = anchor_words
    info["candidate_word_count"] = cand_words

    length_ratio = cand_words / float(max(anchor_words, 1))
    info["length_ratio"] = length_ratio
    min_len = float(thresholds.get("vllm_guard_length_ratio_min", 0.7))
    max_len = float(thresholds.get("vllm_guard_length_ratio_max", 1.3))
    if length_ratio < min_len or length_ratio > max_len:
        info["accept"] = False
        info["reason"] = "length_ratio"
        return info

    if anchor_norm.strip():
        if jiwer_wer is not None:
            anchor_wer = float(jiwer_wer(anchor_norm, cand_norm))
        else:
            from difflib import SequenceMatcher

            anchor_wer = 1.0 - SequenceMatcher(None, anchor_norm, cand_norm).ratio()
        info["anchor_wer"] = anchor_wer
        max_wer = float(thresholds.get("vllm_guard_anchor_wer_max", 0.2))
        if anchor_wer > max_wer:
            info["accept"] = False
            info["reason"] = "anchor_wer"
            return info

    return info

'''

text = text[:end_idx] + insert + text[end_idx:]
path.write_text(text, encoding='utf-8')
PY


Output:
Exit code: 0
Wall time: 0 seconds
Output:


Summary: auto