Skip to content

Let people retry "No speech found" meetings, and treat mis-taps as cancels - #1801

Merged
claude[bot] merged 10 commits into
mainfrom
claude/project-thread-j6ookf
Sep 24, 2026
Merged

claude[bot] merged 10 commits into
mainfrom
claude/project-thread-j6ookf

Conversation

@claude

@claude claude Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Justin · project thread

Why

Before: a meeting judged "No speech found" became a failed row with no way to retry it, even though its audio was kept. Real speech could land there too: one "no speech" verdict from the diarizer ended the whole meeting, and a quiet track never got a second look. Stopping a meeting a few seconds after starting it by accident showed an error and left a failed row to clean up. A sub-second dictation mis-tap showed "Recording ended too soon".

After: a "No speech found" meeting keeps a Try again button while its audio exists. Before giving up on a meeting (or a split-mode mic) with no words, the pipeline takes one more careful look for quiet speech. Normal meetings with words are processed exactly as before. A meeting stopped within a few seconds that holds only silence closes like a cancel: no error, no "Saved" flash, no failed row. Anything that might hold a word, or any sign capture broke, still ends as a visible, retryable failure. A quick dictation tap closes like a cancel too.

This fixes customer pain points #5 and #10. PostHog, CI builds excluded, last 30 days: 66 no-speech skips on 31 devices (about 6% of stops), 42 of them under 10 seconds, and 0 retries because retry wasn't offered. Plus 196 dictation too-short events on 40 devices. Write-up: /mnt/project-files/customers/no-speech-analysis.md.

Product Impact

  • Affects: meetings / dictation
  • Lane: meeting reliability / dictation reliability / activation (12 of the no-speech skips were first meetings, 11 of those 2-15 second test taps)
  • Why this matters: a person who really had a conversation can recover it, and a mis-tap no longer looks like the app failed.

