Update to Rust programming language.

This commit is contained in:
Abraham
2023-04-27 00:28:36 +05:00
parent 943efbfbdb
commit f88248643b
201 changed files with 12954 additions and 1690 deletions

4
src-tauri/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
# Generated by Cargo
# will have compiled files and executables
/target/
app.db

1
src-tauri/.taurignore Normal file
View File

@@ -0,0 +1 @@
*.db

4024
src-tauri/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

39
src-tauri/Cargo.toml Normal file
View File

@@ -0,0 +1,39 @@
[package]
name = "jarvis-app"
version = "0.0.1"
description = "Jarvis Voice Assistant"
authors = ["Abraham Tugalov"]
license = "GPL-3.0-only"
repository = "https://github.com/Priler/jarvis"
edition = "2021"
[lib]
name = "const_concat"
path = "src/lib.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
tauri-build = { version = "1.2", features = [] }
[dependencies]
tauri = { version = "1.2", features = ["dialog-message", "path-all", "shell-open"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
lazy_static = "1.4.0"
pickledb = "0.5.1"
peak_alloc = "0.2.0"
systemstat = "0.2.3"
hound = "3.5.0"
pv_recorder = "1.1.1"
pv_porcupine = "2.2.0"
rodio = "0.17.1"
serde_yaml = "0.9.21"
seqdiff = "0.3.0"
vosk = "0.2.0"
rand = "0.8.5"
[features]
# this feature is used for production builds or when `devPath` points to the filesystem
# DO NOT REMOVE!!
custom-protocol = ["tauri/custom-protocol"]

45
src-tauri/Makefile.toml Normal file
View File

@@ -0,0 +1,45 @@
[tasks.format]
install_crate = "rustfmt"
command = "cargo"
args = ["fmt", "--", "--emit=files"]
[tasks.clean]
command = "cargo"
args = ["clean"]
[tasks.build_debug]
command = "cargo"
args = ["build"]
[tasks.run]
command = "cargo"
args = ["run"]
[tasks.build_release]
command = "cargo"
args = ["build", "--release"]
dependencies = ["clean"]
[tasks.test]
command = "cargo"
args = ["test"]
# dependencies = ["clean"]
[tasks.vosk]
script_runner = "python"
script_extension = "py"
script = { file = "vosk_build.py" }
[tasks.debug]
dependencies = [
"format",
"build_debug",
"vosk"
]
[tasks.release]
dependencies = [
"format",
"build_release",
"vosk"
]

View File

@@ -0,0 +1 @@
[{"assistant_voice":"\"jarvis-remake\"","selected_microphone":"\"0\"","api_key__picovoice":"\"Hl7tfFyDT+S6fLhcT2nngK2qXsbhAwMsrVVp0Y9G0A2IfLlsPTm9eg==\"","api_key__openai":"\"\""},{}]

9
src-tauri/build.rs Normal file
View File

@@ -0,0 +1,9 @@
fn main() {
// link to Vosk lib
println!("cargo:rustc-link-search=vosk/");
// println!("cargo:rustc-link-lib=dylib=D:/Rust/vosk/libvosk.dll");
// Tauri build
tauri_build::build()
}

View File

@@ -0,0 +1,6 @@
#NoEnv ; Recommended for performance and compatibility with future AutoHotkey releases.
; #Warn ; Enable warnings to assist with detecting common errors.
SendMode Input ; Recommended for new scripts due to its superior speed and reliability.
SetWorkingDir %A_ScriptDir% ; Ensures a consistent starting directory.
Run C:/Program Files (x86)/Google/Chrome/Application/chrome.exe

Binary file not shown.

View File

@@ -0,0 +1,6 @@
#NoEnv ; Recommended for performance and compatibility with future AutoHotkey releases.
; #Warn ; Enable warnings to assist with detecting common errors.
SendMode Input ; Recommended for new scripts due to its superior speed and reliability.
SetWorkingDir %A_ScriptDir% ; Ensures a consistent starting directory.
Run C:/Program Files (x86)/Google/Chrome/Application/chrome.exe "https://google.com"

Binary file not shown.

View File

@@ -0,0 +1,6 @@
#NoEnv ; Recommended for performance and compatibility with future AutoHotkey releases.
; #Warn ; Enable warnings to assist with detecting common errors.
SendMode Input ; Recommended for new scripts due to its superior speed and reliability.
SetWorkingDir %A_ScriptDir% ; Ensures a consistent starting directory.
Run C:/Program Files (x86)/Google/Chrome/Application/chrome.exe "https://youtube.com"

Binary file not shown.

View File

@@ -0,0 +1,46 @@
list:
- command:
action: ahk
exe_path: ahk/Run browser.exe
exe_args:
voice:
sounds:
- ok1
- ok2
- ok3
phrases:
- открой браузер
- запусти браузер
- открой гугл хром
- гугл хром
- command:
action: ahk
exe_path: ahk/Run google.exe
exe_args:
voice:
sounds:
- ok1
- ok2
- ok3
- ok4
phrases:
- открой гугл
- гугл
- запусти гугл
- перейди в гугл
- command:
action: ahk
exe_path: ahk/Run youtube.exe
exe_args:
voice:
sounds:
- ok1
- ok2
- ok3
- ok4
phrases:
- открой ютуб
- ютуб
- запусти ютуб

View File

@@ -0,0 +1,53 @@
list:
- command:
action: voice
exe_path:
exe_args:
voice:
sounds:
- joke1
- joke2
- joke3
- joke4
- joke5
phrases:
- расскажи анекдот
- рассмеши
- пошути
- шутка
- расскажи шутку
- развесели меня
- что нибудь смешное
- подними мне настроение
- мне скучно
- хочу шутку
- хочу анекдот
- пошути
- расскажи что нибудь смешное
- расскажи смешное что нибудь
- хочу посмеяться
- command:
action: voice
exe_path:
exe_args:
voice:
sounds:
- thanks
phrases:
- спасибо
- молодец
- респект
- ты супер
- отличная работа
- ты крут
- ты большой молодец
- ты реально крут
- ты афигенный
- классная шутка
- очень смешно
- ты меня рассмешил
- веселая шутка
- смешной анекдот
- это было весело
- интересная шутка

View File

@@ -0,0 +1,13 @@
list:
- command:
action: voice
exe_path:
exe_args:
voice:
sounds:
- stupid
phrases:
- ты дурак
- ты дебил
- ты глупый
- ты тупой

View File

@@ -0,0 +1,19 @@
list:
- command:
action: terminate
exe_path:
exe_args:
voice:
sounds:
- off
phrases:
- выключись
- вырубись
- завершить работу
- закройся
- отключись
- заверши свою работу
- на сегодня хватит
- выгрузи себя из памяти
- ты мне надоел
- пора спать

View File

@@ -0,0 +1,20 @@
list:
- command:
action: voice
exe_path:
exe_args:
voice:
sounds:
- thanks
phrases:
- спасибо
- молодец
- респект
- ты супер
- отличная работа
- ты крут
- ты большой молодец
- ты реально крут
- ты афигенный
- ты отец
- вечно ты молодец

View File

@@ -0,0 +1,6 @@
#NoEnv ; Recommended for performance and compatibility with future AutoHotkey releases.
; #Warn ; Enable warnings to assist with detecting common errors.
SendMode Input ; Recommended for new scripts due to its superior speed and reliability.
SetWorkingDir %A_ScriptDir% ; Ensures a consistent starting directory.
Send {Volume_Mute} ; Mute/unmute the master volume.

Binary file not shown.

View File

@@ -0,0 +1,31 @@
list:
- command:
action: ahk
exe_path: ahk/Mute volume.exe
exe_args:
voice:
sounds:
- ok1
- ok2
- ok3
- ok4
phrases:
- выключи звук
- беззвучный режим
- режим без звука
- отключи звук
- command:
action: ahk
exe_path: ahk/Mute volume.exe
exe_args:
voice:
sounds:
- ok1
- ok2
- ok3
- ok4
phrases:
- включи звук
- режим со звуком
- верни звук

BIN
src-tauri/icons/128x128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

BIN
src-tauri/icons/32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 974 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 903 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
src-tauri/icons/icon.icns Normal file

Binary file not shown.

BIN
src-tauri/icons/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

BIN
src-tauri/icons/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

BIN
src-tauri/libstdc++-6.dll Normal file

Binary file not shown.

BIN
src-tauri/libvosk.dll Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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()),
}
}

View 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
View 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
View 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
View 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
View 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");
}

View 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::*;

View 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
}
}

View 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")
}
}

View 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)
}

View 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
}

View 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")
}
}

View 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
View 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
}

65
src-tauri/tauri.conf.json Normal file
View File

@@ -0,0 +1,65 @@
{
"build": {
"beforeDevCommand": "npm run dev",
"beforeBuildCommand": "npm run build",
"devPath": "http://localhost:1420",
"distDir": "../dist",
"withGlobalTauri": false
},
"package": {
"productName": "jarvis-app",
"version": "0.0.0"
},
"tauri": {
"allowlist": {
"all": false,
"shell": {
"all": false,
"open": true
},
"dialog": { "message": true },
"fs": {
"scope": ["$RESOURCE/*"]
},
"path": {
"all": true
}
},
"bundle": {
"active": true,
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"identifier": "com.priler.jarvis",
"targets": "all",
"resources": [
"commands",
"sound",
"vosk/model_small",
"libvosk.dll",
"libstdc++-6.dll",
"libwinpthread-1.dll",
"libgcc_s_seh-1.dll"
]
},
"security": {
"csp": null
},
"updater": {
"active": false
},
"windows": [
{
"fullscreen": false,
"resizable": false,
"title": "Jarvis Voice Assistant",
"width": 550,
"height": 820
}
]
}
}

