Five-slice navigable map of the entire codebase under
docs/architecture-map/. Each slice is a self-contained
breadcrumbed sub-tree:
01-frontend (16) Svelte/SvelteKit UI
02-tauri-runtime (26) src-tauri commands + lifecycle
03-audio-transcription (16) audio + transcription crates
04-llm-formatting-mcp (19) llm, ai-formatting, mcp, cloud
05-core-storage-hotkey-build core, storage, hotkey, workspace,
(26) CI, dev glue
Plus master README.md and data-flow-end-to-end.md tracing
audio bytes from microphone to FTS5 search to MCP read.
Generated by 5 parallel subagents on 2026/05/09 against
HEAD 3c47000. Each page has YAML frontmatter, file:line code
refs, sibling cross-links, plain-English summaries.
Aggregated debt surfaced (full lists in master README):
RB-08 macOS power assertion, schema head drift v14 vs v15,
VAD blocked on ort version conflict, streaming primitives
not wired into live.rs, no prompt versioning, MCP has no
auth, cloud-providers in-memory keystore, SettingsPage
2 484 LOC, commands/live.rs 1 737 LOC, dual theme system,
brand rename to Lumenote pending across the codebase.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
16 KiB
name, type, last_verified
| name | type | last_verified |
|---|---|---|
| Data flow end to end | architecture-map-data-flow | 2026/05/09 |
Data flow end to end
Where you are: Architecture map → Data flow
Plain English summary. This page walks one recording from beginning to end. You press the hotkey, you speak, your speech becomes text, that text lands on disk in SQLite, and later it can be searched from the UI or read by an external agent over MCP. Every step names the slice that owns it and the file where the work happens.
The shape of a session, at a glance
Hotkey press
│
▼
[Backend evdev listener] ──► [Tauri runtime forwards event] ──► [Frontend opens session]
│
▼
[Microphone capture]
│
▼
[Streaming resampler 16 kHz mono]
│
▼
[VAD / chunker]
│ per chunk
▼
[Whisper or Parakeet inference]
│
partial │ final
▼ ▼
[Live Channel] [Stop session]
│
▼
[Formatting pipeline]
│
▼
┌──────────────────┴──────────────────┐
▼ ▼
[Optional LLM cleanup] [Optional content tags]
│ │
└──────────────────┬──────────────────┘
▼
[Save transcript]
│
▼
[SQLite + FTS5 trigger]
│
▼
[Paste to focused window]
(later)
│
▼
[History page search] ◄─── [search_transcripts] ───► [MCP search_transcripts]
Phase 1: hotkey press
Owner: slice 05 (magnotia-hotkey, Linux only) → slice 02 (commands/hotkey.rs).
The Linux evdev listener (crates/hotkey/src/linux.rs) watches /dev/input/event* after a one-time access check. The user-configured combo (default Ctrl+Shift+R, parsed from a Tauri-style string by HotkeyCombo::from_tauri_str) is checked against modifier state and key events. On match the listener emits HotkeyEvent::Pressed over an mpsc::Sender it was given at startup. On release it emits HotkeyEvent::Released. Hotplug is supported. macOS and Windows fall back to the tauri-plugin-global-shortcut plugin via the same command surface.
The commands::hotkey::start_evdev_hotkey command (src-tauri/src/commands/hotkey.rs:67) holds the listener inside tauri::State and runs a forwarder task that translates each event into a Tauri global event:
magnotia:hotkey-pressed(payload())magnotia:hotkey-released(payload())
Slice 01's +layout.svelte listens for magnotia:hotkey-pressed and toggles recording on the active page.
Phase 2: session opens
Owner: slice 01 (DictationPage) → slice 02 (commands/live.rs).
DictationPage opens two typed Tauri 2 channels (tauri::ipc::Channel<LiveResultMessage> for results, tauri::ipc::Channel<LiveStatusMessage> for warnings / overload / error / finished) and calls invoke('start_live_transcription_session', ...) with the channels passed as arguments.
commands::live::start_live_transcription_session (the 1 737-LOC file at src-tauri/src/commands/live.rs) spawns the runtime task. The runtime owns:
- a
MicrophoneCapturefrommagnotia-audio - a
StreamingResamplerfrommagnotia-audio - a
WavWriterfor progressive crash-safe append - a chunker (
RmsVadChunkerfrommagnotia-transcription) - a
LocalEnginereference (already loaded intoAppStateat startup or load step) - a power assertion (
commands::power::PowerAssertion::begin) to keep the system awake (note: no-op on Linux / Windows / macOS today, see master debt list)
Phase 3: microphone capture
Owner: slice 03 (magnotia-audio).
MicrophoneCapture (crates/audio/src/capture.rs) opens the selected cpal input device. It rejects monitor sources, RMS-validates the first samples for "is this actually live audio", and forwards device hot-unplug as CaptureRuntimeError rather than panicking. Sample format is normalised to f32. Samples leave the capture thread as AudioChunk { samples, channels, sample_rate }.
Phase 4: resampling
Owner: slice 03 (magnotia-audio).
StreamingResampler (crates/audio/src/streaming_resample.rs) runs rubato sinc resampling to 16 kHz mono. The streaming variant is used here; a separate resample_to_16khz (crates/audio/src/resample.rs) is used for file imports in phase F (file-import path, see end of this document). The 16 kHz mono PCM is what every transcription engine downstream wants. The constant WHISPER_SAMPLE_RATE lives in crates/core/src/constants.rs (slice 05) and the pcm-processor.js AudioWorklet in static/ hard-codes the same number — they must move together.
Phase 5: voice-activity detection and chunking
Owner: slice 03 (magnotia-transcription).
The Silero-based VAD module in magnotia-audio (crates/audio/src/vad.rs) is a stub today (the candidate ort crate revision conflicts with Parakeet's transcribe-rs). Live capture therefore uses an energy-based fallback, RmsVadChunker, in magnotia-transcription (crates/transcription/src/streaming/rms_vad.rs). It groups samples into chunks at silence boundaries with hysteresis, returning VadChunk { samples, start_sec, end_sec } to the runtime. WAV append happens in parallel via WavWriter::append_chunk.
The
LocalAgreementchunker andtrim_buffer_to_commit_point(crates/transcription/src/streaming/) are unit-tested but not yet wired intocommands/live.rs. Future work.
Phase 6: inference
Owner: slice 03 (magnotia-transcription) using slice 05 (magnotia-core) for thread tuning.
For each completed chunk the runtime calls LocalEngine::transcribe_sync(samples, options). LocalEngine is mutex-guarded and holds whichever backend was loaded:
- Whisper.
WhisperRsBackend::transcribe(crates/transcription/src/whisper_rs_backend.rs) builds a freshWhisperStateper call, appliesset_initial_promptif provided, setsn_threadsfrommagnotia_core::tuning::inference_thread_count(Workload::Whisper, ...), runs Vulkan offload ifvulkan_loader_available()and the binary was built withwhisper-vulkan. - Parakeet.
SpeechModelAdapterwrapstranscribe_rsONNX with quantisation hard-coded toInt8. Same thread-count helper.
The result is TimedTranscript { segments, language, inference_ms }. Each Segment carries { start_time, end_time, text }. Segment is defined in crates/core/src/types.rs (slice 05) and is the lingua franca that crosses all three transcription, formatting, and storage seams.
Phase 7: live partial results back to UI
Owner: slice 02 (commands/live.rs) → slice 01 (DictationPage).
The runtime sends a LiveResultMessage::Chunk { chunk_id, segments, raw_text, language, duration } over the result Channel. DictationPage appends it to the visible transcript and (if the preview overlay window is open) emits preview-append on the Tauri event bus. The preview window listens and renders the partial.
Phase 8: stop session, finalise
Owner: slice 01 → slice 02 (commands/live.rs).
User releases the hotkey or clicks stop. +layout.svelte calls invoke('stop_live_transcription_session'). The runtime drains its chunker, writes the WAV trailer, releases the power assertion, and emits LiveStatusMessage::Finished on the status channel. The accumulated Segment[] is the input to the next phase.
Phase 9: formatting pipeline
Owner: slice 04 (magnotia-ai-formatting).
post_process_segments(segments, options) (crates/ai-formatting/src/pipeline.rs) runs in this order:
- Anti-hallucination drop.
is_hallucinationfilter rejects known Whisper noise patterns and single-token repetition (multi-token repetition is a known gap, see master debt list). - Filler removal. Configurable list of "um", "uh", "you know", etc.
- British English conversion. Regex set
to_british_english.-izeverbs convert;-izingforms slip through (known gap). - Repetition collapse. Single-token repetition collapse, separate from the anti-hallucination pass.
- Smart paragraph breaks. A new paragraph starts when the gap between segments exceeds
magnotia_core::constants::SMART_PARAGRAPH_GAP_SECS.
Output: a String of formatted plain text plus the structured Segment[].
Phase 10: optional LLM cleanup
Owner: slice 04 (magnotia-ai-formatting::llm_client → magnotia-llm).
If the user has a model loaded and "LLM cleanup" enabled, commands::llm::cleanup_transcript_text_cmd (src-tauri/src/commands/llm.rs:363) is called from the frontend. It composes:
CLEANUP_PROMPT(crates/ai-formatting/src/llm_client.rs:26, prompt-injection-hardened)- a per-profile dictionary suffix (read from
magnotia_storage::list_profile_termsandget_profile) - an optional style preset (
Email,Notes,Code)
LlmEngine::cleanup_text runs llama-cpp-2 with Workload::Llm thread tuning. Output is freeform (no GBNF). The cleaned string replaces the formatted text.
Phase 11: optional content tags
Owner: slice 04 (magnotia-llm).
Phase 9 of the brand rollout added extract_content_tags. The frontend can call extract_content_tags_cmd (src-tauri/src/commands/llm.rs:408). LlmEngine::extract_content_tags runs the CONTENT_TAGS_SYSTEM prompt under the CONTENT_TAGS_GRAMMAR GBNF and returns ContentTags { topic, intent } where intent is constrained to INTENT_CLOSED_SET (six values, also enforced redundantly in Rust). The result writes to transcripts.llm_tags (added in migration v14).
Phase 12: optional task extraction
Owner: slice 04 (magnotia-llm) → slice 05 (magnotia-storage).
If the user runs "extract tasks" on the transcript, commands::tasks::extract_tasks_from_transcript_cmd (src-tauri/src/commands/tasks.rs:346) calls LlmEngine::extract_tasks_with_feedback under the OPTIONAL_TASK_ARRAY_GRAMMAR GBNF. Returned tasks are inserted via magnotia_storage::insert_task.
Phase 13: persist
Owner: slice 02 (commands/transcripts.rs) → slice 05 (magnotia-storage).
Frontend calls invoke('add_transcript', ...). commands::transcripts::add_transcript builds an InsertTranscriptParams and calls magnotia_storage::insert_transcript (crates/storage/src/database.rs). The row lands in the transcripts table. The transcripts_ai AFTER INSERT trigger (crates/storage/src/migrations.rs, migration v9 rebuild) updates the transcripts_fts external-content FTS5 table for text and title. UUIDs come from Uuid::new_v4() (the storage Cargo.toml comment still claims v7, see master debt list).
Phase 14: paste to focused window
Owner: slice 02 (commands/paste.rs).
In parallel with persist, the frontend calls paste_text or paste_text_replacing. The 26 kB commands/paste.rs handles cross-platform paste, including target-window detection and pre-existing-selection replacement on Linux (Wayland handling, X11 fallback) and platform-specific clipboard plumbing.
Phase 15: search later
Owner: slice 01 (HistoryPage) → slice 02 (commands/transcripts.rs) → slice 05 (magnotia-storage).
In HistoryPage the user types a query. The page calls invoke('search_transcripts', { query }). commands::transcripts::search_transcripts calls magnotia_storage::search_transcripts which runs a parameterised SELECT against transcripts_fts MATCH ?. Results merge with row metadata from transcripts and return ranked. The Porter stemmer + unicode61 + remove_diacritics 2 tokenizer was chosen at FTS5 table creation time.
Phase F: file import (alternative entry path)
Owner: slice 01 (FilesPage) → slice 02 (commands/transcription.rs) → slice 03.
If the user drops an audio file instead of dictating, the entry point is transcribe_file rather than the live runtime. The slice 03 path becomes:
decode_audio_file_limited(symphonia) for input format support (mp3, aac, flac, pcm, vorbis, wav, ogg, isomp4)resample_to_16khz(file resampler)- a single
LocalEngine::transcribe_synccall over the whole buffer - formatting + persist as in phases 9 to 14, with no live channels involved
Phase M: MCP read by an external agent
Owner: slice 04 (magnotia-mcp) → slice 05 (magnotia-storage::init_readonly).
A separate magnotia-mcp binary (Cargo target in crates/mcp/) implements the Model Context Protocol revision 2024-11-05 over JSON-RPC 2.0 on stdio. At startup it opens the same SQLite database via magnotia_storage::init_readonly (connection mode ro). Four tools:
list_transcriptsget_transcriptsearch_transcriptslist_tasks
Each tool maps to the corresponding magnotia_storage function. There is no auth at the MCP layer; stdio is the trust boundary. Any future HTTP / SSE transport needs an auth design before it ships.
Boundary contracts at a glance
| Boundary | Type | Defined in |
|---|---|---|
| Frontend ↔ Tauri | invoke() arg structs + return types (serde) |
each #[tauri::command] signature in src-tauri/src/commands/*.rs |
| Frontend ↔ Tauri (events) | string event names + JSON payloads | listed in 01-frontend/frontend-tauri-bridge.md and 02-tauri-runtime/README.md |
| Live session | tauri::ipc::Channel<LiveResultMessage>, Channel<LiveStatusMessage> |
src-tauri/src/commands/live.rs |
| Transcription engines | Transcriber trait + Segment |
crates/transcription/src/lib.rs, crates/core/src/types.rs |
| Formatting input/output | Segment[] in, String out |
crates/ai-formatting/src/pipeline.rs |
| LLM surfaces | typed Rust args, GBNF-validated JSON output, parsed into typed Rust | crates/llm/src/lib.rs, crates/llm/src/grammars.rs |
| Storage | InsertTranscriptParams, TranscriptRow, TaskRow, ProfileRow, etc |
crates/storage/src/database.rs |
| MCP | JSON-RPC 2.0 messages over stdio | crates/mcp/src/lib.rs |