Repository navigation
Move a mic back to the engine after two empty pinned takes - #1880
Merged
Merged
Conversation
1.1.66 put plain built-in and wired mics on the pinned recorder for a faster start. On one M1 MacBook Air every held dictation it recorded came back with no words (six "no speech", one kept for recovery), while the same Mac dictated through the engine minutes earlier on 1.1.64. The recorder reported no restart, fallback or silent input, so it could not catch this. Each take the recorder made only for speed is now scored once STTRouter.transcribe knows the result. Two empty takes in a row move that mic back to the engine path, and the start click back to after recording starts, for the rest of the app version. Takes the recorder is required for (a skipped Bluetooth input, a picked mic) are never scored. The move reports pinned_microphone_fell_back_to_engine with stage empty_takes plus the take's channels, rate and bucketed gaps, dropped callbacks and restarts, so the next report can tell a wrong layout from lost buffers. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016DYGa1i8HDv497ewpWgaCM
The privacy doc still called the pinned_mic_*_bucket fields meeting-only and didn't list input_channels or input_rate_hz on the dictation fallback. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016DYGa1i8HDv497ewpWgaCM
DictationInputDeviceSelectionPolicy now defaults to PinnedDictationSpeedPath, which needs DictationEmptyTranscriptionReason from ParakeetShortAudioGate. The smoke's hand-kept source list had neither, so spm-tests failed with "cannot find in scope". Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016DYGa1i8HDv497ewpWgaCM
r3dbars
marked this pull request as ready for review
September 27, 2026 13:12
13 of 14 tasks
r3dbars
added a commit
that referenced
this pull request
Sep 28, 2026
Keeps #1880's speed-path fallback and this branch's warmup rule. Warmup now asks the same question the recorder does, including whether empty takes moved the mic back to the engine, so that mic's engine stays warm. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7 of 8 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.
Why
Before: in 1.1.66, #1857 moved built-in and wired mics onto the pinned Core Audio recorder, only to make dictation start faster. On one M1 MacBook Air (macOS 27), every held dictation on 1.1.66 came back empty: six were "no speech" and one was "signal but no words". The same Mac dictated fine on the engine path in 1.1.64, minutes before it updated. The person kept pressing the key, and those short taps showed up in Sentry as APPLE-MACOS-2J ("Dictation ended before the microphone finished opening"). PostHog shows the same Mac going 0 for 37. On the "no speech" takes the words are lost, because nothing is pasted and the audio isn't kept.
After: a mic that is on the pinned recorder only for speed, and gives two empty held takes in a row, goes back to the engine path it used in 1.1.65 for the rest of that app version. An update tries the recorder again. The switch sends one
dictation_pinned_microphone_fell_back_to_engineevent with stageempty_takes, plus the input's format and bucketed health counts, so we can see what the recorder saw on a Mac like this.The recorder's own health checks found nothing wrong on that Mac: no restart, no silent-input flag, no fallback. So the cause inside the recorder is still unknown (n=1). This PR is the safety net, plus the data to find the cause.
Product Impact
dictationdictation reliabilityWhat changed
Sources/Speech/PinnedDictationSpeedPath.swiftscores each held take on a speed-only pinned mic as words or empty.recordingTooShort,modelFailureand cancelled takes aren't scored. It stores a per-mic, per-app-version count in UserDefaults, keyed by UID.PinnedDictationInputPolicy.recorderIsNeededskips the speed path for a mic that was moved back. Bluetooth-default and user-chosen overrides still always use the recorder. NewrecorderIsSpeedOnly.DictationStartCuePolicyplays the click after recording starts for a moved mic, because the engine start is slower again.ParakeetPinnedMicrophonekeeps the take's format and health counts.STTRouter.transcribe(preparedRecording:)scores the take after transcription. Starting a new take clears any stale marker.empty_takesstage.dictation_pinned_microphone_fell_back_to_enginealso carriesinput_channels,input_rate_hz,pinned_mic_restart_bucket,pinned_mic_gap_bucketandpinned_mic_dropped_callback_bucket, andanalytics-events.psvis updated. No device names or UIDs leave the Mac.PinnedDictationSpeedPathTests, plus new suites in the input-policy, start-cue and analytics-forwarding tests.run-tests.shandscripts/dev/test-parakeet-lifecycle.shsource lists, the Speech/UI CLAUDE.md files, and the pinned-mic bullet in the privacy doc.How I checked it
scripts/dev/agent-preflight.shbash scripts/dev/linux-checks.sh(runs without Swift): 48 passed, 0 failed.agents/test-matrix.ymlfor the files changed (CI green on 1381c6e)bash build.sh --no-open(CIapp-build)bash run-tests.sh(CIchecks)bash run-integration-smoke.sh(CIspm-tests; its Parakeet lifecycle smoke compiles the input policy)swift test: not neededbash run-e2e-smoke.sh/swift test --package-path Tools/<Package>: not mappedChecks I could not run, and why:
check-analytics-emitters.py, the taxonomy normalizer andcheck-telemetry-keys.pypassed as part of linux-checks.Mac or hardware test still needed? If yes: the steps, the log line or UI change each step should show, and whether each step fails on
main:empty_takesfallback warning.pinned_ioproc) and paste the words, with the click after recording starts.main, the third take stays on the pinned recorder.CFBundleShortVersionStringshould use the pinned recorder again.Risk Review
check-source-pins.py --changed-only: PASS)pinned-dictation-speed-path).agent-review/visuals/evidence: no UI changesNotes
Agent handoff
COORD_DONE: GREEN | this PR | adaptive fallback from the pinned recorder after 2 empty speed-only takes + format/health telemetry | none | none | linux-checks, source pins, CI | ship in 1.1.67🤖 Generated with Claude Code
https://claude.ai/code/session_016DYGa1i8HDv497ewpWgaCM