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

174 lines
9.4 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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-<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`
```rust
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
```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-<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).