Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Sources/Config/ModelCatalogASRFiles.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
18 changes: 18 additions & 0 deletions Tests/OpenTypeTests/ConfigurationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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.
Loading