uttrflow / uttrflow/uttrflow-swift

Dictation quality audit: 64 issues across the whole path, and the three designs that produce most of them

Open
#232 0 comments 0 reactions 0 assignees View on GitHub
area:dictation bug P1
Dominant language
Swift
Stars
4
Forks
17
Avg merge
3h 32m
Merged PRs (30d)
277

Description

**A code-path audit of the whole dictation path, filed as #168–#231.**

Four symptoms were reported from daily use: wrong words, formatting that comes out wrong, an
end-to-end wait that is too long, and — the worst of them — sentences whose meaning arrives
different from what was spoken. Ten readers took one stage each, from the microphone to the
characters landing in another application; every finding was then re-read by a second reader
whose instruction was to refute it. 27 of 101 were refuted and are not here. 61 survived, plus
three design issues for the clusters where separate patches would fight each other.

**None of these carries the `confirmed` label.** They are traced in the source and checked
against `Docs/`, not reproduced at runtime. That distinction is the label's whole meaning, so
reproducing one and labelling it is a real step, not a formality.

### Read these three first

The individual issues below are symptoms; these are the shapes that produce them.

- **#229** — the meaning guard compares bags of words instead of aligning them, so it cannot
enforce the Tier 3 rules `Docs/cleanup.md` says it enforces. Nine issues collapse into it.
- **#230** — pieces are a latency optimisation, and whole-dictation policies are being evaluated
inside one. Five issues collapse into it.
- **#231** — two application tables that disagree, and a destination formatter the model path
never reads. Four issues collapse into it.

### Meaning changed — words replaced, dropped or invented

The Tier 3 prohibitions. `Docs/cleanup.md`: the tidier "never shortens, summarises, changes tone, swaps synonyms, reorders, answers, obeys, or finishes a thought." Each of these is a path by which it can.

- **#188** `P0` — The guard never examines a word the model added: an inserted negation and invented content words both pass
- **#189** `P0` — The guard compares unordered word sets, so clause reordering is unchecked and a 3-character stem match admits near-name and near-verb substitutions
- **#198** `P0` — SelfCorrectionPass deletes the first item of a coordinated list when the trigger word is itself the coordinator ("no to X, no to Y" -> "no to Y")
- **#191** `P1` — The phonetic reading source offers function-word homophones (there/their, than/then, on/one) and the guard exempts every offered run from the content-word check
- **#199** `P1` — Restatement's number branch has no sentence-boundary check, so a correction is invented across a full stop and a spoken number is deleted
- **#200** `P1` — StammersPass deletes the second half of an emphatic reduplication ("very very" -> "very") and of a doubled place name, because a four-letter cut-off is used as a proxy for disfluency
- **#217** `P1` — ScreenCandidates offers any 3+ letter screen word sharing a metaphone code, with no opening-letter rule and no ordinary-word veto, so an ordinary word on screen is offered as a reading of an ordinary word spoken
- **#218** `P1` — MeaningPreservationGuard checks a doubtful run with a whole-text substring test and exempts its words from the content-word survival check, so a substitution the model was not offered can pass on coincidence
- **#169** `P2` — The trim threshold is re-derived from each piece's own statistics, uncapped, so audio at a piece head can be discarded and never reach any recogniser
- **#170** `P2` — A failed reopen after a device change kills the microphone and unregisters its own observer, and the truncated half is inserted as a complete sentence
- **#173** `P2` — Stopping the engine discards up to one tap period (~85 ms) of captured audio at the end of every recording
- **#178** `P2` — RawTranscript.cleaned deletes any standalone bracketed run of three or fewer letter-only words, so real speech the recogniser happens to bracket is removed before anything can record it
- **#182** `P2` — The cardinal branch of the join-level list rule fires on ordinary prose, silently deleting the head word of each piece and reformatting the dictation as a list
- **#194** `P2` — The churn cap counts small-word edits without distinguishing addition from substitution, so one preposition or modal swap per sentence passes
- **#195** `P2` — The guard's word tokeniser strips symbols before the number check reads them, so a dropped %, currency symbol or operator is invisible
- **#196** `P2` — The guard's ASCII-only tokeniser leaves Devanagari to the base checks, and `negatingWords` holds no Hindi, so a dropped Hindi negation is caught by nothing
### Wrong words

