Files
Lumotia/docs/architecture-map/03-audio-transcription/transcription-model-manager.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

133 lines
8.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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 `.lumotia-verified` manifest is written so a subsequent process restart can short-circuit re-hashing.
## At a glance
- Crate: `lumotia-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 `lumotia_core::paths::app_paths`. Catalogue from `lumotia_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%/lumotia/models` on Windows, `~/.lumotia/models` on Unix (delegates to `lumotia_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 `lumotia_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(".lumotia-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 `lumotia_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 Lumotia 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.
- `lumotia_core::model_registry::{find_model, all_models, ModelEntry, ModelFile}` (slice 5) — the source of URLs and hashes.
- `lumotia_core::paths::app_paths` (slice 5) — platform path resolution.
- `lumotia_core::types::DownloadProgress` (slice 5) — the progress event shape.