Files
Lumotia/docs/architecture-map/02-tauri-runtime/commands/live.md
Jake 26c7307607
Some checks failed
check / cargo check (macos-latest) (push) Has been cancelled
check / cargo check (ubuntu-22.04) (push) Has been cancelled
check / cargo check (windows-latest) (push) Has been cancelled
check / svelte build + lint (push) Has been cancelled
agent: lumotia-rebrand — docs, scripts, root config, residuals
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>
2026-05-13 12:38:03 +01:00

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 mapTauri runtimeCommands → 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 typed tauri::ipc::Channel<T> API instead. The frontend creates the channel pair on the JS side via new 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}. Plus commands::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 by lib.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-init StreamingResampler, 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 exceeds MAX_PENDING_SAMPLES, drop the oldest samples and emit LiveStatusMessage::Overload with the cumulative dropped-audio counter (:344).
  • flush_tail_if_stopping — flush the resampler and the WAV header on shutdown (:365).
  • dispatch_inference_if_ready — wraps maybe_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, return LiveSessionSummary (:433).

start_live_transcription_session (src-tauri/src/commands/live.rs:484)

  1. ensure_main_window.
  2. lifecycle.lock().await — barrier against concurrent start/stop.
  3. Reject if a session is already running.
  4. Resolve profile_id, fetch profile + profile_terms from lumotia_storage.
  5. Collapse the effective initial_prompt via build_initial_prompt (so the worker doesn't have to know about profile fallback).
  6. Resolve model_id via default_model_id_for_engine if absent.
  7. ensure_model_loaded(state, engine, model_id, None)None means don't enforce sequential-GPU mode (Settings owns that toggle).
  8. Resolve audio_path via commands::audio::resolve_recording_path if save_audio is true.
  9. 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.
  10. Stash the new RunningLiveSession. Return the session_id.

stop_live_transcription_session (src-tauri/src/commands/live.rs:591)

  1. ensure_main_window, lifecycle lock.
  2. Take the running session out of state.
  3. Validate session_id matches; on mismatch, restore the session and return an error.
  4. Set the stop flag and await the worker JoinHandle.
  5. Read the summary, send LiveStatusMessage::Finished on 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, with OVERLAP_SAMPLES of 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::thread that calls engine.transcribe_sync and posts the result back via a std::sync::mpsc channel. 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_segments history via filter_duplicate_boundary_segments.
  • Post-process with post_process_segments (using the dictionary terms and PostProcessOptions).
  • Build a LiveResultMessage and emit_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_overlap and transcripts_loosely_overlap — token-coverage / longest-common-subsequence checks against LOW_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_THRESHOLD catches 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 as commands::audio.
  • pick_engine (:638) — state.whisper_engine or state.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::spawn for 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_inference busy-loops with 10 ms sleeps in drain_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-buffer MAX_PENDING_SAMPLES cap; it does NOT detect a JS-side stalled listener except via the emit_live_result-failure path.
  • ensure_model_loaded(state, engine, model_id, None) intentionally passes None for concurrent, so live sessions never trigger the sequential-GPU guard in commands::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 captureresolve_recording_path and recording_filename are shared.
  • Modelsdefault_model_id_for_engine and ensure_model_loaded are 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_window guard.
  • commands::modbuild_initial_prompt is the prompt assembler used here.