diff --git a/requirements.txt b/requirements.txt index b384b8a0..d4115f63 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,6 +15,7 @@ pydub==0.25.1 psutil==5.9.8 pykakasi==2.3.0 pycaw==20240210 +websockets==15.0.1 translators @ git+https://github.com/misyaguziya/translators@5.9.2.1 SpeechRecognition @ git+https://github.com/misyaguziya/custom_speech_recognition@3.10.4.1 tinyoscquery @ git+https://github.com/cyberkitsune/tinyoscquery@0.1.3 \ No newline at end of file diff --git a/requirements_cuda.txt b/requirements_cuda.txt index 8642db8b..0f259323 100644 --- a/requirements_cuda.txt +++ b/requirements_cuda.txt @@ -16,6 +16,7 @@ pydub==0.25.1 psutil==5.9.8 pykakasi==2.3.0 pycaw==20240210 +websockets==15.0.1 translators @ git+https://github.com/misyaguziya/translators@5.9.2.1 SpeechRecognition @ git+https://github.com/misyaguziya/custom_speech_recognition@3.10.4.1 tinyoscquery @ git+https://github.com/cyberkitsune/tinyoscquery@0.1.3 \ No newline at end of file diff --git a/src-python/config.py b/src-python/config.py index 175ffa0f..7910400b 100644 --- a/src-python/config.py +++ b/src-python/config.py @@ -954,6 +954,37 @@ 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 + 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 + 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.1" @@ -1139,6 +1170,9 @@ class Config: self._LOGGER_FEATURE = False self._VRC_MIC_MUTE_SYNC = False self._NOTIFICATION_VRC_SFX = True + self._WEBSOCKET_SERVER = True + self._WEBSOCKET_HOST = "0.0.0.0" + self._WEBSOCKET_PORT = 8765 def load_config(self): if os_path.isfile(self.PATH_CONFIG) is not False: diff --git a/src-python/controller.py b/src-python/controller.py index cdc1b037..398e3e37 100644 --- a/src-python/controller.py +++ b/src-python/controller.py @@ -294,6 +294,17 @@ class Controller: "translation":translation, "transliteration":transliteration }) + + if config.WEBSOCKET_SERVER is True: + model.websocketSendMessage( + { + "type":"SEND", + "message":message, + "translation":translation, + "transliteration":transliteration + } + ) + if config.LOGGER_FEATURE is True: if len(translation) > 0: translation = " (" + "/".join(translation) + ")" @@ -377,6 +388,17 @@ class Controller: "translation":translation, "transliteration":transliteration, }) + + if config.WEBSOCKET_SERVER is True: + model.websocketSendMessage( + { + "type":"RECEIVED", + "message":message, + "translation":translation, + "transliteration":transliteration + } + ) + if config.LOGGER_FEATURE is True: if len(translation) > 0: translation = " (" + "/".join(translation) + ")" @@ -434,6 +456,16 @@ class Controller: overlay_image = model.createOverlayImageLargeLog("send", message, translation[0] if len(translation) > 0 else "") model.updateOverlayLargeLog(overlay_image) + if config.WEBSOCKET_SERVER is True: + model.websocketSendMessage( + { + "type":"CHAT", + "message":message, + "translation":translation, + "transliteration":transliteration + } + ) + # update textbox message log (Sent) if config.LOGGER_FEATURE is True: if len(translation) > 0: @@ -1778,6 +1810,46 @@ 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: + config.WEBSOCKET_HOST = data + if model.checkWebSocketServer() is True: + model.stopWebSocketServer() + model.startWebSocketServer() + return {"status":200, "result":config.WEBSOCKET_HOST} + + @staticmethod + def getWebSocketPort(*args, **kwargs) -> dict: + return {"status":200, "result":config.WEBSOCKET_PORT} + + @staticmethod + def setWebSocketPort(data, *args, **kwargs) -> dict: + config.WEBSOCKET_PORT = int(data) + if model.checkWebSocketServer() is True: + model.stopWebSocketServer() + model.startWebSocketServer() + return {"status":200, "result":config.WEBSOCKET_PORT} + + @staticmethod + def getWebSocketServer(*args, **kwargs) -> dict: + return {"status":200, "result":config.WEBSOCKET_SERVER} + + @staticmethod + def setEnableWebSocketServer(*args, **kwargs) -> dict: + config.WEBSOCKET_SERVER = True + model.startWebSocketServer() + return {"status":200, "result":config.WEBSOCKET_SERVER} + + @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) @@ -1825,41 +1897,7 @@ class Controller: printLog("Init Translation Engine Status") for engine in config.SELECTABLE_TRANSLATION_ENGINE_LIST: - match engine: - case "CTranslate2": - if model.checkTranslatorCTranslate2ModelWeight(config.CTRANSLATE2_WEIGHT_TYPE) is True: - config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = True - else: - config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = False - case "DeepL_API": - printLog("Start check DeepL API Key") - config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = False - if config.AUTH_KEYS[engine] is not None: - if model.authenticationTranslatorDeepLAuthKey(auth_key=config.AUTH_KEYS[engine]) is True: - config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = True - else: - # error update Auth key - auth_keys = config.AUTH_KEYS - auth_keys[engine] = None - config.AUTH_KEYS = auth_keys - case _: - if connected_network is True: - config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = True - else: - config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = False - - for engine in config.SELECTABLE_TRANSCRIPTION_ENGINE_LIST: - match engine: - case "Whisper": - if model.checkTranscriptionWhisperModelWeight(config.WHISPER_WEIGHT_TYPE) is True: - config.SELECTABLE_TRANSCRIPTION_ENGINE_STATUS[engine] = True - else: - config.SELECTABLE_TRANSCRIPTION_ENGINE_STATUS[engine] = False - case _: - if connected_network is True: - config.SELECTABLE_TRANSCRIPTION_ENGINE_STATUS[engine] = True - else: - config.SELECTABLE_TRANSCRIPTION_ENGINE_STATUS[engine] = False + config.SELECTABLE_TRANSCRIPTION_ENGINE_STATUS[engine] = False self.initializationProgress(2) # set Translation Engine @@ -1911,6 +1949,10 @@ 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: + model.startWebSocketServer() + printLog("Update settings") self.updateConfigSettings() diff --git a/src-python/mainloop.py b/src-python/mainloop.py index 5c5fb744..d8ad52e7 100644 --- a/src-python/mainloop.py +++ b/src-python/mainloop.py @@ -291,6 +291,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}, diff --git a/src-python/model.py b/src-python/model.py index 8f62aa5e..ee8ee250 100644 --- a/src-python/model.py +++ b/src-python/model.py @@ -29,6 +29,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): @@ -100,6 +101,50 @@ class Model: self.watchdog = Watchdog(config.WATCHDOG_TIMEOUT, config.WATCHDOG_INTERVAL) self.osc_handler = OSCHandler(config.OSC_IP_ADDRESS, config.OSC_PORT) + # WebSocketサーバーの初期化 + self.websocket_server = WebSocketServer( + host=config.WEBSOCKET_HOST, + port=config.WEBSOCKET_PORT + ) + self.th_websocket_server = None + + def startWebSocketServer(self): + if self.th_websocket_server is None: + from threading import Thread + def run_server(): + try: + self.websocket_server.start() + except Exception: + errorLogging() + self.th_websocket_server = Thread(target=run_server, daemon=True) + self.th_websocket_server.start() + + def stopWebSocketServer(self): + if self.websocket_server: + try: + self.websocket_server.stop() + except Exception: + errorLogging() + self.th_websocket_server = None + + def checkWebSocketServer(self): + if self.websocket_server: + try: + return self.websocket_server.is_running + except Exception: + errorLogging() + return False + + def websocketSendMessage(self, message_dict): + """ + WebSocketサーバーから全クライアントにメッセージを送信する + :param message_dict: 送信する辞書型データ + """ + try: + self.websocket_server.send_message(message_dict) + except Exception: + errorLogging() + def checkTranslatorCTranslate2ModelWeight(self, weight_type:str): return checkCTranslate2Weight(config.PATH_LOCAL, weight_type) diff --git a/src-python/models/websocket/model_part.py b/src-python/models/websocket/model_part.py deleted file mode 100644 index 47758bf0..00000000 --- a/src-python/models/websocket/model_part.py +++ /dev/null @@ -1,103 +0,0 @@ -import copy -import gc -from subprocess import Popen -from os import makedirs as os_makedirs -from os import path as os_path -from datetime import datetime -from time import sleep -from queue import Queue -from threading import Thread -from requests import get as requests_get -from typing import Callable -from packaging.version import parse - -from flashtext import KeywordProcessor -from pykakasi import kakasi - -from device_manager import device_manager -from config import config - -from models.translation.translation_translator import Translator -from models.osc.osc import OSCHandler -from models.transcription.transcription_recorder import SelectedMicEnergyAndAudioRecorder, SelectedSpeakerEnergyAndAudioRecorder -from models.transcription.transcription_recorder import SelectedMicEnergyRecorder, SelectedSpeakerEnergyRecorder -from models.transcription.transcription_transcriber import AudioTranscriber -from models.translation.translation_languages import translation_lang -from models.transcription.transcription_languages import transcription_lang -from models.translation.translation_utils import checkCTranslate2Weight, downloadCTranslate2Weight, downloadCTranslate2Tokenizer -from models.transcription.transcription_whisper import checkWhisperWeight, downloadWhisperWeight -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): - def __init__(self, fnc, end_fnc=None, daemon=True, *args, **kwargs): - super(threadFnc, self).__init__(daemon=daemon, target=fnc, *args, **kwargs) - self.fnc = fnc - self.end_fnc = end_fnc - self.loop = True - self._pause = False - - def stop(self): - self.loop = False - - def pause(self): - self._pause = True - - def resume(self): - self._pause = False - - def run(self): - while self.loop: - self.fnc(*self._args, **self._kwargs) - while self._pause: - sleep(0.1) - - if callable(self.end_fnc): - self.end_fnc() - return - -class Model: - _instance = None - - def __new__(cls): - if cls._instance is None: - cls._instance = super(Model, cls).__new__(cls) - cls._instance.init() - return cls._instance - - def init(self): - self.logger = None - self.th_check_device = None - self.mic_print_transcript = None - self.mic_audio_recorder = None - self.mic_transcriber = None - self.mic_energy_recorder = None - self.mic_energy_plot_progressbar = None - self.speaker_print_transcript = None - self.speaker_audio_recorder = None - self.speaker_transcriber = None - self.speaker_energy_recorder = None - self.speaker_energy_plot_progressbar = None - - self.previous_send_message = "" - self.previous_receive_message = "" - self.translator = Translator() - self.keyword_processor = KeywordProcessor() - overlay_small_log_settings = copy.deepcopy(config.OVERLAY_SMALL_LOG_SETTINGS) - overlay_large_log_settings = copy.deepcopy(config.OVERLAY_LARGE_LOG_SETTINGS) - overlay_large_log_settings["ui_scaling"] = overlay_large_log_settings["ui_scaling"] * 0.25 - overlay_settings = { - "small": overlay_small_log_settings, - "large": overlay_large_log_settings, - } - self.overlay = Overlay(overlay_settings) - self.overlay_image = OverlayImage() - self.mic_audio_queue = None - self.mic_mute_status = None - 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 = WebSocketServer(host="0.0.0.0", port=8765) diff --git a/src-python/models/websocket/websocket_server.py b/src-python/models/websocket/websocket_server.py index 73ee3787..330f6b79 100644 --- a/src-python/models/websocket/websocket_server.py +++ b/src-python/models/websocket/websocket_server.py @@ -1,30 +1,29 @@ import asyncio import json import logging -from typing import Dict, Set, Optional +from typing import Dict, Set import websockets -from websockets.server import WebSocketServerProtocol class WebSocketServer: def __init__(self, host: str = "0.0.0.0", port: int = 8765): self.host = host self.port = port - self.clients: Set[WebSocketServerProtocol] = set() + self.clients = set() self.server = None self.is_running = False self.logger = logging.getLogger('websocket_server') - async def register(self, websocket: WebSocketServerProtocol): + async def register(self, websocket): """クライアント接続を登録する""" self.clients.add(websocket) self.logger.info(f"クライアント接続: {websocket.remote_address}, 現在の接続数: {len(self.clients)}") - async def unregister(self, websocket: WebSocketServerProtocol): + async def unregister(self, websocket): """クライアント接続を解除する""" self.clients.remove(websocket) self.logger.info(f"クライアント切断: {websocket.remote_address}, 現在の接続数: {len(self.clients)}") - async def handler(self, websocket: WebSocketServerProtocol, path: str): + async def handler(self, websocket): """WebSocket接続ハンドラー""" await self.register(websocket) try: @@ -46,17 +45,17 @@ class WebSocketServer: tasks = [client.send(json_message) for client in self.clients] await asyncio.gather(*tasks, return_exceptions=True) - def send_transcription(self, transcription_data: Dict): - """文字起こし結果を送信する関数(通常のコードから呼び出し可能)""" + def send_message(self, message: Dict): + """メッセージを送信する関数(通常のコードから呼び出し可能)""" if not self.is_running or not self.clients: return # asyncioのイベントループがあれば利用し、なければ新たに作成 loop = asyncio.get_event_loop() if asyncio.get_event_loop().is_running() else asyncio.new_event_loop() - + # ブロードキャスト関数を実行 asyncio.run_coroutine_threadsafe( - self.broadcast(transcription_data), + self.broadcast(message), loop ) @@ -100,3 +99,15 @@ class WebSocketServer: loop = asyncio.get_event_loop() asyncio.run_coroutine_threadsafe(self.stop_server(), loop) + + def is_running(self) -> bool: + """サーバーが実行中かどうかを確認する""" + return self.is_running + + def get_clients(self) -> Set: + """現在のクライアント接続を取得する""" + return self.clients + + def get_client_count(self) -> int: + """現在のクライアント接続数を取得する""" + return len(self.clients) \ No newline at end of file