9.4 KiB
Spec: sherpa-onnx como sidecar ASR
Status: rascunho aguardando aprovação
Data: 2026-06-11
Decisão vinculada a: Plano 2026-06-10-transcription-app-native-integrations.md, decisão do usuário de 2026-06-11 (sherpa-onnx, sidecar embutido, modelo baixado, manter botão de download, fixture oficial).
Problema
O pipeline atual chama parakeet-main (CrispASR/GGUF). Esse binário não tem release oficial pré-buildado: precisa ser compilado a partir do source do CrispStrobe/CrispASR branch parakeet. O usuário não quer compilar nada.
Evidência capturada em 2026-06-11 via TranscriptionAttempt (error: "program not found", exitCode: null, audioSizeBytes: 407084): captura e modelo GGUF estão OK, mas o sidecar simplesmente não existe no PATH.
Decisão
Trocar o sidecar para sherpa-onnx (binário sherpa-onnx-offline pré-buildado do release oficial do k2-fsa), com o modelo sherpa-onnx-nemo-parakeet-tdt-0.6b-v3-int8 (4 arquivos: encoder.int8.onnx, decoder.int8.onnx, joiner.int8.onnx, tokens.txt).
- O Parakeet TDT v3 da NVIDIA é o mesmo modelo base do GGUF; só muda o export (ONNX int8 em vez de GGUF Q8). O modelo v3 sherpa-onnx suporta 25 línguas europeias, incluindo português.
- O instalador passa a embutir o sidecar (
sherpa-onnx-offline-<triple>), mas o modelo continua sendo baixado na primeira execução (mantém o instalador leve e o botão "Baixar modelo" continua útil). - O fluxo de "Baixar modelo GGUF" permanece para casos futuros (Q5/Q4 ou modelo customizado). Não é o caminho primário, mas o código continua compilando e o comando Tauri segue existindo.
Comportamento esperado
- Usuário grava áudio (atalho global ou botão).
- App grava WAV via
cpal+hound(já implementado). - App chama
sherpa-onnx-offline --encoder=... --decoder=... --joiner=... --tokens=... --model-type=nemo_transducer <recording.wav>. - stdout do sidecar é parseado para extrair o texto (primeira linha não-vazia, ignorando linhas de cabeçalho do sherpa-onnx).
- Texto aparece no editor da UI;
TranscriptionAttempté populado comstage=ok,exitCode=0,stdoutTailcontendo o texto,audioSizeBytes>0. - Se sidecar falhar,
last_attempt.stage=sidecar_error,errorpopulado, UI mostra painel "Detalhes técnicos".
Critérios verificáveis
Cada critério abaixo deve ter um teste automatizado que falha hoje e passa após a implementação.
CV1 — Sidecar sherpa-onnx-offline está empacotado
src-tauri/tauri.conf.jsondeclarabundle.externalBin = ["binaries/sherpa-onnx-offline"].src-tauri/binaries/sherpa-onnx-offline-x86_64-pc-windows-msvc.exeexiste localmente para Windows (commitado vianpm run setup:sidecar).- Mesmo para Linux (
x86_64-unknown-linux-gnu) e macOS (aarch64-apple-darwin). - Script
scripts/setup-sidecar.mjsbaixa o binário do release oficial do k2-fsa para a tripla atual.
CV2 — Modelo Parakeet v3 int8 ONNX é baixável
- Catálogo de modelos em
src-tauri/src/models/mod.rsadicionasherpa-onnx-nemo-parakeet-tdt-0.6b-v3-int8apontando para o tarball oficial emhttps://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherpa-onnx-nemo-parakeet-tdt-0.6b-v3-int8.tar.bz2. download_default_model(ou novodownload_default_model_set) baixa o tarball, extrai os 4 arquivos ONNX para~/.local/share/transcript/models/sherpa-onnx/.- Botão "Baixar modelo" na UI continua funcionando e popula
models[0].status = "ready"quando a extração termina.
CV3 — Transcrição real funciona end-to-end (TDD)
- Teste de integração em
src-tauri/src/transcription/sherpa_onnx.rs::tests:- Fixture:
tests/fixtures/jfk.wavbaixada dehttps://github.com/k2-fsa/sherpa-onnx/raw/master/sherpa-onnx/test_wavs/jfk.wav(commitada como binário viascripts/setup-fixtures.mjs). - Setup de CI:
scripts/setup-sidecar.mjsbaixa o binário oficial se ausente. - Teste executa
sherpa_onnx.transcribe(jfk.wav)e asserta que o texto retornado contém"ask not what your country can do for you"(trecho conhecido do JFK, presente em modelos parakeet treinados nesse áudio). - Se o sidecar não estiver presente (skip explícito), o teste marca
eprintln!("sidecar ausente, pulando")e sai com sucesso. CI deve falhar se o sidecar não baixar.
- Fixture:
CV4 — Pipeline de erro funciona
- Teste
sherpa_onnx.rs::tests::sidecar_missing_populates_attempt: sidecar apontando para binário inexistente, WAV de 8 bytes, assertaErr(AppError::Transcription(_))e que oTranscriptionAttemptpopulado temstage=sidecar_error,audioSizeBytes=8,error.is_some().
CV5 — UI mostra o novo sidecar e a evidência
- Painel "Detalhes técnicos" mostra
sidecar: sherpa-onnx-offline. last_attempt.stageem sucesso aparece comook.- Mensagem de sucesso indica modelo usado (ex: "Transcrição concluída (407 KB, sherpa-onnx-offline, parakeet v3 int8).").
CV6 — CI Linux/Windows baixa o binário e roda o teste
.github/workflows/ci.ymle.github/workflows/build.ymlganham stepRun setup-sidecarantes docargo test.- O release workflow
.github/workflows/release.ymlganha stepBundle sidecar into installerque executascripts/setup-sidecar.mjsantes dotauri build.
Especificação técnica
Rust: novo módulo src-tauri/src/transcription/sherpa_onnx.rs
pub struct SherpaOnnxConfig {
pub sidecar_name: Option<String>, // "sherpa-onnx-offline"
pub model_dir: Option<PathBuf>, // diretório com os 4 ONNX
pub num_threads: Option<usize>, // default = clamp(1, 4)
}
pub struct SherpaOnnxTranscriber { /* ... */ }
impl Transcriber for SherpaOnnxTranscriber {
fn transcribe(&self, input: TranscriptionInput) -> AppResult<TranscriptionResult> {
// 1. validar 4 arquivos em model_dir
// 2. executar: sherpa-onnx-offline --encoder=... --decoder=... --joiner=...
// --tokens=... --model-type=nemo_transducer --num-threads=N <wav>
// 3. parsear stdout (primeira linha não-vazia)
// 4. retornar TranscriptionResult com attempt populado
}
}
Comportamento do Transcriber
- Reaproveita
TranscriptionInput/TranscriptionResult/TranscriptionAttempt(já existentes). last_attempt.stagevalores:audio_check,sidecar_error,ok.last_attempt.stdout_tailcontém no mínimo a primeira linha do stdout.
Catálogo de modelos (Rust)
- Adiciona
SherpaOnnxModel { id, repo, files: ["encoder.int8.onnx", ...], size_bytes_total, ... }. - Mantém
OfflineModelGGUF existente para o botão "Baixar modelo" continuar funcionando.
UI (TypeScript)
- Atualiza
formatAttempte painel "Detalhes técnicos" para mostrar a string do sidecar dinamicamente. - Mensagem de sucesso inclui o nome do sidecar (lido de
last_attempt.sidecar).
tauri.conf.json
{
"bundle": {
"externalBin": ["binaries/sherpa-onnx-offline"]
}
}
E em src-tauri/capabilities/default.json:
{
"permissions": [
"core:default",
{
"identifier": "shell:allow-execute",
"allow": [{ "name": "binaries/sherpa-onnx-offline", "sidecar": true }]
}
]
}
Scripts novos
scripts/setup-sidecar.mjs: baixa o binário correto do release oficial do k2-fsa para a tripla do host e move parasrc-tauri/binaries/sherpa-onnx-offline-<triple>.scripts/setup-fixtures.mjs: baixa ojfk.wavparasrc-tauri/tests/fixtures/jfk.wavse ausente.
Fora de escopo
- UI premium (Fase 3 do design).
- Painel flutuante com slide-up (Fase 2 do design).
- Suporte a CoreML/Metal/Vulkan (só CPU nesta v1).
- Auto-detecção de campo focado para inserção direta.
- Hotkey customizável.
- Punctuation restoration (--punc-model).
- Streaming (VAD + decode incremental). Mantemos o modo offline igual ao MVP.
Riscos e mitigações
- Release do k2-fsa muda de URL/tag: fixar versão no script de setup (ex:
v1.12.23). Documentar upgrade. - Mirror do GitHub pode estar fora do ar: manter URL alternativa via
huggingface.cose sherpa-onnx migrar assets. - Modelo 670 MB: usuário precisa de ~1 GB livre para download. Documentar no README.
- Teste pesado em CI: a transcrição do
jfk.wavdeve levar <5s em CPU. Se passar de 30s em CI, marcar#[ignore]com motivo. - Tarball .tar.bz2: precisa de
bzip2para extrair; Rust tembzip2crate puro-Rust. Adicionar dependência.
Critério de "Pronto"
Implementação só é considerada pronta quando:
- Todos os testes Rust passam (
cargo test --manifest-path src-tauri/Cargo.toml). - Todos os testes JS passam (
npm run test -- --run). npm run tauri:buildno Windows gera.msie.nsissem erro.- Em uma máquina limpa: instalar o
.msi, abrir o app, baixar o modelo, gravar, ver o texto transcrito. - CV1–CV6 todos verdes.
Plano de implementação (3 etapas incrementais, cada uma commita)
- E1: Scripts de setup + sidecar empacotado (CV1). Sem mudar o pipeline.
- E2: Novo
SherpaOnnxTranscriber(CV3, CV4) + comando Tauri para selecionar. UI ainda usa o CrispASR. - E3: Trocar o transcriber padrão para sherpa-onnx (CV5) + integração no CI (CV6). Botão "Baixar modelo" agora baixa o pacote sherpa-onnx.
Cada etapa termina com cargo test + npm run test verdes. Etapa só avança para a próxima quando a anterior estiver validada manualmente (você roda e confirma).