ba13132298
Saying OR typing a configured stop phrase (voice.stop_phrases, default
"stop") now ends the voice chat everywhere, not just classic CLI PTT:
- hermes_cli/voice.py: new explicit on_stop_phrase callback through
start_continuous/stop_continuous. The force-transcribe path previously
DISCARDED the stop phrase silently — with auto_restart=False the client
re-arms the next capture, so the conversation never ended. Both halt
paths now fire on_stop_phrase (fallback: on_silent_limit for legacy
callers) as user intent, distinct from the no-speech timeout.
- tui_gateway/server.py: voice.record wires on_stop_phrase and emits
voice.transcript {stop_phrase: true} after flipping HERMES_VOICE(_TTS)
off and stopping streaming TTS — same teardown as /voice off. The TTS
barge-in monitor stop-checks its transcript too. prompt.submit consumes
a TYPED bare stop phrase at the server-side choke point when voice mode
is on (returns {voice_stopped: true}, no turn starts).
- ui-tui: voice.transcript {stop_phrase} ends voice mode with a clear
'voice chat ended' notice (distinct from the no-speech-limit message);
submitPrompt releases the busy latch on a consumed voice_stopped reply.
- cli.py: _typed_voice_stop in process_loop — typing a bare stop phrase
while voice mode/continuous is active ends voice mode instead of
sending 'stop' to the agent; typed 'stop' outside voice mode is
unchanged. Voice transcripts skip the check (already stop-checked).
- desktop: interceptsTypedVoiceStop — the composer's onSubmit ends the
live voice conversation (same path as clicking end on the pill) when a
bare stop command is typed with no attachments; renderer-owned loop, so
handled client-side like the existing spoken isVoiceStopCommand.
- tools/voice_mode.py: transcribe_recording never lets the Whisper
hallucination filter swallow a configured stop phrase (e.g. 'bye'
configured as a stop phrase is both a hallucination-blocklist entry and
a stop phrase — stop-phrase check now wins).
Tests: continuous-loop signal (sabotage-verified), force-transcribe stop
signal + legacy fallback, hallucination-filter ordering, typed-stop CLI
unit tests (voice on/off/longer text), prompt.submit typed-stop gateway
tests, TUI vitest for stop_phrase event handling, desktop vitest for the
typed-stop interceptor.
87 lines
2.6 KiB
TypeScript
87 lines
2.6 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
|
|
import { interceptsTypedVoiceStop, isVoiceStopCommand } from './voice-stop-word'
|
|
|
|
describe('isVoiceStopCommand', () => {
|
|
it('matches bare stop commands', () => {
|
|
for (const phrase of ['stop', 'Stop', 'STOP', 'stop.', 'stop!', ' stop ', 'stop…']) {
|
|
expect(isVoiceStopCommand(phrase)).toBe(true)
|
|
}
|
|
})
|
|
|
|
it('matches multi-word stop phrases', () => {
|
|
for (const phrase of [
|
|
'stop listening',
|
|
'stop it',
|
|
'please stop',
|
|
'stop please',
|
|
"that's all",
|
|
'that is all',
|
|
'never mind',
|
|
'nevermind',
|
|
'end conversation',
|
|
'end the conversation',
|
|
'goodbye',
|
|
'bye',
|
|
'cancel'
|
|
]) {
|
|
expect(isVoiceStopCommand(phrase)).toBe(true)
|
|
}
|
|
})
|
|
|
|
it('matches stop commands addressed to Hermes', () => {
|
|
for (const phrase of ['hermes stop', 'hey hermes stop', 'hey hermes, stop', 'ok stop', 'okay stop']) {
|
|
expect(isVoiceStopCommand(phrase)).toBe(true)
|
|
}
|
|
})
|
|
|
|
it('does NOT match substantive requests that merely contain "stop"', () => {
|
|
for (const phrase of [
|
|
'stop the docker container',
|
|
'how do I stop a running process',
|
|
'can you stop the deployment',
|
|
'stop the music and play something else',
|
|
"don't stop now",
|
|
'the bus stop is closed'
|
|
]) {
|
|
expect(isVoiceStopCommand(phrase)).toBe(false)
|
|
}
|
|
})
|
|
|
|
it('does not match bare address words or empty input', () => {
|
|
for (const phrase of ['', ' ', 'hermes', 'hey hermes', 'ok', 'okay', 'hey']) {
|
|
expect(isVoiceStopCommand(phrase)).toBe(false)
|
|
}
|
|
})
|
|
|
|
it('does not match unrelated short utterances', () => {
|
|
for (const phrase of ['hello', 'yes', 'what time is it', 'thanks']) {
|
|
expect(isVoiceStopCommand(phrase)).toBe(false)
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('interceptsTypedVoiceStop', () => {
|
|
it('intercepts a typed bare stop command while the conversation is active', () => {
|
|
for (const text of ['stop', 'Stop.', 'never mind', 'hey hermes, stop']) {
|
|
expect(interceptsTypedVoiceStop(true, text)).toBe(true)
|
|
}
|
|
})
|
|
|
|
it('never intercepts when the voice conversation is inactive', () => {
|
|
for (const text of ['stop', 'never mind', 'goodbye']) {
|
|
expect(interceptsTypedVoiceStop(false, text)).toBe(false)
|
|
}
|
|
})
|
|
|
|
it('passes through substantive messages during a conversation', () => {
|
|
for (const text of ['stop the docker container', 'how do I stop a process', 'hello']) {
|
|
expect(interceptsTypedVoiceStop(true, text)).toBe(false)
|
|
}
|
|
})
|
|
|
|
it('passes through when attachments ride along (real payload)', () => {
|
|
expect(interceptsTypedVoiceStop(true, 'stop', 1)).toBe(false)
|
|
})
|
|
})
|