Building Production Speech-to-Text: Architecture, Optimization, and Case Studies

We design and deploy artificial intelligence systems: from prototype to production-ready solutions. Our team combines expertise in machine learning, data engineering and MLOps to make AI work not in the lab, but in real business.
Showing 1 of 1All 1564 services
Building Production Speech-to-Text: Architecture, Optimization, and Case Studies
Medium
from 1 week to 3 months
Frequently Asked Questions

AI Development Areas

AI Solution Development Stages

Latest works

  • image_website-b2b-advance_0.webp
    B2B ADVANCE company website development
    1347
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1247
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    948
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1183
  • image_logo-advance_0.webp
    B2B Advance company logo design
    642
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    921

Developing a Speech-to-Text System: From Provider Selection to Production Pipeline

Problem: standard STT models yield 20% Word Error Rate (WER) on medical terminology, which is unacceptable for telemedicine and court stenography. Building a production-grade system goes beyond simply calling the Whisper API. The challenge involves selecting a model suited for the accent and domain, post-processing to correct specialized vocabulary, streaming recognition with p99 latency <500ms, and orchestrating multiple providers for reliability. We have 5+ years of experience in STT development and have delivered 20+ projects for contact centers, healthcare, and finance. In one case for a legal platform, we reduced Domain WER from 18% to 6% using a custom dictionary and fine-tuning Whisper on 200 hours of court recordings.

How to Choose the STT Stack for Your Task?

Each provider has strengths and weaknesses. Here is a comparison of key metrics for Russian:

Provider WER (clean speech) WER (noisy speech) Latency (ms) Streaming Cost Efficiency
OpenAI Whisper (API) 5% 12% 600 No High (pay-per-use)
Deepgram Nova-2 8% 15% 250 Yes Medium
Azure Speech 9% 14% 300 Yes Low
Whisper Large-v3 (self-hosted) 5% 12% 400 No Very high (pays off in 3-4 months)

For streaming scenarios (chatbots, live transcription), choose Deepgram or Azure. For maximum quality in batch processing, use Whisper API or self-hosted. Self-hosted yields 4-5x savings compared to Deepgram at volumes above 500 hours per month, plus full control over fine-tuning and privacy.

Why Post-Processing is a Key Stage?

Any STT model makes mistakes on terms, names, abbreviations. Without post-processing, Domain WER reaches 18-20%. We implement a DomainSpecificPostProcessor that:

  • corrects the transcript against a custom dictionary;
  • normalizes numbers and dates;
  • detects and corrects proper nouns.

This reduces Domain WER to 5-8%. Example: the word "pittsburg" is corrected to "Pittsburgh", "ekcel" → "Excel". The dictionary is built from your text corpora. In one project for the financial sector, we added 1500 terms, reducing error count by 60%.

When is Fine-Tuning Justified?

Fine-tuning the model on domain data yields an additional 2-3% WER reduction over post-processing alone. This is relevant if you have 50+ hours of labeled audio recordings. We use LoRA adapters for fast adaptation of Whisper Large-v3 — training takes 2-3 days on an A100. After fine-tuning, Domain WER drops to 4-6%.

How We Build a Production-Grade STT Pipeline

We use an architecture with automatic failover between providers. Example implementation in Python:

import asyncio
import io
import json
from typing import AsyncGenerator, Optional
import httpx
import websockets
import numpy as np

class STTProviderComparator:
    """Compare STT providers by metrics"""

    PROVIDERS = {
        "openai_whisper": {
            "wer_general": 0.05,    # Word Error Rate for standard speech
            "wer_noisy": 0.12,
            "russian_support": "excellent",
            "latency_ms": 600,      # Batch mode
            "streaming": False,
            "cost_per_hour": 0.36,
        },
        "deepgram_nova2": {
            "wer_general": 0.08,
            "wer_noisy": 0.15,
            "russian_support": "good",
            "latency_ms": 250,
            "streaming": True,
            "cost_per_hour": 0.35,
        },
        "azure_speech": {
            "wer_general": 0.09,
            "wer_noisy": 0.14,
            "russian_support": "excellent",
            "latency_ms": 300,
            "streaming": True,
            "cost_per_hour": 0.96,
        },
        "whisper_selfhosted": {
            "wer_general": 0.05,
            "wer_noisy": 0.12,
            "russian_support": "excellent",
            "latency_ms": 400,      # Large-v3 on A100
            "streaming": False,
            "cost_per_hour": 0.08,  # Self-hosted
        },
    }

    def recommend_provider(self, requirements: dict) -> str:
        """
        Select provider based on requirements.
        requirements: {'streaming': bool, 'max_latency_ms': int, 'language': str,
                       'volume_hours_monthly': float}
        """
        candidates = []

        for name, props in self.PROVIDERS.items():
            # Filter by hard constraints
            if requirements.get('streaming') and not props['streaming']:
                continue
            if props['latency_ms'] > requirements.get('max_latency_ms', 9999):
                continue

            # Scoring
            wer_score = 1 - props['wer_general']
            latency_score = 1 - props['latency_ms'] / 1000

            # Economics at high volume
            monthly_cost = props['cost_per_hour'] * requirements.get('volume_hours_monthly', 100)
            cost_score = 1 / (1 + monthly_cost / 1000)

            total_score = wer_score * 0.4 + latency_score * 0.3 + cost_score * 0.3
            candidates.append((name, round(total_score, 3)))

        return max(candidates, key=lambda x: x[1])[0] if candidates else "openai_whisper"


class StreamingSTTClient:
    """Streaming speech recognition via Deepgram WebSocket"""

    def __init__(self, api_key: str, language: str = "ru"):
        self.api_key = api_key
        self.language = language
        self.base_url = "wss://api.deepgram.com/v1/listen"

    async def transcribe_stream(self, audio_chunks: AsyncGenerator[bytes, None],
                                  sample_rate: int = 16000) -> AsyncGenerator[str, None]:
        """
        Streaming audio recognition.
        Returns interim and final transcripts.
        """
        params = (
            f"?language={self.language}"
            f"&encoding=linear16"
            f"&sample_rate={sample_rate}"
            f"&channels=1"
            f"&model=nova-2"
            f"&smart_format=true"
            f"&punctuate=true"
            f"&endpointing=300"     # ms silence to detect end of phrase
            f"&interim_results=true"
        )

        async with websockets.connect(
            self.base_url + params,
            extra_headers={"Authorization": f"Token {self.api_key}"},
            max_size=10_000_000
        ) as ws:

            async def send_audio():
                async for chunk in audio_chunks:
                    await ws.send(chunk)
                await ws.send(json.dumps({"type": "CloseStream"}))

            asyncio.create_task(send_audio())

            async for message in ws:
                data = json.loads(message)

                if data.get("type") == "Results":
                    channel = data.get("channel", {})
                    alternatives = channel.get("alternatives", [])
                    if alternatives:
                        transcript = alternatives[0].get("transcript", "")
                        is_final = data.get("is_final", False)
                        if transcript:
                            yield transcript if is_final else f"[interim] {transcript}"


class DomainSpecificPostProcessor:
    """
    Post-processing transcript for a specific domain.
    STT models often make mistakes on terms, proper nouns, abbreviations.
    """

    def __init__(self, domain_vocabulary: dict):
        """
        domain_vocabulary: {'incorrect_word': 'correct_word'}
        Example: {'pittsburg': 'Pittsburgh', 'ekcel': 'Excel'}
        """
        self.vocabulary = {k.lower(): v for k, v in domain_vocabulary.items()}

    def correct_transcript(self, transcript: str) -> str:
        """Replace misrecognized words"""
        words = transcript.split()
        corrected = []
        for word in words:
            clean = word.lower().rstrip('.,!?;:')
            punct = word[len(clean):]
            corrected.append(self.vocabulary.get(clean, word.rstrip('.,!?;:')) + punct)
        return ' '.join(corrected)

    def normalize_numbers_and_dates(self, transcript: str) -> str:
        """Normalize numbers and dates from text to structured format"""
        import re

        # Simple digit replacements (production: use pymorphy2)
        number_words = {
            'zero': '0', 'one': '1', 'two': '2', 'three': '3', 'four': '4',
            'five': '5', 'six': '6', 'seven': '7', 'eight': '8', 'nine': '9',
        }

        result = transcript.lower()
        for word, digit in number_words.items():
            result = result.replace(word, digit)

        return result


class STTPipeline:
    """Full STT pipeline with fallback and monitoring"""

    def __init__(self, primary_provider, fallback_provider=None,
                  post_processor: Optional[DomainSpecificPostProcessor] = None):
        self.primary = primary_provider
        self.fallback = fallback_provider
        self.post_processor = post_processor
        self._error_count = 0

    async def transcribe(self, audio_data: bytes,
                          language: str = "ru") -> dict:
        """
        Transcription with automatic fallback.
        """
        try:
            transcript, confidence = await self._call_provider(
                self.primary, audio_data, language
            )
            provider_used = "primary"

        except Exception as e:
            self._error_count += 1
            if self.fallback:
                transcript, confidence = await self._call_provider(
                    self.fallback, audio_data, language
                )
                provider_used = "fallback"
            else:
                raise

        # Post-processing
        if self.post_processor:
            transcript = self.post_processor.correct_transcript(transcript)

        return {
            "transcript": transcript,
            "confidence": confidence,
            "provider": provider_used,
            "language": language,
        }

    async def _call_provider(self, provider, audio: bytes, language: str) -> tuple:
        """Stub: replace with actual provider call"""
        raise NotImplementedError

