Update to Rust programming language.

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

View File

@@ -0,0 +1,189 @@
use rand::seq::SliceRandom;
use seqdiff::ratio;
use serde_yaml;
use std::path::Path;
use std::{fs, fs::File};
use core::time::Duration;
use std::path::PathBuf;
use std::process::Child;
use std::process::Command;
use tauri::Manager;
mod structs;
pub use structs::*;
use crate::config;
use crate::events;
pub fn parse_commands() -> Result<Vec<AssistantCommand>, String> {
// collect commands
let mut commands: Vec<AssistantCommand> = vec![];
// read commands directories first
if let Ok(cpaths) = fs::read_dir(config::COMMANDS_PATH) {
for cpath in cpaths {
// validate this command, check if required files exists
let _cpath = cpath.unwrap().path();
let cc_file = Path::new(&_cpath).join("command.yaml");
if cc_file.exists() {
// try parse config files
let cc_reader = std::fs::File::open(&cc_file).unwrap();
let cc_yaml: CommandsList;
// try parse command.yaml
if let Ok(parse_result) = serde_yaml::from_reader::<File, CommandsList>(cc_reader) {
cc_yaml = parse_result;
} else {
println!("Can't parse {}, skipping ...", &cc_file.display());
continue;
// return Err(format!("Can't parse {}", &cc_file.display()));
}
// everything seems to be Ok
commands.push(AssistantCommand {
path: _cpath,
commands: cc_yaml,
});
}
}
if commands.len() > 0 {
Ok(commands)
} else {
Err("No commands were found".into())
}
} else {
return Err("Error reading commands directory".into());
}
}
pub fn fetch_command<'a>(
phrase: &str,
commands: &'a Vec<AssistantCommand>,
) -> Option<(&'a PathBuf, &'a Config)> {
// result scmd
let mut result_scmd: Option<(&PathBuf, &Config)> = None;
let mut current_max_ratio = config::CMD_RATIO_THRESHOLD;
// convert fetch phrase to sequence
let fetch_phrase_chars = phrase.chars().collect::<Vec<_>>();
// list all the commands
for cmd in commands {
// list all subcommands
for scmd in &cmd.commands.list {
// list all phrases in command
for cmd_phrase in &scmd.phrases {
// convert cmd phrase to sequence
let cmd_phrase_chars = cmd_phrase.chars().collect::<Vec<_>>();
// compare fetch phrase with cmd phrase
let ratio = ratio(&fetch_phrase_chars, &cmd_phrase_chars);
// return, if it fits the given threshold
if ratio >= current_max_ratio {
result_scmd = Some((&cmd.path, &scmd));
current_max_ratio = ratio;
// println!("Ratio is: {}", ratio);
// return Some((&cmd.path, &scmd))
}
}
}
}
if let Some((cmd_path, scmd)) = result_scmd {
println!("Ratio is: {}", current_max_ratio);
Some((&cmd_path, &scmd))
} else {
None
}
}
pub fn execute_exe(exe: &str, args: &Vec<String>) -> std::io::Result<Child> {
Command::new(exe).args(args).spawn()
}
pub fn execute_command(
cmd_path: &PathBuf,
cmd_config: &Config,
app_handle: &tauri::AppHandle,
) -> Result<(), String> {
match cmd_config.command.action.as_str() {
"voice" => {
// VOICE command type
let random_cmd_sound = cmd_config
.voice
.sounds
.choose(&mut rand::thread_rng())
.unwrap();
events::play(random_cmd_sound, app_handle);
Ok(())
}
"ahk" => {
// AutoHotkey command type
let exe_path_absolute = Path::new(&cmd_config.command.exe_path);
let exe_path_local = Path::new(&cmd_path).join(&cmd_config.command.exe_path);
if let Ok(_) = execute_exe(
if exe_path_absolute.exists() {
exe_path_absolute.to_str().unwrap()
} else {
exe_path_local.to_str().unwrap()
},
&cmd_config.command.exe_args,
) {
let random_cmd_sound = cmd_config
.voice
.sounds
.choose(&mut rand::thread_rng())
.unwrap();
events::play(random_cmd_sound, app_handle);
Ok(())
} else {
Err("AHK process spawn error (does exe path is valid?)".into())
}
}
"cli" => {
// CLI command type
let exe_path_absolute = Path::new(&cmd_config.command.exe_path);
let exe_path_local = Path::new(&cmd_path).join(&cmd_config.command.exe_path);
if let Ok(_) = execute_exe(
if exe_path_absolute.exists() {
exe_path_absolute.to_str().unwrap()
} else {
exe_path_local.to_str().unwrap()
},
&cmd_config.command.exe_args,
) {
let random_cmd_sound = cmd_config
.voice
.sounds
.choose(&mut rand::thread_rng())
.unwrap();
events::play(random_cmd_sound, app_handle);
Ok(())
} else {
Err("Shell process spawn error (does cli command is valid?)".into())
}
}
"terminate" => {
// TERMINATE command type
let random_cmd_sound = cmd_config
.voice
.sounds
.choose(&mut rand::thread_rng())
.unwrap();
events::play(random_cmd_sound, app_handle);
std::thread::sleep(Duration::from_secs(2));
std::process::exit(0);
}
_ => Err("Command type unknown".into()),
}
}

View File

@@ -0,0 +1,32 @@
use serde::Deserialize;
use std::path::PathBuf;
#[derive(Debug)]
pub struct AssistantCommand {
pub path: PathBuf,
pub commands: CommandsList,
}
#[derive(Deserialize, Debug)]
pub struct CommandsList {
pub list: Vec<Config>,
}
#[derive(Deserialize, Debug)]
pub struct Config {
pub command: ConfigCommandSection,
pub voice: ConfigVoiceSection,
pub phrases: Vec<String>,
}
#[derive(Deserialize, Debug)]
pub struct ConfigCommandSection {
pub action: String,
pub exe_path: String,
pub exe_args: Vec<String>,
}
#[derive(Deserialize, Debug)]
pub struct ConfigVoiceSection {
pub sounds: Vec<String>,
}

42
src-tauri/src/config.rs Normal file
View File

@@ -0,0 +1,42 @@
use const_concat::const_concat;
use std::env::current_dir;
// pub const IS_DEV: bool = cfg!(debug_assertions);// cfg!(debug_assertions);
// pub const PUBLIC_PATH: &str = if IS_DEV {
// "D:/Rust/jarvis-app/public"
// } else {
// "./public"
// };
pub const COMMANDS_PATH: &str = "commands/";
pub const DB_FILE_NAME: &str = "app.db";
pub const APP_VERSION: Option<&str> = option_env!("CARGO_PKG_VERSION");
pub const AUTHOR_NAME: Option<&str> = option_env!("CARGO_PKG_AUTHORS");
pub const REPOSITORY_LINK: Option<&str> = option_env!("CARGO_PKG_REPOSITORY");
// pub const VOSK_MODEL_PATH: &str = const_concat!(PUBLIC_PATH, "/vosk/model_small");
pub const VOSK_MODEL_PATH: &str = "vosk/model_small";
pub const CMD_RATIO_THRESHOLD: f64 = 60f64;
pub const CMS_WAIT_DELAY: std::time::Duration = std::time::Duration::from_secs(10);
pub const ASSISTANT_GREET_PHRASES: [&str; 3] = ["greet1", "greet2", "greet3"];
pub const ASSISTANT_PHRASES_TBR: [&str; 16] = [
"сэр",
"слушаю сэр",
"всегда к услугам",
"произнеси",
"ответь",
"покажи",
"скажи",
"давай",
"да сэр",
"к вашим услугам сэр",
"всегда к вашим услугам сэр",
"запрос выполнен сэр",
"выполнен сэр",
"есть",
"загружаю сэр",
"очень тонкое замечание сэр",
];

40
src-tauri/src/events.rs Normal file
View File

@@ -0,0 +1,40 @@
use tauri::Manager;
// the payload type must implement `Serialize` and `Clone`.
#[derive(Clone, serde::Serialize)]
pub struct Payload {
pub data: String,
}
pub enum EventTypes {
AudioPlay,
AssistantWaiting,
AssistantGreet,
CommandStart,
CommandInProcess,
CommandEnd,
}
impl EventTypes {
pub fn get(&self) -> &str {
match self {
Self::AudioPlay => "audio-play",
Self::AssistantWaiting => "assistant-waiting",
Self::AssistantGreet => "assistant-greet",
Self::CommandStart => "command-start",
Self::CommandInProcess => "command-in-process",
Self::CommandEnd => "command-end",
}
}
}
pub fn play(phrase: &str, app_handle: &tauri::AppHandle) {
app_handle
.emit_all(
EventTypes::AudioPlay.get(),
Payload {
data: phrase.into(),
},
)
.unwrap();
}

71
src-tauri/src/lib.rs Normal file
View File

@@ -0,0 +1,71 @@
// Taken from https://github.com/Vurich/const-concat/issues/13
#![no_std]
use core::mem::ManuallyDrop;
const unsafe fn transmute_prefix<From, To>(from: From) -> To {
union Transmute<From, To> {
from: ManuallyDrop<From>,
to: ManuallyDrop<To>,
}
ManuallyDrop::into_inner(
Transmute {
from: ManuallyDrop::new(from),
}
.to,
)
}
/// # Safety
///
/// `Len1 + Len2 >= Len3`
#[doc(hidden)]
#[allow(non_upper_case_globals)]
pub const unsafe fn concat<const Len1: usize, const Len2: usize, const Len3: usize>(
arr1: [u8; Len1],
arr2: [u8; Len2],
) -> [u8; Len3] {
#[repr(C)]
struct Concat<A, B>(A, B);
transmute_prefix(Concat(arr1, arr2))
}
#[macro_export]
macro_rules! const_concat {
() => ("");
($a:expr) => ($a);
($a:expr, $b:expr $(,)?) => {{
const A: &str = $a;
const B: &str = $b;
const BYTES: [u8; { A.len() + B.len() }] = unsafe {
$crate::concat::<
{ A.len() },
{ B.len() },
{ A.len() + B.len() }
>(
*A.as_ptr().cast(),
*B.as_ptr().cast(),
)
};
unsafe { ::core::str::from_utf8_unchecked(&BYTES) }
}};
($a:expr, $b:expr, $($rest:expr),+ $(,)?) => {{
const TAIL: &str = $crate::const_concat!($b, $($rest),+);
$crate::const_concat!($a, TAIL)
}}
}
#[test]
fn tests() {
const SALUTATION: &str = "Hello";
const TARGET: &str = "world";
const GREETING: &str = const_concat!(SALUTATION, ", ", TARGET, "!");
const GREETING_TRAILING_COMMA: &str = const_concat!(SALUTATION, ", ", TARGET, "!",);
assert_eq!(GREETING, "Hello, world!");
assert_eq!(GREETING_TRAILING_COMMA, "Hello, world!");
}

90
src-tauri/src/main.rs Normal file
View File

@@ -0,0 +1,90 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
// #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
#[macro_use]
extern crate lazy_static; // better switch to once_cell ?
use pickledb::{PickleDb, PickleDbDumpPolicy, SerializationMethod};
use std::sync::Mutex;
// expose the config
mod config;
use config::*;
// include tauri commands
mod tauri_commands;
// include assistant commands
mod assistant_commands;
use assistant_commands::AssistantCommand;
// include vosk
mod vosk;
// include events
mod events;
// app dir
lazy_static! {
static ref APP_CONFIG_DIR: Mutex<String> = Mutex::new(String::new());
}
// init PickleDb connection
lazy_static! {
static ref DB: Mutex<PickleDb> = Mutex::new(
PickleDb::load(
format!("{}/{}", APP_CONFIG_DIR.lock().unwrap(), DB_FILE_NAME),
PickleDbDumpPolicy::AutoDump,
SerializationMethod::Json
)
.unwrap_or_else(|_x: _| {
println!("Creating new db file at {} ...", format!("{}/{}", APP_CONFIG_DIR.lock().unwrap(), DB_FILE_NAME));
PickleDb::new(
format!("{}/{}", APP_CONFIG_DIR.lock().unwrap(), DB_FILE_NAME),
PickleDbDumpPolicy::AutoDump,
SerializationMethod::Json,
)
})
);
}
// init commands
lazy_static! {
static ref COMMANDS: Vec<AssistantCommand> = assistant_commands::parse_commands().unwrap();
}
fn main() {
vosk::init_vosk();
tauri::Builder::default()
.setup(|app| {
std::fs::create_dir_all(app.path_resolver().app_config_dir().unwrap())?;
APP_CONFIG_DIR.lock().unwrap().push_str(app.path_resolver().app_config_dir().unwrap().to_str().unwrap());
Ok(())
})
.invoke_handler(tauri::generate_handler![
// db commands
tauri_commands::db_read,
tauri_commands::db_write,
// recorder commands
tauri_commands::pv_get_audio_devices,
tauri_commands::pv_get_audio_device_name,
// listener commands
tauri_commands::start_listening,
tauri_commands::stop_listening,
tauri_commands::is_listening,
// sys commands
tauri_commands::get_current_ram_usage,
tauri_commands::get_peak_ram_usage,
tauri_commands::get_cpu_temp,
tauri_commands::get_cpu_usage,
// sound commands
tauri_commands::play_sound,
// etc commands
tauri_commands::get_app_version,
tauri_commands::get_author_name,
tauri_commands::get_repository_link
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

View File

@@ -0,0 +1,23 @@
// import DB related commands
mod db;
pub use db::*;
// import RECORDER commands
mod recorder;
pub use recorder::*;
// import PORCUPINE commands
mod listener;
pub use listener::*;
// import SYS commands
mod sys;
pub use sys::*;
// import VOICE commands
mod voice;
pub use voice::*;
// import ETC commands
mod etc;
pub use etc::*;

View File

@@ -0,0 +1,19 @@
use crate::DB;
#[tauri::command]
pub fn db_read(key: &str) -> String {
if let Some(value) = DB.lock().unwrap().get(key) {
value
} else {
String::from("")
}
}
#[tauri::command]
pub fn db_write(key: &str, val: &str) -> bool {
if let Ok(_) = DB.lock().unwrap().set(key, &val) {
true
} else {
false
}
}

View File

@@ -0,0 +1,32 @@
use crate::config::APP_VERSION;
use crate::config::AUTHOR_NAME;
use crate::config::REPOSITORY_LINK;
// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
#[tauri::command]
pub fn get_app_version() -> String {
if let Some(ver) = APP_VERSION {
ver.to_string()
} else {
String::from("error")
}
}
#[tauri::command]
pub fn get_author_name() -> String {
if let Some(ver) = AUTHOR_NAME {
ver.to_string()
} else {
String::from("error")
}
}
#[tauri::command]
pub fn get_repository_link() -> String {
if let Some(ver) = REPOSITORY_LINK {
ver.to_string()
} else {
String::from("error")
}
}

View File

@@ -0,0 +1,193 @@
use porcupine::{BuiltinKeywords, Porcupine, PorcupineBuilder};
use pv_recorder::RecorderBuilder;
use std::sync::atomic::{AtomicBool, Ordering};
use crate::events::Payload;
use tauri::Manager;
use rand::seq::SliceRandom;
use std::time::SystemTime;
use crate::assistant_commands;
use crate::events;
use crate::config;
use crate::vosk;
use crate::COMMANDS;
use crate::DB;
// track listening state
static LISTENING: AtomicBool = AtomicBool::new(false);
// stop listening with Atomic flag (to make it work between different threads)
static STOP_LISTENING: AtomicBool = AtomicBool::new(false);
#[tauri::command]
pub fn is_listening() -> bool {
LISTENING.load(Ordering::SeqCst)
}
#[tauri::command]
pub fn stop_listening() {
if is_listening() {
STOP_LISTENING.store(true, Ordering::SeqCst);
}
// wait until listening stops
while is_listening() {}
}
#[tauri::command(async)]
pub fn start_listening(app_handle: tauri::AppHandle) -> Result<bool, String> {
// only one listener thread is allowed
if is_listening() {
return Err("Already listening.".into());
}
// vars
let porcupine: Porcupine;
let picovoice_api_key: String;
let selected_microphone: i32;
let mut start = SystemTime::now();
// Retrieve API key from DB
if let Some(pkey) = DB.lock().unwrap().get::<String>("api_key__picovoice") {
picovoice_api_key = pkey;
} else {
return Err("Picovoice API key is not set!".into());
}
// Create instance of Porcupine with the given API key
if let Ok(pinstance) =
PorcupineBuilder::new_with_keywords(picovoice_api_key, &[BuiltinKeywords::Jarvis])
.sensitivities(&[1.0f32]) // max sensitivity possible
.init()
{
// porcupine successfully initialized with the valid API key
porcupine = pinstance;
} else {
// something went wrong
return Err(
"Porcupine error: either API key is not valid or there is no internet connection"
.into(),
);
}
// Retrieve microphone index
if let Some(smic) = DB.lock().unwrap().get::<String>("selected_microphone") {
selected_microphone = smic.parse().unwrap_or(-1);
} else {
selected_microphone = -1; // use default, if not selected
}
// Create recorder instance
let recorder = RecorderBuilder::new()
.device_index(selected_microphone)
.frame_length(porcupine.frame_length() as i32)
.init()
.expect("Failed to initialize pvrecorder");
// Start recording
println!("Listening (microphone idx = {selected_microphone}) ...");
recorder.start().expect("Failed to start audio recording");
LISTENING.store(true, Ordering::SeqCst);
// Greet user
events::play("run", &app_handle);
// Listen until stop flag will be true
let mut frame_buffer = vec![0; porcupine.frame_length() as usize];
while !STOP_LISTENING.load(Ordering::SeqCst) {
recorder
.read(&mut frame_buffer)
.expect("Failed to read audio frame");
if let Ok(keyword_index) = porcupine.process(&frame_buffer) {
if keyword_index >= 0 {
println!("Yes, sir! {}", keyword_index);
events::play(
config::ASSISTANT_GREET_PHRASES
.choose(&mut rand::thread_rng())
.unwrap(),
&app_handle,
);
start = SystemTime::now();
app_handle
.emit_all(events::EventTypes::AssistantGreet.get(), ())
.unwrap();
loop {
recorder
.read(&mut frame_buffer)
.expect("Failed to read audio frame");
// vosk part (partials included)
if let Some(mut test) = vosk::recognize(&frame_buffer) {
if !test.is_empty() {
println!("Recognized: {}", test);
// some filtration
test = test.to_lowercase();
for tbr in config::ASSISTANT_PHRASES_TBR {
test = test.replace(tbr, "");
}
// infer command
if let Some((cmd_path, cmd_config)) =
assistant_commands::fetch_command(&test, &COMMANDS)
{
println!("Recognized (filtered): {}", test);
println!("Command found: {:?}", cmd_path);
println!("Executing ...");
let cmd_result = assistant_commands::execute_command(
&cmd_path,
&cmd_config,
&app_handle,
);
match cmd_result {
Ok(_) => {
println!("Command executed successfully!");
start = SystemTime::now(); // listen for more commands
continue;
}
Err(error_message) => {
println!("Error executing command: {}", error_message);
}
}
app_handle
.emit_all(events::EventTypes::AssistantWaiting.get(), ())
.unwrap();
break; // return to picovoice after command execution (no matter successfull or not)
}
}
}
match start.elapsed() {
Ok(elapsed) if elapsed > config::CMS_WAIT_DELAY => {
// return to picovoice after N seconds
app_handle
.emit_all(events::EventTypes::AssistantWaiting.get(), ())
.unwrap();
break;
}
_ => (),
}
}
}
}
}
// Stop listening
println!("Stop listening ...");
recorder.stop().expect("Failed to stop audio recording");
LISTENING.store(false, Ordering::SeqCst);
STOP_LISTENING.store(false, Ordering::SeqCst);
Ok(true)
}

View File

@@ -0,0 +1,33 @@
use pv_recorder::RecorderBuilder;
#[tauri::command]
pub fn pv_get_audio_devices() -> Vec<String> {
let audio_devices = RecorderBuilder::default().get_audio_devices();
match audio_devices {
Ok(audio_devices) => audio_devices,
Err(err) => panic!("Failed to get audio devices: {}", err),
}
}
#[tauri::command]
pub fn pv_get_audio_device_name(idx: i32) -> String {
let audio_devices = RecorderBuilder::default().get_audio_devices();
let mut first_device: String = String::new();
match audio_devices {
Ok(audio_devices) => {
for (_idx, device) in audio_devices.iter().enumerate() {
if idx as usize == _idx {
return device.to_string();
}
if _idx == 0 {
first_device = device.to_string()
}
}
}
Err(err) => panic!("Failed to get audio devices: {}", err),
};
// return first device as default, if none were matched
first_device
}

View File

@@ -0,0 +1,48 @@
use peak_alloc::PeakAlloc;
#[global_allocator]
static PEAK_ALLOC: PeakAlloc = PeakAlloc;
extern crate systemstat;
use std::thread;
use std::time::Duration;
use systemstat::{saturating_sub_bytes, Platform, System};
lazy_static! {
static ref SYS: System = System::new();
}
#[tauri::command]
pub fn get_current_ram_usage() -> String {
let result = String::from(format!("{}", PEAK_ALLOC.current_usage_as_mb()));
result
}
#[tauri::command]
pub fn get_peak_ram_usage() -> String {
let result = String::from(format!("{}", PEAK_ALLOC.peak_usage_as_gb()));
result
}
#[tauri::command]
pub fn get_cpu_temp() -> String {
if let Ok(cpu_temp) = SYS.cpu_temp() {
String::from(format!("{}", cpu_temp))
} else {
String::from("error")
}
}
// https://github.com/valpackett/systemstat/blob/trunk/examples/info.rs
#[tauri::command(async)]
pub async fn get_cpu_usage() -> String {
if let Ok(cpu) = SYS.cpu_load_aggregate() {
thread::sleep(Duration::from_secs(1));
let cpu = cpu.done().unwrap();
String::from(format!("{}", cpu.user * 100.0))
} else {
String::from("error")
}
}

View File

@@ -0,0 +1,29 @@
use std::fs::File;
use std::io::BufReader;
use rodio::{Decoder, OutputStream, Sink};
#[tauri::command(async)]
pub fn play_sound(filename: &str, sleep: bool) {
// Get a output stream handle to the default physical sound device
let (_stream, stream_handle) = OutputStream::try_default().unwrap();
let sink = Sink::try_new(&stream_handle).unwrap();
// Load a sound from a file, using a path relative to Cargo.toml
// let filepath = format!("{PUBLIC_PATH}/sound/{filename}.wav");
let filepath = filename;
let file = BufReader::new(File::open(&filepath).unwrap());
// Decode that sound file into a source
let source = Decoder::new(file).unwrap();
// Play the sound directly on the device
println!("Playing {} ...", filepath);
// stream_handle.play_raw(source.convert_samples());
sink.append(source);
if sleep {
// The sound plays in a separate thread. This call will block the current thread until the sink
// has finished playing all its queued sounds.
sink.sleep_until_end();
}
}

58
src-tauri/src/vosk.rs Normal file
View File

@@ -0,0 +1,58 @@
use std::sync::Mutex;
use vosk::{CompleteResult, DecodingState, Model, Recognizer};
use crate::config::VOSK_MODEL_PATH;
lazy_static! {
static ref MODEL: Model = Model::new(VOSK_MODEL_PATH).unwrap();
}
lazy_static! {
static ref RECOGNIZER: Mutex<Recognizer> =
Mutex::new(Recognizer::new(&MODEL, 16000.0).unwrap());
}
pub fn init_vosk() {
RECOGNIZER.lock().unwrap().set_max_alternatives(10);
RECOGNIZER.lock().unwrap().set_words(true);
RECOGNIZER.lock().unwrap().set_partial_words(true);
}
pub fn recognize(data: &[i16]) -> Option<String> {
let state = RECOGNIZER.lock().unwrap().accept_waveform(data);
match state {
DecodingState::Running => {
None
// Some(RECOGNIZER.lock().unwrap().partial_result().partial.into())
}
DecodingState::Finalized => {
// Result will always be multiple because we called set_max_alternatives
Some(
RECOGNIZER
.lock()
.unwrap()
.result()
.multiple()
.unwrap()
.alternatives
.first()
.unwrap()
.text
.into(),
)
}
DecodingState::Failed => None,
}
}
pub fn stereo_to_mono(input_data: &[i16]) -> Vec<i16> {
let mut result = Vec::with_capacity(input_data.len() / 2);
result.extend(
input_data
.chunks_exact(2)
.map(|chunk| chunk[0] / 2 + chunk[1] / 2),
);
result
}