diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..ddd8dc8 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,56 @@ +name: Desktop Build + +on: + workflow_dispatch: + push: + branches: [main] + +jobs: + build: + strategy: + fail-fast: false + matrix: + include: + - platform: windows-latest + artifact-name: windows-bundles + - platform: ubuntu-22.04 + artifact-name: linux-bundles + + runs-on: ${{ matrix.platform }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Tauri Linux dependencies + if: matrix.platform == 'ubuntu-22.04' + run: | + sudo apt-get update + sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache Rust + uses: swatinem/rust-cache@v2 + with: + workspaces: ./src-tauri -> target + + - name: Install frontend dependencies + run: npm ci + + - name: Build Tauri bundles + run: npm run tauri:build + + - name: Upload bundles + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.artifact-name }} + path: src-tauri/target/release/bundle/**/* + if-no-files-found: error diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..2270341 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,48 @@ +name: CI + +on: + pull_request: + branches: [main] + push: + branches: [main] + +jobs: + quality: + runs-on: ubuntu-22.04 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Tauri Linux dependencies + run: | + sudo apt-get update + sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache Rust + uses: swatinem/rust-cache@v2 + with: + workspaces: ./src-tauri -> target + + - name: Install frontend dependencies + run: npm ci + + - name: Typecheck + run: npm run typecheck + + - name: Frontend tests + run: npm run test -- --run + + - name: Frontend build + run: npm run build + + - name: Rust tests + run: cargo test --manifest-path src-tauri/Cargo.toml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..5d899af --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,66 @@ +name: Release + +on: + push: + tags: + - 'v*.*.*' + workflow_dispatch: + inputs: + tag: + description: 'Tag de release, por exemplo v0.1.0' + required: true + type: string + +jobs: + release: + permissions: + contents: write + strategy: + fail-fast: false + matrix: + include: + - platform: windows-latest + args: '' + - platform: ubuntu-22.04 + args: '' + + runs-on: ${{ matrix.platform }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Tauri Linux dependencies + if: matrix.platform == 'ubuntu-22.04' + run: | + sudo apt-get update + sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache Rust + uses: swatinem/rust-cache@v2 + with: + workspaces: ./src-tauri -> target + + - name: Install frontend dependencies + run: npm ci + + - name: Publish Tauri release + uses: tauri-apps/tauri-action@v0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tagName: ${{ github.event.inputs.tag || github.ref_name }} + releaseName: Transcript ${{ github.event.inputs.tag || github.ref_name }} + releaseBody: 'Build automatizado do Transcript para Windows e Linux.' + releaseDraft: false + prerelease: false + args: ${{ matrix.args }} diff --git a/README.md b/README.md new file mode 100644 index 0000000..85c4a94 --- /dev/null +++ b/README.md @@ -0,0 +1,82 @@ +# Transcript Desktop + +Aplicativo desktop de transcrição para Windows e Linux, construído com Tauri + Rust + TypeScript. O objetivo é oferecer ditado/transcrição offline-first com revisão obrigatória em modal antes de inserir texto em outro aplicativo. + +## Estado atual + +Este repositório contém o MVP estrutural: + +- app Tauri 2 funcional; +- modal editável em TypeScript sem framework pesado; +- backend Rust modular; +- catálogo do modelo `cstr/parakeet-tdt-0.6b-v3-GGUF` em Q8; +- contratos para CrispASR sidecar, API OpenAI-compatible, gravação e inserção de texto; +- tray/system tray básico; +- CI, build e release via GitHub Actions. + +Ainda não implementa captura real de áudio, hotkey global, download real do modelo ou injeção nativa em outros aplicativos. Essas integrações já têm módulos dedicados para implementação incremental. + +## Arquitetura + +```text +src/ Frontend TypeScript/CSS +src-tauri/src/ + audio/ Estado de gravação MVP + transcription/ Contratos, CrispASR sidecar e API compatível com OpenAI + injection/ Contratos/fallback de inserção + models/ Catálogo do modelo offline + settings/ Configuração padrão + tray.rs Menu da bandeja do sistema + ui_commands.rs Comandos Tauri expostos ao frontend + app_state.rs Estado compartilhado mínimo +``` + +A regra de design é manter arquivos pequenos, responsabilidades claras e zero overengineering. O app começa com contratos e stubs explícitos para as partes nativas mais arriscadas. + +## Modelo offline principal + +- Repositório: `cstr/parakeet-tdt-0.6b-v3-GGUF` +- Arquivo: `parakeet-tdt-0.6b-v3-q8_0.gguf` +- Quantização: `Q8_0` +- Tamanho registrado no catálogo: `745121600` bytes +- Modelo base: NVIDIA Parakeet TDT 0.6B v3 +- Idioma: detecção automática; português incluído no conjunto suportado pelo modelo base. + +O modelo não deve ser embutido no instalador principal. A estratégia recomendada é instalar o app leve e baixar o GGUF pelo gerenciador de modelos. + +## Compatibilidade Windows/Linux + +- Windows: alvo principal para inserção automática futura via APIs Win32. +- Linux X11: alvo de automação completa quando possível. +- Linux Wayland: suporte parcial por limitação de segurança dos compositors. O fallback seguro é copiar o texto e orientar o usuário a colar manualmente. + +## Comandos locais + +```bash +npm install +npm run typecheck +npm run test -- --run +npm run build +cargo test --manifest-path src-tauri/Cargo.toml +npm run tauri:build +``` + +## CI/CD + +Workflows: + +- `.github/workflows/ci.yml`: valida PRs e pushes em `main`. +- `.github/workflows/build.yml`: gera bundles Windows/Linux em push para `main` e manualmente. +- `.github/workflows/release.yml`: publica GitHub Releases em tags `v*.*.*`. + +A lógica fica em comandos npm/cargo para facilitar migração futura para GitLab CI. + +## Fontes técnicas usadas + +- Tauri 2 commands: https://v2.tauri.app/develop/calling-rust +- Tauri 2 system tray: https://v2.tauri.app/learn/system-tray +- Tauri 2 sidecars: https://v2.tauri.app/develop/sidecar +- Tauri GitHub pipelines: https://v2.tauri.app/distribute/pipelines/github +- Tauri Linux prerequisites: https://v2.tauri.app/start/prerequisites +- NVIDIA Parakeet v3 model card: https://huggingface.co/nvidia/parakeet-tdt-0.6b-v3/blob/main/README.md +- cstr Parakeet GGUF: https://huggingface.co/cstr/parakeet-tdt-0.6b-v3-GGUF diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 41b8d61..82d3104 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -1,5 +1,4 @@ { - "$schema": "../gen/schemas/desktop-schema.json", "identifier": "default", "description": "Permissões padrão do aplicativo Transcript.", "windows": ["main"], diff --git a/src-tauri/src/app_state.rs b/src-tauri/src/app_state.rs index 225b504..40ed996 100644 --- a/src-tauri/src/app_state.rs +++ b/src-tauri/src/app_state.rs @@ -74,8 +74,10 @@ mod tests { #[test] fn default_snapshot_starts_idle() { let state = ManagedAppState::default(); - let snapshot = state.snapshot().expect("snapshot should work"); - + let snapshot = match state.snapshot() { + Ok(snapshot) => snapshot, + Err(error) => panic!("snapshot should work: {error}"), + }; assert_eq!(snapshot.recording_status, RecordingStatus::Idle); assert!(snapshot.transcript.text.is_empty()); assert_eq!(snapshot.models.len(), 1); diff --git a/src-tauri/src/audio/mod.rs b/src-tauri/src/audio/mod.rs index 31a5ac6..90fd48e 100644 --- a/src-tauri/src/audio/mod.rs +++ b/src-tauri/src/audio/mod.rs @@ -55,7 +55,7 @@ mod tests { let mut session = RecordingSession::default(); assert_eq!(session.stop(), Err(AppError::NoActiveRecording)); - session.start().expect("start should work"); + assert_eq!(session.start(), Ok(())); assert_eq!(session.stop(), Ok(1)); } } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index eb07266..4c8ddf3 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -30,5 +30,8 @@ pub fn run() { ui_commands::save_settings ]) .run(tauri::generate_context!()) - .expect("erro ao executar o aplicativo Tauri"); + .unwrap_or_else(|error| { + eprintln!("erro ao executar o aplicativo Tauri: {error}"); + std::process::exit(1); + }); } diff --git a/vite.config.ts b/vite.config.ts index 0cb5cd2..e624133 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,4 +1,4 @@ -import { defineConfig } from 'vite'; +import { defineConfig } from 'vitest/config'; export default defineConfig({ clearScreen: false,