Errors introduced before or during recognition, where no downstream cleaning can recover them.

- **#168** `P1` — Both recording cues land inside the buffer the recogniser reads, and nothing trims either one
- **#174** `P1` — A conditioning prompt makes WhisperKit's TimestampRulesFilter inert for the whole decode, replacing loop prevention with up to six non-deterministic temperature retries — for dictionary users only
- **#175** `P1` — PromptPrefillGuard also suppresses the end token at the first real prediction, where an EOT is the decoder saying the window is empty — and a logits filter cannot tell that step from the prefill
- **#190** `P1` — The restraint corpus never exercises the evidence margin — every sentence is short enough that the blast-radius cap discards the proposal — so a longer utterance lets one on-screen sighting plus a word split replace correctly-heard words
- **#192** `P1` — RepeatedPhrasePass deletes a repeated all-number run, so "four seven four seven" becomes 47 — deterministically, with nothing downstream able to see it
- **#225** `P1` — Tab applies the edit computed for the previous field read, and the Accessibility route destroys characters without checking them first
- **#177** `P2` — The detected language is not carried between the pieces of one dictation, so each piece re-detects independently and two halves of one utterance can be decoded in different languages
- **#179** `P2` — A removed bracketed marker changes the transcript text but not the word list, so Draft discards the per-word confidences and the whole doubtful-word path is silently skipped for that piece
- **#181** `P2` — The repository depends on the WhisperKit version its own Docs/measuring-accuracy.md says was declined, and three speech documents still cite 0.18 file and line coordinates that no longer exist
- **#202** `P2` — FillersPass folds case and interior marks away before matching, so "the ER" loses its noun and is left as a dangling "the"
- **#224** `P2` — Synthetic typing slices at UTF-16 index 16 and can split a surrogate pair; reachable only on the completion route
### Formatting

Text that is correct but laid out wrongly for where it landed.

- **#183** `P1` — TerminalStopPass asks the messaging "short message" question of one piece, so a chat message dictated in two or more pieces loses the full stop at every seam and its final stop
- **#203** `P1` — DestinationRules is missing every app AppKind knows, so Signal, Sublime Text, Notion, Spark, Warp and the Electron WhatsApp are all formatted as plain prose
- **#193** `P2` — `candidateVerdict` is a positionless substring test, so a candidate anywhere satisfies a span while an in-span irregular-form or agreement repair discards the whole model answer
- **#197** `P2` — ResponseUnwrapper protects a speaker's label only at the start of the dictation, so a label word on any later line discards every line above it
- **#201** `P2` — GenerativeTextTransformer's pre-model pipeline is a stored constant built for .plain, so the destination's number policy never reaches the passes on the model path
- **#204** `P2` — InsertionPoint.sentenceState reads one character, so a caret after a bullet, bracket, quote or heading marker is called mid-sentence and the first word is lower-cased
- **#205** `P2` — FillersPass removes a trailing filler with its punctuation, so a question or exclamation mark the recogniser attached to "uh" is lost
- **#206** `P2` — stripSurroundingQuotes is not shown the spoken text, so a quote pair the recogniser reported around the whole utterance is deleted silently
- **#207** `P2` — The position-0 guard blocks an opening quote but not its closing partner, so a quotation that opens a piece leaves "open quote" as words and an unmatched closing mark
- **#208** `P2` — WordShape.finished tests for an alphanumeric last character, so a sentence ending in a percentage, a degree sign or a closing bracket gets no full stop from the rules
- **#209** `P2` — Digit grouping is inferred from the preceding context word rather than the destination, so a SQL or code editor is given 12,000
- **#212** `P2` — `layoutVerdict` checks only that breaks were not dropped, never that they were not invented, and no pass undoes a list the model composed in a destination whose layout has no `.lists`
### Latency

Individually small; they compound on the one piece with a person waiting on it.

