App architecture modifications.

Now GUI and the app itself is divided into two different binaries.
The app also provides system tray icon.
Whereas the GUI can be used to configure the app.
This commit is contained in:
Abraham
2023-06-11 19:18:58 +05:00
parent 4f3d572b26
commit 4e860d63c3
91 changed files with 5818 additions and 0 deletions

14
gui/index.html Normal file
View File

@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/media/header-logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Проект J.A.R.V.I.S.</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

2825
gui/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

39
gui/package.json Normal file
View File

@@ -0,0 +1,39 @@
{
"name": "jarvis-app",
"private": true,
"version": "0.0.3",
"type": "module",
"scripts": {
"dev": "routify -c dev:vite",
"dev:routify": "routify",
"dev:vite": "vite",
"build": "svelte-check && routify -b && vite build",
"preview": "vite preview",
"check": "svelte-check --tsconfig ./tsconfig.json",
"tauri": "tauri"
},
"dependencies": {
"@svelteuidev/composables": "^0.11.1",
"@svelteuidev/core": "^0.11.1",
"@svelteuidev/motion": "^0.11.1",
"@tauri-apps/api": "^1.3.0",
"howler": "^2.2.3",
"radix-icons-svelte": "^1.2.1",
"worker-timers": "^7.0.64"
},
"devDependencies": {
"@roxi/routify": "^2.18.11",
"@sveltejs/vite-plugin-svelte": "^2.0.0",
"@tauri-apps/cli": "^1.3.1",
"@tsconfig/svelte": "^3.0.0",
"@types/node": "^18.7.10",
"sass": "^1.62.0",
"svelte": "^3.54.0",
"svelte-check": "^3.0.0",
"svelte-preprocess": "^5.0.0",
"tslib": "^2.4.1",
"typescript": "^4.9.5",
"vite": "^4.2.1",
"vite-tsconfig-paths": "^4.2.0"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 853 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 742 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 902 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

5
gui/src-tauri/.gitignore vendored Normal file
View File

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

View File

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

38
gui/src-tauri/Cargo.toml Normal file
View File

@@ -0,0 +1,38 @@
[package]
name = "jarvis-app"
version = "0.0.3"
description = "Jarvis Voice Assistant"
authors = ["Abraham Tugalov"]
license = "GPL-3.0-only"
repository = "https://github.com/Priler/jarvis"
edition = "2021"
# 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.3", features = ["dialog-message", "path-all", "shell-open"], optional = true }
serde = { version = "1.0.164", features = ["derive"] }
serde_json = "1.0.96"
hound = "3.5.0"
pv_recorder = "1.1.2"
pv_porcupine = "2.2.1"
seqdiff = "0.3.0"
vosk = "0.2.0"
rand = "0.8.5"
rodio = "0.17.1"
rustpotter = "2.0.0"
log = "0.4.18"
once_cell = "1.18.0"
arc-swap = "1.6.0"
atomic_enum = "0.2.0"
portaudio = "0.7.0"
platform-dirs = "0.3.0"
simple-log = "1.6.0"
[features]
# this feature is used for production builds or when `devPath` points to the filesystem
# DO NOT REMOVE!!
custom-protocol = []

BIN
gui/src-tauri/app-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 KiB

4
gui/src-tauri/build.rs Normal file
View File

@@ -0,0 +1,4 @@
fn main() {
// Tauri build
tauri_build::build()
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

48
gui/src-tauri/src/main.rs Normal file
View File

@@ -0,0 +1,48 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use std::path::PathBuf;
use once_cell::sync::OnceCell;
use platform_dirs::{AppDirs};
// expose the config
mod config;
// include log
#[macro_use]
extern crate simple_log;
mod log;
// include db
mod db;
// include tray
mod tray;
// include tauri commands
// mod tauri_commands;
// some global data
static APP_DIRS: OnceCell<AppDirs> = OnceCell::new();
static APP_CONFIG_DIR: OnceCell<PathBuf> = OnceCell::new();
static APP_LOG_DIR: OnceCell<PathBuf> = OnceCell::new();
static DB: OnceCell<db::structs::Settings> = OnceCell::new();
fn main() -> Result<(), String> {
// initialize directories
config::init_dirs()?;
// initialize logging
log::init_logging()?;
// log some base info
info!("Starting Jarvis v{} ...", config::APP_VERSION.unwrap());
info!("Config directory is: {}", APP_CONFIG_DIR.get().unwrap().display());
info!("Log directory is: {}", APP_LOG_DIR.get().unwrap().display());
// initialize database (settings)
DB.set(db::init_settings());
Ok(())
}

View File

@@ -0,0 +1,27 @@
// import DB related commands
mod db;
pub use db::*;
// import RECORDER commands
mod audio;
pub use audio::*;
// 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 FS commands
mod fs;
pub use fs::*;
// import ETC commands
mod etc;
pub use etc::*;

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,19 @@
use crate::DB;
#[tauri::command]
pub fn db_read(key: &str) -> String {
if let Some(value) = DB.lock().unwrap().get::<String>(key) {
return value
}
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,54 @@
use crate::config;
use crate::APP_LOG_DIR;
// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
#[tauri::command]
pub fn get_app_version() -> String {
if let Some(res) = config::APP_VERSION {
res.to_string()
} else {
String::from("error")
}
}
#[tauri::command]
pub fn get_author_name() -> String {
if let Some(res) = config::AUTHOR_NAME {
res.to_string()
} else {
String::from("error")
}
}
#[tauri::command]
pub fn get_repository_link() -> String {
if let Some(res) = config::REPOSITORY_LINK {
res.to_string()
} else {
String::from("error")
}
}
#[tauri::command]
pub fn get_tg_official_link() -> String {
if let Some(ver) = config::TG_OFFICIAL_LINK {
ver.to_string()
} else {
String::from("error")
}
}
#[tauri::command]
pub fn get_feedback_link() -> String {
if let Some(res) = config::FEEDBACK_LINK {
res.to_string()
} else {
String::from("error")
}
}
#[tauri::command]
pub fn get_log_file_path() -> String {
format!("{}", APP_LOG_DIR.lock().unwrap())
}

View File

@@ -0,0 +1,47 @@
use std::process::Command;
// taken from https://github.com/tauri-apps/tauri/issues/4062#issuecomment-1338048169
#[tauri::command]
pub fn show_in_folder(path: String) {
#[cfg(target_os = "windows")]
{
Command::new("explorer")
.args(["/select,", &path]) // The comma after select is not a typo
.spawn()
.unwrap();
}
#[cfg(target_os = "linux")]
{
if path.contains(",") {
// see https://gitlab.freedesktop.org/dbus/dbus/-/issues/76
let new_path = match metadata(&path).unwrap().is_dir() {
true => path,
false => {
let mut path2 = PathBuf::from(path);
path2.pop();
path2.into_os_string().into_string().unwrap()
}
};
Command::new("xdg-open")
.arg(&new_path)
.spawn()
.unwrap();
} else {
Command::new("dbus-send")
.args(["--session", "--dest=org.freedesktop.FileManager1", "--type=method_call",
"/org/freedesktop/FileManager1", "org.freedesktop.FileManager1.ShowItems",
format!("array:string:\"file://{path}\"").as_str(), "string:\"\""])
.spawn()
.unwrap();
}
}
#[cfg(target_os = "macos")]
{
Command::new("open")
.args(["-R", &path])
.spawn()
.unwrap();
}
}

View File

@@ -0,0 +1,416 @@
use porcupine::{Porcupine, PorcupineBuilder};
use std::ops::Sub;
use std::sync::atomic::{AtomicBool, Ordering};
use std::path::Path;
use log::{info, warn, error};
use rustpotter::{Rustpotter, RustpotterConfig, WavFmt, DetectorConfig, FiltersConfig, ScoreMode, GainNormalizationConfig, BandPassConfig};
// use dasp::{sample::ToSample, Sample};
// use crate::events::Payload;
use tauri::Manager;
use rand::seq::SliceRandom;
use std::time::SystemTime;
use once_cell::sync::OnceCell;
use std::sync::Mutex;
use crate::assistant_commands;
use crate::events;
use crate::config;
use crate::vosk;
use crate::recorder::{self, FRAME_LENGTH};
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);
// store tauri app_handle
static TAURI_APP_HANDLE: OnceCell<tauri::AppHandle> = OnceCell::new();
// store porcupine instance
static PORCUPINE: OnceCell<Porcupine> = OnceCell::new();
// store rustpotter instance
static RUSTPOTTER: OnceCell<Mutex<Rustpotter>> = OnceCell::new();
#[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);
stop_recording();
}
// wait until listening stops
while is_listening() {}
}
fn get_wake_word_engine() -> config::WakeWordEngine {
let selected_wake_word_engine;
if let Some(wwengine) = DB.lock().unwrap().get::<String>("selected_wake_word_engine") {
// from db
match wwengine.trim().to_lowercase().as_str() {
"rustpotter" => selected_wake_word_engine = config::WakeWordEngine::Rustpotter,
"vosk" => selected_wake_word_engine = config::WakeWordEngine::Vosk,
"picovoice" => selected_wake_word_engine = config::WakeWordEngine::Porcupine,
_ => selected_wake_word_engine = config::DEFAULT_WAKE_WORD_ENGINE
}
} else {
// default
selected_wake_word_engine = config::DEFAULT_WAKE_WORD_ENGINE; // set default wake_word engine
}
selected_wake_word_engine
}
#[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());
}
// keep app handle
if TAURI_APP_HANDLE.get().is_none() {
TAURI_APP_HANDLE.set(app_handle);
}
// call selected wake-word engine listener command
match get_wake_word_engine() {
config::WakeWordEngine::Rustpotter => {
info!("Starting RUSTPOTTER wake-word engine ...");
return rustpotter_init();
},
config::WakeWordEngine::Vosk => {
info!("Starting VOSK wake-word engine ...");
return vosk_init();
},
config::WakeWordEngine::Porcupine => {
info!("Starting PICOVOICE PORCUPINE wake-word engine ...");
return picovoice_init();
}
}
}
fn keyword_callback(_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(),
TAURI_APP_HANDLE.get().unwrap(),
);
// emit assistant greet event
TAURI_APP_HANDLE.get().unwrap()
.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, false) {
if !test.is_empty() {
println!("Recognized: {}", test);
// some filtration
test = test.to_lowercase();
for tbr in config::ASSISTANT_PHRASES_TBR {
test = test.replace(tbr, "");
}
test = test.trim().into();
// 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,
TAURI_APP_HANDLE.get().unwrap(),
);
match cmd_result {
Ok(chain) => {
println!("Command executed successfully!");
if chain {
// continue chaining commands
start = SystemTime::now(); // listen for more commands
} else {
// skip forward if chaining is not required
start = start.checked_sub(core::time::Duration::from_secs(1000)).unwrap();
}
continue;
}
Err(error_message) => {
println!("Error executing command: {}", error_message);
}
}
TAURI_APP_HANDLE.get().unwrap()
.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
TAURI_APP_HANDLE.get().unwrap()
.emit_all(events::EventTypes::AssistantWaiting.get(), ())
.unwrap();
break;
}
_ => (),
}
}
}
pub fn data_callback(frame_buffer: &[i16]) {
// println!("DATA CALLBACK {}", frame_buffer.len());
match get_wake_word_engine() {
config::WakeWordEngine::Rustpotter => {
let mut lock = RUSTPOTTER.get().unwrap().lock();
let rustpotter = lock.as_mut().unwrap();
let detection = rustpotter.process_i16(&frame_buffer);
if let Some(detection) = detection {
if detection.score > config::RUSPOTTER_MIN_SCORE {
info!("Rustpotter detection info:\n{:?}", detection);
keyword_callback(0);
} else {
info!("Rustpotter detection info:\n{:?}", detection);
}
}
},
config::WakeWordEngine::Vosk => {
// recognize & convert to sequence
let recognized_phrase = vosk::recognize(&frame_buffer, true).unwrap_or("".into());
if !recognized_phrase.trim().is_empty() {
info!("Rec: {}", recognized_phrase);
let recognized_phrases = recognized_phrase.split_whitespace();
for phrase in recognized_phrases {
let recognized_phrase_chars = phrase.trim().to_lowercase().chars().collect::<Vec<_>>();
// compare
let compare_ratio = seqdiff::ratio(&config::VOSK_FETCH_PHRASE.chars().collect::<Vec<_>>(), &recognized_phrase_chars);
info!("OG phrase: {:?}", &config::VOSK_FETCH_PHRASE);
info!("Recognized phrase: {:?}", &recognized_phrase_chars);
info!("Compare ratio: {}", compare_ratio);
if compare_ratio >= config::VOSK_MIN_RATIO {
info!("Phrase activated.");
keyword_callback(0);
break;
}
}
}
},
config::WakeWordEngine::Porcupine => {
if let Ok(keyword_index) = PORCUPINE.get().unwrap().process(&frame_buffer) {
if keyword_index >= 0 {
// println!("Yes, sir! {}", keyword_index);
keyword_callback(keyword_index);
}
}
}
}
}
fn start_recording() -> Result<bool, String> {
// vars
let frame_length: usize;
// idenfity frame length
match get_wake_word_engine() {
config::WakeWordEngine::Rustpotter => {
// start recording for Rustpotter
// You need a buffer of size `rustpotter.get_samples_per_frame()` when using samples.
// You need a buffer of size `rustpotter.get_bytes_per_frame()` when using bytes.
frame_length = RUSTPOTTER.get().unwrap().lock().unwrap().get_samples_per_frame();
recorder::FRAME_LENGTH.store(frame_length as u32, Ordering::SeqCst);
},
config::WakeWordEngine::Vosk => {
// start recording for Vosk
frame_length = 128;
recorder::FRAME_LENGTH.store(frame_length as u32, Ordering::SeqCst);
},
config::WakeWordEngine::Porcupine => {
// start recording for Porcupine
frame_length = PORCUPINE.get().unwrap().frame_length() as usize;
recorder::FRAME_LENGTH.store(PORCUPINE.get().unwrap().frame_length(), Ordering::SeqCst);
}
}
// define frame buffer
let mut frame_buffer: Vec<i16> = vec![0; frame_length];
// init stuff
recorder::init(); // init
recorder::start_recording(); // start
LISTENING.store(true, Ordering::SeqCst);
info!("START listening ...");
// greet user
events::play("run", TAURI_APP_HANDLE.get().unwrap());
// record
match recorder::RECORDER_TYPE.load(Ordering::SeqCst) {
recorder::RecorderType::PvRecorder => {
while !STOP_LISTENING.load(Ordering::SeqCst) {
recorder::read_microphone(&mut frame_buffer);
data_callback(&frame_buffer);
}
// stop
stop_recording();
Ok(true)
},
recorder::RecorderType::PortAudio => {
while !STOP_LISTENING.load(Ordering::SeqCst) {
recorder::read_microphone(&mut frame_buffer);
data_callback(&frame_buffer);
}
// stop
stop_recording();
Ok(true)
}
recorder::RecorderType::Cpal => {
todo!()
}
}
}
fn stop_recording() {
// Stop listening
recorder::stop_recording();
LISTENING.store(false, Ordering::SeqCst);
STOP_LISTENING.store(false, Ordering::SeqCst);
info!("STOP listening ...");
}
fn rustpotter_init() -> Result<bool, String> {
// init rustpotter
let rustpotter_config = RustpotterConfig {
fmt: WavFmt::default(),
detector: DetectorConfig {
avg_threshold: 0.,
threshold: 0.5,
min_scores: 15,
score_mode: ScoreMode::Average,
comparator_band_size: 5,
comparator_ref: 0.22
},
filters: FiltersConfig {
gain_normalizer: GainNormalizationConfig {
enabled: true,
gain_ref: None,
min_gain: 0.7,
max_gain: 1.0,
},
band_pass: BandPassConfig {
enabled: true,
low_cutoff: 80.,
high_cutoff: 400.,
}
}
};
let mut rustpotter = Rustpotter::new(&rustpotter_config).unwrap();
// load a wakeword
let rustpotter_wake_word_files: [&str; 5] = [
"rustpotter/jarvis-default.rpw",
"rustpotter/jarvis-community-1.rpw",
"rustpotter/jarvis-community-2.rpw",
"rustpotter/jarvis-community-3.rpw",
"rustpotter/jarvis-community-4.rpw",
// "rustpotter/jarvis-community-5.rpw",
];
for rpw in rustpotter_wake_word_files {
rustpotter.add_wakeword_from_file(rpw).unwrap();
}
// store rustpotter
if RUSTPOTTER.get().is_none() {
RUSTPOTTER.set(Mutex::new(rustpotter));
}
// start recording
start_recording()
}
fn vosk_init() -> Result<bool, String> {
start_recording()
}
fn picovoice_init() -> Result<bool, String> {
// 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") {
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
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(),
);
}
}
// store
if PORCUPINE.get().is_none() {
PORCUPINE.set(porcupine);
}
// start recording
start_recording()
}

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::{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();
}
}

View File

@@ -0,0 +1,68 @@
{
"build": {
"beforeDevCommand": "npm run dev",
"beforeBuildCommand": "npm run build",
"devPath": "http://localhost:1420",
"distDir": "../dist",
"withGlobalTauri": false
},
"package": {
"productName": "jarvis-app",
"version": "0.0.3"
},
"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",
"picovoice",
"rustpotter",
"libvosk.dll",
"libstdc++-6.dll",
"libwinpthread-1.dll",
"libgcc_s_seh-1.dll",
"libvosk.lib"
]
},
"security": {
"csp": null
},
"updater": {
"active": false
},
"windows": [
{
"fullscreen": false,
"resizable": false,
"title": "Jarvis Voice Assistant",
"width": 550,
"height": 700
}
]
}
}

19
gui/src/App.svelte Normal file
View File

@@ -0,0 +1,19 @@
<!-- src/App.svelte -->
<script>
import { Router } from "@roxi/routify";
import { routes } from "../.routify/routes";
import { SvelteUIProvider } from '@svelteuidev/core';
import Events from "./Events.svelte";
/** START LISTENING **/
import { startListening } from "./functions";
startListening();
</script>
<SvelteUIProvider themeObserver='dark' withNormalizeCSS withGlobalStyles>
<Router {routes} />
</SvelteUIProvider>
<Events />

44
gui/src/Events.svelte Normal file
View File

@@ -0,0 +1,44 @@
<script>
import { onMount, onDestroy } from 'svelte'
import { emit, listen } from '@tauri-apps/api/event'
import { resolveResource } from '@tauri-apps/api/path'
import {Howl, Howler} from 'howler';
let assistant_voice_val = "jarvis-og";
import { assistant_voice } from "@/stores"
import { invoke } from '@tauri-apps/api/tauri';
assistant_voice.subscribe(value => {
assistant_voice_val = value;
});
onMount(async () => {
await listen('audio-play', async (event) => {
// event.event is the event name (useful if you want to use a single callback fn for multiple event types)
// event.payload is the payload object
// let path = await resolveResource('sound/' + (assistant_voice_val == "" ? "jarvis-remake":assistant_voice_val) + '/' + event.payload['data'] + '.wav');
// console.log(path);
// let sound = new Howl({
// src: [path],
// html5: true
// });
// sound.play();
let filename = 'sound/' + (assistant_voice_val == "" ? "jarvis-remake":assistant_voice_val) + '/' + event.payload['data'] + '.wav';
await invoke("play_sound", {
filename: filename,
sleep: true
});
});
await listen('assistant-greet', (event) => {
document.getElementById("arc-reactor").classList.add("active");
});
await listen('assistant-waiting', (event) => {
document.getElementById("arc-reactor").classList.remove("active");
});
});
</script>

View File

@@ -0,0 +1,73 @@
<script>
import { invoke } from "@tauri-apps/api/tauri"
import { tg_official_link, github_repository_link } from "@/stores";
let current_year = new Date().getFullYear();
let author_name = "";
(async () => {
author_name = await invoke("get_author_name")
})().catch(err => {
console.error(err);
});
</script>
<footer id="footer">
<p>© {current_year}. Автор проекта: {author_name}</p>
<p style="margin-top: 5px;margin-bottom: 15px;">
<a href="{tg_official_link}" target="_blank" class="special-link"><img src="/media/icons/howdy-logo.png" alt="" width="20px">&nbsp;&nbsp;Наш телеграм</a> канал.
&nbsp;&nbsp;
<a href="{github_repository_link}" target="_blank"><img src="/media/icons/github-logo.png" alt="" width="18px">&nbsp;Github репозиторий</a> проекта.</p>
</footer>
<style lang="scss">
#footer {
text-align: center;
color: #565759;
font-size: 12px;
font-weight: bold;
line-height: 1.7em;
margin-top: 15px;
p {
margin: 0;
padding: 0;
}
a {
color: #185876;
text-decoration: none;
transition: opacity .5s;
img {
opacity: .5;
transition: opacity .5s;
margin-top: -4px;
}
&:hover {
color: #2A9CD0;
img {
opacity: 1;
}
}
&.special-link {
color: #941d92;
display: inline-block;
&:hover {
color: #FF07FC;
background: url(/media/images/bg/bg24.gif);
background-repeat: no-repeat;
background-size: contain;
}
}
}
}
</style>

View File

@@ -0,0 +1,28 @@
<script lang="ts">
import { invoke } from "@tauri-apps/api/tauri"
import { Dashboard, Gear } from 'radix-icons-svelte'
import {isActive} from '@roxi/routify'
let app_version = "";
(async () => {
app_version = await invoke("get_app_version")
})().catch(err => {
console.error(err);
});
</script>
<header id="header">
<div class="logo">
<a href="/" title="Проект канала Хауди Хо!"><img src="/media/header-logo.png" alt=""></a>
<div>
<h1><a href="/">JARVIS</a></h1>
<h2>v{app_version} <small style="color: #8AC832;opacity: .9;font-size: 13px;">BETA</small></h2>
</div>
</div>
<nav class="top-menu">
<ul>
<li><a href="/commands" class:active={$isActive('/commands')}><Dashboard /> Команды</a></li>
<li><a href="/settings" class:active={$isActive('/settings')}><Gear /> Настройки</a></li>
</ul>
</nav>
</header>

View File

@@ -0,0 +1,5 @@
<nav>
<a href="/index">Main Page</a>
<a href="/settings">Настройки</a>
</nav>

View File

