From fec499cfade35c8a04075349f2fcfae31075604c Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Sat, 15 Mar 2025 13:32:40 +0900 Subject: [PATCH 01/26] [Update] Adjust localization and design. --- locales/en.yml | 11 ++++------- locales/ja.yml | 16 ++++++++-------- locales/ko.yml | 2 +- locales/zh-Hans.yml | 2 +- locales/zh-Hant.yml | 2 +- .../_components/word_filter/WordFilter.jsx | 7 ++++--- .../setting_box/translation/Translation.jsx | 13 +++++++++---- .../sidebar_section/SidebarSection.jsx | 9 ++++++++- .../TranslatorSelectorOpenButton.jsx | 11 ++++++----- .../translator_selector/TranslatorSelector.jsx | 2 +- .../TranslatorSelector.module.scss | 6 +++--- src-ui/ui_configs.js | 4 ++-- 12 files changed, 48 insertions(+), 37 deletions(-) diff --git a/locales/en.yml b/locales/en.yml index 9afc8b00..10969bf9 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -17,10 +17,10 @@ main_page: swap_button_label: "Swap Languages" target_language: "Target Language" translator: "Translator" - translator_ctranslate2: "Internal (Default)" + # translator_label_default: "Default" translator_selector: - is_selected_same_language: "Since the same language is selected for both '{{your_language}}' and '{{target_language}}', only '{{translator_ctranslate2}}' is available." + is_selected_same_language: "Since the same language is selected for both '{{your_language}}' and '{{target_language}}', only '{{ctranslate2}}' is available." message_log: all: "All" @@ -59,12 +59,9 @@ config_page: appearance: "Appearance" translation: "Translation" transcription: "Transcription" - vr: "VR" others: "Others" hotkeys: "Hotkeys" advanced_settings: "Advanced Settings" - supporters: "Supporters" - about_vrct: "About VRCT" device: check_volume: "Check Volume" @@ -112,12 +109,12 @@ config_page: translation: ctranslate2_weight_type: - label: "Internal Translation Model" + label: "{{ctranslate2}} Model" desc: "You can choose the translation model to use for the internal translation engine." small: "Basic model ({{capacity}})" large: "High accuracy model ({{capacity}})" ctranslate2_compute_device: - label: "Internal Translation Compute Device" + label: "{{ctranslate2}} Compute Device" deepl_auth_key: label: "DeepL Auth Key" desc: "Please select {{translator}} on the main screen with DeepL_API when using. ※Some languages may not be supported." diff --git a/locales/ja.yml b/locales/ja.yml index 4fc04836..e92853e9 100644 --- a/locales/ja.yml +++ b/locales/ja.yml @@ -10,17 +10,17 @@ main_page: translation: "翻訳" transcription_send: "音声認識(マイク)" transcription_receive: "音声認識(スピーカー)" - foreground: "最前面表示" + foreground: "最前面固定" language_settings: "言語設定" your_language: "あなたの言語" translate_each_other_label: "双方向に翻訳" swap_button_label: "言語を入れ替え" target_language: "相手の言語" translator: "翻訳エンジン" - translator_ctranslate2: "オフライン翻訳 (Default)" + # translator_label_default: "Default" translator_selector: - is_selected_same_language: "{{your_language}}」と「{{target_language}}」に同じ言語が選択がされているため、「{{translator_ctranslate2}}」のみが使用できます。" + is_selected_same_language: "「{{your_language}}」と「{{target_language}}」に同じ言語が選択がされているため、「{{ctranslate2}}」のみが使用できます。" message_log: all: "全て" @@ -98,7 +98,7 @@ config_page: desc: "ログに表示されるフォントのサイズを、UIサイズを基準にして倍率を変えられます。" send_message_button_type: label: "メッセージ送信ボタン" - hide: "非表示 (エンターキーを使って送信)" + hide: "非表示 (エンターキーを使って送信)" show: "表示" show_and_disable_enter_key: "表示し、エンターキーでの送信を無効" font_family: @@ -108,14 +108,14 @@ config_page: translation: ctranslate2_weight_type: - label: "オフライン翻訳のタイプ" - desc: "翻訳エンジン(オフライン翻訳)で翻訳する際に、使用する翻訳モデルを選択できます。" + label: "AI翻訳 {{ctranslate2}} のモデルタイプ" + desc: "翻訳エンジン「{{ctranslate2}}」で翻訳する際に、使用する翻訳モデルを選択できます。" small: "通常モデル ({{capacity}})" large: "高精度モデル ({{capacity}})" ctranslate2_compute_device: - label: "オフライン翻訳の処理デバイス" + label: "AI翻訳 {{ctranslate2}} の処理デバイス" deepl_auth_key: - label: "DeepL 認証キー" + label: "DeepL API 認証キー" desc: "使用の際は、メイン画面にある {{translator}} をDeepL_APIに変更してください。\n※対応していない言語もあります。" open_auth_key_webpage: "DeepLアカウントページを開く" save: "保存" diff --git a/locales/ko.yml b/locales/ko.yml index 6011d3ef..8f223f10 100644 --- a/locales/ko.yml +++ b/locales/ko.yml @@ -17,7 +17,7 @@ main_page: swap_button_label: "언어 교체" target_language: "상대방의 언어" translator: "번역 엔진" - translator_ctranslate2: "오프라인 번역 (기본값)" + # translator_label_default: "기본값" message_log: all: "전체" diff --git a/locales/zh-Hans.yml b/locales/zh-Hans.yml index 0f317e5d..d0a8d260 100644 --- a/locales/zh-Hans.yml +++ b/locales/zh-Hans.yml @@ -17,7 +17,7 @@ main_page: swap_button_label: "互换" target_language: "目标语言" translator: "翻译器" - translator_ctranslate2: "离线翻译(默认)" + # translator_label_default: "默认" message_log: all: "全部" diff --git a/locales/zh-Hant.yml b/locales/zh-Hant.yml index cc56ca58..0a70011b 100644 --- a/locales/zh-Hant.yml +++ b/locales/zh-Hant.yml @@ -17,7 +17,7 @@ main_page: swap_button_label: "交換語言" target_language: "目標語言" translator: "翻譯器" - translator_ctranslate2: "離線翻譯(預設)" + # translator_label_default: "預設" message_log: all: "全部" diff --git a/src-ui/app/config_page/setting_section/setting_box/_components/word_filter/WordFilter.jsx b/src-ui/app/config_page/setting_section/setting_box/_components/word_filter/WordFilter.jsx index d2a4a62d..bc338d4b 100644 --- a/src-ui/app/config_page/setting_section/setting_box/_components/word_filter/WordFilter.jsx +++ b/src-ui/app/config_page/setting_section/setting_box/_components/word_filter/WordFilter.jsx @@ -1,3 +1,4 @@ +import { useTranslation } from "react-i18next"; import styles from "./WordFilter.module.scss"; import { _Entry } from "../_atoms/_entry/_Entry"; import { useState } from "react"; @@ -5,6 +6,8 @@ import { useStore_IsOpenedMicWordFilterList } from "@store"; import { useMicWordFilterList } from "@logics_configs"; export const WordFilter = () => { + const { t } = useTranslation(); + const [input_value, setInputValue] = useState(""); const { currentMicWordFilterList, updateMicWordFilterList, setMicWordFilterList } = useMicWordFilterList(); const { currentIsOpenedMicWordFilterList, updateIsOpenedMicWordFilterList } = useStore_IsOpenedMicWordFilterList(); @@ -82,7 +85,7 @@ export const WordFilter = () => { }
<_Entry width="30rem" onChange={onChangeEntry} ui_variable={input_value}/> - +
); @@ -121,8 +124,6 @@ const WordFilterItem = (props) => { ); }; -import { useTranslation } from "react-i18next"; - import ArrowLeftSvg from "@images/arrow_left.svg?react"; export const WordFilterListToggleComponent = (props) => { const { t } = useTranslation(); diff --git a/src-ui/app/config_page/setting_section/setting_box/translation/Translation.jsx b/src-ui/app/config_page/setting_section/setting_box/translation/Translation.jsx index b1bbc0fc..63f0630d 100644 --- a/src-ui/app/config_page/setting_section/setting_box/translation/Translation.jsx +++ b/src-ui/app/config_page/setting_section/setting_box/translation/Translation.jsx @@ -56,7 +56,10 @@ const CTranslate2WeightType_Box = () => { return ( <> { const { currentComputeMode } = useComputeMode(); + const ctranslate2_compute_device_label = t("config_page.translation.ctranslate2_compute_device.label", { + ctranslate2: "Ctranslate2" + }); if (currentComputeMode.data === "cpu") { return ( { return ( { [styles["is_selected"]]: (currentSelectedConfigTabId.data === props.tab_id) ? true : false }); + const getLabel = () => { + if (props.tab_id === "vr") return "VR"; + if (props.tab_id === "supporters") return "Supporters"; + if (props.tab_id === "about_vrct") return "About VRCT"; + return t(`config_page.side_menu_labels.${props.tab_id}`); + }; + return (
-

{t(`config_page.side_menu_labels.${props.tab_id}`)}

+

{getLabel()}

); diff --git a/src-ui/app/main_page/sidebar_section/language_settings/translator_selector_open_button/TranslatorSelectorOpenButton.jsx b/src-ui/app/main_page/sidebar_section/language_settings/translator_selector_open_button/TranslatorSelectorOpenButton.jsx index e6b68cd4..7a869e8a 100644 --- a/src-ui/app/main_page/sidebar_section/language_settings/translator_selector_open_button/TranslatorSelectorOpenButton.jsx +++ b/src-ui/app/main_page/sidebar_section/language_settings/translator_selector_open_button/TranslatorSelectorOpenButton.jsx @@ -16,11 +16,12 @@ export const TranslatorSelectorOpenButton = () => { currentSelectedTranslationEngines, } = useLanguageSettings(); - const new_labels = [ - {id: "CTranslate2", label: t("main_page.translator_ctranslate2")} - ]; + // const new_labels = [ + // {id: "CTranslate2", label: "AI\nCTranslate2"} + // ]; - const translation_engines = updateLabelsById(currentTranslationEngines.data, new_labels); + const translation_engines = currentTranslationEngines.data; + // const translation_engines = updateLabelsById(currentTranslationEngines.data, new_labels); const selected_engine_id = currentSelectedTranslationEngines.data[currentSelectedPresetTabNumber.data]; @@ -74,7 +75,7 @@ export const TranslatorSelectorOpenButton = () => { return (
-

{t("main_page.translator")}:

+

{t("main_page.translator")}:

{selected_label}

{is_selected_same_language ? diff --git a/src-ui/app/main_page/sidebar_section/language_settings/translator_selector_open_button/translator_selector/TranslatorSelector.jsx b/src-ui/app/main_page/sidebar_section/language_settings/translator_selector_open_button/translator_selector/TranslatorSelector.jsx index 45a45fad..54e50f3e 100644 --- a/src-ui/app/main_page/sidebar_section/language_settings/translator_selector_open_button/translator_selector/TranslatorSelector.jsx +++ b/src-ui/app/main_page/sidebar_section/language_settings/translator_selector_open_button/translator_selector/TranslatorSelector.jsx @@ -33,7 +33,7 @@ export const TranslatorSelector = ({selected_id, translation_engines, is_selecte {t("main_page.translator_selector.is_selected_same_language", { your_language: t("main_page.your_language"), target_language: t("main_page.target_language"), - translator_ctranslate2: t("main_page.translator_ctranslate2"), + ctranslate2: "CTranslate2", })}

diff --git a/src-ui/app/main_page/sidebar_section/language_settings/translator_selector_open_button/translator_selector/TranslatorSelector.module.scss b/src-ui/app/main_page/sidebar_section/language_settings/translator_selector_open_button/translator_selector/TranslatorSelector.module.scss index efdd0854..b33d0782 100644 --- a/src-ui/app/main_page/sidebar_section/language_settings/translator_selector_open_button/translator_selector/TranslatorSelector.module.scss +++ b/src-ui/app/main_page/sidebar_section/language_settings/translator_selector_open_button/translator_selector/TranslatorSelector.module.scss @@ -34,9 +34,9 @@ gap: 1rem; } -$box_size: 6.8rem; +$box_size: 6rem; .box { - width: $box_size; + width: 9.2rem; height: $box_size; background-color: var(--dark_875_color); display: flex; @@ -44,7 +44,7 @@ $box_size: 6.8rem; align-items: center; white-space: pre-wrap; text-align: center; - border-radius: 0.6rem; + border-radius: 0.2rem; cursor: pointer; &:hover { background-color: var(--dark_825_color); diff --git a/src-ui/ui_configs.js b/src-ui/ui_configs.js index ef58fba8..4bee9957 100644 --- a/src-ui/ui_configs.js +++ b/src-ui/ui_configs.js @@ -54,11 +54,11 @@ export const ui_configs = { export const translator_status = [ { id: "DeepL", label: "DeepL", is_available: false }, - { id: "DeepL_API", label: `DeepL\nAPI`, is_available: false }, + { id: "DeepL_API", label: `DeepL API`, is_available: false }, { id: "Google", label: "Google", is_available: false }, { id: "Bing", label: "Bing", is_available: false }, { id: "Papago", label: "Papago", is_available: false }, - { id: "CTranslate2", label: `Internal\n(Default)`, is_available: false }, + { id: "CTranslate2", label: `AI\nCTranslate2`, is_available: false, is_default: true }, ]; export const ctranslate2_weight_type_status = [ From fa2c851c5bfb138ddb18931b03ebc840280a2c9c Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Sat, 15 Mar 2025 15:06:50 +0900 Subject: [PATCH 02/26] [bugfix] Main Page: LanguageSelector: Go back button: add min width for prevent to shrink its width too much. Config Page: SidebarSection: to not scroll x axis even if its label overflowed. --- .../config_page/sidebar_section/SidebarSection.module.scss | 3 +++ .../LanguageSelectorTopBar.module.scss | 7 ++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src-ui/app/config_page/sidebar_section/SidebarSection.module.scss b/src-ui/app/config_page/sidebar_section/SidebarSection.module.scss index 383ef8cb..2e27f0a8 100644 --- a/src-ui/app/config_page/sidebar_section/SidebarSection.module.scss +++ b/src-ui/app/config_page/sidebar_section/SidebarSection.module.scss @@ -9,6 +9,7 @@ flex-direction: column; justify-content: space-between; overflow-y: auto; + // overflow-x: hidden; height: 100%; max-height: 60rem; } @@ -59,7 +60,9 @@ } .tab_text { + overflow: hidden; font-size: 1.6rem; + text-overflow: ellipsis; } .separated_tabs_wrapper { diff --git a/src-ui/app/main_page/main_section/language_selector/language_selector_top_bar/LanguageSelectorTopBar.module.scss b/src-ui/app/main_page/main_section/language_selector/language_selector_top_bar/LanguageSelectorTopBar.module.scss index b2e7d219..9d0bc94a 100644 --- a/src-ui/app/main_page/main_section/language_selector/language_selector_top_bar/LanguageSelectorTopBar.module.scss +++ b/src-ui/app/main_page/main_section/language_selector/language_selector_top_bar/LanguageSelectorTopBar.module.scss @@ -16,7 +16,12 @@ position: absolute; left: 0; background-color: var(--dark_800_color); - padding: 1.2rem; + padding: 0 2rem 0 1.6rem; + height: 100%; + min-width: 8rem; + display: flex; + align-items: center; + justify-content: center; cursor: pointer; &:hover{ background-color: var(--dark_750_color); From ab39421c79dbf458f133b706b076bf987e300bad Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Sun, 16 Mar 2025 09:56:30 +0900 Subject: [PATCH 03/26] [Update/bugfix] Config Page: Transcription: Change the input design entry to dropdowns. It will fix the bug that cant put e.g. the user wanna put "10" actually, then, type "1" at first, but validation says "1" is not allowed. --- .../transcription/Transcription.jsx | 157 +++++++----------- src-ui/utils.js | 8 + 2 files changed, 64 insertions(+), 101 deletions(-) diff --git a/src-ui/app/config_page/setting_section/setting_box/transcription/Transcription.jsx b/src-ui/app/config_page/setting_section/setting_box/transcription/Transcription.jsx index e7294a18..c7c9368d 100644 --- a/src-ui/app/config_page/setting_section/setting_box/transcription/Transcription.jsx +++ b/src-ui/app/config_page/setting_section/setting_box/transcription/Transcription.jsx @@ -1,7 +1,6 @@ -import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import styles from "./Transcription.module.scss"; -import { updateLabelsById } from "@utils"; +import { updateLabelsById, genNumObjArray } from "@utils"; import { useMicRecordTimeout, @@ -21,7 +20,6 @@ import { } from "@logics_configs"; import { - EntryContainer, WordFilterContainer, DownloadModelsContainer, RadioButtonContainer, @@ -59,82 +57,61 @@ const Mic_Container = () => { const MicRecordTimeout_Box = () => { const { t } = useTranslation(); - const [ui_variable, setUiVariable] = useState(""); const { currentMicRecordTimeout, setMicRecordTimeout } = useMicRecordTimeout(); - const onChangeFunction = (e) => { - const value = e.currentTarget.value; - if (value === "") { - setUiVariable(""); - } else { - setUiVariable(value); - setMicRecordTimeout(value); - } + + const selectFunction = (selected_data) => { + setMicRecordTimeout(selected_data.selected_id); }; - useEffect(()=> { - setUiVariable(currentMicRecordTimeout.data); - }, [currentMicRecordTimeout]); - return ( - ); }; const MicPhraseTimeout_Box = () => { const { t } = useTranslation(); - const [ui_variable, setUiVariable] = useState(""); const { currentMicPhraseTimeout, setMicPhraseTimeout } = useMicPhraseTimeout(); - const onChangeFunction = (e) => { - const value = e.currentTarget.value; - if (value === "") { - setUiVariable(""); - } else { - setUiVariable(value); - setMicPhraseTimeout(value); - } + + const selectFunction = (selected_data) => { + setMicPhraseTimeout(selected_data.selected_id); }; - useEffect(()=> { - setUiVariable(currentMicPhraseTimeout.data); - }, [currentMicPhraseTimeout]); - return ( - ); }; const MicMaxWords_Box = () => { const { t } = useTranslation(); - const [ui_variable, setUiVariable] = useState(""); const { currentMicMaxWords, setMicMaxWords } = useMicMaxWords(); - const onChangeFunction = (e) => { - const value = e.currentTarget.value; - if (value === "") { - setUiVariable(""); - } else { - setUiVariable(value); - setMicMaxWords(value); - } + + const selectFunction = (selected_data) => { + setMicMaxWords(selected_data.selected_id); }; - useEffect(()=> { - setUiVariable(currentMicMaxWords.data); - }, [currentMicMaxWords]); - return ( - ); }; @@ -167,82 +144,60 @@ const Speaker_Container = () => { const SpeakerRecordTimeout_Box = () => { const { t } = useTranslation(); - const [ui_variable, setUiVariable] = useState(""); const { currentSpeakerRecordTimeout, setSpeakerRecordTimeout } = useSpeakerRecordTimeout(); - const onChangeFunction = (e) => { - const value = e.currentTarget.value; - if (value === "") { - setUiVariable(""); - } else { - setUiVariable(value); - setSpeakerRecordTimeout(value); - } + + const selectFunction = (selected_data) => { + setSpeakerRecordTimeout(selected_data.selected_id); }; - useEffect(()=> { - setUiVariable(currentSpeakerRecordTimeout.data); - }, [currentSpeakerRecordTimeout]); - return ( - ); }; const SpeakerPhraseTimeout_Box = () => { const { t } = useTranslation(); - const [ui_variable, setUiVariable] = useState(""); const { currentSpeakerPhraseTimeout, setSpeakerPhraseTimeout } = useSpeakerPhraseTimeout(); - const onChangeFunction = (e) => { - const value = e.currentTarget.value; - if (value === "") { - setUiVariable(""); - } else { - setUiVariable(value); - setSpeakerPhraseTimeout(value); - } + + const selectFunction = (selected_data) => { + setSpeakerPhraseTimeout(selected_data.selected_id); }; - - useEffect(()=> { - setUiVariable(currentSpeakerPhraseTimeout.data); - }, [currentSpeakerPhraseTimeout]); - return ( - ); }; const SpeakerMaxWords_Box = () => { const { t } = useTranslation(); - const [ui_variable, setUiVariable] = useState(""); const { currentSpeakerMaxWords, setSpeakerMaxWords } = useSpeakerMaxWords(); - const onChangeFunction = (e) => { - const value = e.currentTarget.value; - if (value === "") { - setUiVariable(""); - } else { - setUiVariable(value); - setSpeakerMaxWords(value); - } + + const selectFunction = (selected_data) => { + setSpeakerMaxWords(selected_data.selected_id); }; - useEffect(()=> { - setUiVariable(currentSpeakerMaxWords.data); - }, [currentSpeakerMaxWords]); - return ( - ); }; diff --git a/src-ui/utils.js b/src-ui/utils.js index 145dc403..96a87c75 100644 --- a/src-ui/utils.js +++ b/src-ui/utils.js @@ -49,4 +49,12 @@ export const updateLabelsById = (data_array, updates) => { const update = updates.find(update_item => update_item.id === item.id); return update ? { ...item, label: update.label } : item; }); +}; + +export const genNumArray = (count, start_from = 0) => { + return [...Array(count).keys()].map(i => i + start_from); +}; + +export const genNumObjArray = (count, start_from = 0) => { + return arrayToObject(genNumArray(count, start_from)); }; \ No newline at end of file From 84a116291c4f5722392d657b3caab93029e6827d Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Sun, 16 Mar 2025 10:44:01 +0900 Subject: [PATCH 04/26] [bugfix] controller.py: fix typo mic/speaker, error messages. --- src-python/controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src-python/controller.py b/src-python/controller.py index bb94bf26..f0356287 100644 --- a/src-python/controller.py +++ b/src-python/controller.py @@ -150,7 +150,7 @@ class Controller: 400, self.run_mapping["error_device"], { - "message":"No mic device detected", + "message":"No Speaker device detected", "data": None }, ) @@ -777,7 +777,7 @@ class Controller: response = { "status":400, "result":{ - "message":"Speaker energy threshold value is out of range", + "message":"Mic energy threshold value is out of range", "data": config.MIC_THRESHOLD } } From c82a89a7fb4fe9f8822babbbf6c65fa811a5547e Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Mon, 17 Mar 2025 15:58:16 +0900 Subject: [PATCH 05/26] [Update] UI: Add error handlings and show error notifications. adjust each localization for it. --- locales/en.yml | 27 +++-- locales/ja.yml | 28 +++-- locales/ko.yml | 28 +++-- locales/zh-Hans.yml | 28 +++-- locales/zh-Hant.yml | 28 +++-- src-ui/logics/_useBackendErrorHandling.js | 124 ++++++++++++++++++++++ src-ui/logics/useReceiveRoutes.js | 66 ++++++++---- 7 files changed, 262 insertions(+), 67 deletions(-) create mode 100644 src-ui/logics/_useBackendErrorHandling.js diff --git a/locales/en.yml b/locales/en.yml index 10969bf9..7cdd86aa 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -6,6 +6,24 @@ common: go_back_button_label: "Go Back" +common_error: + no_device_mic: "No mic device detected." + no_device_speaker: "No Speaker device detected." + threshold_invalid_value: "You can set it with a value between {{min}} to {{max}}." + failed_download_weight_ctranslate2: "CTranslate2 weight download error." + failed_download_weight_whisper: "Whisper weight download error." + translation_limit: "Translation engine limit error." + deepl_auth_key_invalid_length: "DeepL auth key length is not correct." + deepl_auth_key_failed_authentication: "Authentication failure of deepL auth key." + + invalid_value_mic_record_timeout: "It cannot be greater than '{{mic_phrase_timeout_label}}' with a value of 0 or more." + invalid_value_mic_phrase_timeout: "It cannot be set lower than '{{mic_record_timeout_label}}' with a value of 0 or more." + invalid_value_mic_max_phrase: "You can set a number equal to or greater than 0." + + invalid_value_speaker_record_timeout: "It cannot be greater than '{{speaker_phrase_timeout_label}}' with a value of 0 or more." + invalid_value_speaker_phrase_timeout: "It cannot be set lower than '{{speaker_record_timeout_label}}' with a value of 0 or more." + invalid_value_speaker_max_phrase: "You can set a number equal to or greater than 0." + main_page: translation: "Translation" transcription_send: "Voice2Chatbox" @@ -75,7 +93,6 @@ config_page: desc_for_automatic: "Automatically determine microphone input sensitivity." label_for_manual: "Mic Energy Threshold (Current Setting: Manual)" desc_for_manual: "Manually determine the microphone input sensitivity using the slider. Press the microphone icon to input your voice and adjust the sensitivity while monitoring the volume." - error_message: "You can set it with a value between 0 to {{max}}." speaker_device: label: "Speaker Device" label_auto_select: "Auto Select" @@ -85,8 +102,6 @@ config_page: desc_for_automatic: "Automatically determine speaker input sensitivity." label_for_manual: "Speaker Energy Threshold (Current Setting: Manual)" desc_for_manual: "Manually determine the speaker input sensitivity using the slider. Press the headphones icon to listen to the audio and adjust the sensitivity while monitoring the volume." - error_message: "You can set it with a value between 0 to {{max}}." - no_device_error_message: "No speaker device detected." appearance: transparency: @@ -131,15 +146,12 @@ config_page: mic_record_timeout: label: "Mic Record Timeout" desc: "Detects silence and, when the specified number of seconds has passed, considers the mic input to have ended. (Second(s))" - error_message: "It cannot be greater than '{{mic_phrase_timeout_label}}' with a value of 0 or more." mic_phrase_timeout: label: "Mic Phrase Timeout" desc: "Transcription processing is performed at intervals of the specified number of seconds." - error_message: "It cannot be set lower than '{{mic_record_timeout_label}}' with a value of 0 or more." mic_max_phrase: label: "Mic Max Words" desc: "It is the lower limit for the number of transcribed words, and only when this number is exceeded will the transcription results be displayed logs and send to VRChat." - error_message: "You can set a number equal to or greater than 0." mic_word_filter: label: "Mic Word Filter" desc: "If a registered word is detected, the text will not be sent. To add multiple words at once, separate them with a ',' (comma).\n*Duplicate words will not be registered." @@ -148,15 +160,12 @@ config_page: speaker_record_timeout: label: "Speaker Record Timeout" desc: "Detects silence and, when the specified number of seconds has passed, considers the speaker input to have ended. (Second(s))" - error_message: "It cannot be greater than '{{speaker_phrase_timeout_label}}' with a value of 0 or more." speaker_phrase_timeout: label: "Speaker Phrase Timeout" desc: "Transcription processing is performed at intervals of the specified number of seconds." - error_message: "It cannot be set lower than '{{speaker_record_timeout_label}}' with a value of 0 or more." speaker_max_phrase: label: "Speaker Max Words" desc: "It is the lower limit for the number of transcribed words, and only when this number is exceeded will the transcription results be displayed logs." - error_message: "You can set a number equal to or greater than 0." select_transcription_engine: label: "Transcription Engine" whisper_weight_type: diff --git a/locales/ja.yml b/locales/ja.yml index e92853e9..51a7c631 100644 --- a/locales/ja.yml +++ b/locales/ja.yml @@ -6,6 +6,24 @@ common: go_back_button_label: "戻る" +common_error: + no_device_mic: "マイクデバイスが検出されませんでした。" + no_device_speaker: "スピーカーデバイスが検出されませんでした。" + threshold_invalid_value: "{{min}} から {{max}} までの数値で設定できます。" + failed_download_weight_ctranslate2: "CTranslate2 モデルのダウンロードに失敗しました。" + failed_download_weight_whisper: "Whisper モデルのダウンロードに失敗しました。" + translation_limit: "翻訳エンジンの使用制限に達したか、一時的に制限がかけられています。" + deepl_auth_key_invalid_length: "認証キーの文字数が間違っています。" + deepl_auth_key_failed_authentication: "認証キーが間違っているか、API使用制限が上限に達しています。" + + invalid_value_mic_record_timeout: "0 以上で 「{{mic_phrase_timeout_label}}」 より大きくすることはできません。" + invalid_value_mic_phrase_timeout: "0 以上で 「{{mic_record_timeout_label}}」 より小さくすることはできません。" + invalid_value_mic_max_phrase: "0以上の数値を設定できます。" + + invalid_value_speaker_record_timeout: "0 以上で 「{{speaker_phrase_timeout_label}}」 より大きくすることはできません。" + invalid_value_speaker_phrase_timeout: "0 以上で 「{{speaker_record_timeout_label}}」 より小さくすることはできません。" + invalid_value_speaker_max_phrase: "0以上の数値を設定できます。" + main_page: translation: "翻訳" transcription_send: "音声認識(マイク)" @@ -75,7 +93,6 @@ config_page: desc_for_automatic: "マイクの入力感度を自動的に調節する。" label_for_manual: "マイク入力感度の調整 (現在の設定: 手動)" desc_for_manual: "スライダーを調整して入力感度を手動で決められます。マイクのアイコンを押すと、実際に声を入力し、音量を確認しながら調節できます。" - error_message: "0 から {{max}} までの数値で設定できます。" speaker_device: label: "スピーカー (デバイス)" label_auto_select: "自動選択" @@ -84,8 +101,6 @@ config_page: desc_for_automatic: "スピーカーの入力感度を自動的に調節する。" label_for_manual: "スピーカー入力感度の調整 (現在の設定: 手動)" desc_for_manual: "スライダーを調整して入力感度を手動で決められます。ヘッドフォンのアイコンを押すと、実際に音声を聞き取り、音量を確認しながら調節できます。" - error_message: "0 から {{max}} までの数値で設定できます。" - no_device_error_message: "スピーカーデバイスが検出されませんでした。" appearance: transparency: @@ -121,7 +136,6 @@ config_page: save: "保存" edit: "編集" auth_key_success: "認証キーの更新が完了しました。" - auth_key_error: "認証キーが間違っているか、API使用制限が上限に達しています。" transcription: section_label_mic: "マイク" @@ -130,15 +144,12 @@ config_page: mic_record_timeout: label: "入力が終了したとみなす無音時間" desc: "無音を検出し、設定された秒数経過すると、音声入力が終了したとみなします。" - error_message: "0 以上で 「{{mic_phrase_timeout_label}}」より大きくすることはできません。" mic_phrase_timeout: label: "一度に文字起こしする時間の長さ" desc: "設定された秒数ごとに文字起こし処理が行われます。" - error_message: "0 以上で 「{{mic_record_timeout_label}}」より小さくすることはできません。" mic_max_phrase: label: "送信するまでに保持する単語数" desc: "文字起こしされた単語数の下限値で、この数値を超えた場合のみ結果をVRChatへ送信し、ログに表示します。" - error_message: "0以上の数値を設定できます。" mic_word_filter: label: "ワードフィルター" desc: "登録された単語を検出すると、その文章は送信されません。\n「,」カンマで区切ると、まとめて複数の単語を追加できます。\n※重複した単語は登録されません。" @@ -147,15 +158,12 @@ config_page: speaker_record_timeout: label: "入力が終了したとみなす無音時間" desc: "無音を検出し、設定された秒数経過すると、音声入力が終了したとみなします。" - error_message: "0 以上で「{{speaker_phrase_timeout_label}}」より大きくすることはできません。" speaker_phrase_timeout: label: "一度に文字起こしする時間の長さ" desc: "設定された秒数ごとに文字起こし処理が行われます。" - error_message: "0 以上で「{{speaker_record_timeout_label}}」より小さくすることはできません。" speaker_max_phrase: label: "ログとして表示するまでに保持する単語数" desc: "文字起こしされた単語数の下限値で、この数値を超えた場合のみ結果をログに表示します。" - error_message: "0以上の数値を設定できます。" select_transcription_engine: label: "音声認識で使用するエンジン" whisper_weight_type: diff --git a/locales/ko.yml b/locales/ko.yml index 8f223f10..289db240 100644 --- a/locales/ko.yml +++ b/locales/ko.yml @@ -6,6 +6,24 @@ common: go_back_button_label: "돌아가기" +common_error: + no_device_mic: "마이크 디바이스를 찾지 못했습니다." + no_device_speaker: "스피커 디바이스를 찾지 못했습니다." + threshold_invalid_value: + failed_download_weight_ctranslate2: + failed_download_weight_whisper: + translation_limit: + deepl_auth_key_invalid_length: + deepl_auth_key_failed_authentication: "인증키가 잘못되었거나 API 사용 제한이 상한에 도달했습니다." + + invalid_value_mic_record_timeout: "0 이상에서 '{{mic_phrase_timeout_label}}'보다 클 수 없습니다." + invalid_value_mic_phrase_timeout: "0 이상에서 '{{mic_record_timeout_label}}'보다 작을 수 없습니다." + invalid_value_mic_max_phrase: "0 이상의 숫자만 설정할 수 있습니다." + + invalid_value_speaker_record_timeout: "0 이상에서 '{{speaker_phrase_timeout_label}}'보다 클 수 없습니다." + invalid_value_speaker_phrase_timeout: "0 이상에서 '{{speaker_record_timeout_label}}'보다 작을 수 없습니다." + invalid_value_speaker_max_phrase: "0 이상의 숫자만 설정할 수 있습니다." + main_page: translation: "번역" transcription_send: "음성인식 (마이크)" @@ -60,7 +78,6 @@ config_page: desc_for_automatic: "마이크의 입력 감도를 자동으로 조절합니다." label_for_manual: "음성 입력 최소 볼륨 (현재 설정: 수동)" desc_for_manual: "슬라이더를 움직여 입력 감도를 수동으로 조절합니다. 마이크 아이콘을 누르면 실제 음성의 볼륨을 확인하며 감도를 조절할 수 있습니다." - error_message: "0에서 {{max}}까지의 숫자로만 설정할 수 있습니다." speaker_device: label: "스피커 장치" speaker_dynamic_energy_threshold: @@ -68,8 +85,6 @@ config_page: desc_for_automatic: "스피커의 입력 감도를 자동으로 조절합니다." label_for_manual: "음성 입력 최소 볼륨 (현재 설정: 수동)" desc_for_manual: "슬라이더를 움직여 입력 감도를 수동으로 조절합니다. 헤드폰 아이콘을 누르면 실제 음성의 볼륨을 확인하며 감도를 조절할 수 있습니다." - error_message: "0에서 {{max}}까지의 숫자로만 설정할 수 있습니다." - no_device_error_message: "스피커 디바이스를 찾지 못했습니다." appearance: transparency: @@ -101,7 +116,6 @@ config_page: desc: "사용시 메인화면에 있는 {{translator}}를 DeepL_API로 변경해 주세요.\n지원하지 않는 언어도 있습니다." open_auth_key_webpage: "DeepL 계정 페이지 열기" auth_key_success: "인증키 갱신이 완료되었습니다." - auth_key_error: "인증키가 잘못되었거나 API 사용 제한이 상한에 도달했습니다." transcription: section_label_mic: "마이크" @@ -109,15 +123,12 @@ config_page: mic_record_timeout: label: "최대 무음 시간" desc: "무음을 감지하고 설정된 시간(초)만큼의 시간이 지나면 음성 입력이 종료된 것으로 판단합니다." - error_message: "0 이상에서 '{{mic_phrase_timeout_label}}'보다 클 수 없습니다." mic_phrase_timeout: label: "최대 인식 시간" desc: "설정된 초 단위로 음성인식 처리가 이루어집니다." - error_message: "0 이상에서 '{{mic_record_timeout_label}}'보다 작을 수 없습니다." mic_max_phrase: label: "최대 입력 절(phrases) 수" desc: "인식된 단어 수의 하한값으로, 이 수치를 초과하는 경우에만 결과를 VRChat으로 전송하고 로그에 표시합니다." - error_message: "0 이상의 숫자만 설정할 수 있습니다." mic_word_filter: label: "단어 필터" desc: "등록된 단어가 감지되면 해당 문장은 전송되지 않습니다.\n',' 쉼표로 구분하면 여러 단어를 추가할 수 있습니다.\n* 중복된 단어는 등록되지 않습니다." @@ -126,15 +137,12 @@ config_page: speaker_record_timeout: label: "최대 무음 시간" desc: "무음을 감지하고 설정된 시간(초)만큼의 시간이 지나면 음성 입력이 종료된 것으로 판단합니다." - error_message: "0 이상에서 '{{speaker_phrase_timeout_label}}'보다 클 수 없습니다." speaker_phrase_timeout: label: "최대 인식 시간" desc: "설정된 초 단위로 음성인식 처리가 이루어집니다." - error_message: "0 이상에서 '{{speaker_record_timeout_label}}'보다 작을 수 없습니다." speaker_max_phrase: label: "최대 입력 절(phrases) 수" desc: "식된 단어 수의 하한값으로, 이 수치를 초과하는 경우에만 결과를 로그에 표시합니다." - error_message: "0 이상의 숫자만 설정할 수 있습니다." use_whisper_feature: label: "음성 인식에 Whisper 모델을 사용" desc: "일부 언어에서는 음성 인식의 정확도가 향상될 수 있어요. 음성 인식 중 CPU 사용률이 올라가기 때문에 사용하시는 PC의 사양을 고려하여 이 기능을 사용해주세요." diff --git a/locales/zh-Hans.yml b/locales/zh-Hans.yml index d0a8d260..9a7eff18 100644 --- a/locales/zh-Hans.yml +++ b/locales/zh-Hans.yml @@ -6,6 +6,24 @@ common: go_back_button_label: "返回" +common_error: + no_device_mic: # 未检测到他人语音 ? + no_device_speaker: # 未检测到他人语音 ? + threshold_invalid_value: # 数值应为 {{min}} 至 {{max}} 之间。 ? 设定的数值从 {{min}} 到 {{max}} ? + failed_download_weight_ctranslate2: + failed_download_weight_whisper: + translation_limit: + deepl_auth_key_invalid_length: + deepl_auth_key_failed_authentication: "授权密匙错误或已达API使用上限" + + invalid_value_mic_record_timeout: "数值应为 0 至 「{{mic_phrase_timeout_label}}」" + invalid_value_mic_phrase_timeout: "转录间隔时间大于0秒且不能小于「{{mic_record_timeout_label}}」" + invalid_value_mic_max_phrase: "数值应为 0 以上" + + invalid_value_speaker_record_timeout: "数值应为 0 至 「{{speaker_phrase_timeout_label}}」" + invalid_value_speaker_phrase_timeout: "转录间隔时间大于0秒且不能小于「{{speaker_record_timeout_label}}」" + invalid_value_speaker_max_phrase: "数值应为 0 以上" + main_page: translation: "翻译" transcription_send: "你的语音转文字" @@ -60,7 +78,6 @@ config_page: desc_for_automatic: "自动调整麦克风输入阈值" label_for_manual: "麦克风输入阈值(当前设置:手动)" desc_for_manual: "使用滑杆手动确定麦克风输入灵敏度。按下麦克风图标输入语音,并在监控音量的同时调节灵敏度。" - error_message: "数值应为 0 至 {{max}} 之间。" speaker_device: label: "他人语音 (设备)" speaker_dynamic_energy_threshold: @@ -68,8 +85,6 @@ config_page: desc_for_automatic: "自动调节他人语音接收阈值" label_for_manual: "他人语音接收阈值(当前设置:手动)" desc_for_manual: "使用滑杆手动调整他人语音接收阈值.在按下耳机按钮时,请根据实际听到的声音调整该大小" - error_message: "设定的数值从 0 到 {{max}}" - no_device_error_message: "未检测到他人语音" appearance: transparency: @@ -101,7 +116,6 @@ config_page: desc: "在使用的时候,使用时请在主屏幕上通过 DeepL_API 选择 {{translator}}\n※某些语言可能不支持" open_auth_key_webpage: "打开DeepL账号页面" auth_key_success: "授权密匙认证完成。" - auth_key_error: "授权密匙错误或已达API使用上限" transcription: section_label_mic: "你的麦克风" @@ -109,15 +123,12 @@ config_page: mic_record_timeout: label: "语音输入结束后的静音时间" desc: "当检测到静音并经过设定的秒数后,语音输入即被视为完成。" - error_message: "数值应为 0 至 [{{mic_phrase_timeout_label}}]" mic_phrase_timeout: label: "转录间隔" desc: "在经过设定的时间后执行转录" - error_message: "转录间隔时间大于0秒且不能小于「{{mic_record_timeout_label}}」" mic_max_phrase: label: "麦克风发送时的最小单词数" desc: "转录字数的下限,只有超过这个数字,才会记录翻译结果并发送到VRC" - error_message: "数值应为 0 以上" mic_word_filter: label: "单词过滤器" desc: "检测出被记录的单词时,不会发送这段话\n如要添加多个单词,可以用逗号来分割\n※不会记录重复的单词" @@ -126,15 +137,12 @@ config_page: speaker_record_timeout: label: "语音接收结束后的静音时间" desc: "当检测到静音并经过设定的秒数后,语音接收即被视为完成。" - error_message: "数值应为 0 至 「{{speaker_phrase_timeout_label}}」" speaker_phrase_timeout: label: "转录间隔" desc: "在经过设定的时间后执行转录" - error_message: "转录间隔时间大于0秒且不能小于「{{speaker_record_timeout_label}}」" speaker_max_phrase: label: "语音接收时的最小单词数" desc: "转录字数的下限,只有超过这个数字,才会记录转录结果" - error_message: "数值应为 0 以上" use_whisper_feature: label: "使用Whisper模型翻译" desc: "在某些语言中,语音识别的准确性可能会提高.语音识别的过程中,CPU占有率可能会提高,请根据你的pc性能来决定是否使用它." diff --git a/locales/zh-Hant.yml b/locales/zh-Hant.yml index 0a70011b..7b074da9 100644 --- a/locales/zh-Hant.yml +++ b/locales/zh-Hant.yml @@ -6,6 +6,24 @@ common: go_back_button_label: "返回" +common_error: + no_device_mic: + no_device_speaker: "未偵測到喇叭裝置。" + threshold_invalid_value: "可以設置 {{min}} 到 {{max}} 之間的值。" + failed_download_weight_ctranslate2: + failed_download_weight_whisper: + translation_limit: + deepl_auth_key_invalid_length: + deepl_auth_key_failed_authentication: "授權金鑰錯誤或已達使用上限。" + + invalid_value_mic_record_timeout: "不能大於「{{mic_phrase_timeout_label}}」,應為 0 或更高。" + invalid_value_mic_phrase_timeout: "不能小於「{{mic_record_timeout_label}}」,應為 0 或更高。" + invalid_value_mic_max_phrase: "可以設置 0 或更高的數值。" + + invalid_value_speaker_record_timeout: "不能大於「{{speaker_phrase_timeout_label}}」,應為 0 或更高。" + invalid_value_speaker_phrase_timeout: "不能小於「{{speaker_record_timeout_label}}」,應為 0 或更高。" + invalid_value_speaker_max_phrase: "可以設置 0 或更高的數值。" + main_page: translation: "翻譯" transcription_send: "麥克風轉文字" @@ -61,7 +79,6 @@ config_page: desc_for_automatic: "自動判定麥克風輸入靈敏度。" label_for_manual: "麥克風能量閾值(當前設置:手動)" desc_for_manual: "使用滑桿調整麥克風輸入靈敏度,你可以按下麥克風圖示來測試。" - error_message: "可以設置 0 到 {{max}} 之間的值。" speaker_device: label: "喇叭裝置" speaker_dynamic_energy_threshold: @@ -69,8 +86,6 @@ config_page: desc_for_automatic: "自動確定喇叭輸入靈敏度。" label_for_manual: "喇叭能量閾值(當前設置:手動)" desc_for_manual: "使用滑桿調整喇叭輸入靈敏度,你可以按下喇叭圖示來測試。" - error_message: "可以設置 0 到 {{max}} 之間的值。" - no_device_error_message: "未偵測到喇叭裝置。" appearance: transparency: @@ -102,7 +117,6 @@ config_page: desc: "使用 DeepL API 時請在主螢幕選擇 {{translator}}。※可能不支援某些語言。" open_auth_key_webpage: "打開 DeepL 帳號頁面" auth_key_success: "授權金鑰更新完成。" - auth_key_error: "授權金鑰錯誤或已達使用上限。" transcription: section_label_mic: "麥克風" @@ -110,15 +124,12 @@ config_page: mic_record_timeout: label: "麥克風音訊 - 判定結束時間" desc: "麥克風未收到音訊後,結束一段話的判定時間(秒)。" - error_message: "不能大於「{{mic_phrase_timeout_label}}」,應為 0 或更高。" mic_phrase_timeout: label: "麥克風音訊 - 紀錄間隔時間" desc: "每隔多久要紀錄一次音訊。" - error_message: "不能小於「{{mic_record_timeout_label}}」,應為 0 或更高。" mic_max_phrase: label: "麥克風音訊 - 最大單詞數量" desc: "只有在單詞超過此數量時,才會記錄結果並發送到 VRChat。" - error_message: "可以設置為 0 或更高的數值。" mic_word_filter: label: "麥克風單詞過濾器" desc: "如果偵測到清單內的單詞,則不會發送訊息。要一次新增多個詞語,請用「,」(半形逗號)分隔。\n*重複詞語會被忽略。" @@ -127,15 +138,12 @@ config_page: speaker_record_timeout: label: "喇叭音訊 - 判定結束時間" desc: "偵測到靜音並在指定秒數後認為喇叭輸入已結束。(秒)" - error_message: "不能大於「{{speaker_phrase_timeout_label}}」,應為 0 或更高。" speaker_phrase_timeout: label: "喇叭音訊 - 紀錄間隔時間" desc: "以指定秒數間隔進行轉錄處理。" - error_message: "不能小於「{{speaker_record_timeout_label}}」,應為 0 或更高。" speaker_max_phrase: label: "喇叭音訊 - 最大單詞數量" desc: "只有在單詞超過此數量時,才會記錄結果並發送到 VRChat。" - error_message: "可以設置 0 或更高的數值。" use_whisper_feature: label: "使用 Whisper 模型進行轉錄" desc: "在某些語言中,語音識別的準確性可能會提高。使用語音識別時,CPU使用率會增加,請根據你的PC規格考慮是否使用此功能。" diff --git a/src-ui/logics/_useBackendErrorHandling.js b/src-ui/logics/_useBackendErrorHandling.js new file mode 100644 index 00000000..446330cc --- /dev/null +++ b/src-ui/logics/_useBackendErrorHandling.js @@ -0,0 +1,124 @@ +import { useTranslation } from "react-i18next"; + +import { + useNotificationStatus, +} from "@logics_common"; + +import { + useMicRecordTimeout, + useMicPhraseTimeout, + useMicMaxWords, + + useSpeakerRecordTimeout, + useSpeakerPhraseTimeout, + useSpeakerMaxWords, + + useDeepLAuthKey, +} from "@logics_configs"; +import { ui_configs } from "../ui_configs"; + +export const _useBackendErrorHandling = () => { + const { t } = useTranslation(); + const { showNotification_Error } = useNotificationStatus(); + + const { updateMicRecordTimeout } = useMicRecordTimeout(); + const { updateMicPhraseTimeout } = useMicPhraseTimeout(); + const { updateMicMaxWords } = useMicMaxWords(); + + const { updateSpeakerRecordTimeout } = useSpeakerRecordTimeout(); + const { updateSpeakerPhraseTimeout } = useSpeakerPhraseTimeout(); + const { updateSpeakerMaxWords } = useSpeakerMaxWords(); + + const { updateDeepLAuthKey } = useDeepLAuthKey(); + + const errorHandling_Backend = ({message, data, endpoint}) => { + switch (message) { + case "No mic device detected": + showNotification_Error(t("common_error.no_device_mic")); + break; + case "No Speaker device detected": + showNotification_Error(t("common_error.no_device_speaker")); + break; + + case "Mic energy threshold value is out of range": + showNotification_Error(t("common_error.threshold_invalid_value", + { min: ui_configs.mic_threshold_min, max: ui_configs.mic_threshold_max }, + )); + break; + case "Speaker energy threshold value is out of range": + showNotification_Error(t("common_error.threshold_invalid_value", + { min: ui_configs.speaker_threshold_min, max: ui_configs.speaker_threshold_max }, + )); + break; + + case "CTranslate2 weight download error": + showNotification_Error(t("common_error.failed_download_weight_ctranslate2")); + break; + case "Whisper weight download error": + showNotification_Error(t("common_error.failed_download_weight_whisper")); + break; + + case "Translation engine limit error": + showNotification_Error(t("common_error.translation_limit")); + break; + + case "DeepL auth key length is not correct": + updateDeepLAuthKey(data); + showNotification_Error(t("common_error.deepl_auth_key_invalid_length")); + break; + case "Authentication failure of deepL auth key": + updateDeepLAuthKey(data); + showNotification_Error(t("common_error.deepl_auth_key_failed_authentication")); + break; + + case "Mic record timeout value is out of range": + updateMicRecordTimeout(data); + showNotification_Error( + t("common_error.invalid_value_mic_record_timeout", + { mic_phrase_timeout_label: t("config_page.transcription.mic_phrase_timeout.label") } + )); + break; + case "Mic phrase timeout value is out of range": + updateMicPhraseTimeout(data); + showNotification_Error( + t("common_error.invalid_value_mic_phrase_timeout", + { mic_record_timeout_label: t("config_page.transcription.mic_record_timeout.label") } + )); + break; + case "Mic max phrases value is out of range": + updateMicMaxWords(data); + showNotification_Error(t("common_error.invalid_value_mic_max_phrase")); + break; + + + case "Speaker record timeout value is out of range": + updateSpeakerRecordTimeout(data); + showNotification_Error( + t("common_error.invalid_value_speaker_record_timeout", + { speaker_phrase_timeout_label: t("config_page.transcription.speaker_phrase_timeout.label") } + )); + break; + case "Speaker phrase timeout value is out of range": + updateSpeakerPhraseTimeout(data); + showNotification_Error( + t("common_error.invalid_value_speaker_phrase_timeout", + { speaker_record_timeout_label: t("config_page.transcription.speaker_record_timeout.label") } + )); + break; + case "Speaker max phrases value is out of range": + updateSpeakerMaxWords(data); + showNotification_Error(t("common_error.invalid_value_speaker_max_phrase")); + break; + + default: + if (endpoint === "/set/data/deepl_auth_key") updateDeepLAuthKey(data); + showNotification_Error(message); + break; + } + + } + + return { + errorHandling_Backend, + } +}; \ No newline at end of file diff --git a/src-ui/logics/useReceiveRoutes.js b/src-ui/logics/useReceiveRoutes.js index b3a5c4cd..3ddd38f8 100644 --- a/src-ui/logics/useReceiveRoutes.js +++ b/src-ui/logics/useReceiveRoutes.js @@ -1,6 +1,8 @@ import { translator_status } from "@ui_configs"; import { arrayToObject } from "@utils"; +import { _useBackendErrorHandling } from "./_useBackendErrorHandling"; + import { useIsVrctAvailable, useNotificationStatus, @@ -184,6 +186,10 @@ export const useReceiveRoutes = () => { const { handleNetworkConnection } = useHandleNetworkConnection(); + const { + errorHandling_Backend, + } = _useBackendErrorHandling(); + const routes = { // Common "/run/feed_watchdog": () => {}, @@ -498,16 +504,25 @@ export const useReceiveRoutes = () => { "/get/data/transcription_engines": ()=>{}, // Not implemented on UI yet. (if ai_models has not been detected, this will be blank array[]. if the ai_models are ok but just network has not connected, it'l be only ["Whisper"]) }; - const error_routes = { - "/set/data/mic_record_timeout": updateMicRecordTimeout, - "/set/data/mic_phrase_timeout": updateMicPhraseTimeout, - "/set/data/mic_max_phrases": updateMicMaxWords, + const error_status_routes = { + "/run/error_device": errorHandling_Backend, - "/set/data/speaker_record_timeout": updateSpeakerRecordTimeout, - "/set/data/speaker_phrase_timeout": updateSpeakerPhraseTimeout, - "/set/data/speaker_max_phrases": updateSpeakerMaxWords, + "/run/error_ctranslate2_weight": errorHandling_Backend, + "/run/error_whisper_weight": errorHandling_Backend, - "/set/data/deepl_auth_key": updateDeepLAuthKey, + "/set/data/deepl_auth_key": errorHandling_Backend, + + "/run/error_translation_engine": errorHandling_Backend, + + "/set/data/mic_threshold": errorHandling_Backend, + "/set/data/mic_record_timeout": errorHandling_Backend, + "/set/data/mic_phrase_timeout": errorHandling_Backend, + "/set/data/mic_max_phrases": errorHandling_Backend, + + "/set/data/speaker_threshold": errorHandling_Backend, + "/set/data/speaker_record_timeout": errorHandling_Backend, + "/set/data/speaker_phrase_timeout": errorHandling_Backend, + "/set/data/speaker_max_phrases": errorHandling_Backend, }; @@ -519,22 +534,37 @@ export const useReceiveRoutes = () => { } }; + const handleInvalidEndpoint = (parsed_data) => { + console.error(`Invalid endpoint: ${parsed_data.endpoint}\nresult: ${JSON.stringify(parsed_data.result)}`); + }; + + if (parsed_data.endpoint === "/run/initialization_complete") { + initDataSyncProcess(parsed_data.result); + updateIsBackendReady(true); + return; + }; + switch (parsed_data.status) { case 200: - if (parsed_data.endpoint === "/run/initialization_complete") { - initDataSyncProcess(parsed_data.result); - updateIsBackendReady(true); - break; - }; const route = routes[parsed_data.endpoint]; - (route) ? route(parsed_data.result) : console.error(`Invalid endpoint: ${parsed_data.endpoint}\nresult: ${JSON.stringify(parsed_data.result)}`); + if (route) { + route(parsed_data.result); + } else { + handleInvalidEndpoint(parsed_data); + } break; case 400: - const error_route = error_routes[parsed_data.endpoint]; - (error_route) ? error_route(parsed_data.result.data) : console.error(`Invalid endpoint: ${parsed_data.endpoint}\nresult: ${JSON.stringify(parsed_data.result)}`); - console.error(`status 400: ${JSON.stringify(parsed_data.result)}`); - showNotification_Error(parsed_data.result.message); + const error_route = error_status_routes[parsed_data.endpoint]; + if (error_route) { + error_route({ + message: parsed_data.result.message, + data: parsed_data.result.data, + endpoint: parsed_data.endpoint, + }); + } else { + handleInvalidEndpoint(parsed_data); + } break; case 348: From adf98b8c332281aeb249aae0a2291220411c9db2 Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Tue, 18 Mar 2025 08:23:53 +0900 Subject: [PATCH 06/26] [Update] Change sentences, Japanese. --- locales/ja.yml | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/locales/ja.yml b/locales/ja.yml index 51a7c631..b372bf9e 100644 --- a/locales/ja.yml +++ b/locales/ja.yml @@ -12,22 +12,22 @@ common_error: threshold_invalid_value: "{{min}} から {{max}} までの数値で設定できます。" failed_download_weight_ctranslate2: "CTranslate2 モデルのダウンロードに失敗しました。" failed_download_weight_whisper: "Whisper モデルのダウンロードに失敗しました。" - translation_limit: "翻訳エンジンの使用制限に達したか、一時的に制限がかけられています。" + translation_limit: "翻訳エンジンの使用制限に達したか、一時的に利用制限がかけられています。" deepl_auth_key_invalid_length: "認証キーの文字数が間違っています。" deepl_auth_key_failed_authentication: "認証キーが間違っているか、API使用制限が上限に達しています。" invalid_value_mic_record_timeout: "0 以上で 「{{mic_phrase_timeout_label}}」 より大きくすることはできません。" invalid_value_mic_phrase_timeout: "0 以上で 「{{mic_record_timeout_label}}」 より小さくすることはできません。" - invalid_value_mic_max_phrase: "0以上の数値を設定できます。" + invalid_value_mic_max_phrase: "0 以上の数値を設定できます。" invalid_value_speaker_record_timeout: "0 以上で 「{{speaker_phrase_timeout_label}}」 より大きくすることはできません。" invalid_value_speaker_phrase_timeout: "0 以上で 「{{speaker_record_timeout_label}}」 より小さくすることはできません。" - invalid_value_speaker_max_phrase: "0以上の数値を設定できます。" + invalid_value_speaker_max_phrase: "0 以上の数値を設定できます。" main_page: translation: "翻訳" - transcription_send: "音声認識(マイク)" - transcription_receive: "音声認識(スピーカー)" + transcription_send: "音声認識 マイク" + transcription_receive: "音声認識 スピーカー" foreground: "最前面固定" language_settings: "言語設定" your_language: "あなたの言語" @@ -89,17 +89,17 @@ config_page: label_host: "ホスト/ドライバー" label_device: "デバイス" mic_dynamic_energy_threshold: - label_for_automatic: "マイク入力感度の調整 (現在の設定: 自動)" + label_for_automatic: "マイク入力感度の調整 (現在の設定: 自動)" desc_for_automatic: "マイクの入力感度を自動的に調節する。" - label_for_manual: "マイク入力感度の調整 (現在の設定: 手動)" + label_for_manual: "マイク入力感度の調整 (現在の設定: 手動)" desc_for_manual: "スライダーを調整して入力感度を手動で決められます。マイクのアイコンを押すと、実際に声を入力し、音量を確認しながら調節できます。" speaker_device: label: "スピーカー (デバイス)" label_auto_select: "自動選択" speaker_dynamic_energy_threshold: - label_for_automatic: "スピーカー入力感度の調整 (現在の設定: 自動)" + label_for_automatic: "スピーカー入力感度の調整 (現在の設定: 自動)" desc_for_automatic: "スピーカーの入力感度を自動的に調節する。" - label_for_manual: "スピーカー入力感度の調整 (現在の設定: 手動)" + label_for_manual: "スピーカー入力感度の調整 (現在の設定: 手動)" desc_for_manual: "スライダーを調整して入力感度を手動で決められます。ヘッドフォンのアイコンを押すと、実際に音声を聞き取り、音量を確認しながら調節できます。" appearance: @@ -125,8 +125,8 @@ config_page: ctranslate2_weight_type: label: "AI翻訳 {{ctranslate2}} のモデルタイプ" desc: "翻訳エンジン「{{ctranslate2}}」で翻訳する際に、使用する翻訳モデルを選択できます。" - small: "通常モデル ({{capacity}})" - large: "高精度モデル ({{capacity}})" + small: "通常モデル ({{capacity}})" + large: "高精度モデル ({{capacity}})" ctranslate2_compute_device: label: "AI翻訳 {{ctranslate2}} の処理デバイス" deepl_auth_key: @@ -169,8 +169,8 @@ config_page: whisper_weight_type: label: "Whisperモデルのタイプ" desc: "容量が大きいモデルほど精度は高いですが、その分CPUやGPUを占有します。\n※特にmediumより容量の大きいモデルは、CPU/GPUの性能によっては使用すらも困難です。" - model_template: "{{model_name}} モデル ({{capacity}})" - recommended_model_template: "{{model_name}} モデル ({{capacity}}) (推奨)" + model_template: "{{model_name}} モデル ({{capacity}})" + recommended_model_template: "{{model_name}} モデル ({{capacity}}) [推奨]" whisper_compute_device: label: "Whisperで使用する処理デバイス" @@ -229,11 +229,11 @@ config_page: toggle_vrct_visibility: label: "VRCTの最小化/アクティブ化の切り替え" toggle_translation: - label: "{{translation}}機能切り替え" + label: "「{{translation}}」 オン/オフの切り替え" toggle_transcription_send: - label: "{{transcription_send}}機能切り替え" + label: "「{{transcription_send}}」 オン/オフの切り替え" toggle_transcription_receive: - label: "{{transcription_receive}}機能切り替え" + label: "「{{transcription_receive}}」 オン/オフの切り替え" advanced_settings: osc_ip_address: From c9e40fc682992885e0d459dd427f86500992e82e Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Tue, 18 Mar 2025 11:18:03 +0900 Subject: [PATCH 07/26] [Update] Adjust localizations. (every yaml key and lines are same with en.yml) --- locales/en.yml | 16 ++- locales/ja.yml | 10 +- locales/ko.yml | 100 +++++++++++++++--- locales/zh-Hans.yml | 87 ++++++++++++--- locales/zh-Hant.yml | 91 ++++++++++++---- .../setting_box/device/Device.jsx | 10 +- 6 files changed, 244 insertions(+), 70 deletions(-) diff --git a/locales/en.yml b/locales/en.yml index 7cdd86aa..175bbadb 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -14,7 +14,7 @@ common_error: failed_download_weight_whisper: "Whisper weight download error." translation_limit: "Translation engine limit error." deepl_auth_key_invalid_length: "DeepL auth key length is not correct." - deepl_auth_key_failed_authentication: "Authentication failure of deepL auth key." + deepl_auth_key_failed_authentication: "Auth Key is incorrect or Usage limit reached." invalid_value_mic_record_timeout: "It cannot be greater than '{{mic_phrase_timeout_label}}' with a value of 0 or more." invalid_value_mic_phrase_timeout: "It cannot be set lower than '{{mic_record_timeout_label}}' with a value of 0 or more." @@ -29,6 +29,7 @@ main_page: transcription_send: "Voice2Chatbox" transcription_receive: "Speaker2Log" foreground: "Foreground" + language_settings: "Language Settings" your_language: "Your Language" translate_each_other_label: "Translate Each Other" @@ -83,11 +84,11 @@ config_page: device: check_volume: "Check Volume" + label_auto_select: "Auto Select" + label_host: "Host/Driver" + label_device: "Device" mic_host_device: label: "Mic Device" - label_auto_select: "Auto Select" - label_host: "Host/Driver" - label_device: "Device" mic_dynamic_energy_threshold: label_for_automatic: "Mic Energy Threshold (Current Setting: Automatic)" desc_for_automatic: "Automatically determine microphone input sensitivity." @@ -95,8 +96,6 @@ config_page: desc_for_manual: "Manually determine the microphone input sensitivity using the slider. Press the microphone icon to input your voice and adjust the sensitivity while monitoring the volume." speaker_device: label: "Speaker Device" - label_auto_select: "Auto Select" - label_device: "Device" speaker_dynamic_energy_threshold: label_for_automatic: "Speaker Energy Threshold (Current Setting: Automatic)" desc_for_automatic: "Automatically determine speaker input sensitivity." @@ -133,11 +132,10 @@ config_page: deepl_auth_key: label: "DeepL Auth Key" desc: "Please select {{translator}} on the main screen with DeepL_API when using. ※Some languages may not be supported." + open_auth_key_webpage: "Open DeepL Account Webpage" save: "Save" edit: "Edit" - open_auth_key_webpage: "Open DeepL Account Webpage" auth_key_success: "Auth key update completed." - auth_key_error: "Auth Key is incorrect or Usage limit reached." transcription: section_label_mic: "Mic" @@ -197,11 +195,11 @@ config_page: ui_scaling: "UI Scaling" display_duration: "Display duration" fadeout_duration: "Fadeout duration" + common_settings: "Common Settings" tracker: "Tracker" hmd: "HMD" left_hand: "Left hand" right_hand: "Right hand" - common_settings: "Common Settings" overlay_show_only_translated_messages: label: "Show Only Translated Messages" diff --git a/locales/ja.yml b/locales/ja.yml index b372bf9e..8cecd68e 100644 --- a/locales/ja.yml +++ b/locales/ja.yml @@ -29,6 +29,7 @@ main_page: transcription_send: "音声認識 マイク" transcription_receive: "音声認識 スピーカー" foreground: "最前面固定" + language_settings: "言語設定" your_language: "あなたの言語" translate_each_other_label: "双方向に翻訳" @@ -83,11 +84,11 @@ config_page: device: check_volume: "音量チェック" + label_auto_select: "自動選択" + label_host: "ホスト/ドライバー" + label_device: "デバイス" mic_host_device: label: "マイク (デバイス)" - label_auto_select: "自動選択" - label_host: "ホスト/ドライバー" - label_device: "デバイス" mic_dynamic_energy_threshold: label_for_automatic: "マイク入力感度の調整 (現在の設定: 自動)" desc_for_automatic: "マイクの入力感度を自動的に調節する。" @@ -95,7 +96,6 @@ config_page: desc_for_manual: "スライダーを調整して入力感度を手動で決められます。マイクのアイコンを押すと、実際に声を入力し、音量を確認しながら調節できます。" speaker_device: label: "スピーカー (デバイス)" - label_auto_select: "自動選択" speaker_dynamic_energy_threshold: label_for_automatic: "スピーカー入力感度の調整 (現在の設定: 自動)" desc_for_automatic: "スピーカーの入力感度を自動的に調節する。" @@ -196,10 +196,10 @@ config_page: display_duration: "表示時間" fadeout_duration: "フェードアウト時間" common_settings: "共通設定" + tracker: "表示するトラッカーの位置" hmd: "HMD" left_hand: "左手" right_hand: "右手" - tracker: "表示するトラッカーの位置" overlay_show_only_translated_messages: label: "翻訳後のメッセージのみ表示する" diff --git a/locales/ko.yml b/locales/ko.yml index 289db240..853c179e 100644 --- a/locales/ko.yml +++ b/locales/ko.yml @@ -29,6 +29,7 @@ main_page: transcription_send: "음성인식 (마이크)" transcription_receive: "음성인식 (스피커)" foreground: "항상 위로" + language_settings: "언어 설정" your_language: "당신의 언어" translate_each_other_label: "양방향으로 번역" @@ -37,12 +38,18 @@ main_page: translator: "번역 엔진" # translator_label_default: "기본값" + translator_selector: + is_selected_same_language: + message_log: all: "전체" sent: "전송" received: "수신" system: "시스템" + show_resend_button: + resend_button_on_hover_desc: + state_text_enabled: "Enabled" state_text_disabled: "Disabled" @@ -54,24 +61,33 @@ main_page: updating: "업데이트 중..." update_modal: - update_software: "새 버전을 다운로드하고 재시작합니다.\n조금 시간이 걸립니다. 지금 시작할까요?" - deny_update_software: "나중에 하기" - accept_update_software: "업데이트 및 재시작" - + cpu_desc: + cuda_desc: + cuda_compare_cpu_desc: + cuda_disk_space_desc: + close_modal: + download_latest_and_restart: + is_latest_version_already: + is_current_compute_device: config_page: version: "버전 {{version}}" + model_download_button_label: side_menu_labels: + device: appearance: "모양" translation: "번역" transcription: "음성인식" others: "기타" + hotkeys: advanced_settings: "고급 설정" device: - mic_host: - label: "마이크 호스트/드라이버" - mic_device: + check_volume: + label_auto_select: + label_host: "호스트/드라이버" + label_device: + mic_host_device: label: "마이크 장치" mic_dynamic_energy_threshold: label_for_automatic: "음성 입력 최소 볼륨 (현재 설정: 자동)" @@ -111,15 +127,20 @@ config_page: desc: "오프라인 번역 시의 번역 모델을 변경합니다." small: "일반 모델 ({{capacity}})" large: "정밀 모델 ({{capacity}})" + ctranslate2_compute_device: + label: deepl_auth_key: label: "DeepL 인증키" desc: "사용시 메인화면에 있는 {{translator}}를 DeepL_API로 변경해 주세요.\n지원하지 않는 언어도 있습니다." open_auth_key_webpage: "DeepL 계정 페이지 열기" + save: + edit: auth_key_success: "인증키 갱신이 완료되었습니다." transcription: section_label_mic: "마이크" section_label_speaker: "스피커" + section_label_transcription_engines: mic_record_timeout: label: "최대 무음 시간" desc: "무음을 감지하고 설정된 시간(초)만큼의 시간이 지나면 음성 입력이 종료된 것으로 판단합니다." @@ -143,29 +164,76 @@ config_page: speaker_max_phrase: label: "최대 입력 절(phrases) 수" desc: "식된 단어 수의 하한값으로, 이 수치를 초과하는 경우에만 결과를 로그에 표시합니다." - use_whisper_feature: - label: "음성 인식에 Whisper 모델을 사용" - desc: "일부 언어에서는 음성 인식의 정확도가 향상될 수 있어요. 음성 인식 중 CPU 사용률이 올라가기 때문에 사용하시는 PC의 사양을 고려하여 이 기능을 사용해주세요." + select_transcription_engine: + label: whisper_weight_type: label: "Whisper 모델 타입" - # desc: "기본적으로 용량이 많은 모델일수록 정밀도는 높지만, 음성 인식의 시간이 늘어나며 CPU 사용률도 늘어나요.각 모델의 설명은 문서를 참조해주세요.\n※특히 medium보다 용량이 큰 모델은 CPU의 성능에 따라서는 사용조차 어려울 수 있어요. " + desc: model_template: "{{model_name}} 모델 ({{capacity}})" recommended_model_template: "{{model_name}} 모델 ({{capacity}}) (권장)" + whisper_compute_device: + label: + + vr: + single_line: + multi_lines: + overlay_enable: + restore_default_settings: + position: + rotation: + x_position: + y_position: + z_position: + x_rotation: + y_rotation: + z_rotation: + sample_text_button: + start: + stop: + sample_text: + opacity: + ui_scaling: + display_duration: + fadeout_duration: + common_settings: + tracker: + hmd: + left_hand: + right_hand: + overlay_show_only_translated_messages: + label: others: + section_label_sounds: auto_clear_the_message_box: label: "챗박스 자동 삭제" send_only_translated_messages: label: "번역된 메시지만 전송" - notice_xsoverlay: - label: "XSOverlay에서 알림 수신 기능 활성화" - desc: "수신된 메시지를 XSOverlay의 기능을 통해 알림으로 받아볼 수 있습니다." auto_export_message_logs: label: "대화 로그 자동 저장" desc: "logs 폴더에 텍스트 파일로 로그가 저장됩니다." + vrc_mic_mute_sync: + label: + desc: send_message_to_vrc: label: "VRChat에 메시지 전송" desc: "VRChat에 메시지를 보내지 않고 사용할 수 있는 방법이 있지만 지원되지 않습니다. VRChat에 메시지를 보내려면 이 기능을 활성화하세요." + notification_vrc_sfx: + label: + desc: + send_received_message_to_vrc: + label: + desc: + + hotkeys: + toggle_vrct_visibility: + label: + toggle_translation: + label: + toggle_transcription_send: + label: + toggle_transcription_receive: + label: advanced_settings: osc_ip_address: @@ -173,4 +241,6 @@ config_page: osc_port: label: "OSC 포트" open_config_filepath: - label: "설정 파일 열기" \ No newline at end of file + label: "설정 파일 열기" + switch_compute_device: + label: \ No newline at end of file diff --git a/locales/zh-Hans.yml b/locales/zh-Hans.yml index 9a7eff18..4e301a92 100644 --- a/locales/zh-Hans.yml +++ b/locales/zh-Hans.yml @@ -29,6 +29,7 @@ main_page: transcription_send: "你的语音转文字" transcription_receive: "他人语音转文字" foreground: "顶层显示" + language_settings: "语言设定" your_language: "你的语言" translate_each_other_label: "双向翻译" @@ -37,12 +38,18 @@ main_page: translator: "翻译器" # translator_label_default: "默认" + translator_selector: + is_selected_same_language: + message_log: all: "全部" sent: "发送" received: "接受" system: "系统" + show_resend_button: + resend_button_on_hover_desc: + state_text_enabled: "启用" state_text_disabled: "停用" @@ -54,24 +61,33 @@ main_page: updating: "更新中..." update_modal: - update_software_desc: "下载新版本并自动启动\n会花少许时间,现在更新吗?" - deny_update_software: "稍后再说" - accept_update_software: "更新后自动启动" - + cpu_desc: + cuda_desc: + cuda_compare_cpu_desc: + cuda_disk_space_desc: + close_modal: + download_latest_and_restart: + is_latest_version_already: + is_current_compute_device: config_page: version: "版本 {{version}}" + model_download_button_label: side_menu_labels: + device: appearance: "外观" translation: "翻译" transcription: "转录" others: "其他" + hotkeys: advanced_settings: "高级设置" device: - mic_host: - label: "麦克风(host/driver)" - mic_device: + check_volume: "Check Volume" + label_auto_select: "Auto Select" + label_host: "Host/Driver" + label_device: "Device" + mic_host_device: label: "麦克风 (设备)" mic_dynamic_energy_threshold: label_for_automatic: "麦克风输入阈值(当前设置:自动)" @@ -111,15 +127,20 @@ config_page: desc: "可以选择用于离线翻译的翻译模型" small: "普通模型 ({{capacity}})" large: "高精度模型 ({{capacity}})" + ctranslate2_compute_device: + label: deepl_auth_key: label: "DeepL 授权密匙" desc: "在使用的时候,使用时请在主屏幕上通过 DeepL_API 选择 {{translator}}\n※某些语言可能不支持" open_auth_key_webpage: "打开DeepL账号页面" + save: + edit: auth_key_success: "授权密匙认证完成。" transcription: section_label_mic: "你的麦克风" section_label_speaker: "他人声音" + section_label_transcription_engines: mic_record_timeout: label: "语音输入结束后的静音时间" desc: "当检测到静音并经过设定的秒数后,语音输入即被视为完成。" @@ -143,31 +164,47 @@ config_page: speaker_max_phrase: label: "语音接收时的最小单词数" desc: "转录字数的下限,只有超过这个数字,才会记录转录结果" - use_whisper_feature: - label: "使用Whisper模型翻译" - desc: "在某些语言中,语音识别的准确性可能会提高.语音识别的过程中,CPU占有率可能会提高,请根据你的pc性能来决定是否使用它." + select_transcription_engine: + label: whisper_weight_type: label: "选择某个Whisper模型" - # desc: |- - # 通常来说,容量越大的模型精度也会越高,但也会增加文字显示所需要的时间和CPU的使用率。请浏览各个模型的文档 - # ※特别是大于medium容量的模型、因CPU性能原因甚至无法使用。 + desc: model_template: "{{model_name}} 模型 ({{capacity}})" recommended_model_template: "{{model_name}} 模型 ({{capacity}}) (推荐)" + whisper_compute_device: + label: vr: + single_line: + multi_lines: + overlay_enable: restore_default_settings: "恢复默认设置" - opacity: "透明度" - ui_scaling: "大小" + position: + rotation: x_position: "X轴(左右)" y_position: "Y轴(上下)" z_position: "Z轴(前后)" x_rotation: "X轴旋转" y_rotation: "Y轴旋转" z_rotation: "Z轴旋转" + sample_text_button: + start: + stop: + sample_text: + opacity: "透明度" + ui_scaling: "大小" display_duration: "显示持续时间" fadeout_duration: "渐隐持续时间" + common_settings: + tracker: + hmd: + left_hand: + right_hand: + overlay_show_only_translated_messages: + label: others: + section_label_sounds: auto_clear_the_message_box: label: "发言后自动清空chatbox" send_only_translated_messages: @@ -181,6 +218,22 @@ config_page: send_message_to_vrc: label: "发送信息至VRChat" desc: "不发送信息至VRChat的情况下也能使用它,但该功能现在并未完成.在想要发送信息时,请不要忘记打开这个功能." + notification_vrc_sfx: + label: + desc: + send_received_message_to_vrc: + label: + desc: + + hotkeys: + toggle_vrct_visibility: + label: + toggle_translation: + label: + toggle_transcription_send: + label: + toggle_transcription_receive: + label: advanced_settings: osc_ip_address: @@ -188,4 +241,6 @@ config_page: osc_port: label: "OSC 端口" open_config_filepath: - label: "打开设置文件" \ No newline at end of file + label: "打开设置文件" + switch_compute_device: + label: \ No newline at end of file diff --git a/locales/zh-Hant.yml b/locales/zh-Hant.yml index 7b074da9..a204619f 100644 --- a/locales/zh-Hant.yml +++ b/locales/zh-Hant.yml @@ -29,6 +29,7 @@ main_page: transcription_send: "麥克風轉文字" transcription_receive: "喇叭轉文字" foreground: "最上層顯示" + language_settings: "語言設定" your_language: "你的語言" translate_each_other_label: "互相翻譯" @@ -37,12 +38,18 @@ main_page: translator: "翻譯器" # translator_label_default: "預設" + translator_selector: + is_selected_same_language: + message_log: all: "全部" sent: "已發送" received: "已接收" system: "系統" + show_resend_button: + resend_button_on_hover_desc: + state_text_enabled: "啟用" state_text_disabled: "停用" @@ -54,25 +61,33 @@ main_page: updating: "正在更新..." update_modal: - update_software_desc: "下載新版本並自動更新 VRCT。\n會花一些時間,現在更新嗎?" - deny_update_software: "稍後再說" - accept_update_software: "更新" - + cpu_desc: + cuda_desc: + cuda_compare_cpu_desc: + cuda_disk_space_desc: + close_modal: + download_latest_and_restart: + is_latest_version_already: + is_current_compute_device: config_page: version: "版本 {{version}}" + model_download_button_label: side_menu_labels: + device: appearance: "外觀" translation: "翻譯" transcription: "轉錄" - vr: "VR" others: "其他" + hotkeys: advanced_settings: "進階設定" device: - mic_host: - label: "麥克風 Host/Driver" - mic_device: + check_volume: "Check Volume" + label_auto_select: "Auto Select" + label_host: "Host/Driver" + label_device: "Device" + mic_host_device: label: "麥克風裝置" mic_dynamic_energy_threshold: label_for_automatic: "麥克風能量閾值(當前設置:自動)" @@ -112,15 +127,20 @@ config_page: desc: "你可以選擇用於離線翻譯引擎的翻譯模型。" small: "基本模型({{capacity}})" large: "高準確率模型({{capacity}})" + ctranslate2_compute_device: + label: deepl_auth_key: label: "DeepL 授權金鑰" desc: "使用 DeepL API 時請在主螢幕選擇 {{translator}}。※可能不支援某些語言。" open_auth_key_webpage: "打開 DeepL 帳號頁面" + save: + edit: auth_key_success: "授權金鑰更新完成。" transcription: section_label_mic: "麥克風" section_label_speaker: "喇叭" + section_label_transcription_engines: mic_record_timeout: label: "麥克風音訊 - 判定結束時間" desc: "麥克風未收到音訊後,結束一段話的判定時間(秒)。" @@ -144,38 +164,51 @@ config_page: speaker_max_phrase: label: "喇叭音訊 - 最大單詞數量" desc: "只有在單詞超過此數量時,才會記錄結果並發送到 VRChat。" - use_whisper_feature: - label: "使用 Whisper 模型進行轉錄" - desc: "在某些語言中,語音識別的準確性可能會提高。使用語音識別時,CPU使用率會增加,請根據你的PC規格考慮是否使用此功能。" + select_transcription_engine: + label: whisper_weight_type: label: "選擇 Whisper 模型" - # desc: |- - # 一般來說,容量較大的模型往往具有更高的準確性,但這也導致轉錄時間較長和CPU使用率增加。請參考文檔了解各模型的說明。 - # ※特別是超過中等大小的模型,根據CPU性能可能難以運行。 + desc: model_template: "{{model_name}}模型({{capacity}})" recommended_model_template: "{{model_name}}模型({{capacity}})(推薦)" + whisper_compute_device: + label: vr: + single_line: + multi_lines: + overlay_enable: restore_default_settings: "恢復預設設定" - opacity: "透明度" - ui_scaling: "介面縮放" + position: + rotation: x_position: "X軸(左右)" y_position: "Y軸(上下)" z_position: "Z軸(前後)" x_rotation: "X軸旋轉" y_rotation: "Y軸旋轉" z_rotation: "Z軸旋轉" + sample_text_button: + start: + stop: + sample_text: + opacity: "透明度" + ui_scaling: "介面縮放" display_duration: "顯示持續時間" fadeout_duration: "淡出持續時間" + common_settings: + tracker: + hmd: + left_hand: + right_hand: + overlay_show_only_translated_messages: + label: others: + section_label_sounds: auto_clear_the_message_box: label: "自動清除 Chatbox" send_only_translated_messages: label: "僅發送翻譯訊息" - notice_xsoverlay: - label: "XSOverlay 通知" - desc: "從 XSOverlay 的通知功能接收訊息。" auto_export_message_logs: label: "自動匯出訊息記錄" desc: "自動將對話訊息匯出為文字文件。" @@ -185,6 +218,22 @@ config_page: send_message_to_vrc: label: "發送訊息到 VRChat" desc: "當你打算向 VRChat 發送訊息時啟用此功能。" + notification_vrc_sfx: + label: + desc: + send_received_message_to_vrc: + label: + desc: + + hotkeys: + toggle_vrct_visibility: + label: + toggle_translation: + label: + toggle_transcription_send: + label: + toggle_transcription_receive: + label: advanced_settings: osc_ip_address: @@ -192,4 +241,6 @@ config_page: osc_port: label: "OSC 端口" open_config_filepath: - label: "打開設定文件" \ No newline at end of file + label: "打開設定文件" + switch_compute_device: + label: \ No newline at end of file diff --git a/src-ui/app/config_page/setting_section/setting_box/device/Device.jsx b/src-ui/app/config_page/setting_section/setting_box/device/Device.jsx index 958cddf8..56b11398 100644 --- a/src-ui/app/config_page/setting_section/setting_box/device/Device.jsx +++ b/src-ui/app/config_page/setting_section/setting_box/device/Device.jsx @@ -82,7 +82,7 @@ const Mic_Container = () => {
-

{t("config_page.device.mic_host_device.label_auto_select")}

+

{t("config_page.device.label_auto_select")}

{
-

{t("config_page.device.mic_host_device.label_host")}

+

{t("config_page.device.label_host")}

{
-

{t("config_page.device.mic_host_device.label_device")}

+

{t("config_page.device.label_device")}

{
-

{t("config_page.device.speaker_device.label_auto_select")}

+

{t("config_page.device.label_auto_select")}

{
-

{t("config_page.device.mic_host_device.label_device")}

+

{t("config_page.device.label_device")}

Date: Tue, 18 Mar 2025 15:26:22 +0900 Subject: [PATCH 08/26] [Update] Main Page: TranslatorSelector: Add the label 'default'. --- locales/en.yml | 2 +- locales/ja.yml | 2 +- locales/ko.yml | 2 +- locales/zh-Hans.yml | 2 +- locales/zh-Hant.yml | 2 +- .../TranslatorSelector.jsx | 10 ++++++- .../TranslatorSelector.module.scss | 30 ++++++++++++++----- 7 files changed, 37 insertions(+), 13 deletions(-) diff --git a/locales/en.yml b/locales/en.yml index 175bbadb..7c677718 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -36,7 +36,7 @@ main_page: swap_button_label: "Swap Languages" target_language: "Target Language" translator: "Translator" - # translator_label_default: "Default" + translator_label_default: "Default" translator_selector: is_selected_same_language: "Since the same language is selected for both '{{your_language}}' and '{{target_language}}', only '{{ctranslate2}}' is available." diff --git a/locales/ja.yml b/locales/ja.yml index 8cecd68e..dfda745c 100644 --- a/locales/ja.yml +++ b/locales/ja.yml @@ -36,7 +36,7 @@ main_page: swap_button_label: "言語を入れ替え" target_language: "相手の言語" translator: "翻訳エンジン" - # translator_label_default: "Default" + translator_label_default: "デフォルト" translator_selector: is_selected_same_language: "「{{your_language}}」と「{{target_language}}」に同じ言語が選択がされているため、「{{ctranslate2}}」のみが使用できます。" diff --git a/locales/ko.yml b/locales/ko.yml index 853c179e..139f41a7 100644 --- a/locales/ko.yml +++ b/locales/ko.yml @@ -36,7 +36,7 @@ main_page: swap_button_label: "언어 교체" target_language: "상대방의 언어" translator: "번역 엔진" - # translator_label_default: "기본값" + translator_label_default: "기본값" translator_selector: is_selected_same_language: diff --git a/locales/zh-Hans.yml b/locales/zh-Hans.yml index 4e301a92..0a13c7f8 100644 --- a/locales/zh-Hans.yml +++ b/locales/zh-Hans.yml @@ -36,7 +36,7 @@ main_page: swap_button_label: "互换" target_language: "目标语言" translator: "翻译器" - # translator_label_default: "默认" + translator_label_default: "默认" translator_selector: is_selected_same_language: diff --git a/locales/zh-Hant.yml b/locales/zh-Hant.yml index a204619f..4fd2fce4 100644 --- a/locales/zh-Hant.yml +++ b/locales/zh-Hant.yml @@ -36,7 +36,7 @@ main_page: swap_button_label: "交換語言" target_language: "目標語言" translator: "翻譯器" - # translator_label_default: "預設" + translator_label_default: "預設" translator_selector: is_selected_same_language: diff --git a/src-ui/app/main_page/sidebar_section/language_settings/translator_selector_open_button/translator_selector/TranslatorSelector.jsx b/src-ui/app/main_page/sidebar_section/language_settings/translator_selector_open_button/translator_selector/TranslatorSelector.jsx index 54e50f3e..cdbc159e 100644 --- a/src-ui/app/main_page/sidebar_section/language_settings/translator_selector_open_button/translator_selector/TranslatorSelector.jsx +++ b/src-ui/app/main_page/sidebar_section/language_settings/translator_selector_open_button/translator_selector/TranslatorSelector.jsx @@ -15,12 +15,13 @@ export const TranslatorSelector = ({selected_id, translation_engines, is_selecte
{columns.map((column, column_index) => (
- {column.map(({ id, label, is_available }) => ( + {column.map(({ id, label, is_available, is_default }) => ( ))} @@ -45,6 +46,7 @@ export const TranslatorSelector = ({selected_id, translation_engines, is_selecte }; const TranslatorBox = (props) => { + const { t } = useTranslation(); const { setSelectedTranslationEngines} = useLanguageSettings(); const { updateIsOpenedTranslatorSelector} = useStore_IsOpenedTranslatorSelector(); @@ -53,6 +55,10 @@ const TranslatorBox = (props) => { { [styles.is_selected]: props.is_selected }, { [styles.is_available]: props.is_available } ); + const label_default_class_name = clsx( + styles.label_default, + { [styles.is_selected]: props.is_selected }, + ); const selectTranslator = () => { if (props.is_selected === false) { @@ -60,9 +66,11 @@ const TranslatorBox = (props) => { } updateIsOpenedTranslatorSelector(false); }; + return (

{props.label}

+ {props.is_default &&

{t("main_page.translator_label_default")}

}
); }; \ No newline at end of file diff --git a/src-ui/app/main_page/sidebar_section/language_settings/translator_selector_open_button/translator_selector/TranslatorSelector.module.scss b/src-ui/app/main_page/sidebar_section/language_settings/translator_selector_open_button/translator_selector/TranslatorSelector.module.scss index b33d0782..b73fef4c 100644 --- a/src-ui/app/main_page/sidebar_section/language_settings/translator_selector_open_button/translator_selector/TranslatorSelector.module.scss +++ b/src-ui/app/main_page/sidebar_section/language_settings/translator_selector_open_button/translator_selector/TranslatorSelector.module.scss @@ -17,26 +17,26 @@ } .wrapper { - // padding: 1rem; width: 100%; height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; - gap: 1rem; + gap: 1.4rem; } .column_wrapper { display: flex; justify-content: center; align-items: center; - gap: 1rem; + gap: 1.2rem; } -$box_size: 6rem; +$box_size: 6.2rem; .box { - width: 9.2rem; + position: relative; + width: 9.4rem; height: $box_size; background-color: var(--dark_875_color); display: flex; @@ -51,10 +51,10 @@ $box_size: 6rem; } &:active { background-color: var(--dark_900_color); - border: 0.1rem solid var(--primary_300_color); + outline: 0.1rem solid var(--primary_300_color); } &.is_selected { - border: 0.2rem solid var(--primary_300_color); + outline: 0.2rem solid var(--primary_300_color); } &:not(.is_available) { pointer-events: none; @@ -69,6 +69,22 @@ $box_size: 6rem; font-size: 1.4rem; } +.label_default { + background-color: var(--dark_875_color); + outline: 0.1rem solid var(--dark_1000_color); + padding: 0.2rem 0.4rem; + border-radius: 0.2rem; + font-size: 1.2rem; + position: absolute; + top: -0.8rem; + right: -0.8rem; + pointer-events: none; + &.is_selected { + outline: 0.1rem solid var(--primary_300_color); + + } +} + .is_selected_same_language_wrapper { position: absolute; bottom: 0; From e670672e15763766be634bd04a95795640ff77ab Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Tue, 18 Mar 2025 16:42:05 +0900 Subject: [PATCH 09/26] [Chore/update] Change localization, Japanese. --- locales/ja.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/ja.yml b/locales/ja.yml index dfda745c..188c5325 100644 --- a/locales/ja.yml +++ b/locales/ja.yml @@ -206,7 +206,7 @@ config_page: others: section_label_sounds: "サウンド" auto_clear_the_message_box: - label: "送信後はチャットボックスを空にする" + label: "送信後はメッセージ入力欄を空にする" send_only_translated_messages: label: "翻訳後のメッセージのみ送信する" auto_export_message_logs: From f40f916bec2640abb7dc814433e34f591b1b6a98 Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Tue, 18 Mar 2025 20:25:54 +0900 Subject: [PATCH 10/26] [Update] Add success notification when deepL Auth key has updated successfully. --- src-ui/logics/common/useNotificationStatus.js | 2 +- src-ui/logics/configs/translation/useDeepLAuthKey.js | 9 +++++++++ src-ui/logics/useReceiveRoutes.js | 4 ++-- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src-ui/logics/common/useNotificationStatus.js b/src-ui/logics/common/useNotificationStatus.js index aa1ac703..a5b7bc3c 100644 --- a/src-ui/logics/common/useNotificationStatus.js +++ b/src-ui/logics/common/useNotificationStatus.js @@ -15,7 +15,7 @@ export const useNotificationStatus = () => { }); }; - const showNotification_Success = (message) => { + const showNotification_Success = (message, options = {}) => { updateNotificationStatus({ status: "success", is_open: true, diff --git a/src-ui/logics/configs/translation/useDeepLAuthKey.js b/src-ui/logics/configs/translation/useDeepLAuthKey.js index e379ce0f..3d66dfba 100644 --- a/src-ui/logics/configs/translation/useDeepLAuthKey.js +++ b/src-ui/logics/configs/translation/useDeepLAuthKey.js @@ -1,7 +1,11 @@ import { useStore_DeepLAuthKey } from "@store"; import { useStdoutToPython } from "@logics/useStdoutToPython"; +import { useTranslation } from "react-i18next"; +import { useNotificationStatus } from "@logics_common"; export const useDeepLAuthKey = () => { + const { t } = useTranslation(); + const { showNotification_Success, showNotification_Error } = useNotificationStatus(); const { asyncStdoutToPython } = useStdoutToPython(); const { currentDeepLAuthKey, updateDeepLAuthKey, pendingDeepLAuthKey } = useStore_DeepLAuthKey(); @@ -14,6 +18,10 @@ export const useDeepLAuthKey = () => { pendingDeepLAuthKey(); asyncStdoutToPython("/set/data/deepl_auth_key", selected_deepl_auth_key); }; + const saveSuccessDeepLAuthKey = (saved_deepl_auth_key) => { + updateDeepLAuthKey(saved_deepl_auth_key); + showNotification_Success(t("config_page.translation.deepl_auth_key.auth_key_success")); + }; const deleteDeepLAuthKey = () => { pendingDeepLAuthKey(); @@ -25,6 +33,7 @@ export const useDeepLAuthKey = () => { getDeepLAuthKey, updateDeepLAuthKey, setDeepLAuthKey, + saveSuccessDeepLAuthKey, deleteDeepLAuthKey, }; }; \ No newline at end of file diff --git a/src-ui/logics/useReceiveRoutes.js b/src-ui/logics/useReceiveRoutes.js index 3ddd38f8..53a2feb3 100644 --- a/src-ui/logics/useReceiveRoutes.js +++ b/src-ui/logics/useReceiveRoutes.js @@ -148,7 +148,7 @@ export const useReceiveRoutes = () => { const { updateSpeakerPhraseTimeout } = useSpeakerPhraseTimeout(); const { updateSpeakerMaxWords } = useSpeakerMaxWords(); - const { updateDeepLAuthKey } = useDeepLAuthKey(); + const { updateDeepLAuthKey, saveSuccessDeepLAuthKey } = useDeepLAuthKey(); const { updateSelectedCTranslate2WeightType } = useSelectedCTranslate2WeightType(); const { updateDownloadedCTranslate2WeightTypeStatus, @@ -352,7 +352,7 @@ export const useReceiveRoutes = () => { // Translation "/get/data/deepl_auth_key": updateDeepLAuthKey, - "/set/data/deepl_auth_key": updateDeepLAuthKey, + "/set/data/deepl_auth_key": saveSuccessDeepLAuthKey, "/delete/data/deepl_auth_key": () => updateDeepLAuthKey(""), "/get/data/ctranslate2_weight_type": updateSelectedCTranslate2WeightType, From 92752d5953270de085dfdf72d6e8c42a30a1eb35 Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Tue, 18 Mar 2025 21:09:59 +0900 Subject: [PATCH 11/26] [Update] Main Page: MainFunctionSwitch: to be a bit smaller than before for allow long label on its main function. --- .../MainFunctionSwitch.module.scss | 4 ++- src-ui/common_css/mixins.scss | 28 +++++++++++-------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src-ui/app/main_page/sidebar_section/main_function_switch/MainFunctionSwitch.module.scss b/src-ui/app/main_page/sidebar_section/main_function_switch/MainFunctionSwitch.module.scss index 750505f7..0c1949bf 100644 --- a/src-ui/app/main_page/sidebar_section/main_function_switch/MainFunctionSwitch.module.scss +++ b/src-ui/app/main_page/sidebar_section/main_function_switch/MainFunctionSwitch.module.scss @@ -78,7 +78,9 @@ $pending_label_color: var(--dark_500_color); } .toggle_control { - @include toggle_control_styles; + // @include toggle_control_styles; + @include toggle_control_styles($toggle_width: 3.6rem, $toggle_height: 1.4rem); + display: flex; justify-content: end; align-items: center; diff --git a/src-ui/common_css/mixins.scss b/src-ui/common_css/mixins.scss index 4e563960..b0229e88 100644 --- a/src-ui/common_css/mixins.scss +++ b/src-ui/common_css/mixins.scss @@ -48,35 +48,41 @@ $toggle_width: 4rem; $toggle_height: 1.6rem; $toggle_gutter: 0.1rem; $toggle_radius: 50%; -$toggle_control_speed: .15s; +$toggle_control_speed: 0.15s; $toggle_control_ease: ease-out; -$toggle_radius: calc($toggle_height / 2); -$toggle_control_size: $toggle_height - calc($toggle_gutter * 2); +@mixin toggle_control_styles( + $toggle_width: $toggle_width, + $toggle_height: $toggle_height, + $toggle_gutter: $toggle_gutter, + $toggle_background_color_on: $toggle_background_color_on, + $toggle_background_color_off: $toggle_background_color_off, + $toggle_control_color: $toggle_control_color, + $toggle_control_speed: $toggle_control_speed, + $toggle_control_ease: $toggle_control_ease +) { + $toggle_radius: calc($toggle_height / 2); + $toggle_control_size: calc($toggle_height - ($toggle_gutter * 2)); -@mixin toggle_control_styles { - display: block; - position: relative; - height: 100%; - width: auto; .control { position: relative; - height: $toggle_height; width: $toggle_width; + height: $toggle_height; border-radius: $toggle_radius; background-color: $toggle_background_color_off; + transition: background-color $toggle_control_speed $toggle_control_ease; &:after { content: ""; position: absolute; - left: $toggle_gutter; top: $toggle_gutter; + left: $toggle_gutter; width: $toggle_control_size; height: $toggle_control_size; border-radius: $toggle_radius; background: $toggle_control_color; transition: left $toggle_control_speed $toggle_control_ease; } - &.is_pending:after{ + &.is_pending:after { background-color: var(--dark_600_color); } &.is_hovered { From f3d2de54b59213ee9e40a654885a8c18f0d43a8a Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Wed, 19 Mar 2025 14:15:42 +0900 Subject: [PATCH 12/26] [Refactor] tauri.conf.json: Align indentation to 4 spaces. --- src-tauri/tauri.conf.json | 162 ++++++++++++++++++-------------------- 1 file changed, 78 insertions(+), 84 deletions(-) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 9b869f1a..12896f1b 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,89 +1,83 @@ { - "build": { - "beforeDevCommand": "", - "beforeBuildCommand": "", - "devPath": "http://localhost:1420", - "distDir": "../dist" - }, - "package": { - "productName": "VRCT", - "version": "3.0.0" - }, - "tauri": { - "allowlist": { - "all": false, - "window": { - "all": false, - "setAlwaysOnTop": true, - "setFocus": true, - "setDecorations": true, - "close": true, - "hide": true, - "setPosition": true, - "setSize": true, - "maximize": true, - "minimize": true, - "unmaximize": true, - "unminimize": true, - "startDragging": true - }, - "globalShortcut": { - "all": true - }, - "shell": { - "all": false, - "open": true, - "sidecar": true, - "scope": [ - { - "name": "bin/VRCT-sidecar", "sidecar": true,"args": true - } - ] - } -}, -"windows": [ - { - "title": "VRCT", - "center": true, - "width": 450, - "height": 220, - "minWidth": 400, - "minHeight": 200, - "transparent": true, - "decorations": false - } - ], - "security": { - "csp": null + "build": { + "beforeDevCommand": "", + "beforeBuildCommand": "", + "devPath": "http://localhost:1420", + "distDir": "../dist" }, - "bundle": { - "active": true, - "targets": "nsis", - "identifier": "com.vrct.dev", - "publisher": "m's software", - "copyright": "Copyright m's software", - "shortDescription": "VRCT", - "icon": [ - "icons/32x32.png", - "icons/128x128.png", - "icons/128x128@2x.png", - "icons/icon.icns", - "icons/icon.ico" - ], - "externalBin": [ - "bin/VRCT-sidecar" - ], - "resources":{ - "bin/_internal": "_internal" - }, - "windows": { - "nsis": { - "template": "nsis/template.nsi", - "license": "../LICENSE", - "installMode": "currentUser", - "displayLanguageSelector": true + "package": { + "productName": "VRCT", + "version": "3.0.0" + }, + "tauri": { + "allowlist": { + "all": false, + "window": { + "all": false, + "setAlwaysOnTop": true, + "setFocus": true, + "setDecorations": true, + "close": true, + "hide": true, + "setPosition": true, + "setSize": true, + "maximize": true, + "minimize": true, + "unmaximize": true, + "unminimize": true, + "startDragging": true + }, + "globalShortcut": { + "all": true + }, + "shell": { + "all": false, + "open": true, + "sidecar": true, + "scope": [ + { "name": "bin/VRCT-sidecar", "sidecar": true, "args": true } + ] + } + }, + "windows": [{ + "title": "VRCT", + "center": true, + "width": 450, + "height": 220, + "minWidth": 400, + "minHeight": 200, + "transparent": true, + "decorations": false + }], + "security": { "csp": null }, + "bundle": { + "active": true, + "targets": "nsis", + "identifier": "com.vrct.dev", + "publisher": "m's software", + "copyright": "Copyright m's software", + "shortDescription": "VRCT", + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ], + "externalBin": [ + "bin/VRCT-sidecar" + ], + "resources": { + "bin/_internal": "_internal" + }, + "windows": { + "nsis": { + "template": "nsis/template.nsi", + "license": "../LICENSE", + "installMode": "currentUser", + "displayLanguageSelector": true + } + } } - } } - } } From 6f01ee22065fd3196ebd32f9f2422bd6ada4b829 Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Wed, 19 Mar 2025 15:45:31 +0900 Subject: [PATCH 13/26] [Refactor] store.js. --- src-ui/store.js | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/src-ui/store.js b/src-ui/store.js index 5eb0746f..4aa4a694 100644 --- a/src-ui/store.js +++ b/src-ui/store.js @@ -126,37 +126,38 @@ export const { atomInstance: Atom_NotificationStatus, useHook: useStore_Notifica }, "NotificationStatus"); // Main Page -// Functions +// Common +export const { atomInstance: Atom_IsMainPageCompactMode, useHook: useStore_IsMainPageCompactMode } = createAtomWithHook(false, "IsMainPageCompactMode"); + +// Sidebar Section export const { atomInstance: Atom_TranslationStatus, useHook: useStore_TranslationStatus } = createAtomWithHook(false, "TranslationStatus", {is_state_ok: true}); export const { atomInstance: Atom_TranscriptionSendStatus, useHook: useStore_TranscriptionSendStatus } = createAtomWithHook(false, "TranscriptionSendStatus", {is_state_ok: true}); export const { atomInstance: Atom_TranscriptionReceiveStatus, useHook: useStore_TranscriptionReceiveStatus } = createAtomWithHook(false, "TranscriptionReceiveStatus", {is_state_ok: true}); export const { atomInstance: Atom_ForegroundStatus, useHook: useStore_ForegroundStatus } = createAtomWithHook(false, "ForegroundStatus", {is_state_ok: true}); -export const { atomInstance: Atom_MessageLogs, useHook: useStore_MessageLogs } = createAtomWithHook([], "MessageLogs"); -// export const { atomInstance: Atom_MessageLogs, useHook: useStore_MessageLogs } = createAtomWithHook(generateTestData(20), "MessageLogs"); // For testing -export const { atomInstance: Atom_MessageInputValue, useHook: useStore_MessageInputValue } = createAtomWithHook("", "MessageInputValue"); -export const { atomInstance: Atom_IsVisibleResendButton, useHook: useStore_IsVisibleResendButton } = createAtomWithHook(false, "IsVisibleResendButton", {is_state_ok: true}); -export const { atomInstance: Atom_IsAppliedInitMessageBoxHeight, useHook: useStore_IsAppliedInitMessageBoxHeight } = createAtomWithHook(false, "IsAppliedInitMessageBoxHeight"); - -export const { atomInstance: Atom_SelectableLanguageList, useHook: useStore_SelectableLanguageList } = createAtomWithHook([], "SelectableLanguageList"); - export const { atomInstance: Atom_SelectedPresetTabNumber, useHook: useStore_SelectedPresetTabNumber } = createAtomWithHook("1", "SelectedPresetTabNumber"); export const { atomInstance: Atom_EnableMultiTranslation, useHook: useStore_EnableMultiTranslation } = createAtomWithHook(false, "EnableMultiTranslation"); export const { atomInstance: Atom_SelectedYourLanguages, useHook: useStore_SelectedYourLanguages } = createAtomWithHook({}, "SelectedYourLanguages"); export const { atomInstance: Atom_SelectedTargetLanguages, useHook: useStore_SelectedTargetLanguages } = createAtomWithHook({}, "SelectedTargetLanguages"); - export const { atomInstance: Atom_TranslationEngines, useHook: useStore_TranslationEngines } = createAtomWithHook(translator_status, "TranslationEngines"); export const { atomInstance: Atom_SelectedTranslationEngines, useHook: useStore_SelectedTranslationEngines } = createAtomWithHook({1:"", 2:"", 3:""}, "SelectedTranslationEngines"); +export const { atomInstance: Atom_IsOpenedTranslatorSelector, useHook: useStore_IsOpenedTranslatorSelector } = createAtomWithHook(false, "IsOpenedTranslatorSelector"); - -// Designs -export const { atomInstance: Atom_IsMainPageCompactMode, useHook: useStore_IsMainPageCompactMode } = createAtomWithHook(false, "IsMainPageCompactMode"); -export const { atomInstance: Atom_MessageInputBoxRatio, useHook: useStore_MessageInputBoxRatio } = createAtomWithHook(20, "MessageInputBoxRatio"); +// Language Selector export const { atomInstance: Atom_IsOpenedLanguageSelector, useHook: useStore_IsOpenedLanguageSelector } = createAtomWithHook( { your_language: false, target_language: false, target_key: "1" }, "IsOpenedLanguageSelector" ); +export const { atomInstance: Atom_SelectableLanguageList, useHook: useStore_SelectableLanguageList } = createAtomWithHook([], "SelectableLanguageList"); + +// Message Container +export const { atomInstance: Atom_MessageLogs, useHook: useStore_MessageLogs } = createAtomWithHook([], "MessageLogs"); +// export const { atomInstance: Atom_MessageLogs, useHook: useStore_MessageLogs } = createAtomWithHook(generateTestData(20), "MessageLogs"); // For testing +export const { atomInstance: Atom_MessageInputBoxRatio, useHook: useStore_MessageInputBoxRatio } = createAtomWithHook(20, "MessageInputBoxRatio"); +export const { atomInstance: Atom_MessageInputValue, useHook: useStore_MessageInputValue } = createAtomWithHook("", "MessageInputValue"); +export const { atomInstance: Atom_IsVisibleResendButton, useHook: useStore_IsVisibleResendButton } = createAtomWithHook(false, "IsVisibleResendButton", {is_state_ok: true}); + // Config Page @@ -164,8 +165,6 @@ export const { atomInstance: Atom_IsOpenedLanguageSelector, useHook: useStore_Is export const { atomInstance: Atom_SoftwareVersion, useHook: useStore_SoftwareVersion } = createAtomWithHook("-", "SoftwareVersion"); export const { atomInstance: Atom_SelectedConfigTabId, useHook: useStore_SelectedConfigTabId } = createAtomWithHook("device", "SelectedConfigTabId"); export const { atomInstance: Atom_SettingBoxScrollPosition, useHook: useStore_SettingBoxScrollPosition } = createAtomWithHook(0, "SettingBoxScrollPosition"); - -// Designs export const { atomInstance: Atom_IsOpenedDropdownMenu, useHook: useStore_IsOpenedDropdownMenu } = createAtomWithHook("", "IsOpenedDropdownMenu"); // Device @@ -280,9 +279,9 @@ export const { atomInstance: Atom_OscPort, useHook: useStore_OscPort } = createA -export const { atomInstance: Atom_IsOpenedTranslatorSelector, useHook: useStore_IsOpenedTranslatorSelector } = createAtomWithHook(false, "IsOpenedTranslatorSelector"); - +// Supporters export const { atomInstance: Atom_SupportersData, useHook: useStore_SupportersData } = createAtomWithHook(null, "SupportersData", {is_state_ok: true}); +// About VRCT export const { atomInstance: Atom_VrctPosterIndex, useHook: useStore_VrctPosterIndex } = createAtomWithHook(0, "VrctPosterIndex"); export const { atomInstance: Atom_PosterShowcaseWorldPageIndex, useHook: useStore_PosterShowcaseWorldPageIndex } = createAtomWithHook(0, "PosterShowcaseWorldPageIndex"); \ No newline at end of file From 4da99ab4d478c2732d79335a0fe3981cbf0bd856 Mon Sep 17 00:00:00 2001 From: misyaguziya <53165965+misyaguziya@users.noreply.github.com> Date: Fri, 21 Mar 2025 09:06:01 +0900 Subject: [PATCH 14/26] [Update] transcription_whisper.py: Add new Whisper model entries for large-v3-turbo and large-v3-turbo-int8. --- src-python/models/transcription/transcription_whisper.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src-python/models/transcription/transcription_whisper.py b/src-python/models/transcription/transcription_whisper.py index 080054b5..69499260 100644 --- a/src-python/models/transcription/transcription_whisper.py +++ b/src-python/models/transcription/transcription_whisper.py @@ -17,6 +17,8 @@ _MODELS = { "large-v1": "Systran/faster-whisper-large-v1", "large-v2": "Systran/faster-whisper-large-v2", "large-v3": "Systran/faster-whisper-large-v3", + "large-v3-turbo-int8": "Zoont/faster-whisper-large-v3-turbo-int8-ct2", #794MB + "large-v3-turbo": "deepdml/faster-whisper-large-v3-turbo-ct2", #1.58GB } _FILENAMES = [ From b8ade54e85422a0a4236f88542cd75d638eff28a Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Fri, 21 Mar 2025 17:38:15 +0900 Subject: [PATCH 15/26] [bugfix] Config Page: AdvancedSettings: Add save button to Entry components. Add error handlings. --- .../_components/_atoms/_entry/_Entry.jsx | 2 +- .../EntryWithSaveButton.jsx | 32 +++++++++++ .../EntryWithSaveButton.module.scss | 30 ++++++++++ .../setting_box/_components/index.js | 1 + .../setting_box/_templates/Templates.jsx | 4 ++ .../advanced_settings/AdvancedSettings.jsx | 57 ++++++++++--------- src-ui/logics/_useBackendErrorHandling.js | 20 +++++-- .../advanced_settings/useOscIpAddress.js | 9 +++ .../configs/advanced_settings/useOscPort.js | 9 +++ .../configs/translation/useDeepLAuthKey.js | 18 ++++-- src-ui/logics/useReceiveRoutes.js | 9 ++- 11 files changed, 153 insertions(+), 38 deletions(-) create mode 100644 src-ui/app/config_page/setting_section/setting_box/_components/entry_with_save_button/EntryWithSaveButton.jsx create mode 100644 src-ui/app/config_page/setting_section/setting_box/_components/entry_with_save_button/EntryWithSaveButton.module.scss diff --git a/src-ui/app/config_page/setting_section/setting_box/_components/_atoms/_entry/_Entry.jsx b/src-ui/app/config_page/setting_section/setting_box/_components/_atoms/_entry/_Entry.jsx index d9b0d4cb..641e7104 100644 --- a/src-ui/app/config_page/setting_section/setting_box/_components/_atoms/_entry/_Entry.jsx +++ b/src-ui/app/config_page/setting_section/setting_box/_components/_atoms/_entry/_Entry.jsx @@ -24,7 +24,7 @@ const _Entry = forwardRef((props, ref) => {
{ + const { t } = useTranslation(); + const onChangeFunction = (e) => { + props.onChangeFunction?.(e.target.value); + }; + const saveFunction = () => { + props.saveFunction(); + }; + const is_disabled = props.state === "pending"; + + const save_button_class_names = clsx(styles.save_button, { + [styles.is_disabled]: is_disabled + }); + + return ( +
+ <_Entry width={props.width} onChange={onChangeFunction} ui_variable={props.variable} is_disabled={is_disabled}/> + +
+ ); +}; \ No newline at end of file diff --git a/src-ui/app/config_page/setting_section/setting_box/_components/entry_with_save_button/EntryWithSaveButton.module.scss b/src-ui/app/config_page/setting_section/setting_box/_components/entry_with_save_button/EntryWithSaveButton.module.scss new file mode 100644 index 00000000..deb86f89 --- /dev/null +++ b/src-ui/app/config_page/setting_section/setting_box/_components/entry_with_save_button/EntryWithSaveButton.module.scss @@ -0,0 +1,30 @@ +.container { + display: flex; + justify-content: center; + align-items: center; + gap: 1rem; + flex-shrink: 0; +} + +.save_button { + padding: 0.8rem 1.2rem; + background-color: var(--primary_600_color); + border-radius: 0.4rem; + text-align: center; + flex-shrink: 0; + min-width: 5.4rem; + &:hover { + background-color: var(--primary_500_color); + } + &:active { + background-color: var(--primary_700_color); + } + &.is_disabled { + pointer-events: none; + background-color: var(--primary_800_color); + } +} + +.save_button_label { + font-size: 1.4rem; +} \ No newline at end of file diff --git a/src-ui/app/config_page/setting_section/setting_box/_components/index.js b/src-ui/app/config_page/setting_section/setting_box/_components/index.js index 3a295d3c..7b695b35 100644 --- a/src-ui/app/config_page/setting_section/setting_box/_components/index.js +++ b/src-ui/app/config_page/setting_section/setting_box/_components/index.js @@ -3,6 +3,7 @@ export { ComputeDevice } from "./compute_device/ComputeDevice"; export { DeeplAuthKey, OpenWebpage_DeeplAuthKey } from "./deepl_auth_key/DeeplAuthKey"; export { DropdownMenu } from "./dropdown_menu/DropdownMenu"; export { Entry } from "./entry/Entry"; +export { EntryWithSaveButton } from "./entry_with_save_button/EntryWithSaveButton"; export { HotkeysEntry } from "./hotkeys_entry/HotkeysEntry"; export { LabelComponent } from "./label_component/LabelComponent"; export { RadioButton } from "./radio_button/RadioButton"; diff --git a/src-ui/app/config_page/setting_section/setting_box/_templates/Templates.jsx b/src-ui/app/config_page/setting_section/setting_box/_templates/Templates.jsx index 716a15c9..b25af8b9 100644 --- a/src-ui/app/config_page/setting_section/setting_box/_templates/Templates.jsx +++ b/src-ui/app/config_page/setting_section/setting_box/_templates/Templates.jsx @@ -8,6 +8,7 @@ import { Slider, SwitchBox, Entry, + EntryWithSaveButton, HotkeysEntry, RadioButton, OpenWebpage_DeeplAuthKey, @@ -75,6 +76,9 @@ export const SwitchBoxContainer = (props) => ( export const EntryContainer = (props) => ( ); +export const EntryWithSaveButtonContainer = (props) => ( + +); export const HotkeysEntryContainer = (props) => ( diff --git a/src-ui/app/config_page/setting_section/setting_box/advanced_settings/AdvancedSettings.jsx b/src-ui/app/config_page/setting_section/setting_box/advanced_settings/AdvancedSettings.jsx index 94dcd358..966a3c21 100644 --- a/src-ui/app/config_page/setting_section/setting_box/advanced_settings/AdvancedSettings.jsx +++ b/src-ui/app/config_page/setting_section/setting_box/advanced_settings/AdvancedSettings.jsx @@ -11,6 +11,7 @@ import { import { ActionButtonContainer, EntryContainer, + EntryWithSaveButtonContainer, } from "../_templates/Templates"; @@ -30,54 +31,58 @@ export const AdvancedSettings = () => { const OscIpAddressContainer = () => { const { t } = useTranslation(); - const [ui_variable, setUiVariable] = useState(""); const { currentOscIpAddress, setOscIpAddress } = useOscIpAddress(); - const onChangeFunction = (e) => { - const value = e.currentTarget.value; - if (value === "") { - setUiVariable(""); - } else { - setUiVariable(value); - setOscIpAddress(value); - } + const [input_value, seInputValue] = useState(currentOscIpAddress.data); + + const onChangeFunction = (value) => { + seInputValue(value); + }; + + const saveFunction = () => { + setOscIpAddress(input_value); }; useEffect(()=> { - setUiVariable(currentOscIpAddress.data); + seInputValue(currentOscIpAddress.data); }, [currentOscIpAddress]); return ( - ); }; const OscPortContainer = () => { const { t } = useTranslation(); - const [ui_variable, setUiVariable] = useState(""); const { currentOscPort, setOscPort } = useOscPort(); - const onChangeFunction = (e) => { - const value = e.currentTarget.value; - if (value === "") { - setUiVariable(""); - } else { - setUiVariable(value); - setOscPort(value); - } + const [input_value, seInputValue] = useState(currentOscPort.data); + + const onChangeFunction = (value) => { + seInputValue(value); + }; + + const saveFunction = () => { + setOscPort(input_value); }; useEffect(()=> { - setUiVariable(currentOscPort.data); + seInputValue(currentOscPort.data); }, [currentOscPort]); return ( - ); }; diff --git a/src-ui/logics/_useBackendErrorHandling.js b/src-ui/logics/_useBackendErrorHandling.js index 446330cc..1145cd80 100644 --- a/src-ui/logics/_useBackendErrorHandling.js +++ b/src-ui/logics/_useBackendErrorHandling.js @@ -14,6 +14,10 @@ import { useSpeakerMaxWords, useDeepLAuthKey, + + + useOscIpAddress, + useOscPort, } from "@logics_configs"; import { ui_configs } from "../ui_configs"; @@ -29,9 +33,13 @@ export const _useBackendErrorHandling = () => { const { updateSpeakerPhraseTimeout } = useSpeakerPhraseTimeout(); const { updateSpeakerMaxWords } = useSpeakerMaxWords(); - const { updateDeepLAuthKey } = useDeepLAuthKey(); + const { updateDeepLAuthKey, saveErrorDeepLAuthKey } = useDeepLAuthKey(); - const errorHandling_Backend = ({message, data, endpoint}) => { + + const { saveErrorOscIpAddress } = useOscIpAddress(); + const { saveErrorOscPort } = useOscPort(); + + const errorHandling_Backend = ({message, data, endpoint, _result}) => { switch (message) { case "No mic device detected": showNotification_Error(t("common_error.no_device_mic")); @@ -111,8 +119,12 @@ export const _useBackendErrorHandling = () => { break; default: - if (endpoint === "/set/data/deepl_auth_key") updateDeepLAuthKey(data); - showNotification_Error(message); + // determine by endpoint, not message. + if (endpoint === "/set/data/deepl_auth_key") saveErrorDeepLAuthKey({message, data, endpoint, _result}); + if (endpoint === "/set/data/osc_ip_address") saveErrorOscIpAddress({message, data, endpoint, _result}); + if (endpoint === "/set/data/osc_port") saveErrorOscPort({message, data, endpoint, _result}); + + break; } diff --git a/src-ui/logics/configs/advanced_settings/useOscIpAddress.js b/src-ui/logics/configs/advanced_settings/useOscIpAddress.js index 702a6d98..354e2751 100644 --- a/src-ui/logics/configs/advanced_settings/useOscIpAddress.js +++ b/src-ui/logics/configs/advanced_settings/useOscIpAddress.js @@ -1,7 +1,9 @@ import { useStore_OscIpAddress } from "@store"; import { useStdoutToPython } from "@logics/useStdoutToPython"; +import { useNotificationStatus } from "@logics_common"; export const useOscIpAddress = () => { + const { showNotification_Error } = useNotificationStatus(); const { asyncStdoutToPython } = useStdoutToPython(); const { currentOscIpAddress, updateOscIpAddress, pendingOscIpAddress } = useStore_OscIpAddress(); @@ -15,10 +17,17 @@ export const useOscIpAddress = () => { asyncStdoutToPython("/set/data/osc_ip_address", osc_ip_address); }; + const saveErrorOscIpAddress = ({data, message, _result}) => { + updateOscIpAddress(d => d.data); + showNotification_Error(_result); + }; + return { currentOscIpAddress, getOscIpAddress, updateOscIpAddress, setOscIpAddress, + + saveErrorOscIpAddress, }; }; \ No newline at end of file diff --git a/src-ui/logics/configs/advanced_settings/useOscPort.js b/src-ui/logics/configs/advanced_settings/useOscPort.js index 947d613d..d3e28557 100644 --- a/src-ui/logics/configs/advanced_settings/useOscPort.js +++ b/src-ui/logics/configs/advanced_settings/useOscPort.js @@ -1,7 +1,9 @@ import { useStore_OscPort } from "@store"; import { useStdoutToPython } from "@logics/useStdoutToPython"; +import { useNotificationStatus } from "@logics_common"; export const useOscPort = () => { + const { showNotification_Error } = useNotificationStatus(); const { asyncStdoutToPython } = useStdoutToPython(); const { currentOscPort, updateOscPort, pendingOscPort } = useStore_OscPort(); @@ -15,10 +17,17 @@ export const useOscPort = () => { asyncStdoutToPython("/set/data/osc_port", osc_port); }; + const saveErrorOscPort = ({data, message, _result}) => { + updateOscPort(d => d.data); + showNotification_Error(_result); + }; + return { currentOscPort, getOscPort, updateOscPort, setOscPort, + + saveErrorOscPort, }; }; \ No newline at end of file diff --git a/src-ui/logics/configs/translation/useDeepLAuthKey.js b/src-ui/logics/configs/translation/useDeepLAuthKey.js index 3d66dfba..f93fdd4f 100644 --- a/src-ui/logics/configs/translation/useDeepLAuthKey.js +++ b/src-ui/logics/configs/translation/useDeepLAuthKey.js @@ -18,22 +18,30 @@ export const useDeepLAuthKey = () => { pendingDeepLAuthKey(); asyncStdoutToPython("/set/data/deepl_auth_key", selected_deepl_auth_key); }; - const saveSuccessDeepLAuthKey = (saved_deepl_auth_key) => { - updateDeepLAuthKey(saved_deepl_auth_key); - showNotification_Success(t("config_page.translation.deepl_auth_key.auth_key_success")); - }; const deleteDeepLAuthKey = () => { pendingDeepLAuthKey(); asyncStdoutToPython("/delete/data/deepl_auth_key"); }; + const savedDeepLAuthKey = (data) => { + updateDeepLAuthKey(data); + showNotification_Success(t("config_page.translation.deepl_auth_key.auth_key_success")); + }; + + const saveErrorDeepLAuthKey = ({data, message}) => { + updateDeepLAuthKey(data); + showNotification_Error(message); + }; + return { currentDeepLAuthKey, getDeepLAuthKey, updateDeepLAuthKey, setDeepLAuthKey, - saveSuccessDeepLAuthKey, deleteDeepLAuthKey, + + saveErrorDeepLAuthKey, + savedDeepLAuthKey, }; }; \ No newline at end of file diff --git a/src-ui/logics/useReceiveRoutes.js b/src-ui/logics/useReceiveRoutes.js index 53a2feb3..383f1faf 100644 --- a/src-ui/logics/useReceiveRoutes.js +++ b/src-ui/logics/useReceiveRoutes.js @@ -148,7 +148,7 @@ export const useReceiveRoutes = () => { const { updateSpeakerPhraseTimeout } = useSpeakerPhraseTimeout(); const { updateSpeakerMaxWords } = useSpeakerMaxWords(); - const { updateDeepLAuthKey, saveSuccessDeepLAuthKey } = useDeepLAuthKey(); + const { updateDeepLAuthKey, savedDeepLAuthKey } = useDeepLAuthKey(); const { updateSelectedCTranslate2WeightType } = useSelectedCTranslate2WeightType(); const { updateDownloadedCTranslate2WeightTypeStatus, @@ -352,7 +352,7 @@ export const useReceiveRoutes = () => { // Translation "/get/data/deepl_auth_key": updateDeepLAuthKey, - "/set/data/deepl_auth_key": saveSuccessDeepLAuthKey, + "/set/data/deepl_auth_key": savedDeepLAuthKey, "/delete/data/deepl_auth_key": () => updateDeepLAuthKey(""), "/get/data/ctranslate2_weight_type": updateSelectedCTranslate2WeightType, @@ -523,6 +523,9 @@ export const useReceiveRoutes = () => { "/set/data/speaker_record_timeout": errorHandling_Backend, "/set/data/speaker_phrase_timeout": errorHandling_Backend, "/set/data/speaker_max_phrases": errorHandling_Backend, + + "/set/data/osc_ip_address": errorHandling_Backend, + "/set/data/osc_port": errorHandling_Backend, }; @@ -555,12 +558,14 @@ export const useReceiveRoutes = () => { break; case 400: + case 500: const error_route = error_status_routes[parsed_data.endpoint]; if (error_route) { error_route({ message: parsed_data.result.message, data: parsed_data.result.data, endpoint: parsed_data.endpoint, + _result: parsed_data.result, }); } else { handleInvalidEndpoint(parsed_data); From d091f1b6038ffc05a8e386c05cff13060f92e2f0 Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Fri, 21 Mar 2025 18:24:34 +0900 Subject: [PATCH 16/26] [Update] UI: Add selectable Whisper models. --- .../setting_section/setting_box/transcription/Transcription.jsx | 2 ++ src-ui/ui_configs.js | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src-ui/app/config_page/setting_section/setting_box/transcription/Transcription.jsx b/src-ui/app/config_page/setting_section/setting_box/transcription/Transcription.jsx index c7c9368d..d881b330 100644 --- a/src-ui/app/config_page/setting_section/setting_box/transcription/Transcription.jsx +++ b/src-ui/app/config_page/setting_section/setting_box/transcription/Transcription.jsx @@ -260,6 +260,8 @@ const WhisperWeightType_Box = () => { { id: "large-v1", label: t("config_page.transcription.whisper_weight_type.model_template", {model_name: "large-v1", capacity: "2.87GB"}) }, { id: "large-v2", label: t("config_page.transcription.whisper_weight_type.model_template", {model_name: "large-v2", capacity: "2.87GB"}) }, { id: "large-v3", label: t("config_page.transcription.whisper_weight_type.model_template", {model_name: "large-v3", capacity: "2.87GB"}) }, + { id: "large-v3-turbo-int8", label: t("config_page.transcription.whisper_weight_type.model_template", {model_name: "large-v3-turbo-int8", capacity: "794MB"}) }, + { id: "large-v3-turbo", label: t("config_page.transcription.whisper_weight_type.model_template", {model_name: "large-v3-turbo", capacity: "1.58GB"}) }, ]; const whisper_weight_types = updateLabelsById(currentWhisperWeightTypeStatus.data, new_labels); diff --git a/src-ui/ui_configs.js b/src-ui/ui_configs.js index 4bee9957..24111092 100644 --- a/src-ui/ui_configs.js +++ b/src-ui/ui_configs.js @@ -74,6 +74,8 @@ export const whisper_weight_type_status = [ { id: "large-v1", label: "large-v1", is_downloaded: false, progress: null }, { id: "large-v2", label: "large-v2", is_downloaded: false, progress: null }, { id: "large-v3", label: "large-v3", is_downloaded: false, progress: null }, + { id: "large-v3-turbo-int8", label: "large-v3-turbo-int8", is_downloaded: false, progress: null }, + { id: "large-v3-turbo", label: "large-v3-turbo", is_downloaded: false, progress: null }, ]; export const supporters_data_url = "https://shiinasakamoto.github.io/vrct_supporters/assets/supporters/data.json"; From 3d53652b2dce55e2d11d788d4981463f4f6181f8 Mon Sep 17 00:00:00 2001 From: misyaguziya <53165965+misyaguziya@users.noreply.github.com> Date: Sat, 22 Mar 2025 14:37:05 +0900 Subject: [PATCH 17/26] [Update] Controller: Validate and handle IP address setting in setOscIpAddress method. [Update] Utils: Implement isValidIpAddress function to check IP address validity. --- src-python/controller.py | 27 +++++++++++++++++++++++---- src-python/utils.py | 10 +++++++++- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/src-python/controller.py b/src-python/controller.py index f0356287..98b84844 100644 --- a/src-python/controller.py +++ b/src-python/controller.py @@ -6,7 +6,7 @@ import re from device_manager import device_manager from config import config from model import model -from utils import removeLog, printLog, errorLogging, isConnectedNetwork +from utils import removeLog, printLog, errorLogging, isConnectedNetwork, isValidIpAddress class Controller: def __init__(self) -> None: @@ -1085,9 +1085,28 @@ class Controller: @staticmethod def setOscIpAddress(data, *args, **kwargs) -> dict: - config.OSC_IP_ADDRESS = data - model.setOscIpAddress(config.OSC_IP_ADDRESS) - return {"status":200, "result":config.OSC_IP_ADDRESS} + if isValidIpAddress(data) is False: + return { + "status":400, + "result":{ + "message":"Invalid IP address", + "data": config.OSC_IP_ADDRESS + } + } + else: + try: + model.setOscIpAddress(data) + config.OSC_IP_ADDRESS = data + return {"status":200, "result":config.OSC_IP_ADDRESS} + except Exception: + model.setOscIpAddress(config.OSC_IP_ADDRESS) + return { + "status":400, + "result":{ + "message":"Cannot set IP address", + "data": config.OSC_IP_ADDRESS + } + } @staticmethod def getOscPort(*args, **kwargs) -> dict: diff --git a/src-python/utils.py b/src-python/utils.py index e31a1f46..de538dae 100644 --- a/src-python/utils.py +++ b/src-python/utils.py @@ -7,14 +7,22 @@ from logging.handlers import RotatingFileHandler from ctranslate2 import get_supported_compute_types import requests +import ipaddress -def isConnectedNetwork(url="http://www.google.com", timeout=3): +def isConnectedNetwork(url="http://www.google.com", timeout=3) -> bool: try: response = requests.get(url, timeout=timeout) return response.status_code == 200 except requests.RequestException: return False +def isValidIpAddress(ip_address: str) -> bool: + try: + ipaddress.ip_address(ip_address) + return True + except ValueError: + return False + def getBestComputeType(device, device_index) -> str: compute_types = get_supported_compute_types(device, device_index) compute_types = set(compute_types) From df344baa069d2ee2228243cb4a10731d1847e3c8 Mon Sep 17 00:00:00 2001 From: misyaguziya <53165965+misyaguziya@users.noreply.github.com> Date: Sat, 22 Mar 2025 14:43:52 +0900 Subject: [PATCH 18/26] [bugfix] Controller: Fix response handling in setOscIpAddress method. --- src-python/controller.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src-python/controller.py b/src-python/controller.py index 98b84844..df6267ff 100644 --- a/src-python/controller.py +++ b/src-python/controller.py @@ -1086,7 +1086,7 @@ class Controller: @staticmethod def setOscIpAddress(data, *args, **kwargs) -> dict: if isValidIpAddress(data) is False: - return { + response = { "status":400, "result":{ "message":"Invalid IP address", @@ -1097,16 +1097,17 @@ class Controller: try: model.setOscIpAddress(data) config.OSC_IP_ADDRESS = data - return {"status":200, "result":config.OSC_IP_ADDRESS} + response = {"status":200, "result":config.OSC_IP_ADDRESS} except Exception: model.setOscIpAddress(config.OSC_IP_ADDRESS) - return { + response = { "status":400, "result":{ "message":"Cannot set IP address", "data": config.OSC_IP_ADDRESS } } + return response @staticmethod def getOscPort(*args, **kwargs) -> dict: From 7a3a2cfe076ca6c349c82ccf6c053bc2e2885290 Mon Sep 17 00:00:00 2001 From: misyaguziya <53165965+misyaguziya@users.noreply.github.com> Date: Sat, 22 Mar 2025 17:20:26 +0900 Subject: [PATCH 19/26] [fix] Model: Handle cases where selected microphone or speaker devices are not available --- src-python/model.py | 357 ++++++++++++++++++++++---------------------- 1 file changed, 180 insertions(+), 177 deletions(-) diff --git a/src-python/model.py b/src-python/model.py index f393314d..377fcfe7 100644 --- a/src-python/model.py +++ b/src-python/model.py @@ -396,83 +396,83 @@ class Model: mic_device_list = device_manager.getMicDevices().get(mic_host_name, [{"name": "NoDevice"}]) selected_mic_device = [device for device in mic_device_list if device["name"] == mic_device_name] - if len(selected_mic_device) == 0: - return False + if len(selected_mic_device) == 0 or mic_device_name == "NoDevice": + fnc({"text": False, "language": None}) + else: + self.mic_audio_queue = Queue() + # self.mic_energy_queue = Queue() - self.mic_audio_queue = Queue() - # self.mic_energy_queue = Queue() + mic_device = selected_mic_device[0] + record_timeout = config.MIC_RECORD_TIMEOUT + phrase_timeout = config.MIC_PHRASE_TIMEOUT + if record_timeout > phrase_timeout: + record_timeout = phrase_timeout - mic_device = selected_mic_device[0] - record_timeout = config.MIC_RECORD_TIMEOUT - phrase_timeout = config.MIC_PHRASE_TIMEOUT - if record_timeout > phrase_timeout: - record_timeout = phrase_timeout + self.mic_audio_recorder = SelectedMicEnergyAndAudioRecorder( + device=mic_device, + energy_threshold=config.MIC_THRESHOLD, + dynamic_energy_threshold=config.MIC_AUTOMATIC_THRESHOLD, + phrase_time_limit=record_timeout, + ) + # self.mic_audio_recorder.recordIntoQueue(self.mic_audio_queue, mic_energy_queue) + self.mic_audio_recorder.recordIntoQueue(self.mic_audio_queue, None) + self.mic_transcriber = AudioTranscriber( + speaker=False, + source=self.mic_audio_recorder.source, + phrase_timeout=phrase_timeout, + max_phrases=config.MIC_MAX_PHRASES, + transcription_engine=config.SELECTED_TRANSCRIPTION_ENGINE, + root=config.PATH_LOCAL, + whisper_weight_type=config.WHISPER_WEIGHT_TYPE, + device=config.SELECTED_TRANSCRIPTION_COMPUTE_DEVICE["device"], + device_index=config.SELECTED_TRANSCRIPTION_COMPUTE_DEVICE["device_index"], + ) + def sendMicTranscript(): + try: + selected_your_languages = config.SELECTED_YOUR_LANGUAGES[config.SELECTED_TAB_NO] + languages = [data["language"] for data in selected_your_languages.values() if data["enable"] is True] + countries = [data["country"] for data in selected_your_languages.values() if data["enable"] is True] + if isinstance(self.mic_transcriber, AudioTranscriber) is True: + res = self.mic_transcriber.transcribeAudioQueue( + self.mic_audio_queue, + languages, + countries, + config.MIC_AVG_LOGPROB, + config.MIC_NO_SPEECH_PROB + ) + if res: + result = self.mic_transcriber.getTranscript() + fnc(result) + except Exception: + errorLogging() - self.mic_audio_recorder = SelectedMicEnergyAndAudioRecorder( - device=mic_device, - energy_threshold=config.MIC_THRESHOLD, - dynamic_energy_threshold=config.MIC_AUTOMATIC_THRESHOLD, - phrase_time_limit=record_timeout, - ) - # self.mic_audio_recorder.recordIntoQueue(self.mic_audio_queue, mic_energy_queue) - self.mic_audio_recorder.recordIntoQueue(self.mic_audio_queue, None) - self.mic_transcriber = AudioTranscriber( - speaker=False, - source=self.mic_audio_recorder.source, - phrase_timeout=phrase_timeout, - max_phrases=config.MIC_MAX_PHRASES, - transcription_engine=config.SELECTED_TRANSCRIPTION_ENGINE, - root=config.PATH_LOCAL, - whisper_weight_type=config.WHISPER_WEIGHT_TYPE, - device=config.SELECTED_TRANSCRIPTION_COMPUTE_DEVICE["device"], - device_index=config.SELECTED_TRANSCRIPTION_COMPUTE_DEVICE["device_index"], - ) - def sendMicTranscript(): - try: - selected_your_languages = config.SELECTED_YOUR_LANGUAGES[config.SELECTED_TAB_NO] - languages = [data["language"] for data in selected_your_languages.values() if data["enable"] is True] - countries = [data["country"] for data in selected_your_languages.values() if data["enable"] is True] - if isinstance(self.mic_transcriber, AudioTranscriber) is True: - res = self.mic_transcriber.transcribeAudioQueue( - self.mic_audio_queue, - languages, - countries, - config.MIC_AVG_LOGPROB, - config.MIC_NO_SPEECH_PROB - ) - if res: - result = self.mic_transcriber.getTranscript() - fnc(result) - except Exception: - errorLogging() + def endMicTranscript(): + while not self.mic_audio_queue.empty(): + self.mic_audio_queue.get() + # while not self.mic_energy_queue.empty(): + # self.mic_energy_queue.get() + self.mic_transcriber = None + gc.collect() - def endMicTranscript(): - while not self.mic_audio_queue.empty(): - self.mic_audio_queue.get() - # while not self.mic_energy_queue.empty(): - # self.mic_energy_queue.get() - self.mic_transcriber = None - gc.collect() + # def sendMicEnergy(): + # if mic_energy_queue.empty() is False: + # energy = mic_energy_queue.get() + # # print("mic energy:", energy) + # try: + # fnc(energy) + # except Exception: + # pass + # sleep(0.01) - # def sendMicEnergy(): - # if mic_energy_queue.empty() is False: - # energy = mic_energy_queue.get() - # # print("mic energy:", energy) - # try: - # fnc(energy) - # except Exception: - # pass - # sleep(0.01) + self.mic_print_transcript = threadFnc(sendMicTranscript, end_fnc=endMicTranscript) + self.mic_print_transcript.daemon = True + self.mic_print_transcript.start() - self.mic_print_transcript = threadFnc(sendMicTranscript, end_fnc=endMicTranscript) - self.mic_print_transcript.daemon = True - self.mic_print_transcript.start() + # self.mic_get_energy = threadFnc(sendMicEnergy) + # self.mic_get_energy.daemon = True + # self.mic_get_energy.start() - # self.mic_get_energy = threadFnc(sendMicEnergy) - # self.mic_get_energy.daemon = True - # self.mic_get_energy.start() - - self.changeMicTranscriptStatus() + self.changeMicTranscriptStatus() def resumeMicTranscript(self): # キューをクリア @@ -531,25 +531,25 @@ class Model: mic_device_list = device_manager.getMicDevices().get(mic_host_name, [{"name": "NoDevice"}]) selected_mic_device = [device for device in mic_device_list if device["name"] == mic_device_name] - if len(selected_mic_device) == 0: - return False + if len(selected_mic_device) == 0 or mic_device_name == "NoDevice": + self.check_mic_energy_fnc(False) + else: + def sendMicEnergy(): + if mic_energy_queue.empty() is False: + energy = mic_energy_queue.get() + try: + self.check_mic_energy_fnc(energy) + except Exception: + errorLogging() + sleep(0.01) - def sendMicEnergy(): - if mic_energy_queue.empty() is False: - energy = mic_energy_queue.get() - try: - self.check_mic_energy_fnc(energy) - except Exception: - errorLogging() - sleep(0.01) - - mic_energy_queue = Queue() - mic_device = selected_mic_device[0] - self.mic_energy_recorder = SelectedMicEnergyRecorder(mic_device) - self.mic_energy_recorder.recordIntoQueue(mic_energy_queue) - self.mic_energy_plot_progressbar = threadFnc(sendMicEnergy) - self.mic_energy_plot_progressbar.daemon = True - self.mic_energy_plot_progressbar.start() + mic_energy_queue = Queue() + mic_device = selected_mic_device[0] + self.mic_energy_recorder = SelectedMicEnergyRecorder(mic_device) + self.mic_energy_recorder.recordIntoQueue(mic_energy_queue) + self.mic_energy_plot_progressbar = threadFnc(sendMicEnergy) + self.mic_energy_plot_progressbar.daemon = True + self.mic_energy_plot_progressbar.start() def stopCheckMicEnergy(self): if isinstance(self.mic_energy_plot_progressbar, threadFnc): @@ -562,83 +562,85 @@ class Model: self.mic_energy_recorder = None def startSpeakerTranscript(self, fnc): + speaker_device_name = config.SELECTED_SPEAKER_DEVICE + speaker_device_list = device_manager.getSpeakerDevices() - selected_speaker_device = [device for device in speaker_device_list if device["name"] == config.SELECTED_SPEAKER_DEVICE] + selected_speaker_device = [device for device in speaker_device_list if device["name"] == speaker_device_name] - if len(selected_speaker_device) == 0: - return False + if len(selected_speaker_device) == 0 or speaker_device_name == "NoDevice": + fnc({"text": False, "language": None}) + else: + speaker_audio_queue = Queue() + # speaker_energy_queue = Queue() + speaker_device = selected_speaker_device[0] + record_timeout = config.SPEAKER_RECORD_TIMEOUT + phrase_timeout = config.SPEAKER_PHRASE_TIMEOUT + if record_timeout > phrase_timeout: + record_timeout = phrase_timeout - speaker_audio_queue = Queue() - # speaker_energy_queue = Queue() - speaker_device = selected_speaker_device[0] - record_timeout = config.SPEAKER_RECORD_TIMEOUT - phrase_timeout = config.SPEAKER_PHRASE_TIMEOUT - if record_timeout > phrase_timeout: - record_timeout = phrase_timeout + self.speaker_audio_recorder = SelectedSpeakerEnergyAndAudioRecorder( + device=speaker_device, + energy_threshold=config.SPEAKER_THRESHOLD, + dynamic_energy_threshold=config.SPEAKER_AUTOMATIC_THRESHOLD, + phrase_time_limit=record_timeout, + ) + # self.speaker_audio_recorder.recordIntoQueue(speaker_audio_queue, speaker_energy_queue) + self.speaker_audio_recorder.recordIntoQueue(speaker_audio_queue, None) + self.speaker_transcriber = AudioTranscriber( + speaker=True, + source=self.speaker_audio_recorder.source, + phrase_timeout=phrase_timeout, + max_phrases=config.SPEAKER_MAX_PHRASES, + transcription_engine=config.SELECTED_TRANSCRIPTION_ENGINE, + root=config.PATH_LOCAL, + whisper_weight_type=config.WHISPER_WEIGHT_TYPE, + device=config.SELECTED_TRANSCRIPTION_COMPUTE_DEVICE["device"], + device_index=config.SELECTED_TRANSCRIPTION_COMPUTE_DEVICE["device_index"], + ) + def sendSpeakerTranscript(): + try: + selected_target_languages = config.SELECTED_TARGET_LANGUAGES[config.SELECTED_TAB_NO] + languages = [data["language"] for data in selected_target_languages.values() if data["enable"] is True] + countries = [data["country"] for data in selected_target_languages.values() if data["enable"] is True] + if isinstance(self.speaker_transcriber, AudioTranscriber) is True: + res = self.speaker_transcriber.transcribeAudioQueue( + speaker_audio_queue, + languages, + countries, + config.SPEAKER_AVG_LOGPROB, + config.SPEAKER_NO_SPEECH_PROB + ) + if res: + result = self.speaker_transcriber.getTranscript() + fnc(result) + except Exception: + errorLogging() - self.speaker_audio_recorder = SelectedSpeakerEnergyAndAudioRecorder( - device=speaker_device, - energy_threshold=config.SPEAKER_THRESHOLD, - dynamic_energy_threshold=config.SPEAKER_AUTOMATIC_THRESHOLD, - phrase_time_limit=record_timeout, - ) - # self.speaker_audio_recorder.recordIntoQueue(speaker_audio_queue, speaker_energy_queue) - self.speaker_audio_recorder.recordIntoQueue(speaker_audio_queue, None) - self.speaker_transcriber = AudioTranscriber( - speaker=True, - source=self.speaker_audio_recorder.source, - phrase_timeout=phrase_timeout, - max_phrases=config.SPEAKER_MAX_PHRASES, - transcription_engine=config.SELECTED_TRANSCRIPTION_ENGINE, - root=config.PATH_LOCAL, - whisper_weight_type=config.WHISPER_WEIGHT_TYPE, - device=config.SELECTED_TRANSCRIPTION_COMPUTE_DEVICE["device"], - device_index=config.SELECTED_TRANSCRIPTION_COMPUTE_DEVICE["device_index"], - ) - def sendSpeakerTranscript(): - try: - selected_target_languages = config.SELECTED_TARGET_LANGUAGES[config.SELECTED_TAB_NO] - languages = [data["language"] for data in selected_target_languages.values() if data["enable"] is True] - countries = [data["country"] for data in selected_target_languages.values() if data["enable"] is True] - if isinstance(self.speaker_transcriber, AudioTranscriber) is True: - res = self.speaker_transcriber.transcribeAudioQueue( - speaker_audio_queue, - languages, - countries, - config.SPEAKER_AVG_LOGPROB, - config.SPEAKER_NO_SPEECH_PROB - ) - if res: - result = self.speaker_transcriber.getTranscript() - fnc(result) - except Exception: - errorLogging() + def endSpeakerTranscript(): + while not speaker_audio_queue.empty(): + speaker_audio_queue.get() + # while not speaker_energy_queue.empty(): + # speaker_energy_queue.get() + self.speaker_transcriber = None + gc.collect() - def endSpeakerTranscript(): - while not speaker_audio_queue.empty(): - speaker_audio_queue.get() - # while not speaker_energy_queue.empty(): - # speaker_energy_queue.get() - self.speaker_transcriber = None - gc.collect() + # def sendSpeakerEnergy(): + # if speaker_energy_queue.empty() is False: + # energy = speaker_energy_queue.get() + # # print("speaker energy:", energy) + # try: + # fnc(energy) + # except Exception: + # pass + # sleep(0.01) - # def sendSpeakerEnergy(): - # if speaker_energy_queue.empty() is False: - # energy = speaker_energy_queue.get() - # # print("speaker energy:", energy) - # try: - # fnc(energy) - # except Exception: - # pass - # sleep(0.01) + self.speaker_print_transcript = threadFnc(sendSpeakerTranscript, end_fnc=endSpeakerTranscript) + self.speaker_print_transcript.daemon = True + self.speaker_print_transcript.start() - self.speaker_print_transcript = threadFnc(sendSpeakerTranscript, end_fnc=endSpeakerTranscript) - self.speaker_print_transcript.daemon = True - self.speaker_print_transcript.start() - - # self.speaker_get_energy = threadFnc(sendSpeakerEnergy) - # self.speaker_get_energy.daemon = True - # self.speaker_get_energy.start() + # self.speaker_get_energy = threadFnc(sendSpeakerEnergy) + # self.speaker_get_energy.daemon = True + # self.speaker_get_energy.start() def stopSpeakerTranscript(self): if isinstance(self.speaker_print_transcript, threadFnc): @@ -656,28 +658,29 @@ class Model: if isinstance(fnc, Callable): self.check_speaker_energy_fnc = fnc + speaker_device_name = config.SELECTED_SPEAKER_DEVICE speaker_device_list = device_manager.getSpeakerDevices() - selected_speaker_device = [device for device in speaker_device_list if device["name"] == config.SELECTED_SPEAKER_DEVICE] + selected_speaker_device = [device for device in speaker_device_list if device["name"] == speaker_device_name] - if len(selected_speaker_device) == 0: - return False + if len(selected_speaker_device) == 0 or speaker_device_name == "NoDevice": + self.check_speaker_energy_fnc(False) + else: + def sendSpeakerEnergy(): + if speaker_energy_queue.empty() is False: + energy = speaker_energy_queue.get() + try: + self.check_speaker_energy_fnc(energy) + except Exception: + errorLogging() + sleep(0.01) - def sendSpeakerEnergy(): - if speaker_energy_queue.empty() is False: - energy = speaker_energy_queue.get() - try: - self.check_speaker_energy_fnc(energy) - except Exception: - errorLogging() - sleep(0.01) - - speaker_energy_queue = Queue() - speaker_device = selected_speaker_device[0] - self.speaker_energy_recorder = SelectedSpeakerEnergyRecorder(speaker_device) - self.speaker_energy_recorder.recordIntoQueue(speaker_energy_queue) - self.speaker_energy_plot_progressbar = threadFnc(sendSpeakerEnergy) - self.speaker_energy_plot_progressbar.daemon = True - self.speaker_energy_plot_progressbar.start() + speaker_energy_queue = Queue() + speaker_device = selected_speaker_device[0] + self.speaker_energy_recorder = SelectedSpeakerEnergyRecorder(speaker_device) + self.speaker_energy_recorder.recordIntoQueue(speaker_energy_queue) + self.speaker_energy_plot_progressbar = threadFnc(sendSpeakerEnergy) + self.speaker_energy_plot_progressbar.daemon = True + self.speaker_energy_plot_progressbar.start() def stopCheckSpeakerEnergy(self): if isinstance(self.speaker_energy_plot_progressbar, threadFnc): From a5e59af8850170edaf3ef8b87133a7e17778b1b9 Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Sat, 22 Mar 2025 17:28:31 +0900 Subject: [PATCH 20/26] [Update] AdvancedSettings: OSC IP Address: Add error notifications. Add 500 status error message. --- src-ui/logics/_useBackendErrorHandling.js | 27 +++++++++++-------- .../advanced_settings/useOscIpAddress.js | 9 ------- src-ui/logics/useReceiveRoutes.js | 6 +++-- 3 files changed, 20 insertions(+), 22 deletions(-) diff --git a/src-ui/logics/_useBackendErrorHandling.js b/src-ui/logics/_useBackendErrorHandling.js index 1145cd80..5cca840f 100644 --- a/src-ui/logics/_useBackendErrorHandling.js +++ b/src-ui/logics/_useBackendErrorHandling.js @@ -15,9 +15,7 @@ import { useDeepLAuthKey, - useOscIpAddress, - useOscPort, } from "@logics_configs"; import { ui_configs } from "../ui_configs"; @@ -35,9 +33,7 @@ export const _useBackendErrorHandling = () => { const { updateDeepLAuthKey, saveErrorDeepLAuthKey } = useDeepLAuthKey(); - - const { saveErrorOscIpAddress } = useOscIpAddress(); - const { saveErrorOscPort } = useOscPort(); + const { updateOscIpAddress } = useOscIpAddress(); const errorHandling_Backend = ({message, data, endpoint, _result}) => { switch (message) { @@ -55,9 +51,9 @@ export const _useBackendErrorHandling = () => { break; case "Speaker energy threshold value is out of range": showNotification_Error(t("common_error.threshold_invalid_value", - { min: ui_configs.speaker_threshold_min, max: ui_configs.speaker_threshold_max }, - )); - break; + { min: ui_configs.speaker_threshold_min, max: ui_configs.speaker_threshold_max }, + )); + break; case "CTranslate2 weight download error": showNotification_Error(t("common_error.failed_download_weight_ctranslate2")); @@ -118,12 +114,21 @@ export const _useBackendErrorHandling = () => { showNotification_Error(t("common_error.invalid_value_speaker_max_phrase")); break; + + // Advanced Settings, error messages are set by Backend (EN only) + case "Invalid IP address": + updateOscIpAddress(data); + showNotification_Error(message); + break; + + case "Cannot set IP address": + updateOscIpAddress(data); + showNotification_Error(message); + break; + default: // determine by endpoint, not message. if (endpoint === "/set/data/deepl_auth_key") saveErrorDeepLAuthKey({message, data, endpoint, _result}); - if (endpoint === "/set/data/osc_ip_address") saveErrorOscIpAddress({message, data, endpoint, _result}); - if (endpoint === "/set/data/osc_port") saveErrorOscPort({message, data, endpoint, _result}); - break; } diff --git a/src-ui/logics/configs/advanced_settings/useOscIpAddress.js b/src-ui/logics/configs/advanced_settings/useOscIpAddress.js index 354e2751..702a6d98 100644 --- a/src-ui/logics/configs/advanced_settings/useOscIpAddress.js +++ b/src-ui/logics/configs/advanced_settings/useOscIpAddress.js @@ -1,9 +1,7 @@ import { useStore_OscIpAddress } from "@store"; import { useStdoutToPython } from "@logics/useStdoutToPython"; -import { useNotificationStatus } from "@logics_common"; export const useOscIpAddress = () => { - const { showNotification_Error } = useNotificationStatus(); const { asyncStdoutToPython } = useStdoutToPython(); const { currentOscIpAddress, updateOscIpAddress, pendingOscIpAddress } = useStore_OscIpAddress(); @@ -17,17 +15,10 @@ export const useOscIpAddress = () => { asyncStdoutToPython("/set/data/osc_ip_address", osc_ip_address); }; - const saveErrorOscIpAddress = ({data, message, _result}) => { - updateOscIpAddress(d => d.data); - showNotification_Error(_result); - }; - return { currentOscIpAddress, getOscIpAddress, updateOscIpAddress, setOscIpAddress, - - saveErrorOscIpAddress, }; }; \ No newline at end of file diff --git a/src-ui/logics/useReceiveRoutes.js b/src-ui/logics/useReceiveRoutes.js index 383f1faf..8df5dc46 100644 --- a/src-ui/logics/useReceiveRoutes.js +++ b/src-ui/logics/useReceiveRoutes.js @@ -525,7 +525,6 @@ export const useReceiveRoutes = () => { "/set/data/speaker_max_phrases": errorHandling_Backend, "/set/data/osc_ip_address": errorHandling_Backend, - "/set/data/osc_port": errorHandling_Backend, }; @@ -558,7 +557,6 @@ export const useReceiveRoutes = () => { break; case 400: - case 500: const error_route = error_status_routes[parsed_data.endpoint]; if (error_route) { error_route({ @@ -571,6 +569,10 @@ export const useReceiveRoutes = () => { handleInvalidEndpoint(parsed_data); } break; + case 500: + showNotification_Error( + `An error occurred. Please restart VRCT or contact the developers. ${JSON.stringify(parsed_data.result)}`, { hide_duration: null }); + break; case 348: // console.log(`from backend: %c ${JSON.stringify(parsed_data)}`, style_348); From 377899d5e163ffd412073f71bd490e96eeccd3a2 Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Sat, 22 Mar 2025 18:06:27 +0900 Subject: [PATCH 21/26] [bugfix] typo, Speaker -> speaker. --- src-python/controller.py | 2 +- src-ui/logics/_useBackendErrorHandling.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src-python/controller.py b/src-python/controller.py index f0356287..258b7f5b 100644 --- a/src-python/controller.py +++ b/src-python/controller.py @@ -150,7 +150,7 @@ class Controller: 400, self.run_mapping["error_device"], { - "message":"No Speaker device detected", + "message":"No speaker device detected", "data": None }, ) diff --git a/src-ui/logics/_useBackendErrorHandling.js b/src-ui/logics/_useBackendErrorHandling.js index 1145cd80..e76e486a 100644 --- a/src-ui/logics/_useBackendErrorHandling.js +++ b/src-ui/logics/_useBackendErrorHandling.js @@ -44,7 +44,7 @@ export const _useBackendErrorHandling = () => { case "No mic device detected": showNotification_Error(t("common_error.no_device_mic")); break; - case "No Speaker device detected": + case "No speaker device detected": showNotification_Error(t("common_error.no_device_speaker")); break; From 9d98f8c4f27b58016a8bab2f8fd4cf028f3f7702 Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Sun, 30 Mar 2025 20:14:42 +0900 Subject: [PATCH 22/26] [Update] Show the error notification, prompting the user to contact us or restart VRCT, when an error occurred in stdout that error occur without status number. --- src-ui/logics/useStartPython.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src-ui/logics/useStartPython.js b/src-ui/logics/useStartPython.js index 394e8cd2..7db442b9 100644 --- a/src-ui/logics/useStartPython.js +++ b/src-ui/logics/useStartPython.js @@ -1,8 +1,13 @@ import { Command } from "@tauri-apps/api/shell"; import { store } from "@store"; import { useReceiveRoutes } from "./useReceiveRoutes"; +import { + useNotificationStatus, +} from "@logics_common"; + export const useStartPython = () => { const { receiveRoutes } = useReceiveRoutes(); + const { showNotification_Success, showNotification_Error } = useNotificationStatus(); const asyncStartPython = async () => { const command = Command.sidecar("bin/VRCT-sidecar"); @@ -16,7 +21,11 @@ export const useStartPython = () => { console.log(error, line); } }); - command.stderr.on("data", line => console.error("stderr:", line)); + command.stderr.on("data", line => { + showNotification_Error( + `An error occurred. Please restart VRCT or contact the developers. The last line:${JSON.stringify(line)}`, { hide_duration: null }); + console.error("stderr", line) + }); const backend_subprocess = await command.spawn(); store.backend_subprocess = backend_subprocess; }; From 4eca60d495731f8f3bde843c6cca04b80a762bb5 Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Sat, 19 Apr 2025 23:51:47 +0900 Subject: [PATCH 23/26] [bugfix] AdvancedSettings: OSC Port: Add the validation that the entry component is only allowed numeric. --- .../advanced_settings/AdvancedSettings.jsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src-ui/app/config_page/setting_section/setting_box/advanced_settings/AdvancedSettings.jsx b/src-ui/app/config_page/setting_section/setting_box/advanced_settings/AdvancedSettings.jsx index 966a3c21..b96c7fa9 100644 --- a/src-ui/app/config_page/setting_section/setting_box/advanced_settings/AdvancedSettings.jsx +++ b/src-ui/app/config_page/setting_section/setting_box/advanced_settings/AdvancedSettings.jsx @@ -32,10 +32,10 @@ export const AdvancedSettings = () => { const OscIpAddressContainer = () => { const { t } = useTranslation(); const { currentOscIpAddress, setOscIpAddress } = useOscIpAddress(); - const [input_value, seInputValue] = useState(currentOscIpAddress.data); + const [input_value, setInputValue] = useState(currentOscIpAddress.data); const onChangeFunction = (value) => { - seInputValue(value); + setInputValue(value); }; const saveFunction = () => { @@ -43,7 +43,7 @@ const OscIpAddressContainer = () => { }; useEffect(()=> { - seInputValue(currentOscIpAddress.data); + setInputValue(currentOscIpAddress.data); }, [currentOscIpAddress]); return ( @@ -61,10 +61,11 @@ const OscIpAddressContainer = () => { const OscPortContainer = () => { const { t } = useTranslation(); const { currentOscPort, setOscPort } = useOscPort(); - const [input_value, seInputValue] = useState(currentOscPort.data); + const [input_value, setInputValue] = useState(currentOscPort.data); const onChangeFunction = (value) => { - seInputValue(value); + value = value.replace(/[^0-9]/g, ""); + setInputValue(value); }; const saveFunction = () => { @@ -72,7 +73,7 @@ const OscPortContainer = () => { }; useEffect(()=> { - seInputValue(currentOscPort.data); + setInputValue(currentOscPort.data); }, [currentOscPort]); return ( From 2f27e5a491ce44de4c720fb76593b30d52a76da8 Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Mon, 21 Apr 2025 16:21:59 +0900 Subject: [PATCH 24/26] [Update] Localization(EN): Localized by RIKU_730. --- locales/en.yml | 104 +++++++++--------- .../setting_box/translation/Translation.jsx | 2 +- 2 files changed, 53 insertions(+), 53 deletions(-) diff --git a/locales/en.yml b/locales/en.yml index 7c677718..a52ac107 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -8,13 +8,13 @@ common: common_error: no_device_mic: "No mic device detected." - no_device_speaker: "No Speaker device detected." + no_device_speaker: "No speaker device detected." threshold_invalid_value: "You can set it with a value between {{min}} to {{max}}." failed_download_weight_ctranslate2: "CTranslate2 weight download error." failed_download_weight_whisper: "Whisper weight download error." - translation_limit: "Translation engine limit error." + translation_limit: "Translation engine limit reached or temporarily restricted." deepl_auth_key_invalid_length: "DeepL auth key length is not correct." - deepl_auth_key_failed_authentication: "Auth Key is incorrect or Usage limit reached." + deepl_auth_key_failed_authentication: "Auth key is incorrect or API usage limit reached." invalid_value_mic_record_timeout: "It cannot be greater than '{{mic_phrase_timeout_label}}' with a value of 0 or more." invalid_value_mic_phrase_timeout: "It cannot be set lower than '{{mic_record_timeout_label}}' with a value of 0 or more." @@ -28,18 +28,18 @@ main_page: translation: "Translation" transcription_send: "Voice2Chatbox" transcription_receive: "Speaker2Log" - foreground: "Foreground" + foreground: "Set To Stay On Top" language_settings: "Language Settings" your_language: "Your Language" - translate_each_other_label: "Translate Each Other" - swap_button_label: "Swap Languages" + translate_each_other_label: "Translate Both Languages" + swap_button_label: "Switch Languages" target_language: "Target Language" translator: "Translator" translator_label_default: "Default" translator_selector: - is_selected_same_language: "Since the same language is selected for both '{{your_language}}' and '{{target_language}}', only '{{ctranslate2}}' is available." + is_selected_same_language: "You are selecting the same language for '{{your_language}}' and '{{target_language}}' so only '{{ctranslate2}}' is available." message_log: all: "All" @@ -48,7 +48,7 @@ main_page: system: "System" show_resend_button: "Show Resend Button" - resend_button_on_hover_desc: "Press and hold to send" + resend_button_on_hover_desc: "Press And Hold To Send" state_text_enabled: "Enabled" state_text_disabled: "Disabled" @@ -57,28 +57,28 @@ main_page: title_your_language: "Select Your Language" title_target_language: "Select Target Language" - update_available: "New version is here!" + update_available: "New version is ready!" updating: "Now updating..." update_modal: cpu_desc: "Use CPU only as the compute device." cuda_desc: "Selectable between CPU and NVIDIA GPUs as compute devices." - cuda_compare_cpu_desc: "With GPU selection, processing is faster compared to a CPU." + cuda_compare_cpu_desc: "GPUs offer faster processing than CPUs." cuda_disk_space_desc: "Requires approximately {{size}} of disk space." close_modal: "Close" download_latest_and_restart: "The latest version will be downloaded,\nand the app will automatically restart." is_latest_version_already: "Already using the latest version" - is_current_compute_device: "Currently using this version" + is_current_compute_device: "The version currently in use" config_page: - version: "version {{version}}" + version: "Version {{version}}" model_download_button_label: "Download" side_menu_labels: device: "Device" appearance: "Appearance" translation: "Translation" transcription: "Transcription" - others: "Others" + others: "Other" hotkeys: "Hotkeys" advanced_settings: "Advanced Settings" @@ -90,17 +90,17 @@ config_page: mic_host_device: label: "Mic Device" mic_dynamic_energy_threshold: - label_for_automatic: "Mic Energy Threshold (Current Setting: Automatic)" - desc_for_automatic: "Automatically determine microphone input sensitivity." - label_for_manual: "Mic Energy Threshold (Current Setting: Manual)" - desc_for_manual: "Manually determine the microphone input sensitivity using the slider. Press the microphone icon to input your voice and adjust the sensitivity while monitoring the volume." + label_for_automatic: "Mic Sensitivity Settings (Current Setting: Automatic)" + desc_for_automatic: "Automatically control mic input sensitivity." + label_for_manual: "Mic Sensitivity Settings (Current Setting: Manual)" + desc_for_manual: "Input sensitivity can be manually adjusted using the slider. Click the mic icon to test your voice input and adjust the level while monitoring the volume." speaker_device: label: "Speaker Device" speaker_dynamic_energy_threshold: - label_for_automatic: "Speaker Energy Threshold (Current Setting: Automatic)" - desc_for_automatic: "Automatically determine speaker input sensitivity." - label_for_manual: "Speaker Energy Threshold (Current Setting: Manual)" - desc_for_manual: "Manually determine the speaker input sensitivity using the slider. Press the headphones icon to listen to the audio and adjust the sensitivity while monitoring the volume." + label_for_automatic: "Speaker Input Sensitivity Adjustment (Current Setting: Automatic)" + desc_for_automatic: "Automatically control speaker input sensitivity." + label_for_manual: "Speaker Input Sensitivity Adjustment (Current Setting: Manual)" + desc_for_manual: "Input sensitivity can be manually adjusted using the slider. Click the headphone icon to listen to the audio and adjust the level while checking the volume." appearance: transparency: @@ -110,12 +110,12 @@ config_page: label: "UI Size" textbox_ui_size: label: "Message Logs Font Size" - desc: "You can adjust the font size used in the logs relative to the UI size." + desc: "You can adjust the log font size by changing the scaling factor relative to the UI size." send_message_button_type: label: "Send Message Button" - hide: "Hide (Use enter key to send)" + hide: "Hide (Use Enter key to send)" show: "Show" - show_and_disable_enter_key: "Show and disable to send when pressed enter key" + show_and_disable_enter_key: "Show and disable sending using the Enter key." font_family: label: "Font Family" ui_language: @@ -124,14 +124,14 @@ config_page: translation: ctranslate2_weight_type: label: "{{ctranslate2}} Model" - desc: "You can choose the translation model to use for the internal translation engine." - small: "Basic model ({{capacity}})" - large: "High accuracy model ({{capacity}})" + desc: "You can choose the translation model when using the {{ctranslate2}} translation engine." + small: "Basic Model ({{capacity}})" + large: "High Accuracy Model ({{capacity}})" ctranslate2_compute_device: - label: "{{ctranslate2}} Compute Device" + label: "Processing device for AI translation {{ctranslate2}}" deepl_auth_key: label: "DeepL Auth Key" - desc: "Please select {{translator}} on the main screen with DeepL_API when using. ※Some languages may not be supported." + desc: "When using it, please change {{translator}} on the main screen to DeepL_API. ※Some languages may not be supported." open_auth_key_webpage: "Open DeepL Account Webpage" save: "Save" edit: "Edit" @@ -143,7 +143,7 @@ config_page: section_label_transcription_engines: "Transcription Engines" mic_record_timeout: label: "Mic Record Timeout" - desc: "Detects silence and, when the specified number of seconds has passed, considers the mic input to have ended. (Second(s))" + desc: "Detects silence and, when the specified number of seconds passes, the system considers the voice input to have ended. (Second(s))" mic_phrase_timeout: label: "Mic Phrase Timeout" desc: "Transcription processing is performed at intervals of the specified number of seconds." @@ -152,9 +152,9 @@ config_page: desc: "It is the lower limit for the number of transcribed words, and only when this number is exceeded will the transcription results be displayed logs and send to VRChat." mic_word_filter: label: "Mic Word Filter" - desc: "If a registered word is detected, the text will not be sent. To add multiple words at once, separate them with a ',' (comma).\n*Duplicate words will not be registered." + desc: "If a registered word is detected, the message will not be sent. To add multiple words at once, separate them with ',' (comma).\n*Duplicate words will not be registered." add_button_label: "Add" - count_desc: "Current registered word count: {{count}}" + count_desc: "Words Currently Registered: {{count}}" speaker_record_timeout: label: "Speaker Record Timeout" desc: "Detects silence and, when the specified number of seconds has passed, considers the speaker input to have ended. (Second(s))" @@ -165,20 +165,20 @@ config_page: label: "Speaker Max Words" desc: "It is the lower limit for the number of transcribed words, and only when this number is exceeded will the transcription results be displayed logs." select_transcription_engine: - label: "Transcription Engine" + label: "Transcription Engine Used For Speech Recognition" whisper_weight_type: label: "Whisper Model" - desc: "Larger models tend to have higher accuracy, but they also consume more CPU or GPU resources.\nEspecially for models larger than medium, it may be difficult or even impossible to use them depending on the performance of your CPU/GPU." + desc: "Larger models have higher accuracy, but they also consume more CPU or GPU resources.\nEspecially for models larger than medium, it may be difficult or even impossible to use them depending on the performance of your CPU/GPU." model_template: "{{model_name}} model ({{capacity}})" recommended_model_template: "{{model_name}} model ({{capacity}}) (Recommended)" whisper_compute_device: - label: "Whisper Compute Device" + label: "Processing Device Used For Whisper" vr: single_line: "Single line" - multi_lines: "Multi lines" + multi_lines: "Multiple Lines" overlay_enable: "Enable" - restore_default_settings: "Restore Default Settings" + restore_default_settings: "Reset to Default Settings" position: "Position" rotation: "Rotation" x_position: "X-axis (left-right)" @@ -188,46 +188,46 @@ config_page: y_rotation: "Y-axis rotation" z_rotation: "Z-axis rotation" sample_text_button: - start: "Send sample texts\nto Overlay" + start: "Send Sample Texts\nTo Overlay" stop: "Stop Sending" - sample_text: "Sample text." + sample_text: "Sample Text." opacity: "Opacity" ui_scaling: "UI Scaling" - display_duration: "Display duration" - fadeout_duration: "Fadeout duration" + display_duration: "Display Duration" + fadeout_duration: "Fadeout Duration" common_settings: "Common Settings" tracker: "Tracker" hmd: "HMD" - left_hand: "Left hand" - right_hand: "Right hand" + left_hand: "Left Hand" + right_hand: "Right Hand" overlay_show_only_translated_messages: label: "Show Only Translated Messages" others: section_label_sounds: "Sounds" auto_clear_the_message_box: - label: "Auto Clear The Message Box" + label: "Auto Clear Message box" send_only_translated_messages: label: "Send Only Translated Messages" auto_export_message_logs: - label: "Auto Export Message Logs" - desc: "Automatically export the conversation messages as a text file." + label: "Auto Save Message Logs" + desc: "Automatically saves the conversation messages as text files." vrc_mic_mute_sync: label: "VRC Mic Mute Sync" - desc: "VRCT will not send the message to VRChat while VRChat's mic is muted.\n*There is a bit latency and Push-To-Talk is not supported." + desc: "Messages will not be sent to VRCT while VRChat's mic is muted.\n*There may be a slight delay. Push-To-Talk is not supported." send_message_to_vrc: label: "Send Message To VRChat" - desc: "There is a way to use it without sending messages to VRChat, but it is not supported. Enable this feature when you intend to send a message to VRChat." + desc: "This feature is not supported, but there is a way to use it without sending messages to VRChat. Make sure to enable this feature when you wish to send messages to VRChat." notification_vrc_sfx: label: "Enable Notification Sound When Sending Chat" - desc: "Disabling this feature will send chats quietly without playing a notification sound that others can hear." + desc: "When this feature is disabled, messages will be sent silently without playing the chatbox notification sound that others can hear." send_received_message_to_vrc: label: "Send Received Message To VRChat" - desc: "Send the message you received from the speaker's sound to VRChat's chatbox." + desc: "Send the message you received from the speaker's voice to VRChat's chatbox." hotkeys: toggle_vrct_visibility: - label: "Toggle VRCT Visibility" + label: "Toggle VRCT visibility" toggle_translation: label: "Toggle {{translation}}" toggle_transcription_send: @@ -243,4 +243,4 @@ config_page: open_config_filepath: label: "Open Config File" switch_compute_device: - label: "Switch VRCT to CPU/GPU Version" \ No newline at end of file + label: "Switch VRCT To CPU/GPU Version" \ No newline at end of file diff --git a/src-ui/app/config_page/setting_section/setting_box/translation/Translation.jsx b/src-ui/app/config_page/setting_section/setting_box/translation/Translation.jsx index 63f0630d..5c4dfa51 100644 --- a/src-ui/app/config_page/setting_section/setting_box/translation/Translation.jsx +++ b/src-ui/app/config_page/setting_section/setting_box/translation/Translation.jsx @@ -62,7 +62,7 @@ const CTranslate2WeightType_Box = () => { )} desc={t( "config_page.translation.ctranslate2_weight_type.desc", - {translator: t("main_page.translator")} + {ctranslate2: "CTranslate2"} )} name="ctransalte2_weight_type" options={c_translate2_weight_types} From ff4f40328ae1236e612318a516564068a3786568 Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Mon, 21 Apr 2025 17:41:42 +0900 Subject: [PATCH 25/26] [Update] About VRCT: Add a localizer and a contributor. --- .../setting_box/about_vrct/AboutVrct.jsx | 6 ++++++ .../about_vrct/AboutVrct.module.scss | 3 +++ src-ui/assets/about_vrct/contributor_riku.png | Bin 0 -> 60630 bytes src-ui/assets/about_vrct/localization_1.png | Bin 7268 -> 5569 bytes 4 files changed, 9 insertions(+) create mode 100644 src-ui/assets/about_vrct/contributor_riku.png diff --git a/src-ui/app/config_page/setting_section/setting_box/about_vrct/AboutVrct.jsx b/src-ui/app/config_page/setting_section/setting_box/about_vrct/AboutVrct.jsx index e36a39f5..d64f4e3c 100644 --- a/src-ui/app/config_page/setting_section/setting_box/about_vrct/AboutVrct.jsx +++ b/src-ui/app/config_page/setting_section/setting_box/about_vrct/AboutVrct.jsx @@ -10,6 +10,7 @@ import contributor_iya from "@images/about_vrct/contributor_iya.png"; import contributor_rera from "@images/about_vrct/contributor_rera.png"; import contributor_poposuke from "@images/about_vrct/contributor_poposuke.png"; import contributor_kumaguma from "@images/about_vrct/contributor_kumaguma.png"; +import contributor_riku from "@images/about_vrct/contributor_riku.png"; import localization_section_title from "@images/about_vrct/localization_section_title.png"; import localization_1 from "@images/about_vrct/localization_1.png"; @@ -86,6 +87,10 @@ export const AboutVrct = () => {
+
+ + +
@@ -158,6 +163,7 @@ const about_vrct_links = { contributors_rera_github: { img: contributors_github_icon, href: "https://github.com/soumt-r" }, contributors_poposuke_x: { img: contributors_x_icon, href: "https://twitter.com/sig_popo" }, contributors_kumaguma_x: { img: contributors_x_icon, href: "https://twitter.com/K_kumaguma_A" }, + contributors_riku_x: { img: contributors_x_icon, href: "https://twitter.com/Riku7302" }, }; const OpenLinkContainer = ({className, href_id}) => { diff --git a/src-ui/app/config_page/setting_section/setting_box/about_vrct/AboutVrct.module.scss b/src-ui/app/config_page/setting_section/setting_box/about_vrct/AboutVrct.module.scss index a4d06e2e..f9efc103 100644 --- a/src-ui/app/config_page/setting_section/setting_box/about_vrct/AboutVrct.module.scss +++ b/src-ui/app/config_page/setting_section/setting_box/about_vrct/AboutVrct.module.scss @@ -160,6 +160,9 @@ $sns_left_pos: 0.8rem; .contributors_kumaguma_x { @include contributors_sns_styles($bottom_pos, $sns_left_pos); } +.contributors_riku_x { + @include contributors_sns_styles($bottom_pos, $sns_left_pos); +} .localization_section { display: flex; diff --git a/src-ui/assets/about_vrct/contributor_riku.png b/src-ui/assets/about_vrct/contributor_riku.png new file mode 100644 index 0000000000000000000000000000000000000000..8d5aaf8372fe34aff29bb0235e02b37bacab9b9b GIT binary patch literal 60630 zcmeFY1;0}X33=Y99xDSNj?oMzB?(Xgy+#Qkt!QCZ60t6l0-SzT&p7*_f z!TosG>RxB{`Ea_sdhe=Tb*iJ36=l&;h*1Cl0JGNZRMoI)jeU3 zU#wFt7Tm~@PF>FY%q9fP;s*v)q=K^}aNun8q}hD%dNo#WivJlXyzb9~HJ6$HR%iLG z(^Ti)+8s3~H5VrrwM)Qg#N8U%G})$ez}@ot8UQ6sqEMLu^#2e4ADEz#&FSHCXGyV~ zOX;jZsexj+M)_Ep@ZGx6!U)fzcVYbTdnO(Y=CRNPV|MM_oE*L2qtMBrZ)J=pccjOF z)>_>6qrI_~?9_w&uDxNNb9Q#ts=gu4KoD~wxHcYs_;Tf@gP;x{w78i`5s`x?B)WJ0 z%+mM4#LdkOW}UouA^2zd>Cpp64D06l0d-e?|6RSJqGA^kANH%Rl*%VyPdFAAuE)@S~OZplrYn26AOzSB40t|1sD*!G35m{0^T0G@%^C#LZM#l z6Dh(;LN7)j%yJ6@+=({roWPhI=2 zd7DlYAGCPxZ|~3g{KOvx++cP>FL*+3aX~Sna49h>*B)@oZ~NJtJuDIx`h?v?_%SuU=d}pWH6{^+L1Rva9gsFl7)>ezo?rH zZ8N_^{!%=%yj$&=L^z=MRi@v$e`*vF3a(99_Cf)n43;Uw7QL@#r5q>@)8FE77YLK^ zcB~mxl#Ds(E_MjRq%>^?c=Kv)1Nf6d@E-9Up4!UF{;c;n#N{AO#DNu22K_^Du-=){ z6W9-rHoX?Vt4eL;`JBIKCz71NB`*Tf3jRd#guljy+VE6x-D7p(c#Z2swlOm7;FvQt zd?hadidF#^{6{lP&Ro^7oZ;Id@dxjoqy&1toP^$O9WX^cT$m{;|7D{Eicz4;y>}9d zFC~wfOvn^*3?c8x=B5edQJ^@8XXiwqN|DRA2Yc6m1U{zkIkazoY z83SlJ=)dP|%om$?&iglt0$Nlq*a!|~M&NHH)ySvDy%~n$Bkr_2N{e9aCHnf}fcS7g z`yYDH1FH{R-B&tT#@#_I^m3Oq%WIZTx&tj-t*iBRc#(qCl6Tvqlj()s)2k@^WH14y z-ygUgIJcFQl)4+1IIF9x)oG93KO&2?-K+bl_G5&okHV-tDMPlc@shwn;zE?Xz-zJ+i(kN2clwpj-oi<+LKY3 zeQ@wS^L@m=lj%R{ZZ=*Ua?I+#+j`r@3*d( z5S$CW-wQ5zc(gxw_>T15?l*z&EL=tfZ6xNyz{LzRjI7xSpeQ(Kh$7t(_O@8@<|x51 zBw>Cg`sD`oMW!E~T#g#s558|a_>|h5T6R#pOiMwve>V3L_ram*L~Y^m?jRL^(fmo> z73w1RKvJ-dC$y&I(AaJDBdAbD&?PM~{DRkSnE-6(OG$L1^LSMeX|b*m%-KQuk-;oqP}lMW~)*m;2^~&w=%=6zjj>AGo(hVvT0<*qh4Xb$NR$&m_}ABayrE`mNbIS<*g( zqW$1efalixS)beD*<3$>#8qsIgGKbkR)z^AJ=$jw53KwT>GI3pPo zsNK*1f^j^w4tQntbKu%3Gpr8)uD6^tsxUZ6Gr%mtdE(&3!<0}+248s6iB^2?3F_A3 zQ>Zfs&7!A-M9x;B^d*Wzq|Mv3cu|^E{B`kguJvBq zf-Va+kO^n+{LE#gMyIXCql#{0y>FOT#eV;6BU^){Pjd{N9JADl5k_U@uQKe{l=B3Y zot_qWv{O;eKxl9|ffPWfcZ5`fy_6>m*8G9f-=VOnk=$Z)ZnAF(8_|Zu7g* zT3iOw=vI?PCAU$8!>&l0e^U2hluxhGz=UWS;rR2j>>N~bWAStJA zpb)3D@!$t);Sp##%Ah^bKnUNs->Bd z`M`GPi+#PEb8cYUv|_TZ8jCco4~9JtD_%nMxy^8NPfNv**MJ^Hl_%g z>a{fY&6pj69C%_NQ>3+#Uu4YN{7eYKdM8VBXgahL#uH~gp1$z5BkbomVUeh{^n+9Q zS+InZSDU&(YQhQ#_je4EM7@kxUJpVjER!B#dARxlY6b`GYBA&prMq|WCfF@9GIwy{ z0kKC&5c3@Qohev-7{gSz-(j2L=xoc~`x$B$DHi2l7RT>;HA`*VS5G;Q0d+PDNc*J6 zmj4KQ8A;7Xg{$IG3zS!el;O8O2F+oJ{e%nnn^z#OW=cs=+utUy)?DHW8ah`t0x+av zoOLoBVa~f&Hpy?!7WcixmRV#q`dq)EBPZSEiCKdO`EYBza3G0~WpuCiF zRfqX@ySa1^zwin!Pi?LTHrM<$DSO?|$W04d!SuB{y3+2i_ZN0LkaR=vLA?blAt= zI2fgF)$(KfJ8<&gK_4Y^Zv_ACJl-sMChbNelH>0RuF-z+$=GXaLQ?`HTjb;b{v3&y zYid^l@xoT*9x)S3{X|Iw`$a1!vbuUH5yg@Xw+3r3|-fJBoylMk|5)APvl-R~*GrM^)@ zguSzf`z$XyXOuzoVeL{lxD-D;O63rq#kTQZ$qCD7PdYD7W=4zVw>pbd%?v|M1|Y1x zl%-=nbMp641(Fx5!-l_TfCx>Gd7Fel3FjOM8d1*`EU6=Auqlz1Sf+3wTLm=q1#GR)XxFgo^8QMZCCoQiyq) z>fs*D;J|!rbra&N(BL(?@?DTMD+fIKO!qy#&WZ_?TuQ)~C4z}EXpcfmsTC9e8>dEO zms=jKd571WrG zaHE!Sasmi-ro__lHL(9J#yqH4OVLqU@0@IWRQgNlLym`Cn_b%K@Sj%U{CSl#A%f^b zy1f7Om0E;V^U|5bRJ4l1RkXgYPNZ808@?^SUS_oahL>h<;fbnW`)e^!%2`nI^S)q@ zX!vfhiCMV;L(LL`!YG1a=n8KIsm95UO3&^akKP9r;9J;%An^eej%I$91FO1_orQOr zaja1{d$|?~PCBzO-cz8lH@AEDAcw}mCwBWh`FjzxoAO62;W8PwLARd0;m@w(|4%~_VBw>S*Yfma-fz=-sAI0uG-$Z9Wt!;l@74wFMBEw!lx$Q zHppI=C_EqUf^2UQx1~8Yzeu;n_c(XAjn;U*I~Fwtm|w7v8^FFu2+X-JVU1*>vU#wU zP7DL$Klqb#-9WNGioOMyz0J`l4~II~B{kBEIQB?t*?s4Re=ojGj1TmeJK(LwNbE42 zb!P4*lu@EthDN6PM8z^uLNCOu09Vm>UWElR}i>cGH z4?Y|exA30|aJLzUGGh8+{9FIHxBxIe22|!Gza))aW|5XbkCO=hgHl~_tgy|fj30rd zX6sJE)dZ<{$M5Ar9`X7su}4){XP_&S&;g+OB@ybsNr04pW%2i`&sX2gcOfP)7k>c!d zwPvp=-tt5+o zV(#v^0CzC9=w!}N=hsfO2bwOYIZzT7+PvoyTNHT+;1b3=*SX0v24|pubHu* zk~q^!>dwhXk&%&AoE3;t=LZ^fl4aJk(M>MxY+=> zPOgI1w2OOVc2CkeSP;OD9;v_;b1z3bR2T`(a`(bsaJxT*$y_+(Ov_?L<5=`~uWk=- zV(SR}Dx5c9rmL%?;}t(c?eXO3g2_aKGRTVgx%X>9YV~@0YV~vo4MlKM<8lCoTeO5U zgKE*3yksQ1GMoIWOb+mTN1ayhK@+`2CRL;hvDP~v3I4f+rVJc)8NzsxRtHrGb4m#2^ z+2b-Ys^VKV`ZIWLXAjr@=2}g$IIOj2ddDKvD9LPU&nOIRe1ua($N_~oS&Iz zBQAglLkc`;*#9p76_?yw9TWhkh1FI&F)DmTT*0GUp4*bfBIO zdBU?EMB;YvO`OqzSP|!AC8%$p@;9VUJO4O*LT4@(bJ@eLP)w=yB74%~XXBCgW6pR* zcR8a_q5^A4wfK3Frfl65ml8!zJJRYu?GG~E;Xxn1hYMElCANFAijIUhoT{Gi-OeSnl`OmI?pfNZQ@!y!4taQ8_zZ5*V zdU<(Sr**UYsFTuoLF+Xfmy#^gs6C$N86W<_EDMunA78x> zPgBg4u&zc;65GJ<0p-A3X2tEj%tE_MzwmLMVi_>blcEK7)q~B~;X)D#RBHJ z=t8q!hg#H%2;S)MFC0x*YL09q$}iWqn9dE(F1t|%4TtPAf~Ch~L5T?$){4bvMGUE{ zgqOAKb`e(q2_7mix=Emsrltgitaek2P}lDTjt@5@jxBcJh_S|0IT^QWjmhGVGH@CO zC6GN%vs*JJZuH!}AmG8n&@g?ju6xcu{|uC=?huK)!?&WJv0p$^K=IxW_7C!$Xn8ewM{E1qfghDShk{ zR?neHUmz3y*nV(3wj9jyY00>oN}7>^LrfgMXa-wzfq9l*kI32!-2BxKEptqy{{hdX zGdXb?^qzAcu)a?Fn{;bLL>KK}oDlBTZy;t3o(Q2v>%p|d-kEW=KE158dgUb5b1YM) znb)Xt5~3;gxB)7;KA^h-AytO(VVk^z_L5?XR1bmr4-64k(DEu>n1_z@D8HMm&gx$C z>N&WADpDy+Zb-PV2=Iw@V#{)3X8tAOiA6nxVX^s*uc{_hmINo8eS|FqNXV6ap?Bl+ zQ~C2Q6KQm=3k zeFYy_q99UL0$_r=5w3*82msXzK)F6r%5-QJ-Wy#@%EFZnorG%YRB%`<1{Fy7RsQi! z*~%JBG6|KwLi3kh8XsXw-3pq{I61hK>0B&ex2A)Z%=1KNe%p|r(xA#1l*-Ic7h;RR zQBiro3BgA%qo`|GRf|0nXXl)FC906|(!-a#x91+<|2pAo+q$O7nf&rox4y47NYC)) zT#WWA?6c*`b{FvzNrM624pYODu;;sGTPAD~f_Y%2qd4FGLTU8O_pDAwA|gGGAkLfm z_rlzSd!VJ%UcFM`&q%|gu)g4R#Ue^A!(&Y7*3B7$1d}SpZ(O|sN5?&eebi^qiC&|%VKv5HuFA)6Em15eGQNFgsNxe@z_2K|#vFZG!# z8N?Aj;1uTNC@a3e_Z)vLwB2;REFK@4*;QUtCO(>?C-jz-M`%OkA74cBcmm!m|ILpt zFOHf)O7ob5_HlaIfzS9_?aAem(C?RrxL>&}R~Zq>)|Ix zG9|ylnRS4mB6f)l=1g0xDd9{44CS17wqh27Tgd(V)CA;F%7}tZJW7zNmST#%eJ?C| z(BI);)nu*0_Yy!=BIeO8vXUY*@~_$rA?!WgEa7%IuA?qEa>3GiyFnl%B-n z++{oRysd;Isz@*B>uvQZ8 z{HQv7+=@(EG7m68I>nBwIT@X^tTft+)D+lW&#jBg(IU0JsP#6qi_%_9-Yb?j9XjD) zBD2-6>gVwm!it4FQ{D&2cqYsN+<9K48e4*3Mh+%-O&XjIH8sSM^d7{q=1QEjo++o; zg?LBGUC{Ud%*@R6CMkh8nncZNG+x^q?Cia#uK{k6Pd>l3Q&Nhg0-Vb-{OAeSWtkM z&wr5ux%5KwsVp_2YSHLV#6Us|;T^x{C3GFz%0`fjgXxx|t|a()xT!<17w?&pJ|iQ; zBZe|})%@`o{c_pPn=GOqs%7$K#-h`qvW5x1%qhb%sE<~SY(yDUR6A^@ENC~tcKq(6 z{2$yB^x^}t9!4mA)|?2A*@fssEBRla?hEpjZv$f0zzSVNhBNUWY=h6&W0AYYx0sRS zyrif&7)GLND~K(@nf1457iz3_bxZ(t4ZM`{;;=pYYR;~$QQ6kBJ`qRRbj-N>;*zR+ z2iZFM9tLM-igNf0Quu)US~wveG-Gm53w+&11iu^e-EeC%1u*3M^wx!u3n1!gk$a?j z&7TCUqSQ+lFb%44C=b0b3|I5m9iD6tT1yyY-8EoKvo%?fvQb761mOIx@1X#=G-6Br$^4xI27m^kUJ1R1z>Ct6Bn#lyzuGi5N3Wzjh4MAw(Gx%Zr z(rGWU?A~+pE@q46N#eiyy$fRj>eJp?zHxQ9@t6g^iaYxKz7%pJ$8q&D#jVIrF#AiB zyJlFIXe)iK!4w00j2&g&VfGB47fKP4ljxt(*SI5Bsu*`f`aakD{?uT?)sI;FsFyVM z4dGhri=w<)0BIe}H2|(f^skc40%W!x6s;EjCcM%DY$%dAsWK(+o0{AGrE zf?t1TnK2B7YBfW2+x-rEeao1AL>6PYV0Lx(Sf0&MwU zHHew*QJa-HE<7nhUJ`QZ{5_Sil%7`2Z#$}Mcrh0M9+(bQ5JP)0C8mA+fVb~jT<+Af z{A=HH+(f$Ze7-Y)v|{vv19OATt_bx-teK$YL4EX8YJe)s>p$g(gK8H{l94hq5o5rz z>(q6S(smp=Hx3-lc=|A|NF^KM9KhmnyZ-=5i0m>os%#L37D<%Ar|~qCmxiQa^p42{ z`5>Ci#V*e`^l+fnMLSQkloLyTsVW;A{;8?-yIJse)ly|1-}x6GkmQVwMD3qIncCa! zC|vqoap!S3xQcQpucmnO0isTzEx_SzOAlBHL{XpKQai2oK2+#rKfe)*bF1EQLQ|@| zGxHD!`&X}h+^AzMRXVx;=Wx}Q*Pb4j_xf5dRnV-*9yG_NF7l-#=R-gYmT{P=meVJ! zL?q7Sm8)_?-aGQt{P7IQ^UbA>(H1r260*DVBOQjEwwE=)P$CiY8>26sg8>O~!+2Hi zJ^d3FaI$OK8@NTvGS4o!+xLTmkg(hY#NCsUe#u)Dm>X*1^AH;@tFq5Ef22mI+}u>A z#6u!nz4KkYq$LXnnemKyti?ihrX~7{+Dc|b7Pp*s5@GJ}x@y+8*4Dk|zB;;+F* zbi8e%*7relgr~H)mC}v5bZ`p{Uk!5IJd#5Z3#+Yswq4GuEvl@&|{&B}dT zs_iGXp1*UGQ&Z1(!x4X{k)KX}Y|grb4z-^lH%K(*K#cRZD0&*pSxT<11<+i=LQ}1W zJm;xONbx09RVOmMR9=5+i=ve6h8eE{-{y>9p$tmUA~((t06}6g5(c$g_AF*piMz;d zg4Bo>>y>sg2@ow7-#_DIq(^H^Reg`lvfpQzPQ`Wic8WrF2ih@*Y5)z3p@=tsGF{$D zQAv@q8VPKPY=qvfXktCUR&trl^kl2RAbHjJKQZJhS@|U!OmW4i4?#D7nteZR)EbKcH{5{ zQ&7-C3}KD%?lX}djt_zBht}!qInjJRLNw!Yr|qq5*!j-D&NBZ{r@QGDO{F(7#TnEP z?*N00m6+2go$M~McMrp3!UyXuk2~OV<)U}fQ|Oay*cwZ5N8QSyXItNjslPkHDiMem zZP4FEM+p(ZKUaDh4F-fNjlxu!_bQR_b=R1L4isybcWk%5cwr&|p+BKj+SfFAJK!Q^ z*sAN1^ky=InIYlT@(prH_Yl2)`ymzdY{XsT+xYfE9znR#0r1>E)(WL&DtS{dO_utO z#18Ga>a*y%$aTBHeocMvWht%aUR(?G8>*wY2mAKPEV_6otli;b_J+T+sDwkP_PO(- zvOrT^y^O#jTa~uBn3POFO*IO{la)$)T1-QZNF!fn53kZPFV#C)3Jfj{vvGqYdJZ^C z?21R8znSm9eL26X?EcwsJ})ZdaZ1YIpz~Wz8BI*lvCRGVtX}TBHjA6OWrmb4_DPC| zUrehNAZsDZN{~x8-96KpJ1#(ByY+Uc`pOOw$+xsN=-`IMs^%;fb&%I|7EN|~SHTqI zt+iZ`0!w$X?cJ7Nx%54cl%EPKjMs4z)!0Qz#Lu$qwwKyuE675HnWOhkioGSjq?5~v zpv?Zv$1fw0#u7QhR7YE<7zjPkfg)|E)M8;P@qc|Zmzu5oJk>Mt&7=3YQPB9XPLRrm z&m_{Nt)I$~MtY_($mF(xZ9R(Rp8nG{5%K*%w-*)$*+;Yp3V%X!Ij()yS_zs;XEBkU z6Mfw_v=?ZgD!H+}8JuPtS!=@&V5e*3Y*G#3QO9i;QvGZoOGBGlBE8-sLx5s18C^VxeEt(F9Lw2JD&xQ}XpCf-y%kvj6iClx%+!ongH zO}d?qA>!y^4ETy4QmzDuJ>g=HF;A7Z$qUyb1pTHGzu(56udfgtL;bL`t9yG<;q%WI zrw&&?@$r`}(UD=_d%Z^jOMLL?3Fp;c#7h`Lhb`YxS6)Bd4^U-rIv%gPAE+x?Uo!=2 z7cOcX>%ppqK-KQ|geeeOLfP#Zz&IB0CECqmzf?6FrIPZ@<1FT3f=~VhZh)+{{h4<> zRH??Q6d)pizR;?##Q2KhrDlgy?a&dW7toIcvM`n?Mekl6d%x-|rm)a0Nn#oY(q+fmNJE<+C4<)&g=s7!2Gu%paMF}Fi zqejQ5gCK{%M8ScL;fsAiIT*Qq&>ppGU~Tv!nxsdn&WRtA5axV!ZdkVQbf9MdUl>Rh zVj#~)@&sCb5U;CD$`_2tFS$C19DzN?Isf>lnGw=CUpJrkpFOjNZGEQTFX>G@oZ=(# zadHVZJ_e^^kv_RBd~|R7kowoFbGTah*y{Kb?Bz1MXoATZu1OJ&*cY5TrSb3BIGZLi zYyA!LIg8_IKd8Z2*p462qWYP1;E@^sGIvlw$zNWZo^#Y7BP9;c9%#|axW7MoBE`R4 zPm5ihixX1*BGerVlc9wTd5eW0G7{=UoA>whlzk8$vtJK?!dKmtB}!5nl&FhW{yiyc zW9^egN)Nwve+lDl=71pLxes`rQ1U}c*VKd}*1r*U-EC<>CqJDPeGLXjs!g(-QM>HL z-6RUZv#Rq+=&F_`D`%hjF;ev+J=K6@wL7PF4)u6{w+g{T9pE7&Q*BC(MIbw1J&Sg! zQM~w2yc|+IvjsR|fLd4>2uMTDTpFTKHiMJ(@*;7t``3dfpDDldCwO}Zx!1yol$t}A z&FVacaZ}9#(3K{qp^wv0$0aQXp5#`exdg2E0-RByxU=nV_rT-CFF08x2qAN}+cLbT zS7t^A2Blxy=uNDown@7nH}_2M4L7VN#uS@xXm`28V%}@py>`0_t23g@Vqd5uX6}$s z2C1u9aMRG1!VWqr`a!Atle1nZq4}ThDfIq+u`9?s_b|JZ_9R9a_*qGm+4%;O>9a2m z!I&uem-N;3vteA2nqYVd`)|Q1l8+X2$>q0A1XjG|_LhoBh1P$OkfB`RQX$RK13@!M z5RJlsBsbYlVA^Gox{{)zKfHs9@?Zs6j=cBHh#y+VX0YJJmovHVY9J?EF>kY{?B$Pli6}TXP&c-U(vn4h5f>*&O9fD{{g-e-eGsurI#?@F-khPwLysDVg9Clo|Iy0iwiC0r zCz6vxfs2kpGT%Y^KuhtAYOp9BH%4I&i#UQ&mkuSM!K+f=(WLO^NkK}=C^A=fW@sZv zwJkgZG#X`>(t?>>lt!OLg-f#WRca*inOlW5vj~vqOH;A&d>*#|O?W$`ZSCmjGux|9 z9=*L^#l-eUM&EVMF%@$3?C|oly$8FPX#|v082fQ zqR#Yv?#L~^n#!QC)vW+1^`T)j z%$ctT@t(7cmNw4 zUYVmt8WRoqi^4N|;*zmfInEBj!qnfKNxd%O`k}<)o^qhSDNm3In1u^x%zRfweT7R(j;+d8si1yc$&fR;QA3io`*NI8;?J%8BDT|CYC>BPDCOA|dl$HU)T3%X zQ5PR+aVtl_Z8<%A2nvvkd=)UNo z2DUDY?1YJ+NF`J`Y+NjL%f}@wycm%mByNiE#aD}i*0rrebk{6bU06i8a;)5)iercl zDeCz0Hy`;0dOu@meH6YgKI=f*TlYUT zy2?OP?tlrWg_`LAeiX<^;ifKfSQ84ar5mV~fPvOs(Ty~uFbC*s{0FOd(?x%y3HA%> zO_6R>FLVTj7Q`0Ocr6{6ke-}>f&tn@G5Ui2my|gecSF%pW3J1c2G;HLbpRY$o%AGi6IYqr-6!THRndlbD8Ym8ds#G!aO{lNS&Ac8*e9P zdm`O9I?}wX5Y0~UCRALM4-qj>jGhU~C&v{|2pjMJnL6$w%cvCr9H62wFbuq;DV zy2aDdo(N1Ha#@vTtSxgQfa0C+64K9*K^#{Q@y{$jGWp-S#*SNLoQ`wBH4#+(u-Y|T# z5gS9~8IGYH93vp2WBUY~%=;sH+oG}^1Cg%$R}LQ|i+XlTn7D5^>h-tokmjwYP;T4> zW0g(YI^Dq+VW(9QMsO?HKZ29u-C&nP?BsLTKw^i44tn$8JE^dqVL9NC8?KT8pSzOH z7yWH6f!}#O7N(107NvZIBRXFvF-noCoouuW>zKvT6vJm-lw{e5fwoy=A0J$a*-GQ* z&nM`xaSM<{UAX$RQ|SZr;jN+lA{QHyn0`Au8AgAjwf~QC1bg!6DZ7D)q6vj3Q8v@) zD%5tb0{h3x)cF1nND}aasbhQIoUj11zvw>`IjN9I@W!4cG%m&>?MM3C9NSuS)E*7{}e^g&^i!P+dwg#*h5hD7x@kpd`&*y0tyQ!tWiYP41wf6vT>c4 z#5^)|Fa~Jlj%&#h9pz48TRlHC?gSe*KXkS_^4fgrT=B@VA{+@uZUkY|JA6)stkAlW zhMQZ&v8nuY^M{wJ#G;vaNoXW`{K@_`3&Jzcewid4mF5Q%3~Xvn#XnW5ZJQsEKJW4t z5Ya&fnCcukY86zz=$FySTrp5mm8C<}hDS=fv(66%agvuxGXk!Q3I6*R;@#y)G}<3q z!%D1Sq8=_%X|{>TXq9j38N3L0-%sSow=Rd6v`#O{J+>fd zeAd?*T?JM8fcM6$9a%8qs5+*qYi4BYS&et=On+02XqRsS{b|=;-rLjW zrYXUH_V~{%%YAvbfyRWL%KoP;?6ghTLTTZD5>v3?)yDGxYE)>6j)*;5DPrZx0*eQcKX_)(Pujqcl!kcPtH4O=$Ga%)$`~Vf)(@g4STR z^^YVmu)YAZsi_#4>Q-qtix7#UuV9^+j$z2}&Nq$(pZL`nHVW2Qz67vMGEgun3`+v-xb47&%dc! z-=^A4Y;0h0(C%?Rx*w_po7c+7+fNx4;t95j;QS;aziAYny zTwMIz+bK$k>?u%Co?wl#>VTN;bHvUkFsQU+{i_QPM{K#^nr_`Xw}e$wr6xBen+D-J zVWv0|Bvn^gAnH?6#{WP~k0bSge-Jg=kP*oFO>GQ6E@dd!$;(jX(`72Hti;Gx-m80z=yCEQLe;&&(7?5Q?g*w4f$Bi_0gN;7Y2? zGtCKheN9gWAef}&QY3ZE&=kVPY$t&I^A_|!ym`k-bDVHa@QqQQBt1v2$bniJO1Cs2 zAROVks2dItn>vNQ5W{3HJyS)gEu#H56>e&#IW`}`%Lapw?s^*z?lH>eEbQs+wmw7- zEoW-e&fB21wGkGv9Mefo4~KIkM?4QrRl_uc zPUasPqN8nDniJ~xr^@Ni}2jb%9u~x`y zC8a8MV=wi+`BF|e1YAR7Csw#WT;>lzSSsZ4u~Fr{J_-rLJF>D}Wmc-DtDiOr_)(gQ zm3r|>iYuc&YTpFL*5OuA54Fay9B6IC8_hJH6YAe^ZsCEu?No%TAMy|>yrGC}-5u`& z@PA`8v*|PwB_V78%QHSgDfD`?8;X0V+TPPx_=V?UEM2uuo~s}`*djP$FHE-AX|}ho z5leiD}F;~`)6G6DOqYd`tFZjpd zO@}RcHI7Vb9@0fdMUeGL zni@x9n0h(+foCBy@ou!Yoq0B9Y-q?Bz>!p{i$+31wzz+r1(V;pXPB9t8EW}GhzT-a z9{o(eO(AE4?4ir{A)M&ft;J6og+?X=9`;g?5xL}@Yi4GY5wV$iZFr*4N^QmzDkWka zov(PXHR;iXIHQIhjlvFci?#f?x@xnXmqCuG zJD*oK=A-W&+Y1VJ0;!2aWLoN!kOg9tH0F%*C-L;~M?j-5;lTy= z1MmaKu-ZQjVZK;B!|>U5>We}t2ZL^05v>aU8V?^KLmNPA+gebXB7vpeXMS!0WLZp7 z^E1R!W9R8)shP$vx@!X*6mD73kJT#tzSI?KF!(H)&fL5qD^KDBA%@lzaR0 zNbY3W&^9*R=}+Gf4~2@8#N&I|t;fr#DOIpq*+ZJY$cuEARW3DC7@d zKH)Vb^PhlGb%1F!c{dRWXLo)u%{uq{C^p?0K<|8DULUl573D zDT8D(c|<~S5HTruxg<|>VJhB(eKOC`Zp9|pCiM1)8d zmf8yT&RR{12#48RO{-WqJ|zgO~4N>!%*zf4vFH zAYS7h(g@?}3k?1KcT93wuDAY~HjT7?V16~B(OZ@NKer$Muo3HD3tRXi+KK)|t`GDi z}T_pslJW2 zx9R$xvNjZLLrScvH*j)ojdE&_fgxwSp9j9MI6+E2^7!6t`a4_7O}78VuTgd3=$e=yDy2r=3%tq7FVvg~Q%O=$OHm7u zjeq1c4Fgyk2 zqDnB%Apz{>o3X|~5<25Bb4kK%Qw`f0Go5?3%&_@4>K-N#YyPkA5F$kgf2>;hz0Lg5 z?}Ka4<2egIi=X}Ghku@S3pMM-zf;kA3fzoEQj{p}1{*-w?!_)I$!35S;Mj)L5fN$exy3 zLtVn1mK%^#zjuRyJHa(yzBz?OzT{-M!QGb0%%h3sRf^F5X~ zVQzCGh{e%WfWJIY^kKvcyFSbEqDinSq`yJWs+k z^rD*AaJ*yfHriNG34I2*)U;WGY}|iu$g8+ods{#3rM`zUWRuH5Wv#nkzjRt7&zO%4 zGDSVH3?D}melcmbEDWC@RBGQ7qnX{>h#gcB-1S$OT9S@N!t#{VBx|JM(y6$T6@fHp z!CI~`=R#$kq#IS1S>SP7Zaosz#^?#yIzQs~f+uT`TDjunrqYFwh=l;O9ICWL{VK)w zdU}?p6M7V>6!16~b?AQ~FcD)lm8%2$PR_+W_J#S}yKazr%!4f}9hRms5vJCXs7O+_kE&dF@?Uh`~8RDu6?R$<8 zCQP3f%FX|X&4t3G-O|$Ve`{6fVC0TN+XodmmcSY2rAuQYAWM&Zn~CU$(RxZwF%!_R zB`UXoWJ2a4PA^^3VG&iH*in+n9D-b7yJc8##MML8uuYpuhI}0n?*^ zQGHCBpr3IVN-4I&oA7Md;~aC;Mt$k(iA+!nNmMBwu;>l2$P(5$#E{=qi2m}_J6yRf z=Mx@Hy}U5z)myjdW3iUFv&Q|km7;LO{{t^U(7re9Bd|l@t+t4l?Zqs!FWNMEVW+|2 zr%s)kk5c=5X`9(_;Y(lo(x%t)v!6ZncxLS9!!dm6i%&lWSBX!5_S93jkx!JyfZMDk z=Hugn&QJg56X#(E0D;*+28rnKl=(O6bi*Y#U4{YsQOhk z==V>s!|z4vjYpw{$TwReF6Ctb%1Jr?-P-U8xuiN7>r=rZe)?~$f9h?e%&jZE8`qm~ z6;c_^5SAlYfvCtw%>lK>(MQ6i~dq*4$8$@svsJk`j!A+U$R`%%aT zVGANA&USE$O7WS`keki)i$7a#irB$zHH}!}=Yb?odZJKTnJ8AK?n)=eDSL0o*Fpp@uGJsM#hwPmRRRCTX7JR{ z!w!L0Wgh>tz<;xNkA3CASPLsGebr1BZOnDbY>oCfCcZado_*u~%Y?5WTF&KJ)- z2iJh#_{^zOW@w+Zn-C8VB1L$gFW^1?6yD>HU2i2up-?EU2lkW)BnJC2GiG`@8LN^a zRawpkpEF4W8rep5Z3|(k-(b2eEY3bP$!8`Rr+Ro@Wa{4gVEVp$F&L!|;X|$xML~9t z`~fb!r82yY$dMLD0@yo2-!n+ii38$QHx49F#l*~2WQo}tjfX(ibiWR%ms>lXsDRb#)Rzi2G_{0Y7ECZk9M2ZZR z3a0m%Q@Ao75Bov*yk)uhybjXXkWAeqml*5Q*Kiu8YvDG!NT#MIny|Lk#pAZ{@X@(~s0^;RvQwl6?@btP!pAiZG zZhEnWa=KKMZzEj49BLMO3sn*S$pnKdRuV!YF*}lkW(y8~@Pm*>340w#{h*!~RM{yV z3JIcMJ&I!vW=U<}03OK+sTy2@m(9=k-KJ1)HhH^sBW7=Y8t;`1+K!P9>6{AHCG&QI zfxLwVhW|tY=!raaOsDl{C)?axgVY!rFm)>DhJ&t02%k+N5fqYVVjTC129b)$pFUr5 zHj{KPJ};@w)e+;2?Ns8I$^*-Cz^Vz693oRhoVb@pCe@?pol?n6QbXdg!DFT4Tf^Y~ zfg?wtjU=jGud$CM;xs2|bVww`)jx)DDOH6E;uGx(mBR*PWz?!}O5~OgnXb#g5s-ey z4GM{X47)@=24YTkpXr6W)nH(Ev4j_8amyDiGaj>eU!J8W%dkg5DL1ufH0=og-cR0^ zsyTpRo3r%*wGtJpXG9*q!X{P{VD z|2^gROXC2h_Dl-aKY#}pG71==NV)PPAR0-T8D}~#)Q`fD zTAnYB%?^C3@L{hzq zd=d#Y`5O%Ks!o(P#8eCe{s|-|R2gf%(}flW^OUaD8}!8w;!?}PbHyf(tT=8yBc!&_ zn6je0E+t)2Y8a8eUOIq@sK)D4r+3O)Yf>Xd92=3Ws3zW$2+7!hOx`ADb)koZZECU! ztLqzZNBbbmpbFG!b$Gofi=^yt60?DyK8w;)_;^B6;mae3_d;>66tuQT(JC%)>8|o5 zWaws6>KCPwQ4s-W{wD>>A`uyopJ$>q`}aTk(S`rv5B}gBD0Tt%%$L9L&rV-yDK-nI z@HpO^pD>lYeR{HtPl?a%wSYeGJXD@~+f%n=e*f>D;Z&mIaE&1U2A;!b@(JYC7{;$| zwme-27*@y!trKM@f9DHdcwx*cU&YsH-j6M-7WiQ28N3%C-L5CZZDVBL`2Qv2JS(){ z&wcu{zlF=2r-JFTal*&@7$_79#Xi9vQb5cjoi)5@0P?A?dK|Jnfi2J3XQ3WX`I5;jU=}YS| zaiHyz0UQJ97Ah^1%?S+X6PRc>V4~H6cC!IZBrr6;hKf!LkFQm$b4PSiO-R{k)oQGm zSl3mNx|&atY6GHn&;S}-ne9ub^t3k& zUZjW&VTj7jpwlC@WXC3I#kGL~zAt~_%-P@h;?s{=Yd%+{DO< z;y+Ul(KVqG&>MSpE1yI>tfapo0U&S1CgdXr-EWj8SKb%Q_lMah;A+R}_PrHZ#{1*6 zQ(=PDU|~O`8!8kEg~wh>!OC;zi$t0*(aO&mO_?fEYYGoQ*$)EwH12HyqpFAxM@?m4 z;HT%@$asqYf1W&g44MZIkWU%v2|oQF4mfD-QzN@nQj>c9p=?bg240y6MT&v9csWub zE67TS4A4V%QY0uq=MLRg`h(DNoL)4LAfB4}QTi2%0}vBY!R1o7ed;?x1f_>Wq)(m& z8Vji*&z2G#W6~YO*(@Ny-Br%Urt!(>Sfp4G=|H zX&@nKQ>SuH#}c`u&y}xIHrg9UIA|LK95e87h`PY3Xmm31+>i;K0{w}06V^96j)1s^ zgwE{GhW}Vf^ZIL3T*)e(^vc*M=`1f9s%ny6SGvorq{vBkdPf!q!x9O}n3SYQK)Og$ z1`D9LY51KlojFq_ArzqPm0WpJYqlUCr$KSm5Je$4uN2S^qpxhXJP|=#QOtH?>=vR> zCIk4}u`M6i$XIuRZAXb|GA&p=qAg10QPAPGDRIDP9 z@kjV^@9qRZkUA{9WCg6N3*-)klams`US<4l>WFL}R zMdf^%%;5N`jBrX_!?VmOJ}J~)pB0|w#zTaNZo&-Lq4cuV)iszwl0yp8;mK+4wBAN_ zg~(gv%H2wX!o+oluc};rS|ybX3SBE)Hk?-!8z|4sFT1ONMI_03GC~G*Zdxcrq`%&S zL01XL%?6PWsCYwa(cWkjIT$MWxPI7~`jHIFo-OiS?6j&B{(`+w*~YerF8+o-0p%6X z?quIt!X&%6$5DN`8V`y>p-|im>Z8X`Aqy1t^$?s7Xq$hI(tii{1d>bO_?VRL_l0ua`u#DLWo## z!OvX`?5P(BlaB$VXAPKqP^uP{w@uxPi3gGfnnna;FzEAGXgyL;$XuVLLY`=T9|;E& zAlIO9;PxjeOwt&UdiN2LvQ8BtazTAVBG;UejOy#_9Zo9)CO8AQ^T0t)!IF;Xz8aQc zvpaP~FRm)&SImy+8)L6>MF;bDkyp5if9{qLvg#BjJ|KCT@*RX+&$D|R204eUGi3H|OD&z!4Fn*GdY zKC|WDzf)dH_W_lShMny9%$Y^JcSb;L6w+z zicwWeJZT6U5QhE{Gv;}CxIYtUBVl@$pPZ+0)oO6pFa82-U;qtNnJLa-9RpBKxf;0A zyVSf71N4@~pqacMu0mBd?6cn^VueIVMrOpwO{zu%e3dI#{?_>ANWmEJy4(wdhzh9! zSwS#j*Oi>NNh-roI(9SZVgl0RF67j$&|rX1G=|LNL^O;^r9TLj%q0b<(}Ny+I5H+J zv@99IX+P|TK$4Z3j0qSWFR4G2#>HL1NzouO1(7RfETp22??dNjq5Y6@Qm>1eL+6gW zpAV#IVHfxR(2+afjY}8VUqfmR^8*998j@zi(iIJ_r}qJ605GbB87L7|SH;V^ieBYA z!!2!#<^6q0t8o{2I$mm+*fr2u;mTwSxX_ltkK+0Qug^>2;|ILAZ)ytGxN1j4KECvY zr#}gbUBEc?gF(0S1mX0nq2=mVv|Fx7MNj-``;mLH&S^~<_rDe-V7VQ}6cSTU?qN)-_g3hHUvzzW$RL$*(6;?yQ1#&zS>3HF+ZdSbnL!UlGHTAxav5=wod zz`utoMwdFc<2!dgcj;QxzogH85hNS^J{#^iy~@h=hVCT_jihJQxx!YKlef%O%%al3 z-1`KaGD62(>K|fZb#0x~u6nqigA?zre^96@z*~B_X({ifiug z9od8k_pMr#~{6;hR^&0ku&|UAr+S0m~OtZnK!n0BGh1kuu5pFG1NMK~YPWliNG=;So_#Xbi2VekXw(i4ZtIeJTQW84- zzKn#_hIGghlcC0WR#JxO>ArPO%zYN ziV73EYn+&QvN4o*QA4C^R+hQMB1!|Jl&Z{=2qYWB;ziR)6(KV{rELwJA)bztjO0Ve z@)V_F-|kpI8Xaa)K>(OYMdhS6PP1aa%229#Sy_5*y~AaDX*&5#j?NxnLXuxje=VZ0 zpn!$Y_Ypz0RCB)9V5#szJ81<1qT&`PI$XQ-E5D{K?loVgna)MOi)3!2I}Zv4NQY|} zQ|+jhn8&tTu2pd*?9BVItB6N^B->bPp|p;%S5+dK?jKX>Nm4hNR^d}96nh?f#e~lO zk5K|pBt?Z&2w}Kcoo-6fb~!aGOzjHOYr_5z;Yj@2)_QdYpfNcG2Ohj1R@T>{o+Qw$ z)tGo>nPIhn(8up&l&1b3CYIKCy!BLigHZaAln}B3)XK#riq~)cpV}YL-KuCF{7tkN?2Yg`yQkW zFpZOv%(9$xxX-*zIq#7p7esK%DQzy!x301n$a`e{HL*x|q=HwRM9}Yb;o#&n)bLoX zBZB^&6cKBnbgjzQv3f5-g_4A@BTEDlM@}-T_m@}4q@uEXW-my8s81npGVpFJ1!C6R zctzwlK6~nM*Wo%Ch?H7Y^07O(nd%4k5E&tt_Uu;cP!%+&^nPi?@k?;EsQOkSqrW{0 zg<{WRk0>Bg0jmrju-f2zStT)JriTHeU(Qc0NJ_QgV7Sdf6Dg=kuoS8o2jBZ17$WId z?RKEuXh^yh<^PkZdnoD!cSCoyg9BG9#DdG`(iTY}a3yF{Teuda{BB|1W`%;oeUM63 zT>VO@26tAc)TiOlDGZX5MnO4~0{@vg41PC$T}R*yxT3{{%EHyLs4^ALvy#4LooU_$ z{ssMW`vBRWSAHZS&$v2Q>gV(RL6jSr;LfLXiMQN8MU3WDUYPoTP>K~1xq}lE&}=p! zX&~WB3^hp+Qb#1jL|_A#hb>Xg@al$BN9&_;fdDAqUqzahqpmho*u3IqOYE0Z`EWRc z?qD7iHyM>Qmm8dtMX4|b#`l=GVNd683)~#_gUClbw9i(_&y|`;;!oOqWzy2MkWu_y zS}c9fs*Nob3WZ|t;ij9=|0{UtMDdKgpb0)uETvO1$_X@~iV?0=ENw>&=|RVbd?_;$ zthy~4aOPv#N)3bI!|#7TEN^V^c4pdBFreNa8F?E9l2$|pZ6pKSFp#=-J7Y7cA>2T) zmT=#WVditHj59w+($6DxX;Uu1B||uw*E!sGWQI)bL{@m4;0Xq>vNDWPPER7jAwCPI zo`vkWfXLxRV zc;)&+yrSSP2x)6bDhiSjUSoIu^Si^7j3SN8hIc4$G`L^fx>+y=>bvr2^N#zz)qV3- zP~331?$)J#40sLC!3~0IG)TeeY%l;NAG-|KNqjz^z~}h-svo3a^oMW?@2@9pS;<(U z>wBg3@h5sVD^FU4Yk`TDgFI3CX_^OxLZR5>xakB0!_HX>Wj%Eelv8#9$|+W%^*l_a z3Q>fYb_~5B?|_9c0V_mkPL|Y~NIv8Hq4&NIOuG#$?_Pk})&!?nt#vmzu%_~~v?O^Z zV(v^nL~Xqxfqg8MrL60I?mZr&Qba}Ynz{ZTE>+4KprarJmX7IhUYa+}Bn?Vb2$$yM zd?H@mz@;$YPuwBXQbZpjArW5HFhXSbJ!eZPlgW6x5#NfD_iW(e@<`5C zj1&7rUi|s9GDdDyBHdB&2Bc*~eW~GvP5FQgbb39gqv|o_lD`AEW9|UFy7D%}minO# z`8`T;AB~~}aX~DKD;ZwXYFd`RZ^Ft~CB*}(dWcs-Ah-jKV1AH=Fy4o0+HfqsV;Rax{F6T-%35~HzKokna-p3v&!<#;0T$o#w$TgK`**2IO29iZ8 zkUW7deuh2y7@KZ0vh89jj4WG>B|$QA_z({Uf9fRGve*wif48+9%tOllCZ z(6}zD2$u}*W^P;|RSoqr8(h=!mFM|xJT@d2IpwnfIVra1dxrQajyuU7SUF2H72*ct zv!6Y+&`&w-X}&ZA1Jg5Sp1$s$i_ORf4;qBKl2yyx8S*Y zEqeOP>giD^6pC%*rV|kS@tzISXY#2~5QPZBz%WG7ufam;84!5#5pL55lTi>{7D+ed z<%(VqAWr!Rt9QismUDhK}5&@bXrx)?muQ*cj(Qc*T6 zHPIvALMCQ)PMZQD3>1KSqd9vctW=W{ledhG(xCwNN5qDd0bkLohby-zg~^aQVk9LD zB>jxzVP`n!r}Ng424{lz1ja?pInnS2by82jRkUKlLVZHqdVCB%*pH+!qL1dS$)p)0 z9(OA)!hY)iG00q+nl-r(#kk%mbj}8@Lpv)xNHAvG6VRyRQLjTz2xv^TCMqKR7}B1o zMrBp2D6eb)O7GR{ElQCoZXYP8XnE2V8q>j)OGaDp#`=8TJSeUScy1Ry{n_8T@`BdP ziX`7Ot=Qv8@qsik+DsN=Nl` zpin3jif!Sh6A+{q^QQ8g-au#_A77WMARR?OSftq$Prt(?oN7P{5D=SuGcKs+N>h<5 zpUaQxVeY~EVWZPwll08oA?WtI&_WeteZ9-+R}|P&?}`YO2=0?%VkzYsxze~U@aO&? zu7VV+92vI=7b23ma6GfY(HG}}&rKW`9n@VDLo%1wP$97FGs#@_DM6N#FCQdFT*=BZ z$&vED))eVOv1EerNull1=h*oqBKI!mM~D-|(rl0#lV`3MiBBCPGSMTV6FWmZ$Ts-6 zBS~9=ph{O>`<6%u9ZMu0k(qjvNu2ks&~hxE5TV0!S6t|F+EsDoi?di=sx-}*(miL3 z8!snZUg2QlKdm3k&-cUmSY{p+*96PuO1II$+-$mm#qH$f?-FJ$j5Ir>r{Z1-n~f9u z?5R_yv;x*O;%0b1#zNAwPU0`z$xnag3HA6W6bi*Qanq|{8TXX9s6Z9=^cbZi)wLm5 zVqA{DpI3UKos*7WKQ>&)kq#e8Q`5Van9nb*uS2We;GmZ)M&WCt+m({Dg8>KdHMfo! z*d?!s{U9{LBo=eJ&GMt92-a9sO(;c)AGN!Zx<*o1;!8csOLm!gt{F+elr?oUigl z{&Z4)dQ@hZ7RB>pm{f`3eCUpsNr#ax=+;%{@`NOHrGY&~?77b8v}V6QgjTKYsH)A6 zjkgyIK`dYMQ0W-GcRU%h;XPs;nQ(-YNFk+rQFSpo7E8)fY`AG;#`}#p6rU#J z`%dIbd3A%Is25u>rRbHuL~!uIhd7At4f-%UJp=tgj}7SDd=RJ6^3*llr#tzl<&ht8 zDrCWkOdwUOT(1uHTo|AJRFtjdK%btnyRorfBo+cA=~Tc0JSAd9NzBW{Ng{+HD@^Ge zFoTdD&q(|}Se-GxX(06>p>t96Cm|&!jvOttt~sJ3H+gsWSdQW7gLgt}ZW7WcJl7Ty zGU&U~!afbmdEE=5=S=rGTf%A7{E)~Obz$GI12{Z$K+N}^;5p)$?<%}(03-TG`MwDi z5_cTVMNX+?OhD+bgGfgr$H<--dZ%ejLihsbWskY6F&pVKNpUkq3CPXCbJ@@w|I!zp z{@l${8u0c!`{ghGv(vx(#WUxS(3iTME)S86gCEDteI zt|1YiJ{mFmLnNThT>lP^_bJfNB;ANB)`C=!ig^rKK4R}@$SvtEy@v<}5f~1Pjihw- zahc|1TL^)b;CIN~(T!B9a%n>0*%EID5f{7#YBB)s(_uVG5Jd!>I^mU|%vsN+Y8UVw z5P}C1#1*CYP zf)r)*ptx2%XCixYs~1$O2w6CBGt*%3R(P=%7o_8NzI5h{srUm>eSK1&lwU1wV%PHu zzWjwVXHgM(tTYixNPn0;vDrHKlUBW7uZe)*FY31_J9c;W(|Z!-l~dW z11gZO1Lf!2^xw?=7lJLxyWgg90qdw@QW&o#bwAh}2D|OUhhgH-A$a?(H(_p4`hHNf zPNjC2I?KGyfI6YGT0#9Sq|>=c-8M4UxwMW8vNyg#sX|hy>MEs?|!vLaKh{ zOzK=|iaV5NFmregrVmWRaJ}mq8)jUAiz;Q2hk`dk0W!|3#f7>e0)jg~>_s7>l5&tu z31hT8rE`%|!yQ1Zkb%@SJ>)bi++b9TSuZ&5gojRrp5 zPE+&oFMYAByrhZwWTiURtb0=asQN*+k3Hl42tbO+r$75!PvMRCR58_%!gYGw^7T3_ z_J_d)ykAK%x!U^xe{<)9HX!uVwFnA@Lb1oO2RtA|K#EkD&^SDfj+Ezl6iwp{gsEDU z?QNE(s&n3_NWGU&6{f}WK320b>B6Df&emp10j$ock! zSoqJr`c%e7YDXx-U^Ym<5(T0NYN zh5Bq;$cOjXMA9HJb!lUMI{)Jf=DM7en4$AiWMbY=kz}-}`j@G@^B^6^hXWihI-F+IvU+2vJX> zrz?|YH&eJA(I}CP?hvkC4YTEStDVsm3WZ`XVo#)C`47GqNP*3sAs2@A@Y!=UU8|U0 zOdZ*(CmxP;NS2n{HU`bLRtpY2_#j+fTZ4(_1SA;z4~H2v8!hPcs3H~y>w_Wp$}o|8 zdLKo_({_4NKTL}e2I2}huLxTyf=5`sI+i?w7te)nvqyowsY@_VQ)ccs0CzlmkC>&M zhlHB=CB#0EAo%jVy!Y5R;(*6l19_*p+=C%*@VpICHU`!P>Px`}`E+E3Dny`EH9C*}JNZFcl+MLU zjg<;ucpZp*q=PJ1wru_iwU4su6>4Mo9pU^e;8Lr63KkI$POoC3Aul$W6A|HaVI3h6 z(viAz#Cn&>lz`kIj0E>U4dXUXgSjrSu(VY$Onwkt=&8~~11ERO4^m4;QgN;d)Tz5V z?Mq+!(yMTjaWlOiR|1oY3JI>8?2*lur!>9rJL=4vE_)@|O64{D4Sx|73WZ{iV~?27 zy-E>rm}~OsW!N#(LzTmpl#t4k?m!zUZNZkeC|yUS@)1VYaOl17hXDqZ8yM8jP0mm~ zT{fQA>kU{ZRRqZena}BoJSzfemIuzh#|C>ge2`m7-GL$(3c`dQCI!Ay6^LP-$x0(( z!+4sdJjM_G`Y+IsTth#@eKRO9PZ~AI$g_}Ud9y#`17iSJ2G`Bcz!|75l7x&SkR(am zXflwhSrVW#5h7w%PdN38%LctE*hnx1QlXC#MK1%MyP{8Wx4?=eVlm~ z#3^g0ARm$I@NT@h9Jpblyo=X{35WI7uUz#qph{>Nv`q$z8v_dN%}5^xbLu9jAX|cr z>1Naq^7*UsgH%&(vTJyXUfRq^FuX@n#P+88;bH9dQ=jlx>s)DFFptAkfEsT?#f1Jl zU--fcpin3jdmMY94@hVVAKy4m<`fMCZRt68u^maZ$;O7F#a0S(0)8K+vaI1p;2Z=W zeeZi=d2JnHBmnK!1oVe%u75@boo<&6D?CJSjF2T#5lH!nB$&21_FA0eDvUH9g#27lHLDnjMncEyQo3lH_kQ3$TuxM* zgLfTa0>t|xB?LJGc~?vddCv^Kmh734jr;Pxa$YdV}DJX6nkdmLqtF{nKnsp^_KL>jVWBed+m41+O_&Qm9rKJu>@Vy|5*g+ig& z4)%Zo5>%ve?;-i{^IV(0e45uYs@o~nvB6f$egISYZ1P2nGe>Yx9LLa_I{?i?2jJq; z63k7{z##2$b*W~f2^*+>P`7SsI7q>0qh4oYG;Jt~*iz0iWKs+(<6yo?1O!!w)K#ULx?>hqhzPoC8@vp8Gls(f)Y}c|SAk>of`aiqE@BCu z4bH=Zfi%iEg@{V|(lIAo#y66{JCm;EGG<9HqSPw(ebgNJpbqheX5i@YyF@j@<&q{6 z6dVIM^}s6%ab8KwGAR<~6_#EGAw2FJaZxH9Q<@DSiR-Yk(Sa7eQ*kboYFN36IqeMs z4a|;Q`6$r6%IZi!WlbPPnJBL?mIb{%$dsSDekJ)UlNS+7YB1Trr6-Xz%|rv3#h2p7 zVVnv=C2v3VsZ;Z?hp?F+WH7YP!j52Gs7yHV8=pOORZr4AM$()J);f_RAPg7r4Ey;VioWnc_MGcI4*o8W7BtLha9MLW4}m( zej8aGnrM$J*J#=Sx~Wy?ajY%GX(V%X2bz#V5W;Jy|SDjIc8 zsS=`M#AA|*cO}cyuQK+0)Y`bLBPw)Ok3D=3w5BJx^E;N~;17I&5q zIw2o6Z_?qwlWWEzUr@WZU%d?iXiZZ!It8Bsgp#6AOi z4CDMD6aatvv!|Zg?l$T%5HTn{F~`5&XRelTQ9xS_^hs&>oiA-&h3tA`ocCil#zaIA zD(5eTt7{S&r=#bMs$*@r(g;~FJ0i<&pR?gUZ}zoPC=`nAVh<3IqF`z`B}|$sriW{l zO~x?tEllJpKNB}4mg7>fB$5UCzaSG~a9vN}&_fTxg$oy8W@<_**~Y;B4+?nKyFCs% zsYCY!KJl+%K%P1ON8~yga!2w^Qo<<6rqry`sI6~KSf~_{tFA@M^=&GofdY0U2lU<} zze%G8_k8#vh*~wceBmNoJpZ~Yy*q?vtI7NHMsjeSys4TLsWk(9k+;EO^b#TIsNsvq z2c=Vqzrjcar(r5&1SEhb137JrNJ^cFgmei{CYo^X2ksBT{)kWG#Gz?iHjxhKl$}Oh z+H2Ck!^it_L2aJ;l=%jUID?uO3fW8e+zoI%%NrZqgN3y!Pe7oMF2YjIKGDin?i)YP zpLbTEa*vEtXo9r4)TN?jt|L8>Au1}64GZEyO(qk3pTILUZH6$N$ZIm*n)oPo9~AUa z6Tom91@@nkT=_kPaefdY0NWiarR(5BhtrkGv($}yrzC{PM>Q>sVeE}BWjuo2S3iQS zohfg7XC#n7%j3_($SV#j{-T84ol#5ARM2p-Gp z@KtXkVL}qarG!mU2`h5vYKSDoSZ6+u!~;R(ByKfu-6j%=gx90#hab2D4jen+dZ%Qr zJ9;DpBr#k)N(3b%WXOn#J8w5{#T-w&9=eKOd=8v4B25q@=;66v#q-`ovLp(TH>ev? zhbmXpdx5HoTAA!t?=9;sIdaU-10!AQQzjo|@V8unYv^7?KvJl)`hlb)qF0LK1E8H~ zL$I5HH0GduSDtKNZ;&6Pnu>#e@y~9TK#Xg0NaXLl)>iu_@=@x0OM=__g6%Yfu|KwAyLi0KV+YQN$Jot|Dr}|XB z_TYW@!R6&8sBwTEA<1argYGUrkzZO{V>Mye?{mOS{W8cK!f97g6g*Y;eL>tljSqh< zr&tvu2Tq^LTmVk45(?7W2d2Sx2AmR9Z;Ah--JXPB{ono?TsZ$WG^X0nM?&)2#SN~~ zMcZC`=Q8&xp+K1{ibbM6M83k46%~hR4XG0*3VbSJuBX5{Hc{@4ASEIu@`A*yN!|`C z=QW;e!b3mvKB<^x+?jWs6Pwc$FnM4GIz$jUeeq!=qBNyW`S6WbGAreQhg=Fc&WTve zsz{1_FlL3Pmy+7lVgf{|Z~lhhriCNd*HqLGf9KfpJ>WZ_v*834npk`v0A;gzCLJD` zd{|OGxR`*D9CG|L@YRkon21wo<2IT~KyDf+wdT{G{Vfa(i^gzX1(DRJy#pF6j_Z^7 z*YCZm#9DaTg|Hxj==UOV+YfkisY}y}v;_ezM z!cE8ZR6m3S;Q9Y}<^0o+!$~A@CqMJqC(hwMpTl3wSMg7JDdoOZ*?m;C3eO#RKC0(# zX<*IAzr0N|IT6UueD>4{+{Y(^eG`E^k0j_U{uaK7ze+F3e&GbL={zvwJ5c`_P$(3N zeS|&X0VzVpYT#(WjA$T&vIC@SKCUhqDk?>)jtPQM_n4l@1E(3)VCIfv@YZXu!qGeL zgF&~$PxE7nPH`0~ze^RQ21q)%DIiA5q=@9i1H`kzPtt8U;Pr##GPv$(yTsu|AWVkv zdp>X+4jeon=~xtq(>^fhz5m1SftkazoE$VU(}H2AhvXoE*`ssp!=Or8)R2%~R56Q* z2c>5rsp$6y&V!OUGqsSO)R6{GIl^V6Q#i2qgS{ZM9oP3Ga7?lf+Lold6$Fq^3zxn1dJmGWqz27e6W6P;qO^wl zY1bQU@~1)p(r?9`HwcL(i&Ey8L^_WODImba zBV}Tgxy~O6zFPD$)T;!bfc3IZeTgXe$`3QspZm z0x3R$r}U-<2C5X+QvVIsY9iPC&l#1$il2Gi`X^FFwLeDF%Zf)Eaf@tgNvza%lbz9Pb8>Bjf#(@tX((sRb1Hb7yku zD(*}3x^WPph#Y+z;Qst+v3bQRU{gDhEm8w|oi22H9T>zGI%Wue`R6aezyG5@g@5yV ze*}O0@BRb4_|3n9#UCxAT9I;afBgN&A;od{=6l52qpGe4;zwq>NxHg4=&e>D7`6-d=Thf{n8hoeiVO_cILN1LS{|2pvH5BLa`rl(+LQMn@gT<_#iZViajoMstEop zlszBi(91~7aXI>)_J` z^0!1@5pbzveI#2%UWlKy+jSfQk%E|ed_3|iC*Ws(0bf7=2>jeH{|vsLfa4F&bI^X_ z!n^Rjzy3Zft**lBuf72@cOC-PRbnxyC!{#=#b%3rUXECq$1V0=LxDxMC7RXVj`(}u`BLN zIgHfzgNO&I9aFJB7`74E@on1Dj z_k+#16FIU&b7FkLZsG z<@Gn9ySf4E9V8|bO=z^5P;1skz>Qi_25HyLWV*A+S`|*XZx_cw8j;=-t&tefM59aU*sl$L|T6Mt?$F3 zyNEFT>l8D4zg{4btP;-8iE{-js28%eoiHbuV&)w|IEcB4nt#7Qu!2^f*Ts`yk z7vPh>@DcdeFMSg(Ub@7F{0(}MD%0=h9p*j4biNF1K|)-b7_lHzi~H)KJ_!oN zO#*dTuO-=i+$G(b`Fw`4i}+`@*dInOZTG1?b=}UYUWwg+g|lD&($jnL@8mar^EW?& zzj?EG{^mhau9on;EzxXqs6bgl6ui>Vf(2+bWN*Esc zG;E+(h>lTCud(^JVqPBlf`RhW@NinlxQwGXUL`P%1m^C&7dpKTl8OeeH8t7hv?_~F z;roLO`u!A!g8{EYjRDzIjbtJvPe3dU2qUMy^oOn#E{(u>F|0FKXU>34-UdzwqQEz? zu=4H_Q>(r~^#p_C$wSjz>i0vx{D{!5bSO{Wk(BhieKt)Wyz4NmBLSlHty;4t!8RY0 z_(v#+jwt2Ih*6#t7~fliw#VM|EX(zRA-)nKA4qm;`2Vtvb`$z*9Z8EiI0HY8M2P(n zq_oMh%G~HGAwM0hpSYfXhpl^ODEj3sOzrvtR=fGRn23l&_qB_T;c3AM*4( z=hX z_fjYnidznQAO$OVHKYmvVG50W3k~Phvbtf0f3ddAHI-?(@z`(QRlWk3enr)yjvl`s z-g)ayIC%62Umqk zb|uE{p-YYOEej2q+v~Awre~Pk^pPmE=ceIje(h)Z$^Wr?k8(h3h$P^4$$wHq0@7&I zAja1$k^&0S>+L#!k0R+do~I?b>RQ}V#E|qG>*syE!aopp@#&Jj%%g60d-Q0@{}nfsb!VxT&1evonb0> zAVHc&^2u_O0#M4?b96bgl61UFp) zNg!KtogYdnM;Ii^cmLlM3crUbJ?$=soCeB{0NG;1ZfNVCVn?iQuu^~#bE*uWq&mJ+YH-ugxdBlwg9^(hKKQQe@_Ch=5QDZvZUs4#O~#Qk_Ea}_a2yk?>v0xuYLfBj?O_HNrl0Ho+@6|YIQkpF{vF1y!VlZ zp*7Kh6qSX!V~614Pu^kWf*T1&#*q)32mDmXiPFOiNXrxM=1r#hOib~$$tKjPA{43~ z2k$t<#`YR13>bKS@UfqTBljGWMvFv-GH=Xw_HLUx^+T#^wWr(A+n}B#18CzI@3{Y7 zcxUl-RtmU_I_wXkZq-9xGUf9j{{ZM#5k;&LMIn2Bu!elj~b4g7n-9AZgY5)?#m`Eu|%Aj0E z+JvS`C~g_xpZpU344#8shA~$EP814-;x@xgCmW2cb(PUoi+Daz!c2PvQF*2=@m) z<}H&`6FdsCnR8=0jBewgsuh=Pbz|W%QB|zem)^Cx0wUfC>DfVPU=M%zA(*-AApGUG zz6(e1JqG9hW)W5{T!O{_<=;soL>zAu$qy+EwWJP5jvRz(91D%Rg@HMpAucCNUCIGi z8Hr5H??f`FlrWFerc7GWOnQM(`dO_Z6`z`@sLb7Qn4kFfkboH6-+O-f1RVe117cKX zHO0z1A4i&3hsvx^eag7E$rNsD>FtYf0Jlf!VQDRbtUHwT#V-$3UyBq5GQ3kt+Aub` zL7sL;C61f8{Z_pW8+Z;5PaWVc^Kl#%m8?uiLPMDRDiHbf4G!HUVG=^#7$QS?31zp8 z&3t;9ZzSpNC!h9(46cD>uaVV|q*3n_%e_m;8p8xHg=J7EZV`N0(zzZ3g+ifFD6SDV zoq+r|@cqSq2oFQqtKkDgZx**DN+Aey{6#^K^&>F(XZO$er}U722jhBC#8P@UX*N+o zxC35Z{2?5^Yo0rc-*M<}@cqF_{h-vRp&df2(LhzA4qbc#53V-^xC%;<3sNPpFG}wE zidO_C1+HOX*`hl<4k!*bJmGkh{+8ShUya8sl1NDrKvB$yv%EkuVcWf38PETR5 zJAl{UdJDe$@&+85oPwjZ8uW16y>1`s7#MTEjm$7fLdgIcE-Q=3(XcPJ{TPW?!d@2O zfd1g|yI^8=8m92|)(>7`FU*5K|5I>3kqvrLugLOHl zkyQ5L_1UMBmF|lWg-68XL?d(R2CZi3m4ZuMi@0nscR)9Rj%CE>!YmTEK~>P|WSX}L zr;o)^3huHp5&7T$P!$k`LZMJ76pAYLKqqv@A_j-^VeWo`veUzCvVp+m0io$w%FKG2 z;@6I)nH_mQkbKZ31NhQ6s-PKZ&okh-6si7fWla9JtfxiB?OL7;BveB4d-e zqyq^b@$+2F&O~a+Y6SK4NHIW7a)CxtrC5}JA1^JzE0-^Eb*DbcFD)GV1oE7BHoDNK zUL&~98}BTm>M;d}=O*Fq!_!=i>6JI$f;Zp10PlPF0a$K3({;gQN^%a z7P!~%!w{eN*QQ!9fy9H7riQ45w5OVIX#Q?!O|{|R9Y^8(OW)%s{P#cl{(^jDl3Ld5 z<5+rB6)a^FIvd9msc+>@Hy;=&4u_5&g71F)J8alLa?cTHqN?=6uYcDyYUC4@_w<-) zv|t?xMkB64hngpHSz|Hw_ftx9is91w8Z=|^-UyL2k{Xq#VcE*vWBFRzkQsR;gGRuW z!3qM&o#732gLj1kqQa}fcM;1bLP8=ooEs!bR9vQ1t7>-&QQ)T2;6g6**y5lQJ)Q?GN1(V=_pfp;#v19JzD zKxbnerY5JkX&*fur>F9L4BQ9!qLiN|swJsQv1&G&NKyvU_|K z?eGQf+98vQ%zJ3?oX)i;;N7(q_J5EOoI0|zG29Ulxhsl9JaE^Z)6$HA&}C#Dd9g0V zipE0hBIE01xFjzT5O94*Yy>4$g{zkVr9^!LW1QXAc5JIbW1|zZv2ELJ*s#&WPNRvf zCYhK`W7}zL+iaYC^PK0L_xlU`zV}}1TI<3uy^eDb=adYct5*MPK-GV$wm3V6Pf&m< zL!&M&KGe_MUjJ%so)HRr#Y1rT*4OhUCo4@0zn012Cqll);xA2zNLog=%8I=spT9wD z5fK;fK{QIh<&D&1O|IOG={dV;onV{}+1H`rFolaPdnP%BZMqTvmisI#8Q*Adok*0*Ds&3uT5k<4qh|N9rVtgH}J$h3mlPNAZ3u>_&>sqw>R zOyhcx$>S1cZ|B2@cOjO_iJljBj_IEnelaPIFtCs~D2WX=W9+KoTF_1^eHR@j{HIf$x(y5XsB zPl!%O(>PpLtEBu~sO5GiY!p;MJIjaY&fe(ftviIk2cba_o>Fi2{l`5&x~2d(ar(Jo zP(wt^*u93P(6MUVafKp!^vV8t^p+ujP;QEbY{9^8G?Y>>D(Xx!=WGe|EE$5G8~kCf z_=Y86mX{+b@mS2RxD+2p{6mr;<1Bs7yfeS9nzCf!k)`F{^^@Zf=xRM@jrZgqCFnnT zIg~|e6xo*qw|fk*-a!WgPW=cTZH1SW3#+W-6TQt2`pfgy5ByNYguioU4L6WYTIOYujk9oq>50YVuYX5K z1IZYbRYaMD{@j-UicgS&O;MhXT>X8a!s9*$%J{z>cp7<+Y>0B3MZ6zZa`X8iI@Zqz z@vgI9-gQ19NS0*0>>3{f7XoiCDtZuwZ~X|joAOD!S3wp)<#n%DheciEV1xur4zP;q zPR~z7%DrFJXt89U5!TQ(O^javsqem%k@v31M)K7ymm^ed_vb$^=_cNLMV=ALBvT7X z_{mH#qSC`{E&%u<2rxiV2#Rb3`6^M;%aF9>g6!)4fdo5w%G!_W6ElA<0GK1#>@MV~f1TF2yUc&J^C3BOt6H zZr*vUhlG>AVP&>J;4~UFym$itRlK;^6KEl`NfCSZ{#WIl@g@A*i-?)~ z4%H$gM`7=}bxJ-Sj^!O+@M$Ko=C@!-=Uwg#DW9M(lHUW^9|;k2Lfq#GYK&AkKwcsy zcn2;y#~3`=R}4X*P;YbOfF3Ct#``>T=Y^}*rjzFAxvs6xu|4GPkGxwxBA!I}DHv^D z&hU{_aEOQ1omX__5@*PoDwkQLLf#?6y(;U*`)&WVT96Rq zK8D03yFW@OISfk*OLbZPkoZG!e{cd8kH`er z#hnyw$-4vvi3j{oVM6Ig`U?l0FR%NKL{^aX2$|~bwUY!UK7#VwK1B4ce5J^1F?+q$FLY(nC6?5ZsS! z3^eZ&9Me3NaRCfdQx)cy@woLciSSG(ip=e*&2OFj0g=eC7w?;B6;=0J*H*Ci8}C?( zjbJm5SNg2np?rEy>!|BZui$nDj4U7MYE=ZH=m~F92Mjnt*Uk4GrHrLXX)pV zYXA3E)y%SP|EmXo=uuf|zuVJJNH5}vcnXi&&;;H2cK-%=Iqva?tIjqF$!&Wg2JTFD zAbeQ&3_d>W$@U-%Blq>k-p3g0THfP6-10jTtYaN;x3i=&v%qGkxMj#sOp}D{#qGGv za4?=&D=GETGs1qPvhLaF3e%$MFs&Kns772M!d$CifmjlRKg~l1ZR*fJs((;ZN~l-F zC*knk2vg0kt}0n8UG>nW2$$m5{Cley2F$B9yQx~7FgD0o=bX@iAN}ODV3C*5o3Gw8)03;&jS<`Qkya~%BALhA{Y{UcgTK0_dQ4C(5kN0 zTTac4*$Fw2d$I5L*eqj{i`3~=nTxbUGg?-aSGg_i(T0UR(akA=Xe^F%1EJsZOXboO z1*V=yMZ)dhC}rab8|dls;w)aep8cVXpC-IkH%{0%mx9C3dHLoEGtLlJQ$L1VzqYe+ z^N(uP7CU96f88yW_p;HK5wy$-8$~_{q50xJz)h^kK4cO!_~!AB=y|i`4~@Af08V}F zeBbkri;^VOer9&M4_2ZP5YjdI?KHUN(N+8TJmB+r8?N7RAGOnF7t2feK~8WZYa8mu z!#U=r$m?EisE)}Sa>b`g7Zp4zQzC36u$v^sI1#m6Qn0YjqP(u};e8le;QxRyIe-M3vPAJpjYO|0xZ>}I-`oI#vK8EL5RXV)>Amp=Z&{K6-E{9Ra*+nn@?1o!fW{*4!^Bx`Yxcg>F`xs0td zoPH`v<*RZ2e9Z>Dp?e7?@Zliu`>FAR%G>(jmdpWW&qmud`Q~;gt**gYH>el8?cLWw zw0Iut?hB`5rPwicO2iTrWJpJd(lfYftqz|X_iGNa73qkQAh}+j+>Ccq#k-=*T+caY2vR`=8m?fs)8Hj?M z-RSs^b6c-t=I4iC>x{k*v?IkJFa0Ty*mZfx@{8OHE|yH_812c$s^r_?biYUU=Hv6E z@b!&n4!!AKw${0)vY^8h>24U1$wPrAMjj56!NP$p09rN<+x8)qzdua=8Rs$Y{KyjH zo(aiA-2(v)mw`+>Yx_DoLWU~LRk^im+Szqlvcle$qF`&kq`JN6Aww#HU?VF0 z4n=#|;Imn7ay`bh_8Xsf$9Orgu0E^ zlxv5g(hbGO{Y0xHiRvw9}(->DaN=vE9BbHadk9G}xl+kwT+0H4vI^_^0eI z>ehx(F;OTi?T|wAiuAHe?~j7Gf9&+toA|uhE~fp3K*s$56xZdmapMus5V37;sDeR9 z&irBP{q|2>SFi^e?2LUEx!=x5umXCkuXccLQBBuK_?{~z>G)CGr5D{E6Tt;W_5tjf z+@h$0kb=KuRKDoY^hE}To z=Z}TDP|4tyhMnKfp4Dg%_`MXPDQ{bt&#eFWM*U|P$?{hfg`ivg{3Qsv7|;5)u6q@1 zv}B8+cDba1Eq4*gA3r{R;EI^{`BVZn_*J>f&HMrQ z9giB!S6x3JSbFsP<2O2Xr&&kpQlh+s&8RWy3C#(FQ@%^jhs~w*528TYtf*vW3X1Hu zR~sgd6vLOZ1$=YSsA%7N`l(X`&p0o_b>NPBnt&iG;)QKD6!N5WBl7)6FTCXj0uOz9 z5u|cmPi0o%tIFoj3w{hlYwN{_&fD`=!Zt84tI*FArVBuanTET>;<~`y^dk$AMlt#4 zJNk2p{MM6@V55;>zYtTtV&Pm}s@%WlEMANJ7Z17{>ViM|j3DSj%#?H*NACBvuQ*5Q z`SwgzkK^HLHf(N{>r*l{`7KQcH#e#;4MigE%#_SrpnAFN97WMj-bK^kN82yy8i8Mb zBoRvn`lkqtNQ7N$QqA=Vg?P?rb^rVj9C10G5e*GO@PRPGqK)+a7jT|xy+CS8Z#$#97m7-$h>i8lV)n3_ zVOmnVnvko|r>{>|wuj+6`DC~@BVKxn6o8I)xcknFb@3RsgdBXYxFE1jg^R2?7P%K0 zC&V!yasHRlK~#Q(utKPbWuSSVZB(KA;|(CW*^s$kQI-N5yp*n&=GeFy8;sbs?L{>g z;Nwv^9T;5w!|vmdFkUL%@0L0kwf26aN&dU2K~k}C)noM(kFd!hi$4_aQEt_7M(JzO znEVz}mJU$w*r$}LB3Qjj$U;4E={@-BUrBTP(#(%u z@_!p7Y9wXp1dSiKr)A{?3p0^a&O;JsOK*?5`b!(bB02sNs$y-Np+Zy6+)VX9mnzKU zgZDIpJ{an)&7S=c5doJelqRo}fBf}yzRqXWuoC`$1nI$E9(hf|$lQ%pvReX24S`?u z&=%(JlA*w=51Pz7kwMO{pUn61g}Ec*R_`tu@UW^=EZbP+oeR|4MYD&mlJjx;Y|I@= zRZJ^1L|wRt9dZ>fYp(__(%Q)jon|wXO2)^@uBTT5+Yv zqXICXrj@eVIDLZ;Qqk5+H5#a#_T2Xj`#jr7JdNn$flj?WLwLTTSuj+E5<>{j!qLnP zCrp8Q2l?#h^X$oV36Etyov~u_w9Xpmykg>FKmP;>pVub$Wr^>#TDs1a^$=0~Y*fb1+e}sHd-5YF_>M(+ zl;;8={5jh+VQ6q7C*~0&oVcScXC{8qZTW?Oh@nKq7(WYel6H_I;t*!~P|*F1%^GL_ZCfcgeVrz7C9wrAHA(}4}3(b2@iBFyzhA=s~vU>U09N>nmBonSayIRm|tO;8yqJ zzMBcB)Em#{X%3^duFl+y+``*?xeFjZDMREfJo0e(ep%$}EBv;3;cu-o84me(I~O5Q zp3weACJc3^Z5?V_LS?a722cB`2RY=01gZjf@C)c=KyG*vk`r0sk_T4u8KrOGV9S3~7*$GD_&Z zU_eQ8v3tU`WYrY#kZ6a_le)wTOrai%3a*ctHce!?DITR{nc>A;qk6DQw# z8{f`6%oclHk3)#$4a#ZJuR5A2M6Bxl;3&PKjPy*X^nw+K4xm!)@?pdexLuAc^0%^3 za;yyRMuvXJAWvkr=4M?(cemp)k%b=gGP1>P;thC!Gd9g(ElWv(m6n(I|M#Cp3PF^vVo?%L?`U=|A5vkBb>6V}jDiSHRu?~ENJWd!e1I?m?LWd!)~gh;!J!{|1W9=~Ht z&D!`3*+{|C5YDCs!G5O~8&87Po$&XKZ;04zkHH3qSA3M9fWQX z{Buz-!eL1sT4_85;6~e%oiA zkf>fc*!jYg6!|26qyzdK8#0)*$6OSRL#f_ zEG$O_v`RDVy8~Vr-3%)Ua=?o|lfgF*SsB}~*9>6bN8n(`RW9Q`$%PWOzO420-Vf5t zerO=#MVV1^4T)H+%iqD#OdVUh$OKQ;U%m~$$FB`J+ zg}7kUb8liy4Y|DY;eeN^y^WXucr+t3)i0-N5u_;ke~(L*fv1=3HbC`Z!GBr&tW;2i zJSfYT982O4B8_Jr`Z_8Okz6O021hB9reEW_ZC$m6_c=vEK!;{X0s+yK>3y%uz>FUb}WrZXWwK zLiYASwASMEd+du}cupM0Eifsc4DvrZmz%0EQGm^yhiiz8untiJ2@=9bXsM{kVswGH zUQhV$!qpaC20ORZ^1a7Y-E4RjEoIuf?}xYES?Ue7wa^keTT$CuBUj%Bk$o^1af1Sn z*hpH^1QV23e%+7lBiIpTj>CG1c&iJjA2eP9^M1ftBIh37mrznEKm5%F#>H*LK3{}D zR7pW;Ov03M5sWd>OkQ(nI=2N?=eu^$S<03JbOo`p-UKn@0(*T$!fFyCWmS74*F36^ zvQBGbxw3z=_w}LU2*}7 z+K-d%1=C4{7`rqR1#kL0Sr*c;m_4_N(gXrfEJDtYqVq5=#@hiWD&7*)BGAd>)+j~z zeY6TIDjfF-G3#>8dt|4#qjyPXXh-U*g+;-^`mycZKRP7H)s8wdRl&gupSjHye;tx0 z82X$o(Lj)_A#CVG!cFT$zad>OG@OHe40Hqq=xTCg#L(Tp>t47uMH-VwLNSxhMrFa6 ztvb$dQ|^MAG?`GMI7NiPEH!ar+h#|RTRByko1aG+xpp!*6_@AelH>D=9z ztkSRO-$$=j=~NHKMStv2p#v95R1cQOJ=BwmN@cU?T8&0bfM_gH0L>{~x_nq=-$9@2 zkDaQA+v>>3lYkfL)A9^tI|b``e>R$6-2aBpk71x98EJZr{rMfyw~CXJ;z|Yy zYL`)#qlX3amhpA;nMhX5w7%_Xhc)NYXNEhi?@<+v*EQ*tPbvf{5*udMCuyp#mn-<9 z-IMHT_5iMN<;x^uwx%KLH#`PEbp9p1b1++M&6K!0yuvKfbgub_TMYqi>XbJsabfYD zrt$Fytabj01v4W?t9O2SlHaN`bvTbhtEYK1>=6RN7J5L3yKSBSOIrAJY6TBxmR-Bx zg9-(#x#(5EJ)|&?h&4n>!M>1LdxT>+>=SJ_wyS*>WY;~n=*l#W zcE}Su(R0pH&6Cs005-aw$e*}`I}W*gtM?>)ag#}^D71N`f04vsIE-tq4n+;fm%VFV zJPUHrJw$80q@R3aWHHkJG8rC2yLj+{NI78>0VC&c7i=U)s%plfJjrv8z44k4p7 z3y}swSQ3ZSvmzQV8P`OfW2iKTO_U*#8DIgmBQr7-;=^*Jo5OH96mwH4S=^&K%BbDp zqP33-*EIQ(ZBmyt#@cak5|*^E2>!pr`X^Y--0XoX%v0gbK<||h90>JH2y`!qm`+jU z&buh|i%&J@Q#4y#+ni;#<&lL3a6{c5=?H03(y7r#8r7iLGsJoEQ<6f>^P>u0$wtQ` z_r@Ga+C((zL||&8o`szV5y@>Ngx{t6=*iftdwMvK3csAh_1ym9_k8n*?4;j@;kC(F zye91mM6oDq>6~dKndF)USs5c0tJw@jW~?68S0AcomM%{Gk5_gjWV~=qN0u@0lXt213b2O|z#>v%Ho{$)(jp^C)fP+oapO zrOzpGqU{%V0Q-2rc~BCUExPWCZus|d^fhD5&IRBWHBU_AW$SGTo77ACk|1}9;WWNw zzv3*Mm)x=TeoE2oC*!dLbGgE^GP!&d7aMfrr-A!T@cTsgmdMj2d9cETVc3K>Jw243 zWFeE;o@m*kbd|r!ZuIV^;8g(=-XBlrbs%Ih*<-sEM+tE08}jHzkyVnvP$wn;hk*Dy z&WDiBKOiF$b#>m{U35c=@tCv=zqRQ#h*@V~H@RC~fYoPIIMfy$iO3y3w#sZp)gy)Q5gDXm zF$nNhypKTfkHdPDA0QT?GSsHL;epn7t=-)n80NuXkZT_NLhp&nXxxqn$#EX>^ znqufd=Lfjjx@2g?0U#`vu~`s)&%qCEx`}N6>`SIBLm|0Ohk~A^O!T%c>$}A@O zii&52U9i5HY`xVJo@e6?!c$_h-+1iwzR5-)=|nCjatC9%`7 zv5yrVJP*Sj=*TE{>4(%}`QRt^N8w9OhtOb^3tlGzR43BkF*~&Dd}pvWRq#$qZ4c35?&faG^a{!`9KA%wmI}V z5FJ5JXJiGqxs&pAJX2Lv7zUO631||6ZkVT`IU`6wWK>%{LpLKe@ep|tHQTo)h_4}+ z#L!I`@+7_xhw)C_B9ce1095uY_`kE}#|;i}4d6VMxaznHAPR@Zyk3Gdy9;(Ig0p@` z&vakUhsRbtXmaxpubJgmQ+t#R+9(O8lJI>3`>gWz;oVCkNTFz7j&weUg_joUI%g!` z2Q?MIZp(sxNX+#{$IEDm{P*rw!T97r@WZTN%;c&HL8z9mNBDL#uSUEq9I3|OW(t#m za|3-@$XvkEu->AsiP>FnYVT(KT#lsqG=!h#he}5eHvKshIQ$)#$J-a9%ixSr*T^I# zi4;)HJeN^{f7km!K}xeq`;PETSJL+BzL23s)zd1-JBEvKlN`fxEbwVdqv;>)KDL?Z z|0Z$%cm*2#kVn)=K`YV5%#ErxVvK1`y!yvCjpVW&PvS&*-5GCrMpn}A8U=0L6aF-G zH0cgbD5_z&ditxxSzvCE#r--neqUB(k0*c61H*cRA*gxcRC@v$6jRigx~cNs$=Y?@ zTCPZ(flX$?1C=j1tr(w-N`LP}w5~gXBa@!CJ<~NvnOsI?v-q`3Z(8lo(Y+~7^%vaa z500lua|Lj}FN5JEdvPV-vaA6`%F5pd5FS``Q z@^t0vVw=G%_Nf}sODb~_7ITj9F*`FPd7_Nv1HB<-dFb)5(`L?u#t3dHWWyHQ8SGO1 zv1^}S0mtn}#^wvv!=uX=(%A7afu5~}58{$ye96_HWPe!Ayzsu=`mja9ZG%OwXQnc; zC2?>pOeazt907-!rV$+5vXMG)U+*pPa&v+Xw=l(lldz@>Dc#>ci}&f{VyP(A%J&t! zq>?4yw6+B$q29D#@5H88V6Qb`yc}J;GFBKKi?>43t^FyIJ0Pwc+@BP)RR%nG5pB4J z2IWJhv`$9aY|WxLyOdB~kq0m#nO>aZHtW*1kchGPXR+%jt@LWZ{tL&(OHyK^+l7bRMF zg^WQWHQM7UjwI(JzHvIg%HDuB@?hgCKh=ifU_?wY@Jc9viA)1`rsnq*0b;@IBXB4N z>64-g#M8BQbtT?bp-PXm$d(8rN9mk>6GgXU7XaT`%glU|gpi?(eygo&lWwcrrQeCV ze&%@Hv_zEMU8DiR_KWW(e?R%wb39G_GA-i|Z5{h@yC&_~gKet>Un^ZVP!82Ug4IEx z)TSy?U_{8}BglC~%{j-nB>}5sHb#ON={jODBKNv>ns3ARmGoix!k5(e@o%})0;pH{ zA|8*YkWiFLHHDXu%Zs{?A+q_13jVgx1x*cm?n^Yf3K}%D( z_cn>NU`*F2skjGvQf)(QVSEEl;1dLvcd}Ud%>OJII$JKNCj3=oidezuEA#s=EH-bnJ5RLg%^45!Qfyt*C$q^^=rez0Ekt}QgSs)E7i z=BlWN?57(whZDvXLYa03vRk6G(+Nk0bz?`Oi*CXW6yDG;O(4ssfFbnr@PvUwsV|x> z#iGi}4;>0K;PvCOr~j^8oEsK-;X+I)y@=mi;T}0pR>MA(s?{=1dt^O{mFfk@xXbz& z$+f#6>>M50oL@L!O-rTaq=m#dA|lJ;wh)%6ODbTXyGU?ZECj^=yre>p9A9uW6vSbM z_9QzsP(!!3zb<6w8N8uGtC;d+Q+1?wk7!TsE5ehabK?s5NHT=`E}*RE`-ty(@GKJY zYh}3HE0UMiozAvX3@zWeqcKh*6e*+w*AZrjhT5utvLiA=X`yaB@FbNz;LRJcvVhyt?8#dO80W`{0w%v2N|igsfvCq+NJGm!IT9Wr#9uJLOsbkRfw;(i2X5EQUS z=&j0jqP|;h5bd`%ed4igl~?HzL-7xF7i+H`kQ`jJ<;e@QAZ~laXuGPs5IvZ#E5x^g zM7C-u7CKgul#)JtY6)0NVp16XgVjtJ#7fdH24up|k4TEA^jkdXHDG;*^SkJLhJASI z^h2Ic;R&&9t|Z|}p;yGG*i zQ^BOBbMaO?))qe|FlK#1J68oFn<@7exW+B0<5lNt2&vzzL@3Jg_2|fkC|(j>1cBFs z&Z+)4(yAhUjtanfV{G$9khpgBYKgzm-EDr;+h4YkF?2~$1W3?)*c=|rwY%)U}!_&D};x!BocV=_ShNL-|3e2+y#Wj8kCoNarX){rr0t_7xhX4S3&rTK`q}>@{{LUkm_8Vh7F`)x4u(1U^+P$%W(Join65zB@Vv0Toos~gT3`` z4;xX~@R{sCJSD!i5^DwvqK*m_!CO8u(kQQT_F4iLxq+WLUx+K(-Jvns@1fHKyoc^E z-Hzmpk(`BiD(LyL&iCZUIlOM9QRMrHTa>Se+S(rkfUj}JAq##F1orl56l8^lb=qx| z>6{&zL4r~V-)Xh+dWFOb2q)2TSj~Afwa}*1HV|Kv(2pfW+XAdOt1XFSVlTa^k(nky zp>uwi!k(wby81>CgVEmU*{J7{iO}Ct&uOWpNA0M6WhDD#|R|fxw zmYv5JQ{i?y!#pNmarvAOdV4i^Hpw>2@rtl#53HL^2KrGiM3Y8l{87?&q)B5xw&KnU z*wxgvpg`nod=(xoPL_@+fNBrEF47jZWIR-PGGe}Mv4Jlc65W?m5xhYtj(T4t*@{cH zEsKnJcsUAx(OgTZHW`B*PhpWu_=hGcPC^`A?WD=jGV4Zy5aG_WHBbz#-iAAGdxqu( zF83EY(V0ES0;w;2uo5DF!@X{P`2FIT)lA8oeWP0xMffbs@OAL&_2C=AR-FI)qt`dMQRv4UOoj`J`Ni6b z$lml!)#H1;gEdS=C96bhgm@1CaBA6gk*q364P{&;$`-Qr-%Z;Oc@JXQD+`wu_C5vW0 zO+F5!wuF53?aQ=+0pk1q`hCoKUqr{gp~qJ154iUW-m{RVAi$poz&?)*|HAFY{Q+B}mVUQZLEt5`*A zpEH!l4=EeuyX!UK)bQ$mktaZ$5=O6q`)3pK&(zdzu;V7cGb^l!7FhfyhvCI|z!2F4 z_szwWxcH(yvz+cOReNcHa{Pd1?Qp8Lm9M1T>5=LqzgzV9T%nt)KL&=USkO@j%)ajR zJLkdA&0o~e$W%KcgVKtUC9VR=`$p1Qp)I+P;N2+EOk}aX?z#wy0Rugh zNg31GO^T%=+Xi})lEQfY)`7ApS;~Rr0{nOQ>PY6w&cAeR-sh8BXW#rkcr%Y0Iz$bH zC`o1H?FXd9?2+m|4wGEY^LD_B4D5Zb?!frg>4iP1z!U@Htashl*O7sLMge=!$cT1R zv77gV1WAhN(0bpXd^N*bQd3N8+UPpshMb_t zcL*1Fy%qB~&O@nezIIqvtFVsB`>GY<)kaWbKlJoULh1VEqi)I~xeGUfZr>;h5}MT8 zf2Y>64pupcVh$3_mH7CMyhG+WZp&IgMROs}rtR|n!XR&A4xrHg!Q znzu@hYtuI%Zc4!=80y8~_0aSGPjlctHGuEyRUQQYw0@O>BxwD8+hOD99PGX%DQ9Lo zfjv;937a2d;nGL%QD{8ijK~$yI(?bFhh@=I!OhHVkToz2`42a_u?@scTD4c3qBbB;TGnxkh(SW zhdKvyM6j>1j_NfUXV;{oZ#(IR?s%zGklEI(W*9+6^ppirdPlKIhp1GViHvc=V#)@vUAsa0XOYyOt$M?a@hJ zi}g7etMz(ud{BA^;Ho?dOxhZU=_Z6lTkL&HwwCo;l>a~B-|vH*D0g@ zn72?&zje7~0r6<+$pJ-LlW5P+H)*mKnMe_WFort2jGaythr={L6_^D^V3$eGCN7po zq9%6ZK1u0)g*;)?JDQziUA)a|^Qjj=LcAXsma~$P5}}HnGVrt4=l`2z zoR|0aT|1}0_itFk)A7$g;$1}hf~Q9#B6QV-(xUNLPcmTiaq%TXW^!JbzgxLl8Ia7X zgkU89CifPkaB;UP7T5I0biEI&v$Fw6v`<%T7ChhY8^7IcqocW~n0 zVz1e$GlvQEw?#Jn$0PGU&tPk;K@gUWgEF88Er#QAd1yqK zHotkM@WIS+ABwfU<?bkc;JQ5KMh{xr8_ywiV6o+tlOQzVN}QY zx}7B~wkiJcV_HB2gb;G=x=Hj`q zyJFoLl2Oi!AXJ-U(*!}(Rv>w8#XYBI+&cnZy2AZf0*{50zYm5k#~<*~MPX)SIAo16 zm#NNjW0x*3cTZKQy+C+;sLZbz383Ls3Kg0VEv&ZUyOMp!^RBVZ{#V=*@-OqyISBtA z6-3nfy#VRUK1GjEHp@i&unFasO9CN za+dv#vZpJzOlg?Q>o~Vrld*so)>t}c_Di}QO`2S8JmJBNtFQ8bxjraGJsYVINA+@b z%=QJYbLN8osE_Ym2BE$zto`n9Ko1YycZ&w~^brLtjE+*X^`j}#s=rfyye%Y7W)4Ic1>zDRQHn=? zYE+b5pYEdKH)3zf&=5+QiCBzLdoAIb3Uo6fTQp%ym;3Q+JYih`!+AMdSMIGwY1x_L z>hvpT82w7hqpBBiuxc9XplX*R15GVxf@AFMcTyaN zQyLn)FRJ#&+c)mpkJX!L-EHb=-u(;Jjpnp2+P#hPE;^pqE(r8}Yf7GpW-YR3{_GW- zHbY4U$7N-P;U9|@b73VvBz4OqydtH?L8A-*MgcVC=jZ#}I{wW1@+5TRU}s0&)fVyb z^Y;n^R;p8ZvXhOkqVLiqa8OQ5Efp~Fl|E017kd5+*^`)LOASq`hbdE_R*N_Bqpx`D+TF*ln3rl56ziI3{HiJ-f_OXKE3k@34zi60gI(ADg zTOl!V=rhTeV*ty!skMKACgp8hrWbyMv≧a2RK}Ow#J%o*aPelt+)tQ%1{@e^@Og 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 literal 0 HcmV?d00001 diff --git a/src-ui/assets/about_vrct/localization_1.png b/src-ui/assets/about_vrct/localization_1.png index 4d7a4633f7df638147fcd366e523bb2cab48e51f..b798bdbccd7660256da1c3476abb990ec116ad2f 100644 GIT binary patch delta 5432 zcmV-8702r2IKe9+iBL{Q4GJ0x0000DNk~Le0005}0000~2nGNE0B352B#|LBR~1%C zL_t(|0qvcOaTG@r#~W;_M487C!M8+Uh=6khmLuSZ01*L41c(SYBCs3*M+6oTu#Ny> zRE4S}o3H<;UcAiB>|=KJZuj*6Rh=Glui4$%eoar0kssXPWJZpCMx5yqD^Jq$*o(rwj%GoeEE`Xt1qn; zK2F|`KmPc;^oX8-g`R*nLTdT^`SVnNJukOizkdDjr=NcMBlI3s_H$GAWlsM2=bvAO zru0uwPal8%`gMO+ZqxBMyLt2GUqTw8=kT?j!%X5h(f8&$UUL2W{q5Vgm(md(vv14) z>*t@`xpU{Yy7%dEJkZy^bl!Wc?|mi-`1|j_U+Qr_aNG4e7Wy4;n!ZPc`LjOmBlo-k zy&k{F7V!0@4`T0i=qZ(h2l{xV*KZnnonE^o^v(JC`8$8TX1Vl4A*Z!37%LO0=u4lv z??-9Rr0N7FVkx8%Cdj8w+oI|{I;fs{nkPT}@WW&MJt&2bt0**Menh>8Y=TX6#sQ0 zSCL7}c0$U`R6u@E!SoseRz{9Ryw;3hD0?-Qw&FpP_UL%TxcpO0*|!8!o6mJD{F!P@ zW7<^5(Ax;(DARGbz_^seP~l%qmMz-}@c{w*cFccDKQO%oWuL3i{Y|47Cqf2~DaY5+ z(P=@%#>RA~&#`g!QaWG;$XpT<6VmsAo8m|kp>tsGh4hAoW1ncq=&cT@tY}jKH?Im^ zlAUAG@O79{nfT_mi|0*1{?g|hXdjdkSHHuQ{pgNo!z?5{Gl?K{9gU@4&m+B_@AU5+ z2AqH8q5Z6F7}E!3IcQ)^BTp?Vx`RlAIC)hi7MQ1pjw|=Y@|0%oi%sdDsBAuz1SC`V zckbMIB4iYpAiJ;gOGs$R)Bt$T4E?J`+xykL_^BF-&jSJy-qmQr-zt>%_1BsQMrGfO z)bifu-e=`DT#LGdJ-jbuwCPx#-M)SMo%?^BMQzajPT&8==KK$wmreD#slIk8EssMy z%0AQ|r!OyDM+_iRpwBH+$Y_T2uP_*V$UdgBCv`vp>SLjv#(n+Q=WVP6q>KRVCp^2? zZaY>BP}y$>Q@Y=wif-QUecy%O3!}}a>|LO6ZI#Di;Ims{@5y5NgK?*?dx+c-&*y)3 znjR>i!Y72Z0s>Nn(%SS-jDXxU=1C%4>naH<`z>La?6Yzklw@crWP#1I&)oKy>xfk+ zayd0!W@^S^Bh0&>Nr(Qx0733Zpf6MD2o*jd8vud>YnL^Y{81kt`pCfDBr22;0$meI zU%q_#p9~aJ_Eyx!NB z6b3aFJ|R6*C>mRy1FvK??}(x;GE!{rapAVBr!BD->p*zr_7@==*P_&U zpQr)Tpnu=3JGQLRsGFs9o*!5tpkh#dJ6`=8`2xKNRTJj>R#of4Ca-l9eDjRPfqKM; zBd!{_wwlH=p9ii)0w|B& zb}IWp5bfRffy&~{ZL>WjYC#m)ZBvC^yGo!6GicVr`qF`&Quvnb87s^WVii%Ts#1*N zF0zJ4L1{j^&D@sDL06?)Q*5J-*(Q~K#XR1!$&CxS{hY6qt;Fio~bYxmM-&@WzcB86G%ZRP_s-?^*0IpPilEFQ0SspcgH#^-Qnz zzH9(k>UtiYIemXY+o+3GVQS6OoXY^HQ1%Nd`#~@$@7}kh4)<#lgWR`tuW4kU0lC{? z(|}Qoat_~z?GTsQzbj!HNSIh{8><50xo?13g4I$Q5L4wzE~_fNGEW&%ftAuhgrGt> z4FW3}nU&jY{qmDQ!XM_Qal8xP`Np}^^Uyl6#j3g$c>{lk`u#5g)^y#q#`)h!cDD8p zhTmafo$tu@zH3?12?bp=6{Ss&RN0T($l3+E&fTY`v|Q6=Aq`N&cjD&g=zVZ3X;@1_ z^i;ZR%r1HO8Gb+kXjrJW&;hJ`Pl6n1T;RYrNZM3Y!XDsGA2Jy*z8%G^s6?&O44m(Y zyV{~58Ge6QX!coM=&a&eXPd4L2SvXl%3ds@;W@^0ukUkw-`A8~3+dw`J!|@UyP!+Ay-Bf!fVbPa4tJ;zLj#G0o_YJDHpNQH z$7*5tqu8hh47)a=+0c2`oBdZItqG0)ZRR9|Jhgw)eL3a?$(;bJYBo7{54ojLwXFO& zJv~Jctfwf1h3#u4Ajg}2hbrZ@IrUW0+KY>e+|wMLT%zP0H2x(k73 z#e=35FP(40D?;DYN%*}==-T$8$8EPdCCM37W_}s%2ju?)9VdUK7{@v$b4h^dCrN+B zl~?%o-rNxwSc|N0)!Vh?#gTV&gQob+zFtNfx8{K8BW3d{Bb}wl(1y@X4eGOC! zwC9jK8D0IW3&>cm>RSO;c5Hf_lK&XEcVASnLjvC+qODc|HJ3=v&d%=104r#wRQO9t zz!)lz`AVgBtl?aIZvM4fL7QIDo8W(qZw_4BKHw(N{=doH`d=FFCJHcrMON!f~bk3CClns|kk0bhCZ_LzF!hH9YKDUj#snf4zQ>`pA&f{4faJ`8_=y}iAU zLblDq>L27XAc`t_Dkxuh5@DyPrm3>3@k$=joo#ye6*)tz@3#C87$Y<1`Bh#>6Qyq( zbjb$EB@%9r8-ZDgWpcS9sKxx7873hnwt3JYmb_s9cPxLwGMLoXD%l$Qsx+SrP}zT5 zs)@b@N*cGWrb#+8*tvVFhw^^}6+%5r>58aO9c#{Bm*O~O0@W;|ol7eG0WpLsWpyBu zp+faxppcD^Zw7#c{$Eq^Z=a(4aot1wQ*}d3EmqwCcTjJfA#mNpi7#pVMX9n+iVx2`_FYBLSY2TE&0&q4--CnZW$m z2ixh6Y>A@cU)>|%B~MRJPh%=1_5#LVsEVEn@dMBJ#8SP{Y*;mN2J79sKKe&UIDv(C&Zp+>m&iVw>i#6ce~(LjJ1!u`Fd(@T6+Ty0PpMm;#jbgb=iCoJT{zd&c_r zr?O=*4OW*+0M9KnpKnR1UFdE8woE|2YIycQZC~r#Y2}pSf*a z%2!k2$B}#5I$z+L!X~2vH-ReuKade^5diNtxgGYj9~GFwI6puCpntx$WX2C;nMXE- zx^UaEdCm~L{3e9v^OeJn*I`ZJYeXUWN*gt<);@E{(S)zg8-7>qS^Z%r4 za;Q~weNgi~t)PFxk7I4keb-G`#^&|41>V1fR$V5({iJsZ2tSWu6WWq$p zA$a*s3C-u*@~c?G#aDkL`FPfTuSFuRrQ3qF4|?_PeVgKJ)A;mVi>^AsQ}YCLy5>~) z5vcGx@-)x73^?Bny=&N-*oE7EgWnvSFcRQM6toXyf{j`ns;V9sPDAQ}-X}in%1gR-9XmE%l7#57=bq-eu#F8thzTB7#=AD$&8! z+YS-(^5t?9rKyaB(0smaveP=^f|Apo=4pzhr|5rAAkSqn#C-vLib`2_Ra73}IIK-E zbxi{H8EQ?Plz>h+XzLtVRG^nB5i0yhLd%tGK5!}xz(7z7Ftn}bV1S^v_4ZTwZ|UGH z#>>YI;I`?FG8&~91L}GE5*Bez$F`XZynNrWr1WSvL0~?=D9Y0+42%4fP(o@KrH@Ej z$QgeKL@9>AMl$owqh5Hv>v`-nnO&=y+urnC2XsQ~nh*N=x%8CqNQK`4TduTbW#T5q z8uQcBQ}4ioK*8Wbt?wcW(*(v+hReeFe>ITX2uD3;c!ycJ4aZ$k?|LVIQpZ|xxjctU zx!ikuT9knu6E+X-Eygo4FTZL&zjaX_{VacP;h&=3L>uF{kPnEYiKjNlPz}WmdiN_} zNJ~Kew(<1sd>^KWr1*>jtNHM@ZVZ(A@znDfb4fs|&2uJA_t$O+mUMMWPS^w(2-puk zP;YsrLZG~v%nF;&&DOF|Yb^zT2^P|o#! z`Oe6Nu*cFny+@GQJ(EBYE`5G6U_)`E>+Vhd`mWyu(=&R$pTb){KRqMw=PV~QtO z{PcoJ#U=}ry7l#i#jXQIhDyn}ac{0nZwe@YxfwPqL56a}db3URx&2V-VcYCv4C``< z13VeuR>^G@<+j0mzV`WnyOP4@^N%e|+vc>c$e|n{v~eot^ILE9 zWP42bY8Lp&>`{BZU>su$@)&;wv>~=uRS)ONJrtEWx1n;jYF`h|H zUrI(tE{UMRPlVN#FY3uJ8@y$SEgAJauVh4&ZmuZ7gX&;rTr zD!E%=>+^j|bsDqDPHCTYt2BY)!k1%KN6q&(*g&=*u>zVk3)baWA z=SQfsZyTZ%A!d-{2SUJl4;^1zT)b~9>{WKuCIECr`!=ge&K)97pF5w9+8@|> z3B`*xIZaZG0m#a7Ut+I&Td42{gx<7R4zyox8-v0pglrcoeA0gehV%zSAKPjYLI@#% z3O@l>TRy6~Q-N-pTIt)embK+S$krS|hAtt5kP*O;emrX-c)47+i}Hz$_O>r- zCyT#I;gZUp5JG=QD(K;NK$Z3LiuIP{ipX}}JO*AwHL*bDGYGz|S zW*vxJ6Jxt;VejK#QD)Y&MdC`q7hB2d?;{l^=Mh2(=^HBi4PeQ7ck%bl in>YUwk-;VxL*@U3M#4~04(WgZ0000kM8FWing|dPtchSn1egdg5nv*)904W*h6sQN&_;k+ zwhF57uKs(Z+uBj9XEcwNG&4G1l`tAf_Glj6x9{Uz5s~pHk?<&y@F#zL<(d#eNRRpY z_3QB3wQGO)+kgD{@#5~?yAvUVkQQ)V2qC1$OsCUilEb%e-xBwKy~t{*+VZ}q>I_V9m6q&gED#9J{SW6V;6 z?6bFda{Kn}`$9+#I`$mub$q1b=|jCo4rOkjgt&wef_Y2B;P2`#M(T{^vH9z-zs~f> z$lHwj`9a5;{hDEHKZcLP~QWWFHw++)hX( zbTWIykRa(A8a+6L$GG7laHPh_TQzWwq$gN;VsIYLBr5t{HvLNe8MICqjsUu(o=}-sVIP>X4{W_8RFS(h-oZf_pslnvN4i zVbi?Sgt3rH#7gSNpdKw>L{I)lAsa*8=}$DIguy3q+c1gVxpRkkd;15BEF|P24`zZS zW%ojvJUxF*V%cy726VlSUI-x_LXD@h0I<_-uAO zbs$$)Lztg#-@e_6u~j|Wk9BT%?Y;{ZWBQ|Tfrap_eg@zF&}~DgmGen;q8Vg*!95Yu z3GC*&uaoIRtBU)`3d{Y(!iuk?238O-%x=>``9yz6tFa=7p^n+Cf|GfXll};W7gJo% zzyA8`yPS_=!Gdw~js2eIIoJQG9`kn{@EXs&WV){p^!j+HPX}tCy;+xfV6Q*me9|1s zUhFY8(7Y2uYM_JfAKvpxS60RFcE}#0lPyANsH#oWb91Uj*a|?y z*uZ}?tiSKvk3Pt}UbfmOM(lk-V)m_tSgsSGAE@hY$U2qBdSdB>DfX({_+Y(U7g=yiB+ z)bj%|T>o4pW|S3vzK@vTYxenvNV#4sixc&n?cMPu)#zjn83x$v*RL@6RgP@V9jp_R8O1{oGJL_b`9E zP2=*>E7olPmdd+P*;-dwJ#(B7P2AAA<`>^r|Nm}NNyL_b7@noO_rO}4g<#- zk5nJ;be~g+L?;b0JwZ@!Kk>GuV%kDj>FM*At^&+c5l-y+p$=dpy(ms}FguAcS=+^Y zW~$Ahw+x&bvkrd8NUAkO=S%wz5B=|@Y(hd{;Qt-npRrVwE^+-ZoiJeFJ1>8}uRid7 zC$WyTLw_f#qmVF*iXi{xjh*W{m;yKq?n@iwCelFyVV-fF?*fmGUU!#$>IJw>Y+^wC`>Z9Pf6@29blW5jqCw`20f8 z^NCa-G`DJ~bs}pzxCG{{RC0fm{T&!byejR9WGcR`#oyB$;aPt!t<@dD$8~8?gBCyY z<}~;J*?VHnYrP9uy2RCKd8Oq&=2=R%PF# z9BYS_tIsB+k{@w@|4g=w?(5hTz)w)Y)zuJ@^{q>?+b|GH4fI^R)`5TTlm5=BK0bFz zM6(Up%G{qQkvu*u@o^XpSP(veHv`@loxB&5Av^?_oZMqwIJAqH(d2u?TKPS3-&xPu zr0i#$23prb_8F00EH289?IW8@Yn?y$H*0ojaP!LBrgM3(3^KEB>rS@Ebqxixc+8L|v=A@{jqr?yA%a4-LHVMNE2I z-DU~sOGbrXa8ZPwyD{2a>3^?b{KG;E#xV?Ko1A73eGj%zF=?rB_D7HM@_d7KfcsTi zM8kOvePuf{zkoF&y+sV_EOc#7dp~X7GkFNf6KO!N3^KTLy-k0pD#FcmgcYF7#s9{6 z?ID!ova3sn8Dn_^U@G$jTExnS7W2pc_@TjJ+XjI=oQ6^0rP;C83F3jexY3Lj`aVOq z0uQ`@RQEf7|Ni}c_Ly7c*tp`k9&M}F9P)c|UV@>}8P7w=JEq5aEcLlu^E?U~hFLMl z3a*Pz0b^Lt!Kr_@d99ZW3ju`tVPM&0g!}r~7FtDp3y1IqU>xYmm{o)xyEL%vd7He~ zCFix%ylWbHP=1bATd^@_F)rKKUSz`5*!Ie-a(LBd7axB-Y;B(;LK8A>80hZg6a+xY z8sB#uI^H($T~zh3n8>2bTRYXi{~||CdfmCk@xPGGlRLqwu`rd6U^yR)=SZl%efxH8 zAxM`ClllD{G06H33V-w(A7#Zz^Tsik5H9m7*}-&L`|`us_fp4%dtDZny}G)3g&ZVV zzDF@hzt?|b`b@*Pr?NwIY&((i^V%~VPmk@~FO4}cAoR6Y>s0r54EA{zrE)EsvSIWL zi#aqP!aFmH;KePI(`9o&5_qZTSTFV=G`BQ4)_wAjCHMyuvt{ zW?3%*qY5E_f_57-uz$LrkG*|Jq{W09@8Y|bsm_1QIo<=mqNgMY{+Z7EZ$4Xh;TN3i zq^d`>Cc)!_Yo)DyTahya101V83uV2YuC_^<#LO?+SM~fD1jI-e{ zmav5(uGz6Y@c%UD?Z;it{lnOimjh`A=O+u%?ken^;2(@%W#%=@3w#`SsqR?2O=gvh zw#a{flw*u7FcIDdAx8#^xMU4E@;x)>PMKp3tZ%PBzOSell%;PW%Sjc5WjmOuzulLf z2$AD0nwWtyk&@)?L)*ZB6v!L9sM516`(14XbWztUIGwjPDVz3tn3hOb_ro`~tybAMPN=ms+0`nn3@iJl-?y|e=Fso@Oby;U z4MSZpd@DD^4xMY+f>+XvuKPMbjI3!dv}`Wcc~Q54EAyV_OSP271|d8UHo@=4#4rwN zB6YBsq4oIJH$pd(lcNYD4EHa_ENg!Ty>?+>=MZukZ*06;pwHnE=xpdRBm?4uMTwJ? zfYgD^4d6HEfUwDhRh`ZO7K%ObHV>ucOl$PRg#oJPBbC+fhigAA_}oN)%#N)!Mjs*l zMJj@Gub=M@8nLgwEjS0F|MNM=cK|;?^;I?Z&Niu496VHko@Oc8(>pB5!dd8^Vmdp&4fv zFhVnV9f|+`O@(hm*Qt&(6X_8qp!u75yNbe*xc@G&ps8y_;2zfVdE$AMM$%z}48T){ zoYrh^ZvlzSzw>C_;-evT$GU%zkubn@(&jv}Yc>1fVr=o}WcBuzuG((e7~Srx#s*n$ zLY8&BH6TZerVzaW4+DSyLK>i}ptdWWhmO(FjyxT;k98nVDUm$`UNMBRB#=J=TsT?g zm0Kr)w_Mo3IhJ`9(%Sn6o(0qDndRgJ?xD+;CmgpD+<(a7a$!lyH^zTb(;9mKZ&1@? zjm{|Erop#w-!>&%vNc?8uT?LW9%nMur_j|qix}3dy?`|%VTqeJZ{Cp>18=R=K)>ss zfo+L&jNVjP)-}lNvlH*+C2KzNFz`DX8Du%$o7R8A!q$g9Fa8dl@&^`sA} z0=?E(!ZH@Z8sj?7dYXSNS8@r2odY4rS$GUNd6=B^_vBs2vz(m3_QsymeIgrbUiV(h zi+LmwF|WS#73VQA!wL(SS^J)nABWd$R7WY_=iuO=$$inqv2G$Yuoii;LxvK8mfLym7G%GPrr8uE1K3KNwD8 zTu6a9bgMuPln#F|c`HR+lu*FGG;bw5h95U!py|!l3@;5z)GnD~b2w{Kw>^-p!-rn`H+y;Tw=|uJf ztoA~=2^j{K%#Hk`nS=u55Fn}0<k4ptqg^S0fs!J6biaFfLSOL zt$Si$CmU?Zv^7Gq77iF<$o13Xci-~ECIR*SQOtZ#tE$ZI!(`tK4tRy3m4&=KNPsOrZUhVi$Ilpjy1n0k&b`~ zzzIc8s|k9JwlU$7CH&R-TD?4fMQ^e)QNoGqa&PQC$iMkSc#y)Kx} zTe_@_ReCQX72#}M#|4|XV3e9fO~K`UyEt zOAy@oy~fIbNe7j7`gm59&~2__vLOO8Duu4~oQAvOSRN3+P(VzHJQD(+ zc<=ALv2I>Lg!!-{e=S=i5K0-w5^#Stxs-jXuCK~ONSitGH%zUG`>xvB(IBhGiso3; zCe1`WjpX)V&=i@ci(=(j3hN=EP>uP1CRn$HA9#Maa9!UGK z`F3e+G;eR+T*xTtMhscjKv(kUcjhOyqO81jOA@^pdY|x#(3AD-9A&rPxN+k*p;G%+ zu+VXl?${{1EmnSz>7cDhCHVbc?QcC+;|+#kCnx(XNSF_JDUn?NW*b7S)?s5TnLfEl zM3#B(=ZtJ0zf4ce#HAUik&J&_noGS;(>{gULY3nB{m)|=nU;IEw5{)2ZCxQ$h<9*s zFqWEx&UGE=A*+Giu&K9sP`9i+Crs2@u)3&Q{nn(#6{(J2j;)jHF!VkdzN=pa=xd z`(%vS7(Vg0tI^pg(&5M{UfDaXwidHukk#!HWam0cl!pdcO+sbfSaDSizghD8TWK-A zU>*ctePPHZU?~1C8qIkzFRJoL|Q6?N2aq?REJ?g2rFB=4&u~2>_ zzBhV-$`&!>Z|N8`O|gII2`l-OIu@Ns1Z)Aa#J+2_wTx(UPE65X8e}!eQ_jZ3R#pHI z6QO$5a?5Vp0wzY&8ym`hD~nMbFPVi)me2{R_f&QKwiXOqvVBMAo1G;4o-*K|A_a4aC1a3L)R zCsl94XMqHNFv9x?&Wuk~vFJcI(ASQpIjyDODe&wV2s#LnHLbw(K`4ule$uH?=07tEj9m2Q6(^tb}|Ar?%te9$_^lC6-jq8{bF z2c`6odufA`Jwj&Q1auff&^i(;W5hO68SLJ8Y#!{;I5on=tFPmHV{BU-E4R2_BbO}dW5r>>v5;CH>X9^rvKJ9Bqb#lBg>WFJDn_o(Hqh^R zs*`{1ogA-LVguB|OP~W$?(fp~j`v03ds@(Vwr*ulRcu1Iuh%Iim*4GN4QwCKDknu% zqn?x6`(rtgXDtoco$LL19JoJmJcfE*pX<*H_4MY3&%b^9mi+kf1A{S)=%eYH-m@m0 z`l2NU&n#>S!y?1LstR|$C<-0RZKX2c@1TE!LWu=Sr4X-O^N;(UlF?PH#nmIrpe|f;IRc%$W??O#$-SA zw_By*R`W`*c#ze6z{Z%w!_$yAv?U=s48BitDw?6W^<4MyxuqCPWYrTgP-Cpr^=^OP z7x#9ny!;ln#P?c!56dCksX=DBd@CZIvPGO1*(TPJG^B0DI0<9t{XtD<%3@q&FUywV zCOjvws3+_Yp*y_h1Qt=<)8!i#Mi~SYEsj1cb0Is0Z|GX>+aQH&LJgLCS^<`1@?LLiI3BVzdaa&ST@mNerNsumyYD4L9!*hE&Zd8{YD&La zZPbRgSym~0sEg}+UP0Dn=JBC#T@3b_uV23+C1L2D?}9(M}44v=QczMF8dR>c+fm;t>e-!`gOYDc}4s>a`D}?MJ>}6S>Bv2kd>seT~akTZcJe6>lgOtV?A*9dfc$s*c zQXyndp+Qy?Xc|pd`(S?o*GlNq*RNlnBEJj@-*^EA82*nkZ+*N(wG+fN{a=9G4r^Ra z2qAsuq4&I1V!DKE6NW4s;wsD}64v|d5p}x=A$>;|tZ%cewsawSm+JOpU!jMrA*e8e z=8|Mut<)vcih;%$A*AhTH4_PEkg}qiEw0&+XL^C&Xwg8g(Z{ECl2mfckGm-Hpk?;l>(gOYuqVoE8 TCkK$400000NkvXXu0mjfG#LU` From 066c4fa4e46386c98a3aab4b47be92abf9e6a630 Mon Sep 17 00:00:00 2001 From: misyaguziya <53165965+misyaguziya@users.noreply.github.com> Date: Tue, 22 Apr 2025 10:24:49 +0900 Subject: [PATCH 26/26] =?UTF-8?q?=F0=9F=91=8D=EF=B8=8F[Update]=20Version?= =?UTF-8?q?=203.0.4=20->=203.0.5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-python/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-python/config.py b/src-python/config.py index b607863b..df1f12e9 100644 --- a/src-python/config.py +++ b/src-python/config.py @@ -944,7 +944,7 @@ class Config: def init_config(self): # Read Only - self._VERSION = "3.0.4" + self._VERSION = "3.0.5" if getattr(sys, 'frozen', False): self._PATH_LOCAL = os_path.dirname(sys.executable) else: