feat(etapa1): baixar e empacotar sidecar sherpa-onnx-offline
This commit is contained in:
1 parent
b2554006a4
commit
c5baf7a23f
11 files changed
+416
-6
No files matched your search
@@ -0,0 +1,173 @@
|
|||||||
|
# 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).
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
// Baixa o release `*-shared` oficial do k2-fsa/sherpa-onnx v1.12.23 e extrai
|
||||||
|
// apenas o binário `sherpa-onnx-offline` para `src-tauri/binaries/sherpa-onnx-offline-<triple><ext>`.
|
||||||
|
//
|
||||||
|
// O shared lib já vem com as DLLs/so de `onnxruntime` no diretório `bin/`. Essas
|
||||||
|
// DLLs adicionais são empacotadas como `resources` do Tauri (ver tauri.conf.json)
|
||||||
|
// para que o executável as encontre em produção.
|
||||||
|
//
|
||||||
|
// Uso:
|
||||||
|
// node scripts/setup-sidecar.mjs
|
||||||
|
//
|
||||||
|
// Variáveis de ambiente opcionais:
|
||||||
|
// SHERPA_ONNX_VERSION (default: v1.12.23)
|
||||||
|
// SHERPA_ONNX_TRIPLE (default: detectado via `rustc -vV`)
|
||||||
|
|
||||||
|
import { execFileSync } from "node:child_process";
|
||||||
|
import {
|
||||||
|
existsSync,
|
||||||
|
mkdirSync,
|
||||||
|
chmodSync,
|
||||||
|
createWriteStream,
|
||||||
|
statSync,
|
||||||
|
renameSync,
|
||||||
|
copyFileSync,
|
||||||
|
} from "node:fs";
|
||||||
|
import { dirname, join, resolve, sep as pathSep } from "node:path";
|
||||||
|
import { pipeline } from "node:stream/promises";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import process from "node:process";
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
const repoRoot = resolve(__dirname, "..");
|
||||||
|
const binariesDir = join(repoRoot, "src-tauri", "binaries");
|
||||||
|
mkdirSync(binariesDir, { recursive: true });
|
||||||
|
|
||||||
|
const DEFAULT_VERSION = "v1.12.23";
|
||||||
|
|
||||||
|
const PLATFORM_MAP = [
|
||||||
|
{ keys: ["linux", "x64"], asset: "sherpa-onnx-v1.12.23-linux-x64-shared.tar.bz2", triple: "x86_64-unknown-linux-gnu", ext: "" },
|
||||||
|
{ keys: ["linux", "arm64"], asset: "sherpa-onnx-v1.12.23-linux-aarch64-shared.tar.bz2", triple: "aarch64-unknown-linux-gnu", ext: "" },
|
||||||
|
{ keys: ["darwin", "x64"], asset: "sherpa-onnx-v1.12.23-osx-universal2-shared.tar.bz2", triple: "x86_64-apple-darwin", ext: "" },
|
||||||
|
{ keys: ["darwin", "arm64"], asset: "sherpa-onnx-v1.12.23-osx-universal2-shared.tar.bz2", triple: "aarch64-apple-darwin", ext: "" },
|
||||||
|
{ keys: ["win32", "x64"], asset: "sherpa-onnx-v1.12.23-win-x64-shared.tar.bz2", triple: "x86_64-pc-windows-msvc", ext: ".exe" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const RELEASE_BASE = "https://github.com/k2-fsa/sherpa-onnx/releases/download";
|
||||||
|
|
||||||
|
function detectTriple() {
|
||||||
|
if (process.env.SHERPA_ONNX_TRIPLE) return process.env.SHERPA_ONNX_TRIPLE;
|
||||||
|
try {
|
||||||
|
const output = execFileSync("rustc", ["-vV"], { encoding: "utf8" });
|
||||||
|
const line = output.split("\n").find((l) => l.startsWith("host:"));
|
||||||
|
if (line) return line.replace("host:", "").trim();
|
||||||
|
} catch {
|
||||||
|
// cai para detecção por platform/arch
|
||||||
|
}
|
||||||
|
const os = process.platform;
|
||||||
|
const archName = process.arch;
|
||||||
|
return PLATFORM_MAP.find(
|
||||||
|
(p) => (p.keys[0] === "win32" ? os === "win32" : p.keys[0] === os) && p.keys[1] === archName,
|
||||||
|
)?.triple;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickAsset(triple) {
|
||||||
|
const found = PLATFORM_MAP.find((p) => p.triple === triple);
|
||||||
|
if (!found) {
|
||||||
|
throw new Error(
|
||||||
|
`Não há binário pré-buildado para o target ${triple}. Edite scripts/setup-sidecar.mjs para apontar para um build customizado.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function download(url, dest) {
|
||||||
|
const res = await fetch(url);
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`download falhou: ${res.status} ${res.statusText} em ${url}`);
|
||||||
|
}
|
||||||
|
if (!res.body) {
|
||||||
|
throw new Error(`resposta vazia em ${url}`);
|
||||||
|
}
|
||||||
|
await pipeline(res.body, createWriteStream(dest));
|
||||||
|
return statSync(dest).size;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractTarBzip2(archive, cwd) {
|
||||||
|
// `tar` (bsdtar) no Windows não aceita bem paths com backslash nem `D:`
|
||||||
|
// em `-C`. Usamos `cwd` + path relativo para evitar isso.
|
||||||
|
const prefix = cwd.endsWith(pathSep) ? cwd : cwd + pathSep;
|
||||||
|
const relArchive = archive.startsWith(prefix) ? archive.slice(prefix.length) : archive;
|
||||||
|
execFileSync("tar", ["-xjf", relArchive], { stdio: "inherit", cwd });
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveExtractedBin(extractedBinDir, expectedNames, dest) {
|
||||||
|
for (const name of expectedNames) {
|
||||||
|
const candidate = join(extractedBinDir, name);
|
||||||
|
if (existsSync(candidate)) {
|
||||||
|
renameSync(candidate, dest);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error(
|
||||||
|
`binário sherpa-onnx-offline não encontrado em ${extractedBinDir}; nomes esperados: ${expectedNames.join(", ")}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyExtraSharedLibs(extractedRoot, ext) {
|
||||||
|
// As DLLs adicionais (onnxruntime, providers_shared) precisam acompanhar o
|
||||||
|
// executável em runtime. Copiamos para `src-tauri/binaries/` lado a lado
|
||||||
|
// do binário renomeado; o empacotamento final via tauri.conf.json
|
||||||
|
// resources cuida de movê-las para junto do .exe no instalador.
|
||||||
|
const binDir = join(extractedRoot, "bin");
|
||||||
|
if (!existsSync(binDir)) return;
|
||||||
|
for (const file of ["onnxruntime.dll", "onnxruntime_providers_shared.dll", "cargs.dll"]) {
|
||||||
|
const src = join(binDir, file);
|
||||||
|
if (existsSync(src)) {
|
||||||
|
const destName = ext === ".exe" ? file : file.replace(/\.dll$/, ".so");
|
||||||
|
copyFileSync(src, join(binariesDir, destName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const triple = detectTriple();
|
||||||
|
if (!triple) {
|
||||||
|
throw new Error("Não foi possível detectar o target triple do host.");
|
||||||
|
}
|
||||||
|
const asset = pickAsset(triple);
|
||||||
|
const version = process.env.SHERPA_ONNX_VERSION || DEFAULT_VERSION;
|
||||||
|
const assetName = asset.asset.replace(/v1\.12\.23/, version);
|
||||||
|
const versionNoV = version.replace(/^v/, "");
|
||||||
|
const url = `${RELEASE_BASE}/v${versionNoV}/${assetName}`;
|
||||||
|
const finalPath = join(binariesDir, `sherpa-onnx-offline-${triple}${asset.ext}`);
|
||||||
|
|
||||||
|
if (existsSync(finalPath) && statSync(finalPath).size > 1024 * 1024) {
|
||||||
|
console.log(`✓ sidecar já presente em ${finalPath} (${statSync(finalPath).size} bytes)`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const archivePath = join(binariesDir, assetName);
|
||||||
|
console.log(`↓ baixando ${url}`);
|
||||||
|
const size = await download(url, archivePath);
|
||||||
|
console.log(`✓ ${size} bytes baixados em ${archivePath}`);
|
||||||
|
|
||||||
|
console.log("↓ extraindo…");
|
||||||
|
extractTarBzip2(archivePath, binariesDir);
|
||||||
|
|
||||||
|
const extractedRoot = join(binariesDir, assetName.replace(/\.tar\.bz2$/, ""));
|
||||||
|
const expected = asset.ext === ".exe" ? ["sherpa-onnx-offline.exe", "sherpa-onnx-offline"] : ["sherpa-onnx-offline"];
|
||||||
|
moveExtractedBin(join(extractedRoot, "bin"), expected, finalPath);
|
||||||
|
copyExtraSharedLibs(extractedRoot, asset.ext);
|
||||||
|
|
||||||
|
if (asset.ext === "") {
|
||||||
|
try {
|
||||||
|
chmodSync(finalPath, 0o755);
|
||||||
|
} catch {
|
||||||
|
// ignore em Windows
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✓ sidecar pronto em ${finalPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error(`falha: ${error.message}`);
|
||||||
|
process.exitCode = 1;
|
||||||
|
});
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { describe, it, expect, beforeAll } from "vitest";
|
||||||
|
import { execFileSync } from "node:child_process";
|
||||||
|
import { existsSync, statSync, rmSync } from "node:fs";
|
||||||
|
import { join, dirname } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
const repoRoot = join(__dirname, "..");
|
||||||
|
|
||||||
|
const SUPPORTED_HOST = "x86_64-pc-windows-msvc";
|
||||||
|
|
||||||
|
function detectHostTriple() {
|
||||||
|
try {
|
||||||
|
const output = execFileSync("rustc", ["-vV"], { encoding: "utf8" });
|
||||||
|
const line = output.split("\n").find((l) => l.startsWith("host:"));
|
||||||
|
return line ? line.replace("host:", "").trim() : null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("setup-sidecar", () => {
|
||||||
|
const hostTriple = detectHostTriple();
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
if (!hostTriple) {
|
||||||
|
console.warn("rustc não disponível; pulando teste de sidecar");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("baixa o binário oficial e o coloca em src-tauri/binaries com o target triple", async () => {
|
||||||
|
if (!hostTriple) return;
|
||||||
|
if (hostTriple !== SUPPORTED_HOST) {
|
||||||
|
console.log(`host triple ${hostTriple} fora da matriz de testes; pulando`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const binaryPath = join(repoRoot, "src-tauri", "binaries", `sherpa-onnx-offline-${hostTriple}.exe`);
|
||||||
|
|
||||||
|
if (existsSync(binaryPath)) {
|
||||||
|
rmSync(binaryPath, { force: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
execFileSync("node", [join(repoRoot, "scripts", "setup-sidecar.mjs")], {
|
||||||
|
stdio: "pipe",
|
||||||
|
env: { ...process.env, SHERPA_ONNX_VERSION: "v1.12.23" },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(existsSync(binaryPath)).toBe(true);
|
||||||
|
expect(statSync(binaryPath).size).toBeGreaterThan(1024 * 1024);
|
||||||
|
}, 180_000);
|
||||||
|
});
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -130,7 +130,13 @@ impl RecordingSession {
|
|||||||
.map_err(|_| AppError::Audio("thread de gravação encerrou com panic".to_string()))?;
|
.map_err(|_| AppError::Audio("thread de gravação encerrou com panic".to_string()))?;
|
||||||
|
|
||||||
self.status = RecordingStatus::Idle;
|
self.status = RecordingStatus::Idle;
|
||||||
result.map(|path| if path.as_os_str().is_empty() { expected_path } else { path })
|
result.map(|path| {
|
||||||
|
if path.as_os_str().is_empty() {
|
||||||
|
expected_path
|
||||||
|
} else {
|
||||||
|
path
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,7 +197,9 @@ fn run_capture_thread(
|
|||||||
}
|
}
|
||||||
.map_err(|error| AppError::Audio(error.to_string()))?;
|
.map_err(|error| AppError::Audio(error.to_string()))?;
|
||||||
|
|
||||||
stream.play().map_err(|error| AppError::Audio(error.to_string()))?;
|
stream
|
||||||
|
.play()
|
||||||
|
.map_err(|error| AppError::Audio(error.to_string()))?;
|
||||||
Ok((stream, writer))
|
Ok((stream, writer))
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|||||||
@@ -142,7 +142,8 @@ fn current_display_server() -> DisplayServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn copy_to_clipboard(text: &str) -> Result<String, String> {
|
fn copy_to_clipboard(text: &str) -> Result<String, String> {
|
||||||
let mut clipboard = Clipboard::new().map_err(|error| format!("Falha ao acessar clipboard: {error}"))?;
|
let mut clipboard =
|
||||||
|
Clipboard::new().map_err(|error| format!("Falha ao acessar clipboard: {error}"))?;
|
||||||
clipboard
|
clipboard
|
||||||
.set_text(text.to_string())
|
.set_text(text.to_string())
|
||||||
.map_err(|error| format!("Falha ao copiar texto: {error}"))?;
|
.map_err(|error| format!("Falha ao copiar texto: {error}"))?;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
pub mod app_state;
|
pub mod app_state;
|
||||||
pub mod audio;
|
pub mod audio;
|
||||||
|
pub mod diagnostics;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod injection;
|
pub mod injection;
|
||||||
pub mod diagnostics;
|
|
||||||
pub mod models;
|
pub mod models;
|
||||||
pub mod settings;
|
pub mod settings;
|
||||||
pub mod shortcuts;
|
pub mod shortcuts;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use tauri::State;
|
use tauri::State;
|
||||||
|
|
||||||
use crate::app_state::{AppSnapshot, ManagedAppState};
|
use crate::app_state::{AppSnapshot, ManagedAppState};
|
||||||
|
use crate::diagnostics::{audio_or_error, log_attempt, tail, TranscriptionAttempt};
|
||||||
use crate::injection::{FallbackInjector, InsertMethod, InsertRequest, InsertResult, TextInjector};
|
use crate::injection::{FallbackInjector, InsertMethod, InsertRequest, InsertResult, TextInjector};
|
||||||
use crate::models::{
|
use crate::models::{
|
||||||
default_model_path, download_default_model, model_statuses, ModelDownloadResult,
|
default_model_path, download_default_model, model_statuses, ModelDownloadResult,
|
||||||
@@ -8,7 +9,6 @@ use crate::models::{
|
|||||||
};
|
};
|
||||||
use crate::settings::AppSettings;
|
use crate::settings::AppSettings;
|
||||||
use crate::transcription::crispasr::{CrispAsrConfig, CrispAsrTranscriber};
|
use crate::transcription::crispasr::{CrispAsrConfig, CrispAsrTranscriber};
|
||||||
use crate::diagnostics::{audio_or_error, log_attempt, tail, TranscriptionAttempt};
|
|
||||||
use crate::transcription::{Transcriber, TranscriptionInput, TranscriptionOptions};
|
use crate::transcription::{Transcriber, TranscriptionInput, TranscriptionOptions};
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
|||||||
+9
-1
@@ -261,7 +261,15 @@ export function createTranscriptionApp(
|
|||||||
diagnostics.append(diagnosticsSummary, diagnosticsBody);
|
diagnostics.append(diagnosticsSummary, diagnosticsBody);
|
||||||
diagnostics.open = false;
|
diagnostics.open = false;
|
||||||
|
|
||||||
shell.append(topbar, editorLabel, editor, message, actions, modelPanel, diagnostics);
|
shell.append(
|
||||||
|
topbar,
|
||||||
|
editorLabel,
|
||||||
|
editor,
|
||||||
|
message,
|
||||||
|
actions,
|
||||||
|
modelPanel,
|
||||||
|
diagnostics,
|
||||||
|
);
|
||||||
mount.append(shell);
|
mount.append(shell);
|
||||||
|
|
||||||
const render = (next: AppSnapshot): void => {
|
const render = (next: AppSnapshot): void => {
|
||||||
|
|||||||
Reference in new issue
Block a user