Update to Rust programming language.
4
src-tauri/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
app.db
|
||||
1
src-tauri/.taurignore
Normal file
@@ -0,0 +1 @@
|
||||
*.db
|
||||
4024
src-tauri/Cargo.lock
generated
Normal file
39
src-tauri/Cargo.toml
Normal 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
@@ -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"
|
||||
]
|
||||
1
src-tauri/app.db.temp.1682532589
Normal 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
@@ -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()
|
||||
}
|
||||
6
src-tauri/commands/browser/ahk/Run browser.ahk
Normal 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
|
||||
BIN
src-tauri/commands/browser/ahk/Run browser.exe
Normal file
6
src-tauri/commands/browser/ahk/Run google.ahk
Normal 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"
|
||||
BIN
src-tauri/commands/browser/ahk/Run google.exe
Normal file
6
src-tauri/commands/browser/ahk/Run youtube.ahk
Normal 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"
|
||||
BIN
src-tauri/commands/browser/ahk/Run youtube.exe
Normal file
46
src-tauri/commands/browser/command.yaml
Normal 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:
|
||||
- открой ютуб
|
||||
- ютуб
|
||||
- запусти ютуб
|
||||
53
src-tauri/commands/humor/command.yaml
Normal 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:
|
||||
- спасибо
|
||||
- молодец
|
||||
- респект
|
||||
- ты супер
|
||||
- отличная работа
|
||||
- ты крут
|
||||
- ты большой молодец
|
||||
- ты реально крут
|
||||
- ты афигенный
|
||||
- классная шутка
|
||||
- очень смешно
|
||||
- ты меня рассмешил
|
||||
- веселая шутка
|
||||
- смешной анекдот
|
||||
- это было весело
|
||||
- интересная шутка
|
||||
13
src-tauri/commands/stupid/command.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
list:
|
||||
- command:
|
||||
action: voice
|
||||
exe_path:
|
||||
exe_args:
|
||||
voice:
|
||||
sounds:
|
||||
- stupid
|
||||
phrases:
|
||||
- ты дурак
|
||||
- ты дебил
|
||||
- ты глупый
|
||||
- ты тупой
|
||||
19
src-tauri/commands/terminate/command.yaml
Normal file
@@ -0,0 +1,19 @@
|
||||
list:
|
||||
- command:
|
||||
action: terminate
|
||||
exe_path:
|
||||
exe_args:
|
||||
voice:
|
||||
sounds:
|
||||
- off
|
||||
phrases:
|
||||
- выключись
|
||||
- вырубись
|
||||
- завершить работу
|
||||
- закройся
|
||||
- отключись
|
||||
- заверши свою работу
|
||||
- на сегодня хватит
|
||||
- выгрузи себя из памяти
|
||||
- ты мне надоел
|
||||
- пора спать
|
||||
20
src-tauri/commands/thanks/command.yaml
Normal file
@@ -0,0 +1,20 @@
|
||||
list:
|
||||
- command:
|
||||
action: voice
|
||||
exe_path:
|
||||
exe_args:
|
||||
voice:
|
||||
sounds:
|
||||
- thanks
|
||||
phrases:
|
||||
- спасибо
|
||||
- молодец
|
||||
- респект
|
||||
- ты супер
|
||||
- отличная работа
|
||||
- ты крут
|
||||
- ты большой молодец
|
||||
- ты реально крут
|
||||
- ты афигенный
|
||||
- ты отец
|
||||
- вечно ты молодец
|
||||
6
src-tauri/commands/volume/ahk/Mute volume.ahk
Normal 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.
|
||||
BIN
src-tauri/commands/volume/ahk/Mute volume.exe
Normal file
31
src-tauri/commands/volume/command.yaml
Normal 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
|
After Width: | Height: | Size: 3.4 KiB |
BIN
src-tauri/icons/128x128@2x.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
src-tauri/icons/32x32.png
Normal file
|
After Width: | Height: | Size: 974 B |
BIN
src-tauri/icons/Square107x107Logo.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
src-tauri/icons/Square142x142Logo.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
src-tauri/icons/Square150x150Logo.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
src-tauri/icons/Square284x284Logo.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
src-tauri/icons/Square30x30Logo.png
Normal file
|
After Width: | Height: | Size: 903 B |
BIN
src-tauri/icons/Square310x310Logo.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
src-tauri/icons/Square44x44Logo.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
src-tauri/icons/Square71x71Logo.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
src-tauri/icons/Square89x89Logo.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
src-tauri/icons/StoreLogo.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
src-tauri/icons/icon.icns
Normal file
BIN
src-tauri/icons/icon.ico
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
src-tauri/icons/icon.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
src-tauri/libgcc_s_seh-1.dll
Normal file
BIN
src-tauri/libstdc++-6.dll
Normal file
BIN
src-tauri/libvosk.dll
Normal file
BIN
src-tauri/libwinpthread-1.dll
Normal file
BIN
src-tauri/sound/jarvis-og/game_mode.wav
Normal file
BIN
src-tauri/sound/jarvis-og/greet1.wav
Normal file
BIN
src-tauri/sound/jarvis-og/greet2.wav
Normal file
BIN
src-tauri/sound/jarvis-og/greet3.wav
Normal file
BIN
src-tauri/sound/jarvis-og/not_found.wav
Normal file
BIN
src-tauri/sound/jarvis-og/off.wav
Normal file
BIN
src-tauri/sound/jarvis-og/ok1.wav
Normal file
BIN
src-tauri/sound/jarvis-og/ok2.wav
Normal file
BIN
src-tauri/sound/jarvis-og/ok3.wav
Normal file
BIN
src-tauri/sound/jarvis-og/ok4.wav
Normal file
BIN
src-tauri/sound/jarvis-og/run.wav
Normal file
BIN
src-tauri/sound/jarvis-og/stupid.wav
Normal file
BIN
src-tauri/sound/jarvis-og/thanks.wav
Normal file
BIN
src-tauri/sound/jarvis-remake/game_mode.wav
Normal file
BIN
src-tauri/sound/jarvis-remake/greet1.wav
Normal file
BIN
src-tauri/sound/jarvis-remake/greet2.wav
Normal file
BIN
src-tauri/sound/jarvis-remake/greet3.wav
Normal file
BIN
src-tauri/sound/jarvis-remake/joke1.wav
Normal file
BIN
src-tauri/sound/jarvis-remake/joke2.wav
Normal file
BIN
src-tauri/sound/jarvis-remake/joke3.wav
Normal file
BIN
src-tauri/sound/jarvis-remake/joke4.wav
Normal file
BIN
src-tauri/sound/jarvis-remake/joke5.wav
Normal file
BIN
src-tauri/sound/jarvis-remake/not_found.wav
Normal file
BIN
src-tauri/sound/jarvis-remake/ok1.wav
Normal file
BIN
src-tauri/sound/jarvis-remake/ok2.wav
Normal file
BIN
src-tauri/sound/jarvis-remake/ok3.wav
Normal file
BIN
src-tauri/sound/jarvis-remake/ok4.wav
Normal file
BIN
src-tauri/sound/jarvis-remake/ready.wav
Normal file
BIN
src-tauri/sound/jarvis-remake/run.wav
Normal file
BIN
src-tauri/sound/jarvis-remake/stupid.wav
Normal file
BIN
src-tauri/sound/jarvis-remake/thanks.wav
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
BIN
src-tauri/vosk/libgcc_s_seh-1.dll
Normal file
BIN
src-tauri/vosk/libstdc++-6.dll
Normal file
BIN
src-tauri/vosk/libvosk.dll
Normal file
BIN
src-tauri/vosk/libvosk.lib
Normal file
BIN
src-tauri/vosk/libwinpthread-1.dll
Normal file
8
src-tauri/vosk/model_small/README
Normal 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
|
||||
|
||||
BIN
src-tauri/vosk/model_small/am/final.mdl
Normal file
7
src-tauri/vosk/model_small/conf/mfcc.conf
Normal 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
|
||||
10
src-tauri/vosk/model_small/conf/model.conf
Normal 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
|
||||
BIN
src-tauri/vosk/model_small/graph/Gr.fst
Normal file
BIN
src-tauri/vosk/model_small/graph/HCLr.fst
Normal file
5
src-tauri/vosk/model_small/graph/disambig_tid.int
Normal file
@@ -0,0 +1,5 @@
|
||||
9855
|
||||
9856
|
||||
9857
|
||||
9858
|
||||
9859
|
||||
202
src-tauri/vosk/model_small/graph/phones/word_boundary.int
Normal 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
|
||||