App icon, logs are now stored to log.txt file, work in progress on intergration with rustpotter

This commit is contained in:
Abraham
2023-04-29 14:58:16 +05:00
parent 4e1413d400
commit 5f35beb5cf
72 changed files with 515 additions and 2993 deletions

View File

@@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="icon" type="image/svg+xml" href="/media/app-icon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tauri + Svelte + TS</title>
</head>

View File

@@ -1,7 +1,7 @@
{
"name": "jarvis-app",
"private": true,
"version": "0.0.0",
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "routify -c dev:vite",

BIN
public/media/app-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 30 KiB

View File

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

148
src-tauri/Cargo.lock generated
View File

@@ -312,6 +312,33 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "ciborium"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0c137568cc60b904a7724001b35ce2630fd00d5d84805fbb608ab89509d788f"
dependencies = [
"ciborium-io",
"ciborium-ll",
"serde",
]
[[package]]
name = "ciborium-io"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "346de753af073cc87b52b2083a506b38ac176a44cfb05497b622e27be899b369"
[[package]]
name = "ciborium-ll"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "213030a2b5a4e0c0892b6652260cf6ccac84827b83a85a534e178e3906c4cf1b"
dependencies = [
"ciborium-io",
"half",
]
[[package]]
name = "clang-sys"
version = "1.6.1"
@@ -1145,6 +1172,12 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "half"
version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7"
[[package]]
name = "hashbrown"
version = "0.12.3"
@@ -1332,16 +1365,20 @@ version = "0.0.1"
dependencies = [
"hound",
"lazy_static",
"log",
"once_cell",
"peak_alloc",
"pickledb",
"pv_porcupine",
"pv_recorder",
"rand 0.8.5",
"rodio",
"rustpotter",
"seqdiff",
"serde",
"serde_json",
"serde_yaml",
"simple-logging",
"systemstat",
"tauri",
"tauri-build",
@@ -1721,6 +1758,15 @@ dependencies = [
"winapi",
]
[[package]]
name = "num-complex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02e0d21255c828d6f128a1e41534206671e8c3ea0c62f32291e808dc82cff17d"
dependencies = [
"num-traits",
]
[[package]]
name = "num-derive"
version = "0.3.3"
@@ -2129,6 +2175,15 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
[[package]]
name = "primal-check"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9df7f93fd637f083201473dab4fee2db4c429d32e55e3299980ab3957ab916a0"
dependencies = [
"num-integer",
]
[[package]]
name = "proc-macro-crate"
version = "1.3.1"
@@ -2303,6 +2358,21 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9"
[[package]]
name = "realfft"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93d6b8e8f0c6d2234aa58048d7290c60bf92cd36fd2888cd8331c66ad4f2e1d2"
dependencies = [
"rustfft",
]
[[package]]
name = "redox_syscall"
version = "0.1.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
[[package]]
name = "redox_syscall"
version = "0.2.16"
@@ -2401,6 +2471,18 @@ dependencies = [
"symphonia",
]
[[package]]
name = "rubato"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd70209c27d5b08f5528bdc779ea3ffb418954e28987f9f9775c6eac41003f9c"
dependencies = [
"num-complex",
"num-integer",
"num-traits",
"realfft",
]
[[package]]
name = "rustc-hash"
version = "1.1.0"
@@ -2416,6 +2498,21 @@ dependencies = [
"semver",
]
[[package]]
name = "rustfft"
version = "6.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e17d4f6cbdb180c9f4b2a26bbf01c4e647f1e1dea22fe8eb9db54198b32f9434"
dependencies = [
"num-complex",
"num-integer",
"num-traits",
"primal-check",
"strength_reduce",
"transpose",
"version_check",
]
[[package]]
name = "rustix"
version = "0.37.13"
@@ -2430,6 +2527,19 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "rustpotter"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6a88d0514dd5dcc988c78f61be4f60fdb37d4872e23c7d8d6d2d3ea23655f97"
dependencies = [
"ciborium",
"hound",
"rubato",
"rustfft",
"serde",
]
[[package]]
name = "rustversion"
version = "1.0.12"
@@ -2654,6 +2764,17 @@ version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "238abfbb77c1915110ad968465608b68e869e0772622c9656714e73e5a1a522f"
[[package]]
name = "simple-logging"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b00d48e85675326bb182a2286ea7c1a0b264333ae10f27a937a72be08628b542"
dependencies = [
"lazy_static",
"log",
"thread-id",
]
[[package]]
name = "siphasher"
version = "0.3.10"
@@ -2718,6 +2839,12 @@ dependencies = [
"loom",
]
[[package]]
name = "strength_reduce"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82"
[[package]]
name = "string_cache"
version = "0.8.7"
@@ -3147,6 +3274,17 @@ dependencies = [
"syn 2.0.15",
]
[[package]]
name = "thread-id"
version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7fbf4c9d56b320106cd64fd024dadfa0be7cb4706725fc44a7d7ce952d820c1"
dependencies = [
"libc",
"redox_syscall 0.1.57",
"winapi",
]
[[package]]
name = "thread_local"
version = "1.1.7"
@@ -3317,6 +3455,16 @@ dependencies = [
"tracing-log",
]
[[package]]
name = "transpose"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6522d49d03727ffb138ae4cbc1283d3774f0d10aa7f9bf52e6784c45daf9b23"
dependencies = [
"num-integer",
"strength_reduce",
]
[[package]]
name = "treediff"
version = "3.0.2"

View File

@@ -27,11 +27,15 @@ 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"
rodio = "0.17.1"
rustpotter = "2.0.0"
simple-logging = "2.0.2"
log = "0.4.17"
once_cell = "1.17.1"
[features]
# this feature is used for production builds or when `devPath` points to the filesystem

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 974 B

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 903 B

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

View File

@@ -8,7 +8,7 @@ use core::time::Duration;
use std::path::PathBuf;
use std::process::Child;
use std::process::Command;
use tauri::Manager;
// use tauri::Manager;
mod structs;
pub use structs::*;

View File

@@ -1,5 +1,4 @@
use const_concat::const_concat;
use std::env::current_dir;
// use const_concat::const_concat;
// pub const IS_DEV: bool = cfg!(debug_assertions);// cfg!(debug_assertions);
// pub const PUBLIC_PATH: &str = if IS_DEV {
@@ -8,14 +7,17 @@ use std::env::current_dir;
// "./public"
// };
pub const COMMANDS_PATH: &str = "commands/";
pub const KEYWORDS_PATH: &str = "picovoice/keywords/";
pub const WAKE_WORD_ENGINES: [&str; 2] = ["rustpotter", "picovoice"];
pub const DB_FILE_NAME: &str = "app.db";
pub const LOG_FILE_NAME: &str = "log.txt";
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 COMMANDS_PATH: &str = "commands/";
pub const KEYWORDS_PATH: &str = "picovoice/keywords/";
// pub const VOSK_MODEL_PATH: &str = const_concat!(PUBLIC_PATH, "/vosk/model_small");
pub const VOSK_MODEL_PATH: &str = "vosk/model_small";

View File

@@ -6,6 +6,7 @@ pub struct Payload {
pub data: String,
}
#[allow(dead_code)]
pub enum EventTypes {
AudioPlay,
AssistantWaiting,

View File

@@ -4,6 +4,8 @@
#[macro_use]
extern crate lazy_static; // better switch to once_cell ?
use pickledb::{PickleDb, PickleDbDumpPolicy, SerializationMethod};
use log::{info};
use log::LevelFilter;
use std::sync::Mutex;
// expose the config
@@ -23,6 +25,9 @@ mod vosk;
// include events
mod events;
// include recorder
mod recorder;
// app dir
lazy_static! {
static ref APP_CONFIG_DIR: Mutex<String> = Mutex::new(String::new());
@@ -37,7 +42,7 @@ lazy_static! {
SerializationMethod::Json
)
.unwrap_or_else(|_x: _| {
println!("Creating new db file at {} ...", format!("{}/{}", APP_CONFIG_DIR.lock().unwrap(), DB_FILE_NAME));
info!("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,
@@ -53,8 +58,13 @@ lazy_static! {
}
fn main() {
// log to file
simple_logging::log_to_file(config::LOG_FILE_NAME, LevelFilter::max()).expect("Failed to start logger ... is directory writable?");
// init vosk
vosk::init_vosk();
// run the app
tauri::Builder::default()
.setup(|app| {
std::fs::create_dir_all(app.path_resolver().app_config_dir().unwrap())?;

63
src-tauri/src/recorder.rs Normal file
View File

@@ -0,0 +1,63 @@
use once_cell::sync::OnceCell;
use std::sync::atomic::{AtomicU32, AtomicBool, Ordering};
use pv_recorder::{Recorder, RecorderBuilder};
use log::{info};
use crate::DB;
pub static FRAME_LENGTH: AtomicU32 = AtomicU32::new(0);
static RECORDER: OnceCell<Recorder> = OnceCell::new();
pub static IS_RECORDING: AtomicBool = AtomicBool::new(false);
fn init_microphone() {
if RECORDER.get().is_none() {
RECORDER.get_or_init(|| RecorderBuilder::new()
.device_index(get_selected_microphone_index())
.frame_length(FRAME_LENGTH.load(Ordering::SeqCst) as i32)
.init()
.expect("Failed to initialize pvrecorder"));
info!("Microphone recorder initialized!")
}
}
pub fn read_microphone(frame_buffer: &mut [i16]) {
// ensure microphone is initialized
init_microphone();
// read to frame buffer
RECORDER.get().unwrap().read(frame_buffer).expect("Failed to read audio frame");
}
pub fn start_recording() {
// ensure microphone is initialized
init_microphone();
RECORDER.get().unwrap().start().expect("Failed to start audio recording!");
IS_RECORDING.store(true, Ordering::SeqCst);
info!("START recording from microphone ...");
}
pub fn stop_recording() {
// ensure microphone is initialized
init_microphone();
RECORDER.get().unwrap().start().expect("Failed to start audio recording!");
IS_RECORDING.store(false, Ordering::SeqCst);
info!("STOP recording from microphone ...");
}
pub fn get_selected_microphone_index() -> i32 {
let selected_microphone: i32;
// 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;
}
// return microphone index
info!("Selected microphone index = {selected_microphone}");
selected_microphone
}

View File

@@ -1,9 +1,9 @@
use porcupine::{BuiltinKeywords, Porcupine, PorcupineBuilder};
use pv_recorder::RecorderBuilder;
use porcupine::{Porcupine, PorcupineBuilder};
use std::sync::atomic::{AtomicBool, Ordering};
use std::path::Path;
use log::{info, warn, error};
use crate::events::Payload;
// use crate::events::Payload;
use tauri::Manager;
use rand::seq::SliceRandom;
@@ -14,6 +14,7 @@ use crate::events;
use crate::config;
use crate::vosk;
use crate::recorder;
use crate::COMMANDS;
use crate::DB;
@@ -46,154 +47,173 @@ pub fn start_listening(app_handle: tauri::AppHandle) -> Result<bool, String> {
return Err("Already listening.".into());
}
// vars
let porcupine: Porcupine;
let mut picovoice_api_key: String = String::from("");
let selected_microphone: i32;
// Retrieve selected wake-word engine from DB
let selected_wake_word_engine;
if let Some(wwengine) = DB.lock().unwrap().get::<String>("selected_wake_word_engine") {
// from db
selected_wake_word_engine = wwengine;
} else {
// default
selected_wake_word_engine = config::WAKE_WORD_ENGINES.first().expect("No wake-word engines found ...").to_string(); // set default wake_word engine
}
let mut start = SystemTime::now();
// call selected wake-word engine listener command
match selected_wake_word_engine.as_str() {
"rustpotter" => {
info!("Starting rustpotter wake-word engine ...");
return picovoice_listen(&app_handle, |_app| {
// Greet user
events::play("run", &app_handle);
}, |app, kidx| keyword_callback(app, kidx));
},
"picovoice" => {
info!("Starting picovoice wake-word engine ...");
return picovoice_listen(&app_handle, |_app| {
// Greet user
events::play("run", &app_handle);
}, |app, kidx| keyword_callback(app, kidx));
},
_ => Err("No wake-word engine selected ...".into())
}
}
pub fn keyword_callback(app_handle: &tauri::AppHandle, _keyword_index: i32) {
// vars
let mut start: SystemTime = SystemTime::now();
let mut frame_buffer = vec![0; recorder::FRAME_LENGTH.load(Ordering::SeqCst) as usize];
// play greet phrase
events::play(
config::ASSISTANT_GREET_PHRASES
.choose(&mut rand::thread_rng())
.unwrap(),
&app_handle,
);
// emit assistant greet event
app_handle
.emit_all(events::EventTypes::AssistantGreet.get(), ())
.unwrap();
// the loop
while !STOP_LISTENING.load(Ordering::SeqCst) {
recorder::read_microphone(&mut frame_buffer);
// 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;
}
_ => (),
}
}
}
pub fn picovoice_listen<'s, S, K>(app_handle: &tauri::AppHandle, start_callback: S, mut keyword_callback: K) -> Result<bool, String>
where S: Fn(&tauri::AppHandle),
K: FnMut(&tauri::AppHandle, i32) {
// VARS
let porcupine: Porcupine;
let picovoice_api_key: String;
// Retrieve API key from DB
if let Some(pkey) = DB.lock().unwrap().get::<String>("api_key__picovoice") {
if !pkey.is_empty() {
picovoice_api_key = pkey;
}
}
if picovoice_api_key.is_empty() {
picovoice_api_key = pkey;
} else {
warn!("Picovoice API key is not set!");
return Err("Picovoice API key is not set!".into());
}
// Create instance of Porcupine with the given API key
match PorcupineBuilder::new_with_keyword_paths(picovoice_api_key, &[Path::new(config::KEYWORDS_PATH).join("jarvis_windows.ppn")])
.sensitivities(&[1.0f32]) // max sensitivity possible
.init() {
Ok(pinstance) => {
// porcupine successfully initialized with the valid API key
println!("Porcupine successfully initialized with the valid API key ...");
porcupine = pinstance;
}
Err(e) => {
println!("Porcupine error: either API key is not valid or there is no internet connection");
println!("Error details: {}", e);
return Err(
"Porcupine error: either API key is not valid or there is no internet connection"
.into(),
);
}
.sensitivities(&[1.0f32]) // max sensitivity possible
.init() {
Ok(pinstance) => {
// porcupine successfully initialized with the valid API key
info!("Porcupine successfully initialized with the valid API key ...");
porcupine = pinstance;
}
Err(e) => {
error!("Porcupine error: either API key is not valid or there is no internet connection");
error!("Error details: {}", e);
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");
let mut frame_buffer = vec![0; porcupine.frame_length() as usize];
recorder::FRAME_LENGTH.store(porcupine.frame_length(), Ordering::SeqCst);
recorder::start_recording();
LISTENING.store(true, Ordering::SeqCst);
// Greet user
events::play("run", &app_handle);
// run start callback
start_callback(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");
recorder::read_microphone(&mut frame_buffer);
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;
}
_ => (),
}
}
// println!("Yes, sir! {}", keyword_index);
keyword_callback(&app_handle, keyword_index);
}
}
}
// Stop listening
println!("Stop listening ...");
recorder.stop().expect("Failed to stop audio recording");
recorder::stop_recording();
LISTENING.store(false, Ordering::SeqCst);
STOP_LISTENING.store(false, Ordering::SeqCst);

View File

@@ -6,7 +6,7 @@ static PEAK_ALLOC: PeakAlloc = PeakAlloc;
extern crate systemstat;
use std::thread;
use std::time::Duration;
use systemstat::{saturating_sub_bytes, Platform, System};
use systemstat::{Platform, System};
lazy_static! {
static ref SYS: System = System::new();

View File

@@ -1,5 +1,5 @@
use std::sync::Mutex;
use vosk::{CompleteResult, DecodingState, Model, Recognizer};
use vosk::{DecodingState, Model, Recognizer};
use crate::config::VOSK_MODEL_PATH;
@@ -46,13 +46,13 @@ pub fn recognize(data: &[i16]) -> Option<String> {
}
}
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),
);
// 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
}
// result
// }

View File

@@ -8,7 +8,7 @@
},
"package": {
"productName": "jarvis-app",
"version": "0.0.0"
"version": "0.0.1"
},
"tauri": {
"allowlist": {
@@ -45,7 +45,7 @@
"libstdc++-6.dll",
"libwinpthread-1.dll",
"libgcc_s_seh-1.dll",
"libpv_recorder.dll"
"libvosk.lib"
]
},
"security": {

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,361 +0,0 @@
// Copyright 2020-2021 Alpha Cephei Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/* This header contains the C API for Vosk speech recognition system */
#ifndef VOSK_API_H
#define VOSK_API_H
#ifdef __cplusplus
extern "C" {
#endif
/** Model stores all the data required for recognition
* it contains static data and can be shared across processing
* threads. */
typedef struct VoskModel VoskModel;
/** Speaker model is the same as model but contains the data
* for speaker identification. */
typedef struct VoskSpkModel VoskSpkModel;
/** Recognizer object is the main object which processes data.
* Each recognizer usually runs in own thread and takes audio as input.
* Once audio is processed recognizer returns JSON object as a string
* which represent decoded information - words, confidences, times, n-best lists,
* speaker information and so on */
typedef struct VoskRecognizer VoskRecognizer;
/**
* Batch model object
*/
typedef struct VoskBatchModel VoskBatchModel;
/**
* Batch recognizer object
*/
typedef struct VoskBatchRecognizer VoskBatchRecognizer;
/** Loads model data from the file and returns the model object
*
* @param model_path: the path of the model on the filesystem
* @returns model object or NULL if problem occured */
VoskModel *vosk_model_new(const char *model_path);
/** Releases the model memory
*
* The model object is reference-counted so if some recognizer
* depends on this model, model might still stay alive. When
* last recognizer is released, model will be released too. */
void vosk_model_free(VoskModel *model);
/** Check if a word can be recognized by the model
* @param word: the word
* @returns the word symbol if @param word exists inside the model
* or -1 otherwise.
* Reminding that word symbol 0 is for <epsilon> */
int vosk_model_find_word(VoskModel *model, const char *word);
/** Loads speaker model data from the file and returns the model object
*
* @param model_path: the path of the model on the filesystem
* @returns model object or NULL if problem occured */
VoskSpkModel *vosk_spk_model_new(const char *model_path);
/** Releases the model memory
*
* The model object is reference-counted so if some recognizer
* depends on this model, model might still stay alive. When
* last recognizer is released, model will be released too. */
void vosk_spk_model_free(VoskSpkModel *model);
/** Creates the recognizer object
*
* The recognizers process the speech and return text using shared model data
* @param model VoskModel containing static data for recognizer. Model can be
* shared across recognizers, even running in different threads.
* @param sample_rate The sample rate of the audio you going to feed into the recognizer.
* Make sure this rate matches the audio content, it is a common
* issue causing accuracy problems.
* @returns recognizer object or NULL if problem occured */
VoskRecognizer *vosk_recognizer_new(VoskModel *model, float sample_rate);
/** Creates the recognizer object with speaker recognition
*
* With the speaker recognition mode the recognizer not just recognize
* text but also return speaker vectors one can use for speaker identification
*
* @param model VoskModel containing static data for recognizer. Model can be
* shared across recognizers, even running in different threads.
* @param sample_rate The sample rate of the audio you going to feed into the recognizer.
* Make sure this rate matches the audio content, it is a common
* issue causing accuracy problems.
* @param spk_model speaker model for speaker identification
* @returns recognizer object or NULL if problem occured */
VoskRecognizer *vosk_recognizer_new_spk(VoskModel *model, float sample_rate, VoskSpkModel *spk_model);
/** Creates the recognizer object with the phrase list
*
* Sometimes when you want to improve recognition accuracy and when you don't need
* to recognize large vocabulary you can specify a list of phrases to recognize. This
* will improve recognizer speed and accuracy but might return [unk] if user said
* something different.
*
* Only recognizers with lookahead models support this type of quick configuration.
* Precompiled HCLG graph models are not supported.
*
* @param model VoskModel containing static data for recognizer. Model can be
* shared across recognizers, even running in different threads.
* @param sample_rate The sample rate of the audio you going to feed into the recognizer.
* Make sure this rate matches the audio content, it is a common
* issue causing accuracy problems.
* @param grammar The string with the list of phrases to recognize as JSON array of strings,
* for example "["one two three four five", "[unk]"]".
*
* @returns recognizer object or NULL if problem occured */
VoskRecognizer *vosk_recognizer_new_grm(VoskModel *model, float sample_rate, const char *grammar);
/** Adds speaker model to already initialized recognizer
*
* Can add speaker recognition model to already created recognizer. Helps to initialize
* speaker recognition for grammar-based recognizer.
*
* @param spk_model Speaker recognition model */
void vosk_recognizer_set_spk_model(VoskRecognizer *recognizer, VoskSpkModel *spk_model);
/** Reconfigures recognizer to use grammar
*
* @param recognizer Already running VoskRecognizer
* @param grammar Set of phrases in JSON array of strings or "[]" to use default model graph.
* See also vosk_recognizer_new_grm
*/
void vosk_recognizer_set_grm(VoskRecognizer *recognizer, char const *grammar);
/** Configures recognizer to output n-best results
*
* <pre>
* {
* "alternatives": [
* { "text": "one two three four five", "confidence": 0.97 },
* { "text": "one two three for five", "confidence": 0.03 },
* ]
* }
* </pre>
*
* @param max_alternatives - maximum alternatives to return from recognition results
*/
void vosk_recognizer_set_max_alternatives(VoskRecognizer *recognizer, int max_alternatives);
/** Enables words with times in the output
*
* <pre>
* "result" : [{
* "conf" : 1.000000,
* "end" : 1.110000,
* "start" : 0.870000,
* "word" : "what"
* }, {
* "conf" : 1.000000,
* "end" : 1.530000,
* "start" : 1.110000,
* "word" : "zero"
* }, {
* "conf" : 1.000000,
* "end" : 1.950000,
* "start" : 1.530000,
* "word" : "zero"
* }, {
* "conf" : 1.000000,
* "end" : 2.340000,
* "start" : 1.950000,
* "word" : "zero"
* }, {
* "conf" : 1.000000,
* "end" : 2.610000,
* "start" : 2.340000,
* "word" : "one"
* }],
* </pre>
*
* @param words - boolean value
*/
void vosk_recognizer_set_words(VoskRecognizer *recognizer, int words);
/** Like above return words and confidences in partial results
*
* @param partial_words - boolean value
*/
void vosk_recognizer_set_partial_words(VoskRecognizer *recognizer, int partial_words);
/** Set NLSML output
* @param nlsml - boolean value
*/
void vosk_recognizer_set_nlsml(VoskRecognizer *recognizer, int nlsml);
/** Accept voice data
*
* accept and process new chunk of voice data
*
* @param data - audio data in PCM 16-bit mono format
* @param length - length of the audio data
* @returns 1 if silence is occured and you can retrieve a new utterance with result method
* 0 if decoding continues
* -1 if exception occured */
int vosk_recognizer_accept_waveform(VoskRecognizer *recognizer, const char *data, int length);
/** Same as above but the version with the short data for language bindings where you have
* audio as array of shorts */
int vosk_recognizer_accept_waveform_s(VoskRecognizer *recognizer, const short *data, int length);
/** Same as above but the version with the float data for language bindings where you have
* audio as array of floats */
int vosk_recognizer_accept_waveform_f(VoskRecognizer *recognizer, const float *data, int length);
/** Returns speech recognition result
*
* @returns the result in JSON format which contains decoded line, decoded
* words, times in seconds and confidences. You can parse this result
* with any json parser
*
* <pre>
* {
* "text" : "what zero zero zero one"
* }
* </pre>
*
* If alternatives enabled it returns result with alternatives, see also vosk_recognizer_set_max_alternatives().
*
* If word times enabled returns word time, see also vosk_recognizer_set_word_times().
*/
const char *vosk_recognizer_result(VoskRecognizer *recognizer);
/** Returns partial speech recognition
*
* @returns partial speech recognition text which is not yet finalized.
* result may change as recognizer process more data.
*
* <pre>
* {
* "partial" : "cyril one eight zero"
* }
* </pre>
*/
const char *vosk_recognizer_partial_result(VoskRecognizer *recognizer);
/** Returns speech recognition result. Same as result, but doesn't wait for silence
* You usually call it in the end of the stream to get final bits of audio. It
* flushes the feature pipeline, so all remaining audio chunks got processed.
*
* @returns speech result in JSON format.
*/
const char *vosk_recognizer_final_result(VoskRecognizer *recognizer);
/** Resets the recognizer
*
* Resets current results so the recognition can continue from scratch */
void vosk_recognizer_reset(VoskRecognizer *recognizer);
/** Releases recognizer object
*
* Underlying model is also unreferenced and if needed released */
void vosk_recognizer_free(VoskRecognizer *recognizer);
/** Set log level for Kaldi messages
*
* @param log_level the level
* 0 - default value to print info and error messages but no debug
* less than 0 - don't print info messages
* greather than 0 - more verbose mode
*/
void vosk_set_log_level(int log_level);
/**
* Init, automatically select a CUDA device and allow multithreading.
* Must be called once from the main thread.
* Has no effect if HAVE_CUDA flag is not set.
*/
void vosk_gpu_init();
/**
* Init CUDA device in a multi-threaded environment.
* Must be called for each thread.
* Has no effect if HAVE_CUDA flag is not set.
*/
void vosk_gpu_thread_init();
/** Creates the batch recognizer object
*
* @returns model object or NULL if problem occured */
VoskBatchModel *vosk_batch_model_new(const char *model_path);
/** Releases batch model object */
void vosk_batch_model_free(VoskBatchModel *model);
/** Wait for the processing */
void vosk_batch_model_wait(VoskBatchModel *model);
/** Creates batch recognizer object
* @returns recognizer object or NULL if problem occured */
VoskBatchRecognizer *vosk_batch_recognizer_new(VoskBatchModel *model, float sample_rate);
/** Releases batch recognizer object */
void vosk_batch_recognizer_free(VoskBatchRecognizer *recognizer);
/** Accept batch voice data */
void vosk_batch_recognizer_accept_waveform(VoskBatchRecognizer *recognizer, const char *data, int length);
/** Set NLSML output
* @param nlsml - boolean value
*/
void vosk_batch_recognizer_set_nlsml(VoskBatchRecognizer *recognizer, int nlsml);
/** Closes the stream */
void vosk_batch_recognizer_finish_stream(VoskBatchRecognizer *recognizer);
/** Return results */
const char *vosk_batch_recognizer_front_result(VoskBatchRecognizer *recognizer);
/** Release and free first retrieved result */
void vosk_batch_recognizer_pop(VoskBatchRecognizer *recognizer);
/** Get amount of pending chunks for more intelligent waiting */
int vosk_batch_recognizer_get_pending_chunks(VoskBatchRecognizer *recognizer);
#ifdef __cplusplus
}
#endif
#endif /* VOSK_API_H */

View File

@@ -13,10 +13,10 @@
</script>
<header id="header">
<div class="logo">
<a href="/" title="Хауди Хо!"><img src="/media/app-logo.png"></a>
<a href="/" title="Хауди Хо!"><img src="/media/app-logo.png" alt=""></a>
<div>
<h1><a href="/">JARVIS</a></h1>
<h2>v{app_version} beta</h2>
<h2>v{app_version} <small style="color: #8AC832;opacity: .9;font-size: 13px;">BETA</small></h2>
</div>
</div>
<nav class="top-menu">

View File

@@ -1,13 +1,23 @@
<script>
// IMPORTS
import { invoke } from "@tauri-apps/api/tauri"
import { onMount } from 'svelte'
import { capitalizeFirstLetter } from "@/functions";
// VARIABLES
let selected_microphone = 0;
let microphone_label = "";
let nn_label = "Picovoice + Vosk";
let nn_details = {
"ww_engine": "",
"stt_engine": "Vosk"
}
// let resources_cpu_temp = 0;
// let resources_cpu_usage = 0;
let resources_ram_usage = "-";
// CODE
setInterval(() => {
(async () => {
resources_ram_usage = Number(await invoke("get_current_ram_usage")).toFixed(2);
@@ -18,12 +28,13 @@
});
}, 1000);
import { onMount } from 'svelte';
onMount(async () => {
(async () => {
selected_microphone = +Number(await invoke("db_read", {key: "selected_microphone"}));
microphone_label = await invoke("pv_get_audio_device_name", {idx: selected_microphone});
nn_details["ww_engine"] = capitalizeFirstLetter(await invoke("db_read", {key: "selected_wake_word_engine"}));
// resources_cpu_temp = await invoke("get_cpu_temp");
// resources_cpu_usage = +Number(await invoke("get_cpu_usage")).toFixed(2);
})().catch(err => {
@@ -44,7 +55,7 @@
<div class="pulse"><div class="wave"></div></div>
<div class="info">
<span class="num">Нейросети</span>
<small title="{nn_label}">{nn_label}</small>
<small>{nn_details["ww_engine"]} + {nn_details["stt_engine"]}</small>
</div>
</div>
<div class="downloads hint--bottom" aria-label="Общее количество скачиваний по всему проекту">

File diff suppressed because it is too large Load Diff

View File

@@ -71,4 +71,22 @@ a {
select, input {
color: #ccc!important;
}
.form {
label {
font-weight: bold!important;
color: #8AC832!important;
font-size: 15px!important;
border-bottom: 1px dotted gray;
margin-bottom: 10px!important;
& + div {
line-height: 1.2em;
}
}
.svelteui-Tab-label {
font-size: 18px!important;
}
}

View File

@@ -41,4 +41,8 @@ export function stopListening(callback) {
})().catch(err => {
console.error(err);
});
}
export function capitalizeFirstLetter(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}

View File

@@ -7,6 +7,8 @@
</script>
<HDivider />
<Notification title='Этот раздел еще находится в разработке.' icon={InfoCircled} color='blue'></Notification>
<Notification title='404' icon={InfoCircled} color='blue' withCloseButton={false}>
Этот раздел еще находится в разработке.
</Notification>
<HDivider />
<Footer />

View File

@@ -1,31 +1,62 @@
<script lang="ts">
// IMPORTS
import { invoke } from "@tauri-apps/api/tauri"
import { goto } from '@roxi/routify'
import { onMount } from 'svelte'
import { startListening, stopListening } from "@/functions";
import { setTimeout } from 'worker-timers';
// COMPONENTS & STUFF
import HDivider from "@/components/elements/HDivider.svelte"
import Footer from "@/components/Footer.svelte"
import { Notification, Button, Text, Tabs, Space, Alert, Input, InputWrapper, NativeSelect } from '@svelteuidev/core';
import { Check, Mix, Code, Gear, InfoCircled } from 'radix-icons-svelte';
import { Check, Mix, Cube, Code, Gear, QuestionMarkCircled } from 'radix-icons-svelte';
// vars
// VARIABLES
let available_microphones = [];
let settings_saved = false;
// settings field values
let assistant_voice_val = ""; // shared
let selected_microphone = "";
let selected_wake_word_engine = "";
let api_key__picovoice = "";
let api_key__openai = "";
// shared values
// SHARED VALUES
import { assistant_voice } from "@/stores"
assistant_voice.subscribe(value => {
assistant_voice_val = value;
});
(async () => {
// FUNCTIONS
async function save_settings() {
settings_saved = false; // hide alert
await invoke("db_write", {key: "assistant_voice", val: assistant_voice_val});
await invoke("db_write", {key: "selected_microphone", val: selected_microphone});
await invoke("db_write", {key: "selected_wake_word_engine", val: selected_wake_word_engine});
await invoke("db_write", {key: "api_key__picovoice", val: api_key__picovoice});
await invoke("db_write", {key: "api_key__openai", val: api_key__openai});
// update shared
assistant_voice.set(assistant_voice_val);
settings_saved = true; // show alert
setTimeout(() => {
settings_saved = false; // hide alert again after N seconds
}, 5000);
// restart listening everytime new settings is saved
stopListening(() => {
startListening();
});
}
// CODE
onMount(async () => {
// preload some vars
let _available_microphones: Array<Number> = await invoke("pv_get_audio_devices");
Object.entries(_available_microphones).forEach(entry => {
@@ -43,53 +74,31 @@
// assistant_voice.set(await invoke("db_read", {key: "assistant_voice"}));
selected_microphone = await invoke("db_read", {key: "selected_microphone"});
selected_wake_word_engine = await invoke("db_read", {key: "selected_wake_word_engine"});
api_key__picovoice = await invoke("db_read", {key: "api_key__picovoice"});
api_key__openai = await invoke("db_read", {key: "api_key__openai"});
})().catch(err => {
console.error(err);
});
});
// subscribe to listen state
import { startListening, stopListening } from "@/functions";
// save values to db
async function save_settings(event) {
await invoke("db_write", {key: "assistant_voice", val: assistant_voice_val});
await invoke("db_write", {key: "selected_microphone", val: selected_microphone});
await invoke("db_write", {key: "api_key__picovoice", val: api_key__picovoice});
await invoke("db_write", {key: "api_key__openai", val: api_key__openai});
// update shared
assistant_voice.set(assistant_voice_val);
settings_saved = true;
// restart listening everytime
stopListening(() => {
startListening();
});
}
</script>
<Space h="xl" />
<Alert icon={InfoCircled} title="Внимание!" color="cyan" variant="outline">
Приложение находится в <strong>БЕТА</strong> режиме.<br />
<Notification title='БЕТА версия!' icon={QuestionMarkCircled} color='blue' withCloseButton={false}>
Часть функций может работать некорректно.<br />
Сообщайте обо всех найденных багах в <a href="https://t.me/hhsharebot" target="_blank">наш телеграм бот</a>.
</Alert>
</Notification>
<Space h="xl" />
{#if settings_saved }
<Notification title='Настройки сохранены!' icon={Check} color='teal' on:close="{() => {settings_saved = false}}"></Notification>
<Space h="xl" />
{/if}
<Space h="xl" />
<Tabs color='cyan' position="left">
<Tabs class="form" color='#8AC832' position="left">
<Tabs.Tab label='Общее' icon={Gear}>
<Space h="sm" />
<NativeSelect data={[
{ label: 'Jarvis ремейк (от Хауди)', value: 'jarvis-remake' },
{ label: 'Jarvis OG (из фильмов)', value: 'jarvis-og' }
@@ -101,6 +110,8 @@
/>
</Tabs.Tab>
<Tabs.Tab label='Устройства' icon={Mix}>
<Space h="sm" />
<NativeSelect data={available_microphones}
label="Выберите микрофон"
description="Его будет слушать ассистент."
@@ -108,17 +119,37 @@
bind:value={selected_microphone}
/>
</Tabs.Tab>
<Tabs.Tab label='API ключи' icon={Code}>
<InputWrapper label="Ключ Picovoice">
<Text size='xs'>Введите сюда свой ключ Picovoice.<br />Он выдается бесплатно при регистрации в <a href='https://console.picovoice.ai/' target="_blank">Picovoice Console</a>.</Text>
<Tabs.Tab label='Нейросети' icon={Cube}>
<Space h="sm" />
<NativeSelect data={[
{ label: 'Rustpotter', value: 'rustpotter' },
{ label: 'Picovoice Porcupine (требует API ключ)', value: 'picovoice' }
]}
label="Распознавание активационной фразы (Wake Word)"
description="Выберите, какая нейросеть будет отвечать за распознавание активационной фразы."
variant="filled"
bind:value={selected_wake_word_engine}
/>
{#if selected_wake_word_engine == "picovoice"}
<Space h="sm" />
<Input icon={Code} placeholder='Ключ Picovoice' variant='filled' autocomplete="off" bind:value={api_key__picovoice}/>
</InputWrapper>
<Alert title="Внимание!" color="#868E96" variant="outline">
<Text size='sm' color="gray">Введите сюда свой ключ Picovoice.<br />Он выдается бесплатно при регистрации в <a href='https://console.picovoice.ai/' target="_blank">Picovoice Console</a>.</Text>
<Space h="sm" />
<Input icon={Code} placeholder='Ключ Picovoice' variant='filled' autocomplete="off" bind:value={api_key__picovoice}/>
</Alert>
{/if}
<Space h="xl" />
<InputWrapper label="Ключ OpenAI">
<Text size='xs'>Введите сюда свой ключ OpenAI, он требуется для работы ChatGPT.<br />Получить его можно <a href="https://chat.openai.com/auth/login" target="_blank">на официальном сайте OpenAI</a>.</Text>
<!-- <Text size='sm' color="gray">Введите сюда свой ключ OpenAI, он требуется для работы ChatGPT.<br />Получить его можно <a href="https://chat.openai.com/auth/login" target="_blank">на официальном сайте OpenAI</a>.</Text> -->
<Text size='sm' color="gray">В данный момент ChatGPT <u>не поддерживается</u>. Он будет добавлен в ближайших обновлениях.</Text>
<Space h="sm" />
<Input icon={Code} placeholder='Ключ OpenAI' variant='filled' autocomplete="off" bind:value={api_key__openai}/>
<Input icon={Code} placeholder='Ключ OpenAI' variant='filled' autocomplete="off" bind:value={api_key__openai} disabled/>
</InputWrapper>
</Tabs.Tab>
</Tabs>