feat: backend-agnostic HF download helper with byte progress - #10
Merged
Conversation
2 tasks
wavekat-eason
force-pushed
the
feat/download-progress
branch
from
May 16, 2026 23:03
b72eb49 to
7d15697
Compare
Adds `wavekat_asr::download::download_files_with_progress(repo_id, files, dest_dir, on_progress)` and `DownloadProgress` — a backend- agnostic helper for streaming a fixed list of files from HuggingFace into a caller-chosen directory while a `FnMut` closure receives byte- level progress updates. Gated behind a new `download` Cargo feature that the `sherpa-onnx` feature now turns on transitively, so any future backend whose model weights live on HF Hub (Whisper, Qwen3-ASR-local, etc.) can enable `download` and reuse the same helper instead of reimplementing it. The sherpa-onnx backend keeps its convenience wrapper: `backends::sherpa_onnx::download_preset_with_progress(preset, dest_dir, on_progress)`. It's now a 4-line shim over the generic helper that turns a `ModelPreset` into a `(model_id, &[files])` pair via the also-new `ModelPreset::files()`. Behavior notes preserved from the first iteration: - Streams byte-level progress per file (file_index/file_count, bytes_done, bytes_total). - Copies each file into a caller-chosen `dest_dir` rather than only populating hf-hub's cache, so consumers can ship a "model lives here" path users can introspect, move, or delete. - Synthesizes a final 100% event in `finish()` if hf-hub's last `update` short-counted, so a UI progress bar can't get stuck at 99%. Implementation note on the closure-capture trick: hf-hub's `Progress` trait wants ownership of the reporter per `download_with_progress` call, so the adapter holds a `&mut F` and is rebuilt per file — that lets one `FnMut(DownloadProgress)` closure span all files in a batch without `Rc<RefCell<…>>`. Tests (all without network access): - `download::tests::callback_progress_reports_init_update_and_finish` drives the hf-hub `Progress` bridge directly and asserts the init+update sequence reaches the user closure intact. - `download::tests::callback_progress_finish_synthesizes_completion` covers the short-count safeguard. - `backends::sherpa_onnx::tests::preset_files_transducer_includes_joiner` / `preset_files_paraformer_omits_joiner` cover the preset-shape glue. Verified locally: cargo fmt --check, cargo clippy --all-features -D warnings, cargo test --features sherpa-onnx (7 unit tests + 1 doc-test pass), cargo doc --all-features, cargo check on each of: default features, `--features download` alone, `--features sherpa-onnx`. Step 2 of 3 toward wavekat-asr 0.0.5. The wavekat-core 0.0.11 bump (#8) is the other prerequisite and is merged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
wavekat-eason
force-pushed
the
feat/download-progress
branch
from
May 16, 2026 23:27
7d15697 to
093c297
Compare
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a backend-agnostic helper for streaming HuggingFace files with byte-level progress, so any current or future backend whose model weights live on HF Hub can drive a download UI without reimplementing the protocol.
Structure:
wavekat_asr::downloadmodule (gated behind a newdownloadCargo feature):pub struct DownloadProgress { file, file_index, file_count, bytes_done, bytes_total }pub fn download_files_with_progress<F>(repo_id: &str, files: &[&str], dest_dir: &Path, on_progress: F) -> Result<(), AsrError> where F: FnMut(DownloadProgress)sherpa-onnxbackend keeps a small convenience wrapper:pub fn backends::sherpa_onnx::download_preset_with_progress(preset: ModelPreset, dest_dir, F) -> Result<(), AsrError>— 4-line shim over the generic helperpub fn ModelPreset::files(&self) -> Vec<&'static str>— HF file list in download orderdownload = ["dep:hf-hub"],sherpa-onnx = ["dep:sherpa-onnx", "download"]. Thedownloadfeature is independently usable, so a future Whisper or Qwen3-ASR backend can dowhisper = ["dep:whisper-rs", "download"]without pulling in sherpa-onnx.Why
A consumer driving a model-download UI needs byte progress to render a real progress bar. Without this helper they'd either shell out to hf-hub's CLI or reimplement HuggingFace's download protocol. The first version put the helper inside
backends::sherpa_onnx, but the mechanics (hf-hub call, progress bridge, copy intodest_dir) are entirely generic — only the input shape (which files does this preset need?) is backend-specific. Splitting at that seam means a future Whisper / Qwen3-ASR local backend lands a thin wrapper, not a re-implementation.Behavior
file_index/file_count,bytes_done,bytes_total).dest_dir(not just rename), so the cache stays consistent and the caller gets adest_dirthey can move / delete independently.finish()if hf-hub's lastupdateshort-counted, so a UI progress bar can't stick at 99%.Implementation note
hf-hub's
Progresstrait takes ownership of the reporter perdownload_with_progresscall, so the bridge (CallbackProgress<'_, F>) borrows&mut Fand is rebuilt per iteration. That lets oneFnMut(DownloadProgress)closure span all files in a batch withoutRc<RefCell<…>>or restricting the callback toFn. Trade-off: the closure can't beSendacross threads — fine for the synchronous download path.Tests
All without network access:
download::tests::callback_progress_reports_init_update_and_finish— init + 2 updates = 3 emissions reaching the user closure;finish()adds no extra event when bytes already match.download::tests::callback_progress_finish_synthesizes_completion— when hf-hub's lastupdateshort-counts,finish()emits the synthetic 100% event.backends::sherpa_onnx::tests::preset_files_transducer_includes_joiner—BILINGUAL_ZH_EN.files()returns 4 names in encoder/decoder/joiner/tokens order.backends::sherpa_onnx::tests::preset_files_paraformer_omits_joiner—PARAFORMER_ZH.files()returns 3 names with no joiner.Verified locally on the branch:
cargo fmt --all -- --checkcargo clippy --workspace --all-features -- -D warningscargo test --workspace --features sherpa-onnx— 7 unit tests + 1 doc-test passcargo doc --no-deps -p wavekat-asr --all-featurescargo check --workspace(default features),cargo check --workspace --features download(alone), andcargo check --workspace --features sherpa-onnxall build — confirming thedownloadfeature is independently usable and the cfg gates are correct.Follow-up
Once this lands (#8 is already merged), release-plz will open a
chore: release v0.0.5PR; merging it publishes 0.0.5 to crates.io. Downstream consumers can then calldownload_preset_with_progress(for sherpa-onnx) ordownload_files_with_progress(generic) from their own model-download flow.🤖 Generated with Claude Code