diff --git a/backend.spec b/backend.spec index c5c33da0..605c82fb 100644 --- a/backend.spec +++ b/backend.spec @@ -5,7 +5,15 @@ a = Analysis( ['src-python\\mainloop.py'], pathex=[], binaries=[], - datas=[('./fonts', 'fonts/'), ('.venv/Lib/site-packages/zeroconf', 'zeroconf/'), ('.venv/Lib/site-packages/openvr', 'openvr/'), ('.venv/Lib/site-packages/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=[], hookspath=[], hooksconfig={}, diff --git a/backend_cuda.spec b/backend_cuda.spec index e9cb35c8..8d80b6ec 100644 --- a/backend_cuda.spec +++ b/backend_cuda.spec @@ -5,7 +5,15 @@ a = Analysis( ['src-python\\mainloop.py'], pathex=[], binaries=[], - datas=[('./fonts', 'fonts/'), ('.venv_cuda/Lib/site-packages/zeroconf', 'zeroconf/'), ('.venv_cuda/Lib/site-packages/openvr', 'openvr/'), ('.venv_cuda/Lib/site-packages/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=[], hookspath=[], hooksconfig={}, diff --git a/package.json b/package.json index c15471e5..184d5c9a 100644 --- a/package.json +++ b/package.json @@ -12,9 +12,10 @@ "vite-preview": "vite preview", "tauri": "tauri", "tauri-dev": "tauri dev", + "task-kill": "python task_kill.py", "clean": "python clean.py", - "dev": "npm run build-python && npm run dev-ui", - "dev-cuda": "npm run build-python-cuda && npm run dev-ui", + "dev": "npm run task-kill && npm run build-python && 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", "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", diff --git a/requirements.txt b/requirements.txt index 11f1d694..6b269f15 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,7 @@ transformers==4.40.2 pillow == 10.0.0 PyAudioWPatch == 0.2.12.6 python-osc == 1.9.0 +PyYAML==6.0.2 deepl == 1.22.0 flashtext ==2.7 pyinstaller==6.10.0 @@ -18,6 +19,11 @@ websockets==15.0.1 huggingface_hub==0.32.2 hf-xet==1.1.2 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 SudachiDict-core==20250825 SudachiDict-full==20250825 diff --git a/requirements_cuda.txt b/requirements_cuda.txt index cc0743df..a93729f8 100644 --- a/requirements_cuda.txt +++ b/requirements_cuda.txt @@ -6,6 +6,7 @@ transformers==4.40.2 pillow == 10.0.0 PyAudioWPatch == 0.2.12.6 python-osc == 1.9.0 +PyYAML==6.0.2 deepl == 1.22.0 flashtext ==2.7 pyinstaller==6.10.0 @@ -19,6 +20,11 @@ websockets==15.0.1 huggingface_hub==0.32.2 hf-xet==1.1.2 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 SudachiDict-core==20250825 SudachiDict-full==20250825 diff --git a/src-python/backend_test.py b/src-python/backend_test.py index 8fea1092..c43a622d 100644 --- a/src-python/backend_test.py +++ b/src-python/backend_test.py @@ -65,6 +65,8 @@ class TestMainloop(): for endpoint in self.main.mapping.keys(): if endpoint.startswith("/set/data/"): 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 = [] for endpoint in self.main.mapping.keys(): @@ -225,9 +227,57 @@ class TestMainloop(): data = random.choice(self.config_dict["transcription_compute_device_list"]) case "/set/data/ctranslate2_weight_type": 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": - data = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - expected_status = [400] + data = "DEEPL_DUMMY_KEY" + 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": data = random.choice(self.config_dict["mic_host_list"]) case "/set/data/selected_mic_device": @@ -363,6 +413,15 @@ class TestMainloop(): data = random.choice(self.config_dict["selected_translation_compute_device"]["compute_types"]) case "/set/data/selected_transcription_compute_type": 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 _: data = None expected_status = [404] @@ -469,6 +528,12 @@ class TestMainloop(): case "/run/feed_watchdog": data = None 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 _: data = None expected_status = [404] diff --git a/src-python/config.py b/src-python/config.py index 03d87338..e1683534 100644 --- a/src-python/config.py +++ b/src-python/config.py @@ -16,9 +16,11 @@ except Exception: # pragma: no cover - optional runtime device_manager = None # type: ignore 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 translation_lang = {} # type: ignore + def loadTranslationLanguages(path: str, force: bool = False) -> Dict[str, Any]: + return {} try: from models.translation.translation_utils import ctranslate2_weights @@ -311,6 +313,51 @@ class Config: if isinstance(value, dict): 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 ## Main Window @property @@ -894,6 +941,77 @@ class Config: self._SELECTED_TRANSCRIPTION_COMPUTE_TYPE = 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 @json_serializable('AUTO_CLEAR_MESSAGE_BOX') 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 self._SELECTABLE_CTRANSLATE2_WEIGHT_TYPE_LIST = getattr(ctranslate2_weights, '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: [])() try: # transcription_lang is nested dict; attempt to extract keys defensively @@ -1168,6 +1287,11 @@ class Config: self._SELECTABLE_TRANSCRIPTION_ENGINE_STATUS = {} for engine in self.SELECTABLE_TRANSCRIPTION_ENGINE_LIST: 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 ## Main Window @@ -1267,11 +1391,20 @@ class Config: self._OSC_PORT = 9000 self._AUTH_KEYS = { "DeepL_API": None, + "Plamo_API": None, + "Gemini_API": None, + "OpenAI_API": None, } self._USE_EXCLUDE_WORDS = True 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._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._WHISPER_WEIGHT_TYPE = "base" self._SELECTED_TRANSCRIPTION_COMPUTE_TYPE = "auto" diff --git a/src-python/controller.py b/src-python/controller.py index f70be1e1..63fc489d 100644 --- a/src-python/controller.py +++ b/src-python/controller.py @@ -1598,6 +1598,467 @@ class Controller: self.updateTranslationEngineAndEngineList() 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 def getCtranslate2WeightType(*args, **kwargs) -> dict: return {"status":200, "result":config.CTRANSLATE2_WEIGHT_TYPE} @@ -2465,11 +2926,94 @@ class Controller: if config.AUTH_KEYS[engine] is not None: if model.authenticationTranslatorDeepLAuthKey(auth_key=config.AUTH_KEYS[engine]) is True: config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = True + printLog("DeepL API Key is valid") else: # error update Auth key auth_keys = config.AUTH_KEYS auth_keys[engine] = None 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 _: if connected_network is True: config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = True diff --git a/src-python/docs/details/config.md b/src-python/docs/details/config.md index fd619d77..1c008b03 100644 --- a/src-python/docs/details/config.md +++ b/src-python/docs/details/config.md @@ -389,4 +389,61 @@ config.saveConfig("ENABLE_TRANSLATION", True, immediate_save=True) - 設定ファイルの適切な権限管理 - 外部入力値の検証 - APIキー等の機密情報の適切な取り扱い -- パスインジェクション攻撃の防止 \ No newline at end of file +- パスインジェクション攻撃の防止 + +## 最近の更新 (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検知 | 安全停止と軽量エンジン切替で安定性向上 | +| テスト増強 | 大量ペア検証で言語/モデル設定の信頼性向上 | diff --git a/src-python/docs/details/controller.md b/src-python/docs/details/controller.md index ca1fefda..3dd3e73b 100644 --- a/src-python/docs/details/controller.md +++ b/src-python/docs/details/controller.md @@ -4,25 +4,88 @@ 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オーバーレイの管理 - WebSocketサーバーの制御 ### 設定管理 + - アプリケーション設定の取得・更新 - デバイス設定の管理 - 言語・エンジン設定の制御 ### 状態管理 + - システム状態の監視 - エラー状態の管理 - 初期化プロセスの制御 ### 通信制御 + - OSC通信の管理 - WebSocket通信の制御 - 外部アプリケーション連携 @@ -30,6 +93,7 @@ VRCTアプリケーションのビジネスロジックを制御するコント ## クラス構造 ### Controller クラス + ```python class Controller: def __init__(self) -> None @@ -40,19 +104,23 @@ class Controller: ### 内部ヘルパークラス #### DownloadCTranslate2 クラス + ```python class DownloadCTranslate2: def progressBar(self, progress) -> None def downloaded(self) -> None ``` + - 翻訳モデルのダウンロード進捗管理 -#### DownloadWhisper クラス +#### DownloadWhisper クラス + ```python class DownloadWhisper: def progressBar(self, progress) -> None def downloaded(self) -> None ``` + - 音声認識モデルのダウンロード進捗管理 ## 主要メソッド @@ -62,6 +130,7 @@ class DownloadWhisper: ```python init() -> None ``` + - コントローラーの初期化 - 各コンポーネントの起動 - 初期設定の適用 @@ -71,6 +140,7 @@ setInitMapping(init_mapping: dict) -> None setRunMapping(run_mapping: dict) -> None setRun(run: Callable) -> None ``` + - エンドポイント・コールバック設定 ### 翻訳機能制御 @@ -79,23 +149,27 @@ setRun(run: Callable) -> None setEnableTranslation(data) -> dict setDisableTranslation(data) -> dict ``` + - 翻訳機能の有効化・無効化 ```python setSelectedTranslationEngines(data) -> dict getSelectedTranslationEngines(data) -> dict ``` + - 翻訳エンジンの選択・取得 ```python setSelectedYourLanguages(data) -> dict setSelectedTargetLanguages(data) -> dict ``` + - 送信・受信言語の設定 ```python sendMessageBox(data) -> dict ``` + - メッセージの翻訳・送信処理 ### 音声認識機能制御 @@ -104,24 +178,28 @@ sendMessageBox(data) -> dict setEnableTranscriptionSend(data) -> dict setEnableTranscriptionReceive(data) -> dict ``` + - 音声認識機能の有効化 ```python setSelectedTranscriptionEngine(data) -> dict getSelectedTranscriptionEngine(data) -> dict ``` + - 音声認識エンジンの選択・取得 ```python setSelectedMicDevice(data) -> dict setSelectedSpeakerDevice(data) -> dict ``` + - 音声デバイスの選択 ```python setMicThreshold(data) -> dict setSpeakerThreshold(data) -> dict ``` + - 音声しきい値の設定 ### VRオーバーレイ制御 @@ -130,12 +208,14 @@ setSpeakerThreshold(data) -> dict setEnableOverlaySmallLog(data) -> dict setEnableOverlayLargeLog(data) -> dict ``` + - VRオーバーレイの有効化 ```python setOverlaySmallLogSettings(data) -> dict setOverlayLargeLogSettings(data) -> dict ``` + - オーバーレイ設定の更新 ### WebSocket制御 @@ -144,12 +224,14 @@ setOverlayLargeLogSettings(data) -> dict setEnableWebSocketServer(data) -> dict setDisableWebSocketServer(data) -> dict ``` + - WebSocketサーバーの制御 ```python setWebSocketHost(data) -> dict setWebSocketPort(data) -> dict ``` + - WebSocket接続設定 ### システム管理 @@ -158,17 +240,20 @@ setWebSocketPort(data) -> dict updateSoftware(data) -> dict updateCudaSoftware(data) -> dict ``` + - ソフトウェアアップデート ```python downloadCtranslate2Weight(data) -> dict downloadWhisperWeight(data) -> dict ``` + - AIモデルのダウンロード ```python feedWatchdog(data) -> dict ``` + - ウォッチドッグの生存シグナル送信 ## 使用方法 @@ -233,6 +318,7 @@ result = controller.setEnableTranscriptionSend(None) ``` ### 成功レスポンス例 + ```python { "status": 200, @@ -241,6 +327,7 @@ result = controller.setEnableTranscriptionSend(None) ``` ### エラーレスポンス例 + ```python { "status": 400, @@ -248,19 +335,22 @@ result = controller.setEnableTranscriptionSend(None) } ``` -## 状態管理 +## 詳細状態管理 ### システム状態 + - 各機能の有効・無効状態 - デバイスの接続状態 - ネットワーク接続状態 -### エラー状態 +### エラー状態 + - デバイスエラー - 翻訳エンジンエラー - VRAMオーバーフローエラー ### 初期化状態 + - 段階的な初期化プロセス - 依存関係の解決状態 @@ -271,32 +361,38 @@ result = controller.setEnableTranscriptionSend(None) ```python micMessage(result: dict) -> None ``` + - マイク音声認識結果の処理 - 翻訳・フィルタリング・送信 ```python -speakerMessage(result: dict) -> None +speakerMessage(result: dict) -> None ``` + - スピーカー音声認識結果の処理 ### ダウンロードイベント + - 進捗通知 -- 完了通知 +- 完了通知 - エラー通知 ### デバイス変更イベント + - マイク・スピーカーの選択変更 - 計算デバイスの変更 ## 依存関係 ### 直接依存 + - `config`: 設定管理 -- `model`: コアモデル機能 +- `model`: コアモデル機能 - `device_manager`: デバイス管理 - `utils`: ユーティリティ機能 ### 間接依存 + - 各種モデルモジュール(翻訳、音声認識等) - VRオーバーレイモジュール - 通信モジュール @@ -304,32 +400,39 @@ speakerMessage(result: dict) -> None ## エラーハンドリング ### VRAM不足エラー + - 自動的にCTranslate2への切り替え - ユーザーへの適切な通知 ### デバイスエラー + - デバイス接続状態の監視 - 自動復旧機能 -### ネットワークエラー +### ネットワークエラー + - 接続状態の定期確認 - オフライン機能への切り替え ### 設定エラー + - 設定値の妥当性チェック - デフォルト値への復帰 ## パフォーマンス考慮事項 ### 遅延初期化 + - 必要な時点での機能初期化 - メモリ使用量の最適化 ### 非同期処理 + - バックグラウンドでの重い処理 - UI の応答性維持 ### キャッシュ機能 + - 設定値のキャッシュ - 翻訳結果のキャッシュ @@ -346,4 +449,4 @@ speakerMessage(result: dict) -> None - 外部入力の適切な検証 - APIキーの安全な管理 - ファイルアクセスの制限 -- ネットワーク通信の暗号化(該当する場合) \ No newline at end of file +- ネットワーク通信の暗号化(該当する場合) diff --git a/src-python/docs/details/mainloop.md b/src-python/docs/details/mainloop.md index 22a086ed..02175264 100644 --- a/src-python/docs/details/mainloop.md +++ b/src-python/docs/details/mainloop.md @@ -4,19 +4,61 @@ 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/` に正規化し排他制御強化 +- ロック取得失敗時は再キュー投入し軽量リトライでデッドロック防止 + +### 並列ワーカー処理の安定化 + +- ハンドラ処理後に短い `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形式の標準入力からのリクエスト受信 - エンドポイントベースのルーティング - 非同期・並列処理対応 ### エンドポイント管理 + - RESTライクなエンドポイント構造 - 機能別のエンドポイント分類 - 排他制御によるスレッドセーフティ ### 初期化システム + - アプリケーション設定の初期化 - コンポーネント間の依存関係解決 - 段階的な機能有効化 @@ -24,6 +66,7 @@ VRCTアプリケーションのメインイベントループを管理するモ ## クラス構造 ### Main クラス + ```python class Main: def __init__(self, controller_instance: Controller, mapping_data: dict, worker_count: int = 3) @@ -36,46 +79,54 @@ class Main: ## エンドポイント分類 ### 機能制御系 -``` + +```text /set/enable/* - 各機能の有効化 /set/disable/* - 各機能の無効化 ``` ### データ操作系 -``` + +```text /get/data/* - 設定データの取得 /set/data/* - 設定データの更新 /delete/data/* - データの削除 ``` ### 実行系 -``` + +```text /run/* - 各種処理の実行 ``` ## 主要エンドポイント ### 翻訳機能 + - `/set/enable/translation`: 翻訳機能の有効化 - `/set/disable/translation`: 翻訳機能の無効化 - `/set/data/selected_translation_engines`: 翻訳エンジンの選択 - `/run/send_message_box`: メッセージ送信 ### 音声認識機能 + - `/set/enable/transcription_send`: 送信音声認識の有効化 - `/set/enable/transcription_receive`: 受信音声認識の有効化 - `/set/data/selected_transcription_engine`: 音声認識エンジン選択 ### VR機能 + - `/set/data/overlay_small_log_settings`: 小型オーバーレイ設定 - `/set/data/overlay_large_log_settings`: 大型オーバーレイ設定 ### WebSocket機能 + - `/set/enable/websocket_server`: WebSocketサーバー有効化 - `/set/data/websocket_host`: サーバーホスト設定 - `/set/data/websocket_port`: サーバーポート設定 ### システム管理 + - `/run/update_software`: ソフトウェアアップデート - `/run/download_ctranslate2_weight`: 翻訳モデルダウンロード - `/run/download_whisper_weight`: 音声認識モデルダウンロード @@ -87,18 +138,21 @@ class Main: ```python receiver() -> None ``` + - 標準入力からのJSONリクエスト受信 - パースエラーの適切な処理 ```python handleRequest(endpoint: str, data: Any = None) -> tuple ``` + - エンドポイント処理の実行 - ステータスコードと結果の返却 ```python handler() -> None ``` + - ワーカースレッドのメイン処理 - キューからのリクエスト取得・処理 @@ -107,21 +161,25 @@ handler() -> None ```python startReceiver() -> None ``` + - レシーバースレッドの起動 ```python startHandler() -> None ``` + - ハンドラースレッドプールの起動 ```python start() -> None ``` + - 全スレッドの起動 ```python stop(wait: float = 2.0) -> None ``` + - 全スレッドの安全な停止 ## 使用方法 @@ -164,6 +222,7 @@ result, status = main_instance.handleRequest("/set/enable/translation", None) ## リクエスト形式 ### 入力形式 + ```json { "endpoint": "string", // 必須:処理対象のエンドポイント @@ -172,6 +231,7 @@ result, status = main_instance.handleRequest("/set/enable/translation", None) ``` ### 出力形式 + ```json { "status": 200, // HTTPステータスコード @@ -191,11 +251,13 @@ result, status = main_instance.handleRequest("/set/enable/translation", None) ## 排他制御 ### ロック機能 + - enable/disableペアは同一ロックキーを共有 - 同一機能の同時実行を防止 - デッドロックを回避する設計 ### ロックキー正規化 + ```python /set/enable/translation -> /lock/set/translation /set/disable/translation -> /lock/set/translation @@ -204,32 +266,38 @@ result, status = main_instance.handleRequest("/set/enable/translation", None) ## 初期化プロセス ### 段階的初期化 + 1. コントローラーの初期化 2. デバイスマネージャーの初期化 3. モデルの初期化 4. 各機能の段階的有効化 ### 初期化mapping + - `/get/data/*`エンドポイントから初期化設定を自動抽出 - システム起動時の設定復元 ## ログ機能 ### プロセスログ + - 全リクエスト・レスポンスの記録 - JSON形式での構造化ログ ### エラーログ + - 例外の詳細記録 - スタックトレースの保存 ## 依存関係 ### 直接依存 + - `controller`: ビジネスロジック制御 - `utils`: ユーティリティ機能(ログ、エンコード等) ### 間接依存 + - `config`: 設定管理 - `model`: コアモデル機能 - `device_manager`: デバイス管理 @@ -237,11 +305,13 @@ result, status = main_instance.handleRequest("/set/enable/translation", None) ## 設定項目 ### ワーカー数 + ```python DEFAULT_WORKER_COUNT = 3 # 並列処理スレッド数 ``` ### タイムアウト + - キュー待機タイムアウト: 0.5秒 - スレッド停止待機: 2.0秒 - 処理安定化待機: 0.2秒 @@ -256,14 +326,17 @@ DEFAULT_WORKER_COUNT = 3 # 並列処理スレッド数 ## パフォーマンス特性 ### スループット + - 複数ワーカーによる並列処理 - ノンブロッキングI/O ### レイテンシ + - キューイング遅延の最小化 - 排他制御による一時的な遅延あり ### メモリ使用量 + - リクエストキューのサイズ制限なし(要注意) - スレッドプールによる固定オーバーヘッド @@ -272,4 +345,4 @@ DEFAULT_WORKER_COUNT = 3 # 並列処理スレッド数 - 標準入力をブロッキングで読み取るため、パイプ経由での使用を想定 - エンドポイント名の大文字小文字は区別される - Base64データは自動的にデコードされる -- 長時間のブロッキング処理は他のリクエストに影響する可能性 \ No newline at end of file +- 長時間のブロッキング処理は他のリクエストに影響する可能性 diff --git a/src-python/docs/details/model.md b/src-python/docs/details/model.md index 9880730c..9a7da932 100644 --- a/src-python/docs/details/model.md +++ b/src-python/docs/details/model.md @@ -4,40 +4,95 @@ 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インスタンスを保証 - 遅延初期化による軽量なインポート ### 音声認識機能 + - マイク音声のリアルタイム文字起こし - スピーカー出力の音声認識 - エネルギーレベル監視 - 複数言語対応 ### 翻訳機能 + - 複数の翻訳エンジン対応(DeepL、Google、CTranslate2等) - 言語自動検出 - バッチ翻訳処理 ### VRオーバーレイ + - OpenVR統合 - 小型・大型ログオーバーレイ - 動的配置・透明度制御 ### OSC通信 + - VRChatとのOSC通信 - タイピング状態の同期 - ミュート状態の監視 ### WebSocketサーバー + - 外部アプリケーションとの通信 - リアルタイムメッセージ配信 ## クラス構造 ### threadFnc クラス + ```python class threadFnc(Thread): def __init__(self, fnc, end_fnc=None, daemon: bool = True, *args, **kwargs) @@ -48,6 +103,7 @@ class threadFnc(Thread): - エラー保護機能 ### Model クラス + ```python class Model: def __new__(cls) # シングルトンパターン @@ -62,56 +118,65 @@ class Model: ```python init() -> None ``` + - 全コンポーネントの初期化 - 重い処理のため明示的に呼び出し ```python ensure_initialized() -> None ``` + - 必要時の自動初期化 - 安全な遅延初期化 -### 翻訳機能 +### 翻訳機能メソッド ```python getInputTranslate(message, source_language=None) -> Tuple[List[str], List[bool]] ``` + - 入力メッセージの多言語翻訳 - 成功フラグも同時に返却 ```python getOutputTranslate(message, source_language=None) -> Tuple[List[str], List[bool]] ``` + - 出力メッセージの翻訳(逆方向) ```python authenticationTranslatorDeepLAuthKey(auth_key) -> bool ``` + - DeepL APIキーの認証 -### 音声認識機能 +### 音声認識機能メソッド ```python startMicTranscript(fnc: Callable) -> None ``` + - マイク音声認識の開始 - コールバック関数で結果を通知 ```python startSpeakerTranscript(fnc: Callable) -> None ``` + - スピーカー音声認識の開始 ```python pauseMicTranscript() -> None resumeMicTranscript() -> None ``` + - 音声認識の一時停止・再開 ```python startCheckMicEnergy(fnc: Callable) -> None startCheckSpeakerEnergy(fnc: Callable) -> None ``` + - 音声エネルギーレベルの監視 ### VRオーバーレイ機能 @@ -119,17 +184,20 @@ startCheckSpeakerEnergy(fnc: Callable) -> None ```python createOverlayImageSmallLog(message, your_language, translation, target_language) -> Image ``` + - 小型ログオーバーレイ画像の生成 ```python createOverlayImageLargeLog(message_type, message, your_language, translation, target_language) -> Image ``` + - 大型ログオーバーレイ画像の生成 ```python updateOverlaySmallLogSettings() -> None updateOverlayLargeLogSettings() -> None ``` + - オーバーレイ設定の更新 ### OSC通信機能 @@ -137,17 +205,20 @@ updateOverlayLargeLogSettings() -> None ```python oscSendMessage(message: str) -> None ``` + - VRChatへのメッセージ送信 ```python oscStartSendTyping() -> None oscStopSendTyping() -> None ``` + - タイピング状態の通知 ```python setMuteSelfStatus() -> None ``` + - VRChatミュート状態の取得 ### WebSocket機能 @@ -155,16 +226,19 @@ setMuteSelfStatus() -> None ```python startWebSocketServer(host: str, port: int) -> None ``` + - WebSocketサーバーの起動 ```python websocketSendMessage(message_dict: dict) -> bool ``` + - 全クライアントへのメッセージ送信 ```python checkWebSocketServerAlive() -> bool ``` + - サーバー稼働状態の確認 ### ファイルダウンロード機能 @@ -172,11 +246,13 @@ checkWebSocketServerAlive() -> bool ```python downloadCTranslate2ModelWeight(weight_type, callback=None, end_callback=None) ``` + - 翻訳モデルのダウンロード ```python downloadWhisperModelWeight(weight_type, callback=None, end_callback=None) ``` + - 音声認識モデルのダウンロード ### ウォッチドッグ機能 @@ -186,6 +262,7 @@ startWatchdog() -> None feedWatchdog() -> None setWatchdogCallback(callback: Callable) -> None ``` + - システム監視とタイムアウト処理 ## 使用方法 @@ -241,21 +318,25 @@ success = model.websocketSendMessage(message) ## 依存関係 ### 必須モジュール + - `controller`: アプリケーション制御 - `config`: 設定管理 - `device_manager`: デバイス管理 ### 音声・翻訳関連 + - `models.transcription.*`: 音声認識 - `models.translation.*`: 翻訳機能 - `models.transliteration.*`: 音写変換 ### VR・通信関連 + - `models.overlay.*`: VRオーバーレイ - `models.osc.*`: OSC通信 - `models.websocket.*`: WebSocket通信 ### ユーティリティ + - `models.watchdog.*`: 監視機能 - `utils`: 共通ユーティリティ - `flashtext`: キーワードフィルタリング @@ -289,4 +370,4 @@ success = model.websocketSendMessage(message) - 遅延初期化によるメモリ使用量の最適化 - スレッドプールによる並行処理 - モデルの重複読み込み防止 -- キューイングによる非同期処理 \ No newline at end of file +- キューイングによる非同期処理 diff --git a/src-python/docs/details/overlay.md b/src-python/docs/details/overlay.md index e28382a0..51970cee 100644 --- a/src-python/docs/details/overlay.md +++ b/src-python/docs/details/overlay.md @@ -745,10 +745,31 @@ platform_limitations = { - `model.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要素対応 - アニメーション・エフェクト機能 - カスタムフォント・テーマシステム - パフォーマンス監視・自動最適化 -- 他のVRプラットフォーム対応検討 \ No newline at end of file +- 他のVRプラットフォーム対応検討 diff --git a/src-python/docs/details/translation_gemini.md b/src-python/docs/details/translation_gemini.md new file mode 100644 index 00000000..29ef16c5 --- /dev/null +++ b/src-python/docs/details/translation_gemini.md @@ -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` + diff --git a/src-python/docs/details/translation_languages.md b/src-python/docs/details/translation_languages.md index 9943e3e6..64d5f8d3 100644 --- a/src-python/docs/details/translation_languages.md +++ b/src-python/docs/details/translation_languages.md @@ -7,6 +7,7 @@ ## 主要機能 ### 多エンジン対応 + - DeepL(無料版・API版) - Google Translate - Microsoft Translator(Bing) @@ -14,6 +15,7 @@ - その他のWeb翻訳サービス ### 言語コード統合管理 + - 各エンジン固有の言語コード形式を統一 - 送信元(source)と送信先(target)言語の分離管理 - 地域固有言語バリエーションの対応 @@ -21,6 +23,7 @@ ## データ構造 ### translation_lang + ```python translation_lang: Dict[str, Dict[str, Dict[str, str]]] = { "エンジン名": { @@ -52,7 +55,7 @@ translation_lang["DeepL"] = { } ``` -### DeepL API(有料版) +### DeepL API(有料版 概要) ```python translation_lang["DeepL_API"] = { @@ -73,6 +76,7 @@ translation_lang["DeepL_API"] = { ## 主要対応言語 ### 西欧言語 + - **English**: 英語(米国・英国バリエーション) - **German**: ドイツ語 - **French**: フランス語 @@ -84,6 +88,7 @@ translation_lang["DeepL_API"] = { - **Norwegian**: ノルウェー語 ### 東欧・スラブ言語 + - **Russian**: ロシア語 - **Polish**: ポーランド語 - **Czech**: チェコ語 @@ -94,6 +99,7 @@ translation_lang["DeepL_API"] = { - **Slovenian**: スロベニア語 ### アジア言語 + - **Japanese**: 日本語 - **Korean**: 韓国語 - **Chinese Simplified**: 中国語(簡体字) @@ -101,6 +107,7 @@ translation_lang["DeepL_API"] = { - **Indonesian**: インドネシア語 ### その他の言語 + - **Arabic**: アラビア語 - **Turkish**: トルコ語 - **Finnish**: フィンランド語 @@ -218,21 +225,25 @@ en_code = manager.get_language_code("DeepL", "English", "target") ## エンジン別特徴 ### DeepL(無料版) + - **強み**: 高精度、自然な翻訳 - **制限**: 月間使用量制限、API制限 - **対応**: 26言語 -### DeepL API(有料版) +### DeepL API(有料版) + - **強み**: DeepLの高精度、地域別言語対応 - **制限**: 従量課金 - **対応**: 地域固有言語バリエーション ### Google Translate + - **強み**: 多言語対応、高速 - **制限**: API制限、精度のばらつき - **対応**: 100+言語 ### Microsoft Translator + - **強み**: リアルタイム翻訳、音声対応 - **制限**: APIキー必要 - **対応**: 70+言語 @@ -240,6 +251,7 @@ en_code = manager.get_language_code("DeepL", "English", "target") ## 地域バリエーション対応 ### 英語の地域別対応 + ```python # DeepL APIでの英語バリエーション "English American": "en-US", # アメリカ英語 @@ -247,6 +259,7 @@ en_code = manager.get_language_code("DeepL", "English", "target") ``` ### ポルトガル語の地域別対応 + ```python # ブラジル・ポルトガル語とヨーロッパ・ポルトガル語 "Portuguese Brazilian": "pt-BR", @@ -254,6 +267,7 @@ en_code = manager.get_language_code("DeepL", "English", "target") ``` ### 中国語の文字体系対応 + ```python # 簡体字・繁体字の区別 "Chinese Simplified": "zh", # 簡体字(中国本土) @@ -263,6 +277,7 @@ en_code = manager.get_language_code("DeepL", "English", "target") ## 拡張性 ### 新エンジンの追加 + ```python # 新しい翻訳エンジンの追加例 translation_lang["NewEngine"] = { @@ -280,6 +295,7 @@ translation_lang["NewEngine"] = { ``` ### 新言語の追加 + ```python # 既存エンジンへの新言語追加 translation_lang["DeepL"]["source"]["Hindi"] = "hi" @@ -289,6 +305,7 @@ translation_lang["DeepL"]["target"]["Hindi"] = "hi" ## エラーハンドリング ### 安全な言語コード取得 + ```python 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 def validate_translation_pair(engine, source_lang, target_lang): """翻訳ペアの有効性検証""" @@ -339,4 +357,33 @@ def validate_translation_pair(engine, source_lang, target_lang): - `translation_utils.py`: 翻訳ユーティリティ - `transcription_languages.py`: 音声認識言語マッピング - `config.py`: 翻訳言語設定管理 -- `controller.py`: 言語選択UI制御 \ No newline at end of file +- `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/設定整合性向上 | diff --git a/src-python/docs/details/translation_lmstudio.md b/src-python/docs/details/translation_lmstudio.md new file mode 100644 index 00000000..b54d46a8 --- /dev/null +++ b/src-python/docs/details/translation_lmstudio.md @@ -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` + diff --git a/src-python/docs/details/translation_ollama.md b/src-python/docs/details/translation_ollama.md new file mode 100644 index 00000000..d176644e --- /dev/null +++ b/src-python/docs/details/translation_ollama.md @@ -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` + diff --git a/src-python/docs/details/translation_openai.md b/src-python/docs/details/translation_openai.md new file mode 100644 index 00000000..2b99f0c3 --- /dev/null +++ b/src-python/docs/details/translation_openai.md @@ -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` + diff --git a/src-python/docs/details/translation_plamo.md b/src-python/docs/details/translation_plamo.md new file mode 100644 index 00000000..9b6bd5e1 --- /dev/null +++ b/src-python/docs/details/translation_plamo.md @@ -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` + diff --git a/src-python/docs/details/translation_translator.md b/src-python/docs/details/translation_translator.md index 970385ae..fd423213 100644 --- a/src-python/docs/details/translation_translator.md +++ b/src-python/docs/details/translation_translator.md @@ -403,4 +403,50 @@ root/ - `translation_utils.py`: CTranslate2ユーティリティ - `config.py`: 翻訳設定管理 - `model.py`: 翻訳機能統合 -- `controller.py`: 翻訳制御インターフェース \ No newline at end of file +- `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修正 | 初回セットアップ失敗減少 | +| 包括テスト | 言語組合せの網羅的品質担保 | diff --git a/src-python/docs/仕様書.md b/src-python/docs/仕様書.md index 1ccd8ea2..366e4afb 100644 --- a/src-python/docs/仕様書.md +++ b/src-python/docs/仕様書.md @@ -1,15 +1,18 @@ # 仕様書 概要 + - プロジェクト名: VRCT (VR Chat Translator) - 目的: マイク入力とスピーカー出力をリアルタイムに文字起こし・翻訳し、VR オーバーレイや OSC/WebSocket 経由で外部に送出するバックエンドロジック。 - 言語: Python 対象ユーザー + - VR 環境でリアルタイム翻訳・文字起こしを利用したいエンドユーザー - フロントエンド(GUI)や VR クライアント(OSC)と連携するアプリケーション開発者 主要機能(機能要件) + 1. 音声の取り込み・文字起こし - マイク(送信)およびスピーカー(受信)から音声を取得し、ローカル Whisper(faster-whisper)または外部サービスによりテキスト化する。 - 音声エネルギー(音量)監視を行い、閾値ベースで検出する。 @@ -36,23 +39,94 @@ - ウォッチドッグ機構で定期的に死活チェック・コールバック。 非機能要件 + - プラットフォーム: 主に Windows(Audio 周りは WASAPI を利用)を想定。クロスプラットフォームでの import 安全性を考慮。 - 可用性: 外部依存(PyAudio, CUDA, ctranslate2 等)が無い環境でも安全にインポートでき、機能劣化しつつ動作する。 - パフォーマンス: ローカルモデル利用時は GPU を利用して計算性能を確保。compute type 選択ロジックを実装。 - セキュリティ: 外部への API キー(DeepL など)は設定で扱い、コード上では平文保持を避ける(設定ファイルに保存)。 運用フロー + - 起動: stdin でコマンドを受け付ける mainloop を実行。必要な初期化は遅延実行(lazy init)を採用。 - モデル重ダウンロード: CTranslate2/Whisper 重みは `weights/` 配下にダウンロードし、チェックサム等で整合性確認。 - 障害時: 例外は utils.errorLogging() でトレースを error.log に出力。重要機能はフォールバック実装。 インターフェース(抜粋) -- stdin(JSON): {"endpoint": "/set/..." | "/get/..." | "/run/...", "data": } + +- stdin(JSON): {"endpoint": "/set/..." | "/get/..." | "/run/...", "data": } - stdout(JSON): 標準化されたレスポンスを printResponse/printLog が出力(status, endpoint, result など)。 依存関係(オプション含む) + - 必須(実装時想定): requests, packaging, flashtext, pillow, pyaudiowpatch, speech_recognition - ローカル推奨: faster-whisper, ctranslate2, torch(GPU 利用時) - Windows 固有(音声ループバック): pycaw, comtypes 参考: 実装上の安全設計として 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 ライセンス許容範囲内)。 diff --git a/src-python/mainloop.py b/src-python/mainloop.py index 0ad28507..f4c3a451 100644 --- a/src-python/mainloop.py +++ b/src-python/mainloop.py @@ -52,6 +52,17 @@ run_mapping = { "selected_translation_compute_type":"/run/selected_translation_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_device_list":"/run/mic_device_list", "speaker_device_list":"/run/speaker_device_list", @@ -175,6 +186,40 @@ mapping = { "/set/data/deepl_auth_key": {"status": False, "variable":controller.setDeeplAuthKey}, "/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}, "/set/enable/convert_message_to_romaji": {"status": True, "variable":controller.setEnableConvertMessageToRomaji}, "/set/disable/convert_message_to_romaji": {"status": True, "variable":controller.setDisableConvertMessageToRomaji}, diff --git a/src-python/model.py b/src-python/model.py index 140e45b5..e8d35aec 100644 --- a/src-python/model.py +++ b/src-python/model.py @@ -193,11 +193,94 @@ class Model: del self.keyword_processor self.keyword_processor = KeywordProcessor() - def authenticationTranslatorDeepLAuthKey(self, auth_key): + def authenticationTranslatorDeepLAuthKey(self, auth_key: str) -> bool: self.ensure_initialized() result = self.translator.authenticationDeepLAuthKey(auth_key) 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): self.ensure_initialized() os_makedirs(config.PATH_LOGS, exist_ok=True) @@ -214,8 +297,12 @@ class Model: transcription_langs = list(transcription_lang.keys()) translation_langs = [] for tl_key in translation_lang.keys(): - for lang in translation_lang[tl_key]["source"]: - translation_langs.append(lang) + 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"]: + translation_langs.append(lang) translation_langs = list(set(translation_langs)) supported_langs = list(filter(lambda x: x in transcription_langs, translation_langs)) @@ -235,7 +322,10 @@ class Model: selectable_engines = [key for key, value in engines_status.items() if value is True] compatible_engines = [] for engine in list(translation_lang.keys()): - languages = translation_lang.get(engine, {}).get("source", {}) + if engine == "CTranslate2": + languages = translation_lang.get(engine, {}).get(config.CTRANSLATE2_WEIGHT_TYPE, {}).get("source", {}) + else: + languages = translation_lang.get(engine, {}).get("source", {}) 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] language_list = list(languages.keys()) @@ -251,6 +341,7 @@ class Model: success_flag = False translation = self.translator.translate( translator_name=translator_name, + weight_type=config.CTRANSLATE2_WEIGHT_TYPE, source_language=source_language, target_language=target_language, target_country=target_country, @@ -264,6 +355,7 @@ class Model: while True: translation = self.translator.translate( translator_name="CTranslate2", + weight_type=config.CTRANSLATE2_WEIGHT_TYPE, source_language=source_language, target_language=target_language, target_country=target_country, diff --git a/fonts/NotoSansJP-Regular.ttf b/src-python/models/overlay/fonts/NotoSansJP-Regular.ttf similarity index 100% rename from fonts/NotoSansJP-Regular.ttf rename to src-python/models/overlay/fonts/NotoSansJP-Regular.ttf diff --git a/fonts/NotoSansKR-Regular.ttf b/src-python/models/overlay/fonts/NotoSansKR-Regular.ttf similarity index 100% rename from fonts/NotoSansKR-Regular.ttf rename to src-python/models/overlay/fonts/NotoSansKR-Regular.ttf diff --git a/fonts/NotoSansSC-Regular.ttf b/src-python/models/overlay/fonts/NotoSansSC-Regular.ttf similarity index 100% rename from fonts/NotoSansSC-Regular.ttf rename to src-python/models/overlay/fonts/NotoSansSC-Regular.ttf diff --git a/fonts/NotoSansTC-Regular.ttf b/src-python/models/overlay/fonts/NotoSansTC-Regular.ttf similarity index 100% rename from fonts/NotoSansTC-Regular.ttf rename to src-python/models/overlay/fonts/NotoSansTC-Regular.ttf diff --git a/src-python/models/overlay/overlay_image.py b/src-python/models/overlay/overlay_image.py index 21520278..d307411e 100644 --- a/src-python/models/overlay/overlay_image.py +++ b/src-python/models/overlay/overlay_image.py @@ -26,10 +26,17 @@ class OverlayImage: defaults to repository `fonts` directory. """ self.message_log: List[dict] = [] - if root_path is None: - self.root_path = os_path.join(os_path.dirname(__file__), "..", "..", "..", "fonts") - else: + # PyInstallerでビルドされた場合のパス + if root_path and os_path.exists(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 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)) draw = ImageDraw.Draw(img) - try: - font_path = os_path.join(self.root_path, font_family) - 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}") + font_path = os_path.join(self.root_path, font_family) + font = ImageFont.truetype(font_path, font_size) text_width = draw.textlength(text, font) character_width = text_width // len(text) @@ -180,18 +175,8 @@ class OverlayImage: img = Image.new("RGBA", (0, 0), (0, 0, 0, 0)) draw = ImageDraw.Draw(img) - try: - font_path = os_path.join(self.root_path, font_family) - 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}") + font_path = os_path.join(self.root_path, font_family) + font = ImageFont.truetype(font_path, font_size) # 改行を含んだtextの最大の文字数を計算する 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)) draw = ImageDraw.Draw(img) - try: - font_path = os_path.join(self.root_path, self.LANGUAGES["Default"]) - 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']}") + font_path = os_path.join(self.root_path, self.LANGUAGES["Default"]) + font = ImageFont.truetype(font_path, font_size) text_height = font_size + ui_padding text_width = draw.textlength(date_time, font) diff --git a/src-python/models/translation/languages/languages.yml b/src-python/models/translation/languages/languages.yml new file mode 100644 index 00000000..cfde921e --- /dev/null +++ b/src-python/models/translation/languages/languages.yml @@ -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 diff --git a/src-python/models/translation/prompt/translation_gemini.yml b/src-python/models/translation/prompt/translation_gemini.yml new file mode 100644 index 00000000..8dbe4927 --- /dev/null +++ b/src-python/models/translation/prompt/translation_gemini.yml @@ -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. \ No newline at end of file diff --git a/src-python/models/translation/prompt/translation_lmstudio.yml b/src-python/models/translation/prompt/translation_lmstudio.yml new file mode 100644 index 00000000..8dbe4927 --- /dev/null +++ b/src-python/models/translation/prompt/translation_lmstudio.yml @@ -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. \ No newline at end of file diff --git a/src-python/models/translation/prompt/translation_ollama.yml b/src-python/models/translation/prompt/translation_ollama.yml new file mode 100644 index 00000000..8dbe4927 --- /dev/null +++ b/src-python/models/translation/prompt/translation_ollama.yml @@ -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. \ No newline at end of file diff --git a/src-python/models/translation/prompt/translation_openai.yml b/src-python/models/translation/prompt/translation_openai.yml new file mode 100644 index 00000000..8dbe4927 --- /dev/null +++ b/src-python/models/translation/prompt/translation_openai.yml @@ -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. \ No newline at end of file diff --git a/src-python/models/translation/prompt/translation_plamo.yml b/src-python/models/translation/prompt/translation_plamo.yml new file mode 100644 index 00000000..c0afe533 --- /dev/null +++ b/src-python/models/translation/prompt/translation_plamo.yml @@ -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. \ No newline at end of file diff --git a/src-python/models/translation/translation_gemini.py b/src-python/models/translation/translation_gemini.py new file mode 100644 index 00000000..1c8a0161 --- /dev/null +++ b/src-python/models/translation/translation_gemini.py @@ -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")) \ No newline at end of file diff --git a/src-python/models/translation/translation_languages.py b/src-python/models/translation/translation_languages.py index 804b2921..eccd5665 100644 --- a/src-python/models/translation/translation_languages.py +++ b/src-python/models/translation/translation_languages.py @@ -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 -`target` maps used by `Translator.getLanguageCode`. +YAML ファイル: languages/languages.yml +構造: + : + source: { DisplayName: Code, ... } + target: { DisplayName: Code, ... } + CTranslate2: + : + 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]]] = {} +_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 = { - "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":"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} +def _load_languages(path: str, filename: str) -> str: + """Get absolute path to resource file relative to this module. -dict_google_languages = { - "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", - "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} + Args: + filename: relative filename from this module's directory -dict_bing_languages = { - "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", - "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} + Returns: + Absolute path to the resource file + """ + if os.path.exists(os.path.join(path, "_internal", "languages", "languages.yml")): + languages_path = os.path.join(path, "_internal", "languages", "languages.yml") + elif os.path.exists(os.path.join(os.path.dirname(os.path.abspath(__file__)), "models", "translation", "languages", "languages.yml")): + languages_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "models", "translation", "languages", "languages.yml") + elif os.path.exists(os.path.join(os.path.dirname(os.path.abspath(__file__)), "languages", "languages.yml")): + languages_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "languages", "languages.yml") + else: + raise FileNotFoundError(f"Prompt file not found: {filename}") + with open(languages_path, "r", encoding="utf-8") as f: + return yaml.safe_load(f) -dict_papago_languages = { - "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", -} +def _validate_source_target(backend: str, mapping: Any) -> None: + """Validate that a backend mapping has proper source/target structure. -translation_lang["Papago"] = {"source": dict_papago_languages, "target": dict_papago_languages} + Args: + backend: backend name for error messages + mapping: mapping to validate -dict_ctranslate2_languages = { - "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" -} + Raises: + ValueError: If mapping structure is invalid + """ + if not isinstance(mapping, dict): + raise ValueError(f"{backend}: 値は dict である必要があります。") + if "source" not in mapping or "target" not in mapping: + raise ValueError(f"{backend}: 'source' と 'target' が必要です。") -translation_lang["CTranslate2"] = {"source": dict_ctranslate2_languages, "target": dict_ctranslate2_languages} \ No newline at end of file + 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 = validated + _loaded = True + return translation_lang + +if __name__ == "__main__": + try: + langs = loadTranslationLanguages(path=".", force=True) + printLog("Loaded translation languages:") + printLog(langs) + except Exception: + errorLogging() \ No newline at end of file diff --git a/src-python/models/translation/translation_lmstudio.py b/src-python/models/translation/translation_lmstudio.py new file mode 100644 index 00000000..7751dc16 --- /dev/null +++ b/src-python/models/translation/translation_lmstudio.py @@ -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")) \ No newline at end of file diff --git a/src-python/models/translation/translation_ollama.py b/src-python/models/translation/translation_ollama.py new file mode 100644 index 00000000..4152c953 --- /dev/null +++ b/src-python/models/translation/translation_ollama.py @@ -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")) \ No newline at end of file diff --git a/src-python/models/translation/translation_openai.py b/src-python/models/translation/translation_openai.py new file mode 100644 index 00000000..b7115e51 --- /dev/null +++ b/src-python/models/translation/translation_openai.py @@ -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")) \ No newline at end of file diff --git a/src-python/models/translation/translation_plamo.py b/src-python/models/translation/translation_plamo.py new file mode 100644 index 00000000..ea54fd61 --- /dev/null +++ b/src-python/models/translation/translation_plamo.py @@ -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")) \ No newline at end of file diff --git a/src-python/models/translation/translation_translator.py b/src-python/models/translation/translation_translator.py index faab0327..249a09f1 100644 --- a/src-python/models/translation/translation_translator.py +++ b/src-python/models/translation/translation_translator.py @@ -7,8 +7,24 @@ except Exception: other_web_Translator = None # type: ignore ENABLE_TRANSLATORS = False -from .translation_languages import translation_lang -from .translation_utils import ctranslate2_weights +try: + 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 +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 transformers @@ -31,20 +47,25 @@ class Translator: def __init__(self) -> 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_tokenizer: Any = None self.is_loaded_ctranslate2_model: bool = False self.is_changed_translator_parameters: bool = False 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. Returns True on success, False on failure. """ result = True try: - self.deepl_client = DeepLClient(authkey) + self.deepl_client = DeepLClient(auth_key) # quick smoke test self.deepl_client.translate_text(" ", target_lang="EN-US") except Exception: @@ -53,6 +74,169 @@ class Translator: result = False 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: """Load a CTranslate2 model from weights. @@ -92,7 +276,7 @@ class Translator: def setChangedTranslatorParameters(self, is_changed: bool) -> None: 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. Returns a string on success or False on failure (keeps legacy behavior). @@ -102,7 +286,13 @@ class Translator: try: self.ctranslate2_tokenizer.src_lang = source_language source = self.ctranslate2_tokenizer.convert_ids_to_tokens(self.ctranslate2_tokenizer.encode(message)) - target_prefix = [self.ctranslate2_tokenizer.lang_code_to_token[target_language]] + match weight_type: + case "m2m100_418M-ct2-int8" | "m2m100_1.2B-ct2-int8": + 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]) target = results[0].hypotheses[0][1:] result = self.ctranslate2_tokenizer.decode(self.ctranslate2_tokenizer.convert_tokens_to_ids(target)) @@ -111,7 +301,7 @@ class Translator: return result @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. Returns (source_code, target_code). @@ -128,13 +318,17 @@ class Translator: target_language = "Portuguese European" else: 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 _: - pass - source_language = translation_lang[translator_name]["source"][source_language] - target_language = translation_lang[translator_name]["target"][target_language] + source_language = translation_lang[translator_name]["source"][source_language] + target_language = translation_lang[translator_name]["target"][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. Returns translated string on success, or False on failure. When @@ -145,7 +339,7 @@ class Translator: return message 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: case "DeepL": 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: result = False 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": if self.is_enable_translators is True and other_web_Translator is not None: result = other_web_Translator( @@ -186,8 +429,27 @@ class Translator: to_language=target_language, ) 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: errorLogging() result = False - return result \ No newline at end of file + 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) \ No newline at end of file diff --git a/src-python/models/translation/translation_utils.py b/src-python/models/translation/translation_utils.py index 003da354..8c3e4e46 100644 --- a/src-python/models/translation/translation_utils.py +++ b/src-python/models/translation/translation_utils.py @@ -1,12 +1,19 @@ -import tempfile -from zipfile import ZipFile from os import path as os_path from os import makedirs as os_makedirs from requests import get as requests_get -from typing import Callable, Optional -import hashlib +from typing import Callable 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. @@ -18,125 +25,115 @@ raising, which matches the repository's defensive style. """ ctranslate2_weights = { - "small": { - "url": "https://github.com/misyaguziya/VRCT-weights/releases/download/v1.0/m2m100_418m.zip", - "directory_name": "m2m100_418m", + "m2m100_418M-ct2-int8": { + "hf_repo": "jncraton/m2m100_418M-ct2-int8", + "directory_name": "m2m100_418M-ct2-int8", "tokenizer": "facebook/m2m100_418M", - "hash": { - "model.bin": "e7c26a9abb5260abd0268fbe3040714070dec254a990b4d7fd3f74c5230e3acb", - "sentencepiece.model": "d8f7c76ed2a5e0822be39f0a4f95a55eb19c78f4593ce609e2edbc2aea4d380a", - "shared_vocabulary.txt": "bd440aa21b8ca3453fc792a0018a1f3fe68b3464aadddd4d16a4b72f73c86d8c", - }, }, - "large": { - "url": "https://github.com/misyaguziya/VRCT-weights/releases/download/v1.0/m2m100_12b.zip", - "directory_name": "m2m100_12b", - "tokenizer": "facebook/m2m100_1.2b", - "hash": { - "model.bin": "abb7bf4ba7e5e016b6e3ed480c752459b2f783ac8fca372e7587675e5bf3a919", - "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", + }, + "nllb-200-distilled-1.3B-ct2-int8": { + "hf_repo": "OpenNMT/nllb-200-distilled-1.3B-ct2-int8", + "directory_name": "nllb-200-distilled-1.3B-ct2-int8", + "tokenizer": "facebook/nllb-200-distilled-1.3B", + }, + "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 calculate_file_hash(file_path: str, block_size: int = 65536) -> str: - hash_object = hashlib.sha256() - 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: +def checkCTranslate2Weight(root: str, weight_type: str = "m2m100_418M-ct2-int8"): + weight_directory_name = ctranslate2_weights[weight_type]["directory_name"] + path = os_path.join(root, "weights", "ctranslate2", weight_directory_name) + try: + # モデルロード可能かどうかで判定 + compute_type = getBestComputeType("cpu", 0) + ctranslate2.Translator(path, compute_type=compute_type) + return True + except Exception: 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) + +def downloadCTranslate2Weight(root: str, weight_type: str = "m2m100_418M-ct2-int8", callback: Callable = None, end_callback: Callable = None): + hf_repo = ctranslate2_weights[weight_type]["hf_repo"] + files = list_repo_files(repo_id=hf_repo) + path = os_path.join(root, "weights", "ctranslate2", ctranslate2_weights[weight_type]["directory_name"]) + if checkCTranslate2Weight(root, weight_type): + return True + os_makedirs(path, exist_ok=True) + + def downloadFile(url: str, file_path: str, func: Callable = None): try: - if calculate_file_hash(p) != hash_data[f]: - return False + res = requests_get(url, stream=True) + res.raise_for_status() + file_size = int(res.headers.get('content-length', 0)) + total_chunk = 0 + with open(file_path, 'wb') as file: + for chunk in res.iter_content(chunk_size=1024*2000): + file.write(chunk) + if func is not None: + total_chunk += len(chunk) + func(total_chunk/file_size) except Exception: errorLogging() - return False - return True + for filename in files: + file_path = os_path.join(path, filename) + url = hf_hub_url(hf_repo, filename) + downloadFile(url, file_path, func=callback if filename == "model.bin" else None) -def downloadCTranslate2Weight(root: str, weight_type: str = "small", callback: Optional[Callable[[float], None]] = None, end_callback: Optional[Callable[[], None]] = None) -> None: - """Download and extract ctranslate2 weights for the given type. + if end_callback is not None: + end_callback() - 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 callable(end_callback): - end_callback() - return +def downloadCTranslate2Tokenizer(path: str, weight_type: str = "m2m100_418M-ct2-int8"): + directory_name = ctranslate2_weights[weight_type]["directory_name"] + tokenizer = ctranslate2_weights[weight_type]["tokenizer"] + tokenizer_path = os_path.join(path, "weights", "ctranslate2", directory_name, "tokenizer") try: - with tempfile.TemporaryDirectory() as tmp_path: - res = requests_get(url, stream=True, timeout=30) - total = int(res.headers.get("content-length", 0) or 0) - written = 0 - out_path = os_path.join(tmp_path, filename) - with open(out_path, "wb") as out: - for chunk in res.iter_content(chunk_size=1024 * 1024): - if not chunk: - continue - out.write(chunk) - written += len(chunk) - if callable(callback) and total: - try: - callback(written / total) - except Exception: - errorLogging() - with ZipFile(out_path) as zf: - zf.extractall(dst_path) + os_makedirs(tokenizer_path, exist_ok=True) + transformers.AutoTokenizer.from_pretrained(tokenizer, cache_dir=tokenizer_path) except Exception: errorLogging() - finally: - if callable(end_callback): - end_callback() + 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) -def downloadCTranslate2Tokenizer(root: str, weight_type: str = "small") -> None: - """Ensure a tokenizer for the requested weight is available (cached). +# テスト用コード(直接実行時のみ) +if __name__ == "__main__": + def progress_callback(percent): + print(f"Download progress: {percent*100:.2f}%") - 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: - os_makedirs(tokenizer_cache, exist_ok=True) - transformers.AutoTokenizer.from_pretrained(tokenizer_name, cache_dir=tokenizer_cache) - except Exception: - errorLogging() \ No newline at end of file + 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}") \ No newline at end of file diff --git a/task_kill.py b/task_kill.py new file mode 100644 index 00000000..ee869511 --- /dev/null +++ b/task_kill.py @@ -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 \ No newline at end of file