STT Quality Assessment and KPIs

Compare metrics we guarantee after deployment (on your data):

Metric Description Target Value
WER (Word Error Rate) % of words with errors < 8% for clean speech
CER (Character Error Rate) % of characters with errors < 3%
RTF (Real-Time Factor) time/audio duration < 0.3 for streaming
First-word Latency delay to first result < 400ms
Domain WER WER on specialized terms < 12%

Work Process for STT System

  1. Analytics and Data Collection — study your acoustic environment, collect a sample of audio with typical accents and vocabulary. For a contact center, we record 50 hours of real conversations.
  2. Architecture Selection — determine best-fit provider, need for self-hosted, design fallback and post-processing.
  3. Pipeline Development — implement streaming client, custom dictionary, monitoring of metrics (WER, latency, error rates).
  4. Testing and Fine-Tuning — optimize WER on test set, fine-tune model if self-hosted.
  5. Deployment and Integration — deploy in your cloud or on-prem, set up CI/CD, provide API access.
  6. Training and Support — deliver documentation, conduct workshop for your engineers, provide SLA support.

What’s Included (Deliverables)

  • Repository with pipeline code (Python, Docker/Kubernetes configs)
  • Custom dictionary and scripts to build it
  • Instructions for deployment and monitoring
  • Access to test environment for 1 month
  • Team training (up to 2 days)
  • Technical support for 3 months after release

We guarantee quality: we measure WER on your data before and after deployment. Achieved target metrics are fixed in the contract.

Want to see the pipeline work on your data? Order a pilot project — we’ll deploy the system in your environment and provide a report with metrics. Get a consultation on STT architecture — describe your task, and we’ll propose the optimal solution.

Speech Recognition and Synthesis: ASR, TTS, Voice Cloning

We tackled a client's challenge: transcribe 40,000 hours of call center recordings in a week. Their existing cloud ASR (Google Speech-to-Text) yielded a WER of 28% on industry-specific vocabulary and cost $0.006 per minute — prohibitively expensive at that volume. The goal was to reduce WER below 10% and switch to self-hosted inference. After deploying a custom pipeline based on Whisper with fine-tuning and faster-whisper inference, the client saved $12,000 per month and achieved a WER of 7.3%.

How does speech recognition ASR handle noisy call center recordings?

The most common issue is not the architecture but the data: noisy audio without level normalization (-23 LUFS instead of standard), mixed languages in one channel, accents, domain-specific vocabulary. Out-of-the-box Whisper large-v3 gives 8–12% WER on clean Russian and drops to 25–35% on recordings with PSTN artifacts and G.711 narrowband codec. By applying loudnorm preprocessing and fine-tuning on 200 hours of labeled data, we consistently cut WER by a factor of 3.

Typical problems we encounter

WER does not converge to the desired metric. Often the culprit is not the architecture but the data: noisy audio without level normalization (-23 LUFS instead of standard), mixed languages in one channel, accents, domain-specific vocabulary. Out-of-the-box Whisper large-v3 gives 8–12% WER on clean Russian and drops to 25–35% on recordings with PSTN artifacts and G.711 narrowband codec.

Diarization fails with more than two speakers. pyannote/speaker-diarization-3.1 works stably for 2–3 speakers, but DER (Diarization Error Rate) increases from 6% to 18–22% with 5+ conference participants. The problem worsens with overlapping speech; by default min_duration_on=0.1 cuts short interjections. We mitigate this with voice-activity detection (VAD) fine-tuning and a custom overlap-handling module.

Voice cloning — latency vs. quality. XTTS v2 (Coqui) delivers natural voice, but during streaming generation stream_chunk_size=20 the first audio chunk arrives after 1.4–2.0 seconds — unacceptable for interactive scenarios. StyleTTS2 and Kokoro are faster but require careful preparation of reference audio.

How do we solve it in practice?

