Tray options implementation + Voice system rewrite + build fixes

This commit is contained in:
Priler
2026-01-07 23:29:46 +05:00
parent 412acb7e2d
commit 47b7e7a65d
117 changed files with 1300 additions and 2334 deletions

1
Cargo.lock generated
View File

@@ -3123,6 +3123,7 @@ dependencies = [
name = "jarvis-core"
version = "0.1.0"
dependencies = [
"chrono",
"fluent",
"fluent-bundle",
"futures-util",

View File

@@ -39,4 +39,5 @@ tokio-tungstenite = "0.28.0"
futures-util = "0.3"
fluent = "0.17.0"
fluent-bundle = "0.16.0"
unic-langid = "0.9"
unic-langid = "0.9"
chrono = "0.4"

View File

@@ -1,26 +1,27 @@
use std::sync::mpsc::Receiver;
use std::time::SystemTime;
use jarvis_core::{audio, audio_processing, commands, config, listener, recorder, stt, COMMANDS_LIST, intent, ipc::{self, IpcEvent}};
use jarvis_core::{audio, audio_processing, commands, config, listener, recorder, stt, COMMANDS_LIST, intent, voices, ipc::{self, IpcEvent}};
use rand::prelude::*;
use crate::should_stop;
pub fn start() -> Result<(), ()> {
pub fn start(text_cmd_rx: Receiver<String>) -> Result<(), ()> {
// start the loop
main_loop()
main_loop(text_cmd_rx)
}
fn main_loop() -> Result<(), ()> {
fn main_loop(text_cmd_rx: Receiver<String>) -> Result<(), ()> {
let rt = tokio::runtime::Runtime::new().expect("Failed to create tokio runtime");
let mut start: SystemTime;
let sounds_directory = audio::get_sound_directory().unwrap();
// let sounds_directory = audio::get_sound_directory().unwrap();
let frame_length: usize = 512; // default for every wake-word engine
let mut frame_buffer: Vec<i16> = vec![0; frame_length];
let mut silence_frames: u32 = 0;
// play some run phrase
// @TODO. Different sounds? Or better make it via commands or upcoming events system.
audio::play_sound(&sounds_directory.join("run.wav"));
// play some startup phrase
// audio::play_sound(&sounds_directory.join("run.wav"));
voices::play_greet();
// start recording
match recorder::start_recording() {
@@ -39,10 +40,17 @@ fn main_loop() -> Result<(), ()> {
// check for stop signal
if should_stop() {
info!("Stop signal received, shutting down...");
voices::play_goodbye();
ipc::send(IpcEvent::Stopping);
break;
}
// check for text commands
if let Ok(text) = text_cmd_rx.try_recv() {
process_text_command(&text, &rt);
continue 'wake_word;
}
// read from microphone
recorder::read_microphone(&mut frame_buffer);
@@ -70,14 +78,10 @@ fn main_loop() -> Result<(), ()> {
start = SystemTime::now();
silence_frames = 0;
// play some greet phrase
// play some reply phrase
// @TODO. Make it via commands or upcoming events system.
audio::play_sound(&sounds_directory.join(format!(
"{}.wav",
config::ASSISTANT_GREET_PHRASES
.choose(&mut rand::thread_rng())
.unwrap()
)));
voices::play_reply();
// notify GUI we're listening
ipc::send(IpcEvent::Listening);
@@ -125,12 +129,13 @@ fn main_loop() -> Result<(), ()> {
info!("Wake word detected during chaining, reactivating...");
// play greet sound
audio::play_sound(&sounds_directory.join(format!(
"{}.wav",
config::ASSISTANT_GREET_PHRASES
.choose(&mut rand::thread_rng())
.unwrap()
)));
// audio::play_sound(&sounds_directory.join(format!(
// "{}.wav",
// config::ASSISTANT_GREET_PHRASES
// .choose(&mut rand::thread_rng())
// .unwrap()
// )));
voices::play_reply();
// reset timer and continue listening
start = SystemTime::now();
@@ -152,59 +157,8 @@ fn main_loop() -> Result<(), ()> {
continue 'voice_recognition;
}
// infer command (try intent recognition first, fallback to levenshtein)
let cmd_result = if let Some((intent_id, confidence)) =
rt.block_on(intent::classify(&recognized_voice))
{
info!("Intent recognized: {} (confidence: {:.2})", intent_id, confidence);
intent::get_command_by_intent(COMMANDS_LIST.get().unwrap(), &intent_id)
} else {
info!("Intent not recognized, trying levenshtein fallback ...");
commands::fetch_command(&recognized_voice, COMMANDS_LIST.get().unwrap())
};
if let Some((cmd_path, cmd_config)) = cmd_result {
info!("Command found: {:?}", cmd_path);
info!("Executing!");
// execute the command
match commands::execute_command(&cmd_path, &cmd_config) {
Ok(chain) => {
// success
info!("Command executed successfully.");
// notify GUI
ipc::send(IpcEvent::CommandExecuted {
id: cmd_config.id.clone(),
success: true,
});
if chain {
// chain commands
start = SystemTime::now();
} else {
// skip, if chaining is not required
start = start
.checked_sub(core::time::Duration::from_secs(1000))
.unwrap();
}
continue 'voice_recognition; // continue voice recognition
}
Err(msg) => {
// fail
error!("Error executing command: {}", msg);
ipc::send(IpcEvent::CommandExecuted {
id: cmd_config.id.clone(),
success: false,
});
ipc::send(IpcEvent::Error {
message: msg.to_string(),
});
}
}
}
// execute command (shared executor)
execute_command(&recognized_voice, &rt);
// return to wake-word listening after command execution (no matter successful or not)
break 'voice_recognition;
@@ -236,10 +190,93 @@ fn main_loop() -> Result<(), ()> {
Ok(())
}
// process text command from GUI
fn process_text_command(text: &str, rt: &tokio::runtime::Runtime) {
info!("Processing text command: {}", text);
ipc::send(IpcEvent::SpeechRecognized { text: text.to_string() });
// filter text same as voice
let mut filtered = text.to_lowercase();
for tbr in config::ASSISTANT_PHRASES_TBR {
filtered = filtered.replace(tbr, "");
}
let filtered = filtered.trim();
if filtered.is_empty() {
ipc::send(IpcEvent::Idle);
return;
}
execute_command(filtered, rt);
}
// shared command execution logic (manual & voice)
fn execute_command(text: &str, rt: &tokio::runtime::Runtime) {
let commands_list = match COMMANDS_LIST.get() {
Some(c) => c,
None => {
ipc::send(IpcEvent::Error { message: "Commands not loaded".to_string() });
ipc::send(IpcEvent::Idle);
return;
}
};
// let sounds_directory = audio::get_sound_directory().unwrap();
// try intent recognition first, fallback to levenshtein
let cmd_result = if let Some((intent_id, confidence)) =
rt.block_on(intent::classify(text))
{
info!("Intent recognized: {} (confidence: {:.2})", intent_id, confidence);
intent::get_command_by_intent(commands_list, &intent_id)
} else {
info!("Intent not recognized, trying levenshtein fallback...");
commands::fetch_command(text, commands_list)
};
if let Some((cmd_path, cmd_config)) = cmd_result {
info!("Command found: {:?}", cmd_path);
match commands::execute_command(&cmd_path, &cmd_config) {
Ok(_) => {
info!("Command executed successfully");
voices::play_ok(); // command executed sound
ipc::send(IpcEvent::CommandExecuted {
id: cmd_config.id.clone(),
success: true,
});
}
Err(msg) => {
error!("Error executing command: {}", msg);
voices::play_error();
ipc::send(IpcEvent::CommandExecuted {
id: cmd_config.id.clone(),
success: false,
});
ipc::send(IpcEvent::Error { message: msg.to_string() });
}
}
} else {
info!("No command found for: {}", text);
// play "not understood" sound
// audio::play_sound(&sounds_directory.join("not_understand.wav"));
voices::play_not_found();
ipc::send(IpcEvent::Error {
message: format!("Command not found: {}", text)
});
}
ipc::send(IpcEvent::Idle);
}
fn keyword_callback(keyword_index: i32) {}
pub fn close(code: i32) {
info!("Closing application.");
voices::play_goodbye();
ipc::send(IpcEvent::Stopping);
std::process::exit(code);
}

View File

@@ -1,12 +1,13 @@
use parking_lot::RwLock;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc;
// include core
use jarvis_core::{
audio, audio_processing, commands, config, db, listener, recorder, stt, intent,
ipc::{self, IpcAction},
i18n,
i18n, voices,
APP_CONFIG_DIR, APP_LOG_DIR, COMMANDS_LIST, DB,
};
@@ -41,6 +42,12 @@ fn main() -> Result<(), String> {
DB.set(Arc::new(RwLock::new(db::init_settings())))
.expect("DB already initialized");
// init voices
let voice_id = DB.get().unwrap().read().voice.clone();
if let Err(e) = voices::init(&voice_id) {
warn!("Failed to init voices: {}", e);
}
// init i18n
i18n::init(&DB.get().unwrap().read().language);
@@ -108,7 +115,10 @@ fn main() -> Result<(), String> {
info!("Initializing IPC...");
ipc::init();
ipc::set_action_handler(|action| {
// channel for text commands (manually written in the GUI)
let (text_cmd_tx, text_cmd_rx) = mpsc::channel::<String>();
ipc::set_action_handler(move |action| {
match action {
IpcAction::Stop => {
info!("Received stop command from GUI");
@@ -122,6 +132,15 @@ fn main() -> Result<(), String> {
info!("Received mute request: {}", muted);
// TODO: implement mute
}
IpcAction::TextCommand { text } => {
info!("Received text command: {}", text);
if let Err(e) = text_cmd_tx.send(text) {
error!("Failed to send text command to app: {}", e);
}
}
IpcAction::Ping => {
// handled internally by server
}
_ => {}
}
});
@@ -134,7 +153,7 @@ fn main() -> Result<(), String> {
// start the app (in the background thread)
std::thread::spawn(|| {
let _ = app::start();
let _ = app::start(text_cmd_rx);
});
tray::init_blocking();

View File

@@ -6,11 +6,12 @@ use tray_icon::{
};
use winit::event_loop::{ControlFlow, EventLoopBuilder};
use image;
use std::process::Command;
#[cfg(target_os="windows")]
use winit::platform::windows::EventLoopBuilderExtWindows;
use jarvis_core::{config, i18n};
use jarvis_core::{config, i18n, ipc::{self, IpcEvent}};
const TRAY_ICON_BYTES: &[u8] = include_bytes!("../../../resources/icons/32x32.png");
@@ -103,8 +104,14 @@ pub fn init_blocking() {
fn handle_menu_event(event: &MenuEvent) {
match event.id.0.as_str() {
"exit" => std::process::exit(0),
"restart" => { /* restart logic */ }
"settings" => { /* open settings */ }
"restart" => {
info!("Restarting from tray menu...");
restart_app();
}
"settings" => {
info!("Opening settings from tray menu...");
open_settings();
}
_ => {}
}
}
@@ -129,3 +136,74 @@ fn load_icon(path: &std::path::Path) -> tray_icon::Icon {
};
tray_icon::Icon::from_rgba(icon_rgba, icon_width, icon_height).expect("Failed to open icon")
}
fn restart_app() {
// get current executable path
let exe_path = match std::env::current_exe() {
Ok(path) => path,
Err(e) => {
error!("Failed to get executable path: {}", e);
return;
}
};
// spawn new instance
match Command::new(&exe_path).spawn() {
Ok(_) => {
info!("Spawned new instance, exiting current...");
std::process::exit(0);
}
Err(e) => {
error!("Failed to restart: {}", e);
}
}
}
fn open_settings() {
// check if jarvis-gui is connected via IPC
if ipc::has_clients() {
// gui is running, send reveal event
info!("GUI is connected, sending reveal event");
ipc::send(IpcEvent::RevealWindow);
} else {
// gui not running, launch it
info!("GUI not connected, launching jarvis-gui");
launch_gui();
}
}
fn launch_gui() {
let exe_path = match std::env::current_exe() {
Ok(path) => path,
Err(e) => {
error!("Failed to get executable path: {}", e);
return;
}
};
// jarvis-gui should be in same directory as jarvis-app
let gui_path = exe_path.parent()
.map(|p| p.join(get_gui_executable_name()))
.unwrap_or_else(|| get_gui_executable_name().into());
info!("Launching GUI: {:?}", gui_path);
match Command::new(&gui_path).spawn() {
Ok(_) => {
info!("Launched jarvis-gui");
}
Err(e) => {
error!("Failed to launch jarvis-gui: {}", e);
}
}
}
#[cfg(target_os = "windows")]
fn get_gui_executable_name() -> &'static str {
"jarvis-gui.exe"
}
#[cfg(not(target_os = "windows"))]
fn get_gui_executable_name() -> &'static str {
"jarvis-gui"
}

View File

@@ -29,6 +29,7 @@ futures-util = { workspace = true, optional = true }
fluent.workspace = true
fluent-bundle.workspace = true
unic-langid.workspace = true
chrono.workspace = true
# pv_recorder = { workspace = true, optional = true }
vosk = { version = "0.3.1", optional = true }

View File

@@ -57,9 +57,17 @@ pub fn init() -> Result<(), ()> {
}
pub fn play_sound(filename: &PathBuf) {
let audio_type = match AUDIO_TYPE.get() {
Some(t) => t,
None => {
warn!("Audio not initialized, cannot play: {}", filename.display());
return;
}
};
info!("Playing {}", filename.display());
match AUDIO_TYPE.get().unwrap() {
match audio_type {
AudioType::Rodio => {
rodio::play_sound(filename, true);
}
@@ -75,18 +83,11 @@ pub fn get_sound_directory() -> Option<PathBuf> {
SOUND_DIR.join(&s.voice)
};
match voice_path.exists() && voice_path.cmp(&SOUND_DIR) != Ordering::Equal {
match voice_path.exists() {
true => Some(voice_path),
_ => {
let default_voice_path = SOUND_DIR.join(config::DEFAULT_VOICE);
match default_voice_path.exists() {
true => Some(default_voice_path),
_ => {
error!("No sounds found. Search path - {:?}", voice_path);
None
}
}
error!("No sounds folder found. Search path - {:?}", voice_path);
None
}
}
}

View File

@@ -1,6 +1,7 @@
use crate::APP_DIR;
use rand::prelude::*;
use seqdiff::ratio;
use serde_yaml;
// use serde_yaml;
use std::path::Path;
use std::{fs, fs::File};
@@ -14,15 +15,16 @@ pub use structs::*;
use std::collections::HashMap;
use crate::{audio, config};
use crate::{config};
// @TODO. Allow commands both in yaml and json format.
pub fn parse_commands() -> Result<Vec<JCommandsList>, String> {
// collect commands
let mut commands: Vec<JCommandsList> = Vec::new();
let cmd_dirs = fs::read_dir(config::COMMANDS_PATH)
.map_err(|e| format!("Error reading commands directory: {}", e))?;
let commands_path = APP_DIR.join(config::COMMANDS_PATH);
let cmd_dirs = fs::read_dir(&commands_path)
.map_err(|e| format!("Error reading commands directory {:?}: {}", commands_path, e))?;
for entry in cmd_dirs {
let entry = match entry {
@@ -204,7 +206,7 @@ pub fn execute_command(
cmd_config: &JCommand,
// app_handle: &tauri::AppHandle,
) -> Result<bool, String> {
let sounds_directory = audio::get_sound_directory().unwrap();
// let sounds_directory = audio::get_sound_directory().unwrap();
match cmd_config.action.as_str() {
"voice" => {
@@ -217,7 +219,7 @@ pub fn execute_command(
.unwrap()
);
// events::play(random_cmd_sound, app_handle);
audio::play_sound(&sounds_directory.join(random_cmd_sound));
// audio::play_sound(&sounds_directory.join(random_cmd_sound));
Ok(true)
}
@@ -242,7 +244,7 @@ pub fn execute_command(
.unwrap()
);
// events::play(random_cmd_sound, app_handle);
audio::play_sound(&sounds_directory.join(random_cmd_sound));
// audio::play_sound(&sounds_directory.join(random_cmd_sound));
Ok(true)
} else {
@@ -264,7 +266,7 @@ pub fn execute_command(
.unwrap()
);
// events::play(random_cmd_sound, app_handle);
audio::play_sound(&sounds_directory.join(random_cmd_sound));
// audio::play_sound(&sounds_directory.join(random_cmd_sound));
Ok(true)
}
@@ -284,7 +286,7 @@ pub fn execute_command(
.unwrap()
);
// events::play(random_cmd_sound, app_handle);
audio::play_sound(&sounds_directory.join(random_cmd_sound));
// audio::play_sound(&sounds_directory.join(random_cmd_sound));
std::thread::sleep(Duration::from_secs(2));
std::process::exit(0);
@@ -299,7 +301,7 @@ pub fn execute_command(
.unwrap()
);
// events::play(random_cmd_sound, app_handle);
audio::play_sound(&sounds_directory.join(random_cmd_sound));
// audio::play_sound(&sounds_directory.join(random_cmd_sound));
Ok(false)
}

View File

@@ -1,7 +1,7 @@
use std::path::PathBuf;
use serde::Deserialize;
use serde::{Serialize, Deserialize};
#[derive(Deserialize, Debug)]
#[derive(Serialize, Deserialize, Debug)]
pub struct JCommandsList {
#[serde(skip)]
pub path: PathBuf,
@@ -11,7 +11,7 @@ pub struct JCommandsList {
#[derive(Deserialize, Debug, Clone)]
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct JCommand {
pub id: String,
pub action: String,

View File

@@ -70,7 +70,9 @@ pub const DEFAULT_WAKE_WORD_ENGINE: WakeWordEngine = WakeWordEngine::Vosk;
pub const DEFAULT_INTENT_RECOGNITION_ENGINE: IntentRecognitionEngine = IntentRecognitionEngine::IntentClassifier;
pub const DEFAULT_SPEECH_TO_TEXT_ENGINE: SpeechToTextEngine = SpeechToTextEngine::Vosk;
pub const DEFAULT_VOICE: &str = "jarvis-og";
pub const DEFAULT_VOICE: &str = "jarvis-remaster";
pub const SOUND_PATH: &str = "resources/sound"; // extended from SOUND_DIR (resources/sound)
pub const VOICES_PATH: &str = "voices"; // extended from SOUND_PATH (resources/sound)
pub const BUNDLE_IDENTIFIER: &str = "com.priler.jarvis";
pub const DB_FILE_NAME: &str = "app.db";
@@ -81,7 +83,7 @@ pub const REPOSITORY_LINK: Option<&str> = option_env!("CARGO_PKG_REPOSITORY");
pub const TG_OFFICIAL_LINK: Option<&str> = Some("https://t.me/howdyho_official");
pub const FEEDBACK_LINK: Option<&str> = Some("https://t.me/jarvis_feedback_bot");
pub const SUPPORT_BOOSTY_LINK: Option<&str> = Some("https://boosty.to/howdyho");
pub const SUPPORT_PATREON_LINK: Option<&str> = Some("https://www.patreon.com/user?u=22843414");
pub const SUPPORT_PATREON_LINK: Option<&str> = Some("https://www.patreon.com/c/priler");
/*
Tray.

View File

@@ -54,7 +54,9 @@ settings-microphone = Microphone
settings-microphone-desc = The assistant will listen to this microphone.
settings-mic-default = Default (System)
settings-voice = Assistant voice
settings-voice-desc = Not all commands work with all sound packs.
settings-voice-desc =
Not all commands work with all sound packs.
Click to listen the preview of sound.
settings-wake-word-engine = Wake word engine
settings-wake-word-desc = Choose the engine for wake word recognition.
settings-stt-engine = Speech recognition
@@ -117,3 +119,8 @@ notification-saved = Settings saved!
notification-error = Error
notification-assistant-started = Assistant started
notification-assistant-stopped = Assistant stopped
# ETC
search-error-not-running = Assistant is not running
search-error-failed = Failed to execute command
settings-no-voices = No voices found

View File

@@ -54,7 +54,9 @@ settings-microphone = Микрофон
settings-microphone-desc = Его будет слушать ассистент.
settings-mic-default = По умолчанию (Система)
settings-voice = Голос ассистента
settings-voice-desc = Не все команды работают со всеми звуковыми пакетами.
settings-voice-desc =
Не все команды работают со всеми звуковыми пакетами.
Кликните, чтобы прослушать как звучит голос.
settings-wake-word-engine = Движок активации
settings-wake-word-desc = Выберите нейросеть для распознавания активационной фразы.
settings-stt-engine = Распознавание речи
@@ -117,3 +119,8 @@ notification-saved = Настройки сохранены!
notification-error = Ошибка
notification-assistant-started = Ассистент запущен
notification-assistant-stopped = Ассистент остановлен
# ETC
search-error-not-running = Ассистент не запущен
search-error-failed = Не удалось выполнить команду
settings-no-voices = Голоса не найдены

View File

@@ -54,7 +54,9 @@ settings-microphone = Мікрофон
settings-microphone-desc = Його буде слухати асистент.
settings-mic-default = За замовчуванням (Система)
settings-voice = Голос асистента
settings-voice-desc = Не всі команди працюють з усіма звуковими пакетами.
settings-voice-desc =
Не всі команди працюють з усіма звуковими пакетами.
Натисніть, щоб прослухати як звучить голос.
settings-wake-word-engine = Рушій активації
settings-wake-word-desc = Виберіть нейромережу для розпізнавання активаційної фрази.
settings-stt-engine = Розпізнавання мовлення
@@ -117,3 +119,9 @@ notification-saved = Налаштування збережено!
notification-error = Помилка
notification-assistant-started = Асистент запущено
notification-assistant-stopped = Асистент зупинено
# ETC
search-error-not-running = Асистент не запущено
search-error-failed = Не вдалося виконати команду
settings-no-voices = Голоси не знайдено

View File

@@ -2,4 +2,4 @@ mod events;
mod server;
pub use events::{IpcAction, IpcEvent};
pub use server::{init, send, set_action_handler, start_server, IPC_ADDR, IPC_PORT};
pub use server::{init, send, set_action_handler, start_server, has_clients, IPC_ADDR, IPC_PORT};

View File

@@ -30,6 +30,9 @@ pub enum IpcEvent {
// Pong response
Pong,
// request GUI to reveal/focus window
RevealWindow,
}
// Actions sent from GUI to jarvis-app
@@ -47,4 +50,7 @@ pub enum IpcAction {
// Mute/unmute listening
SetMuted { muted: bool },
// Execute text command
TextCommand { text: String },
}

View File

@@ -187,4 +187,12 @@ async fn handle_client(
}
info!("IPC: Client disconnected: {}", peer_addr);
}
pub fn has_clients() -> bool {
if let Some(tx) = BROADCAST_TX.get() {
tx.receiver_count() > 0
} else {
false
}
}

View File

@@ -7,6 +7,8 @@ use std::path::PathBuf;
#[macro_use]
extern crate log;
pub mod time;
pub mod audio;
pub mod commands;
pub mod config;
@@ -32,6 +34,8 @@ pub mod audio_processing;
#[cfg(feature = "jarvis_app")]
pub mod ipc;
pub mod voices;
// shared statics
// pub static APP_DIR: Lazy<PathBuf> = Lazy::new(|| std::env::current_dir().unwrap());
pub static APP_DIR: Lazy<PathBuf> = Lazy::new(|| {
@@ -40,7 +44,7 @@ pub static APP_DIR: Lazy<PathBuf> = Lazy::new(|| {
.and_then(|p| p.parent().map(|p| p.to_path_buf()))
.unwrap_or_else(|| std::env::current_dir().unwrap())
});
pub static SOUND_DIR: Lazy<PathBuf> = Lazy::new(|| APP_DIR.clone().join("resources/sound"));
pub static SOUND_DIR: Lazy<PathBuf> = Lazy::new(|| APP_DIR.clone().join(config::SOUND_PATH));
pub static APP_DIRS: OnceCell<AppDirs> = OnceCell::new();
pub static APP_CONFIG_DIR: OnceCell<PathBuf> = OnceCell::new();
pub static APP_LOG_DIR: OnceCell<PathBuf> = OnceCell::new();

View File

@@ -0,0 +1,2 @@
pub mod structs;
pub use structs::*;

View File

@@ -0,0 +1,21 @@
use chrono::Timelike;
#[derive(Debug, Clone, Copy)]
pub enum TimeOfDay {
Morning, // 5:00 - 11:59
Day, // 12:00 - 16:59
Evening, // 17:00 - 21:59
Night, // 22:00 - 4:59
}
impl TimeOfDay {
pub fn now() -> Self {
let hour = chrono::Local::now().hour();
match hour {
5..=11 => TimeOfDay::Morning,
12..=16 => TimeOfDay::Day,
17..=21 => TimeOfDay::Evening,
_ => TimeOfDay::Night,
}
}
}

View File

@@ -0,0 +1,234 @@
use std::fs;
use std::path::{Path, PathBuf};
use rand::prelude::*;
use once_cell::sync::OnceCell;
use parking_lot::RwLock;
// use chrono::Timelike;
use crate::{DB, SOUND_DIR, audio, config, time};
pub mod structs;
static VOICES: OnceCell<Vec<structs::VoiceConfig>> = OnceCell::new();
static CURRENT_VOICE_ID: OnceCell<RwLock<String>> = OnceCell::new();
pub fn init(default_voice: &str) -> Result<(), String> {
CURRENT_VOICE_ID.get_or_init(|| RwLock::new(default_voice.to_string()));
let voices = scan_voices()?;
if voices.is_empty() {
return Err("No voices found".into());
}
info!("Loaded {} voice(s): {:?}",
voices.len(),
voices.iter().map(|v| &v.voice.id).collect::<Vec<_>>()
);
VOICES.set(voices).map_err(|_| "Voices already initialized")?;
Ok(())
}
pub fn scan_voices() -> Result<Vec<structs::VoiceConfig>, String> {
let voices_dir = SOUND_DIR.join(&config::VOICES_PATH);
if !voices_dir.exists() {
return Err(format!("Voices directory not found: {:?}", voices_dir));
}
let mut voices = Vec::new();
let entries = fs::read_dir(&voices_dir)
.map_err(|e| format!("Failed to read voices directory: {}", e))?;
for entry in entries.flatten() {
let voice_path = entry.path();
if !voice_path.is_dir() {
continue;
}
let toml_path = voice_path.join("voice.toml");
if !toml_path.exists() {
warn!("Voice folder {:?} missing voice.toml, skipping", voice_path);
continue;
}
match load_voice_config(&toml_path, &voice_path) {
Ok(config) => voices.push(config),
Err(e) => warn!("Failed to load voice {:?}: {}", voice_path, e),
}
}
Ok(voices)
}
fn load_voice_config(toml_path: &Path, voice_path: &Path) -> Result<structs::VoiceConfig, String> {
let content = fs::read_to_string(toml_path)
.map_err(|e| format!("Failed to read voice.toml: {}", e))?;
let mut config: structs::VoiceConfig = toml::from_str(&content)
.map_err(|e| format!("Failed to parse voice.toml: {}", e))?;
config.path = voice_path.to_path_buf();
Ok(config)
}
pub fn list_voices() -> Vec<structs::VoiceConfig> {
VOICES.get().cloned().unwrap_or_default()
}
pub fn get_voice(voice_id: &str) -> Option<structs::VoiceConfig> {
VOICES.get()?.iter().find(|v| v.voice.id == voice_id).cloned()
}
pub fn get_current_voice() -> Option<structs::VoiceConfig> {
let current_id = CURRENT_VOICE_ID.get()?.read().clone();
get_voice(&current_id)
}
pub fn set_current_voice(voice_id: &str) {
if let Some(lock) = CURRENT_VOICE_ID.get() {
*lock.write() = voice_id.to_string();
}
}
fn get_current_language() -> String {
DB.get()
.map(|db| db.read().language.clone())
.unwrap_or_else(|| "ru".to_string())
}
fn find_sound_file(voice_path: &Path, lang: &str, sound_name: &str) -> Option<PathBuf> {
let extensions = ["mp3", "wav", "ogg"];
let lang_path = voice_path.join(lang);
// try language subfolder first
for ext in &extensions {
let file_path = lang_path.join(format!("{}.{}", sound_name, ext));
if file_path.exists() {
return Some(file_path);
}
}
// fallback to root voice folder
for ext in &extensions {
let file_path = voice_path.join(format!("{}.{}", sound_name, ext));
if file_path.exists() {
return Some(file_path);
}
}
None
}
fn play_random_from(sounds: &[String]) {
if sounds.is_empty() {
return;
}
let voice = match get_current_voice() {
Some(v) => v,
None => {
warn!("No current voice set");
return;
}
};
let lang = get_current_language();
let sound_name = sounds.choose(&mut rand::thread_rng()).unwrap();
match find_sound_file(&voice.path, &lang, sound_name) {
Some(path) => {
debug!("Playing: {:?}", path);
audio::play_sound(&path);
}
None => {
warn!("Sound not found: {} (lang: {}, voice: {})", sound_name, lang, voice.voice.id);
}
}
}
pub fn play(reaction: structs::Reaction) {
let voice = match get_current_voice() {
Some(v) => v,
None => {
warn!("No current voice set");
return;
}
};
let sounds = match reaction {
structs::Reaction::Greet => {
// try time specific first
let time_specific = match time::TimeOfDay::now() {
time::TimeOfDay::Morning => &voice.reactions.greet_morning,
time::TimeOfDay::Day => &voice.reactions.greet_day,
time::TimeOfDay::Evening => &voice.reactions.greet_evening,
time::TimeOfDay::Night => &voice.reactions.greet_night,
};
if time_specific.is_empty() {
// fallback to simple run voice (not time specific)
&voice.reactions.greet
} else {
time_specific
}
}
structs::Reaction::Reply => &voice.reactions.reply,
structs::Reaction::Ok => &voice.reactions.ok,
structs::Reaction::NotFound => &voice.reactions.not_found,
structs::Reaction::Thanks => &voice.reactions.thanks,
structs::Reaction::Error => &voice.reactions.error,
structs::Reaction::Goodbye => &voice.reactions.goodbye,
};
play_random_from(sounds);
}
// Play a preview sound for a specific voice
pub fn play_preview(voice_id: &str) {
let voice = match get_voice(voice_id) {
Some(v) => v,
None => {
warn!("Voice not found for preview: {}", voice_id);
return;
}
};
let lang = get_current_language();
// pick from reply or ok sounds for preview
let sounds: Vec<&String> = voice.reactions.reply.iter()
.chain(voice.reactions.ok.iter())
.chain(voice.reactions.greet.iter())
.collect();
if sounds.is_empty() {
warn!("No preview sounds for voice: {}", voice_id);
return;
}
let sound_name = sounds.choose(&mut rand::thread_rng()).unwrap();
if let Some(path) = find_sound_file(&voice.path, &lang, sound_name) {
debug!("Playing preview: {:?}", path);
audio::play_sound(&path);
}
}
// shortcuts
pub fn play_greet() { play(structs::Reaction::Greet); } // app startup
pub fn play_reply() { play(structs::Reaction::Reply); } // wake word detected
pub fn play_ok() { play(structs::Reaction::Ok); } // command executed
pub fn play_not_found() { play(structs::Reaction::NotFound); }
pub fn play_thanks() { play(structs::Reaction::Thanks); }
pub fn play_error() { play(structs::Reaction::Error); }
pub fn play_goodbye() { play(structs::Reaction::Goodbye); }

View File

@@ -0,0 +1,70 @@
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VoiceConfig {
#[serde(skip)]
pub path: PathBuf,
pub voice: VoiceMeta,
pub reactions: VoiceReactions,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VoiceMeta {
pub id: String,
pub name: String,
#[serde(default)]
pub author: String,
pub languages: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct VoiceReactions {
// app startup (time-based or generic)
#[serde(default)]
pub greet: Vec<String>,
#[serde(default)]
pub greet_morning: Vec<String>,
#[serde(default)]
pub greet_day: Vec<String>,
#[serde(default)]
pub greet_evening: Vec<String>,
#[serde(default)]
pub greet_night: Vec<String>,
// wake word detected
#[serde(default)]
pub reply: Vec<String>,
// command executed
#[serde(default)]
pub ok: Vec<String>,
// command not found
#[serde(default)]
pub not_found: Vec<String>,
// thank you
#[serde(default)]
pub thanks: Vec<String>,
// error
#[serde(default)]
pub error: Vec<String>,
// shutdown
#[serde(default)]
pub goodbye: Vec<String>,
}
#[derive(Debug, Clone, Copy)]
pub enum Reaction {
Greet, // app startup
Reply, // wake word detected
Ok, // command executed
NotFound,
Thanks,
Error,
Goodbye,
}

View File

@@ -15,6 +15,12 @@
"allow": [
"$RESOURCE/**"
]
}
},
"core:window:allow-show",
"core:window:allow-hide",
"core:window:allow-set-focus",
"core:window:allow-unminimize",
"core:window:allow-minimize",
"core:window:allow-close"
]
}

View File

@@ -1 +1 @@
{"default":{"identifier":"default","description":"Default capabilities for Jarvis","local":true,"windows":["main"],"permissions":["core:default","shell:allow-open","dialog:allow-message","fs:default","fs:allow-read","fs:allow-write",{"identifier":"fs:scope","allow":["$RESOURCE/**"]}]}}
{"default":{"identifier":"default","description":"Default capabilities for Jarvis","local":true,"windows":["main"],"permissions":["core:default","shell:allow-open","dialog:allow-message","fs:default","fs:allow-read","fs:allow-write",{"identifier":"fs:scope","allow":["$RESOURCE/**"]},"core:window:allow-show","core:window:allow-hide","core:window:allow-set-focus","core:window:allow-unminimize","core:window:allow-minimize","core:window:allow-close"]}}

View File

@@ -1,7 +1,7 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use jarvis_core::{config, db, i18n, APP_CONFIG_DIR, APP_LOG_DIR, DB};
use jarvis_core::{config, db, i18n, voices, APP_CONFIG_DIR, APP_LOG_DIR, DB};
use parking_lot::RwLock;
use std::sync::Arc;
@@ -30,6 +30,16 @@ fn main() {
// init i18n
i18n::init(&settings.language);
// init voices
if let Err(e) = voices::init(&settings.voice) {
eprintln!("Failed to init voices: {}", e);
}
// init audio backend
if let Err(e) = jarvis_core::audio::init() {
eprintln!("Failed to init audio: {:?}", e);
}
// set db
DB.set(Arc::new(RwLock::new(settings)))
.expect("DB already initialized");
@@ -55,6 +65,8 @@ fn main() {
tauri_commands::get_author_name,
tauri_commands::get_repository_link,
tauri_commands::get_tg_official_link,
tauri_commands::get_boosty_link,
tauri_commands::get_patreon_link,
tauri_commands::get_feedback_link,
// fs
@@ -79,6 +91,15 @@ fn main() {
tauri_commands::get_current_language,
tauri_commands::set_language,
tauri_commands::get_supported_languages,
// commands
tauri_commands::get_commands_count,
tauri_commands::get_commands_list,
// voices
tauri_commands::list_voices,
tauri_commands::get_voice,
tauri_commands::preview_voice,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");

View File

@@ -29,4 +29,12 @@ pub use stt::*;
// import i18n commands
mod i18n;
pub use i18n::*;
pub use i18n::*;
// import commands commands xD
mod commands;
pub use commands::*;
// import voices commands
mod voices;
pub use voices::*;

View File

@@ -0,0 +1,22 @@
use jarvis_core::commands::{self, JCommand, JCommandsList};
use once_cell::sync::Lazy;
static COMMANDS: Lazy<Vec<JCommandsList>> = Lazy::new(|| {
commands::parse_commands().unwrap_or_default()
});
#[tauri::command]
pub fn get_commands_count() -> usize {
COMMANDS
.iter()
.map(|list| list.commands.len())
.sum()
}
#[tauri::command]
pub fn get_commands_list() -> Vec<JCommand> {
COMMANDS
.iter()
.flat_map(|list| list.commands.clone())
.collect()
}

View File

@@ -0,0 +1,16 @@
use jarvis_core::voices::{self, structs::VoiceConfig};
#[tauri::command]
pub fn list_voices() -> Vec<VoiceConfig> {
voices::list_voices()
}
#[tauri::command]
pub fn get_voice(voice_id: String) -> Option<VoiceConfig> {
voices::get_voice(&voice_id)
}
#[tauri::command]
pub fn preview_voice(voice_id: String) {
voices::play_preview(&voice_id);
}

View File

@@ -7,7 +7,7 @@
"beforeDevCommand": "npm run dev",
"beforeBuildCommand": "npm run build",
"devUrl": "http://localhost:1420",
"frontendDist": "../../frontend/dist"
"frontendDist": "../../frontend/dist/client"
},
"app": {
"withGlobalTauri": false,
@@ -35,11 +35,11 @@
],
"targets": "all",
"resources": {
"../../resources/commands": "commands",
"../../resources/sound": "sound",
"../../resources/rustpotter": "rustpotter",
"../../resources/vosk": "vosk",
"../../resources/keywords": "keywords"
"../../resources/commands": "resources/commands",
"../../resources/sound": "resources/sound",
"../../resources/rustpotter": "resources/rustpotter",
"../../resources/vosk": "resources/vosk",
"../../resources/keywords": "resources/keywords"
}
}
}

View File

@@ -1,19 +0,0 @@
<!-- src/App.svelte -->
<script>
import { Router } from "@roxi/routify";
import routes from "../.routify/routes.default.js";
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 />

View File

@@ -1,44 +0,0 @@
<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/core";
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

@@ -1,73 +0,0 @@
<script>
import { invoke } from "@tauri-apps/api/core"
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

@@ -1,28 +0,0 @@
<script lang="ts">
import { invoke } from "@tauri-apps/api/core"
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

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

View File

@@ -1,647 +0,0 @@
<!-- 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

@@ -1,18 +0,0 @@
<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

@@ -1,11 +0,0 @@
<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

@@ -1,377 +0,0 @@
<script>
// IMPORTS
import { invoke } from "@tauri-apps/api/core"
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>

View File

@@ -1,308 +0,0 @@
$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;
}
}

View File

@@ -1,92 +0,0 @@
/*
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;
}
}

View File

@@ -1,61 +0,0 @@
import { invoke } from "@tauri-apps/api/core"
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: () => void) {
(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: string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}
export function showInExplorer(path: any) {
(async () => {
invoke('show_in_folder', {path: path})
.then((message) => {})
.catch((error) => {
console.error(error);
// alert("Ошибка: " + error);
})
})().catch(err => {
console.error(err);
});
}

View File

@@ -1,13 +0,0 @@
// 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

@@ -1,12 +0,0 @@
<!-- _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

@@ -1,23 +0,0 @@
<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

@@ -1,71 +0,0 @@
<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

@@ -1,188 +0,0 @@
<script lang="ts">
// IMPORTS
import { invoke } from "@tauri-apps/api/core"
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: { label: string; value: number }[] = [];
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: String(v),
value: Number(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 />

View File

@@ -1,34 +0,0 @@
import { invoke } from "@tauri-apps/api/core"
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);
});

View File

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

View File

@@ -11,7 +11,7 @@
"@svelteuidev/composables": "^0.15.7",
"@svelteuidev/core": "^0.15.7",
"@svelteuidev/motion": "^0.15.7",
"@tauri-apps/api": "^2",
"@tauri-apps/api": "^2.9.1",
"@tauri-apps/plugin-dialog": "^2",
"@tauri-apps/plugin-fs": "^2",
"@tauri-apps/plugin-shell": "^2",
@@ -819,9 +819,9 @@
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz",
"integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==",
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz",
"integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==",
"cpu": [
"arm"
],
@@ -833,9 +833,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz",
"integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==",
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz",
"integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==",
"cpu": [
"arm64"
],
@@ -847,9 +847,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz",
"integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==",
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz",
"integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==",
"cpu": [
"arm64"
],
@@ -861,9 +861,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz",
"integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==",
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz",
"integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==",
"cpu": [
"x64"
],
@@ -875,9 +875,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz",
"integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==",
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz",
"integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==",
"cpu": [
"arm64"
],
@@ -889,9 +889,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz",
"integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==",
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz",
"integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==",
"cpu": [
"x64"
],
@@ -903,9 +903,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz",
"integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==",
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz",
"integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==",
"cpu": [
"arm"
],
@@ -917,9 +917,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz",
"integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==",
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz",
"integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==",
"cpu": [
"arm"
],
@@ -931,9 +931,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz",
"integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==",
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz",
"integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==",
"cpu": [
"arm64"
],
@@ -945,9 +945,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz",
"integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==",
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz",
"integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==",
"cpu": [
"arm64"
],
@@ -959,9 +959,23 @@
]
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz",
"integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==",
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz",
"integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-loong64-musl": {
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz",
"integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==",
"cpu": [
"loong64"
],
@@ -973,9 +987,23 @@
]
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz",
"integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==",
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz",
"integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-ppc64-musl": {
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz",
"integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==",
"cpu": [
"ppc64"
],
@@ -987,9 +1015,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz",
"integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==",
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz",
"integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==",
"cpu": [
"riscv64"
],
@@ -1001,9 +1029,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz",
"integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==",
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz",
"integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==",
"cpu": [
"riscv64"
],
@@ -1015,9 +1043,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz",
"integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==",
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz",
"integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==",
"cpu": [
"s390x"
],
@@ -1029,9 +1057,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz",
"integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==",
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz",
"integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==",
"cpu": [
"x64"
],
@@ -1043,9 +1071,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz",
"integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==",
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz",
"integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==",
"cpu": [
"x64"
],
@@ -1056,10 +1084,24 @@
"linux"
]
},
"node_modules/@rollup/rollup-openbsd-x64": {
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz",
"integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
]
},
"node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz",
"integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==",
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz",
"integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==",
"cpu": [
"arm64"
],
@@ -1071,9 +1113,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz",
"integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==",
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz",
"integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==",
"cpu": [
"arm64"
],
@@ -1085,9 +1127,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz",
"integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==",
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz",
"integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==",
"cpu": [
"ia32"
],
@@ -1099,9 +1141,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.54.0.tgz",
"integrity": "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==",
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz",
"integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==",
"cpu": [
"x64"
],
@@ -1113,9 +1155,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.54.0.tgz",
"integrity": "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==",
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz",
"integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==",
"cpu": [
"x64"
],
@@ -2784,9 +2826,9 @@
}
},
"node_modules/rollup": {
"version": "4.54.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz",
"integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==",
"version": "4.55.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz",
"integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2800,28 +2842,31 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.54.0",
"@rollup/rollup-android-arm64": "4.54.0",
"@rollup/rollup-darwin-arm64": "4.54.0",
"@rollup/rollup-darwin-x64": "4.54.0",
"@rollup/rollup-freebsd-arm64": "4.54.0",
"@rollup/rollup-freebsd-x64": "4.54.0",
"@rollup/rollup-linux-arm-gnueabihf": "4.54.0",
"@rollup/rollup-linux-arm-musleabihf": "4.54.0",
"@rollup/rollup-linux-arm64-gnu": "4.54.0",
"@rollup/rollup-linux-arm64-musl": "4.54.0",
"@rollup/rollup-linux-loong64-gnu": "4.54.0",
"@rollup/rollup-linux-ppc64-gnu": "4.54.0",
"@rollup/rollup-linux-riscv64-gnu": "4.54.0",
"@rollup/rollup-linux-riscv64-musl": "4.54.0",
"@rollup/rollup-linux-s390x-gnu": "4.54.0",
"@rollup/rollup-linux-x64-gnu": "4.54.0",
"@rollup/rollup-linux-x64-musl": "4.54.0",
"@rollup/rollup-openharmony-arm64": "4.54.0",
"@rollup/rollup-win32-arm64-msvc": "4.54.0",
"@rollup/rollup-win32-ia32-msvc": "4.54.0",
"@rollup/rollup-win32-x64-gnu": "4.54.0",
"@rollup/rollup-win32-x64-msvc": "4.54.0",
"@rollup/rollup-android-arm-eabi": "4.55.1",
"@rollup/rollup-android-arm64": "4.55.1",
"@rollup/rollup-darwin-arm64": "4.55.1",
"@rollup/rollup-darwin-x64": "4.55.1",
"@rollup/rollup-freebsd-arm64": "4.55.1",
"@rollup/rollup-freebsd-x64": "4.55.1",
"@rollup/rollup-linux-arm-gnueabihf": "4.55.1",
"@rollup/rollup-linux-arm-musleabihf": "4.55.1",
"@rollup/rollup-linux-arm64-gnu": "4.55.1",
"@rollup/rollup-linux-arm64-musl": "4.55.1",
"@rollup/rollup-linux-loong64-gnu": "4.55.1",
"@rollup/rollup-linux-loong64-musl": "4.55.1",
"@rollup/rollup-linux-ppc64-gnu": "4.55.1",
"@rollup/rollup-linux-ppc64-musl": "4.55.1",
"@rollup/rollup-linux-riscv64-gnu": "4.55.1",
"@rollup/rollup-linux-riscv64-musl": "4.55.1",
"@rollup/rollup-linux-s390x-gnu": "4.55.1",
"@rollup/rollup-linux-x64-gnu": "4.55.1",
"@rollup/rollup-linux-x64-musl": "4.55.1",
"@rollup/rollup-openbsd-x64": "4.55.1",
"@rollup/rollup-openharmony-arm64": "4.55.1",
"@rollup/rollup-win32-arm64-msvc": "4.55.1",
"@rollup/rollup-win32-ia32-msvc": "4.55.1",
"@rollup/rollup-win32-x64-gnu": "4.55.1",
"@rollup/rollup-win32-x64-msvc": "4.55.1",
"fsevents": "~2.3.2"
}
},
@@ -2859,9 +2904,9 @@
}
},
"node_modules/sass": {
"version": "1.97.1",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.97.1.tgz",
"integrity": "sha512-uf6HoO8fy6ClsrShvMgaKUn14f2EHQLQRtpsZZLeU/Mv0Q1K5P0+x2uvH6Cub39TVVbWNSrraUhDAoFph6vh0A==",
"version": "1.97.2",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.97.2.tgz",
"integrity": "sha512-y5LWb0IlbO4e97Zr7c3mlpabcbBtS+ieiZ9iwDooShpFKWXf62zz5pEPdwrLYm+Bxn1fnbwFGzHuCLSA9tBmrw==",
"dev": true,
"license": "MIT",
"peer": true,
@@ -3151,9 +3196,9 @@
}
},
"node_modules/undici": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz",
"integrity": "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==",
"version": "7.18.2",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.18.2.tgz",
"integrity": "sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==",
"dev": true,
"license": "MIT",
"engines": {

View File

@@ -14,7 +14,7 @@
"@svelteuidev/composables": "^0.15.7",
"@svelteuidev/core": "^0.15.7",
"@svelteuidev/motion": "^0.15.7",
"@tauri-apps/api": "^2",
"@tauri-apps/api": "^2.9.1",
"@tauri-apps/plugin-dialog": "^2",
"@tauri-apps/plugin-fs": "^2",
"@tauri-apps/plugin-shell": "^2",

View File

@@ -46,13 +46,13 @@
</p>
<p class="links last">
{#if $currentLanguage === "ru"}
{t('footer-support')} <a href={tgLink} target="_blank" class="telegram-link">
{t('footer-support')} <a href={boostyLink} target="_blank" class="telegram-link">
<img src="/media/icons/boosty.webp" alt="Boosty" width="18px" />
<span>Boosty</span>
</a>.
{/if}
{#if $currentLanguage === "ua" || $currentLanguage === "en"}
{t('footer-support')} <a href={tgLink} target="_blank" class="telegram-link">
{t('footer-support')} <a href={patreonLink} target="_blank" class="telegram-link">
<img src="/media/icons/patreon.png" alt="Patreon" width="18px" />
<span>Patreon</span>
</a>.

View File

@@ -1,20 +1,12 @@
<script lang="ts">
import { jarvisState } from "@/stores"
$: stateClass = getStateClass($jarvisState)
function getStateClass(state: string): string {
switch (state) {
case "listening":
case "processing":
return "active"
case "idle":
return "idle"
case "disconnected":
default:
return "disconnected"
}
}
$: stateClass = {
'disconnected': 'disconnected',
'idle': 'idle',
'listening': 'active',
'processing': 'active'
}[$jarvisState] || 'disconnected'
</script>
<div id="arc-reactor" class="reactor-container {stateClass} arc-white">

View File

@@ -1,22 +1,85 @@
<script lang="ts">
import { translations, translate } from "@/stores"
import { translations, translate, isJarvisRunning, ipcConnected, sendTextCommand } from "@/stores"
$: t = (key: string) => translate($translations, key)
let searchQuery = ""
let isProcessing = false
let statusMessage = ""
async function handleSubmit(e: Event) {
e.preventDefault()
const command = searchQuery.trim()
if (!command || isProcessing) return
if (!$isJarvisRunning || !$ipcConnected) {
statusMessage = t('search-error-not-running')
setTimeout(() => statusMessage = "", 3000)
return
}
isProcessing = true
statusMessage = ""
try {
await sendTextCommand(command)
searchQuery = ""
} catch (err) {
console.error("Failed to send command:", err)
statusMessage = t('search-error-failed')
setTimeout(() => statusMessage = "", 3000)
} finally {
isProcessing = false
}
}
function handleKeydown(e: KeyboardEvent) {
if (e.key === "Escape") {
searchQuery = ""
}
}
</script>
<div id="search-form" class="search" class:active={searchQuery !== ""}>
<form action="#" method="GET">
<div id="search-form" class="search" class:active={searchQuery !== ""} class:processing={isProcessing}>
<form on:submit={handleSubmit}>
<input
bind:value={searchQuery}
on:keydown={handleKeydown}
type="text"
name="q"
placeholder={t('search-placeholder')}
autocomplete="off"
minlength="3"
maxlength="30"
minlength="1"
maxlength="200"
disabled={isProcessing}
/>
<small>Enter</small>
<small>{isProcessing ? '...' : 'Enter'}</small>
</form>
</div>
{#if statusMessage}
<div class="search-status">{statusMessage}</div>
{/if}
</div>
<style lang="scss">
.search.processing input {
opacity: 0.6;
cursor: wait;
}
.search-status {
position: absolute;
bottom: -24px;
left: 50%;
transform: translateX(-50%);
font-size: 0.75rem;
color: rgba(82, 254, 254, 0.8);
white-space: nowrap;
animation: fadeIn 0.2s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateX(-50%) translateY(-5px); }
to { opacity: 1; transform: translateX(-50%) translateY(0); }
}
</style>

View File

@@ -12,10 +12,10 @@
$: t = (key: string) => translate($translations, key)
let microphoneName = "Загрузка..."
let microphoneName = ""
let wakeWordEngine = "Rustpotter"
let sttEngine = "Vosk"
let vadInfo = "Snip + ChatGPT"
let vadInfo = ""
onMount(async () => {
microphoneName = t('stats-loading')
@@ -68,7 +68,7 @@
<span class="stat-dot" class:active={$ipcConnected} style="--color: #3b82f6;"></span>
<div class="stat-content">
<span class="stat-label">{t('stats-resources')}</span>
<span class="stat-value">{#if jarvisRamUsage }RAM {$jarvisRamUsage}mb{:else}...{/if}</span>
<span class="stat-value">{#if $jarvisRamUsage }RAM {$jarvisRamUsage}mb{:else}...{/if}</span>
</div>
</div>
</div>

View File

@@ -1,4 +1,6 @@
import { writable, get } from "svelte/store"
import { invoke } from "@tauri-apps/api/core"
import { getCurrentWindow } from "@tauri-apps/api/window"
// ### IPC STORES ###
@@ -30,57 +32,32 @@ export function disableIpc() {
disconnectIpc()
}
export function connectIpc() {
if (!enabled) {
console.log("IPC: Not enabled, skipping connection")
return
export function connectIpc(port: number = 9712) {
if (ws?.readyState === WebSocket.OPEN) return
ws = new WebSocket(`ws://127.0.0.1:${port}`)
ws.onopen = () => {
ipcConnected.set(true)
jarvisState.set("idle")
console.log("[IPC] connected")
}
if (ws?.readyState === WebSocket.OPEN || ws?.readyState === WebSocket.CONNECTING) {
return
ws.onclose = () => {
ipcConnected.set(false)
console.log("[IPC] disconnected")
}
manualDisconnect = false
ws.onerror = (err) => {
console.error("[IPC] error:", err)
}
console.log("IPC: Connecting to", IPC_URL)
try {
ws = new WebSocket(IPC_URL)
ws.onopen = () => {
console.log("IPC: Connected")
ipcConnected.set(true)
jarvisState.set("idle")
sendAction("ping")
}
ws.onclose = (event) => {
console.log("IPC: Disconnected", event.code)
ipcConnected.set(false)
jarvisState.set("disconnected")
ws = null
if (!manualDisconnect && enabled) {
scheduleReconnect()
}
}
ws.onerror = () => {
// error is handled in onclose, just suppress console spam
}
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data)
handleEvent(data)
} catch (e) {
console.error("IPC: Failed to parse message:", event.data, e)
}
}
} catch (e) {
// suppress errors when server isn't running
if (enabled) {
scheduleReconnect()
ws.onmessage = (event) => {
try {
const msg = JSON.parse(event.data)
handleEvent(msg)
} catch (e) {
console.error("[IPC] failed to parse message:", e)
}
}
}
@@ -151,6 +128,11 @@ function handleEvent(data: any) {
case "pong":
// connection verified
break
case "reveal_window":
// bring window to foreground
revealWindow()
break
}
}
@@ -171,4 +153,35 @@ export function stopJarvisApp() {
export function reloadCommands() {
return sendAction("reload_commands")
}
}
export function sendIpcMessage(message: object): Promise<void> {
return new Promise((resolve, reject) => {
if (!ws || ws.readyState !== WebSocket.OPEN) {
reject(new Error("IPC not connected"))
return
}
try {
ws.send(JSON.stringify(message))
resolve()
} catch (err) {
reject(err)
}
})
}
export function sendTextCommand(text: string): boolean {
return sendAction("text_command", { text })
}
async function revealWindow() {
try {
const window = getCurrentWindow()
await window.show()
await window.unminimize()
await window.setFocus()
} catch (e) {
console.error("[IPC] Failed to reveal window:", e)
}
}

View File

@@ -21,13 +21,17 @@
let processRunning = false
let launching = false
let wasRunning = false // track previous state
isJarvisRunning.subscribe((value) => {
processRunning = value
if (value) {
enableIpc()
} else {
wasRunning = true
} else if (wasRunning) {
// only disable if it was running before
disableIpc()
wasRunning = false
}
})

View File

@@ -35,6 +35,30 @@
$: t = (key: string) => translate($translations, key)
interface VoiceMeta {
id: string
name: string
author: string
languages: string[]
}
interface VoiceConfig {
voice: VoiceMeta
}
let availableVoices: VoiceMeta[] = []
async function selectVoice(voiceId: string) {
voiceVal = voiceId
// play preview sound
try {
await invoke("preview_voice", { voiceId })
} catch (err) {
console.error("Failed to preview voice:", err)
}
}
// ### STATE
interface MicrophoneOption {
label: string
@@ -113,11 +137,20 @@
// ### INIT
onMount(async () => {
// load voices
try {
const voices = await invoke<VoiceConfig[]>("list_voices")
availableVoices = voices.map(v => v.voice)
} catch (err) {
console.error("Failed to load voices:", err)
availableVoices = []
}
try {
// load microphones
const mics = await invoke<string[]>("pv_get_audio_devices")
availableMicrophones = [
{ label: "По умолчанию (Система)", value: "-1" }, // system default
{ label: t('settings-mic-default'), value: "-1" }, // system default
...mics.map((name, idx) => ({
label: name,
value: String(idx)
@@ -200,18 +233,42 @@
<Tabs class="form" color="#8AC832" position="left">
<Tabs.Tab label={t('settings-general')} icon={Gear}>
<Space h="sm" />
<NativeSelect
data={[
{ label: "Jarvis New (ремастер)", value: "jarvis-remaster" },
{ label: "Рик из «Рик и Морти»", value: "rick-morty" },
{ label: "Jarvis (от Хауди)", value: "jarvis-howdy" },
{ label: "Jarvis OG (из фильмов)", value: "jarvis-og" }
]}
label={t('settings-voice')}
description={t('settings-voice-desc')}
variant="filled"
bind:value={voiceVal}
/>
<div class="voice-select">
<label>{t('settings-voice')}</label>
<p class="description">{t('settings-voice-desc')}</p>
<div class="voice-options">
{#each availableVoices as voice}
<button
type="button"
class="voice-option"
class:selected={voiceVal === voice.id}
on:click={() => selectVoice(voice.id)}
>
<div class="voice-info">
<span class="voice-name">{voice.name}</span>
{#if voice.author}
<span class="voice-author">by {voice.author}</span>
{/if}
</div>
<div class="voice-languages">
{#each voice.languages as lang}
<img
src="/media/flags/{lang.toUpperCase()}.png"
alt={lang}
width="20"
title={lang}
/>
{/each}
</div>
</button>
{/each}
{#if availableVoices.length === 0}
<p class="no-voices">{t('settings-no-voices')}</p>
{/if}
</div>
</div>
</Tabs.Tab>
<Tabs.Tab label={t('settings-devices')} icon={Mix}>
@@ -390,3 +447,112 @@
<HDivider />
<Footer />
<style lang="scss">
.voice-select {
margin-bottom: 1rem;
label {
font-weight: 600;
font-size: 0.9rem;
color: #fff;
display: block;
margin-bottom: 0.25rem;
}
.description {
font-size: 0.75rem;
color: rgba(255,255,255,0.5);
margin: 0 0 0.75rem;
white-space: pre-line;
}
}
$voice-item-height: 70px;
$voice-item-gap: 0.5rem;
$voice-max-visible: 3;
.voice-options {
display: flex;
flex-direction: column;
gap: $voice-item-gap;
max-height: $voice-item-height * $voice-max-visible;
overflow-y: auto;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
border-radius: 3px;
}
&::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 3px;
&:hover {
background: rgba(255, 255, 255, 0.3);
}
}
}
.voice-option {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 1rem;
background: rgba(30, 40, 45, 0.8);
border: 1px solid rgba(255,255,255,0.1);
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
text-align: left;
width: 100%;
&:hover {
background: rgba(40, 55, 60, 0.9);
border-color: rgba(255,255,255,0.2);
}
&.selected {
background: rgba(82, 254, 254, 0.1);
border-color: rgba(82, 254, 254, 0.4);
}
}
.voice-info {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.15rem;
}
.voice-name {
font-size: 0.85rem;
color: #fff;
font-weight: 500;
}
.voice-author {
font-size: 0.7rem;
color: rgba(255,255,255,0.4);
}
.voice-languages {
display: flex;
gap: 0.35rem;
img {
opacity: 0.8;
border-radius: 2px;
}
}
.no-voices {
font-size: 0.8rem;
color: rgba(255,255,255,0.4);
font-style: italic;
}
</style>

View File

@@ -13,6 +13,8 @@ export {
disableIpc,
disconnectIpc,
sendAction,
sendIpcMessage,
sendTextCommand,
stopJarvisApp,
reloadCommands
} from "./lib/ipc"

View File

@@ -0,0 +1,40 @@
// vite.config.ts
import { defineConfig } from "file:///D:/Rust/jarvis-app/frontend/node_modules/vite/dist/node/index.js";
import { svelte } from "file:///D:/Rust/jarvis-app/frontend/node_modules/@sveltejs/vite-plugin-svelte/src/index.js";
import sveltePreprocess from "file:///D:/Rust/jarvis-app/frontend/node_modules/svelte-preprocess/dist/index.js";
import tsconfigPaths from "file:///D:/Rust/jarvis-app/frontend/node_modules/vite-tsconfig-paths/dist/index.mjs";
import routify from "file:///D:/Rust/jarvis-app/frontend/node_modules/@roxi/routify/lib/extra/vite-plugin/vite-plugin.js";
var vite_config_default = defineConfig({
plugins: [
svelte({
preprocess: [
sveltePreprocess({
typescript: true
})
],
onwarn: (warning, handler) => {
const { code, frame } = warning;
if (code === "css-unused-selector")
return;
handler(warning);
}
}),
routify(),
tsconfigPaths()
],
clearScreen: false,
server: {
port: 1420,
strictPort: true
},
envPrefix: ["VITE_", "TAURI_"],
build: {
target: process.env.TAURI_PLATFORM == "windows" ? "chrome105" : "safari13",
minify: !process.env.TAURI_DEBUG ? "esbuild" : false,
sourcemap: !!process.env.TAURI_DEBUG
}
});
export {
vite_config_default as default
};
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCJEOlxcXFxSdXN0XFxcXGphcnZpcy1hcHBcXFxcZnJvbnRlbmRcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZmlsZW5hbWUgPSBcIkQ6XFxcXFJ1c3RcXFxcamFydmlzLWFwcFxcXFxmcm9udGVuZFxcXFx2aXRlLmNvbmZpZy50c1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9pbXBvcnRfbWV0YV91cmwgPSBcImZpbGU6Ly8vRDovUnVzdC9qYXJ2aXMtYXBwL2Zyb250ZW5kL3ZpdGUuY29uZmlnLnRzXCI7aW1wb3J0IHsgZGVmaW5lQ29uZmlnIH0gZnJvbSBcInZpdGVcIjtcclxuaW1wb3J0IHsgc3ZlbHRlIH0gZnJvbSBcIkBzdmVsdGVqcy92aXRlLXBsdWdpbi1zdmVsdGVcIjtcclxuaW1wb3J0IHN2ZWx0ZVByZXByb2Nlc3MgZnJvbSBcInN2ZWx0ZS1wcmVwcm9jZXNzXCI7XHJcbmltcG9ydCB0c2NvbmZpZ1BhdGhzIGZyb20gJ3ZpdGUtdHNjb25maWctcGF0aHMnXHJcbmltcG9ydCByb3V0aWZ5IGZyb20gJ0Byb3hpL3JvdXRpZnkvdml0ZS1wbHVnaW4nXHJcblxyXG5leHBvcnQgZGVmYXVsdCBkZWZpbmVDb25maWcoe1xyXG4gIHBsdWdpbnM6IFtcclxuICAgIHN2ZWx0ZSh7XHJcbiAgICAgIHByZXByb2Nlc3M6IFtcclxuICAgICAgICBzdmVsdGVQcmVwcm9jZXNzKHtcclxuICAgICAgICAgIHR5cGVzY3JpcHQ6IHRydWUsXHJcbiAgICAgICAgfSksXHJcbiAgICAgIF0sXHJcbiAgICAgIG9ud2FybjogKHdhcm5pbmcsIGhhbmRsZXIpID0+IHtcclxuICAgICAgICBjb25zdCB7IGNvZGUsIGZyYW1lIH0gPSB3YXJuaW5nO1xyXG4gICAgICAgIGlmIChjb2RlID09PSBcImNzcy11bnVzZWQtc2VsZWN0b3JcIilcclxuICAgICAgICAgICAgcmV0dXJuO1xyXG5cclxuICAgICAgICBoYW5kbGVyKHdhcm5pbmcpO1xyXG4gICAgICB9LFxyXG4gICAgfSksXHJcbiAgICByb3V0aWZ5KCksXHJcbiAgICB0c2NvbmZpZ1BhdGhzKClcclxuICBdLFxyXG5cclxuICBjbGVhclNjcmVlbjogZmFsc2UsXHJcbiAgc2VydmVyOiB7XHJcbiAgICBwb3J0OiAxNDIwLFxyXG4gICAgc3RyaWN0UG9ydDogdHJ1ZSxcclxuICB9LFxyXG4gIGVudlByZWZpeDogW1wiVklURV9cIiwgXCJUQVVSSV9cIl0sXHJcbiAgYnVpbGQ6IHtcclxuICAgIHRhcmdldDogcHJvY2Vzcy5lbnYuVEFVUklfUExBVEZPUk0gPT0gXCJ3aW5kb3dzXCIgPyBcImNocm9tZTEwNVwiIDogXCJzYWZhcmkxM1wiLFxyXG4gICAgbWluaWZ5OiAhcHJvY2Vzcy5lbnYuVEFVUklfREVCVUcgPyBcImVzYnVpbGRcIiA6IGZhbHNlLFxyXG4gICAgc291cmNlbWFwOiAhIXByb2Nlc3MuZW52LlRBVVJJX0RFQlVHLFxyXG4gIH0sXHJcbn0pOyJdLAogICJtYXBwaW5ncyI6ICI7QUFBMlEsU0FBUyxvQkFBb0I7QUFDeFMsU0FBUyxjQUFjO0FBQ3ZCLE9BQU8sc0JBQXNCO0FBQzdCLE9BQU8sbUJBQW1CO0FBQzFCLE9BQU8sYUFBYTtBQUVwQixJQUFPLHNCQUFRLGFBQWE7QUFBQSxFQUMxQixTQUFTO0FBQUEsSUFDUCxPQUFPO0FBQUEsTUFDTCxZQUFZO0FBQUEsUUFDVixpQkFBaUI7QUFBQSxVQUNmLFlBQVk7QUFBQSxRQUNkLENBQUM7QUFBQSxNQUNIO0FBQUEsTUFDQSxRQUFRLENBQUMsU0FBUyxZQUFZO0FBQzVCLGNBQU0sRUFBRSxNQUFNLE1BQU0sSUFBSTtBQUN4QixZQUFJLFNBQVM7QUFDVDtBQUVKLGdCQUFRLE9BQU87QUFBQSxNQUNqQjtBQUFBLElBQ0YsQ0FBQztBQUFBLElBQ0QsUUFBUTtBQUFBLElBQ1IsY0FBYztBQUFBLEVBQ2hCO0FBQUEsRUFFQSxhQUFhO0FBQUEsRUFDYixRQUFRO0FBQUEsSUFDTixNQUFNO0FBQUEsSUFDTixZQUFZO0FBQUEsRUFDZDtBQUFBLEVBQ0EsV0FBVyxDQUFDLFNBQVMsUUFBUTtBQUFBLEVBQzdCLE9BQU87QUFBQSxJQUNMLFFBQVEsUUFBUSxJQUFJLGtCQUFrQixZQUFZLGNBQWM7QUFBQSxJQUNoRSxRQUFRLENBQUMsUUFBUSxJQUFJLGNBQWMsWUFBWTtBQUFBLElBQy9DLFdBQVcsQ0FBQyxDQUFDLFFBQVEsSUFBSTtBQUFBLEVBQzNCO0FBQ0YsQ0FBQzsiLAogICJuYW1lcyI6IFtdCn0K

View File

@@ -0,0 +1,27 @@
[voice]
id = "jarvis-howdy"
name = "Jarvis Howdy"
author = "Abraham (Priler)"
languages = ["ru"]
[reactions]
# app startup
greet = ["run", "ready"]
# wake word detected
reply = ["reply1", "reply2", "reply3"]
# command executed
ok = ["ok1", "ok2", "ok3", "ok4"]
# command not found
not_found = ["not_found"]
# thanks
thanks = ["thanks"]
# error
error = []
# goodbye
goodbye = ["off"]

View File

@@ -0,0 +1,27 @@
[voice]
id = "jarvis-og"
name = "Jarvis OG"
author = "Abraham (Priler)"
languages = ["ru"]
[reactions]
# app startup
greet = ["run"]
# wake word detected
reply = ["reply1", "reply2", "reply3"]
# command executed
ok = ["ok1", "ok2", "ok3", "ok4"]
# command not found
not_found = ["not_found"]
# thanks
thanks = ["thanks"]
# error
error = []
# goodbye
goodbye = ["off"]

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