feat(preview): floating transcription overlay with listening→live→cleanup→final phases

Ported the best bits of OpenWhispr's TranscriptionPreviewOverlay into Kon's
window conventions. Off by default — toggle in Settings → Output → "Floating
preview when Kon is unfocused". Opens only when the main window isn't
focused at the start of a recording, so it never adds noise when the user
can already see the transcript in the main surface.

Phase state machine (src/routes/preview/+page.svelte):
- listening — pulsing dot, no text yet
- live      — animated bars + streaming raw Whisper output
- cleanup   — accent bars while the LLM cleanup pass runs
- final     — checkmark + formatted text + 4s auto-hide

Data plumbing: raw segment text is captured before post_process_segments in
live.rs (new raw_text field on LiveResultMessage) and in transcription.rs
(new raw_text in the transcription-result payload). DictationPage forwards
raw_text to the overlay via Tauri global events — preview-listening on
start, preview-append per chunk, preview-cleanup before the LLM pass,
preview-final with the formatted text, preview-hide when a run produced no
transcript (empty recording / cancel).

Window is always_on_top, skip_taskbar, focused=false so it never steals
focus from whatever the user is dictating into. open_preview_window shows
an existing hidden preview or builds it fresh; close_preview_window hides
without destroying so the next open is instant.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-21 08:49:29 +01:00
parent 42b32a4f1a
commit eb60a8bfd3
10 changed files with 403 additions and 0 deletions

View File

@@ -112,6 +112,11 @@ pub struct LiveResultMessage {
pub language: String,
pub inference_ms: u64,
pub segments: Vec<Segment>,
/// Concatenated text BEFORE post-processing (no filler removal, no
/// British conversion, no LLM cleanup). Used by the transcription
/// preview overlay so the user can see raw Whisper output as it
/// streams in.
pub raw_text: String,
}
#[derive(Debug, Clone, Serialize)]
@@ -616,6 +621,14 @@ fn poll_inference(
Ok(Ok(timed)) => {
let mut segments: Vec<Segment> = timed.transcript.segments().to_vec();
trim_overlap_segments(&mut segments, task.trim_before_secs);
// Capture raw text BEFORE any post-processing so the preview
// overlay can show what Whisper actually returned.
let raw_text = segments
.iter()
.map(|segment| segment.text.trim())
.filter(|segment| !segment.is_empty())
.collect::<Vec<_>>()
.join(" ");
post_process_segments(
&mut segments,
&PostProcessOptions {
@@ -645,6 +658,7 @@ fn poll_inference(
language: timed.transcript.language().to_string(),
inference_ms: timed.inference_ms,
segments,
raw_text,
})
.map_err(|e| e.to_string())?;
remember_recent_segments(recent_segments, &delivered_segments, chunk_start_secs);