Multilingual Transcription

Transcribe audio containing multiple languages with high accuracy.


Overview

Multilingual transcription handles audio where speakers switch between languages within the same recording or even within sentences. This solution combines cloud ASR (AssemblyAI) for accuracy with local models (Whisper) for fallback, plus per-segment language detection for code-switching scenarios.

Key capability: 95%+ accuracy on Chinese-English bilingual audio with automatic language detection.


Problem

Multilingual audio is common in diverse contexts:

  • Business meetings with participants from different countries
  • Educational content with bilingual instructors
  • Customer service calls with language switching
  • Media content with code-switching speakers

Challenge: Standard ASR systems optimize for single-language input:

  • Per-file language setting fails with code-switching
  • Mixed-language utterances get corrupted
  • Language detection must happen at segment level

Requirement: Accurate transcription regardless of language, with proper detection per segment.


Technical Approach

Service Comparison

ServiceEN AccuracyZH AccuracyCode-SwitchCostSelected
AssemblyAI95%+ (WER 5.2%)92%+ (CER 8.1%)Good$0.015/min✅ Primary
Whisper (large)93%88%FairFree (local)✅ Fallback
Google Speech94%90%Good$0.024/min
AWS Transcribe92%85%Limited$0.024/min

Decision: AssemblyAI as primary (best multilingual support), Whisper as offline fallback.

Architecture

┌─────────────────────────────────────────────────────────────┐
│                         Input                                │
├─────────────────────────────────────────────────────────────┤
│  Audio file + Optional: expected language(s)                │
└───────────────────────────┬─────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│                   Primary: AssemblyAI                        │
├─────────────────────────────────────────────────────────────┤
│  1. Upload audio to AssemblyAI                              │
│  2. Submit transcription job with language_detection=True   │
│  3. Poll for completion (~30s for 10-min audio)            │
│  4. Receive word-level transcript with timestamps           │
└───────────────────────────┬─────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│                 Language Detection Layer                     │
├─────────────────────────────────────────────────────────────┤
│  • Per-segment language classification                      │
│  • Code-switching detection within segments                 │
│  • Language tag assignment (en/zh/mixed)                    │
└───────────────────────────┬─────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│                 Fallback: Local Whisper                      │
├─────────────────────────────────────────────────────────────┤
│  • Triggered on: network error, API limit, offline mode    │
│  • Local GPU inference (~60s for 10-min audio)             │
│  • Lower accuracy but always available                      │
└───────────────────────────┬─────────────────────────────────┘
                            ↓
┌─────────────────────────────────────────────────────────────┐
│                        Output                                │
├─────────────────────────────────────────────────────────────┤
│  {                                                           │
│    "text": "Full transcript...",                            │
│    "words": [{"text": "hello", "start": 0.1, ...}],        │
│    "segments": [{"text": "...", "language": "en", ...}],   │
│    "languages_detected": ["en", "zh"]                       │
│  }                                                           │
└─────────────────────────────────────────────────────────────┘

Code-Switching Handling

Input audio: "Please translate: 请把这句话翻译成英文"

Standard ASR output (wrong):
  "Please translate: ching ba zhe ju hua fan yi cheng ying wen"
  
Our solution output (correct):
  [
    { "text": "Please translate:", "language": "en", "start": 0.0 },
    { "text": "请把这句话翻译成英文", "language": "zh", "start": 1.5 }
  ]

Code Sample

import assemblyai as aai
from typing import Optional, Literal
import time
import os

