diff --git a/.gitignore b/.gitignore index e628117a..6ff13fd6 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,8 @@ weights/ error.log *.exe *.ipynb - +VRCT.zip +VRCT_cuda.zip # Added by WebUI migration # Logs diff --git a/backend.spec b/backend.spec index 33ec3cae..3933d942 100644 --- a/backend.spec +++ b/backend.spec @@ -5,7 +5,7 @@ a = Analysis( ['src-python\\mainloop.py'], pathex=[], binaries=[], - datas=[('./fonts', 'fonts/'), ('.venv/Lib/site-packages/zeroconf', 'zeroconf/'), ('.venv/Lib/site-packages/openvr', 'openvr/'), ('.venv/Lib/site-packages/pykakasi', 'pykakasi/'), ('.venv/Lib/site-packages/faster_whisper', 'faster_whisper/')], + datas=[('./fonts', 'fonts/'), ('.venv/Lib/site-packages/zeroconf', 'zeroconf/'), ('.venv/Lib/site-packages/openvr', 'openvr/'), ('.venv/Lib/site-packages/pykakasi', 'pykakasi/'), ('.venv/Lib/site-packages/faster_whisper', 'faster_whisper/'), ('.venv/Lib/site-packages/hf_xet', 'hf_xet/')], hiddenimports=[], hookspath=[], hooksconfig={}, diff --git a/backend_cuda.spec b/backend_cuda.spec index 34ed248f..08ba5fd7 100644 --- a/backend_cuda.spec +++ b/backend_cuda.spec @@ -5,7 +5,7 @@ a = Analysis( ['src-python\\mainloop.py'], pathex=[], binaries=[], - datas=[('./fonts', 'fonts/'), ('.venv_cuda/Lib/site-packages/zeroconf', 'zeroconf/'), ('.venv_cuda/Lib/site-packages/openvr', 'openvr/'), ('.venv_cuda/Lib/site-packages/pykakasi', 'pykakasi/'), ('.venv_cuda/Lib/site-packages/faster_whisper', 'faster_whisper/')], + datas=[('./fonts', 'fonts/'), ('.venv_cuda/Lib/site-packages/zeroconf', 'zeroconf/'), ('.venv_cuda/Lib/site-packages/openvr', 'openvr/'), ('.venv_cuda/Lib/site-packages/pykakasi', 'pykakasi/'), ('.venv_cuda/Lib/site-packages/faster_whisper', 'faster_whisper/'), ('.venv/Lib/site-packages/hf_xet', 'hf_xet/')], hiddenimports=[], hookspath=[], hooksconfig={}, diff --git a/build.bat b/build.bat index 89ee8cbb..3c08629c 100644 --- a/build.bat +++ b/build.bat @@ -1,2 +1,2 @@ call .venv/Scripts/activate -pyinstaller backend.spec --distpath src-tauri/bin --clean --noconfirm \ No newline at end of file +pyinstaller backend.spec --distpath src-tauri/bin --clean --noconfirm --log-level ERROR \ No newline at end of file diff --git a/build_cuda.bat b/build_cuda.bat index 73c85676..308b9174 100644 --- a/build_cuda.bat +++ b/build_cuda.bat @@ -1,2 +1,2 @@ call .venv_cuda/Scripts/activate -pyinstaller backend_cuda.spec --distpath src-tauri/bin --clean --noconfirm \ No newline at end of file +pyinstaller backend_cuda.spec --distpath src-tauri/bin --clean --noconfirm --log-level ERROR \ No newline at end of file diff --git a/install.bat b/install.bat index 5a57e817..aeb3d9b5 100644 --- a/install.bat +++ b/install.bat @@ -1,10 +1,25 @@ -python -m venv .venv -python -m venv .venv_cuda +REM .venv exists +if exist .venv ( + rmdir /s /q .venv +) +REM make .venv +python -m venv .venv + +REM install packages for .venv call .venv/Scripts/activate python.exe -m pip install --upgrade pip pip install --no-cache-dir --force-reinstall -r requirements.txt +REM if .venv_cuda exists +if exist .venv_cuda ( + rmdir /s /q .venv_cuda +) + +REM make .venv_cuda +python -m venv .venv_cuda + +REM install packages for .venv_cuda call .venv_cuda/Scripts/activate python.exe -m pip install --upgrade pip pip install --no-cache-dir --force-reinstall -r requirements_cuda.txt \ No newline at end of file diff --git a/locales/en.yml b/locales/en.yml index 9c45be98..5138f697 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -24,6 +24,9 @@ common_error: invalid_value_speaker_phrase_timeout: "It cannot be set lower than '{{speaker_record_timeout_label}}' with a value of 0 or more." invalid_value_speaker_max_phrase: "You can set a number equal to or greater than 0." +common_warning: + unable_to_use_osc_query: "The functions below have been automatically disabled because receiving OSC data is not possible due to OSC IP Address settings." + main_page: translation: "Translation" transcription_send: "Voice2Chatbox" @@ -257,6 +260,12 @@ config_page: label: "Open Config File" switch_compute_device: label: "Switch VRCT To CPU/GPU Version" + enable_websocket: + label: "Enable WebSocket Server" + websocket_host: + label: "WebSocket Host" + websocket_port: + label: "WebSocket Port" plugin_notifications: downloading: Downloading the plugin. diff --git a/locales/ja.yml b/locales/ja.yml index 8241d324..a9a6901b 100644 --- a/locales/ja.yml +++ b/locales/ja.yml @@ -24,10 +24,13 @@ common_error: invalid_value_speaker_phrase_timeout: "0 以上で 「{{speaker_record_timeout_label}}」 より小さくすることはできません。" invalid_value_speaker_max_phrase: "0 以上の数値を設定できます。" +common_warning: + unable_to_use_osc_query: "OSC IP Address の設定によりOSCデータの受信ができないため、以下の機能が自動的に無効になっています。" + main_page: translation: "翻訳" - transcription_send: "音声認識 マイク" - transcription_receive: "音声認識 スピーカー" + transcription_send: "マイク入力" + transcription_receive: "聞き取り" foreground: "最前面固定" language_settings: "言語設定" @@ -131,7 +134,7 @@ config_page: ctranslate2_compute_device: label: "AI翻訳 {{ctranslate2}} の処理デバイス" deepl_auth_key: - label: "DeepL API 認証キー" + label: "DeepL APIキーの登録" desc: "使用の際は、メイン画面にある {{translator}} をDeepL_APIに変更してください。\n※対応していない言語もあります。" open_auth_key_webpage: "DeepLアカウントページを開く" save: "保存" @@ -257,7 +260,12 @@ config_page: label: "設定ファイルを開く" switch_compute_device: label: "VRCT CPU/GPUバージョンの切り替え" - + enable_websocket: + label: "WebSocketサーバーを有効にする" + websocket_host: + label: "WebSocket Host" + websocket_port: + label: "WebSocket Port" plugin_notifications: downloading: プラグインをダウンロード中。 diff --git a/requirements.txt b/requirements.txt index 93733326..244a9238 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,6 +15,10 @@ pydub==0.25.1 psutil==5.9.8 pykakasi==2.3.0 pycaw==20240210 +websockets==15.0.1 +huggingface_hub==0.32.2 +hf-xet==1.1.2 +setuptools==80.8.0 translators @ git+https://github.com/misyaguziya/translators@5.9.2.1 SpeechRecognition @ git+https://github.com/misyaguziya/custom_speech_recognition@3.10.4.1 tinyoscquery @ git+https://github.com/cyberkitsune/tinyoscquery@0.1.3 \ No newline at end of file diff --git a/requirements_cuda.txt b/requirements_cuda.txt index 898364b7..4dadf192 100644 --- a/requirements_cuda.txt +++ b/requirements_cuda.txt @@ -16,6 +16,10 @@ pydub==0.25.1 psutil==5.9.8 pykakasi==2.3.0 pycaw==20240210 +websockets==15.0.1 +huggingface_hub==0.32.2 +hf-xet==1.1.2 +setuptools==80.8.0 translators @ git+https://github.com/misyaguziya/translators@5.9.2.1 SpeechRecognition @ git+https://github.com/misyaguziya/custom_speech_recognition@3.10.4.1 tinyoscquery @ git+https://github.com/cyberkitsune/tinyoscquery@0.1.3 \ No newline at end of file diff --git a/src-python/config.py b/src-python/config.py index 2f8471e2..64c09440 100644 --- a/src-python/config.py +++ b/src-python/config.py @@ -954,9 +954,42 @@ class Config: self._NOTIFICATION_VRC_SFX = value self.saveConfig(inspect.currentframe().f_code.co_name, value) + @property + def WEBSOCKET_SERVER(self): + return self._WEBSOCKET_SERVER + + @WEBSOCKET_SERVER.setter + def WEBSOCKET_SERVER(self, value): + if isinstance(value, bool): + self._WEBSOCKET_SERVER = value + self.saveConfig(inspect.currentframe().f_code.co_name, value) + + @property + @json_serializable('WEBSOCKET_HOST') + def WEBSOCKET_HOST(self): + return self._WEBSOCKET_HOST + + @WEBSOCKET_HOST.setter + def WEBSOCKET_HOST(self, value): + if isinstance(value, str): + self._WEBSOCKET_HOST = value + self.saveConfig(inspect.currentframe().f_code.co_name, value) + + @property + @json_serializable('WEBSOCKET_PORT') + def WEBSOCKET_PORT(self): + return self._WEBSOCKET_PORT + + @WEBSOCKET_PORT.setter + def WEBSOCKET_PORT(self, value): + if isinstance(value, int): + self._WEBSOCKET_PORT = value + self.saveConfig(inspect.currentframe().f_code.co_name, value) + + def init_config(self): # Read Only - self._VERSION = "3.1.2" + self._VERSION = "3.2.0" if getattr(sys, 'frozen', False): self._PATH_LOCAL = os_path.dirname(sys.executable) else: @@ -1139,6 +1172,9 @@ class Config: self._LOGGER_FEATURE = False self._VRC_MIC_MUTE_SYNC = False self._NOTIFICATION_VRC_SFX = True + self._WEBSOCKET_SERVER = False + self._WEBSOCKET_HOST = "127.0.0.1" + self._WEBSOCKET_PORT = 2231 def load_config(self): if os_path.isfile(self.PATH_CONFIG) is not False: diff --git a/src-python/controller.py b/src-python/controller.py index 16c2f53f..89a07d3a 100644 --- a/src-python/controller.py +++ b/src-python/controller.py @@ -1,4 +1,4 @@ -from typing import Callable, Union, Any +from typing import Callable, Any from time import sleep from subprocess import Popen from threading import Thread @@ -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, isValidIpAddress +from utils import removeLog, printLog, errorLogging, isConnectedNetwork, isValidIpAddress, isAvailableWebSocketServer class Controller: def __init__(self) -> None: @@ -294,6 +294,19 @@ class Controller: "translation":translation, "transliteration":transliteration }) + + if model.checkWebSocketServerAlive() is True: + model.websocketSendMessage( + { + "type":"SENT", + "src_languages":config.SELECTED_YOUR_LANGUAGES[config.SELECTED_TAB_NO], + "dst_languages":config.SELECTED_TARGET_LANGUAGES[config.SELECTED_TAB_NO], + "message":message, + "translation":translation, + "transliteration":transliteration + } + ) + if config.LOGGER_FEATURE is True: if len(translation) > 0: translation = " (" + "/".join(translation) + ")" @@ -377,6 +390,19 @@ class Controller: "translation":translation, "transliteration":transliteration, }) + + if model.checkWebSocketServerAlive() is True: + model.websocketSendMessage( + { + "type":"RECEIVED", + "src_languages":config.SELECTED_TARGET_LANGUAGES[config.SELECTED_TAB_NO], + "dst_languages":config.SELECTED_YOUR_LANGUAGES[config.SELECTED_TAB_NO], + "message":message, + "translation":translation, + "transliteration":transliteration + } + ) + if config.LOGGER_FEATURE is True: if len(translation) > 0: translation = " (" + "/".join(translation) + ")" @@ -434,11 +460,23 @@ class Controller: overlay_image = model.createOverlayImageLargeLog("send", message, translation[0] if len(translation) > 0 else "") model.updateOverlayLargeLog(overlay_image) - # update textbox message log (Sent) + if model.checkWebSocketServerAlive() is True: + model.websocketSendMessage( + { + "type":"CHAT", + "src_languages":config.SELECTED_YOUR_LANGUAGES[config.SELECTED_TAB_NO], + "dst_languages":config.SELECTED_TARGET_LANGUAGES[config.SELECTED_TAB_NO], + "message":message, + "translation":translation, + "transliteration":transliteration + } + ) + + # update textbox message log (Chat) if config.LOGGER_FEATURE is True: if len(translation) > 0: translation_text = " (" + "/".join(translation) + ")" - model.logger.info(f"[SENT] {message}{translation_text}") + model.logger.info(f"[CHAT] {message}{translation_text}") return {"status":200, "result":{ @@ -1099,8 +1137,7 @@ class Controller: def getOscIpAddress(*args, **kwargs) -> dict: return {"status":200, "result":config.OSC_IP_ADDRESS} - @staticmethod - def setOscIpAddress(data, *args, **kwargs) -> dict: + def setOscIpAddress(self, data, *args, **kwargs) -> dict: if isValidIpAddress(data) is False: response = { "status":400, @@ -1113,6 +1150,11 @@ class Controller: try: model.setOscIpAddress(data) config.OSC_IP_ADDRESS = data + if model.getIsOscQueryEnabled() is True: + self.enableOscQuery() + else: + self.setDisableVrcMicMuteSync() + self.disableOscQuery() response = {"status":200, "result":config.OSC_IP_ADDRESS} except Exception: model.setOscIpAddress(config.OSC_IP_ADDRESS) @@ -1388,10 +1430,20 @@ class Controller: @staticmethod def setEnableVrcMicMuteSync(*args, **kwargs) -> dict: - config.VRC_MIC_MUTE_SYNC = True - model.setMuteSelfStatus() - model.changeMicTranscriptStatus() - return {"status":200, "result":config.VRC_MIC_MUTE_SYNC} + if model.getIsOscQueryEnabled() is True: + config.VRC_MIC_MUTE_SYNC = True + model.setMuteSelfStatus() + model.changeMicTranscriptStatus() + response = {"status":200, "result":config.VRC_MIC_MUTE_SYNC} + else: + response = { + "status":400, + "result":{ + "message":"Cannot enable VRC mic mute sync while OSC query is disabled", + "data": config.VRC_MIC_MUTE_SYNC + } + } + return response @staticmethod def setDisableVrcMicMuteSync(*args, **kwargs) -> dict: @@ -1778,9 +1830,117 @@ class Controller: model.stopWatchdog() return {"status":200, "result":True} + @staticmethod + def getWebSocketHost(*args, **kwargs) -> dict: + return {"status":200, "result":config.WEBSOCKET_HOST} + + @staticmethod + def setWebSocketHost(data, *args, **kwargs) -> dict: + if isValidIpAddress(data) is False: + response = { + "status":400, + "result":{ + "message":"Invalid IP address", + "data": config.WEBSOCKET_HOST + } + } + else: + if model.checkWebSocketServerAlive() is False: + config.WEBSOCKET_HOST = data + response = {"status":200, "result":config.WEBSOCKET_HOST} + else: + if data == config.WEBSOCKET_HOST: + response = {"status":200, "result":config.WEBSOCKET_HOST} + elif isAvailableWebSocketServer(data, config.WEBSOCKET_PORT): + model.stopWebSocketServer() + model.startWebSocketServer(data, config.WEBSOCKET_PORT) + config.WEBSOCKET_HOST = data + response = {"status":200, "result":config.WEBSOCKET_HOST} + else: + response = { + "status":400, + "result":{ + "message":"WebSocket server host is not available", + "data": config.WEBSOCKET_HOST + } + } + + return response + + @staticmethod + def getWebSocketPort(*args, **kwargs) -> dict: + return {"status":200, "result":config.WEBSOCKET_PORT} + + @staticmethod + def setWebSocketPort(data, *args, **kwargs) -> dict: + if model.checkWebSocketServerAlive() is False: + config.WEBSOCKET_PORT = int(data) + response = {"status":200, "result":config.WEBSOCKET_PORT} + else: + if int(data) == config.WEBSOCKET_PORT: + return {"status":200, "result":config.WEBSOCKET_PORT} + elif isAvailableWebSocketServer(config.WEBSOCKET_HOST, int(data)) is True: + model.stopWebSocketServer() + model.startWebSocketServer(config.WEBSOCKET_HOST, int(data)) + config.WEBSOCKET_PORT = int(data) + response = {"status":200, "result":config.WEBSOCKET_PORT} + else: + response = { + "status":400, + "result":{ + "message":"WebSocket server port is not available", + "data": config.WEBSOCKET_PORT + } + } + return response + + @staticmethod + def getWebSocketServer(*args, **kwargs) -> dict: + return {"status":200, "result":config.WEBSOCKET_SERVER} + + @staticmethod + def setEnableWebSocketServer(*args, **kwargs) -> dict: + if isAvailableWebSocketServer(config.WEBSOCKET_HOST, config.WEBSOCKET_PORT) is True: + model.startWebSocketServer(config.WEBSOCKET_HOST, config.WEBSOCKET_PORT) + config.WEBSOCKET_SERVER = True + response = {"status":200, "result":config.WEBSOCKET_SERVER} + else: + response = { + "status":400, + "result":{ + "message":"WebSocket server host or port is not available", + "data": config.WEBSOCKET_SERVER + } + } + return response + + @staticmethod + def setDisableWebSocketServer(*args, **kwargs) -> dict: + config.WEBSOCKET_SERVER = False + model.stopWebSocketServer() + return {"status":200, "result":config.WEBSOCKET_SERVER} + def initializationProgress(self, progress): self.run(200, self.run_mapping["initialization_progress"], progress) + def enableOscQuery(self): + self.run( + 200, + self.run_mapping["enable_osc_query"], + { + "data": True, + "disabled_functions": [] + } + ) + + def disableOscQuery(self): + self.run(200, self.run_mapping["enable_osc_query"], { + "data": False, + "disabled_functions": [ + "vrc_mic_mute_sync", + ] + }) + def init(self, *args, **kwargs) -> None: removeLog() printLog("Start Initialization") @@ -1892,6 +2052,14 @@ class Controller: # init OSC receive printLog("Init OSC Receive") model.startReceiveOSC() + osc_query_enabled = model.getIsOscQueryEnabled() + if osc_query_enabled is True: + self.enableOscQuery() + else: + # OSC Query is disabled, so disable VRC some features + config.VRC_MIC_MUTE_SYNC = False + self.disableOscQuery() + if config.VRC_MIC_MUTE_SYNC is True: self.setEnableVrcMicMuteSync() @@ -1911,6 +2079,15 @@ class Controller: if (config.OVERLAY_SMALL_LOG is True or config.OVERLAY_LARGE_LOG is True): model.startOverlay() + printLog("Init WebSocket Server") + if config.WEBSOCKET_SERVER is True: + if isAvailableWebSocketServer(config.WEBSOCKET_HOST, config.WEBSOCKET_PORT) is True: + model.startWebSocketServer(config.WEBSOCKET_HOST, config.WEBSOCKET_PORT) + else: + config.WEBSOCKET_SERVER = False + model.stopWebSocketServer() + printLog("WebSocket server host or port is not available") + printLog("Update settings") self.updateConfigSettings() diff --git a/src-python/mainloop.py b/src-python/mainloop.py index 5c5fb744..00bc8cb5 100644 --- a/src-python/mainloop.py +++ b/src-python/mainloop.py @@ -4,8 +4,11 @@ import time from typing import Any from threading import Thread from queue import Queue -from controller import Controller -from utils import printLog, printResponse, errorLogging, encodeBase64 +import logging +from controller import Controller # noqa: E402 +from utils import printLog, printResponse, errorLogging, encodeBase64 # noqa: E402 + +logging.getLogger("huggingface_hub").setLevel(logging.ERROR) run_mapping = { "connected_network":"/run/connected_network", @@ -42,6 +45,8 @@ run_mapping = { "initialization_progress":"/run/initialization_progress", "initialization_complete":"/run/initialization_complete", + + "enable_osc_query":"/run/enable_osc_query", } def run(status:int, endpoint:str, result:Any) -> None: @@ -291,6 +296,15 @@ mapping = { "/set/enable/send_received_message_to_vrc": {"status": True, "variable":controller.setEnableSendReceivedMessageToVrc}, "/set/disable/send_received_message_to_vrc": {"status": True, "variable":controller.setDisableSendReceivedMessageToVrc}, + # WebSocket Settings + "/get/data/websocket_host": {"status": True, "variable":controller.getWebSocketHost}, + "/set/data/websocket_host": {"status": True, "variable":controller.setWebSocketHost}, + "/get/data/websocket_port": {"status": True, "variable":controller.getWebSocketPort}, + "/set/data/websocket_port": {"status": True, "variable":controller.setWebSocketPort}, + "/get/data/websocket_server": {"status": True, "variable":controller.getWebSocketServer}, + "/set/enable/websocket_server": {"status": True, "variable":controller.setEnableWebSocketServer}, + "/set/disable/websocket_server": {"status": True, "variable":controller.setDisableWebSocketServer}, + # Advanced Settings "/get/data/osc_ip_address": {"status": True, "variable":controller.getOscIpAddress}, "/set/data/osc_ip_address": {"status": True, "variable":controller.setOscIpAddress}, @@ -367,7 +381,7 @@ class Main: if status == 423: self.queue.put((endpoint, data)) else: - printLog(endpoint, {"send_data":result}) + printLog(endpoint, {"status": status, "send_data": result}) printResponse(status, endpoint, result) time.sleep(0.1) diff --git a/src-python/model.py b/src-python/model.py index 8f62aa5e..3b4e51b6 100644 --- a/src-python/model.py +++ b/src-python/model.py @@ -1,5 +1,7 @@ import copy import gc +import asyncio +import json from subprocess import Popen from os import makedirs as os_makedirs from os import path as os_path @@ -29,6 +31,7 @@ from models.transcription.transcription_whisper import checkWhisperWeight, downl from models.overlay.overlay import Overlay from models.overlay.overlay_image import OverlayImage from models.watchdog.watchdog import Watchdog +from models.websocket.websocket_server import WebSocketServer from utils import errorLogging, setupLogger class threadFnc(Thread): @@ -99,6 +102,10 @@ class Model: self.kks = kakasi() self.watchdog = Watchdog(config.WATCHDOG_TIMEOUT, config.WATCHDOG_INTERVAL) self.osc_handler = OSCHandler(config.OSC_IP_ADDRESS, config.OSC_PORT) + self.websocket_server = None + self.websocket_server_loop = False + self.websocket_server_alive = False + self.th_websocket_server = None def checkTranslatorCTranslate2ModelWeight(self, weight_type:str): return checkCTranslate2Weight(config.PATH_LOCAL, weight_type) @@ -292,11 +299,8 @@ class Model: def oscSendMessage(self, message:str): self.osc_handler.sendMessage(message=message, notification=config.NOTIFICATION_VRC_SFX) - def getMuteSelfStatus(self): - return self.osc_handler.getOSCParameterMuteSelf() - def setMuteSelfStatus(self): - self.mic_mute_status = self.getMuteSelfStatus() + self.mic_mute_status = self.osc_handler.getOSCParameterMuteSelf() def startReceiveOSC(self): def changeHandlerMute(address, osc_arguments): @@ -311,11 +315,15 @@ class Model: dict_filter_and_target = { self.osc_handler.osc_parameter_muteself: changeHandlerMute, } - self.osc_handler.receiveOscParameters(dict_filter_and_target) + self.osc_handler.setDictFilterAndTarget(dict_filter_and_target) + self.osc_handler.receiveOscParameters() def stopReceiveOSC(self): self.osc_handler.oscServerStop() + def getIsOscQueryEnabled(self): + return self.osc_handler.getIsOscQueryEnabled() + @staticmethod def checkSoftwareUpdated(): # check update @@ -503,12 +511,16 @@ class Model: def changeMicTranscriptStatus(self): if config.VRC_MIC_MUTE_SYNC is True: - if self.mic_mute_status is True: - self.pauseMicTranscript() - elif self.mic_mute_status is False: - self.resumeMicTranscript() - else: - pass + match self.mic_mute_status: + case True: + self.pauseMicTranscript() + case False: + self.resumeMicTranscript() + case None: + # mute selfの状態が不明な場合は一時停止しない + self.resumeMicTranscript() + case _: + pass else: self.resumeMicTranscript() @@ -827,4 +839,86 @@ class Model: self.th_watchdog.join() self.th_watchdog = None + def message_handler(websocket, message): + """WebSocketメッセージ受信時の処理""" + pass + + def startWebSocketServer(self, host, port): + """WebSocketサーバーを起動し、別スレッドで実行する""" + if self.websocket_server_alive is True: + # サーバーが既に起動している場合は何もしない + return + + self.websocket_server_loop = True + self.websocket_server_alive = False # 初期状態を明示 + + async def WebSocketServerMain(): + try: + self.websocket_server = WebSocketServer( + host=host, + port=port, + ) + self.websocket_server.set_message_handler(self.message_handler) + self.websocket_server.start() + self.websocket_server_alive = True + + # イベントループが終了するまで待機 + while self.websocket_server_loop: + # self.websocket_server.send("Server is running...") + await asyncio.sleep(0.5) # 応答性向上のため間隔短縮 + + except Exception: + errorLogging() + # 具体的なエラー内容をログに残す場合 + # self.logger.error(f"WebSocket server error: {str(e)}") + finally: + # 確実にサーバーを停止 + if hasattr(self, 'websocket_server') and self.websocket_server: + self.websocket_server.stop() + self.websocket_server_alive = False + + self.th_websocket_server = Thread(target=lambda: asyncio.run(WebSocketServerMain())) + self.th_websocket_server.daemon = True + self.th_websocket_server.start() + + def stopWebSocketServer(self): + """WebSocketサーバーを停止する""" + if not hasattr(self, 'th_websocket_server') or self.th_websocket_server is None: + return + + self.websocket_server_loop = False + + try: + # 一定時間待機してからタイムアウト + self.th_websocket_server.join(timeout=2.0) + + if self.th_websocket_server.is_alive(): + # タイムアウト後もスレッドが生きている場合の処理 + self.logger.warning("WebSocket server thread did not terminate properly") + except Exception: + errorLogging() + finally: + self.th_websocket_server = None + self.websocket_server = None + self.websocket_server_alive = False + + def checkWebSocketServerAlive(self): + """WebSocketサーバーの稼働状態を確認する""" + return self.websocket_server_alive + + def websocketSendMessage(self, message_dict:dict): + """ + WebSocketサーバーから全クライアントにメッセージを送信する + :param message_dict: 送信するメッセージの辞書 + :return: 送信成功したかどうか + """ + if not self.websocket_server_alive or not self.websocket_server: + return False + try: + message_json = json.dumps(message_dict) + return self.websocket_server.send(message_json) + except Exception: + errorLogging() + return False + model = Model() \ No newline at end of file diff --git a/src-python/models/osc/osc.py b/src-python/models/osc/osc.py index 758918af..ef4e1152 100644 --- a/src-python/models/osc/osc.py +++ b/src-python/models/osc/osc.py @@ -7,10 +7,22 @@ from tinyoscquery.queryservice import OSCQueryService from tinyoscquery.query import OSCQueryBrowser, OSCQueryClient from tinyoscquery.utility import get_open_udp_port, get_open_tcp_port from tinyoscquery.shared.node import OSCAccess -from utils import errorLogging + +try: + from utils import errorLogging +except ImportError: + def errorLogging(): + import traceback + print("Error occurred:", traceback.format_exc()) class OSCHandler: def __init__(self, ip_address="127.0.0.1", port=9000) -> None: + + if ip_address in ["127.0.0.1", "localhost"]: + self.is_osc_query_enabled = True + else: + self.is_osc_query_enabled = False + self.osc_ip_address = ip_address self.osc_port = port self.osc_parameter_muteself = "/avatar/parameters/MuteSelf" @@ -24,15 +36,28 @@ class OSCHandler: self.osc_server_ip_address = ip_address self.http_port = None self.osc_server_port = None + self.dict_filter_and_target = {} self.browser = None + def getIsOscQueryEnabled(self) -> bool: + return self.is_osc_query_enabled + def setOscIpAddress(self, ip_address:str) -> None: + if ip_address in ["127.0.0.1", "localhost"]: + self.is_osc_query_enabled = True + else: + self.is_osc_query_enabled = False + + self.oscServerStop() self.osc_ip_address = ip_address self.udp_client = udp_client.SimpleUDPClient(self.osc_ip_address, self.osc_port) + self.receiveOscParameters() def setOscPort(self, port:int) -> None: + self.oscServerStop() self.osc_port = port self.udp_client = udp_client.SimpleUDPClient(self.osc_ip_address, self.osc_port) + self.receiveOscParameters() # send OSC message typing def sendTyping(self, flag:bool=False) -> None: @@ -44,6 +69,10 @@ class OSCHandler: self.udp_client.send_message(self.osc_parameter_chatbox_input, [f"{message}", True, notification]) def getOSCParameterValue(self, address:str) -> Any: + if not self.is_osc_query_enabled: + # OSCQueryが無効な場合はNoneを返す + return None + value = None try: # browserインスタンスを再利用し、毎回の生成と破棄を避ける @@ -71,19 +100,26 @@ class OSCHandler: def getOSCParameterMuteSelf(self) -> bool: return self.getOSCParameterValue(self.osc_parameter_muteself) - def receiveOscParameters(self, dict_filter_and_target:dict) -> None: + def setDictFilterAndTarget(self, dict_filter_and_target:dict) -> None: + self.dict_filter_and_target = dict_filter_and_target + + def receiveOscParameters(self) -> None: + if self.is_osc_query_enabled is False: + # OSCQueryが無効な場合は何もしない + return + self.osc_server_port = get_open_udp_port() self.http_port = get_open_tcp_port() osc_dispatcher = dispatcher.Dispatcher() - for filter, target in dict_filter_and_target.items(): + for filter, target in self.dict_filter_and_target.items(): osc_dispatcher.map(filter, target) - self.osc_server = osc_server.ThreadingOSCUDPServer((self.osc_server_ip_address, self.osc_server_port), osc_dispatcher, asyncio.get_event_loop()) + self.osc_server = osc_server.ThreadingOSCUDPServer((self.osc_server_ip_address, self.osc_server_port), osc_dispatcher) Thread(target=self.oscServerServe, daemon=True).start() while True: try: self.osc_query_service = OSCQueryService(self.osc_query_service_name, self.http_port, self.osc_server_port) - for filter, target in dict_filter_and_target.items(): + for filter, target in self.dict_filter_and_target.items(): self.osc_query_service.advertise_endpoint(filter, access=OSCAccess.READWRITE_VALUE) break except Exception: @@ -112,12 +148,26 @@ class OSCHandler: if __name__ == "__main__": handler = OSCHandler() - handler.receiveOscParameters({ - "/avatar/parameters/MuteSelf": print, + handler.setDictFilterAndTarget({ + "/avatar/parameters/MuteSelf": lambda address, *args: print(f"Received {address} with args {args}"), + "/chatbox/typing": lambda address, *args: print(f"Received {address} with args {args}"), + "/chatbox/input": lambda address, *args: print(f"Received {address} with args {args}"), }) + handler.receiveOscParameters() sleep(5) handler.sendTyping(True) sleep(1) - handler.sendMessage(message="Hello World", notification=True) - sleep(60) + handler.sendMessage(message="Hello World 1", notification=True) + sleep(10) + + print("IP address changed to 192.168.193.2") + handler.setOscIpAddress("192.168.193.2") + sleep(5) + handler.sendMessage(message="Hello World 2", notification=True) + + print("IP address changed to 127.0.0.1") + handler.setOscIpAddress("127.0.0.1") + sleep(5) + handler.sendMessage(message="Hello World 3", notification=True) + sleep(10) handler.oscServerStop() \ No newline at end of file diff --git a/src-python/models/websocket/__init__.py b/src-python/models/websocket/__init__.py new file mode 100644 index 00000000..a01d3ba6 --- /dev/null +++ b/src-python/models/websocket/__init__.py @@ -0,0 +1,4 @@ +# WebSocketサーバーモジュール +from .websocket_server import WebSocketServer + +__all__ = ["WebSocketServer"] diff --git a/src-python/models/websocket/websocket_server.py b/src-python/models/websocket/websocket_server.py new file mode 100644 index 00000000..34762e55 --- /dev/null +++ b/src-python/models/websocket/websocket_server.py @@ -0,0 +1,221 @@ +import asyncio +import threading +import websockets +from websockets.legacy.server import WebSocketServerProtocol +from typing import Callable, Set, Optional + +class WebSocketServer: + """ + WebSocketサーバーを管理するクラス。 + 主な機能: + - サーバーの起動・停止 + - クライアント接続管理 (接続/切断の追跡) + - メッセージ受信のコールバック処理 + - メッセージのブロードキャスト機能 + - GUIスレッド等からメッセージ送信するためのキュー + """ + def __init__(self, host: str='127.0.0.1', port: int=8765): + """ + サーバーのホスト名とポートを指定して初期化します。 + """ + self.host = host + self.port = port + self.clients: Set[WebSocketServerProtocol] = set() # 接続クライアント集合 + self._message_handler: Optional[Callable[['WebSocketServer', WebSocketServerProtocol, str], None]] = None + self._loop: Optional[asyncio.AbstractEventLoop] = None + self._server: Optional[websockets.serve] = None + self._thread: Optional[threading.Thread] = None + self._send_queue: Optional[asyncio.Queue] = None # 外部スレッド向け非同期キュー + self.is_running: bool = False # サーバーの起動状態を示すフラグ + + def set_message_handler(self, handler: Callable[['WebSocketServer', WebSocketServerProtocol, str], None]): + """ + クライアントからメッセージ受信時に呼び出すコールバックを設定します。 + コールバックのシグネチャ: (server, websocket, message) -> None + """ + self._message_handler = handler + + async def _handler(self, websocket): + """ + 単一クライアントとのセッションを処理するハンドラです。 + 新規接続時にクライアントを集合に追加し、メッセージを受信してコールバックを呼び出します。 + 切断時には集合からクライアントを削除します。 + """ + # 接続クライアントを集合に追加 + self.clients.add(websocket) + try: + async for message in websocket: + # メッセージ受信時にコールバック呼び出し + if self._message_handler: + self._message_handler(self, websocket, message) + except websockets.exceptions.ConnectionClosed: + # クライアントが切断した場合 + pass + finally: + # 切断時に集合から削除 + self.clients.remove(websocket) + + async def _broadcast_async(self, message: str): + """ + すべての接続クライアントにメッセージを送信する非同期メソッド。 + """ + if not self.clients: + return + # 全クライアントへ並列に送信 + await asyncio.gather( + *[client.send(message) for client in self.clients], + return_exceptions=True + ) + + async def _send_loop(self): + """ + 内部キューからメッセージを取り出し、すべてのクライアントに送信するループ処理。 + GUIなど他スレッドから送信メッセージをキューに入れてもらい、このコルーチンで配信します。 + """ + assert self._send_queue is not None + while True: + message = await self._send_queue.get() + if message is None: + # Noneを受け取ったらシャットダウン指示とみなしてループを抜ける + break + await self._broadcast_async(message) + + def send(self, message: str): + """ + 外部スレッドからサーバーにメッセージを送信するためのメソッドです。 + イベントループ上で安全にキューにメッセージを積み、_send_loop()経由でブロードキャストします。 + """ + if self._loop and self._send_queue: + # キューにput_nowaitするコールをイベントループにスケジュール + self._loop.call_soon_threadsafe(self._send_queue.put_nowait, message) + + def broadcast(self, message: str): + """ + 外部スレッドや他コルーチンから全クライアントにメッセージを送信するユーティリティ。 + asyncio.run_coroutine_threadsafe を使ってループ上でブロードキャストを実行します。 + """ + if self._loop: + # コルーチン自体をrun_coroutine_threadsafeに渡す + asyncio.run_coroutine_threadsafe( + self._broadcast_async(message), self._loop + ) + + def start(self): + """ + サーバーを起動します。新しいスレッド上で asyncio イベントループを動かし、serve()を実行します。 + """ + if self._thread and self._thread.is_alive(): + return # 既に起動中 + # 新しいスレッドでイベントループを開始 + self._thread = threading.Thread(target=self._run_loop, daemon=True) + self._thread.start() + + def _run_loop(self): + """ + 別スレッド上で実行されるイベントループ用のメソッド。 + サーバーの起動と、送信用キューのタスク登録を行います。 + """ + # 新しいイベントループを作成してこのスレッドの現在のループとして設定 + self._loop = asyncio.new_event_loop() + asyncio.set_event_loop(self._loop) + + async def setup_server(): + # サーバーを起動し、listenを開始 + self._server = await websockets.serve(self._handler, self.host, self.port) + # 送信キューを初期化 + self._send_queue = asyncio.Queue() + # 送信ループタスクを開始 + self._loop.create_task(self._send_loop()) + # サーバーの起動を待機 + # 設定関数を実行してサーバーを起動 + self._loop.run_until_complete(setup_server()) + self.is_running = True + # サーバーが起動したら、接続待機を開始 + # print(f"WebSocket server started on ws://{self.host}:{self.port}") + try: + # サーバーが停止するまでループを継続 + self._loop.run_forever() + finally: + # 停止指示が出たらすべての接続を閉じ、イベントループを終了 + self._loop.run_until_complete(self._shutdown()) + self._loop.close() + + async def _shutdown(self): + """ + サーバーとクライアントを安全にシャットダウンする非同期処理。 + serveオブジェクトをcloseし、wait_closed()で完全に終了を待ちます。 + さらに接続中の各WebSocketをcloseします。 + """ + # サーバーのListenを停止 + if self._server: + self._server.close() + await self._server.wait_closed() + # 接続中クライアントを順次クローズ + for ws in list(self.clients): + try: + await ws.close() + except Exception: + pass + + def stop(self): + """ + サーバーを停止します。別スレッドで動作中のイベントループに停止を指示し、スレッドを終了させます。 + """ + self.is_running = False + if self._loop: + # サーバーのlistenを停止し、ループ停止をスケジュール + self._loop.call_soon_threadsafe(self._server.close) + # None をキューに入れて_send_loopを抜けさせる + self._loop.call_soon_threadsafe(self._send_queue.put_nowait, None) + # ループ停止 + self._loop.call_soon_threadsafe(self._loop.stop) + # スレッドの終了を待つ + if self._thread: + self._thread.join() + +if __name__ == "__main__": + # テスト用の簡単なメッセージハンドラ + def message_handler(server: WebSocketServer, websocket: WebSocketServerProtocol, message: str): + print(f"Received message from {websocket.remote_address}: {message}") + server.send(f"Echo: {message}") + + def send_message(server: WebSocketServer, message: str): + server.send(message) + + # メイン処理を非同期関数に変更 + async def main(): + # サーバーを起動してメッセージハンドラを設定 + ws_server = WebSocketServer() + ws_server.set_message_handler(message_handler) + ws_server.start() + print("WebSocket server started.") + # 定期的にサーバーからメッセージを送信する例 + import threading + import time + def periodic_send(): + print("Starting periodic message sender...") + while ws_server.is_running: + time.sleep(5) + print("Sending periodic message...") + send_message(ws_server, "Periodic message") + print("Periodic message sender stopped.") + # 別スレッドで定期的にメッセージを送信 + time.sleep(5) + send_thread = threading.Thread(target=periodic_send, daemon=True) + send_thread.start() + # メインスレッドでサーバーを動かし続ける + try: + while True: + # 非同期スリープで待機 + await asyncio.sleep(1) + + except KeyboardInterrupt: + # Ctrl+Cでサーバーを停止 + print("Stopping WebSocket server...") + ws_server.stop() + + # 非同期メイン関数を実行 + try: + asyncio.run(main()) + except KeyboardInterrupt: + print("Stopping WebSocket server...") \ No newline at end of file diff --git a/src-python/utils.py b/src-python/utils.py index de538dae..7bc0997b 100644 --- a/src-python/utils.py +++ b/src-python/utils.py @@ -8,6 +8,7 @@ from logging.handlers import RotatingFileHandler from ctranslate2 import get_supported_compute_types import requests import ipaddress +import socket def isConnectedNetwork(url="http://www.google.com", timeout=3) -> bool: try: @@ -16,6 +17,25 @@ def isConnectedNetwork(url="http://www.google.com", timeout=3) -> bool: except requests.RequestException: return False +def isAvailableWebSocketServer(host:str, port:int) -> bool: + """WebSocketサーバーのポートが使用中かどうかを確認する""" + response = True + try: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as chk: + try: + # SO_REUSEADDRを設定してソケットの再利用を許可 + chk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + chk.bind((host, port)) + # シャットダウン前にリッスン状態にする必要はない + chk.close() + except Exception: + response = False + except Exception: + errorLogging() + response = False + + return response + def isValidIpAddress(ip_address: str) -> bool: try: ipaddress.ip_address(ip_address) diff --git a/src-ui/app/_index_css/variables.css b/src-ui/app/_index_css/variables.css index 6a3c48e4..ca2beca1 100644 --- a/src-ui/app/_index_css/variables.css +++ b/src-ui/app/_index_css/variables.css @@ -25,6 +25,7 @@ --error_bc_active_color: #9c3938; --success_bc_color: #368777; --waring_color: #cb944f; + --waring_bc_color: #cf7b1b; --dark_basic_text_color: #f2f2f2; --dark_100_color: #f5f7fb; diff --git a/src-ui/app/config_page/setting_section/setting_box/_components/slider/Slider.jsx b/src-ui/app/config_page/setting_section/setting_box/_components/slider/Slider.jsx index 8f61d725..827c0253 100644 --- a/src-ui/app/config_page/setting_section/setting_box/_components/slider/Slider.jsx +++ b/src-ui/app/config_page/setting_section/setting_box/_components/slider/Slider.jsx @@ -4,44 +4,107 @@ import MUI_Slider from "@mui/material/Slider"; import clsx from "clsx"; export const Slider = (props) => { + const location = props.valueLabelDisplayLocation || "top"; + + const sliderSx = { + color: "var(--dark_700_color)", + "& .MuiSlider-thumb": { + backgroundColor: "var(--primary_600_color)", + "&:hover, &.Mui-focusVisible, &.Mui-active": { + boxShadow: `0 0 0 0.8rem var(--primary_600_color_44)`, + }, + "& .MuiSlider-valueLabel": { + position: "absolute", + backgroundColor: "var(--dark_800_color)", + width: "fit-content", + minWidth: "4.8rem", + padding: "0.4rem 0.8rem", + lineHeight: "1.15", + "& .MuiSlider-valueLabelLabel": { + fontSize: "1.4rem", + }, + ...(location === "top" && { + top: "-110%", + left: "50%", + transform: "translate(-50%, -50%) scale(0)", + transformOrigin: "bottom center", + "&.MuiSlider-valueLabelOpen": { + transform: "translate(-50%, -50%) scale(1)", + }, + "&::before": { + bottom: "0%", + left: "50%", + }, + }), + ...(location === "right" && { + top: "50%", + left: "150%", + transform: "translate(0, -50%) scale(0)", + transformOrigin: "left center", + "&.MuiSlider-valueLabelOpen": { + transform: "translate(0, -50%) scale(1)", + }, + "&::before": { + bottom: "50%", + left: "0", + }, + }), + ...(location === "left" && { + // top: "50%", + // right: "50%", + // transform: "translate(-50%, -50%) scale(0)", + // transformOrigin: "bottom center", + // "&.MuiSlider-valueLabelOpen": { + // transform: "translate(-50%, -50%) scale(1)", + // }, + // "&::before": { + // bottom: "50%", + // left: "100%", + // }, + }), + }, + }, + "& .MuiSlider-markLabel": { + fontSize: "1.4rem", + color: "var(--dark_550_color)", + whiteSpace: "nowrap", + }, + "& .MuiSlider-markLabelActive": { + color: "var(--primary_300_color)", + }, + }; + return ( -
+
props.onchangeFunction(value)} - onChangeCommitted={(_e, value) => props.onchangeCommittedFunction ? props.onchangeCommittedFunction(value) : null} + onChangeCommitted={(_e, value) => + props.onchangeCommittedFunction ? props.onchangeCommittedFunction(value) : null + } + onMouseEnter={(event) => + props.onMouseEnterFunction ? props.onMouseEnterFunction(event) : null + } + onMouseLeave={(event) => + props.onMouseLeaveFunction ? props.onMouseLeaveFunction(event) : null + } marks={props.marks} track={props.track} orientation={props.orientation} valueLabelFormat={`${props.valueLabelFormat ? props.valueLabelFormat : props.variable}`} - sx={{ - color: "var(--dark_700_color)", - "& .MuiSlider-thumb": { - backgroundColor: "var(--primary_600_color)", - "&:hover, &.Mui-focusVisible, &.Mui-active": { - boxShadow: `0 0 0 0.8rem var(--primary_600_color_44)`, - }, - "& .MuiSlider-valueLabel": { - fontSize: "1.4rem", - backgroundColor: "var(--dark_800_color)", - padding: "0.6rem 1rem", - lineHeight: "1.15", - }, - }, - "& .MuiSlider-markLabel": { - fontSize: "1.4rem", - color: "var(--dark_550_color)", - whiteSpace: "nowrap", - }, - "& .MuiSlider-markLabelActive": { - color: "var(--primary_300_color)", - }, - }} + sx={sliderSx} />
); 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 7d96cda5..2f4c15cd 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 @@ -6,13 +6,19 @@ import { useOpenFolder } from "@logics_common"; import { useOscIpAddress, useOscPort, + useWebsocket, } from "@logics_configs"; import { + CheckboxContainer, ActionButtonContainer, EntryWithSaveButtonContainer, } from "../_templates/Templates"; +import { + SectionLabelComponent, +} from "../_components/"; + import OpenFolderSvg from "@images/open_folder.svg?react"; import HelpSvg from "@images/help.svg?react"; @@ -25,6 +31,7 @@ export const AdvancedSettings = () => {
+ ); }; @@ -87,6 +94,7 @@ const OscPortContainer = () => { /> ); }; + const OpenConfigFolderContainer = () => { const { t } = useTranslation(); const { openFolder_ConfigFile } = useOpenFolder(); @@ -120,4 +128,88 @@ const OpenSwitchComputeDeviceModalContainer = () => { /> ); +}; + + +const WebsocketContainer = () => { + return ( +
+ + + + +
+ ); +}; + +const EnableWebsocketContainer = () => { + const { t } = useTranslation(); + const { currentEnableWebsocket, toggleEnableWebsocket } = useWebsocket(); + + return ( + + ); +}; + +const WebsocketHostContainer = () => { + const { t } = useTranslation(); + const { currentWebsocketHost, setWebsocketHost } = useWebsocket(); + const [input_value, setInputValue] = useState(currentWebsocketHost.data); + + const onChangeFunction = (value) => { + setInputValue(value); + }; + + const saveFunction = () => { + setWebsocketHost(input_value); + }; + + useEffect(()=> { + setInputValue(currentWebsocketHost.data); + }, [currentWebsocketHost]); + + return ( + + ); +}; + +const WebsocketPortContainer = () => { + const { t } = useTranslation(); + const { currentWebsocketPort, setWebsocketPort } = useWebsocket(); + const [input_value, setInputValue] = useState(currentWebsocketPort.data); + + const onChangeFunction = (value) => { + value = value.replace(/[^0-9]/g, ""); + setInputValue(value); + }; + + const saveFunction = () => { + setWebsocketPort(input_value); + }; + + useEffect(()=> { + setInputValue(currentWebsocketPort.data); + }, [currentWebsocketPort]); + + return ( + + ); }; \ No newline at end of file diff --git a/src-ui/app/config_page/setting_section/setting_box/others/Others.jsx b/src-ui/app/config_page/setting_section/setting_box/others/Others.jsx index d8d7b818..b6c9c9b6 100644 --- a/src-ui/app/config_page/setting_section/setting_box/others/Others.jsx +++ b/src-ui/app/config_page/setting_section/setting_box/others/Others.jsx @@ -101,11 +101,17 @@ export const VrcMicMuteSyncContainer = () => { const { t } = useTranslation(); const { currentEnableVrcMicMuteSync, toggleEnableVrcMicMuteSync } = useEnableVrcMicMuteSync(); + const variable = { + state: currentEnableVrcMicMuteSync.state, + data: currentEnableVrcMicMuteSync.data.is_enabled, + }; + return ( ); diff --git a/src-ui/app/config_page/setting_section/setting_box/supporters/supporters_container/supporters_wrapper/SupportersWrapper.jsx b/src-ui/app/config_page/setting_section/setting_box/supporters/supporters_container/supporters_wrapper/SupportersWrapper.jsx index 73209c10..3cfde7f1 100644 --- a/src-ui/app/config_page/setting_section/setting_box/supporters/supporters_container/supporters_wrapper/SupportersWrapper.jsx +++ b/src-ui/app/config_page/setting_section/setting_box/supporters/supporters_container/supporters_wrapper/SupportersWrapper.jsx @@ -272,30 +272,32 @@ const SupporterPeriodContainer = ({ settings, calc_support_period }) => { return (
- {Object.entries(period_data).map(([key, item], index) => { - if (item === "") return null; - const period_box_class_name = clsx(styles.period_box, { - [styles.mogu_bar]: item === "mogu_2000", - [styles.mochi_bar]: item === "mochi_1000", - [styles.fuwa_bar]: item === "fuwa_500", - [styles.basic_bar]: item === "basic_300", - }); +
+ {Object.entries(period_data).map(([key, item], index) => { + if (item === "") return null; + const period_box_class_name = clsx(styles.period_box, { + [styles.mogu_bar]: item === "mogu_2000", + [styles.mochi_bar]: item === "mochi_1000", + [styles.fuwa_bar]: item === "fuwa_500", + [styles.basic_bar]: item === "basic_300", + }); - return ( - {key}

- } - placement="top" - slotProps={offset} - > -
-
-
-
- ); - })} + return ( + {key}

+ } + placement="top" + slotProps={offset} + > +
+
+
+
+ ); + })} +
); }; diff --git a/src-ui/app/config_page/setting_section/setting_box/supporters/supporters_container/supporters_wrapper/SupportersWrapper.module.scss b/src-ui/app/config_page/setting_section/setting_box/supporters/supporters_container/supporters_wrapper/SupportersWrapper.module.scss index c0c35d76..3d392642 100644 --- a/src-ui/app/config_page/setting_section/setting_box/supporters/supporters_container/supporters_wrapper/SupportersWrapper.module.scss +++ b/src-ui/app/config_page/setting_section/setting_box/supporters/supporters_container/supporters_wrapper/SupportersWrapper.module.scss @@ -13,7 +13,7 @@ align-content: start; flex-wrap: wrap; column-gap: 1.8rem; - row-gap: 0.4rem; + row-gap: 2rem; } .supporter_image_container { @@ -209,13 +209,24 @@ $progress_ease: cubic-bezier(0, 1, 0.75, 1); .supporter_period_container { + position: absolute; + top: 100%; + left: 0; +} +.supporter_period_wrapper { display: flex; - gap: 0.2rem; - padding-left: 0.4rem; + gap: 0.4rem 0.2rem; + flex-shrink: 0; + flex-wrap: wrap; + padding: 0.3rem 0.4rem 0.4rem 0.4rem; +} + +.period_box_wrapper { + flex-shrink: 0; } .period_box { - width: 1.8rem; + width: 1.7rem; height: 0.3rem; border-radius: 0.3rem; &.mogu_bar { @@ -231,9 +242,7 @@ $progress_ease: cubic-bezier(0, 1, 0.75, 1); background-color: var(--dark_800_color); } } -.period_box_wrapper { - padding: 0.3rem 0 0.4rem 0; -} + .tooltip_period_label { font-size: 1.4rem; } diff --git a/src-ui/app/config_page/setting_section/setting_box/vr/Vr.jsx b/src-ui/app/config_page/setting_section/setting_box/vr/Vr.jsx index 06920712..b145bb44 100644 --- a/src-ui/app/config_page/setting_section/setting_box/vr/Vr.jsx +++ b/src-ui/app/config_page/setting_section/setting_box/vr/Vr.jsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useRef } from "react"; import { useTranslation } from "react-i18next"; import clsx from "clsx"; import styles from "./Vr.module.scss"; @@ -25,6 +25,10 @@ import { import RedoSvg from "@images/redo.svg?react"; +import SquareSvg from "@images/square.svg?react"; +import TriangleSvg from "@images/triangle.svg?react"; +import { randomIntMinMax } from "@utils"; + export const Vr = () => { const { t } = useTranslation(); const [is_opened_small_settings, setIsOpenedSmallSettings] = useState(true); @@ -147,7 +151,7 @@ const OverlaySettingsContainer = ({ ) : ( )} - + { ); }; -const PositionControls = ({settings, onchangeFunction, selectFunction, ui_configs, default_ui_configs}) => { + +export const PositionControls = ({ settings, onchangeFunction, selectFunction, ui_configs, default_ui_configs }) => { const { t } = useTranslation(); + const { + variable_display: x_variable_display, + is_max: is_max_position_x, + is_min: is_min_position_x, + countUp: countUpPositionX, + countDown: countDownPositionX, + } = useVariableControl("x_pos", settings, onchangeFunction, ui_configs); + + const { + variable_display: y_variable_display, + is_max: is_max_position_y, + is_min: is_min_position_y, + countUp: countUpPositionY, + countDown: countDownPositionY, + } = useVariableControl("y_pos", settings, onchangeFunction, ui_configs); + + const { + variable_display: z_variable_display, + is_max: is_max_position_z, + is_min: is_min_position_z, + countUp: countUpPositionZ, + countDown: countDownPositionZ, + } = useVariableControl("z_pos", settings, onchangeFunction, ui_configs); + return (
@@ -205,8 +234,18 @@ const PositionControls = ({settings, onchangeFunction, selectFunction, ui_config min={ui_configs.x_pos.min} max={ui_configs.x_pos.max} onchangeFunction={(value) => onchangeFunction("x_pos", value)} + valueLabelDisplay={x_variable_display} + valueLabelDisplayLocation="top" + /> +
+

{t("config_page.vr.y_position")} @@ -221,8 +260,18 @@ const PositionControls = ({settings, onchangeFunction, selectFunction, ui_config max={ui_configs.y_pos.max} onchangeFunction={(value) => onchangeFunction("y_pos", value)} orientation="vertical" + valueLabelDisplay={y_variable_display} + valueLabelDisplayLocation="right" + /> +

+

{t("config_page.vr.z_position")} @@ -237,69 +286,162 @@ const PositionControls = ({settings, onchangeFunction, selectFunction, ui_config max={ui_configs.z_pos.max} onchangeFunction={(value) => onchangeFunction("z_pos", value)} orientation="vertical" + valueLabelDisplay={z_variable_display} + valueLabelDisplayLocation="left" + /> +

); }; -const RotationControls = ({settings, onchangeFunction, selectFunction, default_ui_configs}) => { +export const RotationControls = ({ settings, onchangeFunction, selectFunction, ui_configs, default_ui_configs }) => { const { t } = useTranslation(); + const { + variable_display: x_variable_display, + is_max: is_max_rotation_x, + is_min: is_min_rotation_x, + countUp: countUpRotationX, + countDown: countDownRotationX, + } = useVariableControl("x_rotation", settings, onchangeFunction, ui_configs); + + const { + variable_display: y_variable_display, + is_max: is_max_rotation_y, + is_min: is_min_rotation_y, + countUp: countUpRotationY, + countDown: countDownRotationY, + } = useVariableControl("y_rotation", settings, onchangeFunction, ui_configs); + + const { + variable_display: z_variable_display, + is_max: is_max_rotation_z, + is_min: is_min_rotation_z, + countUp: countUpRotationZ, + countDown: countDownRotationZ, + } = useVariableControl("z_rotation", settings, onchangeFunction, ui_configs); + return (

{t("config_page.vr.x_rotation")} - selectFunction("x_rotation", default_ui_configs.y_pos)} /> + selectFunction("x_rotation", default_ui_configs.x_rotation)} />

onchangeFunction("x_rotation", -value)} orientation="vertical" + valueLabelDisplay={x_variable_display} + valueLabelDisplayLocation="right" + /> +
+

{t("config_page.vr.y_rotation")} - selectFunction("y_rotation", default_ui_configs.y_pos)} /> + selectFunction("y_rotation", default_ui_configs.y_rotation)} />

onchangeFunction("y_rotation", value)} + valueLabelDisplay={y_variable_display} + valueLabelDisplayLocation="top" + /> +
+

{t("config_page.vr.z_rotation")} - selectFunction("z_rotation", default_ui_configs.y_pos)} /> + selectFunction("z_rotation", default_ui_configs.z_rotation)} />

onchangeFunction("z_rotation", value)} orientation="vertical" + valueLabelDisplay={z_variable_display} + valueLabelDisplayLocation="left" + /> +
); }; +const AdjustButtonContainer = ({ wrapper_class_name, is_max, is_min, countUp, countDown }) => { + return ( +
+
+ +
+
+ +
+
+ ); +}; + + const OtherControls = ({settings, onchangeFunction, ui_configs}) => { const { t } = useTranslation(); @@ -393,10 +535,6 @@ const ResetButton = ({onClickFunction}) => { ); }; -import SquareSvg from "@images/square.svg?react"; -import TriangleSvg from "@images/triangle.svg?react"; -import { randomIntMinMax } from "@utils"; - const SendSampleTextToggleButton = () => { const { t } = useTranslation(); const { sendTextToOverlay } = useSendTextToOverlay(); @@ -445,4 +583,58 @@ const SendSampleTextToggleButton = () => {

{label}

); +}; + + + +const useVariableControl = (key, settings, onchangeFunction, ui_configs) => { + const [variable_display, setVariableDisplay] = useState("auto"); + + const [is_max, setIsMax] = useState(settings[key] >= ui_configs[key].max); + const [is_min, setIsMin] = useState(settings[key] <= ui_configs[key].min); + + const timerRef = useRef(); + + useEffect(() => { + return () => { + clearTimeout(timerRef.current); + }; + }, []); + + const triggerDisplay = () => { + setVariableDisplay("on"); + clearTimeout(timerRef.current); + timerRef.current = setTimeout(() => { + setVariableDisplay("auto"); + }, 2000); + }; + + useEffect(() => { + setIsMax(settings[key] >= ui_configs[key].max); + setIsMin(settings[key] <= ui_configs[key].min); + }, [settings[key]]); + + const countUp = () => { + if (is_max) return; + const step = ui_configs[key].step; + const new_value = parseFloat((settings[key] + step).toFixed(2)); + onchangeFunction(key, new_value); + triggerDisplay(); + }; + + const countDown = () => { + if (is_min) return; + const step = ui_configs[key].step; + const new_value = parseFloat((settings[key] - step).toFixed(2)); + onchangeFunction(key, new_value); + triggerDisplay(); + }; + + return { + variable_display, + is_max, + is_min, + countUp, + countDown, + }; }; \ No newline at end of file diff --git a/src-ui/app/config_page/setting_section/setting_box/vr/Vr.module.scss b/src-ui/app/config_page/setting_section/setting_box/vr/Vr.module.scss index 35798530..9d0963e8 100644 --- a/src-ui/app/config_page/setting_section/setting_box/vr/Vr.module.scss +++ b/src-ui/app/config_page/setting_section/setting_box/vr/Vr.module.scss @@ -16,7 +16,7 @@ justify-content: center; width: 100%; max-width: 56rem; - gap: 2rem; + gap: 4rem; } .controller_type_switch { @@ -58,12 +58,13 @@ .sample_text_button_wrapper { position: absolute; - bottom: 0; - left: -74%; + bottom: -12%; + left: -80%; display: flex; justify-content: center; align-items: center; flex-direction: column; + // transform: translate(-50%, -50%); } .sample_text_button { background-color: var(--dark_850_color); @@ -121,20 +122,20 @@ } .x_position_label { position: absolute; - bottom: -4.6rem; + bottom: -5rem; right: -46%; justify-content: end; } .y_position_label { position: absolute; - top: -44%; - left: 10%; - justify-content: start; + bottom: 110%; + right: 119%; + justify-content: end; } .z_position_label { position: absolute; - top: 30%; - left: 80%; + top: 14%; + left: 110%; } .x_position_slider { @@ -155,14 +156,84 @@ .z_position_slider { position: absolute; - bottom: 61%; - left: 61%; + bottom: 80%; + left: 88%; transform: translate(50%,50%) rotate(45deg); width: 0%; height: 100%; } +%variable-button { + width: 3.8rem; + border-radius: 0.4rem; + aspect-ratio: 1.2 / 1; + background-color: var(--dark_850_color); + display: flex; + justify-content: center; + align-items: center; + font-size: 1.6rem; + cursor: pointer; + + &:hover { + background-color: var(--primary_500_color); + } + &:active { + background-color: var(--primary_600_color); + } + &.is_disabled { + pointer-events: none; + background-color: var(--dark_875_color); + & .adjust_button_triangle_svg { + color: var(--dark_800_color); + } + } +} + +@mixin variable-button-wrapper($vertical-pos, $vertical-value, $horizontal-pos, $horizontal-value, $rotate: 0deg) { + position: absolute; + #{$vertical-pos}: $vertical-value; + #{$horizontal-pos}: $horizontal-value; + display: flex; + gap: 1.6rem; + flex-direction: column; + transform: translate(-50%) rotate($rotate); +} + +.button_wrapper { + @extend %variable-button; + + &.up .adjust_button_triangle_svg { + transform: rotate(0deg); + } + &:not(.up) .adjust_button_triangle_svg { + transform: rotate(180deg); + } + &.is_disabled { + pointer-events: none; + color: var(--dark_875_color); + } +} + +.adjust_button_triangle_svg { + width: 1.8rem; + color: var(--dark_400_color); +} + + + +.y_position_button_wrapper { + @include variable-button-wrapper(top, 30%, left, -26%); +} + +.x_position_button_wrapper { + @include variable-button-wrapper(bottom, -38%, left, 46%, 90deg); +} + +.z_position_button_wrapper { + @include variable-button-wrapper(bottom, 26%, right, -4%, 45deg); +} + // .rotation_controls { @@ -175,19 +246,20 @@ .x_rotation_label { position: absolute; - top: -44%; - left: 10%; + bottom: 110%; + right: 119%; + justify-content: end; } .y_rotation_label { position: absolute; - bottom: -4.6rem; + bottom: -5rem; right: -46%; justify-content: end; } .z_rotation_label { position: absolute; - top: -10%; - right: -110%; + top: -20%; + right: -100%; } .x_rotation_slider { @@ -216,6 +288,21 @@ } + +.x_rotation_button_wrapper { + @include variable-button-wrapper(top, 30%, left, -26%); +} + +.y_rotation_button_wrapper { + @include variable-button-wrapper(bottom, -38%, left, 46%, 90deg); +} + +.z_rotation_button_wrapper { + @include variable-button-wrapper(bottom, 50%, right, -60%, -45deg); +} + + + .slider_reset_button { background-color: var(--dark_875_color); padding: 0.6rem; diff --git a/src-ui/app/main_page/main_section/top_bar/right_side_components/RightSideComponents.jsx b/src-ui/app/main_page/main_section/top_bar/right_side_components/RightSideComponents.jsx index 7bd3ae59..3f8be6e2 100644 --- a/src-ui/app/main_page/main_section/top_bar/right_side_components/RightSideComponents.jsx +++ b/src-ui/app/main_page/main_section/top_bar/right_side_components/RightSideComponents.jsx @@ -77,7 +77,7 @@ const OpenVrcMicMuteSyncQuickSetting = () => { return ( ); diff --git a/src-ui/app/modal_controller/update_modal/UpdateModal.jsx b/src-ui/app/modal_controller/update_modal/UpdateModal.jsx index c35a240c..dd8f0f43 100644 --- a/src-ui/app/modal_controller/update_modal/UpdateModal.jsx +++ b/src-ui/app/modal_controller/update_modal/UpdateModal.jsx @@ -59,7 +59,7 @@ export const UpdateModal = () => {
- + {!is_cpu_version ? : null}
@@ -85,7 +85,7 @@ const VersionDescComponent = (props) => { return (
-

{props.desc}

+

{`- ${props.desc}`}

); }; diff --git a/src-ui/app/snackbar_controller/SnackbarController.jsx b/src-ui/app/snackbar_controller/SnackbarController.jsx index 0b7c9609..e503e695 100644 --- a/src-ui/app/snackbar_controller/SnackbarController.jsx +++ b/src-ui/app/snackbar_controller/SnackbarController.jsx @@ -14,6 +14,7 @@ export const SnackbarController = () => { const snackbar_classname = clsx(styles.snackbar_content, { [styles.is_success]: currentNotificationStatus.data.status === "success", + [styles.is_warning]: currentNotificationStatus.data.status === "warning", [styles.is_error]: currentNotificationStatus.data.status === "error", }); diff --git a/src-ui/app/snackbar_controller/SnackbarController.module.scss b/src-ui/app/snackbar_controller/SnackbarController.module.scss index b0b96ce9..075e4419 100644 --- a/src-ui/app/snackbar_controller/SnackbarController.module.scss +++ b/src-ui/app/snackbar_controller/SnackbarController.module.scss @@ -6,6 +6,9 @@ &.is_success { background-color: var(--success_bc_color); } + &.is_warning { + background-color: var(--waring_bc_color); + } &.is_error { background-color: var(--error_bc_color); } diff --git a/src-ui/common_components/checkbox/Checkbox.jsx b/src-ui/common_components/checkbox/Checkbox.jsx index 50ee51a9..64904243 100644 --- a/src-ui/common_components/checkbox/Checkbox.jsx +++ b/src-ui/common_components/checkbox/Checkbox.jsx @@ -3,15 +3,16 @@ import styles from "./Checkbox.module.scss"; export const Checkbox = ({ checkboxId, variable, + is_available = true, toggleFunction, size = "2.8rem", - color = "var(--primary_600_color)", borderWidth = "0.2rem", padding = "2rem", }) => { const wrapper_class_names = clsx(styles.checkbox_wrapper, { - [styles.is_disabled]: variable.state === "pending", + [styles.is_disabled]: !is_available, + [styles.is_pending]: variable.state === "pending", }); return ( @@ -21,7 +22,6 @@ export const Checkbox = ({ htmlFor={checkboxId} style={{ "--checkbox-size": size, - "--checkbox-color": color, "--checkbox-border-width": borderWidth, "--checkbox-padding": padding, }} diff --git a/src-ui/common_components/checkbox/Checkbox.module.scss b/src-ui/common_components/checkbox/Checkbox.module.scss index 70f47654..090fed2b 100644 --- a/src-ui/common_components/checkbox/Checkbox.module.scss +++ b/src-ui/common_components/checkbox/Checkbox.module.scss @@ -18,12 +18,19 @@ border: var(--checkbox-color, var(--primary_600_color)) solid var(--checkbox-border-width, 0.2rem); } } - &.is_disabled { + &.is_pending { pointer-events: none; & .cbx { border-color: var(--primary_800_color); } } + &.is_disabled { + pointer-events: none; + & .cbx { + filter: grayscale(100%); + border-color: var(--dark_800_color); + } + } } .checkbox_wrapper .cbx { diff --git a/src-ui/logics/_useBackendErrorHandling.js b/src-ui/logics/_useBackendErrorHandling.js index c9d99e50..d5987a1b 100644 --- a/src-ui/logics/_useBackendErrorHandling.js +++ b/src-ui/logics/_useBackendErrorHandling.js @@ -16,6 +16,7 @@ import { useDeepLAuthKey, useOscIpAddress, + useWebsocket, } from "@logics_configs"; import { ui_configs } from "../ui_configs"; @@ -31,106 +32,144 @@ export const _useBackendErrorHandling = () => { const { updateSpeakerPhraseTimeout } = useSpeakerPhraseTimeout(); const { updateSpeakerMaxWords } = useSpeakerMaxWords(); - const { updateDeepLAuthKey, saveErrorDeepLAuthKey } = useDeepLAuthKey(); + const { updateDeepLAuthKey } = useDeepLAuthKey(); const { updateOscIpAddress } = useOscIpAddress(); + const { updateEnableWebsocket, updateWebsocketHost, updateWebsocketPort } = useWebsocket(); - const errorHandling_Backend = ({message, data, endpoint, _result}) => { - switch (message) { - case "No mic device detected": - showNotification_Error(t("common_error.no_device_mic")); - break; - case "No speaker device detected": - showNotification_Error(t("common_error.no_device_speaker")); - break; + const errorHandling_Backend = ({message, data, endpoint, result}) => { + switch (endpoint) { + case "/run/error_device": + if (message === "No mic device detected") showNotification_Error(t("common_error.no_device_mic")); + if (message === "No speaker device detected") showNotification_Error(t("common_error.no_device_speaker")); + return; - case "Mic energy threshold value is out of range": - showNotification_Error(t("common_error.threshold_invalid_value", - { min: ui_configs.mic_threshold_min, max: ui_configs.mic_threshold_max }, - )); - break; - case "Speaker energy threshold value is out of range": - showNotification_Error(t("common_error.threshold_invalid_value", - { min: ui_configs.speaker_threshold_min, max: ui_configs.speaker_threshold_max }, - )); - break; + case "/set/data/mic_threshold": + if (message === "Mic energy threshold value is out of range") { + showNotification_Error(t("common_error.threshold_invalid_value", + { min: ui_configs.mic_threshold_min, max: ui_configs.mic_threshold_max }, + )); + }; + return; + case "/set/data/speaker_threshold": + if (message === "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 }, + )); + } + return; - case "CTranslate2 weight download error": - showNotification_Error(t("common_error.failed_download_weight_ctranslate2")); - break; - case "Whisper weight download error": - showNotification_Error(t("common_error.failed_download_weight_whisper")); - break; + case "/run/error_ctranslate2_weight": + if (message === "CTranslate2 weight download error") showNotification_Error(t("common_error.failed_download_weight_ctranslate2")); + return; + case "/run/error_whisper_weight": + if (message === "Whisper weight download error") showNotification_Error(t("common_error.failed_download_weight_whisper")); + return; - case "Translation engine limit error": - showNotification_Error(t("common_error.translation_limit")); - break; + case "/run/error_translation_engine": + if (message === "Translation engine limit error") showNotification_Error(t("common_error.translation_limit")); + return; - case "DeepL auth key length is not correct": - updateDeepLAuthKey(data); - showNotification_Error(t("common_error.deepl_auth_key_invalid_length")); - break; - case "Authentication failure of deepL auth key": - updateDeepLAuthKey(data); - showNotification_Error(t("common_error.deepl_auth_key_failed_authentication")); - break; + case "/set/data/deepl_auth_key": + if (message === "DeepL auth key length is not correct") { + updateDeepLAuthKey(data); + showNotification_Error(t("common_error.deepl_auth_key_invalid_length")); + } else if (message === "Authentication failure of deepL auth key") { + updateDeepLAuthKey(data); + showNotification_Error(t("common_error.deepl_auth_key_failed_authentication")); + } else { // Exception + updateDeepLAuthKey(data); + showNotification_Error(message); + } + return; - case "Mic record timeout value is out of range": - updateMicRecordTimeout(data); - showNotification_Error( - t("common_error.invalid_value_mic_record_timeout", - { mic_phrase_timeout_label: t("config_page.transcription.mic_phrase_timeout.label") } - )); - break; - case "Mic phrase timeout value is out of range": - updateMicPhraseTimeout(data); - showNotification_Error( - t("common_error.invalid_value_mic_phrase_timeout", - { mic_record_timeout_label: t("config_page.transcription.mic_record_timeout.label") } - )); - break; - case "Mic max phrases value is out of range": - updateMicMaxWords(data); - showNotification_Error(t("common_error.invalid_value_mic_max_phrase")); - break; + case "/set/data/mic_record_timeout": + if (message === "Mic record timeout value is out of range") { + updateMicRecordTimeout(data); + showNotification_Error(t("common_error.invalid_value_mic_record_timeout", { + mic_phrase_timeout_label: t("config_page.transcription.mic_phrase_timeout.label") + })); + } + return; + case "/set/data/mic_phrase_timeout": + if (message === "Mic phrase timeout value is out of range") { + updateMicPhraseTimeout(data); + showNotification_Error(t("common_error.invalid_value_mic_phrase_timeout", { + mic_record_timeout_label: t("config_page.transcription.mic_record_timeout.label") + })); + } + return; + case "/set/data/mic_max_phrases": + if (message === "Mic max phrases value is out of range") { + updateMicMaxWords(data); + showNotification_Error(t("common_error.invalid_value_mic_max_phrase")); + } + return; - case "Speaker record timeout value is out of range": - updateSpeakerRecordTimeout(data); - showNotification_Error( - t("common_error.invalid_value_speaker_record_timeout", - { speaker_phrase_timeout_label: t("config_page.transcription.speaker_phrase_timeout.label") } - )); - break; - case "Speaker phrase timeout value is out of range": - updateSpeakerPhraseTimeout(data); - showNotification_Error( - t("common_error.invalid_value_speaker_phrase_timeout", - { speaker_record_timeout_label: t("config_page.transcription.speaker_record_timeout.label") } - )); - break; - case "Speaker max phrases value is out of range": - updateSpeakerMaxWords(data); - showNotification_Error(t("common_error.invalid_value_speaker_max_phrase")); - break; + case "/set/data/speaker_record_timeout": + if (message === "Speaker record timeout value is out of range") { + updateSpeakerRecordTimeout(data); + showNotification_Error(t("common_error.invalid_value_speaker_record_timeout", { + speaker_phrase_timeout_label: t("config_page.transcription.speaker_phrase_timeout.label") + })); + } + return; + case "/set/data/speaker_phrase_timeout": + if (message === "Speaker phrase timeout value is out of range") { + updateSpeakerPhraseTimeout(data); + showNotification_Error(t("common_error.invalid_value_speaker_phrase_timeout", { + speaker_record_timeout_label: t("config_page.transcription.speaker_record_timeout.label") + })); + } + return; + case "/set/data/speaker_max_phrases": + if (message === "Speaker max phrases value is out of range") { + updateSpeakerMaxWords(data); + showNotification_Error(t("common_error.invalid_value_speaker_max_phrase")); + } + return; // Advanced Settings, error messages are set by Backend (EN only) - case "Invalid IP address": - updateOscIpAddress(data); - showNotification_Error(message); - break; + case "/set/data/osc_ip_address": + if (message === "Invalid IP address") { + updateOscIpAddress(data); + showNotification_Error(message); + } else if (message === "Cannot set IP address") { + updateOscIpAddress(data); + showNotification_Error(message); + } // else? (Backend will send the message "Cannot set IP address" when throw Exception) + return; - case "Cannot set IP address": - updateOscIpAddress(data); - showNotification_Error(message); - break; + + case "/set/enable/websocket_server": + if (message === "WebSocket server host or port is not available") { + updateEnableWebsocket(data); + showNotification_Error(message); + } + return; + + case "/set/data/websocket_host": + if (message === "Invalid IP address") { + updateWebsocketHost(data); + showNotification_Error(message); + } else if (message === "WebSocket server host is not available") { + updateWebsocketHost(data); + showNotification_Error(message); + } + return; + + case "/set/data/websocket_port": + if (message === "WebSocket server port is not available") { + updateWebsocketPort(data); + showNotification_Error(message); + } + return; default: - // determine by endpoint, not message. - if (endpoint === "/set/data/deepl_auth_key") saveErrorDeepLAuthKey({message, data, endpoint, _result}); - - break; + console.error(`Invalid endpoint or message: ${endpoint}\nmessage: ${message}\nresult: ${JSON.stringify(result)}`); + return; } } diff --git a/src-ui/logics/common/index.js b/src-ui/logics/common/index.js index e95bae8f..c03d13eb 100644 --- a/src-ui/logics/common/index.js +++ b/src-ui/logics/common/index.js @@ -11,5 +11,6 @@ export { useMessage } from "./useMessage"; export { useUpdateSoftware } from "./useUpdateSoftware"; export { useVolume } from "./useVolume"; export { useHandleNetworkConnection } from "./useHandleNetworkConnection"; +export { useHandleOscQuery } from "./useHandleOscQuery"; export { useIsVrctAvailable } from "./useIsVrctAvailable"; export { useFetch } from "./useFetch"; \ No newline at end of file diff --git a/src-ui/logics/common/useHandleOscQuery.js b/src-ui/logics/common/useHandleOscQuery.js new file mode 100644 index 00000000..2e6b1681 --- /dev/null +++ b/src-ui/logics/common/useHandleOscQuery.js @@ -0,0 +1,44 @@ +import { useTranslation } from "react-i18next"; +import { useNotificationStatus } from "@logics_common"; +import { + useEnableVrcMicMuteSync, +} from "@logics_configs"; + +export const useHandleOscQuery = () => { + const { t } = useTranslation(); + + const { showNotification_Warning } = useNotificationStatus(); + const { updateEnableVrcMicMuteSync } = useEnableVrcMicMuteSync(); + + const handleOscQuery = ({is_osc_query_enabled, disabled_functions}) => { + if (!is_osc_query_enabled && disabled_functions.length > 0) { + const BASE_LABEL = t("common_warning.unable_to_use_osc_query"); + let items_label = ""; + + for (const disabled_function of disabled_functions) { + if (disabled_function === "vrc_mic_mute_sync") { + updateEnableVrcMicMuteSync({ + is_enabled: false, + is_available: false, + }); + const item = `- ${t("config_page.others.vrc_mic_mute_sync.label")}`; + items_label = `${items_label}\n${item}`; + } + } + const label = `${BASE_LABEL}${items_label}`; + showNotification_Warning( + label, + { hide_duration: 10000, } + ); + } else if (is_osc_query_enabled) { + updateEnableVrcMicMuteSync((old_value) => ({ + ...old_value.data, + is_available: true, + })); + } + }; + + return { + handleOscQuery, + }; +}; \ No newline at end of file diff --git a/src-ui/logics/common/useNotificationStatus.js b/src-ui/logics/common/useNotificationStatus.js index a5b7bc3c..f4aab3ed 100644 --- a/src-ui/logics/common/useNotificationStatus.js +++ b/src-ui/logics/common/useNotificationStatus.js @@ -5,6 +5,16 @@ export const useNotificationStatus = () => { const generateRandomKey = () => Math.random(); + const showNotification_Warning = (message, options = {}) => { + updateNotificationStatus({ + status: "warning", + is_open: true, + key: generateRandomKey(), + message: message, + options: options, + }); + }; + const showNotification_Error = (message, options = {}) => { updateNotificationStatus({ status: "error", @@ -37,6 +47,7 @@ export const useNotificationStatus = () => { currentNotificationStatus, updateNotificationStatus, + showNotification_Warning, showNotification_Error, showNotification_Success, closeNotification, diff --git a/src-ui/logics/configs/advanced_settings/useWebsocket.js b/src-ui/logics/configs/advanced_settings/useWebsocket.js new file mode 100644 index 00000000..51361489 --- /dev/null +++ b/src-ui/logics/configs/advanced_settings/useWebsocket.js @@ -0,0 +1,67 @@ +import { + useStore_EnableWebsocket, + useStore_WebsocketHost, + useStore_WebsocketPort, +} from "@store"; +import { useStdoutToPython } from "@logics/useStdoutToPython"; + +export const useWebsocket = () => { + const { asyncStdoutToPython } = useStdoutToPython(); + const { currentEnableWebsocket, updateEnableWebsocket, pendingEnableWebsocket } = useStore_EnableWebsocket(); + const { currentWebsocketHost, updateWebsocketHost, pendingWebsocketHost } = useStore_WebsocketHost(); + const { currentWebsocketPort, updateWebsocketPort, pendingWebsocketPort } = useStore_WebsocketPort(); + + const getEnableWebsocket = () => { + pendingEnableWebsocket(); + asyncStdoutToPython("/get/data/websocket_server"); + }; + + const toggleEnableWebsocket = () => { + pendingEnableWebsocket(); + if (currentEnableWebsocket.data) { + asyncStdoutToPython("/set/disable/websocket_server"); + } else { + asyncStdoutToPython("/set/enable/websocket_server"); + } + }; + + + const getWebsocketHost = () => { + pendingWebsocketHost(); + asyncStdoutToPython("/get/data/websocket_host"); + }; + + const setWebsocketHost = (websocket_host) => { + pendingWebsocketHost(); + asyncStdoutToPython("/set/data/websocket_host", websocket_host); + }; + + + const getWebsocketPort = () => { + pendingWebsocketPort(); + asyncStdoutToPython("/get/data/websocket_port"); + }; + + const setWebsocketPort = (websocket_port) => { + pendingWebsocketPort(); + asyncStdoutToPython("/set/data/websocket_port", websocket_port); + }; + + return { + currentEnableWebsocket, + updateEnableWebsocket, + getEnableWebsocket, + toggleEnableWebsocket, + + currentWebsocketHost, + updateWebsocketHost, + getWebsocketHost, + setWebsocketHost, + + currentWebsocketPort, + updateWebsocketPort, + getWebsocketPort, + setWebsocketPort, + + }; +}; \ No newline at end of file diff --git a/src-ui/logics/configs/index.js b/src-ui/logics/configs/index.js index 912e2a7d..03214771 100644 --- a/src-ui/logics/configs/index.js +++ b/src-ui/logics/configs/index.js @@ -56,6 +56,7 @@ export { useHotkeys } from "./hotkeys/useHotkeys"; export { useOscIpAddress } from "./advanced_settings/useOscIpAddress"; export { useOscPort } from "./advanced_settings/useOscPort"; +export { useWebsocket } from "./advanced_settings/useWebsocket"; export { useSupporters } from "./supporters/useSupporters"; diff --git a/src-ui/logics/configs/others/useEnableVrcMicMuteSync.js b/src-ui/logics/configs/others/useEnableVrcMicMuteSync.js index 16a853ab..6515ffc2 100644 --- a/src-ui/logics/configs/others/useEnableVrcMicMuteSync.js +++ b/src-ui/logics/configs/others/useEnableVrcMicMuteSync.js @@ -12,7 +12,7 @@ export const useEnableVrcMicMuteSync = () => { const toggleEnableVrcMicMuteSync = () => { pendingEnableVrcMicMuteSync(); - if (currentEnableVrcMicMuteSync.data) { + if (currentEnableVrcMicMuteSync.data.is_enabled) { asyncStdoutToPython("/set/disable/vrc_mic_mute_sync"); } else { asyncStdoutToPython("/set/enable/vrc_mic_mute_sync"); diff --git a/src-ui/logics/configs/translation/useDeepLAuthKey.js b/src-ui/logics/configs/translation/useDeepLAuthKey.js index f93fdd4f..a568dfe9 100644 --- a/src-ui/logics/configs/translation/useDeepLAuthKey.js +++ b/src-ui/logics/configs/translation/useDeepLAuthKey.js @@ -29,11 +29,6 @@ export const useDeepLAuthKey = () => { showNotification_Success(t("config_page.translation.deepl_auth_key.auth_key_success")); }; - const saveErrorDeepLAuthKey = ({data, message}) => { - updateDeepLAuthKey(data); - showNotification_Error(message); - }; - return { currentDeepLAuthKey, getDeepLAuthKey, @@ -41,7 +36,6 @@ export const useDeepLAuthKey = () => { setDeepLAuthKey, deleteDeepLAuthKey, - saveErrorDeepLAuthKey, savedDeepLAuthKey, }; }; \ No newline at end of file diff --git a/src-ui/logics/useReceiveRoutes.js b/src-ui/logics/useReceiveRoutes.js index 4a4f944c..b1d3421b 100644 --- a/src-ui/logics/useReceiveRoutes.js +++ b/src-ui/logics/useReceiveRoutes.js @@ -7,6 +7,7 @@ import { useIsVrctAvailable, useNotificationStatus, useHandleNetworkConnection, + useHandleOscQuery, useSoftwareVersion, useComputeMode, @@ -75,6 +76,7 @@ import { usePlugins, useOscIpAddress, useOscPort, + useWebsocket, } from "@logics_configs"; export const useReceiveRoutes = () => { @@ -82,6 +84,7 @@ export const useReceiveRoutes = () => { const { updateComputeMode } = useComputeMode(); const { updateInitProgress } = useInitProgress(); const { updateIsBackendReady } = useIsBackendReady(); + const { handleOscQuery } = useHandleOscQuery(); const { restoreWindowGeometry } = useWindow(); const { updateIsMainPageCompactMode } = useIsMainPageCompactMode(); const { @@ -180,6 +183,11 @@ export const useReceiveRoutes = () => { const { updateOscIpAddress } = useOscIpAddress(); const { updateOscPort } = useOscPort(); + const { + updateEnableWebsocket, + updateWebsocketHost, + updateWebsocketPort, + } = useWebsocket(); @@ -213,6 +221,12 @@ export const useReceiveRoutes = () => { })); }, "/run/connected_network": handleNetworkConnection, + "/run/enable_osc_query": ({data, disabled_functions}) => { + handleOscQuery({ + is_osc_query_enabled: data, + disabled_functions: disabled_functions, + }); + }, // Main Page // Page Controls @@ -474,9 +488,15 @@ export const useReceiveRoutes = () => { "/set/enable/logger_feature": updateEnableAutoExportMessageLogs, "/set/disable/logger_feature": updateEnableAutoExportMessageLogs, - "/get/data/vrc_mic_mute_sync": updateEnableVrcMicMuteSync, - "/set/enable/vrc_mic_mute_sync": updateEnableVrcMicMuteSync, - "/set/disable/vrc_mic_mute_sync": updateEnableVrcMicMuteSync, + "/get/data/vrc_mic_mute_sync": (payload) => updateEnableVrcMicMuteSync((old_value) => { + return {...old_value.data, is_enabled: payload}; + }), + "/set/enable/vrc_mic_mute_sync": (payload) => updateEnableVrcMicMuteSync((old_value) => { + return {...old_value.data, is_enabled: payload}; + }), + "/set/disable/vrc_mic_mute_sync": (payload) => updateEnableVrcMicMuteSync((old_value) => { + return {...old_value.data, is_enabled: payload}; + }), "/get/data/send_message_to_vrc": updateEnableSendMessageToVrc, "/set/enable/send_message_to_vrc": updateEnableSendMessageToVrc, @@ -505,6 +525,16 @@ export const useReceiveRoutes = () => { "/get/data/osc_port": updateOscPort, "/set/data/osc_port": updateOscPort, + "/get/data/websocket_server": updateEnableWebsocket, + "/set/enable/websocket_server": updateEnableWebsocket, + "/set/disable/websocket_server": updateEnableWebsocket, + + "/get/data/websocket_host": updateWebsocketHost, + "/set/data/websocket_host": updateWebsocketHost, + + "/get/data/websocket_port": updateWebsocketPort, + "/set/data/websocket_port": updateWebsocketPort, + "/get/data/mic_avg_logprob": ()=>{}, // Not implemented on UI yet "/get/data/mic_no_speech_prob": ()=>{}, // Not implemented on UI yet "/get/data/speaker_avg_logprob": ()=>{}, // Not implemented on UI yet @@ -514,30 +544,6 @@ export const useReceiveRoutes = () => { "/get/data/transcription_engines": ()=>{}, // Not implemented on UI yet. (if ai_models has not been detected, this will be blank array[]. if the ai_models are ok but just network has not connected, it'l be only ["Whisper"]) }; - const error_status_routes = { - "/run/error_device": errorHandling_Backend, - - "/run/error_ctranslate2_weight": errorHandling_Backend, - "/run/error_whisper_weight": errorHandling_Backend, - - "/set/data/deepl_auth_key": errorHandling_Backend, - - "/run/error_translation_engine": errorHandling_Backend, - - "/set/data/mic_threshold": errorHandling_Backend, - "/set/data/mic_record_timeout": errorHandling_Backend, - "/set/data/mic_phrase_timeout": errorHandling_Backend, - "/set/data/mic_max_phrases": errorHandling_Backend, - - "/set/data/speaker_threshold": errorHandling_Backend, - "/set/data/speaker_record_timeout": errorHandling_Backend, - "/set/data/speaker_phrase_timeout": errorHandling_Backend, - "/set/data/speaker_max_phrases": errorHandling_Backend, - - "/set/data/osc_ip_address": errorHandling_Backend, - }; - - const receiveRoutes = (parsed_data) => { const initDataSyncProcess = (payload) => { for (const [endpoint, value] of Object.entries(payload)) { @@ -567,17 +573,12 @@ export const useReceiveRoutes = () => { break; case 400: - 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); - } + errorHandling_Backend({ + message: parsed_data.result.message, + data: parsed_data.result.data, + endpoint: parsed_data.endpoint, + result: parsed_data.result, + }); break; case 500: showNotification_Error( diff --git a/src-ui/store.js b/src-ui/store.js index 8f5317a3..06fc859a 100644 --- a/src-ui/store.js +++ b/src-ui/store.js @@ -60,10 +60,19 @@ export const createAtomWithHook = (initialValue, base_name, options) => { }; const updateAtom = (payload, options = {}) => { - const { remain_state = false, set_state } = options; + const { remain_state = false, set_state, lock_state } = options; setAtom((currentValue) => { - const new_state = set_state ?? (remain_state ? currentValue.state : "ok"); + let new_state; + if (lock_state) { + new_state = set_state; + } else { + if (currentValue.lock_state) { + new_state = currentValue.state; + } else { + new_state = set_state ?? (remain_state ? currentValue.state : "ok"); + } + } const updated_data = typeof payload === "function" ? payload(currentValue) @@ -290,6 +299,10 @@ export const { atomInstance: Atom_PluginsData, useHook: useStore_PluginsData } = export const { atomInstance: Atom_OscIpAddress, useHook: useStore_OscIpAddress } = createAtomWithHook("127.0.0.1", "OscIpAddress"); export const { atomInstance: Atom_OscPort, useHook: useStore_OscPort } = createAtomWithHook("9000", "OscPort"); +export const { atomInstance: Atom_EnableWebsocket, useHook: useStore_EnableWebsocket } = createAtomWithHook(true, "EnableWebsocket"); +export const { atomInstance: Atom_WebsocketHost, useHook: useStore_WebsocketHost } = createAtomWithHook("127.0.0.1", "WebsocketHost"); +export const { atomInstance: Atom_WebsocketPort, useHook: useStore_WebsocketPort } = createAtomWithHook("2231", "WebsocketPort"); + // Supporters diff --git a/src-ui/ui_configs.js b/src-ui/ui_configs.js index d6ef28eb..915d6a6b 100644 --- a/src-ui/ui_configs.js +++ b/src-ui/ui_configs.js @@ -7,12 +7,18 @@ export const ui_configs = { x_pos: { step: 0.05, min: -0.5, max: 0.5 }, y_pos: { step: 0.05, min: -0.8, max: 0.8 }, z_pos: { step: 0.05, min: -0.5, max: 1.5 }, + x_rotation: { min: -180, max: 180, step: 5 }, + y_rotation: { min: -180, max: 180, step: 5 }, + z_rotation: { min: -180, max: 180, step: 5 }, ui_scaling: { step: 10, min: 40, max: 200 }, }, overlay_large_log: { x_pos: { step: 0.05, min: -0.5, max: 0.5 }, y_pos: { step: 0.05, min: -0.8, max: 0.8 }, z_pos: { step: 0.05, min: -0.5, max: 1.5 }, + x_rotation: { min: -180, max: 180, step: 5 }, + y_rotation: { min: -180, max: 180, step: 5 }, + z_rotation: { min: -180, max: 180, step: 5 }, ui_scaling: { step: 10, min: 40, max: 200 }, }, diff --git a/zip.py b/zip.py index 168c5c87..553e51b3 100644 --- a/zip.py +++ b/zip.py @@ -1,37 +1,70 @@ -import os import zipfile import argparse +from pathlib import Path +import time +from tqdm import tqdm # tqdmをインポート -def zip_files_and_directory(zip_name, file_paths, dir_paths): +def zip_files_and_directory(zip_name, file_paths, dir_paths, verbose=False): + zip_file_path = Path(zip_name) # ZIPファイルを作成 - with zipfile.ZipFile(zip_name, 'w', zipfile.ZIP_DEFLATED) as zipf: - # ファイルを追加 - for file_path in file_paths: - if os.path.isfile(file_path): - zipf.write(file_path, os.path.basename(file_path)) - print(f"Add file: {file_path}") + try: + with zipfile.ZipFile(zip_file_path, 'w', zipfile.ZIP_DEFLATED) as zipf: + # ファイルを追加 + for file_path_str in tqdm(file_paths, desc="Adding files", unit="file"): + file_path = Path(file_path_str) + if file_path.is_file(): + zipf.write(file_path, file_path.name) + if verbose: + print(f"Add file: {file_path}") + else: + print(f"Warning: File not found or is not a file: {file_path}") - # ディレクトリを追加 - for dir_path in dir_paths: - if os.path.isdir(dir_path): - for foldername, subfolders, filenames in os.walk(dir_path): - for filename in filenames: - file_full_path = os.path.join(foldername, filename) - # ディレクトリを保持しつつ、ルートに配置 - arcname = os.path.join( - os.path.basename(dir_path), - os.path.relpath(file_full_path, dir_path) - ) - zipf.write(file_full_path, arcname) - print(f"Add file: {file_full_path}") + # ディレクトリを追加 + for dir_path_str in dir_paths: + dir_path = Path(dir_path_str) + if dir_path.is_dir(): + all_files_in_dir = [item for item in dir_path.rglob("*") if item.is_file()] + for item in tqdm(all_files_in_dir, desc=f"Adding files from {dir_path.name}", unit="file"): + # ディレクトリ構造を保持しつつ、ルートに配置 + arcname = Path(dir_path.name) / item.relative_to(dir_path) + zipf.write(item, arcname) + if verbose: + print(f"Add file: {item}") + else: + print(f"Warning: Directory not found or is not a directory: {dir_path}") + print(f"Successfully created zip file: {zip_file_path}") + except IOError as e: + print(f"Error: Could not create zip file {zip_file_path}. Reason: {e}") + except Exception as e: + print(f"An unexpected error occurred: {e}") if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument("--zip_name", type=str, default="VRCT.zip") - parser.add_argument("--file_paths", type=str, nargs="*", default=["src-tauri/target/release/VRCT.exe", "src-tauri/target/release/VRCT-sidecar.exe"]) - parser.add_argument("--dir_paths", type=str, nargs="*", default=["src-tauri/target/release/_internal"]) + start_time = time.time() + parser = argparse.ArgumentParser(description="Create a zip file from specified files and directories.") + parser.add_argument("--zip_name", type=str, default="VRCT.zip", help="Name of the output zip file.") + parser.add_argument( + "--file_paths", + type=str, + nargs="*", + default=["src-tauri/target/release/VRCT.exe", "src-tauri/target/release/VRCT-sidecar.exe"], + help="List of file paths to include in the zip." + ) + parser.add_argument( + "--dir_paths", + type=str, + nargs="*", + default=["src-tauri/target/release/_internal"], + help="List of directory paths to include in the zip." + ) + parser.add_argument( + "-v", "--verbose", + action="store_true", + help="Increase output verbosity." + ) args = parser.parse_args() - zip_files_and_directory(args.zip_name, args.file_paths, args.dir_paths) - print("Complete!") \ No newline at end of file + zip_files_and_directory(args.zip_name, args.file_paths, args.dir_paths, args.verbose) + end_time = time.time() + processing_time = end_time - start_time + print(f"Complete! Processing time: {processing_time:.2f} seconds") \ No newline at end of file