Merge branch 'develop'

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

3
.gitignore vendored
View File

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

View File

@@ -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={},

View File

@@ -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={},

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -24,6 +24,9 @@ common_error:
invalid_value_speaker_phrase_timeout: "It cannot be set lower than '{{speaker_record_timeout_label}}' with a value of 0 or more."
invalid_value_speaker_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.

View File

@@ -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: プラグインをダウンロード中。

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -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()

View File

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

View File

@@ -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()

View File

@@ -7,10 +7,22 @@ from tinyoscquery.queryservice import OSCQueryService
from tinyoscquery.query import OSCQueryBrowser, OSCQueryClient
from tinyoscquery.utility import get_open_udp_port, get_open_tcp_port
from tinyoscquery.shared.node import OSCAccess
from utils import errorLogging
try:
from utils import errorLogging
except ImportError:
def errorLogging():
import traceback
print("Error occurred:", traceback.format_exc())
class OSCHandler:
def __init__(self, ip_address="127.0.0.1", port=9000) -> None:
if ip_address in ["127.0.0.1", "localhost"]:
self.is_osc_query_enabled = True
else:
self.is_osc_query_enabled = False
self.osc_ip_address = ip_address
self.osc_port = port
self.osc_parameter_muteself = "/avatar/parameters/MuteSelf"
@@ -24,15 +36,28 @@ class OSCHandler:
self.osc_server_ip_address = ip_address
self.http_port = None
self.osc_server_port = None
self.dict_filter_and_target = {}
self.browser = None
def getIsOscQueryEnabled(self) -> bool:
return self.is_osc_query_enabled
def setOscIpAddress(self, ip_address:str) -> None:
if ip_address in ["127.0.0.1", "localhost"]:
self.is_osc_query_enabled = True
else:
self.is_osc_query_enabled = False
self.oscServerStop()
self.osc_ip_address = ip_address
self.udp_client = udp_client.SimpleUDPClient(self.osc_ip_address, self.osc_port)
self.receiveOscParameters()
def setOscPort(self, port:int) -> None:
self.oscServerStop()
self.osc_port = port
self.udp_client = udp_client.SimpleUDPClient(self.osc_ip_address, self.osc_port)
self.receiveOscParameters()
# send OSC message typing
def sendTyping(self, flag:bool=False) -> None:
@@ -44,6 +69,10 @@ class OSCHandler:
self.udp_client.send_message(self.osc_parameter_chatbox_input, [f"{message}", True, notification])
def getOSCParameterValue(self, address:str) -> Any:
if not self.is_osc_query_enabled:
# OSCQueryが無効な場合はNoneを返す
return None
value = None
try:
# browserインスタンスを再利用し、毎回の生成と破棄を避ける
@@ -71,19 +100,26 @@ class OSCHandler:
def getOSCParameterMuteSelf(self) -> bool:
return self.getOSCParameterValue(self.osc_parameter_muteself)
def receiveOscParameters(self, dict_filter_and_target:dict) -> None:
def setDictFilterAndTarget(self, dict_filter_and_target:dict) -> None:
self.dict_filter_and_target = dict_filter_and_target
def receiveOscParameters(self) -> None:
if self.is_osc_query_enabled is False:
# OSCQueryが無効な場合は何もしない
return
self.osc_server_port = get_open_udp_port()
self.http_port = get_open_tcp_port()
osc_dispatcher = dispatcher.Dispatcher()
for filter, target in dict_filter_and_target.items():
for filter, target in self.dict_filter_and_target.items():
osc_dispatcher.map(filter, target)
self.osc_server = osc_server.ThreadingOSCUDPServer((self.osc_server_ip_address, self.osc_server_port), osc_dispatcher, asyncio.get_event_loop())
self.osc_server = osc_server.ThreadingOSCUDPServer((self.osc_server_ip_address, self.osc_server_port), osc_dispatcher)
Thread(target=self.oscServerServe, daemon=True).start()
while True:
try:
self.osc_query_service = OSCQueryService(self.osc_query_service_name, self.http_port, self.osc_server_port)
for filter, target in dict_filter_and_target.items():
for filter, target in self.dict_filter_and_target.items():
self.osc_query_service.advertise_endpoint(filter, access=OSCAccess.READWRITE_VALUE)
break
except Exception:
@@ -112,12 +148,26 @@ class OSCHandler:
if __name__ == "__main__":
handler = OSCHandler()
handler.receiveOscParameters({
"/avatar/parameters/MuteSelf": print,
handler.setDictFilterAndTarget({
"/avatar/parameters/MuteSelf": lambda address, *args: print(f"Received {address} with args {args}"),
"/chatbox/typing": lambda address, *args: print(f"Received {address} with args {args}"),
"/chatbox/input": lambda address, *args: print(f"Received {address} with args {args}"),
})
handler.receiveOscParameters()
sleep(5)
handler.sendTyping(True)
sleep(1)
handler.sendMessage(message="Hello World", notification=True)
sleep(60)
handler.sendMessage(message="Hello World 1", notification=True)
sleep(10)
print("IP address changed to 192.168.193.2")
handler.setOscIpAddress("192.168.193.2")
sleep(5)
handler.sendMessage(message="Hello World 2", notification=True)
print("IP address changed to 127.0.0.1")
handler.setOscIpAddress("127.0.0.1")
sleep(5)
handler.sendMessage(message="Hello World 3", notification=True)
sleep(10)
handler.oscServerStop()

View File

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

View File

@@ -0,0 +1,221 @@
import asyncio
import threading
import websockets
from websockets.legacy.server import WebSocketServerProtocol
from typing import Callable, Set, Optional
class WebSocketServer:
"""
WebSocketサーバーを管理するクラス。
主な機能:
- サーバーの起動・停止
- クライアント接続管理 (接続/切断の追跡)
- メッセージ受信のコールバック処理
- メッセージのブロードキャスト機能
- GUIスレッド等からメッセージ送信するためのキュー
"""
def __init__(self, host: str='127.0.0.1', port: int=8765):
"""
サーバーのホスト名とポートを指定して初期化します。
"""
self.host = host
self.port = port
self.clients: Set[WebSocketServerProtocol] = set() # 接続クライアント集合
self._message_handler: Optional[Callable[['WebSocketServer', WebSocketServerProtocol, str], None]] = None
self._loop: Optional[asyncio.AbstractEventLoop] = None
self._server: Optional[websockets.serve] = None
self._thread: Optional[threading.Thread] = None
self._send_queue: Optional[asyncio.Queue] = None # 外部スレッド向け非同期キュー
self.is_running: bool = False # サーバーの起動状態を示すフラグ
def set_message_handler(self, handler: Callable[['WebSocketServer', WebSocketServerProtocol, str], None]):
"""
クライアントからメッセージ受信時に呼び出すコールバックを設定します。
コールバックのシグネチャ: (server, websocket, message) -> None
"""
self._message_handler = handler
async def _handler(self, websocket):
"""
単一クライアントとのセッションを処理するハンドラです。
新規接続時にクライアントを集合に追加し、メッセージを受信してコールバックを呼び出します。
切断時には集合からクライアントを削除します。
"""
# 接続クライアントを集合に追加
self.clients.add(websocket)
try:
async for message in websocket:
# メッセージ受信時にコールバック呼び出し
if self._message_handler:
self._message_handler(self, websocket, message)
except websockets.exceptions.ConnectionClosed:
# クライアントが切断した場合
pass
finally:
# 切断時に集合から削除
self.clients.remove(websocket)
async def _broadcast_async(self, message: str):
"""
すべての接続クライアントにメッセージを送信する非同期メソッド。
"""
if not self.clients:
return
# 全クライアントへ並列に送信
await asyncio.gather(
*[client.send(message) for client in self.clients],
return_exceptions=True
)
async def _send_loop(self):
"""
内部キューからメッセージを取り出し、すべてのクライアントに送信するループ処理。
GUIなど他スレッドから送信メッセージをキューに入れてもらい、このコルーチンで配信します。
"""
assert self._send_queue is not None
while True:
message = await self._send_queue.get()
if message is None:
# Noneを受け取ったらシャットダウン指示とみなしてループを抜ける
break
await self._broadcast_async(message)
def send(self, message: str):
"""
外部スレッドからサーバーにメッセージを送信するためのメソッドです。
イベントループ上で安全にキューにメッセージを積み、_send_loop()経由でブロードキャストします。
"""
if self._loop and self._send_queue:
# キューにput_nowaitするコールをイベントループにスケジュール
self._loop.call_soon_threadsafe(self._send_queue.put_nowait, message)
def broadcast(self, message: str):
"""
外部スレッドや他コルーチンから全クライアントにメッセージを送信するユーティリティ。
asyncio.run_coroutine_threadsafe を使ってループ上でブロードキャストを実行します。
"""
if self._loop:
# コルーチン自体をrun_coroutine_threadsafeに渡す
asyncio.run_coroutine_threadsafe(
self._broadcast_async(message), self._loop
)
def start(self):
"""
サーバーを起動します。新しいスレッド上で asyncio イベントループを動かし、serve()を実行します。
"""
if self._thread and self._thread.is_alive():
return # 既に起動中
# 新しいスレッドでイベントループを開始
self._thread = threading.Thread(target=self._run_loop, daemon=True)
self._thread.start()
def _run_loop(self):
"""
別スレッド上で実行されるイベントループ用のメソッド。
サーバーの起動と、送信用キューのタスク登録を行います。
"""
# 新しいイベントループを作成してこのスレッドの現在のループとして設定
self._loop = asyncio.new_event_loop()
asyncio.set_event_loop(self._loop)
async def setup_server():
# サーバーを起動し、listenを開始
self._server = await websockets.serve(self._handler, self.host, self.port)
# 送信キューを初期化
self._send_queue = asyncio.Queue()
# 送信ループタスクを開始
self._loop.create_task(self._send_loop())
# サーバーの起動を待機
# 設定関数を実行してサーバーを起動
self._loop.run_until_complete(setup_server())
self.is_running = True
# サーバーが起動したら、接続待機を開始
# print(f"WebSocket server started on ws://{self.host}:{self.port}")
try:
# サーバーが停止するまでループを継続
self._loop.run_forever()
finally:
# 停止指示が出たらすべての接続を閉じ、イベントループを終了
self._loop.run_until_complete(self._shutdown())
self._loop.close()
async def _shutdown(self):
"""
サーバーとクライアントを安全にシャットダウンする非同期処理。
serveオブジェクトをcloseし、wait_closed()で完全に終了を待ちます。
さらに接続中の各WebSocketをcloseします。
"""
# サーバーのListenを停止
if self._server:
self._server.close()
await self._server.wait_closed()
# 接続中クライアントを順次クローズ
for ws in list(self.clients):
try:
await ws.close()
except Exception:
pass
def stop(self):
"""
サーバーを停止します。別スレッドで動作中のイベントループに停止を指示し、スレッドを終了させます。
"""
self.is_running = False
if self._loop:
# サーバーのlistenを停止し、ループ停止をスケジュール
self._loop.call_soon_threadsafe(self._server.close)
# None をキューに入れて_send_loopを抜けさせる
self._loop.call_soon_threadsafe(self._send_queue.put_nowait, None)
# ループ停止
self._loop.call_soon_threadsafe(self._loop.stop)
# スレッドの終了を待つ
if self._thread:
self._thread.join()
if __name__ == "__main__":
# テスト用の簡単なメッセージハンドラ
def message_handler(server: WebSocketServer, websocket: WebSocketServerProtocol, message: str):
print(f"Received message from {websocket.remote_address}: {message}")
server.send(f"Echo: {message}")
def send_message(server: WebSocketServer, message: str):
server.send(message)
# メイン処理を非同期関数に変更
async def main():
# サーバーを起動してメッセージハンドラを設定
ws_server = WebSocketServer()
ws_server.set_message_handler(message_handler)
ws_server.start()
print("WebSocket server started.")
# 定期的にサーバーからメッセージを送信する例
import threading
import time
def periodic_send():
print("Starting periodic message sender...")
while ws_server.is_running:
time.sleep(5)
print("Sending periodic message...")
send_message(ws_server, "Periodic message")
print("Periodic message sender stopped.")
# 別スレッドで定期的にメッセージを送信
time.sleep(5)
send_thread = threading.Thread(target=periodic_send, daemon=True)
send_thread.start()
# メインスレッドでサーバーを動かし続ける
try:
while True:
# 非同期スリープで待機
await asyncio.sleep(1)
except KeyboardInterrupt:
# Ctrl+Cでサーバーを停止
print("Stopping WebSocket server...")
ws_server.stop()
# 非同期メイン関数を実行
try:
asyncio.run(main())
except KeyboardInterrupt:
print("Stopping WebSocket server...")

View File

@@ -8,6 +8,7 @@ from logging.handlers import RotatingFileHandler
from ctranslate2 import get_supported_compute_types
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)

View File

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

View File

@@ -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>
);