class MultilingualTranscriber:
    """
    Transcribe multilingual audio with automatic language detection.
    
    Requirements:
        pip install assemblyai openai-whisper langdetect
        
    Usage:
        transcriber = MultilingualTranscriber(api_key=os.environ["ASSEMBLYAI_KEY"])
        result = transcriber.transcribe("bilingual_audio.mp3")
        
        for segment in result["segments"]:
            print(f"[{segment['language']}] {segment['text']}")
    """
    
    def __init__(
        self,
        api_key: str,
        fallback_model: str = "base"
    ):
        """
        Initialize transcription service.
        
        Args:
            api_key: AssemblyAI API key
            fallback_model: Whisper model size for fallback ("tiny"/"base"/"small"/"medium"/"large")
        """
        aai.settings.api_key = api_key
        self.transcriber = aai.Transcriber()
        self.fallback_model = fallback_model
        self._whisper = None  # Lazy load
    
    def transcribe(
        self,
        audio_path: str,
        language_hint: Optional[str] = None,
        speaker_labels: bool = True,
        use_fallback: bool = True
    ) -> dict:
        """
        Transcribe audio with multilingual support.
        
        Args:
            audio_path: Path or URL to audio file
            language_hint: Expected primary language (None for auto-detect)
            speaker_labels: Enable speaker diarization
            use_fallback: Use Whisper if AssemblyAI fails
        
        Returns:
            {
                "text": "Full transcript...",
                "words": [...],
                "segments": [...],
                "languages_detected": ["en", "zh"],
                "duration": 600.5,
                "cost": 0.36
            }
        """
        try:
            return self._transcribe_assemblyai(
                audio_path,
                language_hint,
                speaker_labels
            )
        except Exception as e:
            if use_fallback:
                print(f"AssemblyAI failed: {e}. Using Whisper fallback.")
                return self._transcribe_whisper(audio_path, language_hint)
            raise
    
    def _transcribe_assemblyai(
        self,
        audio_path: str,
        language_hint: Optional[str],
        speaker_labels: bool
    ) -> dict:
        """
        Primary transcription via AssemblyAI.
        """
        config = aai.TranscriptionConfig(
            speech_model=aai.SpeechModel.best,
            language_detection=language_hint is None,
            language_code=language_hint,
            speaker_labels=speaker_labels,
            punctuate=True,
            format_text=True
        )
        
        # Submit and wait
        transcript = self.transcriber.transcribe(audio_path, config=config)
        
        if transcript.status == aai.TranscriptStatus.error:
            raise Exception(f"Transcription failed: {transcript.error}")
        
        # Process words
        words = []
        for w in (transcript.words or []):
            words.append({
                "text": w.text,
                "start": w.start / 1000,  # ms to seconds
                "end": w.end / 1000,
                "confidence": w.confidence,
                "speaker": getattr(w, 'speaker', None)
            })
        
        # Create segments with language detection
        segments = self._create_segments(words, transcript.utterances)
        
        # Detect languages
        languages = list(set(s["language"] for s in segments if s["language"] != "unknown"))
        
        # Calculate cost
        duration = transcript.audio_duration or 0
        cost = (duration / 60) * 0.015  # $0.015 per minute
        
        return {
            "text": transcript.text,
            "words": words,
            "segments": segments,
            "languages_detected": languages,
            "duration": duration,
            "cost": round(cost, 3)
        }
    
    def _create_segments(
        self,
        words: list[dict],
        utterances: Optional[list] = None
    ) -> list[dict]:
        """
        Group words into segments with language detection.
        """
        if utterances:
            # Use AssemblyAI utterances if available
            segments = []
            for u in utterances:
                text = u.text
                language = self._detect_language(text)
                segments.append({
                    "text": text,
                    "start": u.start / 1000,
                    "end": u.end / 1000,
                    "speaker": u.speaker,
                    "language": language,
                    "confidence": u.confidence
                })
            return segments
        
        # Fallback: Group words by pause gaps
        if not words:
            return []
        
        segments = []
        current_words = [words[0]]
        
        for word in words[1:]:
            gap = word["start"] - current_words[-1]["end"]
            
            if gap > 0.5:  # New segment after 500ms pause
                segments.append(self._words_to_segment(current_words))
                current_words = [word]
            else:
                current_words.append(word)
        
        if current_words:
            segments.append(self._words_to_segment(current_words))
        
        return segments
    
    def _words_to_segment(self, words: list[dict]) -> dict:
        """
        Convert word list to segment with language.
        """
        text = " ".join(w["text"] for w in words)
        return {
            "text": text,
            "start": words[0]["start"],
            "end": words[-1]["end"],
            "speaker": words[0].get("speaker"),
            "language": self._detect_language(text),
            "confidence": sum(w.get("confidence", 0) for w in words) / len(words)
        }
    
    def _detect_language(self, text: str) -> str:
        """
        Detect language of text segment.
        
        Returns:
            "en" - English
            "zh" - Chinese (Mandarin)
            "mixed" - Code-switching detected
            "unknown" - Cannot determine
        """
        if not text or len(text.strip()) < 3:
            return "unknown"
        
        # Count Chinese characters
        chinese_chars = sum(1 for c in text if '\u4e00' <= c <= '\u9fff')
        total_chars = len(text.replace(" ", "").replace(".", "").replace(",", ""))
        
        if total_chars == 0:
            return "unknown"
        
        chinese_ratio = chinese_chars / total_chars
        
        if chinese_ratio > 0.7:
            return "zh"
        elif chinese_ratio < 0.1:
            return "en"
        else:
            return "mixed"
    
    def _transcribe_whisper(
        self,
        audio_path: str,
        language_hint: Optional[str]
    ) -> dict:
        """
        Fallback transcription via local Whisper.
        """
        if self._whisper is None:
            import whisper
            self._whisper = whisper.load_model(self.fallback_model)
        
        result = self._whisper.transcribe(
            audio_path,
            language=language_hint,
            task="transcribe"
        )
        
        # Convert to our format
        segments = []
        for seg in result["segments"]:
            segments.append({
                "text": seg["text"].strip(),
                "start": seg["start"],
                "end": seg["end"],
                "speaker": None,
                "language": self._detect_language(seg["text"]),
                "confidence": seg.get("no_speech_prob", 1) < 0.5
            })
        
        languages = list(set(s["language"] for s in segments if s["language"] != "unknown"))
        
        return {
            "text": result["text"],
            "words": [],  # Whisper doesn't provide word-level by default
            "segments": segments,
            "languages_detected": languages,
            "duration": segments[-1]["end"] if segments else 0,
            "cost": 0  # Local processing
        }
    
    def transcribe_segment(
        self,
        audio_path: str,
        start: float,
        end: float
    ) -> dict:
        """
        Transcribe a specific segment of audio.
        
        Args:
            audio_path: Path to audio file
            start: Start time in seconds
            end: End time in seconds
        
        Returns:
            Transcription result for segment only
        """
        import subprocess
        import tempfile
        
        # Extract segment
        with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as f:
            segment_path = f.name
        
        try:
            subprocess.run([
                "ffmpeg", "-y",
                "-i", audio_path,
                "-ss", str(start),
                "-to", str(end),
                "-c:a", "libmp3lame",
                "-q:a", "2",
                segment_path
            ], capture_output=True, check=True)
            
            result = self.transcribe(segment_path, use_fallback=True)
            
            # Adjust timestamps
            for word in result.get("words", []):
                word["start"] += start
                word["end"] += start
            for seg in result.get("segments", []):
                seg["start"] += start
                seg["end"] += start
            
            return result
            
        finally:
            if os.path.exists(segment_path):
                os.remove(segment_path)


