# Como transcrever áudio localmente com faster-whisper sem gastar token

# Como transcrever áudio localmente com faster-whisper sem gastar token

Eu precisava resolver um problema bem específico: falar com a minha OpenClaw por áudio, em canais como WhatsApp, Telegram e Discord, sem mandar o arquivo para uma API externa e sem adicionar custo por transcrição.

O objetivo era simples. Transformar voz em texto localmente, usar esse texto como entrada normal do sistema e eliminar a etapa de speech-to-text como mais uma conta variável por mensagem.

Para um serviço Python simples, a abordagem que funcionou melhor aqui foi montar um pipeline local com `faster-whisper`.

A tese deste post é só essa: não é a melhor stack universal de speech-to-text. É uma escolha prática para colocar uma transcrição local de pé rápido, com boa qualidade e baixo atrito operacional.

## O recorte do problema

O pipeline precisava fazer quatro coisas bem:

1.  receber um arquivo de áudio comum, como `.ogg`
    
2.  transcrever localmente
    
3.  devolver texto estruturado em JSON
    
4.  permitir correções locais para termos recorrentes
    

Sem streaming, sem diarização, sem uma plataforma inteira de speech logo de saída.

## Por que `faster-whisper`

O recorte favorecia uma stack com estas características:

*   integração simples em Python
    
*   boa qualidade de transcrição offline
    
*   suporte prático para CPU
    
*   caminho curto até um MVP utilizável
    

Foi por isso que `faster-whisper` entrou primeiro.

Se o problema principal fosse outro, a escolha poderia mudar.

*   Eu olharia para `whisper.cpp` se o foco fosse footprint menor, distribuição local mais enxuta ou menos dependência de Python.
    
*   Eu reavaliaria a stack se o requisito central fosse streaming forte, VAD mais sofisticado ou uma plataforma de speech mais ampla.
    

## Setup mínimo

```bash
python3 -m venv .venvs/audio-stt
. .venvs/audio-stt/bin/activate
python -m pip install --upgrade pip setuptools wheel
python -m pip install faster-whisper av
```

O detalhe importante aqui é `av`. Ele simplifica o caminho para lidar com formatos de áudio comuns sem depender logo de um `ffmpeg` instalado no host.

## O script

A versão inicial ficou pequena de propósito. A ideia era chegar rápido num ponto utilizável e só depois endurecer o fluxo.

```python
#!/usr/bin/env python3
import argparse
import json
import os
import re
from pathlib import Path

from faster_whisper import WhisperModel


def detect_device() -> str:
    try:
        import subprocess
        p = subprocess.run(
            ['nvidia-smi', '--query-gpu=name', '--format=csv,noheader'],
            capture_output=True,
            text=True,
        )
        if p.returncode == 0 and p.stdout.strip():
            return 'cuda'
    except Exception:
        pass
    return 'cpu'


def build_model(model_name: str, compute_type: str | None, device: str | None):
    resolved_device = device or os.environ.get('FASTER_WHISPER_DEVICE') or detect_device()
    resolved_compute = compute_type or os.environ.get('FASTER_WHISPER_COMPUTE')
    if not resolved_compute:
        resolved_compute = 'float16' if resolved_device == 'cuda' else 'int8'
    model = WhisperModel(model_name, device=resolved_device, compute_type=resolved_compute)
    return model, resolved_device, resolved_compute


def load_corrections(base_dir: Path):
    path = base_dir / 'corrections.json'
    if not path.exists():
        return {'whole_word': {}, 'substring': {}}
    with path.open('r', encoding='utf-8') as f:
        data = json.load(f)
    return {
        'whole_word': data.get('whole_word', {}) or {},
        'substring': data.get('substring', {}) or {},
    }


def apply_corrections(text: str, corrections: dict) -> str:
    out = text
    for src, dst in corrections.get('substring', {}).items():
        out = out.replace(src, dst)
    for src, dst in corrections.get('whole_word', {}).items():
        pattern = re.compile(rf'\b{re.escape(src)}\b', flags=re.IGNORECASE)
        out = pattern.sub(dst, out)
    return re.sub(r'\s+', ' ', out).strip()


def transcribe_file(input_path: Path, model_name: str):
    model, resolved_device, resolved_compute = build_model(model_name, None, None)
    segments, info = model.transcribe(
        str(input_path),
        vad_filter=True,
        word_timestamps=False,
    )

    texts = []
    out_segments = []
    for seg in segments:
        text = seg.text.strip()
        if text:
            texts.append(text)
        out_segments.append({
            'id': seg.id,
            'start': round(seg.start, 2),
            'end': round(seg.end, 2),
            'text': text,
        })

    raw_text = ' '.join(texts).strip()
    corrections = load_corrections(Path(__file__).resolve().parent)
    corrected_text = apply_corrections(raw_text, corrections)

    return {
        'ok': True,
        'text': corrected_text,
        'raw_text': raw_text,
        'segments': out_segments,
        'info': {
            'language': getattr(info, 'language', None),
            'language_probability': getattr(info, 'language_probability', None),
            'model': model_name,
            'device': resolved_device,
            'compute_type': resolved_compute,
        },
    }
```

