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.*
|
||||||
!.env.example
|
!.env.example
|
||||||
*.local
|
*.local
|
||||||
|
.mcp.json
|
||||||
|
|
||||||
# OS/editor
|
# OS/editor
|
||||||
.DS_Store
|
.DS_Store
|
||||||
@@ -32,3 +33,4 @@ Thumbs.db
|
|||||||
# Superpowers local worktrees
|
# Superpowers local worktrees
|
||||||
.worktrees/
|
.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;
|
## Por que este projeto existe?
|
||||||
- catálogo e download do modelo `cstr/parakeet-tdt-0.6b-v3-GGUF` em Q8;
|
|
||||||
- captura real de microfone em WAV via CPAL;
|
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.
|
||||||
- execução do sidecar CrispASR/Parakeet;
|
|
||||||
- hotkey global `Ctrl+Shift+Space`;
|
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.
|
||||||
- inserção híbrida por clipboard + paste automático no Windows/X11 quando disponível;
|
|
||||||
- fallback manual no Wayland;
|
O **Transcript** resolve esse problema com um fluxo direto:
|
||||||
- tray/system tray básico;
|
|
||||||
- CI, build e release via GitHub Actions.
|
```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
|
## Arquitetura
|
||||||
|
|
||||||
```text
|
```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/
|
src-tauri/src/
|
||||||
audio/ Captura de microfone e gravação WAV
|
audio/ Captura de microfone e escrita WAV via CPAL + hound
|
||||||
transcription/ Contratos, CrispASR sidecar e API compatível com OpenAI
|
transcription/ Cliente OpenAI-compatible multipart/form-data
|
||||||
injection/ Clipboard, paste automático e fallback manual
|
text_processing.rs Normalização e engenharia de prompt via OpenAI/Anthropic
|
||||||
models/ Catálogo do modelo offline
|
injection/ Clipboard, Ctrl+V automático e fallback manual
|
||||||
settings/ Configuração padrão
|
secrets.rs Integração com Credential Manager via keyring
|
||||||
tray.rs Menu da bandeja do sistema
|
shortcuts.rs Hotkey global
|
||||||
ui_commands.rs Comandos Tauri expostos ao frontend
|
ui_commands.rs Comandos Tauri e fluxo nativo de ditado
|
||||||
app_state.rs Estado compartilhado mínimo
|
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`
|
Pré-requisitos:
|
||||||
- Arquivo: `parakeet-tdt-0.6b-v3-q8_0.gguf`
|
|
||||||
- Quantização: `Q8_0`
|
|
||||||
- Tamanho registrado no catálogo: `745121600` bytes
|
|
||||||
- Modelo base: NVIDIA Parakeet TDT 0.6B v3
|
|
||||||
- Idioma: detecção automática; português incluído no conjunto suportado pelo modelo base.
|
|
||||||
|
|
||||||
O modelo não deve ser embutido no instalador principal. A estratégia recomendada é instalar o app leve e baixar o GGUF pelo gerenciador de modelos.
|
- Node.js
|
||||||
|
- npm
|
||||||
|
- Rust stable
|
||||||
|
- Dependências do Tauri 2 para o seu sistema operacional
|
||||||
|
|
||||||
## Sidecar CrispASR
|
Instalação:
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
Rodar em modo desenvolvimento:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run tauri:dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Validação:
|
||||||
|
|
||||||
|
```bash
|
||||||
npm run typecheck
|
npm run typecheck
|
||||||
npm run test -- --run
|
npm run test -- --run
|
||||||
npm run build
|
|
||||||
cargo test --manifest-path src-tauri/Cargo.toml
|
cargo test --manifest-path src-tauri/Cargo.toml
|
||||||
|
```
|
||||||
|
|
||||||
|
Build frontend:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Gerar instalador Windows
|
||||||
|
|
||||||
|
```bash
|
||||||
npm run tauri:build
|
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`.
|
Use o instalador NSIS para uso diário enquanto novas versões são desenvolvidas.
|
||||||
- `.github/workflows/build.yml`: gera bundles Windows/Linux em push para `main` e manualmente.
|
|
||||||
- `.github/workflows/release.yml`: publica GitHub Releases em tags `v*.*.*`.
|
|
||||||
|
|
||||||
A lógica fica em comandos npm/cargo para facilitar migração futura para GitLab CI.
|
---
|
||||||
|
|
||||||
## Fontes técnicas usadas
|
## Estado atual
|
||||||
|
|
||||||
- Tauri 2 commands: https://v2.tauri.app/develop/calling-rust
|
O projeto já possui:
|
||||||
- Tauri 2 system tray: https://v2.tauri.app/learn/system-tray
|
|
||||||
- Tauri 2 sidecars: https://v2.tauri.app/develop/sidecar
|
- gravação real de microfone;
|
||||||
- Tauri GitHub pipelines: https://v2.tauri.app/distribute/pipelines/github
|
- transcrição local embutida com whisper.cpp base multilingual;
|
||||||
- Tauri Linux prerequisites: https://v2.tauri.app/start/prerequisites
|
- transcrição opcional via API compatível com OpenAI;
|
||||||
- NVIDIA Parakeet v3 model card: https://huggingface.co/nvidia/parakeet-tdt-0.6b-v3/blob/main/README.md
|
- hotkey global;
|
||||||
- cstr Parakeet GGUF: https://huggingface.co/cstr/parakeet-tdt-0.6b-v3-GGUF
|
- 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 charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Transcript</title>
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main id="app"></main>
|
<main id="app"></main>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "transcript-desktop",
|
"name": "transcript-desktop",
|
||||||
"version": "0.1.0",
|
"version": "0.3.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -12,7 +12,9 @@
|
|||||||
"tauri:build": "tauri build"
|
"tauri:build": "tauri build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "^2.9.0",
|
"@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",
|
"objc2-foundation",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.60.2",
|
||||||
"x11rb",
|
"x11rb",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -819,6 +819,16 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"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]]
|
[[package]]
|
||||||
name = "deranged"
|
name = "deranged"
|
||||||
version = "0.5.8"
|
version = "0.5.8"
|
||||||
@@ -2137,6 +2147,20 @@ dependencies = [
|
|||||||
"unicode-segmentation",
|
"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]]
|
[[package]]
|
||||||
name = "leb128fmt"
|
name = "leb128fmt"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -2211,6 +2235,16 @@ dependencies = [
|
|||||||
"libc",
|
"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]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
version = "0.12.1"
|
version = "0.12.1"
|
||||||
@@ -2285,6 +2319,16 @@ version = "0.3.17"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
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]]
|
[[package]]
|
||||||
name = "minimal-lexical"
|
name = "minimal-lexical"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
@@ -2584,6 +2628,7 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.13.0",
|
"bitflags 2.13.0",
|
||||||
"block2",
|
"block2",
|
||||||
|
"libc",
|
||||||
"objc2",
|
"objc2",
|
||||||
"objc2-core-foundation",
|
"objc2-core-foundation",
|
||||||
]
|
]
|
||||||
@@ -3083,7 +3128,7 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
"socket2",
|
"socket2",
|
||||||
"tracing",
|
"tracing",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.60.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3230,6 +3275,7 @@ dependencies = [
|
|||||||
"hyper-util",
|
"hyper-util",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"log",
|
"log",
|
||||||
|
"mime_guess",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"quinn",
|
"quinn",
|
||||||
@@ -3285,6 +3331,30 @@ dependencies = [
|
|||||||
"web-sys",
|
"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]]
|
[[package]]
|
||||||
name = "ring"
|
name = "ring"
|
||||||
version = "0.17.14"
|
version = "0.17.14"
|
||||||
@@ -4048,6 +4118,48 @@ dependencies = [
|
|||||||
"walkdir",
|
"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]]
|
[[package]]
|
||||||
name = "tauri-plugin-global-shortcut"
|
name = "tauri-plugin-global-shortcut"
|
||||||
version = "2.3.2"
|
version = "2.3.2"
|
||||||
@@ -4553,16 +4665,18 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "transcript-desktop"
|
name = "transcript-desktop"
|
||||||
version = "0.1.0"
|
version = "0.3.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arboard",
|
"arboard",
|
||||||
"cpal",
|
"cpal",
|
||||||
"hound",
|
"hound",
|
||||||
|
"keyring",
|
||||||
"reqwest 0.12.28",
|
"reqwest 0.12.28",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
|
"tauri-plugin-dialog",
|
||||||
"tauri-plugin-global-shortcut",
|
"tauri-plugin-global-shortcut",
|
||||||
"tauri-plugin-opener",
|
"tauri-plugin-opener",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
@@ -4660,6 +4774,12 @@ dependencies = [
|
|||||||
"unic-common",
|
"unic-common",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicase"
|
||||||
|
version = "2.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.24"
|
version = "1.0.24"
|
||||||
@@ -5278,6 +5398,15 @@ dependencies = [
|
|||||||
"windows-targets 0.52.6",
|
"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]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.61.2"
|
version = "0.61.2"
|
||||||
@@ -5311,13 +5440,30 @@ dependencies = [
|
|||||||
"windows_aarch64_gnullvm 0.52.6",
|
"windows_aarch64_gnullvm 0.52.6",
|
||||||
"windows_aarch64_msvc 0.52.6",
|
"windows_aarch64_msvc 0.52.6",
|
||||||
"windows_i686_gnu 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_i686_msvc 0.52.6",
|
||||||
"windows_x86_64_gnu 0.52.6",
|
"windows_x86_64_gnu 0.52.6",
|
||||||
"windows_x86_64_gnullvm 0.52.6",
|
"windows_x86_64_gnullvm 0.52.6",
|
||||||
"windows_x86_64_msvc 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]]
|
[[package]]
|
||||||
name = "windows-threading"
|
name = "windows-threading"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -5348,6 +5494,12 @@ version = "0.52.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_gnullvm"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_aarch64_msvc"
|
name = "windows_aarch64_msvc"
|
||||||
version = "0.42.2"
|
version = "0.42.2"
|
||||||
@@ -5360,6 +5512,12 @@ version = "0.52.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_msvc"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_i686_gnu"
|
name = "windows_i686_gnu"
|
||||||
version = "0.42.2"
|
version = "0.42.2"
|
||||||
@@ -5372,12 +5530,24 @@ version = "0.52.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_gnu"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_i686_gnullvm"
|
name = "windows_i686_gnullvm"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_gnullvm"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_i686_msvc"
|
name = "windows_i686_msvc"
|
||||||
version = "0.42.2"
|
version = "0.42.2"
|
||||||
@@ -5390,6 +5560,12 @@ version = "0.52.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_msvc"
|
||||||
|
version = "0.53.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_x86_64_gnu"
|
name = "windows_x86_64_gnu"
|
||||||
version = "0.42.2"
|
version = "0.42.2"
|
||||||
@@ -5402,6 +5578,12 @@ version = "0.52.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
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]]
|
[[package]]
|
||||||
name = "windows_x86_64_gnullvm"
|
name = "windows_x86_64_gnullvm"
|
||||||
version = "0.42.2"
|
version = "0.42.2"
|
||||||
@@ -5414,6 +5596,12 @@ version = "0.52.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
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]]
|
[[package]]
|
||||||
name = "windows_x86_64_msvc"
|
name = "windows_x86_64_msvc"
|
||||||
version = "0.42.2"
|
version = "0.42.2"
|
||||||
@@ -5426,6 +5614,12 @@ version = "0.52.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
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]]
|
[[package]]
|
||||||
name = "winnow"
|
name = "winnow"
|
||||||
version = "0.5.40"
|
version = "0.5.40"
|
||||||
@@ -5778,6 +5972,20 @@ name = "zeroize"
|
|||||||
version = "1.8.2"
|
version = "1.8.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
|
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]]
|
[[package]]
|
||||||
name = "zerotrie"
|
name = "zerotrie"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "transcript-desktop"
|
name = "transcript-desktop"
|
||||||
version = "0.1.0"
|
version = "0.3.1"
|
||||||
description = "Offline-first desktop transcription app for Windows and Linux"
|
description = "Offline-first desktop transcription app for Windows and Linux"
|
||||||
authors = ["Transcript contributors"]
|
authors = ["Transcript contributors"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
@@ -18,11 +18,17 @@ serde_json = "1"
|
|||||||
tauri = { version = "2", features = ["tray-icon", "image-png"] }
|
tauri = { version = "2", features = ["tray-icon", "image-png"] }
|
||||||
tauri-plugin-opener = "2"
|
tauri-plugin-opener = "2"
|
||||||
tauri-plugin-global-shortcut = "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 = [
|
reqwest = { version = "0.12", default-features = false, features = [
|
||||||
"blocking",
|
"blocking",
|
||||||
"rustls-tls",
|
"rustls-tls",
|
||||||
|
"multipart",
|
||||||
] }
|
] }
|
||||||
cpal = "0.15.3"
|
cpal = "0.15.3"
|
||||||
hound = "3.5"
|
hound = "3.5"
|
||||||
arboard = "3.6"
|
arboard = "3.6"
|
||||||
thiserror = "2"
|
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::fs;
|
||||||
use std::sync::Mutex;
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::audio::{RecordingSession, RecordingStatus};
|
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::models::{model_statuses, ModelWithStatus};
|
||||||
|
use crate::secrets::SecretStore;
|
||||||
use crate::settings::AppSettings;
|
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 struct ManagedAppState {
|
||||||
pub recording: Mutex<RecordingSession>,
|
pub recording: Mutex<RecordingSession>,
|
||||||
pub transcript: Mutex<TranscriptDraft>,
|
pub transcript: Mutex<TranscriptDraft>,
|
||||||
pub settings: Mutex<AppSettings>,
|
pub settings: Mutex<AppSettings>,
|
||||||
pub model_dir: PathBuf,
|
pub model_dir: PathBuf,
|
||||||
pub recordings_dir: PathBuf,
|
pub recordings_dir: PathBuf,
|
||||||
}
|
settings_path: PathBuf,
|
||||||
|
pub secrets: Mutex<Option<Arc<SecretStore>>>,
|
||||||
#[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>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ManagedAppState {
|
impl Default for ManagedAppState {
|
||||||
@@ -42,13 +57,41 @@ impl Default for ManagedAppState {
|
|||||||
settings: Mutex::new(AppSettings::default()),
|
settings: Mutex::new(AppSettings::default()),
|
||||||
model_dir: default_model_dir(),
|
model_dir: default_model_dir(),
|
||||||
recordings_dir: default_recordings_dir(),
|
recordings_dir: default_recordings_dir(),
|
||||||
|
settings_path: dirs_app_data().join("settings.json"),
|
||||||
|
secrets: Mutex::new(None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ManagedAppState {
|
impl ManagedAppState {
|
||||||
pub fn sherpa_model_dir(&self) -> PathBuf {
|
pub fn from_app_data_dir(app_data_dir: PathBuf) -> Self {
|
||||||
crate::models::sherpa_model_dir(&self.model_dir)
|
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> {
|
pub fn snapshot(&self) -> Result<AppSnapshot, String> {
|
||||||
@@ -57,56 +100,103 @@ impl ManagedAppState {
|
|||||||
.lock()
|
.lock()
|
||||||
.map_err(|_| "falha ao acessar configurações".to_string())?
|
.map_err(|_| "falha ao acessar configurações".to_string())?
|
||||||
.clone();
|
.clone();
|
||||||
|
let recording_status = self
|
||||||
|
.recording
|
||||||
|
.lock()
|
||||||
|
.map_err(|_| "falha ao acessar gravação".to_string())?
|
||||||
|
.status
|
||||||
|
.clone();
|
||||||
let transcript = self
|
let transcript = self
|
||||||
.transcript
|
.transcript
|
||||||
.lock()
|
.lock()
|
||||||
.map_err(|_| "falha ao acessar transcrição".to_string())?
|
.map_err(|_| "falha ao acessar transcrição".to_string())?
|
||||||
.clone();
|
.clone();
|
||||||
let recording_status = self
|
let models = model_statuses(&self.model_dir);
|
||||||
.recording
|
let audio_history = list_audio_history(&self.recordings_dir).unwrap_or_default();
|
||||||
.lock()
|
|
||||||
.map(|guard| guard.status.clone())
|
|
||||||
.unwrap_or(RecordingStatus::Idle);
|
|
||||||
Ok(AppSnapshot {
|
Ok(AppSnapshot {
|
||||||
|
settings,
|
||||||
recording_status,
|
recording_status,
|
||||||
transcript,
|
transcript,
|
||||||
settings,
|
models,
|
||||||
models: model_statuses(&self.model_dir),
|
app_version: env!("CARGO_PKG_VERSION").to_string(),
|
||||||
|
audio_history,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn app_data_dir() -> PathBuf {
|
fn load_settings(path: &Path) -> AppSettings {
|
||||||
#[cfg(target_os = "windows")]
|
fs::read_to_string(path)
|
||||||
{
|
.ok()
|
||||||
std::env::var("LOCALAPPDATA")
|
.and_then(|content| serde_json::from_str(&content).ok())
|
||||||
.map(PathBuf::from)
|
.unwrap_or_default()
|
||||||
.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 default_model_dir() -> PathBuf {
|
fn default_model_dir() -> PathBuf {
|
||||||
app_data_dir().join("models")
|
dirs_app_data().join("models")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_recordings_dir() -> PathBuf {
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||