mirror of
https://github.com/Priler/jarvis.git
synced 2026-06-03 19:09:45 +00:00
Update to Rust programming language.
This commit is contained in:
189
src-tauri/src/assistant_commands.rs
Normal file
189
src-tauri/src/assistant_commands.rs
Normal file
@@ -0,0 +1,189 @@
|
||||
use rand::seq::SliceRandom;
|
||||
use seqdiff::ratio;
|
||||
use serde_yaml;
|
||||
use std::path::Path;
|
||||
use std::{fs, fs::File};
|
||||
|
||||
use core::time::Duration;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Child;
|
||||
use std::process::Command;
|
||||
use tauri::Manager;
|
||||
|
||||
mod structs;
|
||||
pub use structs::*;
|
||||
|
||||
use crate::config;
|
||||
use crate::events;
|
||||
|
||||
pub fn parse_commands() -> Result<Vec<AssistantCommand>, String> {
|
||||
// collect commands
|
||||
let mut commands: Vec<AssistantCommand> = vec![];
|
||||
|
||||
// read commands directories first
|
||||
if let Ok(cpaths) = fs::read_dir(config::COMMANDS_PATH) {
|
||||
for cpath in cpaths {
|
||||
// validate this command, check if required files exists
|
||||
let _cpath = cpath.unwrap().path();
|
||||
let cc_file = Path::new(&_cpath).join("command.yaml");
|
||||
|
||||
if cc_file.exists() {
|
||||
// try parse config files
|
||||
let cc_reader = std::fs::File::open(&cc_file).unwrap();
|
||||
let cc_yaml: CommandsList;
|
||||
|
||||
// try parse command.yaml
|
||||
if let Ok(parse_result) = serde_yaml::from_reader::<File, CommandsList>(cc_reader) {
|
||||
cc_yaml = parse_result;
|
||||
} else {
|
||||
println!("Can't parse {}, skipping ...", &cc_file.display());
|
||||
continue;
|
||||
// return Err(format!("Can't parse {}", &cc_file.display()));
|
||||
}
|
||||
|
||||
// everything seems to be Ok
|
||||
commands.push(AssistantCommand {
|
||||
path: _cpath,
|
||||
commands: cc_yaml,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if commands.len() > 0 {
|
||||
Ok(commands)
|
||||
} else {
|
||||
Err("No commands were found".into())
|
||||
}
|
||||
} else {
|
||||
return Err("Error reading commands directory".into());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fetch_command<'a>(
|
||||
phrase: &str,
|
||||
commands: &'a Vec<AssistantCommand>,
|
||||
) -> Option<(&'a PathBuf, &'a Config)> {
|
||||
// result scmd
|
||||
let mut result_scmd: Option<(&PathBuf, &Config)> = None;
|
||||
let mut current_max_ratio = config::CMD_RATIO_THRESHOLD;
|
||||
|
||||
// convert fetch phrase to sequence
|
||||
let fetch_phrase_chars = phrase.chars().collect::<Vec<_>>();
|
||||
|
||||
// list all the commands
|
||||
for cmd in commands {
|
||||
// list all subcommands
|
||||
for scmd in &cmd.commands.list {
|
||||
// list all phrases in command
|
||||
for cmd_phrase in &scmd.phrases {
|
||||
// convert cmd phrase to sequence
|
||||
let cmd_phrase_chars = cmd_phrase.chars().collect::<Vec<_>>();
|
||||
|
||||
// compare fetch phrase with cmd phrase
|
||||
let ratio = ratio(&fetch_phrase_chars, &cmd_phrase_chars);
|
||||
|
||||
// return, if it fits the given threshold
|
||||
if ratio >= current_max_ratio {
|
||||
result_scmd = Some((&cmd.path, &scmd));
|
||||
current_max_ratio = ratio;
|
||||
// println!("Ratio is: {}", ratio);
|
||||
// return Some((&cmd.path, &scmd))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((cmd_path, scmd)) = result_scmd {
|
||||
println!("Ratio is: {}", current_max_ratio);
|
||||
Some((&cmd_path, &scmd))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn execute_exe(exe: &str, args: &Vec<String>) -> std::io::Result<Child> {
|
||||
Command::new(exe).args(args).spawn()
|
||||
}
|
||||
|
||||
pub fn execute_command(
|
||||
cmd_path: &PathBuf,
|
||||
cmd_config: &Config,
|
||||
app_handle: &tauri::AppHandle,
|
||||
) -> Result<(), String> {
|
||||
match cmd_config.command.action.as_str() {
|
||||
"voice" => {
|
||||
// VOICE command type
|
||||
let random_cmd_sound = cmd_config
|
||||
.voice
|
||||
.sounds
|
||||
.choose(&mut rand::thread_rng())
|
||||
.unwrap();
|
||||
events::play(random_cmd_sound, app_handle);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
"ahk" => {
|
||||
// AutoHotkey command type
|
||||
let exe_path_absolute = Path::new(&cmd_config.command.exe_path);
|
||||
let exe_path_local = Path::new(&cmd_path).join(&cmd_config.command.exe_path);
|
||||
|
||||
if let Ok(_) = execute_exe(
|
||||
if exe_path_absolute.exists() {
|
||||
exe_path_absolute.to_str().unwrap()
|
||||
} else {
|
||||
exe_path_local.to_str().unwrap()
|
||||
},
|
||||
&cmd_config.command.exe_args,
|
||||
) {
|
||||
let random_cmd_sound = cmd_config
|
||||
.voice
|
||||
.sounds
|
||||
.choose(&mut rand::thread_rng())
|
||||
.unwrap();
|
||||
events::play(random_cmd_sound, app_handle);
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
Err("AHK process spawn error (does exe path is valid?)".into())
|
||||
}
|
||||
}
|
||||
"cli" => {
|
||||
// CLI command type
|
||||
let exe_path_absolute = Path::new(&cmd_config.command.exe_path);
|
||||
let exe_path_local = Path::new(&cmd_path).join(&cmd_config.command.exe_path);
|
||||
|
||||
if let Ok(_) = execute_exe(
|
||||
if exe_path_absolute.exists() {
|
||||
exe_path_absolute.to_str().unwrap()
|
||||
} else {
|
||||
exe_path_local.to_str().unwrap()
|
||||
},
|
||||
&cmd_config.command.exe_args,
|
||||
) {
|
||||
let random_cmd_sound = cmd_config
|
||||
.voice
|
||||
.sounds
|
||||
.choose(&mut rand::thread_rng())
|
||||
.unwrap();
|
||||
events::play(random_cmd_sound, app_handle);
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
Err("Shell process spawn error (does cli command is valid?)".into())
|
||||
}
|
||||
}
|
||||
"terminate" => {
|
||||
// TERMINATE command type
|
||||
let random_cmd_sound = cmd_config
|
||||
.voice
|
||||
.sounds
|
||||
.choose(&mut rand::thread_rng())
|
||||
.unwrap();
|
||||
events::play(random_cmd_sound, app_handle);
|
||||
|
||||
std::thread::sleep(Duration::from_secs(2));
|
||||
std::process::exit(0);
|
||||
}
|
||||
_ => Err("Command type unknown".into()),
|
||||
}
|
||||
}
|
||||
32
src-tauri/src/assistant_commands/structs.rs
Normal file
32
src-tauri/src/assistant_commands/structs.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
use serde::Deserialize;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AssistantCommand {
|
||||
pub path: PathBuf,
|
||||
pub commands: CommandsList,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct CommandsList {
|
||||
pub list: Vec<Config>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct Config {
|
||||
pub command: ConfigCommandSection,
|
||||
pub voice: ConfigVoiceSection,
|
||||
pub phrases: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct ConfigCommandSection {
|
||||
pub action: String,
|
||||
pub exe_path: String,
|
||||
pub exe_args: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct ConfigVoiceSection {
|
||||
pub sounds: Vec<String>,
|
||||
}
|
||||
42
src-tauri/src/config.rs
Normal file
42
src-tauri/src/config.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
use const_concat::const_concat;
|
||||
use std::env::current_dir;
|
||||
|
||||
// pub const IS_DEV: bool = cfg!(debug_assertions);// cfg!(debug_assertions);
|
||||
// pub const PUBLIC_PATH: &str = if IS_DEV {
|
||||
// "D:/Rust/jarvis-app/public"
|
||||
// } else {
|
||||
// "./public"
|
||||
// };
|
||||
|
||||
pub const COMMANDS_PATH: &str = "commands/";
|
||||
|
||||
pub const DB_FILE_NAME: &str = "app.db";
|
||||
pub const APP_VERSION: Option<&str> = option_env!("CARGO_PKG_VERSION");
|
||||
pub const AUTHOR_NAME: Option<&str> = option_env!("CARGO_PKG_AUTHORS");
|
||||
pub const REPOSITORY_LINK: Option<&str> = option_env!("CARGO_PKG_REPOSITORY");
|
||||
|
||||
// pub const VOSK_MODEL_PATH: &str = const_concat!(PUBLIC_PATH, "/vosk/model_small");
|
||||
pub const VOSK_MODEL_PATH: &str = "vosk/model_small";
|
||||
|
||||
pub const CMD_RATIO_THRESHOLD: f64 = 60f64;
|
||||
pub const CMS_WAIT_DELAY: std::time::Duration = std::time::Duration::from_secs(10);
|
||||
|
||||
pub const ASSISTANT_GREET_PHRASES: [&str; 3] = ["greet1", "greet2", "greet3"];
|
||||
pub const ASSISTANT_PHRASES_TBR: [&str; 16] = [
|
||||
"сэр",
|
||||
"слушаю сэр",
|
||||
"всегда к услугам",
|
||||
"произнеси",
|
||||
"ответь",
|
||||
"покажи",
|
||||
"скажи",
|
||||
"давай",
|
||||
"да сэр",
|
||||
"к вашим услугам сэр",
|
||||
"всегда к вашим услугам сэр",
|
||||
"запрос выполнен сэр",
|
||||
"выполнен сэр",
|
||||
"есть",
|
||||
"загружаю сэр",
|
||||
"очень тонкое замечание сэр",
|
||||
];
|
||||
40
src-tauri/src/events.rs
Normal file
40
src-tauri/src/events.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
use tauri::Manager;
|
||||
|
||||
// the payload type must implement `Serialize` and `Clone`.
|
||||
#[derive(Clone, serde::Serialize)]
|
||||
pub struct Payload {
|
||||
pub data: String,
|
||||
}
|
||||
|
||||
pub enum EventTypes {
|
||||
AudioPlay,
|
||||
AssistantWaiting,
|
||||
AssistantGreet,
|
||||
CommandStart,
|
||||
CommandInProcess,
|
||||
CommandEnd,
|
||||
}
|
||||
|
||||
impl EventTypes {
|
||||
pub fn get(&self) -> &str {
|
||||
match self {
|
||||
Self::AudioPlay => "audio-play",
|
||||
Self::AssistantWaiting => "assistant-waiting",
|
||||
Self::AssistantGreet => "assistant-greet",
|
||||
Self::CommandStart => "command-start",
|
||||
Self::CommandInProcess => "command-in-process",
|
||||
Self::CommandEnd => "command-end",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn play(phrase: &str, app_handle: &tauri::AppHandle) {
|
||||
app_handle
|
||||
.emit_all(
|
||||
EventTypes::AudioPlay.get(),
|
||||
Payload {
|
||||
data: phrase.into(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
71
src-tauri/src/lib.rs
Normal file
71
src-tauri/src/lib.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
// Taken from https://github.com/Vurich/const-concat/issues/13
|
||||
|
||||
#![no_std]
|
||||
|
||||
use core::mem::ManuallyDrop;
|
||||
|
||||
const unsafe fn transmute_prefix<From, To>(from: From) -> To {
|
||||
union Transmute<From, To> {
|
||||
from: ManuallyDrop<From>,
|
||||
to: ManuallyDrop<To>,
|
||||
}
|
||||
|
||||
ManuallyDrop::into_inner(
|
||||
Transmute {
|
||||
from: ManuallyDrop::new(from),
|
||||
}
|
||||
.to,
|
||||
)
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// `Len1 + Len2 >= Len3`
|
||||
#[doc(hidden)]
|
||||
#[allow(non_upper_case_globals)]
|
||||
pub const unsafe fn concat<const Len1: usize, const Len2: usize, const Len3: usize>(
|
||||
arr1: [u8; Len1],
|
||||
arr2: [u8; Len2],
|
||||
) -> [u8; Len3] {
|
||||
#[repr(C)]
|
||||
struct Concat<A, B>(A, B);
|
||||
transmute_prefix(Concat(arr1, arr2))
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! const_concat {
|
||||
() => ("");
|
||||
($a:expr) => ($a);
|
||||
|
||||
($a:expr, $b:expr $(,)?) => {{
|
||||
const A: &str = $a;
|
||||
const B: &str = $b;
|
||||
const BYTES: [u8; { A.len() + B.len() }] = unsafe {
|
||||
$crate::concat::<
|
||||
{ A.len() },
|
||||
{ B.len() },
|
||||
{ A.len() + B.len() }
|
||||
>(
|
||||
*A.as_ptr().cast(),
|
||||
*B.as_ptr().cast(),
|
||||
)
|
||||
};
|
||||
unsafe { ::core::str::from_utf8_unchecked(&BYTES) }
|
||||
}};
|
||||
|
||||
($a:expr, $b:expr, $($rest:expr),+ $(,)?) => {{
|
||||
const TAIL: &str = $crate::const_concat!($b, $($rest),+);
|
||||
$crate::const_concat!($a, TAIL)
|
||||
}}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tests() {
|
||||
const SALUTATION: &str = "Hello";
|
||||
const TARGET: &str = "world";
|
||||
const GREETING: &str = const_concat!(SALUTATION, ", ", TARGET, "!");
|
||||
const GREETING_TRAILING_COMMA: &str = const_concat!(SALUTATION, ", ", TARGET, "!",);
|
||||
|
||||
assert_eq!(GREETING, "Hello, world!");
|
||||
assert_eq!(GREETING_TRAILING_COMMA, "Hello, world!");
|
||||
}
|
||||
90
src-tauri/src/main.rs
Normal file
90
src-tauri/src/main.rs
Normal file
@@ -0,0 +1,90 @@
|
||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||
// #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
#[macro_use]
|
||||
extern crate lazy_static; // better switch to once_cell ?
|
||||
use pickledb::{PickleDb, PickleDbDumpPolicy, SerializationMethod};
|
||||
use std::sync::Mutex;
|
||||
|
||||
// expose the config
|
||||
mod config;
|
||||
use config::*;
|
||||
|
||||
// include tauri commands
|
||||
mod tauri_commands;
|
||||
|
||||
// include assistant commands
|
||||
mod assistant_commands;
|
||||
use assistant_commands::AssistantCommand;
|
||||
|
||||
// include vosk
|
||||
mod vosk;
|
||||
|
||||
// include events
|
||||
mod events;
|
||||
|
||||
// app dir
|
||||
lazy_static! {
|
||||
static ref APP_CONFIG_DIR: Mutex<String> = Mutex::new(String::new());
|
||||
}
|
||||
|
||||
// init PickleDb connection
|
||||
lazy_static! {
|
||||
static ref DB: Mutex<PickleDb> = Mutex::new(
|
||||
PickleDb::load(
|
||||
format!("{}/{}", APP_CONFIG_DIR.lock().unwrap(), DB_FILE_NAME),
|
||||
PickleDbDumpPolicy::AutoDump,
|
||||
SerializationMethod::Json
|
||||
)
|
||||
.unwrap_or_else(|_x: _| {
|
||||
println!("Creating new db file at {} ...", format!("{}/{}", APP_CONFIG_DIR.lock().unwrap(), DB_FILE_NAME));
|
||||
PickleDb::new(
|
||||
format!("{}/{}", APP_CONFIG_DIR.lock().unwrap(), DB_FILE_NAME),
|
||||
PickleDbDumpPolicy::AutoDump,
|
||||
SerializationMethod::Json,
|
||||
)
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// init commands
|
||||
lazy_static! {
|
||||
static ref COMMANDS: Vec<AssistantCommand> = assistant_commands::parse_commands().unwrap();
|
||||
}
|
||||
|
||||
fn main() {
|
||||
vosk::init_vosk();
|
||||
|
||||
tauri::Builder::default()
|
||||
.setup(|app| {
|
||||
std::fs::create_dir_all(app.path_resolver().app_config_dir().unwrap())?;
|
||||
APP_CONFIG_DIR.lock().unwrap().push_str(app.path_resolver().app_config_dir().unwrap().to_str().unwrap());
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
// db commands
|
||||
tauri_commands::db_read,
|
||||
tauri_commands::db_write,
|
||||
// recorder commands
|
||||
tauri_commands::pv_get_audio_devices,
|
||||
tauri_commands::pv_get_audio_device_name,
|
||||
// listener commands
|
||||
tauri_commands::start_listening,
|
||||
tauri_commands::stop_listening,
|
||||
tauri_commands::is_listening,
|
||||
// sys commands
|
||||
tauri_commands::get_current_ram_usage,
|
||||
tauri_commands::get_peak_ram_usage,
|
||||
tauri_commands::get_cpu_temp,
|
||||
tauri_commands::get_cpu_usage,
|
||||
// sound commands
|
||||
tauri_commands::play_sound,
|
||||
// etc commands
|
||||
tauri_commands::get_app_version,
|
||||
tauri_commands::get_author_name,
|
||||
tauri_commands::get_repository_link
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
23
src-tauri/src/tauri_commands.rs
Normal file
23
src-tauri/src/tauri_commands.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
// import DB related commands
|
||||
mod db;
|
||||
pub use db::*;
|
||||
|
||||
// import RECORDER commands
|
||||
mod recorder;
|
||||
pub use recorder::*;
|
||||
|
||||
// import PORCUPINE commands
|
||||
mod listener;
|
||||
pub use listener::*;
|
||||
|
||||
// import SYS commands
|
||||
mod sys;
|
||||
pub use sys::*;
|
||||
|
||||
// import VOICE commands
|
||||
mod voice;
|
||||
pub use voice::*;
|
||||
|
||||
// import ETC commands
|
||||
mod etc;
|
||||
pub use etc::*;
|
||||
19
src-tauri/src/tauri_commands/db.rs
Normal file
19
src-tauri/src/tauri_commands/db.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
use crate::DB;
|
||||
|
||||
#[tauri::command]
|
||||
pub fn db_read(key: &str) -> String {
|
||||
if let Some(value) = DB.lock().unwrap().get(key) {
|
||||
value
|
||||
} else {
|
||||
String::from("")
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn db_write(key: &str, val: &str) -> bool {
|
||||
if let Ok(_) = DB.lock().unwrap().set(key, &val) {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
32
src-tauri/src/tauri_commands/etc.rs
Normal file
32
src-tauri/src/tauri_commands/etc.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
use crate::config::APP_VERSION;
|
||||
use crate::config::AUTHOR_NAME;
|
||||
use crate::config::REPOSITORY_LINK;
|
||||
|
||||
// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_app_version() -> String {
|
||||
if let Some(ver) = APP_VERSION {
|
||||
ver.to_string()
|
||||
} else {
|
||||
String::from("error")
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_author_name() -> String {
|
||||
if let Some(ver) = AUTHOR_NAME {
|
||||
ver.to_string()
|
||||
} else {
|
||||
String::from("error")
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_repository_link() -> String {
|
||||
if let Some(ver) = REPOSITORY_LINK {
|
||||
ver.to_string()
|
||||
} else {
|
||||
String::from("error")
|
||||
}
|
||||
}
|
||||
193
src-tauri/src/tauri_commands/listener.rs
Normal file
193
src-tauri/src/tauri_commands/listener.rs
Normal file
@@ -0,0 +1,193 @@
|
||||
use porcupine::{BuiltinKeywords, Porcupine, PorcupineBuilder};
|
||||
use pv_recorder::RecorderBuilder;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
use crate::events::Payload;
|
||||
use tauri::Manager;
|
||||
|
||||
use rand::seq::SliceRandom;
|
||||
use std::time::SystemTime;
|
||||
|
||||
use crate::assistant_commands;
|
||||
use crate::events;
|
||||
|
||||
use crate::config;
|
||||
use crate::vosk;
|
||||
|
||||
use crate::COMMANDS;
|
||||
use crate::DB;
|
||||
|
||||
// track listening state
|
||||
static LISTENING: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
// stop listening with Atomic flag (to make it work between different threads)
|
||||
static STOP_LISTENING: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
#[tauri::command]
|
||||
pub fn is_listening() -> bool {
|
||||
LISTENING.load(Ordering::SeqCst)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn stop_listening() {
|
||||
if is_listening() {
|
||||
STOP_LISTENING.store(true, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
// wait until listening stops
|
||||
while is_listening() {}
|
||||
}
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn start_listening(app_handle: tauri::AppHandle) -> Result<bool, String> {
|
||||
// only one listener thread is allowed
|
||||
if is_listening() {
|
||||
return Err("Already listening.".into());
|
||||
}
|
||||
|
||||
// vars
|
||||
let porcupine: Porcupine;
|
||||
let picovoice_api_key: String;
|
||||
let selected_microphone: i32;
|
||||
|
||||
let mut start = SystemTime::now();
|
||||
|
||||
// Retrieve API key from DB
|
||||
if let Some(pkey) = DB.lock().unwrap().get::<String>("api_key__picovoice") {
|
||||
picovoice_api_key = pkey;
|
||||
} else {
|
||||
return Err("Picovoice API key is not set!".into());
|
||||
}
|
||||
|
||||
// Create instance of Porcupine with the given API key
|
||||
if let Ok(pinstance) =
|
||||
PorcupineBuilder::new_with_keywords(picovoice_api_key, &[BuiltinKeywords::Jarvis])
|
||||
.sensitivities(&[1.0f32]) // max sensitivity possible
|
||||
.init()
|
||||
{
|
||||
// porcupine successfully initialized with the valid API key
|
||||
porcupine = pinstance;
|
||||
} else {
|
||||
// something went wrong
|
||||
return Err(
|
||||
"Porcupine error: either API key is not valid or there is no internet connection"
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
// Retrieve microphone index
|
||||
if let Some(smic) = DB.lock().unwrap().get::<String>("selected_microphone") {
|
||||
selected_microphone = smic.parse().unwrap_or(-1);
|
||||
} else {
|
||||
selected_microphone = -1; // use default, if not selected
|
||||
}
|
||||
|
||||
// Create recorder instance
|
||||
let recorder = RecorderBuilder::new()
|
||||
.device_index(selected_microphone)
|
||||
.frame_length(porcupine.frame_length() as i32)
|
||||
.init()
|
||||
.expect("Failed to initialize pvrecorder");
|
||||
|
||||
// Start recording
|
||||
println!("Listening (microphone idx = {selected_microphone}) ...");
|
||||
recorder.start().expect("Failed to start audio recording");
|
||||
LISTENING.store(true, Ordering::SeqCst);
|
||||
|
||||
// Greet user
|
||||
events::play("run", &app_handle);
|
||||
|
||||
// Listen until stop flag will be true
|
||||
let mut frame_buffer = vec![0; porcupine.frame_length() as usize];
|
||||
while !STOP_LISTENING.load(Ordering::SeqCst) {
|
||||
recorder
|
||||
.read(&mut frame_buffer)
|
||||
.expect("Failed to read audio frame");
|
||||
|
||||
if let Ok(keyword_index) = porcupine.process(&frame_buffer) {
|
||||
if keyword_index >= 0 {
|
||||
println!("Yes, sir! {}", keyword_index);
|
||||
events::play(
|
||||
config::ASSISTANT_GREET_PHRASES
|
||||
.choose(&mut rand::thread_rng())
|
||||
.unwrap(),
|
||||
&app_handle,
|
||||
);
|
||||
start = SystemTime::now();
|
||||
|
||||
app_handle
|
||||
.emit_all(events::EventTypes::AssistantGreet.get(), ())
|
||||
.unwrap();
|
||||
|
||||
loop {
|
||||
recorder
|
||||
.read(&mut frame_buffer)
|
||||
.expect("Failed to read audio frame");
|
||||
|
||||
// vosk part (partials included)
|
||||
if let Some(mut test) = vosk::recognize(&frame_buffer) {
|
||||
if !test.is_empty() {
|
||||
println!("Recognized: {}", test);
|
||||
|
||||
// some filtration
|
||||
test = test.to_lowercase();
|
||||
for tbr in config::ASSISTANT_PHRASES_TBR {
|
||||
test = test.replace(tbr, "");
|
||||
}
|
||||
|
||||
// infer command
|
||||
if let Some((cmd_path, cmd_config)) =
|
||||
assistant_commands::fetch_command(&test, &COMMANDS)
|
||||
{
|
||||
println!("Recognized (filtered): {}", test);
|
||||
println!("Command found: {:?}", cmd_path);
|
||||
println!("Executing ...");
|
||||
|
||||
let cmd_result = assistant_commands::execute_command(
|
||||
&cmd_path,
|
||||
&cmd_config,
|
||||
&app_handle,
|
||||
);
|
||||
|
||||
match cmd_result {
|
||||
Ok(_) => {
|
||||
println!("Command executed successfully!");
|
||||
start = SystemTime::now(); // listen for more commands
|
||||
continue;
|
||||
}
|
||||
Err(error_message) => {
|
||||
println!("Error executing command: {}", error_message);
|
||||
}
|
||||
}
|
||||
|
||||
app_handle
|
||||
.emit_all(events::EventTypes::AssistantWaiting.get(), ())
|
||||
.unwrap();
|
||||
break; // return to picovoice after command execution (no matter successfull or not)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match start.elapsed() {
|
||||
Ok(elapsed) if elapsed > config::CMS_WAIT_DELAY => {
|
||||
// return to picovoice after N seconds
|
||||
app_handle
|
||||
.emit_all(events::EventTypes::AssistantWaiting.get(), ())
|
||||
.unwrap();
|
||||
break;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stop listening
|
||||
println!("Stop listening ...");
|
||||
recorder.stop().expect("Failed to stop audio recording");
|
||||
LISTENING.store(false, Ordering::SeqCst);
|
||||
STOP_LISTENING.store(false, Ordering::SeqCst);
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
33
src-tauri/src/tauri_commands/recorder.rs
Normal file
33
src-tauri/src/tauri_commands/recorder.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
use pv_recorder::RecorderBuilder;
|
||||
|
||||
#[tauri::command]
|
||||
pub fn pv_get_audio_devices() -> Vec<String> {
|
||||
let audio_devices = RecorderBuilder::default().get_audio_devices();
|
||||
match audio_devices {
|
||||
Ok(audio_devices) => audio_devices,
|
||||
Err(err) => panic!("Failed to get audio devices: {}", err),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn pv_get_audio_device_name(idx: i32) -> String {
|
||||
let audio_devices = RecorderBuilder::default().get_audio_devices();
|
||||
let mut first_device: String = String::new();
|
||||
match audio_devices {
|
||||
Ok(audio_devices) => {
|
||||
for (_idx, device) in audio_devices.iter().enumerate() {
|
||||
if idx as usize == _idx {
|
||||
return device.to_string();
|
||||
}
|
||||
|
||||
if _idx == 0 {
|
||||
first_device = device.to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => panic!("Failed to get audio devices: {}", err),
|
||||
};
|
||||
|
||||
// return first device as default, if none were matched
|
||||
first_device
|
||||
}
|
||||
48
src-tauri/src/tauri_commands/sys.rs
Normal file
48
src-tauri/src/tauri_commands/sys.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
use peak_alloc::PeakAlloc;
|
||||
|
||||
#[global_allocator]
|
||||
static PEAK_ALLOC: PeakAlloc = PeakAlloc;
|
||||
|
||||
extern crate systemstat;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use systemstat::{saturating_sub_bytes, Platform, System};
|
||||
|
||||
lazy_static! {
|
||||
static ref SYS: System = System::new();
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_current_ram_usage() -> String {
|
||||
let result = String::from(format!("{}", PEAK_ALLOC.current_usage_as_mb()));
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_peak_ram_usage() -> String {
|
||||
let result = String::from(format!("{}", PEAK_ALLOC.peak_usage_as_gb()));
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_cpu_temp() -> String {
|
||||
if let Ok(cpu_temp) = SYS.cpu_temp() {
|
||||
String::from(format!("{}", cpu_temp))
|
||||
} else {
|
||||
String::from("error")
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/valpackett/systemstat/blob/trunk/examples/info.rs
|
||||
#[tauri::command(async)]
|
||||
pub async fn get_cpu_usage() -> String {
|
||||
if let Ok(cpu) = SYS.cpu_load_aggregate() {
|
||||
thread::sleep(Duration::from_secs(1));
|
||||
let cpu = cpu.done().unwrap();
|
||||
String::from(format!("{}", cpu.user * 100.0))
|
||||
} else {
|
||||
String::from("error")
|
||||
}
|
||||
}
|
||||
29
src-tauri/src/tauri_commands/voice.rs
Normal file
29
src-tauri/src/tauri_commands/voice.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
use std::fs::File;
|
||||
use std::io::BufReader;
|
||||
use rodio::{Decoder, OutputStream, Sink};
|
||||
|
||||
#[tauri::command(async)]
|
||||
pub fn play_sound(filename: &str, sleep: bool) {
|
||||
// Get a output stream handle to the default physical sound device
|
||||
let (_stream, stream_handle) = OutputStream::try_default().unwrap();
|
||||
let sink = Sink::try_new(&stream_handle).unwrap();
|
||||
|
||||
// Load a sound from a file, using a path relative to Cargo.toml
|
||||
// let filepath = format!("{PUBLIC_PATH}/sound/{filename}.wav");
|
||||
let filepath = filename;
|
||||
let file = BufReader::new(File::open(&filepath).unwrap());
|
||||
|
||||
// Decode that sound file into a source
|
||||
let source = Decoder::new(file).unwrap();
|
||||
|
||||
// Play the sound directly on the device
|
||||
println!("Playing {} ...", filepath);
|
||||
// stream_handle.play_raw(source.convert_samples());
|
||||
sink.append(source);
|
||||
|
||||
if sleep {
|
||||
// The sound plays in a separate thread. This call will block the current thread until the sink
|
||||
// has finished playing all its queued sounds.
|
||||
sink.sleep_until_end();
|
||||
}
|
||||
}
|
||||
58
src-tauri/src/vosk.rs
Normal file
58
src-tauri/src/vosk.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
use std::sync::Mutex;
|
||||
use vosk::{CompleteResult, DecodingState, Model, Recognizer};
|
||||
|
||||
use crate::config::VOSK_MODEL_PATH;
|
||||
|
||||
lazy_static! {
|
||||
static ref MODEL: Model = Model::new(VOSK_MODEL_PATH).unwrap();
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref RECOGNIZER: Mutex<Recognizer> =
|
||||
Mutex::new(Recognizer::new(&MODEL, 16000.0).unwrap());
|
||||
}
|
||||
|
||||
pub fn init_vosk() {
|
||||
RECOGNIZER.lock().unwrap().set_max_alternatives(10);
|
||||
RECOGNIZER.lock().unwrap().set_words(true);
|
||||
RECOGNIZER.lock().unwrap().set_partial_words(true);
|
||||
}
|
||||
|
||||
pub fn recognize(data: &[i16]) -> Option<String> {
|
||||
let state = RECOGNIZER.lock().unwrap().accept_waveform(data);
|
||||
|
||||
match state {
|
||||
DecodingState::Running => {
|
||||
None
|
||||
// Some(RECOGNIZER.lock().unwrap().partial_result().partial.into())
|
||||
}
|
||||
DecodingState::Finalized => {
|
||||
// Result will always be multiple because we called set_max_alternatives
|
||||
Some(
|
||||
RECOGNIZER
|
||||
.lock()
|
||||
.unwrap()
|
||||
.result()
|
||||
.multiple()
|
||||
.unwrap()
|
||||
.alternatives
|
||||
.first()
|
||||
.unwrap()
|
||||
.text
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
DecodingState::Failed => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stereo_to_mono(input_data: &[i16]) -> Vec<i16> {
|
||||
let mut result = Vec::with_capacity(input_data.len() / 2);
|
||||
result.extend(
|
||||
input_data
|
||||
.chunks_exact(2)
|
||||
.map(|chunk| chunk[0] / 2 + chunk[1] / 2),
|
||||
);
|
||||
|
||||
result
|
||||
}
|
||||
Reference in New Issue
Block a user