Esse script já entrega o suficiente para um primeiro ciclo de uso:

*   escolhe CPU ou GPU
    
*   usa `int8` em CPU por padrão
    
*   transcreve áudio local
    
*   retorna texto bruto e texto corrigido
    
*   preserva segmentos e metadados úteis
    

## Uso

```bash
python transcribe_local.py audio.ogg
```

Para testar um modelo menor e mais rápido:

```bash
FASTER_WHISPER_MODEL=base python transcribe_local.py audio.ogg
```

Para forçar CPU com `int8`:

```bash
FASTER_WHISPER_DEVICE=cpu FASTER_WHISPER_COMPUTE=int8 python transcribe_local.py audio.ogg
```

## O ponto que fez diferença no uso real

O primeiro problema não foi setup. Foi vocabulário.

Modelos de transcrição acertam bastante coisa e ainda assim tropeçam exatamente no que mais importa para o domínio: nome de produto, marca, sigla, nome próprio e termos técnicos recorrentes.

Foi aí que entrou uma camada simples de correção local.

```json
{
  "whole_word": {
    "OpenCloud": "OpenClaw",
    "Rostinger": "Hostinger",
    "Rony": "Ronie"
  },
  "substring": {
    "trabalho em mais de 22 anos": "trabalho há mais de 22 anos",
    "todo Linux": "todo o Linux"
  }
}
```

Não é uma solução sofisticada. Também não precisa ser.

Quando o domínio é conhecido, uma camada explícita, pequena e auditável resolve um volume grande de erro chato sem puxar mais dependência para dentro do sistema.

## `base` ou `small`

Eu comparei os dois modelos em CPU com áudio real curto e médio.

O `base` foi mais rápido. Isso apareceu de forma consistente.

Só que o `small` foi claramente melhor no que importava mais:

*   nomes próprios
    
*   termos técnicos
    
*   frases curtas que não podem sair deformadas
    

Na prática, a troca foi esta:

*   `base`: melhor latência, pior texto
    
*   `small`: pior latência, texto bem mais utilizável
    

Por isso o default do MVP ficou em `small`.

Esse é o tipo de decisão que benchmark superficial não resolve sozinho. Se a fala sai rápido, mas deforma os termos mais importantes, o usuário percebe o sistema como ruim de qualquer jeito.

## O que esse pipeline resolve bem

*   transcrição local de áudio comum
    
*   custo previsível na etapa de STT
    
*   integração simples com serviços Python
    
*   correção local de vocabulário sem API externa
    

## O que ele não resolve

*   entendimento perfeito de fala natural
    
*   casos mais pesados de streaming
    
*   uma stack completa de speech
    
*   correção automática ilimitada sem risco de mascarar erro real
    

Aqui mora uma distinção importante: resolver bem a primeira etapa não é o mesmo que resolver toda a plataforma de voz.

## Quando eu usaria essa abordagem

*   entrada de voz para assistentes e automações internas
    
*   serviços Python que precisam transcrever áudio localmente
    
*   fluxos em que privacidade e custo previsível importam
    

## Quando eu reavaliaria a stack

*   se footprint mínimo for requisito central
    
*   se streaming for o caso principal
    
*   se o produto estiver mais perto de SDK embarcável do que de serviço Python
    
*   se o pipeline de speech exigir bem mais do que transcrição
    

## Fechando

No meu caso, o problema ficou resolvido. Eu precisava de uma etapa local de áudio para texto que não consumisse token e não criasse mais uma conta variável por mensagem.

Com esse pipeline, passei a usar voz como entrada da OpenClaw em canais como WhatsApp, Discord e Telegram sem custo adicional de transcrição.

`faster-whisper` não entrou aqui como tese de mercado. Entrou como decisão de engenharia.

Para esse recorte, funcionou bem: pouco atrito, boa qualidade e um caminho curto até algo realmente útil.
