Files
transcript/docs/superpowers/specs/2026-06-11-sherpa-onnx-sidecar.md
T

9.4 KiB
Raw Blame History

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

  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 <recording.wav>.
  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

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.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

{
  "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 para src-tauri/binaries/sherpa-onnx-offline-<triple>.
  • 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).