diff --git a/config.py b/config.py index 393d723c..dd13ca4c 100644 --- a/config.py +++ b/config.py @@ -892,6 +892,17 @@ class Config: self._ENABLE_LOGGER = value saveJson(self.PATH_CONFIG, inspect.currentframe().f_code.co_name, value) + @property + @json_serializable('ENABLE_VRC_MIC_MUTE_SYNC') + def ENABLE_VRC_MIC_MUTE_SYNC(self): + return self._ENABLE_VRC_MIC_MUTE_SYNC + + @ENABLE_VRC_MIC_MUTE_SYNC.setter + def ENABLE_VRC_MIC_MUTE_SYNC(self, value): + if isinstance(value, bool): + self._ENABLE_VRC_MIC_MUTE_SYNC = value + saveJson(self.PATH_CONFIG, inspect.currentframe().f_code.co_name, value) + @property @json_serializable('IS_CONFIG_WINDOW_COMPACT_MODE') def IS_CONFIG_WINDOW_COMPACT_MODE(self): @@ -1071,6 +1082,7 @@ class Config: self._ENABLE_SEND_RECEIVED_MESSAGE_TO_VRC = False # Speaker2Chatbox self._ENABLE_SPEAKER2CHATBOX_PASS = "000000000" self._ENABLE_LOGGER = False + self._ENABLE_VRC_MIC_MUTE_SYNC = False self._IS_CONFIG_WINDOW_COMPACT_MODE = False def load_config(self): diff --git a/controller.py b/controller.py index dbc88560..cde88458 100644 --- a/controller.py +++ b/controller.py @@ -924,6 +924,15 @@ def callbackSetEnableAutoExportMessageLogs(value): else: model.stopLogger() +def callbackSetEnableVrcMicMuteSync(value): + print("callbackSetEnableVrcMicMuteSync", value) + config.ENABLE_VRC_MIC_MUTE_SYNC = value + if config.ENABLE_VRC_MIC_MUTE_SYNC is True: + model.startCheckMuteSelfStatus() + else: + model.stopCheckMuteSelfStatus() + + def callbackSetEnableSendMessageToVrc(value): print("callbackSetEnableSendMessageToVrc", value) config.ENABLE_SEND_MESSAGE_TO_VRC = value @@ -978,7 +987,6 @@ def callbackSetEnableSendReceivedMessageToVrc(value): config.ENABLE_SEND_RECEIVED_MESSAGE_TO_VRC = value # ---------------------Speaker2Chatbox--------------------- - # Advanced Settings Tab def callbackSetOscIpAddress(value): if value == "": @@ -1044,6 +1052,11 @@ def createMainWindow(splash): if config.ENABLE_LOGGER is True: model.startLogger() + # init OSC receive + model.startReceiveOSC() + if config.ENABLE_VRC_MIC_MUTE_SYNC is True: + model.startCheckMuteSelfStatus() + splash.toProgress(3) # Last one. # set UI and callback @@ -1148,6 +1161,7 @@ def createMainWindow(splash): "callback_set_send_message_button_type": callbackSetSendMessageButtonType, "callback_set_enable_notice_xsoverlay": callbackSetEnableNoticeXsoverlay, "callback_set_enable_auto_export_message_logs": callbackSetEnableAutoExportMessageLogs, + "callback_set_enable_vrc_mic_mute_sync": callbackSetEnableVrcMicMuteSync, "callback_set_enable_send_message_to_vrc": callbackSetEnableSendMessageToVrc, # Others(Message Formats(Send) "callback_set_send_message_format": callbackSetSendMessageFormat, diff --git a/locales/en.yml b/locales/en.yml index 2951af23..82571147 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -243,6 +243,10 @@ config_window: label: Auto Export Message Logs desc: Automatically export the conversation messages as a text file. + vrc_mic_mute_sync: + label: VRChat Mic Mute Sync + desc: VRCT will not send the message to VRChat while VRChat's mic is muted. + send_message_to_vrc: label: Send Message To VRChat desc: There is a way to use it without sending messages to VRChat, but it is not supported. Enable this feature when you intend to send a message to VRChat. diff --git a/locales/ja.yml b/locales/ja.yml index bd550712..893470d6 100644 --- a/locales/ja.yml +++ b/locales/ja.yml @@ -242,6 +242,11 @@ config_window: label: 会話ログを自動的に保存する desc: テキストファイルとしてログがlogsフォルダ内に保存されます。 + vrc_mic_mute_sync: + label: VRChatマイクミュート同期機能 + desc: VRChatのマイクがミュートされている間は、メッセージをVRChatに送信しません。 + + send_message_to_vrc: label: VRChatにメッセージを送信する desc: "サポート対象外ですが、VRChatにメッセージを送信せずに使う方法があります。送信したい場合、この機能を有効にする事を忘れないでください。" diff --git a/model.py b/model.py index bc0b2706..ffee5e26 100644 --- a/model.py +++ b/model.py @@ -17,7 +17,7 @@ from typing import Callable from flashtext import KeywordProcessor from models.translation.translation_translator import Translator from models.transcription.transcription_utils import getInputDevices, getOutputDevices -from models.osc.osc_tools import sendTyping, sendMessage, sendTestAction, receiveOscParameters +from models.osc.osc_tools import sendTyping, sendMessage, receiveOscParameters, getOSCParameterValue from models.transcription.transcription_recorder import SelectedMicEnergyAndAudioRecorder, SelectedSpeakerEnergyAndAudioRecorder from models.transcription.transcription_recorder import SelectedMicEnergyRecorder, SelectedSpeakerEnergyRecorder from models.transcription.transcription_transcriber import AudioTranscriber @@ -37,16 +37,27 @@ class threadFnc(Thread): 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 @@ -82,6 +93,9 @@ class Model: # self.overlay_image = OverlayImage() # self.pre_overlay_message = None # self.th_overlay = None + self.mic_audio_queue = None + self.mic_mute_status = None + self.mic_mute_status_check = None def checkCTranslatorCTranslate2ModelWeight(self): return checkCTranslate2Weight(config.PATH_LOCAL, config.CTRANSLATE2_WEIGHT_TYPE) @@ -238,39 +252,51 @@ class Model: def oscSendMessage(message): sendMessage(message, config.OSC_IP_ADDRESS, config.OSC_PORT) - def checkOSCStarted(self, fnc): - self.is_valid_osc = False - def checkOscReceive(address, osc_arguments): - if self.is_valid_osc is False: - self.is_valid_osc = True + @staticmethod + def getMuteSelfStatus(): + return getOSCParameterValue(address="/avatar/parameters/MuteSelf") - self.listening_server = receiveOscParameters(checkOscReceive) - def oscListener(): - self.listening_server.serve_forever() + def startCheckMuteSelfStatus(self): + def checkMuteSelfStatus(): + if self.mic_mute_status is not None: + self.changeMicTranscriptStatus() + self.stopCheckMuteSelfStatus() - def sendTestActionLoop(): - for _ in range(10): - sendTestAction() - if self.is_valid_osc is True: - break - sleep(0.1) - self.listening_server.shutdown() + status = self.getMuteSelfStatus() + if status is not None: + self.mic_mute_status = status + self.changeMicTranscriptStatus() + self.stopCheckMuteSelfStatus() - # start receive osc - th_receive_osc_parameters = Thread(target=oscListener) - th_receive_osc_parameters.daemon = True - th_receive_osc_parameters.start() + if not isinstance(self.mic_mute_status_check, threadFnc): + self.mic_mute_status_check = threadFnc(checkMuteSelfStatus) + self.mic_mute_status_check.daemon = True + self.mic_mute_status_check.start() - # check osc started - th_send_osc_test_action = Thread(target=sendTestActionLoop) - th_send_osc_test_action.daemon = True - th_send_osc_test_action.start() + def stopCheckMuteSelfStatus(self): + if isinstance(self.mic_mute_status_check, threadFnc): + self.mic_mute_status_check.stop() + self.mic_mute_status_check = None - th_receive_osc_parameters.join() - th_send_osc_test_action.join() + def startReceiveOSC(self): + osc_parameter_prefix = "/avatar/parameters/" + param_MuteSelf = "MuteSelf" - if self.is_valid_osc is False: - fnc() + def change_handler_mute(address, osc_arguments): + if osc_arguments is True and self.mic_mute_status is False: + self.mic_mute_status = osc_arguments + self.changeMicTranscriptStatus() + elif osc_arguments is False and self.mic_mute_status is True: + self.mic_mute_status = osc_arguments + self.changeMicTranscriptStatus() + + dict_filter_and_target = { + osc_parameter_prefix + param_MuteSelf: change_handler_mute, + } + + th_osc_server = Thread(target=receiveOscParameters, args=(dict_filter_and_target,)) + th_osc_server.daemon = True + th_osc_server.start() @staticmethod def checkSoftwareUpdated(): @@ -363,8 +389,9 @@ class Model: pass return - mic_audio_queue = Queue() - # mic_energy_queue = Queue() + self.mic_audio_queue = Queue() + # self.mic_energy_queue = Queue() + mic_device = choice_mic_device[0] record_timeout = config.INPUT_MIC_RECORD_TIMEOUT phase_timeout = config.INPUT_MIC_PHRASE_TIMEOUT @@ -377,8 +404,8 @@ class Model: dynamic_energy_threshold=config.INPUT_MIC_DYNAMIC_ENERGY_THRESHOLD, record_timeout=record_timeout, ) - # self.mic_audio_recorder.recordIntoQueue(mic_audio_queue, mic_energy_queue) - self.mic_audio_recorder.recordIntoQueue(mic_audio_queue, None) + # self.mic_audio_recorder.recordIntoQueue(self.mic_audio_queue, mic_energy_queue) + self.mic_audio_recorder.recordIntoQueue(self.mic_audio_queue, None) self.mic_transcriber = AudioTranscriber( speaker=False, source=self.mic_audio_recorder.source, @@ -390,7 +417,7 @@ class Model: ) def sendMicTranscript(): try: - res = self.mic_transcriber.transcribeAudioQueue(mic_audio_queue, config.SOURCE_LANGUAGE, config.SOURCE_COUNTRY) + res = self.mic_transcriber.transcribeAudioQueue(self.mic_audio_queue, config.SOURCE_LANGUAGE, config.SOURCE_COUNTRY) if res: message = self.mic_transcriber.getTranscript() fnc(message) @@ -398,8 +425,10 @@ class Model: pass def endMicTranscript(): - mic_audio_queue.queue.clear() - # mic_energy_queue.queue.clear() + while not self.mic_audio_queue.empty(): + self.mic_audio_queue.get() + # while not self.mic_energy_queue.empty(): + # self.mic_energy_queue.get() del self.mic_transcriber gc.collect() @@ -421,6 +450,42 @@ class Model: # self.mic_get_energy.daemon = True # self.mic_get_energy.start() + self.changeMicTranscriptStatus() + + def resumeMicTranscript(self): + # キューをクリア + if isinstance(self.mic_audio_queue, Queue): + while not self.mic_audio_queue.empty(): + self.mic_audio_queue.get() + + # 文字起こしを再開 + # if isinstance(self.mic_print_transcript, threadFnc): + # self.mic_print_transcript.resume() + + # 音声のレコードを再開 + if isinstance(self.mic_audio_recorder, SelectedMicEnergyAndAudioRecorder): + self.mic_audio_recorder.resume() + + def pauseMicTranscript(self): + # 文字起こしを一時停止 + # if isinstance(self.mic_print_transcript, threadFnc): + # self.mic_print_transcript.pause() + + # 音声のレコードを一時停止 + if isinstance(self.mic_audio_recorder, SelectedMicEnergyAndAudioRecorder): + self.mic_audio_recorder.pause() + + def changeMicTranscriptStatus(self): + if config.ENABLE_VRC_MIC_MUTE_SYNC is True: + if self.mic_mute_status is True: + self.pauseMicTranscript() + elif self.mic_mute_status is False: + self.resumeMicTranscript() + else: + pass + else: + self.resumeMicTranscript() + def stopMicTranscript(self): if isinstance(self.mic_print_transcript, threadFnc): self.mic_print_transcript.stop() @@ -493,7 +558,7 @@ class Model: record_timeout=record_timeout, ) # self.speaker_audio_recorder.recordIntoQueue(speaker_audio_queue, speaker_energy_queue) - self.speaker_audio_recorder.recordIntoQueue(speaker_audio_queue ,None) + self.speaker_audio_recorder.recordIntoQueue(speaker_audio_queue, None) self.speaker_transcriber = AudioTranscriber( speaker=True, source=self.speaker_audio_recorder.source, diff --git a/models/osc/osc_tools.py b/models/osc/osc_tools.py index 51608f66..0f0958c3 100644 --- a/models/osc/osc_tools.py +++ b/models/osc/osc_tools.py @@ -1,10 +1,10 @@ from time import sleep -import threading from pythonosc import osc_message_builder from pythonosc import udp_client from pythonosc import dispatcher from pythonosc import osc_server from tinyoscquery.queryservice import OSCQueryService +from tinyoscquery.query import OSCQueryBrowser, OSCQueryClient from tinyoscquery.utility import get_open_udp_port, get_open_tcp_port # send OSC message typing @@ -48,6 +48,19 @@ def sendChangeVoice(ip_address="127.0.0.1", port=9000): sendInputVoice(flag=0, ip_address=ip_address, port=port) sleep(0.05) +def getOSCParameterValue(address, server_name="VRChat-Client"): + value = None + try: + browser = OSCQueryBrowser() + sleep(1) + service = browser.find_service_by_name(server_name) + if service is not None: + oscq = OSCQueryClient(service) + mute_self_node = oscq.query_node(address) + value = mute_self_node.value[0] + except Exception: + pass + return value def receiveOscParameters(dict_filter_and_target, ip_address="127.0.0.1", title="VRCT"): osc_port = get_open_udp_port() @@ -55,14 +68,14 @@ def receiveOscParameters(dict_filter_and_target, ip_address="127.0.0.1", title=" osc_dispatcher = dispatcher.Dispatcher() for filter, target in dict_filter_and_target.items(): osc_dispatcher.map(filter, target) - osc_udp_server = osc_server.ThreadingOSCUDPServer((ip_address, osc_port), osc_dispatcher) - threading.Thread(target=osc_udp_server.serve_forever, daemon = True).start() osc_client = OSCQueryService(title, http_port, osc_port) for filter, target in dict_filter_and_target.items(): osc_client.advertise_endpoint(filter) + osc_udp_server.serve_forever() + if __name__ == "__main__": osc_parameter_prefix = "/avatar/parameters/" osc_avatar_change_path = "/avatar/change" diff --git a/models/transcription/transcription_recorder.py b/models/transcription/transcription_recorder.py index 0128a37b..0e9b147d 100644 --- a/models/transcription/transcription_recorder.py +++ b/models/transcription/transcription_recorder.py @@ -24,7 +24,7 @@ class BaseRecorder: def record_callback(_, audio): audio_queue.put((audio.get_raw_data(), datetime.now())) - self.stop = self.recorder.listen_in_background(self.source, record_callback, phrase_time_limit=self.record_timeout) + self.stop, self.pause, self.resume = self.recorder.listen_in_background(self.source, record_callback, phrase_time_limit=self.record_timeout) class SelectedMicRecorder(BaseRecorder): def __init__(self, device, energy_threshold, dynamic_energy_threshold, record_timeout): @@ -68,7 +68,7 @@ class BaseEnergyRecorder: def recordCallback(_, energy): energy_queue.put(energy) - self.stop = self.recorder.listen_energy_in_background(self.source, recordCallback) + self.stop, self.pause, self.resume = self.recorder.listen_energy_in_background(self.source, recordCallback) class SelectedMicEnergyRecorder(BaseEnergyRecorder): def __init__(self, device): @@ -107,17 +107,18 @@ class BaseEnergyAndAudioRecorder: with self.source: self.recorder.adjust_for_ambient_noise(self.source) - def recordIntoQueue(self, audio_queue, energy_queue): + def recordIntoQueue(self, audio_queue, energy_queue=None): def audioRecordCallback(_, audio): audio_queue.put((audio.get_raw_data(), datetime.now())) def energyRecordCallback(energy): energy_queue.put(energy) - if isinstance(energy_queue, Queue): - self.stop = self.recorder.listen_energy_and_audio_in_background(self.source, audioRecordCallback, phrase_time_limit=self.record_timeout, callback_energy=energyRecordCallback) - else: - self.stop = self.recorder.listen_energy_and_audio_in_background(self.source, audioRecordCallback, phrase_time_limit=self.record_timeout) + self.stop, self.pause, self.resume = self.recorder.listen_energy_and_audio_in_background( + source=self.source, + callback=audioRecordCallback, + phrase_time_limit=self.record_timeout, + callback_energy=energyRecordCallback if energy_queue is not None else None) class SelectedMicEnergyAndAudioRecorder(BaseEnergyAndAudioRecorder): def __init__(self, device, energy_threshold, dynamic_energy_threshold, record_timeout): diff --git a/view.py b/view.py index e222da60..8674e9a6 100644 --- a/view.py +++ b/view.py @@ -519,6 +519,12 @@ class View(): VAR_ENABLE_AUTO_EXPORT_MESSAGE_LOGS=BooleanVar(value=config.ENABLE_LOGGER), + VAR_LABEL_ENABLE_VRC_MIC_MUTE_SYNC=StringVar(value=i18n.t("config_window.vrc_mic_mute_sync.label")), + VAR_DESC_ENABLE_VRC_MIC_MUTE_SYNC=StringVar(value=i18n.t("config_window.vrc_mic_mute_sync.desc")), + CALLBACK_SET_ENABLE_VRC_MIC_MUTE_SYNC=None, + VAR_ENABLE_VRC_MIC_MUTE_SYNC=BooleanVar(value=config.ENABLE_VRC_MIC_MUTE_SYNC), + + VAR_LABEL_ENABLE_SEND_MESSAGE_TO_VRC=StringVar(value=i18n.t("config_window.send_message_to_vrc.label")), VAR_DESC_ENABLE_SEND_MESSAGE_TO_VRC=StringVar(value=i18n.t("config_window.send_message_to_vrc.desc")), CALLBACK_SET_ENABLE_SEND_MESSAGE_TO_VRC=None, @@ -770,6 +776,7 @@ class View(): self.view_variable.CALLBACK_SET_ENABLE_NOTICE_XSOVERLAY=config_window_registers.get("callback_set_enable_notice_xsoverlay", None) self.view_variable.CALLBACK_SET_ENABLE_AUTO_EXPORT_MESSAGE_LOGS=config_window_registers.get("callback_set_enable_auto_export_message_logs", None) + self.view_variable.CALLBACK_SET_ENABLE_VRC_MIC_MUTE_SYNC=config_window_registers.get("callback_set_enable_vrc_mic_mute_sync", None) self.view_variable.CALLBACK_SET_ENABLE_SEND_MESSAGE_TO_VRC=config_window_registers.get("callback_set_enable_send_message_to_vrc", None) self.view_variable.CALLBACK_SET_SEND_MESSAGE_FORMAT=config_window_registers.get("callback_set_send_message_format", None) diff --git a/vrct_gui/config_window/widgets/createSideMenuAndSettingsBoxContainers/setting_box_containers/setting_box_others/createSettingBox_Others.py b/vrct_gui/config_window/widgets/createSideMenuAndSettingsBoxContainers/setting_box_containers/setting_box_others/createSettingBox_Others.py index 93a249c7..72391a8a 100644 --- a/vrct_gui/config_window/widgets/createSideMenuAndSettingsBoxContainers/setting_box_containers/setting_box_others/createSettingBox_Others.py +++ b/vrct_gui/config_window/widgets/createSideMenuAndSettingsBoxContainers/setting_box_containers/setting_box_others/createSettingBox_Others.py @@ -25,6 +25,9 @@ def createSettingBox_Others(setting_box_wrapper, config_window, settings, view_v def buttonAutoExportMessageLogsCallback(): callFunctionIfCallable(view_variable.CALLBACK_OPEN_FILEPATH_LOGS) + def checkboxVrcMuteSyncCallback(checkbox_box_widget): + callFunctionIfCallable(view_variable.CALLBACK_SET_ENABLE_VRC_MIC_MUTE_SYNC, checkbox_box_widget.get()) + def checkboxEnableSendMessageToVrcCallback(checkbox_box_widget): callFunctionIfCallable(view_variable.CALLBACK_SET_ENABLE_SEND_MESSAGE_TO_VRC, checkbox_box_widget.get()) @@ -74,6 +77,17 @@ def createSettingBox_Others(setting_box_wrapper, config_window, settings, view_v row+=1 + config_window.sb__vrc_mic_mute_sync = createSettingBoxCheckbox( + for_var_label_text=view_variable.VAR_LABEL_ENABLE_VRC_MIC_MUTE_SYNC, + for_var_desc_text=view_variable.VAR_DESC_ENABLE_VRC_MIC_MUTE_SYNC, + checkbox_attr_name="sb__checkbox_vrc_mic_mute_sync", + command=lambda: checkboxVrcMuteSyncCallback(config_window.sb__checkbox_vrc_mic_mute_sync), + variable=view_variable.VAR_ENABLE_VRC_MIC_MUTE_SYNC, + ) + config_window.sb__vrc_mic_mute_sync.grid(row=row) + row+=1 + + config_window.sb__enable_send_message_to_vrc = createSettingBoxCheckbox( for_var_label_text=view_variable.VAR_LABEL_ENABLE_SEND_MESSAGE_TO_VRC, for_var_desc_text=view_variable.VAR_DESC_ENABLE_SEND_MESSAGE_TO_VRC,