Merge branch 'develop' into translate_api

# Conflicts:
#	requirements.txt
#	requirements_cuda.txt
#	src-python/config.py
#	src-python/mainloop.py
#	src-python/model.py
#	src-python/models/osc/osc.py
#	src-python/models/translation/translation_languages.py
#	src-python/models/translation/translation_translator.py
#	src-python/models/translation/translation_utils.py
This commit is contained in:
misyaguziya
2025-10-14 12:47:47 +09:00
86 changed files with 20001 additions and 1584 deletions

View File

@@ -1,9 +1,9 @@
import sys
import json
import time
from typing import Any
from threading import Thread
from queue import Queue
from typing import Any, Tuple
from threading import Thread, Event, Lock
from queue import Queue, Empty
import logging
from controller import Controller # noqa: E402
from utils import printLog, printResponse, errorLogging, encodeBase64 # noqa: E402
@@ -48,6 +48,9 @@ run_mapping = {
"selected_translation_engines":"/run/selected_translation_engines",
"translation_engines":"/run/translation_engines",
"selected_translation_compute_type":"/run/selected_translation_compute_type",
"selected_transcription_compute_type":"/run/selected_transcription_compute_type",
"mic_host_list":"/run/mic_host_list",
"mic_device_list":"/run/mic_device_list",
"speaker_device_list":"/run/speaker_device_list",
@@ -162,6 +165,9 @@ mapping = {
"/get/data/ctranslate2_weight_type": {"status": True, "variable":controller.getCtranslate2WeightType},
"/set/data/ctranslate2_weight_type": {"status": True, "variable":controller.setCtranslate2WeightType},
"/get/data/selected_translation_compute_type": {"status": True, "variable":controller.getSelectedTranslationComputeType},
"/set/data/selected_translation_compute_type": {"status": True, "variable":controller.setSelectedTranslationComputeType},
"/run/download_ctranslate2_weight": {"status": True, "variable":controller.downloadCtranslate2Weight},
"/get/data/deepl_auth_key": {"status": False, "variable":controller.getDeepLAuthKey},
@@ -273,8 +279,13 @@ mapping = {
"/set/disable/check_speaker_threshold": {"status": True, "variable":controller.setDisableCheckSpeakerThreshold},
"/get/data/selectable_whisper_weight_type_dict": {"status": True, "variable":controller.getSelectableWhisperWeightTypeDict},
"/get/data/whisper_weight_type": {"status": True, "variable":controller.getWhisperWeightType},
"/set/data/whisper_weight_type": {"status": True, "variable":controller.setWhisperWeightType},
"/get/data/selected_transcription_compute_type": {"status": True, "variable":controller.getSelectedTranscriptionComputeType},
"/set/data/selected_transcription_compute_type": {"status": True, "variable":controller.setSelectedTranscriptionComputeType},
"/run/download_whisper_weight": {"status": True, "variable":controller.downloadWhisperWeight},
# VR
@@ -358,33 +369,76 @@ mapping = {
init_mapping = {key:value for key, value in mapping.items() if key.startswith("/get/data/")}
controller.setInitMapping(init_mapping)
DEFAULT_WORKER_COUNT = 3 # 必要なら増やす
class Main:
def __init__(self) -> None:
self.queue = Queue()
self.main_loop = True
def __init__(self, controller_instance: Controller, mapping_data: dict, worker_count: int = DEFAULT_WORKER_COUNT) -> None:
self.queue: Queue[Tuple[str, Any]] = Queue()
self._stop_event: Event = Event()
self.controller = controller_instance
self.mapping = mapping_data
self._threads: list[Thread] = []
self._worker_count = worker_count
# エンドポイントごとの排他制御用 Lock を作成
# enable/disable ペアは同じロックキーに正規化する
def _canonical_lock_key(endpoint: str) -> str:
if not isinstance(endpoint, str):
return str(endpoint)
if endpoint.startswith("/set/enable/"):
return "/lock/set/" + endpoint[len("/set/enable/"):]
if endpoint.startswith("/set/disable/"):
return "/lock/set/" + endpoint[len("/set/disable/"):]
return endpoint
# mapping に含まれるすべてのエンドポイントを走査して正規化キー集合を作る
lock_keys = set()
for key in self.mapping.keys():
lock_keys.add(_canonical_lock_key(key))
# 正規化キーごとに Lock を割り当てる
self._endpoint_locks: dict[str, Lock] = {k: Lock() for k in lock_keys}
# 正規化関数をインスタンスに保存
self._canonical_lock_key = _canonical_lock_key
def receiver(self) -> None:
while self.main_loop:
received_data = sys.stdin.readline().strip()
if not received_data:
time.sleep(0.1)
continue
received_data = json.loads(received_data)
"""Read lines from stdin, parse JSON and enqueue requests.
if received_data:
endpoint = received_data.get("endpoint", None)
data = received_data.get("data", None)
data = encodeBase64(data) if data is not None else None
printLog(endpoint, {"receive_data":data})
self.queue.put((endpoint, data))
Uses blocking readline but honors stop via _stop_event checked between reads.
"""
while not self._stop_event.is_set():
try:
line = sys.stdin.readline()
if not line:
# EOF reached; sleep briefly and re-check stop event
time.sleep(0.1)
continue
received_data = json.loads(line.strip())
if received_data:
endpoint = received_data.get("endpoint")
data = received_data.get("data")
data = encodeBase64(data) if data is not None else None
printLog(endpoint, {"receive_data": data})
self.queue.put((endpoint, data))
except json.JSONDecodeError:
# malformed input; log and continue
errorLogging()
except Exception:
errorLogging()
def startReceiver(self) -> None:
th_receiver = Thread(target=self.receiver)
th_receiver = Thread(target=self.receiver, name="main_receiver")
th_receiver.daemon = True
th_receiver.start()
self._threads.append(th_receiver)
def handleRequest(self, endpoint, data=None) -> tuple:
handler = mapping.get(endpoint)
def handleRequest(self, endpoint: str, data: Any = None) -> tuple:
result = None # デフォルト値を設定
status = 500 # デフォルト値を設定
handler = self.mapping.get(endpoint)
if handler is None:
response = "Invalid endpoint"
status = 404
@@ -394,269 +448,104 @@ class Main:
else:
try:
response = handler["variable"](data)
status = response.get("status", None)
result = response.get("result", None)
except Exception as e:
status = response.get("status")
result = response.get("result")
time.sleep(0.2) # 処理の安定化のために少し待機
except Exception:
errorLogging()
result = str(e)
result = "Internal error"
status = 500
return result, status
def _call_handler(self, endpoint: str, data: Any = None) -> tuple:
result = None
status = 500
handler = self.mapping.get(endpoint)
if handler is None:
response = "Invalid endpoint"
status = 404
else:
try:
response = handler["variable"](data)
status = response.get("status", 500)
result = response.get("result", None)
time.sleep(0.2)
except Exception:
errorLogging()
result = "Internal error"
status = 500
return result, status
def handler(self) -> None:
while True:
if not self.queue.empty():
try:
endpoint, data = self.queue.get()
result, status = self.handleRequest(endpoint, data)
except Exception as e:
errorLogging()
result = str(e)
status = 500
while not self._stop_event.is_set():
try:
endpoint, data = self.queue.get(timeout=0.5)
except Empty:
continue
if status == 423:
# endpoint をロック用の正規化キーに変換してロックを取得
lock_key = self._canonical_lock_key(endpoint)
lock = self._endpoint_locks.get(lock_key)
if lock is not None:
acquired = lock.acquire(blocking=False)
if not acquired:
# 同一機能で既に処理中 -> 少し待って再キュー
time.sleep(0.05)
self.queue.put((endpoint, data))
else:
printLog(endpoint, {"status": status, "send_data": result})
printResponse(status, endpoint, result)
time.sleep(0.1)
continue
try:
result, status = self._call_handler(endpoint, data)
finally:
lock.release()
else:
result, status = self._call_handler(endpoint, data)
if status == 423:
time.sleep(0.1)
self.queue.put((endpoint, data))
else:
printLog(endpoint, {"status": status, "send_data": result})
printResponse(status, endpoint, result)
def startHandler(self) -> None:
th_handler = Thread(target=self.handler)
th_handler.daemon = True
th_handler.start()
for i in range(max(1, self._worker_count)):
th_handler = Thread(target=self.handler, name=f"main_handler_{i}")
th_handler.daemon = True
th_handler.start()
self._threads.append(th_handler)
def start(self) -> None:
while self.main_loop:
time.sleep(1)
"""Start receiver and handler threads."""
self.startReceiver()
self.startHandler()
def stop(self) -> None:
self.main_loop = False
def stop(self, wait: float = 2.0) -> None:
"""Signal threads to stop and wait for them to finish.
Args:
wait: maximum seconds to wait for threads to join.
"""
self._stop_event.set()
# give threads a chance to exit
start = time.time()
for th in self._threads:
remaining = max(0.0, wait - (time.time() - start))
th.join(timeout=remaining)
# 外部から参照可能なインスタンスを提供
main_instance = Main(controller_instance=controller, mapping_data=mapping)
if __name__ == "__main__":
main = Main()
main.startReceiver()
main.startHandler()
main_instance.startReceiver()
main_instance.startHandler()
controller.setWatchdogCallback(main.stop)
controller.init()
main_instance.controller.setWatchdogCallback(main_instance.stop)
main_instance.controller.init()
# mappingのすべてのstatusをTrueにする
for key in mapping.keys():
mapping[key]["status"] = True
process = "main"
match process:
case "main":
main.start()
case "test":
endpoint = "/set/enable/translation"
result, status = main.handleRequest(endpoint)
printResponse(status, endpoint, result)
endpoint = "/run/send_message_box"
data = {"id":"123456", "message":"テスト"}
result, status = main.handleRequest(endpoint, data)
printResponse(status, endpoint, result)
case "test_all":
import time
for endpoint, value in mapping.items():
printLog("endpoint", endpoint)
match endpoint:
case "/run/send_message_box":
# handleRequest("/set/enable/translation")
# handleRequest("/set/enable/convert_message_to_romaji")
data = {"id":"123456", "message":"テスト"}
case "/set/data/selected_translation_engines":
data = {
"1":"CTranslate2",
"2":"CTranslate2",
"3":"CTranslate2",
}
case "/set/data/selected_your_languages":
data = {
"1":{
"1":{
"language": "English",
"country": "Hong Kong"
},
},
"2":{
"1":{
"language":"Japanese",
"country":"Japan"
},
},
"3":{
"1":{
"language":"Japanese",
"country":"Japan"
},
},
}
case "/set/data/selected_target_languages":
data ={
"1":{
"1": {
"language": "Japanese",
"country": "Japan",
"enabled": True,
},
"secondary": {
"language": "English",
"country": "United States",
"enabled": True,
},
"tertiary": {
"language": "Chinese Simplified",
"country": "China",
"enabled": True,
}
},
"2":{
"1":{
"language":"English",
"country":"United States",
"enabled": True,
},
"secondary":{
"language":"English",
"country":"United States",
"enabled": True,
},
"tertiary":{
"language":"English",
"country":"United States",
"enabled": True,
},
},
"3":{
"1":{
"language":"English",
"country":"United States",
"enabled": True,
},
"secondary":{
"language":"English",
"country":"United States",
"enabled": True,
},
"tertiary":{
"language":"English",
"country":"United States",
"enabled": True,
},
},
}
case "/set/data/transparency":
data = 0.5
case "/set/appearance":
data = "Dark"
case "/set/data/ui_scaling":
data = 1.5
case "/set/data/appearance_theme":
data = "Dark"
case "/set/data/textbox_ui_scaling":
data = 1.5
case "/set/data/message_box_ratio":
data = 0.5
case "/set/data/send_message_button_type":
data = "show"
case "/set/data/font_family":
data = "Yu Gothic UI"
case "/set/data/ui_language":
data = "ja"
case "/set/data/ctranslate2_weight_type":
data = "small"
case "/set/data/deepl_auth_key":
data = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee:fx"
case "/set/data/selected_mic_host":
data = "MME"
case "/set/data/selected_mic_device":
data = "マイク (Realtek High Definition Audio)"
case "/set/data/mic_threshold":
data = 0.5
case "/set/data/mic_record_timeout":
data = 1
case "/set/data/mic_phrase_timeout":
data = 5
case "/set/data/mic_max_phrases":
data = 5
case "/set/data/mic_word_filter":
data = "test0, test1, test2"
case "/set/data/selected_speaker_device":
data = "スピーカー (Realtek High Definition Audio)"
case "/set/data/speaker_threshold":
data = 0.5
case "/set/data/speaker_record_timeout":
data = 5
case "/set/data/speaker_phrase_timeout":
data = 5
case "/set/data/speaker_max_phrases":
data = 5
case "/set/data/whisper_weight_type":
data = "base"
case "/set/data/overlay_settings":
data = {
"opacity": 0.5,
"ui_scaling": 1.5,
}
case "/set/data/overlay_small_log_settings":
data = {
"x_pos": 0,
"y_pos": 0,
"z_pos": 0,
"x_rotation": 0,
"y_rotation": 0,
"z_rotation": 0,
"display_duration": 5,
"fadeout_duration": 0.5,
}
case "/set/data/send_message_format_parts":
data = {
"message": {
"prefix": "",
"suffix": ""
},
"between_separator": "\n",
"translation": {
"prefix": "(",
"separator": "\\",
"suffix": ")"
},
"translation_first": False,
}
case "/set/data/received_message_format_parts":
data = {
"message": {
"prefix": "",
"suffix": ""
},
"between_separator": "\n",
"translation": {
"prefix": "(",
"separator": "\\",
"suffix": ")"
},
"translation_first": True,
}
case "/set/data/osc_ip_address":
data = "127.0.0.1"
case "/set/data/osc_port":
data = 8000
case "/set/data/speaker_no_speech_prob":
data = 0.5
case "/set/data/speaker_avg_logprob":
data = 0.5
case "/set/data/mic_no_speech_prob":
data = 0.5
case "/set/data/mic_avg_logprob":
data = 0.5
case _:
data = None
result, status = main.handleRequest(endpoint, data)
printResponse(status, endpoint, result)
time.sleep(0.5)
main.stop()
main_instance.start()