QuartzNet-15x5 CTC para português: do áudio ao texto
A oferta então limitada de ASR aberto para português motivou este projeto. Ajustei o stt_en_quartznet15x5, criado pela NVIDIA e pré-treinado em fala inglesa, com 26 horas do Mozilla Common Voice 9.0 em português. Publiquei checkpoint .nemo, notebook, demonstração e métricas; a principal contribuição é uma base compacta e ajustável para reconhecimento de fala [1][2].
No GitHub, o projeto stt_pt_quartznet15x5_ctc_small inclui Readme.md com instruções de uso por ASRModel.from_pretrained, transcrição de áudio único e lote, amostra de inferência e limites explícitos para discurso técnico. Esse documento também vincula a dependência no NeMo, os scripts de avaliação e o pré-processamento com manifest antes da métrica [1].
A motivação é prática: disponibilizar uma base aberta, relativamente compacta e ajustável para português, quando modelos de ASR ainda eram grandes e a oferta pública para o idioma era limitada. QuartzNet troca as convoluções densas do Jasper por convoluções 1D separáveis em tempo e canal. O artigo original apresenta o 15x5 com 18,9 milhões de parâmetros, contra 333 milhões do Jasper DR 10x5 comparado no trabalho, preservando uma pilha puramente convolucional adequada a paralelismo [4][5]. “Small”, portanto, é relativo à família e à época, não uma promessa de execução instantânea em qualquer dispositivo.
| Propriedade | Valor verificável |
|---|---|
| Entrada do modelo | áudio mono, 16.000 Hz |
| Saída | letras a-z minúsculas e espaço, sem pontuação ou acentos |
| Dados usados pelo projeto | 26 horas do Common Voice 9.0 em português |
| Arquitetura | EncDecCTCModel, QuartzNet-15x5, 64 filtros Mel |
| Versão gravada no checkpoint | NeMo 1.9.0 |
| Resultado publicado | WER 49,17%; CER 18,59% no split de teste indicado no cartão |
| Licença declarada no cartão | CC BY 4.0 |
Dados e preparação do áudio
O Common Voice é construído por contribuições voluntárias de frases lidas e validações comunitárias [8]. A ficha da versão cv-corpus-9.0-2022-04-27 registra, para português, 134,68 horas totais, 115,65 horas validadas, 115.489 clipes e 2.453 participantes. Os splits oficiais continham 17.559 clipes de treino, 8.528 de desenvolvimento e 8.539 de teste [7]. As 26 horas declaradas pelo projeto são, portanto, apenas a parcela efetivamente usada no ajuste, não todo o corpus português validado.
No notebook, baixei train.tsv, dev.tsv e test.tsv, converti o áudio para WAV mono de 16 kHz e produzi manifestos JSON Lines. Cada linha contém audio_filepath, duration em segundos e text, formato esperado pelo NeMo [1][3]. Depois de obter e revisar o script de preparação indicado no projeto, use esta execução:
python scripts/get_commonvoice_data.py \
--data_root "datasets/pt/" \
--manifest_dir="manifests/pt" \
--sample_rate=16000 \
--n_channels=1 \
--version="cv-corpus-9.0-2022-04-27" \
--language="pt" \
--log \
--files_to_process train.tsv dev.tsv test.tsvNo cartão, escrevi “16000 KHz”; o valor correto, registrado como sample_rate: 16000, é 16 kHz, ou 16.000 amostras por segundo. O MP3 de exemplo que publiquei é mono, dura aproximadamente 3,636 s e está codificado a 32 kHz; isso pode ser verificado com ffprobe. Para remover dependência de decodificação e tornar a entrada inequívoca, converta-o antes da inferência:
wget -O common_voice_pt_25555332.mp3 \
"https://github.com/DominguesM/stt_pt_quartznet15x5_ctc_small/raw/main/audios/common_voice_pt_25555332.mp3"
ffprobe -v error -show_entries stream=sample_rate,channels \
-show_entries format=duration -of json common_voice_pt_25555332.mp3
ffmpeg -i common_voice_pt_25555332.mp3 -ac 1 -ar 16000 \
-c:a pcm_s16le common_voice_pt_25555332.wavConverti o texto para minúsculas e removi pontuação como vírgula, ponto, hífen, dois-pontos, ponto e vírgula, aspas e interrogação. O vocabulário final tem 27 símbolos: as 26 letras latinas não acentuadas e espaço. Isso reduz a ambiguidade para poucos dados, mas também torna impossível emitir diretamente á, ç, maiúsculas ou pontuação.
Renderizando diagrama...
QuartzNet-15x5 por dentro
Jasper organiza o codificador como B x R: B blocos residuais, cada um com R sub-blocos convolucionais [5]. QuartzNet mantém a organização, batch normalization, ReLU e conexões residuais, mas fatora a convolução temporal densa em duas etapas [4]:
- uma convolução depthwise percorre o tempo separadamente em cada canal;
- uma convolução pointwise de kernel 1 mistura os canais.
Para kernel temporal de tamanho K, C_in canais de entrada e C_out de saída, ignorando vieses, a economia é:
Em um módulo com K=33 e C_in=C_out=256, presente na configuração, isso passa de 2.162.688 para 73.984 pesos, redução de aproximadamente 96,58% naquele módulo. O valor não significa a mesma redução no modelo inteiro: projeções residuais, normalizações e convoluções finais continuam existindo.
Configurei um prólogo com kernel 33, 256 canais e stride 2; depois, 15 blocos, cada um repetindo cinco módulos. Os grupos de kernels/canais são 33/256, 39/256, 51/512, 63/512 e 75/512, cada grupo repetido três vezes. No epílogo, usei kernel 87 com dilatação 2 e 512 canais, seguido de projeção para 1.024 canais e decodificador com vocabulário de 27 símbolos, além do blank CTC. O preprocessor calcula 64 filtros Mel em janelas de 20 ms, avanço de 10 ms, janela Hann e FFT de 512 pontos [2].
Renderizando diagrama...
O alinhamento CTC
Áudio e transcrição não chegam alinhados quadro a quadro. A Connectionist Temporal Classification (CTC) introduz o símbolo vazio (blank) e soma a probabilidade de todos os caminhos temporais que, depois de remover blanks e colapsar repetições, geram a transcrição y [6]. Se B é essa operação de colapso e pi um caminho de comprimento T:
Assim, caminhos como c blank a a blank s a podem colapsar para casa. Na inferência padrão que publiquei, usei decodificação gulosa, sem modelo de linguagem externo. Isso simplifica o sistema, mas não corrige palavras acusticamente plausíveis com contexto linguístico.
Ambiente, configuração e treinamento
Para reprodução histórica, use o lançamento nomeado NeMo 1.9.0 empregado pelo notebook, em vez de assumir compatibilidade silenciosa com a API atual:
python -m pip install wget unidecode "matplotlib>=3.3.2"
python -m pip install \
"git+https://github.com/NVIDIA/NeMo.git@r1.9.0#egg=nemo_toolkit[all]"Treinei com batch 32 e validei com batch 8; usei 8 workers, trim_silence=true, duração máxima de 16,7 s, dropout 0, SpecCutout com 5 máscaras retangulares (rect_freq=50, rect_time=120), NovoGrad com taxa 0.01, betas [0.95, 0.5], weight decay 0.001, cosine annealing, warmup de 5% e taxa mínima 1e-5 [1][2]. Mantive o codificador descongelado.
O núcleo da configuração no notebook é:
import copy
import nemo.collections.asr as nemo_asr
from omegaconf import open_dict
model = nemo_asr.models.ASRModel.from_pretrained(
"stt_en_quartznet15x5", map_location="cpu"
)
model.change_vocabulary(new_vocabulary=list(train_dev_set))
model.encoder.unfreeze()
cfg = copy.deepcopy(model.cfg)
with open_dict(cfg):
cfg.train_ds.manifest_filepath = (
f"{train_manifest_cleaned},{dev_manifest_cleaned}"
)
cfg.train_ds.batch_size = 32
cfg.train_ds.num_workers = 8
cfg.train_ds.normalize_transcripts = False
cfg.train_ds.trim_silence = True
cfg.validation_ds.manifest_filepath = test_manifest_cleaned
cfg.validation_ds.batch_size = 8
cfg.validation_ds.num_workers = 8
cfg.validation_ds.normalize_transcripts = False
cfg.validation_ds.trim_silence = True
model.setup_training_data(cfg.train_ds)
model.setup_multiple_validation_data(cfg.validation_ds)Usei um dispositivo, 50 épocas, acumulação de gradiente 1, logs a cada 5 passos e validação a cada 10 épocas; trainer.fit(model) executou o ajuste. Registrei 11 horas para 50 épocas no Google Colab, mas não preservei o modelo exato da GPU, a semente, um manifesto dos dados ou as versões completas do ambiente. Por isso, não apresento esse tempo como benchmark universal.
Há uma ressalva metodológica: o notebook concatena treino e desenvolvimento e usa o split de teste como validação, inclusive monitorando val_wer para salvar o melhor modelo. Logo, o teste participa da seleção do checkpoint. Uma reprodução rigorosa deve reservar o teste até o final, usar dev para seleção e publicar também um resultado nesse protocolo corrigido; isso não permite chamar o novo número de reprodução direta dos 49,17%.
Avaliação: WER, CER e RTF
Para referência r e hipótese h, a distância de Levenshtein conta substituições S, deleções D e inserções I. Com N_w palavras e N_c caracteres na referência:
No cartão, publiquei WER 49,17% e CER 18,59% no teste processado do Common Voice português [2]. Em termos intuitivos, a primeira razão representa cerca de 49 operações de edição por 100 palavras de referência; não significa que exatamente 50,83% das palavras estejam corretas, pois inserções podem levar WER acima de 100%. O CER menor mostra que muitas hipóteses permanecem próximas na grafia, mas isso não basta para aplicações que dependam de palavras exatas.
A avaliação publicada foi executada com NeMo 1.9.0:
wget -P scripts/ \
"https://raw.githubusercontent.com/NVIDIA/NeMo/v1.9.0/examples/asr/speech_to_text_eval.py"
wget -P scripts/ \
"https://raw.githubusercontent.com/NVIDIA/NeMo/v1.9.0/examples/asr/transcribe_speech.py"
python scripts/speech_to_text_eval.py \
pretrained_name="dominguesm/stt_pt_quartznet15x5_ctc_small" \
dataset_manifest="manifests/pt/commonvoice_test_manifest_processed.json" \
output_filename="evaluation_transcripts.json" \
batch_size=32 amp=true use_cer=falsePara CER, repita com use_cer=true, mantendo checkpoint, manifesto e normalização idênticos. Não compare WER de referências pontuadas/acentuadas com hipóteses sem esses símbolos sem antes aplicar a mesma normalização aos dois lados.
O real-time factor mede velocidade, não qualidade:
RTF < 1 é mais rápido que tempo real. Não publiquei RTF; atribuir um valor sem medição esconderia diferenças de hardware, batch, aquecimento e I/O. Meça excluindo o carregamento do modelo:
import time
import soundfile as sf
import torch
import nemo.collections.asr as nemo_asr
files = ["common_voice_pt_25555332.wav"]
model = nemo_asr.models.ASRModel.from_pretrained(
"dominguesm/stt_pt_quartznet15x5_ctc_small"
)
duration = sum(sf.info(path).duration for path in files)
if torch.cuda.is_available():
torch.cuda.synchronize()
start = time.perf_counter()
texts = model.transcribe(files)
if torch.cuda.is_available():
torch.cuda.synchronize()
elapsed = time.perf_counter() - start
print(texts)
print({"seconds": elapsed, "audio_seconds": duration, "rtf": elapsed / duration})Inferência local e em Jetson
A menor inferência possível é a mesma do cartão:
import nemo.collections.asr as nemo_asr
asr_model = nemo_asr.models.ASRModel.from_pretrained(
"dominguesm/stt_pt_quartznet15x5_ctc_small"
)
print(asr_model.transcribe(["common_voice_pt_25555332.wav"]))Para um diretório, use o script versionado já baixado:
python scripts/transcribe_speech.py \
pretrained_name="dominguesm/stt_pt_quartznet15x5_ctc_small" \
audio_dir="/caminho/para/wavs"Em CPU local, comece com um arquivo e monitore RAM e RTF; lotes maiores aumentam padding e memória. Em GPU desktop, amp=true é apropriado no comando de avaliação publicado, mas valide se as transcrições permanecem equivalentes. Em Jetson, não instale uma wheel PyTorch genérica esperando CUDA: a NVIDIA exige uma wheel compatível com a versão do JetPack e fornece uma matriz específica [9]. NeMo 1.9.0 é antigo e suas dependências podem não ter wheels AArch64 compatíveis com JetPack moderno. A estratégia segura é fixar uma imagem/ambiente funcional, usar batch 1, medir temperatura e consumo, e comparar texto, WER e RTF contra a execução de referência. Exportar ou quantizar pode ajudar, mas é um novo artefato e requer teste de paridade; não publiquei no repositório ONNX, TensorRT nem métricas Jetson.
Reprodutibilidade, limitações e ética
Uma reprodução auditável deve registrar a edição Common Voice Corpus 9.0 de 27 de abril de 2022, o lançamento NeMo 1.9.0, a data de acesso ao modelo, os manifestos usados, o ambiente Python/CUDA/NeMo, o modelo da GPU, a semente, o protocolo de normalização e os resultados por split. Preserve os manifestos JSONL, o checkpoint .nemo e o model_config.yaml em armazenamento controlado. Não misture a configuração atual do NeMo com a histórica sem relatar a migração [3].
As limitações técnicas são substanciais. Vinte e seis horas são poucas para cobrir variedades do português, fala espontânea, ruído, microfones, velocidades, nomes e jargão. O modelo não restaura acentos nem pontuação, não faz diarização, timestamps, detecção de idioma ou streaming garantido. WER 49,17% impede uso autônomo em legendas críticas, saúde, justiça, crédito, segurança ou acessibilidade sem revisão humana.
A ficha do Common Voice 9.0 também evidencia representatividade desigual: entre os metadados portugueses, 74% foram marcados como masculinos, 4% femininos, 2% “outro” e 20% sem gênero; o campo de sotaque ficou 100% sem informação [7]. Metadados incompletos não me permitem concluir viés, mas impedem demonstrar equidade. Antes de produção, avalie por região, gênero, idade, ambiente acústico e deficiência de fala com consentimento e amostras suficientes. Não use a transcrição para inferir origem, identidade, saúde ou confiabilidade de uma pessoa. Informe erros, minimize retenção de voz, proteja gravações como dado pessoal e ofereça contestação humana.
Conclusão
Com o ajuste do QuartzNet, publiquei uma base compacta de ASR em português, acompanhada por notebook, checkpoint e métricas, e registrei o desempenho obtido com 26 horas do Common Voice 9.0. Aprendi que ampliação de dados e avaliação representativa são tão importantes quanto a arquitetura.
Referências
- DominguesM, repositório, notebook e áudio de
stt_pt_quartznet15x5_ctc_small, 2022. - Domingues, M. Cartão e checkpoint
stt_pt_quartznet15x5_ctc_small, 2022. - NVIDIA. NeMo ASR: configuração e preparação de datasets.
- Kriman, S. et al. QuartzNet: Deep Automatic Speech Recognition with 1D Time-Channel Separable Convolutions, ICASSP, 2020.
- Li, J. et al. Jasper: An End-to-End Convolutional Neural Acoustic Model, Interspeech, 2019.
- Graves, A. et al. Connectionist Temporal Classification: Labelling Unsegmented Sequence Data with Recurrent Neural Networks, ICML, 2006.
- Mozilla Common Voice. Ficha estatística do Corpus 9.0, corte de 27 de abril de 2022.
- Ardila, R. et al. Common Voice: A Massively-Multilingual Speech Corpus, LREC, 2020.
- NVIDIA. Installing PyTorch for Jetson Platform.
BibTeX
@misc{domingues2022quartznet15x5smallportuguese,
author = {Maicon Domingues},
title = {Fine-tuned QuartzNet-15x5 CTC small model for speech recognition in Portuguese},
year = {2022},
howpublished = {\url{https://huggingface.co/dominguesm/stt_pt_quartznet15x5_ctc_small}}
}
@inproceedings{kriman2020quartznet,
author = {Kriman, Samuel and Beliaev, Stanislav and Ginsburg, Boris and Huang, Jocelyn and Kuchaiev, Oleksii and Lavrukhin, Vitaly and Leary, Ryan and Li, Jason and Zhang, Yang},
title = {QuartzNet: Deep Automatic Speech Recognition with 1D Time-Channel Separable Convolutions},
booktitle = {ICASSP},
year = {2020},
doi = {10.1109/ICASSP40776.2020.9053889}
}
@inproceedings{li2019jasper,
author = {Li, Jason and Lavrukhin, Vitaly and Ginsburg, Boris and Leary, Ryan and Kuchaiev, Oleksii and Cohen, Jonathan M. and Nguyen, Huyen and Gadde, Ravi Teja},
title = {Jasper: An End-to-End Convolutional Neural Acoustic Model},
booktitle = {Interspeech},
year = {2019},
doi = {10.21437/Interspeech.2019-1819}
}
@inproceedings{graves2006ctc,
author = {Graves, Alex and Fern\'{a}ndez, Santiago and Gomez, Faustino and Schmidhuber, J\"{u}rgen},
title = {Connectionist Temporal Classification: Labelling Unsegmented Sequence Data with Recurrent Neural Networks},
booktitle = {Proceedings of the 23rd International Conference on Machine Learning},
pages = {369--376},
year = {2006}
}
@inproceedings{ardila2020commonvoice,
author = {Ardila, Rosana and Branson, Megan and Davis, Kelly and Henretty, Michael and Kohler, Michael and Meyer, Josh and Morais, Reuben and Saunders, Lindsay and Tyers, Francis M. and Weber, Gregor},
title = {Common Voice: A Massively-Multilingual Speech Corpus},
booktitle = {Proceedings of LREC 2020},
pages = {4218--4222},
year = {2020}
}