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>
8.6 KiB
name, type, slice, last_verified
| name | type | slice | last_verified |
|---|---|---|---|
| Model manager (download, verify, locate) | architecture-map-page | 03-audio-transcription | 2026/05/09 |
Model manager (download, verify, locate)
Where you are: Architecture map → Audio + Transcription → 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 fromlumotia_core::paths::app_paths. Catalogue fromlumotia_core::model_registry. - Internal callers (best effort, slice 2 reconciles): the
modelsTauri command consumes all five public functions. The frontend Settings → Models view drivesdownloadwith a progress callback.
Public surface:
pub fn models_dir() -> PathBuf—crates/transcription/src/model_manager.rs:42. Resolves to%LOCALAPPDATA%/lumotia/modelson Windows,~/.lumotia/modelson Unix (delegates tolumotia_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:
- Every file declared in the registry entry must exist at
model_dir(id).join(filename). - The verified-manifest at
model_dir.join(".lumotia-verified")must record the samefilename\tsha256\tsizetriple 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:
- If
destexists, hash it withsha256_of_file. Match → skip the download. Mismatch → delete and re-fetch. Hash IO error → propagate asMagnotiaError::DownloadFailed. - 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:
- Build a
reqwest::Clientwith a 30 s connect timeout. - If
dest.with_extension("part")exists, send aRange: bytes={existing}-header. - Inspect the response status:
206 Partial Content→ server is resuming. Continue appending.200 OKafter a Range request → server ignored Range. Discard the stale.partand start fresh (regression-tested atmodel_manager.rs:494).4xx/5xx(non-resume path) →MagnotiaError::DownloadFailed("download returned HTTP {status} for {filename}"). Crucial becausereqwestdoes not auto-error on bad status; without this check, a 404 body would be streamed into.partand renamed over the destination (regression atmodel_manager.rs:556).- Other status on resume → propagate as unexpected.
- Determine
total_bytesfromContent-Range(resume) orContent-Length(fresh). - Hasher state: when resuming, hash the on-disk
.partfirst to catch the hash up to where the network stream is about to resume. Otherwise start fresh. - Stream the body in chunks, writing each to the file, updating the running SHA256, emitting
DownloadProgressper percent step. - On stream end, finalise the hash. Mismatch → remove
.partandMagnotiaError::DownloadFailed("SHA256 mismatch for {filename}: expected ..., got ..."). 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 → destatomic rename is the only thing standing between them and a torn file. DownloadProgressis 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\tsizelines. 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::timeoutwrap if the download ever became headless. ModelFile::sha256is&'static str. Theleakhelper 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.reqwestis configureddefault-features = false, features = ["rustls-tls", "stream"]. No native-tls fallback. Linux without ca-certificates will fail at connect.models_diris 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 —
load_whisper/load_parakeetare called againstmodel_dir(id)outputs. - Tests and fixtures — 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.