Merge branch 'translate_api' into develop

This commit is contained in:
misyaguziya
2025-10-20 17:20:02 +09:00
43 changed files with 3835 additions and 563 deletions

View File

@@ -5,7 +5,15 @@ a = Analysis(
['src-python\\mainloop.py'], ['src-python\\mainloop.py'],
pathex=[], pathex=[],
binaries=[], binaries=[],
datas=[('./fonts', 'fonts/'), ('.venv/Lib/site-packages/zeroconf', 'zeroconf/'), ('.venv/Lib/site-packages/openvr', 'openvr/'), ('.venv/Lib/site-packages/faster_whisper', 'faster_whisper/'), ('.venv/Lib/site-packages/hf_xet', 'hf_xet/')], datas=[
('./src-python/models/overlay/fonts', 'fonts/'),
('./src-python/models/translation/prompt', 'prompt/'),
('./src-python/models/translation/languages', 'languages/'),
('.venv/Lib/site-packages/zeroconf', 'zeroconf/'),
('.venv/Lib/site-packages/openvr', 'openvr/'),
('.venv/Lib/site-packages/faster_whisper', 'faster_whisper/'),
('.venv/Lib/site-packages/hf_xet', 'hf_xet/')
],
hiddenimports=[], hiddenimports=[],
hookspath=[], hookspath=[],
hooksconfig={}, hooksconfig={},

View File

@@ -5,7 +5,15 @@ a = Analysis(
['src-python\\mainloop.py'], ['src-python\\mainloop.py'],
pathex=[], pathex=[],
binaries=[], binaries=[],
datas=[('./fonts', 'fonts/'), ('.venv_cuda/Lib/site-packages/zeroconf', 'zeroconf/'), ('.venv_cuda/Lib/site-packages/openvr', 'openvr/'), ('.venv_cuda/Lib/site-packages/faster_whisper', 'faster_whisper/'), ('.venv/Lib/site-packages/hf_xet', 'hf_xet/')], datas=[
('./src-python/models/overlay/fonts', 'fonts/'),
('./src-python/models/translation/prompt', 'prompt/'),
('./src-python/models/translation/languages', 'languages/'),
('.venv_cuda/Lib/site-packages/zeroconf', 'zeroconf/'),
('.venv_cuda/Lib/site-packages/openvr', 'openvr/'),
('.venv_cuda/Lib/site-packages/faster_whisper', 'faster_whisper/'),
('.venv/Lib/site-packages/hf_xet', 'hf_xet/')
],
hiddenimports=[], hiddenimports=[],
hookspath=[], hookspath=[],
hooksconfig={}, hooksconfig={},

View File

@@ -12,9 +12,10 @@
"vite-preview": "vite preview", "vite-preview": "vite preview",
"tauri": "tauri", "tauri": "tauri",
"tauri-dev": "tauri dev", "tauri-dev": "tauri dev",
"task-kill": "python task_kill.py",
"clean": "python clean.py", "clean": "python clean.py",
"dev": "npm run build-python && npm run dev-ui", "dev": "npm run task-kill && npm run build-python && npm run dev-ui",
"dev-cuda": "npm run build-python-cuda && npm run dev-ui", "dev-cuda": "npm run task-kill && npm run build-python-cuda && npm run dev-ui",
"dev-ui": "npm-run-all --parallel vite tauri-dev", "dev-ui": "npm-run-all --parallel vite tauri-dev",
"build": "npm run clean && npm run build-python && npm run vite-build && npm run tauri build", "build": "npm run clean && npm run build-python && npm run vite-build && npm run tauri build",
"build-cuda": "npm run clean && npm run build-python-cuda && npm run vite-build && npm run tauri build", "build-cuda": "npm run clean && npm run build-python-cuda && npm run vite-build && npm run tauri build",

View File

@@ -5,6 +5,7 @@ transformers==4.40.2
pillow == 10.0.0 pillow == 10.0.0
PyAudioWPatch == 0.2.12.6 PyAudioWPatch == 0.2.12.6
python-osc == 1.9.0 python-osc == 1.9.0
PyYAML==6.0.2
deepl == 1.22.0 deepl == 1.22.0
flashtext ==2.7 flashtext ==2.7
pyinstaller==6.10.0 pyinstaller==6.10.0
@@ -18,6 +19,11 @@ websockets==15.0.1
huggingface_hub==0.32.2 huggingface_hub==0.32.2
hf-xet==1.1.2 hf-xet==1.1.2
setuptools==80.8.0 setuptools==80.8.0
langchain-openai==0.3.32
langchain-google-genai==2.1.10
google-genai==1.45.0
grpcio==1.67.1
langchain-ollama==0.3.10
SudachiPy==0.6.10 SudachiPy==0.6.10
SudachiDict-core==20250825 SudachiDict-core==20250825
SudachiDict-full==20250825 SudachiDict-full==20250825

View File

@@ -6,6 +6,7 @@ transformers==4.40.2
pillow == 10.0.0 pillow == 10.0.0
PyAudioWPatch == 0.2.12.6 PyAudioWPatch == 0.2.12.6
python-osc == 1.9.0 python-osc == 1.9.0
PyYAML==6.0.2
deepl == 1.22.0 deepl == 1.22.0
flashtext ==2.7 flashtext ==2.7
pyinstaller==6.10.0 pyinstaller==6.10.0
@@ -19,6 +20,11 @@ websockets==15.0.1
huggingface_hub==0.32.2 huggingface_hub==0.32.2
hf-xet==1.1.2 hf-xet==1.1.2
setuptools==80.8.0 setuptools==80.8.0
langchain-openai==0.3.32
langchain-google-genai==2.1.10
google-genai==1.45.0
grpcio==1.67.1
langchain-ollama==0.3.10
SudachiPy==0.6.10 SudachiPy==0.6.10
SudachiDict-core==20250825 SudachiDict-core==20250825
SudachiDict-full==20250825 SudachiDict-full==20250825

View File

@@ -65,6 +65,8 @@ class TestMainloop():
for endpoint in self.main.mapping.keys(): for endpoint in self.main.mapping.keys():
if endpoint.startswith("/set/data/"): if endpoint.startswith("/set/data/"):
self.set_data_endpoints.append(endpoint) self.set_data_endpoints.append(endpoint)
# 新規: local LLM/API キー/モデル選択関連の存在確認ログ
print(f"[DEBUG] set_data_endpoints count: {len(self.set_data_endpoints)}", flush=True)
self.delete_data_endpoints = [] self.delete_data_endpoints = []
for endpoint in self.main.mapping.keys(): for endpoint in self.main.mapping.keys():
@@ -225,9 +227,57 @@ class TestMainloop():
data = random.choice(self.config_dict["transcription_compute_device_list"]) data = random.choice(self.config_dict["transcription_compute_device_list"])
case "/set/data/ctranslate2_weight_type": case "/set/data/ctranslate2_weight_type":
data = random.choice(list(self.config_dict["selectable_ctranslate2_weight_type_dict"].keys())) data = random.choice(list(self.config_dict["selectable_ctranslate2_weight_type_dict"].keys()))
# LLM / API Clients
case "/set/data/plamo_model":
# 事前にモデルリストを取得
self.config_dict["plamo_model_list"], _ = self.main.handleRequest("/get/data/plamo_model_list", None)
model_list = self.config_dict.get("plamo_model_list", [])
data = random.choice(model_list) if model_list else None
case "/set/data/plamo_auth_key":
data = "PLAMO_DUMMY_KEY" # 成功か失敗かは内部判定に依存
expected_status = [200, 400]
case "/set/data/gemini_model":
self.config_dict["gemini_model_list"], _ = self.main.handleRequest("/get/data/gemini_model_list", None)
model_list = self.config_dict.get("gemini_model_list", [])
data = random.choice(model_list) if model_list else None
case "/set/data/gemini_auth_key":
data = "GEMINI_DUMMY_KEY"
expected_status = [200, 400]
case "/set/data/openai_model":
self.config_dict["openai_model_list"], _ = self.main.handleRequest("/get/data/openai_model_list", None)
model_list = self.config_dict.get("openai_model_list", [])
data = random.choice(model_list) if model_list else None
case "/set/data/openai_auth_key":
data = "OPENAI_DUMMY_KEY"
expected_status = [200, 400]
case "/set/data/lmstudio_model":
self.config_dict["lmstudio_model_list"], _ = self.main.handleRequest("/get/data/lmstudio_model_list", None)
model_list = self.config_dict.get("lmstudio_model_list", [])
data = random.choice(model_list) if model_list else None
case "/set/data/lmstudio_url":
# 正常/異常 URL をランダム投入
data = random.choice([
"http://localhost:1234/v1",
"http://127.0.0.1:1234/v1",
"http://invalid_host:9999/v1",
])
expected_status = [200, 400]
case "/set/data/ollama_model":
self.config_dict["ollama_model_list"], _ = self.main.handleRequest("/get/data/ollama_model_list", None)
model_list = self.config_dict.get("ollama_model_list", [])
data = random.choice(model_list) if model_list else None
case "/set/data/deepl_auth_key": case "/set/data/deepl_auth_key":
data = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" data = "DEEPL_DUMMY_KEY"
expected_status = [400] expected_status = [200, 400]
case "/set/data/plamo_auth_key":
data = "PLAMO_DUMMY_KEY"
expected_status = [200, 400]
case "/set/data/gemini_auth_key":
data = "GEMINI_DUMMY_KEY"
expected_status = [200, 400]
case "/set/data/openai_auth_key":
data = "OPENAI_DUMMY_KEY"
expected_status = [200, 400]
case "/set/data/selected_mic_host": case "/set/data/selected_mic_host":
data = random.choice(self.config_dict["mic_host_list"]) data = random.choice(self.config_dict["mic_host_list"])
case "/set/data/selected_mic_device": case "/set/data/selected_mic_device":
@@ -363,6 +413,15 @@ class TestMainloop():
data = random.choice(self.config_dict["selected_translation_compute_device"]["compute_types"]) data = random.choice(self.config_dict["selected_translation_compute_device"]["compute_types"])
case "/set/data/selected_transcription_compute_type": case "/set/data/selected_transcription_compute_type":
data = random.choice(self.config_dict["selected_transcription_compute_device"]["compute_types"]) data = random.choice(self.config_dict["selected_transcription_compute_device"]["compute_types"])
# 言語変換設定(新規 transliteration 機能 ON/OFF テスト補助)
case "/set/enable/convert_message_to_romaji":
data = None
case "/set/disable/convert_message_to_romaji":
data = None
case "/set/enable/convert_message_to_hiragana":
data = None
case "/set/disable/convert_message_to_hiragana":
data = None
case _: case _:
data = None data = None
expected_status = [404] expected_status = [404]
@@ -469,6 +528,12 @@ class TestMainloop():
case "/run/feed_watchdog": case "/run/feed_watchdog":
data = None data = None
expected_status = [401] # !!!Cant be tested here!!! expected_status = [401] # !!!Cant be tested here!!!
case "/run/lmstudio_connection":
data = None
expected_status = [200, 400]
case "/run/ollama_connection":
data = None
expected_status = [200, 400]
case _: case _:
data = None data = None
expected_status = [404] expected_status = [404]

View File

@@ -16,9 +16,11 @@ except Exception: # pragma: no cover - optional runtime
device_manager = None # type: ignore device_manager = None # type: ignore
try: try:
from models.translation.translation_languages import translation_lang from models.translation.translation_languages import translation_lang, loadTranslationLanguages
except Exception: # pragma: no cover - optional runtime except Exception: # pragma: no cover - optional runtime
translation_lang = {} # type: ignore translation_lang = {} # type: ignore
def loadTranslationLanguages(path: str, force: bool = False) -> Dict[str, Any]:
return {}
try: try:
from models.translation.translation_utils import ctranslate2_weights from models.translation.translation_utils import ctranslate2_weights
@@ -311,6 +313,51 @@ class Config:
if isinstance(value, dict): if isinstance(value, dict):
self._SELECTABLE_TRANSCRIPTION_ENGINE_STATUS = value self._SELECTABLE_TRANSCRIPTION_ENGINE_STATUS = value
@property
def SELECTABLE_PLAMO_MODEL_LIST(self):
return self._SELECTABLE_PLAMO_MODEL_LIST
@SELECTABLE_PLAMO_MODEL_LIST.setter
def SELECTABLE_PLAMO_MODEL_LIST(self, value):
if isinstance(value, list):
self._SELECTABLE_PLAMO_MODEL_LIST = value
@property
def SELECTABLE_GEMINI_MODEL_LIST(self):
return self._SELECTABLE_GEMINI_MODEL_LIST
@SELECTABLE_GEMINI_MODEL_LIST.setter
def SELECTABLE_GEMINI_MODEL_LIST(self, value):
if isinstance(value, list):
self._SELECTABLE_GEMINI_MODEL_LIST = value
@property
def SELECTABLE_OPENAI_MODEL_LIST(self):
return self._SELECTABLE_OPENAI_MODEL_LIST
@SELECTABLE_OPENAI_MODEL_LIST.setter
def SELECTABLE_OPENAI_MODEL_LIST(self, value):
if isinstance(value, list):
self._SELECTABLE_OPENAI_MODEL_LIST = value
@property
def SELECTABLE_LMSTUDIO_MODEL_LIST(self):
return self._SELECTABLE_LMSTUDIO_MODEL_LIST
@SELECTABLE_LMSTUDIO_MODEL_LIST.setter
def SELECTABLE_LMSTUDIO_MODEL_LIST(self, value):
if isinstance(value, list):
self._SELECTABLE_LMSTUDIO_MODEL_LIST = value
@property
def SELECTABLE_OLLAMA_MODEL_LIST(self):
return self._SELECTABLE_OLLAMA_MODEL_LIST
@SELECTABLE_OLLAMA_MODEL_LIST.setter
def SELECTABLE_OLLAMA_MODEL_LIST(self, value):
if isinstance(value, list):
self._SELECTABLE_OLLAMA_MODEL_LIST = value
# Save Json Data # Save Json Data
## Main Window ## Main Window
@property @property
@@ -894,6 +941,77 @@ class Config:
self._SELECTED_TRANSCRIPTION_COMPUTE_TYPE = value self._SELECTED_TRANSCRIPTION_COMPUTE_TYPE = value
self.saveConfig(inspect.currentframe().f_code.co_name, value) self.saveConfig(inspect.currentframe().f_code.co_name, value)
@property
@json_serializable('SELECTED_PLAMO_MODEL')
def SELECTED_PLAMO_MODEL(self):
return self._SELECTED_PLAMO_MODEL
@SELECTED_PLAMO_MODEL.setter
def SELECTED_PLAMO_MODEL(self, value):
if isinstance(value, str):
if value in self.SELECTABLE_PLAMO_MODEL_LIST:
self._SELECTED_PLAMO_MODEL = value
self.saveConfig(inspect.currentframe().f_code.co_name, value)
@property
@json_serializable('GEMINI_MODEL')
def SELECTED_GEMINI_MODEL(self):
return self._SELECTED_GEMINI_MODEL
@SELECTED_GEMINI_MODEL.setter
def SELECTED_GEMINI_MODEL(self, value):
if isinstance(value, str):
if value in self.SELECTABLE_GEMINI_MODEL_LIST:
self._SELECTED_GEMINI_MODEL = value
self.saveConfig(inspect.currentframe().f_code.co_name, value)
@property
@json_serializable('SELECTED_OPENAI_MODEL')
def SELECTED_OPENAI_MODEL(self):
return self._SELECTED_OPENAI_MODEL
@SELECTED_OPENAI_MODEL.setter
def SELECTED_OPENAI_MODEL(self, value):
if isinstance(value, str):
if value in self.SELECTABLE_OPENAI_MODEL_LIST:
self._SELECTED_OPENAI_MODEL = value
self.saveConfig(inspect.currentframe().f_code.co_name, value)
@property
@json_serializable('LMSTUDIO_URL')
def LMSTUDIO_URL(self):
return self._LMSTUDIO_URL
@LMSTUDIO_URL.setter
def LMSTUDIO_URL(self, value):
if isinstance(value, str):
self._LMSTUDIO_URL = value
self.saveConfig(inspect.currentframe().f_code.co_name, value)
@property
@json_serializable('SELECTED_LMSTUDIO_MODEL')
def SELECTED_LMSTUDIO_MODEL(self):
return self._SELECTED_LMSTUDIO_MODEL
@SELECTED_LMSTUDIO_MODEL.setter
def SELECTED_LMSTUDIO_MODEL(self, value):
if isinstance(value, str):
if value in self.SELECTABLE_LMSTUDIO_MODEL_LIST:
self._SELECTED_LMSTUDIO_MODEL = value
self.saveConfig(inspect.currentframe().f_code.co_name, value)
@property
@json_serializable('SELECTED_OLLAMA_MODEL')
def SELECTED_OLLAMA_MODEL(self):
return self._SELECTED_OLLAMA_MODEL
@SELECTED_OLLAMA_MODEL.setter
def SELECTED_OLLAMA_MODEL(self, value):
if isinstance(value, str):
if value in self.SELECTABLE_OLLAMA_MODEL_LIST:
self._SELECTED_OLLAMA_MODEL = value
self.saveConfig(inspect.currentframe().f_code.co_name, value)
@property @property
@json_serializable('AUTO_CLEAR_MESSAGE_BOX') @json_serializable('AUTO_CLEAR_MESSAGE_BOX')
def AUTO_CLEAR_MESSAGE_BOX(self): def AUTO_CLEAR_MESSAGE_BOX(self):
@@ -1111,6 +1229,7 @@ class Config:
# these external mappings may be empty dicts if the optional modules failed to import # these external mappings may be empty dicts if the optional modules failed to import
self._SELECTABLE_CTRANSLATE2_WEIGHT_TYPE_LIST = getattr(ctranslate2_weights, 'keys', lambda: [])() self._SELECTABLE_CTRANSLATE2_WEIGHT_TYPE_LIST = getattr(ctranslate2_weights, 'keys', lambda: [])()
self._SELECTABLE_WHISPER_WEIGHT_TYPE_LIST = getattr(whisper_models, 'keys', lambda: [])() self._SELECTABLE_WHISPER_WEIGHT_TYPE_LIST = getattr(whisper_models, 'keys', lambda: [])()
translation_lang = loadTranslationLanguages(self.PATH_LOCAL)
self._SELECTABLE_TRANSLATION_ENGINE_LIST = getattr(translation_lang, 'keys', lambda: [])() self._SELECTABLE_TRANSLATION_ENGINE_LIST = getattr(translation_lang, 'keys', lambda: [])()
try: try:
# transcription_lang is nested dict; attempt to extract keys defensively # transcription_lang is nested dict; attempt to extract keys defensively
@@ -1168,6 +1287,11 @@ class Config:
self._SELECTABLE_TRANSCRIPTION_ENGINE_STATUS = {} self._SELECTABLE_TRANSCRIPTION_ENGINE_STATUS = {}
for engine in self.SELECTABLE_TRANSCRIPTION_ENGINE_LIST: for engine in self.SELECTABLE_TRANSCRIPTION_ENGINE_LIST:
self._SELECTABLE_TRANSCRIPTION_ENGINE_STATUS[engine] = False self._SELECTABLE_TRANSCRIPTION_ENGINE_STATUS[engine] = False
self._SELECTABLE_PLAMO_MODEL_LIST = []
self._SELECTABLE_GEMINI_MODEL_LIST = []
self._SELECTABLE_OPENAI_MODEL_LIST = []
self._SELECTABLE_LMSTUDIO_MODEL_LIST = []
self._SELECTABLE_OLLAMA_MODEL_LIST = []
# Save Json Data # Save Json Data
## Main Window ## Main Window
@@ -1267,11 +1391,20 @@ class Config:
self._OSC_PORT = 9000 self._OSC_PORT = 9000
self._AUTH_KEYS = { self._AUTH_KEYS = {
"DeepL_API": None, "DeepL_API": None,
"Plamo_API": None,
"Gemini_API": None,
"OpenAI_API": None,
} }
self._USE_EXCLUDE_WORDS = True self._USE_EXCLUDE_WORDS = True
self._SELECTED_TRANSLATION_COMPUTE_DEVICE = copy.deepcopy(self.SELECTABLE_COMPUTE_DEVICE_LIST[0]) self._SELECTED_TRANSLATION_COMPUTE_DEVICE = copy.deepcopy(self.SELECTABLE_COMPUTE_DEVICE_LIST[0])
self._SELECTED_TRANSCRIPTION_COMPUTE_DEVICE = copy.deepcopy(self.SELECTABLE_COMPUTE_DEVICE_LIST[0]) self._SELECTED_TRANSCRIPTION_COMPUTE_DEVICE = copy.deepcopy(self.SELECTABLE_COMPUTE_DEVICE_LIST[0])
self._CTRANSLATE2_WEIGHT_TYPE = "small" self._CTRANSLATE2_WEIGHT_TYPE = "m2m100_418M-ct2-int8"
self._SELECTED_PLAMO_MODEL = None
self._SELECTED_GEMINI_MODEL = None
self._SELECTED_OPENAI_MODEL = None
self._LMSTUDIO_URL = "http://127.0.0.1:1234/v1"
self._SELECTED_LMSTUDIO_MODEL = None
self._SELECTED_OLLAMA_MODEL = None
self._SELECTED_TRANSLATION_COMPUTE_TYPE = "auto" self._SELECTED_TRANSLATION_COMPUTE_TYPE = "auto"
self._WHISPER_WEIGHT_TYPE = "base" self._WHISPER_WEIGHT_TYPE = "base"
self._SELECTED_TRANSCRIPTION_COMPUTE_TYPE = "auto" self._SELECTED_TRANSCRIPTION_COMPUTE_TYPE = "auto"

View File

@@ -1598,6 +1598,467 @@ class Controller:
self.updateTranslationEngineAndEngineList() self.updateTranslationEngineAndEngineList()
return {"status":200, "result":config.AUTH_KEYS[translator_name]} return {"status":200, "result":config.AUTH_KEYS[translator_name]}
def getPlamoAuthKey(self, *args, **kwargs) -> dict:
return {"status":200, "result":config.AUTH_KEYS["Plamo_API"]}
def setPlamoAuthKey(self, data, *args, **kwargs) -> dict:
printLog("Set Plamo Auth Key", data)
translator_name = "Plamo_API"
try:
data = str(data)
if len(data) >= 72:
result = model.authenticationTranslatorPlamoAuthKey(auth_key=data)
if result is True:
key = data
auth_keys = config.AUTH_KEYS
auth_keys[translator_name] = key
config.AUTH_KEYS = auth_keys
config.SELECTABLE_TRANSLATION_ENGINE_STATUS[translator_name] = True
config.SELECTABLE_PLAMO_MODEL_LIST = model.getTranslatorPlamoModelList()
self.run(200, self.run_mapping["selectable_plamo_model_list"], config.SELECTABLE_PLAMO_MODEL_LIST)
if config.SELECTED_PLAMO_MODEL not in config.SELECTABLE_PLAMO_MODEL_LIST:
config.SELECTED_PLAMO_MODEL = config.SELECTABLE_PLAMO_MODEL_LIST[0]
model.setTranslatorPlamoModel(model=config.SELECTED_PLAMO_MODEL)
self.run(200, self.run_mapping["selected_plamo_model"], config.SELECTED_PLAMO_MODEL)
model.updateTranslatorPlamoClient()
self.updateTranslationEngineAndEngineList()
response = {"status":200, "result":config.AUTH_KEYS[translator_name]}
else:
response = {
"status":400,
"result":{
"message":"Authentication failure of plamo auth key",
"data": config.AUTH_KEYS[translator_name]
}
}
else:
response = {
"status":400,
"result":{
"message":"Plamo auth key length is not correct",
"data": config.AUTH_KEYS[translator_name]
}
}
except Exception as e:
errorLogging()
response = {
"status":400,
"result":{
"message":f"Error {e}",
"data": config.AUTH_KEYS[translator_name]
}
}
return response
def delPlamoAuthKey(self, *args, **kwargs) -> dict:
translator_name = "Plamo_API"
auth_keys = config.AUTH_KEYS
auth_keys[translator_name] = None
config.AUTH_KEYS = auth_keys
config.SELECTABLE_TRANSLATION_ENGINE_STATUS[translator_name] = False
self.updateTranslationEngineAndEngineList()
return {"status":200, "result":config.AUTH_KEYS[translator_name]}
def getPlamoModelList(self, *args, **kwargs) -> dict:
return {"status":200, "result": config.SELECTABLE_PLAMO_MODEL_LIST}
def getPlamoModel(self, *args, **kwargs) -> dict:
return {"status":200, "result":config.SELECTED_PLAMO_MODEL}
def setPlamoModel(self, data, *args, **kwargs) -> dict:
printLog("Set Plamo Model", data)
try:
data = str(data)
result = model.setTranslatorPlamoModel(model=data)
if result is True:
config.SELECTED_PLAMO_MODEL = data
model.setTranslatorPlamoModel(model=config.SELECTED_PLAMO_MODEL)
model.updateTranslatorPlamoClient()
response = {"status":200, "result":config.SELECTED_PLAMO_MODEL}
else:
response = {
"status":400,
"result":{
"message":"Plamo model is not valid",
"data": config.SELECTED_PLAMO_MODEL
}
}
except Exception as e:
errorLogging()
response = {
"status":400,
"result":{
"message":f"Error {e}",
"data": config.SELECTED_PLAMO_MODEL
}
}
return response
def getGeminiAuthKey(self, *args, **kwargs) -> dict:
return {"status":200, "result":config.AUTH_KEYS["Gemini_API"]}
def setGeminiAuthKey(self, data, *args, **kwargs) -> dict:
printLog("Set Gemini Auth Key", data)
translator_name = "Gemini_API"
try:
data = str(data)
if len(data) >= 39:
result = model.authenticationTranslatorGeminiAuthKey(auth_key=data)
if result is True:
key = data
auth_keys = config.AUTH_KEYS
auth_keys[translator_name] = key
config.AUTH_KEYS = auth_keys
config.SELECTABLE_TRANSLATION_ENGINE_STATUS[translator_name] = True
config.SELECTABLE_GEMINI_MODEL_LIST = model.getTranslatorGeminiModelList()
self.run(200, self.run_mapping["selectable_gemini_model_list"], config.SELECTABLE_GEMINI_MODEL_LIST)
if config.SELECTED_GEMINI_MODEL not in config.SELECTABLE_GEMINI_MODEL_LIST:
config.SELECTED_GEMINI_MODEL = config.SELECTABLE_GEMINI_MODEL_LIST[0]
model.setTranslatorGeminiModel(model=config.SELECTED_GEMINI_MODEL)
self.run(200, self.run_mapping["selected_gemini_model"], config.SELECTED_GEMINI_MODEL)
model.updateTranslatorGeminiClient()
self.updateTranslationEngineAndEngineList()
response = {"status":200, "result":config.AUTH_KEYS[translator_name]}
else:
response = {
"status":400,
"result":{
"message":"Authentication failure of gemini auth key",
"data": config.AUTH_KEYS[translator_name]
}
}
else:
response = {
"status":400,
"result":{
"message":"Gemini auth key length is not correct",
"data": config.AUTH_KEYS[translator_name]
}
}
except Exception as e:
errorLogging()
response = {
"status":400,
"result":{
"message":f"Error {e}",
"data": config.AUTH_KEYS[translator_name]
}
}
return response
def delGeminiAuthKey(self, *args, **kwargs) -> dict:
translator_name = "Gemini_API"
auth_keys = config.AUTH_KEYS
auth_keys[translator_name] = None
config.AUTH_KEYS = auth_keys
config.SELECTABLE_TRANSLATION_ENGINE_STATUS[translator_name] = False
self.updateTranslationEngineAndEngineList()
return {"status":200, "result":config.AUTH_KEYS[translator_name]}
def getGeminiModelList(self, *args, **kwargs) -> dict:
return {"status":200, "result": config.SELECTABLE_GEMINI_MODEL_LIST}
def getGeminiModel(self, *args, **kwargs) -> dict:
return {"status":200, "result":config.SELECTED_GEMINI_MODEL}
def setGeminiModel(self, data, *args, **kwargs) -> dict:
printLog("Set Gemini Model", data)
try:
data = str(data)
result = model.setTranslatorGeminiModel(model=data)
if result is True:
config.SELECTED_GEMINI_MODEL = data
model.setTranslatorGeminiModel(model=config.SELECTED_GEMINI_MODEL)
model.updateTranslatorGeminiClient()
response = {"status":200, "result":config.SELECTED_GEMINI_MODEL}
else:
response = {
"status":400,
"result":{
"message":"Gemini model is not valid",
"data": config.SELECTED_GEMINI_MODEL
}
}
except Exception as e:
errorLogging()
response = {
"status":400,
"result":{
"message":f"Error {e}",
"data": config.SELECTED_GEMINI_MODEL
}
}
return response
@staticmethod
def getOpenAIAuthKey(*args, **kwargs) -> dict:
return {"status":200, "result":config.AUTH_KEYS["OpenAI_API"]}
def setOpenAIAuthKey(self, data, *args, **kwargs) -> dict:
printLog("Set OpenAI Auth Key", data)
translator_name = "OpenAI_API"
try:
data = str(data)
if data.startswith("sk-") and len(data) >= 164:
key = data
auth_keys = config.AUTH_KEYS
auth_keys[translator_name] = key
config.AUTH_KEYS = auth_keys
config.SELECTABLE_TRANSLATION_ENGINE_STATUS[translator_name] = True
config.SELECTABLE_OPENAI_MODEL_LIST = model.getTranslatorOpenAIModelList()
self.run(200, self.run_mapping["selectable_openai_model_list"], config.SELECTABLE_OPENAI_MODEL_LIST)
if config.SELECTED_OPENAI_MODEL not in config.SELECTABLE_OPENAI_MODEL_LIST:
config.SELECTED_OPENAI_MODEL = config.SELECTABLE_OPENAI_MODEL_LIST[0]
model.setTranslatorOpenAIModel(model=config.SELECTED_OPENAI_MODEL)
self.run(200, self.run_mapping["selected_openai_model"], config.SELECTED_OPENAI_MODEL)
model.updateTranslatorOpenAIClient()
self.updateTranslationEngineAndEngineList()
response = {"status":200, "result":config.AUTH_KEYS[translator_name]}
else:
response = {
"status":400,
"result":{
"message":"OpenAI auth key is not valid",
"data": config.AUTH_KEYS[translator_name]
}
}
except Exception as e:
errorLogging()
response = {
"status":400,
"result":{
"message":f"Error {e}",
"data": config.AUTH_KEYS[translator_name]
}
}
return response
def delOpenAIAuthKey(self, *args, **kwargs) -> dict:
translator_name = "OpenAI_API"
auth_keys = config.AUTH_KEYS
auth_keys[translator_name] = None
config.AUTH_KEYS = auth_keys
config.SELECTABLE_TRANSLATION_ENGINE_STATUS[translator_name] = False
self.updateTranslationEngineAndEngineList()
return {"status":200, "result":config.AUTH_KEYS[translator_name]}
def getOpenAIModelList(self, *args, **kwargs) -> dict:
return {"status":200, "result": config.SELECTABLE_OPENAI_MODEL_LIST}
def getOpenAIModel(self, *args, **kwargs) -> dict:
return {"status":200, "result":config.SELECTED_OPENAI_MODEL}
def setOpenAIModel(self, data, *args, **kwargs) -> dict:
printLog("Set OpenAI Model", data)
try:
data = str(data)
result = model.setTranslatorOpenAIModel(model=data)
if result is True:
config.SELECTED_OPENAI_MODEL = data
model.setTranslatorOpenAIModel(model=config.SELECTED_OPENAI_MODEL)
model.updateTranslatorOpenAIClient()
response = {"status":200, "result":config.SELECTED_OPENAI_MODEL}
else:
response = {
"status":400,
"result":{
"message":"OpenAI model is not valid",
"data": config.SELECTED_OPENAI_MODEL
}
}
except Exception as e:
errorLogging()
response = {
"status":400,
"result":{
"message":f"Error {e}",
"data": config.SELECTED_OPENAI_MODEL
}
}
return response
def checkTranslatorLMStudioConnection(self, *args, **kwargs) -> dict:
printLog("Check Translator LMStudio Connection")
translator_name = "LMStudio"
try:
result = model.authenticationTranslatorLMStudio(base_url=config.LMSTUDIO_URL)
if result is True:
config.SELECTABLE_TRANSLATION_ENGINE_STATUS[translator_name] = True
config.SELECTABLE_LMSTUDIO_MODEL_LIST = model.getTranslatorLMStudioModelList()
self.run(200, self.run_mapping["selectable_lmstudio_model_list"], config.SELECTABLE_LMSTUDIO_MODEL_LIST)
if config.SELECTED_LMSTUDIO_MODEL not in config.SELECTABLE_LMSTUDIO_MODEL_LIST:
config.SELECTED_LMSTUDIO_MODEL = config.SELECTABLE_LMSTUDIO_MODEL_LIST[0]
model.setTranslatorLMStudioModel(model=config.SELECTED_LMSTUDIO_MODEL)
self.run(200, self.run_mapping["selected_lmstudio_model"], config.SELECTED_LMSTUDIO_MODEL)
model.updateTranslatorLMStudioClient()
self.updateTranslationEngineAndEngineList()
response = {"status":200, "result":True}
else:
response = {
"status":400,
"result":{
"message":"Cannot connect to LMStudio server",
"data": False
}
}
except Exception as e:
errorLogging()
response = {
"status":400,
"result":{
"message":f"Error {e}",
"data": False
}
}
return response
def getTranslatorLMStudioURL(self, *args, **kwargs) -> dict:
return {"status":200, "result":config.LMSTUDIO_URL}
def setTranslatorLMStudioURL(self, data, *args, **kwargs) -> dict:
printLog("Set Translator LMStudio URL", data)
translator_name = "LMStudio"
try:
data = str(data)
result = model.authenticationTranslatorLMStudio(base_url=data)
if result is True:
config.LMSTUDIO_URL = data
config.SELECTABLE_TRANSLATION_ENGINE_STATUS[translator_name] = True
config.SELECTABLE_LMSTUDIO_MODEL_LIST = model.getTranslatorLMStudioModelList()
self.run(200, self.run_mapping["selectable_lmstudio_model_list"], config.SELECTABLE_LMSTUDIO_MODEL_LIST)
if config.SELECTED_LMSTUDIO_MODEL not in config.SELECTABLE_LMSTUDIO_MODEL_LIST:
config.SELECTED_LMSTUDIO_MODEL = config.SELECTABLE_LMSTUDIO_MODEL_LIST[0]
model.setTranslatorLMStudioModel(model=config.SELECTED_LMSTUDIO_MODEL)
self.run(200, self.run_mapping["selected_lmstudio_model"], config.SELECTED_LMSTUDIO_MODEL)
model.updateTranslatorLMStudioClient()
self.updateTranslationEngineAndEngineList()
response = {"status":200, "result":config.LMSTUDIO_URL}
else:
response = {
"status":400,
"result":{
"message":"LMStudio URL is not valid",
"data": config.LMSTUDIO_URL
}
}
except Exception as e:
errorLogging()
response = {
"status":400,
"result":{
"message":f"Error {e}",
"data": config.LMSTUDIO_URL
}
}
return response
def getTranslatorLStudioModelList(self, *args, **kwargs) -> dict:
model_list = model.getTranslatorLMStudioModelList()
return {"status":200, "result": model_list}
def getTranslatorLMStudioModel(self, *args, **kwargs) -> dict:
return {"status":200, "result":config.SELECTED_LMSTUDIO_MODEL}
def setTranslatorLMStudioModel(self, data, *args, **kwargs) -> dict:
printLog("Set Translator LMStudio Model", data)
try:
data = str(data)
result = model.setTranslatorLMStudioModel(model=data)
if result is True:
config.SELECTED_LMSTUDIO_MODEL = data
model.setTranslatorLMStudioModel(model=config.SELECTED_LMSTUDIO_MODEL)
model.updateTranslatorLMStudioClient()
response = {"status":200, "result":config.SELECTED_LMSTUDIO_MODEL}
else:
response = {
"status":400,
"result":{
"message":"LMStudio model is not valid",
"data": config.SELECTED_LMSTUDIO_MODEL
}
}
except Exception as e:
errorLogging()
response = {
"status":400,
"result":{
"message":f"Error {e}",
"data": config.SELECTED_LMSTUDIO_MODEL
}
}
return response
def checkTranslatorOllamaConnection(self, *args, **kwargs) -> dict:
printLog("Check Translator Ollama Connection")
translator_name = "Ollama"
try:
result = model.authenticationTranslatorOllama()
if result is True:
config.SELECTABLE_TRANSLATION_ENGINE_STATUS[translator_name] = True
config.SELECTABLE_OLLAMA_MODEL_LIST = model.getTranslatorOllamaModelList()
self.run(200, self.run_mapping["selectable_ollama_model_list"], config.SELECTABLE_OLLAMA_MODEL_LIST)
if config.SELECTED_OLLAMA_MODEL not in config.SELECTABLE_OLLAMA_MODEL_LIST:
config.SELECTED_OLLAMA_MODEL = config.SELECTABLE_OLLAMA_MODEL_LIST[0]
model.setTranslatorOllamaModel(model=config.SELECTED_OLLAMA_MODEL)
self.run(200, self.run_mapping["selected_ollama_model"], config.SELECTED_OLLAMA_MODEL)
model.updateTranslatorOllamaClient()
self.updateTranslationEngineAndEngineList()
response = {"status":200, "result":True}
else:
response = {
"status":400,
"result":{
"message":"Cannot connect to ollama server",
"data": False
}
}
except Exception as e:
errorLogging()
response = {
"status":400,
"result":{
"message":f"Error {e}",
"data": False
}
}
return response
def getTranslatorOllamaModelList(self, *args, **kwargs) -> dict:
model_list = model.getTranslatorOllamaModelList()
return {"status":200, "result": model_list}
def getTranslatorOllamaModel(self, *args, **kwargs) -> dict:
return {"status":200, "result":config.SELECTED_OLLAMA_MODEL}
def setTranslatorOllamaModel(self, data, *args, **kwargs) -> dict:
printLog("Set Translator Ollama Model", data)
try:
data = str(data)
result = model.setTranslatorOllamaModel(model=data)
if result is True:
config.SELECTED_OLLAMA_MODEL = data
model.setTranslatorOllamaModel(model=config.SELECTED_OLLAMA_MODEL)
model.updateTranslatorOllamaClient()
response = {"status":200, "result":config.SELECTED_OLLAMA_MODEL}
else:
response = {
"status":400,
"result":{
"message":"ollama model is not valid",
"data": config.SELECTED_OLLAMA_MODEL
}
}
except Exception as e:
errorLogging()
response = {
"status":400,
"result":{
"message":f"Error {e}",
"data": config.SELECTED_OLLAMA_MODEL
}
}
return response
@staticmethod @staticmethod
def getCtranslate2WeightType(*args, **kwargs) -> dict: def getCtranslate2WeightType(*args, **kwargs) -> dict:
return {"status":200, "result":config.CTRANSLATE2_WEIGHT_TYPE} return {"status":200, "result":config.CTRANSLATE2_WEIGHT_TYPE}
@@ -2465,11 +2926,94 @@ class Controller:
if config.AUTH_KEYS[engine] is not None: if config.AUTH_KEYS[engine] is not None:
if model.authenticationTranslatorDeepLAuthKey(auth_key=config.AUTH_KEYS[engine]) is True: if model.authenticationTranslatorDeepLAuthKey(auth_key=config.AUTH_KEYS[engine]) is True:
config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = True config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = True
printLog("DeepL API Key is valid")
else: else:
# error update Auth key # error update Auth key
auth_keys = config.AUTH_KEYS auth_keys = config.AUTH_KEYS
auth_keys[engine] = None auth_keys[engine] = None
config.AUTH_KEYS = auth_keys config.AUTH_KEYS = auth_keys
printLog("DeepL API Key is invalid")
case "Plamo_API":
printLog("Start check Plamo API Key")
config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = False
if config.AUTH_KEYS[engine] is not None:
if model.authenticationTranslatorPlamoAuthKey(auth_key=config.AUTH_KEYS[engine]) is True:
config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = True
printLog("Plamo API Key is valid")
config.SELECTABLE_PLAMO_MODEL_LIST = model.getTranslatorPlamoModelList()
if config.SELECTED_PLAMO_MODEL not in config.SELECTABLE_PLAMO_MODEL_LIST:
config.SELECTED_PLAMO_MODEL = config.SELECTABLE_PLAMO_MODEL_LIST[0]
model.setTranslatorPlamoModel(config.SELECTED_PLAMO_MODEL)
model.updateTranslatorPlamoClient()
else:
# error update Auth key
auth_keys = config.AUTH_KEYS
auth_keys[engine] = None
config.AUTH_KEYS = auth_keys
printLog("Plamo API Key is invalid")
case "Gemini_API":
printLog("Start check Gemini API Key")
config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = False
if config.AUTH_KEYS[engine] is not None:
if model.authenticationTranslatorGeminiAuthKey(auth_key=config.AUTH_KEYS[engine]) is True:
config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = True
printLog("Gemini API Key is valid")
config.SELECTABLE_GEMINI_MODEL_LIST = model.getTranslatorGeminiModelList()
if config.SELECTED_GEMINI_MODEL not in config.SELECTABLE_GEMINI_MODEL_LIST:
config.SELECTED_GEMINI_MODEL = config.SELECTABLE_GEMINI_MODEL_LIST[0]
model.setTranslatorGeminiModel(config.SELECTED_GEMINI_MODEL)
model.updateTranslatorGeminiClient()
else:
# error update Auth key
auth_keys = config.AUTH_KEYS
auth_keys[engine] = None
config.AUTH_KEYS = auth_keys
printLog("Gemini API Key is invalid")
case "OpenAI_API":
printLog("Start check OpenAI API Key")
config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = False
if config.AUTH_KEYS[engine] is not None:
if model.authenticationTranslatorOpenAIAuthKey(auth_key=config.AUTH_KEYS[engine]) is True:
config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = True
printLog("OpenAI API Key is valid")
config.SELECTABLE_OPENAI_MODEL_LIST = model.getTranslatorOpenAIModelList()
if config.SELECTED_OPENAI_MODEL not in config.SELECTABLE_OPENAI_MODEL_LIST:
config.SELECTED_OPENAI_MODEL = config.SELECTABLE_OPENAI_MODEL_LIST[0]
model.setTranslatorOpenAIModel(config.SELECTED_OPENAI_MODEL)
model.updateTranslatorOpenAIClient()
else:
# error update Auth key
auth_keys = config.AUTH_KEYS
auth_keys[engine] = None
config.AUTH_KEYS = auth_keys
printLog("OpenAI API Key is invalid")
case "LMStudio":
printLog("Start check LMStudio API Key")
config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = False
if config.LMSTUDIO_URL is not None:
if model.authenticationTranslatorLMStudio(base_url=config.LMSTUDIO_URL) is True:
config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = True
printLog("LMStudio URL is valid")
config.SELECTABLE_LMSTUDIO_MODEL_LIST = model.getTranslatorLMStudioModelList()
if config.SELECTED_LMSTUDIO_MODEL not in config.SELECTABLE_LMSTUDIO_MODEL_LIST:
config.SELECTED_LMSTUDIO_MODEL = config.SELECTABLE_LMSTUDIO_MODEL_LIST[0]
model.setTranslatorLMStudioModel(config.SELECTED_LMSTUDIO_MODEL)
model.updateTranslatorLMStudioClient()
else:
printLog("LMStudio is not available")
case "Ollama":
printLog("Start check Ollama API Key")
config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = False
if model.authenticationTranslatorOllama() is True:
config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = True
printLog("Ollama is available")
config.SELECTABLE_OLLAMA_MODEL_LIST = model.getTranslatorOllamaModelList()
if config.SELECTED_OLLAMA_MODEL not in config.SELECTABLE_OLLAMA_MODEL_LIST:
config.SELECTED_OLLAMA_MODEL = config.SELECTABLE_OLLAMA_MODEL_LIST[0]
model.setTranslatorOllamaModel(config.SELECTED_OLLAMA_MODEL)
model.updateTranslatorOllamaClient()
else:
printLog("Ollama is not available")
case _: case _:
if connected_network is True: if connected_network is True:
config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = True config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = True

View File

@@ -390,3 +390,60 @@ config.saveConfig("ENABLE_TRANSLATION", True, immediate_save=True)
- 外部入力値の検証 - 外部入力値の検証
- APIキー等の機密情報の適切な取り扱い - APIキー等の機密情報の適切な取り扱い
- パスインジェクション攻撃の防止 - パスインジェクション攻撃の防止
## 最近の更新 (2025-10-20)
### LMStudio / Ollama ローカル LLM 設定プロパティ追加
- `LMSTUDIO_URL` / `SELECTABLE_LMSTUDIO_MODEL_LIST` / `SELECTED_LMSTUDIO_MODEL`
- `SELECTABLE_OLLAMA_MODEL_LIST` / `SELECTED_OLLAMA_MODEL`
ローカル推論エンジン接続用 URL と動的モデルリスト取得・選択プロパティを追加。認証は不要で接続テスト後自動でモデルリストを更新。
### モデル選択プロパティ名称統一
Plamo / Gemini / OpenAI の選択モデルを `PLAMO_MODEL` / `GEMINI_MODEL` / `OPENAI_MODEL` から `SELECTED_PLAMO_MODEL` / `SELECTED_GEMINI_MODEL` / `SELECTED_OPENAI_MODEL` へ統一。設定 JSON の保存キーも `SELECTED_*` に変更し UI との整合性を確保。
### CTranslate2 言語マッピング構造変更対応
`translation_lang` 内の CTranslate2 言語辞書が `translation_lang["CTranslate2"][weight_type]["source"|"target"]` へネスト化。`CTRANSLATE2_WEIGHT_TYPE` プロパティがアクセスのキーとなるため、重みタイプ変更時に翻訳エンジン再初期化が必要。
### YAML 言語外部定義導入
`loadTranslationLanguages()` を初期化時に呼び出し、`models/translation/languages/languages.yml` を読み込んで既存マッピングへマージ。失敗時は空辞書フォールバック。動的言語追加がコード改修無しで可能になったため設定初期化の失敗ログ確認が重要。
### OpenAI モデルリスト自動更新
`setOpenAIAuthKey()` 成功後に `SELECTABLE_OPENAI_MODEL_LIST` を取得し未選択の場合は先頭を自動選択。Gemini / Plamo / LMStudio / Ollama も同様に認証/接続確立時にリスト更新と未選択モデル補完。
### フォント設定のパッケージ対応
`overlay_image.py` で PyInstaller ビルド環境(`_internal/fonts/`)検出を追加。開発環境とバンドル後でフォント探索パスが異なるため、`FONT_FAMILY` はファイル名基準のまま変更無し。
### 依存関係追加
- `PyYAML`: 言語マッピング YAML 読み込み
- `google-genai`: Gemini 連携
- `grpcio`: OpenAI 連携(ストリーミング等)
### VRAM エラー時の自動フォールバック
翻訳有効化や翻訳実行時に VRAM 不足検出で `ENABLE_TRANSLATION` を False にし CTranslate2 へ強制切替。設定値は保持されるが UI には無効化状態を通知。再度有効化要求時に重いモデル再初期化を試行。
### テスト関連
包括的翻訳ペアテストにより `SELECTED_*` モデルと言語マッピング組合せを大量実行。設定値の変更頻度増加に伴いデバウンス 2 秒でファイル書き込み負荷を抑制。
### 影響まとめ
| 項目 | 内容 |
|------|------|
| ローカルLLM | LMStudio / Ollama の導入でオフライン翻訳拡張 |
| プロパティ統一 | SELECTED_* 命名で一貫性・ドキュメント整備性向上 |
| 言語ネスト化 | CTranslate2 重みタイプ切替処理の再初期化必要性増加 |
| YAML外部化 | 言語追加が設定初期化のみで反映可能 |
| モデルリスト自動更新 | 認証後の選択ミス防止・初回 UX 改善 |
| フォント探索 | PyInstaller ビルド後でも同一コードで動作 |
| 依存追加 | 新機能対応で環境構築ステップ増加 |
| VRAM検知 | 安全停止と軽量エンジン切替で安定性向上 |
| テスト増強 | 大量ペア検証で言語/モデル設定の信頼性向上 |

View File

@@ -4,25 +4,88 @@
VRCTアプリケーションのビジネスロジックを制御するコントローラークラスです。UI層とモデル層の間に位置し、ユーザーの入力を適切な処理に変換し、結果を UI に返す役割を担います。全ての機能制御、設定管理、状態管理を一元的に行います。 VRCTアプリケーションのビジネスロジックを制御するコントローラークラスです。UI層とモデル層の間に位置し、ユーザーの入力を適切な処理に変換し、結果を UI に返す役割を担います。全ての機能制御、設定管理、状態管理を一元的に行います。
## 最近の更新 (2025-10-20)
### 新規ローカルLLM翻訳エンジン統合
- LMStudio / Ollama への接続確認エンドポイント追加: `/run/lmstudio_connection`, `/run/ollama_connection`
- LMStudio URL 設定: `/get|set/data/lmstudio_url`
- モデルリスト取得と選択: `/get/data/*_model_list`, `/get|set/data/*_model` (lmstudio / ollama / plamo / gemini / openai)
- 認証・接続成功時に `selectable_*_model_list` / `selected_*_model` を run 経由で通知 (例: `/run/selectable_lmstudio_model_list`, `/run/selected_lmstudio_model`)
### モデルリスト自動更新フロー
- Plamo / Gemini / OpenAI 認証後に動的に最新モデルリストを取得し未選択時は先頭モデルへ自動設定
- LMStudio / Ollama は接続成功時にローカル列挙したモデルを即座に選択候補へ反映
### VRAMエラー検出と自動フォールバック
- 翻訳処理中の `CUDA out of memory` / `CUBLAS_STATUS_ALLOC_FAILED` などを検出し `/run/error_translation_*_vram_overflow` で通知
- 自動で翻訳機能を無効化し CTranslate2 へフォールバック、再度有効化試行時も VRAM エラー検出で安全に解除
### CTranslate2 ウェイト / Whisper ウェイト管理
- ダウンロード進捗/完了/エラー用 run エンドポイント: `download_progress_ctranslate2_weight`, `downloaded_ctranslate2_weight`, `error_ctranslate2_weight` / Whisper も同様
- `SELECTABLE_CTRANSLATE2_WEIGHT_TYPE_DICT` / `SELECTABLE_WHISPER_WEIGHT_TYPE_DICT` の値更新を完了時に反映
### 命名・構造整理
- 翻訳エンジン選択はタブ別 `SELECTED_TRANSLATION_ENGINES[tab_no]` を使用
- 言語選択構造: `SELECTED_YOUR_LANGUAGES[tab_no]`, `SELECTED_TARGET_LANGUAGES[tab_no]` のネストを前提にエンジン適合性判定
- CTranslate2 の言語マッピングはウェイト種別 (`CTRANSLATE2_WEIGHT_TYPE`) でネストされた構造に対応
### 言語マッピング外部化
- `getListLanguageAndCountry()` が YAML からロード済み `translation_lang` / `transcription_lang` を統合して互換言語のみ抽出
### APIキー検証の厳格化
- Plamo API: キー長判定を「==72」から「>=72」へ変更し 72 文字以上を許容
- Gemini API: 最小キー長を 20 → 39 へ引き上げ
- OpenAI API: "sk-" 接頭辞必須かつ長さ 164 文字以上の厳格化、エラーメッセージを「無効」に統一
### 翻訳モデル選択時の適用確実化
- OpenAI / Plamo / Gemini / LMStudio / Ollama でモデル設定後に `setTranslatorXModel()``updateTranslatorXClient()` を必ず呼び出してクライアント状態を確実反映
- デフォルトモデル自動選択時もモデル適用を即座実行
### デバイス自動選択改善
- マイク/スピーカー自動選択機能で更新前後に適切な停止/再開コールバックをチェーン設定 (energy チェック再起動含む)
### 影響
| 項目 | 内容 |
|------|------|
| 翻訳柔軟性 | ローカルLLM (LMStudio/Ollama) によりネットワーク不要運用が可能 |
| 回復性 | VRAMエラー時自動フォールバックで異常終了を防止 |
| 拡張性 | モデルリスト自動更新により新モデル追加時の手動変更不要化 |
| 一貫性 | 統一された SELECTED_/SELECTABLE_ 命名とタブ別管理で UI 連携が簡易化 |
| 可読性 | 外部 YAML 言語定義によりコード側のハードコード削減 |
## 主要機能 ## 主要機能
### 機能制御 ### 機能制御
- 翻訳機能の有効化・無効化 - 翻訳機能の有効化・無効化
- 音声認識機能の制御 - 音声認識機能の制御
- VRオーバーレイの管理 - VRオーバーレイの管理
- WebSocketサーバーの制御 - WebSocketサーバーの制御
### 設定管理 ### 設定管理
- アプリケーション設定の取得・更新 - アプリケーション設定の取得・更新
- デバイス設定の管理 - デバイス設定の管理
- 言語・エンジン設定の制御 - 言語・エンジン設定の制御
### 状態管理 ### 状態管理
- システム状態の監視 - システム状態の監視
- エラー状態の管理 - エラー状態の管理
- 初期化プロセスの制御 - 初期化プロセスの制御
### 通信制御 ### 通信制御
- OSC通信の管理 - OSC通信の管理
- WebSocket通信の制御 - WebSocket通信の制御
- 外部アプリケーション連携 - 外部アプリケーション連携
@@ -30,6 +93,7 @@ VRCTアプリケーションのビジネスロジックを制御するコント
## クラス構造 ## クラス構造
### Controller クラス ### Controller クラス
```python ```python
class Controller: class Controller:
def __init__(self) -> None def __init__(self) -> None
@@ -40,19 +104,23 @@ class Controller:
### 内部ヘルパークラス ### 内部ヘルパークラス
#### DownloadCTranslate2 クラス #### DownloadCTranslate2 クラス
```python ```python
class DownloadCTranslate2: class DownloadCTranslate2:
def progressBar(self, progress) -> None def progressBar(self, progress) -> None
def downloaded(self) -> None def downloaded(self) -> None
``` ```
- 翻訳モデルのダウンロード進捗管理 - 翻訳モデルのダウンロード進捗管理
#### DownloadWhisper クラス #### DownloadWhisper クラス
```python ```python
class DownloadWhisper: class DownloadWhisper:
def progressBar(self, progress) -> None def progressBar(self, progress) -> None
def downloaded(self) -> None def downloaded(self) -> None
``` ```
- 音声認識モデルのダウンロード進捗管理 - 音声認識モデルのダウンロード進捗管理
## 主要メソッド ## 主要メソッド
@@ -62,6 +130,7 @@ class DownloadWhisper:
```python ```python
init() -> None init() -> None
``` ```
- コントローラーの初期化 - コントローラーの初期化
- 各コンポーネントの起動 - 各コンポーネントの起動
- 初期設定の適用 - 初期設定の適用
@@ -71,6 +140,7 @@ setInitMapping(init_mapping: dict) -> None
setRunMapping(run_mapping: dict) -> None setRunMapping(run_mapping: dict) -> None
setRun(run: Callable) -> None setRun(run: Callable) -> None
``` ```
- エンドポイント・コールバック設定 - エンドポイント・コールバック設定
### 翻訳機能制御 ### 翻訳機能制御
@@ -79,23 +149,27 @@ setRun(run: Callable) -> None
setEnableTranslation(data) -> dict setEnableTranslation(data) -> dict
setDisableTranslation(data) -> dict setDisableTranslation(data) -> dict
``` ```
- 翻訳機能の有効化・無効化 - 翻訳機能の有効化・無効化
```python ```python
setSelectedTranslationEngines(data) -> dict setSelectedTranslationEngines(data) -> dict
getSelectedTranslationEngines(data) -> dict getSelectedTranslationEngines(data) -> dict
``` ```
- 翻訳エンジンの選択・取得 - 翻訳エンジンの選択・取得
```python ```python
setSelectedYourLanguages(data) -> dict setSelectedYourLanguages(data) -> dict
setSelectedTargetLanguages(data) -> dict setSelectedTargetLanguages(data) -> dict
``` ```
- 送信・受信言語の設定 - 送信・受信言語の設定
```python ```python
sendMessageBox(data) -> dict sendMessageBox(data) -> dict
``` ```
- メッセージの翻訳・送信処理 - メッセージの翻訳・送信処理
### 音声認識機能制御 ### 音声認識機能制御
@@ -104,24 +178,28 @@ sendMessageBox(data) -> dict
setEnableTranscriptionSend(data) -> dict setEnableTranscriptionSend(data) -> dict
setEnableTranscriptionReceive(data) -> dict setEnableTranscriptionReceive(data) -> dict
``` ```
- 音声認識機能の有効化 - 音声認識機能の有効化
```python ```python
setSelectedTranscriptionEngine(data) -> dict setSelectedTranscriptionEngine(data) -> dict
getSelectedTranscriptionEngine(data) -> dict getSelectedTranscriptionEngine(data) -> dict
``` ```
- 音声認識エンジンの選択・取得 - 音声認識エンジンの選択・取得
```python ```python
setSelectedMicDevice(data) -> dict setSelectedMicDevice(data) -> dict
setSelectedSpeakerDevice(data) -> dict setSelectedSpeakerDevice(data) -> dict
``` ```
- 音声デバイスの選択 - 音声デバイスの選択
```python ```python
setMicThreshold(data) -> dict setMicThreshold(data) -> dict
setSpeakerThreshold(data) -> dict setSpeakerThreshold(data) -> dict
``` ```
- 音声しきい値の設定 - 音声しきい値の設定
### VRオーバーレイ制御 ### VRオーバーレイ制御
@@ -130,12 +208,14 @@ setSpeakerThreshold(data) -> dict
setEnableOverlaySmallLog(data) -> dict setEnableOverlaySmallLog(data) -> dict
setEnableOverlayLargeLog(data) -> dict setEnableOverlayLargeLog(data) -> dict
``` ```
- VRオーバーレイの有効化 - VRオーバーレイの有効化
```python ```python
setOverlaySmallLogSettings(data) -> dict setOverlaySmallLogSettings(data) -> dict
setOverlayLargeLogSettings(data) -> dict setOverlayLargeLogSettings(data) -> dict
``` ```
- オーバーレイ設定の更新 - オーバーレイ設定の更新
### WebSocket制御 ### WebSocket制御
@@ -144,12 +224,14 @@ setOverlayLargeLogSettings(data) -> dict
setEnableWebSocketServer(data) -> dict setEnableWebSocketServer(data) -> dict
setDisableWebSocketServer(data) -> dict setDisableWebSocketServer(data) -> dict
``` ```
- WebSocketサーバーの制御 - WebSocketサーバーの制御
```python ```python
setWebSocketHost(data) -> dict setWebSocketHost(data) -> dict
setWebSocketPort(data) -> dict setWebSocketPort(data) -> dict
``` ```
- WebSocket接続設定 - WebSocket接続設定
### システム管理 ### システム管理
@@ -158,17 +240,20 @@ setWebSocketPort(data) -> dict
updateSoftware(data) -> dict updateSoftware(data) -> dict
updateCudaSoftware(data) -> dict updateCudaSoftware(data) -> dict
``` ```
- ソフトウェアアップデート - ソフトウェアアップデート
```python ```python
downloadCtranslate2Weight(data) -> dict downloadCtranslate2Weight(data) -> dict
downloadWhisperWeight(data) -> dict downloadWhisperWeight(data) -> dict
``` ```
- AIモデルのダウンロード - AIモデルのダウンロード
```python ```python
feedWatchdog(data) -> dict feedWatchdog(data) -> dict
``` ```
- ウォッチドッグの生存シグナル送信 - ウォッチドッグの生存シグナル送信
## 使用方法 ## 使用方法
@@ -233,6 +318,7 @@ result = controller.setEnableTranscriptionSend(None)
``` ```
### 成功レスポンス例 ### 成功レスポンス例
```python ```python
{ {
"status": 200, "status": 200,
@@ -241,6 +327,7 @@ result = controller.setEnableTranscriptionSend(None)
``` ```
### エラーレスポンス例 ### エラーレスポンス例
```python ```python
{ {
"status": 400, "status": 400,
@@ -248,19 +335,22 @@ result = controller.setEnableTranscriptionSend(None)
} }
``` ```
## 状態管理 ## 詳細状態管理
### システム状態 ### システム状態
- 各機能の有効・無効状態 - 各機能の有効・無効状態
- デバイスの接続状態 - デバイスの接続状態
- ネットワーク接続状態 - ネットワーク接続状態
### エラー状態 ### エラー状態
- デバイスエラー - デバイスエラー
- 翻訳エンジンエラー - 翻訳エンジンエラー
- VRAMオーバーフローエラー - VRAMオーバーフローエラー
### 初期化状態 ### 初期化状態
- 段階的な初期化プロセス - 段階的な初期化プロセス
- 依存関係の解決状態 - 依存関係の解決状態
@@ -271,32 +361,38 @@ result = controller.setEnableTranscriptionSend(None)
```python ```python
micMessage(result: dict) -> None micMessage(result: dict) -> None
``` ```
- マイク音声認識結果の処理 - マイク音声認識結果の処理
- 翻訳・フィルタリング・送信 - 翻訳・フィルタリング・送信
```python ```python
speakerMessage(result: dict) -> None speakerMessage(result: dict) -> None
``` ```
- スピーカー音声認識結果の処理 - スピーカー音声認識結果の処理
### ダウンロードイベント ### ダウンロードイベント
- 進捗通知 - 進捗通知
- 完了通知 - 完了通知
- エラー通知 - エラー通知
### デバイス変更イベント ### デバイス変更イベント
- マイク・スピーカーの選択変更 - マイク・スピーカーの選択変更
- 計算デバイスの変更 - 計算デバイスの変更
## 依存関係 ## 依存関係
### 直接依存 ### 直接依存
- `config`: 設定管理 - `config`: 設定管理
- `model`: コアモデル機能 - `model`: コアモデル機能
- `device_manager`: デバイス管理 - `device_manager`: デバイス管理
- `utils`: ユーティリティ機能 - `utils`: ユーティリティ機能
### 間接依存 ### 間接依存
- 各種モデルモジュール(翻訳、音声認識等) - 各種モデルモジュール(翻訳、音声認識等)
- VRオーバーレイモジュール - VRオーバーレイモジュール
- 通信モジュール - 通信モジュール
@@ -304,32 +400,39 @@ speakerMessage(result: dict) -> None
## エラーハンドリング ## エラーハンドリング
### VRAM不足エラー ### VRAM不足エラー
- 自動的にCTranslate2への切り替え - 自動的にCTranslate2への切り替え
- ユーザーへの適切な通知 - ユーザーへの適切な通知
### デバイスエラー ### デバイスエラー
- デバイス接続状態の監視 - デバイス接続状態の監視
- 自動復旧機能 - 自動復旧機能
### ネットワークエラー ### ネットワークエラー
- 接続状態の定期確認 - 接続状態の定期確認
- オフライン機能への切り替え - オフライン機能への切り替え
### 設定エラー ### 設定エラー
- 設定値の妥当性チェック - 設定値の妥当性チェック
- デフォルト値への復帰 - デフォルト値への復帰
## パフォーマンス考慮事項 ## パフォーマンス考慮事項
### 遅延初期化 ### 遅延初期化
- 必要な時点での機能初期化 - 必要な時点での機能初期化
- メモリ使用量の最適化 - メモリ使用量の最適化
### 非同期処理 ### 非同期処理
- バックグラウンドでの重い処理 - バックグラウンドでの重い処理
- UI の応答性維持 - UI の応答性維持
### キャッシュ機能 ### キャッシュ機能
- 設定値のキャッシュ - 設定値のキャッシュ
- 翻訳結果のキャッシュ - 翻訳結果のキャッシュ

View File

@@ -4,19 +4,61 @@
VRCTアプリケーションのメインイベントループを管理するモジュールです。標準入力からのJSONリクエストを処理し、適切なコントローラーメソッドを呼び出してレスポンスを返す、アプリケーションの中枢的な役割を担います。 VRCTアプリケーションのメインイベントループを管理するモジュールです。標準入力からのJSONリクエストを処理し、適切なコントローラーメソッドを呼び出してレスポンスを返す、アプリケーションの中枢的な役割を担います。
## 最近の更新 (2025-10-20)
### 新規エンドポイントと run_mapping 拡張
- VRAM 関連エラー通知エンドポイント追加: `/run/error_translation_chat_vram_overflow` など 5 種類 (翻訳/音声認識送受信別)
- ローカル LLM (LMStudio/Ollama) モデルリスト通知: `/run/selectable_lmstudio_model_list`, `/run/selectable_ollama_model_list` と選択モデル `/run/selected_*_model`
- 従来の Plamo/Gemini/OpenAI モデル取得通知と形式統一
- LMStudio/Ollama 接続確認エンドポイント: `/get/data/lmstudio_connection`, `/get/data/ollama_connection``/run/lmstudio_connection`, `/run/ollama_connection` に移動して非同期通知統一
- 文字変換エンドポイント追加: `/set/data/convert_message_to_romaji`, `/set/data/convert_message_to_hiragana`, `/set/enable/convert_message_to_romaji`, `/set/enable/convert_message_to_hiragana`, `/set/disable/convert_message_to_romaji`, `/set/disable/convert_message_to_hiragana` で音訳機能制御
### エンドポイントロックキー正規化
- `/set/enable/*` `/set/disable/*` の競合を `/lock/set/<name>` に正規化し排他制御強化
- ロック取得失敗時は再キュー投入し軽量リトライでデッドロック防止
### 並列ワーカー処理の安定化
- ハンドラ処理後に短い `sleep(0.2)` により大量高速連続要求時のスレッド飢餓を緩和
- 423 (Locked) ステータス時に指数的ではなく固定短期リトライ採用で応答時間予測性向上
### VRAM エラーフォールバック連携
- Controller が VRAM 検出し翻訳 OFF / CTranslate2 フォールバック後、run_mapping 経由で UI へ状態反映
- ハンドラはエラー時でもスレッド継続し `Internal error` を 500 応答で返しつつログ出力
### モデルリスト動的更新通知
- 認証・接続成功後に対象モデルリスト/選択モデルを run で逐次通知 (Plamo/Gemini/OpenAI/LMStudio/Ollama)
### 影響
| 項目 | 内容 |
|------|------|
| 安定性 | 排他制御と再キュー投入で競合時の落ち込み回避 |
| 可観測性 | VRAM/ダウンロード進捗/モデル更新イベントを run 経由で即時通知 |
| 拡張性 | 新規ローカル LLM エンジン追加に伴う汎用モデル通知フォーマット統一 |
| 応答予測性 | 固定リトライ戦略で待ち時間が読みやすい |
| フォールバック | VRAM エラー時の自動翻訳停止と CTranslate2 への切替連携 |
## 主要機能 ## 主要機能
### リクエスト処理システム ### リクエスト処理システム
- JSON形式の標準入力からのリクエスト受信 - JSON形式の標準入力からのリクエスト受信
- エンドポイントベースのルーティング - エンドポイントベースのルーティング
- 非同期・並列処理対応 - 非同期・並列処理対応
### エンドポイント管理 ### エンドポイント管理
- RESTライクなエンドポイント構造 - RESTライクなエンドポイント構造
- 機能別のエンドポイント分類 - 機能別のエンドポイント分類
- 排他制御によるスレッドセーフティ - 排他制御によるスレッドセーフティ
### 初期化システム ### 初期化システム
- アプリケーション設定の初期化 - アプリケーション設定の初期化
- コンポーネント間の依存関係解決 - コンポーネント間の依存関係解決
- 段階的な機能有効化 - 段階的な機能有効化
@@ -24,6 +66,7 @@ VRCTアプリケーションのメインイベントループを管理するモ
## クラス構造 ## クラス構造
### Main クラス ### Main クラス
```python ```python
class Main: class Main:
def __init__(self, controller_instance: Controller, mapping_data: dict, worker_count: int = 3) def __init__(self, controller_instance: Controller, mapping_data: dict, worker_count: int = 3)
@@ -36,46 +79,54 @@ class Main:
## エンドポイント分類 ## エンドポイント分類
### 機能制御系 ### 機能制御系
```
```text
/set/enable/* - 各機能の有効化 /set/enable/* - 各機能の有効化
/set/disable/* - 各機能の無効化 /set/disable/* - 各機能の無効化
``` ```
### データ操作系 ### データ操作系
```
```text
/get/data/* - 設定データの取得 /get/data/* - 設定データの取得
/set/data/* - 設定データの更新 /set/data/* - 設定データの更新
/delete/data/* - データの削除 /delete/data/* - データの削除
``` ```
### 実行系 ### 実行系
```
```text
/run/* - 各種処理の実行 /run/* - 各種処理の実行
``` ```
## 主要エンドポイント ## 主要エンドポイント
### 翻訳機能 ### 翻訳機能
- `/set/enable/translation`: 翻訳機能の有効化 - `/set/enable/translation`: 翻訳機能の有効化
- `/set/disable/translation`: 翻訳機能の無効化 - `/set/disable/translation`: 翻訳機能の無効化
- `/set/data/selected_translation_engines`: 翻訳エンジンの選択 - `/set/data/selected_translation_engines`: 翻訳エンジンの選択
- `/run/send_message_box`: メッセージ送信 - `/run/send_message_box`: メッセージ送信
### 音声認識機能 ### 音声認識機能
- `/set/enable/transcription_send`: 送信音声認識の有効化 - `/set/enable/transcription_send`: 送信音声認識の有効化
- `/set/enable/transcription_receive`: 受信音声認識の有効化 - `/set/enable/transcription_receive`: 受信音声認識の有効化
- `/set/data/selected_transcription_engine`: 音声認識エンジン選択 - `/set/data/selected_transcription_engine`: 音声認識エンジン選択
### VR機能 ### VR機能
- `/set/data/overlay_small_log_settings`: 小型オーバーレイ設定 - `/set/data/overlay_small_log_settings`: 小型オーバーレイ設定
- `/set/data/overlay_large_log_settings`: 大型オーバーレイ設定 - `/set/data/overlay_large_log_settings`: 大型オーバーレイ設定
### WebSocket機能 ### WebSocket機能
- `/set/enable/websocket_server`: WebSocketサーバー有効化 - `/set/enable/websocket_server`: WebSocketサーバー有効化
- `/set/data/websocket_host`: サーバーホスト設定 - `/set/data/websocket_host`: サーバーホスト設定
- `/set/data/websocket_port`: サーバーポート設定 - `/set/data/websocket_port`: サーバーポート設定
### システム管理 ### システム管理
- `/run/update_software`: ソフトウェアアップデート - `/run/update_software`: ソフトウェアアップデート
- `/run/download_ctranslate2_weight`: 翻訳モデルダウンロード - `/run/download_ctranslate2_weight`: 翻訳モデルダウンロード
- `/run/download_whisper_weight`: 音声認識モデルダウンロード - `/run/download_whisper_weight`: 音声認識モデルダウンロード
@@ -87,18 +138,21 @@ class Main:
```python ```python
receiver() -> None receiver() -> None
``` ```
- 標準入力からのJSONリクエスト受信 - 標準入力からのJSONリクエスト受信
- パースエラーの適切な処理 - パースエラーの適切な処理
```python ```python
handleRequest(endpoint: str, data: Any = None) -> tuple handleRequest(endpoint: str, data: Any = None) -> tuple
``` ```
- エンドポイント処理の実行 - エンドポイント処理の実行
- ステータスコードと結果の返却 - ステータスコードと結果の返却
```python ```python
handler() -> None handler() -> None
``` ```
- ワーカースレッドのメイン処理 - ワーカースレッドのメイン処理
- キューからのリクエスト取得・処理 - キューからのリクエスト取得・処理
@@ -107,21 +161,25 @@ handler() -> None
```python ```python
startReceiver() -> None startReceiver() -> None
``` ```
- レシーバースレッドの起動 - レシーバースレッドの起動
```python ```python
startHandler() -> None startHandler() -> None
``` ```
- ハンドラースレッドプールの起動 - ハンドラースレッドプールの起動
```python ```python
start() -> None start() -> None
``` ```
- 全スレッドの起動 - 全スレッドの起動
```python ```python
stop(wait: float = 2.0) -> None stop(wait: float = 2.0) -> None
``` ```
- 全スレッドの安全な停止 - 全スレッドの安全な停止
## 使用方法 ## 使用方法
@@ -164,6 +222,7 @@ result, status = main_instance.handleRequest("/set/enable/translation", None)
## リクエスト形式 ## リクエスト形式
### 入力形式 ### 入力形式
```json ```json
{ {
"endpoint": "string", // 必須:処理対象のエンドポイント "endpoint": "string", // 必須:処理対象のエンドポイント
@@ -172,6 +231,7 @@ result, status = main_instance.handleRequest("/set/enable/translation", None)
``` ```
### 出力形式 ### 出力形式
```json ```json
{ {
"status": 200, // HTTPステータスコード "status": 200, // HTTPステータスコード
@@ -191,11 +251,13 @@ result, status = main_instance.handleRequest("/set/enable/translation", None)
## 排他制御 ## 排他制御
### ロック機能 ### ロック機能
- enable/disableペアは同一ロックキーを共有 - enable/disableペアは同一ロックキーを共有
- 同一機能の同時実行を防止 - 同一機能の同時実行を防止
- デッドロックを回避する設計 - デッドロックを回避する設計
### ロックキー正規化 ### ロックキー正規化
```python ```python
/set/enable/translation -> /lock/set/translation /set/enable/translation -> /lock/set/translation
/set/disable/translation -> /lock/set/translation /set/disable/translation -> /lock/set/translation
@@ -204,32 +266,38 @@ result, status = main_instance.handleRequest("/set/enable/translation", None)
## 初期化プロセス ## 初期化プロセス
### 段階的初期化 ### 段階的初期化
1. コントローラーの初期化 1. コントローラーの初期化
2. デバイスマネージャーの初期化 2. デバイスマネージャーの初期化
3. モデルの初期化 3. モデルの初期化
4. 各機能の段階的有効化 4. 各機能の段階的有効化
### 初期化mapping ### 初期化mapping
- `/get/data/*`エンドポイントから初期化設定を自動抽出 - `/get/data/*`エンドポイントから初期化設定を自動抽出
- システム起動時の設定復元 - システム起動時の設定復元
## ログ機能 ## ログ機能
### プロセスログ ### プロセスログ
- 全リクエスト・レスポンスの記録 - 全リクエスト・レスポンスの記録
- JSON形式での構造化ログ - JSON形式での構造化ログ
### エラーログ ### エラーログ
- 例外の詳細記録 - 例外の詳細記録
- スタックトレースの保存 - スタックトレースの保存
## 依存関係 ## 依存関係
### 直接依存 ### 直接依存
- `controller`: ビジネスロジック制御 - `controller`: ビジネスロジック制御
- `utils`: ユーティリティ機能(ログ、エンコード等) - `utils`: ユーティリティ機能(ログ、エンコード等)
### 間接依存 ### 間接依存
- `config`: 設定管理 - `config`: 設定管理
- `model`: コアモデル機能 - `model`: コアモデル機能
- `device_manager`: デバイス管理 - `device_manager`: デバイス管理
@@ -237,11 +305,13 @@ result, status = main_instance.handleRequest("/set/enable/translation", None)
## 設定項目 ## 設定項目
### ワーカー数 ### ワーカー数
```python ```python
DEFAULT_WORKER_COUNT = 3 # 並列処理スレッド数 DEFAULT_WORKER_COUNT = 3 # 並列処理スレッド数
``` ```
### タイムアウト ### タイムアウト
- キュー待機タイムアウト: 0.5秒 - キュー待機タイムアウト: 0.5秒
- スレッド停止待機: 2.0秒 - スレッド停止待機: 2.0秒
- 処理安定化待機: 0.2秒 - 処理安定化待機: 0.2秒
@@ -256,14 +326,17 @@ DEFAULT_WORKER_COUNT = 3 # 並列処理スレッド数
## パフォーマンス特性 ## パフォーマンス特性
### スループット ### スループット
- 複数ワーカーによる並列処理 - 複数ワーカーによる並列処理
- ンブロッキングI/O - ンブロッキングI/O
### レイテンシ ### レイテンシ
- キューイング遅延の最小化 - キューイング遅延の最小化
- 排他制御による一時的な遅延あり - 排他制御による一時的な遅延あり
### メモリ使用量 ### メモリ使用量
- リクエストキューのサイズ制限なし(要注意) - リクエストキューのサイズ制限なし(要注意)
- スレッドプールによる固定オーバーヘッド - スレッドプールによる固定オーバーヘッド

View File

@@ -4,40 +4,95 @@
VRCTアプリケーションの中核となるModelクラスを定義するモジュールです。音声認識、翻訳、VRオーバーレイ、OSC通信、WebSocketサーバーなどの主要機能を統合管理し、システム全体の動作を制御します。 VRCTアプリケーションの中核となるModelクラスを定義するモジュールです。音声認識、翻訳、VRオーバーレイ、OSC通信、WebSocketサーバーなどの主要機能を統合管理し、システム全体の動作を制御します。
## 最近の更新 (2025-10-20)
### VRAMエラー検出とフォールバック
- `detectVRAMError()` を追加し CUDA メモリ関連メッセージ/独自例外 `VRAM_OUT_OF_MEMORY` を判別
- 翻訳/音声認識実行中に VRAM エラー検出時、Controller 側で翻訳機能を無効化し CTranslate2 へフォールバックする運用を支援
- エラー詳細文字列を UI へ通知するためのメッセージ抽出を標準化
### CTranslate2 言語マッピングネスト対応
- `getListLanguageAndCountry()` / `findTranslationEngines()``translation_lang['CTranslate2'][CTRANSLATE2_WEIGHT_TYPE]['source']` を参照するネスト構造へ更新
- ウェイト種別切替時に対応言語集合が動的に変化しエンジン再判定をトリガー
### ローカル LLM 翻訳エンジン統合
- LMStudio / Ollama 用クライアント初期化・モデルリスト取得メソッド追加: `authenticationTranslatorLMStudio()`, `getTranslatorLMStudioModelList()`, `setTranslatorLMStudioModel()`, `updateTranslatorLMStudioClient()` など
- Ollama も同様のインターフェースで統一 (`getTranslatorOllamaModelList`, `setTranslatorOllamaModel`, `updateTranslatorOllamaClient`)
- Plamo / Gemini / OpenAI と同一フォーマットでモデル選択ロジックを実装し Controller からの呼び出しを簡素化
### トークナイザ・リソース取得安定化
- CTranslate2 トークナイザダウンロード処理を `downloadCTranslate2ModelTokenizer()` で明示化し PyInstaller パス周りの不整合回避
- フォントパス探索は OverlayImage 側へ委譲 (`OverlayImage(config.PATH_LOCAL)`) し Model は生成と更新呼び出しのみ保持
### 翻訳失敗時のフェールセーフ再試行
- `getTranslate()` 内で翻訳失敗(非文字列)時に CTranslate2 をリトライループして安定した結果を返却
- 成功判定フラグを返却し上位層でエンジン制限エラー検出/フォールバックを容易化
### キーワードフィルタ再初期化改善
- `resetKeywordProcessor()` でインスタンス再生成し `addKeywords()` により設定変更後のフィルタ更新即時反映
### WebSocket サーバー管理強化
- 非同期サーバー起動を `asyncio.run` ラッパースレッドで安定化
- ループフラグ `websocket_server_loop` と状態フラグ `websocket_server_alive` を追加し安全な停止処理と存活確認を標準化
### 影響
| 項目 | 内容 |
|------|------|
| 安定性 | VRAM 検出とフェールセーフ再試行で異常終了回避 |
| 拡張性 | ローカル LLM 統合によりネットワーク不要環境対応 |
| 柔軟性 | CTranslate2 ウェイト種別に応じた言語集合動的切替 |
| 保守性 | トークナイザ/フォント取得責務分離で可読性向上 |
| 観測性 | エラー詳細標準化により UI/ログでの診断容易 |
## 主要機能 ## 主要機能
### シングルトンパターン ### シングルトンパターン
- アプリケーション全体で単一のModelインスタンスを保証 - アプリケーション全体で単一のModelインスタンスを保証
- 遅延初期化による軽量なインポート - 遅延初期化による軽量なインポート
### 音声認識機能 ### 音声認識機能
- マイク音声のリアルタイム文字起こし - マイク音声のリアルタイム文字起こし
- スピーカー出力の音声認識 - スピーカー出力の音声認識
- エネルギーレベル監視 - エネルギーレベル監視
- 複数言語対応 - 複数言語対応
### 翻訳機能 ### 翻訳機能
- 複数の翻訳エンジン対応DeepL、Google、CTranslate2等 - 複数の翻訳エンジン対応DeepL、Google、CTranslate2等
- 言語自動検出 - 言語自動検出
- バッチ翻訳処理 - バッチ翻訳処理
### VRオーバーレイ ### VRオーバーレイ
- OpenVR統合 - OpenVR統合
- 小型・大型ログオーバーレイ - 小型・大型ログオーバーレイ
- 動的配置・透明度制御 - 動的配置・透明度制御
### OSC通信 ### OSC通信
- VRChatとのOSC通信 - VRChatとのOSC通信
- タイピング状態の同期 - タイピング状態の同期
- ミュート状態の監視 - ミュート状態の監視
### WebSocketサーバー ### WebSocketサーバー
- 外部アプリケーションとの通信 - 外部アプリケーションとの通信
- リアルタイムメッセージ配信 - リアルタイムメッセージ配信
## クラス構造 ## クラス構造
### threadFnc クラス ### threadFnc クラス
```python ```python
class threadFnc(Thread): class threadFnc(Thread):
def __init__(self, fnc, end_fnc=None, daemon: bool = True, *args, **kwargs) def __init__(self, fnc, end_fnc=None, daemon: bool = True, *args, **kwargs)
@@ -48,6 +103,7 @@ class threadFnc(Thread):
- エラー保護機能 - エラー保護機能
### Model クラス ### Model クラス
```python ```python
class Model: class Model:
def __new__(cls) # シングルトンパターン def __new__(cls) # シングルトンパターン
@@ -62,56 +118,65 @@ class Model:
```python ```python
init() -> None init() -> None
``` ```
- 全コンポーネントの初期化 - 全コンポーネントの初期化
- 重い処理のため明示的に呼び出し - 重い処理のため明示的に呼び出し
```python ```python
ensure_initialized() -> None ensure_initialized() -> None
``` ```
- 必要時の自動初期化 - 必要時の自動初期化
- 安全な遅延初期化 - 安全な遅延初期化
### 翻訳機能 ### 翻訳機能メソッド
```python ```python
getInputTranslate(message, source_language=None) -> Tuple[List[str], List[bool]] getInputTranslate(message, source_language=None) -> Tuple[List[str], List[bool]]
``` ```
- 入力メッセージの多言語翻訳 - 入力メッセージの多言語翻訳
- 成功フラグも同時に返却 - 成功フラグも同時に返却
```python ```python
getOutputTranslate(message, source_language=None) -> Tuple[List[str], List[bool]] getOutputTranslate(message, source_language=None) -> Tuple[List[str], List[bool]]
``` ```
- 出力メッセージの翻訳(逆方向) - 出力メッセージの翻訳(逆方向)
```python ```python
authenticationTranslatorDeepLAuthKey(auth_key) -> bool authenticationTranslatorDeepLAuthKey(auth_key) -> bool
``` ```
- DeepL APIキーの認証 - DeepL APIキーの認証
### 音声認識機能 ### 音声認識機能メソッド
```python ```python
startMicTranscript(fnc: Callable) -> None startMicTranscript(fnc: Callable) -> None
``` ```
- マイク音声認識の開始 - マイク音声認識の開始
- コールバック関数で結果を通知 - コールバック関数で結果を通知
```python ```python
startSpeakerTranscript(fnc: Callable) -> None startSpeakerTranscript(fnc: Callable) -> None
``` ```
- スピーカー音声認識の開始 - スピーカー音声認識の開始
```python ```python
pauseMicTranscript() -> None pauseMicTranscript() -> None
resumeMicTranscript() -> None resumeMicTranscript() -> None
``` ```
- 音声認識の一時停止・再開 - 音声認識の一時停止・再開
```python ```python
startCheckMicEnergy(fnc: Callable) -> None startCheckMicEnergy(fnc: Callable) -> None
startCheckSpeakerEnergy(fnc: Callable) -> None startCheckSpeakerEnergy(fnc: Callable) -> None
``` ```
- 音声エネルギーレベルの監視 - 音声エネルギーレベルの監視
### VRオーバーレイ機能 ### VRオーバーレイ機能
@@ -119,17 +184,20 @@ startCheckSpeakerEnergy(fnc: Callable) -> None
```python ```python
createOverlayImageSmallLog(message, your_language, translation, target_language) -> Image createOverlayImageSmallLog(message, your_language, translation, target_language) -> Image
``` ```
- 小型ログオーバーレイ画像の生成 - 小型ログオーバーレイ画像の生成
```python ```python
createOverlayImageLargeLog(message_type, message, your_language, translation, target_language) -> Image createOverlayImageLargeLog(message_type, message, your_language, translation, target_language) -> Image
``` ```
- 大型ログオーバーレイ画像の生成 - 大型ログオーバーレイ画像の生成
```python ```python
updateOverlaySmallLogSettings() -> None updateOverlaySmallLogSettings() -> None
updateOverlayLargeLogSettings() -> None updateOverlayLargeLogSettings() -> None
``` ```
- オーバーレイ設定の更新 - オーバーレイ設定の更新
### OSC通信機能 ### OSC通信機能
@@ -137,17 +205,20 @@ updateOverlayLargeLogSettings() -> None
```python ```python
oscSendMessage(message: str) -> None oscSendMessage(message: str) -> None
``` ```
- VRChatへのメッセージ送信 - VRChatへのメッセージ送信
```python ```python
oscStartSendTyping() -> None oscStartSendTyping() -> None
oscStopSendTyping() -> None oscStopSendTyping() -> None
``` ```
- タイピング状態の通知 - タイピング状態の通知
```python ```python
setMuteSelfStatus() -> None setMuteSelfStatus() -> None
``` ```
- VRChatミュート状態の取得 - VRChatミュート状態の取得
### WebSocket機能 ### WebSocket機能
@@ -155,16 +226,19 @@ setMuteSelfStatus() -> None
```python ```python
startWebSocketServer(host: str, port: int) -> None startWebSocketServer(host: str, port: int) -> None
``` ```
- WebSocketサーバーの起動 - WebSocketサーバーの起動
```python ```python
websocketSendMessage(message_dict: dict) -> bool websocketSendMessage(message_dict: dict) -> bool
``` ```
- 全クライアントへのメッセージ送信 - 全クライアントへのメッセージ送信
```python ```python
checkWebSocketServerAlive() -> bool checkWebSocketServerAlive() -> bool
``` ```
- サーバー稼働状態の確認 - サーバー稼働状態の確認
### ファイルダウンロード機能 ### ファイルダウンロード機能
@@ -172,11 +246,13 @@ checkWebSocketServerAlive() -> bool
```python ```python
downloadCTranslate2ModelWeight(weight_type, callback=None, end_callback=None) downloadCTranslate2ModelWeight(weight_type, callback=None, end_callback=None)
``` ```
- 翻訳モデルのダウンロード - 翻訳モデルのダウンロード
```python ```python
downloadWhisperModelWeight(weight_type, callback=None, end_callback=None) downloadWhisperModelWeight(weight_type, callback=None, end_callback=None)
``` ```
- 音声認識モデルのダウンロード - 音声認識モデルのダウンロード
### ウォッチドッグ機能 ### ウォッチドッグ機能
@@ -186,6 +262,7 @@ startWatchdog() -> None
feedWatchdog() -> None feedWatchdog() -> None
setWatchdogCallback(callback: Callable) -> None setWatchdogCallback(callback: Callable) -> None
``` ```
- システム監視とタイムアウト処理 - システム監視とタイムアウト処理
## 使用方法 ## 使用方法
@@ -241,21 +318,25 @@ success = model.websocketSendMessage(message)
## 依存関係 ## 依存関係
### 必須モジュール ### 必須モジュール
- `controller`: アプリケーション制御 - `controller`: アプリケーション制御
- `config`: 設定管理 - `config`: 設定管理
- `device_manager`: デバイス管理 - `device_manager`: デバイス管理
### 音声・翻訳関連 ### 音声・翻訳関連
- `models.transcription.*`: 音声認識 - `models.transcription.*`: 音声認識
- `models.translation.*`: 翻訳機能 - `models.translation.*`: 翻訳機能
- `models.transliteration.*`: 音写変換 - `models.transliteration.*`: 音写変換
### VR・通信関連 ### VR・通信関連
- `models.overlay.*`: VRオーバーレイ - `models.overlay.*`: VRオーバーレイ
- `models.osc.*`: OSC通信 - `models.osc.*`: OSC通信
- `models.websocket.*`: WebSocket通信 - `models.websocket.*`: WebSocket通信
### ユーティリティ ### ユーティリティ
- `models.watchdog.*`: 監視機能 - `models.watchdog.*`: 監視機能
- `utils`: 共通ユーティリティ - `utils`: 共通ユーティリティ
- `flashtext`: キーワードフィルタリング - `flashtext`: キーワードフィルタリング

View File

@@ -745,6 +745,27 @@ platform_limitations = {
- `model.py`: オーバーレイ機能統合 - `model.py`: オーバーレイ機能統合
- `utils.py`: エラーログ・ユーティリティ - `utils.py`: エラーログ・ユーティリティ
## 最近の更新 (2025-10-20)
### フォント探索仕様の強化
`overlay_image.py` に PyInstaller ビルド後の `_internal/fonts/` ディレクトリ検出ロジックを追加。以下の優先順位でフォントディレクトリを探索:
1. `root_path/_internal/fonts/` (PyInstallerバンドル環境)
2. `src-python/models/overlay/fonts/` (開発環境相対パス)
3. `models/overlay/fonts/` (直接実行時)
見つからない場合は `FileNotFoundError` で早期通知。これにより配布バイナリと開発環境で同一コードパスを維持。
### 影響
| 項目 | 内容 |
|------|------|
| PyInstaller対応 | バンドル後のフォント読み込み失敗を防止 |
| 移植性 | 環境差異をコード内条件分岐で吸収 |
| エラー検知 | フォント未配置時の早期例外で不正描画防止 |
## 将来の改善点 ## 将来の改善点
- よりリッチなUI要素対応 - よりリッチなUI要素対応

View File

@@ -0,0 +1,87 @@
# translation_gemini.py - Gemini 翻訳クライアント
## 概要
Google Gemini / Gemma 系モデルを翻訳用途で利用するためのクライアントラッパー。モデル一覧取得・認証・モデル選択・翻訳実行を統一インターフェースで提供する。
## 最近の更新 (2025-10-20)
- 新規追加: Gemini クライアント統合
- 除外キーワード (`audio`, `image`, `veo`, `tts`, `robotics`, `computer-use`) により非テキスト指向モデルをフィルタ
- `generateContent` をサポートするモデルのみ採用
- YAML (`prompt/translation_gemini.yml`) からシステムプロンプト (`system_prompt`) をロード
### 影響
| 項目 | 内容 |
|------|------|
| 正確性 | 非テキスト特化モデル除外で翻訳品質安定 |
| 保守性 | 明示的フィルタリングロジックで再利用容易 |
| 一貫性 | 他 LLM クライアントとの API 形状統一 |
## 責務
- API Key 認証確認
- Gemini/Gemma 系モデル列挙とフィルタリング
- モデル選択検証と内部保持
- LangChain `ChatGoogleGenerativeAI` インスタンス生成
- システムプロンプトによる翻訳実行
## 公開API (メソッド)
```python
class GeminiClient:
def __init__(root_path: str = None)
def getModelList() -> list[str]
def getAuthKey() -> str | None
def setAuthKey(api_key: str) -> bool
def getModel() -> str | None
def setModel(model: str) -> bool
def updateClient() -> None
def translate(text: str, input_lang: str, output_lang: str) -> str
```
### メソッド詳細
- `setAuthKey`: `_authentication_check` 成功時のみ内部保存
- `getModelList`: フィルタリング適用後ソート
- `setModel`: 取得済みモデル一覧内のみ受理
- `updateClient`: `ChatGoogleGenerativeAI` を再構築
- `translate`: システム + ユーザメッセージ構築→呼び出し→レスポンス正規化
## 使用例
```python
client = GeminiClient()
if client.setAuthKey("GEMINI_API_KEY"):
models = client.getModelList()
if models:
client.setModel(models[0])
client.updateClient()
result = client.translate("こんにちは世界", "Japanese", "English")
print(result)
```
## 依存関係
- `google.genai`: モデル列挙 / 認証
- `langchain_google_genai.ChatGoogleGenerativeAI`: LangChain ラッパー
- `translation_languages.translation_lang`: 対応言語集合
- `translation_utils.loadPromptConfig`: プロンプト YAML ロード
## 注意事項
- 非テキスト向けモデル (画像/音声/ロボティクス等) は除外
- ストリーミング無効 (streaming=False)
- API Key 必須 (未設定時 getModelList 不可)
## 制限事項
- 詳細エラーを包括的に扱わない (上位層でロギング/フォールバック)
- 複雑レスポンス構造は単純文字列へ normalize のみ
## 関連ドキュメント
- `details/translation_translator.md`
- `details/translation_languages.md`

View File

@@ -7,6 +7,7 @@
## 主要機能 ## 主要機能
### 多エンジン対応 ### 多エンジン対応
- DeepL無料版・API版 - DeepL無料版・API版
- Google Translate - Google Translate
- Microsoft TranslatorBing - Microsoft TranslatorBing
@@ -14,6 +15,7 @@
- その他のWeb翻訳サービス - その他のWeb翻訳サービス
### 言語コード統合管理 ### 言語コード統合管理
- 各エンジン固有の言語コード形式を統一 - 各エンジン固有の言語コード形式を統一
- 送信元sourceと送信先target言語の分離管理 - 送信元sourceと送信先target言語の分離管理
- 地域固有言語バリエーションの対応 - 地域固有言語バリエーションの対応
@@ -21,6 +23,7 @@
## データ構造 ## データ構造
### translation_lang ### translation_lang
```python ```python
translation_lang: Dict[str, Dict[str, Dict[str, str]]] = { translation_lang: Dict[str, Dict[str, Dict[str, str]]] = {
"エンジン名": { "エンジン名": {
@@ -52,7 +55,7 @@ translation_lang["DeepL"] = {
} }
``` ```
### DeepL API有料版 ### DeepL API有料版 概要
```python ```python
translation_lang["DeepL_API"] = { translation_lang["DeepL_API"] = {
@@ -73,6 +76,7 @@ translation_lang["DeepL_API"] = {
## 主要対応言語 ## 主要対応言語
### 西欧言語 ### 西欧言語
- **English**: 英語(米国・英国バリエーション) - **English**: 英語(米国・英国バリエーション)
- **German**: ドイツ語 - **German**: ドイツ語
- **French**: フランス語 - **French**: フランス語
@@ -84,6 +88,7 @@ translation_lang["DeepL_API"] = {
- **Norwegian**: ノルウェー語 - **Norwegian**: ノルウェー語
### 東欧・スラブ言語 ### 東欧・スラブ言語
- **Russian**: ロシア語 - **Russian**: ロシア語
- **Polish**: ポーランド語 - **Polish**: ポーランド語
- **Czech**: チェコ語 - **Czech**: チェコ語
@@ -94,6 +99,7 @@ translation_lang["DeepL_API"] = {
- **Slovenian**: スロベニア語 - **Slovenian**: スロベニア語
### アジア言語 ### アジア言語
- **Japanese**: 日本語 - **Japanese**: 日本語
- **Korean**: 韓国語 - **Korean**: 韓国語
- **Chinese Simplified**: 中国語(簡体字) - **Chinese Simplified**: 中国語(簡体字)
@@ -101,6 +107,7 @@ translation_lang["DeepL_API"] = {
- **Indonesian**: インドネシア語 - **Indonesian**: インドネシア語
### その他の言語 ### その他の言語
- **Arabic**: アラビア語 - **Arabic**: アラビア語
- **Turkish**: トルコ語 - **Turkish**: トルコ語
- **Finnish**: フィンランド語 - **Finnish**: フィンランド語
@@ -218,21 +225,25 @@ en_code = manager.get_language_code("DeepL", "English", "target")
## エンジン別特徴 ## エンジン別特徴
### DeepL無料版 ### DeepL無料版
- **強み**: 高精度、自然な翻訳 - **強み**: 高精度、自然な翻訳
- **制限**: 月間使用量制限、API制限 - **制限**: 月間使用量制限、API制限
- **対応**: 26言語 - **対応**: 26言語
### DeepL API有料版 ### DeepL API有料版
- **強み**: DeepLの高精度、地域別言語対応 - **強み**: DeepLの高精度、地域別言語対応
- **制限**: 従量課金 - **制限**: 従量課金
- **対応**: 地域固有言語バリエーション - **対応**: 地域固有言語バリエーション
### Google Translate ### Google Translate
- **強み**: 多言語対応、高速 - **強み**: 多言語対応、高速
- **制限**: API制限、精度のばらつき - **制限**: API制限、精度のばらつき
- **対応**: 100+言語 - **対応**: 100+言語
### Microsoft Translator ### Microsoft Translator
- **強み**: リアルタイム翻訳、音声対応 - **強み**: リアルタイム翻訳、音声対応
- **制限**: APIキー必要 - **制限**: APIキー必要
- **対応**: 70+言語 - **対応**: 70+言語
@@ -240,6 +251,7 @@ en_code = manager.get_language_code("DeepL", "English", "target")
## 地域バリエーション対応 ## 地域バリエーション対応
### 英語の地域別対応 ### 英語の地域別対応
```python ```python
# DeepL APIでの英語バリエーション # DeepL APIでの英語バリエーション
"English American": "en-US", # アメリカ英語 "English American": "en-US", # アメリカ英語
@@ -247,6 +259,7 @@ en_code = manager.get_language_code("DeepL", "English", "target")
``` ```
### ポルトガル語の地域別対応 ### ポルトガル語の地域別対応
```python ```python
# ブラジル・ポルトガル語とヨーロッパ・ポルトガル語 # ブラジル・ポルトガル語とヨーロッパ・ポルトガル語
"Portuguese Brazilian": "pt-BR", "Portuguese Brazilian": "pt-BR",
@@ -254,6 +267,7 @@ en_code = manager.get_language_code("DeepL", "English", "target")
``` ```
### 中国語の文字体系対応 ### 中国語の文字体系対応
```python ```python
# 簡体字・繁体字の区別 # 簡体字・繁体字の区別
"Chinese Simplified": "zh", # 簡体字(中国本土) "Chinese Simplified": "zh", # 簡体字(中国本土)
@@ -263,6 +277,7 @@ en_code = manager.get_language_code("DeepL", "English", "target")
## 拡張性 ## 拡張性
### 新エンジンの追加 ### 新エンジンの追加
```python ```python
# 新しい翻訳エンジンの追加例 # 新しい翻訳エンジンの追加例
translation_lang["NewEngine"] = { translation_lang["NewEngine"] = {
@@ -280,6 +295,7 @@ translation_lang["NewEngine"] = {
``` ```
### 新言語の追加 ### 新言語の追加
```python ```python
# 既存エンジンへの新言語追加 # 既存エンジンへの新言語追加
translation_lang["DeepL"]["source"]["Hindi"] = "hi" translation_lang["DeepL"]["source"]["Hindi"] = "hi"
@@ -289,6 +305,7 @@ translation_lang["DeepL"]["target"]["Hindi"] = "hi"
## エラーハンドリング ## エラーハンドリング
### 安全な言語コード取得 ### 安全な言語コード取得
```python ```python
def safe_get_language_code(engine, language, direction="target", fallback="en"): def safe_get_language_code(engine, language, direction="target", fallback="en"):
"""フォールバック機能付き言語コード取得""" """フォールバック機能付き言語コード取得"""
@@ -303,6 +320,7 @@ def safe_get_language_code(engine, language, direction="target", fallback="en"):
``` ```
### 言語サポート検証 ### 言語サポート検証
```python ```python
def validate_translation_pair(engine, source_lang, target_lang): def validate_translation_pair(engine, source_lang, target_lang):
"""翻訳ペアの有効性検証""" """翻訳ペアの有効性検証"""
@@ -340,3 +358,32 @@ def validate_translation_pair(engine, source_lang, target_lang):
- `transcription_languages.py`: 音声認識言語マッピング - `transcription_languages.py`: 音声認識言語マッピング
- `config.py`: 翻訳言語設定管理 - `config.py`: 翻訳言語設定管理
- `controller.py`: 言語選択UI制御 - `controller.py`: 言語選択UI制御
## 最近の更新 (2025-10-20)
### CTranslate2 言語構造変更
従来: 重みタイプがトップレベルキー (`translation_lang["m2m100_418M-ct2-int8"]`).
現在: `translation_lang["CTranslate2"][weight_type]["source"|"target"]` のネスト構造。`model.findTranslationEngines` / `translation_translator``engine == "CTranslate2"` の場合は `CTRANSLATE2_WEIGHT_TYPE` を用いて内部辞書へアクセス。
### 外部 YAML 言語マッピング導入
`models/translation/languages/languages.yml` を追加し、`config.init_config()` 内で `loadTranslationLanguages(path=config.PATH_LOCAL)` を呼び出し、既存 `translation_lang` にマージ/上書き。読込失敗時は空辞書を返しフォールバック。PyYAML 追加)
### LMStudio / Ollama 翻訳モデル対応準備
新規ローカル LLM 接続用として LMStudio / Ollama 追加。現段階ではモデルリスト・選択用のエンドポイントとプロパティ (`SELECTABLE_LMSTUDIO_MODEL_LIST`, `SELECTED_LMSTUDIO_MODEL`, `SELECTABLE_OLLAMA_MODEL_LIST`, `SELECTED_OLLAMA_MODEL`) を定義。言語マッピングは今後 YAML 拡張で統合予定(未実装部分は翻訳本体の `translate()` に未統合)。
### モデル選択プロパティ名称統一
Plamo / Gemini / OpenAI の選択モデルプロパティを `PLAMO_MODEL` / `GEMINI_MODEL` / `OPENAI_MODEL` から `SELECTED_PLAMO_MODEL` / `SELECTED_GEMINI_MODEL` / `SELECTED_OPENAI_MODEL` へ統一。保存キーも `SELECTED_*` に更新。
### 影響
| 項目 | 内容 |
|------|------|
| CTranslate2 | ネスト化により言語参照コード修正が必要 |
| YAML | 動的言語追加がコード編集無しに可能 |
| LLM接続 | 今後言語マッピングを YAML で拡張予定(未実装) |
| プロパティ | SELECTED_* へ統一で UI/設定整合性向上 |

View File

@@ -0,0 +1,86 @@
# translation_lmstudio.py - LMStudio ローカル LLM 翻訳クライアント
## 概要
LMStudio 互換 OpenAI API を利用したローカル LLM 翻訳クライアントラッパー。モデル一覧取得・モデル選択・翻訳処理を統一インターフェースで提供する。
## 最近の更新 (2025-10-20)
- 新規追加: ローカル LLM (LMStudio) を翻訳エンジン群へ統合
- `getModelList()` により現在起動中インスタンスから利用可能モデルを取得
- `setModel()` 成功時に `updateClient()` を呼ぶことで LangChain `ChatOpenAI` を再構築
- YAML (`prompt/translation_lmstudio.yml`) からシステムプロンプト (`system_prompt`) と対応言語をロード
### 影響
| 項目 | 内容 |
|------|------|
| 拡張性 | ネットワーク不要のローカル推論を利用可能 |
| 一貫性 | 他 API クライアント (OpenAI/Plamo/Gemini/Ollama) と同一メソッド構成 |
| 保守性 | 翻訳ロジックを共通フォーマットへ集約 |
## 責務
- LMStudio エンドポイントへの疎通確認 (認証代替)
- 利用可能モデル一覧の収集とソート
- 選択モデルの検証と内部保持
- LangChain ラッパーインスタンス生成
- システムプロンプトによる指示付き翻訳実行
## 公開API (メソッド)
```python
class LMStudioClient:
def __init__(base_url: str | None = None, root_path: str = None)
def getBaseURL() -> str | None
def setBaseURL(base_url: str | None) -> bool
def getModelList() -> list[str]
def getModel() -> str | None
def setModel(model: str) -> bool
def updateClient() -> None
def translate(text: str, input_lang: str, output_lang: str) -> str
```
### メソッド詳細
- `setBaseURL`: 疎通確認 (_authentication_check) に成功した場合のみ内部更新
- `getModelList`: `OpenAI` クライアントで `/models` を列挙し id を抽出
- `setModel`: `getModelList` 内のモデル名のみ受理
- `updateClient`: `ChatOpenAI` インスタンスを最新モデルで再生成
- `translate`: システム / ユーザメッセージで LLM へ問い合わせし文字列レスポンスを正規化
## 使用例
```python
client = LMStudioClient(base_url="http://localhost:1234/v1")
models = client.getModelList()
if models:
client.setModel(models[0])
client.updateClient()
translated = client.translate("こんにちは世界", "Japanese", "English")
print(translated)
```
## 依存関係
- `openai.OpenAI`: LMStudio OpenAI 互換 API 呼び出し
- `langchain_openai.ChatOpenAI`: LangChain 抽象化
- `translation_languages.translation_lang`: 対応言語集合
- `translation_utils.loadPromptConfig`: プロンプト YAML ロード
## 注意事項
- `api_key` は固定文字列 "lmstudio" (LMStudio 側で不要のため) を利用
- モデル一覧取得はエンドポイントの互換性に依存 (古いバージョン非対応の可能性)
- `updateClient()` 呼び出し前は `translate()` を利用できない
## 制限事項
- ストリーミング未対応 (streaming=False)
- エラーハンドリングは包括的ではなく詳細原因は上位層で処理必要
## 関連ドキュメント
- `details/translation_translator.md`
- `details/translation_languages.md`

View File

@@ -0,0 +1,86 @@
# translation_ollama.py - Ollama ローカル LLM 翻訳クライアント
## 概要
Ollama サーバー上で稼働するローカル LLM を翻訳エンジンとして扱うためのクライアントラッパー。モデル一覧取得・モデル選択・翻訳実行を統一パターンで提供する。
## 最近の更新 (2025-10-20)
- 新規追加: Ollama を翻訳エンジン群へ統合
- `/api/ping` を用いた疎通確認による簡易認証
- `/api/tags` から利用可能モデル一覧抽出・ソート
- YAML (`prompt/translation_ollama.yml`) からシステムプロンプト (`system_prompt`) と対応言語をロード
### 影響
| 項目 | 内容 |
|------|------|
| 拡張性 | LAN 内ローカル推論を利用可能 |
| 可搬性 | GPU/CPU 任意構成の Ollama 環境に適応 |
| 一貫性 | 他翻訳クライアント (OpenAI/Gemini/Plamo/LMStudio) と統一 API |
## 責務
- Ollama インスタンスへの接続確認
- モデル一覧取得 (タグ列挙) とソート
- 選択モデルの検証と内部保持
- LangChain `ChatOllama` インスタンス生成
- システムプロンプトとユーザ入力を組み立てて翻訳実行
## 公開API (メソッド)
```python
class OllamaClient:
def __init__(root_path: str = None)
def authenticationCheck() -> bool
def getModelList() -> list[str]
def getModel() -> str | None
def setModel(model: str) -> bool
def updateClient() -> None
def translate(text: str, input_lang: str, output_lang: str) -> str
```
### メソッド詳細
- `authenticationCheck`: `/api/ping` が 200 を返すかで利用可否判定
- `getModelList`: 認証成功時のみ `/api/tags` 結果から name 抽出
- `setModel`: 取得済みモデル一覧内に存在する場合のみ設定
- `updateClient`: `ChatOllama` を最新モデルで再生成
- `translate`: system / user メッセージを LLM へ送信し結合結果を正規化
## 使用例
```python
client = OllamaClient()
if client.authenticationCheck():
models = client.getModelList()
if models:
client.setModel(models[0])
client.updateClient()
translated = client.translate("こんにちは世界", "Japanese", "English")
print(translated)
```
## 依存関係
- `requests`: Ping/タグ API 呼び出し
- `langchain_ollama.ChatOllama`: LangChain LLM ラッパー
- `translation_languages.translation_lang`: 対応言語集合
- `translation_utils.loadPromptConfig`: プロンプト YAML ロード
## 注意事項
- サーバー既定 URL: `http://localhost:11434`
- モデル一覧取得は起動しているローカルサーバー状態に依存
- `updateClient()` 呼び出し前は `translate()` を利用不可
## 制限事項
- ストリーミング未対応 (streaming=False)
- エラー詳細は包括的に扱わない (上位層でフォールバック)
## 関連ドキュメント
- `details/translation_translator.md`
- `details/translation_languages.md`

View File

@@ -0,0 +1,85 @@
# translation_openai.py - OpenAI 翻訳クライアント
## 概要
OpenAI API (公式または互換エンドポイント) を用いた汎用 LLM 翻訳クライアントラッパー。モデル一覧取得・認証・モデル選択・翻訳実行を提供する。
## 最近の更新 (2025-10-20)
- 除外キーワード (`whisper`, `embedding`, `image`, `tts`, `audio`, `search`, `transcribe`, `diarize`, `vision`) を用いて翻訳非適合モデルをフィルタ
- Fine-tune モデル (`ft:`) は root が `gpt-` で始まる場合採用
- YAML (`prompt/translation_openai.yml`) からシステムプロンプト (`system_prompt`) をロードする構成へ統合
### 影響
| 項目 | 内容 |
|------|------|
| 正確性 | 不適合モデル除外で翻訳品質安定 |
| 保守性 | フィルタリングロジック明示化で再利用容易 |
| 一貫性 | 他翻訳クライアントと API 形状統一 |
## 責務
- OpenAI API Key を用いた認証確認
- 利用可能モデルのフィルタリングとソート
- 選択モデルの検証と内部保持
- LangChain `ChatOpenAI` インスタンス生成
- システムプロンプトによる翻訳実行
## 公開API (メソッド)
```python
class OpenAIClient:
def __init__(base_url: str | None = None, root_path: str = None)
def getModelList() -> list[str]
def getAuthKey() -> str | None
def setAuthKey(api_key: str) -> bool
def getModel() -> str | None
def setModel(model: str) -> bool
def updateClient() -> None
def translate(text: str, input_lang: str, output_lang: str) -> str
```
### メソッド詳細
- `setAuthKey`: `_authentication_check` に成功した場合のみ内部保存
- `getModelList`: モデル列挙後フィルタリング適用しソート
- `setModel`: 取得済みリスト内のモデルのみ受理
- `updateClient`: `ChatOpenAI` を選択モデルで再生成
- `translate`: システム + ユーザメッセージ構築→LLM呼び出し→レスポンス正規化
## 使用例
```python
client = OpenAIClient()
if client.setAuthKey("OPENAI_API_KEY"):
models = client.getModelList()
client.setModel(models[0])
client.updateClient()
result = client.translate("こんにちは世界", "Japanese", "English")
print(result)
```
## 依存関係
- `openai.OpenAI`: モデル列挙 / 推論
- `langchain_openai.ChatOpenAI`: LangChain ラッパー
- `translation_languages.translation_lang`: 対応言語集合
- `translation_utils.loadPromptConfig`: プロンプト YAML ロード
## 注意事項
- `base_url` が None の場合公式エンドポイント
- ストリーミング無効 (streaming=False) 固定
- API Key 無設定時 `getModelList()` は空
## 制限事項
- エラーメッセージ詳細は包括的に扱わない (上位層でロギング)
- 翻訳結果の構造が複雑 (list/dict) 場合を単純文字列へ normalize するのみ
## 関連ドキュメント
- `details/translation_translator.md`
- `details/translation_languages.md`

View File

@@ -0,0 +1,86 @@
# translation_plamo.py - Plamo 翻訳クライアント
## 概要
Preferred Networks 提供の Plamo API を利用した翻訳向け LLM クライアントラッパー。モデル一覧取得・認証・モデル選択・翻訳実行を統一インターフェースで提供する。
## 最近の更新 (2025-10-20)
- 新規追加: Plamo クライアント統合
- プロンプト設定を YAML (`prompt/translation_plamo.yml`) からロード(システムプロンプト `system_prompt`
- モデル一覧取得後ソートして再現性を確保
### 影響
| 項目 | 内容 |
|------|------|
| 拡張性 | 日本発 API への対応により選択肢拡大 |
| 保守性 | 他 LLM クライアントと同一構造でメンテ容易 |
| 一貫性 | メソッド命名/責務の統一化 |
## 責務
- API Key 認証確認
- 利用可能モデルの列挙とソート
- モデル選択の検証
- LangChain `ChatOpenAI` インスタンス生成
- システムプロンプトによる翻訳実行
## 公開API (メソッド)
```python
class PlamoClient:
def __init__(root_path: str = None)
def getModelList() -> list[str]
def getAuthKey() -> str | None
def setAuthKey(api_key: str) -> bool
def getModel() -> str | None
def setModel(model: str) -> bool
def updateClient() -> None
def translate(text: str, input_lang: str, output_lang: str) -> str
```
### メソッド詳細
- `setAuthKey`: `_authentication_check` 成功時のみ内部保存
- `getModelList`: 認証済み状態でモデル列挙→ソート
- `setModel`: 列挙済みリスト内モデルのみ受理
- `updateClient`: `ChatOpenAI` を再構築
- `translate`: システム + ユーザメッセージで推論し応答正規化
## 使用例
```python
client = PlamoClient()
if client.setAuthKey("PLAMO_API_KEY"):
models = client.getModelList()
if models:
client.setModel(models[0])
client.updateClient()
result = client.translate("こんにちは世界", "Japanese", "English")
print(result)
```
## 依存関係
- `openai.OpenAI`: Plamo 互換 API 呼び出し
- `langchain_openai.ChatOpenAI`: LangChain ラッパー
- `translation_languages.translation_lang`: 対応言語集合
- `translation_utils.loadPromptConfig`: プロンプト YAML ロード
## 注意事項
- BASE_URL 固定: `https://api.platform.preferredai.jp/v1`
- API Key 未設定時はモデル一覧取得不可
- ストリーミング無効 (streaming=False)
## 制限事項
- 詳細エラーは包括的に扱わず (上位層でログ/フォールバック)
- 翻訳結果の構造が複雑な場合単純文字列へ normalize のみ
## 関連ドキュメント
- `details/translation_translator.md`
- `details/translation_languages.md`

View File

@@ -404,3 +404,49 @@ root/
- `config.py`: 翻訳設定管理 - `config.py`: 翻訳設定管理
- `model.py`: 翻訳機能統合 - `model.py`: 翻訳機能統合
- `controller.py`: 翻訳制御インターフェース - `controller.py`: 翻訳制御インターフェース
## 最近の更新 (2025-10-20)
### 新規ローカル LLM エンジン追加
LMStudio / Ollama を翻訳エンジンとして追加。接続確認後にモデルリスト (`SELECTABLE_LMSTUDIO_MODEL_LIST` / `SELECTABLE_OLLAMA_MODEL_LIST`) を取得し、未選択なら先頭モデルを自動選択 (`SELECTED_LMSTUDIO_MODEL` / `SELECTED_OLLAMA_MODEL`)。現時点では CTranslate2 と同様にローカル動作を想定し、翻訳関数側は将来の統合(温度等パラメータ)に備えて抽象化維持。
### モデル選択プロパティ名称統一
Plamo / Gemini / OpenAI の選択モデルプロパティを `SELECTED_*` 形式へ変更。旧名称 (`PLAMO_MODEL` / `GEMINI_MODEL` / `OPENAI_MODEL`) は利用停止。自動認証後のモデルリスト更新ロジックで未選択時に先頭補完を行う。
### OpenAI / Gemini / Plamo 認証後のモデルリスト自動更新
Auth設定メソッド完了時に `SELECTABLE_*_MODEL_LIST` を再取得し不足時は UI へ push。OpenAI はキー設定直後に最新モデルリストを反映し高速化。Gemini / Plamo も同様に `updateTranslator*Client()` 呼び出しでクライアント再生成。
### CTranslate2 言語ネスト化対応
`translation_lang["CTranslate2"][weight_type]["source"|"target"]` へ構造変更。`CTRANSLATE2_WEIGHT_TYPE` により重みタイプ別の言語集合を参照。Translator 内では `translator_name == "CTranslate2"` の分岐で weight_type を参照して言語判定を行う実装に変更。
### YAML 言語マッピング導入
外部ファイル `languages.yml` を読み込んで翻訳エンジン別対応言語を動的拡張。新言語追加は YAML 編集のみで実現(コード再デプロイ不要)。読み込み失敗時は空辞書でフォールバックし既存ハードコードを保持。
### VRAM エラー検知とフォールバック
DeepL / Plamo / Gemini / OpenAI 実行時の VRAM 不足検知で自動的に CTranslate2 へ切替し翻訳を停止 (`ENABLE_TRANSLATION=False`)。ユーザー通知後は再度有効化要求時に再初期化を試行。安定性向上のためログへ VRAM エラー詳細を記録。
### トークナイザーパス修正
CTranslate2 トークナイザーのダウンロード処理で保存ディレクトリ作成とパス使用順序不整合を修正。これにより初回起動時の失敗率低下。
### 全言語ペア包括テスト導入
`backend_test.py` にて `test_translate_all_language_pairs()` を追加。複数エンジン・全言語ペアを列挙実行し `translation_test_results.json` を生成。失敗ペアの早期検出と YAML 追加言語検証に活用。
### 影響
| 項目 | 内容 |
|------|------|
| ローカルLLM | オフライン翻訳候補拡充 (LMStudio/Ollama) |
| プロパティ統一 | SELECTED_* 命名で一貫性と保守性向上 |
| CTranslate2構造 | 重みタイプ毎に最適言語集合参照可能 |
| YAML外部化 | 言語追加/削除が設定ファイル編集のみで完結 |
| VRAM検知 | エラー時自動停止 + 軽量エンジン切替で安定性向上 |
| Tokenizer修正 | 初回セットアップ失敗減少 |
| 包括テスト | 言語組合せの網羅的品質担保 |

View File

@@ -1,15 +1,18 @@
# 仕様書 # 仕様書
概要 概要
- プロジェクト名: VRCT (VR Chat Translator) - プロジェクト名: VRCT (VR Chat Translator)
- 目的: マイク入力とスピーカー出力をリアルタイムに文字起こし・翻訳し、VR オーバーレイや OSC/WebSocket 経由で外部に送出するバックエンドロジック。 - 目的: マイク入力とスピーカー出力をリアルタイムに文字起こし・翻訳し、VR オーバーレイや OSC/WebSocket 経由で外部に送出するバックエンドロジック。
- 言語: Python - 言語: Python
対象ユーザー 対象ユーザー
- VR 環境でリアルタイム翻訳・文字起こしを利用したいエンドユーザー - VR 環境でリアルタイム翻訳・文字起こしを利用したいエンドユーザー
- フロントエンドGUIや VR クライアントOSCと連携するアプリケーション開発者 - フロントエンドGUIや VR クライアントOSCと連携するアプリケーション開発者
主要機能(機能要件) 主要機能(機能要件)
1. 音声の取り込み・文字起こし 1. 音声の取り込み・文字起こし
- マイク(送信)およびスピーカー(受信)から音声を取得し、ローカル Whisperfaster-whisperまたは外部サービスによりテキスト化する。 - マイク(送信)およびスピーカー(受信)から音声を取得し、ローカル Whisperfaster-whisperまたは外部サービスによりテキスト化する。
- 音声エネルギー(音量)監視を行い、閾値ベースで検出する。 - 音声エネルギー(音量)監視を行い、閾値ベースで検出する。
@@ -36,23 +39,94 @@
- ウォッチドッグ機構で定期的に死活チェック・コールバック。 - ウォッチドッグ機構で定期的に死活チェック・コールバック。
非機能要件 非機能要件
- プラットフォーム: 主に WindowsAudio 周りは WASAPI を利用)を想定。クロスプラットフォームでの import 安全性を考慮。 - プラットフォーム: 主に WindowsAudio 周りは WASAPI を利用)を想定。クロスプラットフォームでの import 安全性を考慮。
- 可用性: 外部依存PyAudio, CUDA, ctranslate2 等)が無い環境でも安全にインポートでき、機能劣化しつつ動作する。 - 可用性: 外部依存PyAudio, CUDA, ctranslate2 等)が無い環境でも安全にインポートでき、機能劣化しつつ動作する。
- パフォーマンス: ローカルモデル利用時は GPU を利用して計算性能を確保。compute type 選択ロジックを実装。 - パフォーマンス: ローカルモデル利用時は GPU を利用して計算性能を確保。compute type 選択ロジックを実装。
- セキュリティ: 外部への API キーDeepL など)は設定で扱い、コード上では平文保持を避ける(設定ファイルに保存)。 - セキュリティ: 外部への API キーDeepL など)は設定で扱い、コード上では平文保持を避ける(設定ファイルに保存)。
運用フロー 運用フロー
- 起動: stdin でコマンドを受け付ける mainloop を実行。必要な初期化は遅延実行lazy initを採用。 - 起動: stdin でコマンドを受け付ける mainloop を実行。必要な初期化は遅延実行lazy initを採用。
- モデル重ダウンロード: CTranslate2/Whisper 重みは `weights/` 配下にダウンロードし、チェックサム等で整合性確認。 - モデル重ダウンロード: CTranslate2/Whisper 重みは `weights/` 配下にダウンロードし、チェックサム等で整合性確認。
- 障害時: 例外は utils.errorLogging() でトレースを error.log に出力。重要機能はフォールバック実装。 - 障害時: 例外は utils.errorLogging() でトレースを error.log に出力。重要機能はフォールバック実装。
インターフェース(抜粋) インターフェース(抜粋)
- stdin(JSON): {"endpoint": "/set/..." | "/get/..." | "/run/...", "data": <base64(JSON)|any>} - stdin(JSON): {"endpoint": "/set/..." | "/get/..." | "/run/...", "data": <base64(JSON)|any>}
- stdout(JSON): 標準化されたレスポンスを printResponse/printLog が出力status, endpoint, result など)。 - stdout(JSON): 標準化されたレスポンスを printResponse/printLog が出力status, endpoint, result など)。
依存関係(オプション含む) 依存関係(オプション含む)
- 必須(実装時想定): requests, packaging, flashtext, pillow, pyaudiowpatch, speech_recognition - 必須(実装時想定): requests, packaging, flashtext, pillow, pyaudiowpatch, speech_recognition
- ローカル推奨: faster-whisper, ctranslate2, torchGPU 利用時) - ローカル推奨: faster-whisper, ctranslate2, torchGPU 利用時)
- Windows 固有(音声ループバック): pycaw, comtypes - Windows 固有(音声ループバック): pycaw, comtypes
参考: 実装上の安全設計として optional な import は try/except でガードしており、存在しない依存があっても import 時にクラッシュしない。 参考: 実装上の安全設計として optional な import は try/except でガードしており、存在しない依存があっても import 時にクラッシュしない。
## 最近の更新 (2025-10-20 translate_api ブランチ)
本章は既存仕様への差分のみを記載します。コードベースの事実に基づく更新点です。
### 翻訳エンジン / モデル管理の拡張
- OpenAI / Plamo / Gemini の選択モデルプロパティを `PLAMO_MODEL` / `GEMINI_MODEL` / `OPENAI_MODEL` から `SELECTED_PLAMO_MODEL` / `SELECTED_GEMINI_MODEL` / `SELECTED_OPENAI_MODEL` にリネーム (旧名称は保存対象から移行)。
- 新規ローカル LLM 接続: LMStudio (`LMSTUDIO_URL`, `SELECTABLE_LMSTUDIO_MODEL_LIST`, `SELECTED_LMSTUDIO_MODEL`) を追加。
- 新規ローカル LLM 接続: Ollama (`SELECTABLE_OLLAMA_MODEL_LIST`, `SELECTED_OLLAMA_MODEL`) を追加。
- OpenAI 認証キー設定時にモデル一覧 (`SELECTABLE_OPENAI_MODEL_LIST`) を自動取得し、未選択時に第一候補へフォールバックする処理を追加。メソッド名を `OpenAi``OpenAI` に統一。
### エンドポイント追加 / 変更
`mainloop.py``mapping` / `run_mapping` に以下を追加:
- `/get/data/lmstudio_model_list`, `/get/data/lmstudio_model`, `/set/data/lmstudio_model`, `/get/data/lmstudio_url`, `/set/data/lmstudio_url`
- `/get/data/ollama_connection`, `/get/data/ollama_model_list`, `/get/data/ollama_model`, `/set/data/ollama_model`
- OpenAI 系: `getOpenAIAuthKey`, `setOpenAIAuthKey`, `delOpenAIAuthKey`, `getOpenAIModelList`, `getOpenAIModel`, `setOpenAIModel` に名称統一。
### 翻訳言語定義の構造変更
- CTranslate2 の言語定義をトップレベル直接キー (例: `m2m100_418M-ct2-int8`) から `translation_lang['CTranslate2'][weight_type]` のネスト構造へ再編。利用側の互換ロジック (`model.findTranslationEngines`) は weight_type 経由で参照するよう修正。
- 新規 YAML 言語マッピングファイル `models/translation/languages/languages.yml` を追加。`config.init_config()` 内で `loadTranslationLanguages()` を呼び出し統合 (失敗時は空辞書フォールバック)。
### プロンプトファイルの整理
- `translation_gemini.yml`, `translation_lmstudio.yml` から `supported_languages` ブロックを削除し、`system_prompt` 内に簡潔化。
### リソース / PyInstaller
- PyInstaller spec (`backend.spec`, `backend_cuda.spec`) の `datas``./src-python/models/translation/prompt` および `./src-python/models/translation/languages` を追加。
- フォント配置を `fonts/` 直下から `src-python/models/overlay/fonts/` へ移動し、`overlay_image.py` にビルド時 (`_internal/fonts`) と開発時の動的探索ロジックを追加。
### 依存パッケージの追加
- `requirements.txt` / `requirements_cuda.txt``PyYAML==6.0.2` (YAML読込), `google-genai==1.45.0`, `grpcio==1.67.1` を追加。
### 認証処理の微調整
- Plamo / Gemini 認証メソッドで `root_path=config.PATH_LOCAL` を渡すよう変更し、ローカル参照を統一。
- OpenAI モデル設定メソッドの名称を `setTranslatorOpenAiModel``setTranslatorOpenAIModel` に変更。
### テスト拡張
- `backend_test.py` に全言語ペア翻訳網羅テスト `test_translate_all_language_pairs()` を追加。結果を `translation_test_results.json` として保存。
### 内部ユーティリティの修正
- `downloadCTranslate2Tokenizer()``tokenizer_path` を正しく作成し `transformers.AutoTokenizer.from_pretrained(tokenizer, cache_dir=tokenizer_path)` を使用するよう修正。
### 影響範囲まとめ
| 区分 | 影響内容 |
|------|----------|
| 設定 | 新規プロパティ / 既存名称変更 (`SELECTED_*`, `LMSTUDIO_URL`) |
| エンドポイント | LMStudio / Ollama / OpenAI 名称統一追加 |
| 翻訳言語 | CTranslate2 ネスト構造 / YAML マッピング導入 |
| リソース | PyInstaller datas 追加 / フォントパス移行 |
| 依存 | PyYAML / google-genai / grpcio 追加 |
| 認証 | OpenAI/Plamo/Gemini 認証後モデルリスト更新 & root_path 引数追加 |
| テスト | 全言語ペア網羅テスト追加 |
| ユーティリティ | Tokenizer ダウンロード処理修正 |
### ライセンス影響
追加された依存は既存 LICENSE の記載範囲に変更を強制するものではなく、ライセンス本文の更新不要 (現行 OSS ライセンス許容範囲内)。

View File

@@ -52,6 +52,17 @@ run_mapping = {
"selected_translation_compute_type":"/run/selected_translation_compute_type", "selected_translation_compute_type":"/run/selected_translation_compute_type",
"selected_transcription_compute_type":"/run/selected_transcription_compute_type", "selected_transcription_compute_type":"/run/selected_transcription_compute_type",
"selectable_plamo_model_list":"/run/selectable_plamo_model_list",
"selected_plamo_model":"/run/selected_plamo_model",
"selectable_gemini_model_list":"/run/selectable_gemini_model_list",
"selected_gemini_model":"/run/selected_gemini_model",
"selectable_openai_model_list":"/run/selectable_openai_model_list",
"selected_openai_model":"/run/selected_openai_model",
"selectable_lmstudio_model_list":"/run/selectable_lmstudio_model_list",
"selected_lmstudio_model":"/run/selected_lmstudio_model",
"selectable_ollama_model_list":"/run/selectable_ollama_model_list",
"selected_ollama_model":"/run/selected_ollama_model",
"mic_host_list":"/run/mic_host_list", "mic_host_list":"/run/mic_host_list",
"mic_device_list":"/run/mic_device_list", "mic_device_list":"/run/mic_device_list",
"speaker_device_list":"/run/speaker_device_list", "speaker_device_list":"/run/speaker_device_list",
@@ -175,6 +186,40 @@ mapping = {
"/set/data/deepl_auth_key": {"status": False, "variable":controller.setDeeplAuthKey}, "/set/data/deepl_auth_key": {"status": False, "variable":controller.setDeeplAuthKey},
"/delete/data/deepl_auth_key": {"status": False, "variable":controller.delDeeplAuthKey}, "/delete/data/deepl_auth_key": {"status": False, "variable":controller.delDeeplAuthKey},
"/get/data/plamo_model_list": {"status": False, "variable":controller.getPlamoModelList},
"/get/data/plamo_model": {"status": False, "variable":controller.getPlamoModel},
"/set/data/plamo_model": {"status": False, "variable":controller.setPlamoModel},
"/get/data/plamo_auth_key": {"status": False, "variable":controller.getPlamoAuthKey},
"/set/data/plamo_auth_key": {"status": False, "variable":controller.setPlamoAuthKey},
"/delete/data/plamo_auth_key": {"status": False, "variable":controller.delPlamoAuthKey},
"/get/data/gemini_model_list": {"status": True, "variable":controller.getGeminiModelList},
"/get/data/gemini_model": {"status": True, "variable":controller.getGeminiModel},
"/set/data/gemini_model": {"status": True, "variable":controller.setGeminiModel},
"/get/data/gemini_auth_key": {"status": True, "variable":controller.getGeminiAuthKey},
"/set/data/gemini_auth_key": {"status": True, "variable":controller.setGeminiAuthKey},
"/delete/data/gemini_auth_key": {"status": True, "variable":controller.delGeminiAuthKey},
"/get/data/openai_model_list": {"status": True, "variable":controller.getOpenAIModelList},
"/get/data/openai_model": {"status": True, "variable":controller.getOpenAIModel},
"/set/data/openai_model": {"status": True, "variable":controller.setOpenAIModel},
"/get/data/openai_auth_key": {"status": True, "variable":controller.getOpenAIAuthKey},
"/set/data/openai_auth_key": {"status": True, "variable":controller.setOpenAIAuthKey},
"/delete/data/openai_auth_key": {"status": True, "variable":controller.delOpenAIAuthKey},
"/run/lmstudio_connection": {"status": True, "variable":controller.checkTranslatorLMStudioConnection},
"/get/data/lmstudio_model_list": {"status": True, "variable":controller.getTranslatorLStudioModelList},
"/get/data/lmstudio_model": {"status": True, "variable":controller.getTranslatorLMStudioModel},
"/set/data/lmstudio_model": {"status": True, "variable":controller.setTranslatorLMStudioModel},
"/get/data/lmstudio_url": {"status": True, "variable":controller.getTranslatorLMStudioURL},
"/set/data/lmstudio_url": {"status": True, "variable":controller.setTranslatorLMStudioURL},
"/run/ollama_connection": {"status": True, "variable":controller.checkTranslatorOllamaConnection},
"/get/data/ollama_model_list": {"status": True, "variable":controller.getTranslatorOllamaModelList},
"/get/data/ollama_model": {"status": True, "variable":controller.getTranslatorOllamaModel},
"/set/data/ollama_model": {"status": True, "variable":controller.setTranslatorOllamaModel},
# Transliteration
"/get/data/convert_message_to_romaji": {"status": True, "variable":controller.getConvertMessageToRomaji}, "/get/data/convert_message_to_romaji": {"status": True, "variable":controller.getConvertMessageToRomaji},
"/set/enable/convert_message_to_romaji": {"status": True, "variable":controller.setEnableConvertMessageToRomaji}, "/set/enable/convert_message_to_romaji": {"status": True, "variable":controller.setEnableConvertMessageToRomaji},
"/set/disable/convert_message_to_romaji": {"status": True, "variable":controller.setDisableConvertMessageToRomaji}, "/set/disable/convert_message_to_romaji": {"status": True, "variable":controller.setDisableConvertMessageToRomaji},

View File

@@ -193,11 +193,94 @@ class Model:
del self.keyword_processor del self.keyword_processor
self.keyword_processor = KeywordProcessor() self.keyword_processor = KeywordProcessor()
def authenticationTranslatorDeepLAuthKey(self, auth_key): def authenticationTranslatorDeepLAuthKey(self, auth_key: str) -> bool:
self.ensure_initialized() self.ensure_initialized()
result = self.translator.authenticationDeepLAuthKey(auth_key) result = self.translator.authenticationDeepLAuthKey(auth_key)
return result return result
def authenticationTranslatorPlamoAuthKey(self, auth_key: str) -> bool:
result = self.translator.authenticationPlamoAuthKey(auth_key, root_path=config.PATH_LOCAL)
return result
def getTranslatorPlamoModelList(self) -> list[str]:
self.ensure_initialized()
return self.translator.getPlamoModelList()
def setTranslatorPlamoModel(self, model: str) -> bool:
self.ensure_initialized()
result = self.translator.setPlamoModel(model=model)
return result
def updateTranslatorPlamoClient(self) -> None:
self.ensure_initialized()
self.translator.updatePlamoClient()
def authenticationTranslatorGeminiAuthKey(self, auth_key: str) -> bool:
result = self.translator.authenticationGeminiAuthKey(auth_key, root_path=config.PATH_LOCAL)
return result
def getTranslatorGeminiModelList(self) -> list[str]:
self.ensure_initialized()
return self.translator.getGeminiModelList()
def setTranslatorGeminiModel(self, model: str) -> bool:
self.ensure_initialized()
result = self.translator.setGeminiModel(model=model)
return result
def updateTranslatorGeminiClient(self) -> None:
self.ensure_initialized()
self.translator.updateGeminiClient()
def authenticationTranslatorOpenAIAuthKey(self, auth_key: str, base_url: Optional[str] = None) -> bool:
result = self.translator.authenticationOpenAIAuthKey(auth_key, base_url=base_url, root_path=config.PATH_LOCAL)
return result
def getTranslatorOpenAIModelList(self) -> list[str]:
self.ensure_initialized()
return self.translator.getOpenAIModelList()
def setTranslatorOpenAIModel(self, model: str) -> bool:
self.ensure_initialized()
result = self.translator.setOpenAIModel(model=model)
return result
def updateTranslatorOpenAIClient(self) -> None:
self.ensure_initialized()
self.translator.updateOpenAIClient()
def authenticationTranslatorLMStudio(self, base_url: str) -> bool:
result = self.translator.setLMStudioClientURL(base_url=base_url, root_path=config.PATH_LOCAL)
return result
def getTranslatorLMStudioModelList(self) -> list[str]:
self.ensure_initialized()
return self.translator.getLMStudioModelList()
def setTranslatorLMStudioModel(self, model: str) -> bool:
self.ensure_initialized()
return self.translator.setLMStudioModel(model=model)
def updateTranslatorLMStudioClient(self) -> None:
self.ensure_initialized()
self.translator.updateLMStudioClient()
def authenticationTranslatorOllama(self) -> bool:
result = self.translator.checkOllamaClient(root_path=config.PATH_LOCAL)
return result
def getTranslatorOllamaModelList(self) -> list[str]:
self.ensure_initialized()
return self.translator.getOllamaModelList()
def setTranslatorOllamaModel(self, model: str) -> bool:
self.ensure_initialized()
return self.translator.setOllamaModel(model=model)
def updateTranslatorOllamaClient(self) -> None:
self.ensure_initialized()
self.translator.updateOllamaClient()
def startLogger(self): def startLogger(self):
self.ensure_initialized() self.ensure_initialized()
os_makedirs(config.PATH_LOGS, exist_ok=True) os_makedirs(config.PATH_LOGS, exist_ok=True)
@@ -214,6 +297,10 @@ class Model:
transcription_langs = list(transcription_lang.keys()) transcription_langs = list(transcription_lang.keys())
translation_langs = [] translation_langs = []
for tl_key in translation_lang.keys(): for tl_key in translation_lang.keys():
if tl_key == "CTranslate2":
for lang in translation_lang[tl_key][config.CTRANSLATE2_WEIGHT_TYPE]["source"]:
translation_langs.append(lang)
else:
for lang in translation_lang[tl_key]["source"]: for lang in translation_lang[tl_key]["source"]:
translation_langs.append(lang) translation_langs.append(lang)
translation_langs = list(set(translation_langs)) translation_langs = list(set(translation_langs))
@@ -235,6 +322,9 @@ class Model:
selectable_engines = [key for key, value in engines_status.items() if value is True] selectable_engines = [key for key, value in engines_status.items() if value is True]
compatible_engines = [] compatible_engines = []
for engine in list(translation_lang.keys()): for engine in list(translation_lang.keys()):
if engine == "CTranslate2":
languages = translation_lang.get(engine, {}).get(config.CTRANSLATE2_WEIGHT_TYPE, {}).get("source", {})
else:
languages = translation_lang.get(engine, {}).get("source", {}) languages = translation_lang.get(engine, {}).get("source", {})
source_langs = [e["language"] for e in list(source_lang.values()) if e["enable"] is True] source_langs = [e["language"] for e in list(source_lang.values()) if e["enable"] is True]
target_langs = [e["language"] for e in list(target_lang.values()) if e["enable"] is True] target_langs = [e["language"] for e in list(target_lang.values()) if e["enable"] is True]
@@ -251,6 +341,7 @@ class Model:
success_flag = False success_flag = False
translation = self.translator.translate( translation = self.translator.translate(
translator_name=translator_name, translator_name=translator_name,
weight_type=config.CTRANSLATE2_WEIGHT_TYPE,
source_language=source_language, source_language=source_language,
target_language=target_language, target_language=target_language,
target_country=target_country, target_country=target_country,
@@ -264,6 +355,7 @@ class Model:
while True: while True:
translation = self.translator.translate( translation = self.translator.translate(
translator_name="CTranslate2", translator_name="CTranslate2",
weight_type=config.CTRANSLATE2_WEIGHT_TYPE,
source_language=source_language, source_language=source_language,
target_language=target_language, target_language=target_language,
target_country=target_country, target_country=target_country,

View File

@@ -26,10 +26,17 @@ class OverlayImage:
defaults to repository `fonts` directory. defaults to repository `fonts` directory.
""" """
self.message_log: List[dict] = [] self.message_log: List[dict] = []
if root_path is None: # PyInstallerでビルドされた場合のパス
self.root_path = os_path.join(os_path.dirname(__file__), "..", "..", "..", "fonts") if root_path and os_path.exists(os_path.join(root_path, "_internal", "fonts")):
else:
self.root_path = os_path.join(root_path, "_internal", "fonts") self.root_path = os_path.join(root_path, "_internal", "fonts")
# src-pythonフォルダから直接実行している場合のパス
elif os_path.exists(os_path.join(os_path.dirname(__file__), "models", "overlay", "fonts")):
self.root_path = os_path.join(os_path.dirname(__file__), "models", "overlay", "fonts")
# overlayフォルダから直接実行している場合のパス
elif os_path.exists(os_path.join(os_path.dirname(__file__), "fonts")):
self.root_path = os_path.join(os_path.dirname(__file__), "fonts")
else:
raise FileNotFoundError("Font directory not found.")
@staticmethod @staticmethod
def concatenateImagesVertically(img1: Image, img2: Image, margin: int = 0) -> Image: def concatenateImagesVertically(img1: Image, img2: Image, margin: int = 0) -> Image:
@@ -69,20 +76,8 @@ class OverlayImage:
img = Image.new("RGBA", (base_width, base_height), (0, 0, 0, 0)) img = Image.new("RGBA", (base_width, base_height), (0, 0, 0, 0))
draw = ImageDraw.Draw(img) draw = ImageDraw.Draw(img)
try:
font_path = os_path.join(self.root_path, font_family) font_path = os_path.join(self.root_path, font_family)
font = ImageFont.truetype(font_path, font_size) font = ImageFont.truetype(font_path, font_size)
except Exception:
# overlayフォルダから操作している場合
if os_path.exists(os_path.join(os_path.dirname(__file__), "..", "..", "..", "fonts", font_family)):
font_path = os_path.join(os_path.dirname(__file__), "..", "..", "..", "fonts", font_family)
font = ImageFont.truetype(font_path, font_size)
elif os_path.exists(os_path.join(os_path.dirname(__file__), "fonts", font_family)):
# src-pythonフォルダから操作している場合
font_path = os_path.join(os_path.dirname(__file__), "fonts", font_family)
font = ImageFont.truetype(font_path, font_size)
else:
raise FileNotFoundError(f"Font file not found: {font_family}")
text_width = draw.textlength(text, font) text_width = draw.textlength(text, font)
character_width = text_width // len(text) character_width = text_width // len(text)
@@ -180,18 +175,8 @@ class OverlayImage:
img = Image.new("RGBA", (0, 0), (0, 0, 0, 0)) img = Image.new("RGBA", (0, 0), (0, 0, 0, 0))
draw = ImageDraw.Draw(img) draw = ImageDraw.Draw(img)
try:
font_path = os_path.join(self.root_path, font_family) font_path = os_path.join(self.root_path, font_family)
font = ImageFont.truetype(font_path, font_size) font = ImageFont.truetype(font_path, font_size)
except Exception:
if os_path.exists(os_path.join(os_path.dirname(__file__), "..", "..", "..", "fonts", font_family)):
font_path = os_path.join(os_path.dirname(__file__), "..", "..", "..", "fonts", font_family)
font = ImageFont.truetype(font_path, font_size)
elif os_path.exists(os_path.join(os_path.dirname(__file__), "fonts", font_family)):
font_path = os_path.join(os_path.dirname(__file__), "fonts", font_family)
font = ImageFont.truetype(font_path, font_size)
else:
raise FileNotFoundError(f"Font file not found: {font_family}")
# 改行を含んだtextの最大の文字数を計算する # 改行を含んだtextの最大の文字数を計算する
text_width = max(draw.textlength(line, font) for line in text.split("\n")) text_width = max(draw.textlength(line, font) for line in text.split("\n"))
@@ -221,20 +206,8 @@ class OverlayImage:
img = Image.new("RGBA", (0, 0), (0, 0, 0, 0)) img = Image.new("RGBA", (0, 0), (0, 0, 0, 0))
draw = ImageDraw.Draw(img) draw = ImageDraw.Draw(img)
try:
font_path = os_path.join(self.root_path, self.LANGUAGES["Default"]) font_path = os_path.join(self.root_path, self.LANGUAGES["Default"])
font = ImageFont.truetype(font_path, font_size) font = ImageFont.truetype(font_path, font_size)
except Exception:
# overlayフォルダから操作している場合
if os_path.exists(os_path.join(os_path.dirname(__file__), "..", "..", "..", "fonts", self.LANGUAGES["Default"])):
font_path = os_path.join(os_path.dirname(__file__), "..", "..", "..", "fonts", self.LANGUAGES["Default"])
font = ImageFont.truetype(font_path, font_size)
elif os_path.exists(os_path.join(os_path.dirname(__file__), "fonts", self.LANGUAGES["Default"])):
# src-pythonフォルダから操作している場合
font_path = os_path.join(os_path.dirname(__file__), "fonts", self.LANGUAGES["Default"])
font = ImageFont.truetype(font_path, font_size)
else:
raise FileNotFoundError(f"Font file not found: {self.LANGUAGES['Default']}")
text_height = font_size + ui_padding text_height = font_size + ui_padding
text_width = draw.textlength(date_time, font) text_width = draw.textlength(date_time, font)

View File

@@ -0,0 +1,771 @@
# Translation Language Mappings
# Each backend defines source (input) and target (output) language codes
DeepL:
source: &deepl_langs
Arabic: ar
Bulgarian: bg
Czech: cs
Danish: da
German: de
Greek: el
English: en
Spanish: es
Estonian: et
Finnish: fi
French: fr
Irish: ga
Croatian: hr
Hungarian: hu
Indonesian: id
Italian: it
Japanese: ja
Korean: ko
Lithuanian: lt
Latvian: lv
Maltese: mt
Norwegian: 'no'
Dutch: nl
Polish: pl
Portuguese: pt
Romanian: ro
Russian: ru
Slovak: sk
Slovenian: sl
Swedish: sv
Turkish: tr
Ukrainian: uk
Chinese Simplified: zh
Chinese Traditional: zh
target: *deepl_langs
DeepL_API:
source:
Japanese: ja
English: en
Bulgarian: bg
Czech: cs
Danish: da
German: de
Greek: el
Spanish: es
Estonian: et
Finnish: fi
French: fr
Hungarian: hu
Indonesian: id
Italian: it
Korean: ko
Lithuanian: lt
Latvian: lv
Norwegian: 'no'
Dutch: nl
Polish: pl
Portuguese: pt
Romanian: ro
Russian: ru
Slovak: sk
Slovenian: sl
Swedish: sv
Turkish: tr
Ukrainian: uk
Chinese Simplified: zh
Chinese Traditional: zh
target:
Japanese: ja
English: en
English (American): en-US
English (British): en-GB
Bulgarian: bg
Czech: cs
Danish: da
German: de
Greek: el
Spanish: es
Estonian: et
Finnish: fi
French: fr
Hungarian: hu
Indonesian: id
Italian: it
Korean: ko
Lithuanian: lt
Latvian: lv
Norwegian: 'no'
Dutch: nl
Polish: pl
Portuguese (Brazilian): pt-BR
Portuguese (European): pt-PT
Romanian: ro
Russian: ru
Slovak: sk
Slovenian: sl
Swedish: sv
Turkish: tr
Ukrainian: uk
Chinese Simplified: zh
Chinese Traditional: zh
Google:
source: &google_langs
Japanese: ja
English: en
Chinese Simplified: zh
Chinese Traditional: zh-TW
Arabic: ar
Russian: ru
French: fr
German: de
Spanish: es
Portuguese: pt
Italian: it
Korean: ko
Greek: el
Dutch: nl
Hindi: hi
Turkish: tr
Malay: ms
Thai: th
Vietnamese: vi
Indonesian: id
Hebrew: he
Polish: pl
Mongolian: mn
Czech: cs
Hungarian: hu
Estonian: et
Bulgarian: bg
Danish: da
Finnish: fi
Romanian: ro
Swedish: sv
Slovenian: sl
Persian/Farsi: fa
Bosnian: bs
Serbian: sr
Croatian: hr
Slovak: sk
Albanian: sq
Lithuanian: lt
Latvian: lv
Macedonian: mk
Ukrainian: uk
Belarusian: be
Kazakh: kk
Uzbek: uz
Armenian: hy
Azerbaijani: az
Georgian: ka
Kyrgyz: ky
Tajik: tg
Turkmen: tk
Nepali: ne
Sinhala: si
Khmer: km
Lao: lo
Burmese: my
Malayalam: ml
Telugu: te
Tamil: ta
Kannada: kn
Marathi: mr
Gujarati: gu
Punjabi: pa
Bengali: bn
Odia: or
Assamese: as
Urdu: ur
Amharic: am
Tigrinya: ti
Oromo: om
Somali: so
Swahili: sw
Kinyarwanda: rw
Yoruba: yo
Zulu: zu
Xhosa: xh
Afrikaans: af
Sesotho: st
Chichewa: ny
Malagasy: mg
Esperanto: eo
Hawaiian: haw
Samoan: sm
Shona: sn
Sindhi: sd
Pashto: ps
Kurdish: ku
Hausa: ha
Igbo: ig
Maltese: mt
Welsh: cy
Luxembourgish: lb
Icelandic: is
Irish: ga
Scottish Gaelic: gd
Basque: eu
Galician: gl
Catalan: ca
Corsican: co
Latin: la
Maori: mi
Hmong: hmn
Cebuano: ceb
Filipino: tl
Javanese: jw
Sundanese: su
Yiddish: yi
Frisian: fy
target: *google_langs
Bing:
source: &bing_langs
Japanese: ja
English: en
Chinese Simplified: zh
Chinese Traditional: zh-Hant
Arabic: ar
Russian: ru
French: fr
German: de
Spanish: es
Portuguese: pt
Italian: it
Korean: ko
Greek: el
Dutch: nl
Hindi: hi
Turkish: tr
Malay: ms
Thai: th
Vietnamese: vi
Indonesian: id
Hebrew: he
Polish: pl
Czech: cs
Hungarian: hu
Estonian: et
Bulgarian: bg
Danish: da
Finnish: fi
Romanian: ro
Swedish: sv
Slovenian: sl
Persian/Farsi: fa
Bosnian: bs
Serbian: sr
Croatian: hr
Albanian: sq
Lithuanian: lt
Latvian: lv
Ukrainian: uk
Welsh: cy
Belarusian: be
Icelandic: is
Irish: ga
Maltese: mt
Yiddish: yi
Afrikaans: af
Norwegian: 'no'
Bengali: bn
Malagasy: mg
Samoan: sm
Slovak: sk
Swahili: sw
Filipino: tl
Urdu: ur
Haitian Creole: ht
Catalan: ca
Fijian: fj
Kiswahili: sw
Tahitian: ty
Tongan: to
Klingon: tlh
Queretaro Otomi: otl
Gujarati: gu
Tamil: ta
Telugu: te
Punjabi: pa
target: *bing_langs
Papago:
source: &papago_langs
German: de
English: en
Spanish: es
French: fr
Hindi: hi
Indonesian: id
Italian: it
Japanese: ja
Korean: ko
Portuguese: pt
Russian: ru
Thai: th
Vietnamese: vi
Chinese Simplified: zh-CN
Chinese Traditional: zh-TW
target: *papago_langs
CTranslate2:
m2m100_418M-ct2-int8:
source: &m2m100_langs
English: en
Chinese Simplified: zh
Chinese Traditional: zh
German: de
Spanish: es
Russian: ru
Korean: ko
French: fr
Japanese: ja
Portuguese: pt
Turkish: tr
Polish: pl
Catalan: ca
Dutch: nl
Arabic: ar
Swedish: sv
Italian: it
Indonesian: id
Hindi: hi
Finnish: fi
Vietnamese: vi
Hebrew: he
Ukrainian: uk
Greek: el
Malay: ms
Czech: cs
Romanian: ro
Danish: da
Hungarian: hu
Tamil: ta
Norwegian: 'no'
Thai: th
Urdu: ur
Croatian: hr
Bulgarian: bg
Lithuanian: lt
Latin: la
Maori: mi
Malayalam: ml
Welsh: cy
Slovak: sk
Telugu: te
Persian: fa
Latvian: lv
Bengali: bn
Serbian: sr
Azerbaijani: az
Slovenian: sl
Kannada: kn
Estonian: et
Macedonian: mk
Breton: br
Basque: eu
Icelandic: is
Armenian: hy
Nepali: ne
Mongolian: mn
Bosnian: bs
Kazakh: kk
Albanian: sq
Swahili: sw
Galician: gl
Marathi: mr
Punjabi: pa
Sinhala: si
Khmer: km
Shona: sn
Yoruba: yo
Somali: so
Afrikaans: af
Occitan: oc
Georgian: ka
Belarusian: be
Tajik: tg
Sindhi: sd
Gujarati: gu
Amharic: am
Yiddish: yi
Lao: lo
Uzbek: uz
Faroese: fo
Haitian creole: ht
Pashto: ps
Turkmen: tk
Nynorsk: nn
Maltese: mt
Sanskrit: sa
Luxembourgish: lb
Myanmar: my
Tibetan: bo
Filipino: tl
Malagasy: mg
Assamese: as
Tatar: tt
Hawaiian: haw
Lingala: ln
Hausa: ha
Bashkir: ba
Javanese: jw
Sundanese: su
target: *m2m100_langs
m2m100_1.2B-ct2-int8:
source: *m2m100_langs
target: *m2m100_langs
nllb-200-distilled-1.3B-ct2-int8:
source: &nllb_langs
Acehnese (Arabic script): ace_Arab
Acehnese (Latin script): ace_Latn
Mesopotamian Arabic: acm_Arab
Ta'izzi-Adeni Arabic: acq_Arab
Tunisian Arabic: aeb_Arab
Afrikaans: afr_Latn
South Levantine Arabic: ajp_Arab
Akan: aka_Latn
Amharic: amh_Ethi
North Levantine Arabic: apc_Arab
Standard Arabic: arb_Arab
Modern Standard Arabic (Romanized): arb_Latn
Najdi Arabic: ars_Arab
Moroccan Arabic: ary_Arab
Egyptian Arabic: arz_Arab
Assamese: asm_Beng
Asturian: ast_Latn
Awadhi: awa_Deva
Central Aymara: ayr_Latn
South Azerbaijani: azb_Arab
North Azerbaijani: azj_Latn
Bashkir: bak_Cyrl
Bambara: bam_Latn
Balinese: ban_Latn
Belarusian: bel_Cyrl
Bemba: bem_Latn
Bengali: ben_Beng
Bhojpuri: bho_Deva
Banjar (Arabic script): bjn_Arab
Banjar (Latin script): bjn_Latn
Standard Tibetan: bod_Tibt
Bosnian: bos_Latn
Buginese: bug_Latn
Bulgarian: bul_Cyrl
Catalan: cat_Latn
Cebuano: ceb_Latn
Czech: ces_Latn
Chokwe: cjk_Latn
Central Kurdish: ckb_Arab
Crimean Tatar: crh_Latn
Welsh: cym_Latn
Danish: dan_Latn
German: deu_Latn
Southwestern Dinka: dik_Latn
Dyula: dyu_Latn
Dzongkha: dzo_Tibt
Greek: ell_Grek
English: eng_Latn
Esperanto: epo_Latn
Estonian: est_Latn
Basque: eus_Latn
Ewe: ewe_Latn
Faroese: fao_Latn
Fijian: fij_Latn
Finnish: fin_Latn
Fon: fon_Latn
French: fra_Latn
Friulian: fur_Latn
Nigerian Fulfulde: fuv_Latn
Scottish Gaelic: gla_Latn
Irish: gle_Latn
Galician: glg_Latn
Guarani: grn_Latn
Gujarati: guj_Gujr
Haitian Creole: hat_Latn
Hausa: hau_Latn
Hebrew: heb_Hebr
Hindi: hin_Deva
Chhattisgarhi: hne_Deva
Croatian: hrv_Latn
Hungarian: hun_Latn
Armenian: hye_Armn
Igbo: ibo_Latn
Ilocano: ilo_Latn
Indonesian: ind_Latn
Icelandic: isl_Latn
Italian: ita_Latn
Javanese: jav_Latn
Japanese: jpn_Jpan
Kabyle: kab_Latn
Jingpho: kac_Latn
Kamba: kam_Latn
Kannada: kan_Knda
Kashmiri (Arabic script): kas_Arab
Kashmiri (Devanagari script): kas_Deva
Georgian: kat_Geor
Central Kanuri (Arabic script): knc_Arab
Central Kanuri (Latin script): knc_Latn
Kazakh: kaz_Cyrl
Kabiyè: kbp_Latn
Kabuverdianu: kea_Latn
Khmer: khm_Khmr
Kikuyu: kik_Latn
Kinyarwanda: kin_Latn
Kyrgyz: kir_Cyrl
Kimbundu: kmb_Latn
Northern Kurdish: kmr_Latn
Kikongo: kon_Latn
Korean: kor_Hang
Lao: lao_Laoo
Ligurian: lij_Latn
Limburgish: lim_Latn
Lingala: lin_Latn
Lithuanian: lit_Latn
Lombard: lmo_Latn
Latgalian: ltg_Latn
Luxembourgish: ltz_Latn
Luba-Kasai: lua_Latn
Ganda: lug_Latn
Luo: luo_Latn
Mizo: lus_Latn
Standard Latvian: lvs_Latn
Magahi: mag_Deva
Maithili: mai_Deva
Malayalam: mal_Mlym
Marathi: mar_Deva
Minangkabau (Arabic script): min_Arab
Minangkabau (Latin script): min_Latn
Macedonian: mkd_Cyrl
Plateau Malagasy: plt_Latn
Maltese: mlt_Latn
Meitei (Bengali script): mni_Beng
Halh Mongolian: khk_Cyrl
Mossi: mos_Latn
Maori: mri_Latn
Burmese: mya_Mymr
Dutch: nld_Latn
Norwegian Nynorsk: nno_Latn
Norwegian Bokmål: nob_Latn
Nepali: npi_Deva
Northern Sotho: nso_Latn
Nuer: nus_Latn
Nyanja: nya_Latn
Occitan: oci_Latn
West Central Oromo: gaz_Latn
Odia: ory_Orya
Pangasinan: pag_Latn
Eastern Panjabi: pan_Guru
Papiamento: pap_Latn
Western Persian: pes_Arab
Polish: pol_Latn
Portuguese: por_Latn
Dari: prs_Arab
Southern Pashto: pbt_Arab
Ayacucho Quechua: quy_Latn
Romanian: ron_Latn
Rundi: run_Latn
Russian: rus_Cyrl
Sango: sag_Latn
Sanskrit: san_Deva
Santali: sat_Olck
Sicilian: scn_Latn
Shan: shn_Mymr
Sinhala: sin_Sinh
Slovak: slk_Latn
Slovenian: slv_Latn
Samoan: smo_Latn
Shona: sna_Latn
Sindhi: snd_Arab
Somali: som_Latn
Southern Sotho: sot_Latn
Spanish: spa_Latn
Tosk Albanian: als_Latn
Sardinian: srd_Latn
Serbian: srp_Cyrl
Swati: ssw_Latn
Sundanese: sun_Latn
Swedish: swe_Latn
Swahili: swh_Latn
Silesian: szl_Latn
Tamil: tam_Taml
Tatar: tat_Cyrl
Telugu: tel_Telu
Tajik: tgk_Cyrl
Tagalog: tgl_Latn
Thai: tha_Thai
Tigrinya: tir_Ethi
Tamasheq (Latin script): taq_Latn
Tamasheq (Tifinagh script): taq_Tfng
Tok Pisin: tpi_Latn
Tswana: tsn_Latn
Tsonga: tso_Latn
Turkmen: tuk_Latn
Tumbuka: tum_Latn
Turkish: tur_Latn
Twi: twi_Latn
Central Atlas Tamazight: tzm_Tfng
Uyghur: uig_Arab
Ukrainian: ukr_Cyrl
Umbundu: umb_Latn
Urdu: urd_Arab
Northern Uzbek: uzn_Latn
Venetian: vec_Latn
Vietnamese: vie_Latn
Waray: war_Latn
Wolof: wol_Latn
Xhosa: xho_Latn
Eastern Yiddish: ydd_Hebr
Yoruba: yor_Latn
Yue Chinese: yue_Hant
Chinese Simplified: zho_Hans
Chinese Traditional: zho_Hant
Standard Malay: zsm_Latn
Zulu: zul_Latn
target: *nllb_langs
nllb-200-3.3B-ct2-int8:
source: *nllb_langs
target: *nllb_langs
Plamo_API:
source: &plamo_langs
English: English
Japanese: Japanese
Korean: Korean
French: French
German: German
Spanish: Spanish
Portuguese: Portuguese
Russian: Russian
Italian: Italian
Dutch: Dutch
Polish: Polish
Turkish: Turkish
Arabic: Arabic
Hindi: Hindi
Thai: Thai
Vietnamese: Vietnamese
Indonesian: Indonesian
Malay: Malay
Filipino: Filipino
Swedish: Swedish
Finnish: Finnish
Danish: Danish
Norwegian: Norwegian
Romanian: Romanian
Czech: Czech
Hungarian: Hungarian
Greek: Greek
Hebrew: Hebrew
Chinese Simplified: Simplified Chinese
Chinese Traditional: Traditional Chinese
target: *plamo_langs
Gemini_API:
source: &gemini_langs
Arabic: Arabic
Bengali: Bengali
Bulgarian: Bulgarian
Chinese Simplified: Simplified Chinese
Chinese Traditional: Traditional Chinese
Croatian: Croatian
Czech: Czech
Danish: Danish
Dutch: Dutch
English: English
Estonian: Estonian
Finnish: Finnish
French: French
German: German
Greek: Greek
Hebrew: Hebrew
Hindi: Hindi
Hungarian: Hungarian
Indonesian: Indonesian
Italian: Italian
Japanese: Japanese
Korean: Korean
Latvian: Latvian
Lithuanian: Lithuanian
Norwegian: Norwegian
Polish: Polish
Portuguese: Portuguese
Romanian: Romanian
Russian: Russian
Serbian: Serbian
Slovak: Slovak
Slovenian: Slovenian
Spanish: Spanish
Swedish: Swedish
Thai: Thai
Turkish: Turkish
Ukrainian: Ukrainian
Vietnamese: Vietnamese
target: *gemini_langs
OpenAI_API:
source: &openai_langs
Afrikaans: Afrikaans
Arabic: Arabic
Armenian: Armenian
Azerbaijani: Azerbaijani
Belarusian: Belarusian
Bosnian: Bosnian
Bulgarian: Bulgarian
Catalan: Catalan
Chinese: Chinese
Croatian: Croatian
Czech: Czech
Danish: Danish
Dutch: Dutch
English: English
Estonian: Estonian
Finnish: Finnish
French: French
Galician: Galician
German: German
Greek: Greek
Hebrew: Hebrew
Hindi: Hindi
Hungarian: Hungarian
Icelandic: Icelandic
Indonesian: Indonesian
Italian: Italian
Japanese: Japanese
Kannada: Kannada
Kazakh: Kazakh
Korean: Korean
Latvian: Latvian
Lithuanian: Lithuanian
Macedonian: Macedonian
Malay: Malay
Marathi: Marathi
Maori: Maori
Nepali: Nepali
Norwegian: Norwegian
Persian: Persian
Polish: Polish
Portuguese: Portuguese
Romanian: Romanian
Russian: Russian
Serbian: Serbian
Slovak: Slovak
Slovenian: Slovenian
Spanish: Spanish
Swahili: Swahili
Swedish: Swedish
Tagalog: Tagalog
Tamil: Tamil
Thai: Thai
Turkish: Turkish
Ukrainian: Ukrainian
Urdu: Urdu
Vietnamese: Vietnamese
Welsh: Welsh
target: *openai_langs
LMStudio:
source: *openai_langs
target: *openai_langs
Ollama:
source: *openai_langs
target: *openai_langs

View File

@@ -0,0 +1,7 @@
system_prompt: |
You are a helpful translation assistant.
Supported languages:
{supported_languages}
Translate the user provided text from {input_lang} to {output_lang}.
Return ONLY the translated text. Do not add quotes or extra commentary.

View File

@@ -0,0 +1,7 @@
system_prompt: |
You are a helpful translation assistant.
Supported languages:
{supported_languages}
Translate the user provided text from {input_lang} to {output_lang}.
Return ONLY the translated text. Do not add quotes or extra commentary.

View File

@@ -0,0 +1,7 @@
system_prompt: |
You are a helpful translation assistant.
Supported languages:
{supported_languages}
Translate the user provided text from {input_lang} to {output_lang}.
Return ONLY the translated text. Do not add quotes or extra commentary.

View File

@@ -0,0 +1,7 @@
system_prompt: |
You are a helpful translation assistant.
Supported languages:
{supported_languages}
Translate the user provided text from {input_lang} to {output_lang}.
Return ONLY the translated text. Do not add quotes or extra commentary.

View File

@@ -0,0 +1,7 @@
system_prompt: |
You are a translation assistant that uses the `plamo-translate` tool.
Translate the following text.Supported languages include:
{supported_languages}
Translate the following text from {input_lang} to {output_lang}.
output only the translated text without any additional commentary.

View File

@@ -0,0 +1,128 @@
import logging
from google import genai
from langchain_google_genai import ChatGoogleGenerativeAI
try:
from .translation_languages import translation_lang
from .translation_utils import loadPromptConfig
except Exception:
import sys
from os import path as os_path
print(os_path.dirname(os_path.dirname(os_path.dirname(os_path.abspath(__file__)))))
sys.path.append(os_path.dirname(os_path.dirname(os_path.dirname(os_path.abspath(__file__)))))
from translation_languages import translation_lang
from translation_utils import loadPromptConfig
logger = logging.getLogger("langchain_google_genai")
logger.setLevel(logging.ERROR)
def _authentication_check(api_key: str) -> bool:
"""Check if the provided API key is valid by attempting to list models.
"""
try:
client = genai.Client(api_key=api_key)
client.models.list()
return True
except Exception:
return False
def _get_available_text_models(api_key: str) -> list[str]:
"""Extract only Gemini models suitable for translation and chat applications
"""
client = genai.Client(api_key=api_key)
res = client.models.list()
allowed_models = []
# 除外対象のキーワード
exclude_keywords = [
"audio",
"image",
"veo",
"tts",
"robotics",
"computer-use"
]
for model in res:
model_id = model.name
if ("gemini" in model_id.lower() or "gemma" in model_id.lower()) and "generateContent" in model.supported_actions:
if any(x in model_id for x in exclude_keywords):
continue
allowed_models.append(model_id.replace("models/", ""))
allowed_models.sort()
return allowed_models
class GeminiClient:
def __init__(self, root_path: str = None):
self.api_key = None
self.model = None
# プロンプト設定をYAMLファイルから読み込む
prompt_config = loadPromptConfig(root_path, "translation_gemini.yml")
self.supported_languages = list(translation_lang["Gemini_API"]["source"].keys())
self.prompt_template = prompt_config["system_prompt"]
self.gemini_llm = None
def getModelList(self) -> list[str]:
return _get_available_text_models(self.api_key)
def getAuthKey(self) -> str:
return self.api_key
def setAuthKey(self, api_key: str) -> bool:
result = _authentication_check(api_key)
if result:
self.api_key = api_key
return result
def getModel(self) -> str:
return self.model
def setModel(self, model: str) -> bool:
if model in self.getModelList():
self.model = model
return True
else:
return False
def updateClient(self) -> None:
self.gemini_llm = ChatGoogleGenerativeAI(
model=self.model,
api_key=self.api_key,
)
def translate(self, text: str, input_lang: str, output_lang: str) -> str:
system_prompt = self.prompt_template.format(
supported_languages=self.supported_languages,
input_lang=input_lang,
output_lang=output_lang
)
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": text}
]
resp = self.gemini_llm.invoke(messages)
content = ""
if isinstance(resp.content, str):
content = resp.content
elif isinstance(resp.content, list):
for part in resp.content:
if isinstance(part, str):
content += part
elif isinstance(part, dict) and "content" in part and isinstance(part["content"], str):
content += part["content"]
return content.strip()
if __name__ == "__main__":
AUTH_KEY = "AUTH_KEY"
client = GeminiClient()
client.setAuthKey(AUTH_KEY)
models = client.getModelList()
if models:
print("Available models:", models)
model = input("Select a model: ")
client.setModel(model)
client.updateClient()
print(client.translate("こんにちは世界", "Japanese", "English"))

View File

@@ -1,375 +1,144 @@
"""Language code mappings for supported translation backends. """Load translation language code mappings from YAML.
Provides `translation_lang` mapping keyed by backend name with `source` and YAML ファイル: languages/languages.yml
`target` maps used by `Translator.getLanguageCode`. 構造:
<BackendName>:
source: { DisplayName: Code, ... }
target: { DisplayName: Code, ... }
CTranslate2:
<ModelName>:
source: {...}
target: {...}
""" """
from typing import Dict import os
import threading
from typing import Any, Dict
import yaml
try:
from utils import printLog, errorLogging
except ImportError:
def printLog(data, *args, **kwargs):
print(data, *args, **kwargs)
def errorLogging():
import traceback
traceback.print_exc()
# 型: translation_lang[backend][(model)?]['source'|'target'][display_name] = code
translation_lang: Dict[str, Dict[str, Dict[str, str]]] = {} translation_lang: Dict[str, Dict[str, Dict[str, str]]] = {}
_loaded = False
_lock = threading.Lock()
dict_deepl_languages = {
"Arabic":"ar",
"Bulgarian":"bg",
"Czech":"cs",
"Danish":"da",
"German":"de",
"Greek":"el",
"English":"en",
"Spanish":"es",
"Estonian":"et",
"Finnish":"fi",
"French":"fr",
"Irish":"ga",
"Croatian":"hr",
"Hungarian":"hu",
"Indonesian":"id",
"Icelandic":"is",
"Italian":"it",
"Japanese":"ja",
"Korean":"ko",
"Lithuanian":"lt",
"Latvian":"lv",
"Maltese":"mt",
"Bokmal":"nb",
"Dutch":"nl",
"Norwegian":"no",
"Polish":"pl",
"Portuguese":"pt",
"Romanian":"ro",
"Russian":"ru",
"Slovak":"sk",
"Slovenian":"sl",
"Swedish":"sv",
"Turkish":"tr",
"Ukrainian":"uk",
"Chinese Simplified":"zh",
"Chinese Traditional":"zh"
}
translation_lang["DeepL"] = {"source": dict_deepl_languages, "target": dict_deepl_languages}
dict_deepl_api_source_languages = { def _load_languages(path: str, filename: str) -> str:
"Japanese":"ja", """Get absolute path to resource file relative to this module.
"English":"en",
"Bulgarian":"bg",
"Czech":"cs",
"Danish":"da",
"German":"de",
"Greek":"el",
"Spanish":"es",
"Estonian":"et",
"Finnish":"fi",
"French":"fr",
"Hungarian":"hu",
"Indonesian":"id",
"Italian":"it",
"Korean":"ko",
"Lithuanian":"lt",
"Latvian":"lv",
"Norwegian":"nb",
"Dutch":"nl",
"Polish":"pl",
"Portuguese":"pt",
"Romanian":"ro",
"Russian":"ru",
"Slovak":"sk",
"Slovenian":"sl",
"Swedish":"sv",
"Turkish":"tr",
"Ukrainian":"uk",
"Chinese Simplified":"zh",
"Chinese Traditional":"zh"
}
dict_deepl_api_target_languages = {
"Japanese":"ja",
"English American":"en-US",
"English British":"en-GB",
"Bulgarian":"bg",
"Czech":"cs",
"Danish":"da",
"German":"de",
"Greek":"el",
"English":"en",
"Spanish":"es",
"Estonian":"et",
"Finnish":"fi",
"French":"fr",
"Hungarian":"hu",
"Indonesian":"id",
"Italian":"it",
"Korean":"ko",
"Lithuanian":"lt",
"Latvian":"lv",
"Norwegian":"nb",
"Dutch":"nl",
"Polish":"pl",
"Portuguese Brazilian":"pt-BR",
"Portuguese European":"pt-PT",
"Romanian":"ro",
"Russian":"ru",
"Slovak":"sk",
"Slovenian":"sl",
"Swedish":"sv",
"Turkish":"tr",
"Ukrainian":"uk",
"Chinese Simplified":"zh",
"Chinese Traditional":"zh"
}
translation_lang["DeepL_API"] = {"source": dict_deepl_api_source_languages, "target": dict_deepl_api_target_languages}
dict_google_languages = { Args:
"Japanese":"ja", filename: relative filename from this module's directory
"English":"en",
"Chinese Simplified":"zh",
"Chinese Traditional":"zh-TW",
"Arabic":"ar",
"Russian":"ru",
"French":"fr",
"German":"de",
"Spanish":"es",
"Portuguese":"pt",
"Italian":"it",
"Korean":"ko",
"Greek":"el",
"Dutch":"nl",
"Hindi":"hi",
"Turkish":"tr",
"Malay":"ms",
"Thai":"th",
"Vietnamese":"vi",
"Indonesian":"id",
"Hebrew":"he",
"Polish":"pl",
"Mongolian":"mn",
"Czech":"cs",
"Hungarian":"hu",
"Estonian":"et",
"Bulgarian":"bg",
"Danish":"da",
"Finnish":"fi",
"Romanian":"ro",
"Swedish":"sv",
"Slovenian":"sl",
"Persian/Farsi":"fa",
"Bosnian":"bs",
"Serbian":"sr",
"Filipino":"tl",
"Haitiancreole":"ht",
"Catalan":"ca",
"Croatian":"hr",
"Latvian":"lv",
"Lithuanian":"lt",
"Urdu":"ur",
"Ukrainian":"uk",
"Welsh":"cy",
"Swahili":"sw",
"Samoan":"sm",
"Slovak":"sk",
"Afrikaans":"af",
"Norwegian":"no",
"Bengali":"bn",
"Malagasy":"mg",
"Maltese":"mt",
"Gujarati":"gu",
"Tamil":"ta",
"Telugu":"te",
"Punjabi":"pa",
"Amharic":"am",
"Azerbaijani":"az",
"Belarusian":"be",
"Cebuano":"ceb",
"Esperanto":"eo",
# "Basque":"eu",
"Irish":"ga"
}
translation_lang["Google"] = {"source": dict_google_languages, "target": dict_google_languages}
dict_bing_languages = { Returns:
"Japanese":"ja", Absolute path to the resource file
"English":"en", """
"Chinese Simplified":"zh", if os.path.exists(os.path.join(path, "_internal", "languages", "languages.yml")):
"Chinese Traditional":"zh-Hant", languages_path = os.path.join(path, "_internal", "languages", "languages.yml")
"Arabic":"ar", elif os.path.exists(os.path.join(os.path.dirname(os.path.abspath(__file__)), "models", "translation", "languages", "languages.yml")):
"Russian":"ru", languages_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "models", "translation", "languages", "languages.yml")
"French":"fr", elif os.path.exists(os.path.join(os.path.dirname(os.path.abspath(__file__)), "languages", "languages.yml")):
"German":"de", languages_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "languages", "languages.yml")
"Spanish":"es", else:
"Portuguese":"pt", raise FileNotFoundError(f"Prompt file not found: {filename}")
"Italian":"it", with open(languages_path, "r", encoding="utf-8") as f:
"Korean":"ko", return yaml.safe_load(f)
"Greek":"el",
"Dutch":"nl",
"Hindi":"hi",
"Turkish":"tr",
"Malay":"ms",
"Thai":"th",
"Vietnamese":"vi",
"Indonesian":"id",
"Hebrew":"he",
"Polish":"pl",
"Czech":"cs",
"Hungarian":"hu",
"Estonian":"et",
"Bulgarian":"bg",
"Danish":"da",
"Finnish":"fi",
"Romanian":"ro",
"Swedish":"sv",
"Slovenian":"sl",
"Persian/Farsi":"fa",
"Bosnian":"bs",
"Serbian":"sr",
"Fijian":"fj",
"Filipino":"tl",
"Haitiancreole":"ht",
"Catalan":"ca",
"Croatian":"hr",
"Latvian":"lv",
"Lithuanian":"lt",
"Urdu":"ur",
"Ukrainian":"uk",
"Welsh":"cy",
"Tahiti":"ty",
"Tongan":"to",
"Swahili":"sw",
"Samoan":"sm",
"Slovak":"sk",
"Afrikaans":"af",
"Norwegian":"no",
"Bengali":"bn",
"Malagasy":"mg",
"Maltese":"mt",
"Queretaro otomi":"otq",
"Klingon/tlhingan Hol":"tlh",
"Gujarati":"gu",
"Tamil":"ta",
"Telugu":"te",
"Punjabi":"pa",
"Irish":"ga"
}
translation_lang["Bing"] = {"source": dict_bing_languages, "target": dict_bing_languages}
dict_papago_languages = { def _validate_source_target(backend: str, mapping: Any) -> None:
"German": "de", """Validate that a backend mapping has proper source/target structure.
"English": "en",
"Spanish":"es", Args:
"French": "fr", backend: backend name for error messages
"Hindi": "hi", mapping: mapping to validate
"Indonesian": "id",
"Italian": "it", Raises:
"Japanese": "ja", ValueError: If mapping structure is invalid
"Korean": "ko", """
"Portuguese": "pt", if not isinstance(mapping, dict):
"Russian": "ru", raise ValueError(f"{backend}: 値は dict である必要があります。")
"Thai": "th", if "source" not in mapping or "target" not in mapping:
"Vietnamese": "vi", raise ValueError(f"{backend}: 'source''target' が必要です。")
"Chinese Simplified":"zh-CN",
"Chinese Traditional":"zh-TW", for key in ("source", "target"):
if not isinstance(mapping[key], dict):
raise ValueError(f"{backend}: '{key}' は dict である必要があります。")
# value は str を想定
for disp, code in mapping[key].items():
if not isinstance(disp, str) or not isinstance(code, str):
raise ValueError(
f"{backend}: '{key}' のエントリは str: str である必要があります。 ({disp} => {code})"
)
def loadTranslationLanguages(path: str, force: bool = False) -> Dict[str, Any]:
"""Load translation language mappings from YAML file.
Args:
path: Path to the YAML file
force: If True, reload even if already loaded
Returns:
Dictionary of translation language mappings
Raises:
FileNotFoundError: If languages/languages.yml is not found
ValueError: If YAML structure is invalid
"""
global _loaded, translation_lang
if _loaded and not force:
return translation_lang
with _lock:
if _loaded and not force:
return translation_lang
data = _load_languages(path, "languages/languages.yml")
if not isinstance(data, dict):
raise ValueError(
"languages/languages.yml のルートはマッピング(dict)である必要があります。"
)
# 検証と正規化
validated: Dict[str, Dict[str, Dict[str, str]]] = {}
for backend, value in data.items():
if backend == "CTranslate2":
# NOTE: CTranslate2 はモデルごとに異なる言語セットを持つ
if not isinstance(value, dict):
raise ValueError(
"CTranslate2 の値はモデル名→ {source:, target:} の dict である必要があります。"
)
validated["CTranslate2"] = {}
for model_name, model_map in value.items():
_validate_source_target(
backend=f"CTranslate2/{model_name}", mapping=model_map
)
validated["CTranslate2"][model_name] = {
"source": model_map["source"],
"target": model_map["target"],
}
else:
_validate_source_target(backend=backend, mapping=value)
validated[backend] = {
"source": value["source"],
"target": value["target"],
} }
translation_lang["Papago"] = {"source": dict_papago_languages, "target": dict_papago_languages} translation_lang = validated
_loaded = True
return translation_lang
dict_ctranslate2_languages = { if __name__ == "__main__":
"English": "en", try:
"Chinese Simplified": "zh", langs = loadTranslationLanguages(path=".", force=True)
"Chinese Traditional":"zh", printLog("Loaded translation languages:")
"German": "de", printLog(langs)
"Spanish": "es", except Exception:
"Russian": "ru", errorLogging()
"Korean": "ko",
"French": "fr",
"Japanese": "ja",
"Portuguese": "pt",
"Turkish": "tr",
"Polish": "pl",
"Catalan": "ca",
"Dutch": "nl",
"Arabic": "ar",
"Swedish": "sv",
"Italian": "it",
"Indonesian": "id",
"Hindi": "hi",
"Finnish": "fi",
"Vietnamese": "vi",
"Hebrew": "he",
"Ukrainian": "uk",
"Greek": "el",
"Malay": "ms",
"Czech": "cs",
"Romanian": "ro",
"Danish": "da",
"Hungarian": "hu",
"Tamil": "ta",
"Norwegian": "no",
"Thai": "th",
"Urdu": "ur",
"Croatian": "hr",
"Bulgarian": "bg",
"Lithuanian": "lt",
"Latin": "la",
"Maori": "mi",
"Malayalam": "ml",
"Welsh": "cy",
"Slovak": "sk",
# "Telugu": "te",
"Persian": "fa",
"Latvian": "lv",
"Bengali": "bn",
"Serbian": "sr",
"Azerbaijani": "az",
"Slovenian": "sl",
"Kannada": "kn",
"Estonian": "et",
"Macedonian": "mk",
"Breton": "br",
# "Basque": "eu",
"Icelandic": "is",
"Armenian": "hy",
"Nepali": "ne",
"Mongolian": "mn",
"Bosnian": "bs",
"Kazakh": "kk",
"Albanian": "sq",
"Swahili": "sw",
"Galician": "gl",
"Marathi": "mr",
"Punjabi": "pa",
"Sinhala": "si",
"Khmer": "km",
"Shona": "sn",
"Yoruba": "yo",
"Somali": "so",
"Afrikaans": "af",
"Occitan": "oc",
"Georgian": "ka",
"Belarusian": "be",
"Tajik": "tg",
"Sindhi": "sd",
"Gujarati": "gu",
"Amharic": "am",
"Yiddish": "yi",
"Lao": "lo",
"Uzbek": "uz",
"Faroese": "fo",
"Haitian creole": "ht",
"Pashto": "ps",
"Turkmen": "tk",
"Nynorsk": "nn",
"Maltese": "mt",
"Sanskrit": "sa",
"Luxembourgish": "lb",
"Myanmar": "my",
"Tibetan": "bo",
"Filipino": "tl",
"Malagasy": "mg",
"Assamese": "as",
"Tatar": "tt",
"Hawaiian": "haw",
"Lingala": "ln",
"Hausa": "ha",
"Bashkir": "ba",
"Javanese": "jw",
"Sundanese": "su"
}
translation_lang["CTranslate2"] = {"source": dict_ctranslate2_languages, "target": dict_ctranslate2_languages}

View File

@@ -0,0 +1,118 @@
from openai import OpenAI
from langchain_openai import ChatOpenAI
from pydantic import SecretStr
try:
from .translation_languages import translation_lang
from .translation_utils import loadPromptConfig
except Exception:
import sys
from os import path as os_path
sys.path.append(os_path.dirname(os_path.dirname(os_path.dirname(os_path.abspath(__file__)))))
from translation_languages import translation_lang
from translation_utils import loadPromptConfig
def _authentication_check(api_key: str, base_url: str | None = None) -> bool:
"""Check if the provided API key is valid by attempting to list models.
"""
try:
client = OpenAI(api_key=api_key, base_url=base_url)
client.models.list()
return True
except Exception:
return False
def _get_available_text_models(api_key: str, base_url: str | None = None) -> list[str]:
"""Extract the list of available text models from the LM Studio.
"""
try:
client = OpenAI(api_key=api_key, base_url=base_url)
res = client.models.list()
models = res.data
except Exception:
models = []
allowed_models = []
for model in models:
allowed_models.append(model.id)
allowed_models.sort()
return allowed_models
class LMStudioClient:
"""LM Studio Translation simple wrapper.
prompt/translation_lmstudio.yml から system_prompt / supported_languages を読み込む。
"""
def __init__(self, base_url: str | None = None, root_path: str = None):
self.api_key = "lmstudio"
self.model = None
self.base_url = base_url # None の場合は公式エンドポイント
prompt_config = loadPromptConfig(root_path, "translation_lmstudio.yml")
self.supported_languages = list(translation_lang["LMStudio"]["source"].keys())
self.prompt_template = prompt_config["system_prompt"]
self.openai_llm = None
def getBaseURL(self) -> str | None:
return self.base_url
def setBaseURL(self, base_url: str | None) -> None:
result = _authentication_check(api_key=self.api_key, base_url=base_url)
if result:
self.base_url = base_url
return result
def getModelList(self) -> list[str]:
return _get_available_text_models(api_key=self.api_key, base_url=self.base_url) if self.base_url else []
def getModel(self) -> str:
return self.model
def setModel(self, model: str) -> bool:
if model in self.getModelList():
self.model = model
return True
else:
return False
def updateClient(self) -> None:
self.openai_llm = ChatOpenAI(
base_url=self.base_url,
model=self.model,
api_key=SecretStr(self.api_key),
streaming=False,
)
def translate(self, text: str, input_lang: str, output_lang: str) -> str:
system_prompt = self.prompt_template.format(
supported_languages=self.supported_languages,
input_lang=input_lang,
output_lang=output_lang,
)
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": text},
]
resp = self.openai_llm.invoke(messages)
content = ""
if isinstance(resp.content, str):
content = resp.content
elif isinstance(resp.content, list):
for part in resp.content:
if isinstance(part, str):
content += part
elif isinstance(part, dict) and "content" in part and isinstance(part["content"], str):
content += part["content"]
return content.strip()
if __name__ == "__main__":
client = LMStudioClient(base_url="http://192.168.68.110:1234/v1")
models = client.getModelList()
if models:
print("Available models:", models)
model = input("Select a model: ")
client.setModel(model)
client.updateClient()
print(client.translate("こんにちは世界", "Japanese", "English"))

View File

@@ -0,0 +1,112 @@
import requests
from langchain_ollama import ChatOllama
try:
from .translation_languages import translation_lang
from .translation_utils import loadPromptConfig
except Exception:
import sys
from os import path as os_path
sys.path.append(os_path.dirname(os_path.dirname(os_path.dirname(os_path.abspath(__file__)))))
from translation_languages import translation_lang
from translation_utils import loadPromptConfig
def _authentication_check(base_url: str | None = None) -> bool:
"""Check authentication for Ollama API.
"""
try:
response = requests.get(f"{base_url}")
if response.status_code == 200:
return True
else:
return False
except Exception:
return False
def _get_available_text_models(base_url: str | None = None) -> list[str]:
"""Extract available text models from Ollama.
"""
try:
response = requests.get(f"{base_url}/api/tags")
models = response.json()["models"]
except Exception:
models = []
allowed_models = []
for model in models:
allowed_models.append(model["name"])
allowed_models.sort()
return allowed_models
class OllamaClient:
"""Ollama Translation simple wrapper.
prompt/translation_ollama.yml から system_prompt / supported_languages を読み込む。
"""
def __init__(self, root_path: str = None):
self.model = None
self.base_url = "http://localhost:11434"
prompt_config = loadPromptConfig(root_path, "translation_ollama.yml")
self.supported_languages = list(translation_lang["Ollama"]["source"].keys())
self.prompt_template = prompt_config["system_prompt"]
self.openai_llm = None
def authenticationCheck(self) -> bool:
return _authentication_check(self.base_url)
def getModelList(self) -> list[str]:
if self.authenticationCheck():
return _get_available_text_models(self.base_url)
return []
def getModel(self) -> str:
return self.model
def setModel(self, model: str) -> bool:
if model in self.getModelList():
self.model = model
return True
else:
return False
def updateClient(self) -> None:
self.openai_llm = ChatOllama(
base_url=self.base_url,
model=self.model,
streaming=False,
)
def translate(self, text: str, input_lang: str, output_lang: str) -> str:
system_prompt = self.prompt_template.format(
supported_languages=self.supported_languages,
input_lang=input_lang,
output_lang=output_lang,
)
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": text},
]
resp = self.openai_llm.invoke(messages)
content = ""
if isinstance(resp.content, str):
content = resp.content
elif isinstance(resp.content, list):
for part in resp.content:
if isinstance(part, str):
content += part
elif isinstance(part, dict) and "content" in part and isinstance(part["content"], str):
content += part["content"]
return content.strip()
if __name__ == "__main__":
client = OllamaClient()
models = client.getModelList()
if models:
print("Available models:", models)
model = input("Select a model: ")
client.setModel(model)
client.updateClient()
print(client.translate("こんにちは世界", "Japanese", "English"))

View File

@@ -0,0 +1,140 @@
from openai import OpenAI
from langchain_openai import ChatOpenAI
from pydantic import SecretStr
try:
from .translation_languages import translation_lang
from .translation_utils import loadPromptConfig
except Exception:
import sys
from os import path as os_path
sys.path.append(os_path.dirname(os_path.dirname(os_path.dirname(os_path.abspath(__file__)))))
from translation_languages import translation_lang
from translation_utils import loadPromptConfig
def _authentication_check(api_key: str, base_url: str | None = None) -> bool:
"""Check if the provided API key is valid by attempting to list models.
"""
try:
client = OpenAI(api_key=api_key, base_url=base_url)
client.models.list()
return True
except Exception:
return False
def _get_available_text_models(api_key: str, base_url: str | None = None) -> list[str]:
"""Extract only GPT models suitable for translation and chat applications (plus those with fine-tuning)
"""
client = OpenAI(api_key=api_key, base_url=base_url)
res = client.models.list()
allowed_models = []
for model in res.data:
model_id = model.id
root = getattr(model, "root", "")
# 除外対象のキーワード
exclude_keywords = [
"whisper", # 音声認識
"embedding", # 埋め込み
"image", # 画像生成
"tts", # 音声合成
"audio", # 音声系transcribe, diarize含む
"search", # 検索補助モデル
"transcribe", # 音声→文字起こし
"diarize", # 話者分離
"vision" # 画像入力系旧gpt-4-visionなど
]
# 除外キーワードが含まれているモデルをスキップ
if any(kw in model_id for kw in exclude_keywords):
continue
# GPTモデルまたはFine-tune GPTモデルのみ対象
if model_id.startswith("gpt-"):
allowed_models.append(model_id)
elif model_id.startswith("ft:") and root.startswith("gpt-"):
allowed_models.append(model_id)
allowed_models.sort()
return allowed_models
class OpenAIClient:
"""OpenAI Translation simple wrapper.
prompt/translation_openai.yml から system_prompt / supported_languages を読み込む。
"""
def __init__(self, base_url: str | None = None, root_path: str = None):
self.api_key = None
self.model = None
self.base_url = base_url # None の場合は公式エンドポイント
prompt_config = loadPromptConfig(root_path, "translation_openai.yml")
self.supported_languages = list(translation_lang["OpenAI_API"]["source"].keys())
self.prompt_template = prompt_config["system_prompt"]
self.openai_llm = None
def getModelList(self) -> list[str]:
return _get_available_text_models(self.api_key, self.base_url) if self.api_key else []
def getAuthKey(self) -> str:
return self.api_key
def setAuthKey(self, api_key: str) -> bool:
result = _authentication_check(api_key, self.base_url)
if result:
self.api_key = api_key
return result
def getModel(self) -> str:
return self.model
def setModel(self, model: str) -> bool:
if model in self.getModelList():
self.model = model
return True
else:
return False
def updateClient(self) -> None:
self.openai_llm = ChatOpenAI(
base_url=self.base_url,
model=self.model,
api_key=SecretStr(self.api_key),
streaming=False,
)
def translate(self, text: str, input_lang: str, output_lang: str) -> str:
system_prompt = self.prompt_template.format(
supported_languages=self.supported_languages,
input_lang=input_lang,
output_lang=output_lang,
)
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": text},
]
resp = self.openai_llm.invoke(messages)
content = ""
if isinstance(resp.content, str):
content = resp.content
elif isinstance(resp.content, list):
for part in resp.content:
if isinstance(part, str):
content += part
elif isinstance(part, dict) and "content" in part and isinstance(part["content"], str):
content += part["content"]
return content.strip()
if __name__ == "__main__":
AUTH_KEY = "OPENAI_API_KEY"
client = OpenAIClient()
client.setAuthKey(AUTH_KEY)
models = client.getModelList()
if models:
print("Available models:", models)
model = input("Select a model: ")
client.setModel(model)
client.updateClient()
print(client.translate("こんにちは世界", "Japanese", "English"))

View File

@@ -0,0 +1,115 @@
from openai import OpenAI
from langchain_openai import ChatOpenAI
from pydantic import SecretStr
try:
from .translation_languages import translation_lang
from .translation_utils import loadPromptConfig
except Exception:
import sys
from os import path as os_path
sys.path.append(os_path.dirname(os_path.dirname(os_path.dirname(os_path.abspath(__file__)))))
from translation_languages import translation_lang
from translation_utils import loadPromptConfig
BASE_URL = "https://api.platform.preferredai.jp/v1"
def _authentication_check(api_key: str) -> bool:
"""Check if the provided API key is valid by attempting to list models.
"""
try:
client = OpenAI(api_key=api_key, base_url=BASE_URL)
client.models.list()
return True
except Exception:
return False
def _get_available_text_models(api_key: str) -> list[str]:
"""Extract all available models from the PLAMO API
"""
client = OpenAI(api_key=api_key, base_url=BASE_URL)
res = client.models.list()
allowed_models = []
for model in res.data:
allowed_models.append(model.id)
allowed_models.sort()
return allowed_models
class PlamoClient:
def __init__(self, root_path: str = None):
self.api_key = None
self.base_url = BASE_URL
self.model = None
prompt_config = loadPromptConfig(root_path, "translation_plamo.yml")
self.supported_languages = list(translation_lang["Plamo_API"]["source"].keys())
self.prompt_template = prompt_config["system_prompt"]
self.plamo_llm = None
def getModelList(self) -> list[str]:
return _get_available_text_models(self.api_key) if self.api_key else []
def getAuthKey(self) -> str:
return self.api_key
def setAuthKey(self, api_key: str) -> bool:
result = _authentication_check(api_key)
if result:
self.api_key = api_key
return result
def getModel(self) -> str:
return self.model
def setModel(self, model: str) -> bool:
if model in self.getModelList():
self.model = model
return True
else:
return False
def updateClient(self) -> None:
self.plamo_llm = ChatOpenAI(
base_url=self.base_url,
model=self.model,
streaming=False,
api_key=SecretStr(self.api_key),
)
def translate(self, text: str, input_lang: str, output_lang: str) -> str:
system_prompt = self.prompt_template.format(
supported_languages=self.supported_languages,
input_lang=input_lang,
output_lang=output_lang
)
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": text},
]
resp = self.plamo_llm.invoke(messages)
content = ""
if isinstance(resp.content, str):
content = resp.content
elif isinstance(resp.content, list):
for part in resp.content:
if isinstance(part, str):
content += part
elif isinstance(part, dict) and "content" in part and isinstance(part["content"], str):
content += part["content"]
return content.strip()
if __name__ == "__main__":
AUTH_KEY = "PLAMO_API_KEY"
client = PlamoClient()
client.setAuthKey(AUTH_KEY)
models = client.getModelList()
if models:
print("Available models:", models)
model = input("Select a model: ")
client.setModel(model)
client.updateClient()
print(client.translate("こんにちは世界", "Japanese", "English"))

View File

@@ -7,8 +7,24 @@ except Exception:
other_web_Translator = None # type: ignore other_web_Translator = None # type: ignore
ENABLE_TRANSLATORS = False ENABLE_TRANSLATORS = False
try:
from .translation_languages import translation_lang from .translation_languages import translation_lang
from .translation_utils import ctranslate2_weights from .translation_utils import ctranslate2_weights
from .translation_plamo import PlamoClient
from .translation_gemini import GeminiClient
from .translation_openai import OpenAIClient
from .translation_lmstudio import LMStudioClient
from .translation_ollama import OllamaClient
except Exception:
import sys
sys.path.append(os_path.dirname(os_path.dirname(os_path.dirname(os_path.abspath(__file__)))))
from translation_languages import translation_lang
from translation_utils import ctranslate2_weights
from translation_plamo import PlamoClient
from translation_gemini import GeminiClient
from translation_openai import OpenAIClient
from translation_lmstudio import LMStudioClient
from translation_ollama import OllamaClient
import ctranslate2 import ctranslate2
import transformers import transformers
@@ -31,20 +47,25 @@ class Translator:
def __init__(self) -> None: def __init__(self) -> None:
self.deepl_client: Optional[DeepLClient] = None self.deepl_client: Optional[DeepLClient] = None
self.plamo_client: Optional[PlamoClient] = None
self.gemini_client: Optional[GeminiClient] = None
self.openai_client: Optional[OpenAIClient] = None
self.lmstudio_client: LMStudioClient[LMStudioClient] = None
self.ollama_client: OllamaClient[OllamaClient] = None
self.ctranslate2_translator: Any = None self.ctranslate2_translator: Any = None
self.ctranslate2_tokenizer: Any = None self.ctranslate2_tokenizer: Any = None
self.is_loaded_ctranslate2_model: bool = False self.is_loaded_ctranslate2_model: bool = False
self.is_changed_translator_parameters: bool = False self.is_changed_translator_parameters: bool = False
self.is_enable_translators: bool = ENABLE_TRANSLATORS self.is_enable_translators: bool = ENABLE_TRANSLATORS
def authenticationDeepLAuthKey(self, authkey: str) -> bool: def authenticationDeepLAuthKey(self, auth_key: str) -> bool:
"""Authenticate DeepL API with the provided key. """Authenticate DeepL API with the provided key.
Returns True on success, False on failure. Returns True on success, False on failure.
""" """
result = True result = True
try: try:
self.deepl_client = DeepLClient(authkey) self.deepl_client = DeepLClient(auth_key)
# quick smoke test # quick smoke test
self.deepl_client.translate_text(" ", target_lang="EN-US") self.deepl_client.translate_text(" ", target_lang="EN-US")
except Exception: except Exception:
@@ -53,6 +74,169 @@ class Translator:
result = False result = False
return result return result
def authenticationPlamoAuthKey(self, auth_key: str, root_path: str = None) -> bool:
"""Authenticate Plamo API with the provided key.
Returns True on success, False on failure.
"""
self.plamo_client = PlamoClient(root_path=root_path)
if self.plamo_client.setAuthKey(auth_key):
return True
else:
self.plamo_client = None
return False
def getPlamoModelList(self) -> list[str]:
"""Get available Plamo models.
Returns a list of model names, or an empty list on failure.
"""
if self.plamo_client is None:
return []
return self.plamo_client.getModelList()
def setPlamoModel(self, model: str) -> bool:
"""Change the Plamo model used for translation.
Returns True on success, False on failure.
"""
if self.plamo_client is None:
return False
return self.plamo_client.setModel(model)
def updatePlamoClient(self) -> None:
"""Update the Plamo client (fetch available models)."""
self.plamo_client.updateClient()
def authenticationGeminiAuthKey(self, auth_key: str, root_path: str = None) -> bool:
"""Authenticate Gemini API with the provided key.
Returns True on success, False on failure.
"""
self.gemini_client = GeminiClient(root_path=root_path)
if self.gemini_client.setAuthKey(auth_key):
return True
else:
return False
def getGeminiModelList(self) -> list[str]:
"""Get available Gemini models.
Returns a list of model names, or an empty list on failure.
"""
if self.gemini_client is None:
return []
return self.gemini_client.getModelList()
def setGeminiModel(self, model: str) -> bool:
"""Change the Gemini model used for translation.
Returns True on success, False on failure.
"""
if self.gemini_client is None:
return False
return self.gemini_client.setModel(model)
def updateGeminiClient(self) -> None:
"""Update the Gemini client (fetch available models)."""
self.gemini_client.updateClient()
def authenticationOpenAIAuthKey(self, auth_key: str, base_url: str | None = None, root_path: str = None) -> bool:
"""Authenticate OpenAI (Chat Completions) API with the provided key.
base_url を指定することで互換エンドポイント (例: Azure OpenAI 互換, Proxy) にも対応可能。
Returns True on success, False on failure.
"""
self.openai_client = OpenAIClient(base_url=base_url, root_path=root_path)
if self.openai_client.setAuthKey(auth_key):
return True
else:
self.openai_client = None
return False
def getOpenAIModelList(self) -> list[str]:
"""Get available OpenAI models.
Returns a list of model names, or an empty list on failure.
"""
if self.openai_client is None:
return []
return self.openai_client.getModelList()
def setOpenAIModel(self, model: str) -> bool:
"""Change the OpenAI model used for translation.
Returns True on success, False on failure.
"""
if self.openai_client is None:
return False
return self.openai_client.setModel(model)
def updateOpenAIClient(self) -> None:
"""Update the OpenAI client (fetch available models)."""
self.openai_client.updateClient()
def setLMStudioClientURL(self, base_url: str | None = None, root_path: str = None) -> bool:
"""Authenticate LM Studio with the provided base URL.
Returns True on success, False on failure.
"""
self.lmstudio_client = LMStudioClient(base_url=base_url, root_path=root_path)
result = self.lmstudio_client.setBaseURL(base_url)
if result is False:
self.lmstudio_client = None
return result
def getLMStudioModelList(self) -> list[str]:
"""Get available LM Studio models.
Returns a list of model names, or an empty list on failure.
"""
if self.lmstudio_client is None:
return []
return self.lmstudio_client.getModelList()
def setLMStudioModel(self, model: str) -> bool:
"""Change the LM Studio model used for translation.
"""
if self.lmstudio_client is None:
return False
return self.lmstudio_client.setModel(model)
def updateLMStudioClient(self) -> None:
"""Update the LM Studio client (fetch available models)."""
self.lmstudio_client.updateClient()
def checkOllamaClient(self, root_path: str = None) -> bool:
"""Check if Ollama client is available.
Returns True if Ollama is reachable, False otherwise.
"""
self.ollama_client = OllamaClient(root_path=root_path)
return self.ollama_client.authenticationCheck()
def getOllamaModelList(self, root_path: str = None) -> bool:
"""Initialize Ollama client and fetch available models.
Returns True on success, False on failure.
"""
if self.ollama_client is None:
return []
return self.ollama_client.getModelList()
def setOllamaModel(self, model: str) -> bool:
"""Change the Ollama model used for translation.
Returns True on success, False on failure.
"""
if self.ollama_client is None:
return False
return self.ollama_client.setModel(model)
def updateOllamaClient(self) -> None:
"""Update the Ollama client (fetch available models)."""
self.ollama_client.updateClient()
def changeCTranslate2Model(self, path: str, model_type: str, device: str = "cpu", device_index: int = 0, compute_type: str = "auto") -> None: def changeCTranslate2Model(self, path: str, model_type: str, device: str = "cpu", device_index: int = 0, compute_type: str = "auto") -> None:
"""Load a CTranslate2 model from weights. """Load a CTranslate2 model from weights.
@@ -92,7 +276,7 @@ class Translator:
def setChangedTranslatorParameters(self, is_changed: bool) -> None: def setChangedTranslatorParameters(self, is_changed: bool) -> None:
self.is_changed_translator_parameters = is_changed self.is_changed_translator_parameters = is_changed
def translateCTranslate2(self, message: str, source_language: str, target_language: str) -> Any: def translateCTranslate2(self, message: str, source_language: str, target_language, weight_type: str) -> Any:
"""Translate using a loaded CTranslate2 model. """Translate using a loaded CTranslate2 model.
Returns a string on success or False on failure (keeps legacy behavior). Returns a string on success or False on failure (keeps legacy behavior).
@@ -102,7 +286,13 @@ class Translator:
try: try:
self.ctranslate2_tokenizer.src_lang = source_language self.ctranslate2_tokenizer.src_lang = source_language
source = self.ctranslate2_tokenizer.convert_ids_to_tokens(self.ctranslate2_tokenizer.encode(message)) source = self.ctranslate2_tokenizer.convert_ids_to_tokens(self.ctranslate2_tokenizer.encode(message))
match weight_type:
case "m2m100_418M-ct2-int8" | "m2m100_1.2B-ct2-int8":
target_prefix = [self.ctranslate2_tokenizer.lang_code_to_token[target_language]] target_prefix = [self.ctranslate2_tokenizer.lang_code_to_token[target_language]]
case "nllb-200-distilled-1.3B-ct2-int8" | "nllb-200-3.3B-ct2-int8":
target_prefix = [target_language]
case _:
return False
results = self.ctranslate2_translator.translate_batch([source], target_prefix=[target_prefix]) results = self.ctranslate2_translator.translate_batch([source], target_prefix=[target_prefix])
target = results[0].hypotheses[0][1:] target = results[0].hypotheses[0][1:]
result = self.ctranslate2_tokenizer.decode(self.ctranslate2_tokenizer.convert_tokens_to_ids(target)) result = self.ctranslate2_tokenizer.decode(self.ctranslate2_tokenizer.convert_tokens_to_ids(target))
@@ -111,7 +301,7 @@ class Translator:
return result return result
@staticmethod @staticmethod
def getLanguageCode(translator_name: str, target_country: str, source_language: str, target_language: str) -> Tuple[str, str]: def getLanguageCode(translator_name: str, weight_type: str, target_country: str, source_language: str, target_language: str) -> Tuple[str, str]:
"""Resolve a friendly language name to translator-specific codes. """Resolve a friendly language name to translator-specific codes.
Returns (source_code, target_code). Returns (source_code, target_code).
@@ -128,13 +318,17 @@ class Translator:
target_language = "Portuguese European" target_language = "Portuguese European"
else: else:
target_language = "Portuguese Brazilian" target_language = "Portuguese Brazilian"
source_language = translation_lang[translator_name]["source"][source_language]
target_language = translation_lang[translator_name]["target"][target_language]
case "CTranslate2":
source_language = translation_lang[translator_name][weight_type]["source"][source_language]
target_language = translation_lang[translator_name][weight_type]["target"][target_language]
case _: case _:
pass
source_language = translation_lang[translator_name]["source"][source_language] source_language = translation_lang[translator_name]["source"][source_language]
target_language = translation_lang[translator_name]["target"][target_language] target_language = translation_lang[translator_name]["target"][target_language]
return source_language, target_language return source_language, target_language
def translate(self, translator_name: str, source_language: str, target_language: str, target_country: str, message: str) -> Any: def translate(self, translator_name: str, weight_type: str, source_language: str, target_language: str, target_country: str, message: str) -> Any:
"""Translate `message` using the named translator backend. """Translate `message` using the named translator backend.
Returns translated string on success, or False on failure. When Returns translated string on success, or False on failure. When
@@ -145,7 +339,7 @@ class Translator:
return message return message
result: Any = "" result: Any = ""
source_language, target_language = self.getLanguageCode(translator_name, target_country, source_language, target_language) source_language, target_language = self.getLanguageCode(translator_name, weight_type, target_country, source_language, target_language)
match translator_name: match translator_name:
case "DeepL": case "DeepL":
if self.is_enable_translators is True and other_web_Translator is not None: if self.is_enable_translators is True and other_web_Translator is not None:
@@ -160,7 +354,56 @@ class Translator:
if self.deepl_client is None: if self.deepl_client is None:
result = False result = False
else: else:
result = self.deepl_client.translate_text(message, source_lang=source_language, target_lang=target_language).text result = self.deepl_client.translate_text(
message,
source_lang=source_language,
target_lang=target_language
).text
case "Plamo_API":
if self.plamo_client is None:
result = False
else:
result = self.plamo_client.translate(
message,
input_lang=source_language,
output_lang=target_language,
)
case "Gemini_API":
if self.gemini_client is None:
result = False
else:
result = self.gemini_client.translate(
message,
input_lang=source_language,
output_lang=target_language,
)
case "OpenAI_API":
if self.openai_client is None:
result = False
else:
result = self.openai_client.translate(
message,
input_lang=source_language,
output_lang=target_language,
)
case "LMStudio":
if self.lmstudio_client is None:
result = False
else:
result = self.lmstudio_client.translate(
message,
input_lang=source_language,
output_lang=target_language,
)
case "Ollama":
if self.ollama_client is None:
result = False
else:
result = self.ollama_client.translate(
message,
input_lang=source_language,
output_lang=target_language,
)
case "Google": case "Google":
if self.is_enable_translators is True and other_web_Translator is not None: if self.is_enable_translators is True and other_web_Translator is not None:
result = other_web_Translator( result = other_web_Translator(
@@ -186,8 +429,27 @@ class Translator:
to_language=target_language, to_language=target_language,
) )
case "CTranslate2": case "CTranslate2":
result = self.translateCTranslate2(message=message, source_language=source_language, target_language=target_language) result = self.translateCTranslate2(
message=message,
source_language=source_language,
target_language=target_language,
weight_type=weight_type,
)
except Exception: except Exception:
errorLogging() errorLogging()
result = False result = False
return result return result
if __name__ == "__main__":
translator = Translator()
# test CTranslate2 model nllb-200-distilled-1.3B-ct2-int8
translator.changeCTranslate2Model(path=".", model_type="nllb-200-distilled-1.3B-ct2-int8", device="cpu", device_index=0)
result = translator.translate(
translator_name="CTranslate2",
weight_type="nllb-200-distilled-1.3B-ct2-int8",
source_language="English",
target_language="Japanese",
target_country="Japan",
message="Hello, world!"
)
print(result)

View File

@@ -1,12 +1,19 @@
import tempfile
from zipfile import ZipFile
from os import path as os_path from os import path as os_path
from os import makedirs as os_makedirs from os import makedirs as os_makedirs
from requests import get as requests_get from requests import get as requests_get
from typing import Callable, Optional from typing import Callable
import hashlib
import transformers import transformers
from utils import errorLogging import ctranslate2
from huggingface_hub import hf_hub_url, list_repo_files
import yaml
try:
from utils import errorLogging, getBestComputeType
except Exception:
import sys
print(os_path.dirname(os_path.dirname(os_path.dirname(os_path.abspath(__file__)))))
sys.path.append(os_path.dirname(os_path.dirname(os_path.dirname(os_path.abspath(__file__)))))
from utils import errorLogging, getBestComputeType
"""Utilities for downloading and verifying CTranslate2 weights and tokenizers. """Utilities for downloading and verifying CTranslate2 weights and tokenizers.
@@ -18,125 +25,115 @@ raising, which matches the repository's defensive style.
""" """
ctranslate2_weights = { ctranslate2_weights = {
"small": { "m2m100_418M-ct2-int8": {
"url": "https://github.com/misyaguziya/VRCT-weights/releases/download/v1.0/m2m100_418m.zip", "hf_repo": "jncraton/m2m100_418M-ct2-int8",
"directory_name": "m2m100_418m", "directory_name": "m2m100_418M-ct2-int8",
"tokenizer": "facebook/m2m100_418M", "tokenizer": "facebook/m2m100_418M",
"hash": {
"model.bin": "e7c26a9abb5260abd0268fbe3040714070dec254a990b4d7fd3f74c5230e3acb",
"sentencepiece.model": "d8f7c76ed2a5e0822be39f0a4f95a55eb19c78f4593ce609e2edbc2aea4d380a",
"shared_vocabulary.txt": "bd440aa21b8ca3453fc792a0018a1f3fe68b3464aadddd4d16a4b72f73c86d8c",
}, },
"m2m100_1.2B-ct2-int8": {
"hf_repo": "jncraton/m2m100_1.2B-ct2-int8",
"directory_name": "m2m100_1.2B-ct2-int8",
"tokenizer": "facebook/m2m100_1.2B",
}, },
"large": { "nllb-200-distilled-1.3B-ct2-int8": {
"url": "https://github.com/misyaguziya/VRCT-weights/releases/download/v1.0/m2m100_12b.zip", "hf_repo": "OpenNMT/nllb-200-distilled-1.3B-ct2-int8",
"directory_name": "m2m100_12b", "directory_name": "nllb-200-distilled-1.3B-ct2-int8",
"tokenizer": "facebook/m2m100_1.2b", "tokenizer": "facebook/nllb-200-distilled-1.3B",
"hash": {
"model.bin": "abb7bf4ba7e5e016b6e3ed480c752459b2f783ac8fca372e7587675e5bf3a919",
"sentencepiece.model": "d8f7c76ed2a5e0822be39f0a4f95a55eb19c78f4593ce609e2edbc2aea4d380a",
"shared_vocabulary.txt": "bd440aa21b8ca3453fc792a0018a1f3fe68b3464aadddd4d16a4b72f73c86d8c",
}, },
"nllb-200-3.3B-ct2-int8": {
"hf_repo": "OpenNMT/nllb-200-3.3B-ct2-int8",
"directory_name": "nllb-200-3.3B-ct2-int8",
"tokenizer": "facebook/nllb-200-3.3B",
}, },
} }
def checkCTranslate2Weight(root: str, weight_type: str = "m2m100_418M-ct2-int8"):
def calculate_file_hash(file_path: str, block_size: int = 65536) -> str: weight_directory_name = ctranslate2_weights[weight_type]["directory_name"]
hash_object = hashlib.sha256() path = os_path.join(root, "weights", "ctranslate2", weight_directory_name)
with open(file_path, "rb") as f:
for block in iter(lambda: f.read(block_size), b""):
hash_object.update(block)
return hash_object.hexdigest()
def checkCTranslate2Weight(root: str, weight_type: str = "small") -> bool:
"""Return True if the requested weight files exist and match their hashes.
This function intentionally avoids raising: callers use the boolean to
decide whether to (re)download weights.
"""
weight_info = ctranslate2_weights.get(weight_type)
if weight_info is None:
return False
weight_directory_name = weight_info["directory_name"]
hash_data = weight_info["hash"]
files = ["model.bin", "sentencepiece.model", "shared_vocabulary.txt"]
base_path = os_path.join(root, "weights", "ctranslate2")
# quick existence check
for f in files:
p = os_path.join(base_path, weight_directory_name, f)
if not os_path.exists(p):
return False
# verify hashes
for f in files:
p = os_path.join(base_path, weight_directory_name, f)
try: try:
if calculate_file_hash(p) != hash_data[f]: # モデルロード可能かどうかで判定
return False compute_type = getBestComputeType("cpu", 0)
except Exception: ctranslate2.Translator(path, compute_type=compute_type)
errorLogging()
return False
return True return True
except Exception:
return False
def downloadCTranslate2Weight(root: str, weight_type: str = "m2m100_418M-ct2-int8", callback: Callable = None, end_callback: Callable = None):
def downloadCTranslate2Weight(root: str, weight_type: str = "small", callback: Optional[Callable[[float], None]] = None, end_callback: Optional[Callable[[], None]] = None) -> None: hf_repo = ctranslate2_weights[weight_type]["hf_repo"]
"""Download and extract ctranslate2 weights for the given type. files = list_repo_files(repo_id=hf_repo)
path = os_path.join(root, "weights", "ctranslate2", ctranslate2_weights[weight_type]["directory_name"])
callback receives a float between 0 and 1 for progress when available.
end_callback is invoked after success or failure to allow caller cleanup.
"""
weight_info = ctranslate2_weights.get(weight_type)
if weight_info is None:
return
url = weight_info["url"]
filename = "weight.zip"
dst_path = os_path.join(root, "weights", "ctranslate2")
os_makedirs(dst_path, exist_ok=True)
if checkCTranslate2Weight(root, weight_type): if checkCTranslate2Weight(root, weight_type):
if callable(end_callback): return True
end_callback() os_makedirs(path, exist_ok=True)
return
def downloadFile(url: str, file_path: str, func: Callable = None):
try: try:
with tempfile.TemporaryDirectory() as tmp_path: res = requests_get(url, stream=True)
res = requests_get(url, stream=True, timeout=30) res.raise_for_status()
total = int(res.headers.get("content-length", 0) or 0) file_size = int(res.headers.get('content-length', 0))
written = 0 total_chunk = 0
out_path = os_path.join(tmp_path, filename) with open(file_path, 'wb') as file:
with open(out_path, "wb") as out: for chunk in res.iter_content(chunk_size=1024*2000):
for chunk in res.iter_content(chunk_size=1024 * 1024): file.write(chunk)
if not chunk: if func is not None:
continue total_chunk += len(chunk)
out.write(chunk) func(total_chunk/file_size)
written += len(chunk)
if callable(callback) and total:
try:
callback(written / total)
except Exception: except Exception:
errorLogging() errorLogging()
with ZipFile(out_path) as zf:
zf.extractall(dst_path) for filename in files:
except Exception: file_path = os_path.join(path, filename)
errorLogging() url = hf_hub_url(hf_repo, filename)
finally: downloadFile(url, file_path, func=callback if filename == "model.bin" else None)
if callable(end_callback):
if end_callback is not None:
end_callback() end_callback()
def downloadCTranslate2Tokenizer(path: str, weight_type: str = "m2m100_418M-ct2-int8"):
def downloadCTranslate2Tokenizer(root: str, weight_type: str = "small") -> None: directory_name = ctranslate2_weights[weight_type]["directory_name"]
"""Ensure a tokenizer for the requested weight is available (cached). tokenizer = ctranslate2_weights[weight_type]["tokenizer"]
tokenizer_path = os_path.join(path, "weights", "ctranslate2", directory_name, "tokenizer")
This will attempt to download the tokenizer via Hugging Face's transformers
and cache it under the weights directory. It logs failures instead of
raising to keep runtime resilient during startup.
"""
weight_info = ctranslate2_weights.get(weight_type)
if weight_info is None:
return
directory_name = weight_info["directory_name"]
tokenizer_name = weight_info["tokenizer"]
tokenizer_cache = os_path.join(root, "weights", "ctranslate2", directory_name, "tokenizer")
try: try:
os_makedirs(tokenizer_cache, exist_ok=True) os_makedirs(tokenizer_path, exist_ok=True)
transformers.AutoTokenizer.from_pretrained(tokenizer_name, cache_dir=tokenizer_cache) transformers.AutoTokenizer.from_pretrained(tokenizer, cache_dir=tokenizer_path)
except Exception: except Exception:
errorLogging() errorLogging()
tokenizer_path = os_path.join("./weights", "ctranslate2", directory_name, "tokenizer")
transformers.AutoTokenizer.from_pretrained(tokenizer, cache_dir=tokenizer_path)
def loadPromptConfig(root_path: str | None = None, prompt_filename: str | None = None) -> dict:
# PyInstaller 展開後
if root_path and prompt_filename and os_path.exists(os_path.join(root_path, "_internal", "prompt", prompt_filename)):
prompt_path = os_path.join(root_path, "_internal", "prompt", prompt_filename)
# src-python 直下実行
elif prompt_filename and os_path.exists(os_path.join(os_path.dirname(__file__), "models", "translation", "prompt", prompt_filename)):
prompt_path = os_path.join(os_path.dirname(__file__), "models", "translation", "prompt", prompt_filename)
# translation フォルダ直下実行
elif prompt_filename and os_path.exists(os_path.join(os_path.dirname(__file__), "prompt", prompt_filename)):
prompt_path = os_path.join(os_path.dirname(__file__), "prompt", prompt_filename)
else:
raise FileNotFoundError(f"Prompt file not found: {prompt_filename}")
with open(prompt_path, "r", encoding="utf-8") as f:
return yaml.safe_load(f)
# テスト用コード(直接実行時のみ)
if __name__ == "__main__":
def progress_callback(percent):
print(f"Download progress: {percent*100:.2f}%")
def end_callback():
print("Download finished.")
root = "./" # 必要に応じてパスを変更
# for weight_type in ctranslate2_weights.keys():
# print(f"Testing download for: {weight_type}")
# downloadCTranslate2Weight(root, weight_type, callback=progress_callback, end_callback=end_callback)
# result = checkCTranslate2Weight(root, weight_type)
# print(f"Model loadable: {result}")
# break
# downloadCTranslate2Tokenizer(root, "m2m100_418M-ct2-int8")
# model download test
downloadCTranslate2Weight(root, "nllb-200-distilled-1.3B", callback=progress_callback, end_callback=end_callback)
result = checkCTranslate2Weight(root, "nllb-200-distilled-1.3B")
print(f"Model loadable: {result}")

12
task_kill.py Normal file
View File

@@ -0,0 +1,12 @@
import subprocess
# VRCT-sidecar.exe を強制終了
try:
subprocess.run(
["taskkill", "/IM", "VRCT-sidecar.exe", "/F"],
check=False,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
except Exception:
pass