Compare commits

...
Author SHA1 Message Date
evandrodevbr 06aaeeaf79 wip: snapshot das alterações não commitadas (2026-10-20)
Origem: D:/Documents/Github/transcript
Arquivos omitidos (segredos, bancos de dados, logs, >50 MB): 2
2026-10-20 00:50:17 -03:00
123 changed files with 20537 additions and 982 deletions

No files matched your search

+6
View File
@@ -0,0 +1,6 @@
{
"enabledMcpjsonServers": [
"obsidian"
],
"enableAllProjectMcpServers": true
}
+2
View File
@@ -21,6 +21,7 @@ src-tauri/gen/
.env.*
!.env.example
*.local
.mcp.json
# OS/editor
.DS_Store
@@ -32,3 +33,4 @@ Thumbs.db
# Superpowers local worktrees
.worktrees/
worktrees/
.opencode
+17
View File
@@ -0,0 +1,17 @@
## Design Context
### Users
Pessoas que escrevem no computador e precisam transformar fala em texto rápido: criadores, profissionais, estudantes e usuários que alternam entre apps de edição, chat, navegador e documentos. O contexto principal é desktop, com foco em velocidade, ditado por atalho global e baixa fricção.
### Brand Personality
Premium, silencioso e preciso. A interface deve parecer uma ferramenta de produtividade de alto nível: discreta enquanto ouve, confiável quando insere texto, clara quando precisa de revisão.
### Aesthetic Direction
Dark-first, overlay-first, inspirado em produtividade premium tipo Superhuman: superfícies profundas, tipografia compacta, brilho violeta controlado, creme quente como acento, nada genérico ou “AI dashboard”. O app deve parecer um instrumento de escrita, não um painel técnico.
### Design Principles
1. Overlay é o produto: a experiência principal é compacta, flutuante e focada em ação imediata.
2. Configurações devem ser explícitas: Whisper integrado vs Docker/API precisa ser impossível de confundir.
3. Visual premium sem peso: preferir CSS, SVG e microinterações nativas a bibliotecas pesadas.
4. Hierarquia acima de decoração: cada cor, borda e animação deve comunicar estado ou foco.
5. Segurança do texto: se inserção automática não for confiável, preservar e revelar revisão editável.
@@ -0,0 +1,83 @@
---
target: src/app.ts + src/styles.css
total_score: 23
p0_count: 0
p1_count: 4
timestamp: 2026-07-11T18-50-55Z
slug: src-app-ts
---
Method: degraded single-context (no sub-agent tool exposed; browser automation unavailable)
# Design critique — src/app.ts + src/styles.css
## Design Health Score
| # | Heuristic | Score | Key issue |
|---|-----------|-------|-----------|
| 1 | Visibility of System Status | 3 | Status exists, but visual priority competes with actions. |
| 2 | Match System / Real World | 2 | Settings copy mixes user language with API/model jargon. |
| 3 | User Control and Freedom | 2 | Modal has close/cancel, but no Escape handling found. |
| 4 | Consistency and Standards | 2 | Many button/control styles and decorative effects for one small tool. |
| 5 | Error Prevention | 2 | Validation exists; settings still exposes too many easy-to-misconfigure fields. |
| 6 | Recognition Rather Than Recall | 2 | Icon compact buttons are accessible, but visually require recognition/hover/title memory. |
| 7 | Flexibility and Efficiency | 3 | Hotkey and compact dictation flow are strong. |
| 8 | Aesthetic and Minimalist Design | 1 | Heavy gradients, glow, blur, shadows, and ambient decoration fight the minimal goal. |
| 9 | Error Recovery | 3 | Inline modal errors and focus recovery are good. |
| 10 | Help and Documentation | 1 | Little contextual help for Docker/API, insertion mode, or failures. |
| Total | | 23/40 | Acceptable, but visually overworked. |
## Anti-patterns verdict
LLM assessment: the interface has a recognizable AI-era dark glass look: violet ambient orbs, radial gradients, glow pills, blurred panels, rounded cards, and many icon buttons. The core product is a transcription utility; it should feel calm, direct, and task-first.
Deterministic scan: `detect.mjs --json src` returned `[]`. No bundled Impeccable detector issues.
Browser evidence: skipped; no browser automation package/MCP available in this session.
## What works
- Core workflow is small: record, edit, copy/insert/save/clear.
- Recent accessibility work helped: modal focus trap, inert, visible labels, reduced motion, inline errors.
- Dictation overlay/review is a good product idea and should stay, but quieter.
## Priority issues
### P1 — CSS height syntax is broken
`src/styles.css:52-53` has `height: 100dvh` without a semicolon before `min-height`. This can invalidate sizing and risks reviving the window-collapse bug.
Fix: make it two clean declarations.
### P1 — The visual system is too decorative for a utility
Metrics: `src/styles.css` has 1373 lines, 174 selectors, 18 gradients, 8 backdrop-filter references, 15 box-shadows, and ambient animated decoration. For a small desktop transcription tool, this is visual noise.
Fix: remove ambient orbs/grid, most radial gradients, most shadows, and glass blur. Use flat surfaces, one border, one accent.
### P1 — Primary screen hierarchy is crowded
The main screen has status, record, four compact action buttons, message, secondary actions, editor, diagnostics. Many elements have similar weight, so the primary job—record and review text—does not dominate.
Fix: keep one primary action, move rare actions into a small overflow/menu or footer row, make the editor the main object.
### P1 — Settings modal exposes implementation detail too early
Mode, endpoint, model, API key, hotkey, insertion, privacy are all in one modal. This feels like a developer config panel, not a minimal product UI.
Fix: split into three plain groups: Recording, Output, Advanced. Hide endpoint/model under Advanced unless Docker/API is chosen.
### P2 — Frontend is hard to reshape safely
`src/app.ts` is 1367 lines and `src/styles.css` is 1373 lines. Design changes require editing a large procedural DOM file and a large global stylesheet.
Fix: minimal split only: `Topbar`, `EditorPane`, `SettingsDialog`, `DictationOverlay`, plus `tokens.css`.
### P2 — Product copy should be calmer and less technical
Examples: `Docker / API`, `Endpoint`, `Modelo no servidor`, `api_key`, `Whisper integrado`. Good for developers, rough for normal users.
Fix: use user-facing labels first, technical details second: “Transcrição local”, “Servidor local”, “Chave da API”, “Modelo”.
## Persona red flags
- Alex, power user: wants speed; compact hotkey helps, but action clutter slows scanning.
- Jordan, first-timer: sees Docker/API/model/endpoint and may not know what is safe to touch.
- Sam, keyboard/screen-reader user: core fixes are good, but modal should support Escape and visible grouping should be simpler.
## Recommended direction
Make the app quiet: dark flat shell, one accent, editor-first layout, fewer controls visible, settings simplified. Do not add a design system framework or dependency.
+8
View File
@@ -0,0 +1,8 @@
{
"permissions": {
"allow": [
"Bash(rtk git:*)",
"mcp__plugin_context-mode_context-mode__ctx_search"
]
}
}
+27
View File
@@ -0,0 +1,27 @@
{
"hud": {
"mode": "footer",
"position": "top-right",
"shortcut": "ctrl+shift+h",
"switchShortcut": "ctrl+.",
"minimizeShortcut": "ctrl+h",
"autoCompactWhileStreaming": true,
"startupNotification": true,
"usageDisplay": "metered",
"contextIndicator": "icon",
"expandedWidth": 42,
"compactWidth": 26,
"minTerminalWidth": 90,
"margin": {
"top": 1,
"right": 1,
"bottom": 1
},
"visibility": {
"context": true,
"project": true,
"worktrees": true,
"mcps": true
}
}
}
+201 -67
View File
@@ -1,96 +1,230 @@
# Transcript Desktop
<p align="center">
<img src="assets/brand/logo.png" alt="Transcript logo" width="132" />
</p>
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.
<h1 align="center">Transcript</h1>
## Estado atual
<p align="center">
Um app desktop rápido para ditado local, transcrição e inserção automática de texto em qualquer lugar.
</p>
Este repositório contém um MVP nativo funcional:
<p align="center">
<img alt="Tauri" src="https://img.shields.io/badge/Tauri-2-111827?style=flat-square&logo=tauri" />
<img alt="Rust" src="https://img.shields.io/badge/Rust-backend-b7410e?style=flat-square&logo=rust" />
<img alt="TypeScript" src="https://img.shields.io/badge/TypeScript-frontend-3178c6?style=flat-square&logo=typescript&logoColor=white" />
<img alt="OpenAI compatible" src="https://img.shields.io/badge/API-OpenAI--compatible-10a37f?style=flat-square" />
<img alt="Windows" src="https://img.shields.io/badge/Windows-supported-0078d4?style=flat-square&logo=windows" />
</p>
- app Tauri 2 funcional;
- modal editável em TypeScript sem framework pesado;
- backend Rust modular;
- catálogo e download do modelo `cstr/parakeet-tdt-0.6b-v3-GGUF` em Q8;
- captura real de microfone em WAV via CPAL;
- execução do sidecar CrispASR/Parakeet;
- hotkey global `Ctrl+Shift+Space`;
- inserção híbrida por clipboard + paste automático no Windows/X11 quando disponível;
- fallback manual no Wayland;
- tray/system tray básico;
- CI, build e release via GitHub Actions.
---
## Por que este projeto existe?
Digitar tudo manualmente é lento. A ideia do **Transcript** nasceu de uma frustração bem prática: muitos fluxos de trabalho exigem escrever o tempo todo — mensagens, prompts, documentos, tickets, código, respostas — mas os recursos nativos de ditado nem sempre entregam velocidade, precisão ou flexibilidade suficientes.
A transcrição nativa do Windows, por exemplo, pode ser útil, mas ainda deixa a desejar quando você quer um fluxo realmente ágil: apertar um atalho global, falar naturalmente e ver o texto aparecer no campo em que você já estava digitando.
O **Transcript** resolve esse problema com um fluxo direto:
```text
atalho global → fala → transcrição rápida → texto inserido no app em foco
```
Ele foi desenhado para quem quer escrever com a voz sem depender de interfaces pesadas, janelas grandes ou cópia manual constante.
---
## O que o Transcript faz
O Transcript é um aplicativo desktop para Windows e Linux construído com **Tauri 2**, **Rust** e **TypeScript**. Ele grava sua voz, transcreve localmente com **whisper.cpp base multilingual** por padrão e insere o texto diretamente onde o cursor estava. APIs compatíveis com OpenAI continuam disponíveis como opção.
### Principais recursos
- **Hotkey global** — por padrão `Ctrl+Shift+Space`.
- **Ditado em qualquer aplicativo** — navegador, editor, chat, terminal, IDE ou campo de texto.
- **Overlay flutuante compacto** — uma barra inferior com waveform enquanto você fala.
- **Transcrição local embutida** — inclui `whisper.cpp` + `ggml-base.bin` multilingual, com suporte a PT-BR sem Docker/API.
- **API key opcional** — endpoints locais não precisam de chave; provedores externos podem usar chave salva com segurança.
- **Credential Manager** — a chave nunca é salva em texto claro.
- **Normalização com IA** — corrige erros comuns de transcrição sem mudar o significado.
- **Engenharia de prompt** — normaliza a entrada e gera um prompt estruturado, adaptado ao domínio e à jurisdição explicitamente informada.
- **Provedores de texto** — presets para OpenAI e Anthropic, além de endpoint customizado nos dois formatos.
- **Inserção automática** — copia para o clipboard e tenta acionar `Ctrl+V` no app em foco.
- **Fallback seguro** — se não conseguir inserir automaticamente, mantém o texto no clipboard.
- **UI dark glass/acrylic** — interface compacta, frameless, sempre no topo e focada em velocidade.
- **Health check antes de transcrever** — detecta API offline ou modelo inexistente antes de deixar o usuário esperando.
---
## Fluxo de uso
1. Posicione o cursor em qualquer campo de texto.
2. Pressione `Ctrl+Shift+Space`.
3. Fale naturalmente.
4. Pressione `Ctrl+Shift+Space` novamente.
5. O Transcript transcreve e tenta inserir o texto automaticamente.
Se a inserção automática não for possível, o texto fica no clipboard para você colar manualmente.
---
## Configuração recomendada
Por padrão, o app funciona localmente com Whisper Base multilingual embutido. Para usar Docker, Parakeet/Speaches ou um provedor remoto, configure uma API compatível com OpenAI:
```text
POST /v1/audio/transcriptions
```
Configuração local recomendada:
```text
Endpoint: http://localhost:8022/v1/audio/transcriptions
Modelo: alefiury/parakeet-tdt-0.6b-v3-ptBR-TAGARELA-onnx
Idioma: pt
api_key: deixe em branco se o servidor local não exigir autenticação
```
Também funciona com provedores remotos compatíveis com OpenAI, incluindo o endpoint oficial:
```text
https://api.openai.com/v1/audio/transcriptions
```
### Assistente de texto
Em **Configurações → Assistente de texto**, escolha OpenAI, Anthropic ou um endpoint customizado, informe o modelo e salve a chave. No editor, alterne entre **Normalizar** e **Criar prompt** e clique no botão de IA. O resultado substitui o texto do editor para poder ser revisado, copiado, salvo ou inserido.
Endpoints customizados aceitam os formatos OpenAI Chat Completions e Anthropic Messages. A chave é opcional para endpoints customizados locais sem autenticação.
---
## Privacidade e segurança
- Áudios são enviados apenas para o endpoint que você configurar.
- Servidores locais permitem manter o fluxo dentro da sua máquina/rede.
- `api_key` é armazenada no Credential Manager do sistema operacional.
- O app não grava chaves em arquivos `.env`, JSON ou texto claro.
- Textos só são enviados ao provedor quando o usuário aciona **Normalizar texto** ou **Criar prompt**.
- Cada chave do assistente fica no Credential Manager e só é devolvida à interface após o usuário clicar em **Mostrar**.
- A inserção de texto usa clipboard + paste automático; se falhar, o texto permanece no clipboard.
---
## Arquitetura
```text
src/ Frontend TypeScript/CSS
src/
app.ts Interface TypeScript sem framework pesado
styles.css UI dark glass/acrylic + overlay de ditado
src-tauri/src/
audio/ Captura de microfone e gravação WAV
transcription/ Contratos, CrispASR sidecar e API compatível com OpenAI
injection/ Clipboard, paste automático e fallback manual
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
audio/ Captura de microfone e escrita WAV via CPAL + hound
transcription/ Cliente OpenAI-compatible multipart/form-data
text_processing.rs Normalização e engenharia de prompt via OpenAI/Anthropic
injection/ Clipboard, Ctrl+V automático e fallback manual
secrets.rs Integração com Credential Manager via keyring
shortcuts.rs Hotkey global
ui_commands.rs Comandos Tauri e fluxo nativo de ditado
app_state.rs Estado compartilhado da aplicação
tray.rs System tray
assets/brand/
logo.png Logo oficial do projeto
logo-*.png Derivações em PNG
src-tauri/icons/
icon.ico Ícone oficial do app/instalador Windows
```
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
## Desenvolvimento local
- 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.
Pré-requisitos:
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.
- Node.js
- npm
- Rust stable
- Dependências do Tauri 2 para o seu sistema operacional
## Sidecar CrispASR
O app chama por padrão o binário `parakeet-main` com:
```bash
parakeet-main -m <modelo.gguf> -f <audio.wav> -t <threads>
```
Para o modelo v3, o projeto CrispASR recomenda compilar o target `parakeet-main` no branch `parakeet`. O app limita automaticamente o número de threads a no máximo 4 para manter compatibilidade com PCs simples.
Enquanto o binário não estiver no `PATH` ou empacotado como sidecar, a transcrição local retorna erro claro após a gravação.
## Compatibilidade Windows/Linux
- Windows: clipboard + tentativa de `Ctrl+V` via `SendKeys`.
- Linux X11: clipboard + tentativa de `Ctrl+V` via `xdotool` quando instalado.
- 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
Instalação:
```bash
npm install
```
Rodar em modo desenvolvimento:
```bash
npm run tauri:dev
```
Validação:
```bash
npm run typecheck
npm run test -- --run
npm run build
cargo test --manifest-path src-tauri/Cargo.toml
```
Build frontend:
```bash
npm run build
```
---
## Gerar instalador Windows
```bash
npm run tauri:build
```
## CI/CD
Artefatos gerados:
Workflows:
```text
src-tauri/target/release/bundle/nsis/Transcript_0.1.0_x64-setup.exe
src-tauri/target/release/bundle/msi/Transcript_0.1.0_x64_en-US.msi
```
- `.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*.*.*`.
Use o instalador NSIS para uso diário enquanto novas versões são desenvolvidas.
A lógica fica em comandos npm/cargo para facilitar migração futura para GitLab CI.
---
## Fontes técnicas usadas
## Estado atual
- 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
O projeto já possui:
- gravação real de microfone;
- transcrição local embutida com whisper.cpp base multilingual;
- transcrição opcional via API compatível com OpenAI;
- hotkey global;
- overlay compacto de ditado;
- health check de endpoint/modelo;
- normalização e engenharia de prompt com OpenAI, Anthropic ou endpoint customizado;
- inserção automática por clipboard + paste;
- fallback manual;
- app Windows sem terminal visível no build release;
- ícone oficial no instalador e no app.
---
## Roadmap
Ideias futuras:
- rename completo para uma marca final, como **TypeVoz**;
- seletor de modelos no app;
- presets para provedores locais/remotos;
- histórico de transcrições;
- restauração segura do clipboard anterior;
- waveform reativo ao volume real do microfone;
- empacotamento e release automatizado.
---
## Licença
Projeto em desenvolvimento. Defina a licença antes de publicar o repositório publicamente.
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+35
View File
@@ -0,0 +1,35 @@
# Multi-agent development pipeline
Goal: ship UI changes with high design quality, code quality, and the least code that works.
## Phases
1. **Scope framer — `product-framer`**
- Convert the request into a short fix ledger: issue, acceptance criteria, files, out-of-scope.
- Output: checklist only; no code.
2. **Design quality — `ui-art-director` + `ux-critic`**
- Validate hierarchy, density, typography, motion, and user flow.
- Cut changes that do not improve the user's experience.
3. **Minimal code preflight — `code-reviewer`**
- Find existing patterns and the shortest safe implementation path.
- Reject new dependencies and speculative abstractions.
4. **Implementation — `frontend-craftsperson`**
- Implement one focused batch at a time.
- Add the smallest tests that prove behavior.
- Reuse tokens and existing DOM helpers.
5. **Final 3-agent review**
- `visual-qa`: visual regressions and missed UX criteria.
- `accessibility-reviewer`: keyboard, focus, labels, motion, semantics.
- `code-reviewer`: correctness, maintainability, unnecessary code.
6. **Fix loop**
- Send only blocking review findings back to `frontend-craftsperson`.
- Rerun the same 3-agent final review once.
## Default rule
If two solutions work, choose the one with fewer files, fewer branches, and no dependency changes.
+11
View File
@@ -0,0 +1,11 @@
# MCP Obsidian
O projeto usa o servidor MCP HTTP `obsidian` no endpoint local:
```text
https://127.0.0.1:27124/mcp/
```
A configuração local fica em `.mcp.json`, que é ignorado pelo Git. Antes de iniciar o OpenClaude neste projeto, defina a variável de ambiente `OBSIDIAN_MCP_TOKEN` com o token do Obsidian.
Nunca commite o token nem o arquivo `.mcp.json`.
@@ -0,0 +1,46 @@
# Minimalist Front Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Make the Transcript desktop UI quieter, more minimal, and better organized without adding dependencies.
**Architecture:** Keep the current vanilla TypeScript DOM structure. Use the shortest safe path: CSS cleanup/overrides, small copy changes, and one keyboard behavior fix. No component split in this pass.
**Tech Stack:** Tauri, TypeScript, Vite, Vitest, CSS.
---
### Task 1: Regression tests
**Files:**
- Modify: `src/app.test.ts`
- [ ] Add tests for Escape closing settings and minimalist CSS baseline.
- [ ] Run `npm test -- --run src/app.test.ts --reporter=verbose`; expect new tests to fail before implementation.
### Task 2: Behavior and copy polish
**Files:**
- Modify: `src/app.ts`
- [ ] Add Escape handling to close the settings modal.
- [ ] Rename technical settings labels to calmer user-facing labels.
### Task 3: Minimal visual pass
**Files:**
- Modify: `src/styles.css`
- [ ] Fix the invalid `height: 100dvh` declaration.
- [ ] Hide ambient decoration.
- [ ] Flatten shell, topbar, buttons, editor, settings, and dictation overlay.
- [ ] Keep reduced motion and existing accessibility styles.
### Task 4: Verification
**Files:**
- Read-only verification.
- [ ] Run `npm test -- --run`.
- [ ] Run `npm run build`.
- [ ] Run `cargo check --manifest-path src-tauri/Cargo.toml` through VS Build Tools environment.
@@ -0,0 +1,209 @@
# Audio History, API Fallback, and Streaming Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Retain the last 10 recordings, export them through a native save dialog, fall back from API to local Whisper PT-BR, and insert streaming API text progressively.
**Architecture:** Add a small `audio_history` module that owns safe filename validation, metadata, retention, and export source resolution. Extend the existing OpenAI-compatible client to detect SSE/NDJSON by response `Content-Type`; the existing UI command pipeline owns partial rendering and global-hotkey insertion so the client remains transport-focused.
**Tech Stack:** Rust, Tauri 2, `tauri-plugin-dialog`, reqwest blocking client, TypeScript, Vitest, Cargo tests.
---
### Task 1: Persist a bounded, safe audio history
**Files:**
- Create: `src-tauri/src/audio_history.rs`
- Modify: `src-tauri/src/lib.rs`
- Modify: `src-tauri/src/app_state.rs`
- Modify: `src-tauri/src/ui_commands.rs`
- Test: `src-tauri/src/audio_history.rs`
- [ ] **Step 1: Write failing history tests**
```rust
#[test]
fn retain_keeps_the_ten_newest_wavs() { /* create 11 timestamped WAV files; assert 10 remain */ }
#[test]
fn audio_id_cannot_escape_recordings_dir() {
assert!(resolve_audio_path(Path::new("recordings"), "../secret.wav").is_err());
}
```
- [ ] **Step 2: Run the focused test**
Run: `cargo test --manifest-path src-tauri/Cargo.toml audio_history::tests --lib`
Expected: compilation failure because `audio_history` does not exist.
- [ ] **Step 3: Implement a minimal history module**
```rust
pub const AUDIO_HISTORY_LIMIT: usize = 10;
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AudioHistoryItem { pub id: String, pub size_bytes: u64, pub modified_at_ms: u64 }
pub fn retain_audio_history(dir: &Path) -> AppResult<Vec<AudioHistoryItem>> { /* list WAVs, sort newest-first, remove beyond 10 */ }
pub fn resolve_audio_path(dir: &Path, id: &str) -> AppResult<PathBuf> { /* filename-only + canonicalized containment */ }
```
- [ ] **Step 4: Call retention after both normal and hotkey transcription flows**
Replace `cleanup_audio_if_needed` calls with `retain_audio_history(&state.recordings_dir)` so captured WAVs are never deleted simply because `keep_audio` is false. Add `audio_history` to `AppSnapshot` from `ManagedAppState::snapshot`.
- [ ] **Step 5: Run the focused test**
Run: `cargo test --manifest-path src-tauri/Cargo.toml audio_history::tests --lib`
Expected: PASS.
### Task 2: Export recordings safely through a native dialog
**Files:**
- Modify: `src-tauri/Cargo.toml`
- Modify: `src-tauri/src/lib.rs`
- Modify: `src-tauri/src/ui_commands.rs`
- Test: `src-tauri/src/audio_history.rs`
- [ ] **Step 1: Add a failing source-resolution test**
```rust
#[test]
fn resolve_audio_path_accepts_a_listed_wav() { /* assert a known WAV resolves inside recordings */ }
```
- [ ] **Step 2: Add the documented Tauri dialog plugin**
```toml
tauri-plugin-dialog = "2"
```
```rust
.plugin(tauri_plugin_dialog::init())
```
Use `DialogExt::dialog().file().add_filter("Áudio WAV", &["wav"]).blocking_save_file()` and `std::fs::copy`; return `false` when the user cancels.
- [ ] **Step 3: Register an explicit command**
```rust
#[tauri::command]
pub(crate) fn download_audio(id: String, app: tauri::AppHandle, state: State<'_, ManagedAppState>) -> Result<bool, String>
```
Resolve `id` through `audio_history::resolve_audio_path`; never accept a caller-provided file path.
- [ ] **Step 4: Run Rust tests**
Run: `cargo test --manifest-path src-tauri/Cargo.toml --lib`
Expected: PASS.
### Task 3: API-to-Whisper PT-BR fallback
**Files:**
- Modify: `src-tauri/src/ui_commands.rs`
- Test: `src-tauri/src/ui_commands.rs`
- [ ] **Step 1: Write a failing pure routing test**
```rust
#[test]
fn api_failure_without_partial_uses_local_fallback() {
assert!(should_fallback_to_whisper(TranscriptionMode::ApiOnly, false));
}
#[test]
fn partial_stream_does_not_fallback_and_duplicate_text() {
assert!(!should_fallback_to_whisper(TranscriptionMode::ApiOnly, true));
}
```
- [ ] **Step 2: Implement the smallest routing rule**
```rust
fn should_fallback_to_whisper(mode: TranscriptionMode, partial_received: bool) -> bool {
matches!(mode, TranscriptionMode::ApiOnly) && !partial_received
}
```
In `run_configured_transcription`, use a cloned settings value with `transcription.language = Some("pt".into())` to invoke existing Whisper after API failure. Preserve local-first’s current local-then-API behavior.
- [ ] **Step 3: Run focused tests**
Run: `cargo test --manifest-path src-tauri/Cargo.toml ui_commands::tests --lib`
Expected: PASS.
### Task 4: Stream response handling and immediate insertion
**Files:**
- Modify: `src-tauri/src/transcription/openai.rs`
- Modify: `src-tauri/src/ui_commands.rs`
- Test: `src-tauri/src/transcription/openai.rs`
- [ ] **Step 1: Write failing parser tests**
```rust
#[test]
fn streaming_content_type_is_detected() { /* text/event-stream and x-ndjson true */ }
#[test]
fn cumulative_stream_text_becomes_only_the_new_suffix() {
assert_eq!(stream_delta("olá", "olá mundo"), " mundo");
}
```
- [ ] **Step 2: Implement response-stream transport**
Set `Accept: text/event-stream, application/x-ndjson, application/json`. For SSE/NDJSON headers, read lines from the blocking response, ignore `[DONE]`, extract `delta`, `text`, `transcript`, `result`, `output`, `data`, and OpenAI `choices[0].delta.content`, then invoke an optional partial callback. Keep JSON/plain-text behavior unchanged.
- [ ] **Step 3: Wire partial callbacks into UI commands**
- Main-window transcriptions append partials to `TranscriptDraft.text` and emit snapshots.
- Global-hotkey transcriptions hide the non-focusable island on first partial and use existing `FallbackInjector` with `ClipboardPaste` for each new suffix.
- Only perform the final full-text insertion when no partial was inserted.
- Suppress API-to-Whisper fallback after a partial to prevent duplicate external text.
- [ ] **Step 4: Run focused tests**
Run: `cargo test --manifest-path src-tauri/Cargo.toml transcription::openai::tests ui_commands::tests --lib`
Expected: PASS.
### Task 5: Minimal audio history UI
**Files:**
- Modify: `src/app.ts`
- Modify: `src/app.test.ts`
- Modify: `src/styles.css`
- [ ] **Step 1: Write failing frontend tests**
```ts
it("shows the latest audio and requests its download", async () => {
// Fake snapshot has audioHistory[0]; click the button; expect download_audio with its id.
});
```
- [ ] **Step 2: Implement a compact details list**
Add `audioHistory` to frontend `AppSnapshot`. Render one collapsed `<details>` section below diagnostics: “Áudios recentes”, a primary “Baixar último áudio” action, and up to 10 safe metadata rows. Show a useful empty state. Call native `download_audio` by item ID.
- [ ] **Step 3: Add restrained CSS**
Use existing tokens and compact buttons. Do not introduce a new modal, gradients, or a card grid. Preserve visible focus styles and reduced-motion behavior.
- [ ] **Step 4: Run focused tests**
Run: `npm test -- --run src/app.test.ts --reporter=verbose`
Expected: PASS.
### Task 6: Full verification and release artifact
**Files:**
- Modify only release metadata if a new artifact version is required.
- [ ] Run `cargo fmt --manifest-path src-tauri/Cargo.toml -- --check`.
- [ ] Run `cargo test --manifest-path src-tauri/Cargo.toml --lib`.
- [ ] Run `npm test -- --run && npm run build`.
- [ ] Run `cargo check --manifest-path src-tauri/Cargo.toml` inside the VS Build Tools environment.
- [ ] Run `npm run tauri:build` inside the VS Build Tools environment and verify installer timestamps.
@@ -0,0 +1,44 @@
# Áudio persistente, fallback local e streaming — Design
## Objetivo
Guardar os 10 áudios mais recentes enviados à transcrição, permitir salvar qualquer um deles em um local escolhido pelo usuário, usar Whisper local em PT-BR quando a API falhar e inserir texto progressivamente quando a API responder em streaming.
## Escopo e decisões
### Histórico de áudio
- Cada gravação finalizada permanece no diretório de dados do aplicativo como WAV.
- O histórico é limitado a 10 arquivos, ordenados por data de modificação decrescente. Ao finalizar uma gravação, arquivos mais antigos são removidos.
- Cada item expõe somente `id` (nome de arquivo validado), nome, tamanho e data. Caminhos absolutos nunca são enviados ao frontend.
- O frontend mostra um `<details>` compacto de “Áudios recentes”, com “Baixar último áudio” e ações de salvar por item.
- O salvamento usa o diálogo nativo de arquivo e copia o WAV para a localização escolhida. Cancelar o diálogo não é erro.
### Fallback API → Whisper
- O modo de servidor tenta a API configurada primeiro.
- Qualquer falha antes de texto parcial aciona Whisper embarcado com idioma fixo `pt`.
- Se uma resposta streaming já tiver emitido texto parcial, não há fallback automático: repetir o texto poderia duplicar o que já foi inserido no campo ativo. O erro é mostrado para revisão.
- O modo local continua usando Whisper primeiro e API como fallback, preservando o comportamento existente.
### Streaming de resposta
- O cliente envia `Accept: text/event-stream, application/x-ndjson, application/json`.
- A API é detectada como streaming exclusivamente pelo `Content-Type` da resposta: `text/event-stream` ou `application/x-ndjson`.
- Eventos SSE/NDJSON aceitam `delta`, `text`, `transcript`, `result`, `output`, `data` e `choices[0].delta.content`; payloads cumulativos são convertidos em somente o sufixo novo.
- Na janela principal, cada fragmento atualiza o editor de transcrição.
- No atalho global, o primeiro fragmento esconde a barra não focável e tenta colar imediatamente no campo externo ativo; a transcrição final não é colada uma segunda vez.
- Respostas JSON normais mantêm o fluxo atual: texto completo somente após concluir.
## Segurança e limites
- IDs de áudio são nomes simples de WAV; separadores de diretório, extensões diferentes e caminhos fora do diretório de gravações são rejeitados.
- O backend nunca recebe um destino de arquivo escolhido pelo frontend; o diálogo nativo escolhe o destino e o backend executa a cópia local.
- Áudio é dado potencialmente sensível. A retenção fixa em 10 arquivos é deliberada e visível na UI; não há telemetria nem upload adicional.
## Testes
- Histórico: limita a 10, remove o mais antigo, rejeita IDs inseguros.
- Fallback: API-only usa Whisper PT-BR quando não houve parcial; streaming parcial não dispara fallback.
- Streaming: detecta Content-Type, extrai deltas/cumulativos e evita duplicação.
- Frontend: renderiza histórico e pede download do item selecionado.
+7
View File
@@ -0,0 +1,7 @@
# Findings
- Available agents differ from memory: accessibility-reviewer, code-reviewer, design-squad-lead, design-system-architect, frontend-craftsperson, product-framer, test-automator, ui-art-director, ux-critic, visual-qa.
- Design-squad-lead recommended six-phase pipeline with final visual/a11y/code review.
- Accessibility-reviewer flagged modal focus/inert, dictation textarea label, compact button names, field-level errors, reduced motion, select focus.
- Test-automator proposed Vitest DOM tests for modal focus/inert, dictation textarea label, compact button aria labels, API defaults.
- Current speech container endpoint is `http://127.0.0.1:8000/v1/stt/transcribe`, model `nvidia/parakeet-tdt-0.6b-v3`.
+5
View File
@@ -4,6 +4,11 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Transcript</title>
<link rel="icon" href="/favicon.ico" />
<meta
name="description"
content="Transcript é um app desktop rápido para ditado, transcrição e inserção automática de texto via voz."
/>
</head>
<body>
<main id="app"></main>
+2104 -4
View File
File diff suppressed because it is too large. Load diff
+4 -2
View File
@@ -1,6 +1,6 @@
{
"name": "transcript-desktop",
"version": "0.1.0",
"version": "0.3.1",
"private": true,
"type": "module",
"scripts": {
@@ -12,7 +12,9 @@
"tauri:build": "tauri build"
},
"dependencies": {
"@tauri-apps/api": "^2.9.0"
"@hypabolic/pi-hypa": "^0.1.6",
"@tauri-apps/api": "^2.9.0",
"lucide-static": "^1.18.0"
},
"devDependencies": {
"@tauri-apps/cli": "^2.9.0",
+2736
View File
File diff suppressed because it is too large. Load diff
+8
View File
@@ -0,0 +1,8 @@
allowBuilds:
'@google/genai': true
'@hypabolic/hypa-win32-x64': true
'@hypabolic/pi-hypa': true
esbuild: true
protobufjs: true
overrides:
esbuild@>=0.27.3 <0.28.1: '>=0.28.1'
+20
View File
@@ -0,0 +1,20 @@
# Progress
- Loaded relevant skills.
- Created `docs/development-pipeline.md`.
- Dispatched design/test/accessibility subagents; incorporated their recommendations.
- Added failing tests first for endpoint defaults, modal focus trap, inert, labels, aria labels, inline errors, reduced motion, and api_key empty-state handling.
- Implemented UI/accessibility fixes in `src/app.ts` and `src/styles.css`.
- Updated Rust-side default API endpoint/model in settings and transcription client.
- Final review loop passed: visual QA, accessibility review, and implementation code review found no blockers introduced by this work.
- Verified `npm test -- --run` and `npm run build` pass.
- Earlier `cargo check` was blocked by missing Cargo; Rust/Cargo + Visual Studio Build Tools are now installed.
- Installed Rust/Cargo + Visual Studio Build Tools; `cargo check --manifest-path src-tauri/Cargo.toml` now passes.
- Built Transcript 0.3.0 artifacts: EXE, NSIS installer, MSI.
- Ran Impeccable critique for the front; score was 23/40 and snapshot saved under `.impeccable/critique/`.
- Implemented the approved minimalist front pass: fixed CSS height syntax, hid ambient decoration, flattened visual treatment, calmed settings copy, and added regression tests.
- Final verification after minimalist pass: `npm test -- --run` passed 13 tests, `npm run build` passed, `cargo check --manifest-path src-tauri/Cargo.toml` passed, and `npm run tauri:build` rebuilt 0.3.0 artifacts.
- Added speech-api compatibility in `src-tauri/src/transcription/openai.rs`: known `/v1/stt/transcribe` endpoints health-check `/readyz`, FastAPI nested errors render as `CODE: message`, and generic APIs without `/models` retain the existing 404-is-reachable behavior.
- TDD verification: new speech-api tests went RED then GREEN; Rust library suite passed 35 tests, frontend suite passed 13 tests, frontend build and Cargo check passed.
- Verified the live local API using `GET /readyz` and multipart `POST /v1/stt/transcribe` with the app's `file`, `model`, and `language` fields; it returned a transcript successfully.
- Bumped patch version to 0.3.1 and built updated EXE/NSIS/MSI artifacts.
Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

+212 -4
View File
@@ -85,7 +85,7 @@ dependencies = [
"objc2-foundation",
"parking_lot",
"percent-encoding",
"windows-sys 0.59.0",
"windows-sys 0.60.2",
"x11rb",
]
@@ -819,6 +819,16 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "dbus-secret-service"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "708b509edf7889e53d7efb0ffadd994cc6c2345ccb62f55cfd6b0682165e4fa6"
dependencies = [
"dbus",
"zeroize",
]
[[package]]
name = "deranged"
version = "0.5.8"
@@ -2137,6 +2147,20 @@ dependencies = [
"unicode-segmentation",
]
[[package]]
name = "keyring"
version = "3.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c"
dependencies = [
"byteorder",
"dbus-secret-service",
"linux-keyutils",
"log",
"windows-sys 0.60.2",
"zeroize",
]
[[package]]
name = "leb128fmt"
version = "0.1.0"
@@ -2211,6 +2235,16 @@ dependencies = [
"libc",
]
[[package]]
name = "linux-keyutils"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83270a18e9f90d0707c41e9f35efada77b64c0e6f3f1810e71c8368a864d5590"
dependencies = [
"bitflags 2.13.0",
"libc",
]
[[package]]
name = "linux-raw-sys"
version = "0.12.1"
@@ -2285,6 +2319,16 @@ version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "mime_guess"
version = "2.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
dependencies = [
"mime",
"unicase",
]
[[package]]
name = "minimal-lexical"
version = "0.2.1"
@@ -2584,6 +2628,7 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
dependencies = [
"bitflags 2.13.0",
"block2",
"libc",
"objc2",
"objc2-core-foundation",
]
@@ -3083,7 +3128,7 @@ dependencies = [
"once_cell",
"socket2",
"tracing",
"windows-sys 0.59.0",
"windows-sys 0.60.2",
]
[[package]]
@@ -3230,6 +3275,7 @@ dependencies = [
"hyper-util",
"js-sys",
"log",
"mime_guess",
"percent-encoding",
"pin-project-lite",
"quinn",
@@ -3285,6 +3331,30 @@ dependencies = [
"web-sys",
]
[[package]]
name = "rfd"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672"
dependencies = [
"block2",
"dispatch2",
"glib-sys",
"gobject-sys",
"gtk-sys",
"js-sys",
"log",
"objc2",
"objc2-app-kit",
"objc2-core-foundation",
"objc2-foundation",
"raw-window-handle",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"windows-sys 0.60.2",
]
[[package]]
name = "ring"
version = "0.17.14"
@@ -4048,6 +4118,48 @@ dependencies = [
"walkdir",
]
[[package]]
name = "tauri-plugin-dialog"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65981abb771e74e571a38196c3baa11c459379164791eba0e67abc1a5fac9884"
dependencies = [
"log",
"raw-window-handle",
"rfd",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"tauri-plugin-fs",
"thiserror 2.0.18",
"url",
]
[[package]]
name = "tauri-plugin-fs"
version = "2.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7ecc274121aca0c036a2b42d1cbe83d368d348f54e0bb8a735c2b1548e8f371"
dependencies = [
"anyhow",
"dunce",
"glob",
"log",
"objc2-foundation",
"percent-encoding",
"schemars 0.8.22",
"serde",
"serde_json",
"serde_repr",
"tauri",
"tauri-plugin",
"tauri-utils",
"thiserror 2.0.18",
"toml 1.1.2+spec-1.1.0",
"url",
]
[[package]]
name = "tauri-plugin-global-shortcut"
version = "2.3.2"
@@ -4553,16 +4665,18 @@ dependencies = [
[[package]]
name = "transcript-desktop"
version = "0.1.0"
version = "0.3.1"
dependencies = [
"arboard",
"cpal",
"hound",
"keyring",
"reqwest 0.12.28",
"serde",
"serde_json",
"tauri",
"tauri-build",
"tauri-plugin-dialog",
"tauri-plugin-global-shortcut",
"tauri-plugin-opener",
"thiserror 2.0.18",
@@ -4660,6 +4774,12 @@ dependencies = [
"unic-common",
]
[[package]]
name = "unicase"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
[[package]]
name = "unicode-ident"
version = "1.0.24"
@@ -5278,6 +5398,15 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
dependencies = [
"windows-targets 0.53.5",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
@@ -5311,13 +5440,30 @@ dependencies = [
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
"windows_i686_gnullvm",
"windows_i686_gnullvm 0.52.6",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6",
]
[[package]]
name = "windows-targets"
version = "0.53.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
dependencies = [
"windows-link 0.2.1",
"windows_aarch64_gnullvm 0.53.1",
"windows_aarch64_msvc 0.53.1",
"windows_i686_gnu 0.53.1",
"windows_i686_gnullvm 0.53.1",
"windows_i686_msvc 0.53.1",
"windows_x86_64_gnu 0.53.1",
"windows_x86_64_gnullvm 0.53.1",
"windows_x86_64_msvc 0.53.1",
]
[[package]]
name = "windows-threading"
version = "0.1.0"
@@ -5348,6 +5494,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.2"
@@ -5360,6 +5512,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_aarch64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
[[package]]
name = "windows_i686_gnu"
version = "0.42.2"
@@ -5372,12 +5530,24 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
[[package]]
name = "windows_i686_msvc"
version = "0.42.2"
@@ -5390,6 +5560,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_i686_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.2"
@@ -5402,6 +5578,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.2"
@@ -5414,6 +5596,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.2"
@@ -5426,6 +5614,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "windows_x86_64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
[[package]]
name = "winnow"
version = "0.5.40"
@@ -5778,6 +5972,20 @@ name = "zeroize"
version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
dependencies = [
"zeroize_derive",
]
[[package]]
name = "zeroize_derive"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "zerotrie"
+7 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "transcript-desktop"
version = "0.1.0"
version = "0.3.1"
description = "Offline-first desktop transcription app for Windows and Linux"
authors = ["Transcript contributors"]
edition = "2021"
@@ -18,11 +18,17 @@ serde_json = "1"
tauri = { version = "2", features = ["tray-icon", "image-png"] }
tauri-plugin-opener = "2"
tauri-plugin-global-shortcut = "2"
keyring = { version = "3", default-features = false, features = [
"windows-native",
"linux-native-sync-persistent",
] }
reqwest = { version = "0.12", default-features = false, features = [
"blocking",
"rustls-tls",
"multipart",
] }
cpal = "0.15.3"
hound = "3.5"
arboard = "3.6"
thiserror = "2"
tauri-plugin-dialog = "2"
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,162 @@
#pragma once
/**
* This is a simple alternative cross-platform implementation of getopt, which
* is used to parse argument strings submitted to the executable (argc and argv
* which are received in the main function).
*/
#ifndef CAG_LIBRARY_H
#define CAG_LIBRARY_H
#include <stdbool.h>
#include <stddef.h>
#include <stdio.h>
#if defined(_WIN32) || defined(__CYGWIN__)
#define CAG_EXPORT __declspec(dllexport)
#define CAG_IMPORT __declspec(dllimport)
#elif __GNUC__ >= 4
#define CAG_EXPORT __attribute__((visibility("default")))
#define CAG_IMPORT __attribute__((visibility("default")))
#else
#define CAG_EXPORT
#define CAG_IMPORT
#endif
#if defined(CAG_SHARED)
#if defined(CAG_EXPORTS)
#define CAG_PUBLIC CAG_EXPORT
#else
#define CAG_PUBLIC CAG_IMPORT
#endif
#else
#define CAG_PUBLIC
#endif
#ifdef __cplusplus
extern "C" {
#endif
/**
* An option is used to describe a flag/argument option submitted when the
* program is run.
*/
typedef struct cag_option
{
const char identifier;
const char *access_letters;
const char *access_name;
const char *value_name;
const char *description;
} cag_option;
/**
* A context is used to iterate over all options provided. It stores the parsing
* state.
*/
typedef struct cag_option_context
{
const struct cag_option *options;
size_t option_count;
int argc;
char **argv;
int index;
int inner_index;
bool forced_end;
char identifier;
char *value;
} cag_option_context;
/**
* This is just a small macro which calculates the size of an array.
*/
#define CAG_ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))
/**
* @brief Prints all options to the terminal.
*
* This function prints all options to the terminal. This can be used to
* generate the output for a "--help" option.
*
* @param options The options which will be printed.
* @param option_count The option count which will be printed.
* @param destination The destination where the output will be printed.
*/
CAG_PUBLIC void cag_option_print(const cag_option *options, size_t option_count,
FILE *destination);
/**
* @brief Prepare argument options context for parsing.
*
* This function prepares the context for iteration and initializes the context
* with the supplied options and arguments. After the context has been prepared,
* it can be used to fetch arguments from it.
*
* @param context The context which will be initialized.
* @param options The registered options which are available for the program.
* @param option_count The amount of options which are available for the
* program.
* @param argc The amount of arguments the user supplied in the main function.
* @param argv A pointer to the arguments of the main function.
*/
CAG_PUBLIC void cag_option_prepare(cag_option_context *context,
const cag_option *options, size_t option_count, int argc, char **argv);
/**
* @brief Fetches an option from the argument list.
*
* This function fetches a single option from the argument list. The context
* will be moved to that item. Information can be extracted from the context
* after the item has been fetched.
* The arguments will be re-ordered, which means that non-option arguments will
* be moved to the end of the argument list. After all options have been
* fetched, all non-option arguments will be positioned after the index of
* the context.
*
* @param context The context from which we will fetch the option.
* @return Returns true if there was another option or false if the end is
* reached.
*/
CAG_PUBLIC bool cag_option_fetch(cag_option_context *context);
/**
* @brief Gets the identifier of the option.
*
* This function gets the identifier of the option, which should be unique to
* this option and can be used to determine what kind of option this is.
*
* @param context The context from which the option was fetched.
* @return Returns the identifier of the option.
*/
CAG_PUBLIC char cag_option_get(const cag_option_context *context);
/**
* @brief Gets the value from the option.
*
* This function gets the value from the option, if any. If the option does not
* contain a value, this function will return NULL.
*
* @param context The context from which the option was fetched.
* @return Returns a pointer to the value or NULL if there is no value.
*/
CAG_PUBLIC const char *cag_option_get_value(const cag_option_context *context);
/**
* @brief Gets the current index of the context.
*
* This function gets the index within the argv arguments of the context. The
* context always points to the next item which it will inspect. This is
* particularly useful to inspect the original argument array, or to get
* non-option arguments after option fetching has finished.
*
* @param context The context from which the option was fetched.
* @return Returns the current index of the context.
*/
CAG_PUBLIC int cag_option_get_index(const cag_option_context *context);
#ifdef __cplusplus
} // extern "C"
#endif
#endif
@@ -0,0 +1,841 @@
// sherpa-onnx/c-api/cxx-api.h
//
// Copyright (c) 2024 Xiaomi Corporation
// C++ Wrapper of the C API for sherpa-onnx
#ifndef SHERPA_ONNX_C_API_CXX_API_H_
#define SHERPA_ONNX_C_API_CXX_API_H_
#include <memory>
#include <string>
#include <vector>
#include "sherpa-onnx/c-api/c-api.h"
namespace sherpa_onnx::cxx {
// ============================================================================
// Streaming ASR
// ============================================================================
struct OnlineTransducerModelConfig {
std::string encoder;
std::string decoder;
std::string joiner;
};
struct OnlineParaformerModelConfig {
std::string encoder;
std::string decoder;
};
struct OnlineZipformer2CtcModelConfig {
std::string model;
};
struct OnlineNemoCtcModelConfig {
std::string model;
};
struct OnlineToneCtcModelConfig {
std::string model;
};
struct OnlineModelConfig {
OnlineTransducerModelConfig transducer;
OnlineParaformerModelConfig paraformer;
OnlineZipformer2CtcModelConfig zipformer2_ctc;
OnlineNemoCtcModelConfig nemo_ctc;
OnlineToneCtcModelConfig t_one_ctc;
std::string tokens;
int32_t num_threads = 1;
std::string provider = "cpu";
bool debug = false;
std::string model_type;
std::string modeling_unit = "cjkchar";
std::string bpe_vocab;
std::string tokens_buf;
};
struct FeatureConfig {
int32_t sample_rate = 16000;
int32_t feature_dim = 80;
};
struct OnlineCtcFstDecoderConfig {
std::string graph;
int32_t max_active = 3000;
};
struct HomophoneReplacerConfig {
std::string dict_dir; // unused
std::string lexicon;
std::string rule_fsts;
};
struct OnlineRecognizerConfig {
FeatureConfig feat_config;
OnlineModelConfig model_config;
std::string decoding_method = "greedy_search";
int32_t max_active_paths = 4;
bool enable_endpoint = false;
float rule1_min_trailing_silence = 2.4;
float rule2_min_trailing_silence = 1.2;
float rule3_min_utterance_length = 20;
std::string hotwords_file;
float hotwords_score = 1.5;
OnlineCtcFstDecoderConfig ctc_fst_decoder_config;
std::string rule_fsts;
std::string rule_fars;
float blank_penalty = 0;
std::string hotwords_buf;
HomophoneReplacerConfig hr;
};
struct OnlineRecognizerResult {
std::string text;
std::vector<std::string> tokens;
std::vector<float> timestamps;
std::string json;
};
struct Wave {
std::vector<float> samples;
int32_t sample_rate;
};
SHERPA_ONNX_API Wave ReadWave(const std::string &filename);
// Return true on success;
// Return false on failure
SHERPA_ONNX_API bool WriteWave(const std::string &filename, const Wave &wave);
template <typename Derived, typename T>
class SHERPA_ONNX_API MoveOnly {
public:
MoveOnly() = default;
explicit MoveOnly(const T *p) : p_(p) {}
~MoveOnly() { Destroy(); }
MoveOnly(const MoveOnly &) = delete;
MoveOnly &operator=(const MoveOnly &) = delete;
MoveOnly(MoveOnly &&other) : p_(other.Release()) {}
MoveOnly &operator=(MoveOnly &&other) {
if (&other == this) {
return *this;
}
Destroy();
p_ = other.Release();
return *this;
}
const T *Get() const { return p_; }
const T *Release() {
const T *p = p_;
p_ = nullptr;
return p;
}
private:
void Destroy() {
if (p_ == nullptr) {
return;
}
static_cast<Derived *>(this)->Destroy(p_);
p_ = nullptr;
}
protected:
const T *p_ = nullptr;
};
class SHERPA_ONNX_API OnlineStream
: public MoveOnly<OnlineStream, SherpaOnnxOnlineStream> {
public:
explicit OnlineStream(const SherpaOnnxOnlineStream *p);
void AcceptWaveform(int32_t sample_rate, const float *samples,
int32_t n) const;
void InputFinished() const;
void Destroy(const SherpaOnnxOnlineStream *p) const;
};
class SHERPA_ONNX_API OnlineRecognizer
: public MoveOnly<OnlineRecognizer, SherpaOnnxOnlineRecognizer> {
public:
static OnlineRecognizer Create(const OnlineRecognizerConfig &config);
void Destroy(const SherpaOnnxOnlineRecognizer *p) const;
OnlineStream CreateStream() const;
OnlineStream CreateStream(const std::string &hotwords) const;
bool IsReady(const OnlineStream *s) const;
void Decode(const OnlineStream *s) const;
void Decode(const OnlineStream *ss, int32_t n) const;
OnlineRecognizerResult GetResult(const OnlineStream *s) const;
void Reset(const OnlineStream *s) const;
bool IsEndpoint(const OnlineStream *s) const;
private:
explicit OnlineRecognizer(const SherpaOnnxOnlineRecognizer *p);
};
// ============================================================================
// Non-streaming ASR
// ============================================================================
struct SHERPA_ONNX_API OfflineTransducerModelConfig {
std::string encoder;
std::string decoder;
std::string joiner;
};
struct SHERPA_ONNX_API OfflineParaformerModelConfig {
std::string model;
};
struct SHERPA_ONNX_API OfflineNemoEncDecCtcModelConfig {
std::string model;
};
struct SHERPA_ONNX_API OfflineWhisperModelConfig {
std::string encoder;
std::string decoder;
std::string language;
std::string task = "transcribe";
int32_t tail_paddings = -1;
};
struct SHERPA_ONNX_API OfflineCanaryModelConfig {
std::string encoder;
std::string decoder;
std::string src_lang;
std::string tgt_lang;
bool use_pnc = true;
};
struct SHERPA_ONNX_API OfflineFireRedAsrModelConfig {
std::string encoder;
std::string decoder;
};
struct SHERPA_ONNX_API OfflineTdnnModelConfig {
std::string model;
};
struct SHERPA_ONNX_API OfflineSenseVoiceModelConfig {
std::string model;
std::string language;
bool use_itn = false;
};
struct SHERPA_ONNX_API OfflineDolphinModelConfig {
std::string model;
};
struct SHERPA_ONNX_API OfflineZipformerCtcModelConfig {
std::string model;
};
struct SHERPA_ONNX_API OfflineWenetCtcModelConfig {
std::string model;
};
struct SHERPA_ONNX_API OfflineOmnilingualAsrCtcModelConfig {
std::string model;
};
struct SHERPA_ONNX_API OfflineMedAsrCtcModelConfig {
std::string model;
};
struct SHERPA_ONNX_API OfflineMoonshineModelConfig {
std::string preprocessor;
std::string encoder;
std::string uncached_decoder;
std::string cached_decoder;
};
struct SHERPA_ONNX_API OfflineFunASRNanoModelConfig {
std::string encoder_adaptor;
std::string llm;
std::string embedding;
std::string tokenizer;
std::string system_prompt = "You are a helpful assistant.";
std::string user_prompt = "语音转写:";
int32_t max_new_tokens = 512;
float temperature = 1e-6f;
float top_p = 0.8f;
int32_t seed = 42;
};
struct SHERPA_ONNX_API OfflineModelConfig {
OfflineTransducerModelConfig transducer;
OfflineParaformerModelConfig paraformer;
OfflineNemoEncDecCtcModelConfig nemo_ctc;
OfflineWhisperModelConfig whisper;
OfflineTdnnModelConfig tdnn;
std::string tokens;
int32_t num_threads = 1;
bool debug = false;
std::string provider = "cpu";
std::string model_type;
std::string modeling_unit = "cjkchar";
std::string bpe_vocab;
std::string telespeech_ctc;
OfflineSenseVoiceModelConfig sense_voice;
OfflineMoonshineModelConfig moonshine;
OfflineFireRedAsrModelConfig fire_red_asr;
OfflineDolphinModelConfig dolphin;
OfflineZipformerCtcModelConfig zipformer_ctc;
OfflineCanaryModelConfig canary;
OfflineWenetCtcModelConfig wenet_ctc;
OfflineOmnilingualAsrCtcModelConfig omnilingual;
OfflineMedAsrCtcModelConfig medasr;
OfflineFunASRNanoModelConfig funasr_nano;
};
struct SHERPA_ONNX_API OfflineLMConfig {
std::string model;
float scale = 1.0;
};
struct SHERPA_ONNX_API OfflineRecognizerConfig {
FeatureConfig feat_config;
OfflineModelConfig model_config;
OfflineLMConfig lm_config;
std::string decoding_method = "greedy_search";
int32_t max_active_paths = 4;
std::string hotwords_file;
float hotwords_score = 1.5;
std::string rule_fsts;
std::string rule_fars;
float blank_penalty = 0;
HomophoneReplacerConfig hr;
};
struct SHERPA_ONNX_API OfflineRecognizerResult {
std::string text;
std::vector<float> timestamps;
std::vector<std::string> tokens;
std::string json;
std::string lang;
std::string emotion;
std::string event;
// non-empty only for TDT models
std::vector<float> durations;
};
class SHERPA_ONNX_API OfflineStream
: public MoveOnly<OfflineStream, SherpaOnnxOfflineStream> {
public:
explicit OfflineStream(const SherpaOnnxOfflineStream *p);
void AcceptWaveform(int32_t sample_rate, const float *samples,
int32_t n) const;
void Destroy(const SherpaOnnxOfflineStream *p) const;
};
class SHERPA_ONNX_API OfflineRecognizer
: public MoveOnly<OfflineRecognizer, SherpaOnnxOfflineRecognizer> {
public:
static OfflineRecognizer Create(const OfflineRecognizerConfig &config);
void Destroy(const SherpaOnnxOfflineRecognizer *p) const;
OfflineStream CreateStream() const;
OfflineStream CreateStream(const std::string &hotwords) const;
void Decode(const OfflineStream *s) const;
void Decode(const OfflineStream *ss, int32_t n) const;
OfflineRecognizerResult GetResult(const OfflineStream *s) const;
// For unreal engine, please use this function
// See also https://github.com/k2-fsa/sherpa-onnx/discussions/1960
std::shared_ptr<OfflineRecognizerResult> GetResultPtr(
const OfflineStream *s) const;
void SetConfig(const OfflineRecognizerConfig &config) const;
private:
explicit OfflineRecognizer(const SherpaOnnxOfflineRecognizer *p);
};
// ============================================================================
// Non-streaming TTS
// ============================================================================
struct OfflineTtsVitsModelConfig {
std::string model;
std::string lexicon;
std::string tokens;
std::string data_dir;
std::string dict_dir; // unused
float noise_scale = 0.667;
float noise_scale_w = 0.8;
float length_scale = 1.0; // < 1, faster in speed; > 1, slower in speed
};
struct OfflineTtsMatchaModelConfig {
std::string acoustic_model;
std::string vocoder;
std::string lexicon;
std::string tokens;
std::string data_dir;
std::string dict_dir; // unused
float noise_scale = 0.667;
float length_scale = 1.0; // < 1, faster in speed; > 1, slower in speed
};
struct OfflineTtsKokoroModelConfig {
std::string model;
std::string voices;
std::string tokens;
std::string data_dir;
std::string dict_dir; // unused
std::string lexicon;
std::string lang;
float length_scale = 1.0; // < 1, faster in speed; > 1, slower in speed
};
struct OfflineTtsKittenModelConfig {
std::string model;
std::string voices;
std::string tokens;
std::string data_dir;
float length_scale = 1.0; // < 1, faster in speed; > 1, slower in speed
};
struct OfflineTtsZipvoiceModelConfig {
std::string tokens;
std::string encoder;
std::string decoder;
std::string vocoder;
std::string data_dir;
std::string lexicon;
float feat_scale = 0.1;
float t_shift = 0.5;
float target_rms = 0.1;
float guidance_scale = 1.0;
};
struct OfflineTtsModelConfig {
OfflineTtsVitsModelConfig vits;
OfflineTtsMatchaModelConfig matcha;
OfflineTtsKokoroModelConfig kokoro;
OfflineTtsKittenModelConfig kitten;
OfflineTtsZipvoiceModelConfig zipvoice;
int32_t num_threads = 1;
bool debug = false;
std::string provider = "cpu";
};
struct OfflineTtsConfig {
OfflineTtsModelConfig model;
std::string rule_fsts;
std::string rule_fars;
int32_t max_num_sentences = 1;
float silence_scale = 0.2;
};
struct GeneratedAudio {
std::vector<float> samples; // in the range [-1, 1]
int32_t sample_rate;
};
// Return 1 to continue generating
// Return 0 to stop generating
using OfflineTtsCallback = int32_t (*)(const float *samples,
int32_t num_samples, float progress,
void *arg);
class SHERPA_ONNX_API OfflineTts
: public MoveOnly<OfflineTts, SherpaOnnxOfflineTts> {
public:
static OfflineTts Create(const OfflineTtsConfig &config);
void Destroy(const SherpaOnnxOfflineTts *p) const;
// Return the sample rate of the generated audio
int32_t SampleRate() const;
// Number of supported speakers.
// If it supports only a single speaker, then it return 0 or 1.
int32_t NumSpeakers() const;
// @param text A string containing words separated by spaces
// @param sid Speaker ID. Used only for multi-speaker models, e.g., models
// trained using the VCTK dataset. It is not used for
// single-speaker models, e.g., models trained using the ljspeech
// dataset.
// @param speed The speed for the generated speech. E.g., 2 means 2x faster.
// @param callback If not NULL, it is called whenever config.max_num_sentences
// sentences have been processed. The callback is called in
// the current thread.
GeneratedAudio Generate(const std::string &text, int32_t sid = 0,
float speed = 1.0,
OfflineTtsCallback callback = nullptr,
void *arg = nullptr) const;
// Like Generate, but return a smart pointer.
//
// See also https://github.com/k2-fsa/sherpa-onnx/issues/2347
std::shared_ptr<GeneratedAudio> Generate2(
const std::string &text, int32_t sid = 0, float speed = 1.0,
OfflineTtsCallback callback = nullptr, void *arg = nullptr) const;
private:
explicit OfflineTts(const SherpaOnnxOfflineTts *p);
};
// ============================================================
// For Keyword Spotter
// ============================================================
struct KeywordResult {
std::string keyword;
std::vector<std::string> tokens;
std::vector<float> timestamps;
float start_time;
std::string json;
};
struct KeywordSpotterConfig {
FeatureConfig feat_config;
OnlineModelConfig model_config;
int32_t max_active_paths = 4;
int32_t num_trailing_blanks = 1;
float keywords_score = 1.0f;
float keywords_threshold = 0.25f;
std::string keywords_file;
};
class SHERPA_ONNX_API KeywordSpotter
: public MoveOnly<KeywordSpotter, SherpaOnnxKeywordSpotter> {
public:
static KeywordSpotter Create(const KeywordSpotterConfig &config);
void Destroy(const SherpaOnnxKeywordSpotter *p) const;
OnlineStream CreateStream() const;
OnlineStream CreateStream(const std::string &keywords) const;
bool IsReady(const OnlineStream *s) const;
void Decode(const OnlineStream *s) const;
void Decode(const OnlineStream *ss, int32_t n) const;
void Reset(const OnlineStream *s) const;
KeywordResult GetResult(const OnlineStream *s) const;
private:
explicit KeywordSpotter(const SherpaOnnxKeywordSpotter *p);
};
struct OfflineSpeechDenoiserGtcrnModelConfig {
std::string model;
};
struct OfflineSpeechDenoiserModelConfig {
OfflineSpeechDenoiserGtcrnModelConfig gtcrn;
int32_t num_threads = 1;
int32_t debug = false;
std::string provider = "cpu";
};
struct OfflineSpeechDenoiserConfig {
OfflineSpeechDenoiserModelConfig model;
};
struct DenoisedAudio {
std::vector<float> samples; // in the range [-1, 1]
int32_t sample_rate;
};
class SHERPA_ONNX_API OfflineSpeechDenoiser
: public MoveOnly<OfflineSpeechDenoiser, SherpaOnnxOfflineSpeechDenoiser> {
public:
static OfflineSpeechDenoiser Create(
const OfflineSpeechDenoiserConfig &config);
void Destroy(const SherpaOnnxOfflineSpeechDenoiser *p) const;
DenoisedAudio Run(const float *samples, int32_t n, int32_t sample_rate) const;
int32_t GetSampleRate() const;
private:
explicit OfflineSpeechDenoiser(const SherpaOnnxOfflineSpeechDenoiser *p);
};
// ==============================
// VAD
// ==============================
struct SileroVadModelConfig {
std::string model;
float threshold = 0.5;
float min_silence_duration = 0.5;
float min_speech_duration = 0.25;
int32_t window_size = 512;
float max_speech_duration = 20;
};
struct TenVadModelConfig {
std::string model;
float threshold = 0.5;
float min_silence_duration = 0.5;
float min_speech_duration = 0.25;
int32_t window_size = 256;
float max_speech_duration = 20;
};
struct VadModelConfig {
SileroVadModelConfig silero_vad;
TenVadModelConfig ten_vad;
int32_t sample_rate = 16000;
int32_t num_threads = 1;
std::string provider = "cpu";
bool debug = false;
};
struct SpeechSegment {
int32_t start;
std::vector<float> samples;
};
class SHERPA_ONNX_API CircularBuffer
: public MoveOnly<CircularBuffer, SherpaOnnxCircularBuffer> {
public:
static CircularBuffer Create(int32_t capacity);
void Destroy(const SherpaOnnxCircularBuffer *p) const;
void Push(const float *p, int32_t n) const;
std::vector<float> Get(int32_t start_index, int32_t n) const;
void Pop(int32_t n) const;
int32_t Size() const;
int32_t Head() const;
void Reset() const;
private:
explicit CircularBuffer(const SherpaOnnxCircularBuffer *p);
};
class SHERPA_ONNX_API VoiceActivityDetector
: public MoveOnly<VoiceActivityDetector, SherpaOnnxVoiceActivityDetector> {
public:
static VoiceActivityDetector Create(const VadModelConfig &config,
float buffer_size_in_seconds);
void Destroy(const SherpaOnnxVoiceActivityDetector *p) const;
void AcceptWaveform(const float *samples, int32_t n) const;
bool IsEmpty() const;
bool IsDetected() const;
void Pop() const;
void Clear() const;
SpeechSegment Front() const;
// For unreal engine, please use this function
// See also https://github.com/k2-fsa/sherpa-onnx/discussions/1960
std::shared_ptr<SpeechSegment> FrontPtr() const;
void Reset() const;
void Flush() const;
private:
explicit VoiceActivityDetector(const SherpaOnnxVoiceActivityDetector *p);
};
class SHERPA_ONNX_API LinearResampler
: public MoveOnly<LinearResampler, SherpaOnnxLinearResampler> {
public:
LinearResampler() = default;
static LinearResampler Create(int32_t samp_rate_in_hz,
int32_t samp_rate_out_hz,
float filter_cutoff_hz, int32_t num_zeros);
void Destroy(const SherpaOnnxLinearResampler *p) const;
void Reset() const;
std::vector<float> Resample(const float *input, int32_t input_dim,
bool flush) const;
int32_t GetInputSamplingRate() const;
int32_t GetOutputSamplingRate() const;
private:
explicit LinearResampler(const SherpaOnnxLinearResampler *p);
};
SHERPA_ONNX_API std::string GetVersionStr();
SHERPA_ONNX_API std::string GetGitSha1();
SHERPA_ONNX_API std::string GetGitDate();
SHERPA_ONNX_API bool FileExists(const std::string &filename);
// ============================================================================
// Offline Punctuation
// ============================================================================
struct OfflinePunctuationModelConfig {
std::string ct_transformer;
int32_t num_threads = 1;
bool debug = false;
std::string provider = "cpu";
};
struct OfflinePunctuationConfig {
OfflinePunctuationModelConfig model;
};
class SHERPA_ONNX_API OfflinePunctuation
: public MoveOnly<OfflinePunctuation, SherpaOnnxOfflinePunctuation> {
public:
static OfflinePunctuation Create(const OfflinePunctuationConfig &config);
void Destroy(const SherpaOnnxOfflinePunctuation *p) const;
// Add punctuations to the input text and return it.
std::string AddPunctuation(const std::string &text) const;
private:
explicit OfflinePunctuation(const SherpaOnnxOfflinePunctuation *p);
};
// ============================================================================
// Online Punctuation
// ============================================================================
struct OnlinePunctuationModelConfig {
std::string cnn_bilstm;
std::string bpe_vocab;
int32_t num_threads = 1;
bool debug = false;
std::string provider = "cpu";
};
struct OnlinePunctuationConfig {
OnlinePunctuationModelConfig model;
};
class SHERPA_ONNX_API OnlinePunctuation
: public MoveOnly<OnlinePunctuation, SherpaOnnxOnlinePunctuation> {
public:
static OnlinePunctuation Create(const OnlinePunctuationConfig &config);
void Destroy(const SherpaOnnxOnlinePunctuation *p) const;
// Add punctuations to the input text and return it.
std::string AddPunctuation(const std::string &text) const;
private:
explicit OnlinePunctuation(const SherpaOnnxOnlinePunctuation *p);
};
// ============================================================================
// Audio tagging
// ============================================================================
struct OfflineZipformerAudioTaggingModelConfig {
std::string model;
};
struct AudioTaggingModelConfig {
OfflineZipformerAudioTaggingModelConfig zipformer;
std::string ced;
int32_t num_threads = 1;
bool debug = false; // true to print debug information of the model
std::string provider = "cpu";
};
struct AudioTaggingConfig {
AudioTaggingModelConfig model;
std::string labels;
int32_t top_k = 5;
};
struct AudioEvent {
std::string name;
int32_t index;
float prob;
};
class SHERPA_ONNX_API AudioTagging
: public MoveOnly<AudioTagging, SherpaOnnxAudioTagging> {
public:
static AudioTagging Create(const AudioTaggingConfig &config);
void Destroy(const SherpaOnnxAudioTagging *p) const;
OfflineStream CreateStream() const;
// when top_k is -1, it uses the top_k from config.top_k
// when top_k is > 0, config.top_k is ignored
std::vector<AudioEvent> Compute(const OfflineStream *s, int32_t top_k = -1);
// For unreal engine, please use this function
// See also https://github.com/k2-fsa/sherpa-onnx/discussions/1960
std::shared_ptr<std::vector<AudioEvent>> ComputePtr(const OfflineStream *s,
int32_t top_k = -1);
private:
explicit AudioTagging(const SherpaOnnxAudioTagging *p);
};
} // namespace sherpa_onnx::cxx
#endif // SHERPA_ONNX_C_API_CXX_API_H_
@@ -0,0 +1,162 @@
#pragma once
/**
* This is a simple alternative cross-platform implementation of getopt, which
* is used to parse argument strings submitted to the executable (argc and argv
* which are received in the main function).
*/
#ifndef CAG_LIBRARY_H
#define CAG_LIBRARY_H
#include <stdbool.h>
#include <stddef.h>
#include <stdio.h>
#if defined(_WIN32) || defined(__CYGWIN__)
#define CAG_EXPORT __declspec(dllexport)
#define CAG_IMPORT __declspec(dllimport)
#elif __GNUC__ >= 4
#define CAG_EXPORT __attribute__((visibility("default")))
#define CAG_IMPORT __attribute__((visibility("default")))
#else
#define CAG_EXPORT
#define CAG_IMPORT
#endif
#if defined(CAG_SHARED)
#if defined(CAG_EXPORTS)
#define CAG_PUBLIC CAG_EXPORT
#else
#define CAG_PUBLIC CAG_IMPORT
#endif
#else
#define CAG_PUBLIC
#endif
#ifdef __cplusplus
extern "C" {
#endif
/**
* An option is used to describe a flag/argument option submitted when the
* program is run.
*/
typedef struct cag_option
{
const char identifier;
const char *access_letters;
const char *access_name;
const char *value_name;
const char *description;
} cag_option;
/**
* A context is used to iterate over all options provided. It stores the parsing
* state.
*/
typedef struct cag_option_context
{
const struct cag_option *options;
size_t option_count;
int argc;
char **argv;
int index;
int inner_index;
bool forced_end;
char identifier;
char *value;
} cag_option_context;
/**
* This is just a small macro which calculates the size of an array.
*/
#define CAG_ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))
/**
* @brief Prints all options to the terminal.
*
* This function prints all options to the terminal. This can be used to
* generate the output for a "--help" option.
*
* @param options The options which will be printed.
* @param option_count The option count which will be printed.
* @param destination The destination where the output will be printed.
*/
CAG_PUBLIC void cag_option_print(const cag_option *options, size_t option_count,
FILE *destination);
/**
* @brief Prepare argument options context for parsing.
*
* This function prepares the context for iteration and initializes the context
* with the supplied options and arguments. After the context has been prepared,
* it can be used to fetch arguments from it.
*
* @param context The context which will be initialized.
* @param options The registered options which are available for the program.
* @param option_count The amount of options which are available for the
* program.
* @param argc The amount of arguments the user supplied in the main function.
* @param argv A pointer to the arguments of the main function.
*/
CAG_PUBLIC void cag_option_prepare(cag_option_context *context,
const cag_option *options, size_t option_count, int argc, char **argv);
/**
* @brief Fetches an option from the argument list.
*
* This function fetches a single option from the argument list. The context
* will be moved to that item. Information can be extracted from the context
* after the item has been fetched.
* The arguments will be re-ordered, which means that non-option arguments will
* be moved to the end of the argument list. After all options have been
* fetched, all non-option arguments will be positioned after the index of
* the context.
*
* @param context The context from which we will fetch the option.
* @return Returns true if there was another option or false if the end is
* reached.
*/
CAG_PUBLIC bool cag_option_fetch(cag_option_context *context);
/**
* @brief Gets the identifier of the option.
*
* This function gets the identifier of the option, which should be unique to
* this option and can be used to determine what kind of option this is.
*
* @param context The context from which the option was fetched.
* @return Returns the identifier of the option.
*/
CAG_PUBLIC char cag_option_get(const cag_option_context *context);
/**
* @brief Gets the value from the option.
*
* This function gets the value from the option, if any. If the option does not
* contain a value, this function will return NULL.
*
* @param context The context from which the option was fetched.
* @return Returns a pointer to the value or NULL if there is no value.
*/
CAG_PUBLIC const char *cag_option_get_value(const cag_option_context *context);
/**
* @brief Gets the current index of the context.
*
* This function gets the index within the argv arguments of the context. The
* context always points to the next item which it will inspect. This is
* particularly useful to inspect the original argument array, or to get
* non-option arguments after option fetching has finished.
*
* @param context The context from which the option was fetched.
* @return Returns the current index of the context.
*/
CAG_PUBLIC int cag_option_get_index(const cag_option_context *context);
#ifdef __cplusplus
} // extern "C"
#endif
#endif
@@ -0,0 +1,11 @@
prefix=D:/a/sherpa-onnx/sherpa-onnx/build/install
exec_prefix=D:/a/sherpa-onnx/sherpa-onnx/build/install
libdir=${exec_prefix}/lib
includedir=${prefix}/include
Name: espeak-ng
Description: espeak-ng is a multi-lingual software speech synthesizer
Version: 1.52.0.1
Requires:
Libs: -L${libdir} -lespeak-ng
Cflags: -I${includedir}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 859 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 B

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+147 -57
View File
@@ -1,37 +1,52 @@
use std::path::PathBuf;
use std::sync::Mutex;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use serde::{Deserialize, Serialize};
use crate::audio::{RecordingSession, RecordingStatus};
use crate::diagnostics::TranscriptionAttempt;
use crate::audio_history::{list_audio_history, AudioHistoryItem};
use crate::models::{model_statuses, ModelWithStatus};
use crate::secrets::SecretStore;
use crate::settings::AppSettings;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct TranscriptDraft {
pub text: String,
pub last_message: Option<String>,
pub last_attempt: Option<crate::diagnostics::TranscriptionAttempt>,
}
impl Default for TranscriptDraft {
fn default() -> Self {
Self {
text: String::new(),
last_message: Some("Revise o texto antes de inserir.".to_string()),
last_attempt: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AppSnapshot {
pub settings: AppSettings,
pub recording_status: RecordingStatus,
pub transcript: TranscriptDraft,
pub models: Vec<ModelWithStatus>,
pub app_version: String,
pub audio_history: Vec<AudioHistoryItem>,
}
pub struct ManagedAppState {
pub recording: Mutex<RecordingSession>,
pub transcript: Mutex<TranscriptDraft>,
pub settings: Mutex<AppSettings>,
pub model_dir: PathBuf,
pub recordings_dir: PathBuf,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct TranscriptDraft {
pub text: String,
pub last_message: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_attempt: Option<TranscriptionAttempt>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct AppSnapshot {
pub recording_status: RecordingStatus,
pub transcript: TranscriptDraft,
pub settings: AppSettings,
pub models: Vec<ModelWithStatus>,
settings_path: PathBuf,
pub secrets: Mutex<Option<Arc<SecretStore>>>,
}
impl Default for ManagedAppState {
@@ -42,13 +57,41 @@ impl Default for ManagedAppState {
settings: Mutex::new(AppSettings::default()),
model_dir: default_model_dir(),
recordings_dir: default_recordings_dir(),
settings_path: dirs_app_data().join("settings.json"),
secrets: Mutex::new(None),
}
}
}
impl ManagedAppState {
pub fn sherpa_model_dir(&self) -> PathBuf {
crate::models::sherpa_model_dir(&self.model_dir)
pub fn from_app_data_dir(app_data_dir: PathBuf) -> Self {
let settings_path = app_data_dir.join("settings.json");
Self {
recording: Mutex::new(RecordingSession::default()),
transcript: Mutex::new(TranscriptDraft::default()),
settings: Mutex::new(load_settings(&settings_path)),
model_dir: app_data_dir.join("models"),
recordings_dir: app_data_dir.join("recordings"),
settings_path,
secrets: Mutex::new(None),
}
}
pub fn save_settings(&self, settings: AppSettings) -> Result<(), String> {
if let Some(parent) = self.settings_path.parent() {
fs::create_dir_all(parent)
.map_err(|error| format!("criar diretório de configurações: {error}"))?;
}
let json = serde_json::to_string_pretty(&settings)
.map_err(|error| format!("serializar configurações: {error}"))?;
fs::write(&self.settings_path, json)
.map_err(|error| format!("salvar configurações: {error}"))?;
let mut guard = self
.settings
.lock()
.map_err(|_| "falha ao acessar configurações".to_string())?;
*guard = settings;
Ok(())
}
pub fn snapshot(&self) -> Result<AppSnapshot, String> {
@@ -57,56 +100,103 @@ impl ManagedAppState {
.lock()
.map_err(|_| "falha ao acessar configurações".to_string())?
.clone();
let recording_status = self
.recording
.lock()
.map_err(|_| "falha ao acessar gravação".to_string())?
.status
.clone();
let transcript = self
.transcript
.lock()
.map_err(|_| "falha ao acessar transcrição".to_string())?
.clone();
let recording_status = self
.recording
.lock()
.map(|guard| guard.status.clone())
.unwrap_or(RecordingStatus::Idle);
let models = model_statuses(&self.model_dir);
let audio_history = list_audio_history(&self.recordings_dir).unwrap_or_default();
Ok(AppSnapshot {
settings,
recording_status,
transcript,
settings,
models: model_statuses(&self.model_dir),
models,
app_version: env!("CARGO_PKG_VERSION").to_string(),
audio_history,
})
}
}
fn app_data_dir() -> PathBuf {
#[cfg(target_os = "windows")]
{
std::env::var("LOCALAPPDATA")
.map(PathBuf::from)
.unwrap_or_else(|_| std::env::temp_dir())
.join("transcript")
}
#[cfg(target_os = "linux")]
{
std::env::var("HOME")
.map(|home| {
PathBuf::from(home)
.join(".local")
.join("share")
.join("transcript")
})
.unwrap_or_else(|_| std::env::temp_dir().join("transcript"))
}
#[cfg(not(any(target_os = "windows", target_os = "linux")))]
{
std::env::temp_dir().join("transcript")
}
fn load_settings(path: &Path) -> AppSettings {
fs::read_to_string(path)
.ok()
.and_then(|content| serde_json::from_str(&content).ok())
.unwrap_or_default()
}
fn default_model_dir() -> PathBuf {
app_data_dir().join("models")
dirs_app_data().join("models")
}
fn default_recordings_dir() -> PathBuf {
app_data_dir().join("recordings")
dirs_app_data().join("recordings")
}
fn dirs_app_data() -> PathBuf {
// Tauri expõe `app_data_dir` no setup; aqui só devolvemos o caminho
// relativo padrão caso o app não tenha um app data configurado.
// O lib.rs é responsável por injetar o diretório real quando apropriado.
std::env::var("TRANSCRIPT_APP_DATA")
.map(PathBuf::from)
.unwrap_or_else(|_| std::env::temp_dir().join("transcript"))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::settings::TranscriptionMode;
#[test]
fn settings_persist_to_app_data_dir() {
let dir = std::env::temp_dir().join(format!(
"transcript-settings-test-{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
let state = ManagedAppState::from_app_data_dir(dir.clone());
let mut settings = AppSettings::default();
settings.transcription.mode = TranscriptionMode::ApiOnly;
settings.hotkey = Some("Ctrl+Alt+R".to_string());
state.save_settings(settings).unwrap();
let reloaded = ManagedAppState::from_app_data_dir(dir.clone());
let snapshot = reloaded.snapshot().unwrap();
assert_eq!(
snapshot.settings.transcription.mode,
TranscriptionMode::ApiOnly
);
assert_eq!(snapshot.settings.hotkey.as_deref(), Some("Ctrl+Alt+R"));
let _ = fs::remove_dir_all(dir);
}
#[test]
fn snapshot_lists_recent_audio() {
let dir = std::env::temp_dir().join(format!(
"transcript-audio-snapshot-test-{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
let state = ManagedAppState::from_app_data_dir(dir.clone());
fs::create_dir_all(&state.recordings_dir).unwrap();
fs::write(state.recordings_dir.join("recent.wav"), b"wav").unwrap();
let snapshot = state.snapshot().unwrap();
assert_eq!(snapshot.audio_history.len(), 1);
assert_eq!(snapshot.audio_history[0].id, "recent.wav");
let _ = fs::remove_dir_all(dir);
}
}
Loaded 100 of 123 files, more files were not shown because too many files have changed in this diff. Show more