ASR de português brasileiro que roda em tempo real sobre CPU, sem GPU e sem chamada de
rede. Zipformer-CTC de 64M parâmetros, quantizado int8, com cabeça de fonema auxiliar.
A aposta é especialização: um modelo que só faz PT-BR cabe em dezenas de milhões de
parâmetros onde um multilíngue de 600M não fecha real-time em CPU.
RTFx é a razão entre duração do áudio e tempo de processamento — 40× significa que 40
segundos de áudio são transcritos em 1 segundo. O requisito do produto é ≥ 6×.
Como este peso foi escolhido
Dois candidatos foram medidos no mesmo conjunto, com a mesma régua de normalização:
peso
WER
CER
RTFx
model.int8.onnx (média de 112k+124k)
15,99%
7,30%
40,0×
alternates/ckpt124k.int8.onnx (checkpoint único)
17,32%
7,39%
34,3×
Bootstrap pareado de 5.000 reamostragens: IC95% do delta = [−2,25, −0,43] pp. O intervalo
não cruza zero — a vantagem do checkpoint averaging é real, não ruído amostral.
O model_card.json é a autoridade legível por máquina sobre qual arquivo é o canônico.
Não deduza o peso oficial pelo nome do arquivo — neste projeto, escolher por nome já
entregou o modelo pior em silêncio.
Uso
python
1import onnxruntime as ort
23sess = ort.InferenceSession("model.int8.onnx", providers=["CPUExecutionProvider"])4tokens =[l.split()[0]for l inopen("tokens.txt", encoding="utf-8")]56# entrada: log-mel fbank 80-bin, kaldi/HTK, 16 kHz -> (N, T, 80)7logits = sess.run(None,{"x": feats,"x_lens": lens})[0]# (N, T, 500)89# decode: CTC greedy — argmax, colapsa repetições, remove o blank (id 0)
O pipeline completo (fbank, segmentação, decode, lote) está em
jvscribe:
contrato legível por máquina (arquivo canônico, sha256, fingerprint do vocabulário, WER e condição da medição)
alternates/ckpt124k.int8.onnx
68 MB
checkpoint único, WER 17,32%
alternates/ckpt124k.fp32.onnx
249 MB
fp32 do acima — ponto de partida para requantizar
finetune/
2,6 GB
tudo para retomar o treino — ver finetune/README.md
O par (modelo, vocabulário) não é intercambiável. Dois artefatos deste projeto têm 500
tokens cada e 492 dos 500 ids mapeiam para tokens diferentes: trocar o tokens.txt produz
texto plausível e errado, sem erro algum. Valide pelo vocab_fingerprint do model_card.json,
nunca pela contagem de tokens.
Arquitetura
encoder
Zipformer, 6 stacks
num-encoder-layers
2,2,3,4,3,2
feedforward-dim
512,768,1024,1536,1024,768
encoder-dim
192,256,384,512,384,256
encoder-unmasked-dim
192,192,256,256,256,192
decoder
CTC greedy (sem transducer)
cabeça auxiliar
CTC de fonema (--use-phoneme-ctc 1)
tokenização
SentencePiece BPE, 500 unidades, PT-BR dedicado
features
log-mel fbank 80-bin, kaldi/HTK, 16 kHz
parâmetros
~64M
quantização
int8 dinâmica (ONNX Runtime)
A cabeça de fonema é auxiliar de treino — ela regulariza o encoder e sai do grafo de
inferência. Ganho medido em M4: −4,74% relativo de WER (IC95% [2,79, 6,72]).
Treino
Receita icefallzipformer sobre
egs/commonvoice/ASR. Corpus: TAGARELA (8.972 h, 91% PT-BR, pseudo-rotulado) com
finetune em CORAA. Suíte de avaliação do projeto inclui Common Voice 21.0 (CC0) e
MLS-PT (CC-BY-4.0).
Para retomar o treino, ver finetune/README.md. Ele traz os comandos
reais, as quatro flags de arquitetura sem as quais o checkpoint não carrega, e as armadilhas
que já custaram runs inteiros neste projeto.
Limitações — leia antes de usar em produção
Telefonia 8 kHz não foi medida. Todos os números aqui são banda larga 16 kHz. A
literatura indica penalidade de 2–3× em canal telefônico; para este modelo isso é
[DESCONHECIDO].
Fala espontânea de call center não foi medida. FLEURS é leitura de notícias, um regime
mais fácil que conversa real.
Só português brasileiro. Não há suporte multilíngue nem code-switching validado.
Overfitting ao professor. O finetune de M5 apresenta overfitting aos pseudo-rótulos do
Whisper presentes no TAGARELA — o gargalo é qualidade de dado, não capacidade do encoder.
Este modelo NÃO é streaming. É não-causal: a atenção enxerga o contexto inteiro nos
dois sentidos. Três confirmações independentes — o metadado do artefato diz
comment: "non-streaming zipformer2 CTC"; o grafo ONNX não tem entrada nem saída de estado
(só x, x_lens → log_probs, log_probs_len); e os scripts de treino nunca passam
--causal 1, cujo default no icefall é False.
Consequência prática: dá para usá-lo ao vivo com janela deslizante + LocalAgreement-2
(é o que jvscribe/realtime/ faz, técnica do Whisper-Streaming), mas cada atualização
reprocessa a janela inteira. Medido: 121 ms para reprocessar 6 s contra 11 ms que
custaria processar só os 0,5 s novos — 10,6× de trabalho redundante por hop, que é o
piso de latência do desenho. Um modelo com cache de estado remove esse termo, e isso exige
retreinar com --causal 1 e reexportar com entradas de estado; não é ajuste de runtime.
avg-124k-112k.pt foi reconstruído. O .pt original chegou truncado da nuvem; ver
finetune/README.md § Incidente. A equivalência bit a bit com o original não foi verificada.
Licença
other — os termos de redistribuição são herdados dos corpora de treino e ainda não foram
formalmente apurados. O TAGARELA carrega pseudo-rótulos gerados por Whisper; CORAA, Common
Voice e MLS-PT têm licenças próprias e distintas entre si. Este repositório é privado
justamente porque essa apuração está pendente.
Não redistribua nem use comercialmente sem antes revisar a licença de cada corpus de origem.
Citação
bibtex
1@misc{jvscribe2026,
2 title = {jvscribe: ASR PT-BR em tempo real sobre CPU},
3 author = {Paulo Henrique},
4 year = {2026},
5 note = {Zipformer-CTC 64M, int8, WER 15,99\% em FLEURS pt_br}
6}