Skip to content

[bug] "Stop-stop." doesn't match the "stop stop" keyword — normalizeTranscript strips punctuation but not hyphens #334

Closed

Description

@serge-ivo

Supersedes the diagnosis in #331 for users who have configured a stop-word. The owner does have
"stop stop" configured, and reports it works sometimes — "seems like it works now, weird".

That intermittency is fully explained.

Cause: the stop-word normaliser does not strip hyphens

packages/sdk/src/voice/convo.ts:243:

function normalizeTranscript(text: string): string {
	return text
		.toLowerCase()
		.replace(/[.,!?¿¡。,!?、]/g, "")   // ← no hyphen
		.replace(/\s+/g, " ")
		.trim();
}

stripStopWord compares the normalised transcript against the normalised stop-word. Measured:

Whisper output normalised matches "stop stop"
"Stop stop." stop stop yes
"Stop, stop." stop stop yes
"Stop-stop." stop-stop no

Whisper renders a repeated word as a hyphenated compound sometimes — it is a formatting choice,
not a transcription difference. So the same spoken phrase silently succeeds or fails depending on
punctuation the user cannot control or even see.

That is exactly the reported behaviour: it works, then it does not, then it works again.

Observed failing case, Coder Lead 2026-08-06 23:51:37 — transcript "Stop-stop." was sent as a
chat message instead of ending the turn, and the agent replied "Got it. Stopped." while the mic
stayed live. The silence that followed produced two phantom turns (#332).

The same file already has the answer

isNoiseTranscript in audio.ts:90 strips a much wider punctuation set, hyphens included:

.replace(/[.,!?"'’“”…·•–—:;()。,!?、:;「」『』()《》-]|\[|\]/g, " ")

So there are two normalisers in the voice package with different ideas of what punctuation is, and
the stricter one is used for the noise filter while the looser one guards the commands. That is
backwards — the command path is where a near-miss is user-visible.

Fix

Normalise hyphens (and en/em dashes) to a space in normalizeTranscript, then collapse whitespace
— which the function already does, so "stop-stop" becomes "stop stop" and matches.

Better still: have both paths share one normaliser. Two functions that must agree about punctuation
and do not is the underlying defect; the hyphen is just the instance that surfaced.

This affects every command path, not only stop-words — matchVoiceCommand and
splitTrailingCommand both run on normalizeTranscript, so any hyphenated rendering of a
multi-word command ("mute-mute", "text-mode") fails the same way.

Verification

  • "Stop-stop.", "Stop stop." and "Stop, stop." all end the turn identically.
  • A hyphenated rendering of any configured multi-word command matches.
  • Ordinary hyphenated speech is unaffected: "well-known" does not become a command, because
    matching is still whole-utterance or trailing-phrase.
  • A unit test pinning the three renderings above, since the failing one depends on Whisper
    formatting and will not reproduce reliably by hand.

Activity

  1. serge-ivo commented on Aug 7, 2026

    @serge-ivo
    ContributorAuthor

    Fixed in 3d7e28b.

    There is now ONE normaliser for spoken text (packages/sdk/src/voice/normalize.ts), shared by the command matcher, the stop-word splitter and the noise filter — the two-normalisers-that-must-agree defect the ticket identifies, not just the hyphen.

    It works in Unicode categories rather than a longer ASCII class: \p{P} → space (a hyphen JOINS two words spoken separately, so spacing is what makes the three renderings one utterance), apostrophes and \p{Cf} deleted (they join letters, or are invisible), NFC first. That covers en/em dashes, the non-breaking hyphen, the soft hyphen and CJK/inverted punctuation in one rule.

    One follow-on defect found while fixing it: stripStopWord dropped the last N whitespace tokens for an N-word stop-word. Once hyphens space out, "run the tests, stop-stop" is four tokens and six words, so that arithmetic ate "tests,". It now cuts at a real whitespace boundary.

    Pinned by test: "Stop-stop.", "Stop stop.", "Stop, stop.", "stop—stop" and "STOP-STOP!" all end the turn identically.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions