fix(recognition): preserve final speech on stop (fixes #401)

Replace hard-coded stop tail trim with configurable stop_sound_guard_ms
(default 200ms) so quick key release no longer clips the final spoken
word in push-to-talk mode.

Co-authored-by: Fakhareddine <fe.sabbari@claranet.com>
This commit is contained in:
jatinkrmalik
2026-05-02 17:33:42 -07:00
co-authored by Fakhareddine
parent 79ae8e45d9
commit d04947374f
8 changed files with 76 additions and 29 deletions
+2
View File
@@ -347,6 +347,7 @@ def main():
vad_sensitivity = saved_settings.get("vad_sensitivity", 3)
silence_timeout = saved_settings.get("silence_timeout", 2.0)
stop_sound_guard_ms = saved_settings.get("stop_sound_guard_ms", 200)
voice_commands_enabled = saved_settings.get("voice_commands_enabled") # None = auto
audio_device_index = audio_settings.get("device_index", None)
@@ -365,6 +366,7 @@ def main():
language=language,
vad_sensitivity=vad_sensitivity,
silence_timeout=silence_timeout,
stop_sound_guard_ms=stop_sound_guard_ms,
voice_commands_enabled=voice_commands_enabled,
audio_device_index=audio_device_index,
)
@@ -527,6 +527,7 @@ class SpeechRecognitionManager:
self.engine = engine
self.model_size = model_size
self.language = language
self.stop_sound_guard_ms = kwargs.get("stop_sound_guard_ms", 200)
self.state = RecognitionState.IDLE
self.audio_thread = None
self.recognition_thread = None
@@ -1516,6 +1517,19 @@ class SpeechRecognitionManager:
"""Check if the model is initialized and ready for recognition."""
return self._model_initialized and self.model is not None
def _get_stop_sound_guard_chunks(self) -> int:
"""Convert the configured stop-sound guard to 16kHz chunk count."""
try:
guard_ms = max(0, int(self.stop_sound_guard_ms))
except (TypeError, ValueError):
logger.warning(
f"Invalid stop_sound_guard_ms value: {self.stop_sound_guard_ms}. Using default 200ms."
)
guard_ms = 200
chunk_duration_ms = (1024 / 16000) * 1000
return int(guard_ms / chunk_duration_ms)
def start_recognition(self, mode: str = "toggle"):
"""Start the speech recognition process."""
if self.state != RecognitionState.IDLE:
@@ -1573,20 +1587,17 @@ class SpeechRecognitionManager:
if self.audio_thread and self.audio_thread.is_alive():
self.audio_thread.join(timeout=2.0)
# Discard the last ~1 second of audio to avoid transcribing the stop sound
# Audio is recorded in 1024-sample chunks at 16000 Hz = ~64ms per chunk
# We discard the last 15 chunks (~1 second) which should contain the feedback sound
# Trim only a small tail to avoid the stop sound without clipping the user's final word.
with self._buffer_lock:
if len(self.audio_buffer) > 15:
discarded_chunks = self.audio_buffer[-15:]
self.audio_buffer = self.audio_buffer[:-15]
stop_sound_guard_chunks = self._get_stop_sound_guard_chunks()
if stop_sound_guard_chunks > 0 and len(self.audio_buffer) > stop_sound_guard_chunks:
discarded_chunks = self.audio_buffer[-stop_sound_guard_chunks:]
self.audio_buffer = self.audio_buffer[:-stop_sound_guard_chunks]
logger.debug(
f"Discarded {len(discarded_chunks)} audio chunks to avoid transcribing feedback sound"
"Discarded %s audio chunks (~%sms) to avoid transcribing feedback sound",
len(discarded_chunks),
self.stop_sound_guard_ms,
)
elif self.audio_buffer:
# If buffer is small, just clear it entirely to be safe
logger.debug(f"Clearing small audio buffer ({len(self.audio_buffer)} chunks)")
self.audio_buffer = []
if self.audio_buffer:
logger.info(f"DEBUG: Enqueuing final buffer with {len(self.audio_buffer)} chunks")
@@ -2094,6 +2105,8 @@ class SpeechRecognitionManager:
if "voice_commands_enabled" in kwargs:
self._voice_commands_preference = kwargs.get("voice_commands_enabled")
if "stop_sound_guard_ms" in kwargs:
self.stop_sound_guard_ms = kwargs.get("stop_sound_guard_ms", self.stop_sound_guard_ms)
self._voice_commands_enabled = self._resolve_voice_commands_enabled()
if restart_needed:
+1
View File
@@ -27,6 +27,7 @@ DEFAULT_CONFIG = {
"whisper_cpp_model_size": "tiny", # Default model for whisper.cpp engine
"vad_sensitivity": 3, # Voice Activity Detection sensitivity (1-5)
"silence_timeout": 2.0, # Seconds of silence before stopping
"stop_sound_guard_ms": 200, # Small tail trim to avoid the stop sound without clipping speech
"voice_commands_enabled": None, # None = auto (enabled for VOSK, disabled for Whisper)
},
"audio": {