diff --git a/crates/jarvis-app/src/llm_fallback.rs b/crates/jarvis-app/src/llm_fallback.rs index 4d8a9f6..2c38ea9 100644 --- a/crates/jarvis-app/src/llm_fallback.rs +++ b/crates/jarvis-app/src/llm_fallback.rs @@ -98,16 +98,50 @@ pub fn handle(prompt: &str) { let reply = reply.trim().to_string(); info!("LLM reply: {}", reply); state.history.lock().push_assistant(reply.clone()); - ipc::send(IpcEvent::LlmReply { text: reply }); + ipc::send(IpcEvent::LlmReply { text: reply.clone() }); voices::play_ok(); + speak_via_sapi(&reply); } Err(e) => { error!("LLM request failed: {}", e); state.history.lock().pop_last_user(); - ipc::send(IpcEvent::LlmReply { - text: config::LLM_FALLBACK_ERROR_RU.to_string(), - }); + let err_text = config::LLM_FALLBACK_ERROR_RU.to_string(); + ipc::send(IpcEvent::LlmReply { text: err_text.clone() }); voices::play_error(); + speak_via_sapi(&err_text); } } } + +#[cfg(target_os = "windows")] +fn speak_via_sapi(text: &str) { + if std::env::var("JARVIS_LLM_TTS").as_deref() == Ok("false") { + return; + } + let escaped = text.replace('\'', "''"); + let ps = format!( + "Add-Type -AssemblyName System.Speech; \ + $s = New-Object System.Speech.Synthesis.SpeechSynthesizer; \ + foreach ($v in $s.GetInstalledVoices()) {{ \ + if ($v.VoiceInfo.Culture.TwoLetterISOLanguageName -eq 'ru') {{ \ + try {{ $s.SelectVoice($v.VoiceInfo.Name); break }} catch {{ }} \ + }} \ + }} \ + $s.Speak('{}')", + escaped + ); + match std::process::Command::new("powershell") + .args(["-NoProfile", "-Command", &ps]) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .spawn() + { + Ok(_) => {} + Err(e) => warn!("SAPI spawn failed: {}", e), + } +} + +#[cfg(not(target_os = "windows"))] +fn speak_via_sapi(_text: &str) { + // No-op on non-Windows; LLM reply still arrives via IPC for the GUI to render. +}