# Example usage
if __name__ == "__main__":
    transcriber = MultilingualTranscriber(
        api_key=os.environ["ASSEMBLYAI_KEY"],
        fallback_model="base"
    )
    
    result = transcriber.transcribe("bilingual_meeting.mp3")
    
    print(f"Duration: {result['duration']:.1f}s")
    print(f"Cost: ${result['cost']:.3f}")
    print(f"Languages: {', '.join(result['languages_detected'])}")
    print("\nSegments:")
    
    for seg in result["segments"]:
        lang = seg["language"].upper()
        speaker = seg.get("speaker", "?")
        print(f"  [{lang}] Speaker {speaker}: {seg['text'][:50]}...")

Performance Metrics

MetricAssemblyAIWhisper (base)Notes
English WER5.2%8.5%Word Error Rate
Chinese CER8.1%12.3%Character Error Rate
Code-Switch Accuracy92%78%Mixed language segments
Processing Speed0.3x real-time0.5x real-timeCloud vs local
Cost$0.015/min$0 (local)API pricing

References:


Use Cases

IndustryApplication
🌐 LocalizationTranscribe content in multiple languages
🎓 EducationBilingual lecture transcription
📞 Customer ServiceMultilingual call transcription
🎙️ PodcastingMulti-language episode transcription
🏛️ LegalCourt proceedings with interpreters

Configuration Guide

Language Support

LanguageAssemblyAI CodeWhisper CodeAccuracy
Englishenen95%+
Chinese (Mandarin)zhzh92%+
Spanisheses94%+
Frenchfrfr93%+
Germandede93%+
Japanesejaja90%+
Koreankoko89%+

Best Practices

  1. Audio Quality: 16kHz+ sample rate, low background noise
  2. Language Hint: Provide if known (improves accuracy by ~3%)
  3. Segment Length: Optimal 5-30 seconds per segment
  4. Code-Switching: Let auto-detect handle; don't force single language

Limitations

LimitationMitigation
Rare languagesCheck AssemblyAI supported languages first
Heavy accentsMay need per-speaker adaptation
Low audio qualityPre-process with noise reduction
Very fast speechDecrease speech rate threshold

Related Projects


Further Reading