Skip to content

feat: backend-agnostic HF download helper with byte progress - #10

Merged
wavekat-eason merged 1 commit into
mainfrom
feat/download-progress
May 16, 2026
Merged

feat: backend-agnostic HF download helper with byte progress#10
wavekat-eason merged 1 commit into
mainfrom
feat/download-progress

Conversation

@wavekat-eason

@wavekat-eason wavekat-eason commented May 16, 2026

Copy link
Copy Markdown
Contributor

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:

  • New wavekat_asr::download module (gated behind a new download Cargo 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-onnx backend 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 helper
    • pub fn ModelPreset::files(&self) -> Vec<&'static str> — HF file list in download order
  • Feature wiring: download = ["dep:hf-hub"], sherpa-onnx = ["dep:sherpa-onnx", "download"]. The download feature is independently usable, so a future Whisper or Qwen3-ASR backend can do whisper = ["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 into dest_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

  • Streams byte-level progress per file (file_index/file_count, bytes_done, bytes_total).
  • Copies each file from hf-hub's cache blob into dest_dir (not just rename), so the cache stays consistent and the caller gets a dest_dir they can move / delete independently.
  • Synthesizes a final 100% event in finish() if hf-hub's last update short-counted, so a UI progress bar can't stick at 99%.

Implementation note

hf-hub's Progress trait takes ownership of the reporter per download_with_progress call, so the bridge (CallbackProgress<'_, F>) borrows &mut F and is rebuilt per iteration. That lets one FnMut(DownloadProgress) closure span all files in a batch without Rc<RefCell<…>> or restricting the callback to Fn. Trade-off: the closure can't be Send across 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 last update short-counts, finish() emits the synthetic 100% event.
  • backends::sherpa_onnx::tests::preset_files_transducer_includes_joinerBILINGUAL_ZH_EN.files() returns 4 names in encoder/decoder/joiner/tokens order.
  • backends::sherpa_onnx::tests::preset_files_paraformer_omits_joinerPARAFORMER_ZH.files() returns 3 names with no joiner.

Verified locally on the branch:

  • cargo fmt --all -- --check
  • cargo clippy --workspace --all-features -- -D warnings
  • cargo test --workspace --features sherpa-onnx — 7 unit tests + 1 doc-test pass
  • cargo doc --no-deps -p wavekat-asr --all-features
  • cargo check --workspace (default features), cargo check --workspace --features download (alone), and cargo check --workspace --features sherpa-onnx all build — confirming the download feature 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.5 PR; merging it publishes 0.0.5 to crates.io. Downstream consumers can then call download_preset_with_progress (for sherpa-onnx) or download_files_with_progress (generic) from their own model-download flow.

🤖 Generated with Claude Code

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
wavekat-eason force-pushed the feat/download-progress branch from 7d15697 to 093c297 Compare May 16, 2026 23:27
@wavekat-eason wavekat-eason changed the title feat(sherpa-onnx): progress-aware preset downloader feat: backend-agnostic HF download helper with byte progress May 16, 2026
@wavekat-eason
wavekat-eason merged commit 99eca09 into main May 16, 2026
3 checks passed
@wavekat-eason
wavekat-eason deleted the feat/download-progress branch May 16, 2026 23:37
@github-actions github-actions Bot mentioned this pull request May 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant