Output:
Exit code: 0
Wall time: 0 seconds
Output:
"""
CTC Forced Aligner
==================

Proper forced alignment using torchaudio's CTC forced_align.
This is equivalent to MFA but using neural CTC instead of GMM-HMM.

Provides:
- Per-character alignment scores (log-prob)
- Per-word alignment scores
- Overall alignment quality score
- Identifies low-confidence regions

Usage:
    aligner = CTCForcedAligner(language="te")
    result = aligner.align("audio.flac", "transcription text")

    print(f"Alignment Score: {result.alignment_score}")  # 0-1
    print(f"Low confidence words: {result.low_confidence_words}")
"""
import torch
import torchaudio
import numpy as np
import soundfile as sf
from dataclasses import dataclass, field
from typing import List, Dict, Optional, Tuple
from pathlib import Path
import time

@dataclass
class WordAlignment:
    """Alignment info for a single word."""
    word: str
    start_frame: int
    end_frame: int
    start_time: float
    end_time: float
    log_prob: float  # Raw log probability
    confidence: float  # Normalized 0-1 score
    char_scores: List[float] = field(default_factory=list)

    @property
    def duration(self) -> float:
        return self.end_time - self.start_time

    @property
    def is_low_confidence(self) -> bool:
        return self.confidence < 0.5

@dataclass
class AlignmentResult:
    """Result of forced alignment."""
    audio_path: str
    transcription: str

    # Scores
    alignment_score: float  # 0-1, overall quality
    mean_log_prob: float  # Raw mean log prob

    # Per-word details
    word_alignments: List[WordAlignment] = field(default_factory=list)

    # Quality flags
    low_confidence_words: List[str] = field(default_factory=list)
    low_confidence_ratio: float = 0.0

    # Timing
    audio_duration_sec: float = 0.0
    processing_time_sec: float = 0.0

    def to_dict(self) -> Dict:
        return {
            "audio_path": self.audio_path,
            "transcription": self.transcription,
            "alignment_score": round(self.alignment_score, 4),
            "mean_log_prob": round(self.mean_log_prob, 4),
            "word_count": len(self.word_alignments),
            "low_confidence_words": self.low_confidence_words,
            "low_confidence_ratio": round(self.low_confidence_ratio, 4),
            "word_alignments": [
                {
                    "word": wa.word,
                    "start_time": round(wa.start_time, 3),
                    "end_time": round(wa.end_time, 3),
                    "confidence": round(wa.confidence, 4),
                    "log_prob": round(wa.log_prob, 4)
                }
                for wa in self.word_alignments
            ],
            "audio_duration_sec": round(self.audio_duration_sec, 3),
            "processing_time_sec": round(self.processing_time_sec, 3)
        }

