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>
133 lines
8.6 KiB
Markdown
133 lines
8.6 KiB
Markdown
---
|
||
name: Model manager (download, verify, locate)
|
||
type: architecture-map-page
|
||
slice: 03-audio-transcription
|
||
last_verified: 2026/05/09
|
||
---
|
||
|
||
# Model manager (download, verify, locate)
|
||
|
||
> **Where you are:** [Architecture map](../README.md) → [Audio + Transcription](README.md) → Model manager
|
||
|
||
**Plain English summary.** Speech models live on disk under a per-platform path. `model_manager` resolves those paths, checks "is this model fully downloaded", lists what is downloaded, and downloads missing files with HTTP Range resume + per-chunk SHA256 verification. A single in-process `HashSet` reservation prevents concurrent downloads of the same model. After a successful download a `.magnotia-verified` manifest is written so a subsequent process restart can short-circuit re-hashing.
|
||
|
||
## At a glance
|
||
|
||
- Crate: `magnotia-transcription`
|
||
- Path: `crates/transcription/src/model_manager.rs`
|
||
- LOC: 617 (production code + extensive in-tree download server tests)
|
||
- External deps: `reqwest 0.12` (rustls-tls, stream), `futures-util 0.3` (StreamExt), `sha2 0.10`. Path resolution comes from `magnotia_core::paths::app_paths`. Catalogue from `magnotia_core::model_registry`.
|
||
- Internal callers (best effort, slice 2 reconciles): the `models` Tauri command consumes all five public functions. The frontend Settings → Models view drives `download` with a progress callback.
|
||
|
||
Public surface:
|
||
|
||
- `pub fn models_dir() -> PathBuf` — `crates/transcription/src/model_manager.rs:42`. Resolves to `%LOCALAPPDATA%/magnotia/models` on Windows, `~/.magnotia/models` on Unix (delegates to `magnotia_core::paths::app_paths`).
|
||
- `pub fn model_dir(id: &ModelId) -> PathBuf` — `crates/transcription/src/model_manager.rs:47`.
|
||
- `pub fn is_downloaded(id: &ModelId) -> bool` — `crates/transcription/src/model_manager.rs:52`.
|
||
- `pub fn list_downloaded() -> Vec<ModelId>` — `crates/transcription/src/model_manager.rs:63`.
|
||
- `pub async fn download(id: &ModelId, progress: impl Fn(DownloadProgress) + Send + 'static) -> Result<()>` — `crates/transcription/src/model_manager.rs:78`.
|
||
|
||
Private helpers:
|
||
|
||
- `DownloadReservation` — RAII guard (`model_manager.rs:12`).
|
||
- `verified_manifest_path` / `verified_manifest_matches` / `write_verified_manifest` (`model_manager.rs:115`–`:153`).
|
||
- `sha256_of_file` (`model_manager.rs:157`).
|
||
- `download_file` (`model_manager.rs:178`).
|
||
|
||
## What's in here
|
||
|
||
### Path resolution
|
||
|
||
`models_dir` and `model_dir` defer entirely to `magnotia_core::paths::app_paths()`. Anything platform-specific lives in slice 5; this crate just consumes the resolution.
|
||
|
||
### Download reservation
|
||
|
||
`DownloadReservation::acquire(id)` (`model_manager.rs:17`) inserts the model id into a process-wide `HashSet` guarded by a `LazyLock<Mutex<...>>`. Returns `Err(MagnotiaError::DownloadFailed("download already in progress for {id}"))` if the id is already present. Drop removes the entry. This protects against re-entry from the same process; cross-process races are still possible but the atomic `.part → dest` rename closes the most damaging window.
|
||
|
||
### `is_downloaded`
|
||
|
||
`model_manager.rs:52`. Two-step:
|
||
|
||
1. Every file declared in the registry entry must exist at `model_dir(id).join(filename)`.
|
||
2. The verified-manifest at `model_dir.join(".magnotia-verified")` must record the same `filename\tsha256\tsize` triple per file.
|
||
|
||
A missing or stale manifest causes `is_downloaded` to return `false` even if the bytes are correct. The next `download` call will revalidate and rewrite the manifest. A truncated or tampered file fails the existence + size check, so a subsequent `download` redownloads it.
|
||
|
||
### `list_downloaded`
|
||
|
||
`model_manager.rs:63`. Filters the static `magnotia_core::model_registry::all_models()` catalogue by `is_downloaded`.
|
||
|
||
### `download` (the orchestrator)
|
||
|
||
`model_manager.rs:78`. For each `ModelFile` in the registry entry:
|
||
|
||
1. If `dest` exists, hash it with `sha256_of_file`. Match → skip the download. Mismatch → delete and re-fetch. Hash IO error → propagate as `MagnotiaError::DownloadFailed`.
|
||
2. Otherwise call `download_file(file, dest, id, &progress)`.
|
||
|
||
After all files are present and verified, `write_verified_manifest` writes the `version\t1` header and per-file `filename\tsha256\tsize` lines.
|
||
|
||
### `download_file` (single file with resume + hash)
|
||
|
||
`model_manager.rs:178`. Implements the Buzz-style resume pattern:
|
||
|
||
1. Build a `reqwest::Client` with a 30 s connect timeout.
|
||
2. If `dest.with_extension("part")` exists, send a `Range: bytes={existing}-` header.
|
||
3. Inspect the response status:
|
||
- `206 Partial Content` → server is resuming. Continue appending.
|
||
- `200 OK` after a Range request → server ignored Range. Discard the stale `.part` and start fresh (regression-tested at `model_manager.rs:494`).
|
||
- `4xx`/`5xx` (non-resume path) → `MagnotiaError::DownloadFailed("download returned HTTP {status} for {filename}")`. Crucial because `reqwest` does not auto-error on bad status; without this check, a 404 body would be streamed into `.part` and renamed over the destination (regression at `model_manager.rs:556`).
|
||
- Other status on resume → propagate as unexpected.
|
||
4. Determine `total_bytes` from `Content-Range` (resume) or `Content-Length` (fresh).
|
||
5. Hasher state: when resuming, hash the on-disk `.part` first to catch the hash up to where the network stream is about to resume. Otherwise start fresh.
|
||
6. Stream the body in chunks, writing each to the file, updating the running SHA256, emitting `DownloadProgress` per percent step.
|
||
7. On stream end, finalise the hash. Mismatch → remove `.part` and `MagnotiaError::DownloadFailed("SHA256 mismatch for {filename}: expected ..., got ...")`.
|
||
8. `std::fs::rename(.part, dest)` — atomic finalise.
|
||
|
||
### `sha256_of_file`
|
||
|
||
`model_manager.rs:157`. 8 KiB buffer streaming hash. Used by `download` to validate an existing complete file before trusting it.
|
||
|
||
## Data flow
|
||
|
||
```
|
||
ModelId
|
||
└─ models_dir / model_dir → on-disk directory
|
||
└─ is_downloaded:
|
||
all files exist AND verified-manifest matches
|
||
→ true / false
|
||
└─ list_downloaded:
|
||
filter all_models() by is_downloaded
|
||
└─ download:
|
||
acquire reservation
|
||
for file in entry.files:
|
||
if dest exists:
|
||
sha256_of_file → match? skip : delete + re-fetch
|
||
else:
|
||
download_file:
|
||
[.part exists? Range request : full GET]
|
||
stream body → file + hasher + progress callback
|
||
match SHA256 → atomic rename
|
||
else → cleanup, error
|
||
write_verified_manifest
|
||
drop reservation
|
||
```
|
||
|
||
## Watch-outs
|
||
|
||
- **Reservation is per-process.** Two Magnotia instances (dev + release, two cargo runs) racing on the same model dir can collide. The `.part → dest` atomic rename is the only thing standing between them and a torn file.
|
||
- **`DownloadProgress` is emitted per percent step, not per byte.** A 10 MB file at 4G download speeds emits ~100 events; a 5 GB file emits the same 100 events. UI must not assume tight cadence.
|
||
- **Manifest schema is `version\t1` + `filename\tsha256\tsize` lines.** Bumping the version is a manual operation; a stale manifest after a registry update will simply trigger a redownload (waste, not corruption).
|
||
- **Cross-process file locking is not used.** A user opening Settings → Models in two windows of the same desktop session will trip the in-process reservation, but two physical machines syncing the same network home would not be detected.
|
||
- **Reqwest connect timeout is 30 s, no read timeout.** A stalled mid-stream connection blocks indefinitely. Acceptable for a foreground UI flow today; would need a `tokio::time::timeout` wrap if the download ever became headless.
|
||
- **`ModelFile::sha256` is `&'static str`.** The `leak` helper in tests reflects that production code reads a static catalogue compiled into the binary. Tampering with the catalogue requires a code change, not a runtime config.
|
||
- **`reqwest` is configured `default-features = false, features = ["rustls-tls", "stream"]`.** No native-tls fallback. Linux without ca-certificates will fail at connect.
|
||
- **`models_dir` is the user-data dir, not a cache dir.** A "clear cache" button in the OS would not nuke models. Documented behaviour.
|
||
|
||
## See also
|
||
|
||
- [Transcription engines overview](transcription-engines-overview.md) — `load_whisper` / `load_parakeet` are called against `model_dir(id)` outputs.
|
||
- [Tests and fixtures](tests-and-fixtures.md) — in-tree TcpListener-backed servers that exercise the resume + sha-mismatch + 5xx paths.
|
||
- `magnotia_core::model_registry::{find_model, all_models, ModelEntry, ModelFile}` (slice 5) — the source of URLs and hashes.
|
||
- `magnotia_core::paths::app_paths` (slice 5) — platform path resolution.
|
||
- `magnotia_core::types::DownloadProgress` (slice 5) — the progress event shape.
|