diff --git a/docs/superpowers/specs/2026-06-11-sherpa-onnx-sidecar.md b/docs/superpowers/specs/2026-06-11-sherpa-onnx-sidecar.md new file mode 100644 index 0000000..0b2e11d --- /dev/null +++ b/docs/superpowers/specs/2026-06-11-sherpa-onnx-sidecar.md @@ -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-`), 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). diff --git a/scripts/setup-sidecar.mjs b/scripts/setup-sidecar.mjs new file mode 100644 index 0000000..dbc1ef9 --- /dev/null +++ b/scripts/setup-sidecar.mjs @@ -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-`. +// +// 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; +}); diff --git a/scripts/setup-sidecar.test.mjs b/scripts/setup-sidecar.test.mjs new file mode 100644 index 0000000..acdfa74 --- /dev/null +++ b/scripts/setup-sidecar.test.mjs @@ -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); +}); diff --git a/src-tauri/binaries/onnxruntime.dll b/src-tauri/binaries/onnxruntime.dll new file mode 100644 index 0000000..ea37949 Binary files /dev/null and b/src-tauri/binaries/onnxruntime.dll differ diff --git a/src-tauri/binaries/onnxruntime_providers_shared.dll b/src-tauri/binaries/onnxruntime_providers_shared.dll new file mode 100644 index 0000000..cc1e520 Binary files /dev/null and b/src-tauri/binaries/onnxruntime_providers_shared.dll differ diff --git a/src-tauri/binaries/sherpa-onnx-offline-x86_64-pc-windows-msvc.exe b/src-tauri/binaries/sherpa-onnx-offline-x86_64-pc-windows-msvc.exe new file mode 100644 index 0000000..155c1ec Binary files /dev/null and b/src-tauri/binaries/sherpa-onnx-offline-x86_64-pc-windows-msvc.exe differ diff --git a/src-tauri/src/audio/mod.rs b/src-tauri/src/audio/mod.rs index de08201..015802c 100644 --- a/src-tauri/src/audio/mod.rs +++ b/src-tauri/src/audio/mod.rs @@ -130,7 +130,13 @@ impl RecordingSession { .map_err(|_| AppError::Audio("thread de gravação encerrou com panic".to_string()))?; 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()))?; - stream.play().map_err(|error| AppError::Audio(error.to_string()))?; + stream + .play() + .map_err(|error| AppError::Audio(error.to_string()))?; Ok((stream, writer)) })(); diff --git a/src-tauri/src/injection/mod.rs b/src-tauri/src/injection/mod.rs index 875b770..70dc62b 100644 --- a/src-tauri/src/injection/mod.rs +++ b/src-tauri/src/injection/mod.rs @@ -142,7 +142,8 @@ fn current_display_server() -> DisplayServer { } fn copy_to_clipboard(text: &str) -> Result { - 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 .set_text(text.to_string()) .map_err(|error| format!("Falha ao copiar texto: {error}"))?; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index fb7a37e..d54c4af 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,8 +1,8 @@ pub mod app_state; pub mod audio; +pub mod diagnostics; pub mod error; pub mod injection; -pub mod diagnostics; pub mod models; pub mod settings; pub mod shortcuts; diff --git a/src-tauri/src/ui_commands.rs b/src-tauri/src/ui_commands.rs index 65db46c..37b667e 100644 --- a/src-tauri/src/ui_commands.rs +++ b/src-tauri/src/ui_commands.rs @@ -1,6 +1,7 @@ use tauri::State; 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::models::{ default_model_path, download_default_model, model_statuses, ModelDownloadResult, @@ -8,7 +9,6 @@ use crate::models::{ }; use crate::settings::AppSettings; use crate::transcription::crispasr::{CrispAsrConfig, CrispAsrTranscriber}; -use crate::diagnostics::{audio_or_error, log_attempt, tail, TranscriptionAttempt}; use crate::transcription::{Transcriber, TranscriptionInput, TranscriptionOptions}; #[tauri::command] @@ -279,4 +279,4 @@ mod tests { let _ = fs::remove_file(audio_path); } -} \ No newline at end of file +} diff --git a/src/app.ts b/src/app.ts index d3a3c3c..53815a2 100644 --- a/src/app.ts +++ b/src/app.ts @@ -261,7 +261,15 @@ export function createTranscriptionApp( diagnostics.append(diagnosticsSummary, diagnosticsBody); diagnostics.open = false; - shell.append(topbar, editorLabel, editor, message, actions, modelPanel, diagnostics); + shell.append( + topbar, + editorLabel, + editor, + message, + actions, + modelPanel, + diagnostics, + ); mount.append(shell); const render = (next: AppSnapshot): void => {