class CTCForcedAligner:
    """
    CTC-based forced alignment using torchaudio.

    This provides MFA-equivalent functionality using neural CTC models.
    """

    # Language-specific Wav2Vec2 models
    MODELS = {
        "te": "anuragshas/wav2vec2-large-xlsr-53-telugu",
        "hi": "theainerd/Wav2Vec2-large-xlsr-hindi",
        "ta": "Harveenchadha/wav2vec2-large-xlsr-53-tamil",
        "kn": "Harveenchadha/wav2vec2-large-xlsr-53-kannada",
        "ml": "gvs/wav2vec2-large-xlsr-malayalam",
        "bn": "ai4bharat/indicwav2vec-hindi",
        "default": "facebook/wav2vec2-large-xlsr-53"
    }

    def __init__(
        self,
        language: str = "te",
        device: str = "auto",
        cache_dir: str = "./models/ctc_aligner"
    ):
        self.language = language.lower()[:2]
        self.cache_dir = Path(cache_dir)
        self.cache_dir.mkdir(parents=True, exist_ok=True)

        if device == "auto":
            self.device = "cuda" if torch.cuda.is_available() else "cpu"
        else:
            self.device = device

        self.model = None
        self.processor = None
        self.vocab = None
        self._setup_complete = False

    def setup(self) -> bool:
        """Load model and processor."""
        if self._setup_complete:
            return True

        try:
            from transformers import Wav2Vec2ForCTC, Wav2Vec2Processor

            model_name = self.MODELS.get(self.language, self.MODELS["default"])
            print(f"[CTCAligner] Loading {model_name}...")

            self.processor = Wav2Vec2Processor.from_pretrained(
                model_name, cache_dir=self.cache_dir
            )
            self.model = Wav2Vec2ForCTC.from_pretrained(
                model_name, cache_dir=self.cache_dir
            )

            self.model.to(self.device)
            self.model.eval()

            self.vocab = self.processor.tokenizer.get_vocab()
            self.blank_id = self.processor.tokenizer.pad_token_id

            print(f"[CTCAligner] Ready on {self.device}")
            self._setup_complete = True
            return True

        except Exception as e:
            print(f"[CTCAligner] Setup failed: {e}")
            return False

    def _load_audio(self, audio_path: str) -> Tuple[np.ndarray, int]:
        """Load and resample audio to 16kHz."""
        data, sr = sf.read(audio_path)

        if len(data.shape) > 1:
            data = data.mean(axis=1)

        if sr != 16000:
            waveform = torch.from_numpy(data).float().unsqueeze(0)
            data = torchaudio.functional.resample(waveform, sr, 16000).squeeze().numpy()
            sr = 16000

        return data, sr

    def _get_emissions(self, audio: np.ndarray) -> torch.Tensor:
        """Get CTC log-probability emissions."""
        inputs = self.processor(
            audio, sampling_rate=16000, return_tensors="pt", padding=True
        )

        with torch.no_grad():
            logits = self.model(inputs.input_values.to(self.device)).logits

        emissions = torch.log_softmax(logits.cpu(), dim=-1)
        return emissions

    def _text_to_tokens(self, text: str) -> torch.Tensor:
        """Convert text to token IDs, using | as word separator."""
        tokens = []
        for char in text:
            if char == " ":
                if "|" in self.vocab:
                    tokens.append(self.vocab["|"])
            elif char in self.vocab:
                tokens.append(self.vocab[char])
            # Skip OOV characters

        return torch.tensor([tokens], dtype=torch.int32)

    def _compute_word_alignments(
        self,
        text: str,
        alignment: torch.Tensor,
        scores: torch.Tensor,
        frame_duration: float
    ) -> List[WordAlignment]:
        """Extract word-level alignments from frame-level results."""
        words = text.split()

        # Build character-to-word mapping
        char_to_word = []
        for word_idx, word in enumerate(words):
            for _ in word:
                char_to_word.append(word_idx)
            char_to_word.append(-1)  # Space/separator

        # Remove trailing separator
        if char_to_word and char_to_word[-1] == -1:
            char_to_word = char_to_word[:-1]

        # Aggregate scores per word
        word_scores = {i: [] for i in range(len(words))}
        word_frames = {i: [] for i in range(len(words))}

        alignment_np = alignment[0].numpy()
        scores_np = scores[0].numpy()

        char_idx = 0
        for frame_idx, (token_idx, score) in enumerate(zip(alignment_np, scores_np)):
            if token_idx != self.blank_id and char_idx < len(char_to_word):
                word_idx = char_to_word[char_idx]
                if word_idx >= 0:
                    word_scores[word_idx].append(score)
                    word_frames[word_idx].append(frame_idx)
                char_idx += 1

        # Create WordAlignment objects
        alignments = []
        for word_idx, word in enumerate(words):
            word_score_list = word_scores.get(word_idx, [])
            frame_list = word_frames.get(word_idx, [])

            if word_score_list:
                mean_log_prob = np.mean(word_score_list)
                # Convert log prob to 0-1 confidence
                # log_prob of 0 = prob of 1 (perfect)
                # log_prob of -5 = prob of ~0.007 (bad)
                confidence = np.exp(mean_log_prob)
                confidence = min(1.0, max(0.0, confidence))

                start_frame = min(frame_list)
                end_frame = max(frame_list) + 1
            else:
                mean_log_prob = -10.0
                confidence = 0.0
                start_frame = 0
                end_frame = 0

            alignments.append(WordAlignment(
                word=word,
                start_frame=start_frame,
                end_frame=end_frame,
                start_time=start_frame * frame_duration,
                end_time=end_frame * frame_duration,
                log_prob=float(mean_log_prob),
                confidence=float(confidence),
                char_scores=word_score_list
            ))

        return alignments

    def align(
        self,
        audio_path: str,
        transcription: str
    ) -> AlignmentResult:
        """
        Perform forced alignment.

        Args:
            audio_path: Path to audio file
            transcription: Text to align

        Returns:
            AlignmentResult with word-level alignments and scores
        """
        if not self._setup_complete:
            if not self.setup():
                return AlignmentResult(
                    audio_path=audio_path,
                    transcription=transcription,
                    alignment_score=0.0,
                    mean_log_prob=-10.0
                )

        start_time = time.time()

        try:
            # Load audio
            audio, sr = self._load_audio(audio_path)
            audio_duration = len(audio) / sr

            # Get emissions
            emissions = self._get_emissions(audio)
            num_frames = emissions.shape[1]
            frame_duration = audio_duration / num_frames

            # Convert text to tokens
            targets = self._text_to_tokens(transcription)

            if targets.shape[1] == 0:
                return AlignmentResult(
                    audio_path=audio_path,
                    transcription=transcription,


Output:
Exit code: 0
Wall time: 0 seconds
Output:
{
  "metadata": {
    "model": "gemini-3-flash-preview",
    "config": "temp0_low",
    "language": "Telugu",
    "segments_scored": 20,
    "timestamp": "2026-02-05T01:05:40.295126"
  },
  "summary": {
    "mean_alignment_score": 0.91371,
    "mean_confidence": 0.91679
  },
  "results": [
    {
      "segment_id": "SPEAKER_00_0000_0.03-2.61.flac",
      "audio_path": "/tmp/maya3_transcribe/pF_BQpHaIdU/extracted/pF_BQpHaIdU/segments/SPEAKER_00_0000_0.03-2.61.flac",
      "duration_sec": 2.582,
      "transcription": "నాకు కొన్ని యాడ్స్ గుర్తుండేవి లైక్ ఎగ్జాంపుల్ కచ్చా మ్యాంగో బైట్",
      "punctuated": "నాకు కొన్ని యాడ్స్ గుర్తుండేవి, లైక్ ఎగ్జాంపుల్ కచ్చా మ్యాంగో బైట్.",
      "scoring": {
        "audio_path": "/tmp/maya3_transcribe/pF_BQpHaIdU/extracted/pF_BQpHaIdU/segments/SPEAKER_00_0000_0.03-2.61.flac",
        "transcription": "నాకు కొన్ని యాడ్స్ గుర్తుండేవి లైక్ ఎగ్జాంపుల్ కచ్చా మ్యాంగో బైట్",
        "alignment_score": 0.8449,
        "average_confidence": 0.8491,
        "min_confidence": 0.7938,
        "word_count": 9,
        "word_scores": [
          {
            "word": "నాకు",
            "start_time": 0.0,
            "end_time": 0.161,
            "confidence": 0.8919
          },
          {
            "word": "కొన్ని",
            "start_time": 0.161,
            "end_time": 0.424,
            "confidence": 0.8626
          },
          {
            "word": "యాడ్స్",
            "start_time": 0.424,
            "end_time": 0.686,
            "confidence": 0.8771
          },
          {
            "word": "గుర్తుండేవి",
            "start_time": 0.686,
            "end_time": 1.17,
            "confidence": 0.8005
          },
          {
            "word": "లైక్",
            "start_time": 1.17,
            "end_time": 1.331,
            "confidence": 0.8114
          },
          {
            "word": "ఎగ్జాంపుల్",
            "start_time": 1.331,
            "end_time": 1.775,
            "confidence": 0.8438
          },
          {
            "word": "కచ్చా",
            "start_time": 1.775,
            "end_time": 1.997,
            "confidence": 0.8758
          },
          {
            "word": "మ్యాంగో",
            "start_time": 1.997,
            "end_time": 2.299,
            "confidence": 0.7938
          },
          {
            "word": "బైట్",
            "start_time": 2.299,
            "end_time": 2.461,
            "confidence": 0.8849
          }
        ],
        "audio_duration_sec": 2.582,
        "processing_time_sec": 0.318,
        "method": "ctc",
        "language": "te"
      }
    },
    {
      "segment_id": "SPEAKER_00_0107_1343.96-1350.94.flac",
      "audio_path": "/tmp/maya3_transcribe/pF_BQpHaIdU/extracted/pF_BQpHaIdU/segments/SPEAKER_00_0107_1343.96-1350.94.flac",
      "duration_sec": 6.973,
      "transcription": "ఇప్పుడు మీరు పాపిన్స్ గురించి ఇంకా కిస్మి గురించి చెప్తున్నప్పుడు రెండిటి గురించి రకరకాల స్టోరీస్ ఉన్నాయి అవును నాకు స్కూల్లో ఉన్నప్పుడు ఐ గాట్ ఏ డేర్ ట్రూత్ ఆర్ డేర్ లో",
      "punctuated": "ఇప్పుడు మీరు పాపిన్స్ గురించి ఇంకా కిస్మి గురించి చెప్తున్నప్పుడు, రెండిటి గురించి రకరకాల స్టోరీస్ ఉన్నాయి. అవును, నాకు స్కూల్లో ఉన్నప్పుడు ఐ గాట్ ఏ డేర్, ట్రూత్ ఆర్ డేర్ లో.",
      "scoring": {
        "audio_path": "/tmp/maya3_transcribe/pF_BQpHaIdU/extracted/pF_BQpHaIdU/segments/SPEAKER_00_0107_1343.96-1350.94.flac",
        "transcription": "ఇప్పుడు మీరు పాపిన్స్ గురించి ఇంకా కిస్మి గురించి చెప్తున్నప్పుడు రెండిటి గురించి రకరకాల స్టోరీస్ ఉన్నాయి అవును నాకు స్కూల్లో ఉన్నప్పుడు ఐ గాట్ ఏ డేర్ ట్రూత్ ఆర్ డేర్ లో",
        "alignment_score": 0.8949,
        "average_confidence": 0.8995,
        "min_confidence": 0.7618,
        "word_count": 25,
        "word_scores": [
          {
            "word": "ఇప్పుడు",
            "start_time": 0.0,
            "end_time": 0.321,
            "confidence": 0.906
          },
          {
            "word": "మీరు",
            "start_time": 0.321,
            "end_time": 0.501,
            "confidence": 0.9452
          },
          {
            "word": "పాపిన్స్",
            "start_time": 0.501,
            "end_time": 0.882,
            "confidence": 0.9045
          },
          {
            "word": "గురించి",
            "start_time": 0.882,
            "end_time": 1.202,
            "confidence": 0.8767
          },
          {
            "word": "ఇంకా",
            "start_time": 1.202,
            "end_time": 1.383,
            "confidence": 0.9318
          },
          {
            "word": "కిస్మి",
            "start_time": 1.383,
            "end_time": 1.663,
            "confidence": 0.812
          },
          {
            "word": "గురించి",
            "start_time": 1.663,
            "end_time": 1.984,
            "confidence": 0.9431
          },
          {
            "word": "చెప్తున్నప్పుడు",
            "start_time": 1.984,
            "end_time": 2.705,
            "confidence": 0.9283
          },
          {
            "word": "రెండిటి",
            "start_time": 2.705,
            "end_time": 3.026,
            "confidence": 0.7987
          },
          {
            "word": "గురించి",
            "start_time": 3.026,
            "end_time": 3.346,
            "confidence": 0.9569
          },
          {
            "word": "రకరకాల",
            "start_time": 3.346,
            "end_time": 3.627,
            "confidence": 0.7868
          },
          {
            "word": "స్టోరీస్",
            "start_time": 3.627,
            "end_time": 4.007,
            "confidence": 0.8224
          },
          {
            "word": "ఉన్నాయి",
            "start_time": 4.007,
            "end_time": 4.328,
            "confidence": 0.8206
          },
          {
            "word": "అవును",
            "start_time": 4.328,
            "end_time": 4.568,
            "confidence": 0.9575
          },
          {
            "word": "నాకు",
            "start_time": 4.568,
            "end_time": 4.749,
            "confidence": 0.9519
          },
          {
            "word": "స్కూల్లో",
            "start_time": 4.749,
            "end_time": 5.129,
            "confidence": 0.8872
          },
          {
            "word": "ఉన్నప్పుడు",
            "start_time": 5.129,
            "end_time": 5.61,
            "confidence": 0.8861
          },
          {
            "word": "ఐ",
            "start_time": 5.61,
            "end_time": 5.65,
            "confidence": 0.9972
          },
          {
            "word": "గాట్",
            "start_time": 5.65,
            "end_time": 5.831,
            "confidence": 0.8811
          },
          {
            "word": "ఏ",
            "start_time": 5.831,
            "end_time": 5.871,
            "confidence": 0.9636
          },
          {
            "word": "డేర్",
            "start_time": 5.871,
            "end_time": 6.051,
            "confidence": 0.9529
          },
          {
            "word": "ట్రూత్",
            "start_time": 6.051,
            "end_time": 6.332,
            "confidence": 0.9992
          },
          {
            "word": "ఆర్",
            "start_time": 6.332,
            "end_time": 6.472,
            "confidence": 0.7618
          },
          {
            "word": "డేర్",
            "start_time": 6.472,
            "end_time": 6.652,
            "confidence": 0.9017
          },
          {
            "word": "లో",
            "start_time": 6.652,
            "end_time": 6.732,
            "confidence": 0.9131
          }
        ],
        "audio_duration_sec": 6.973,
        "processing_time_sec": 0.087,
        "method": "ctc",
        "language": "te"
      }
    },
    {


Output:
Exit code: 0
Wall time: 0 seconds
Output:
{
  "metadata": {
    "video_id": "pF_BQpHaIdU",
    "language": "Telugu",
    "total_segments": 20,
    "models_tested": [
      "gemini-3-pro-preview",
      "gemini-3-flash-preview",
      "gemini-2.5-pro",
      "gemini-2.5-flash",
      "gemini-2.5-flash-lite"
    ],
    "configs_tested": [
      "temp1_high",
      "temp0_low"
    ],
    "created_at": "2026-02-05T00:29:19.723735",
    "notes": {
      "temp1_high": "Temperature=1.0 with thinking_level=high (Gemini 3) or default (2.5)",
      "temp0_low": "Temperature=0.0 with thinking_level=low (Gemini 3) or default (2.5)",
      "thinking_budget": "For Gemini 3 with temp=0, use thinking_budget=300 to prevent loops"
    }
  },
  "summary": {
    "speed_ranking": [
      {
        "model": "gemini-3-flash-preview",
        "temp1_high_avg_sec": 14.27,
        "temp0_low_avg_sec": 2.8
      },
      {
        "model": "gemini-2.5-flash",
        "temp1_high_avg_sec": 4.48,
        "temp0_low_avg_sec": 3.71
      },
      {
        "model": "gemini-3-pro-preview",
        "temp1_high_avg_sec": 30.17,
        "temp0_low_avg_sec": 6.76
      },
      {
        "model": "gemini-2.5-flash-lite",
        "temp1_high_avg_sec": 1.85,
        "temp0_low_avg_sec": 8.1
      },
      {
        "model": "gemini-2.5-pro",
        "temp1_high_avg_sec": 9.26,
        "temp0_low_avg_sec": 8.36
      }
    ],
    "recommendations": {
      "high_volume_production": "gemini-2.5-flash-lite (fastest, 1.5-2s avg)",
      "balanced_quality_speed": "gemini-2.5-flash (good quality, 3-4s avg)",
      "maximum_quality": "gemini-3-pro-preview (best quality, use thinking_budget=300 with temp=0)",
      "settings_notes": {
        "gemini_3_models": "Use temp=0 with thinking_budget=300 to prevent loops",
        "gemini_2.5_models": "temp=0 works fine, no thinking_budget needed"
      }
    }
  },
  "segments": [
    {
      "segment_id": "SPEAKER_00_0000_0.03-2.61.flac",
      "audio_path": "/tmp/maya3_transcribe/pF_BQpHaIdU/extracted/pF_BQpHaIdU/segments/SPEAKER_00_0000_0.03-2.61.flac",
      "duration_sec": 2.582,
      "models": {
        "gemini-3-pro-preview": {
          "temp1_high": {
            "native": "వస్తాదన్నమాట నాకు కొన్ని యాడ్స్ గుర్తుండేవి లైక్ ఎగ్జాంపుల్ కచ్చ మ్యాంగో బైట్",
            "punctuated": "వస్తాదన్నమాట. నాకు కొన్ని యాడ్స్ గుర్తుండేవి, లైక్ ఎగ్జాంపుల్ కచ్చ మ్యాంగో బైట్.",
            "code_switch": "వస్తాదన్నమాట. నాకు కొన్ని ads గుర్తుండేవి like example kaccha mango bite.",
            "romanized": "vasthadhannamata. naaku konni ads gurthundevi like example kaccha mango bite.",
            "time_sec": 20.036017417907715
          },
          "temp0_low": {
            "native": "నాకు కొన్ని యాడ్స్ గుర్తుండేవి లైక్ ఎగ్జాంపుల్ కచ్చా మ్యాంగో బైట్",
            "punctuated": "నాకు కొన్ని యాడ్స్ గుర్తుండేవి, లైక్ ఎగ్జాంపుల్ కచ్చా మ్యాంగో బైట్.",
            "code_switch": "నాకు కొన్ని ads గుర్తుండేవి, like example Kachcha Mango Bite.",
            "romanized": "naaku konni ads gurthundevi, like example kachcha mango bite.",
            "time_sec": 6.039599418640137
          }
        },
        "gemini-3-flash-preview": {
          "temp1_high": {
            "native": "నాకు కొన్ని యాడ్స్ గుర్తుంటాయి లైక్ ఎగ్జాంపుల్ కచ్చా మ్యాంగో బైట్",
            "punctuated": "నాకు కొన్ని యాడ్స్ గుర్తుంటాయి, లైక్ ఎగ్జాంపుల్ కచ్చా మ్యాంగో బైట్।",
            "code_switch": "నాకు కొన్ని ads గుర్తుంటాయి like example kaccha mango bite",
            "romanized": "naku konni ads gurtuntayi like example kaccha mango bite",
            "time_sec": 14.19179916381836
          },
          "temp0_low": {
            "native": "నాకు కొన్ని యాడ్స్ గుర్తుండేవి లైక్ ఎగ్జాంపుల్ కచ్చా మ్యాంగో బైట్",
            "punctuated": "నాకు కొన్ని యాడ్స్ గుర్తుండేవి, లైక్ ఎగ్జాంపుల్ కచ్చా మ్యాంగో బైట్.",
            "code_switch": "నాకు కొన్ని ads గుర్తుండేవి like example కచ్చా mango bite.",
            "romanized": "naaku konni ads gurtundevi like example kaccha mango bite",
            "time_sec": 4.253249168395996
          }
        },
        "gemini-2.5-pro": {
          "temp1_high": {
            "native": "అన్నమాట నాకు కొన్ని యాడ్స్ గుర్తుంటాయి లైక్ ఫర్ ఎక్సామ్పుల్ కచ్చా బాదం",
            "punctuated": "అన్నమాట, నాకు కొన్ని యాడ్స్ గుర్తుంటాయి, లైక్ ఫర్ ఎక్సామ్పుల్ కచ్చా బాదం।",
            "code_switch": "అన్నమాట, నాకు కొన్ని ads గుర్తుంటాయి, like for example కచ్చా బాదం।",
            "romanized": "annamaata, naaku konni ads gurtuntaayi, like for example kacha badam.",
            "time_sec": 10.94771671295166
          },
          "temp0_low": {
            "native": "నాకు కొన్ని యాడ్స్ గుర్తుంటాయి లైక్ ఎగ్జాంపుల్ కచ్చా బాదం",
            "punctuated": "నాకు కొన్ని యాడ్స్ గుర్తుంటాయి, లైక్ ఎగ్జాంపుల్ కచ్చా బాదం।",
            "code_switch": "నాకు కొన్ని ads గుర్తుంటాయి, like example కచ్చా బాదం।",
            "romanized": "naaku konni ads gurtuntaayi, like example kachcha badam.",
            "time_sec": 5.625661134719849
          }
        },
        "gemini-2.5-flash": {
          "temp1_high": {
            "native": "అన్నమాట నా కొన్ని యాడ్స్ గుర్తొస్తాయి లైక్ ఎగ్జాంపుల్ కచ్చా",
            "punctuated": "అన్నమాట. నా కొన్ని యాడ్స్ గుర్తొస్తాయి లైక్, ఎగ్జాంపుల్ కచ్చా.",
            "code_switch": "అన్నమాట. నా కొన్ని ads గుర్తొస్తాయి like, example కచ్చా.",
            "romanized": "annamaata. naa konni ads gurthostayi like, example kaccha.",
            "time_sec": 7.380277395248413
          },
          "temp0_low": {
            "native": "అన్నమాట నాకు కొన్ని యాడ్స్ గుర్తొస్తాయి లైక్ ఎగ్జాంపుల్ కచ్చా",
            "punctuated": "అన్నమాట. నాకు కొన్ని యాడ్స్ గుర్తొస్తాయి లైక్ ఎగ్జాంపుల్ కచ్చా.",
            "code_switch": "అన్నమాట నాకు కొన్ని ads గుర్తొస్తాయి like example కచ్చా",
            "romanized": "annamaata naaku konni ads gurthosthaayi like example kachchaa",
            "time_sec": 3.615159034729004
          }
        },
        "gemini-2.5-flash-lite": {
          "temp1_high": {
            "native": "అన్నమాట నా కొన్ని యాడ్స్ గుర్తుంటాయి లైక్ ఉదాహరణకు కచ్చా",
            "punctuated": "అన్నమాట। నా కొన్ని యాడ్స్ గుర్తుంటాయి లైక్ ఉదాహరణకు కచ్చా",
            "code_switch": "అన్నమాట నా కొన్ని యాడ్స్ గుర్తుంటాయి లైక్ ఉదాహరణకు కచ్చా",
            "romanized": "annamaata na konni ads gurtuntayi like udaaharanaku kachcha",
            "time_sec": 2.2073280811309814
          },
          "temp0_low": {
            "native": "అన్నమాట నా కొన్ని యాడ్స్ గుర్తుంటాయి లైక్ ఎగ్జాంపుల్ కచ్చాబాదం అన్నమాట",
            "punctuated": "అన్నమాట। నా కొన్ని యాడ్స్ గుర్తుంటాయి లైక్ ఎగ్జాంపుల్ కచ్చాబాదం అన్నమాట",
            "code_switch": "అన్నమాట నా కొన్ని యాడ్స్ గుర్తుంటాయి లైక్ ఎగ్జాంపుల్ కచ్చాబాదం అన్నమాట",
            "romanized": "annamaata na konni ads gurtuntaayi like example kachchabaadam annamaata",
            "time_sec": 1.9971544742584229
          }
        }
      },
      "validation": {
        "indicmfa": {
          "transcription": "అన్నమాట నా కొన్ని యాడ్స్ గుర్తుంటాయి లైక్ ఉదాహరణకు కచ్చా",
          "confidence": 0.8448988199234009,
          "word_count": 8,
          "time_sec": 0.0726473331451416
        },
        "indic_conformer": {
          "transcription": "నా కొన్ని యాడ్స్ గుర్తుండే లైక్ ఎగ్జాంపుల్ కచ్చా మ్యాంగో బయట",
          "confidence": null,
          "time_sec": 4.286128044128418
        }
      }
    },
    {
      "segment_id": "SPEAKER_00_0107_1343.96-1350.94.flac",
      "audio_path": "/tmp/maya3_transcribe/pF_BQpHaIdU/extracted/pF_BQpHaIdU/segments/SPEAKER_00_0107_1343.96-1350.94.flac",
      "duration_sec": 6.973,
      "models": {
        "gemini-3-pro-preview": {
          "temp1_high": {
            "native": "ఇప్పుడు మీరు పాపిన్స్ గురించి అని కిస్మీ గురించి చెప్తున్నప్పుడు రెండిటి గురించే రకరకాల స్టోరీస్ ఉంటాయి అవును నాకు స్కూల్లో ఉన్నప్పుడు ఐ గాట్ అ డేర్ ట్రూత్ ఆర్ డేర్ లో ఏదో",
            "punctuated": "ఇప్పుడు మీరు పాపిన్స్ గురించి అని కిస్మీ గురించి చెప్తున్నప్పుడు, రెండిటి గురించే రకరకాల స్టోరీస్ ఉంటాయి. అవును. నాకు స్కూల్లో ఉన్నప్పుడు, ఐ గాట్ అ డేర్. ట్రూత్ ఆర్ డేర్ లో ఏదో.",
            "code_switch": "ఇప్పుడు మీరు Poppins గురించి అని Kismi గురించి చెప్తున్నప్పుడు, రెండిటి గురించే రకరకాల stories ఉంటాయి. అవును. నాకు schoolలో ఉన్నప్పుడు, I got a dare. Truth or dareలో ఏదో.",
            "romanized": "ippudu meeru poppins gurinchi ani kismi gurinchi chepthunnappudu, renditi gurinche rakarakala stories untayi. avunu. naaku schoollo unnappudu, i got a dare. truth or dare lo edho.",
            "time_sec": 26.40731191635132
          },
          "temp0_low": {
            "native": "ఇప్పుడు మీరు పాపిన్స్ గురించి అని కిస్మి గురించి చెప్తున్నప్పుడు రెండిటి గురించి రకరకాల స్టోరీస్ ఉండేవి అవును నాకు స్కూల్లో ఉన్నప్పుడు ఐ గాట్ ఎ డేర్ ట్రూత్ ఆర్ డేర్ లో",
            "punctuated": "ఇప్పుడు మీరు పాపిన్స్ గురించి అని, కిస్మి గురించి చెప్తున్నప్పుడు, రెండిటి గురించి రకరకాల స్టోరీస్ ఉండేవి. అవును. నాకు స్కూల్లో ఉన్నప్పుడు ఐ గాట్ ఎ డేర్, ట్రూత్ ఆర్ డేర్ లో.",
            "code_switch": "ఇప్పుడు మీరు Poppins గురించి అని Kissmi గురించి చెప్తున్నప్పుడు, రెండిటి గురించి రకరకాల stories ఉండేవి. అవును. నాకు school లో ఉన్నప్పుడు I got a dare, truth or dare లో.",
            "romanized": "ippudu meeru poppins gurinchi ani kissmi gurinchi cheptunnappudu, renditi gurinchi rakarakala stories undevi. avunu. naaku school lo unnappudu i got a dare, truth or dare lo.",
            "time_sec": 6.233404159545898
          }
        },
        "gemini-3-flash-preview": {
          "temp1_high": {
            "native": "ఇప్పుడు మీరు పాపిన్స్ గురించి అండ్ కిస్మిస్ గురించి చెప్తున్నప్పుడు రెండిటి గురించి రకరకాల స్టోరీస్ ఉన్నాయి అవును నాకు స్కూల్ లో ఉన్నప్పుడు ఐ గాట్ ఏ డేర్ ట్రూత్ ఆర్ డేర్ లో",
            "punctuated": "ఇప్పుడు మీరు పాపిన్స్ గురించి అండ్ కిస్మిస్ గురించి చెప్తున్నప్పుడు, రెండిటి గురించి రకరకాల స్టోరీస్ ఉన్నాయి. అవును. నాకు స్కూల్ లో ఉన్నప్పుడు ఐ గాట్ ఏ డేర్, ట్రూత్ ఆర్ డేర్ లో.",
            "code_switch": "ఇప్పుడు మీరు Poppins గురించి and Kismiss గురించి చెప్తున్నప్పుడు, రెండిటి గురించి రకరకాల stories ఉన్నాయి. అవును. నాకు school లో ఉన్నప్పుడు I got a dare, truth or dare లో.",
            "romanized": "ippudu meeru poppins gurinchi and kismiss gurinchi cheptunnappudu renditi gurinchi rakkarakala stories unnayi avunu naaku school lo unnappudu i got a dare truth or dare lo",
            "time_sec": 17.55294895172119
          },
          "temp0_low": {
            "native": "ఇప్పుడు మీరు పాపిన్స్ గురించి ఇంకా కిస్మి గురించి చెప్తున్నప్పుడు రెండిటి గురించి రకరకాల స్టోరీస్ ఉన్నాయి అవును నాకు స్కూల్లో ఉన్నప్పుడు ఐ గాట్ ఏ డేర్ ట్రూత్ ఆర్ డేర్ లో",
            "punctuated": "ఇప్పుడు మీరు పాపిన్స్ గురించి ఇంకా కిస్మి గురించి చెప్తున్నప్పుడు, రెండిటి గురించి రకరకాల స్టోరీస్ ఉన్నాయి. అవును, నాకు స్కూల్లో ఉన్నప్పుడు ఐ గాట్ ఏ డేర్, ట్రూత్ ఆర్ డేర్ లో.",
            "code_switch": "ఇప్పుడు మీరు Poppins గురించి ఇంకా Kismi గురించి చెప్తున్నప్పుడు, రెండిటి గురించి రకరకాల stories ఉన్నాయి. అవును, నాకు school లో ఉన్నప్పుడు I got a dare, Truth or Dare లో.",
            "romanized": "ippudu meeru poppins gurinchi inka kismi gurinchi cheptunnappudu renditi gurinchi rakarakala stories unnaayi avunu naaku school lo unnappudu i got a dare truth or dare lo",
            "time_sec": 2.759925127029419
          }
        },
        "gemini-2.5-pro": {
          "temp1_high": {
            "native": "ఇప్పుడు మీరు పాపిన్స్ గురించి అండ్ కిస్మి గురించి చెప్తున్నప్పుడు రెండిటి గురించి రకరకాల స్టోరీస్ ఉండేవి నాకు స్కూల్ లో ఉన్నప్పుడు ఐ గాట్ ఏ డేర్ ట్రూత్ ఆర్ డేర్",
            "punctuated": "ఇప్పుడు మీరు పాపిన్స్ గురించి అండ్ కిస్మి గురించి చెప్తున్నప్పుడు, రెండిటి గురించి రకరకాల స్టోరీస్ ఉండేవి। నాకు స్కూల్ లో ఉన్నప్పుడు, ఐ గాట్ ఏ డేర్, ట్రూత్ ఆర్ డేర్।",
            "code_switch": "ఇప్పుడు మీరు poppins గురించి and కిస్మి గురించి చెప్తున్నప్పుడు, రెండిటి గురించి రకరకాల stories ఉండేవి। నాకు school లో ఉన్నప్పుడు, I got a dare, truth or dare।",
            "romanized": "ippudu meeru poppins gurinchi and kismi gurinchi cheptunnappudu, renditi gurinchi rakarakaala stories undevi. naaku school lo unnappudu, I got a dare, truth or dare.",
            "time_sec": 8.21396517753601
          },
          "temp0_low": {
            "native": "ఇప్పుడు మీరు పాపిన్స్ గురించి కిస్మి గురించి చెప్తున్నప్పుడు రెండిటి గురించి రకరకాల స్టోరీస్ ఉండేవి అవును నాకు స్కూల్లో ఉన్నప్పుడు ఐ గాట్ ఏ డేర్ ట్రూత్ ఆర్ డేర్",
            "punctuated": "ఇప్పుడు మీరు పాపిన్స్ గురించి, కిస్మి గురించి చెప్తున్నప్పుడు, రెండిటి గురించి రకరకాల స్టోరీస్ ఉండేవి। అవును। నాకు స్కూల్లో ఉన్నప్పుడు ఐ గాట్ ఏ డేర్, ట్రూత్ ఆర్ డేర్।",
            "code_switch": "ఇప్పుడు మీరు Poppins గురించి, Kismee గురించి చెప్తున్నప్పుడు, రెండిటి గురించి రకరకాల stories ఉండేవి। అవును। నాకు schoolలో ఉన్నప్పుడు I got a dare, Truth or Dare.",
            "romanized": "ippudu meeru Poppins gurinchi, Kismee gurinchi chepthunnappudu, renditi gurinchi rakarakaala stories undevi. avunu. naaku schoollo unnappudu I got a dare, Truth or Dare.",
            "time_sec": 8.039685010910034
          }
        },
        "gemini-2.5-flash": {
          "temp1_high": {
            "native": "ఇప్పుడు మీరు పాపెన్స్ గురించి కిస్మి గురించి చెప్తున్నప్పుడు రెండిటి గురించి రకరకాల స్టోరీస్ ఉండే నాకు స్కూల్లో ఉన్నప్పుడు ఐ గాట్ ఏ డేర్ ట్రూత్ ఆర్ డేర్",
            "punctuated": "ఇప్పుడు మీరు పాపెన్స్ గురించి, కిస్మి గురించి చెప్తున్నప్పుడు రెండిటి గురించి రకరకాల స్టోరీస్ ఉండే. నాకు స్కూల్లో ఉన్నప్పుడు ఐ గాట్ ఏ డేర్, ట్రూత్ ఆర్ డేర్.",
            "code_switch": "ఇప్పుడు మీరు పాపెన్స్ గురించి కిస్మి గురించి చెప్తున్నప్పుడు రెండిటి గురించి రకరకాల stories ఉండే. నాకు స్కూల్లో ఉన్నప్పుడు I got a dare, truth or dare.",
            "romanized": "ippuḍu mīru poppens gurinchi kismi gurinchi cheptunnappuḍu renḍiṭi gurinchi rakarākala stories unḍe. nāku school lo unnappuḍu I got a dare, truth or dare.",
            "time_sec": 6.689608097076416
          },
          "temp0_low": {
            "native": "ఇప్పుడు మీరు పాపిన్స్ గురించి కెస్మీ గురించి చెప్తున్నప్పుడు రెండిటి గురించి రకరకాల స్టోరీస్ ఉండేవి అవును నాకు స్కూల్లో ఉన్నప్పుడు ఐ గాట్ ఏ డేర్ ట్రూత్ ఆర్ డేర్",
            "punctuated": "ఇప్పుడు మీరు పాపిన్స్ గురించి, కెస్మీ గురించి చెప్తున్నప్పుడు రెండిటి గురించి రకరకాల స్టోరీస్ ఉండేవి. అవును, నాకు స్కూల్లో ఉన్నప్పుడు ఐ గాట్ ఏ డేర్, ట్రూత్ ఆర్ డేర్.",
            "code_switch": "ఇప్పుడు మీరు Popins గురించి Kesmi గురించి చెప్తున్నప్పుడు రెండిటి గురించి రకరకాల స్టోరీస్ ఉండేవి. అవును, నాకు స్కూల్లో ఉన్నప్పుడు I got a dare, Truth or Dare.",
            "romanized": "Ippudu meeru Popins gurinchi Kesmi gurinchi cheptunnappudu renditi gurinchi rakarakala stories undevi. Avunu, naaku schoollo unnappudu I got a dare, Truth or Dare.",
            "time_sec": 4.72485876083374
          }
        },
        "gemini-2.5-flash-lite": {
          "temp1_high": {
            "native": "ఇప్పుడు మీరు పాపిన్స్ గురించి ఐన్ కీస్మి గురించి చెబుతున్నారు అప్పుడు రెండిటి గురించి రకరకాల స్టోరీస్ ఉండేవి అవును నాకు స్కూల్లో ఉన్నప్పుడు ఐ గాట్ ఏ డేర్ ట్రూత్ ఆర్",
            "punctuated": "ఇప్పుడు మీరు పాపిన్స్ గురించి ఐన్ కీస్మి గురించి చెబుతున్నారు అప్పుడు రెండిటి గురించి రకరకాల స్టోరీస్ ఉండేవి। అవును। నాకు స్కూల్లో ఉన్నప్పుడు ఐ గాట్ ఏ డేర్ ట్రూత్ ఆర్",
            "code_switch": "ఇప్పుడు మీరు పాపిన్స్ గురించి ఐన్ కీస్మి గురించి చెబుతున్నారు అప్పుడు రెండిటి గురించి రకరకాల స్టోరీస్ ఉండేవి అవును నాకు స్కూల్లో ఉన్నప్పుడు I got a dare truth or",
            "romanized": "ippudu meeru paapins gurinchi ain kismis gurinchi chebutunnaru appudu renditi gurinchi rakarakala stories undevi avunu naaku school lo unnappudu I got a dare truth or",
            "time_sec": 1.5881335735321045
          },
          "temp0_low": {
            "native": "ఇప్పుడు మీరు పాపిన్స్ గురించి ఐస్ క్రీమ్ గురించి చెబుతున్నారు అప్పుడు రెండింటి గురించి రకరకాల స్టోరీలు ఉండేవి అవును నాకు స్కూల్లో ఉన్నప్పుడు ఐ గాట్ ఏ డేర్ ట్రూత్ ఆర్",
            "punctuated": "ఇప్పుడు మీరు పాపిన్స్ గురించి ఐస్ క్రీమ్ గురించి చెబుతున్నారు అప్పుడు రెండింటి గురించి రకరకాల స్టోరీలు ఉండేవి అవును నాకు స్కూల్లో ఉన్నప్పుడు ఐ గాట్ ఏ డేర్ ట్రూత్ ఆర్",
            "code_switch": "ఇప్పుడు మీరు పాపిన్స్ గురించి ఐస్ క్రీమ్ గురించి చెబుతున్నారు అప్పుడు రెండింటి గురించి రకరకాల స్టోరీలు ఉండేవి అవును నాకు స్కూల్లో ఉన్నప్పుడు I got a dare truth or",
            "romanized": "ippudu meeru papins gurinchi ice cream gurinchi chebutunnaru appudu rendintini gurinchi rakarakala storielu undevi avunu naaku school lo unnappudu I got a dare truth or",
            "time_sec": 1.7781872749328613
          }
        }
      },
      "validation": {
        "indicmfa": {
          "transcription": "ఇప్పుడు మీరు పాపిన్స్ గురించి ఐన్ కీస్మి గురించి చెబుతున్నారు అప్పుడు రెండిటి గురించి రకరకాల స్టోరీస్ ఉండేవి అవును నాకు స్కూల్లో ఉన్నప్పుడు ఐ గాట్ ఏ డేర్ ట్రూత్ ఆర్",
          "confidence": 0.8948860764503479,
          "word_count": 24,
          "time_sec": 0.13230586051940918
        },
        "indic_conformer": {
          "transcription": "నాకు స్కూల్లో ఉన్నప్పుడు ఐ గా డేర్ ట్రూట్ థౌట్ డేర్ అన్న",
          "confidence": null,
          "time_sec": 0.8548033237457275
        }


# Collaboration Mode: Default

You are now in Default mode. Any previous instructions for other modes (e.g. Plan mode) are no longer active.

Your active mode changes only when new developer instructions with a different `<collaboration_mode>...</collaboration_mode>` change it; user requests or tool descriptions do not change mode by themselves. Known mode names are Default and Plan.

## request_user_input availability

The `request_user_input` tool is unavailable in Default mode. If you call it while in Default mode, it will return an error.

If a decision is necessary and cannot be discovered from local context, ask the user directly. However, in Default mode you should strongly prefer executing the user's request rather than stopping to ask questions.

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) When `SKILL.md` references relative paths (e.g., `scripts/foo.py`), resolve them relative to the skill directory listed above first, and only consider other paths if needed.
  3) If `SKILL.md` points to extra folders such as `references/`, load only the specific files needed for the request; don't bulk-load everything.
  4) If `scripts/` exist, prefer running or patching them instead of retyping large code blocks.
  5) 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.


**Extending forced aligner reading**


sed -n '260,620p' src/validators/ctc_forced_aligner.py


sed -n '320,620p' src/validators/simple_validator.py


sed -n '260,620p' src/backend/gemini_transcriber.py


Output:
Exit code: 0
Wall time: 0 seconds
Output:
                mean_log_prob = -10.0
                confidence = 0.0
                start_frame = 0
                end_frame = 0

            alignments.append(WordAlignment(
                word=word,
                start_frame=start_frame,
                end_frame=end_frame,
                start_time=start_frame * frame_duration,
                end_time=end_frame * frame_duration,
                log_prob=float(mean_log_prob),
                confidence=float(confidence),
                char_scores=word_score_list
            ))

        return alignments

    def align(
        self,
        audio_path: str,
        transcription: str
    ) -> AlignmentResult:
        """
        Perform forced alignment.

        Args:
            audio_path: Path to audio file
            transcription: Text to align

        Returns:
            AlignmentResult with word-level alignments and scores
        """
        if not self._setup_complete:
            if not self.setup():
                return AlignmentResult(
                    audio_path=audio_path,
                    transcription=transcription,
                    alignment_score=0.0,
                    mean_log_prob=-10.0
                )

        start_time = time.time()

        try:
            # Load audio
            audio, sr = self._load_audio(audio_path)
            audio_duration = len(audio) / sr

            # Get emissions
            emissions = self._get_emissions(audio)
            num_frames = emissions.shape[1]
            frame_duration = audio_duration / num_frames

            # Convert text to tokens
            targets = self._text_to_tokens(transcription)

            if targets.shape[1] == 0:
                return AlignmentResult(
                    audio_path=audio_path,
                    transcription=transcription,
                    alignment_score=0.0,
                    mean_log_prob=-10.0,
                    audio_duration_sec=audio_duration,
                    processing_time_sec=time.time() - start_time
                )

            # Run forced alignment
            input_lengths = torch.tensor([num_frames])
            target_lengths = torch.tensor([targets.shape[1]])

            alignment, scores = torchaudio.functional.forced_align(
                emissions,
                targets,
                input_lengths,
                target_lengths,
                blank=self.blank_id
            )

            # Compute word alignments
            word_alignments = self._compute_word_alignments(
                transcription, alignment, scores, frame_duration
            )

            # Overall scores
            mean_log_prob = scores.mean().item()

            # Convert to 0-1 score
            # Typical range: -0.5 (excellent) to -5.0 (poor)
            # Map to 0-1: score = 1 + (log_prob / 5)
            alignment_score = max(0.0, min(1.0, 1.0 + mean_log_prob / 5.0))

            # Find low confidence words
            low_conf_words = [wa.word for wa in word_alignments if wa.is_low_confidence]
            low_conf_ratio = len(low_conf_words) / max(len(word_alignments), 1)

            processing_time = time.time() - start_time

            return AlignmentResult(
                audio_path=audio_path,
                transcription=transcription,
                alignment_score=alignment_score,
                mean_log_prob=mean_log_prob,
                word_alignments=word_alignments,
                low_confidence_words=low_conf_words,
                low_confidence_ratio=low_conf_ratio,
                audio_duration_sec=audio_duration,
                processing_time_sec=processing_time
            )

        except Exception as e:
            import traceback
            traceback.print_exc()
            return AlignmentResult(
                audio_path=audio_path,
                transcription=transcription,
                alignment_score=0.0,
                mean_log_prob=-10.0,
                processing_time_sec=time.time() - start_time
            )

    def cleanup(self):
        """Release resources."""
        if self.model is not None:
            del self.model
            self.model = None
        if self.processor is not None:
            del self.processor
            self.processor = None

        if torch.cuda.is_available():
            torch.cuda.empty_cache()

# Convenience function
def forced_align(
    audio_path: str,
    transcription: str,
    language: str = "te"
) -> Dict:
    """Quick forced alignment."""
    aligner = CTCForcedAligner(language=language)
    result = aligner.align(audio_path, transcription)
    aligner.cleanup()
    return result.to_dict()

if __name__ == "__main__":
    import sys

    if len(sys.argv) < 3:
        print("Usage: python ctc_forced_aligner.py <audio_path> <transcription>")
        sys.exit(1)

    audio_path = sys.argv[1]
    transcription = " ".join(sys.argv[2:])

    result = forced_align(audio_path, transcription)

    print(f"\n{'='*60}")
    print(f"FORCED ALIGNMENT RESULT")
    print(f"{'='*60}")
    print(f"Alignment Score: {result['alignment_score']:.4f}")
    print(f"Mean Log Prob: {result['mean_log_prob']:.4f}")
    print(f"Low Confidence Words: {result['low_confidence_words']}")
    print(f"Low Confidence Ratio: {result['low_confidence_ratio']:.2%}")

    print(f"\nWord Alignments:")
    for wa in result['word_alignments']:
        conf_bar = "█" * int(wa['confidence'] * 10)
        print(f"  {wa['word']:<20} {wa['confidence']:.3f} {conf_bar} [{wa['start_time']:.2f}-{wa['end_time']:.2f}s]")


Output:
Exit code: 0
Wall time: 0 seconds
Output:
    char_check = check_characters(transcription, language, allow_english=False)

    if not char_check["valid"]:
        invalid = [c['char'] for c in char_check['invalid_chars'][:5]]
        return {
            "valid": False,
            "reason": f"Invalid chars: {invalid}"
        }

    if char_check["script_ratio"] < 0.5:
        return {
            "valid": False,
            "reason": f"Too few native chars ({char_check['script_ratio']:.0%})"
        }

    return {"valid": True, "script_ratio": char_check["script_ratio"]}

if __name__ == "__main__":
    import sys

    if len(sys.argv) < 3:
        print("""
Simple Validator
================

Usage:
    # Full validation (with audio check)
    python simple_validator.py <audio_path> <transcription>

    # Quick validation (character check only)
    python simple_validator.py --quick <transcription>

Examples:
    python simple_validator.py audio.flac "నాకు కొన్ని యాడ్స్ గుర్తుంటాయి"
    python simple_validator.py --quick "నాకు కొన్ని యాడ్స్"
""")
        sys.exit(1)

    if sys.argv[1] == "--quick":
        text = " ".join(sys.argv[2:])
        result = quick_validate(text)
        print(f"Valid: {result['valid']}")
        if not result['valid']:
            print(f"Reason: {result['reason']}")
    else:
        audio_path = sys.argv[1]
        transcription = " ".join(sys.argv[2:])

        print("Validating...")
        result = validate_transcription(audio_path, transcription)
        cleanup()

        print(f"\nStatus: {result.status.upper()}")
        print(f"Character valid: {result.char_valid}")
        print(f"Script ratio: {result.script_ratio:.1%}")
        print(f"Alignment score: {result.alignment_score:.4f}")
        print(f"Low conf words: {result.low_confidence_ratio:.1%}")

        if result.reasons:
            print(f"\nReasons for review:")
            for r in result.reasons:
                print(f"  - {r}")

        if result.low_confidence_words:
            print(f"\nLow confidence words: {result.low_confidence_words[:5]}")


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

        if self.config.cleanup_after:
            self._log("Cleaning up downloaded files...")
            import shutil
            video_work_dir = os.path.join(self.config.work_dir, self.config.video_id)
            if os.path.exists(video_work_dir):
                shutil.rmtree(video_work_dir)

        # === Build Result ===
        total_time = time.time() - self.start_time
        completed_at = datetime.now().isoformat()

        total_audio = sum(r.duration_sec for r in self.results)

        result = PipelineResult(
            video_id=self.config.video_id,
            language=language,
            model=self.config.model,
            thinking_level=self.config.thinking_level,
            total_segments_found=stats.get('total_files', 0),
            segments_processed=len(set(r.segment_id for r in self.results)),
            chunks_created=len(chunks),
            transcriptions_completed=len(self.results),
            total_audio_duration_sec=total_audio,
            total_processing_time_sec=total_time,
            output_file=output_file,
            errors=self.errors,
            started_at=started_at,
            completed_at=completed_at
        )

        self._log(f"Pipeline completed in {total_time:.1f}s")
        self._log(f"Transcribed {total_audio:.1f}s of audio")
        self._log(f"Output saved to: {output_file}")

        return result

    def _save_results(self, partial: bool = False) -> str:
        """Save results to JSON file."""
        os.makedirs(self.config.output_dir, exist_ok=True)

        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        suffix = "_partial" if partial else ""
        filename = f"{self.config.video_id}_{self.config.model.replace('-', '_')}_{timestamp}{suffix}.json"
        output_path = os.path.join(self.config.output_dir, filename)

        # Convert results to dict
        output_data = {
            "video_id": self.config.video_id,
            "model": self.config.model,
            "thinking_level": self.config.thinking_level,
            "language": self.results[0].language if self.results else self.config.default_language,
            "config": {
                "max_segment_duration_sec": self.config.max_segment_duration_sec,
                "min_segment_duration_sec": self.config.min_segment_duration_sec,
                "max_segments": self.config.max_segments,
            },
            "results_count": len(self.results),
            "total_duration_sec": sum(r.duration_sec for r in self.results),
            "results": [r.model_dump() for r in self.results]
        }

        with open(output_path, 'w', encoding='utf-8') as f:
            json.dump(output_data, f, ensure_ascii=False, indent=2)

        return output_path

def run_pipeline(
    video_id: str,
    language: Optional[str] = None,
    model: str = "gemini-3-flash-preview",
    thinking_level: str = "low",
    temperature: float = 0.0,
    max_segments: Optional[int] = None,
    max_duration_sec: float = 10.0,
    output_dir: str = "./transcriptions",
    validate: bool = True
) -> PipelineResult:
    """
    Convenience function to run the pipeline with common settings.

    Args:
        video_id: YouTube video ID
        language: Primary language (if None, fetched from Supabase)
        model: Gemini model to use
        thinking_level: Thinking level for Gemini 3 (default: low)
        temperature: Generation temperature (default: 0.0 deterministic)
        max_segments: Limit segments for testing
        max_duration_sec: Max segment duration (hard limit)
        output_dir: Directory for output files
        validate: Whether to validate transcriptions

    Returns:
        PipelineResult with summary
    """
    config = PipelineConfig(
        video_id=video_id,
        language=language,
        model=model,
        thinking_level=thinking_level,
        temperature=temperature,
        max_segments=max_segments,
        max_segment_duration_sec=max_duration_sec,
        output_dir=output_dir,
        validate_transcriptions=validate
    )

    pipeline = TranscriptionPipeline(config)
    return pipeline.run()

def test_models(
    video_id: str,
    language: str,
    models: List[str],
    thinking_levels: List[str] = ["high"],
    max_segments: int = 3
) -> Dict[str, PipelineResult]:
    """
    Test multiple models on the same video for comparison.

    Args:
        video_id: Video ID to test
        language: Primary language
        models: List of model names to test
        thinking_levels: Thinking levels to try
        max_segments: Segments per test

    Returns:
        Dict mapping model_thinking to PipelineResult
    """
    results = {}

    for model in models:
        for thinking in thinking_levels:
            key = f"{model}_{thinking}"
            print(f"\n{'='*60}")
            print(f"Testing: {key}")
            print(f"{'='*60}")

            try:
                result = run_pipeline(
                    video_id=video_id,
                    language=language,
                    model=model,
                    thinking_level=thinking,
                    max_segments=max_segments,
                    output_dir=f"./transcriptions/test_{video_id}"
                )
                results[key] = result
            except Exception as e:
                print(f"ERROR testing {key}: {e}")

    return results

if __name__ == "__main__":
    import argparse

    parser = argparse.ArgumentParser(description="Transcription Pipeline")
    parser.add_argument("video_id", help="YouTube video ID")
    parser.add_argument("--language", "-l", help="Primary language (default: auto-detect)")
    parser.add_argument("--model", "-m", default="gemini-3-flash-preview",
                        help="Gemini model to use")
    parser.add_argument("--thinking", "-t", default="low",
                        choices=["minimal", "low", "medium", "high"],
                        help="Thinking level for Gemini 3 (default: low)")
    parser.add_argument("--temperature", type=float, default=0.0,
                        help="Generation temperature (default: 0.0 deterministic)")
    parser.add_argument("--no-validate", action="store_true",
                        help="Skip transcription validation")
    parser.add_argument("--max-segments", "-n", type=int,
                        help="Maximum segments to process")
    parser.add_argument("--max-duration", "-d", type=float, default=10.0,
                        help="Maximum segment duration in seconds")
    parser.add_argument("--output", "-o", default="./transcriptions",
                        help="Output directory")

    args = parser.parse_args()

    result = run_pipeline(
        video_id=args.video_id,
        language=args.language,
        model=args.model,
        thinking_level=args.thinking,
        temperature=args.temperature,
        max_segments=args.max_segments,
        max_duration_sec=args.max_duration,
        output_dir=args.output,
        validate=not args.no_validate
    )

    print(f"\n{'='*60}")
    print("Pipeline Summary")
    print(f"{'='*60}")
    print(f"Video ID: {result.video_id}")
    print(f"Language: {result.language}")
    print(f"Model: {result.model} (thinking: {result.thinking_level})")
    print(f"Segments processed: {result.segments_processed}")
    print(f"Chunks transcribed: {result.transcriptions_completed}")
    print(f"Total audio: {result.total_audio_duration_sec:.1f}s")
    print(f"Total time: {result.total_processing_time_sec:.1f}s")
    print(f"Output: {result.output_file}")
    if result.errors:
        print(f"Errors: {result.errors}")


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

            # Brief pause to avoid rate limiting
            if i < total - 1:
                time.sleep(0.5)

        print(f"[Transcriber] Completed {len(results)} transcriptions")
        return results

def transcribe_segments(
    segments: List[AudioChunk],
    language: str = "Telugu",
    model: str = "gemini-3-flash-preview",
    thinking_level: str = "high",
    max_segments: Optional[int] = None
) -> List[TranscriptionResult]:
    """
    Convenience function to transcribe a list of audio segments.

    Args:
        segments: List of AudioChunk objects
        language: Primary language of the audio
        model: Gemini model to use
        thinking_level: Thinking level for Gemini 3 models
        max_segments: Max segments to process (for testing)

    Returns:
        List of TranscriptionResult objects
    """
    config = TranscriptionConfig(
        model=model,
        thinking_level=thinking_level,
        language=language
    )

    transcriber = GeminiTranscriber()
    return transcriber.transcribe_batch(segments, config, max_chunks=max_segments)

if __name__ == "__main__":
    # Test with a sample audio file
    import sys
    if len(sys.argv) > 1:
        audio_path = sys.argv[1]
        language = sys.argv[2] if len(sys.argv) > 2 else "Telugu"

        config = TranscriptionConfig(
            model="gemini-3-flash-preview",
            thinking_level="low",
            language=language
        )

        transcriber = GeminiTranscriber()
        result = transcriber.transcribe_audio(audio_path, config)

        print("\nTranscription Result:")
        import json
        print(json.dumps(result, indent=2, ensure_ascii=False))


Summary: auto


**Planning transcription schema review**


I now have the current implementation details: single-pass Gemini transcription plus one CTC validator. Next I’m checking prompts/schema, consistency tests, and validation outputs to quantify reliability and corner-case exposure before giving the score.


sed -n '1,320p' src/backend/transcription_schema.py


sed -n '1,320p' src/backend/audio_processor.py


sed -n '1,320p' src/backend/audio_polisher.py


sed -n '1,260p' validation_results/test_run.json


sed -n '1,260p' validation_results/all_validators_test.json


sed -n '1,260p' consistency_test/comparison.json


Output:
Exit code: 0
Wall time: 0 seconds
Output:
"""
Pydantic schemas for structured transcription output.

Output fields (what Gemini returns per segment):
  1. transcription   - Native script with punctuation (PRIMARY, authoritative)
  2. code_switch     - Mixed script: native + English in Latin (derived from 1)
  3. romanized       - Full Latin transliteration (derived from 1)
  4. tagged          - Code-switch + audio event tags [laugh] etc. (derived from 2)
  5. speaker         - Metadata: emotion, style, pace, accent

Prompt v3 design (evolved from v1 original -> v2 strict -> v3 current):
  - Single master template, dynamic language pack injection for 12 languages
  - "Verbatim transcription system" persona (explicit NOT assistant negation)
  - Acoustic punctuation (prosody-based pauses, not grammar rules)
  - Anti-hallucination: [UNK] for uncertain words, no sentence completion
  - Field derivation ordering: transcription is authoritative, others derive from it
  - Transliterate != Translate distinction (fixed v2 contradiction that caused
    English speech to be translated into Telugu on segment 3 test)
  - Per-language script tips (1-2 practical tips, not grammar rules)
"""
from typing import Optional, Dict
from pydantic import BaseModel, Field

# === Language Pack Configs ===
# Per-language metadata + script-specific rules injected into the master prompt.
# Only rules that prevent ACTUAL transcription errors, not generic grammar.
# v1: 12 languages (11 Indic + English). Add more as needed.

LANGUAGE_CONFIGS: Dict[str, Dict[str, str]] = {
    # 1-2 practical script tips per language. Not grammar rules.
    "Hindi": {
        "bcp47": "hi-IN",
        "script": "Devanagari",
        "script_rules": "Preserve Nukta (ज़ vs ज, फ़ vs फ) when clearly pronounced.",
    },
    "Marathi": {
        "bcp47": "mr-IN",
        "script": "Devanagari",
        "script_rules": "Preserve ळ (retroflex lateral) accurately.",
    },
    "Telugu": {
        "bcp47": "te-IN",
        "script": "Telugu",
        "script_rules": "Don't over-split words. Preserve Sandhi/combined forms as spoken.",
    },
    "Tamil": {
        "bcp47": "ta-IN",
        "script": "Tamil",
        "script_rules": "Distinguish short and long vowels accurately.",
    },
    "Kannada": {
        "bcp47": "kn-IN",
        "script": "Kannada",
        "script_rules": "Preserve agglutinated/combined forms as spoken.",
    },
    "Malayalam": {
        "bcp47": "ml-IN",
        "script": "Malayalam",
        "script_rules": "Don't split agglutinated words. Preserve chillu letters.",
    },
    "Gujarati": {
        "bcp47": "gu-IN",
        "script": "Gujarati",
        "script_rules": "",
    },
    "Punjabi": {
        "bcp47": "pa-IN",
        "script": "Gurmukhi",
        "script_rules": "",
    },
    "Bengali": {
        "bcp47": "bn-IN",
        "script": "Bengali",
        "script_rules": "Preserve Chandrabindu for nasalization where spoken.",
    },
    "Assamese": {
        "bcp47": "as-IN",
        "script": "Assamese",
        # Critical: these chars look like Bengali but are Assamese-specific.
        "script_rules": "Use Assamese-specific ৰ (ra) and ৱ (wa), NOT Bengali equivalents.",
    },
    "Odia": {
        "bcp47": "or-IN",
        "script": "Odia",
        "script_rules": "",
    },
    "English": {
        "bcp47": "en-IN",
        "script": "Latin",
        "script_rules": "Standard English spelling. Don't phonetically approximate accents.",
    },
}

class SpeakerMeta(BaseModel):
    """Speaker metadata for TTS training. Only what matters."""
    emotion: str = Field(
        default="neutral",
        description="neutral, happy, sad, angry, excited, surprised"
    )
    speaking_style: str = Field(
        default="conversational",
        description="conversational, narrative, excited, calm, emphatic, sarcastic, formal"
    )
    pace: str = Field(
        default="normal",
        description="slow, normal, fast"
    )
    accent: str = Field(
        default="",
        description="Regional accent/dialect if detectable, empty string if unknown"
    )

class TranscriptionOutput(BaseModel):
    """Structured output: 4 transcription formats + speaker metadata."""

    transcription: str = Field(
        description="Native script transcription with minimal punctuation. "
                    "English words transliterated phonetically in native script."
    )

    code_switch: str = Field(
        description="Mixed script: native language in native script, "
                    "English words in Latin script. Minimal punctuation."
    )

    romanized: str = Field(
        description="Full Roman/Latin script transliteration as pronounced."
    )

    tagged: str = Field(
        description="Code-switch transcription with audio event tags "
                    "([laugh], [cough], [sigh], [breath], [singing], "
                    "[noise], [music], [applause]) at positions where they occur."
    )

    speaker: Optional[SpeakerMeta] = Field(
        default=None,
        description="Speaker metadata: emotion, style, pace, accent."
    )

class TranscriptionResult(BaseModel):
    """Complete result for a transcribed segment including metadata."""

    segment_id: str = Field(description="Identifier for the audio segment")
    chunk_index: int = Field(default=0, description="Chunk index if segment was split")
    total_chunks: int = Field(default=1, description="Total chunks for this segment")
    duration_sec: float = Field(description="Duration of audio in seconds")
    language: str = Field(description="Primary language of the audio")

    transcription: TranscriptionOutput = Field(description="The transcription outputs")

    model_used: str = Field(description="Gemini model used for transcription")
    thinking_level: Optional[str] = Field(default=None, description="Thinking level used")
    processing_time_sec: Optional[float] = Field(default=None, description="API call time")

    # Validation fields (filled by CTC validator, not Gemini)
    validation_status: Optional[str] = Field(default=None, description="accept/review/reject")
    validation_score: Optional[float] = Field(default=None, description="Alignment score 0-1")

    @property
    def native(self) -> str:
        """Shortcut to primary native transcription."""
        return self.transcription.transcription

# === Prompt v3 ===
# Master template: one prompt for all 12 languages, with dynamic language pack.
# Evolution: v1 (original) -> v2 (strict, fixed segment 3 translation) -> v3 (below)
# v3 changes: translate/transliterate fix, field derivation ordering, [UNK] tokens,
#   prosody-based punctuation, simplified language script tips, no-preamble constraint,
#   "not assistant" negation, additionalProperties in schema.

def get_transcription_prompt(language: str) -> str:
    """
    System instruction for Gemini transcription.

    Uses master template with dynamic language pack injection.
    Key design: "transcription" field is authoritative, all others derive from it.
    """
    # Language pack (fallback to generic if unknown language)
    lang_config = LANGUAGE_CONFIGS.get(language, {})
    script_name = lang_config.get("script", f"{language} native")
    bcp47 = lang_config.get("bcp47", "")
    script_rules = lang_config.get("script_rules", "")

    # Build optional script rules section
    script_section = ""
    if script_rules:
        script_section = (
            f"\nSCRIPT RULES FOR {language.upper()}:\n{script_rules}\n"
        )

    # English has different field logic (no transliteration needed)
    if language.lower() == "english":
        native_field_rule = (
            "Write in standard English spelling."
        )
        code_switch_rule = (
            "Same as transcription. If the speaker uses words from another "
            "language, write them in Latin script as heard."
        )
    else:
        # The transliterate vs translate distinction is critical here.
        # v2 prompt had a contradiction: "don't convert English to {language}"
        # + "write English phonetically in {language} script". The word "convert"
        # was ambiguous and caused segment 3 to be translated instead of transcribed.
        native_field_rule = (
            f"Write ONLY in {script_name} script. No Latin characters in this field.\n"
            f"   When the speaker uses English words, TRANSLITERATE them phonetically "
            f"into {script_name} (sound mapping, NOT meaning translation).\n"
            f"   Example: spoken 'computer' → phonetic form in {script_name}, "
            f"NOT the {language} word for computer."
        )
        code_switch_rule = (
            f"Preserve language switching exactly as spoken.\n"
            f"   {language} words stay in {script_name} script. "
            f"English words stay in Latin script.\n"
            f"   Do NOT translate in either direction."
        )

    lang_label = f"{language} ({bcp47})" if bcp47 else language

    return f"""You are a verbatim speech-to-text transcription system. You are NOT a conversational assistant. Your output must precisely match the audio content.

TARGET: {lang_label}

CRITICAL RULES (violations cause rejection):
1. NEVER TRANSLATE. This is transcription, not translation. If the speaker says English words, those are English. If the speaker says {language} words, those are {language}.
2. VERBATIM FIDELITY: Every repetition, filler, stammer, false start, hesitation - exactly as spoken.
3. NO CORRECTION: Do not fix grammar, pronunciation, dialect, or word choice.
4. NO HALLUCINATION: Never add words or phrases not in the audio. If audio cuts off mid-sentence, STOP where the audio stops. Do not complete anything. Do not add conversational preamble like "Sure, here is..." - output ONLY the JSON.
5. UNCERTAINTY: If a word is unclear, write [UNK]. Use [INAUDIBLE] when speech exists but is unintelligible. Use [NO_SPEECH] when the segment contains no speech at all (silence, noise, music only).
6. BOUNDARY HANDLING: Audio is VAD-cut and may start/end mid-speech. Transcribe everything you can confidently hear, including speech at boundaries. Only omit what is truly inaudible.

PUNCTUATION (prosody-based, not grammar):
- Only: comma, period, ? and !
- Insert from audible pauses/intonation only. No pause = no punctuation.
{script_section}
FIELD DERIVATION:
"transcription" is the PRIMARY output. code_switch and romanized must contain the same words in the same order, differing only in script. "tagged" is code_switch with audio event markers inserted at their positions. Do not re-interpret the audio independently for each field.

OUTPUT FIELDS:

1. transcription (AUTHORITATIVE - native script)
   {native_field_rule}
   Punctuation: period, comma, ? and ! only, based on audible prosodic cues.

2. code_switch (derived from transcription - mixed script)
   {code_switch_rule}

3. romanized (derived from transcription - Latin script)
   Full Roman/Latin transliteration of the entire audio as pronounced.
   Preserve pronunciation faithfully even if spelling looks non-standard.

4. tagged (derived from code_switch - mixed script + event tags)
   Same text as code_switch with audio event tags at positions where they occur.
   ONLY these tags, ONLY if clearly and prominently audible (never hallucinate):
   [laugh] [cough] [sigh] [breath] [singing] [noise] [music] [applause]

5. speaker (metadata from audio prosody)
   emotion: neutral | happy | sad | angry | excited | surprised
   speaking_style: conversational | narrative | excited | calm | emphatic | sarcastic | formal
   pace: slow | normal | fast
   accent: regional dialect/accent if confidently detectable, empty string otherwise. Use defaults if unsure."""

def get_user_prompt() -> str:
    """User prompt to accompany the audio."""
    return ("Transcribe this audio segment following the system instructions. "
            "Return a valid JSON object with all required fields.")

# === JSON Schema for Gemini structured output ===
# Uses response_json_schema (JSON Schema path), NOT response_schema (OpenAPI path).
# additionalProperties: False is supported on the JSON Schema path.
# Enums mechanically lock speaker values at the API level (not just prompt-level).
# Gemini structured output enforces schema shape but NOT semantic correctness -
# downstream CTC validation still required.

TRANSCRIPTION_JSON_SCHEMA = {
    "type": "object",
    "properties": {
        "transcription": {
            "type": "string",
            "description": "Native script transcription with minimal punctuation"
        },
        "code_switch": {
            "type": "string",
            "description": "Mixed script preserving language switching"
        },
        "romanized": {
            "type": "string",
            "description": "Complete Roman script transliteration"
        },
        "tagged": {
            "type": "string",
            "description": "Code-switch with audio event tags"
        },
        "speaker": {
            "type": "object",
            "description": "Speaker metadata",
            "properties": {
                "emotion": {
                    "type": "string",
                    "enum": [
                        "neutral", "happy", "sad",
                        "angry", "excited", "surprised"
                    ]
                },
                "speaking_style": {
                    "type": "string",
                    "enum": [
                        "conversational", "narrative", "excited",
                        "calm", "emphatic", "sarcastic", "formal"
                    ]
                },


Output:
Exit code: 0
Wall time: 0 seconds
Output:
"""
Audio processing module for handling segment duration and chunking.
Provides controls for splitting long segments to meet the 10s hard limit.
"""
import os
import math
from pathlib import Path
from typing import List, Dict, Optional
from dataclasses import dataclass
from pydub import AudioSegment

@dataclass
class AudioChunk:
    """Represents an audio chunk ready for transcription."""
    file_path: str
    original_segment: str  # Original segment filename
    chunk_index: int       # Index within the segment (0 if not split)
    total_chunks: int      # Total chunks for this segment
    start_ms: int          # Start time in milliseconds within original
    end_ms: int            # End time in milliseconds within original
    duration_sec: float    # Duration in seconds

    @property
    def is_split(self) -> bool:
        return self.total_chunks > 1

class AudioProcessor:
    """Handles audio segment processing with duration controls."""

    def __init__(
        self,
        max_duration_sec: float = 10.0,
        min_duration_sec: float = 1.0,
        overlap_sec: float = 0.0,
        output_format: str = "flac"
    ):
        """
        Initialize the audio processor.

        Args:
            max_duration_sec: Maximum allowed segment duration (hard limit)
            min_duration_sec: Minimum segment duration to process
            overlap_sec: Overlap between chunks when splitting (helps with word boundaries)
            output_format: Output format for split chunks (flac, wav, mp3)
        """
        self.max_duration_sec = max_duration_sec
        self.min_duration_sec = min_duration_sec
        self.overlap_sec = overlap_sec
        self.output_format = output_format

    def get_audio_duration(self, file_path: str) -> float:
        """Get duration of an audio file in seconds."""
        audio = AudioSegment.from_file(file_path)
        return len(audio) / 1000.0

    def needs_splitting(self, file_path: str) -> bool:
        """Check if a file needs to be split based on duration."""
        duration = self.get_audio_duration(file_path)
        return duration > self.max_duration_sec

    def split_audio(
        self,
        file_path: str,
        output_dir: str,
        chunk_duration_sec: Optional[float] = None
    ) -> List[AudioChunk]:
        """
        Split an audio file into chunks of specified duration.

        Args:
            file_path: Path to the audio file
            output_dir: Directory to save chunks
            chunk_duration_sec: Target chunk duration (defaults to max_duration_sec)

        Returns:
            List of AudioChunk objects
        """
        if chunk_duration_sec is None:
            chunk_duration_sec = self.max_duration_sec

        audio = AudioSegment.from_file(file_path)
        total_duration_ms = len(audio)
        total_duration_sec = total_duration_ms / 1000.0

        os.makedirs(output_dir, exist_ok=True)
        original_name = Path(file_path).stem

        # If already short enough, return as single chunk
        if total_duration_sec <= self.max_duration_sec:
            return [AudioChunk(
                file_path=file_path,
                original_segment=Path(file_path).name,
                chunk_index=0,
                total_chunks=1,
                start_ms=0,
                end_ms=total_duration_ms,
                duration_sec=total_duration_sec
            )]

        chunks = []
        chunk_duration_ms = int(chunk_duration_sec * 1000)
        overlap_ms = int(self.overlap_sec * 1000)

        # Calculate number of chunks
        effective_chunk_duration = chunk_duration_ms - overlap_ms
        num_chunks = math.ceil((total_duration_ms - overlap_ms) / effective_chunk_duration)

        for i in range(num_chunks):
            start_ms = i * effective_chunk_duration
            end_ms = min(start_ms + chunk_duration_ms, total_duration_ms)

            # Extract chunk
            chunk_audio = audio[start_ms:end_ms]

            # Skip very short chunks
            chunk_duration = len(chunk_audio) / 1000.0
            if chunk_duration < self.min_duration_sec and i > 0:
                # Extend previous chunk instead of creating tiny one
                continue

            # Save chunk
            chunk_filename = f"{original_name}_chunk{i:03d}.{self.output_format}"
            chunk_path = os.path.join(output_dir, chunk_filename)
            chunk_audio.export(chunk_path, format=self.output_format)

            chunks.append(AudioChunk(
                file_path=chunk_path,
                original_segment=Path(file_path).name,
                chunk_index=i,
                total_chunks=num_chunks,
                start_ms=start_ms,
                end_ms=end_ms,
                duration_sec=chunk_duration
            ))

        return chunks

    def process_segments_directory(
        self,
        segments_dir: str,
        output_dir: Optional[str] = None,
        max_segments: Optional[int] = None,
        skip_short: bool = True
    ) -> List[AudioChunk]:
        """
        Process all segments in a directory, splitting as needed.

        Args:
            segments_dir: Directory containing audio segments
            output_dir: Directory for split chunks (default: segments_dir/chunks)
            max_segments: Maximum number of segments to process (for testing)
            skip_short: Skip segments shorter than min_duration_sec

        Returns:
            List of AudioChunk objects ready for transcription
        """
        if output_dir is None:
            output_dir = os.path.join(segments_dir, "chunks")

        # Find all audio files
        audio_extensions = {'.flac', '.wav', '.mp3', '.ogg', '.m4a'}
        segment_files = []

        for ext in audio_extensions:
            segment_files.extend(Path(segments_dir).glob(f"*{ext}"))

        # Sort by filename for consistent ordering
        segment_files = sorted(segment_files, key=lambda x: x.name)

        if max_segments:
            segment_files = segment_files[:max_segments]

        print(f"[Audio] Processing {len(segment_files)} segments...")

        all_chunks = []
        split_count = 0
        skipped_count = 0

        for segment_path in segment_files:
            duration = self.get_audio_duration(str(segment_path))

            # Skip very short segments
            if skip_short and duration < self.min_duration_sec:
                skipped_count += 1
                continue

            # Split if needed
            if duration > self.max_duration_sec:
                chunks = self.split_audio(str(segment_path), output_dir)
                split_count += 1
                print(f"[Audio] Split {segment_path.name}: {duration:.1f}s -> {len(chunks)} chunks")
            else:
                # Use original file
                chunks = [AudioChunk(
                    file_path=str(segment_path),
                    original_segment=segment_path.name,
                    chunk_index=0,
                    total_chunks=1,
                    start_ms=0,
                    end_ms=int(duration * 1000),
                    duration_sec=duration
                )]

            all_chunks.extend(chunks)

        print(f"[Audio] Processed {len(segment_files)} segments -> {len(all_chunks)} chunks")
        print(f"[Audio] Split: {split_count}, Skipped (too short): {skipped_count}")

        return all_chunks

def get_segment_stats(segments_dir: str) -> Dict:
    """
    Get statistics about segments in a directory.

    Args:
        segments_dir: Directory containing audio segments

    Returns:
        Dictionary with duration statistics
    """
    processor = AudioProcessor()

    audio_extensions = {'.flac', '.wav', '.mp3', '.ogg', '.m4a'}
    durations = []

    for ext in audio_extensions:
        for f in Path(segments_dir).glob(f"*{ext}"):
            try:
                durations.append(processor.get_audio_duration(str(f)))
            except Exception as e:
                print(f"Error reading {f}: {e}")

    if not durations:
        return {"error": "No audio files found"}

    return {
        "total_files": len(durations),
        "total_duration_sec": sum(durations),
        "avg_duration_sec": sum(durations) / len(durations),
        "min_duration_sec": min(durations),
        "max_duration_sec": max(durations),
        "over_10s_count": sum(1 for d in durations if d > 10),
        "under_1s_count": sum(1 for d in durations if d < 1)
    }

if __name__ == "__main__":
    # Test with a sample directory
    import sys
    if len(sys.argv) > 1:
        segments_dir = sys.argv[1]
        stats = get_segment_stats(segments_dir)
        print("\nSegment Statistics:")
        for key, value in stats.items():
            print(f"  {key}: {value}")


Output:
Exit code: 0
Wall time: 0 seconds
Output:
"""
Audio Polisher - Segment boundary cleanup for transcription quality.
Conservative: only trims CUT SPEECH ARTIFACTS, never silence.

Philosophy:
  - Silence at boundaries is GOOD (natural padding, prevents abrupt starts/ends)
  - Only problem: segments starting/ending with ABRUPTLY CUT SPEECH from imprecise VAD
  - We detect the artifact pattern (burst -> gap -> speech) and remove ONLY the burst
  - The gap becomes natural silence padding (~30-50ms typically)
  - Segments that already start/end with silence pass through untouched

No ML models - pure signal processing, runs in <10ms/segment.
"""
import os
import numpy as np
import soundfile as sf
from dataclasses import dataclass
from typing import Optional, Tuple, List
from pathlib import Path

@dataclass
class PolishResult:
    """Result of audio polishing for a single segment."""
    input_path: str
    output_path: str
    was_modified: bool
    start_trimmed_ms: float = 0.0
    end_trimmed_ms: float = 0.0
    start_quality: str = "clean"
    end_quality: str = "clean"
    original_duration_ms: float = 0.0
    polished_duration_ms: float = 0.0
    snr_db: float = 0.0
    rms_db: float = 0.0
    peak_db: float = 0.0
    is_clipped: bool = False
    volume_adjusted: bool = False
    volume_gain_db: float = 0.0

    @property
    def total_trimmed_ms(self):
        return self.start_trimmed_ms + self.end_trimmed_ms

    def summary(self):
        name = Path(self.input_path).name
        if not self.was_modified:
            return (f"  {name}: SKIP | {self.original_duration_ms:.0f}ms | "
                    f"SNR:{self.snr_db:.1f}dB | start:{self.start_quality} "
                    f"end:{self.end_quality}")
        parts = []
        if self.start_trimmed_ms > 0:
            parts.append(
                f"start -{self.start_trimmed_ms:.0f}ms ({self.start_quality})")
        if self.end_trimmed_ms > 0:
            parts.append(
                f"end -{self.end_trimmed_ms:.0f}ms ({self.end_quality})")
        if self.volume_adjusted:
            parts.append(f"gain +{self.volume_gain_db:.1f}dB")
        changes = " | ".join(parts)
        return (f"  {name}: POLISHED | {self.original_duration_ms:.0f}ms -> "
                f"{self.polished_duration_ms:.0f}ms | {changes} | "
                f"SNR:{self.snr_db:.1f}dB")

class AudioPolisher:
    """
    Conservative audio segment polisher for transcription quality.
    Only modifies segments where artifact patterns are clearly detected.
    Uses energy + ZCR combined heuristic.
    """

    def __init__(
        self,
        frame_ms=10.0,
        hop_ms=5.0,
        max_start_trim_ms=200.0,
        artifact_search_ms=150.0,
        min_gap_ms=15.0,            # Min gap duration to confirm artifact pattern
        max_end_trim_ms=150.0,
        min_burst_ms=10.0,          # Min duration for trailing burst to be artifact
        max_burst_ms=100.0,         # Max duration - longer = probably real speech
        energy_gap_factor=0.35,
        zcr_artifact_threshold=0.15,
        min_dynamic_range_db=6.0,
        min_silence_pad_ms=30.0,    # Desired silence padding after artifact removal
        min_remaining_ms=300.0,
        min_trim_ms=10.0,
        fade_in_ms=5.0,             # Smooth fade-in after start trim
        fade_out_ms=10.0,           # Smooth fade-out on abrupt endings
        target_rms_db=-18.0,
        snr_boost_threshold_db=12.0,
        max_gain_db=12.0,
        clipping_threshold=0.99,
        output_format="flac",
    ):
        self.frame_ms = frame_ms
        self.hop_ms = hop_ms
        self.max_start_trim_ms = max_start_trim_ms
        self.artifact_search_ms = artifact_search_ms
        self.min_gap_ms = min_gap_ms
        self.max_end_trim_ms = max_end_trim_ms
        self.min_burst_ms = min_burst_ms
        self.max_burst_ms = max_burst_ms
        self.energy_gap_factor = energy_gap_factor
        self.zcr_artifact_threshold = zcr_artifact_threshold
        self.min_dynamic_range_db = min_dynamic_range_db
        self.min_silence_pad_ms = min_silence_pad_ms
        self.min_remaining_ms = min_remaining_ms
        self.min_trim_ms = min_trim_ms
        self.fade_in_ms = fade_in_ms
        self.fade_out_ms = fade_out_ms
        self.target_rms_db = target_rms_db
        self.snr_boost_threshold_db = snr_boost_threshold_db
        self.max_gain_db = max_gain_db
        self.clipping_threshold = clipping_threshold
        self.output_format = output_format

    def _compute_frames(self, audio, sr):
        """Compute per-frame energy (dB) and zero-crossing rate."""
        frame_samples = int(self.frame_ms * sr / 1000)
        hop_samples = int(self.hop_ms * sr / 1000)
        n_frames = max(1, (len(audio) - frame_samples) // hop_samples + 1)
        ste_db = np.zeros(n_frames)
        zcr = np.zeros(n_frames)
        frame_times = np.zeros(n_frames)
        for i in range(n_frames):
            start = i * hop_samples
            end = min(start + frame_samples, len(audio))
            frame = audio[start:end]
            rms = np.sqrt(np.mean(frame ** 2))
            ste_db[i] = 20 * np.log10(rms + 1e-10)
            if len(frame) > 1:
                zcr[i] = (np.sum(np.abs(np.diff(np.sign(frame))))
                          / (2 * len(frame)))
            frame_times[i] = (start + frame_samples / 2) / sr
        return ste_db, zcr, frame_times

    def _detect_leading_artifact(self, ste_db, zcr, frame_times):
        """
        Detect CUT SPEECH artifact at segment start.

        ONLY acts when: burst (cut speech) -> gap (silence) -> real speech.
        Trims at gap START = removes burst, keeps gap as silence padding.

        Does NOT trim if segment starts with silence (that's good padding).
        Does NOT trim if segment starts with continuous speech (natural start).
        """
        search_limit = self.artifact_search_ms / 1000
        si = np.where(frame_times < search_limit)[0]
        if len(si) < 3:
            return None, "clean"

        nf = np.percentile(ste_db, 15)
        sl = np.percentile(ste_db, 85)
        dr = sl - nf
        if dr < self.min_dynamic_range_db:
            return None, "clean"

        gap_thresh = nf + self.energy_gap_factor * dr

        # KEY CHECK: does segment start with speech energy or silence?
        # If first frames are below threshold = starts with silence = GOOD, skip
        n_check = min(3, len(si))
        first_frames_energy = ste_db[si[:n_check]]
        if np.all(first_frames_energy < gap_thresh):
            return None, "clean"  # Starts with silence - natural padding, don't touch

        # Segment starts with speech energy. Look for gap pattern.
        below = ste_db[si] < gap_thresh

        gaps = []
        in_gap = False
        gs = 0
        for i in range(len(below)):
            if below[i] and not in_gap:
                in_gap = True
                gs = i
            elif not below[i] and in_gap:
                in_gap = False
                dur = (frame_times[si[i]] - frame_times[si[gs]]) * 1000
                if dur >= self.min_gap_ms:
                    gaps.append((gs, i, dur))
        if in_gap:
            dur = (frame_times[si[-1]] - frame_times[si[gs]]) * 1000
            if dur >= self.min_gap_ms:
                gaps.append((gs, len(below), dur))

        if not gaps:
            return None, "clean"  # No gap = continuous speech from start

        g = gaps[0]
        # Gap must have speech BEFORE it (the artifact burst) and AFTER it (real speech)
        if g[0] == 0:
            return None, "clean"  # Gap at start = silence, already handled above
        if g[1] >= len(si):
            return None, "marginal_kept"  # Gap extends to search boundary

        # Artifact confirmed: speech burst -> gap -> more speech
        pre = si[:g[0]]
        mzcr = np.mean(zcr[pre]) if len(pre) > 0 else 0

        # Trim at GAP START = remove burst, keep gap as silence padding
        gap_start_frame = si[g[0]]
        tt = frame_times[gap_start_frame]

        if tt * 1000 > self.max_start_trim_ms or tt * 1000 < self.min_trim_ms:
            return None, "marginal_kept" if tt * 1000 > self.max_start_trim_ms else "clean"

        # Validate: gap should provide enough silence padding
        gap_ms = g[2]
        if gap_ms < self.min_silence_pad_ms:
            # Gap is too short for comfortable padding. Still trim if artifact
            # is clearly noise-like (high ZCR), but note the tight padding.
            if mzcr <= self.zcr_artifact_threshold:
                return None, "marginal_kept"

        # High ZCR = noise artifact (confident trim)
        if mzcr > self.zcr_artifact_threshold:
            return tt, "artifact_trimmed"
        # Low ZCR but clear gap = trim if gap is substantial
        if g[2] >= self.min_gap_ms * 1.5:
            return tt, "artifact_trimmed"

        return None, "marginal_kept"

    def _detect_trailing_artifact(self, ste_db, frame_times, total_dur):
        """
        Detect CUT SPEECH artifact at segment end.

        ONLY acts when: sustained speech -> silence gap -> isolated burst at end.
        The burst is a leak from the next VAD segment. Remove it, keep the silence.

        Does NOT trim if segment ends with silence (that's good padding).
        Does NOT trim if segment ends with sustained speech (natural or cut, can't help).
        Returns (trim_time, "artifact_trimmed") or applies fade-out flag.
        """
        nf = np.percentile(ste_db, 15)
        sl = np.percentile(ste_db, 85)
        dr = sl - nf
        if dr < self.min_dynamic_range_db:
            return None, "clean", False

        thresh = nf + self.energy_gap_factor * dr
        above = ste_db > thresh
        n = len(ste_db)

        # Check if segment ends with silence (last few frames below threshold)
        n_check = min(3, n)
        if np.all(~above[-n_check:]):
            return None, "clean", False  # Ends with silence - good padding

        # Segment ends with speech energy. Two sub-cases:
        # A) Isolated trailing burst: silence -> burst -> end
        #    = artifact from next segment, trim it
        # B) Sustained speech to the end: natural or cut ending
        #    = apply fade-out to soften abrupt cut

        # Find the last above-threshold frame
        ai = np.where(above)[0]
        if len(ai) == 0:
            return None, "clean", False

        last_above = ai[-1]

        # Check if this trailing speech is ISOLATED (preceded by silence gap)
        # Walk backwards from last_above to find if there's a gap before it
        lookback = min(10, last_above)  # ~50ms at 5ms hop
        if lookback >= 3:
            preceding = above[last_above - lookback:last_above]
            sustained = np.mean(preceding)

            if sustained < 0.3:
                # Low density before last speech = isolated trailing burst
                # Find where the burst starts (first above-thresh going backward)
                burst_start = last_above
                for bi in range(last_above - 1, -1, -1):
                    if above[bi]:
                        burst_start = bi
                    else:
                        break

                burst_dur = (frame_times[last_above] - frame_times[burst_start]) * 1000

                # Only trim if burst is short enough to be an artifact
                if self.min_burst_ms <= burst_dur <= self.max_burst_ms:
                    # Find the silence gap before the burst
                    # Trim at burst_start = keep silence, remove burst
                    tt = frame_times[burst_start]
                    actual_trim = (total_dur - tt) * 1000

                    if actual_trim <= self.max_end_trim_ms and actual_trim >= self.min_trim_ms:
                        return tt, "artifact_trimmed", False

        # Sustained speech to the end = can't trim, but apply fade-out
        # to prevent click artifacts from abrupt cut
        return None, "clean", True  # needs_fade_out=True

    def _measure_snr(self, ste_db):
        """Estimate SNR from energy distribution."""
        return np.percentile(ste_db, 90) - np.percentile(ste_db, 10)

    def _apply_fade(self, audio, sr, fade_in=False, fade_out=False):
        """Apply smooth fade-in/fade-out to prevent click artifacts."""
        out = audio.copy()
        if fade_in and self.fade_in_ms > 0:
            n = int(self.fade_in_ms * sr / 1000)
            if n > 0 and n < len(out):
                out[:n] *= np.linspace(0, 1, n)
        if fade_out and self.fade_out_ms > 0:
            n = int(self.fade_out_ms * sr / 1000)
            if n > 0 and n < len(out):
                out[-n:] *= np.linspace(1, 0, n)
        return out

    def polish(self, audio_path, output_path=None, output_dir=None):
        """
        Polish a single audio segment.

        Only removes cut speech artifacts. Silence at boundaries is preserved.


Output:
Exit code: 0
Wall time: 0 seconds
Output:
{
  "SPEAKER_00_0000_0.03-2.61.flac": {
    "indicwav2vec": {
      "success": true,
      "transcription": "ఉన్న దక్కొన్ని ఆర్గూుడ్తుండి లైకిగ్జాంపులు కచ్చామయంగోబైటి",
      "confidence": 0.8448988199234009,
      "word_count": 5,
      "quality": "good",
      "issues": []
    },
    "vistaar": {
      "success": true,
      "transcription": "ता कुण्या आट्स गूर्तुने लिए लाइक इजामपल कच्चा मेंगो वेट।",
      "confidence": 0.6356242011704658,
      "word_count": 10,
      "quality": "good",
      "issues": []
    }
  },
  "SPEAKER_00_0001_3.05-13.40.flac": {
    "indicwav2vec": {
      "success": true,
      "transcription": "ఆ కచొమెంగుబై టయడత మాడు కొరునడు అల్నారు నీమయం వైాల మరడిరినంటే మాంగో్జిస్కునిజరక్షోు కెల్పెడన్ ఉడావసుంద అయవ్ ఇన్ఫడ్నాకు ఫూటో కోపీ అంటే వేరు వజరాక్సట వీరందెల్చి నకడె పావను",
      "confidence": 0.8854802250862122,
      "word_count": 22,
      "quality": "good",
      "issues": []
    },
    "vistaar": {
      "success": true,
      "transcription": "కొము మువ్టి, యుండికెడిద్లికు వావిత్న్ను ఉద్ లో ఉత్నిలో ఉదిరూడాగుం వావ్నినల లో పివ్లో ఇది పింగ్సి కొమువ్ కొమువికెడికెడి� पावं",
      "confidence": 0.5064475021169011,
      "word_count": 16,
      "quality": "good",
      "issues": []
    }
  },
  "SPEAKER_00_0002_16.08-28.31.flac": {
    "indicwav2vec": {
      "success": true,
      "transcription": "డత పరాససంజెప్పి అర్అడు్ డెణు మాంగొసిస్కెలుపి అడె క్యుదోసితి శావనాంగ తెలీడపోడం గాదు అగరి కెల్లరే అయిదిస్తే అయు వనిజ ఏమొంటడు జుద్దాము హవన అంత కరంటేపేరంటే ఎట్లయఇగ్గావల్ అప్ ఎఒవరిద చూశుంట్రాల కర్ధమి వండ",
      "confidence": 0.8720259666442871,
      "word_count": 26,
      "quality": "good",
      "issues": []
    },
    "vistaar": {
      "success": true,
      "transcription": "यह शन कर engaging रहे देखा, आयें sytuावा! हई असक्क। तीकमे डियानत्तासी, इसक्पर đã पता Wisconsin's없이.. अलॉ परलि में कुमाति dzisiaj आलLaPolA कितिशे हूं!",
      "confidence": 0.1554464439787562,
      "word_count": 24,
      "quality": "good",
      "issues": []
    }
  }
}


Output:
Exit code: 0
Wall time: 0 seconds
Output:
{
  "SPEAKER_00_0000_0.03-2.61.flac": {
    "indicwav2vec": {
      "success": true,
      "transcription": "<s>న<s>కొ<s>న్<s>ని<s>్<s>ా<s>ం<s>పు<s>ల్<s> <s>",
      "confidence": 0.8462892770767212,
      "word_count": 2,
      "quality": "good",
      "issues": [],
      "processing_time": 9.412863731384277,
      "model_info": {
        "language": "te",
        "model": "Harveenchadha/vakyansh-wav2vec2-telugu-tem-100",
        "model_type": "huggingface"
      }
    },
    "indicmfa": {
      "success": true,
      "transcription": "నాకు కొన్ని యాడ్స్ గుర్తుంటాయ్ లైక్ ఎగ్జాంపుల్ కచ్చా మ్యాంగో బైట్",
      "confidence": 0.8448988199234009,
      "word_count": 9,
      "quality": "good",
      "issues": [],
      "processing_time": 0.0643012523651123,
      "model_info": {
        "language": "te",
        "method": "ctc_fallback",
        "word_count": 9
      }
    },
    "vistaar": {
      "success": true,
      "transcription": "కొన్ని యాడ్స్ గుర్తుండి లైక్ ఎగ్జాబుల్ ఖచ్చా మెంగో వైట్ తెలుసు",
      "confidence": null,
      "word_count": 0,
      "quality": "good",
      "issues": [],
      "processing_time": 803.7326574325562,
      "model_info": {
        "language": "te",
        "model_type": "indicwhisper",
        "detected_language": "te",
        "language_probability": null
      }
    },
    "indic_conformer": {
      "success": false,
      "transcription": null,
      "confidence": null,
      "word_count": 0,
      "quality": "failed",
      "issues": [
        "Failed: Model not loaded"
      ],
      "processing_time": null,
      "model_info": null
    }
  },
  "SPEAKER_00_0001_3.05-13.40.flac": {
    "indicwav2vec": {
      "success": true,
      "transcription": "<s>సు<s>కొ<s>ని<s> <s> నా<s>కు<s> <s>ఫో<s>టో<s> <s>కో<s>పి<s> <s>అ<s>ం<s>టే<s> <s>వీ<s>రు<s> <s>",
      "confidence": 0.954967200756073,
      "word_count": 8,
      "quality": "good",
      "issues": [],
      "processing_time": 0.07073807716369629,
      "model_info": {
        "language": "te",
        "model": "Harveenchadha/vakyansh-wav2vec2-telugu-tem-100",
        "model_type": "huggingface"
      }
    },
    "indicmfa": {
      "success": true,
      "transcription": "ఆ కచ్చితంగా ఒక అబ్బాయి అయితే మా వాడు ఒకడున్నాడు ఐ విల్ నాట్ నేమ్ హిమ్ బట్ మరేంటంటే మ్యాంగోస్ తీసుకుని జిరాక్స్ షాప్ కి వెళ్ళిపోయాడు అన్నమాట",
      "confidence": 0.8854802250862122,
      "word_count": 22,
      "quality": "good",
      "issues": [],
      "processing_time": 0.11043858528137207,
      "model_info": {
        "language": "te",
        "method": "ctc_fallback",
        "word_count": 22
      }
    },
    "vistaar": {
      "success": true,
      "transcription": "ఆ ఖచ్చంమెంగు వైట్య ఆయాడితే మా ఓడు కూడిన్నడు నార్ నీమెంబైంబైంబైంద్ మరొకటి ఏదో మాంగోజ్ తీసుకుని జరోక్షాకు కలిపాడనువాటు ఇన్ ఫాంట్ నాకు ఫోటోకోపీ అంటే వేరు జరోక్షన్ వేరం తెలిసిన అక్కడే",
      "confidence": null,
      "word_count": 0,
      "quality": "good",
      "issues": [],
      "processing_time": 7.153819799423218,
      "model_info": {
        "language": "te",
        "model_type": "indicwhisper",
        "detected_language": "te",
        "language_probability": null
      }
    },
    "indic_conformer": {
      "success": false,
      "transcription": null,
      "confidence": null,
      "word_count": 0,
      "quality": "failed",
      "issues": [
        "Failed: Model not loaded"
      ],
      "processing_time": null,
      "model_info": null
    }
  },
  "SPEAKER_00_0002_16.08-28.31.flac": {
    "indicwav2vec": {
      "success": true,
      "transcription": "<s>ప<s>ర<s>సె<s>స<s>అ<s>వు<s>ను<s>తె<s>లి<s>య<s>క<s>పో<s>వ<s>డ<s>ం<s> <s>కా<s>దు<s>అ<s>ం<s>త<s> <s>క<s>రె<s>క్ట్ <s>అయి<s>పో<s>యా<s>రు<s> అ<s>ం<s>టే<s> <s>",
      "confidence": 0.9432598948478699,
      "word_count": 6,
      "quality": "good",
      "issues": [],
      "processing_time": 0.07803678512573242,
      "model_info": {
        "language": "te",
        "model": "Harveenchadha/vakyansh-wav2vec2-telugu-tem-100",
        "model_type": "huggingface"
      }
    },
    "indicmfa": {
      "success": true,
      "transcription": "అదేంటది ప్రాసెస్ అని చెప్పి కరెక్ట్ వాడు రెండు మ్యాంగోస్ తీసుకెళ్లి అంటే క్యూరియాసిటీ అవును అవును తెలియకపోవడం కాదు అక్కడికెళ్ళి అరే ఇది ఇస్తే ఏ నిజంగా ఏమంటాడో చూద్దాం",
      "confidence": 0.8720259666442871,
      "word_count": 23,
      "quality": "good",
      "issues": [],
      "processing_time": 0.10203170776367188,
      "model_info": {
        "language": "te",
        "method": "ctc_fallback",
        "word_count": 23
      }
    },
    "vistaar": {
      "success": true,
      "transcription": "కేటిన్ ప్రాసెస్ అని చెప్పి వారిడు రెండు మాంగోసిస్ కేల్పై అరే క్యురోసిటీ తెలియకపోవడం గాదు అక్కడికి ఇస్తే ఏమనుంటుందో చూద్దాము అంత కనెంట్ ఏప్పారంటే ఇట్స్ లైక్ హార్డ్ చూసుకుంటున్నారాలకు అర్థమేంట్లు ఉంటుంది",
      "confidence": null,
      "word_count": 0,
      "quality": "good",
      "issues": [],
      "processing_time": 7.901504278182983,
      "model_info": {
        "language": "te",
        "model_type": "indicwhisper",
        "detected_language": "te",
        "language_probability": null
      }
    },
    "indic_conformer": {
      "success": false,
      "transcription": null,
      "confidence": null,
      "word_count": 0,
      "quality": "failed",
      "issues": [
        "Failed: Model not loaded"
      ],
      "processing_time": null,
      "model_info": null
    }
  }
}