Como transcrever áudio localmente com faster-whisper sem gastar token
Uma implementação enxuta para usar voz como entrada de assistente sem custo adicional de transcrição

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:
receber um arquivo de áudio comum, como
.oggtranscrever localmente
devolver texto estruturado em JSON
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.cppse 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
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.
#!/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
int8em CPU por padrãotranscreve áudio local
retorna texto bruto e texto corrigido
preserva segmentos e metadados úteis
Uso
python transcribe_local.py audio.ogg
Para testar um modelo menor e mais rápido:
FASTER_WHISPER_MODEL=base python transcribe_local.py audio.ogg
Para forçar CPU com int8:
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.
{
"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 textosmall: 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.