Binary file not shown.

Binary file not shown.

BIN
src-tauri/vosk/libvosk.dll Normal file

Binary file not shown.

BIN
src-tauri/vosk/libvosk.lib Normal file

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,8 @@
Small Russian model for Vosk (Android, RPi, other small devices)
%WER 22.71 [ 9092 / 40042, 1124 ins, 1536 del, 6432 sub ] exp/chain_a/tdnn/decode_test_audiobooks_look_fast/wer_10_0.0
%WER 11.79 [ 5940 / 50394, 894 ins, 832 del, 4214 sub ] exp/chain_a/tdnn/decode_test_golos_crowd_look_fast/wer_11_0.0
%WER 21.34 [ 1789 / 8382, 173 ins, 440 del, 1176 sub ] exp/chain_a/tdnn/decode_test_golos_farfield_look_fast/wer_10_0.0
%WER 29.89 [ 5579 / 18666, 476 ins, 1550 del, 3553 sub ] exp/chain_a/tdnn/decode_test_sova_devices_look_fast/wer_10_0.0
%WER 31.97 [ 13588 / 42496, 1013 ins, 3640 del, 8935 sub ] exp/chain_a/tdnn/decode_test_youtube_look_fast/wer_9_0.0

Binary file not shown.

View File

@@ -0,0 +1,7 @@
--sample-frequency=16000
--use-energy=false
--num-mel-bins=40
--num-ceps=40
--low-freq=20
--high-freq=7600
--allow-downsample=true

View File

@@ -0,0 +1,10 @@
--min-active=200
--max-active=3000
--beam=10.0
--lattice-beam=2.0
--acoustic-scale=1.0
--frame-subsampling-factor=3
--endpoint.silence-phones=1:2:3:4:5:6:7:8:9:10
--endpoint.rule2.min-trailing-silence=0.5
--endpoint.rule3.min-trailing-silence=1.0
--endpoint.rule4.min-trailing-silence=2.0

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,5 @@
9855
9856
9857
9858
9859

View File

@@ -0,0 +1,202 @@
1 nonword
2 begin
3 end
4 internal
5 singleton
6 nonword
7 begin
8 end
9 internal
10 singleton
11 begin
12 end
13 internal
14 singleton
15 begin
16 end
17 internal
18 singleton
19 begin
20 end
21 internal
22 singleton
23 begin
24 end
25 internal
26 singleton
27 begin
28 end
29 internal
30 singleton
31 begin
32 end
33 internal
34 singleton
35 begin
36 end
37 internal
38 singleton
39 begin
40 end
41 internal
42 singleton
43 begin
44 end
45 internal
46 singleton
47 begin
48 end
49 internal
50 singleton
51 begin
52 end
53 internal
54 singleton
55 begin
56 end
57 internal
58 singleton
59 begin
60 end
61 internal
62 singleton
63 begin
64 end
65 internal
66 singleton
67 begin
68 end
69 internal
70 singleton
71 begin
72 end
73 internal
74 singleton
75 begin
76 end
77 internal
78 singleton
79 begin
80 end
81 internal
82 singleton
83 begin
84 end
85 internal
86 singleton
87 begin
88 end
89 internal
90 singleton
91 begin
92 end
93 internal
94 singleton
95 begin
96 end
97 internal
98 singleton
99 begin
100 end
101 internal
102 singleton
103 begin
104 end
105 internal
106 singleton
107 begin
108 end
109 internal
110 singleton
111 begin
112 end
113 internal
114 singleton
115 begin
116 end
117 internal
118 singleton
119 begin
120 end
121 internal
122 singleton
123 begin
124 end
125 internal
126 singleton
127 begin
128 end
129 internal
130 singleton
131 begin
132 end
133 internal
134 singleton
135 begin
136 end
137 internal
138 singleton
139 begin
140 end
141 internal
142 singleton
143 begin
144 end
145 internal
146 singleton
147 begin
148 end
149 internal
150 singleton
151 begin
152 end
153 internal
154 singleton
155 begin
156 end
157 internal
158 singleton
159 begin
160 end
161 internal
162 singleton
163 begin
164 end
165 internal
166 singleton
167 begin
168 end
169 internal
170 singleton
171 begin
172 end
173 internal
174 singleton
175 begin
176 end
177 internal
178 singleton
179 begin
180 end
181 internal
182 singleton
183 begin
184 end
185 internal
186 singleton
187 begin
188 end
189 internal
190 singleton
191 begin
192 end
193 internal
194 singleton
195 begin
196 end
197 internal
198 singleton
199 begin
200 end
201 internal
202 singleton

Some files were not shown because too many files have changed in this diff Show More