Phase 9 of the rebrand cascade. Sweep covers everything the Phase 8
frontend pass deliberately skipped: docs/, root markdown, scripts,
Cargo.toml descriptions, code comments that survived earlier
word-boundary sed, plus a handful of identifiers caught on the final
verify pass.
transcription-app changes:
- README.md, HANDOVER.md, KNOWN-ISSUES.md, run.sh — magnotia/Magnotia
-> lumotia/Lumotia.
- docs/ — sweep across all subdirs except docs/handovers/ (preserved
as immutable audit trail). Includes architecture-map references
to magnotia_core::*, magnotia_storage::*, etc. now pointing at
lumotia_*; dev-setup.md tracing output examples (lumotia_startup
target); brief/ + superpowers/ + issues/ + whisper-ecosystem/ +
audit/.
- Cargo.toml descriptions on 9 crates (core, audio, cloud-providers,
hotkey, llm, mcp, plus referenced others).
- crates/core/src/{error,hardware,recommendation,paths}.rs +
crates/audio/src/wav.rs + crates/llm/src/model_manager.rs +
crates/cloud-providers/src/keystore.rs + crates/mcp/src/lib.rs —
doc comments and a model-manager user-agent string.
- Caught on final pass: BroadcastChannel("magnotia_task_sync") -> ...
("lumotia_task_sync"); magnotia_locale i18n localStorage key
renamed + migration shim added; CSS keyframe names
magnotiaPulse / magnotiaBar / magnotiaFade renamed in the design-
system kit; magnotia_viewer_item / magnotia_viewer_mode handoff
keys renamed in HistoryPage + viewer/+page.svelte; src/assets/
wordmark.svg text.
- src-tauri/src/lib.rs comment cleanup ("magnotia era" was sed'd
to "lumotia era" earlier — restored).
Preserved (intentional):
- crates/core/src/paths.rs — keeps "magnotia" / "Magnotia" / ".magnotia"
legacy detection strings in legacy_and_target_paths() so the
migration shim can still find user data from the magnotia era.
- src/lib/stores/{page,focusTimer}.svelte.ts + src/lib/i18n/index.ts
— migration call sites reference the legacy magnotia keys
deliberately.
- docs/handovers/ — historical audit trail.
cargo build --workspace passes. npm run check: 0 errors / 0 warnings
(3958 files). cargo test --workspace: 339 pass / 0 fail.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
14 KiB
name, type, slice, last_verified
| name | type | slice | last_verified |
|---|---|---|---|
| Live transcription session | architecture-map-page | 02-tauri-runtime | 2026/05/09 |
commands::live
Where you are: Architecture map → Tauri runtime → Commands → Live transcription
Plain English summary. The 1,737-line beast that runs a live dictation session end-to-end. Captures audio via a dedicated MicrophoneCapture, streams chunks through a StreamingResampler, runs a speech gate to skip near-silent chunks, dispatches 2-second windows with 0.25-second overlap to whisper or parakeet, polls inference results on a background thread, dedupes overlapping segments against a recent-history buffer, post-processes with the formatting pipeline, and emits typed messages back to the frontend on two tauri::ipc::Channels (one for results, one for status). Holds a macOS App Nap power assertion for the duration of the session. Writes audio progressively to a WAV file so a crash mid-session leaves a playable recording.
At a glance
- Path:
src-tauri/src/commands/live.rs. - LOC: 1,737. Largest file in the slice.
- Tauri commands exposed:
start_live_transcription_session(window, app, state, live_state, config: StartLiveTranscriptionConfig, result_channel: Channel<LiveResultMessage>, status_channel: Channel<LiveStatusMessage>) -> Result<StartLiveTranscriptionResponse, String>— main-window only.stop_live_transcription_session(window, app, live_state, session_id: u64) -> Result<StopLiveTranscriptionResponse, String>— main-window only.
- Events emitted: NONE in the conventional
app.emit(...)sense. This module uses Tauri 2's typedtauri::ipc::Channel<T>API instead. The frontend creates the channel pair on the JS side vianew Channel<T>(), passes it as a command argument, and Lumotia sends typed messages on it from the worker. Two channels:Channel<LiveResultMessage>— per-chunk transcription results (segments, language, duration, raw_text, inference_ms, chunk_id, chunk_start_secs).Channel<LiveStatusMessage>— tagged enum:Warning { message },Overload { dropped_audio_ms, message },Error { message },Finished { audio_path, dropped_audio_ms }.
- Depends on:
lumotia_audio::{AudioChunk, CaptureRuntimeError, MicrophoneCapture, StreamingResampler, WavWriter},lumotia_core::constants::WHISPER_SAMPLE_RATE,lumotia_core::types::{AudioSamples, Segment, TranscriptionOptions},lumotia_transcription::LocalEngine,lumotia_ai_formatting::{post_process_segments, FormatMode, PostProcessOptions},lumotia_storage::{database::get_profile, database::list_profile_terms, DEFAULT_PROFILE_ID}. Pluscommands::audio::resolve_recording_path,commands::build_initial_prompt,commands::models::{default_model_id_for_engine, ensure_model_loaded},commands::power::PowerAssertion,commands::security::ensure_main_window. - Called from frontend at: dictation page (when the user starts and stops a live session — most common entry).
What's in here
Constants (src-tauri/src/commands/live.rs:30)
Speech-gate, dedup, and chunking parameters. The headline numbers: CHUNK_SAMPLES = 32_000 (2 s at 16 kHz), OVERLAP_SAMPLES = 4_000 (0.25 s), FINAL_CHUNK_MIN_SAMPLES = 4_000, MAX_PENDING_SAMPLES = CHUNK_SAMPLES. Speech-gate thresholds (RMS / peak / consecutive-window counts) follow.
State
LiveTranscriptionState(src-tauri/src/commands/live.rs:62) — the Tauri-managed struct stashed bylib.rs::run. Fields:next_session_id: AtomicU64— monotonic session-id generator.lifecycle: AsyncMutex<()>— start/stop barrier.running: Mutex<Option<RunningLiveSession>>— the currently-running session, if any.
RunningLiveSession(src-tauri/src/commands/live.rs:68) — id, stop_flag, JoinHandle for the blocking worker, the status channel.
Public payload types
StartLiveTranscriptionConfig(src-tauri/src/commands/live.rs:77) — engine, model_id, language, initial_prompt, save_audio, output_folder, post-processing flags, format_mode, microphone_device, profile_id.StartLiveTranscriptionResponse—{ session_id }.StopLiveTranscriptionResponse—{ session_id, audio_path: Option<String>, dropped_audio_ms: u64 }.LiveResultMessage— per-chunk result.LiveStatusMessage— tagged enum (4 variants).
ActiveCapture (src-tauri/src/commands/live.rs:166)
Wraps MicrophoneCapture plus its cpal chunk receiver and the optional runtime-error receiver. drain_runtime_errors posts LiveStatusMessage::Warning for each cpal-side error.
LiveLoopState (src-tauri/src/commands/live.rs:208)
Per-session mutable state: resampler, capture buffer, WAV writer, buffer start sample index, dropped-audio counter, chunk id, in-flight inference task, resampler-flushed flag, result-listener-lost flag, recent-segments dedup history.
LiveSessionRuntime (src-tauri/src/commands/live.rs:231)
Owns everything for one session. Constructor opens the WAV writer. run() is the main loop:
loop {
poll_inference()?;
capture.drain_runtime_errors();
if let Some(chunk) = recv_audio()? { process_audio_chunk(chunk)?; }
drop_pending_overflow(); // bounded buffer; emits Overload status
flush_tail_if_stopping()?;
if dispatch_inference_if_ready() { continue; }
if should_exit_loop() { break; }
}
drain_inference()?;
finish()
Methods:
process_audio_chunk— downmix, lazy-initStreamingResampler, push samples, append to capture buffer + WAV (src-tauri/src/commands/live.rs:323).drop_pending_overflow— when the inflight inference is busy and the buffer exceedsMAX_PENDING_SAMPLES, drop the oldest samples and emitLiveStatusMessage::Overloadwith the cumulative dropped-audio counter (:344).flush_tail_if_stopping— flush the resampler and the WAV header on shutdown (:365).dispatch_inference_if_ready— wrapsmaybe_dispatch_chunk(the chunking + speech-gate + thread-spawn function) (:396).drain_inference— busy-loops with 10 ms sleeps until the in-flight inference completes after stop (:425).finish— finalise WAV, returnLiveSessionSummary(:433).
start_live_transcription_session (src-tauri/src/commands/live.rs:484)
ensure_main_window.lifecycle.lock().await— barrier against concurrent start/stop.- Reject if a session is already running.
- Resolve profile_id, fetch profile + profile_terms from
lumotia_storage. - Collapse the effective
initial_promptviabuild_initial_prompt(so the worker doesn't have to know about profile fallback). - Resolve model_id via
default_model_id_for_engineif absent. ensure_model_loaded(state, engine, model_id, None)—Nonemeans don't enforce sequential-GPU mode (Settings owns that toggle).- Resolve audio_path via
commands::audio::resolve_recording_pathifsave_audiois true. tokio::task::spawn_blocking(move || run_live_session(...))— the real worker runs on a dedicated blocking thread, not the Tokio runtime, because Whisper inference itself spawns its own threads and the work is CPU-bound.- Stash the new
RunningLiveSession. Return the session_id.
stop_live_transcription_session (src-tauri/src/commands/live.rs:591)
ensure_main_window, lifecycle lock.- Take the running session out of state.
- Validate
session_idmatches; on mismatch, restore the session and return an error. - Set the stop flag and await the worker
JoinHandle. - Read the summary, send
LiveStatusMessage::Finishedon the status channel, return the response.
run_live_session (src-tauri/src/commands/live.rs:646)
The blocking entry. Holds a PowerAssertion::begin("lumotia live dictation session") for the entire scope. Constructs and runs LiveSessionRuntime. The drop on the power assertion ends the macOS App Nap pin.
maybe_dispatch_chunk (src-tauri/src/commands/live.rs:753)
The brain of the chunking pipeline. Decides whether to dispatch a chunk now, based on capture buffer size and the stopping flag:
- Full chunk path:
target_len = CHUNK_SAMPLES, withOVERLAP_SAMPLESof trim against the previous chunk to dedupe. - Stopping path: dispatch any partial chunk ≥
FINAL_CHUNK_MIN_SAMPLES. - Speech gate:
evaluate_speech_gate(speech_window)(:1305) returns a decision based on per-frame RMS / peak amplitude / consecutive-speech-window counts. If skipped, drop those samples and emit a Warning. - On dispatch: spawn a
std::threadthat callsengine.transcribe_syncand posts the result back via astd::sync::mpscchannel. The 2025 version of this code used a Tokio task; switching to a plain thread keeps inference off the blocking pool entirely.
poll_inference (src-tauri/src/commands/live.rs:864)
Polls the in-flight InferenceTask's mpsc receiver. On result:
- Trim overlap segments against the previous chunk via
trim_overlap_segments. - Run dedup vs the
recent_segmentshistory viafilter_duplicate_boundary_segments. - Post-process with
post_process_segments(using the dictionary terms andPostProcessOptions). - Build a
LiveResultMessageandemit_live_result(...).
emit_live_result (src-tauri/src/commands/live.rs:971)
Sends on the result channel. If the listener is dead, sets result_listener_lost = true and tries to send a Warning on the status channel. If that also fails, self-asserts the stop flag so the worker drains and exits — otherwise the worker would burn CPU + GPU memory polling forever after the user closes the app window without a clean stop call.
Dedup helpers (src-tauri/src/commands/live.rs:1027 onwards)
filter_duplicate_boundary_segments— drops segments at chunk boundaries that meaningfully overlap the recent-segments history.remember_recent_segments— maintains the rolling window (~DUPLICATE_HISTORY_RETENTION_SECS = 8.0).build_nearby_transcript_candidates— collects candidates in the leading-edge window (DUPLICATE_CHECK_LEADING_SECS = 1.5).transcripts_overlapandtranscripts_loosely_overlap— token-coverage / longest-common-subsequence checks againstLOW_SIGNAL_TOKENS(a stop-word-equivalent list of ~60 high-frequency tokens).
Speech gate (src-tauri/src/commands/live.rs:1251 onwards)
record_speech_window,speech_gate_decision,evaluate_speech_gate. Two thresholds: a strong-speech path (high RMS or high peak, or two consecutive speech windows) and a soft-speech path.FLATLINE_PEAK_THRESHOLDcatches the silent-buffer case (e.g. mic disconnected). The gate keeps Whisper from hallucinating on near-silent audio, which Whisper is famous for doing ("you are watching the show").
Other helpers
downmix_chunk(:1336) — same pattern ascommands::audio.pick_engine(:638) —state.whisper_engineorstate.parakeet_engine.open_wav_writer,finalize_wav_writer,append_resampled_audio— progressive WAV plumbing (brief item #19).
Data flow
frontend invoke('start_live_transcription_session', { config, result_channel, status_channel })
-> Rust: validate, fetch profile, build prompt, ensure model loaded, spawn worker
worker (blocking thread):
loop:
cpal -> ActiveCapture -> StreamingResampler -> capture_buffer + WAV
when buffer >= 32k samples (or stopping with >= 4k):
speech-gate -> if pass: thread::spawn(engine.transcribe_sync)
poll inflight: filter overlap, dedup vs history, post_process_segments
send LiveResultMessage on result_channel
on overflow: drop oldest, send LiveStatusMessage::Overload
on stop flag: flush resampler tail, drain inflight, finalise WAV
return LiveSessionSummary
frontend invoke('stop_live_transcription_session', { session_id })
-> Rust: set stop flag, await worker, send LiveStatusMessage::Finished, return response
Watch-outs
- Size. 1,737 LOC in one file. The runtime + loop + speech gate + dedup + chunker really should be split. The pieces are already modular; pulling each out into its own file under
commands/live/would make the surface much easier to read and audit. thread::spawnfor inference. Each chunk spawns a fresh OS thread (live.rs:841). Inside Whisper this is fine because whisper.cpp uses its own thread pool, and only one chunk is in flight at a time per session. Two simultaneous live sessions would multiply this; the lifecycle lock forbids that today.poll_inferencebusy-loops with 10 ms sleeps indrain_inference. Acceptable because we only enter the drain on stop. Don't reuse this pattern for the main loop.- Result-listener-lost path is critical. Without it, closing the main window without a clean stop would leave the worker spinning forever, holding the GPU memory and the WAV file handle until process exit. The self-asserted stop flag is the safety net.
- Power assertion only does work on macOS. On Linux the function is a no-op (see Power assertions and security). A long live-dictation session on Linux can still be idled by the compositor.
- The recent-segments history is bounded by time, not count. A high chunk rate could grow it more than expected; the retention is
DUPLICATE_HISTORY_RETENTION_SECS = 8.0. - Channel back-pressure. The result channel is the JS-side
Channel<T>queue. If the frontend stops reading, the queue grows. Lumotia's overload-signalling currently uses the in-bufferMAX_PENDING_SAMPLEScap; it does NOT detect a JS-side stalled listener except via theemit_live_result-failure path. ensure_model_loaded(state, engine, model_id, None)intentionally passesNoneforconcurrent, so live sessions never trigger the sequential-GPU guard incommands::models. If you ever ship a tight-VRAM machine and the user has switched to sequential mode, this could OOM. Today's hardware survey indicates this is uncommon; flag this when revisiting Phase A.4.
See also
- Audio capture —
resolve_recording_pathandrecording_filenameare shared. - Models —
default_model_id_for_engineandensure_model_loadedare called from start. - Transcription — the non-live transcription path that shares the post-processing pipeline.
- Profiles — the profile + profile-terms fetch happens before the worker spawns.
- Power assertions and security — the App Nap pin and
ensure_main_windowguard. commands::mod—build_initial_promptis the prompt assembler used here.