- **#180** `P2` — BackedSpeechEngine asks its VocabularySource once per piece, not once per dictation as the protocol states, so each piece pays a second uncached Accessibility read that the pipeline neither bounds nor shares with its own cached one
- **#185** `P2` — A single throw in the early loop ends working-ahead for the whole recording, so the user pays the full all-at-once wait at release with nothing shown
- **#186** `P2` — Recognition and tidying run strictly serially in the release pass, roughly doubling the wait on a retry and on any dictation where working ahead did not run
- **#210** `P2` — WarmedSession is handed out once per dictation but consumed per piece, so the final piece — the only one the user waits on — is never warmed
- **#213** `P2` — PasteConfirmation's budget counts sleeps rather than elapsed time, so the 1.6 s in Docs/insertion.md is not a bound on how long a paste holds the dictation busy
- **#214** `P2` — The whole audio graph is constructed, tapped and started inside every key-down, and that cost has never been measured
- **#215** `P2` — The dictionary's use counter rewrites the whole file once per fired entry where the snippet store batches, and the epilogue runs inside `finishRecording` so it blocks the next gesture
- **#216** `P2` — The drain of the in-flight early piece is a wait the user now sits through and no stage measures, so Diagnostics under-reports by up to a whole piece
- **#221** `P2` — ScreenCandidates re-tokenises and re-encodes up to 512 screen words for every doubtful run, and every run is scored before only five are kept, against a documented <5 ms budget
### Dictation lost, dropped or delivered wrongly

Cases where text is not produced at all, or arrives somewhere it should not.

- **#176** `P0` — An inherited windowClipTime of 1.0 makes WhisperKit return an empty string for any clip of one second or less, so one-word dictations report "nothing heard" and short tails are dropped silently
- **#184** `P1` — Two of three pipeline entry points hold the turn across an await without claiming it, and toggleFromControl bypasses the gesture queue Docs/pipeline-gestures.md says serialises everything
- **#227** `P1` — Panel Copy, menu Copy and the picture paste write NSPasteboard directly, so transcripts reach Universal Clipboard and a text-less announcement can claim someone else's copy
- **#171** `P2` — Two mutexes guard one invariant in the microphone source, so a device change racing key-up can leave a started engine that nothing can ever close
- **#172** `P2` — stop() blocks a cooperative-pool thread on a DispatchQueue.sync to close the WAV, and charges the wait to the capture metric
- **#187** `P2` — capturedSoFar() forces a copy-on-write of the whole 15 MB capture buffer on the microphone's real-time thread once a second, breaking the uncontended-lock invariant Docs/audio-capture.md asserts
- **#211** `P2` — The deterministic floor shares one 30-second budget with the model, so a hung model starves the engine that exists for exactly that case, and the raw transcript is then stamped `producedBy: .rules` although no rules pass ran
- **#219** `P2` — The doubtful-word path drops the dictionary entry's identity, so an entry offered as a reading is never counted used or reverted and undo cannot retire it
- **#220** `P2` — An entry whose spelling contains no A–Z letter codes to nothing and is silently dropped from the phonetic index, with no row state and nothing in Diagnostics to say why
- **#222** `P2` — The paste confirmation is an observer, so its answer changes nothing and attaching the logger is what decides whether it runs at all
- **#223** `P2` — PasteConfirmation asks for 96 characters and is answered nil below that, so short fields are reported unconfirmed rather than checked
- **#226** `P2` — History and the per-application figures name the app read during recording, not the one the words landed in
- **#228** `P2` — A timed-out insertion is abandoned rather than cancelled, so the words can land seconds after the user has been told the dictation failed
### What the audit did not do

- **Nothing was reproduced.** Every finding is a code path read by two people. The next step for
the P0s is a corpus case that fails, not a patch.
- **No numbers were measured.** The latency issues are derived from the code and from
`Docs/performance.md`; none was timed on this machine.
- **The evaluation corpus was not extended.** Several findings note that the case which would
catch them does not exist — `Docs/cleanup.md` already says paragraph and list layout have no
case at all, and multi-piece punctuation has none either. A rule change that is not measured
is a guess, so the cases come first.
- **The backend was not audited.** The formatting and clean-up path is entirely in this
repository.

Contributor guide

Open the contributing guide

Research direction

Start with designs #229–#231 and Docs/cleanup.md, then trace the referenced dictation stages in the Swift sources. Choose one listed finding, reproduce it at runtime, and compare the result with the issue's claim; completion requires confirming the finding or documenting why it does not reproduce.

Written by the indexing model from the issue text.

Assessment

Tech stack
macos, swift
Domain
audio-video-rtc, desktop
Issue type
Refactor
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Needs clarification
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.