Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06aaeeaf79 |
No files matched your search
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"enabledMcpjsonServers": [
|
||||
"obsidian"
|
||||
],
|
||||
"enableAllProjectMcpServers": true
|
||||
}
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(rtk git:*)",
|
||||
"mcp__plugin_context-mode_context-mode__ctx_search"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 180 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 70 KiB |
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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`.
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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'
|
||||
@@ -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.
|
||||
|
After Width: | Height: | Size: 51 KiB |
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
@@ -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}
|
||||
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 859 B |
|
Before Width: | Height: | Size: 70 B After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 180 KiB |
@@ -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);
|
||||
}
|
||||
}
|
||||