Merge branch 'develop'

This commit is contained in:
misyaguziya
2025-06-03 11:09:11 +09:00
44 changed files with 1633 additions and 303 deletions

3
.gitignore vendored
View File

@@ -12,7 +12,8 @@ weights/
error.log error.log
*.exe *.exe
*.ipynb *.ipynb
VRCT.zip
VRCT_cuda.zip
# Added by WebUI migration # Added by WebUI migration
# Logs # Logs

View File

@@ -5,7 +5,7 @@ a = Analysis(
['src-python\\mainloop.py'], ['src-python\\mainloop.py'],
pathex=[], pathex=[],
binaries=[], 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=[], hiddenimports=[],
hookspath=[], hookspath=[],
hooksconfig={}, hooksconfig={},

View File

@@ -5,7 +5,7 @@ a = Analysis(
['src-python\\mainloop.py'], ['src-python\\mainloop.py'],
pathex=[], pathex=[],
binaries=[], 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=[], hiddenimports=[],
hookspath=[], hookspath=[],
hooksconfig={}, hooksconfig={},

View File

@@ -1,2 +1,2 @@
call .venv/Scripts/activate call .venv/Scripts/activate
pyinstaller backend.spec --distpath src-tauri/bin --clean --noconfirm pyinstaller backend.spec --distpath src-tauri/bin --clean --noconfirm --log-level ERROR

View File

@@ -1,2 +1,2 @@
call .venv_cuda/Scripts/activate call .venv_cuda/Scripts/activate
pyinstaller backend_cuda.spec --distpath src-tauri/bin --clean --noconfirm pyinstaller backend_cuda.spec --distpath src-tauri/bin --clean --noconfirm --log-level ERROR

View File

@@ -1,10 +1,25 @@
python -m venv .venv REM .venv exists
python -m venv .venv_cuda if exist .venv (
rmdir /s /q .venv
)
REM make .venv
python -m venv .venv
REM install packages for .venv
call .venv/Scripts/activate call .venv/Scripts/activate
python.exe -m pip install --upgrade pip python.exe -m pip install --upgrade pip
pip install --no-cache-dir --force-reinstall -r requirements.txt 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 call .venv_cuda/Scripts/activate
python.exe -m pip install --upgrade pip python.exe -m pip install --upgrade pip
pip install --no-cache-dir --force-reinstall -r requirements_cuda.txt pip install --no-cache-dir --force-reinstall -r requirements_cuda.txt

View File

@@ -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_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." 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: main_page:
translation: "Translation" translation: "Translation"
transcription_send: "Voice2Chatbox" transcription_send: "Voice2Chatbox"
@@ -257,6 +260,12 @@ config_page:
label: "Open Config File" label: "Open Config File"
switch_compute_device: switch_compute_device:
label: "Switch VRCT To CPU/GPU Version" 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: plugin_notifications:
downloading: Downloading the plugin. downloading: Downloading the plugin.

View File

@@ -24,10 +24,13 @@ common_error:
invalid_value_speaker_phrase_timeout: "0 以上で 「{{speaker_record_timeout_label}}」 より小さくすることはできません。" invalid_value_speaker_phrase_timeout: "0 以上で 「{{speaker_record_timeout_label}}」 より小さくすることはできません。"
invalid_value_speaker_max_phrase: "0 以上の数値を設定できます。" invalid_value_speaker_max_phrase: "0 以上の数値を設定できます。"
common_warning:
unable_to_use_osc_query: "OSC IP Address の設定によりOSCデータの受信ができないため、以下の機能が自動的に無効になっています。"
main_page: main_page:
translation: "翻訳" translation: "翻訳"
transcription_send: "音声認識 マイク" transcription_send: "マイク入力"
transcription_receive: "音声認識 スピーカー" transcription_receive: "聞き取り"
foreground: "最前面固定" foreground: "最前面固定"
language_settings: "言語設定" language_settings: "言語設定"
@@ -131,7 +134,7 @@ config_page:
ctranslate2_compute_device: ctranslate2_compute_device:
label: "AI翻訳 {{ctranslate2}} の処理デバイス" label: "AI翻訳 {{ctranslate2}} の処理デバイス"
deepl_auth_key: deepl_auth_key:
label: "DeepL API 認証キー" label: "DeepL APIキーの登録"
desc: "使用の際は、メイン画面にある {{translator}} をDeepL_APIに変更してください。\n※対応していない言語もあります。" desc: "使用の際は、メイン画面にある {{translator}} をDeepL_APIに変更してください。\n※対応していない言語もあります。"
open_auth_key_webpage: "DeepLアカウントページを開く" open_auth_key_webpage: "DeepLアカウントページを開く"
save: "保存" save: "保存"
@@ -257,7 +260,12 @@ config_page:
label: "設定ファイルを開く" label: "設定ファイルを開く"
switch_compute_device: switch_compute_device:
label: "VRCT CPU/GPUバージョンの切り替え" label: "VRCT CPU/GPUバージョンの切り替え"
enable_websocket:
label: "WebSocketサーバーを有効にする"
websocket_host:
label: "WebSocket Host"
websocket_port:
label: "WebSocket Port"
plugin_notifications: plugin_notifications:
downloading: プラグインをダウンロード中。 downloading: プラグインをダウンロード中。

View File

@@ -15,6 +15,10 @@ pydub==0.25.1
psutil==5.9.8 psutil==5.9.8
pykakasi==2.3.0 pykakasi==2.3.0
pycaw==20240210 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 translators @ git+https://github.com/misyaguziya/translators@5.9.2.1
SpeechRecognition @ git+https://github.com/misyaguziya/custom_speech_recognition@3.10.4.1 SpeechRecognition @ git+https://github.com/misyaguziya/custom_speech_recognition@3.10.4.1
tinyoscquery @ git+https://github.com/cyberkitsune/tinyoscquery@0.1.3 tinyoscquery @ git+https://github.com/cyberkitsune/tinyoscquery@0.1.3

View File

@@ -16,6 +16,10 @@ pydub==0.25.1
psutil==5.9.8 psutil==5.9.8
pykakasi==2.3.0 pykakasi==2.3.0
pycaw==20240210 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 translators @ git+https://github.com/misyaguziya/translators@5.9.2.1
SpeechRecognition @ git+https://github.com/misyaguziya/custom_speech_recognition@3.10.4.1 SpeechRecognition @ git+https://github.com/misyaguziya/custom_speech_recognition@3.10.4.1
tinyoscquery @ git+https://github.com/cyberkitsune/tinyoscquery@0.1.3 tinyoscquery @ git+https://github.com/cyberkitsune/tinyoscquery@0.1.3

View File

@@ -954,9 +954,42 @@ class Config:
self._NOTIFICATION_VRC_SFX = value self._NOTIFICATION_VRC_SFX = value
self.saveConfig(inspect.currentframe().f_code.co_name, 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): def init_config(self):
# Read Only # Read Only
self._VERSION = "3.1.2" self._VERSION = "3.2.0"
if getattr(sys, 'frozen', False): if getattr(sys, 'frozen', False):
self._PATH_LOCAL = os_path.dirname(sys.executable) self._PATH_LOCAL = os_path.dirname(sys.executable)
else: else:
@@ -1139,6 +1172,9 @@ class Config:
self._LOGGER_FEATURE = False self._LOGGER_FEATURE = False
self._VRC_MIC_MUTE_SYNC = False self._VRC_MIC_MUTE_SYNC = False
self._NOTIFICATION_VRC_SFX = True self._NOTIFICATION_VRC_SFX = True
self._WEBSOCKET_SERVER = False
self._WEBSOCKET_HOST = "127.0.0.1"
self._WEBSOCKET_PORT = 2231
def load_config(self): def load_config(self):
if os_path.isfile(self.PATH_CONFIG) is not False: if os_path.isfile(self.PATH_CONFIG) is not False:

View File

@@ -1,4 +1,4 @@
from typing import Callable, Union, Any from typing import Callable, Any
from time import sleep from time import sleep
from subprocess import Popen from subprocess import Popen
from threading import Thread from threading import Thread
@@ -6,7 +6,7 @@ import re
from device_manager import device_manager from device_manager import device_manager
from config import config from config import config
from model import model from model import model
from utils import removeLog, printLog, errorLogging, isConnectedNetwork, isValidIpAddress from utils import removeLog, printLog, errorLogging, isConnectedNetwork, isValidIpAddress, isAvailableWebSocketServer
class Controller: class Controller:
def __init__(self) -> None: def __init__(self) -> None:
@@ -294,6 +294,19 @@ class Controller:
"translation":translation, "translation":translation,
"transliteration":transliteration "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 config.LOGGER_FEATURE is True:
if len(translation) > 0: if len(translation) > 0:
translation = " (" + "/".join(translation) + ")" translation = " (" + "/".join(translation) + ")"
@@ -377,6 +390,19 @@ class Controller:
"translation":translation, "translation":translation,
"transliteration":transliteration, "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 config.LOGGER_FEATURE is True:
if len(translation) > 0: if len(translation) > 0:
translation = " (" + "/".join(translation) + ")" translation = " (" + "/".join(translation) + ")"
@@ -434,11 +460,23 @@ class Controller:
overlay_image = model.createOverlayImageLargeLog("send", message, translation[0] if len(translation) > 0 else "") overlay_image = model.createOverlayImageLargeLog("send", message, translation[0] if len(translation) > 0 else "")
model.updateOverlayLargeLog(overlay_image) 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 config.LOGGER_FEATURE is True:
if len(translation) > 0: if len(translation) > 0:
translation_text = " (" + "/".join(translation) + ")" translation_text = " (" + "/".join(translation) + ")"
model.logger.info(f"[SENT] {message}{translation_text}") model.logger.info(f"[CHAT] {message}{translation_text}")
return {"status":200, return {"status":200,
"result":{ "result":{
@@ -1099,8 +1137,7 @@ class Controller:
def getOscIpAddress(*args, **kwargs) -> dict: def getOscIpAddress(*args, **kwargs) -> dict:
return {"status":200, "result":config.OSC_IP_ADDRESS} return {"status":200, "result":config.OSC_IP_ADDRESS}
@staticmethod def setOscIpAddress(self, data, *args, **kwargs) -> dict:
def setOscIpAddress(data, *args, **kwargs) -> dict:
if isValidIpAddress(data) is False: if isValidIpAddress(data) is False:
response = { response = {
"status":400, "status":400,
@@ -1113,6 +1150,11 @@ class Controller:
try: try:
model.setOscIpAddress(data) model.setOscIpAddress(data)
config.OSC_IP_ADDRESS = 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} response = {"status":200, "result":config.OSC_IP_ADDRESS}
except Exception: except Exception:
model.setOscIpAddress(config.OSC_IP_ADDRESS) model.setOscIpAddress(config.OSC_IP_ADDRESS)
@@ -1388,10 +1430,20 @@ class Controller:
@staticmethod @staticmethod
def setEnableVrcMicMuteSync(*args, **kwargs) -> dict: def setEnableVrcMicMuteSync(*args, **kwargs) -> dict:
if model.getIsOscQueryEnabled() is True:
config.VRC_MIC_MUTE_SYNC = True config.VRC_MIC_MUTE_SYNC = True
model.setMuteSelfStatus() model.setMuteSelfStatus()
model.changeMicTranscriptStatus() model.changeMicTranscriptStatus()
return {"status":200, "result":config.VRC_MIC_MUTE_SYNC} 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 @staticmethod
def setDisableVrcMicMuteSync(*args, **kwargs) -> dict: def setDisableVrcMicMuteSync(*args, **kwargs) -> dict:
@@ -1778,9 +1830,117 @@ class Controller:
model.stopWatchdog() model.stopWatchdog()
return {"status":200, "result":True} 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): def initializationProgress(self, progress):
self.run(200, self.run_mapping["initialization_progress"], 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: def init(self, *args, **kwargs) -> None:
removeLog() removeLog()
printLog("Start Initialization") printLog("Start Initialization")
@@ -1892,6 +2052,14 @@ class Controller:
# init OSC receive # init OSC receive
printLog("Init OSC Receive") printLog("Init OSC Receive")
model.startReceiveOSC() 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: if config.VRC_MIC_MUTE_SYNC is True:
self.setEnableVrcMicMuteSync() self.setEnableVrcMicMuteSync()
@@ -1911,6 +2079,15 @@ class Controller:
if (config.OVERLAY_SMALL_LOG is True or config.OVERLAY_LARGE_LOG is True): if (config.OVERLAY_SMALL_LOG is True or config.OVERLAY_LARGE_LOG is True):
model.startOverlay() 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") printLog("Update settings")
self.updateConfigSettings() self.updateConfigSettings()

View File

@@ -4,8 +4,11 @@ import time
from typing import Any from typing import Any
from threading import Thread from threading import Thread
from queue import Queue from queue import Queue
from controller import Controller import logging
from utils import printLog, printResponse, errorLogging, encodeBase64 from controller import Controller # noqa: E402
from utils import printLog, printResponse, errorLogging, encodeBase64 # noqa: E402
logging.getLogger("huggingface_hub").setLevel(logging.ERROR)
run_mapping = { run_mapping = {
"connected_network":"/run/connected_network", "connected_network":"/run/connected_network",
@@ -42,6 +45,8 @@ run_mapping = {
"initialization_progress":"/run/initialization_progress", "initialization_progress":"/run/initialization_progress",
"initialization_complete":"/run/initialization_complete", "initialization_complete":"/run/initialization_complete",
"enable_osc_query":"/run/enable_osc_query",
} }
def run(status:int, endpoint:str, result:Any) -> None: 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/enable/send_received_message_to_vrc": {"status": True, "variable":controller.setEnableSendReceivedMessageToVrc},
"/set/disable/send_received_message_to_vrc": {"status": True, "variable":controller.setDisableSendReceivedMessageToVrc}, "/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 # Advanced Settings
"/get/data/osc_ip_address": {"status": True, "variable":controller.getOscIpAddress}, "/get/data/osc_ip_address": {"status": True, "variable":controller.getOscIpAddress},
"/set/data/osc_ip_address": {"status": True, "variable":controller.setOscIpAddress}, "/set/data/osc_ip_address": {"status": True, "variable":controller.setOscIpAddress},
@@ -367,7 +381,7 @@ class Main:
if status == 423: if status == 423:
self.queue.put((endpoint, data)) self.queue.put((endpoint, data))
else: else:
printLog(endpoint, {"send_data":result}) printLog(endpoint, {"status": status, "send_data": result})
printResponse(status, endpoint, result) printResponse(status, endpoint, result)
time.sleep(0.1) time.sleep(0.1)

View File

@@ -1,5 +1,7 @@
import copy import copy
import gc import gc
import asyncio
import json
from subprocess import Popen from subprocess import Popen
from os import makedirs as os_makedirs from os import makedirs as os_makedirs
from os import path as os_path 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 import Overlay
from models.overlay.overlay_image import OverlayImage from models.overlay.overlay_image import OverlayImage
from models.watchdog.watchdog import Watchdog from models.watchdog.watchdog import Watchdog
from models.websocket.websocket_server import WebSocketServer
from utils import errorLogging, setupLogger from utils import errorLogging, setupLogger
class threadFnc(Thread): class threadFnc(Thread):
@@ -99,6 +102,10 @@ class Model:
self.kks = kakasi() self.kks = kakasi()
self.watchdog = Watchdog(config.WATCHDOG_TIMEOUT, config.WATCHDOG_INTERVAL) self.watchdog = Watchdog(config.WATCHDOG_TIMEOUT, config.WATCHDOG_INTERVAL)
self.osc_handler = OSCHandler(config.OSC_IP_ADDRESS, config.OSC_PORT) 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): def checkTranslatorCTranslate2ModelWeight(self, weight_type:str):
return checkCTranslate2Weight(config.PATH_LOCAL, weight_type) return checkCTranslate2Weight(config.PATH_LOCAL, weight_type)
@@ -292,11 +299,8 @@ class Model:
def oscSendMessage(self, message:str): def oscSendMessage(self, message:str):
self.osc_handler.sendMessage(message=message, notification=config.NOTIFICATION_VRC_SFX) self.osc_handler.sendMessage(message=message, notification=config.NOTIFICATION_VRC_SFX)
def getMuteSelfStatus(self):
return self.osc_handler.getOSCParameterMuteSelf()
def setMuteSelfStatus(self): def setMuteSelfStatus(self):
self.mic_mute_status = self.getMuteSelfStatus() self.mic_mute_status = self.osc_handler.getOSCParameterMuteSelf()
def startReceiveOSC(self): def startReceiveOSC(self):
def changeHandlerMute(address, osc_arguments): def changeHandlerMute(address, osc_arguments):
@@ -311,11 +315,15 @@ class Model:
dict_filter_and_target = { dict_filter_and_target = {
self.osc_handler.osc_parameter_muteself: changeHandlerMute, 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): def stopReceiveOSC(self):
self.osc_handler.oscServerStop() self.osc_handler.oscServerStop()
def getIsOscQueryEnabled(self):
return self.osc_handler.getIsOscQueryEnabled()
@staticmethod @staticmethod
def checkSoftwareUpdated(): def checkSoftwareUpdated():
# check update # check update
@@ -503,11 +511,15 @@ class Model:
def changeMicTranscriptStatus(self): def changeMicTranscriptStatus(self):
if config.VRC_MIC_MUTE_SYNC is True: if config.VRC_MIC_MUTE_SYNC is True:
if self.mic_mute_status is True: match self.mic_mute_status:
case True:
self.pauseMicTranscript() self.pauseMicTranscript()
elif self.mic_mute_status is False: case False:
self.resumeMicTranscript() self.resumeMicTranscript()
else: case None:
# mute selfの状態が不明な場合は一時停止しない
self.resumeMicTranscript()
case _:
pass pass
else: else:
self.resumeMicTranscript() self.resumeMicTranscript()
@@ -827,4 +839,86 @@ class Model:
self.th_watchdog.join() self.th_watchdog.join()
self.th_watchdog = None 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() model = Model()

View File

@@ -7,10 +7,22 @@ from tinyoscquery.queryservice import OSCQueryService
from tinyoscquery.query import OSCQueryBrowser, OSCQueryClient from tinyoscquery.query import OSCQueryBrowser, OSCQueryClient
from tinyoscquery.utility import get_open_udp_port, get_open_tcp_port from tinyoscquery.utility import get_open_udp_port, get_open_tcp_port
from tinyoscquery.shared.node import OSCAccess 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: class OSCHandler:
def __init__(self, ip_address="127.0.0.1", port=9000) -> None: 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_ip_address = ip_address
self.osc_port = port self.osc_port = port
self.osc_parameter_muteself = "/avatar/parameters/MuteSelf" self.osc_parameter_muteself = "/avatar/parameters/MuteSelf"
@@ -24,15 +36,28 @@ class OSCHandler:
self.osc_server_ip_address = ip_address self.osc_server_ip_address = ip_address
self.http_port = None self.http_port = None
self.osc_server_port = None self.osc_server_port = None
self.dict_filter_and_target = {}
self.browser = None self.browser = None
def getIsOscQueryEnabled(self) -> bool:
return self.is_osc_query_enabled
def setOscIpAddress(self, ip_address:str) -> None: 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.osc_ip_address = ip_address
self.udp_client = udp_client.SimpleUDPClient(self.osc_ip_address, self.osc_port) self.udp_client = udp_client.SimpleUDPClient(self.osc_ip_address, self.osc_port)
self.receiveOscParameters()
def setOscPort(self, port:int) -> None: def setOscPort(self, port:int) -> None:
self.oscServerStop()
self.osc_port = port self.osc_port = port
self.udp_client = udp_client.SimpleUDPClient(self.osc_ip_address, self.osc_port) self.udp_client = udp_client.SimpleUDPClient(self.osc_ip_address, self.osc_port)
self.receiveOscParameters()
# send OSC message typing # send OSC message typing
def sendTyping(self, flag:bool=False) -> None: 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]) self.udp_client.send_message(self.osc_parameter_chatbox_input, [f"{message}", True, notification])
def getOSCParameterValue(self, address:str) -> Any: def getOSCParameterValue(self, address:str) -> Any:
if not self.is_osc_query_enabled:
# OSCQueryが無効な場合はNoneを返す
return None
value = None value = None
try: try:
# browserインスタンスを再利用し、毎回の生成と破棄を避ける # browserインスタンスを再利用し、毎回の生成と破棄を避ける
@@ -71,19 +100,26 @@ class OSCHandler:
def getOSCParameterMuteSelf(self) -> bool: def getOSCParameterMuteSelf(self) -> bool:
return self.getOSCParameterValue(self.osc_parameter_muteself) 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.osc_server_port = get_open_udp_port()
self.http_port = get_open_tcp_port() self.http_port = get_open_tcp_port()
osc_dispatcher = dispatcher.Dispatcher() 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) 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() Thread(target=self.oscServerServe, daemon=True).start()
while True: while True:
try: try:
self.osc_query_service = OSCQueryService(self.osc_query_service_name, self.http_port, self.osc_server_port) 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) self.osc_query_service.advertise_endpoint(filter, access=OSCAccess.READWRITE_VALUE)
break break
except Exception: except Exception:
@@ -112,12 +148,26 @@ class OSCHandler:
if __name__ == "__main__": if __name__ == "__main__":
handler = OSCHandler() handler = OSCHandler()
handler.receiveOscParameters({ handler.setDictFilterAndTarget({
"/avatar/parameters/MuteSelf": print, "/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) sleep(5)
handler.sendTyping(True) handler.sendTyping(True)
sleep(1) sleep(1)
handler.sendMessage(message="Hello World", notification=True) handler.sendMessage(message="Hello World 1", notification=True)
sleep(60) 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() handler.oscServerStop()

View File

@@ -0,0 +1,4 @@
# WebSocketサーバーモジュール
from .websocket_server import WebSocketServer
__all__ = ["WebSocketServer"]

View File

@@ -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...")

View File

@@ -8,6 +8,7 @@ from logging.handlers import RotatingFileHandler
from ctranslate2 import get_supported_compute_types from ctranslate2 import get_supported_compute_types
import requests import requests
import ipaddress import ipaddress
import socket
def isConnectedNetwork(url="http://www.google.com", timeout=3) -> bool: def isConnectedNetwork(url="http://www.google.com", timeout=3) -> bool:
try: try:
@@ -16,6 +17,25 @@ def isConnectedNetwork(url="http://www.google.com", timeout=3) -> bool:
except requests.RequestException: except requests.RequestException:
return False 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: def isValidIpAddress(ip_address: str) -> bool:
try: try:
ipaddress.ip_address(ip_address) ipaddress.ip_address(ip_address)

View File

@@ -25,6 +25,7 @@
--error_bc_active_color: #9c3938; --error_bc_active_color: #9c3938;
--success_bc_color: #368777; --success_bc_color: #368777;
--waring_color: #cb944f; --waring_color: #cb944f;
--waring_bc_color: #cf7b1b;
--dark_basic_text_color: #f2f2f2; --dark_basic_text_color: #f2f2f2;
--dark_100_color: #f5f7fb; --dark_100_color: #f5f7fb;

View File

@@ -4,22 +4,9 @@ import MUI_Slider from "@mui/material/Slider";
import clsx from "clsx"; import clsx from "clsx";
export const Slider = (props) => { export const Slider = (props) => {
return ( const location = props.valueLabelDisplayLocation || "top";
<div className={clsx(styles.container, props.className, {[styles.no_padding]: props.no_padding || props.is_break_point})}>
<MUI_Slider const sliderSx = {
aria-label="Default"
valueLabelDisplay="auto"
value={props.variable}
step={props.step}
min={Number(props.min)}
max={Number(props.max)}
onChange={(_e, value) => props.onchangeFunction(value)}
onChangeCommitted={(_e, value) => props.onchangeCommittedFunction ? props.onchangeCommittedFunction(value) : null}
marks={props.marks}
track={props.track}
orientation={props.orientation}
valueLabelFormat={`${props.valueLabelFormat ? props.valueLabelFormat : props.variable}`}
sx={{
color: "var(--dark_700_color)", color: "var(--dark_700_color)",
"& .MuiSlider-thumb": { "& .MuiSlider-thumb": {
backgroundColor: "var(--primary_600_color)", backgroundColor: "var(--primary_600_color)",
@@ -27,10 +14,54 @@ export const Slider = (props) => {
boxShadow: `0 0 0 0.8rem var(--primary_600_color_44)`, boxShadow: `0 0 0 0.8rem var(--primary_600_color_44)`,
}, },
"& .MuiSlider-valueLabel": { "& .MuiSlider-valueLabel": {
fontSize: "1.4rem", position: "absolute",
backgroundColor: "var(--dark_800_color)", backgroundColor: "var(--dark_800_color)",
padding: "0.6rem 1rem", width: "fit-content",
minWidth: "4.8rem",
padding: "0.4rem 0.8rem",
lineHeight: "1.15", 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": { "& .MuiSlider-markLabel": {
@@ -41,7 +72,39 @@ export const Slider = (props) => {
"& .MuiSlider-markLabelActive": { "& .MuiSlider-markLabelActive": {
color: "var(--primary_300_color)", color: "var(--primary_300_color)",
}, },
}} };
return (
<div
className={clsx(
styles.container,
props.className,
{ [styles.no_padding]: props.no_padding || props.is_break_point }
)}
>
<MUI_Slider
aria-label="Default"
// valueLabelDisplay="on"
valueLabelDisplay={props.valueLabelDisplay ? props.valueLabelDisplay : "auto"}
value={props.variable}
step={props.step}
min={Number(props.min)}
max={Number(props.max)}
onChange={(_e, value) => props.onchangeFunction(value)}
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={sliderSx}
/> />
</div> </div>
); );

View File

@@ -6,13 +6,19 @@ import { useOpenFolder } from "@logics_common";
import { import {
useOscIpAddress, useOscIpAddress,
useOscPort, useOscPort,
useWebsocket,
} from "@logics_configs"; } from "@logics_configs";
import { import {
CheckboxContainer,
ActionButtonContainer, ActionButtonContainer,
EntryWithSaveButtonContainer, EntryWithSaveButtonContainer,
} from "../_templates/Templates"; } from "../_templates/Templates";
import {
SectionLabelComponent,
} from "../_components/";
import OpenFolderSvg from "@images/open_folder.svg?react"; import OpenFolderSvg from "@images/open_folder.svg?react";
import HelpSvg from "@images/help.svg?react"; import HelpSvg from "@images/help.svg?react";
@@ -25,6 +31,7 @@ export const AdvancedSettings = () => {
<OpenConfigFolderContainer /> <OpenConfigFolderContainer />
<OpenSwitchComputeDeviceModalContainer /> <OpenSwitchComputeDeviceModalContainer />
</div> </div>
<WebsocketContainer />
</div> </div>
); );
}; };
@@ -87,6 +94,7 @@ const OscPortContainer = () => {
/> />
); );
}; };
const OpenConfigFolderContainer = () => { const OpenConfigFolderContainer = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { openFolder_ConfigFile } = useOpenFolder(); const { openFolder_ConfigFile } = useOpenFolder();
@@ -121,3 +129,87 @@ const OpenSwitchComputeDeviceModalContainer = () => {
</> </>
); );
}; };
const WebsocketContainer = () => {
return (
<div>
<SectionLabelComponent label="WebSocket" />
<EnableWebsocketContainer />
<WebsocketHostContainer />
<WebsocketPortContainer />
</div>
);
};
const EnableWebsocketContainer = () => {
const { t } = useTranslation();
const { currentEnableWebsocket, toggleEnableWebsocket } = useWebsocket();
return (
<CheckboxContainer
label={t("config_page.advanced_settings.enable_websocket.label")}
variable={currentEnableWebsocket}
toggleFunction={toggleEnableWebsocket}
/>
);
};
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 (
<EntryWithSaveButtonContainer
label={t("config_page.advanced_settings.websocket_host.label")}
variable={input_value}
saveFunction={saveFunction}
onChangeFunction={onChangeFunction}
state={currentWebsocketHost.state}
width="14rem"
/>
);
};
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 (
<EntryWithSaveButtonContainer
label={t("config_page.advanced_settings.websocket_port.label")}
variable={input_value}
saveFunction={saveFunction}
onChangeFunction={onChangeFunction}
state={currentWebsocketPort.state}
width="10rem"
/>
);
};

View File

@@ -101,11 +101,17 @@ export const VrcMicMuteSyncContainer = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { currentEnableVrcMicMuteSync, toggleEnableVrcMicMuteSync } = useEnableVrcMicMuteSync(); const { currentEnableVrcMicMuteSync, toggleEnableVrcMicMuteSync } = useEnableVrcMicMuteSync();
const variable = {
state: currentEnableVrcMicMuteSync.state,
data: currentEnableVrcMicMuteSync.data.is_enabled,
};
return ( return (
<CheckboxContainer <CheckboxContainer
label={t("config_page.others.vrc_mic_mute_sync.label")} label={t("config_page.others.vrc_mic_mute_sync.label")}
desc={t("config_page.others.vrc_mic_mute_sync.desc")} desc={t("config_page.others.vrc_mic_mute_sync.desc")}
variable={currentEnableVrcMicMuteSync} variable={variable}
is_available={currentEnableVrcMicMuteSync.data.is_available}
toggleFunction={toggleEnableVrcMicMuteSync} toggleFunction={toggleEnableVrcMicMuteSync}
/> />
); );

View File

@@ -272,6 +272,7 @@ const SupporterPeriodContainer = ({ settings, calc_support_period }) => {
return ( return (
<div className={styles.supporter_period_container}> <div className={styles.supporter_period_container}>
<div className={styles.supporter_period_wrapper}>
{Object.entries(period_data).map(([key, item], index) => { {Object.entries(period_data).map(([key, item], index) => {
if (item === "") return null; if (item === "") return null;
const period_box_class_name = clsx(styles.period_box, { const period_box_class_name = clsx(styles.period_box, {
@@ -297,6 +298,7 @@ const SupporterPeriodContainer = ({ settings, calc_support_period }) => {
); );
})} })}
</div> </div>
</div>
); );
}; };

View File

@@ -13,7 +13,7 @@
align-content: start; align-content: start;
flex-wrap: wrap; flex-wrap: wrap;
column-gap: 1.8rem; column-gap: 1.8rem;
row-gap: 0.4rem; row-gap: 2rem;
} }
.supporter_image_container { .supporter_image_container {
@@ -209,13 +209,24 @@ $progress_ease: cubic-bezier(0, 1, 0.75, 1);
.supporter_period_container { .supporter_period_container {
position: absolute;
top: 100%;
left: 0;
}
.supporter_period_wrapper {
display: flex; display: flex;
gap: 0.2rem; gap: 0.4rem 0.2rem;
padding-left: 0.4rem; flex-shrink: 0;
flex-wrap: wrap;
padding: 0.3rem 0.4rem 0.4rem 0.4rem;
}
.period_box_wrapper {
flex-shrink: 0;
} }
.period_box { .period_box {
width: 1.8rem; width: 1.7rem;
height: 0.3rem; height: 0.3rem;
border-radius: 0.3rem; border-radius: 0.3rem;
&.mogu_bar { &.mogu_bar {
@@ -231,9 +242,7 @@ $progress_ease: cubic-bezier(0, 1, 0.75, 1);
background-color: var(--dark_800_color); background-color: var(--dark_800_color);
} }
} }
.period_box_wrapper {
padding: 0.3rem 0 0.4rem 0;
}
.tooltip_period_label { .tooltip_period_label {
font-size: 1.4rem; font-size: 1.4rem;
} }

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect, useRef } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import clsx from "clsx"; import clsx from "clsx";
import styles from "./Vr.module.scss"; import styles from "./Vr.module.scss";
@@ -25,6 +25,10 @@ import {
import RedoSvg from "@images/redo.svg?react"; 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 = () => { export const Vr = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const [is_opened_small_settings, setIsOpenedSmallSettings] = useState(true); const [is_opened_small_settings, setIsOpenedSmallSettings] = useState(true);
@@ -187,9 +191,34 @@ const PageSwitcherContainer = (props) => {
); );
}; };
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 { 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 ( return (
<div className={styles.position_controls}> <div className={styles.position_controls}>
<div className={styles.position_wrapper}> <div className={styles.position_wrapper}>
@@ -205,8 +234,18 @@ const PositionControls = ({settings, onchangeFunction, selectFunction, ui_config
min={ui_configs.x_pos.min} min={ui_configs.x_pos.min}
max={ui_configs.x_pos.max} max={ui_configs.x_pos.max}
onchangeFunction={(value) => onchangeFunction("x_pos", value)} onchangeFunction={(value) => onchangeFunction("x_pos", value)}
valueLabelDisplay={x_variable_display}
valueLabelDisplayLocation="top"
/>
<AdjustButtonContainer
wrapper_class_name={styles.x_position_button_wrapper}
is_max={is_max_position_x}
is_min={is_min_position_x}
countUp={countUpPositionX}
countDown={countDownPositionX}
/> />
</div> </div>
<div className={styles.position_wrapper}> <div className={styles.position_wrapper}>
<p className={clsx(styles.slider_label, styles.y_position_label)}> <p className={clsx(styles.slider_label, styles.y_position_label)}>
{t("config_page.vr.y_position")} {t("config_page.vr.y_position")}
@@ -221,8 +260,18 @@ const PositionControls = ({settings, onchangeFunction, selectFunction, ui_config
max={ui_configs.y_pos.max} max={ui_configs.y_pos.max}
onchangeFunction={(value) => onchangeFunction("y_pos", value)} onchangeFunction={(value) => onchangeFunction("y_pos", value)}
orientation="vertical" orientation="vertical"
valueLabelDisplay={y_variable_display}
valueLabelDisplayLocation="right"
/>
<AdjustButtonContainer
wrapper_class_name={styles.y_position_button_wrapper}
is_max={is_max_position_y}
is_min={is_min_position_y}
countUp={countUpPositionY}
countDown={countDownPositionY}
/> />
</div> </div>
<div className={styles.position_wrapper}> <div className={styles.position_wrapper}>
<p className={clsx(styles.slider_label, styles.z_position_label)}> <p className={clsx(styles.slider_label, styles.z_position_label)}>
{t("config_page.vr.z_position")} {t("config_page.vr.z_position")}
@@ -237,69 +286,162 @@ const PositionControls = ({settings, onchangeFunction, selectFunction, ui_config
max={ui_configs.z_pos.max} max={ui_configs.z_pos.max}
onchangeFunction={(value) => onchangeFunction("z_pos", value)} onchangeFunction={(value) => onchangeFunction("z_pos", value)}
orientation="vertical" orientation="vertical"
valueLabelDisplay={z_variable_display}
valueLabelDisplayLocation="left"
/>
<AdjustButtonContainer
wrapper_class_name={styles.z_position_button_wrapper}
is_max={is_max_position_z}
is_min={is_min_position_z}
countUp={countUpPositionZ}
countDown={countDownPositionZ}
/> />
</div> </div>
</div> </div>
); );
}; };
const RotationControls = ({settings, onchangeFunction, selectFunction, default_ui_configs}) => { export const RotationControls = ({ settings, onchangeFunction, selectFunction, ui_configs, default_ui_configs }) => {
const { t } = useTranslation(); 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 ( return (
<div className={styles.rotation_controls}> <div className={styles.rotation_controls}>
<div className={styles.rotation_wrapper}> <div className={styles.rotation_wrapper}>
<p className={clsx(styles.slider_label, styles.x_rotation_label)}> <p className={clsx(styles.slider_label, styles.x_rotation_label)}>
{t("config_page.vr.x_rotation")} {t("config_page.vr.x_rotation")}
<ResetButton onClickFunction={() => selectFunction("x_rotation", default_ui_configs.y_pos)} /> <ResetButton onClickFunction={() => selectFunction("x_rotation", default_ui_configs.x_rotation)} />
</p> </p>
<Slider <Slider
className={styles.x_rotation_slider} className={styles.x_rotation_slider}
no_padding={true} no_padding={true}
variable={-settings.x_rotation} variable={-settings.x_rotation}
valueLabelFormat={settings.x_rotation} valueLabelFormat={settings.x_rotation}
step={5} step={ui_configs.x_rotation.step}
min={-180} min={ui_configs.x_rotation.min}
max={180} max={ui_configs.x_rotation.max}
onchangeFunction={(value) => onchangeFunction("x_rotation", -value)} onchangeFunction={(value) => onchangeFunction("x_rotation", -value)}
orientation="vertical" orientation="vertical"
valueLabelDisplay={x_variable_display}
valueLabelDisplayLocation="right"
/>
<AdjustButtonContainer
wrapper_class_name={styles.x_rotation_button_wrapper}
is_max={is_min_rotation_x}
is_min={is_max_rotation_x}
countUp={countDownRotationX}
countDown={countUpRotationX}
/> />
</div> </div>
<div className={styles.rotation_wrapper}> <div className={styles.rotation_wrapper}>
<p className={clsx(styles.slider_label, styles.y_rotation_label)}> <p className={clsx(styles.slider_label, styles.y_rotation_label)}>
{t("config_page.vr.y_rotation")} {t("config_page.vr.y_rotation")}
<ResetButton onClickFunction={() => selectFunction("y_rotation", default_ui_configs.y_pos)} /> <ResetButton onClickFunction={() => selectFunction("y_rotation", default_ui_configs.y_rotation)} />
</p> </p>
<Slider <Slider
className={styles.y_rotation_slider} className={styles.y_rotation_slider}
no_padding={true} no_padding={true}
variable={settings.y_rotation} variable={settings.y_rotation}
step={5} step={ui_configs.y_rotation.step}
min={-180} min={ui_configs.y_rotation.min}
max={180} max={ui_configs.y_rotation.max}
onchangeFunction={(value) => onchangeFunction("y_rotation", value)} onchangeFunction={(value) => onchangeFunction("y_rotation", value)}
valueLabelDisplay={y_variable_display}
valueLabelDisplayLocation="top"
/>
<AdjustButtonContainer
wrapper_class_name={styles.y_rotation_button_wrapper}
is_max={is_max_rotation_y}
is_min={is_min_rotation_y}
countUp={countUpRotationY}
countDown={countDownRotationY}
/> />
</div> </div>
<div className={styles.rotation_wrapper}> <div className={styles.rotation_wrapper}>
<p className={clsx(styles.slider_label, styles.z_rotation_label)}> <p className={clsx(styles.slider_label, styles.z_rotation_label)}>
{t("config_page.vr.z_rotation")} {t("config_page.vr.z_rotation")}
<ResetButton onClickFunction={() => selectFunction("z_rotation", default_ui_configs.y_pos)} /> <ResetButton onClickFunction={() => selectFunction("z_rotation", default_ui_configs.z_rotation)} />
</p> </p>
<Slider <Slider
className={styles.z_rotation_slider} className={styles.z_rotation_slider}
no_padding={true} no_padding={true}
variable={settings.z_rotation} variable={settings.z_rotation}
step={5} step={ui_configs.z_rotation.step}
min={-180} min={ui_configs.z_rotation.min}
max={180} max={ui_configs.z_rotation.max}
onchangeFunction={(value) => onchangeFunction("z_rotation", value)} onchangeFunction={(value) => onchangeFunction("z_rotation", value)}
orientation="vertical" orientation="vertical"
valueLabelDisplay={z_variable_display}
valueLabelDisplayLocation="left"
/>
<AdjustButtonContainer
wrapper_class_name={styles.z_rotation_button_wrapper}
is_max={is_max_rotation_z}
is_min={is_min_rotation_z}
countUp={countUpRotationZ}
countDown={countDownRotationZ}
/> />
</div> </div>
</div> </div>
); );
}; };
const AdjustButtonContainer = ({ wrapper_class_name, is_max, is_min, countUp, countDown }) => {
return (
<div className={wrapper_class_name}>
<div
className={clsx(
styles.button_wrapper,
{
[styles.is_disabled]: is_max,
[styles.up]: true,
}
)}
onClick={countUp}
>
<TriangleSvg className={styles.adjust_button_triangle_svg} />
</div>
<div
className={clsx(
styles.button_wrapper,
{
[styles.is_disabled]: is_min,
}
)}
onClick={countDown}
>
<TriangleSvg className={styles.adjust_button_triangle_svg} />
</div>
</div>
);
};
const OtherControls = ({settings, onchangeFunction, ui_configs}) => { const OtherControls = ({settings, onchangeFunction, ui_configs}) => {
const { t } = useTranslation(); 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 SendSampleTextToggleButton = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { sendTextToOverlay } = useSendTextToOverlay(); const { sendTextToOverlay } = useSendTextToOverlay();
@@ -446,3 +584,57 @@ const SendSampleTextToggleButton = () => {
</div> </div>
); );
}; };
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,
};
};

View File

@@ -16,7 +16,7 @@
justify-content: center; justify-content: center;
width: 100%; width: 100%;
max-width: 56rem; max-width: 56rem;
gap: 2rem; gap: 4rem;
} }
.controller_type_switch { .controller_type_switch {
@@ -58,12 +58,13 @@
.sample_text_button_wrapper { .sample_text_button_wrapper {
position: absolute; position: absolute;
bottom: 0; bottom: -12%;
left: -74%; left: -80%;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
flex-direction: column; flex-direction: column;
// transform: translate(-50%, -50%);
} }
.sample_text_button { .sample_text_button {
background-color: var(--dark_850_color); background-color: var(--dark_850_color);
@@ -121,20 +122,20 @@
} }
.x_position_label { .x_position_label {
position: absolute; position: absolute;
bottom: -4.6rem; bottom: -5rem;
right: -46%; right: -46%;
justify-content: end; justify-content: end;
} }
.y_position_label { .y_position_label {
position: absolute; position: absolute;
top: -44%; bottom: 110%;
left: 10%; right: 119%;
justify-content: start; justify-content: end;
} }
.z_position_label { .z_position_label {
position: absolute; position: absolute;
top: 30%; top: 14%;
left: 80%; left: 110%;
} }
.x_position_slider { .x_position_slider {
@@ -155,14 +156,84 @@
.z_position_slider { .z_position_slider {
position: absolute; position: absolute;
bottom: 61%; bottom: 80%;
left: 61%; left: 88%;
transform: translate(50%,50%) rotate(45deg); transform: translate(50%,50%) rotate(45deg);
width: 0%; width: 0%;
height: 100%; 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 { // .rotation_controls {
@@ -175,19 +246,20 @@
.x_rotation_label { .x_rotation_label {
position: absolute; position: absolute;
top: -44%; bottom: 110%;
left: 10%; right: 119%;
justify-content: end;
} }
.y_rotation_label { .y_rotation_label {
position: absolute; position: absolute;
bottom: -4.6rem; bottom: -5rem;
right: -46%; right: -46%;
justify-content: end; justify-content: end;
} }
.z_rotation_label { .z_rotation_label {
position: absolute; position: absolute;
top: -10%; top: -20%;
right: -110%; right: -100%;
} }
.x_rotation_slider { .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 { .slider_reset_button {
background-color: var(--dark_875_color); background-color: var(--dark_875_color);
padding: 0.6rem; padding: 0.6rem;

View File

@@ -77,7 +77,7 @@ const OpenVrcMicMuteSyncQuickSetting = () => {
return ( return (
<OpenQuickSettingButton <OpenQuickSettingButton
label={t("config_page.others.vrc_mic_mute_sync.label")} label={t("config_page.others.vrc_mic_mute_sync.label")}
variable={currentEnableVrcMicMuteSync.data} variable={currentEnableVrcMicMuteSync.data.is_enabled}
onClickFunction={onClickFunction} onClickFunction={onClickFunction}
/> />
); );

View File

@@ -59,7 +59,7 @@ export const UpdateModal = () => {
</div> </div>
<div className={styles.cuda_section}> <div className={styles.cuda_section}>
<div className={styles.button_wrapper}> <div className={styles.button_wrapper}>
<button className={cuda_accept_button_class_name} onClick={onClickUpdateSoftware_CUDA}>CUDA (GPU)</button> <button className={cuda_accept_button_class_name} onClick={onClickUpdateSoftware_CUDA}>CUDA (CPU/GPU)</button>
{!is_cpu_version ? <CurrentVersionLabel is_latest_version_already={is_latest_version_already} is_cuda={true}/> : null} {!is_cpu_version ? <CurrentVersionLabel is_latest_version_already={is_latest_version_already} is_cuda={true}/> : null}
</div> </div>
<div className={styles.version_desc_container}> <div className={styles.version_desc_container}>
@@ -85,7 +85,7 @@ const VersionDescComponent = (props) => {
return ( return (
<div className={styles.version_desc_wrapper}> <div className={styles.version_desc_wrapper}>
<div className={styles.version_desc_point}></div> <div className={styles.version_desc_point}></div>
<p className={styles.version_desc}>{props.desc}</p> <p className={styles.version_desc}>{`- ${props.desc}`}</p>
</div> </div>
); );
}; };

View File

@@ -14,6 +14,7 @@ export const SnackbarController = () => {
const snackbar_classname = clsx(styles.snackbar_content, { const snackbar_classname = clsx(styles.snackbar_content, {
[styles.is_success]: currentNotificationStatus.data.status === "success", [styles.is_success]: currentNotificationStatus.data.status === "success",
[styles.is_warning]: currentNotificationStatus.data.status === "warning",
[styles.is_error]: currentNotificationStatus.data.status === "error", [styles.is_error]: currentNotificationStatus.data.status === "error",
}); });

View File

@@ -6,6 +6,9 @@
&.is_success { &.is_success {
background-color: var(--success_bc_color); background-color: var(--success_bc_color);
} }
&.is_warning {
background-color: var(--waring_bc_color);
}
&.is_error { &.is_error {
background-color: var(--error_bc_color); background-color: var(--error_bc_color);
} }

View File

@@ -3,15 +3,16 @@ import styles from "./Checkbox.module.scss";
export const Checkbox = ({ export const Checkbox = ({
checkboxId, checkboxId,
variable, variable,
is_available = true,
toggleFunction, toggleFunction,
size = "2.8rem", size = "2.8rem",
color = "var(--primary_600_color)",
borderWidth = "0.2rem", borderWidth = "0.2rem",
padding = "2rem", padding = "2rem",
}) => { }) => {
const wrapper_class_names = clsx(styles.checkbox_wrapper, { 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 ( return (
@@ -21,7 +22,6 @@ export const Checkbox = ({
htmlFor={checkboxId} htmlFor={checkboxId}
style={{ style={{
"--checkbox-size": size, "--checkbox-size": size,
"--checkbox-color": color,
"--checkbox-border-width": borderWidth, "--checkbox-border-width": borderWidth,
"--checkbox-padding": padding, "--checkbox-padding": padding,
}} }}

View File

@@ -18,12 +18,19 @@
border: var(--checkbox-color, var(--primary_600_color)) solid var(--checkbox-border-width, 0.2rem); border: var(--checkbox-color, var(--primary_600_color)) solid var(--checkbox-border-width, 0.2rem);
} }
} }
&.is_disabled { &.is_pending {
pointer-events: none; pointer-events: none;
& .cbx { & .cbx {
border-color: var(--primary_800_color); border-color: var(--primary_800_color);
} }
} }
&.is_disabled {
pointer-events: none;
& .cbx {
filter: grayscale(100%);
border-color: var(--dark_800_color);
}
}
} }
.checkbox_wrapper .cbx { .checkbox_wrapper .cbx {

View File

@@ -16,6 +16,7 @@ import {
useDeepLAuthKey, useDeepLAuthKey,
useOscIpAddress, useOscIpAddress,
useWebsocket,
} from "@logics_configs"; } from "@logics_configs";
import { ui_configs } from "../ui_configs"; import { ui_configs } from "../ui_configs";
@@ -31,106 +32,144 @@ export const _useBackendErrorHandling = () => {
const { updateSpeakerPhraseTimeout } = useSpeakerPhraseTimeout(); const { updateSpeakerPhraseTimeout } = useSpeakerPhraseTimeout();
const { updateSpeakerMaxWords } = useSpeakerMaxWords(); const { updateSpeakerMaxWords } = useSpeakerMaxWords();
const { updateDeepLAuthKey, saveErrorDeepLAuthKey } = useDeepLAuthKey(); const { updateDeepLAuthKey } = useDeepLAuthKey();
const { updateOscIpAddress } = useOscIpAddress(); const { updateOscIpAddress } = useOscIpAddress();
const { updateEnableWebsocket, updateWebsocketHost, updateWebsocketPort } = useWebsocket();
const errorHandling_Backend = ({message, data, endpoint, _result}) => { const errorHandling_Backend = ({message, data, endpoint, result}) => {
switch (message) { switch (endpoint) {
case "No mic device detected": case "/run/error_device":
showNotification_Error(t("common_error.no_device_mic")); if (message === "No mic device detected") showNotification_Error(t("common_error.no_device_mic"));
break; if (message === "No speaker device detected") showNotification_Error(t("common_error.no_device_speaker"));
case "No speaker device detected": return;
showNotification_Error(t("common_error.no_device_speaker"));
break;
case "Mic energy threshold value is out of range": case "/set/data/mic_threshold":
if (message === "Mic energy threshold value is out of range") {
showNotification_Error(t("common_error.threshold_invalid_value", showNotification_Error(t("common_error.threshold_invalid_value",
{ min: ui_configs.mic_threshold_min, max: ui_configs.mic_threshold_max }, { min: ui_configs.mic_threshold_min, max: ui_configs.mic_threshold_max },
)); ));
break; };
case "Speaker energy threshold value is out of range": return;
case "/set/data/speaker_threshold":
if (message === "Speaker energy threshold value is out of range") {
showNotification_Error(t("common_error.threshold_invalid_value", showNotification_Error(t("common_error.threshold_invalid_value",
{ min: ui_configs.speaker_threshold_min, max: ui_configs.speaker_threshold_max }, { min: ui_configs.speaker_threshold_min, max: ui_configs.speaker_threshold_max },
)); ));
break; }
return;
case "CTranslate2 weight download error": case "/run/error_ctranslate2_weight":
showNotification_Error(t("common_error.failed_download_weight_ctranslate2")); if (message === "CTranslate2 weight download error") showNotification_Error(t("common_error.failed_download_weight_ctranslate2"));
break; return;
case "Whisper weight download error": case "/run/error_whisper_weight":
showNotification_Error(t("common_error.failed_download_weight_whisper")); if (message === "Whisper weight download error") showNotification_Error(t("common_error.failed_download_weight_whisper"));
break; return;
case "Translation engine limit error": case "/run/error_translation_engine":
showNotification_Error(t("common_error.translation_limit")); if (message === "Translation engine limit error") showNotification_Error(t("common_error.translation_limit"));
break; return;
case "DeepL auth key length is not correct": case "/set/data/deepl_auth_key":
if (message === "DeepL auth key length is not correct") {
updateDeepLAuthKey(data); updateDeepLAuthKey(data);
showNotification_Error(t("common_error.deepl_auth_key_invalid_length")); showNotification_Error(t("common_error.deepl_auth_key_invalid_length"));
break; } else if (message === "Authentication failure of deepL auth key") {
case "Authentication failure of deepL auth key":
updateDeepLAuthKey(data); updateDeepLAuthKey(data);
showNotification_Error(t("common_error.deepl_auth_key_failed_authentication")); showNotification_Error(t("common_error.deepl_auth_key_failed_authentication"));
break; } else { // Exception
updateDeepLAuthKey(data);
showNotification_Error(message);
}
return;
case "Mic record timeout value is out of range": case "/set/data/mic_record_timeout":
if (message === "Mic record timeout value is out of range") {
updateMicRecordTimeout(data); updateMicRecordTimeout(data);
showNotification_Error( showNotification_Error(t("common_error.invalid_value_mic_record_timeout", {
t("common_error.invalid_value_mic_record_timeout", mic_phrase_timeout_label: t("config_page.transcription.mic_phrase_timeout.label")
{ mic_phrase_timeout_label: t("config_page.transcription.mic_phrase_timeout.label") } }));
)); }
break; return;
case "Mic phrase timeout value is out of range": case "/set/data/mic_phrase_timeout":
if (message === "Mic phrase timeout value is out of range") {
updateMicPhraseTimeout(data); updateMicPhraseTimeout(data);
showNotification_Error( showNotification_Error(t("common_error.invalid_value_mic_phrase_timeout", {
t("common_error.invalid_value_mic_phrase_timeout", mic_record_timeout_label: t("config_page.transcription.mic_record_timeout.label")
{ mic_record_timeout_label: t("config_page.transcription.mic_record_timeout.label") } }));
)); }
break; return;
case "Mic max phrases value is out of range": case "/set/data/mic_max_phrases":
if (message === "Mic max phrases value is out of range") {
updateMicMaxWords(data); updateMicMaxWords(data);
showNotification_Error(t("common_error.invalid_value_mic_max_phrase")); showNotification_Error(t("common_error.invalid_value_mic_max_phrase"));
break; }
return;
case "Speaker record timeout value is out of range": case "/set/data/speaker_record_timeout":
if (message === "Speaker record timeout value is out of range") {
updateSpeakerRecordTimeout(data); updateSpeakerRecordTimeout(data);
showNotification_Error( showNotification_Error(t("common_error.invalid_value_speaker_record_timeout", {
t("common_error.invalid_value_speaker_record_timeout", speaker_phrase_timeout_label: t("config_page.transcription.speaker_phrase_timeout.label")
{ speaker_phrase_timeout_label: t("config_page.transcription.speaker_phrase_timeout.label") } }));
)); }
break; return;
case "Speaker phrase timeout value is out of range": case "/set/data/speaker_phrase_timeout":
if (message === "Speaker phrase timeout value is out of range") {
updateSpeakerPhraseTimeout(data); updateSpeakerPhraseTimeout(data);
showNotification_Error( showNotification_Error(t("common_error.invalid_value_speaker_phrase_timeout", {
t("common_error.invalid_value_speaker_phrase_timeout", speaker_record_timeout_label: t("config_page.transcription.speaker_record_timeout.label")
{ speaker_record_timeout_label: t("config_page.transcription.speaker_record_timeout.label") } }));
)); }
break; return;
case "Speaker max phrases value is out of range": case "/set/data/speaker_max_phrases":
if (message === "Speaker max phrases value is out of range") {
updateSpeakerMaxWords(data); updateSpeakerMaxWords(data);
showNotification_Error(t("common_error.invalid_value_speaker_max_phrase")); showNotification_Error(t("common_error.invalid_value_speaker_max_phrase"));
break; }
return;
// Advanced Settings, error messages are set by Backend (EN only) // Advanced Settings, error messages are set by Backend (EN only)
case "Invalid IP address": case "/set/data/osc_ip_address":
if (message === "Invalid IP address") {
updateOscIpAddress(data); updateOscIpAddress(data);
showNotification_Error(message); showNotification_Error(message);
break; } 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); case "/set/enable/websocket_server":
if (message === "WebSocket server host or port is not available") {
updateEnableWebsocket(data);
showNotification_Error(message); showNotification_Error(message);
break; }
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: default:
// determine by endpoint, not message. console.error(`Invalid endpoint or message: ${endpoint}\nmessage: ${message}\nresult: ${JSON.stringify(result)}`);
if (endpoint === "/set/data/deepl_auth_key") saveErrorDeepLAuthKey({message, data, endpoint, _result}); return;
break;
} }
} }

View File

@@ -11,5 +11,6 @@ export { useMessage } from "./useMessage";
export { useUpdateSoftware } from "./useUpdateSoftware"; export { useUpdateSoftware } from "./useUpdateSoftware";
export { useVolume } from "./useVolume"; export { useVolume } from "./useVolume";
export { useHandleNetworkConnection } from "./useHandleNetworkConnection"; export { useHandleNetworkConnection } from "./useHandleNetworkConnection";
export { useHandleOscQuery } from "./useHandleOscQuery";
export { useIsVrctAvailable } from "./useIsVrctAvailable"; export { useIsVrctAvailable } from "./useIsVrctAvailable";
export { useFetch } from "./useFetch"; export { useFetch } from "./useFetch";

View File

@@ -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,
};
};

View File

@@ -5,6 +5,16 @@ export const useNotificationStatus = () => {
const generateRandomKey = () => Math.random(); 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 = {}) => { const showNotification_Error = (message, options = {}) => {
updateNotificationStatus({ updateNotificationStatus({
status: "error", status: "error",
@@ -37,6 +47,7 @@ export const useNotificationStatus = () => {
currentNotificationStatus, currentNotificationStatus,
updateNotificationStatus, updateNotificationStatus,
showNotification_Warning,
showNotification_Error, showNotification_Error,
showNotification_Success, showNotification_Success,
closeNotification, closeNotification,

View File

@@ -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,
};
};

View File

@@ -56,6 +56,7 @@ export { useHotkeys } from "./hotkeys/useHotkeys";
export { useOscIpAddress } from "./advanced_settings/useOscIpAddress"; export { useOscIpAddress } from "./advanced_settings/useOscIpAddress";
export { useOscPort } from "./advanced_settings/useOscPort"; export { useOscPort } from "./advanced_settings/useOscPort";
export { useWebsocket } from "./advanced_settings/useWebsocket";
export { useSupporters } from "./supporters/useSupporters"; export { useSupporters } from "./supporters/useSupporters";

View File

@@ -12,7 +12,7 @@ export const useEnableVrcMicMuteSync = () => {
const toggleEnableVrcMicMuteSync = () => { const toggleEnableVrcMicMuteSync = () => {
pendingEnableVrcMicMuteSync(); pendingEnableVrcMicMuteSync();
if (currentEnableVrcMicMuteSync.data) { if (currentEnableVrcMicMuteSync.data.is_enabled) {
asyncStdoutToPython("/set/disable/vrc_mic_mute_sync"); asyncStdoutToPython("/set/disable/vrc_mic_mute_sync");
} else { } else {
asyncStdoutToPython("/set/enable/vrc_mic_mute_sync"); asyncStdoutToPython("/set/enable/vrc_mic_mute_sync");

View File

@@ -29,11 +29,6 @@ export const useDeepLAuthKey = () => {
showNotification_Success(t("config_page.translation.deepl_auth_key.auth_key_success")); showNotification_Success(t("config_page.translation.deepl_auth_key.auth_key_success"));
}; };
const saveErrorDeepLAuthKey = ({data, message}) => {
updateDeepLAuthKey(data);
showNotification_Error(message);
};
return { return {
currentDeepLAuthKey, currentDeepLAuthKey,
getDeepLAuthKey, getDeepLAuthKey,
@@ -41,7 +36,6 @@ export const useDeepLAuthKey = () => {
setDeepLAuthKey, setDeepLAuthKey,
deleteDeepLAuthKey, deleteDeepLAuthKey,
saveErrorDeepLAuthKey,
savedDeepLAuthKey, savedDeepLAuthKey,
}; };
}; };

View File

@@ -7,6 +7,7 @@ import {
useIsVrctAvailable, useIsVrctAvailable,
useNotificationStatus, useNotificationStatus,
useHandleNetworkConnection, useHandleNetworkConnection,
useHandleOscQuery,
useSoftwareVersion, useSoftwareVersion,
useComputeMode, useComputeMode,
@@ -75,6 +76,7 @@ import {
usePlugins, usePlugins,
useOscIpAddress, useOscIpAddress,
useOscPort, useOscPort,
useWebsocket,
} from "@logics_configs"; } from "@logics_configs";
export const useReceiveRoutes = () => { export const useReceiveRoutes = () => {
@@ -82,6 +84,7 @@ export const useReceiveRoutes = () => {
const { updateComputeMode } = useComputeMode(); const { updateComputeMode } = useComputeMode();
const { updateInitProgress } = useInitProgress(); const { updateInitProgress } = useInitProgress();
const { updateIsBackendReady } = useIsBackendReady(); const { updateIsBackendReady } = useIsBackendReady();
const { handleOscQuery } = useHandleOscQuery();
const { restoreWindowGeometry } = useWindow(); const { restoreWindowGeometry } = useWindow();
const { updateIsMainPageCompactMode } = useIsMainPageCompactMode(); const { updateIsMainPageCompactMode } = useIsMainPageCompactMode();
const { const {
@@ -180,6 +183,11 @@ export const useReceiveRoutes = () => {
const { updateOscIpAddress } = useOscIpAddress(); const { updateOscIpAddress } = useOscIpAddress();
const { updateOscPort } = useOscPort(); const { updateOscPort } = useOscPort();
const {
updateEnableWebsocket,
updateWebsocketHost,
updateWebsocketPort,
} = useWebsocket();
@@ -213,6 +221,12 @@ export const useReceiveRoutes = () => {
})); }));
}, },
"/run/connected_network": handleNetworkConnection, "/run/connected_network": handleNetworkConnection,
"/run/enable_osc_query": ({data, disabled_functions}) => {
handleOscQuery({
is_osc_query_enabled: data,
disabled_functions: disabled_functions,
});
},
// Main Page // Main Page
// Page Controls // Page Controls
@@ -474,9 +488,15 @@ export const useReceiveRoutes = () => {
"/set/enable/logger_feature": updateEnableAutoExportMessageLogs, "/set/enable/logger_feature": updateEnableAutoExportMessageLogs,
"/set/disable/logger_feature": updateEnableAutoExportMessageLogs, "/set/disable/logger_feature": updateEnableAutoExportMessageLogs,
"/get/data/vrc_mic_mute_sync": updateEnableVrcMicMuteSync, "/get/data/vrc_mic_mute_sync": (payload) => updateEnableVrcMicMuteSync((old_value) => {
"/set/enable/vrc_mic_mute_sync": updateEnableVrcMicMuteSync, return {...old_value.data, is_enabled: payload};
"/set/disable/vrc_mic_mute_sync": updateEnableVrcMicMuteSync, }),
"/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, "/get/data/send_message_to_vrc": updateEnableSendMessageToVrc,
"/set/enable/send_message_to_vrc": updateEnableSendMessageToVrc, "/set/enable/send_message_to_vrc": updateEnableSendMessageToVrc,
@@ -505,6 +525,16 @@ export const useReceiveRoutes = () => {
"/get/data/osc_port": updateOscPort, "/get/data/osc_port": updateOscPort,
"/set/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_avg_logprob": ()=>{}, // Not implemented on UI yet
"/get/data/mic_no_speech_prob": ()=>{}, // 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 "/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"]) "/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 receiveRoutes = (parsed_data) => {
const initDataSyncProcess = (payload) => { const initDataSyncProcess = (payload) => {
for (const [endpoint, value] of Object.entries(payload)) { for (const [endpoint, value] of Object.entries(payload)) {
@@ -567,17 +573,12 @@ export const useReceiveRoutes = () => {
break; break;
case 400: case 400:
const error_route = error_status_routes[parsed_data.endpoint]; errorHandling_Backend({
if (error_route) {
error_route({
message: parsed_data.result.message, message: parsed_data.result.message,
data: parsed_data.result.data, data: parsed_data.result.data,
endpoint: parsed_data.endpoint, endpoint: parsed_data.endpoint,
_result: parsed_data.result, result: parsed_data.result,
}); });
} else {
handleInvalidEndpoint(parsed_data);
}
break; break;
case 500: case 500:
showNotification_Error( showNotification_Error(

View File

@@ -60,10 +60,19 @@ export const createAtomWithHook = (initialValue, base_name, options) => {
}; };
const updateAtom = (payload, options = {}) => { const updateAtom = (payload, options = {}) => {
const { remain_state = false, set_state } = options; const { remain_state = false, set_state, lock_state } = options;
setAtom((currentValue) => { 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" const updated_data = typeof payload === "function"
? payload(currentValue) ? 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_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_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 // Supporters

View File

@@ -7,12 +7,18 @@ export const ui_configs = {
x_pos: { step: 0.05, min: -0.5, max: 0.5 }, x_pos: { step: 0.05, min: -0.5, max: 0.5 },
y_pos: { step: 0.05, min: -0.8, max: 0.8 }, y_pos: { step: 0.05, min: -0.8, max: 0.8 },
z_pos: { step: 0.05, min: -0.5, max: 1.5 }, 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 }, ui_scaling: { step: 10, min: 40, max: 200 },
}, },
overlay_large_log: { overlay_large_log: {
x_pos: { step: 0.05, min: -0.5, max: 0.5 }, x_pos: { step: 0.05, min: -0.5, max: 0.5 },
y_pos: { step: 0.05, min: -0.8, max: 0.8 }, y_pos: { step: 0.05, min: -0.8, max: 0.8 },
z_pos: { step: 0.05, min: -0.5, max: 1.5 }, 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 }, ui_scaling: { step: 10, min: 40, max: 200 },
}, },

81
zip.py
View File

@@ -1,37 +1,70 @@
import os
import zipfile import zipfile
import argparse 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ファイルを作成 # ZIPファイルを作成
with zipfile.ZipFile(zip_name, 'w', zipfile.ZIP_DEFLATED) as zipf: try:
with zipfile.ZipFile(zip_file_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
# ファイルを追加 # ファイルを追加
for file_path in file_paths: for file_path_str in tqdm(file_paths, desc="Adding files", unit="file"):
if os.path.isfile(file_path): file_path = Path(file_path_str)
zipf.write(file_path, os.path.basename(file_path)) if file_path.is_file():
zipf.write(file_path, file_path.name)
if verbose:
print(f"Add file: {file_path}") 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: for dir_path_str in dir_paths:
if os.path.isdir(dir_path): dir_path = Path(dir_path_str)
for foldername, subfolders, filenames in os.walk(dir_path): if dir_path.is_dir():
for filename in filenames: all_files_in_dir = [item for item in dir_path.rglob("*") if item.is_file()]
file_full_path = os.path.join(foldername, filename) for item in tqdm(all_files_in_dir, desc=f"Adding files from {dir_path.name}", unit="file"):
# ディレクトリを保持しつつ、ルートに配置 # ディレクトリ構造を保持しつつ、ルートに配置
arcname = os.path.join( arcname = Path(dir_path.name) / item.relative_to(dir_path)
os.path.basename(dir_path), zipf.write(item, arcname)
os.path.relpath(file_full_path, dir_path) if verbose:
) print(f"Add file: {item}")
zipf.write(file_full_path, arcname) else:
print(f"Add file: {file_full_path}") 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__": if __name__ == "__main__":
parser = argparse.ArgumentParser() start_time = time.time()
parser.add_argument("--zip_name", type=str, default="VRCT.zip") parser = argparse.ArgumentParser(description="Create a zip file from specified files and directories.")
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("--zip_name", type=str, default="VRCT.zip", help="Name of the output zip file.")
parser.add_argument("--dir_paths", type=str, nargs="*", default=["src-tauri/target/release/_internal"]) 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() args = parser.parse_args()
zip_files_and_directory(args.zip_name, args.file_paths, args.dir_paths) zip_files_and_directory(args.zip_name, args.file_paths, args.dir_paths, args.verbose)
print("Complete!") end_time = time.time()
processing_time = end_time - start_time
print(f"Complete! Processing time: {processing_time:.2f} seconds")