Fix Cohere/MLX local decode loop on long recordings#1
Merged
Conversation
added 2 commits
May 11, 2026 18:05
…ctor Local Cohere transcription collapsed into "I think that's what I'm hearing" ×12 on the first real meeting recording (583s audio, 140 word output) while ElevenLabs cloud returned 1790 words on the same file. Root cause was in the upstream mlx-audio-swift (1200s default chunk, repetition penalty never applied in the Cohere decoder, chunkedParameters dropped penalty config) plus the Scribe wrapper forwarding every broken default verbatim. This change: - Pins mlx-audio-swift to Newarr/mlx-audio-swift@b8ec430 with four targeted patches: anti-loop backstop, sign-aware repetition penalty, chunkedParameters propagation, and per-chunk token budget. Patches are documented in the fork's README. - Overrides upstream defaults at the wrapper layer (CohereMLXBackend.inferenceChunkDurationSeconds=30, repetitionPenalty=1.2, repetitionContextSize=32) via a new makeGenerationParameters static helper on NativeCohereMLXAdapter that is regression-tested. - Adds DegenerateOutputDetector as a pure-Swift safety net (tri-gram density and unique-word fraction) that throws CohereMLXBackendError.degenerateOutput when the model still emits loop output. Worker classifies this as non-transient so the user sees a clear failed transcript rather than a useless one. Tests cover the override regression guard, the detector, and the backend's throw behavior. An env-gated integration test (SCRIBE_RUN_MLX_INTEGRATION=1) runs the real MLX model on the failing recording for end-to-end verification. TranscriberApp/project.yml mirrors the Package.swift dependency swap so xcodebuild + xcodegen produce a binary against the patched fork.
xcodegen regenerates project.pbxproj from project.yml, which now points at Newarr/mlx-audio-swift@b8ec430 for the patched Cohere decode loop. Old UUIDs for the package reference are reissued — no other behavioral change.
3 tasks
Newarr
added a commit
that referenced
this pull request
May 11, 2026
Follow-up cleanups on top of the Cohere/MLX decode-loop fix (PR #1): - Hoist `.lowercased()` out of the tri-gram loop in `DegenerateOutputDetector.evaluate` (was lowercasing each word 3x per iteration plus once more for the unique-word check; now once up front). - Drop the unreachable `if words.count >= 3` guard inside the `>= 30` branch. - Trim bug-story narration from the type-level doc comments (`CohereMLXBackendError`, `DegenerateOutputDetector`) - the changelog belongs in the PR, not the source. - Replace the `try?` chain on the fixture load in `testDegenerateOutputDetectorFlagsRepetitiveTranscripts` with `try` so a missing/broken fixture fails loudly instead of silently asserting on an empty string. - Make the integration test audio path overridable via `SCRIBE_MLX_INTEGRATION_AUDIO` (the dated default path is kept for back-compat). No behavior change. 365 tests still pass (1 integration test skipped). Co-authored-by: Szymon Sypniewicz <szymonsypniewicz@szymons-mac.local>
2 tasks
Newarr
pushed a commit
that referenced
this pull request
May 11, 2026
Two follow-ups to PR #1 raised by an independent codex review: 1. Privacy: drop `sample` from `CohereMLXBackendError.degenerateOutput`. `TranscriptionWorker` persists `String(describing: error)` into the failed `transcript.md` and `metadata.json` artifacts, so the 120-char transcript snippet that used to ride along on the error was landing in on-disk failure metadata. The sample now stays in the unified log at `.private` privacy level for live debugging only. 2. False-positive floor: require the dominant tri-gram to repeat at least 5 times before firing, in addition to the >8% fraction check. At the 30-word minimum, 3 occurrences of an ordinary tri-gram (e.g. "thank you very") cleared the fraction gate at 3/28 ≈ 11% and terminally failed otherwise-usable short transcripts. The observed real-world loop fires 12+ times so the new floor is still well within range. New regression test guards the short-transcript-with-three-repeats case; the existing throw test now also asserts that `String(describing:)` on the error does NOT contain transcript content.
3 tasks
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
Local Cohere transcription collapsed into
"I think that's what I'm hearing" ×12on the first real meeting recording — 583 s of audio produced ~140 words while ElevenLabs cloud returned 1,790 words on the same file. Root cause was a four-part bug across the upstreammlx-audio-swiftdecode path and the Scribe wrapper forwarding unsafe defaults verbatim.What changed
Upstream fork (
Newarr/mlx-audio-swift@b8ec430, diverges fromBlaizzy/mlx-audio-swift@7734cd1):CohereTranscribe.generateSingleChunkandgenerateStream— breaks the decode loop when the last 24 tokens contain ≤3 unique IDs.Qwen3ASR.swift.chunkedParametersnow propagatesrepetitionPenaltyandrepetitionContextSize(it dropped them before).remainingTokensaccumulator ingenerate/generateStreammulti-chunk paths — each chunk gets the full budget;effectiveMaxGenerationTokensalready clamps.Wrapper-side overrides (
CohereMLXBackend):inferenceChunkDurationSeconds=30(model card documents 35 s training; upstream default 1200 s loops)inferenceRepetitionPenalty=1.2,inferenceRepetitionContextSize=32NativeCohereMLXAdapter.makeGenerationParametersstatic helper, regression-testedSafety net:
DegenerateOutputDetector— pure-Swift tri-gram density + unique-word fraction checksCohereMLXBackendError.degenerateOutputif the model still emits loop output;TranscriptionWorker.isTransientdoes not classify this, so it surfaces as a terminal failed transcript instead of a useless oneVerification
SCRIBE_RUN_MLX_INTEGRATION=1integration test skipped by default; passes when manually run with model weights present~/Scribe/2026-05-11-1756/audio.m4a):Test plan
swift testis clean (already verified)scripts/dev-install.sh --buildproduces a signed app (already verified)