From 7e7b3505a17f5df38efd51e48aa2daa3ec3eb20a Mon Sep 17 00:00:00 2001 From: misyaguziya <53165965+misyaguziya@users.noreply.github.com> Date: Fri, 17 Oct 2025 21:48:44 +0900 Subject: [PATCH] =?UTF-8?q?LMStudio=20=E3=81=A8=20Ollama=20=E3=81=AE?= =?UTF-8?q?=E7=BF=BB=E8=A8=B3=E3=83=90=E3=83=83=E3=82=AF=E3=82=A8=E3=83=B3?= =?UTF-8?q?=E3=83=89=E3=82=92=E8=BF=BD=E5=8A=A0=E3=83=BB=E7=B5=B1=E5=90=88?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - config: SELECTABLE_/SELECTED_ の LMStudio/Ollama 項目と LMSTUDIO_URL を追加。 - Controller: LMStudio/Ollama の認証チェック、URL取得/設定、モデル一覧取得/設定のエンドポイントを実装。 - Model/Translator: LMStudio/Ollama 用の認証・モデル一覧・モデル設定・クライアント更新メソッドを追加し、翻訳処理の選択肢に対応。 - translation_* クライアント: 各クライアントでのプロンプト読み込み処理を共通化し、translation_utils.loadPromptConfig を利用するようにリファクタ。 - translation_languages: LMStudio/Ollama 用の言語マッピングを追加。 --- src-python/config.py | 58 +++++++ src-python/controller.py | 150 ++++++++++++++++++ src-python/model.py | 32 ++++ .../models/translation/translation_gemini.py | 32 ++-- .../translation/translation_languages.py | 5 +- .../translation/translation_lmstudio.py | 42 ++--- .../models/translation/translation_ollama.py | 36 ++--- .../models/translation/translation_openai.py | 28 ++-- .../models/translation/translation_plamo.py | 31 ++-- .../translation/translation_translator.py | 86 +++++++++- .../models/translation/translation_utils.py | 18 ++- 11 files changed, 401 insertions(+), 117 deletions(-) diff --git a/src-python/config.py b/src-python/config.py index ffbc8844..9cd41589 100644 --- a/src-python/config.py +++ b/src-python/config.py @@ -338,6 +338,24 @@ class Config: 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 @@ -957,6 +975,41 @@ class Config: 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): @@ -1234,6 +1287,8 @@ class Config: 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 @@ -1344,6 +1399,9 @@ class Config: self._SELECTED_PLAMO_MODEL = None self._SELECTED_GEMINI_MODEL = None self._SELECTED_OPENAI_MODEL = None + self._LMSTUDIO_URL = "http://127.0.0.1:1234" + 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 f4c1e094..55968566 100644 --- a/src-python/controller.py +++ b/src-python/controller.py @@ -1877,6 +1877,129 @@ class Controller: } 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) + try: + data = str(data) + result = model.authenticationTranslatorLMStudio(base_url=data) + if result is True: + config.LMSTUDIO_URL = data + 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 + 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 checkTranslatorLOllamaConnection(self, *args, **kwargs) -> dict: + printLog("Check Translator Lollama Connection") + try: + result = model.authenticationTranslatorOllama() + if result is True: + response = {"status":200, "result":True} + else: + response = { + "status":400, + "result":{ + "message":"Cannot connect to Lollama server", + "data": False + } + } + except Exception as e: + errorLogging() + response = { + "status":400, + "result":{ + "message":f"Error {e}", + "data": False + } + } + return response + + def getTranslatorLOllamaModelList(self, *args, **kwargs) -> dict: + model_list = model.getTranslatorOllamaModelList() + return {"status":200, "result": model_list} + + def getTranslatorLOllamaModel(self, *args, **kwargs) -> dict: + return {"status":200, "result":config.SELECTED_OLLAMA_MODEL} + + def setTranslatorLOllamaModel(self, data, *args, **kwargs) -> dict: + printLog("Set Translator Lollama Model", data) + try: + data = str(data) + result = model.setTranslatorOllamaModel(model=data) + if result is True: + config.SELECTED_OLLAMA_MODEL = data + response = {"status":200, "result":config.SELECTED_OLLAMA_MODEL} + else: + response = { + "status":400, + "result":{ + "message":"Lollama 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} @@ -2805,6 +2928,33 @@ class Controller: 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(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/model.py b/src-python/model.py index ba3fd1cf..e8d35aec 100644 --- a/src-python/model.py +++ b/src-python/model.py @@ -249,6 +249,38 @@ class Model: 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) diff --git a/src-python/models/translation/translation_gemini.py b/src-python/models/translation/translation_gemini.py index ee019c06..fd20c941 100644 --- a/src-python/models/translation/translation_gemini.py +++ b/src-python/models/translation/translation_gemini.py @@ -1,8 +1,15 @@ import logging from google import genai from langchain_google_genai import ChatGoogleGenerativeAI -import yaml -from os import path as os_path + +try: + 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_utils import loadPromptConfig logger = logging.getLogger("langchain_google_genai") logger.setLevel(logging.ERROR) @@ -42,32 +49,13 @@ def _get_available_text_models(api_key: str) -> list[str]: allowed_models.sort() return allowed_models -def _load_prompt_config(root_path: str = None) -> dict: - """プロンプト設定をYAMLファイルから読み込む""" - prompt_filename = "translation_gemini.yml" - - # PyInstallerでビルドされた場合のパス - if root_path 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 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 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) - class GeminiClient: def __init__(self, root_path: str = None): self.api_key = None self.model = None # プロンプト設定をYAMLファイルから読み込む - prompt_config = _load_prompt_config(root_path) + prompt_config = loadPromptConfig(root_path, "translation_gemini.yml") self.supported_languages = prompt_config["supported_languages"] self.prompt_template = prompt_config["system_prompt"] diff --git a/src-python/models/translation/translation_languages.py b/src-python/models/translation/translation_languages.py index ab087e0c..69a594b8 100644 --- a/src-python/models/translation/translation_languages.py +++ b/src-python/models/translation/translation_languages.py @@ -665,7 +665,6 @@ dict_gemini_languages = { translation_lang["Gemini_API"] = {"source":dict_gemini_languages, "target":dict_gemini_languages} -# OpenAI API (Chat Completions) - Gemini とほぼ同等の自然言語名を使用 dict_openai_languages = { "Arabic": "Arabic", "Bengali": "Bengali", @@ -709,4 +708,6 @@ dict_openai_languages = { "Vietnamese": "Vietnamese", } -translation_lang["OpenAI_API"] = {"source": dict_openai_languages, "target": dict_openai_languages} \ No newline at end of file +translation_lang["OpenAI_API"] = {"source": dict_openai_languages, "target": dict_openai_languages} +translation_lang["LMStudio"] = {"source": dict_openai_languages, "target": dict_openai_languages} +translation_lang["Ollama"] = {"source": dict_openai_languages, "target": dict_openai_languages} \ No newline at end of file diff --git a/src-python/models/translation/translation_lmstudio.py b/src-python/models/translation/translation_lmstudio.py index 6de2f615..1827d887 100644 --- a/src-python/models/translation/translation_lmstudio.py +++ b/src-python/models/translation/translation_lmstudio.py @@ -1,8 +1,14 @@ from openai import OpenAI from langchain_openai import ChatOpenAI from pydantic import SecretStr -import yaml -from os import path as os_path + +try: + 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_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. @@ -27,22 +33,6 @@ def _get_available_text_models(api_key: str, base_url: str | None = None) -> lis allowed_models.sort() return allowed_models -def _load_prompt_config(root_path: str = None) -> dict: - prompt_filename = "translation_lmstudio.yml" - # PyInstaller 展開後 - if root_path 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 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 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) - class LMStudioClient: """LM Studio Translation simple wrapper. prompt/translation_lmstudio.yml から system_prompt / supported_languages を読み込む。 @@ -52,7 +42,7 @@ class LMStudioClient: self.model = None self.base_url = base_url # None の場合は公式エンドポイント - prompt_config = _load_prompt_config(root_path) + prompt_config = loadPromptConfig(root_path, "translation_lmstudio.yml") self.supported_languages = prompt_config["supported_languages"] self.prompt_template = prompt_config["system_prompt"] @@ -112,13 +102,11 @@ class LMStudioClient: return content.strip() if __name__ == "__main__": - AUTH_KEY = "lm-studio" client = LMStudioClient(base_url="http://192.168.68.110:1234/v1") models = client.getModelList() - print(models) - # if models: - # print("Available models:", models) - # model = input("Select a model: ") - client.setModel("google/gemma-3n-e4b") - client.updateClient() - print(client.translate("こんにちは世界", "Japanese", "English")) \ No newline at end of file + 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 index 22ce3e4f..8b99d328 100644 --- a/src-python/models/translation/translation_ollama.py +++ b/src-python/models/translation/translation_ollama.py @@ -1,8 +1,13 @@ import requests from langchain_ollama import ChatOllama -import yaml -from os import path as os_path +try: + 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_utils import loadPromptConfig def _authentication_check(base_url: str | None = None) -> bool: """Check authentication for Ollama API. @@ -29,22 +34,6 @@ def _get_available_text_models(base_url: str | None = None) -> list[str]: allowed_models.sort() return allowed_models -def _load_prompt_config(root_path: str = None) -> dict: - prompt_filename = "translation_ollama.yml" - # PyInstaller 展開後 - if root_path 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 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 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) - class OllamaClient: """Ollama Translation simple wrapper. prompt/translation_ollama.yml から system_prompt / supported_languages を読み込む。 @@ -53,14 +42,17 @@ class OllamaClient: self.model = None self.base_url = "http://localhost:11434" - prompt_config = _load_prompt_config(root_path) + prompt_config = loadPromptConfig(root_path) self.supported_languages = prompt_config["supported_languages"] 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 _authentication_check(self.base_url): + if self.authenticationCheck(): return _get_available_text_models(self.base_url) return [] @@ -111,5 +103,5 @@ if __name__ == "__main__": 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 + 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 index f461526c..2741702e 100644 --- a/src-python/models/translation/translation_openai.py +++ b/src-python/models/translation/translation_openai.py @@ -1,8 +1,14 @@ from openai import OpenAI from langchain_openai import ChatOpenAI from pydantic import SecretStr -import yaml -from os import path as os_path + +try: + 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_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. @@ -51,22 +57,6 @@ def _get_available_text_models(api_key: str, base_url: str | None = None) -> lis allowed_models.sort() return allowed_models -def _load_prompt_config(root_path: str = None) -> dict: - prompt_filename = "translation_openai.yml" - # PyInstaller 展開後 - if root_path 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 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 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) - class OpenAIClient: """OpenAI Translation simple wrapper. prompt/translation_openai.yml から system_prompt / supported_languages を読み込む。 @@ -76,7 +66,7 @@ class OpenAIClient: self.model = None self.base_url = base_url # None の場合は公式エンドポイント - prompt_config = _load_prompt_config(root_path) + prompt_config = loadPromptConfig(root_path, "translation_openai.yml") self.supported_languages = prompt_config["supported_languages"] self.prompt_template = prompt_config["system_prompt"] diff --git a/src-python/models/translation/translation_plamo.py b/src-python/models/translation/translation_plamo.py index 81cc55bc..112a962b 100644 --- a/src-python/models/translation/translation_plamo.py +++ b/src-python/models/translation/translation_plamo.py @@ -1,8 +1,14 @@ from openai import OpenAI from langchain_openai import ChatOpenAI from pydantic import SecretStr -import yaml -from os import path as os_path + +try: + 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_utils import loadPromptConfig BASE_URL = "https://api.platform.preferredai.jp/v1" @@ -29,32 +35,13 @@ def _get_available_text_models(api_key: str) -> list[str]: allowed_models.sort() return allowed_models -def _load_prompt_config(root_path: str = None) -> dict: - """プロンプト設定をYAMLファイルから読み込む""" - prompt_filename = "translation_plamo.yml" - - # PyInstallerでビルドされた場合のパス - if root_path 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 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 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) - class PlamoClient: def __init__(self, root_path: str = None): self.api_key = None self.base_url = BASE_URL self.model = None - prompt_config = _load_prompt_config(root_path) + prompt_config = loadPromptConfig(root_path, "translation_plamo.yml") self.supported_languages = prompt_config["supported_languages"] self.prompt_template = prompt_config["system_prompt"] diff --git a/src-python/models/translation/translation_translator.py b/src-python/models/translation/translation_translator.py index 4ea4c0ba..249a09f1 100644 --- a/src-python/models/translation/translation_translator.py +++ b/src-python/models/translation/translation_translator.py @@ -13,15 +13,18 @@ try: 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 - 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 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 @@ -47,6 +50,8 @@ class Translator: 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 @@ -171,6 +176,67 @@ class Translator: """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. @@ -320,6 +386,24 @@ class Translator: 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( diff --git a/src-python/models/translation/translation_utils.py b/src-python/models/translation/translation_utils.py index 895a9680..8c3e4e46 100644 --- a/src-python/models/translation/translation_utils.py +++ b/src-python/models/translation/translation_utils.py @@ -2,11 +2,10 @@ 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 -import hashlib import transformers import ctranslate2 from huggingface_hub import hf_hub_url, list_repo_files -from requests import get as requests_get +import yaml try: from utils import errorLogging, getBestComputeType @@ -102,6 +101,21 @@ def downloadCTranslate2Tokenizer(path: str, weight_type: str = "m2m100_418M-ct2- tokenizer_path = os_path.join("./weights", "ctranslate2", directory_name, "tokenizer") transformers.AutoTokenizer.from_pretrained(tokenizer, cache_dir=tokenizer_path) +def loadPromptConfig(root_path: str | None = None, prompt_filename: str | None = None) -> dict: + # PyInstaller 展開後 + if root_path and prompt_filename and os_path.exists(os_path.join(root_path, "_internal", "prompt", prompt_filename)): + prompt_path = os_path.join(root_path, "_internal", "prompt", prompt_filename) + # src-python 直下実行 + elif prompt_filename and os_path.exists(os_path.join(os_path.dirname(__file__), "models", "translation", "prompt", prompt_filename)): + prompt_path = os_path.join(os_path.dirname(__file__), "models", "translation", "prompt", prompt_filename) + # translation フォルダ直下実行 + elif prompt_filename and os_path.exists(os_path.join(os_path.dirname(__file__), "prompt", prompt_filename)): + prompt_path = os_path.join(os_path.dirname(__file__), "prompt", prompt_filename) + else: + raise FileNotFoundError(f"Prompt file not found: {prompt_filename}") + with open(prompt_path, "r", encoding="utf-8") as f: + return yaml.safe_load(f) + # テスト用コード(直接実行時のみ) if __name__ == "__main__": def progress_callback(percent):