The basic stack for a production pipeline:

  • ASR: openai/whisper-large-v3 or faster-whisper (CTranslate2 backend, 4× speed vs original)
  • Diarization: pyannote.audio 3.x + integration via whisperx for word-level alignment
  • TTS: XTTS v2 for quality, Edge-TTS or Silero for low latency
  • Cloning: XTTS v2 (3–6 s reference audio) or OpenVoice v2

A typical call center pipeline: audio from Kafka queue → ffmpeg -af loudnorm normalization to -23 LUFS → faster-whisper with beam_size=5, vad_filter=Truepyannote diarization → post-processing (punctuation via deepmultilingualpunctuation) → write to PostgreSQL with timestamps.

Case study from our practice. A fintech company with 12,000 calls per day. Initial WER on Russian with banking vocabulary — 22% (Google STT). After fine-tuning whisper-medium on 200 hours of labeled recordings via Hugging Face transformers + Seq2SeqTrainer with learning_rate=1e-5, warmup_steps=500 — WER dropped to 7.3%. Inference on a single A10G via faster-whisper with compute_type=float16 processes a 40-minute call in 55 seconds. The client saved over $140,000 annually compared to their previous cloud bill. Contact us for a free pilot estimate to see similar savings on your data.

How to fine-tune Whisper on domain data?

When a general model underperforms, fine-tuning is the first tool. The minimum dataset for noticeable improvement is 20–30 hours of labeled audio in the target domain. Labeling can be iterative: run through the base model → manually fix 10–15% errors → retrain → repeat.

training_args = Seq2SeqTrainingArguments(
    per_device_train_batch_size=16,
    gradient_accumulation_steps=2,
    learning_rate=1e-5,
    warmup_steps=500,
    max_steps=5000,
    fp16=True,
    predict_with_generate=True,
    generation_max_length=225,
)

Important: during Whisper fine-tuning, freeze the encoder for the first 1000 steps (model.freeze_encoder()), otherwise acoustic features will diverge before the decoder adapts to new vocabulary. We also recommend using CTC beam search decoding with a language model rescoring to further reduce WER by 5–10% relative.

Model WER (clean) WER (noisy) RTF (A10G) Languages
Whisper large-v3 5.2% 27% 0.08 99
Wav2Vec2-XLSR-53 6.8% 32% 0.12 143
Google STT (cloud) 7.0% 28% 125
DeepSpeech 0.9.3 11.5% 41% 0.06 8

Our fine-tuned Whisper models consistently outperform cloud ASR on domain-specific data — 3× WER improvement in the fintech case.

Speech synthesis: How to choose a model for your task?

Model Latency (TTFB) Naturalness MOS Cloning Languages
XTTS v2 1.2–2.0 s 4.1–4.3 Yes, 3 s reference 17
StyleTTS2 0.3–0.6 s 4.0–4.2 Yes, requires adaptation en, + fine-tune
Kokoro-82M 0.08–0.15 s 3.7–3.9 No en, ja
Silero TTS 0.05–0.1 s 3.4–3.6 No ru, en, de, etc.
Edge-TTS ~0.4 s (cloud) 4.0 No 100+

For interactive bots requiring TTFB < 300 ms — Silero or Kokoro. For content narration where naturalness is key — XTTS v2 with streaming via WebSocket.

Our process and deliverables

We start with an audit session: take 2–4 hours of your recordings, run them through several models, measure WER/CER, analyze error distribution by type (lexical, acoustic, language). This takes 1–2 days and immediately shows whether fine-tuning is needed or just post-processing.

Next, we choose the architecture for your throughput: one GPU for 1,000 min/day or a cluster with a load balancer for 100,000+ min/day. Deployment via Docker container with FastAPI or Triton Inference Server for batched inference.

What you get after engagement:

  • Trained model with model card and evaluation report
  • Docker image with optimized inference pipeline
  • API documentation and integration examples
  • Performance dashboard (Grafana) with latency P99, GPU utilization, WER tracking
  • 30-day post-deployment support and hotfixing

Timelines depend on complexity:

  • Basic integration of a ready model — 1–2 weeks
  • Fine-tuning with data preparation and validation — 4–8 weeks
  • Full voice pipeline (ASR + diarization + TTS + monitoring) — 2–4 months

Project investments typically range from $20,000 to $80,000. Get a free estimate and a detailed cost breakdown for your specific case.

Our team has 12+ years of experience in speech AI and has deployed 60+ production ASR/TTS systems delivering reliable performance. Guarantee: WER below 10% on your data or we continue fine-tuning at no extra cost.

Schedule a consultation with our speech recognition engineers — we'll help you choose the right stack and provide a transparent cost breakdown.