# 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](https://github.com/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`](https://github.com/k2-fsa/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`](https://huggingface.co/csukuangfj/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-`), 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 1. Usuário grava áudio (atalho global ou botão). 2. App grava WAV via `cpal` + `hound` (já implementado). 3. App chama `sherpa-onnx-offline --encoder=... --decoder=... --joiner=... --tokens=... --model-type=nemo_transducer `. 4. stdout do sidecar é parseado para extrair o texto (primeira linha não-vazia, ignorando linhas de cabeçalho do sherpa-onnx). 5. Texto aparece no editor da UI; `TranscriptionAttempt` é populado com `stage=ok`, `exitCode=0`, `stdoutTail` contendo o texto, `audioSizeBytes>0`. 6. Se sidecar falhar, `last_attempt.stage=sidecar_error`, `error` populado, 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.json` declara `bundle.externalBin = ["binaries/sherpa-onnx-offline"]`. - `src-tauri/binaries/sherpa-onnx-offline-x86_64-pc-windows-msvc.exe` existe localmente para Windows (commitado via `npm run setup:sidecar`). - Mesmo para Linux (`x86_64-unknown-linux-gnu`) e macOS (`aarch64-apple-darwin`). - Script `scripts/setup-sidecar.mjs` baixa 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.rs` adiciona `sherpa-onnx-nemo-parakeet-tdt-0.6b-v3-int8` apontando para o tarball oficial em `https://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 novo `download_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`: 1. Fixture: `tests/fixtures/jfk.wav` baixada de `https://github.com/k2-fsa/sherpa-onnx/raw/master/sherpa-onnx/test_wavs/jfk.wav` (commitada como binário via `scripts/setup-fixtures.mjs`). 2. Setup de CI: `scripts/setup-sidecar.mjs` baixa o binário oficial se ausente. 3. 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). 4. 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. ### CV4 — Pipeline de erro funciona - Teste `sherpa_onnx.rs::tests::sidecar_missing_populates_attempt`: sidecar apontando para binário inexistente, WAV de 8 bytes, asserta `Err(AppError::Transcription(_))` e que o `TranscriptionAttempt` populado tem `stage=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.stage` em sucesso aparece como `ok`. - 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.yml` e `.github/workflows/build.yml` ganham step `Run setup-sidecar` antes do `cargo test`. - O release workflow `.github/workflows/release.yml` ganha step `Bundle sidecar into installer` que executa `scripts/setup-sidecar.mjs` antes do `tauri build`. ## Especificação técnica ### Rust: novo módulo `src-tauri/src/transcription/sherpa_onnx.rs` ```rust pub struct SherpaOnnxConfig { pub sidecar_name: Option, // "sherpa-onnx-offline" pub model_dir: Option, // diretório com os 4 ONNX pub num_threads: Option, // default = clamp(1, 4) } pub struct SherpaOnnxTranscriber { /* ... */ } impl Transcriber for SherpaOnnxTranscriber { fn transcribe(&self, input: TranscriptionInput) -> AppResult { // 1. validar 4 arquivos em model_dir // 2. executar: sherpa-onnx-offline --encoder=... --decoder=... --joiner=... // --tokens=... --model-type=nemo_transducer --num-threads=N // 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.stage` valores: `audio_check`, `sidecar_error`, `ok`. - `last_attempt.stdout_tail` conté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 `OfflineModel` GGUF existente para o botão "Baixar modelo" continuar funcionando. ### UI (TypeScript) - Atualiza `formatAttempt` e 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 ```jsonc { "bundle": { "externalBin": ["binaries/sherpa-onnx-offline"] } } ``` E em `src-tauri/capabilities/default.json`: ```jsonc { "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 para `src-tauri/binaries/sherpa-onnx-offline-`. - `scripts/setup-fixtures.mjs`: baixa o `jfk.wav` para `src-tauri/tests/fixtures/jfk.wav` se 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.co` se 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.wav` deve levar <5s em CPU. Se passar de 30s em CI, marcar `#[ignore]` com motivo. - **Tarball .tar.bz2**: precisa de `bzip2` para extrair; Rust tem `bzip2` crate puro-Rust. Adicionar dependência. ## Critério de "Pronto" Implementação só é considerada pronta quando: 1. Todos os testes Rust passam (`cargo test --manifest-path src-tauri/Cargo.toml`). 2. Todos os testes JS passam (`npm run test -- --run`). 3. `npm run tauri:build` no Windows gera `.msi` e `.nsis` sem erro. 4. Em uma máquina limpa: instalar o `.msi`, abrir o app, baixar o modelo, gravar, ver o texto transcrito. 5. CV1–CV6 todos verdes. ## Plano de implementação (3 etapas incrementais, cada uma commita) 1. **E1**: Scripts de setup + sidecar empacotado (CV1). Sem mudar o pipeline. 2. **E2**: Novo `SherpaOnnxTranscriber` (CV3, CV4) + comando Tauri para selecionar. UI ainda usa o CrispASR. 3. **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).