From c298a4758450eade67ba0b398ceaa604171340c0 Mon Sep 17 00:00:00 2001 From: idevlab Date: Wed, 30 Sep 2026 15:07:14 +0800 Subject: [PATCH] fix: verify FireRedASR2 against real upstream files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The FireRedASR2-AED (小红书) required-file list referenced tokenizer.json, which does not exist upstream, so a completed download was always reported incomplete. Match the manifest and the mlx-audio-swift loader instead and add a regression test. --- Sources/Config/ModelCatalogASRFiles.swift | 2 +- Tests/OpenTypeTests/ConfigurationTests.swift | 18 +++++ .../intent.md | 57 ++++++++++++++ .../plan.md | 29 ++++++++ .../spec.md | 74 +++++++++++++++++++ .../verification.md | 52 +++++++++++++ 6 files changed, 231 insertions(+), 1 deletion(-) create mode 100644 docs/sdlc/changes/2026-09-30-firered-asr-download-completeness/intent.md create mode 100644 docs/sdlc/changes/2026-09-30-firered-asr-download-completeness/plan.md create mode 100644 docs/sdlc/changes/2026-09-30-firered-asr-download-completeness/spec.md create mode 100644 docs/sdlc/changes/2026-09-30-firered-asr-download-completeness/verification.md diff --git a/Sources/Config/ModelCatalogASRFiles.swift b/Sources/Config/ModelCatalogASRFiles.swift index c663a1a..5e52c5a 100644 --- a/Sources/Config/ModelCatalogASRFiles.swift +++ b/Sources/Config/ModelCatalogASRFiles.swift @@ -30,7 +30,7 @@ extension ModelCatalog { } return required case "mlx-community/FireRedASR2-AED-mlx": - return ["config.json", "tokenizer.json"] + return ["config.json", "cmvn.json", "dict.txt", "model.safetensors"] case "mlx-community/Mega-ASR-6bit": return ["config.json", "tokenizer_config.json"] default: diff --git a/Tests/OpenTypeTests/ConfigurationTests.swift b/Tests/OpenTypeTests/ConfigurationTests.swift index 551c136..f602dc3 100644 --- a/Tests/OpenTypeTests/ConfigurationTests.swift +++ b/Tests/OpenTypeTests/ConfigurationTests.swift @@ -148,6 +148,24 @@ final class ConfigurationTests: XCTestCase { XCTAssertTrue(ModelCatalog.asrRepoContainsRequiredFiles(QwenASRModel.defaultID, at: dir)) } + @MainActor + func testFireRedASRCompletenessMatchesUpstreamManifest() throws { + let id = "mlx-community/FireRedASR2-AED-mlx" + let required = ModelCatalog.asrRequiredFiles(for: id) + XCTAssertEqual(required, ["config.json", "cmvn.json", "dict.txt", "model.safetensors"]) + XCTAssertFalse(required.contains("tokenizer.json")) + + let dir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) + defer { try? FileManager.default.removeItem(at: dir) } + + try writeTestFiles(["config.json", "cmvn.json", "dict.txt"], under: dir) + XCTAssertFalse(ModelCatalog.asrRepoContainsRequiredFiles(id, at: dir)) + + try writeTestFiles(["model.safetensors"], under: dir) + XCTAssertTrue(ModelCatalog.asrRepoContainsRequiredFiles(id, at: dir)) + } + func testAudioCaptureActivityDetectsSilence() { var activity = AudioCaptureActivity() activity.record(rms: 0, frameCount: 16_000) diff --git a/docs/sdlc/changes/2026-09-30-firered-asr-download-completeness/intent.md b/docs/sdlc/changes/2026-09-30-firered-asr-download-completeness/intent.md new file mode 100644 index 0000000..095fdf2 --- /dev/null +++ b/docs/sdlc/changes/2026-09-30-firered-asr-download-completeness/intent.md @@ -0,0 +1,57 @@ +# Intent: Fix FireRedASR2 (小红书) model download completeness + +**Status:** approved +**Approved-by:** chenli (explicit approval in this chat) +**Approved-date:** 2026-09-30 +**Upstream:** — +**Risk:** medium — model/runtime behavior + +## Problem + +The `mlx-community/FireRedASR2-AED-mlx` local ASR model (小红书 FireRedASR2) +downloads, but after a successful transfer Utter always reports the model as +incomplete (`model.asr_incomplete`, "模型只下载了一部分。点击继续下载即可接着完成"). +The user therefore cannot download a complete model, and the model can never be +selected as ready. + +The download itself completes. The published repository is validated by +`ModelCatalog.asrRepoContainsRequiredFiles`, which checks the list returned by +`ModelCatalog.asrRequiredFiles(for:)`. For this model the list is +`["config.json", "tokenizer.json"]`. The upstream repository does not contain +`tokenizer.json`, so the completeness check can never pass. + +## Outcome + +After a successful `snapshot` download of `mlx-community/FireRedASR2-AED-mlx`, +Utter verifies the model against the files that actually exist in the upstream +repository and that the Swift runtime requires, marks it `.downloaded`, and the +model becomes usable. + +## Scope + +- Affected: the `FireRedASR2-AED` entry of `ModelCatalog.asrRequiredFiles`, used + by download status, retry/recovery, and `MLXSTTEngine` readiness. +- Non-goal: changing the upstream repository, the download transport, the + FireRed engine, or the other ASR models' required-file lists. + +## Constraints + +- The required-file list must contain only files present in the upstream + repository and required to load the model with the pinned + `mlx-audio-swift` 0.1.3 `FireRedASR2Model.fromDirectory` implementation. +- No network access is required at test time; the check is path-based. +- Existing Qwen, Confucius, Mega-ASR, Whisper, and LLM behavior must not change. + +## Acceptance criteria + +- `ModelCatalog.asrRequiredFiles(for: "mlx-community/FireRedASR2-AED-mlx")` + equals `["config.json", "cmvn.json", "dict.txt", "model.safetensors"]` and no + longer references the nonexistent `tokenizer.json`. +- `asrRepoContainsRequiredFiles` returns `false` when a required file (for + example the safetensors weights) is absent and `true` for a directory holding + the upstream manifest. +- A regression test fails on the previous list and passes on the corrected one. + +## Open questions + +None. diff --git a/docs/sdlc/changes/2026-09-30-firered-asr-download-completeness/plan.md b/docs/sdlc/changes/2026-09-30-firered-asr-download-completeness/plan.md new file mode 100644 index 0000000..4fb7676 --- /dev/null +++ b/docs/sdlc/changes/2026-09-30-firered-asr-download-completeness/plan.md @@ -0,0 +1,29 @@ +# Plan: Fix FireRedASR2 (小红书) model download completeness + +**Status:** approved +**Approved-by:** chenli (explicit approval in this chat) +**Approved-date:** 2026-09-30 +**Upstream:** docs/sdlc/changes/2026-09-30-firered-asr-download-completeness/spec.md +**Risk:** medium — model/runtime behavior + +## Work items + +- [x] Correct the `mlx-community/FireRedASR2-AED-mlx` case in + `Sources/Config/ModelCatalogASRFiles.swift` to + `["config.json", "cmvn.json", "dict.txt", "model.safetensors"]`. +- [x] Add a regression test in `Tests/OpenTypeTests/ConfigurationTests.swift` + that pins the exact required list and exercises + `asrRepoContainsRequiredFiles` with and without the weight file. +- [x] Record commands, results, and residual risk in `verification.md`. + +## Verification plan + +- [ ] `bash scripts/sdlc-checks.sh` +- [ ] `bash scripts/ci-basic-checks.sh` +- [ ] `swift test` +- [ ] Targeted regression test fails on the pre-fix list and passes after. + +## Human gates + +- Intent and design approval: granted in this chat (2026-09-30). +- PR approval: required before merge. diff --git a/docs/sdlc/changes/2026-09-30-firered-asr-download-completeness/spec.md b/docs/sdlc/changes/2026-09-30-firered-asr-download-completeness/spec.md new file mode 100644 index 0000000..e620e19 --- /dev/null +++ b/docs/sdlc/changes/2026-09-30-firered-asr-download-completeness/spec.md @@ -0,0 +1,74 @@ +# Spec: Fix FireRedASR2 (小红书) model download completeness + +**Status:** approved +**Approved-by:** chenli (explicit confirmation in this chat) +**Approved-date:** 2026-09-30 +**Upstream:** docs/sdlc/changes/2026-09-30-firered-asr-download-completeness/intent.md +**Risk:** medium — model/runtime behavior + +## Context + +ASR downloads for the generic MLX STT models run through +`ModelCatalog.downloadASR` -> `performASRDownload` -> `HubApi.snapshot`. When the +transfer returns, `asrRepoIsComplete` calls +`asrRepoContainsRequiredFiles(repositoryID, at:)`, which requires every path in +`asrRequiredFiles(for:)` to exist with a nonzero size. The same list gates +`MLXSTTEngine.checkModelReady` and therefore whether the engine is considered +usable. + +The upstream `mlx-community/FireRedASR2-AED-mlx` repository manifest (verified +via the Hugging Face tree API) is: `.gitattributes`, `cmvn.json`, `config.json`, +`dict.txt`, `model.safetensors`, `train_bpe1000.model`. The catalog listed +`tokenizer.json`, which is absent. + +The pinned `mlx-audio-swift` 0.1.3 `FireRedASR2Model` confirms the runtime +requirements: + +- `fromDirectory` throws if `config.json` is missing and loads every + `*.safetensors` file in the directory to build weights. +- `loadAssets(from:)` reads `cmvn.json` for CMVN statistics and constructs the + tokenizer from `dict.txt`; the tokenizer is required to decode output. + +`train_bpe1000.model` is only used by the Python reference and is not read by +the Swift runtime, so it is not required. + +## Design + +Change only the FireRed case of `ModelCatalog.asrRequiredFiles`: + +```swift +case "mlx-community/FireRedASR2-AED-mlx": + return ["config.json", "cmvn.json", "dict.txt", "model.safetensors"] +``` + +No interfaces, state, or data flow change. The check remains path-based and +model-scoped, so recovery, retry, and readiness continue to use the same code +paths with a corrected manifest. + +Non-goals: the download transport, staging/publish machinery, and the other +model entries are untouched. `Mega-ASR-6bit`'s existing list is left as-is; its +referenced files exist upstream. + +## Safety and failure modes + +- Under-counting required files could mark a broken download complete. The list + includes the safetensors weights, `cmvn.json`, and `dict.txt`, which are the + files the runtime needs; a missing weight or asset keeps the status as + incomplete and the retry path can recover. +- No privacy, security, or network-boundary change is introduced. +- Rollback is a single-line revert; no stored data is migrated. + +## Test strategy + +- A unit test asserts the exact required list and that it no longer contains + `tokenizer.json`. +- The same test drives `asrRepoContainsRequiredFiles` over a temporary directory + holding the upstream manifest, asserting `false` before the weights are + present and `true` after. +- Full `swift test` and repository checks confirm no other model regressed. + +## Rollout and rollback + +Ship in a normal app release. Observation: after downloading FireRedASR2-AED the +status becomes `.downloaded` and the engine reports ready. Rollback: revert the +required-file list if a runtime asset turns out to differ. diff --git a/docs/sdlc/changes/2026-09-30-firered-asr-download-completeness/verification.md b/docs/sdlc/changes/2026-09-30-firered-asr-download-completeness/verification.md new file mode 100644 index 0000000..8a2f692 --- /dev/null +++ b/docs/sdlc/changes/2026-09-30-firered-asr-download-completeness/verification.md @@ -0,0 +1,52 @@ +# Verification: Fix FireRedASR2 (小红书) model download completeness + +**Status:** approved +**Approved-by:** chenli (explicit approval in this chat) +**Approved-date:** 2026-09-30 +**Upstream:** docs/sdlc/changes/2026-09-30-firered-asr-download-completeness/plan.md +**Risk:** medium — model/runtime behavior + +## Evidence + +| Check | Result | Evidence | +|---|---|---| +| Regression test on pre-fix list | Fails (bug reproduced) | 3 assertion failures: `["config.json", "tokenizer.json"]` vs expected list; `asrRepoContainsRequiredFiles` returned `true`/`false` incorrectly | +| `swift test --filter testFireRedASRCompletenessMatchesUpstreamManifest` | Pass | `Executed 1 test, with 0 failures` | +| `swift test` | Pass | `Executed 794 tests, with 18 tests skipped and 0 failures` | +| `bash scripts/sdlc-checks.sh` | Pass | `SDLC checks passed.` | +| `bash scripts/ci-basic-checks.sh` | Pass | all checks passed | + +Reproduced failure before the fix: + +```text +ConfigurationTests.swift:155: XCTAssertEqual failed: ("["config.json", "tokenizer.json"]") is not equal to ("["config.json", "cmvn.json", "dict.txt", "model.safetensors"]") +ConfigurationTests.swift:156: XCTAssertFalse failed +ConfigurationTests.swift:166: XCTAssertTrue failed +``` + +Upstream manifest cross-checked through the Hugging Face tree API for +`mlx-community/FireRedASR2-AED-mlx`: `.gitattributes`, `cmvn.json`, +`config.json`, `dict.txt`, `model.safetensors`, `train_bpe1000.model`. The +pinned `mlx-audio-swift` 0.1.3 `FireRedASR2Model.fromDirectory` reads +`config.json`, `*.safetensors`, `cmvn.json`, and `dict.txt`. + +## Acceptance criteria + +- Required list equals `["config.json", "cmvn.json", "dict.txt", "model.safetensors"]` and omits `tokenizer.json` — pass (test assertion). +- `asrRepoContainsRequiredFiles` is `false` before the weights exist and `true` + with the upstream manifest — pass (test assertions). +- Regression test fails on the previous list and passes on the corrected one — + pass (pre-fix run failed, post-fix run passed). + +## Residual risk + +- `Mega-ASR-6bit` still requires only `["config.json", "tokenizer_config.json"]`. + Both files exist upstream, so its download completes; tightening its list to + include weights is out of scope for this change and listed here as residual + risk with owner `chenli`. +- `train_bpe1000.model` is intentionally not required because the Swift runtime + does not read it. + +## Decision + +Ready for review. Human approval is recorded separately.