From 1ebd32fea987cd3f66ba7c10293832c6bd8118da Mon Sep 17 00:00:00 2001 From: McHauge Date: Wed, 27 Nov 2024 14:24:58 +0100 Subject: [PATCH 01/23] [Update] Requirements : Upgrade sentencepiece to version 0.2.0 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e5b4bc5a..8963b514 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ pyinstaller==6.2.0 numpy==1.26.4 torch==2.2.2 transformers==4.37.2 -sentencepiece==0.1.99 +sentencepiece==0.2.0 ctranslate2==4.1.0 faster-whisper==1.0.3 openvr==1.26.701 From 6572fb96a32624f484874f992030360a4b0a6bc8 Mon Sep 17 00:00:00 2001 From: McHauge Date: Wed, 27 Nov 2024 16:04:35 +0100 Subject: [PATCH 02/23] Add support for send_only_translated_messages_overlay to only send translation to XS-Overlay --- config.py | 12 ++++++++++++ controller.py | 17 +++++++++++++---- locales/en.yml | 3 +++ locales/ja.yml | 3 +++ locales/ko.yml | 3 +++ locales/zh-Hans.yml | 3 +++ locales/zh-Hant.yml | 3 +++ models/overlay/overlay_image.py | 14 ++++++++++++-- view.py | 6 ++++++ .../createSettingBox_Others.py | 13 +++++++++++++ 10 files changed, 71 insertions(+), 6 deletions(-) diff --git a/config.py b/config.py index f5f89b49..8a898b40 100644 --- a/config.py +++ b/config.py @@ -762,6 +762,17 @@ class Config: self._ENABLE_SEND_ONLY_TRANSLATED_MESSAGES = value saveJson(self.PATH_CONFIG, inspect.currentframe().f_code.co_name, value) + @property + @json_serializable('ENABLE_SEND_ONLY_TRANSLATED_MESSAGES_OVERLAY') + def ENABLE_SEND_ONLY_TRANSLATED_MESSAGES_OVERLAY(self): + return self._ENABLE_SEND_ONLY_TRANSLATED_MESSAGES_OVERLAY + + @ENABLE_SEND_ONLY_TRANSLATED_MESSAGES_OVERLAY.setter + def ENABLE_SEND_ONLY_TRANSLATED_MESSAGES_OVERLAY(self, value): + if isinstance(value, bool): + self._ENABLE_SEND_ONLY_TRANSLATED_MESSAGES_OVERLAY = value + saveJson(self.PATH_CONFIG, inspect.currentframe().f_code.co_name, value) + @property @json_serializable('SEND_MESSAGE_BUTTON_TYPE') def SEND_MESSAGE_BUTTON_TYPE(self): @@ -1107,6 +1118,7 @@ class Config: self._RECEIVED_MESSAGE_FORMAT_WITH_T = "[message]([translation])" self._ENABLE_AUTO_CLEAR_MESSAGE_BOX = True self._ENABLE_SEND_ONLY_TRANSLATED_MESSAGES = False + self._ENABLE_SEND_ONLY_TRANSLATED_MESSAGES_OVERLAY = False self._SEND_MESSAGE_BUTTON_TYPE = "show" self._OVERLAY_SETTINGS = { "opacity": 1.0, diff --git a/controller.py b/controller.py index 57ee6779..aeb64d26 100644 --- a/controller.py +++ b/controller.py @@ -160,10 +160,14 @@ def receiveSpeakerMessage(message): if config.ENABLE_TRANSCRIPTION_RECEIVE is True: if config.ENABLE_OVERLAY_SMALL_LOG is True: if model.overlay.initialized is True: - overlay_image = model.createOverlayImageShort(message, translation) - model.updateOverlay(overlay_image) - # overlay_image = model.createOverlayImageLong("receive", message, translation) - # model.updateOverlay(overlay_image) + if config.ENABLE_SEND_ONLY_TRANSLATED_MESSAGES_OVERLAY is True: + overlay_image = model.createOverlayImageShort("", translation) + model.updateOverlay(overlay_image) + else: + overlay_image = model.createOverlayImageShort(message, translation) + model.updateOverlay(overlay_image) + # overlay_image = model.createOverlayImageLong("receive", message, translation) + # model.updateOverlay(overlay_image) # ------------Speaker2Chatbox------------ if config.ENABLE_SPEAKER2CHATBOX is True: @@ -907,6 +911,10 @@ def callbackSetEnableSendOnlyTranslatedMessages(value): print("callbackSetEnableSendOnlyTranslatedMessages", value) config.ENABLE_SEND_ONLY_TRANSLATED_MESSAGES = value +def callbackSetEnableSendOnlyTranslatedMessagesOverlay(value): + print("callbackSetEnableSendOnlyTranslatedMessagesOverlay", value) + config.ENABLE_SEND_ONLY_TRANSLATED_MESSAGES_OVERLAY = value + def callbackSetSendMessageButtonType(value): print("callbackSetSendMessageButtonType", value) config.SEND_MESSAGE_BUTTON_TYPE = value @@ -1158,6 +1166,7 @@ def createMainWindow(splash): # Others Tab "callback_set_enable_auto_clear_chatbox": callbackSetEnableAutoClearMessageBox, "callback_set_send_only_translated_messages": callbackSetEnableSendOnlyTranslatedMessages, + "callback_set_send_only_translated_messages_overlay": callbackSetEnableSendOnlyTranslatedMessagesOverlay, "callback_set_send_message_button_type": callbackSetSendMessageButtonType, "callback_set_enable_auto_export_message_logs": callbackSetEnableAutoExportMessageLogs, "callback_set_enable_vrc_mic_mute_sync": callbackSetEnableVrcMicMuteSync, diff --git a/locales/en.yml b/locales/en.yml index 2f5cdd0a..fc714817 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -234,6 +234,9 @@ config_window: send_only_translated_messages: label: "Send Only Translated Messages" + send_only_translated_messages_overlay: + label: "Send Only Translated Messages To Overlay" + send_message_button_type: label: "Send Message Button" hide: "Hide (Use enter key to send)" diff --git a/locales/ja.yml b/locales/ja.yml index 1a194973..4bedb378 100644 --- a/locales/ja.yml +++ b/locales/ja.yml @@ -233,6 +233,9 @@ config_window: send_only_translated_messages: label: "翻訳後のメッセージのみ送信する" + send_only_translated_messages_overlay: + label: "翻訳後のメッセージのみ送信する (Overlay)" + send_message_button_type: label: "メッセージ送信ボタン" hide: "非表示 (エンターキーを使って送信)" diff --git a/locales/ko.yml b/locales/ko.yml index 02e70c0b..7843d53f 100644 --- a/locales/ko.yml +++ b/locales/ko.yml @@ -233,6 +233,9 @@ config_window: send_only_translated_messages: label: "번역된 메시지만 전송" + send_only_translated_messages_overlay: + label: "번역된 메시지만 전송 (Overlay)" + send_message_button_type: label: "메시지 전송 버튼" hide: "숨김 (Enter 키를 사용하여 전송)" diff --git a/locales/zh-Hans.yml b/locales/zh-Hans.yml index 204ee9ee..a6f9d58b 100644 --- a/locales/zh-Hans.yml +++ b/locales/zh-Hans.yml @@ -234,6 +234,9 @@ config_window: send_only_translated_messages: label: "只发送翻译后的信息" + send_only_translated_messages_overlay: + label: "只发送翻译后的信息 (Overlay)" + send_message_button_type: label: "发送信息按钮" hide: "隐藏 (可使用回车发送信息)" diff --git a/locales/zh-Hant.yml b/locales/zh-Hant.yml index 26e79a07..7b31fcb4 100644 --- a/locales/zh-Hant.yml +++ b/locales/zh-Hant.yml @@ -227,6 +227,9 @@ config_window: send_only_translated_messages: label: "僅發送翻譯訊息" + send_only_translated_messages_overlay: + label: "僅發送翻譯訊息 (Overlay)" + send_message_button_type: label: "發送訊息按鈕" hide: "隱藏(使用 Enter 鍵發送)" diff --git a/models/overlay/overlay_image.py b/models/overlay/overlay_image.py index c3150f5e..a27f68d8 100644 --- a/models/overlay/overlay_image.py +++ b/models/overlay/overlay_image.py @@ -2,6 +2,7 @@ from os import path as os_path # from datetime import datetime from typing import Tuple from PIL import Image, ImageDraw, ImageFont +from config import config class OverlayImage: # TEXT_COLOR_LARGE = (223, 223, 223) @@ -216,10 +217,19 @@ class OverlayImage: background_color = ui_colors["background_color"] background_outline_color = ui_colors["background_outline_color"] - img = self.createTextboxShort(message, your_language, text_color, width, height, font_size) - if len(translation) > 0 and target_language is not None: + print("TEST") + print(message, your_language, translation, target_language) + + if len(message) > 0 and len(translation) > 0 and target_language is not None: + img = self.createTextboxShort(message, your_language, text_color, width, height, font_size) translation_img = self.createTextboxShort(translation, target_language, text_color, width, height, font_size) img = self.concatenateImagesVertically(img, translation_img) + else: + if len(message) > 0 and len(translation) == 0 : + img = self.createTextboxShort(message, your_language, text_color, width, height, font_size) + else: + if len(translation) > 0 and target_language is not None: + img = self.createTextboxShort(translation, target_language, text_color, width, height, font_size) background = Image.new("RGBA", img.size, (0, 0, 0, 0)) draw = ImageDraw.Draw(background) diff --git a/view.py b/view.py index 848e10a7..05a4f983 100644 --- a/view.py +++ b/view.py @@ -529,6 +529,11 @@ class View(): CALLBACK_SET_ENABLE_SEND_ONLY_TRANSLATED_MESSAGES=None, VAR_ENABLE_SEND_ONLY_TRANSLATED_MESSAGES=BooleanVar(value=config.ENABLE_SEND_ONLY_TRANSLATED_MESSAGES), + VAR_LABEL_ENABLE_SEND_ONLY_TRANSLATED_MESSAGES_OVERLAY=StringVar(value=i18n.t("config_window.send_only_translated_messages_overlay.label")), + VAR_DESC_ENABLE_SEND_ONLY_TRANSLATED_MESSAGES_OVERLAY=None, + CALLBACK_SET_ENABLE_SEND_ONLY_TRANSLATED_MESSAGES_OVERLAY=None, + VAR_ENABLE_SEND_ONLY_TRANSLATED_MESSAGES_OVERLAY=BooleanVar(value=config.ENABLE_SEND_ONLY_TRANSLATED_MESSAGES_OVERLAY), + VAR_LABEL_SEND_MESSAGE_BUTTON_TYPE=StringVar(value=i18n.t("config_window.send_message_button_type.label")), VAR_DESC_SEND_MESSAGE_BUTTON_TYPE=None, CALLBACK_SET_SEND_MESSAGE_BUTTON_TYPE=None, @@ -798,6 +803,7 @@ class View(): # Others Tab self.view_variable.CALLBACK_SET_ENABLE_AUTO_CLEAR_MESSAGE_BOX=config_window_registers.get("callback_set_enable_auto_clear_chatbox", None) self.view_variable.CALLBACK_SET_ENABLE_SEND_ONLY_TRANSLATED_MESSAGES=config_window_registers.get("callback_set_send_only_translated_messages", None) + self.view_variable.CALLBACK_SET_ENABLE_SEND_ONLY_TRANSLATED_MESSAGES_OVERLAY=config_window_registers.get("callback_set_send_only_translated_messages_overlay", None) self.view_variable.CALLBACK_SET_SEND_MESSAGE_BUTTON_TYPE=config_window_registers.get("callback_set_send_message_button_type", None) self.view_variable.CALLBACK_SET_ENABLE_AUTO_EXPORT_MESSAGE_LOGS=config_window_registers.get("callback_set_enable_auto_export_message_logs", None) diff --git a/vrct_gui/config_window/widgets/createSideMenuAndSettingsBoxContainers/setting_box_containers/setting_box_others/createSettingBox_Others.py b/vrct_gui/config_window/widgets/createSideMenuAndSettingsBoxContainers/setting_box_containers/setting_box_others/createSettingBox_Others.py index 72391a8a..03915eb2 100644 --- a/vrct_gui/config_window/widgets/createSideMenuAndSettingsBoxContainers/setting_box_containers/setting_box_others/createSettingBox_Others.py +++ b/vrct_gui/config_window/widgets/createSideMenuAndSettingsBoxContainers/setting_box_containers/setting_box_others/createSettingBox_Others.py @@ -15,6 +15,9 @@ def createSettingBox_Others(setting_box_wrapper, config_window, settings, view_v def checkboxSendOnlyTranslatedMessagesCallback(checkbox_box_widget): callFunctionIfCallable(view_variable.CALLBACK_SET_ENABLE_SEND_ONLY_TRANSLATED_MESSAGES, checkbox_box_widget.get()) + def checkboxSendOnlyTranslatedMessagesOverlayCallback(checkbox_box_widget): + callFunctionIfCallable(view_variable.CALLBACK_SET_ENABLE_SEND_ONLY_TRANSLATED_MESSAGES_OVERLAY, checkbox_box_widget.get()) + def checkboxSendMessageButtonTypeCallback(): callFunctionIfCallable(view_variable.CALLBACK_SET_SEND_MESSAGE_BUTTON_TYPE, view_variable.VAR_SEND_MESSAGE_BUTTON_TYPE.get()) @@ -53,6 +56,16 @@ def createSettingBox_Others(setting_box_wrapper, config_window, settings, view_v config_window.sb__send_only_translated_messages.grid(row=row) row+=1 + config_window.sb__send_only_translated_messages_overlay = createSettingBoxCheckbox( + for_var_label_text=view_variable.VAR_LABEL_ENABLE_SEND_ONLY_TRANSLATED_MESSAGES_OVERLAY, + for_var_desc_text=view_variable.VAR_DESC_ENABLE_SEND_ONLY_TRANSLATED_MESSAGES_OVERLAY, + checkbox_attr_name="sb__checkbox_send_only_translated_messages_overlay", + command=lambda: checkboxSendOnlyTranslatedMessagesOverlayCallback(config_window.sb__checkbox_send_only_translated_messages_overlay), + variable=view_variable.VAR_ENABLE_SEND_ONLY_TRANSLATED_MESSAGES_OVERLAY, + ) + config_window.sb__send_only_translated_messages_overlay.grid(row=row) + row+=1 + config_window.sb__send_message_button_type = createSettingBoxRadioButtons( for_var_label_text=view_variable.VAR_LABEL_SEND_MESSAGE_BUTTON_TYPE, for_var_desc_text=view_variable.VAR_DESC_SEND_MESSAGE_BUTTON_TYPE, From b019e37b31e00c2ac0e72cfa34e662b73b16ef5b Mon Sep 17 00:00:00 2001 From: McHauge Date: Wed, 27 Nov 2024 16:20:23 +0100 Subject: [PATCH 03/23] remove debug logs --- models/overlay/overlay_image.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/models/overlay/overlay_image.py b/models/overlay/overlay_image.py index a27f68d8..7f5733b7 100644 --- a/models/overlay/overlay_image.py +++ b/models/overlay/overlay_image.py @@ -217,9 +217,6 @@ class OverlayImage: background_color = ui_colors["background_color"] background_outline_color = ui_colors["background_outline_color"] - print("TEST") - print(message, your_language, translation, target_language) - if len(message) > 0 and len(translation) > 0 and target_language is not None: img = self.createTextboxShort(message, your_language, text_color, width, height, font_size) translation_img = self.createTextboxShort(translation, target_language, text_color, width, height, font_size) 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 04/23] [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 05/23] [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 06/23] [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 07/23] [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 08/23] [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 09/23] [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 10/23] [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 11/23] [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 12/23] [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 13/23] [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 14/23] [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 15/23] [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 16/23] [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 17/23] [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 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≧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%0A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 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 18/23] =?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: From ec33d4020db22638b48d0d24149bc4d62b708c51 Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Wed, 23 Apr 2025 01:37:23 +0900 Subject: [PATCH 19/23] [Update] Readmes: Remove the outdated information 'How to run in python'. --- README.ja.md | 9 --------- README.ko.md | 9 --------- README.md | 9 --------- README.zh-Hant.md | 9 --------- 4 files changed, 36 deletions(-) diff --git a/README.ja.md b/README.ja.md index 1d7a8c2a..d89c95a6 100644 --- a/README.ja.md +++ b/README.ja.md @@ -91,15 +91,6 @@ VRCTはあなたの会話を以下でサポートをします。
-# pythonで実行したい場合 -1. 以下のバージョンのpythonをインストールしてください。 - `python version 3.11.5` -2. packageのインストールとmain.pyを起動してください。 - ```bash - ./install.bat - python main.py - ``` - ## Author - [みしゃ(misyaguzi)](https://github.com/misyaguziya) (メイン開発) - [しいな(Shiina_12siy)](https://twitter.com/Shiina_12siy) (UI/UX, UI多言語対応) diff --git a/README.ko.md b/README.ko.md index cffa325c..bb260ba0 100644 --- a/README.ko.md +++ b/README.ko.md @@ -91,15 +91,6 @@ VRCT는 다음과 같이 당신의 대화를 도와드려요.
-# python으로 실행하고 싶은 경우 -1. 다음 버전의 python을 설치합니다. - `python version 3.11.5` -2. 패키지를 설치하고 main.py를 실행합니다. - ```bash - ./install.bat - python main.py - ``` - ## Author - [みしゃ(misyaguzi)](https://github.com/misyaguziya) (주요 개발) - [しいな(Shiina_12siy)](https://twitter.com/Shiina_12siy) (UI/UX, UI 다국어 지원) diff --git a/README.md b/README.md index 13c563a6..c7af06b9 100644 --- a/README.md +++ b/README.md @@ -91,15 +91,6 @@ Initial setup, basic functions, and other features are also described.
-# If you want to run it in python -1. Install the following version of python. - `python version 3.11.5` -2. Install package and run main.py. - ```bash - ./install.bat - python main.py - ``` - ## Author - [みしゃ(misyaguzi)](https://github.com/misyaguziya) (Main Development) - [しいな(Shiina_12siy)](https://twitter.com/Shiina_12siy) (UI/UX, UI multilingual support) diff --git a/README.zh-Hant.md b/README.zh-Hant.md index ea79a1b1..e9535ceb 100644 --- a/README.zh-Hant.md +++ b/README.zh-Hant.md @@ -90,15 +90,6 @@ VRCT 可以:
-# 原始碼啟動 -1. 安裝此版本的 Python。 - `python version 3.11.5` -2. 安裝 package 並啟動 main.py。 - ```bash - ./install.bat - python main.py - ``` - ## 作者 - [みしゃ(misyaguzi)](https://github.com/misyaguziya) (主要開發) - [しいな(Shiina_12siy)](https://twitter.com/Shiina_12siy) (UI/UX, UI 多語系支援) From d0dbdf69f0484298a231a043b05d9abd66e1a2d0 Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Wed, 23 Apr 2025 06:36:55 +0900 Subject: [PATCH 20/23] [Refactor] Organize readme files. --- README.md | 108 +----------------- docs/{ => img}/kofi_logo.png | Bin docs/{ => img}/main_window.png | Bin docs/{ => img}/patreon_logo_black.png | Bin docs/{ => img}/patreon_logo_white.png | Bin docs/{ => img}/pixiv_fanbox_black.png | Bin docs/{ => img}/pixiv_fanbox_white.png | Bin docs/{ => img}/supporter_section_border_d.png | Bin docs/{ => img}/supporter_section_border_l.png | Bin docs/{ => img}/vrct_logo.png | Bin docs/{ => img}/vrct_logo_black.png | Bin docs/{ => img}/vrct_logo_white.png | Bin docs/readmes/README.en.md | 107 +++++++++++++++++ README.ja.md => docs/readmes/README.ja.md | 30 ++--- README.ko.md => docs/readmes/README.ko.md | 30 ++--- .../readmes/README.zh-Hant.md | 30 ++--- 16 files changed, 153 insertions(+), 152 deletions(-) rename docs/{ => img}/kofi_logo.png (100%) rename docs/{ => img}/main_window.png (100%) rename docs/{ => img}/patreon_logo_black.png (100%) rename docs/{ => img}/patreon_logo_white.png (100%) rename docs/{ => img}/pixiv_fanbox_black.png (100%) rename docs/{ => img}/pixiv_fanbox_white.png (100%) rename docs/{ => img}/supporter_section_border_d.png (100%) rename docs/{ => img}/supporter_section_border_l.png (100%) rename docs/{ => img}/vrct_logo.png (100%) rename docs/{ => img}/vrct_logo_black.png (100%) rename docs/{ => img}/vrct_logo_white.png (100%) create mode 100644 docs/readmes/README.en.md rename README.ja.md => docs/readmes/README.ja.md (70%) rename README.ko.md => docs/readmes/README.ko.md (70%) rename README.zh-Hant.md => docs/readmes/README.zh-Hant.md (67%) diff --git a/README.md b/README.md index c7af06b9..ab3e36da 100644 --- a/README.md +++ b/README.md @@ -1,107 +1 @@ -
- - - - - VRCT Logo - - -
-
- -[![GitHub release](https://img.shields.io/github/v/release/misyaguziya/VRCT.svg)](https://github.com/misyaguziya/VRCT/releases) -[![Downloads](https://img.shields.io/github/downloads/misyaguziya/VRCT/total)](https://github.com/misyaguziya/VRCT/releases) -[![Licence](https://img.shields.io/github/license/misyaguziya/VRCT)](https://github.com/misyaguziya/VRCT/blob/master/LICENSE) -[![Booth](https://img.shields.io/badge/Store-Booth.pm-red)](https://misyaguziya.booth.pm/items/5155325) -[![Github Sponsors](https://img.shields.io/badge/GitHub%20Sponsors-30363D?&logo=GitHub-Sponsors&logoColor=EA4AAA)](https://github.com/sponsors/misyaguziya) - -

-Become a VRCT Supporter on: -

- -
- - - - PIXIV FANBOX - -   - - - - - - Patreon - -   - - - - Ko-fi - -   - -
- - - - - Supporter Section Border - - -
-
- -| **English** | [日本語](./README.ja.md) | [한국어](./README.ko.md) | [繁體中文](./README.zh-Hant.md) | - -

-VRCT is software that supports VRChat conversations with translation and transcription. -

- -![](docs/main_window.png) - -
- -# Download & Install -Download from anywhere you like. -- [Github.com](https://github.com/misyaguziya/VRCT/releases/) -- [BOOTH.pm](https://misyaguziya.booth.pm/items/5155325) - -Just download and run the exe. - -# What is VRCT? -VRCT is software that supports conversations between people who speak different languages by providing chat or voice translation. -These features are designed for use within VRChat. -*Although not supported, it is also used for other purposes such as watching movies. - -VRCT supports your conversations with -- 💬 **Send chat to VRChat** -- 🌐 **Translation** -- 🎙 **Transcription of audio from microphone** -- 🔈 **Transcription of audio from Speaker** - -# Documents -Initial setup, basic functions, and other features are also described. -- [Documents Link](https://mzsoftware.notion.site/VRCT-Documents-be79b7a165f64442ad8f326d86c22246?pvs=4) - -# How to Use (YouTube) -
- -[![](https://img.youtube.com/vi/rUTad037n8Q/0.jpg)](https://www.youtube.com/watch?v=rUTad037n8Q) - -
- -## Author -- [みしゃ(misyaguzi)](https://github.com/misyaguziya) (Main Development) -- [しいな(Shiina_12siy)](https://twitter.com/Shiina_12siy) (UI/UX, UI multilingual support) -- [レラ](https://github.com/soumt-r) (Technical Support) -- [どね](https://twitter.com/done_vrc) (Logo Design) - -## Thanks to our contributors - - - - ---- - -VRCT is not endorsed by VRChat and does not reflect the views or opinions of VRChat or anyone officially involved in producing or managing VRChat properties. VRChat and all associated properties are trademarks or registered trademarks of VRChat Inc. VRChat © VRChat Inc. \ No newline at end of file +docs/readmes/README.en.md \ No newline at end of file diff --git a/docs/kofi_logo.png b/docs/img/kofi_logo.png similarity index 100% rename from docs/kofi_logo.png rename to docs/img/kofi_logo.png diff --git a/docs/main_window.png b/docs/img/main_window.png similarity index 100% rename from docs/main_window.png rename to docs/img/main_window.png diff --git a/docs/patreon_logo_black.png b/docs/img/patreon_logo_black.png similarity index 100% rename from docs/patreon_logo_black.png rename to docs/img/patreon_logo_black.png diff --git a/docs/patreon_logo_white.png b/docs/img/patreon_logo_white.png similarity index 100% rename from docs/patreon_logo_white.png rename to docs/img/patreon_logo_white.png diff --git a/docs/pixiv_fanbox_black.png b/docs/img/pixiv_fanbox_black.png similarity index 100% rename from docs/pixiv_fanbox_black.png rename to docs/img/pixiv_fanbox_black.png diff --git a/docs/pixiv_fanbox_white.png b/docs/img/pixiv_fanbox_white.png similarity index 100% rename from docs/pixiv_fanbox_white.png rename to docs/img/pixiv_fanbox_white.png diff --git a/docs/supporter_section_border_d.png b/docs/img/supporter_section_border_d.png similarity index 100% rename from docs/supporter_section_border_d.png rename to docs/img/supporter_section_border_d.png diff --git a/docs/supporter_section_border_l.png b/docs/img/supporter_section_border_l.png similarity index 100% rename from docs/supporter_section_border_l.png rename to docs/img/supporter_section_border_l.png diff --git a/docs/vrct_logo.png b/docs/img/vrct_logo.png similarity index 100% rename from docs/vrct_logo.png rename to docs/img/vrct_logo.png diff --git a/docs/vrct_logo_black.png b/docs/img/vrct_logo_black.png similarity index 100% rename from docs/vrct_logo_black.png rename to docs/img/vrct_logo_black.png diff --git a/docs/vrct_logo_white.png b/docs/img/vrct_logo_white.png similarity index 100% rename from docs/vrct_logo_white.png rename to docs/img/vrct_logo_white.png diff --git a/docs/readmes/README.en.md b/docs/readmes/README.en.md new file mode 100644 index 00000000..b99a1da8 --- /dev/null +++ b/docs/readmes/README.en.md @@ -0,0 +1,107 @@ +
+ + + + + VRCT Logo + + +
+
+ +[![GitHub release](https://img.shields.io/github/v/release/misyaguziya/VRCT.svg)](https://github.com/misyaguziya/VRCT/releases) +[![Downloads](https://img.shields.io/github/downloads/misyaguziya/VRCT/total)](https://github.com/misyaguziya/VRCT/releases) +[![Licence](https://img.shields.io/github/license/misyaguziya/VRCT)](https://github.com/misyaguziya/VRCT/blob/master/LICENSE) +[![Booth](https://img.shields.io/badge/Store-Booth.pm-red)](https://misyaguziya.booth.pm/items/5155325) +[![Github Sponsors](https://img.shields.io/badge/GitHub%20Sponsors-30363D?&logo=GitHub-Sponsors&logoColor=EA4AAA)](https://github.com/sponsors/misyaguziya) + +

+Become a VRCT Supporter on: +

+ + + + + + PIXIV FANBOX + +   + + + + + + Patreon + +   + + + + Ko-fi + +   + +
+ + + + + Supporter Section Border + + +
+
+ +| **English** | [日本語](./README.ja.md) | [한국어](./README.ko.md) | [繁體中文](./README.zh-Hant.md) | + +

+VRCT is software that supports VRChat conversations with translation and transcription. +

+ +![](../img/main_window.png) + +
+ +# Download & Install +Download from anywhere you like. +- [Github.com](https://github.com/misyaguziya/VRCT/releases/) +- [BOOTH.pm](https://misyaguziya.booth.pm/items/5155325) + +Just download and run the exe. + +# What is VRCT? +VRCT is software that supports conversations between people who speak different languages by providing chat or voice translation. +These features are designed for use within VRChat. +*Although not supported, it is also used for other purposes such as watching movies. + +VRCT supports your conversations with +- 💬 **Send chat to VRChat** +- 🌐 **Translation** +- 🎙 **Transcription of audio from microphone** +- 🔈 **Transcription of audio from Speaker** + +# Documents +Initial setup, basic functions, and other features are also described. +- [Documents Link](https://mzsoftware.notion.site/VRCT-Documents-be79b7a165f64442ad8f326d86c22246?pvs=4) + +# How to Use (YouTube) +
+ +[![](https://img.youtube.com/vi/rUTad037n8Q/0.jpg)](https://www.youtube.com/watch?v=rUTad037n8Q) + +
+ +## Author +- [みしゃ(misyaguzi)](https://github.com/misyaguziya) (Main Development) +- [しいな(Shiina_12siy)](https://twitter.com/Shiina_12siy) (UI/UX, UI multilingual support) +- [レラ](https://github.com/soumt-r) (Technical Support) +- [どね](https://twitter.com/done_vrc) (Logo Design) + +## Thanks to our contributors + + + + +--- + +VRCT is not endorsed by VRChat and does not reflect the views or opinions of VRChat or anyone officially involved in producing or managing VRChat properties. VRChat and all associated properties are trademarks or registered trademarks of VRChat Inc. VRChat © VRChat Inc. \ No newline at end of file diff --git a/README.ja.md b/docs/readmes/README.ja.md similarity index 70% rename from README.ja.md rename to docs/readmes/README.ja.md index d89c95a6..e760bd7f 100644 --- a/README.ja.md +++ b/docs/readmes/README.ja.md @@ -1,9 +1,9 @@
- - - VRCT Logo + + + VRCT Logo
@@ -21,44 +21,44 @@ Become a VRCT Supporter on: - - - PIXIV FANBOX + + + PIXIV FANBOX    - - - Patreon + + + Patreon    - Ko-fi + Ko-fi   
- - - Supporter Section Border + + + Supporter Section Border

-| [English](./README.md) | **日本語** | [한국어](./README.ko.md) | [繁體中文](./README.zh-Hant.md) | +| [English](./README.en.md) | **日本語** | [한국어](./README.ko.md) | [繁體中文](./README.zh-Hant.md) |

VRCTは翻訳や文字起こしでVRChatの会話をサポートするソフトウェアです。

-![](docs/main_window.png) +![](../img/main_window.png)
diff --git a/README.ko.md b/docs/readmes/README.ko.md similarity index 70% rename from README.ko.md rename to docs/readmes/README.ko.md index bb260ba0..08498602 100644 --- a/README.ko.md +++ b/docs/readmes/README.ko.md @@ -1,9 +1,9 @@
- - - VRCT Logo + + + VRCT Logo
@@ -21,44 +21,44 @@ Become a VRCT Supporter on: - - - PIXIV FANBOX + + + PIXIV FANBOX    - - - Patreon + + + Patreon    - Ko-fi + Ko-fi   
- - - Supporter Section Border + + + Supporter Section Border

-| [English](./README.md) | [日本語](./README.ja.md) | **한국어** | [繁體中文](./README.zh-Hant.md) | +| [English](./README.en.md) | [日本語](./README.ja.md) | **한국어** | [繁體中文](./README.zh-Hant.md) |

VRCT는 음성인식 및 번역 기능을 통해 VRChat의 대화를 지원하는 소프트웨어입니다.

-![](docs/main_window.png) +![](../img/main_window.png)
diff --git a/README.zh-Hant.md b/docs/readmes/README.zh-Hant.md similarity index 67% rename from README.zh-Hant.md rename to docs/readmes/README.zh-Hant.md index e9535ceb..a4dae847 100644 --- a/README.zh-Hant.md +++ b/docs/readmes/README.zh-Hant.md @@ -1,9 +1,9 @@
- - - VRCT Logo + + + VRCT Logo
@@ -21,44 +21,44 @@ Become a VRCT Supporter on: - - - PIXIV FANBOX + + + PIXIV FANBOX    - - - Patreon + + + Patreon    - Ko-fi + Ko-fi   
- - - Supporter Section Border + + + Supporter Section Border

-| [English](./README.md) | [日本語](./README.ja.md) | [한국어](./README.ko.md) | **繁體中文** | +| [English](./README.en.md) | [日本語](./README.ja.md) | [한국어](./README.ko.md) | **繁體中文** |

VRCT 是一個支援 VRChat 對話翻譯和紀錄的軟體。

-![](docs/main_window.png) +![](../img/main_window.png)
From 1b7e315c2b874123a7d7d2e945a3366f8759e7c1 Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Wed, 23 Apr 2025 13:51:34 +0900 Subject: [PATCH 21/23] [Update] Add Symbolic link. --- README.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 120000 README.md diff --git a/README.md b/README.md deleted file mode 100644 index ab3e36da..00000000 --- a/README.md +++ /dev/null @@ -1 +0,0 @@ -docs/readmes/README.en.md \ No newline at end of file diff --git a/README.md b/README.md new file mode 120000 index 00000000..ab3e36da --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +docs/readmes/README.en.md \ No newline at end of file From 1f4c4e76401010c42f3dfc96910fe29b15570824 Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Wed, 23 Apr 2025 14:34:31 +0900 Subject: [PATCH 22/23] [Update] Adjust file paths. --- docs/readmes/README.en.md | 30 +++++++++++++++--------------- docs/readmes/README.ja.md | 30 +++++++++++++++--------------- docs/readmes/README.ko.md | 30 +++++++++++++++--------------- docs/readmes/README.zh-Hant.md | 30 +++++++++++++++--------------- 4 files changed, 60 insertions(+), 60 deletions(-) diff --git a/docs/readmes/README.en.md b/docs/readmes/README.en.md index b99a1da8..5edf2acc 100644 --- a/docs/readmes/README.en.md +++ b/docs/readmes/README.en.md @@ -1,9 +1,9 @@
- - - VRCT Logo + + + VRCT Logo
@@ -21,44 +21,44 @@ Become a VRCT Supporter on: - - - PIXIV FANBOX + + + PIXIV FANBOX    - - - Patreon + + + Patreon    - Ko-fi + Ko-fi   
- - - Supporter Section Border + + + Supporter Section Border

-| **English** | [日本語](./README.ja.md) | [한국어](./README.ko.md) | [繁體中文](./README.zh-Hant.md) | +| **English** | [日本語](docs/readmes/README.ja.md) | [한국어](docs/readmes/README.ko.md) | [繁體中文](docs/readmes/README.zh-Hant.md) |

VRCT is software that supports VRChat conversations with translation and transcription.

-![](../img/main_window.png) +![](docs/img/main_window.png)
diff --git a/docs/readmes/README.ja.md b/docs/readmes/README.ja.md index e760bd7f..1bc6c764 100644 --- a/docs/readmes/README.ja.md +++ b/docs/readmes/README.ja.md @@ -1,9 +1,9 @@
- - - VRCT Logo + + + VRCT Logo
@@ -21,44 +21,44 @@ Become a VRCT Supporter on: - - - PIXIV FANBOX + + + PIXIV FANBOX    - - - Patreon + + + Patreon    - Ko-fi + Ko-fi   
- - - Supporter Section Border + + + Supporter Section Border

-| [English](./README.en.md) | **日本語** | [한국어](./README.ko.md) | [繁體中文](./README.zh-Hant.md) | +| [English](docs/readmes/README.en.md) | **日本語** | [한국어](docs/readmes/README.ko.md) | [繁體中文](docs/readmes/README.zh-Hant.md) |

VRCTは翻訳や文字起こしでVRChatの会話をサポートするソフトウェアです。

-![](../img/main_window.png) +![](docs/img/main_window.png)
diff --git a/docs/readmes/README.ko.md b/docs/readmes/README.ko.md index 08498602..21406969 100644 --- a/docs/readmes/README.ko.md +++ b/docs/readmes/README.ko.md @@ -1,9 +1,9 @@
- - - VRCT Logo + + + VRCT Logo
@@ -21,44 +21,44 @@ Become a VRCT Supporter on: - - - PIXIV FANBOX + + + PIXIV FANBOX    - - - Patreon + + + Patreon    - Ko-fi + Ko-fi   
- - - Supporter Section Border + + + Supporter Section Border

-| [English](./README.en.md) | [日本語](./README.ja.md) | **한국어** | [繁體中文](./README.zh-Hant.md) | +| [English](docs/readmes/README.en.md) | [日本語](docs/readmes/README.ja.md) | **한국어** | [繁體中文](docs/readmes/README.zh-Hant.md) |

VRCT는 음성인식 및 번역 기능을 통해 VRChat의 대화를 지원하는 소프트웨어입니다.

-![](../img/main_window.png) +![](docs/img/main_window.png)
diff --git a/docs/readmes/README.zh-Hant.md b/docs/readmes/README.zh-Hant.md index a4dae847..d75ba4f2 100644 --- a/docs/readmes/README.zh-Hant.md +++ b/docs/readmes/README.zh-Hant.md @@ -1,9 +1,9 @@
- - - VRCT Logo + + + VRCT Logo
@@ -21,44 +21,44 @@ Become a VRCT Supporter on: - - - PIXIV FANBOX + + + PIXIV FANBOX    - - - Patreon + + + Patreon    - Ko-fi + Ko-fi   
- - - Supporter Section Border + + + Supporter Section Border

-| [English](./README.en.md) | [日本語](./README.ja.md) | [한국어](./README.ko.md) | **繁體中文** | +| [English](docs/readmes/README.en.md) | [日本語](docs/readmes/README.ja.md) | [한국어](docs/readmes/README.ko.md) | **繁體中文** |

VRCT 是一個支援 VRChat 對話翻譯和紀錄的軟體。

-![](../img/main_window.png) +![](docs/img/main_window.png)
From 3e010376f4f9ba511fc8859774b9210cec9b3b49 Mon Sep 17 00:00:00 2001 From: Sakamoto Shiina <68018796+ShiinaSakamoto@users.noreply.github.com> Date: Thu, 24 Apr 2025 01:48:15 +0900 Subject: [PATCH 23/23] [bugfix] Change the links relative path to absolute path that is from root dir. --- docs/readmes/README.en.md | 30 +++++++++++++++--------------- docs/readmes/README.ja.md | 30 +++++++++++++++--------------- docs/readmes/README.ko.md | 30 +++++++++++++++--------------- docs/readmes/README.zh-Hant.md | 30 +++++++++++++++--------------- 4 files changed, 60 insertions(+), 60 deletions(-) diff --git a/docs/readmes/README.en.md b/docs/readmes/README.en.md index 5edf2acc..f3f35a16 100644 --- a/docs/readmes/README.en.md +++ b/docs/readmes/README.en.md @@ -1,9 +1,9 @@
- - - VRCT Logo + + + VRCT Logo
@@ -21,44 +21,44 @@ Become a VRCT Supporter on: - - - PIXIV FANBOX + + + PIXIV FANBOX    - - - Patreon + + + Patreon    - Ko-fi + Ko-fi   
- - - Supporter Section Border + + + Supporter Section Border

-| **English** | [日本語](docs/readmes/README.ja.md) | [한국어](docs/readmes/README.ko.md) | [繁體中文](docs/readmes/README.zh-Hant.md) | +| **English** | [日本語](/docs/readmes/README.ja.md) | [한국어](/docs/readmes/README.ko.md) | [繁體中文](/docs/readmes/README.zh-Hant.md) |

VRCT is software that supports VRChat conversations with translation and transcription.

-![](docs/img/main_window.png) +![](/docs/img/main_window.png)
diff --git a/docs/readmes/README.ja.md b/docs/readmes/README.ja.md index 1bc6c764..86efe858 100644 --- a/docs/readmes/README.ja.md +++ b/docs/readmes/README.ja.md @@ -1,9 +1,9 @@
- - - VRCT Logo + + + VRCT Logo
@@ -21,44 +21,44 @@ Become a VRCT Supporter on: - - - PIXIV FANBOX + + + PIXIV FANBOX    - - - Patreon + + + Patreon    - Ko-fi + Ko-fi   
- - - Supporter Section Border + + + Supporter Section Border

-| [English](docs/readmes/README.en.md) | **日本語** | [한국어](docs/readmes/README.ko.md) | [繁體中文](docs/readmes/README.zh-Hant.md) | +| [English](/docs/readmes/README.en.md) | **日本語** | [한국어](/docs/readmes/README.ko.md) | [繁體中文](/docs/readmes/README.zh-Hant.md) |

VRCTは翻訳や文字起こしでVRChatの会話をサポートするソフトウェアです。

-![](docs/img/main_window.png) +![](/docs/img/main_window.png)
diff --git a/docs/readmes/README.ko.md b/docs/readmes/README.ko.md index 21406969..d09e9d36 100644 --- a/docs/readmes/README.ko.md +++ b/docs/readmes/README.ko.md @@ -1,9 +1,9 @@
- - - VRCT Logo + + + VRCT Logo
@@ -21,44 +21,44 @@ Become a VRCT Supporter on: - - - PIXIV FANBOX + + + PIXIV FANBOX    - - - Patreon + + + Patreon    - Ko-fi + Ko-fi   
- - - Supporter Section Border + + + Supporter Section Border

-| [English](docs/readmes/README.en.md) | [日本語](docs/readmes/README.ja.md) | **한국어** | [繁體中文](docs/readmes/README.zh-Hant.md) | +| [English](/docs/readmes/README.en.md) | [日本語](/docs/readmes/README.ja.md) | **한국어** | [繁體中文](/docs/readmes/README.zh-Hant.md) |

VRCT는 음성인식 및 번역 기능을 통해 VRChat의 대화를 지원하는 소프트웨어입니다.

-![](docs/img/main_window.png) +![](/docs/img/main_window.png)
diff --git a/docs/readmes/README.zh-Hant.md b/docs/readmes/README.zh-Hant.md index d75ba4f2..05a8fece 100644 --- a/docs/readmes/README.zh-Hant.md +++ b/docs/readmes/README.zh-Hant.md @@ -1,9 +1,9 @@
- - - VRCT Logo + + + VRCT Logo
@@ -21,44 +21,44 @@ Become a VRCT Supporter on: - - - PIXIV FANBOX + + + PIXIV FANBOX    - - - Patreon + + + Patreon    - Ko-fi + Ko-fi   
- - - Supporter Section Border + + + Supporter Section Border

-| [English](docs/readmes/README.en.md) | [日本語](docs/readmes/README.ja.md) | [한국어](docs/readmes/README.ko.md) | **繁體中文** | +| [English](/docs/readmes/README.en.md) | [日本語](/docs/readmes/README.ja.md) | [한국어](/docs/readmes/README.ko.md) | **繁體中文** |

VRCT 是一個支援 VRChat 對話翻譯和紀錄的軟體。

-![](docs/img/main_window.png) +![](/docs/img/main_window.png)