Merge branch 'develop'
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -12,7 +12,8 @@ weights/
|
||||
error.log
|
||||
*.exe
|
||||
*.ipynb
|
||||
|
||||
VRCT.zip
|
||||
VRCT_cuda.zip
|
||||
|
||||
# Added by WebUI migration
|
||||
# Logs
|
||||
|
||||
@@ -5,7 +5,7 @@ a = Analysis(
|
||||
['src-python\\mainloop.py'],
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=[('./fonts', 'fonts/'), ('.venv/Lib/site-packages/zeroconf', 'zeroconf/'), ('.venv/Lib/site-packages/openvr', 'openvr/'), ('.venv/Lib/site-packages/pykakasi', 'pykakasi/'), ('.venv/Lib/site-packages/faster_whisper', 'faster_whisper/')],
|
||||
datas=[('./fonts', 'fonts/'), ('.venv/Lib/site-packages/zeroconf', 'zeroconf/'), ('.venv/Lib/site-packages/openvr', 'openvr/'), ('.venv/Lib/site-packages/pykakasi', 'pykakasi/'), ('.venv/Lib/site-packages/faster_whisper', 'faster_whisper/'), ('.venv/Lib/site-packages/hf_xet', 'hf_xet/')],
|
||||
hiddenimports=[],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
|
||||
@@ -5,7 +5,7 @@ a = Analysis(
|
||||
['src-python\\mainloop.py'],
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=[('./fonts', 'fonts/'), ('.venv_cuda/Lib/site-packages/zeroconf', 'zeroconf/'), ('.venv_cuda/Lib/site-packages/openvr', 'openvr/'), ('.venv_cuda/Lib/site-packages/pykakasi', 'pykakasi/'), ('.venv_cuda/Lib/site-packages/faster_whisper', 'faster_whisper/')],
|
||||
datas=[('./fonts', 'fonts/'), ('.venv_cuda/Lib/site-packages/zeroconf', 'zeroconf/'), ('.venv_cuda/Lib/site-packages/openvr', 'openvr/'), ('.venv_cuda/Lib/site-packages/pykakasi', 'pykakasi/'), ('.venv_cuda/Lib/site-packages/faster_whisper', 'faster_whisper/'), ('.venv/Lib/site-packages/hf_xet', 'hf_xet/')],
|
||||
hiddenimports=[],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
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
|
||||
@@ -1,2 +1,2 @@
|
||||
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
|
||||
19
install.bat
19
install.bat
@@ -1,10 +1,25 @@
|
||||
python -m venv .venv
|
||||
python -m venv .venv_cuda
|
||||
REM .venv exists
|
||||
if exist .venv (
|
||||
rmdir /s /q .venv
|
||||
)
|
||||
|
||||
REM make .venv
|
||||
python -m venv .venv
|
||||
|
||||
REM install packages for .venv
|
||||
call .venv/Scripts/activate
|
||||
python.exe -m pip install --upgrade pip
|
||||
pip install --no-cache-dir --force-reinstall -r requirements.txt
|
||||
|
||||
REM if .venv_cuda exists
|
||||
if exist .venv_cuda (
|
||||
rmdir /s /q .venv_cuda
|
||||
)
|
||||
|
||||
REM make .venv_cuda
|
||||
python -m venv .venv_cuda
|
||||
|
||||
REM install packages for .venv_cuda
|
||||
call .venv_cuda/Scripts/activate
|
||||
python.exe -m pip install --upgrade pip
|
||||
pip install --no-cache-dir --force-reinstall -r requirements_cuda.txt
|
||||
@@ -24,6 +24,9 @@ common_error:
|
||||
invalid_value_speaker_phrase_timeout: "It cannot be set lower than '{{speaker_record_timeout_label}}' with a value of 0 or more."
|
||||
invalid_value_speaker_max_phrase: "You can set a number equal to or greater than 0."
|
||||
|
||||
common_warning:
|
||||
unable_to_use_osc_query: "The functions below have been automatically disabled because receiving OSC data is not possible due to OSC IP Address settings."
|
||||
|
||||
main_page:
|
||||
translation: "Translation"
|
||||
transcription_send: "Voice2Chatbox"
|
||||
@@ -257,6 +260,12 @@ config_page:
|
||||
label: "Open Config File"
|
||||
switch_compute_device:
|
||||
label: "Switch VRCT To CPU/GPU Version"
|
||||
enable_websocket:
|
||||
label: "Enable WebSocket Server"
|
||||
websocket_host:
|
||||
label: "WebSocket Host"
|
||||
websocket_port:
|
||||
label: "WebSocket Port"
|
||||
|
||||
plugin_notifications:
|
||||
downloading: Downloading the plugin.
|
||||
|
||||
@@ -24,10 +24,13 @@ common_error:
|
||||
invalid_value_speaker_phrase_timeout: "0 以上で 「{{speaker_record_timeout_label}}」 より小さくすることはできません。"
|
||||
invalid_value_speaker_max_phrase: "0 以上の数値を設定できます。"
|
||||
|
||||
common_warning:
|
||||
unable_to_use_osc_query: "OSC IP Address の設定によりOSCデータの受信ができないため、以下の機能が自動的に無効になっています。"
|
||||
|
||||
main_page:
|
||||
translation: "翻訳"
|
||||
transcription_send: "音声認識 マイク"
|
||||
transcription_receive: "音声認識 スピーカー"
|
||||
transcription_send: "マイク入力"
|
||||
transcription_receive: "聞き取り"
|
||||
foreground: "最前面固定"
|
||||
|
||||
language_settings: "言語設定"
|
||||
@@ -131,7 +134,7 @@ config_page:
|
||||
ctranslate2_compute_device:
|
||||
label: "AI翻訳 {{ctranslate2}} の処理デバイス"
|
||||
deepl_auth_key:
|
||||
label: "DeepL API 認証キー"
|
||||
label: "DeepL APIキーの登録"
|
||||
desc: "使用の際は、メイン画面にある {{translator}} をDeepL_APIに変更してください。\n※対応していない言語もあります。"
|
||||
open_auth_key_webpage: "DeepLアカウントページを開く"
|
||||
save: "保存"
|
||||
@@ -257,7 +260,12 @@ config_page:
|
||||
label: "設定ファイルを開く"
|
||||
switch_compute_device:
|
||||
label: "VRCT CPU/GPUバージョンの切り替え"
|
||||
|
||||
enable_websocket:
|
||||
label: "WebSocketサーバーを有効にする"
|
||||
websocket_host:
|
||||
label: "WebSocket Host"
|
||||
websocket_port:
|
||||
label: "WebSocket Port"
|
||||
|
||||
plugin_notifications:
|
||||
downloading: プラグインをダウンロード中。
|
||||
|
||||
@@ -15,6 +15,10 @@ pydub==0.25.1
|
||||
psutil==5.9.8
|
||||
pykakasi==2.3.0
|
||||
pycaw==20240210
|
||||
websockets==15.0.1
|
||||
huggingface_hub==0.32.2
|
||||
hf-xet==1.1.2
|
||||
setuptools==80.8.0
|
||||
translators @ git+https://github.com/misyaguziya/translators@5.9.2.1
|
||||
SpeechRecognition @ git+https://github.com/misyaguziya/custom_speech_recognition@3.10.4.1
|
||||
tinyoscquery @ git+https://github.com/cyberkitsune/tinyoscquery@0.1.3
|
||||
@@ -16,6 +16,10 @@ pydub==0.25.1
|
||||
psutil==5.9.8
|
||||
pykakasi==2.3.0
|
||||
pycaw==20240210
|
||||
websockets==15.0.1
|
||||
huggingface_hub==0.32.2
|
||||
hf-xet==1.1.2
|
||||
setuptools==80.8.0
|
||||
translators @ git+https://github.com/misyaguziya/translators@5.9.2.1
|
||||
SpeechRecognition @ git+https://github.com/misyaguziya/custom_speech_recognition@3.10.4.1
|
||||
tinyoscquery @ git+https://github.com/cyberkitsune/tinyoscquery@0.1.3
|
||||
@@ -954,9 +954,42 @@ class Config:
|
||||
self._NOTIFICATION_VRC_SFX = value
|
||||
self.saveConfig(inspect.currentframe().f_code.co_name, value)
|
||||
|
||||
@property
|
||||
def WEBSOCKET_SERVER(self):
|
||||
return self._WEBSOCKET_SERVER
|
||||
|
||||
@WEBSOCKET_SERVER.setter
|
||||
def WEBSOCKET_SERVER(self, value):
|
||||
if isinstance(value, bool):
|
||||
self._WEBSOCKET_SERVER = value
|
||||
self.saveConfig(inspect.currentframe().f_code.co_name, value)
|
||||
|
||||
@property
|
||||
@json_serializable('WEBSOCKET_HOST')
|
||||
def WEBSOCKET_HOST(self):
|
||||
return self._WEBSOCKET_HOST
|
||||
|
||||
@WEBSOCKET_HOST.setter
|
||||
def WEBSOCKET_HOST(self, value):
|
||||
if isinstance(value, str):
|
||||
self._WEBSOCKET_HOST = value
|
||||
self.saveConfig(inspect.currentframe().f_code.co_name, value)
|
||||
|
||||
@property
|
||||
@json_serializable('WEBSOCKET_PORT')
|
||||
def WEBSOCKET_PORT(self):
|
||||
return self._WEBSOCKET_PORT
|
||||
|
||||
@WEBSOCKET_PORT.setter
|
||||
def WEBSOCKET_PORT(self, value):
|
||||
if isinstance(value, int):
|
||||
self._WEBSOCKET_PORT = value
|
||||
self.saveConfig(inspect.currentframe().f_code.co_name, value)
|
||||
|
||||
|
||||
def init_config(self):
|
||||
# Read Only
|
||||
self._VERSION = "3.1.2"
|
||||
self._VERSION = "3.2.0"
|
||||
if getattr(sys, 'frozen', False):
|
||||
self._PATH_LOCAL = os_path.dirname(sys.executable)
|
||||
else:
|
||||
@@ -1139,6 +1172,9 @@ class Config:
|
||||
self._LOGGER_FEATURE = False
|
||||
self._VRC_MIC_MUTE_SYNC = False
|
||||
self._NOTIFICATION_VRC_SFX = True
|
||||
self._WEBSOCKET_SERVER = False
|
||||
self._WEBSOCKET_HOST = "127.0.0.1"
|
||||
self._WEBSOCKET_PORT = 2231
|
||||
|
||||
def load_config(self):
|
||||
if os_path.isfile(self.PATH_CONFIG) is not False:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Callable, Union, Any
|
||||
from typing import Callable, Any
|
||||
from time import sleep
|
||||
from subprocess import Popen
|
||||
from threading import Thread
|
||||
@@ -6,7 +6,7 @@ import re
|
||||
from device_manager import device_manager
|
||||
from config import config
|
||||
from model import model
|
||||
from utils import removeLog, printLog, errorLogging, isConnectedNetwork, isValidIpAddress
|
||||
from utils import removeLog, printLog, errorLogging, isConnectedNetwork, isValidIpAddress, isAvailableWebSocketServer
|
||||
|
||||
class Controller:
|
||||
def __init__(self) -> None:
|
||||
@@ -294,6 +294,19 @@ class Controller:
|
||||
"translation":translation,
|
||||
"transliteration":transliteration
|
||||
})
|
||||
|
||||
if model.checkWebSocketServerAlive() is True:
|
||||
model.websocketSendMessage(
|
||||
{
|
||||
"type":"SENT",
|
||||
"src_languages":config.SELECTED_YOUR_LANGUAGES[config.SELECTED_TAB_NO],
|
||||
"dst_languages":config.SELECTED_TARGET_LANGUAGES[config.SELECTED_TAB_NO],
|
||||
"message":message,
|
||||
"translation":translation,
|
||||
"transliteration":transliteration
|
||||
}
|
||||
)
|
||||
|
||||
if config.LOGGER_FEATURE is True:
|
||||
if len(translation) > 0:
|
||||
translation = " (" + "/".join(translation) + ")"
|
||||
@@ -377,6 +390,19 @@ class Controller:
|
||||
"translation":translation,
|
||||
"transliteration":transliteration,
|
||||
})
|
||||
|
||||
if model.checkWebSocketServerAlive() is True:
|
||||
model.websocketSendMessage(
|
||||
{
|
||||
"type":"RECEIVED",
|
||||
"src_languages":config.SELECTED_TARGET_LANGUAGES[config.SELECTED_TAB_NO],
|
||||
"dst_languages":config.SELECTED_YOUR_LANGUAGES[config.SELECTED_TAB_NO],
|
||||
"message":message,
|
||||
"translation":translation,
|
||||
"transliteration":transliteration
|
||||
}
|
||||
)
|
||||
|
||||
if config.LOGGER_FEATURE is True:
|
||||
if len(translation) > 0:
|
||||
translation = " (" + "/".join(translation) + ")"
|
||||
@@ -434,11 +460,23 @@ class Controller:
|
||||
overlay_image = model.createOverlayImageLargeLog("send", message, translation[0] if len(translation) > 0 else "")
|
||||
model.updateOverlayLargeLog(overlay_image)
|
||||
|
||||
# update textbox message log (Sent)
|
||||
if model.checkWebSocketServerAlive() is True:
|
||||
model.websocketSendMessage(
|
||||
{
|
||||
"type":"CHAT",
|
||||
"src_languages":config.SELECTED_YOUR_LANGUAGES[config.SELECTED_TAB_NO],
|
||||
"dst_languages":config.SELECTED_TARGET_LANGUAGES[config.SELECTED_TAB_NO],
|
||||
"message":message,
|
||||
"translation":translation,
|
||||
"transliteration":transliteration
|
||||
}
|
||||
)
|
||||
|
||||
# update textbox message log (Chat)
|
||||
if config.LOGGER_FEATURE is True:
|
||||
if len(translation) > 0:
|
||||
translation_text = " (" + "/".join(translation) + ")"
|
||||
model.logger.info(f"[SENT] {message}{translation_text}")
|
||||
model.logger.info(f"[CHAT] {message}{translation_text}")
|
||||
|
||||
return {"status":200,
|
||||
"result":{
|
||||
@@ -1099,8 +1137,7 @@ class Controller:
|
||||
def getOscIpAddress(*args, **kwargs) -> dict:
|
||||
return {"status":200, "result":config.OSC_IP_ADDRESS}
|
||||
|
||||
@staticmethod
|
||||
def setOscIpAddress(data, *args, **kwargs) -> dict:
|
||||
def setOscIpAddress(self, data, *args, **kwargs) -> dict:
|
||||
if isValidIpAddress(data) is False:
|
||||
response = {
|
||||
"status":400,
|
||||
@@ -1113,6 +1150,11 @@ class Controller:
|
||||
try:
|
||||
model.setOscIpAddress(data)
|
||||
config.OSC_IP_ADDRESS = data
|
||||
if model.getIsOscQueryEnabled() is True:
|
||||
self.enableOscQuery()
|
||||
else:
|
||||
self.setDisableVrcMicMuteSync()
|
||||
self.disableOscQuery()
|
||||
response = {"status":200, "result":config.OSC_IP_ADDRESS}
|
||||
except Exception:
|
||||
model.setOscIpAddress(config.OSC_IP_ADDRESS)
|
||||
@@ -1388,10 +1430,20 @@ class Controller:
|
||||
|
||||
@staticmethod
|
||||
def setEnableVrcMicMuteSync(*args, **kwargs) -> dict:
|
||||
if model.getIsOscQueryEnabled() is True:
|
||||
config.VRC_MIC_MUTE_SYNC = True
|
||||
model.setMuteSelfStatus()
|
||||
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
|
||||
def setDisableVrcMicMuteSync(*args, **kwargs) -> dict:
|
||||
@@ -1778,9 +1830,117 @@ class Controller:
|
||||
model.stopWatchdog()
|
||||
return {"status":200, "result":True}
|
||||
|
||||
@staticmethod
|
||||
def getWebSocketHost(*args, **kwargs) -> dict:
|
||||
return {"status":200, "result":config.WEBSOCKET_HOST}
|
||||
|
||||
@staticmethod
|
||||
def setWebSocketHost(data, *args, **kwargs) -> dict:
|
||||
if isValidIpAddress(data) is False:
|
||||
response = {
|
||||
"status":400,
|
||||
"result":{
|
||||
"message":"Invalid IP address",
|
||||
"data": config.WEBSOCKET_HOST
|
||||
}
|
||||
}
|
||||
else:
|
||||
if model.checkWebSocketServerAlive() is False:
|
||||
config.WEBSOCKET_HOST = data
|
||||
response = {"status":200, "result":config.WEBSOCKET_HOST}
|
||||
else:
|
||||
if data == config.WEBSOCKET_HOST:
|
||||
response = {"status":200, "result":config.WEBSOCKET_HOST}
|
||||
elif isAvailableWebSocketServer(data, config.WEBSOCKET_PORT):
|
||||
model.stopWebSocketServer()
|
||||
model.startWebSocketServer(data, config.WEBSOCKET_PORT)
|
||||
config.WEBSOCKET_HOST = data
|
||||
response = {"status":200, "result":config.WEBSOCKET_HOST}
|
||||
else:
|
||||
response = {
|
||||
"status":400,
|
||||
"result":{
|
||||
"message":"WebSocket server host is not available",
|
||||
"data": config.WEBSOCKET_HOST
|
||||
}
|
||||
}
|
||||
|
||||
return response
|
||||
|
||||
@staticmethod
|
||||
def getWebSocketPort(*args, **kwargs) -> dict:
|
||||
return {"status":200, "result":config.WEBSOCKET_PORT}
|
||||
|
||||
@staticmethod
|
||||
def setWebSocketPort(data, *args, **kwargs) -> dict:
|
||||
if model.checkWebSocketServerAlive() is False:
|
||||
config.WEBSOCKET_PORT = int(data)
|
||||
response = {"status":200, "result":config.WEBSOCKET_PORT}
|
||||
else:
|
||||
if int(data) == config.WEBSOCKET_PORT:
|
||||
return {"status":200, "result":config.WEBSOCKET_PORT}
|
||||
elif isAvailableWebSocketServer(config.WEBSOCKET_HOST, int(data)) is True:
|
||||
model.stopWebSocketServer()
|
||||
model.startWebSocketServer(config.WEBSOCKET_HOST, int(data))
|
||||
config.WEBSOCKET_PORT = int(data)
|
||||
response = {"status":200, "result":config.WEBSOCKET_PORT}
|
||||
else:
|
||||
response = {
|
||||
"status":400,
|
||||
"result":{
|
||||
"message":"WebSocket server port is not available",
|
||||
"data": config.WEBSOCKET_PORT
|
||||
}
|
||||
}
|
||||
return response
|
||||
|
||||
@staticmethod
|
||||
def getWebSocketServer(*args, **kwargs) -> dict:
|
||||
return {"status":200, "result":config.WEBSOCKET_SERVER}
|
||||
|
||||
@staticmethod
|
||||
def setEnableWebSocketServer(*args, **kwargs) -> dict:
|
||||
if isAvailableWebSocketServer(config.WEBSOCKET_HOST, config.WEBSOCKET_PORT) is True:
|
||||
model.startWebSocketServer(config.WEBSOCKET_HOST, config.WEBSOCKET_PORT)
|
||||
config.WEBSOCKET_SERVER = True
|
||||
response = {"status":200, "result":config.WEBSOCKET_SERVER}
|
||||
else:
|
||||
response = {
|
||||
"status":400,
|
||||
"result":{
|
||||
"message":"WebSocket server host or port is not available",
|
||||
"data": config.WEBSOCKET_SERVER
|
||||
}
|
||||
}
|
||||
return response
|
||||
|
||||
@staticmethod
|
||||
def setDisableWebSocketServer(*args, **kwargs) -> dict:
|
||||
config.WEBSOCKET_SERVER = False
|
||||
model.stopWebSocketServer()
|
||||
return {"status":200, "result":config.WEBSOCKET_SERVER}
|
||||
|
||||
def initializationProgress(self, progress):
|
||||
self.run(200, self.run_mapping["initialization_progress"], progress)
|
||||
|
||||
def enableOscQuery(self):
|
||||
self.run(
|
||||
200,
|
||||
self.run_mapping["enable_osc_query"],
|
||||
{
|
||||
"data": True,
|
||||
"disabled_functions": []
|
||||
}
|
||||
)
|
||||
|
||||
def disableOscQuery(self):
|
||||
self.run(200, self.run_mapping["enable_osc_query"], {
|
||||
"data": False,
|
||||
"disabled_functions": [
|
||||
"vrc_mic_mute_sync",
|
||||
]
|
||||
})
|
||||
|
||||
def init(self, *args, **kwargs) -> None:
|
||||
removeLog()
|
||||
printLog("Start Initialization")
|
||||
@@ -1892,6 +2052,14 @@ class Controller:
|
||||
# init OSC receive
|
||||
printLog("Init OSC Receive")
|
||||
model.startReceiveOSC()
|
||||
osc_query_enabled = model.getIsOscQueryEnabled()
|
||||
if osc_query_enabled is True:
|
||||
self.enableOscQuery()
|
||||
else:
|
||||
# OSC Query is disabled, so disable VRC some features
|
||||
config.VRC_MIC_MUTE_SYNC = False
|
||||
self.disableOscQuery()
|
||||
|
||||
if config.VRC_MIC_MUTE_SYNC is True:
|
||||
self.setEnableVrcMicMuteSync()
|
||||
|
||||
@@ -1911,6 +2079,15 @@ class Controller:
|
||||
if (config.OVERLAY_SMALL_LOG is True or config.OVERLAY_LARGE_LOG is True):
|
||||
model.startOverlay()
|
||||
|
||||
printLog("Init WebSocket Server")
|
||||
if config.WEBSOCKET_SERVER is True:
|
||||
if isAvailableWebSocketServer(config.WEBSOCKET_HOST, config.WEBSOCKET_PORT) is True:
|
||||
model.startWebSocketServer(config.WEBSOCKET_HOST, config.WEBSOCKET_PORT)
|
||||
else:
|
||||
config.WEBSOCKET_SERVER = False
|
||||
model.stopWebSocketServer()
|
||||
printLog("WebSocket server host or port is not available")
|
||||
|
||||
printLog("Update settings")
|
||||
self.updateConfigSettings()
|
||||
|
||||
|
||||
@@ -4,8 +4,11 @@ import time
|
||||
from typing import Any
|
||||
from threading import Thread
|
||||
from queue import Queue
|
||||
from controller import Controller
|
||||
from utils import printLog, printResponse, errorLogging, encodeBase64
|
||||
import logging
|
||||
from controller import Controller # noqa: E402
|
||||
from utils import printLog, printResponse, errorLogging, encodeBase64 # noqa: E402
|
||||
|
||||
logging.getLogger("huggingface_hub").setLevel(logging.ERROR)
|
||||
|
||||
run_mapping = {
|
||||
"connected_network":"/run/connected_network",
|
||||
@@ -42,6 +45,8 @@ run_mapping = {
|
||||
|
||||
"initialization_progress":"/run/initialization_progress",
|
||||
"initialization_complete":"/run/initialization_complete",
|
||||
|
||||
"enable_osc_query":"/run/enable_osc_query",
|
||||
}
|
||||
|
||||
def run(status:int, endpoint:str, result:Any) -> None:
|
||||
@@ -291,6 +296,15 @@ mapping = {
|
||||
"/set/enable/send_received_message_to_vrc": {"status": True, "variable":controller.setEnableSendReceivedMessageToVrc},
|
||||
"/set/disable/send_received_message_to_vrc": {"status": True, "variable":controller.setDisableSendReceivedMessageToVrc},
|
||||
|
||||
# WebSocket Settings
|
||||
"/get/data/websocket_host": {"status": True, "variable":controller.getWebSocketHost},
|
||||
"/set/data/websocket_host": {"status": True, "variable":controller.setWebSocketHost},
|
||||
"/get/data/websocket_port": {"status": True, "variable":controller.getWebSocketPort},
|
||||
"/set/data/websocket_port": {"status": True, "variable":controller.setWebSocketPort},
|
||||
"/get/data/websocket_server": {"status": True, "variable":controller.getWebSocketServer},
|
||||
"/set/enable/websocket_server": {"status": True, "variable":controller.setEnableWebSocketServer},
|
||||
"/set/disable/websocket_server": {"status": True, "variable":controller.setDisableWebSocketServer},
|
||||
|
||||
# Advanced Settings
|
||||
"/get/data/osc_ip_address": {"status": True, "variable":controller.getOscIpAddress},
|
||||
"/set/data/osc_ip_address": {"status": True, "variable":controller.setOscIpAddress},
|
||||
@@ -367,7 +381,7 @@ class Main:
|
||||
if status == 423:
|
||||
self.queue.put((endpoint, data))
|
||||
else:
|
||||
printLog(endpoint, {"send_data":result})
|
||||
printLog(endpoint, {"status": status, "send_data": result})
|
||||
printResponse(status, endpoint, result)
|
||||
time.sleep(0.1)
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import copy
|
||||
import gc
|
||||
import asyncio
|
||||
import json
|
||||
from subprocess import Popen
|
||||
from os import makedirs as os_makedirs
|
||||
from os import path as os_path
|
||||
@@ -29,6 +31,7 @@ from models.transcription.transcription_whisper import checkWhisperWeight, downl
|
||||
from models.overlay.overlay import Overlay
|
||||
from models.overlay.overlay_image import OverlayImage
|
||||
from models.watchdog.watchdog import Watchdog
|
||||
from models.websocket.websocket_server import WebSocketServer
|
||||
from utils import errorLogging, setupLogger
|
||||
|
||||
class threadFnc(Thread):
|
||||
@@ -99,6 +102,10 @@ class Model:
|
||||
self.kks = kakasi()
|
||||
self.watchdog = Watchdog(config.WATCHDOG_TIMEOUT, config.WATCHDOG_INTERVAL)
|
||||
self.osc_handler = OSCHandler(config.OSC_IP_ADDRESS, config.OSC_PORT)
|
||||
self.websocket_server = None
|
||||
self.websocket_server_loop = False
|
||||
self.websocket_server_alive = False
|
||||
self.th_websocket_server = None
|
||||
|
||||
def checkTranslatorCTranslate2ModelWeight(self, weight_type:str):
|
||||
return checkCTranslate2Weight(config.PATH_LOCAL, weight_type)
|
||||
@@ -292,11 +299,8 @@ class Model:
|
||||
def oscSendMessage(self, message:str):
|
||||
self.osc_handler.sendMessage(message=message, notification=config.NOTIFICATION_VRC_SFX)
|
||||
|
||||
def getMuteSelfStatus(self):
|
||||
return self.osc_handler.getOSCParameterMuteSelf()
|
||||
|
||||
def setMuteSelfStatus(self):
|
||||
self.mic_mute_status = self.getMuteSelfStatus()
|
||||
self.mic_mute_status = self.osc_handler.getOSCParameterMuteSelf()
|
||||
|
||||
def startReceiveOSC(self):
|
||||
def changeHandlerMute(address, osc_arguments):
|
||||
@@ -311,11 +315,15 @@ class Model:
|
||||
dict_filter_and_target = {
|
||||
self.osc_handler.osc_parameter_muteself: changeHandlerMute,
|
||||
}
|
||||
self.osc_handler.receiveOscParameters(dict_filter_and_target)
|
||||
self.osc_handler.setDictFilterAndTarget(dict_filter_and_target)
|
||||
self.osc_handler.receiveOscParameters()
|
||||
|
||||
def stopReceiveOSC(self):
|
||||
self.osc_handler.oscServerStop()
|
||||
|
||||
def getIsOscQueryEnabled(self):
|
||||
return self.osc_handler.getIsOscQueryEnabled()
|
||||
|
||||
@staticmethod
|
||||
def checkSoftwareUpdated():
|
||||
# check update
|
||||
@@ -503,11 +511,15 @@ class Model:
|
||||
|
||||
def changeMicTranscriptStatus(self):
|
||||
if config.VRC_MIC_MUTE_SYNC is True:
|
||||
if self.mic_mute_status is True:
|
||||
match self.mic_mute_status:
|
||||
case True:
|
||||
self.pauseMicTranscript()
|
||||
elif self.mic_mute_status is False:
|
||||
case False:
|
||||
self.resumeMicTranscript()
|
||||
else:
|
||||
case None:
|
||||
# mute selfの状態が不明な場合は一時停止しない
|
||||
self.resumeMicTranscript()
|
||||
case _:
|
||||
pass
|
||||
else:
|
||||
self.resumeMicTranscript()
|
||||
@@ -827,4 +839,86 @@ class Model:
|
||||
self.th_watchdog.join()
|
||||
self.th_watchdog = None
|
||||
|
||||
def message_handler(websocket, message):
|
||||
"""WebSocketメッセージ受信時の処理"""
|
||||
pass
|
||||
|
||||
def startWebSocketServer(self, host, port):
|
||||
"""WebSocketサーバーを起動し、別スレッドで実行する"""
|
||||
if self.websocket_server_alive is True:
|
||||
# サーバーが既に起動している場合は何もしない
|
||||
return
|
||||
|
||||
self.websocket_server_loop = True
|
||||
self.websocket_server_alive = False # 初期状態を明示
|
||||
|
||||
async def WebSocketServerMain():
|
||||
try:
|
||||
self.websocket_server = WebSocketServer(
|
||||
host=host,
|
||||
port=port,
|
||||
)
|
||||
self.websocket_server.set_message_handler(self.message_handler)
|
||||
self.websocket_server.start()
|
||||
self.websocket_server_alive = True
|
||||
|
||||
# イベントループが終了するまで待機
|
||||
while self.websocket_server_loop:
|
||||
# self.websocket_server.send("Server is running...")
|
||||
await asyncio.sleep(0.5) # 応答性向上のため間隔短縮
|
||||
|
||||
except Exception:
|
||||
errorLogging()
|
||||
# 具体的なエラー内容をログに残す場合
|
||||
# self.logger.error(f"WebSocket server error: {str(e)}")
|
||||
finally:
|
||||
# 確実にサーバーを停止
|
||||
if hasattr(self, 'websocket_server') and self.websocket_server:
|
||||
self.websocket_server.stop()
|
||||
self.websocket_server_alive = False
|
||||
|
||||
self.th_websocket_server = Thread(target=lambda: asyncio.run(WebSocketServerMain()))
|
||||
self.th_websocket_server.daemon = True
|
||||
self.th_websocket_server.start()
|
||||
|
||||
def stopWebSocketServer(self):
|
||||
"""WebSocketサーバーを停止する"""
|
||||
if not hasattr(self, 'th_websocket_server') or self.th_websocket_server is None:
|
||||
return
|
||||
|
||||
self.websocket_server_loop = False
|
||||
|
||||
try:
|
||||
# 一定時間待機してからタイムアウト
|
||||
self.th_websocket_server.join(timeout=2.0)
|
||||
|
||||
if self.th_websocket_server.is_alive():
|
||||
# タイムアウト後もスレッドが生きている場合の処理
|
||||
self.logger.warning("WebSocket server thread did not terminate properly")
|
||||
except Exception:
|
||||
errorLogging()
|
||||
finally:
|
||||
self.th_websocket_server = None
|
||||
self.websocket_server = None
|
||||
self.websocket_server_alive = False
|
||||
|
||||
def checkWebSocketServerAlive(self):
|
||||
"""WebSocketサーバーの稼働状態を確認する"""
|
||||
return self.websocket_server_alive
|
||||
|
||||
def websocketSendMessage(self, message_dict:dict):
|
||||
"""
|
||||
WebSocketサーバーから全クライアントにメッセージを送信する
|
||||
:param message_dict: 送信するメッセージの辞書
|
||||
:return: 送信成功したかどうか
|
||||
"""
|
||||
if not self.websocket_server_alive or not self.websocket_server:
|
||||
return False
|
||||
try:
|
||||
message_json = json.dumps(message_dict)
|
||||
return self.websocket_server.send(message_json)
|
||||
except Exception:
|
||||
errorLogging()
|
||||
return False
|
||||
|
||||
model = Model()
|
||||
@@ -7,10 +7,22 @@ from tinyoscquery.queryservice import OSCQueryService
|
||||
from tinyoscquery.query import OSCQueryBrowser, OSCQueryClient
|
||||
from tinyoscquery.utility import get_open_udp_port, get_open_tcp_port
|
||||
from tinyoscquery.shared.node import OSCAccess
|
||||
|
||||
try:
|
||||
from utils import errorLogging
|
||||
except ImportError:
|
||||
def errorLogging():
|
||||
import traceback
|
||||
print("Error occurred:", traceback.format_exc())
|
||||
|
||||
class OSCHandler:
|
||||
def __init__(self, ip_address="127.0.0.1", port=9000) -> None:
|
||||
|
||||
if ip_address in ["127.0.0.1", "localhost"]:
|
||||
self.is_osc_query_enabled = True
|
||||
else:
|
||||
self.is_osc_query_enabled = False
|
||||
|
||||
self.osc_ip_address = ip_address
|
||||
self.osc_port = port
|
||||
self.osc_parameter_muteself = "/avatar/parameters/MuteSelf"
|
||||
@@ -24,15 +36,28 @@ class OSCHandler:
|
||||
self.osc_server_ip_address = ip_address
|
||||
self.http_port = None
|
||||
self.osc_server_port = None
|
||||
self.dict_filter_and_target = {}
|
||||
self.browser = None
|
||||
|
||||
def getIsOscQueryEnabled(self) -> bool:
|
||||
return self.is_osc_query_enabled
|
||||
|
||||
def setOscIpAddress(self, ip_address:str) -> None:
|
||||
if ip_address in ["127.0.0.1", "localhost"]:
|
||||
self.is_osc_query_enabled = True
|
||||
else:
|
||||
self.is_osc_query_enabled = False
|
||||
|
||||
self.oscServerStop()
|
||||
self.osc_ip_address = ip_address
|
||||
self.udp_client = udp_client.SimpleUDPClient(self.osc_ip_address, self.osc_port)
|
||||
self.receiveOscParameters()
|
||||
|
||||
def setOscPort(self, port:int) -> None:
|
||||
self.oscServerStop()
|
||||
self.osc_port = port
|
||||
self.udp_client = udp_client.SimpleUDPClient(self.osc_ip_address, self.osc_port)
|
||||
self.receiveOscParameters()
|
||||
|
||||
# send OSC message typing
|
||||
def sendTyping(self, flag:bool=False) -> None:
|
||||
@@ -44,6 +69,10 @@ class OSCHandler:
|
||||
self.udp_client.send_message(self.osc_parameter_chatbox_input, [f"{message}", True, notification])
|
||||
|
||||
def getOSCParameterValue(self, address:str) -> Any:
|
||||
if not self.is_osc_query_enabled:
|
||||
# OSCQueryが無効な場合はNoneを返す
|
||||
return None
|
||||
|
||||
value = None
|
||||
try:
|
||||
# browserインスタンスを再利用し、毎回の生成と破棄を避ける
|
||||
@@ -71,19 +100,26 @@ class OSCHandler:
|
||||
def getOSCParameterMuteSelf(self) -> bool:
|
||||
return self.getOSCParameterValue(self.osc_parameter_muteself)
|
||||
|
||||
def receiveOscParameters(self, dict_filter_and_target:dict) -> None:
|
||||
def setDictFilterAndTarget(self, dict_filter_and_target:dict) -> None:
|
||||
self.dict_filter_and_target = dict_filter_and_target
|
||||
|
||||
def receiveOscParameters(self) -> None:
|
||||
if self.is_osc_query_enabled is False:
|
||||
# OSCQueryが無効な場合は何もしない
|
||||
return
|
||||
|
||||
self.osc_server_port = get_open_udp_port()
|
||||
self.http_port = get_open_tcp_port()
|
||||
osc_dispatcher = dispatcher.Dispatcher()
|
||||
for filter, target in dict_filter_and_target.items():
|
||||
for filter, target in self.dict_filter_and_target.items():
|
||||
osc_dispatcher.map(filter, target)
|
||||
self.osc_server = osc_server.ThreadingOSCUDPServer((self.osc_server_ip_address, self.osc_server_port), osc_dispatcher, asyncio.get_event_loop())
|
||||
self.osc_server = osc_server.ThreadingOSCUDPServer((self.osc_server_ip_address, self.osc_server_port), osc_dispatcher)
|
||||
Thread(target=self.oscServerServe, daemon=True).start()
|
||||
|
||||
while True:
|
||||
try:
|
||||
self.osc_query_service = OSCQueryService(self.osc_query_service_name, self.http_port, self.osc_server_port)
|
||||
for filter, target in dict_filter_and_target.items():
|
||||
for filter, target in self.dict_filter_and_target.items():
|
||||
self.osc_query_service.advertise_endpoint(filter, access=OSCAccess.READWRITE_VALUE)
|
||||
break
|
||||
except Exception:
|
||||
@@ -112,12 +148,26 @@ class OSCHandler:
|
||||
|
||||
if __name__ == "__main__":
|
||||
handler = OSCHandler()
|
||||
handler.receiveOscParameters({
|
||||
"/avatar/parameters/MuteSelf": print,
|
||||
handler.setDictFilterAndTarget({
|
||||
"/avatar/parameters/MuteSelf": lambda address, *args: print(f"Received {address} with args {args}"),
|
||||
"/chatbox/typing": lambda address, *args: print(f"Received {address} with args {args}"),
|
||||
"/chatbox/input": lambda address, *args: print(f"Received {address} with args {args}"),
|
||||
})
|
||||
handler.receiveOscParameters()
|
||||
sleep(5)
|
||||
handler.sendTyping(True)
|
||||
sleep(1)
|
||||
handler.sendMessage(message="Hello World", notification=True)
|
||||
sleep(60)
|
||||
handler.sendMessage(message="Hello World 1", notification=True)
|
||||
sleep(10)
|
||||
|
||||
print("IP address changed to 192.168.193.2")
|
||||
handler.setOscIpAddress("192.168.193.2")
|
||||
sleep(5)
|
||||
handler.sendMessage(message="Hello World 2", notification=True)
|
||||
|
||||
print("IP address changed to 127.0.0.1")
|
||||
handler.setOscIpAddress("127.0.0.1")
|
||||
sleep(5)
|
||||
handler.sendMessage(message="Hello World 3", notification=True)
|
||||
sleep(10)
|
||||
handler.oscServerStop()
|
||||
4
src-python/models/websocket/__init__.py
Normal file
4
src-python/models/websocket/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
# WebSocketサーバーモジュール
|
||||
from .websocket_server import WebSocketServer
|
||||
|
||||
__all__ = ["WebSocketServer"]
|
||||
221
src-python/models/websocket/websocket_server.py
Normal file
221
src-python/models/websocket/websocket_server.py
Normal 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...")
|
||||
@@ -8,6 +8,7 @@ from logging.handlers import RotatingFileHandler
|
||||
from ctranslate2 import get_supported_compute_types
|
||||
import requests
|
||||
import ipaddress
|
||||
import socket
|
||||
|
||||
def isConnectedNetwork(url="http://www.google.com", timeout=3) -> bool:
|
||||
try:
|
||||
@@ -16,6 +17,25 @@ def isConnectedNetwork(url="http://www.google.com", timeout=3) -> bool:
|
||||
except requests.RequestException:
|
||||
return False
|
||||
|
||||
def isAvailableWebSocketServer(host:str, port:int) -> bool:
|
||||
"""WebSocketサーバーのポートが使用中かどうかを確認する"""
|
||||
response = True
|
||||
try:
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as chk:
|
||||
try:
|
||||
# SO_REUSEADDRを設定してソケットの再利用を許可
|
||||
chk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
chk.bind((host, port))
|
||||
# シャットダウン前にリッスン状態にする必要はない
|
||||
chk.close()
|
||||
except Exception:
|
||||
response = False
|
||||
except Exception:
|
||||
errorLogging()
|
||||
response = False
|
||||
|
||||
return response
|
||||
|
||||
def isValidIpAddress(ip_address: str) -> bool:
|
||||
try:
|
||||
ipaddress.ip_address(ip_address)
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
--error_bc_active_color: #9c3938;
|
||||
--success_bc_color: #368777;
|
||||
--waring_color: #cb944f;
|
||||
--waring_bc_color: #cf7b1b;
|
||||
|
||||
--dark_basic_text_color: #f2f2f2;
|
||||
--dark_100_color: #f5f7fb;
|
||||
|
||||
@@ -4,22 +4,9 @@ import MUI_Slider from "@mui/material/Slider";
|
||||
import clsx from "clsx";
|
||||
|
||||
export const Slider = (props) => {
|
||||
return (
|
||||
<div className={clsx(styles.container, props.className, {[styles.no_padding]: props.no_padding || props.is_break_point})}>
|
||||
<MUI_Slider
|
||||
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={{
|
||||
const location = props.valueLabelDisplayLocation || "top";
|
||||
|
||||
const sliderSx = {
|
||||
color: "var(--dark_700_color)",
|
||||
"& .MuiSlider-thumb": {
|
||||
backgroundColor: "var(--primary_600_color)",
|
||||
@@ -27,10 +14,54 @@ export const Slider = (props) => {
|
||||
boxShadow: `0 0 0 0.8rem var(--primary_600_color_44)`,
|
||||
},
|
||||
"& .MuiSlider-valueLabel": {
|
||||
fontSize: "1.4rem",
|
||||
position: "absolute",
|
||||
backgroundColor: "var(--dark_800_color)",
|
||||
padding: "0.6rem 1rem",
|
||||
width: "fit-content",
|
||||
minWidth: "4.8rem",
|
||||
padding: "0.4rem 0.8rem",
|
||||
lineHeight: "1.15",
|
||||
"& .MuiSlider-valueLabelLabel": {
|
||||
fontSize: "1.4rem",
|
||||
},
|
||||
...(location === "top" && {
|
||||
top: "-110%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%) scale(0)",
|
||||
transformOrigin: "bottom center",
|
||||
"&.MuiSlider-valueLabelOpen": {
|
||||
transform: "translate(-50%, -50%) scale(1)",
|
||||
},
|
||||
"&::before": {
|
||||
bottom: "0%",
|
||||
left: "50%",
|
||||
},
|
||||
}),
|
||||
...(location === "right" && {
|
||||
top: "50%",
|
||||
left: "150%",
|
||||
transform: "translate(0, -50%) scale(0)",
|
||||
transformOrigin: "left center",
|
||||
"&.MuiSlider-valueLabelOpen": {
|
||||
transform: "translate(0, -50%) scale(1)",
|
||||
},
|
||||
"&::before": {
|
||||
bottom: "50%",
|
||||
left: "0",
|
||||
},
|
||||
}),
|
||||
...(location === "left" && {
|
||||
// top: "50%",
|
||||
// right: "50%",
|
||||
// transform: "translate(-50%, -50%) scale(0)",
|
||||
// transformOrigin: "bottom center",
|
||||
// "&.MuiSlider-valueLabelOpen": {
|
||||
// transform: "translate(-50%, -50%) scale(1)",
|
||||
// },
|
||||
// "&::before": {
|
||||
// bottom: "50%",
|
||||
// left: "100%",
|
||||
// },
|
||||
}),
|
||||
},
|
||||
},
|
||||
"& .MuiSlider-markLabel": {
|
||||
@@ -41,7 +72,39 @@ export const Slider = (props) => {
|
||||
"& .MuiSlider-markLabelActive": {
|
||||
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>
|
||||
);
|
||||
|
||||
@@ -6,13 +6,19 @@ import { useOpenFolder } from "@logics_common";
|
||||
import {
|
||||
useOscIpAddress,
|
||||
useOscPort,
|
||||
useWebsocket,
|
||||
} from "@logics_configs";
|
||||
|
||||
import {
|
||||
CheckboxContainer,
|
||||
ActionButtonContainer,
|
||||
EntryWithSaveButtonContainer,
|
||||
} from "../_templates/Templates";
|
||||
|
||||
import {
|
||||
SectionLabelComponent,
|
||||
} from "../_components/";
|
||||
|
||||
import OpenFolderSvg from "@images/open_folder.svg?react";
|
||||
import HelpSvg from "@images/help.svg?react";
|
||||
|
||||
@@ -25,6 +31,7 @@ export const AdvancedSettings = () => {
|
||||
<OpenConfigFolderContainer />
|
||||
<OpenSwitchComputeDeviceModalContainer />
|
||||
</div>
|
||||
<WebsocketContainer />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -87,6 +94,7 @@ const OscPortContainer = () => {
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const OpenConfigFolderContainer = () => {
|
||||
const { t } = useTranslation();
|
||||
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"
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -101,11 +101,17 @@ export const VrcMicMuteSyncContainer = () => {
|
||||
const { t } = useTranslation();
|
||||
const { currentEnableVrcMicMuteSync, toggleEnableVrcMicMuteSync } = useEnableVrcMicMuteSync();
|
||||
|
||||
const variable = {
|
||||
state: currentEnableVrcMicMuteSync.state,
|
||||
data: currentEnableVrcMicMuteSync.data.is_enabled,
|
||||
};
|
||||
|
||||
return (
|
||||
<CheckboxContainer
|
||||
label={t("config_page.others.vrc_mic_mute_sync.label")}
|
||||
desc={t("config_page.others.vrc_mic_mute_sync.desc")}
|
||||
variable={currentEnableVrcMicMuteSync}
|
||||
variable={variable}
|
||||
is_available={currentEnableVrcMicMuteSync.data.is_available}
|
||||
toggleFunction={toggleEnableVrcMicMuteSync}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -272,6 +272,7 @@ const SupporterPeriodContainer = ({ settings, calc_support_period }) => {
|
||||
|
||||
return (
|
||||
<div className={styles.supporter_period_container}>
|
||||
<div className={styles.supporter_period_wrapper}>
|
||||
{Object.entries(period_data).map(([key, item], index) => {
|
||||
if (item === "") return null;
|
||||
const period_box_class_name = clsx(styles.period_box, {
|
||||
@@ -297,6 +298,7 @@ const SupporterPeriodContainer = ({ settings, calc_support_period }) => {
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
align-content: start;
|
||||
flex-wrap: wrap;
|
||||
column-gap: 1.8rem;
|
||||
row-gap: 0.4rem;
|
||||
row-gap: 2rem;
|
||||
}
|
||||
|
||||
.supporter_image_container {
|
||||
@@ -209,13 +209,24 @@ $progress_ease: cubic-bezier(0, 1, 0.75, 1);
|
||||
|
||||
|
||||
.supporter_period_container {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
}
|
||||
.supporter_period_wrapper {
|
||||
display: flex;
|
||||
gap: 0.2rem;
|
||||
padding-left: 0.4rem;
|
||||
gap: 0.4rem 0.2rem;
|
||||
flex-shrink: 0;
|
||||
flex-wrap: wrap;
|
||||
padding: 0.3rem 0.4rem 0.4rem 0.4rem;
|
||||
}
|
||||
|
||||
.period_box_wrapper {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.period_box {
|
||||
width: 1.8rem;
|
||||
width: 1.7rem;
|
||||
height: 0.3rem;
|
||||
border-radius: 0.3rem;
|
||||
&.mogu_bar {
|
||||
@@ -231,9 +242,7 @@ $progress_ease: cubic-bezier(0, 1, 0.75, 1);
|
||||
background-color: var(--dark_800_color);
|
||||
}
|
||||
}
|
||||
.period_box_wrapper {
|
||||
padding: 0.3rem 0 0.4rem 0;
|
||||
}
|
||||
|
||||
.tooltip_period_label {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import clsx from "clsx";
|
||||
import styles from "./Vr.module.scss";
|
||||
@@ -25,6 +25,10 @@ import {
|
||||
|
||||
import RedoSvg from "@images/redo.svg?react";
|
||||
|
||||
import SquareSvg from "@images/square.svg?react";
|
||||
import TriangleSvg from "@images/triangle.svg?react";
|
||||
import { randomIntMinMax } from "@utils";
|
||||
|
||||
export const Vr = () => {
|
||||
const { t } = useTranslation();
|
||||
const [is_opened_small_settings, setIsOpenedSmallSettings] = useState(true);
|
||||
@@ -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 {
|
||||
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 (
|
||||
<div className={styles.position_controls}>
|
||||
<div className={styles.position_wrapper}>
|
||||
@@ -205,8 +234,18 @@ const PositionControls = ({settings, onchangeFunction, selectFunction, ui_config
|
||||
min={ui_configs.x_pos.min}
|
||||
max={ui_configs.x_pos.max}
|
||||
onchangeFunction={(value) => onchangeFunction("x_pos", value)}
|
||||
valueLabelDisplay={x_variable_display}
|
||||
valueLabelDisplayLocation="top"
|
||||
/>
|
||||
<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 className={styles.position_wrapper}>
|
||||
<p className={clsx(styles.slider_label, styles.y_position_label)}>
|
||||
{t("config_page.vr.y_position")}
|
||||
@@ -221,8 +260,18 @@ const PositionControls = ({settings, onchangeFunction, selectFunction, ui_config
|
||||
max={ui_configs.y_pos.max}
|
||||
onchangeFunction={(value) => onchangeFunction("y_pos", value)}
|
||||
orientation="vertical"
|
||||
valueLabelDisplay={y_variable_display}
|
||||
valueLabelDisplayLocation="right"
|
||||
/>
|
||||
<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 className={styles.position_wrapper}>
|
||||
<p className={clsx(styles.slider_label, styles.z_position_label)}>
|
||||
{t("config_page.vr.z_position")}
|
||||
@@ -237,69 +286,162 @@ const PositionControls = ({settings, onchangeFunction, selectFunction, ui_config
|
||||
max={ui_configs.z_pos.max}
|
||||
onchangeFunction={(value) => onchangeFunction("z_pos", value)}
|
||||
orientation="vertical"
|
||||
valueLabelDisplay={z_variable_display}
|
||||
valueLabelDisplayLocation="left"
|
||||
/>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
const RotationControls = ({settings, onchangeFunction, selectFunction, default_ui_configs}) => {
|
||||
export const RotationControls = ({ settings, onchangeFunction, selectFunction, ui_configs, default_ui_configs }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const {
|
||||
variable_display: x_variable_display,
|
||||
is_max: is_max_rotation_x,
|
||||
is_min: is_min_rotation_x,
|
||||
countUp: countUpRotationX,
|
||||
countDown: countDownRotationX,
|
||||
} = useVariableControl("x_rotation", settings, onchangeFunction, ui_configs);
|
||||
|
||||
const {
|
||||
variable_display: y_variable_display,
|
||||
is_max: is_max_rotation_y,
|
||||
is_min: is_min_rotation_y,
|
||||
countUp: countUpRotationY,
|
||||
countDown: countDownRotationY,
|
||||
} = useVariableControl("y_rotation", settings, onchangeFunction, ui_configs);
|
||||
|
||||
const {
|
||||
variable_display: z_variable_display,
|
||||
is_max: is_max_rotation_z,
|
||||
is_min: is_min_rotation_z,
|
||||
countUp: countUpRotationZ,
|
||||
countDown: countDownRotationZ,
|
||||
} = useVariableControl("z_rotation", settings, onchangeFunction, ui_configs);
|
||||
|
||||
return (
|
||||
<div className={styles.rotation_controls}>
|
||||
<div className={styles.rotation_wrapper}>
|
||||
<p className={clsx(styles.slider_label, styles.x_rotation_label)}>
|
||||
{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>
|
||||
<Slider
|
||||
className={styles.x_rotation_slider}
|
||||
no_padding={true}
|
||||
variable={-settings.x_rotation}
|
||||
valueLabelFormat={settings.x_rotation}
|
||||
step={5}
|
||||
min={-180}
|
||||
max={180}
|
||||
step={ui_configs.x_rotation.step}
|
||||
min={ui_configs.x_rotation.min}
|
||||
max={ui_configs.x_rotation.max}
|
||||
onchangeFunction={(value) => onchangeFunction("x_rotation", -value)}
|
||||
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 className={styles.rotation_wrapper}>
|
||||
<p className={clsx(styles.slider_label, styles.y_rotation_label)}>
|
||||
{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>
|
||||
<Slider
|
||||
className={styles.y_rotation_slider}
|
||||
no_padding={true}
|
||||
variable={settings.y_rotation}
|
||||
step={5}
|
||||
min={-180}
|
||||
max={180}
|
||||
step={ui_configs.y_rotation.step}
|
||||
min={ui_configs.y_rotation.min}
|
||||
max={ui_configs.y_rotation.max}
|
||||
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 className={styles.rotation_wrapper}>
|
||||
<p className={clsx(styles.slider_label, styles.z_rotation_label)}>
|
||||
{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>
|
||||
<Slider
|
||||
className={styles.z_rotation_slider}
|
||||
no_padding={true}
|
||||
variable={settings.z_rotation}
|
||||
step={5}
|
||||
min={-180}
|
||||
max={180}
|
||||
step={ui_configs.z_rotation.step}
|
||||
min={ui_configs.z_rotation.min}
|
||||
max={ui_configs.z_rotation.max}
|
||||
onchangeFunction={(value) => onchangeFunction("z_rotation", value)}
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
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 { t } = useTranslation();
|
||||
|
||||
@@ -393,10 +535,6 @@ const ResetButton = ({onClickFunction}) => {
|
||||
);
|
||||
};
|
||||
|
||||
import SquareSvg from "@images/square.svg?react";
|
||||
import TriangleSvg from "@images/triangle.svg?react";
|
||||
import { randomIntMinMax } from "@utils";
|
||||
|
||||
const SendSampleTextToggleButton = () => {
|
||||
const { t } = useTranslation();
|
||||
const { sendTextToOverlay } = useSendTextToOverlay();
|
||||
@@ -446,3 +584,57 @@ const SendSampleTextToggleButton = () => {
|
||||
</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,
|
||||
};
|
||||
};
|
||||
@@ -16,7 +16,7 @@
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
max-width: 56rem;
|
||||
gap: 2rem;
|
||||
gap: 4rem;
|
||||
}
|
||||
|
||||
.controller_type_switch {
|
||||
@@ -58,12 +58,13 @@
|
||||
|
||||
.sample_text_button_wrapper {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: -74%;
|
||||
bottom: -12%;
|
||||
left: -80%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
// transform: translate(-50%, -50%);
|
||||
}
|
||||
.sample_text_button {
|
||||
background-color: var(--dark_850_color);
|
||||
@@ -121,20 +122,20 @@
|
||||
}
|
||||
.x_position_label {
|
||||
position: absolute;
|
||||
bottom: -4.6rem;
|
||||
bottom: -5rem;
|
||||
right: -46%;
|
||||
justify-content: end;
|
||||
}
|
||||
.y_position_label {
|
||||
position: absolute;
|
||||
top: -44%;
|
||||
left: 10%;
|
||||
justify-content: start;
|
||||
bottom: 110%;
|
||||
right: 119%;
|
||||
justify-content: end;
|
||||
}
|
||||
.z_position_label {
|
||||
position: absolute;
|
||||
top: 30%;
|
||||
left: 80%;
|
||||
top: 14%;
|
||||
left: 110%;
|
||||
}
|
||||
|
||||
.x_position_slider {
|
||||
@@ -155,14 +156,84 @@
|
||||
|
||||
.z_position_slider {
|
||||
position: absolute;
|
||||
bottom: 61%;
|
||||
left: 61%;
|
||||
bottom: 80%;
|
||||
left: 88%;
|
||||
transform: translate(50%,50%) rotate(45deg);
|
||||
width: 0%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
|
||||
%variable-button {
|
||||
width: 3.8rem;
|
||||
border-radius: 0.4rem;
|
||||
aspect-ratio: 1.2 / 1;
|
||||
background-color: var(--dark_850_color);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 1.6rem;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--primary_500_color);
|
||||
}
|
||||
&:active {
|
||||
background-color: var(--primary_600_color);
|
||||
}
|
||||
&.is_disabled {
|
||||
pointer-events: none;
|
||||
background-color: var(--dark_875_color);
|
||||
& .adjust_button_triangle_svg {
|
||||
color: var(--dark_800_color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@mixin variable-button-wrapper($vertical-pos, $vertical-value, $horizontal-pos, $horizontal-value, $rotate: 0deg) {
|
||||
position: absolute;
|
||||
#{$vertical-pos}: $vertical-value;
|
||||
#{$horizontal-pos}: $horizontal-value;
|
||||
display: flex;
|
||||
gap: 1.6rem;
|
||||
flex-direction: column;
|
||||
transform: translate(-50%) rotate($rotate);
|
||||
}
|
||||
|
||||
.button_wrapper {
|
||||
@extend %variable-button;
|
||||
|
||||
&.up .adjust_button_triangle_svg {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
&:not(.up) .adjust_button_triangle_svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
&.is_disabled {
|
||||
pointer-events: none;
|
||||
color: var(--dark_875_color);
|
||||
}
|
||||
}
|
||||
|
||||
.adjust_button_triangle_svg {
|
||||
width: 1.8rem;
|
||||
color: var(--dark_400_color);
|
||||
}
|
||||
|
||||
|
||||
|
||||
.y_position_button_wrapper {
|
||||
@include variable-button-wrapper(top, 30%, left, -26%);
|
||||
}
|
||||
|
||||
.x_position_button_wrapper {
|
||||
@include variable-button-wrapper(bottom, -38%, left, 46%, 90deg);
|
||||
}
|
||||
|
||||
.z_position_button_wrapper {
|
||||
@include variable-button-wrapper(bottom, 26%, right, -4%, 45deg);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// .rotation_controls {
|
||||
@@ -175,19 +246,20 @@
|
||||
|
||||
.x_rotation_label {
|
||||
position: absolute;
|
||||
top: -44%;
|
||||
left: 10%;
|
||||
bottom: 110%;
|
||||
right: 119%;
|
||||
justify-content: end;
|
||||
}
|
||||
.y_rotation_label {
|
||||
position: absolute;
|
||||
bottom: -4.6rem;
|
||||
bottom: -5rem;
|
||||
right: -46%;
|
||||
justify-content: end;
|
||||
}
|
||||
.z_rotation_label {
|
||||
position: absolute;
|
||||
top: -10%;
|
||||
right: -110%;
|
||||
top: -20%;
|
||||
right: -100%;
|
||||
}
|
||||
|
||||
.x_rotation_slider {
|
||||
@@ -216,6 +288,21 @@
|
||||
}
|
||||
|
||||
|
||||
|
||||
.x_rotation_button_wrapper {
|
||||
@include variable-button-wrapper(top, 30%, left, -26%);
|
||||
}
|
||||
|
||||
.y_rotation_button_wrapper {
|
||||
@include variable-button-wrapper(bottom, -38%, left, 46%, 90deg);
|
||||
}
|
||||
|
||||
.z_rotation_button_wrapper {
|
||||
@include variable-button-wrapper(bottom, 50%, right, -60%, -45deg);
|
||||
}
|
||||
|
||||
|
||||
|
||||
.slider_reset_button {
|
||||
background-color: var(--dark_875_color);
|
||||
padding: 0.6rem;
|
||||
|
||||
@@ -77,7 +77,7 @@ const OpenVrcMicMuteSyncQuickSetting = () => {
|
||||
return (
|
||||
<OpenQuickSettingButton
|
||||
label={t("config_page.others.vrc_mic_mute_sync.label")}
|
||||
variable={currentEnableVrcMicMuteSync.data}
|
||||
variable={currentEnableVrcMicMuteSync.data.is_enabled}
|
||||
onClickFunction={onClickFunction}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -59,7 +59,7 @@ export const UpdateModal = () => {
|
||||
</div>
|
||||
<div className={styles.cuda_section}>
|
||||
<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}
|
||||
</div>
|
||||
<div className={styles.version_desc_container}>
|
||||
@@ -85,7 +85,7 @@ const VersionDescComponent = (props) => {
|
||||
return (
|
||||
<div className={styles.version_desc_wrapper}>
|
||||
<div className={styles.version_desc_point}></div>
|
||||
<p className={styles.version_desc}>{props.desc}</p>
|
||||
<p className={styles.version_desc}>{`- ${props.desc}`}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -14,6 +14,7 @@ export const SnackbarController = () => {
|
||||
|
||||
const snackbar_classname = clsx(styles.snackbar_content, {
|
||||
[styles.is_success]: currentNotificationStatus.data.status === "success",
|
||||
[styles.is_warning]: currentNotificationStatus.data.status === "warning",
|
||||
[styles.is_error]: currentNotificationStatus.data.status === "error",
|
||||
});
|
||||
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
&.is_success {
|
||||
background-color: var(--success_bc_color);
|
||||
}
|
||||
&.is_warning {
|
||||
background-color: var(--waring_bc_color);
|
||||
}
|
||||
&.is_error {
|
||||
background-color: var(--error_bc_color);
|
||||
}
|
||||
|
||||
@@ -3,15 +3,16 @@ import styles from "./Checkbox.module.scss";
|
||||
export const Checkbox = ({
|
||||
checkboxId,
|
||||
variable,
|
||||
is_available = true,
|
||||
toggleFunction,
|
||||
size = "2.8rem",
|
||||
color = "var(--primary_600_color)",
|
||||
borderWidth = "0.2rem",
|
||||
padding = "2rem",
|
||||
}) => {
|
||||
|
||||
const wrapper_class_names = clsx(styles.checkbox_wrapper, {
|
||||
[styles.is_disabled]: variable.state === "pending",
|
||||
[styles.is_disabled]: !is_available,
|
||||
[styles.is_pending]: variable.state === "pending",
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -21,7 +22,6 @@ export const Checkbox = ({
|
||||
htmlFor={checkboxId}
|
||||
style={{
|
||||
"--checkbox-size": size,
|
||||
"--checkbox-color": color,
|
||||
"--checkbox-border-width": borderWidth,
|
||||
"--checkbox-padding": padding,
|
||||
}}
|
||||
|
||||
@@ -18,12 +18,19 @@
|
||||
border: var(--checkbox-color, var(--primary_600_color)) solid var(--checkbox-border-width, 0.2rem);
|
||||
}
|
||||
}
|
||||
&.is_disabled {
|
||||
&.is_pending {
|
||||
pointer-events: none;
|
||||
& .cbx {
|
||||
border-color: var(--primary_800_color);
|
||||
}
|
||||
}
|
||||
&.is_disabled {
|
||||
pointer-events: none;
|
||||
& .cbx {
|
||||
filter: grayscale(100%);
|
||||
border-color: var(--dark_800_color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox_wrapper .cbx {
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
useDeepLAuthKey,
|
||||
|
||||
useOscIpAddress,
|
||||
useWebsocket,
|
||||
} from "@logics_configs";
|
||||
import { ui_configs } from "../ui_configs";
|
||||
|
||||
@@ -31,106 +32,144 @@ export const _useBackendErrorHandling = () => {
|
||||
const { updateSpeakerPhraseTimeout } = useSpeakerPhraseTimeout();
|
||||
const { updateSpeakerMaxWords } = useSpeakerMaxWords();
|
||||
|
||||
const { updateDeepLAuthKey, saveErrorDeepLAuthKey } = useDeepLAuthKey();
|
||||
const { updateDeepLAuthKey } = useDeepLAuthKey();
|
||||
|
||||
const { updateOscIpAddress } = useOscIpAddress();
|
||||
const { updateEnableWebsocket, updateWebsocketHost, updateWebsocketPort } = useWebsocket();
|
||||
|
||||
const errorHandling_Backend = ({message, data, endpoint, _result}) => {
|
||||
switch (message) {
|
||||
case "No mic device detected":
|
||||
showNotification_Error(t("common_error.no_device_mic"));
|
||||
break;
|
||||
case "No speaker device detected":
|
||||
showNotification_Error(t("common_error.no_device_speaker"));
|
||||
break;
|
||||
const errorHandling_Backend = ({message, data, endpoint, result}) => {
|
||||
switch (endpoint) {
|
||||
case "/run/error_device":
|
||||
if (message === "No mic device detected") showNotification_Error(t("common_error.no_device_mic"));
|
||||
if (message === "No speaker device detected") showNotification_Error(t("common_error.no_device_speaker"));
|
||||
return;
|
||||
|
||||
case "Mic energy threshold value is out of range":
|
||||
case "/set/data/mic_threshold":
|
||||
if (message === "Mic energy threshold value is out of range") {
|
||||
showNotification_Error(t("common_error.threshold_invalid_value",
|
||||
{ min: ui_configs.mic_threshold_min, max: ui_configs.mic_threshold_max },
|
||||
));
|
||||
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",
|
||||
{ min: ui_configs.speaker_threshold_min, max: ui_configs.speaker_threshold_max },
|
||||
));
|
||||
break;
|
||||
}
|
||||
return;
|
||||
|
||||
case "CTranslate2 weight download error":
|
||||
showNotification_Error(t("common_error.failed_download_weight_ctranslate2"));
|
||||
break;
|
||||
case "Whisper weight download error":
|
||||
showNotification_Error(t("common_error.failed_download_weight_whisper"));
|
||||
break;
|
||||
case "/run/error_ctranslate2_weight":
|
||||
if (message === "CTranslate2 weight download error") showNotification_Error(t("common_error.failed_download_weight_ctranslate2"));
|
||||
return;
|
||||
case "/run/error_whisper_weight":
|
||||
if (message === "Whisper weight download error") showNotification_Error(t("common_error.failed_download_weight_whisper"));
|
||||
return;
|
||||
|
||||
case "Translation engine limit error":
|
||||
showNotification_Error(t("common_error.translation_limit"));
|
||||
break;
|
||||
case "/run/error_translation_engine":
|
||||
if (message === "Translation engine limit error") showNotification_Error(t("common_error.translation_limit"));
|
||||
return;
|
||||
|
||||
case "DeepL auth key length is not correct":
|
||||
case "/set/data/deepl_auth_key":
|
||||
if (message === "DeepL auth key length is not correct") {
|
||||
updateDeepLAuthKey(data);
|
||||
showNotification_Error(t("common_error.deepl_auth_key_invalid_length"));
|
||||
break;
|
||||
case "Authentication failure of deepL auth key":
|
||||
} else if (message === "Authentication failure of deepL auth key") {
|
||||
updateDeepLAuthKey(data);
|
||||
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);
|
||||
showNotification_Error(
|
||||
t("common_error.invalid_value_mic_record_timeout",
|
||||
{ mic_phrase_timeout_label: t("config_page.transcription.mic_phrase_timeout.label") }
|
||||
));
|
||||
break;
|
||||
case "Mic phrase timeout value is out of range":
|
||||
showNotification_Error(t("common_error.invalid_value_mic_record_timeout", {
|
||||
mic_phrase_timeout_label: t("config_page.transcription.mic_phrase_timeout.label")
|
||||
}));
|
||||
}
|
||||
return;
|
||||
case "/set/data/mic_phrase_timeout":
|
||||
if (message === "Mic phrase timeout value is out of range") {
|
||||
updateMicPhraseTimeout(data);
|
||||
showNotification_Error(
|
||||
t("common_error.invalid_value_mic_phrase_timeout",
|
||||
{ mic_record_timeout_label: t("config_page.transcription.mic_record_timeout.label") }
|
||||
));
|
||||
break;
|
||||
case "Mic max phrases value is out of range":
|
||||
showNotification_Error(t("common_error.invalid_value_mic_phrase_timeout", {
|
||||
mic_record_timeout_label: t("config_page.transcription.mic_record_timeout.label")
|
||||
}));
|
||||
}
|
||||
return;
|
||||
case "/set/data/mic_max_phrases":
|
||||
if (message === "Mic max phrases value is out of range") {
|
||||
updateMicMaxWords(data);
|
||||
showNotification_Error(t("common_error.invalid_value_mic_max_phrase"));
|
||||
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);
|
||||
showNotification_Error(
|
||||
t("common_error.invalid_value_speaker_record_timeout",
|
||||
{ speaker_phrase_timeout_label: t("config_page.transcription.speaker_phrase_timeout.label") }
|
||||
));
|
||||
break;
|
||||
case "Speaker phrase timeout value is out of range":
|
||||
showNotification_Error(t("common_error.invalid_value_speaker_record_timeout", {
|
||||
speaker_phrase_timeout_label: t("config_page.transcription.speaker_phrase_timeout.label")
|
||||
}));
|
||||
}
|
||||
return;
|
||||
case "/set/data/speaker_phrase_timeout":
|
||||
if (message === "Speaker phrase timeout value is out of range") {
|
||||
updateSpeakerPhraseTimeout(data);
|
||||
showNotification_Error(
|
||||
t("common_error.invalid_value_speaker_phrase_timeout",
|
||||
{ speaker_record_timeout_label: t("config_page.transcription.speaker_record_timeout.label") }
|
||||
));
|
||||
break;
|
||||
case "Speaker max phrases value is out of range":
|
||||
showNotification_Error(t("common_error.invalid_value_speaker_phrase_timeout", {
|
||||
speaker_record_timeout_label: t("config_page.transcription.speaker_record_timeout.label")
|
||||
}));
|
||||
}
|
||||
return;
|
||||
case "/set/data/speaker_max_phrases":
|
||||
if (message === "Speaker max phrases value is out of range") {
|
||||
updateSpeakerMaxWords(data);
|
||||
showNotification_Error(t("common_error.invalid_value_speaker_max_phrase"));
|
||||
break;
|
||||
}
|
||||
return;
|
||||
|
||||
|
||||
// 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);
|
||||
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);
|
||||
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:
|
||||
// determine by endpoint, not message.
|
||||
if (endpoint === "/set/data/deepl_auth_key") saveErrorDeepLAuthKey({message, data, endpoint, _result});
|
||||
|
||||
break;
|
||||
console.error(`Invalid endpoint or message: ${endpoint}\nmessage: ${message}\nresult: ${JSON.stringify(result)}`);
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -11,5 +11,6 @@ export { useMessage } from "./useMessage";
|
||||
export { useUpdateSoftware } from "./useUpdateSoftware";
|
||||
export { useVolume } from "./useVolume";
|
||||
export { useHandleNetworkConnection } from "./useHandleNetworkConnection";
|
||||
export { useHandleOscQuery } from "./useHandleOscQuery";
|
||||
export { useIsVrctAvailable } from "./useIsVrctAvailable";
|
||||
export { useFetch } from "./useFetch";
|
||||
44
src-ui/logics/common/useHandleOscQuery.js
Normal file
44
src-ui/logics/common/useHandleOscQuery.js
Normal 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,
|
||||
};
|
||||
};
|
||||
@@ -5,6 +5,16 @@ export const useNotificationStatus = () => {
|
||||
|
||||
const generateRandomKey = () => Math.random();
|
||||
|
||||
const showNotification_Warning = (message, options = {}) => {
|
||||
updateNotificationStatus({
|
||||
status: "warning",
|
||||
is_open: true,
|
||||
key: generateRandomKey(),
|
||||
message: message,
|
||||
options: options,
|
||||
});
|
||||
};
|
||||
|
||||
const showNotification_Error = (message, options = {}) => {
|
||||
updateNotificationStatus({
|
||||
status: "error",
|
||||
@@ -37,6 +47,7 @@ export const useNotificationStatus = () => {
|
||||
currentNotificationStatus,
|
||||
updateNotificationStatus,
|
||||
|
||||
showNotification_Warning,
|
||||
showNotification_Error,
|
||||
showNotification_Success,
|
||||
closeNotification,
|
||||
|
||||
67
src-ui/logics/configs/advanced_settings/useWebsocket.js
Normal file
67
src-ui/logics/configs/advanced_settings/useWebsocket.js
Normal 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,
|
||||
|
||||
};
|
||||
};
|
||||
@@ -56,6 +56,7 @@ export { useHotkeys } from "./hotkeys/useHotkeys";
|
||||
|
||||
export { useOscIpAddress } from "./advanced_settings/useOscIpAddress";
|
||||
export { useOscPort } from "./advanced_settings/useOscPort";
|
||||
export { useWebsocket } from "./advanced_settings/useWebsocket";
|
||||
|
||||
|
||||
export { useSupporters } from "./supporters/useSupporters";
|
||||
|
||||
@@ -12,7 +12,7 @@ export const useEnableVrcMicMuteSync = () => {
|
||||
|
||||
const toggleEnableVrcMicMuteSync = () => {
|
||||
pendingEnableVrcMicMuteSync();
|
||||
if (currentEnableVrcMicMuteSync.data) {
|
||||
if (currentEnableVrcMicMuteSync.data.is_enabled) {
|
||||
asyncStdoutToPython("/set/disable/vrc_mic_mute_sync");
|
||||
} else {
|
||||
asyncStdoutToPython("/set/enable/vrc_mic_mute_sync");
|
||||
|
||||
@@ -29,11 +29,6 @@ export const useDeepLAuthKey = () => {
|
||||
showNotification_Success(t("config_page.translation.deepl_auth_key.auth_key_success"));
|
||||
};
|
||||
|
||||
const saveErrorDeepLAuthKey = ({data, message}) => {
|
||||
updateDeepLAuthKey(data);
|
||||
showNotification_Error(message);
|
||||
};
|
||||
|
||||
return {
|
||||
currentDeepLAuthKey,
|
||||
getDeepLAuthKey,
|
||||
@@ -41,7 +36,6 @@ export const useDeepLAuthKey = () => {
|
||||
setDeepLAuthKey,
|
||||
deleteDeepLAuthKey,
|
||||
|
||||
saveErrorDeepLAuthKey,
|
||||
savedDeepLAuthKey,
|
||||
};
|
||||
};
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
useIsVrctAvailable,
|
||||
useNotificationStatus,
|
||||
useHandleNetworkConnection,
|
||||
useHandleOscQuery,
|
||||
|
||||
useSoftwareVersion,
|
||||
useComputeMode,
|
||||
@@ -75,6 +76,7 @@ import {
|
||||
usePlugins,
|
||||
useOscIpAddress,
|
||||
useOscPort,
|
||||
useWebsocket,
|
||||
} from "@logics_configs";
|
||||
|
||||
export const useReceiveRoutes = () => {
|
||||
@@ -82,6 +84,7 @@ export const useReceiveRoutes = () => {
|
||||
const { updateComputeMode } = useComputeMode();
|
||||
const { updateInitProgress } = useInitProgress();
|
||||
const { updateIsBackendReady } = useIsBackendReady();
|
||||
const { handleOscQuery } = useHandleOscQuery();
|
||||
const { restoreWindowGeometry } = useWindow();
|
||||
const { updateIsMainPageCompactMode } = useIsMainPageCompactMode();
|
||||
const {
|
||||
@@ -180,6 +183,11 @@ export const useReceiveRoutes = () => {
|
||||
|
||||
const { updateOscIpAddress } = useOscIpAddress();
|
||||
const { updateOscPort } = useOscPort();
|
||||
const {
|
||||
updateEnableWebsocket,
|
||||
updateWebsocketHost,
|
||||
updateWebsocketPort,
|
||||
} = useWebsocket();
|
||||
|
||||
|
||||
|
||||
@@ -213,6 +221,12 @@ export const useReceiveRoutes = () => {
|
||||
}));
|
||||
},
|
||||
"/run/connected_network": handleNetworkConnection,
|
||||
"/run/enable_osc_query": ({data, disabled_functions}) => {
|
||||
handleOscQuery({
|
||||
is_osc_query_enabled: data,
|
||||
disabled_functions: disabled_functions,
|
||||
});
|
||||
},
|
||||
|
||||
// Main Page
|
||||
// Page Controls
|
||||
@@ -474,9 +488,15 @@ export const useReceiveRoutes = () => {
|
||||
"/set/enable/logger_feature": updateEnableAutoExportMessageLogs,
|
||||
"/set/disable/logger_feature": updateEnableAutoExportMessageLogs,
|
||||
|
||||
"/get/data/vrc_mic_mute_sync": updateEnableVrcMicMuteSync,
|
||||
"/set/enable/vrc_mic_mute_sync": updateEnableVrcMicMuteSync,
|
||||
"/set/disable/vrc_mic_mute_sync": updateEnableVrcMicMuteSync,
|
||||
"/get/data/vrc_mic_mute_sync": (payload) => updateEnableVrcMicMuteSync((old_value) => {
|
||||
return {...old_value.data, is_enabled: payload};
|
||||
}),
|
||||
"/set/enable/vrc_mic_mute_sync": (payload) => updateEnableVrcMicMuteSync((old_value) => {
|
||||
return {...old_value.data, is_enabled: payload};
|
||||
}),
|
||||
"/set/disable/vrc_mic_mute_sync": (payload) => updateEnableVrcMicMuteSync((old_value) => {
|
||||
return {...old_value.data, is_enabled: payload};
|
||||
}),
|
||||
|
||||
"/get/data/send_message_to_vrc": updateEnableSendMessageToVrc,
|
||||
"/set/enable/send_message_to_vrc": updateEnableSendMessageToVrc,
|
||||
@@ -505,6 +525,16 @@ export const useReceiveRoutes = () => {
|
||||
"/get/data/osc_port": updateOscPort,
|
||||
"/set/data/osc_port": updateOscPort,
|
||||
|
||||
"/get/data/websocket_server": updateEnableWebsocket,
|
||||
"/set/enable/websocket_server": updateEnableWebsocket,
|
||||
"/set/disable/websocket_server": updateEnableWebsocket,
|
||||
|
||||
"/get/data/websocket_host": updateWebsocketHost,
|
||||
"/set/data/websocket_host": updateWebsocketHost,
|
||||
|
||||
"/get/data/websocket_port": updateWebsocketPort,
|
||||
"/set/data/websocket_port": updateWebsocketPort,
|
||||
|
||||
"/get/data/mic_avg_logprob": ()=>{}, // Not implemented on UI yet
|
||||
"/get/data/mic_no_speech_prob": ()=>{}, // Not implemented on UI yet
|
||||
"/get/data/speaker_avg_logprob": ()=>{}, // Not implemented on UI yet
|
||||
@@ -514,30 +544,6 @@ export const useReceiveRoutes = () => {
|
||||
"/get/data/transcription_engines": ()=>{}, // Not implemented on UI yet. (if ai_models has not been detected, this will be blank array[]. if the ai_models are ok but just network has not connected, it'l be only ["Whisper"])
|
||||
};
|
||||
|
||||
const error_status_routes = {
|
||||
"/run/error_device": errorHandling_Backend,
|
||||
|
||||
"/run/error_ctranslate2_weight": errorHandling_Backend,
|
||||
"/run/error_whisper_weight": errorHandling_Backend,
|
||||
|
||||
"/set/data/deepl_auth_key": errorHandling_Backend,
|
||||
|
||||
"/run/error_translation_engine": errorHandling_Backend,
|
||||
|
||||
"/set/data/mic_threshold": errorHandling_Backend,
|
||||
"/set/data/mic_record_timeout": errorHandling_Backend,
|
||||
"/set/data/mic_phrase_timeout": errorHandling_Backend,
|
||||
"/set/data/mic_max_phrases": errorHandling_Backend,
|
||||
|
||||
"/set/data/speaker_threshold": errorHandling_Backend,
|
||||
"/set/data/speaker_record_timeout": errorHandling_Backend,
|
||||
"/set/data/speaker_phrase_timeout": errorHandling_Backend,
|
||||
"/set/data/speaker_max_phrases": errorHandling_Backend,
|
||||
|
||||
"/set/data/osc_ip_address": errorHandling_Backend,
|
||||
};
|
||||
|
||||
|
||||
const receiveRoutes = (parsed_data) => {
|
||||
const initDataSyncProcess = (payload) => {
|
||||
for (const [endpoint, value] of Object.entries(payload)) {
|
||||
@@ -567,17 +573,12 @@ export const useReceiveRoutes = () => {
|
||||
break;
|
||||
|
||||
case 400:
|
||||
const error_route = error_status_routes[parsed_data.endpoint];
|
||||
if (error_route) {
|
||||
error_route({
|
||||
errorHandling_Backend({
|
||||
message: parsed_data.result.message,
|
||||
data: parsed_data.result.data,
|
||||
endpoint: parsed_data.endpoint,
|
||||
_result: parsed_data.result,
|
||||
result: parsed_data.result,
|
||||
});
|
||||
} else {
|
||||
handleInvalidEndpoint(parsed_data);
|
||||
}
|
||||
break;
|
||||
case 500:
|
||||
showNotification_Error(
|
||||
|
||||
@@ -60,10 +60,19 @@ export const createAtomWithHook = (initialValue, base_name, options) => {
|
||||
};
|
||||
|
||||
const updateAtom = (payload, options = {}) => {
|
||||
const { remain_state = false, set_state } = options;
|
||||
const { remain_state = false, set_state, lock_state } = options;
|
||||
|
||||
setAtom((currentValue) => {
|
||||
const new_state = set_state ?? (remain_state ? currentValue.state : "ok");
|
||||
let new_state;
|
||||
if (lock_state) {
|
||||
new_state = set_state;
|
||||
} else {
|
||||
if (currentValue.lock_state) {
|
||||
new_state = currentValue.state;
|
||||
} else {
|
||||
new_state = set_state ?? (remain_state ? currentValue.state : "ok");
|
||||
}
|
||||
}
|
||||
|
||||
const updated_data = typeof payload === "function"
|
||||
? payload(currentValue)
|
||||
@@ -290,6 +299,10 @@ export const { atomInstance: Atom_PluginsData, useHook: useStore_PluginsData } =
|
||||
export const { atomInstance: Atom_OscIpAddress, useHook: useStore_OscIpAddress } = createAtomWithHook("127.0.0.1", "OscIpAddress");
|
||||
export const { atomInstance: Atom_OscPort, useHook: useStore_OscPort } = createAtomWithHook("9000", "OscPort");
|
||||
|
||||
export const { atomInstance: Atom_EnableWebsocket, useHook: useStore_EnableWebsocket } = createAtomWithHook(true, "EnableWebsocket");
|
||||
export const { atomInstance: Atom_WebsocketHost, useHook: useStore_WebsocketHost } = createAtomWithHook("127.0.0.1", "WebsocketHost");
|
||||
export const { atomInstance: Atom_WebsocketPort, useHook: useStore_WebsocketPort } = createAtomWithHook("2231", "WebsocketPort");
|
||||
|
||||
|
||||
|
||||
// Supporters
|
||||
|
||||
@@ -7,12 +7,18 @@ export const ui_configs = {
|
||||
x_pos: { step: 0.05, min: -0.5, max: 0.5 },
|
||||
y_pos: { step: 0.05, min: -0.8, max: 0.8 },
|
||||
z_pos: { step: 0.05, min: -0.5, max: 1.5 },
|
||||
x_rotation: { min: -180, max: 180, step: 5 },
|
||||
y_rotation: { min: -180, max: 180, step: 5 },
|
||||
z_rotation: { min: -180, max: 180, step: 5 },
|
||||
ui_scaling: { step: 10, min: 40, max: 200 },
|
||||
},
|
||||
overlay_large_log: {
|
||||
x_pos: { step: 0.05, min: -0.5, max: 0.5 },
|
||||
y_pos: { step: 0.05, min: -0.8, max: 0.8 },
|
||||
z_pos: { step: 0.05, min: -0.5, max: 1.5 },
|
||||
x_rotation: { min: -180, max: 180, step: 5 },
|
||||
y_rotation: { min: -180, max: 180, step: 5 },
|
||||
z_rotation: { min: -180, max: 180, step: 5 },
|
||||
ui_scaling: { step: 10, min: 40, max: 200 },
|
||||
},
|
||||
|
||||
|
||||
81
zip.py
81
zip.py
@@ -1,37 +1,70 @@
|
||||
import os
|
||||
import zipfile
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
import time
|
||||
from tqdm import tqdm # tqdmをインポート
|
||||
|
||||
def zip_files_and_directory(zip_name, file_paths, dir_paths):
|
||||
def zip_files_and_directory(zip_name, file_paths, dir_paths, verbose=False):
|
||||
zip_file_path = Path(zip_name)
|
||||
# ZIPファイルを作成
|
||||
with zipfile.ZipFile(zip_name, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
||||
try:
|
||||
with zipfile.ZipFile(zip_file_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
||||
# ファイルを追加
|
||||
for file_path in file_paths:
|
||||
if os.path.isfile(file_path):
|
||||
zipf.write(file_path, os.path.basename(file_path))
|
||||
for file_path_str in tqdm(file_paths, desc="Adding files", unit="file"):
|
||||
file_path = Path(file_path_str)
|
||||
if file_path.is_file():
|
||||
zipf.write(file_path, file_path.name)
|
||||
if verbose:
|
||||
print(f"Add file: {file_path}")
|
||||
else:
|
||||
print(f"Warning: File not found or is not a file: {file_path}")
|
||||
|
||||
# ディレクトリを追加
|
||||
for dir_path in dir_paths:
|
||||
if os.path.isdir(dir_path):
|
||||
for foldername, subfolders, filenames in os.walk(dir_path):
|
||||
for filename in filenames:
|
||||
file_full_path = os.path.join(foldername, filename)
|
||||
# ディレクトリを保持しつつ、ルートに配置
|
||||
arcname = os.path.join(
|
||||
os.path.basename(dir_path),
|
||||
os.path.relpath(file_full_path, dir_path)
|
||||
)
|
||||
zipf.write(file_full_path, arcname)
|
||||
print(f"Add file: {file_full_path}")
|
||||
for dir_path_str in dir_paths:
|
||||
dir_path = Path(dir_path_str)
|
||||
if dir_path.is_dir():
|
||||
all_files_in_dir = [item for item in dir_path.rglob("*") if item.is_file()]
|
||||
for item in tqdm(all_files_in_dir, desc=f"Adding files from {dir_path.name}", unit="file"):
|
||||
# ディレクトリ構造を保持しつつ、ルートに配置
|
||||
arcname = Path(dir_path.name) / item.relative_to(dir_path)
|
||||
zipf.write(item, arcname)
|
||||
if verbose:
|
||||
print(f"Add file: {item}")
|
||||
else:
|
||||
print(f"Warning: Directory not found or is not a directory: {dir_path}")
|
||||
print(f"Successfully created zip file: {zip_file_path}")
|
||||
except IOError as e:
|
||||
print(f"Error: Could not create zip file {zip_file_path}. Reason: {e}")
|
||||
except Exception as e:
|
||||
print(f"An unexpected error occurred: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--zip_name", type=str, default="VRCT.zip")
|
||||
parser.add_argument("--file_paths", type=str, nargs="*", default=["src-tauri/target/release/VRCT.exe", "src-tauri/target/release/VRCT-sidecar.exe"])
|
||||
parser.add_argument("--dir_paths", type=str, nargs="*", default=["src-tauri/target/release/_internal"])
|
||||
start_time = time.time()
|
||||
parser = argparse.ArgumentParser(description="Create a zip file from specified files and directories.")
|
||||
parser.add_argument("--zip_name", type=str, default="VRCT.zip", help="Name of the output zip file.")
|
||||
parser.add_argument(
|
||||
"--file_paths",
|
||||
type=str,
|
||||
nargs="*",
|
||||
default=["src-tauri/target/release/VRCT.exe", "src-tauri/target/release/VRCT-sidecar.exe"],
|
||||
help="List of file paths to include in the zip."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dir_paths",
|
||||
type=str,
|
||||
nargs="*",
|
||||
default=["src-tauri/target/release/_internal"],
|
||||
help="List of directory paths to include in the zip."
|
||||
)
|
||||
parser.add_argument(
|
||||
"-v", "--verbose",
|
||||
action="store_true",
|
||||
help="Increase output verbosity."
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
zip_files_and_directory(args.zip_name, args.file_paths, args.dir_paths)
|
||||
print("Complete!")
|
||||
zip_files_and_directory(args.zip_name, args.file_paths, args.dir_paths, args.verbose)
|
||||
end_time = time.time()
|
||||
processing_time = end_time - start_time
|
||||
print(f"Complete! Processing time: {processing_time:.2f} seconds")
|
||||
Reference in New Issue
Block a user