View File

@@ -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"
/>
);
};

View File

@@ -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}
/>
);

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react";
import React, { useState, useEffect, useRef } from "react";
import { useTranslation } from "react-i18next";
import 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,
};
};

View File

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

View File

@@ -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}
/>
);

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 {

View File

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

View File

@@ -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";

View File

@@ -0,0 +1,44 @@
import { useTranslation } from "react-i18next";
import { useNotificationStatus } from "@logics_common";
import {
useEnableVrcMicMuteSync,
} from "@logics_configs";
export const useHandleOscQuery = () => {
const { t } = useTranslation();
const { showNotification_Warning } = useNotificationStatus();
const { updateEnableVrcMicMuteSync } = useEnableVrcMicMuteSync();
const handleOscQuery = ({is_osc_query_enabled, disabled_functions}) => {
if (!is_osc_query_enabled && disabled_functions.length > 0) {
const BASE_LABEL = t("common_warning.unable_to_use_osc_query");
let items_label = "";
for (const disabled_function of disabled_functions) {
if (disabled_function === "vrc_mic_mute_sync") {
updateEnableVrcMicMuteSync({
is_enabled: false,
is_available: false,
});
const item = `- ${t("config_page.others.vrc_mic_mute_sync.label")}`;
items_label = `${items_label}\n${item}`;
}
}
const label = `${BASE_LABEL}${items_label}`;
showNotification_Warning(
label,
{ hide_duration: 10000, }
);
} else if (is_osc_query_enabled) {
updateEnableVrcMicMuteSync((old_value) => ({
...old_value.data,
is_available: true,
}));
}
};
return {
handleOscQuery,
};
};

View File

@@ -5,6 +5,16 @@ export const useNotificationStatus = () => {
const generateRandomKey = () => Math.random();
const 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,

View File

@@ -0,0 +1,67 @@
import {
useStore_EnableWebsocket,
useStore_WebsocketHost,
useStore_WebsocketPort,
} from "@store";
import { useStdoutToPython } from "@logics/useStdoutToPython";
export const useWebsocket = () => {
const { asyncStdoutToPython } = useStdoutToPython();
const { currentEnableWebsocket, updateEnableWebsocket, pendingEnableWebsocket } = useStore_EnableWebsocket();
const { currentWebsocketHost, updateWebsocketHost, pendingWebsocketHost } = useStore_WebsocketHost();
const { currentWebsocketPort, updateWebsocketPort, pendingWebsocketPort } = useStore_WebsocketPort();
const getEnableWebsocket = () => {
pendingEnableWebsocket();
asyncStdoutToPython("/get/data/websocket_server");
};
const toggleEnableWebsocket = () => {
pendingEnableWebsocket();
if (currentEnableWebsocket.data) {
asyncStdoutToPython("/set/disable/websocket_server");
} else {
asyncStdoutToPython("/set/enable/websocket_server");
}
};
const getWebsocketHost = () => {
pendingWebsocketHost();
asyncStdoutToPython("/get/data/websocket_host");
};
const setWebsocketHost = (websocket_host) => {
pendingWebsocketHost();
asyncStdoutToPython("/set/data/websocket_host", websocket_host);
};
const getWebsocketPort = () => {
pendingWebsocketPort();
asyncStdoutToPython("/get/data/websocket_port");
};
const setWebsocketPort = (websocket_port) => {
pendingWebsocketPort();
asyncStdoutToPython("/set/data/websocket_port", websocket_port);
};
return {
currentEnableWebsocket,
updateEnableWebsocket,
getEnableWebsocket,
toggleEnableWebsocket,
currentWebsocketHost,
updateWebsocketHost,
getWebsocketHost,
setWebsocketHost,
currentWebsocketPort,
updateWebsocketPort,
getWebsocketPort,
setWebsocketPort,
};
};

View File

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

View File

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

View File

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

View File

@@ -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(

View File

@@ -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

View File

@@ -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
View File

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