@@ -0,0 +1,647 @@
<!-- Based on: https://github.com/rembertdesigns/Iron-Man-Arc-Reactor-Pure-CSS and https://codepen.io/FlyingEmu/pen/DZNqEj -->
<div id="arc-reactor" class="reactor-container">
<div class="reactor-container-inner circle abs-center">
<ul class="marks"><li></li><li></li><li></li><li></li><li></li><li></li>
<li></li><li></li><li></li><li></li><li></li><li></li>
<li></li><li></li><li></li><li></li><li></li><li></li>
<li></li><li></li><li></li><li></li><li></li><li></li>
<li></li><li></li><li></li><li></li><li></li><li></li>
<li></li><li></li><li></li><li></li><li></li><li></li>
<li></li><li></li><li></li><li></li><li></li><li></li>
<li></li><li></li><li></li><li></li><li></li><li></li>
<li></li><li></li><li></li><li></li><li></li><li></li>
<li></li><li></li><li></li><li></li><li></li><li></li></ul>
<div class="e7">
<div class="semi_arc_3 e5_1">
<div class="semi_arc_3 e5_2">
<div class="semi_arc_3 e5_3">
<div class="semi_arc_3 e5_4" />
</div>
</div>
</div>
</div>
</div>
<div class="tunnel circle abs-center" />
<div class="core-wrapper circle abs-center" />
<div class="core-outer circle abs-center" />
<div class="core-inner circle abs-center" />
<div class="coil-container">
<div class="coil coil-1" />
<div class="coil coil-2" />
<div class="coil coil-3" />
<div class="coil coil-4" />
<div class="coil coil-5" />
<div class="coil coil-6" />
<div class="coil coil-7" />
<div class="coil coil-8" />
</div>
</div>
<style lang="scss" global>
$arc_radius: 130px;
$size3: 6px;
$cshadow: rgba(2, 254, 255, 0.8);
$marks_color_1: rgba(2, 254, 255, 1);
$marks_color_2: rgba(2, 254, 255, 0.3);
$colour1: rgba(2, 255, 255, 0.15);
$colour3: rgba(2, 255, 255, 0.3);
$cshadow: rgba(2, 254, 255, 0.8);
.reactor-container {
width: 300px;
height: 320px;
margin: auto;
// border: 1px dashed #888;
position: relative;
border-radius: 50%;
transition: scale 1s ease, opacity .5s ease;
scale: 0.9;
opacity: .9;
// background-color: #384c50;
// border: 1px solid #121414;
// box-shadow: 0px 0px 32px 8px #121414, 0px 0px 4px 1px #121414 inset;
ul {
list-style: none;
margin: 0;
padding: 0;
}
}
.reactor-container-inner {
height: 238px;
width: 238px;
background-color: #161a1b;
box-shadow: 0px 0px 50px 15px $colour3, inset 0px 0px 50px 15px $colour3;
// box-shadow: 0px 0px 4px 1px #52fefe;
}
.circle {
border-radius: 50%;
}
.abs-center {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
margin: auto;
}
.core-inner {
width: 70px;
height: 70px;
border: 5px solid #1b4e5f;
background-color: #ffffff;
box-shadow: 0px 0px 7px 5px #52fefe, 0px 0px 10px 10px #52fefe inset;
}
.core-outer {
width: 120px;
height: 120px;
border: 1px solid #52fefe;
background-color: #ffffff;
box-shadow: 0px 0px 2px 1px #52fefe, 0px 0px 10px 5px #52fefe inset;
}
.core-wrapper {
width: 180px;
height: 180px;
background-color: #073c4b;
box-shadow: 0px 0px 5px 4px #52fefe, 0px 0px 6px 2px #52fefe inset;
}
.tunnel {
width: 220px;
height: 220px;
background-color: #ffffff;
box-shadow: 0px 0px 5px 1px #52fefe, 0px 0px 5px 4px #52fefe inset;
}
.coil-container {
position: relative;
width: 100%;
height: 100%;
animation: 10s infinite linear reactor-anim;
transition: animation 1s;
}
.coil {
position: absolute;
width: 30px;
height: 20px;
top: calc(50% - 110px);
left: calc(50% - 15px);
transform-origin: 15px 110px;
background-color: #073c4b;
box-shadow: 0px 0px 5px #52fefe inset;
}
.coil-1 {
transform: rotate(0deg);
}
.coil-2 {
transform: rotate(45deg);
}
.coil-3 {
transform: rotate(90deg);
}
.coil-4 {
transform: rotate(135deg);
}
.coil-5 {
transform: rotate(180deg);
}
.coil-6 {
transform: rotate(225deg);
}
.coil-7 {
transform: rotate(270deg);
}
.coil-8 {
transform: rotate(315deg);
}
@keyframes reactor-anim {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@mixin border-radius($pixel...) {
border-radius: $pixel;
}
.e7 {
position: relative;
z-index: 1;
width: 160%;
height: 160%;
left: -32.5%;
top: -32.5%;
right: 0;
bottom: 0;
margin: auto;
border: $size3 solid transparent;
background: transparent;
@include border-radius(50%);
transform: rotateZ(0deg);
transition: box-shadow 3s ease;
text-align: center;
opacity: .3;
}
.semi_arc {
width: 100px;
height: 100px;
border: 6px solid #02feff;
background: rgba(2, 254, 255, 0.2);
-moz-border-radius: 50%;
-webkit-border-radius: 50%;
border-radius: 50%;
transform: rotateZ(0deg);
transition: box-shadow 3s ease;
text-align: center;
overflow: hidden;
}
.semi_arc:hover {
box-shadow: 0px 0px 30px $cshadow;
transition: 0.3s;
}
.semi_arc_2 {
content: "";
position: absolute;
width: 94%;
height: 94%;
left: 3%;
top: 3%;
border: 5px solid #02feff;
-moz-border-radius: 50%;
-webkit-border-radius: 50%;
border-radius: 50%;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
animation: rotate 4s linear infinite;
text-align: center;
overflow: hidden;
}
.semi_arc_2:after {
content: "";
position: absolute;
width: 94%;
height: 94%;
left: 3%;
top: 3%;
border: 4px solid #02feff;
-moz-border-radius: 50%;
-webkit-border-radius: 50%;
border-radius: 50%;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
animation: rotate_anti 2s linear infinite;
}
.semi_arc_3 {
content: "";
position: absolute;
width: 94%;
height: 94%;
left: 3%;
top: 3%;
border: 5px solid #02feff;
-moz-border-radius: 50%;
-webkit-border-radius: 50%;
border-radius: 50%;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
animation: rotate 4s linear infinite;
text-align: center;
overflow: hidden;
}
.e1:after {
border-color: rgba(2, 255, 255, 0.6);
border-left: 5px solid transparent;
border-right: 5px solid transparent;
}
.e2:after {
border-color: rgba(2, 255, 255, 0.6);
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-bottom: 5px solid transparent;
}
.e3 {
border-left: 6px solid transparent;
border-right: 6px solid transparent;
animation: rotate 5s linear infinite;
}
.e3:after {
border-color: rgba(2, 255, 255, 0.6);
border-top: 5px solid transparent;
border-bottom: 5px solid transparent;
}
.e4 {
width: 150px;
height: 150px;
}
.e4_1 {
border-color: rgba(2, 255, 255, 0.3);
border-left: 5px solid transparent;
border-right: 5px solid transparent;
}
.e4_1:after {
border-color: rgba(2, 255, 255, 0.6);
border-top: 4px solid transparent;
border-bottom: 4px solid transparent;
}
.e5 {
width: 200px;
height: 200px;
}
.e5_1 {
color: rgba(2, 255, 255, 0.15);
border: 2px solid;
border-left: 2px solid transparent;
animation: rotate 5s linear infinite;
}
.e5_2 {
color: rgba(2, 255, 255, 0.7);
border: 4px solid;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
animation: rotate_anti 4s linear infinite;
}
.e5_3 {
color: rgba(2, 255, 255, 0.5);
border: 2px solid;
border-left: 2px solid transparent;
border-right: 2px solid transparent;
animation: rotate 3s linear infinite;
}
.e5_4 {
color: rgba(2, 255, 255, 0.15);
border: 4px solid;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-bottom: 4px solid transparent;
animation: rotate_anti 2s linear infinite;
}
.e6 {
border-color: transparent;
background: rgba(255, 255, 255, 0);
width: 200px;
height: 200px;
}
@keyframes rotate_anti {
0% {
transform: rotateZ(360deg);
}
100% {
transform: rotateZ(0deg);
}
}
@keyframes rotate {
0% {
transform: rotateZ(0deg);
}
100% {
transform: rotateZ(360deg);
}
}
.marks {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
margin: auto;
li {
display: block;
width: 3px;
height: 11px;
background: $cshadow;
position: absolute;
margin-left: 117.5px;
margin-top: 113.5px;
animation: colour_ease2 3s infinite ease-in-out;
}
}
@keyframes colour_ease2 {
0% {
background: $marks_color_1;
}
50% {
background: $marks_color_2;
}
100% {
background: $marks_color_1;
}
}
.marks li:first-child {
transform: rotate(6deg) translateY($arc_radius);
}
.marks li:nth-child(2) {
transform: rotate(12deg) translateY($arc_radius);
}
.marks li:nth-child(3) {
transform: rotate(18deg) translateY($arc_radius);
}
.marks li:nth-child(4) {
transform: rotate(24deg) translateY($arc_radius);
}
.marks li:nth-child(5) {
transform: rotate(30deg) translateY($arc_radius);
}
.marks li:nth-child(6) {
transform: rotate(36deg) translateY($arc_radius);
}
.marks li:nth-child(7) {
transform: rotate(42deg) translateY($arc_radius);
}
.marks li:nth-child(8) {
transform: rotate(48deg) translateY($arc_radius);
}
.marks li:nth-child(9) {
transform: rotate(54deg) translateY($arc_radius);
}
.marks li:nth-child(10) {
transform: rotate(60deg) translateY($arc_radius);
}
.marks li:nth-child(11) {
transform: rotate(66deg) translateY($arc_radius);
}
.marks li:nth-child(12) {
transform: rotate(72deg) translateY($arc_radius);
}
.marks li:nth-child(13) {
transform: rotate(78deg) translateY($arc_radius);
}
.marks li:nth-child(14) {
transform: rotate(84deg) translateY($arc_radius);
}
.marks li:nth-child(15) {
transform: rotate(90deg) translateY($arc_radius);
}
.marks li:nth-child(16) {
transform: rotate(96deg) translateY($arc_radius);
}
.marks li:nth-child(17) {
transform: rotate(102deg) translateY($arc_radius);
}
.marks li:nth-child(18) {
transform: rotate(108deg) translateY($arc_radius);
}
.marks li:nth-child(19) {
transform: rotate(114deg) translateY($arc_radius);
}
.marks li:nth-child(20) {
transform: rotate(120deg) translateY($arc_radius);
}
.marks li:nth-child(21) {
transform: rotate(126deg) translateY($arc_radius);
}
.marks li:nth-child(22) {
transform: rotate(132deg) translateY($arc_radius);
}
.marks li:nth-child(23) {
transform: rotate(138deg) translateY($arc_radius);
}
.marks li:nth-child(24) {
transform: rotate(144deg) translateY($arc_radius);
}
.marks li:nth-child(25) {
transform: rotate(150deg) translateY($arc_radius);
}
.marks li:nth-child(26) {
transform: rotate(156deg) translateY($arc_radius);
}
.marks li:nth-child(27) {
transform: rotate(162deg) translateY($arc_radius);
}
.marks li:nth-child(28) {
transform: rotate(168deg) translateY($arc_radius);
}
.marks li:nth-child(29) {
transform: rotate(174deg) translateY($arc_radius);
}
.marks li:nth-child(30) {
transform: rotate(180deg) translateY($arc_radius);
}
.marks li:nth-child(31) {
transform: rotate(186deg) translateY($arc_radius);
}
.marks li:nth-child(32) {
transform: rotate(192deg) translateY($arc_radius);
}
.marks li:nth-child(33) {
transform: rotate(198deg) translateY($arc_radius);
}
.marks li:nth-child(34) {
transform: rotate(204deg) translateY($arc_radius);
}
.marks li:nth-child(35) {
transform: rotate(210deg) translateY($arc_radius);
}
.marks li:nth-child(36) {
transform: rotate(216deg) translateY($arc_radius);
}
.marks li:nth-child(37) {
transform: rotate(222deg) translateY($arc_radius);
}
.marks li:nth-child(38) {
transform: rotate(228deg) translateY($arc_radius);
}
.marks li:nth-child(39) {
transform: rotate(234deg) translateY($arc_radius);
}
.marks li:nth-child(40) {
transform: rotate(240deg) translateY($arc_radius);
}
.marks li:nth-child(41) {
transform: rotate(246deg) translateY($arc_radius);
}
.marks li:nth-child(42) {
transform: rotate(252deg) translateY($arc_radius);
}
.marks li:nth-child(43) {
transform: rotate(258deg) translateY($arc_radius);
}
.marks li:nth-child(44) {
transform: rotate(264deg) translateY($arc_radius);
}
.marks li:nth-child(45) {
transform: rotate(270deg) translateY($arc_radius);
}
.marks li:nth-child(46) {
transform: rotate(276deg) translateY($arc_radius);
}
.marks li:nth-child(47) {
transform: rotate(282deg) translateY($arc_radius);
}
.marks li:nth-child(48) {
transform: rotate(288deg) translateY($arc_radius);
}
.marks li:nth-child(49) {
transform: rotate(294deg) translateY($arc_radius);
}
.marks li:nth-child(50) {
transform: rotate(300deg) translateY($arc_radius);
}
.marks li:nth-child(51) {
transform: rotate(306deg) translateY($arc_radius);
}
.marks li:nth-child(52) {
transform: rotate(312deg) translateY($arc_radius);
}
.marks li:nth-child(53) {
transform: rotate(318deg) translateY($arc_radius);
}
.marks li:nth-child(54) {
transform: rotate(324deg) translateY($arc_radius);
}
.marks li:nth-child(55) {
transform: rotate(330deg) translateY($arc_radius);
}
.marks li:nth-child(56) {
transform: rotate(336deg) translateY($arc_radius);
}
.marks li:nth-child(57) {
transform: rotate(342deg) translateY($arc_radius);
}
.marks li:nth-child(58) {
transform: rotate(348deg) translateY($arc_radius);
}
.marks li:nth-child(59) {
transform: rotate(354deg) translateY($arc_radius);
}
.marks li:nth-child(60) {
transform: rotate(360deg) translateY($arc_radius);
}
/*
Some overrides.
*/
.reactor-container.active {
$arc_radius: 130px;
$size3: 6px;
$cshadow: rgba(2, 254, 255, 0.8);
$marks_color_1: rgba(2, 254, 255, 1);
$marks_color_2: rgba(2, 254, 255, 0.3);
$colour1: rgba(2, 255, 255, 0.15);
$colour3: rgba(2, 255, 255, 0.3);
$cshadow: rgba(2, 254, 255, 0.8);
scale: 1.1;
opacity: 1;
.coil-container {
animation: 3s infinite linear reactor-anim;
}
.reactor-container-inner {
box-shadow: 0px 0px 50px 15px $colour3, inset 0px 0px 50px 15px $colour3;
}
.core-inner {
border: 5px solid #1b4e5f;
background-color: #ffffff;
box-shadow: 0px 0px 7px 5px #52fefe, 0px 0px 10px 10px #52fefe inset;
}
.core-outer {
border: 1px solid #52fefe;
background-color: #ffffff;
box-shadow: 0px 0px 2px 1px #52fefe, 0px 0px 10px 5px #52fefe inset;
}
.core-wrapper {
background-color: #073c4b;
box-shadow: 0px 0px 5px 4px #52fefe, 0px 0px 6px 2px #52fefe inset;
}
.tunnel {
background-color: #ffffff;
box-shadow: 0px 0px 5px 1px #52fefe, 0px 0px 5px 4px #52fefe inset;
}
.coil {
background-color: #073c4b;
box-shadow: 0px 0px 5px #52fefe inset;
}
.semi_arc {
border: 6px solid #02feff;
background: rgba(2, 254, 255, 0.2);
}
.e5_1 {
animation: rotate 3s linear infinite;
}
.e5_2 {
animation: rotate_anti 2s linear infinite;
}
.e5_3 {
animation: rotate 2s linear infinite;
}
.e5_4 {
animation: rotate_anti 2s linear infinite;
}
}
</style>

View File

@@ -0,0 +1,18 @@
<script>
export let no_margin = false;
</script>
<div class="h-divider" class:no-margin="{no_margin}"></div>
<style lang="scss">
.h-divider {
margin: 20px 0;
height: 40px;
background-image: url(media/images/decor.png);
background-position: center;
&.no-margin {
margin: 0;
}
}
</style>

View File

@@ -0,0 +1,11 @@
<script>
let search_q = "";
</script>
<div id="search-form" class="search" class:active={search_q != ""}>
<form action="#" method="GET">
<input bind:value={search_q} type="text" name="q" placeholder="Введите команду или скажите &laquo;Джарвис&raquo; ..." autocomplete="off" minlength="3" maxlength="30">
<button type="submit"></button>
<small>Enter</small>
</form>
</div>

View File

@@ -0,0 +1,377 @@
<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_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);
// resources_cpu_temp = await invoke("get_cpu_temp");
// resources_cpu_usage = +Number(await invoke("get_cpu_usage")).toFixed(2);
})().catch(err => {
console.error(err);
});
}, 1000);
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 => {
console.error(err);
});
});
</script>
<div class="statistics">
<div class="online">
<div class="pulse"><div class="wave"></div></div>
<div class="info">
<span class="num">Микрофон</span>
<small title="{microphone_label}">{microphone_label}</small>
</div>
</div>
<div class="files">
<div class="pulse"><div class="wave"></div></div>
<div class="info">
<span class="num">Нейросети</span>
<small>{nn_details["ww_engine"]} + {nn_details["stt_engine"]}</small>
</div>
</div>
<div class="downloads hint--bottom" aria-label="Общее количество скачиваний по всему проекту">
<div class="pulse"><div class="wave"></div></div>
<div class="info">
<span class="num">Ресурсы</span>
<small><!-- CPU {resources_cpu_usage}%<br /> -->RAM {resources_ram_usage}mb</small>
</div>
</div>
</div>
<style lang="scss">
.statistics {
position: relative;
z-index: 3;
padding: 0 10px;
height: 100px;
display: flex;
justify-content: space-between;
& > div {
height: 70px;
}
.info {
z-index: 10;
}
& > .online {
position: relative;
width: 40%;
$base-color: rgba(0, 191, 8, 1);
$mid-color: rgba(0, 191, 8, 0.4);
$end-color: rgba(0, 191, 8, 0);
& > .pulse::before {
background-color: rgba(0, 191, 8, 1);
}
& > .pulse::after {
background-color: rgba(0, 191, 8, 1);
animation: online-cdot linear 3s;
animation-iteration-count: infinite;
animation-fill-mode: forwards;
}
& > .pulse .wave {
background-color: rgba(0, 191, 8, 0.4);
animation: online-radarWave cubic-bezier(0, 0.54, 0.53, 1) 3s 0s;
animation-iteration-count: infinite;
}
& > .pulse .wave::after {
background-color: rgba(0, 191, 8, 0.4);
animation: online-radarWave cubic-bezier(0, 0.54, 0.53, 1) 3s
0.1s;
animation-iteration-count: infinite;
}
& > .info {
position: absolute;
top: 26px;
left: 26px;
& > span.num {
font-size: 18px;
font-weight: bold;
color: #00bf08;
}
& > small {
display: block;
color: #535a60;
font-size: 12px;
position: relative;
top: 0;
width: 130px;
max-height: 40px;
overflow: hidden;
line-height: 1.5em;
}
}
@keyframes online-cdot {
0% {
opacity: 0.3;
background: $base-color;
}
50% {
opacity: 0.5;
}
100% {
opacity: 1;
background: $end-color;
}
}
@keyframes online-radarWave {
0% {
opacity: 0.1;
transform: scale(0);
}
5% {
background: $mid-color;
opacity: 1;
}
100% {
transform: scale(1.2);
background: $end-color;
}
}
}
& > .files {
position: relative;
width: 35%;
$base-color: rgba(255, 129, 48, 1);
$mid-color: rgba(255, 129, 48, 0.4);
$end-color: rgba(255, 129, 48, 0);
& > .pulse::before {
background-color: $base-color;
}
& > .pulse::after {
background-color: $base-color;
animation: files-cdot linear 5s;
animation-iteration-count: infinite;
animation-fill-mode: forwards;
}
& > .pulse .wave {
background-color: $mid-color;
animation: files-radarWave cubic-bezier(0, 0.54, 0.53, 1) 5s 0s;
animation-iteration-count: infinite;
}
& > .pulse .wave::after {
background-color: $mid-color;
animation: files-radarWave cubic-bezier(0, 0.54, 0.53, 1) 5s
0.1s;
animation-iteration-count: infinite;
}
& > .info {
position: absolute;
top: 26px;
left: 26px;
& > span.num {
font-size: 18px;
font-weight: bold;
color: #ff8130;
}
& > small {
display: block;
color: #535a60;
font-size: 12px;
position: relative;
top: 0;
}
}
@keyframes files-cdot {
0% {
opacity: 0.3;
background: $base-color;
}
50% {
opacity: 0.5;
}
100% {
opacity: 1;
background: $end-color;
}
}
@keyframes files-radarWave {
0% {
opacity: 0.1;
transform: scale(0);
}
5% {
background: $mid-color;
transform: scale(0.2);
opacity: 1;
}
100% {
transform: scale(0.8);
background: $end-color;
}
}
}
& > .downloads {
position: relative;
$base-color: rgba(11,66,166, 1);
$mid-color: rgba(32, 150, 243, 0.4);
$end-color: rgba(32, 150, 243, 0);
& > .pulse::before {
background: rgba(32, 150, 243, 1);
}
& > .pulse::after {
background: rgba(32, 150, 243, 1);
animation: downloads-cdot linear 7s;
animation-iteration-count: infinite;
animation-fill-mode: forwards;
animation-delay: 1s;
}
& > .pulse .wave {
background-color: $mid-color;
animation: downloads-radarWave cubic-bezier(0, 0.54, 0.53, 1) 7s
0s;
animation-iteration-count: infinite;
animation-delay: 1s;
}
& > .pulse .wave::after {
background-color: $mid-color;
animation: downloads-radarWave cubic-bezier(0, 0.54, 0.53, 1) 7s
0.1s;
animation-iteration-count: infinite;
animation-delay: 1s;
}
& > .info {
position: absolute;
top: 26px;
left: 26px;
& > span.num {
font-size: 18px;
font-weight: bold;
color: #1b78a6;
}
& > small {
display: block;
color: #535a60;
font-size: 12px;
position: relative;
top: 0;
}
}
@keyframes downloads-cdot {
0% {
opacity: 0.3;
background: $base-color;
}
50% {
opacity: 0.5;
}
100% {
opacity: 1;
background: $end-color;
}
}
@keyframes downloads-radarWave {
0% {
opacity: 0.1;
transform: scale(0);
}
5% {
background: $mid-color;
opacity: 1;
}
100% {
transform: scale(0.7);
background: $end-color;
}
}
}
.pulse {
position: relative;
height: 100px;
width: 100px;
margin: 0;
left: -43px;
top: 0px;
z-index: 5;
}
.pulse::before {
content: '';
position: absolute;
width: 11px;
height: 11px;
border-radius: 50%;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
opacity: .5;
}
.pulse::after {
content: '';
position: absolute;
width: 20px;
height: 20px;
border-radius: 50%;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
.pulse .wave {
position: absolute;
left: 7%;
top: 7%;
width: 86%;
height: 86%;
border-radius: 50%;
opacity: 0;
}
.pulse .wave::after {
content: '';
position: absolute;
left: 7%;
top: 7%;
width: 86%;
height: 86%;
border-radius: 50%;
opacity: 0;
}
}
</style>

308
gui/src/css/main.scss Normal file
View File

@@ -0,0 +1,308 @@
$prim-font: "Roboto", sans-serif;
$sec-font: "Roboto Condensed", sans-serif;
html, body {
overflow-x: hidden;
}
::-webkit-scrollbar {
width: 15px;
}
::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3);
}
::-webkit-scrollbar-thumb {
background: -webkit-gradient(linear,left top,left bottom,from(#999),to(#27292F));
background: linear-gradient(to bottom,#999,#27292F);
}
::selection {
background: #FF8901!important; /* WebKit/Blink Browsers */
color: white!important;
text-shadow: 1px 1px 1px rgba(0, 0, 0, .5)!important;
}
::-moz-selection {
background: #FF8901!important; /* WebKit/Blink Browsers */
color: white!important;
text-shadow: 1px 1px 1px rgba(0, 0, 0, .5)!important;
}
/*
* HEADER
*/
#header {
height: 80px;
background-color: #0c1013;
box-shadow: 0 0 19px 2px rgba(0, 0, 0, 0.91);
position: relative;
z-index: 100;
text-align: justify;
display: flex;
justify-content: space-between;
.logo {
margin-top: 12px;
& > a > img {
width: 57px;
display: inline-block;
transition: .5s opacity ease-in;
&:hover {
opacity: .8;
}
}
& > div {
display: inline-block;
margin-left: 13px;
margin-top: 8px;
vertical-align: top;
}
h1 {
color: #ffffff;
font-family: $sec-font;
font-size: 20px;
font-weight: bold;
text-transform: uppercase;
letter-spacing: 0.1px;
margin: 0;
& > a {
color: white;
text-shadow: 1px 1px 1px rgba(0, 0, 0, .5);
text-decoration: none;
&:hover {
color: #8AC832;
}
}
}
h2 {
color: #888;
font-family: $sec-font;
font-size: 14px;
font-weight: bold;
letter-spacing: 0.1px;
margin-top: 2px;
letter-spacing: -0.01px;
}
}
.top-menu {
vertical-align: top;
margin-top: 18px;
margin-left: 50px;
& > ul {
margin: 0;
padding: 0;
list-style: none;
& > li {
display: inline-block;
position: relative;
&:not(:first-child) {
margin-left: 20px;
}
& > a {
display: inline-block;
text-decoration: none;
color: #c6c6c6;
font-family: $sec-font;
font-size: 16px;
text-transform: uppercase;
font-weight: bold;
transition: .2s color;
padding: 10px 0;
& > svg {
height: 20px;
width: 20px;
vertical-align: middle;
margin-bottom: 4px;
margin-right: 3px;
margin-left: 3px;
}
&:hover, &.active {
color: #8AC832;
&:after {
-webkit-transform: scaleX(1);
-ms-transform: scaleX(1);
transform: scaleX(1);
-webkit-transform-origin: left;
-ms-transform-origin: left;
transform-origin: left;
}
}
&:after {
content: "";
display: block;
height: 3px;
border-radius: 10px;
-webkit-transform: scaleX(0);
-ms-transform: scaleX(0);
transform: scaleX(0);
transition: .4s -webkit-transform;
transition: .4s transform;
-webkit-transform-origin: 100% 0;
-ms-transform-origin: 100% 0;
transform-origin: 100% 0;
background: #8AC832;
opacity: 0.90;
position: absolute;
bottom: 5px;
left: 0;
width: 100%;
z-index: 333;
}
}
}
}
}
}
.search {
display: block;
margin: 20px 0;
text-align: center;
& > form {
position: relative;
& > input {
width: 380px;
height: 38px;
box-shadow: inset 0 0 5px 1px rgba(0, 0, 0, 0.6);
border: 1px solid rgba(6, 6, 6, 0.99);
background-color: #0f1012;
outline: none;
color: #D1D1D1;
font-family: $sec-font;
font-size: 17px;
font-weight: 600;
line-height: 70.58px;
padding-left: 20px;
padding-right: 45px;
padding-right: 45px;
&::placeholder {
color: #676767;
font-weight: 400;
}
&:focus + button + small {
opacity: .3;
}
}
& > button {
position: absolute;
padding: 0;
margin: 0;
background: none;
border: none;
right: 35px;
top: 4px;
width: 30px;
height: 30px;
opacity: 1;
transition: .3s opacity;
&::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 110%;
height: 110%;
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAbCAYAAABvCO8sAAAC4ElEQVRIiaXWX4hXRRTA8c/etkwy+gNBEVRCmxFl1ouZGlEWFUQZPfVQRLAjKvSylEFBf16EiChJOxhLQY8RtA+lRZtpRv8ohUJQNMiHooKyojZNtoeZy95+/fZ3r+uBy5xz5pzznYGZc2dodHRUB1lVviUYwXkYwk84iK8wGRHb2goNtQDXYxRXdVkV9mFrRDx/osCl2IxrG75PsAv75Z1NyzsdwQosb8R+jXURsbML8AG82rDH8TI+77eyiAAppSVYg9SYXhsRW5rxVU/+Qw3Yd7ip+PrCesB7ImKNvNMDxb05pbR+NuBSvFL0L7EYH7SB+oA/Lrm7i2tTSmlVL3AYbxf9eyzDkROFNaBTWCmfYJhIKc1rAh/DuUW/HUfnCmtAp3FrMefjmRp4Kh4tE+PYO6DO6fKp7Ao9hJeKOZZSWlBhNc4ozidbaqyUr8W4/x+42aSuOYT7KtxZHF/gcEvyvDI+KF/yFW20iPgZk8W8o8KVxZjsn/IfOdbQL8OulNLTHfJ2lHFRhYuKcbB/bKs8kVLanVK6ZkBMfS8vqLCgGL/NEQjX48MB8/UVm1/hz2KceRLAfbh3wHxde2pYbmFnY+EcYVsiYm1LzKVl/KHCN8W4sUPxUxr6j7i7AwxuKOP+ykxLW4bzWxKnyziBRXirjZRSOstMx9lW4Q38XRyPt+R/hOtwF35tgzVqDhX99QpTeK441pWVzya/49OOICmlCzFWzE0RcaRuT0/hj6Jv71qwg7xbxuNKv66BR820uIuxE6edDCmltB1XFPOeiPiL/B+sZQcexgtyk96L+3X42/eAFuM1+YUHGyJiop4f7ol/Ef/Iv5TL8VlZQMiXexBoRH7hjTXcj0TEs8242V5tNxdo8wC9J5/Sfq+25bitEfut/Gp7p7dw7w5reV/e4Yay6oW4pXyD5DC2YmNEHOsXMBuwlo3lWy3v+mpcgnPku/WL3Br3yA+uNyPi+KCC/wJQGMGINsMjCwAAAABJRU5ErkJggg==");
background-repeat: no-repeat;
background-size: 75%;
background-position: center center;
transition: all .3s;
}
&:hover {
&::before {
cursor: pointer;
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAbCAYAAABvCO8sAAABN2lDQ1BBZG9iZSBSR0IgKDE5OTgpAAAokZWPv0rDUBSHvxtFxaFWCOLgcCdRUGzVwYxJW4ogWKtDkq1JQ5ViEm6uf/oQjm4dXNx9AidHwUHxCXwDxamDQ4QMBYvf9J3fORzOAaNi152GUYbzWKt205Gu58vZF2aYAoBOmKV2q3UAECdxxBjf7wiA10277jTG+38yH6ZKAyNguxtlIYgK0L/SqQYxBMygn2oQD4CpTto1EE9AqZf7G1AKcv8ASsr1fBBfgNlzPR+MOcAMcl8BTB1da4Bakg7UWe9Uy6plWdLuJkEkjweZjs4zuR+HiUoT1dFRF8jvA2AxH2w3HblWtay99X/+PRHX82Vun0cIQCw9F1lBeKEuf1UYO5PrYsdwGQ7vYXpUZLs3cLcBC7dFtlqF8hY8Dn8AwMZP/fNTP8gAAAAJcEhZcwAACxMAAAsTAQCanBgAAAXRaVRYdFhNTDpjb20uYWRvYmUueG1wAAAAAAA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA1LjYtYzE0MiA3OS4xNjA5MjQsIDIwMTcvMDcvMTMtMDE6MDY6MzkgICAgICAgICI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIiB4bWxuczpwaG90b3Nob3A9Imh0dHA6Ly9ucy5hZG9iZS5jb20vcGhvdG9zaG9wLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdEV2dD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlRXZlbnQjIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE4IChXaW5kb3dzKSIgeG1wOkNyZWF0ZURhdGU9IjIwMjMtMDQtMjNUMDQ6MzE6NTgrMDU6MDAiIHhtcDpNb2RpZnlEYXRlPSIyMDIzLTA0LTIzVDA0OjM0OjI3KzA1OjAwIiB4bXA6TWV0YWRhdGFEYXRlPSIyMDIzLTA0LTIzVDA0OjM0OjI3KzA1OjAwIiBkYzpmb3JtYXQ9ImltYWdlL3BuZyIgcGhvdG9zaG9wOkNvbG9yTW9kZT0iMyIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDowZDA5NTdiMi0zYmM3LTcxNDItODcyNS01ODA3MjA2NTFlYTIiIHhtcE1NOkRvY3VtZW50SUQ9ImFkb2JlOmRvY2lkOnBob3Rvc2hvcDoxM2UwZWYxNi03OGM0LTE2NGMtODc1Mi0xYjY5OTQ1OTczMGMiIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDo5YjNkZTI4Yy1iOTBmLTNjNDUtYjAwNS1kNTExOTE3ZDhkNzIiPiA8eG1wTU06SGlzdG9yeT4gPHJkZjpTZXE+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJjcmVhdGVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjliM2RlMjhjLWI5MGYtM2M0NS1iMDA1LWQ1MTE5MTdkOGQ3MiIgc3RFdnQ6d2hlbj0iMjAyMy0wNC0yM1QwNDozMTo1OCswNTowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTggKFdpbmRvd3MpIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDowZDA5NTdiMi0zYmM3LTcxNDItODcyNS01ODA3MjA2NTFlYTIiIHN0RXZ0OndoZW49IjIwMjMtMDQtMjNUMDQ6MzQ6MjcrMDU6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE4IChXaW5kb3dzKSIgc3RFdnQ6Y2hhbmdlZD0iLyIvPiA8L3JkZjpTZXE+IDwveG1wTU06SGlzdG9yeT4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz4Wh528AAAC7UlEQVRIiaXWX4gXVRTA8c9v2jLJ6A8ERVAJbUakTr2YqRFl0QRRRk89FBGYqNDLUgY51PQiRERJWhhLQY8RtA+OFm2mGf2RGqEQDAvyoaigrKhNk+3h3mGnX7/9zbgeGO45555zvvfCvWdub/POpTrI6vilGMVF6OEnHMEXmCyyaldboV4LcCPWYnGXVeEQdhRZ9fypApdhG65v+D7GPhwWdjYt7HQUK7GiEfslNhRZtbcL8EG81rDH8TI+G7SyIqtAXqYp1uGRxvT6Iqu2N+OTvvyHG7DvcEv0DYT1gasiq9YJO/06urflZbpxNuAyvBr1z7EE77eBBoA/irn7o2trXqar+4Ej2Bn177Ecx04V1oBOYZVwgmEiL9N5TeATuDDqGY7PFdaATuP2aM7HMzXwTDweJ8ZxcEids4VT2RX6DV6K5lhepgsSrME50flUS41VwrUY9/8DN5vUNXu4P8Fd0XEAR1uS58XxIeGSr2yjFVn1MyajeWeCa6MxOTjlP3KioV+FfXmZFh3y9sRxUYLLonFkcGyrbM7LdH9eptcNianv5SUJFkTjtzkC4UZ8MGS+vmLzE/wZjXNPA3gI9w2Zr2tPjQgt7HwsnCNse5FV61tirozjDwm+isbNHYqf0dB/xD0dYHBTHA8nZlraclzckjgdxwkswtttpLxMzzPTcXYleBN/R8eTLfkf4gbcjV/bYI2avai/kWAKz0XHhrjy2eR3fNIRJC/TSzEWza1FVh2r29PT+CPqu7sW7CDvxPGk2K9r4HEzLe5y7MVZp0PKy3Q3ronmvUVW/UX4D9ayB4/iBaFJH8QDOvzt+0BL8LrwwoNNRVZN1PMjffEv4h/hl3I1Po0LeEW43MNAo8ILb6zhfqzIqmebcbO92m6N0OYBelc4pYNebStwRyP2W+HVVvYX7t9hLe8JO9wUV70Qt8VvmBzFDmwpsurEoIDZgLVsid8aYddLcQUuEO7WL0JrrIQH11tFVp0cVvBfVZDA+HDoxOQAAAAASUVORK5CYII=");
}
}
}
& > small {
position: absolute;
font-family: $sec-font;
font-size: 14px;
font-weight: bold;
line-height: 1.7em;
top: 8px;
right: 75px;
background-color: #D1D1D1;
color: #080C0F;
padding: 0 7px;
padding-top: 2px;
border-radius: 4px;
opacity: 0;
transition: opacity .3s;
cursor: default;
}
}
&.active {
small {
opacity: .3;
}
}
}
@media (max-width: 1364px) {
#content>.inner>section.materials>header>h1 {
font-size: 26px;
}
#content>.inner>section.materials>article>.details>h1 {
font-size: 25px;
}
#content>.inner>section.materials>article>.details>blockquote {
font-size: 17px;
line-height: 24px;
-webkit-line-clamp: 4;
}
.btn {
font-size: 16px!important;
}
#paginator>header>h1 {
font-size: 30px!important;
}
#paginator>.paginator-wrapper>.paginator-box {
transform: scale(1.5);
margin-top: 15px;
}
#paginator>.paginator-wrapper>small:first-child {
display: none;
}
#paginator>.paginator-wrapper>small:last-child {
display: none;
}
}

92
gui/src/css/styles.scss Normal file
View File

@@ -0,0 +1,92 @@
/*
SOME DEFAULT CSS.
*/
html, body {
margin: 0;
padding: 0;
min-height: 100vh;
min-width: 100vw;
background-color: #4C5062;
color: white;
&.assist-page {
background: rgb(24,123,123);
background: -moz-radial-gradient(circle, rgba(24,123,123,0.4906337535014006) 0%, rgba(13,15,19,1) 64%);
background: -webkit-radial-gradient(circle, rgba(24,123,123,0.4906337535014006) 0%, rgba(13,15,19,1) 64%);
background: radial-gradient(circle, rgba(24,123,123,0.4906337535014006) 0%, rgba(13,15,19,1) 64%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr="#187b7b",endColorstr="#0d0f13",GradientType=1);
&.assist-active {
background: rgb(24,81,123);
background: -moz-radial-gradient(circle, rgba(24,81,123,0.6418942577030813) 0%, rgba(13,15,19,1) 64%);
background: -webkit-radial-gradient(circle, rgba(24,81,123,0.6418942577030813) 0%, rgba(13,15,19,1) 64%);
background: radial-gradient(circle, rgba(24,81,123,0.6418942577030813) 0%, rgba(13,15,19,1) 64%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr="#18517b",endColorstr="#0d0f13",GradientType=1);
}
}
}
:root {
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
font-size: 16px;
line-height: 24px;
font-weight: 400;
color: #0f0f0f;
background-color: #f6f6f6;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
a {
color: #2A9CD0;
text-decoration: none;
font-weight: bold;
&:hover {
color: #1dabed;
text-decoration: underline;
}
}
/*
OVERRIDES.
*/
#wrapper {
padding: 0;
margin: 0;
}
#header, main {
padding: 0 30px;
}
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;
}
}

61
gui/src/functions.ts Normal file
View File

@@ -0,0 +1,61 @@
import { invoke } from "@tauri-apps/api/tauri"
import { is_listening, isListening } from "@/stores"
import { clearInterval, clearTimeout, setInterval, setTimeout } from 'worker-timers';
setInterval(() => {
(async () => {
is_listening.set(await invoke("is_listening"));
})().catch(err => {
console.error(err);
});
}, 1000);
export function startListening() {
(async () => {
invoke('start_listening')
.then((message) => {
is_listening.set(true);
})
.catch((error) => {
is_listening.set(false);
console.error(error);
// alert("Ошибка: " + error);
})
})().catch(err => {
console.error(err);
});
}
export function stopListening(callback) {
(async () => {
invoke('stop_listening')
.then((message) => {
is_listening.set(false);
if(callback) {
callback();
}
})
.catch((error) => {
console.error(error);
})
})().catch(err => {
console.error(err);
});
}
export function capitalizeFirstLetter(string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
export function showInExplorer(path) {
(async () => {
invoke('show_in_folder', {path: path})
.then((message) => {})
.catch((error) => {
console.error(error);
// alert("Ошибка: " + error);
})
})().catch(err => {
console.error(err);
});
}

13
gui/src/main.ts Normal file
View File

@@ -0,0 +1,13 @@
// Klondike project old CSS file
import "./css/main.scss";
// App current CSS file
import "./css/styles.scss";
// deploy app
import App from "./App.svelte";
const app = new App({
target: document.getElementById("app"),
});
export default app;

View File

@@ -0,0 +1,12 @@
<!-- _layout.svelte -->
<script>
import { Container } from '@svelteuidev/core'
import Header from '@/components/Header.svelte'
</script>
<Container fluid id="wrapper">
<Header />
<main>
<slot></slot>
</main>
</Container>

View File

@@ -0,0 +1,23 @@
<script lang="ts">
import HDivider from "@/components/elements/HDivider.svelte"
import Footer from "@/components/Footer.svelte"
import { Notification, Space } from '@svelteuidev/core';
import { InfoCircled } from 'radix-icons-svelte';
import { tg_official_link } from "@/stores";
</script>
<Space h="xl" />
<Notification title='[404] Этот раздел еще находится в разработке!' icon={InfoCircled} color='blue' withCloseButton={false}>
Тут будет список команд + полноценный редактор команд.<br>
Следите за обновлениями в <a href="{tg_official_link}" target="_blank">нашем телеграм канале</a>!
</Notification>
<div style="text-align: center;margin-top: 25px;">
<img src="/media/images/tenor.gif" alt="bruh" width="320px">
</div>
<HDivider />
<Footer />

View File

@@ -0,0 +1,71 @@
<script lang="ts">
import SearchBar from "@/components/elements/SearchBar.svelte"
import ArcReactor from "@/components/elements/ArcReactor.svelte"
import HDivider from "@/components/elements/HDivider.svelte"
import Stats from "@/components/elements/Stats.svelte"
import Footer from "@/components/Footer.svelte"
import { Notification, Space } from '@svelteuidev/core'
import { InfoCircled } from 'radix-icons-svelte'
import { onMount, onDestroy } from 'svelte'
onMount(async () => {
document.body.classList.add('assist-page');
});
onDestroy(async () => {
document.body.classList.remove('assist-page');
});
import { is_listening } from "@/stores"
let is_listening__val: boolean;
is_listening.subscribe(value => {
is_listening__val = value;
});
</script>
<HDivider />
{#if !is_listening__val}
<Notification title='Внимание!' icon={InfoCircled} color='cyan' withCloseButton={false}>
В данный момент ассистент не прослушивает команды.<br />
Пожалуйста, <a href="/settings">перейдите в настройки</a> и введите ключ Picovoice.
</Notification>
<!-- <SearchBar /> -->
{:else}
<!-- <SearchBar /> -->
<ArcReactor />
{/if}
<HDivider no_margin />
<Stats />
<Footer />
<!--
<Title order={1}>This is h1 title</Title>
<Title order={1} variant='gradient' gradient={{from: 'blue', to: 'red', deg: 45}}>This is h1 title with a twist</Title>
<Menu>
<Button slot="control" variant="gradient" gradient={{ from: 'blue', to: 'teal', deg: 50 }} radius="md" size="md">Toggle Menu</Button>
<Menu.Label>Application</Menu.Label>
<Menu.Item icon={Gear}>Settings</Menu.Item>
<Menu.Item icon={ChatBubble}>Messages</Menu.Item>
<Menu.Item icon={Camera}>Gallery</Menu.Item>
<Menu.Item icon={MagnifyingGlass}>
<svelte:fragment slot='rightSection'>
<Text size="xs" color="dimmed">⌘K</Text>
</svelte:fragment>
Search
</Menu.Item>
<Divider />
<Menu.Label>Danger zone</Menu.Label>
<Menu.Item icon={Width}>Transfer my data</Menu.Item>
<Menu.Item color="red" icon={Trash}>Delete my account</Menu.Item>
</Menu>
<Checkbox bind:checked={checked} label="I agree to sell my privacy" />
{checked}
{#if checked}
YEP!
{/if} -->

View File

@@ -0,0 +1,188 @@
<script lang="ts">
// IMPORTS
import { invoke } from "@tauri-apps/api/tauri"
import { goto } from '@roxi/routify'
import { onMount } from 'svelte'
import { startListening, stopListening, showInExplorer } from "@/functions";
import { setTimeout } from 'worker-timers';
import { feedback_link, log_file_path } from "@/stores";
// 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, Cube, Code, Gear, QuestionMarkCircled, CrossCircled } from 'radix-icons-svelte';
// VARIABLES
let available_microphones = [];
let settings_saved = false;
let save_button_disabled = false;
let assistant_voice_val = ""; // shared
let selected_microphone = "";
let selected_wake_word_engine = "";
let api_key__picovoice = "";
let api_key__openai = "";
// SHARED VALUES
import { assistant_voice } from "@/stores"
assistant_voice.subscribe(value => {
assistant_voice_val = value;
});
// FUNCTIONS
async function save_settings() {
save_button_disabled = true; // disable save button for a while
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);
setTimeout(() => {
save_button_disabled = false; // enable save button again
}, 1000);
// 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 => {
const [k, v] = entry;
available_microphones.push({
label: v,
value: k
});
});
available_microphones = available_microphones; // update component options
// load values from db
// 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"});
});
</script>
<Space h="xl" />
<Notification title='БЕТА версия!' icon={QuestionMarkCircled} color='blue' withCloseButton={false}>
Часть функций может работать некорректно.<br />
Сообщайте обо всех найденных багах в <a href="{feedback_link}" target="_blank">наш телеграм бот</a>.
<Space h="sm" />
<Button color="gray" radius="md" size="xs" uppercase on:click={() => {showInExplorer(log_file_path)}}>Открыть папку с логами</Button>
</Notification>
<Space h="xl" />
{#if settings_saved }
<Notification title='Настройки сохранены!' icon={Check} color='teal' on:close="{() => {settings_saved = false}}"></Notification>
<Space h="xl" />
{/if}
<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' }
]}
label="Голос ассистента"
description="Не все команды работают со всеми звуковыми пакетами."
variant="filled"
bind:value={assistant_voice_val}
/>
</Tabs.Tab>
<Tabs.Tab label='Устройства' icon={Mix}>
<Space h="sm" />
<NativeSelect data={available_microphones}
label="Выберите микрофон"
description="Его будет слушать ассистент."
variant="filled"
bind:value={selected_microphone}
/>
</Tabs.Tab>
<Tabs.Tab label='Нейросети' icon={Cube}>
<Space h="sm" />
<NativeSelect data={[
{ label: 'Rustpotter', value: 'rustpotter' },
{ label: 'Vosk (медленный)', value: 'vosk' },
{ 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" />
<Alert title="Внимание!" color="#868E96" variant="outline">
<Notification title='Эта нейросеть работает не у всех!' icon={CrossCircled} color='orange' withCloseButton={false}>
Мы ждем официального патча от разработчиков.
</Notification>
<Space h="sm" />
<Text size='sm' color="gray">
Введите сюда свой ключ Picovoice.<br />
Он выдается бесплатно при регистрации в <a href='https://console.picovoice.ai/' target="_blank">Picovoice Console</a>.<br>
</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='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} disabled/>
</InputWrapper>
</Tabs.Tab>
</Tabs>
<Space h="xl" />
<Button color="lime" radius="md" size="sm" uppercase ripple fullSize on:click={save_settings} disabled={save_button_disabled}>
Сохранить
</Button>
<Space h="sm" />
<Button color="gray" radius="md" size="sm" uppercase fullSize on:click={() => {$goto('/')}}>
Назад
</Button>
<HDivider />
<Footer />

34
gui/src/stores.ts Normal file
View File

@@ -0,0 +1,34 @@
import { invoke } from "@tauri-apps/api/tauri"
import { writable } from 'svelte/store'
// listen state
export const is_listening = writable(true);
let is_listening__val: boolean;
is_listening.subscribe(value => {
is_listening__val = value;
});
export function isListening() {return is_listening__val}
// assistant voice
export const assistant_voice = writable("");
(async () => {
assistant_voice.set(await invoke("db_read", {key: "assistant_voice"}));
})().catch(err => {
console.error(err);
});
// etc
export let tg_official_link = "";
export let feedback_link = "";
export let github_repository_link = "";
export let log_file_path = "";
(async () => {
tg_official_link = await invoke("get_tg_official_link")
feedback_link = await invoke("get_feedback_link")
github_repository_link = await invoke("get_repository_link")
log_file_path = await invoke("get_log_file_path")
})().catch(err => {
console.error(err);
});

2
gui/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
/// <reference types="svelte" />
/// <reference types="vite/client" />

24
gui/tsconfig.json Normal file
View File

@@ -0,0 +1,24 @@
{
"extends": "@tsconfig/svelte/tsconfig.json",
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"resolveJsonModule": true,
"baseUrl": ".",
/**
* Typecheck JS in `.svelte` and `.js` files by default.
* Disable checkJs if you'd like to use dynamic types in JS.
* Note that setting allowJs false does not prevent the use
* of JS in `.svelte` files.
*/
"allowJs": true,
"checkJs": true,
"isolatedModules": true,
"paths": {
"@/*": ["src/*"]
},
},
"include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"],
"references": [{ "path": "./tsconfig.node.json" }]
}

8
gui/tsconfig.node.json Normal file
View File

@@ -0,0 +1,8 @@
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node"
},
"include": ["vite.config.ts"]
}

