Improved wake-word & command detection, with dual mode
This commit is contained in:
parent
baf84e0d8f
commit
acc806d403
10 changed files with 233 additions and 62 deletions
|
|
@ -25,7 +25,7 @@ fn main_loop(text_cmd_rx: Receiver<String>) -> Result<(), ()> {
|
|||
|
||||
// ring buffer: keeps last 2 seconds of audio (pre-roll)
|
||||
let mut audio_buffer = AudioRingBuffer::new(2.0, frame_length, sample_rate);
|
||||
|
||||
|
||||
// VAD state
|
||||
let mut vad_state = VadState::WaitingForVoice;
|
||||
let mut silence_frames: u32 = 0;
|
||||
|
|
@ -37,7 +37,8 @@ fn main_loop(text_cmd_rx: Receiver<String>) -> Result<(), ()> {
|
|||
voices::play_greet();
|
||||
|
||||
match recorder::start_recording() {
|
||||
Ok(_) => info!("Recording started."),
|
||||
Ok(_) => info!("Recording started. Microphone: {}",
|
||||
recorder::get_audio_device_name(recorder::get_selected_microphone_index())),
|
||||
Err(_) => {
|
||||
error!("Cannot start recording.");
|
||||
return Err(());
|
||||
|
|
@ -82,6 +83,9 @@ fn main_loop(text_cmd_rx: Receiver<String>) -> Result<(), ()> {
|
|||
}
|
||||
|
||||
VadState::VoiceActive => {
|
||||
// dual-feed: speech recognizer gets frames in parallel with wake word detector
|
||||
let _ = stt::recognize(&frame_buffer, false);
|
||||
|
||||
// feed to wake word detector
|
||||
if let Some(_keyword_index) = listener::data_callback(&frame_buffer) {
|
||||
// WAKE WORD DETECTED!
|
||||
|
|
@ -89,20 +93,25 @@ fn main_loop(text_cmd_rx: Receiver<String>) -> Result<(), ()> {
|
|||
ipc::send(IpcEvent::WakeWordDetected);
|
||||
|
||||
stt::reset_wake_recognizer();
|
||||
stt::reset_speech_recognizer();
|
||||
audio_processing::reset();
|
||||
|
||||
voices::play_reply();
|
||||
|
||||
// brief sniff to keep feeding STT while transitioning
|
||||
let sniff_frames = ((0.3 * sample_rate as f32) / frame_length as f32) as u32;
|
||||
for _ in 0..sniff_frames {
|
||||
recorder::read_microphone(&mut frame_buffer);
|
||||
audio_processing::process(&frame_buffer);
|
||||
stt::recognize(&frame_buffer, false);
|
||||
}
|
||||
|
||||
ipc::send(IpcEvent::Listening);
|
||||
|
||||
// enter voice recognition mode
|
||||
recognize_command(&mut frame_buffer, &rt, frame_length, sample_rate);
|
||||
|
||||
recognize_command(&mut frame_buffer, &rt, frame_length, sample_rate, true);
|
||||
|
||||
// reset state after command
|
||||
vad_state = VadState::WaitingForVoice;
|
||||
silence_frames = 0;
|
||||
audio_buffer.clear();
|
||||
stt::reset_wake_recognizer();
|
||||
stt::reset_speech_recognizer(); // NOW reset, after command is done
|
||||
audio_processing::reset();
|
||||
ipc::send(IpcEvent::Idle);
|
||||
|
||||
|
|
@ -116,11 +125,11 @@ fn main_loop(text_cmd_rx: Receiver<String>) -> Result<(), ()> {
|
|||
silence_frames += 1;
|
||||
|
||||
if silence_frames > silence_threshold {
|
||||
// silence timeout, back to waiting
|
||||
debug!("VAD: Silence timeout, returning to wait state");
|
||||
vad_state = VadState::WaitingForVoice;
|
||||
silence_frames = 0;
|
||||
stt::reset_wake_recognizer();
|
||||
stt::reset_speech_recognizer(); // reset since we were dual-feeding
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -140,11 +149,17 @@ fn recognize_command(
|
|||
rt: &tokio::runtime::Runtime,
|
||||
frame_length: usize,
|
||||
sample_rate: usize,
|
||||
prefed_audio: bool
|
||||
) {
|
||||
let mut audio_buffer = AudioRingBuffer::new(2.0, frame_length, sample_rate);
|
||||
let mut vad_state = VadState::WaitingForVoice;
|
||||
let mut vad_state = if prefed_audio {
|
||||
VadState::VoiceActive
|
||||
} else {
|
||||
VadState::WaitingForVoice
|
||||
};
|
||||
let mut silence_frames: u32 = 0;
|
||||
let mut start = SystemTime::now();
|
||||
let mut first_recognition = prefed_audio;
|
||||
|
||||
// longer silence threshold for commands (user might pause to think)
|
||||
// 2 seconds
|
||||
|
|
@ -169,6 +184,13 @@ fn recognize_command(
|
|||
}
|
||||
vad_state = VadState::VoiceActive;
|
||||
silence_frames = 0;
|
||||
} else {
|
||||
silence_frames += 1;
|
||||
|
||||
if silence_frames > silence_threshold {
|
||||
info!("Long silence detected, returning to wake word mode.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -184,20 +206,51 @@ fn recognize_command(
|
|||
recognized_voice = recognized_voice.to_lowercase();
|
||||
|
||||
// check if wake word repeated (reactivate)
|
||||
let wake_phrase = config::get_wake_phrase(&i18n::get_language());
|
||||
if recognized_voice.contains(wake_phrase) {
|
||||
info!("Wake word detected during chaining, reactivating...");
|
||||
voices::play_reply();
|
||||
stt::reset_speech_recognizer();
|
||||
ipc::send(IpcEvent::Listening);
|
||||
|
||||
// reset for next command
|
||||
vad_state = VadState::WaitingForVoice;
|
||||
silence_frames = 0;
|
||||
start = SystemTime::now();
|
||||
audio_buffer.clear();
|
||||
continue;
|
||||
let wake_phrases = config::get_wake_phrases(&i18n::get_language());
|
||||
let contains_wake = wake_phrases.iter().any(|wp| recognized_voice.contains(wp));
|
||||
|
||||
if contains_wake {
|
||||
// strip the wake word
|
||||
let mut remaining = recognized_voice.clone();
|
||||
for wp in wake_phrases {
|
||||
remaining = remaining.replace(wp, "");
|
||||
}
|
||||
let remaining = remaining.trim();
|
||||
|
||||
if remaining.is_empty() {
|
||||
if first_recognition {
|
||||
// leftover wake word from dual-feed, just discard it
|
||||
info!("Discarding initial wake word from prefed audio");
|
||||
first_recognition = false;
|
||||
stt::reset_speech_recognizer();
|
||||
voices::play_reply();
|
||||
vad_state = VadState::WaitingForVoice;
|
||||
silence_frames = 0;
|
||||
start = SystemTime::now();
|
||||
audio_buffer.clear();
|
||||
continue;
|
||||
}
|
||||
|
||||
// just wake word, no command - reactivate
|
||||
info!("Wake word repeated during chaining, reactivating...");
|
||||
voices::play_reply();
|
||||
stt::reset_speech_recognizer();
|
||||
ipc::send(IpcEvent::Listening);
|
||||
|
||||
vad_state = VadState::WaitingForVoice;
|
||||
silence_frames = 0;
|
||||
start = SystemTime::now();
|
||||
audio_buffer.clear();
|
||||
continue;
|
||||
} else {
|
||||
// wake word + command in one phrase - execute the command part
|
||||
info!("Wake word + command during chaining: '{}'", remaining);
|
||||
recognized_voice = remaining.to_string();
|
||||
// fall through to command execution below
|
||||
}
|
||||
}
|
||||
|
||||
first_recognition = false;
|
||||
|
||||
// filter activation phrases
|
||||
// for tbr in config::ASSISTANT_PHRASES_TBR {
|
||||
|
|
@ -228,6 +281,7 @@ fn recognize_command(
|
|||
continue;
|
||||
} else {
|
||||
// no chain: return to wake word
|
||||
info!("No chain, returning to wake word mode.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -206,29 +206,30 @@ pub const CMS_WAIT_DELAY: std::time::Duration = std::time::Duration::from_secs(1
|
|||
|
||||
|
||||
|
||||
pub fn get_wake_phrase(lang: &str) -> &'static str {
|
||||
pub fn get_wake_phrases(lang: &str) -> &'static [&'static str] {
|
||||
match lang {
|
||||
"ru" => "джарвис",
|
||||
"ua" => "джарвіс",
|
||||
"en" => "jarvis",
|
||||
_ => "jarvis",
|
||||
"ru" => &["джарвис", "джервис", "гарвис", "джарви", "гарви"],
|
||||
"ua" => &["джарвіс", "джервіс"],
|
||||
"en" => &["jarvis", "jervis"],
|
||||
_ => &["jarvis"],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_phrases_to_remove(lang: &str) -> &'static [&'static str] {
|
||||
match lang {
|
||||
"ru" => &[
|
||||
"джарвис", "сэр", "слушаю сэр", "всегда к услугам",
|
||||
"джарвис", "джервис", "гарвис", "джарви", "гарви",
|
||||
"сэр", "слушаю сэр", "всегда к услугам",
|
||||
"произнеси", "ответь", "покажи", "скажи", "давай",
|
||||
"да сэр", "к вашим услугам сэр", "загружаю сэр",
|
||||
],
|
||||
"ua" => &[
|
||||
"джарвіс", "сер", "слухаю сер", "завжди до послуг",
|
||||
"джарвіс", "джервіс", "сер", "слухаю сер", "завжди до послуг",
|
||||
"скажи", "покажи", "відповідай", "давай",
|
||||
"так сер", "до ваших послуг сер",
|
||||
],
|
||||
"en" => &[
|
||||
"jarvis", "sir", "yes sir", "at your service",
|
||||
"jarvis", "jervis", "sir", "yes sir", "at your service",
|
||||
"please", "say", "show", "tell", "hey",
|
||||
],
|
||||
_ => &["jarvis"],
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ pub async fn init(commands: &Vec<JCommandsList>) -> Result<(), String> {
|
|||
} // already initialized
|
||||
|
||||
// set default ire type
|
||||
// @TODO. Make it configurable?
|
||||
IRE_TYPE.set(config::DEFAULT_INTENT_RECOGNITION_ENGINE).unwrap();
|
||||
|
||||
// load given recorder
|
||||
|
|
|
|||
|
|
@ -17,19 +17,28 @@ pub fn data_callback(frame_buffer: &[i16]) -> Option<i32> {
|
|||
|
||||
// language-specific wake phrase
|
||||
let lang = i18n::get_language();
|
||||
let wake_phrase = config::get_wake_phrase(&lang);
|
||||
let wake_phrases = config::get_wake_phrases(&lang);
|
||||
|
||||
// verify with seqdiff ratio
|
||||
let wake_chars: Vec<char> = wake_phrase.chars().collect();
|
||||
let recognized_chars: Vec<char> = recognized.chars().collect();
|
||||
let similarity = seqdiff::ratio(&wake_chars, &recognized_chars);
|
||||
|
||||
info!("Similarity: {:.1}% ('{}' vs '{}')", similarity, recognized, config::VOSK_FETCH_PHRASE);
|
||||
|
||||
if similarity >= config::VOSK_MIN_RATIO {
|
||||
info!("Wake word activated!");
|
||||
return Some(0);
|
||||
for word in recognized.split_whitespace() {
|
||||
if word == "[unk]" {
|
||||
continue;
|
||||
}
|
||||
|
||||
let word_chars: Vec<char> = word.chars().collect();
|
||||
|
||||
for wake_phrase in wake_phrases {
|
||||
let wake_chars: Vec<char> = wake_phrase.chars().collect();
|
||||
let similarity = seqdiff::ratio(&wake_chars, &word_chars);
|
||||
|
||||
if similarity >= config::VOSK_MIN_RATIO {
|
||||
info!("Wake word match: '{}' ~ '{}' ({:.1}%)", word, wake_phrase, similarity);
|
||||
return Some(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// info!("Similarity: {:.1}% ('{}' vs '{}')", similarity, recognized, config::VOSK_FETCH_PHRASE);
|
||||
}
|
||||
|
||||
None
|
||||
|
|
|
|||
|
|
@ -123,7 +123,19 @@ pub fn stop_recording() -> Result<(), ()> {
|
|||
}
|
||||
|
||||
pub fn get_selected_microphone_index() -> i32 {
|
||||
DB.get().unwrap().read().microphone
|
||||
let idx = DB.get().unwrap().read().microphone;
|
||||
|
||||
if idx > 0 {
|
||||
// validate that this microphone is actually in the list
|
||||
let devices = get_audio_devices();
|
||||
if (idx as usize) >= devices.len() {
|
||||
warn!("Microphone index {} not found ({} available), falling back to default",
|
||||
idx, devices.len());
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
idx
|
||||
}
|
||||
|
||||
pub fn get_audio_devices() -> Vec<String> {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue