From 2ef4e86859d3b88bdf50cc0b48ec564a1a6d6ab7 Mon Sep 17 00:00:00 2001 From: Kodai Aoyama Date: Tue, 14 Mar 2023 01:31:46 +0900 Subject: [PATCH 1/4] add state for header audio settings --- src-tauri/Cargo.toml | 2 +- src-tauri/migrations/001.sql | 7 +++++ src/components/molecules/SpeakerLanguage.tsx | 2 +- .../molecules/TranscriptionAccuracy.tsx | 4 +-- src/lib/sqlite.ts | 12 ++++++++ src/store/atoms/speakerLanguageState.ts | 29 +++++++++++++++++-- src/store/atoms/speechHistoryAtom.ts | 2 +- src/store/atoms/transcriptionAccuracyState.ts | 29 +++++++++++++++++-- 8 files changed, 78 insertions(+), 9 deletions(-) diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 63e894a..a6a4523 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -7,7 +7,7 @@ license = "MIT" repository = "https://github.com/solaoi/lycoris" default-run = "lycoris" edition = "2021" -rust-version = "1.67" +rust-version = "1.68" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [build-dependencies] diff --git a/src-tauri/migrations/001.sql b/src-tauri/migrations/001.sql index ceecfd3..f75e0ff 100644 --- a/src-tauri/migrations/001.sql +++ b/src-tauri/migrations/001.sql @@ -6,6 +6,13 @@ CREATE TABLE speeches ( wav TEXT, model TEXT ); +CREATE TABLE settings ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + setting_name TEXT, + setting_status TEXT +); +INSERT INTO settings(setting_name, setting_status) VALUES("speakerLanguage", NULL); +INSERT INTO settings(setting_name, setting_status) VALUES("transcriptionAccuracy", "off"); CREATE TABLE models ( id INTEGER PRIMARY KEY AUTOINCREMENT, model_name TEXT, diff --git a/src/components/molecules/SpeakerLanguage.tsx b/src/components/molecules/SpeakerLanguage.tsx index 27e04fe..caa351f 100644 --- a/src/components/molecules/SpeakerLanguage.tsx +++ b/src/components/molecules/SpeakerLanguage.tsx @@ -31,7 +31,7 @@ const SpeakerLanguage = (): JSX.Element => { ) diff --git a/src/components/molecules/TranscriptionAccuracy.tsx b/src/components/molecules/TranscriptionAccuracy.tsx index 46cc546..e066c41 100644 --- a/src/components/molecules/TranscriptionAccuracy.tsx +++ b/src/components/molecules/TranscriptionAccuracy.tsx @@ -15,7 +15,7 @@ const TranscriptionAccuracy = (): JSX.Element => { const mapModel = (model: string) => { switch (model) { case "small": - return "追っかけ:低"; + return "追っかけ:小"; case "medium": return "追っかけ:中"; case "large": @@ -30,7 +30,7 @@ const TranscriptionAccuracy = (): JSX.Element => { {downloadedModels?.map((model, i) => ( - + ))} ) diff --git a/src/lib/sqlite.ts b/src/lib/sqlite.ts index 26fe9f2..8ee7394 100644 --- a/src/lib/sqlite.ts +++ b/src/lib/sqlite.ts @@ -34,6 +34,18 @@ export default class DB { return await this.db.select('SELECT model_name FROM models WHERE is_downloaded = 1 AND model_type = $1', [model_type]) } + public async loadSetting(setting_name: string): Promise<{setting_status: string}> { + const settings:{setting_status: string}[] = await this.db.select('SELECT setting_status FROM settings WHERE setting_name = $1', [setting_name]) + return settings[0]; + } + + public async updateSetting(setting_name: string, setting_status: string|null) { + await this.db.execute( + 'UPDATE settings SET setting_status = $2 WHERE setting_name = $1', + [setting_name, setting_status] + ); + } + public async saveSpeech(speech: SpeechHistoryType): Promise { const { lastInsertId } = await this.db.execute( 'INSERT INTO speeches(speech_type, unix_time, content, wav, model) VALUES($1, $2, $3, $4, $5)', diff --git a/src/store/atoms/speakerLanguageState.ts b/src/store/atoms/speakerLanguageState.ts index ec7b083..31f606d 100644 --- a/src/store/atoms/speakerLanguageState.ts +++ b/src/store/atoms/speakerLanguageState.ts @@ -1,6 +1,31 @@ -import { atom } from 'recoil' +import { atom, AtomEffect } from 'recoil' +import DB from '../../lib/sqlite'; + +const sqliteEffect: AtomEffect = ({setSelf, onSet, trigger}) => { + const loadPersisted = async () => { + const db = (await DB.getInstance()) + const savedValue = await db.loadSetting("speakerLanguage"); + setSelf(savedValue.setting_status); + }; + + if (trigger === 'get') { + loadPersisted(); + } + + onSet(async(newValue, _, isReset:any) => { + const db = await DB.getInstance() + if (isReset) { + await db.updateSetting("speakerLanguage", null) + } else { + await db.updateSetting("speakerLanguage", newValue) + } + }); +}; export const speakerLanguageState = atom({ key: 'speakerLanguageState', - default: null + default: null, + effects: [ + sqliteEffect, + ] }) \ No newline at end of file diff --git a/src/store/atoms/speechHistoryAtom.ts b/src/store/atoms/speechHistoryAtom.ts index ee3fe74..0002667 100644 --- a/src/store/atoms/speechHistoryAtom.ts +++ b/src/store/atoms/speechHistoryAtom.ts @@ -24,7 +24,7 @@ const sqliteEffect: AtomEffect = ({setSelf, onSet, trigger} if (old.length !== newValue.length) { const current = newValue[newValue.length - 1]; if (current.speech_type === "memo"){ - db.saveSpeech(current); + await db.saveSpeech(current); } } } diff --git a/src/store/atoms/transcriptionAccuracyState.ts b/src/store/atoms/transcriptionAccuracyState.ts index c6fbad2..1c293f2 100644 --- a/src/store/atoms/transcriptionAccuracyState.ts +++ b/src/store/atoms/transcriptionAccuracyState.ts @@ -1,6 +1,31 @@ -import { atom } from 'recoil' +import { atom, AtomEffect } from 'recoil' +import DB from '../../lib/sqlite'; + +const sqliteEffect: AtomEffect = ({setSelf, onSet, trigger}) => { + const loadPersisted = async () => { + const db = (await DB.getInstance()) + const savedValue = await db.loadSetting("transcriptionAccuracy"); + setSelf(savedValue.setting_status); + }; + + if (trigger === 'get') { + loadPersisted(); + } + + onSet(async(newValue, _, isReset:any) => { + const db = await DB.getInstance() + if (isReset) { + await db.updateSetting("transcriptionAccuracy", "off") + } else { + await db.updateSetting("transcriptionAccuracy", newValue) + } + }); +}; export const transcriptionAccuracyState = atom({ key: 'transcriptionAccuracyState', - default: "off" + default: "off", + effects: [ + sqliteEffect, + ] }) \ No newline at end of file From 256acf282c2d5054e291b1bfdcdce5a3678e25d7 Mon Sep 17 00:00:00 2001 From: Kodai Aoyama Date: Fri, 24 Mar 2023 02:55:21 +0900 Subject: [PATCH 2/4] add note feature --- src-tauri/Cargo.lock | 96 +++++++---- src-tauri/Cargo.toml | 3 + src-tauri/migrations/001.sql | 15 +- src-tauri/src/main.rs | 12 ++ src-tauri/src/module/deleter.rs | 38 +++++ src-tauri/src/module/mod.rs | 3 +- src-tauri/src/module/record.rs | 4 + src-tauri/src/module/sqlite.rs | 59 +++++-- src-tauri/src/module/transcription.rs | 7 +- src/assets/Square142x142Logo.png | Bin 0 -> 18857 bytes src/assets/react.svg | 1 - src/components/Header.tsx | 16 +- src/components/Main.tsx | 5 +- src/components/SideMenu.tsx | 85 ++++++---- src/components/molecules/AudioDevice.tsx | 8 +- src/components/molecules/MemoFilterButton.tsx | 2 +- src/components/molecules/NoteDeleteButton.tsx | 34 ++++ .../molecules/RecordStartButton.tsx | 11 +- src/components/molecules/RecordStopButton.tsx | 7 +- src/components/molecules/SpeakerLanguage.tsx | 4 +- src/components/molecules/Speech.tsx | 3 +- src/components/molecules/SpeechHistory.tsx | 6 +- .../molecules/TranscriptionAccuracy.tsx | 14 +- src/components/organisms/LogoMain.tsx | 19 +++ src/components/organisms/NoteFooter.tsx | 14 +- src/components/organisms/NoteMain.tsx | 158 ++++++++++-------- src/components/organisms/SettingsMain.tsx | 6 +- src/components/organisms/SideMenuColumn.tsx | 50 ++++++ src/index.css | 28 +++- src/lib/sqlite.ts | 48 ++++-- src/store/atoms/notesState.ts | 51 ++++++ src/store/atoms/recordingNoteState.ts | 6 + src/store/atoms/selectedNoteState.ts | 11 ++ src/store/atoms/speakerLanguageState.ts | 6 +- src/store/atoms/speechHistoryAtom.ts | 40 ----- src/store/atoms/speechHistoryState.ts | 43 +++++ src/store/atoms/transcriptionAccuracyState.ts | 6 +- src/type/DeleteCompletion.type.ts | 4 + src/type/Note.type.ts | 6 + src/type/SpeechHistory.type.ts | 6 +- tailwind.config.cjs | 6 + 41 files changed, 708 insertions(+), 233 deletions(-) create mode 100644 src-tauri/src/module/deleter.rs create mode 100644 src/assets/Square142x142Logo.png delete mode 100644 src/assets/react.svg create mode 100644 src/components/molecules/NoteDeleteButton.tsx create mode 100644 src/components/organisms/LogoMain.tsx create mode 100644 src/components/organisms/SideMenuColumn.tsx create mode 100644 src/store/atoms/notesState.ts create mode 100644 src/store/atoms/recordingNoteState.ts create mode 100644 src/store/atoms/selectedNoteState.ts delete mode 100644 src/store/atoms/speechHistoryAtom.ts create mode 100644 src/store/atoms/speechHistoryState.ts create mode 100644 src/type/DeleteCompletion.type.ts create mode 100644 src/type/Note.type.ts diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index de6bb2a..268113d 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -56,7 +56,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5915f52fe2cf65e83924d037b6c5290b7cee097c6b5c8700746e6168a343fd6b" dependencies = [ "alsa-sys", - "bitflags", + "bitflags 1.3.2", "libc", "nix", ] @@ -102,7 +102,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c3d816ce6f0e2909a96830d6911c2aff044370b1ef92d7f267b43bae5addedd" dependencies = [ "atk-sys", - "bitflags", + "bitflags 1.3.2", "glib", "libc", ] @@ -162,13 +162,22 @@ version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bindgen" version = "0.59.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bd2a9a458e8f4304c52c43ebb0cfbd520289f8379a52e329a38afda99bf8eb8" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cexpr", "clang-sys", "lazy_static", @@ -187,7 +196,7 @@ version = "0.64.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4243e6031260db77ede97ad86c27e501d646a27ab57b59a574f725d98ab1fb4" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cexpr", "clang-sys", "lazy_static", @@ -209,6 +218,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5dd14596c0e5b954530d0e6f1fd99b89c03e313aa2086e8da4303701a09e1cf" + [[package]] name = "block" version = "0.1.6" @@ -284,7 +299,7 @@ version = "0.15.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c76ee391b03d35510d9fa917357c7f1855bd9a6659c95a1b392e33f49b3369bc" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cairo-sys-rs", "glib", "libc", @@ -402,7 +417,7 @@ version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f63902e9223530efb4e26ccd0cf55ec30d592d3b42e21a28defc42a9586e832" dependencies = [ - "bitflags", + "bitflags 1.3.2", "block", "cocoa-foundation", "core-foundation", @@ -418,7 +433,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ade49b65d560ca58c403a479bb396592b155c0185eada742ee323d1d68d6318" dependencies = [ - "bitflags", + "bitflags 1.3.2", "block", "core-foundation", "core-graphics-types", @@ -481,7 +496,7 @@ version = "0.22.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" dependencies = [ - "bitflags", + "bitflags 1.3.2", "core-foundation", "core-graphics-types", "foreign-types", @@ -494,7 +509,7 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a68b68b3446082644c91ac778bf50cd4104bfb002b5a6a7c44cca5a2c70788b" dependencies = [ - "bitflags", + "bitflags 1.3.2", "core-foundation", "foreign-types", "libc", @@ -506,7 +521,7 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11894b20ebfe1ff903cbdc52259693389eea03b94918a2def2c30c3bf227ad88" dependencies = [ - "bitflags", + "bitflags 1.3.2", "coreaudio-sys", ] @@ -1187,7 +1202,7 @@ version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6e05c1f572ab0e1f15be94217f0dc29088c248b14f792a5ff0af0d84bcda9e8" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cairo-rs", "gdk-pixbuf", "gdk-sys", @@ -1203,7 +1218,7 @@ version = "0.15.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad38dd9cc8b099cceecdf41375bb6d481b1b5a7cd5cd603e10a69a9383f8619a" dependencies = [ - "bitflags", + "bitflags 1.3.2", "gdk-pixbuf-sys", "gio", "glib", @@ -1304,7 +1319,7 @@ version = "0.15.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68fdbc90312d462781a395f7a16d96a2b379bb6ef8cd6310a2df272771c4283b" dependencies = [ - "bitflags", + "bitflags 1.3.2", "futures-channel", "futures-core", "futures-io", @@ -1334,7 +1349,7 @@ version = "0.15.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edb0306fbad0ab5428b0ca674a23893db909a98582969c9b537be4ced78c505d" dependencies = [ - "bitflags", + "bitflags 1.3.2", "futures-channel", "futures-core", "futures-executor", @@ -1410,7 +1425,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92e3004a2d5d6d8b5057d2b57b3712c9529b62e82c77f25c1fecde1fd5c23bd0" dependencies = [ "atk", - "bitflags", + "bitflags 1.3.2", "cairo-rs", "field-offset", "futures-channel", @@ -1786,7 +1801,7 @@ version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf053e7843f2812ff03ef5afe34bb9c06ffee120385caad4f6b9967fcd37d41c" dependencies = [ - "bitflags", + "bitflags 1.3.2", "glib", "javascriptcore-rs-sys", ] @@ -2006,6 +2021,7 @@ dependencies = [ "tauri", "tauri-build", "tauri-plugin-sql", + "tauri-plugin-window-state", "tokio", "unicode-segmentation", "urlencoding", @@ -2151,7 +2167,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2032c77e030ddee34a6787a64166008da93f6a352b629261d0fee232b8742dd4" dependencies = [ - "bitflags", + "bitflags 1.3.2", "jni-sys", "ndk-sys 0.3.0", "num_enum", @@ -2164,7 +2180,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "451422b7e4718271c8b5b3aadf5adedba43dc76312454b387e98fae0fc951aa0" dependencies = [ - "bitflags", + "bitflags 1.3.2", "jni-sys", "ndk-sys 0.4.0", "num_enum", @@ -2208,7 +2224,7 @@ version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f866317acbd3a240710c63f065ffb1e4fd466259045ccb504130b7f668f35c6" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cc", "cfg-if", "libc", @@ -2407,7 +2423,7 @@ version = "0.10.41" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "618febf65336490dfcf20b73f885f5651a0c89c64c2d4a8c3662585a70bf5bd0" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cfg-if", "foreign-types", "libc", @@ -2473,7 +2489,7 @@ version = "0.15.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22e4045548659aee5313bde6c582b0d83a627b7904dd20dc2d9ef0895d414e4f" dependencies = [ - "bitflags", + "bitflags 1.3.2", "glib", "libc", "once_cell", @@ -2730,7 +2746,7 @@ version = "0.17.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc38c0ad57efb786dd57b9864e5b18bae478c00c824dc55a38bbc9da95dde3ba" dependencies = [ - "bitflags", + "bitflags 1.3.2", "crc32fast", "deflate", "miniz_oxide", @@ -2903,7 +2919,7 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] @@ -3036,7 +3052,7 @@ version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85127183a999f7db96d1a976a309eebbfb6ea3b0b400ddd8340190129de6eb7a" dependencies = [ - "bitflags", + "bitflags 1.3.2", "fallible-iterator", "fallible-streaming-iterator", "hashlink 0.7.0", @@ -3170,7 +3186,7 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc" dependencies = [ - "bitflags", + "bitflags 1.3.2", "core-foundation", "core-foundation-sys", "libc", @@ -3193,7 +3209,7 @@ version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df320f1889ac4ba6bc0cdc9c9af7af4bd64bb927bccdf32d81140dc1f9be12fe" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cssparser", "derive_more", "fxhash", @@ -3424,7 +3440,7 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2b4d76501d8ba387cf0fefbe055c3e0a59891d09f0f995ae4e4b16f6b60f3c0" dependencies = [ - "bitflags", + "bitflags 1.3.2", "gio", "glib", "libc", @@ -3438,7 +3454,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "009ef427103fcb17f802871647a7fa6c60cbb654b4c4e4c0ac60a31c5f6dc9cf" dependencies = [ - "bitflags", + "bitflags 1.3.2", "gio-sys", "glib-sys", "gobject-sys", @@ -3490,7 +3506,7 @@ checksum = "dcbc16ddba161afc99e14d1713a453747a2b07fc097d2009f4c300ec99286105" dependencies = [ "ahash", "atoi", - "bitflags", + "bitflags 1.3.2", "byteorder", "bytes", "crc", @@ -3689,7 +3705,7 @@ version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42c460173627564bde252ca5ebf346ba5b37c5cee1a445782bacc8e9b8d38b5e" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cairo-rs", "cc", "cocoa", @@ -3852,7 +3868,7 @@ dependencies = [ [[package]] name = "tauri-plugin-sql" version = "0.1.0" -source = "git+https://github.com/tauri-apps/plugins-workspace?branch=dev#b49d8addda57e7a111a7b43136b7a4943bcd803a" +source = "git+https://github.com/tauri-apps/plugins-workspace?branch=dev#9b70a79b2cc05bfd671f2317c2de85b77c6773a3" dependencies = [ "futures-core", "log", @@ -3865,6 +3881,20 @@ dependencies = [ "tokio", ] +[[package]] +name = "tauri-plugin-window-state" +version = "0.1.0" +source = "git+https://github.com/tauri-apps/plugins-workspace?branch=dev#9b70a79b2cc05bfd671f2317c2de85b77c6773a3" +dependencies = [ + "bincode", + "bitflags 2.0.1", + "log", + "serde", + "serde_json", + "tauri", + "thiserror", +] + [[package]] name = "tauri-runtime" version = "0.12.1" @@ -4492,7 +4522,7 @@ version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8f859735e4a452aeb28c6c56a852967a8a76c8eb1cc32dbf931ad28a13d6370" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cairo-rs", "gdk", "gdk-sys", @@ -4517,7 +4547,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d76ca6ecc47aeba01ec61e480139dda143796abcae6f83bcddf50d6b5b1dcf3" dependencies = [ "atk-sys", - "bitflags", + "bitflags 1.3.2", "cairo-sys-rs", "gdk-pixbuf-sys", "gdk-sys", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index a6a4523..c9838bc 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -38,6 +38,9 @@ urlencoding = "2.1.0" git = "https://github.com/tauri-apps/plugins-workspace" branch = "dev" features = ["sqlite"] +[dependencies.tauri-plugin-window-state] +git = "https://github.com/tauri-apps/plugins-workspace" +branch = "dev" [features] # by default Tauri runs in production mode diff --git a/src-tauri/migrations/001.sql b/src-tauri/migrations/001.sql index f75e0ff..f9c2196 100644 --- a/src-tauri/migrations/001.sql +++ b/src-tauri/migrations/001.sql @@ -1,10 +1,19 @@ +CREATE TABLE notes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + note_title TEXT, + is_archived INTEGER DEFAULT 0, + created_at_unixtime INTEGER DEFAULT (CAST(strftime('%s','now') AS INTEGER)) +); CREATE TABLE speeches ( id INTEGER PRIMARY KEY AUTOINCREMENT, - speech_type TEXT, - unix_time INTEGER, + speech_type TEXT, -- speech|memo + created_at_unixtime INTEGER DEFAULT (CAST(strftime('%s','now') AS INTEGER)), content TEXT, wav TEXT, - model TEXT + model TEXT, -- manual|vosk|whisper + model_description TEXT, -- ja-0.22|small-ja-0.22|en-us-0.22|small-en-us-0.15|small|medium|large + note_id INTEGER NOT NULL, + FOREIGN KEY(note_id) REFERENCES notes(id) ); CREATE TABLE settings ( id INTEGER PRIMARY KEY AUTOINCREMENT, diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index f5d1f5c..a77cdbc 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -30,6 +30,14 @@ struct RecordState(Arc>>>); const BUNDLE_IDENTIFIER: &str = "blog.aota.Lycoris"; +#[tauri::command] +fn delete_note_command(window: tauri::Window, note_id: u64) { + std::thread::spawn(move || { + let deleter = module::deleter::NoteDeleter::new(window.app_handle().clone()); + deleter.delete(note_id) + }); +} + #[tauri::command] fn download_whisper_model_command(window: tauri::Window, model: String) { std::thread::spawn(move || { @@ -59,6 +67,7 @@ fn start_command( device_label: String, speaker_language: String, transcription_accuracy: String, + note_id: u64, ) { let mut lock = state.0.lock().unwrap(); let (stop_record_tx, stop_record_rx) = unbounded(); @@ -70,6 +79,7 @@ fn start_command( device_label, speaker_language, transcription_accuracy, + note_id, stop_record_rx, ); }); @@ -126,6 +136,7 @@ fn main() { } response.mimetype("audio/wav").status(status_code).body(buf) }) + .plugin(tauri_plugin_window_state::Builder::default().build()) .plugin( tauri_plugin_sql::Builder::default() .add_migrations( @@ -141,6 +152,7 @@ fn main() { ) .manage(RecordState(Default::default())) .invoke_handler(tauri::generate_handler![ + delete_note_command, download_whisper_model_command, download_vosk_model_command, list_devices_command, diff --git a/src-tauri/src/module/deleter.rs b/src-tauri/src/module/deleter.rs new file mode 100644 index 0000000..e640132 --- /dev/null +++ b/src-tauri/src/module/deleter.rs @@ -0,0 +1,38 @@ +use std::fs::remove_file; + +use crate::module::sqlite::Sqlite; +use tauri::{AppHandle, Manager}; + +#[derive(Debug, Clone, serde::Serialize)] +pub struct DeleteCompletion { + pub note_id: u64, + pub is_finished: bool, +} + +pub struct NoteDeleter { + app_handle: AppHandle, +} +impl NoteDeleter { + pub fn new(app_handle: AppHandle) -> Self { + Self { app_handle } + } + + pub fn delete(&self, note_id: u64) { + let sqlite = Sqlite::new(); + let speeches = sqlite.select_all_speeches_by(note_id).unwrap(); + let _ = speeches.iter().for_each(|speech| { + remove_file(&speech.wav).expect("File delete failed"); + }); + + let _ = sqlite.delete_speeches_by(note_id); + let _ = sqlite.delete_note(note_id); + + let _ = &self.app_handle.emit_all( + "deleteCompletion", + DeleteCompletion { + note_id, + is_finished: true, + }, + ); + } +} diff --git a/src-tauri/src/module/mod.rs b/src-tauri/src/module/mod.rs index d78662a..a0e701f 100644 --- a/src-tauri/src/module/mod.rs +++ b/src-tauri/src/module/mod.rs @@ -1,7 +1,8 @@ +pub mod deleter; pub mod device; pub mod downloader; -pub mod model_type_whisper; pub mod model_type_vosk; +pub mod model_type_whisper; mod recognizer; pub mod record; mod sqlite; diff --git a/src-tauri/src/module/record.rs b/src-tauri/src/module/record.rs index 654283a..4188556 100644 --- a/src-tauri/src/module/record.rs +++ b/src-tauri/src/module/record.rs @@ -45,6 +45,7 @@ impl Record { device_label: String, speaker_language: String, transcription_accuracy: String, + note_id: u64, stop_record_rx: Receiver<()>, ) { let host = cpal::default_host(); @@ -159,6 +160,8 @@ impl Record { text, path, "vosk".to_string(), + speaker_language.clone(), + note_id, ); app_handle @@ -188,6 +191,7 @@ impl Record { app_handle_clone, transcription_accuracy_clone, speaker_language_clone, + note_id ); transcription.start(stop_convert_rx_clone); let mut lock = is_converting_clone.lock().unwrap(); diff --git a/src-tauri/src/module/sqlite.rs b/src-tauri/src/module/sqlite.rs index d6ee6d0..e2ae4fc 100644 --- a/src-tauri/src/module/sqlite.rs +++ b/src-tauri/src/module/sqlite.rs @@ -11,10 +11,12 @@ pub struct Sqlite { pub struct Speech { pub id: u16, pub speech_type: String, - pub unix_time: u64, + pub created_at_unixtime: u64, pub content: String, pub wav: String, pub model: String, + pub model_description: String, + pub note_id: u64, } #[derive(Debug, Clone, serde::Serialize)] @@ -29,21 +31,44 @@ impl Sqlite { let db_path = data_dir.join(BUNDLE_IDENTIFIER).join("speeches.db"); let conn = Connection::open(&db_path).unwrap(); println!("{}", conn.is_autocommit()); + conn.pragma_update(None, "foreign_keys", true).unwrap(); Self { conn } } - pub fn select_vosk(&self) -> Result { + pub fn select_all_speeches_by(&self, note_id: u64) -> Result, rusqlite::Error> { + let mut stmt = self.conn.prepare("SELECT id,speech_type,created_at_unixtime,content,wav,model,model_description,note_id FROM speeches WHERE note_id = ?1").unwrap(); + let results = stmt + .query_map(params![note_id], |row| { + Ok(Speech { + id: row.get_unwrap(0), + speech_type: row.get_unwrap(1), + created_at_unixtime: row.get_unwrap(2), + content: row.get_unwrap(3), + wav: row.get_unwrap(4), + model: row.get_unwrap(5), + model_description: row.get_unwrap(6), + note_id: row.get_unwrap(7), + }) + }) + .unwrap() + .collect::, rusqlite::Error>>(); + results + } + + pub fn select_vosk(&self, note_id: u64) -> Result { return self.conn - .query_row("SELECT id,speech_type,unix_time,content,wav,model FROM speeches WHERE model = \"vosk\" ORDER BY unix_time ASC LIMIT 1", - [], + .query_row("SELECT id,speech_type,created_at_unixtime,content,wav,model,model_description,note_id FROM speeches WHERE model = \"vosk\" AND note_id = ?1 ORDER BY created_at_unixtime ASC LIMIT 1", + params![note_id], |row| { Ok(Speech { id: row.get_unwrap(0), speech_type: row.get_unwrap(1), - unix_time: row.get_unwrap(2), + created_at_unixtime: row.get_unwrap(2), content: row.get_unwrap(3), wav: row.get_unwrap(4), model: row.get_unwrap(5), + model_description: row.get_unwrap(6), + note_id: row.get_unwrap(7) }) }); } @@ -55,7 +80,7 @@ impl Sqlite { ) -> Result { if content == "" { match self.conn.execute( - "UPDATE speeches SET model = 'whisper-small' WHERE id = ?1", + "UPDATE speeches SET model = 'whisper' WHERE id = ?1", params![id], ) { Ok(_) => Ok(Updated { id, content }), @@ -63,7 +88,7 @@ impl Sqlite { } } else { match self.conn.execute( - "UPDATE speeches SET model = 'whisper-small', content = ?1 WHERE id = ?2", + "UPDATE speeches SET model = 'whisper', content = ?1 WHERE id = ?2", params![content, id], ) { Ok(_) => Ok(Updated { id, content }), @@ -86,17 +111,29 @@ impl Sqlite { pub fn save_speech( &self, speech_type: String, - unix_time: u64, + created_at_unixtime: u64, content: String, wav: String, model: String, + model_description: String, + note_id: u64, ) -> Result { match self.conn.execute( - "INSERT INTO speeches (speech_type, unix_time, content, wav, model) VALUES (?1, ?2, ?3, ?4, ?5)", - params![speech_type, unix_time, content, wav, model], + "INSERT INTO speeches (speech_type, created_at_unixtime, content, wav, model, model_description, note_id) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", + params![speech_type, created_at_unixtime, content, wav, model, model_description, note_id], ) { - Ok(_) => Ok(Speech { id: self.conn.last_insert_rowid() as u16, speech_type, unix_time, content, wav, model }), + Ok(_) => Ok(Speech { id: self.conn.last_insert_rowid() as u16, speech_type, created_at_unixtime, content, wav, model, model_description, note_id }), Err(err) => Err(err), } } + + pub fn delete_note(&self, note_id: u64) -> Result { + self.conn + .execute("DELETE FROM notes WHERE id = ?1", params![note_id]) + } + + pub fn delete_speeches_by(&self, note_id: u64) -> Result { + self.conn + .execute("DELETE FROM speeches WHERE note_id = ?1", params![note_id]) + } } diff --git a/src-tauri/src/module/transcription.rs b/src-tauri/src/module/transcription.rs index 2b7352b..3dc5bf6 100644 --- a/src-tauri/src/module/transcription.rs +++ b/src-tauri/src/module/transcription.rs @@ -12,6 +12,7 @@ pub struct Transcription { sqlite: Sqlite, ctx: WhisperContext, speaker_language: String, + note_id: u64, } impl Transcription { @@ -19,13 +20,15 @@ impl Transcription { app_handle: AppHandle, transcription_accuracy: String, speaker_language: String, + note_id: u64, ) -> Self { let app_handle_clone = app_handle.clone(); Self { app_handle, sqlite: Sqlite::new(), ctx: Transcriber::build(app_handle_clone, transcription_accuracy), - speaker_language: speaker_language, + speaker_language, + note_id, } } @@ -38,7 +41,7 @@ impl Transcription { } fn convert(&mut self) -> Result<(), rusqlite::Error> { - let vosk_speech = self.sqlite.select_vosk(); + let vosk_speech = self.sqlite.select_vosk(self.note_id); return vosk_speech.and_then(|speech| { let mut reader = hound::WavReader::open(speech.wav).unwrap(); diff --git a/src/assets/Square142x142Logo.png b/src/assets/Square142x142Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..190ad834323ee945fd5b70dd7317a58a888799ee GIT binary patch literal 18857 zcmV)5K*_&}P)83kJQrcfwlZ(3R?Pztn^pHfzVmRWY$5LQA0 z31kog**hNb-mCM_6hG&HKyG+Rw`bz4nq)AtodD6qZ0V&k^f*1B?kV@>3z*m-RXUYnr# z)`?i{L;yx(z!<@hb;FdAJ2*Rk*j@(}44pd9nLB7-rz2yu&F;+Cc<}4UFw76eiy1r= z3i|o`#N1GS8ww1X^@A3HrclU`M}Vge5BZrVCpbH@`4A0c!c~E<5@BK`O0iG?uL1 zNju8x@jBqbd~fwlhid|q@YXsuGU;|F1I$i=ww8*f>aA~ruf73@2XtFL4cmL(F$2fVz0U2)J;(_8D9B)ui(&yIcVbR$DnXDMsR3yj z21mQpFl1#kaye|2?sia?$3^+s=~R@LK?4giskp#Hg9x61dHur zsgarCq@2t&DrAWPPyieN27rP`mM4uD-4T1W`8^GD$s-XE2Gr$uze8WF+CiT5Zh@!M zTeE#YAsa89W_Nn1sk-K~?cY3qW7)dTzUaKBNdOKeRkK5jy0a&^$Im|bzWm{nFA4fv zDCiFa`NQ!dTmOYd*WggL22@GFQY)s^AadAQYNRH~WCLZO~?yUU{m{l3i`mp*e^>38#&VB_;rhoaa}yA6AM(6BvY zz{RHE*jZOyFm%ey#{#})3iz7*8qE6?OPRPTC1Bbb!3Yn-etMu??S1cae>a-dy$nZVG) zh)tadr>+I}Vi<{|Eo2Q@`uM#Kwqbki_gGriHN#iEckitb6xP}sJqC`co>l(nDL)Q}tn()whZ4F-IAcN=1DMwt>yy|^O z9;RH_U61y8_AaE2SlE;2UV35C=$W_pSkh?S1PyKslVQ59!LUIZ<~E$BmogwTO}r5k z5T=KMuK-Jaj+-Tmi)=A=el(R530A$cIQxOYA@JxVN90UT2i#c` z+@tqB{dss#FodZaHVt+UZhqz-gXw}3_YvwG_DEcn(uE;PKae3B6C4&ocDIkBfkW#6 zj=IKH+Ev{|dZao0enQDEEwC*Q|B%}8YMKQ& zkj6fLAS_W})7jYqaDaj44Yp*}PFnKqPCEAB@g1a&`ihW40eg!(`F#=iG?Eq<))0gA z+nw%_z`{50-2N+tjopA_1uO#`MwnGFe2+_fE%k=Q)5IN+8jvj4opXTv><@&(F0UE6 z?SEd10&^rM6Ta3iz=60?Ed!vcwwZy^C=Y<6tg6ujM?)($d42La*mc+wHl7EE07Mt! z&QwM1QE@*%06I7epbf^4C}>A(r#Z$ZgEt9>BH0510|YOyz?gW3!xM`wj0D?(1Gg&_ z4B1>+G-98V?+W-@NEqR6s;I_hAI+UHf+mkHk_mIzb?OhJBRH(<)s*zmBD7%pDsl9L z8k*W@7XzW98ft}s0f1CAh%~9GZ((rwWwFgjH;LK+FtAw=@diK??-Z~I$u|BQ#a27O zIk_AnJqmL&grx=Dx&fuTnL4uHswb+Vt3eyNtH>f!PKzwfU z?3+%X%UA#=ibv1BX6v%o-VO$Rl7?qD(>;U1!twjhaAg(F@wYWg5=YM#)}ZQoFZE^g zgzJd>o2HgFQ6ZIVcPO<2=}}SBByMga|K8gg5OpEo;P+E%1&^!@rv(lY+X0fJwap(6 zXV^sj*uR zPVv~;GfUQfvXmVx_~#N@70tZ4G2xkOfkOrwa=nsQQ<<)?d2G<0# zZAdYLL&(WYRyPQcx?_@((b?+mdylb{ym}t|SO!Zl*p&#wqzFGjUNrl_@pH}wa9BZU z2(tzT(7-)k*)FaDQF~U#@a)aGKPP9pct6IzI0{|4M!Nw@m3xHBx#~zCX z4tOk);81o4CR4Mdkjmi&ID#R+X>3~H@GyavSJWN^RcZyc15^vR5{KOuQ@Tcm{*?MP2LQsZE~H06p2=FrA|~5| zpzVv;&gE!i)8bA^CoZUaEV7!v&hw?fh3mrap&B=AuckDOQYCS8c-_vl?7@tc{TVp( z1vuFQW*%zr&Lq6*FgRB;PXr9`FFoH!C@tCL*#-8Q{d1v zXE@PYxDE>(>TR*;01)C@DiAtkfGIs-oMK`j(qrY;bV*EV9TzyZ^sxRuLVD;?xuEv< zfv0A&$jzd9ca%2>SqRdwk>?N0`$Z>B7q`CiG^(Frh^NXWZ(l$?#2C@D2h5m9^wxa4 z-IYNZ`JVkkfncl*#@G@^*F;jlg%#jLnP5yNCg;u1){11v;Mc(>2n8dEu(p7id=-(h zna?~&L3X-G3`mcm9Lg|l5RZHoUejZ6M3Q~+ZZDc@iR13$ejq(7S&EidnZ|mFrFtP* z;n87}L6sWT{lc4rR|jc4c33_gb->u&0-oyghY!r5R1e%LFp`meQNt>HjjI%xFFblF9)GnC|j{Q*@24kT8h zcYBn)Qy{dj;Gp4cbG-C824D4 z0R!_RxiYDRjt-1C({slHGxqEOqleq<&ccAtEA=?NL!&0@1jeme%uZ;PMS=Le^`bd+ z@bqo6*n<__!&qabi$y?5Wi-2rBs@ zBw?i~J=%?SGeC*Wp**xm>ERKNP75c*14JE(MT-mam^nxTE#??$6ay`HA2dL=#}#LT znqMWoBF###NS)*n`y5!&88dd>?k+HttnMT2TUpZ-_r@dL*+hb)-R_7^7GkGEwZQHG zlMI_ffkfFIkS?%0002Fv(QbKkev?UVclY7ZF;Q6Lmc@l#29FMsbs+MP#NAJe%>`M( z%&ynYOs_zwLUNxRddgfYGHbLn&F(H_RW&h>Bq3ev^{1=)<*GVw#O-wjp7`#p0!L4o zWGpxg^vaqRmVQm*>ce9ZCYhOKa82-BTH6BcF(q3U0m@*z-GSFDBUU`RG^eFvGbF7Q z4D!%olM903o*l>C?=4XnnO>2{5(?nw*-W9Wg96?V}NP{O0LB#c?loi6GG zQUf3vY#HqcKLBJgwq{r|DrZ$0>wQUURniclkCp4pV09|3WK%5}c?fd$*|$@ox}H@5 z#J5x%QtY`=6VMgkfn8qPsF-9kYfiwyK!H(;=yp^l*^(a7!VKOHF(^&E8rU5G2o_=p zN3J6Sl&YL2=0bL9w0DmS>djo^Eg{`ZshMKrm?Os?3I(&ZKHr=n1|U2P7?POqc=-VU z4R$>8TM&#awQ^om90~-PA{7CScD|!MYc492%r=TQ)XpR$xjP(#vW!xw6#ztHPz)L{ z$yyHrl>H%tQMu%)2923U6U?@!dGfN*Z%69`dJJokU|b?q0l|BJ`jbVr0a*q9jH&`*+Wp zE6Ph3qZHBtyAFioK&yHJ@hPaE?tsIZh#tkD`ooBH$)$!olv3yrLQ}vDtExa|BC6By z76IltDlC^g*6ko;P%*x(5k{P5U8p zs?F;I+l=I~hUQiVPHNICBBVq*KGDNM5#lhg~AHKeyJdO;ydg{eAyLbkf(kIB*XiK^BQr8EU9Q48upgc1G z95;1qspBIDo)vbmp*WgYQb$vy*zQJY*iMNOZ|cex0pSPN9=y-a3NtYs+z&`X5R(m7 zFfuZi*bxz_UbUf|K3=+o&OaeHEOpHIQ;26Nou=kbqGvX|LKl5@4V^UlCv@rLbE(K( zU~Pg>Se2y&IF|i3dT3s%1D$H--I2iQD{jP%1~G@|ugR~(PbKZ#s3=w{gOs(fga833 z`7I2P?UKr1N)N^Rs0>0|r`+SLj80VxNDvG7tvn((?J=gPVe2fWfSi&R`#?o_-MbFv<^@HjJiC7}!A? zTJQ}62cQWukCEfbrb=%$-T395^!B#-bmf$bXj;LZ@;az5Tar85g-d1FE^|fnKu+aI z7;seB0n!GQ2XLL^NgVuY^Uhk@xT9KtApl{pR9MoZxz$W%Pz&y`@?4~>B}%+WRZA*< zNRfgdBoSwVTd%?jh0F&*%0x!0qrqHBkkMJgX<&L0l{HtG?{9OEi-GjTu4T0HB%=% zNXlqXmD5_mNPr_FQhG$xSvN=Ixky1ATE*tH=N>D7s3ftpIKfCOwi(s^SH>}<%;>BU zw5@R`OCE>rFL);v!iv--Mx&-3ZH5l4pqE;-<_(kn8}?qg8THe1rVS9A%S zHh#Dgbvsz#^6I9T3#D1*G_QWNMkE9#Ae@|T)mle}6kJvZ^5SfFxalLJ)Wd}LX)kN( zX}qbsjB!Y~8yl;u@$})&&pLp?^sIEsP0OK@<}$kA)2r$I(Vx;^XWvc^%}$1vc<`v( z#HK;44e>oKsog7f8*KoNKT*I@z^JNij(gWgXqr5_kltUsNdl8%dq{G+Tgqk!S*Z*| zEszv3F_@Q?9u7@JU*L|sPvU}+=HKp_H;L>3L&VMopxQz{s%@(mHFeamY3zYbCZ}$O zK9Y4tg$myw+_LQZP^w*Wznu~s_s0%K(mHkV#;RxzhU^XJ9V9F=h`7$Iao51?)JwRB z1#!I@y7i`Gvqw^%D@O=H-Ktxx_chQE&p@_s&ZM6VKa2*r3;DT5`l@0j+dwXwl0Pvi zD;MrW${Q@zxyP7p00)d+1|=YMR9teF`zl`5mFiIJC%QG9aLB|icT`s)1zU<4TXi3U zGm2?s)-YOC{XOZ1E&w`j!l`t{lnX`jJhA3EdTaZ9Dr?zAzMxO8|F-E@)2U-lNSe0= zqd$cVw!%m&fD@zyzAbhlJR;aDwm4~!kY~=$$wt?u(;|Ia>gW-C{(ip8xPVu#X={bQv!v%s_KMS zh4f8%F2)_1O^Gyl04RDtY9kx%-Y}*XOrn~$TKdC`>jX5nEd3K*vhaG^<*gDm1{Yno z7-y&Mqy|=7e^_>Z*oaP;&7-vg2i#RKwOmeOd2++5BZqqkir<{@AIH$G%IL|t<`3*DpZZtj|4f&ju??SjBcNCJ*}+zPC!%O&J!?n1qeJ3@I1EW8Tz{N8?kc$ zsJ>s1`4x@?TyoGLsmv8hIB;-7e)f_ODiGWlKNgvz_JIkIPf9=MI9IErS;SI5_#ISK zTd<98U-lOESsMR_SlxbKGz%l>Nqx(^aLvU`PjUuvLtwG1j8B|-)A{9>D4Xo(!bWfB!KFz%2Mx%7AcC$ zNtI=ggnUOs*kdGf;h~=UF%GrEubuiUDs~r8IyvoZ0qQzRTDF`07>Q0 zZDLsee%XB#2nFK0^-*CHkzRq+6qCzI(Y+_*O|k2IzJTN(TQl{e9lE8SP8)k79W-!1 zR$0yBRRTD%2)Up|3@)u$L4W)9ae1ychH}WtZ$eHI@~G*5S5;Y6os53vlK+TtV-+Hr z3x54xm$J*9A;vOtlOrXFl0r6l*uwS@!WI8q`vQHrYgw;Y&Kd=j(h8;)<&WjB4ccAXwVq zYwER&j>AP#ITD(4+yOi+b}GVc`v3P_a_I(YrHsZ)g#dumJ^pgiSyDQ)vidvu+ty&K zFw8~nLK>bqL=xV?A|r1ZwoOfIEqVDhtJz4toulY~-1kOF%jj;tvU&wnl_Q0RgCqqw zs;V=b3HU#T8qTvxUj#Yht^NXwJgL8>@(hvey;`SGtg zAUc#z9Cb8JFPtjDQK%`zw*W|}GrX7Ga_b$MLlVFMCZwI>IhBgh9RMNF>2T;NI~*iA z62~fMQC?MlJRvXE0AR3y|Jn2=y|(2YT3feK)J$a3nVEZJmugO=jQ(=cnF1h4pK&>( z>G^pN)2OWB?4tYW^RgxM62I57s&A>Rd6xjJEof2|kS?g#8p*(#S+p1JRj`MA4^S%L zhAfE|bsnv!;Etn-6v~P86m%j@n#8f|=J^iD%|Sb~?o@UDkV6`ySnW~X-|;Cuu<9`Z z3?v9R7bIy>WW!1!_CjKxJo^2=>7DH#(eI~UOY?>tD4+vq0G9i{`3IGID+F|qBmhc# zyA(};)#PuX@|Ftvtn>>3W3DSpN^y=DI#*(1gE9t$C1Ga?q`oiAGs}ZerzS@cBw6y< zhEF^0T54}RkdUPfw+-RPF$aya`sf+`Qu0VH4TY>^cb&F3mC$d$x`!&;Y6LWH1`YTb z7_r`QRL*G_AcMB`|_w#V=Rwpv@(jHl07=RC!O>NOnt=saiEu zJf!MykfeE4W>u#@oR{-MI^Se7bYy zjZrU(1C^Yj6$x8p}1MSHQaeAL^gNh&FhI8g0e@Sc!hu!+XA4K#qiLIt8i z6r9g4Zza|6`>kX3SmCV_R~=x7B+7H;iqyOP>-%Y8*_ZT3z8+LubgrBzmh(DOGuO#; z$BCq+NM)?zyc9_qEc!WPXgu8PI%6k)u;VlO z>+C;>q!EcE5pXL$h3Ze1GFbF^Lk^P1!{0qE3;BKRdhx=b;vmI@Md$ZFdDO9V*7%cY z5_3IjK;5-iURDOHDZ3T30%%Zbyrg0|eYkUhTqD4TG}VP{X#f1fU(!Pd-YFhkBo#1t zK&E++eF~71G~lR#{xm<-Q0fQ6ydxssUvWK`uo_VkOu`U`>77B+>^op@x_iHy<@c~#Uf=qjxc0D% zptkImt%H)9;hrI^8b{G|_OQ+#e+tLI&Z8AotJ%nYm_9FGO8G2(s#m36+xR^w4!25`f76%c@7H zlnu^oS7w;~jht#MBFcKPNOK0w3RjAD9x99=oJ+`*Y*0UMY<-^%=oSGRVpqQya~!+c z)5KuD{PUZ|0LL{cTgT6Q4)`5ZfNpNwCgA%ozsHN4{>##9n_R=W6MiAr_(|zvNy|W+ zl?Hz!9X@o9G(ZqU(%)Pl);T|xS+>XsTJa!)%xakick0xiLVGmMV~*xQn@iXY(cqk<)Icg)6qvw$i#}y5u5p zk_H^bFCeMw=mW-@jjN5maXD)75Da{A+tTLnlP>Nm^#k72F}11(`v;f^P*Ri+u}G*7)%iR+2F zqq?>R0pPzkyd1GJt%Hj!qHjG<=uT!|VKcwoXUz zm>wEFA6^m~4*)dyoVkN$)6v5ZrAA*wy<&jxUB`}$t_ic)0s<%vu?9gycq$*Fls z?XfCnbq0emJ9PCFj2JLn;k|rYy+-0YX6l31p%nL1_CDb5cBeap81k@h*ofWPR7S@P zn|yNks{85o#A;eN9aLRcq@8r-o)-x(10cbG?#@tE5|A>#|N1^Ub?gbE zS|giX4l4Adq~amDz#)?aME~JL4yc}bc15w2Y=ceN~mjvm0KKP4nRF zJ4FSmAqTLfIg%cFNJ&>`oIMprQeQ6C+F4+8(PreK1MiZU&czF^rxV{lkFHw$J0Zs~ zOpnIiZS^9%nFEpQYB$p1Z~sE#OdlWnGP}_G$$KR7{04O&;AZw_71qQ=V^8|#C;*}s zePBkBs92@0a9Au3Q8Dw6lLwuo0;j#Ivn2uVWrW4=7UpE&QGmol8(C!2ZK&H!AC-JA zzXxSs-crfZ#}UyJF#aSe>c5J~we3wi2q`Ju=;aPjE-7}|3me~{Q#e+GT$k6jzANP@h-609+DcXGOTi7M97=G{ByKF;12n(GBJ-jG2LZ`|kA`Otx< z=6fR^1$zZy1W3=G#F}tl_i`L-pTa5OIT(f~r`LyOfDup8`a&`MRn!vFN$v!#S&&-{tv!lFhS6_bbswFcT~ zc;;Y94%f+C0OSk*ciRWT_`<`&js)j}auomviB?#kbdCWToq~*zSTKgwc~q$yN~+y7 z?Q)5!ZEe_2Y3$*3x4z0ODYkRe|4}+{$P5}kw3yP;Y(jvE1uAZAa1~Inol7ebL+JdkwZK|MCCLSrpkjuX>CAW3Fc0zHU)L2!-rvNPBW|G|; zp!v@lce3QJ{AbgD#RUhO3pccdNi#?ea5?Z!Zdr0SE#CFDEHpv|imeTcd;WY#p6_bc z(Fr4u5>*5*OO@mTq_8u<3xQWj^}%HdytrPZUYtJmM0#@F3q4j@u;rvPsHj5}n+YlGy=xs_VLL)89ejST|V8k_73P_i%)IE8964jtrSdF9Yi0 zNoUd9?0O@nblOLk(xP2oNzFD?5+<;-hzF2dVnpr~wi)&gR1!`ju4%2K{fhRX7Y}5yMrogysWZp2~$}~2Tprcr&o?d?COYSHI!&RbEb95+A zXO9Du8R$Vv5ZR3k9MJT6%nYB-F1woO$oi`OFw|)^XikZon;p%iVxz^As7gBFUJ-Zu z^YTY%?y!C7#SsmDF9=YgMSc*A95uiX!>iGB1kzRR);r91E z$GVCQp)$aT0I8bj_KJo6F%&Tf?&*{3hZ$v$ zB-SM$j3V$$%(>$+25cOkKM-zv5^Hf#d4LH*u<`0IZli1107gL@CLs{&orfSv=u+<; z;84XS%ngkQ^F<0YFuvC?!`z>$+ZC{HnD%Q)fJd*L9(h^Pq%w{E^!3A3Wh5fwhHmww!Fif*9rZ7om@`%kPNkld z`+w*12Us1REk-v+s2}v$RVpisRo0EnDo1Xd?!?ecM%h6Pfd;yM;XQQVj3IRN0psLI zs6MJJQB{sqO+;B2Rh?!Np^$(EWgjQLe?F_G^=!oE>{guL(;bU20t{)K`10Jw*W|r% zFqzts09GYS8&Lg_a;R!h*VG?|Qdwx9a?0pqBUD!WS})Z@8MYFl@6T>}nQl4fP%6w% zm*$>*P+1NDCt(M<#v9d9&gyKZ{dEZ!T)f~q2@mu|HAU`NNE_5geZ#S;|6BbeyYTs9 zTw^F>co7FeQM8BDQKUAgE9);qNf&g{7%4PGYmXM)Oi**vy~`e@8s|>B{*1ZQ(&nQ+ z)JVEg;)ANHPSt=w-L$eV@ZVooJtqBR(%5B&cumNePW&I3U?iJ6q;*KTT#L_2ztFAS zjMxxroB&wtXm!o~1~`_=LOA6nCI-z76~8zz?4DaeP^hfy7W|3k?mvu<*nb=ogKfPo z6;2IstQso>-q32yEY@qcFTl^-x9T6^u2+e4i{Wn!OfNQj%7lU)+RLhb#3=56=zzPz zHQDAhhx!$Gl*&46$U%}Z7|Y9wHiFjaHOMOX_on~QE$1Fec{yq13-m5CGexYIA;wq( zBV4uVSH#<+Yn~B)1_k~h!$`Po0jY!33wUN~_kxjDy^p^aFTu^nM&wF zCZIZw2r(h_dbyW_n2lm0%OAth?#(14huHGe^sY?xD)^Mgk|P zv!hAdlK|B07%>-0+0qFRbHor+9l`NDpUr3|#tQKkT zu%r~20d?*tSA?q^`kR+^)8QdP?FKiVh=eWSRjwv6+XEQZyey~8|s48NVRfS6Z+HyL&?b(Q5*(>gUHE+*QB0@ z6bZ0YsbE9+2#&fxF#C4-9b#ksa&(FcE0qNm5Aa;}`AxDxx~nvl%4!TW)3vO!o;~y- z`e5luTJ+6!%1U>|I>THoWfNLq_NZD8Y6XU_dAPh5mMl;~8h?M}%!zdCSx3;NC(V}L zI05VMEwSAB|3UdV^!S=*#m4AKrDbo1S)FO-9GfjgD6AG$rz}jG+sf}%-OSaaUj#iP zK@-7dE0qOldBTXJdabe+mM)~YT#_l2&ZwNc3s;-%u zygq41TifWR*0vCxF>eOFc-N_P&ILorH+cokI+p0NlV?$z-%k#^O@K9OKmiRL8>Hvf zyh+(CDwDY|00`7mzAMj6n;_k41Yna&Pjq_t`+;|dD;rd8w`#@ToiiL#1)5-K1%;Yg z+M5n_8aHwTeMp?MLA%;mXlM7pil@k*Q$d%XGMnmKyi(E>OI`sp_Lj!-CU@3AGU8b1 zlJL!e$}z+8>7+SRC@b48q+We}fbt4?7Z2D)_iXtiy|-l{ZLQx)zngVAoig$QVJKmw z7IS3w%F_;{_bT3`vZg93aOWn3W!t#HG{+_CaA)D;FQCZUqhSRiCIUF=V}IJ@ms z9?dVlMV>Q{C(3YlQdwrdV^ptRKkZWb&F8m@WDUj(M}WVQCR2v+Rl8a%=z3bsHo1U&_7=MCwKv%? z%SgH!P|YxXNYTS*C;z9o!(gYDZRgWkpv-8Ry#I(H0@J3l{PkPmQ+ua ztBlW>K%N;bGbiGnXLG4Ed`^Y5L51h60cMR964Bq}=r~StQJcA6 zf=1aimKo~%Y88d*8!6-T_Awu7BiqijgV4F007JNEq~}|w-`Tnbr&3us&U${y0y+BujJeFUW>BloN57wOEuAs$ zM0$Dc2Xz1Hztc|dPQrAUhmzQ-zO|7qXg---!Es&YBT4zRrq$^6yVlf@*|{p_UBgg< zyS8A(7W(A#GJ5FH+vviHXGqO5I+o*r3iRE3@Vh7EYy&J%9usU>M5qg#b)fg+q}!ej zkdH_Y#raX_32dJE9sgwBUM;vRw4a(D{}0vt=5}iRY-#)Nx&u0~RoX!p+*M7rzxe~T zzQ3>?C;)yzE(NyipunbWU3ylL;}(`EC@@5{A0d~9wr09w^2Gw0KP;Y`J@Fp3 zt{=kEW`C*gzHk5AZYHTKhNmI?~q_(B2$cWSNL#Zs3{9L`)#Zp_3KanEZ&&F9&(DOLj(A@aR?Ml`7 z&@<%RdoraTcNivyY=Z{0Pt@+B&;$#l4B~cQuAsIrR){LXv^PEdD(N{{EM4-*F?Aw& z-~NO+fJB;$<47B}QmB><-brJ^(y7bMqnWi+ICSL5VRNahxq^N(^?a$Z|NH8vq-hko zb*VG2u-TAreaN5#>HP7h)6?%Pm-9DVyEmW|a~)U-F=kj*ZQ2$L_$sv?h;f7t_847E z86F$m{N+8;5vs_lxHS9a*<|N~?#DR<=njhR3(4w)sSZtr&GnCi?y(B>g2~}$tcCY| zvzCndCQAR=k@9rQ>mQJN-t2bZMBXcJ-IiD17a|W*#^AV6+k&si`_{)4U{|~KgU`um zXd?fbjpP_L6bahmg4+fak!^4>)!*?Dx%S0cXPkb*I!V?yi!?XCVc=>WRlplS5hQU4Zde_PW|a_P>b*f-cQpa4(?{I5eF zq-h0v>~@Y*G6p1ubN^}bef{qrB$j3~ z<`i=A!+q=_WFI@6H2yn1H=9~sdY95qK1yDj@r$2O?w_uU>xxGX-8xhaUgSUL{zY<& z(Aov63-k$?b1#1nR{f@Fzo91%xtB`HTj<)qzDs_8C>&qx1jZ0B;%lkh0L<6}Y&bPn zH+`9&U374;Ej|foUYL#COvHXx^PObP;X^>K!QGouJcdpmeKO7U97V;M>5{&Hw1#BZ zHc*F{gz-4a@W8vIghZ*yWDkS675m@;!n5cFIh1kQPt~@hh6kP^`@kYfKjml!NhJlh z?i9(SrJI#Vq51~4S6W#@9Y9)!`J9tM)3#_iH9qA?vGsfgO?0Z_Vwv2fnN0*>c0texS`dwemm;1**)jL)!9@+M@ z5>2$#dSx?6xlv`^8*beRuM(vr6iNQvo1=y#4F*U(m8OB{XzE zwlwqHxV=WQF&!-FJn1fr9q7`O2r)KvzpuH3vGRol9L?TbThd%tyXEz~f^nAy{N9kJ zb*)Gx&xO^&s_*LP@zeiGJIWjA>jhqR#phFXW23nGC(hY}+FW%UJ6R-^1&LBqrajXn z-L(F8z#Velwr7ck^(!*#i$JpAIxAmkeXk5ha7D8 z_*SiBl~v7ddmd??baIRzMUDxhEg(iA4-@Ep8dmHgBh5))tuCYWTWZXHK@L0Rc+%vz zp*Xw~dr~30!>u<~Y&zP+ooN99rpLOfPSGTau&u8e&kVzu~ac$$8`H(S!ahefks!k<#e4 zSqx91x<=A?@?A5h3Rv1$ir9w~lW)mN%Dn7MYW~mLk8H zui6?aKh1%@!BQ=`Lscd6Wps`TVrk*s)Y^tDxnTW|~EBxf_52?+qz4GHa-u z{e-a67{kyAXlxY<8QuNkpl)~ATIwqnY-Mba zxH{&7>gwIaYG$@~^Rkz3v^&#b5cSA#kn=a$otrqifIct%sz;}-sFY0%G|pK!zIZG3@`>c$5%>)5i~`daZ)C)a?}Dbfr(tL@JW{CiF8#b4b)P{d~-Ea$0WWP;K@x z+H1c1!ES~sQWTZ0_W^SWjP@JQm{V>Vzpy<~D6plZmcICAhY);5x73so0 zLL@e#0*6Ht`&~A>w_&Y!)6!>89e>dIU;4cbaM@#$f+}i$^y~@rN$K0v6lkUbM>PA! zEY`!xE(lS~DB6pDG5UDgyKssejr`n(*Jw@cdSQW)rh#xqjx$T@q{roq3ZIVBouizS zK7L?TBpQ+ApD=!fQMWQ=%&ZNYH0-?d@dfO-1Y$G86uCb`HitWl)-8DARA2#^h?F+F z0Y@%~U%-~Lz2y53m*fl>cTw@ggCF%Z*D|2&Q7K@vzn@>C<$F%brQ1JP$RUZ{1DaBX zLP4rxk8PiVz3B2i&ttcG9DP)}fG$~ZJ*}(TD2%kqm5n^Z5=JrrG-vQ^x@?bgrAJJ^ z08g^PVIjmYD59d&OydpbOm~_Blq{hu2+l#>w4;Vze1A2SRy0bDTQ@+1tO>)Jkw@ig zKD`JS02VrfCMs~uzc%nf+jpNl^qB5+4=Wlu;})N{J}Aafr%RPwWCqTxJw{NOzl6T6 zUK=iOQv*h=H|Qlj=%CxD-Au=gna9%Qb-Mn`KT>&XIR`;Aq#T#5+*mC(2iXVw7smJN-G;Amj+Eb5wP_6 zgCb?R51L@e>CUlLZd!5&)*ee|VS%~@9FyD8*xNSrZ{NT7)+t9`n_f6#`c-Y-x*&(v z1@yAC8cDyKf*dy;KW8#Mvid=43ARwKy*;lOK}UE>Lmb2D!Gr!thQm)M&A))wvxGtG z*}}AZmP4VaK~T;55VNGiCyDMji@^eSk`EzE20%KeTQhSJJL z+E!LaB@B`hmLTwwaGpC(=J5y3gf*B8c8)!9JU}w|;N^x4>Xv#6v7~VUG+V#Ee-*E} zVAsSz)IW$h^NQ@jy-qpun%j#;&Ag?p8RVcY1~aVmnuZp-?4()rlj95N&{s|uk5#D< zxa8H|Iy#`>0DAuD2WV5vMw{1t$~85porT75xlvyga4cIzQ(E8?>a>z^Jp z@(;qMab@IC#pb1Vta?vCqiK@U(>3DaZdnUF(#UG;tg9{St5h;d0sN zmAlWNJJ#Pp|Jm?%xP81F9n{uH=Z!y&emmm|dUwGla(UQp8CXh7s=uZ+H5+MD?RKhb zZM4pb3R;AsX-eTx4>vQME`8>-((mRk!R1)$Defosz(|lvKs(ItjGT$?VKa`u@4z{8FM0Ud z!|04pE)FiKT#52u8@#M4Z!MiR?s)pszPHk4cfCammT#lNoDAU=n!IhIyIl?k4K2)} zr~hygd9pf6koh|o(2Yy(6haW~W1pP+03B+b%9|8$SlhURdeLGZ$OiAF65dOdsa|&M zVt|x4Fkl)b&jnqs+I*%I)f28>dOcVzF$2%!j*Tgg>TTaWyl&A;S9|N(<%ahphH&?; zq6de7NI_cwK7DiD8ty}t6UWxnqZ_MoUg7&7Yojlt(73DuHhu%-wAtloA$00Otg75wNvDiDmiA^A z8AX2h9TL$I?CgzU+#Bx&5(0)ORLV{U$aV$@3{<2k03a<4Ab2liNg@RfM52*?5-uHy zP%qt%6(vk)!cz$``EIv4SZX)b)O@!6o2PGNMp%-vqVY5d1deqT_y7$Cw_$tfl_$P< z>BA@H3_O1PF^;1OuVcgHpds22`y+?kNsm0YBKZ2JtBs=EOr1d^4g~(*oHQfsNvNcK zwPw$5cvY%CQSpgRS_ciV(~ae54jJhmzw%ku{Cy9)Jt+2 zq(~WiG8GJ(hNd==Fsk8DyY$e=<+QU0W4E?eNA;4ds*bojT(ANn%wR!cv)LUkyQVwI z+f=(?+p1UY*}i=K+o51JHVeDRTCk^}iCnK<4ZbFu5tLq`A$y^n^r@cNf4$+ZbN9+V z@z~2Be77i@L-|3UPY(4^-WdX{Nch1*LY&`u`8+ye)>uiaulG07;cxw1gZmEYa^b`? zw36mBIiOWKVX%08^@t~#NgiU=Oz9M9187}H4K-RUatwfg-68HR+aYC@O$MX~yq7kA zAe?RqfH+{B?yg?66sZ>@+V@dx5Y4K@6>mtqR3>3rkWiqlrJ|{N>zifk7Cl+MarvT9 zpvDxmx=l0Vlf4^#CqgvgB5a09#25~CwO#qe*|~%FI;e2?)OpUFLHoKK8KXJOnr|A} zAuIb{lZBi%VIcke#v{TrbIs!0=sgAsx@GNOJY7z~gY*HZh;9rxYur2~IJ#$L8mDcn zN}2>9aBEF3rjd=)X7OI|xk`0LsuvilW+MFV>ZQL7$#}Fm!$heO-Y~kUlLEeKpTBvN zueNka)sAoGS8rdrpt-iBzWvW_I+(t`_6kiLgVqipNzGADJ2LYqEiI=wyJ*aCmW;#F zvL+UK^3w7{pZJV{k@sI=kp;yS$ZYX*QRR z3PcR$j$<(uIR?oAfNU$R15wt-_J^O