What changed

  • Retry: PipelineErrorKind.offersRetryForSavedAudio makes noSpeechDetected retryable (recordingTooShort still isn't). The copy now says "If people were talking, open Home and choose Try again." Import and saved-meeting retranscription no-speech failures keep their own copy, since they leave no Home row to retry from.
  • Diarizer "no speech" is not final: on system audio with no mic, and on the split-mode mic (both the two-track and mic-only paths), it now becomes an empty channel instead of ending the meeting.
  • Last-chance pass (lastChanceSpeechSweep):
    • When should it run?
      • On the call side, only when the whole meeting came out empty.
      • On a split-mode mic, whenever it produced no words, even if the call side did. If nothing is recovered, the mic is marked unusable as before.
    • How does it work?
      • It silence-splits the track, pads short pieces, and uses click-tolerant gain (capped at 3x for call audio, 12x for the mic).
      • It only runs on tracks that rise and fall like speech (hasSpeechLikeModulation), so silence, hum and DC never reach STT.
    • What gets dropped? A channel that only yields fillers ("Mm.") or a single word.
    • How are recovered words labeled? They stay unnamed ("Speaker 1" / "You"), and diarizer contexts and new profile ids are cleared.
  • Normal meetings are unchanged: the click-tolerant gain (normalizeForSpeech(ignoringSpikes:)) and short-segment padding only run inside the last-chance pass, until a corpus A/B.
  • Accidental starts (meetings): TranscriptionTaskManager.startTranscription takes the controller's sessionLength (Record to Stop).
    • Files under 2 s count as a tap only when the session is known and under 4 s and capture health is clean. Otherwise it's a visible "Recording too short" failure; when the session ran longer, the copy says capture stopped early instead of "Nothing broke".
    • A no-speech result is discarded only when all of these hold:
      • files and session are both under 10 s;
      • health is clean (no gaps, switches, missing or unusable track, or degraded capture);
      • no track has speech-like signal.
    • Otherwise it's the normal retained, retryable failure.
    • A discard deletes scratch and the journal and publishes DisplayStatus.discardedAccidentalStart.
    • Imports and retries never take this path.
  • App side: TerminalTranscriptionOutcome.discarded settles to ready.
    • There's no "Saved" flash.
    • The cancel sound plays only when no other meeting is recording.
    • Telemetry: meeting_transcript_skipped with failure_kind=accidental_start and friction cancelled, plus the runtime-diagnostics terminal event.
  • Dictation: recordingTooShort with a press under 1.5 s closes like a cancel, with friction cancelled. A longer press that captured too little still shows an error, now "Only a moment of audio came through. Try again, and if it keeps happening, check your microphone." instead of telling the user to speak longer. dictationFallback no longer calls a failure on 1 s+ of audio "too short", so that audio is kept as a model failure.

How I checked it

  • scripts/dev/agent-preflight.sh
  • python3 scripts/dev/check-build-source-lists.py, python3 scripts/dev/check-analytics-emitters.py
  • bash build.sh --no-open, bash run-tests.sh, bash build-deps.sh --force, bash run-integration-smoke.sh, swift test: the session has no Swift toolchain, so CI runs these.
  • Manual check: Mac test at /mnt/project-files/hardware-tests/no-speech-retry.md

New tests:

  • spike-tolerant peak, and the default gain unchanged
  • speech-like modulation
  • the sweep recovering call-side speech
  • the sweep recovering split-mode mic speech with and without call-side words
  • a split mic with nothing recoverable marked unusable
  • filler-only recovery dropped
  • steady tracks still ending as no speech
  • short silent sessions discarded
  • short files from a long session kept and retryable
  • no session length means audio is kept
  • sub-2 s files count as a tap only for a known, short, healthy session (8 s session, unknown session and broken-track cases stay visible failures)
  • the "capture stopped early" meeting copy
  • isAccidentalStart edges
  • unreadable tracks count as possible speech
  • the dictation press-length rule and the long-audio fallback

Updated tests:

  • the 2 second gate now expects .discardedAccidentalStart for a 1.4 s session
  • the dictation too-short copy
  • the split-speakers failed-queue test now uses a real STT error instead of silence

Risk Review

  • Privacy / local-first behavior reviewed: accidental_start is a plain category and no new properties were added.
  • Storage path or migration impact reviewed: legacy failed rows with "No speech detected" now decode as retryable. Discarded accidental starts delete only their own scratch audio and journal.
  • Public-facing copy stays concrete
  • Release/update impact: none
  • Agent PR, draft until human review
  • UI evidence: none. The UI change is that an error no longer shows up.
  • No private data included

Notes

  • Overlaps with other open PRs:
  • Deep review (/mnt/project-files/reviews/next-release/1801.md): S1-S5 are addressed in ab1cc55. For S5, the gain and padding changes were split out of normal meetings rather than flagged. Re-check N1-N3 are addressed in 6a996c4 (N3: the copy test reads the messages from PipelineFailureDisplayCopy, so a wording change fails the test).
  • Tradeoff: a quick dictation tap (under 1.5 s) that did hold a word used to show "speak for at least a second". Now it closes like a cancel.
  • Telemetry continuity: sub-2 s meetings from a short, healthy session move from recording_too_short / give_up to accidental_start / cancelled. Dictation mis-taps keep their event name, but friction flips to cancelled. PostHog insights that count these need a note.
  • Not fixed here: meeting_capture_stopped_under_controller always reports duration_bucket=lt_10s (MeetingSessionController stop telemetry), which skews PostHog duration numbers.

Agent handoff

COORD_DONE: BRIEF | this PR | retry for no-speech + last-chance speech pass + accidental starts as cancels | none | none | preflight, source-list and analytics checks (Swift checks via CI) | drive CI green, run the Mac test

🤖 Generated with Claude Code

https://claude.ai/code/session_01X89qzDDfTibdTyHR74c5C1

…and treat mis-taps as cancels [skip ci]

- "No speech found" failed meetings keep a Try again button while the audio exists.
- Short system segments (0.3-1 s) are padded for STT instead of dropped.
- A diarizer "no speech" verdict (system or split-mode mic) no longer ends the meeting; a last-chance silence-split pass runs over tracks that rise and fall like speech before reporting no speech.
- Mic normalization aims at a click-tolerant peak so one knock no longer keeps a quiet voice quiet.
- Live recordings under 2 s, or under 10 s with no speech, are discarded as accidental starts: no failed row, no error, no "Saved" flash; telemetry counts them as accidental_start / cancelled.
- Sub-second dictation mis-taps close like a cancel instead of showing an error.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X89qzDDfTibdTyHR74c5C1
@claude
claude Bot requested a review from r3dbars September 24, 2026 00:04
@claude claude Bot assigned r3dbars Sep 24, 2026
…s-taps [skip ci]

- Meetings: the no-speech discard now also needs the controller's session
  clock under 10 s, clean health, and no track with speech-like signal.
  Short files from a long session stay a visible, retained failure (and the
  sub-2 s gate reports "Recording too short" again in that case).
- Dictation: a mis-tap needs a press under 1.5 s; a stalled mic after a long
  press shows its error again. A long dictation's "invalid audio" inference
  error is a model failure, not "too short", so its audio is kept.
- Split-speakers meetings: the mic gets the last-chance pass whenever it
  produced no words, and is marked unusable again if nothing is recovered.
- Last-chance pass: call-side gain capped at 3x, and filler-only or
  single-word recoveries are dropped instead of saved.
- Normal meetings are unchanged: click-tolerant gain and short-segment
  padding only run inside the last-chance pass until a corpus A/B.
- No cancel sound while another meeting is recording; docs updated.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X89qzDDfTibdTyHR74c5C1
A saved-meeting retranscription or an import that finds no speech leaves
no failed row on Home, so the overlay should not tell people to open Home
and choose Try again. Those flows keep their own no-speech message.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X89qzDDfTibdTyHR74c5C1
The sub-2s gate discarded silently whenever the session ran under 10s,
even with gaps or a broken track. It now needs a known session under 4s
and clean capture health; anything else stays a visible failure, with
copy that says capture stopped early instead of "Nothing broke".

Also: the dictation too-short copy after a real press points at the mic,
and the import/saved-audio no-speech copy test reads its messages from
PipelineFailureDisplayCopy so a wording change can't silently break the
match.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X89qzDDfTibdTyHR74c5C1
Cleanup only deletes files inside the managed audio folder, so the new
test's per-case folders were never cleaned and its deleted-file checks
failed. The status checks already passed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X89qzDDfTibdTyHR74c5C1
r3dbars pushed a commit that referenced this pull request Sep 24, 2026
- Remove the stray "" that broke the app build.
- Read DisplayStatus.progress directly instead of switching over every
  case, so a new case (#1801) can't break the build.
- Saved pill only takes a transcript URL between its own job's save and
  the next job's start, and never the previous job's URL, so a late or
  republished earlier meeting can't be named or opened.
- Saved pill auto-hide checks the pointer instead of trusting hover
  events; a new recording cancels the old saved dwell.
- Error pill offers Open only when this failure left a failed row, and
  never for import or busy-pipeline failures; missing call audio from
  the pipeline now matches Home.
- Drop Esc = Review Later (it could throw away typed names).
- Speaker review reads the meeting name off-main and follows the file
  to its restyled name by capture id.
- Home's Open uses the same timed reveal as the pill and clears a search
  that would hide the row.
- Smaller: VoiceOver help reset on prompts, "Meetings page" wording in
  the error pill, redraw only when the pill text changes, mic copy.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MGacyW51MJ3Hq87uKs3xWC
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X89qzDDfTibdTyHR74c5C1
The pipeline merges same-speaker utterances less than 1.5s apart, so the
0.7s-gapped test bursts come back as one utterance with the stub phrase
repeated. The tests compared for an exact single phrase and failed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X89qzDDfTibdTyHR74c5C1
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X89qzDDfTibdTyHR74c5C1
@claude
claude Bot marked this pull request as ready for review September 24, 2026 05:40
@claude
claude Bot merged commit 01654ed into main Sep 24, 2026
8 checks passed
@claude
claude Bot deleted the claude/project-thread-j6ookf branch September 24, 2026 05:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants