Files
transcript/docs/superpowers/plans/2026-07-13-audio-history-streaming-fallback.md
T
evandrodevbr 06aaeeaf79 wip: snapshot das alterações não commitadas (2026-10-20)
Origem: D:/Documents/Github/transcript
Arquivos omitidos (segredos, bancos de dados, logs, >50 MB): 2
2026-10-20 00:50:17 -03:00

210 lines
7.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.