45
gui/vite.config.ts Normal file
View File

@@ -0,0 +1,45 @@
import { defineConfig } from "vite";
import { svelte } from "@sveltejs/vite-plugin-svelte";
import sveltePreprocess from "svelte-preprocess";
import tsconfigPaths from 'vite-tsconfig-paths'
// https://vitejs.dev/config/
export default defineConfig(async () => ({
plugins: [
svelte({
preprocess: [
sveltePreprocess({
typescript: true,
}),
],
onwarn: (warning, handler) => {
const { code, frame } = warning;
if (code === "css-unused-selector")
return;
handler(warning);
},
}),
tsconfigPaths()
],
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
// prevent vite from obscuring rust errors
clearScreen: false,
// tauri expects a fixed port, fail if that port is not available
server: {
port: 1420,
strictPort: true,
},
// to make use of `TAURI_DEBUG` and other env variables
// https://tauri.studio/v1/api/config#buildconfig.beforedevcommand
envPrefix: ["VITE_", "TAURI_"],
build: {
// Tauri supports es2021
target: process.env.TAURI_PLATFORM == "windows" ? "chrome105" : "safari13",
// don't minify for debug builds
minify: !process.env.TAURI_DEBUG ? "esbuild" : false,
// produce sourcemaps for debug builds
sourcemap: !!process.env.TAURI_DEBUG,
},
}));