diff --git a/src-python/config.py b/src-python/config.py index 08a2b790..f3d05069 100644 --- a/src-python/config.py +++ b/src-python/config.py @@ -638,6 +638,7 @@ class Config: SELECTABLE_PLAMO_MODEL_LIST = ManagedProperty('SELECTABLE_PLAMO_MODEL_LIST', type_=list, serialize=False, mutable_tracking=True) SELECTABLE_GEMINI_MODEL_LIST = ManagedProperty('SELECTABLE_GEMINI_MODEL_LIST', type_=list, serialize=False, mutable_tracking=True) SELECTABLE_OPENAI_MODEL_LIST = ManagedProperty('SELECTABLE_OPENAI_MODEL_LIST', type_=list, serialize=False, mutable_tracking=True) + SELECTABLE_GROQ_MODEL_LIST = ManagedProperty('SELECTABLE_GROQ_MODEL_LIST', type_=list, serialize=False, mutable_tracking=True) SELECTABLE_LMSTUDIO_MODEL_LIST = ManagedProperty('SELECTABLE_LMSTUDIO_MODEL_LIST', type_=list, serialize=False, mutable_tracking=True) SELECTABLE_OLLAMA_MODEL_LIST = ManagedProperty('SELECTABLE_OLLAMA_MODEL_LIST', type_=list, serialize=False, mutable_tracking=True) @@ -735,6 +736,7 @@ class Config: SELECTED_PLAMO_MODEL = ManagedProperty('SELECTED_PLAMO_MODEL', type_=str, allowed=_allowed_in_populated('SELECTABLE_PLAMO_MODEL_LIST')) SELECTED_GEMINI_MODEL = ManagedProperty('SELECTED_GEMINI_MODEL', type_=str, allowed=_allowed_in_populated('SELECTABLE_GEMINI_MODEL_LIST')) SELECTED_OPENAI_MODEL = ManagedProperty('SELECTED_OPENAI_MODEL', type_=str, allowed=_allowed_in_populated('SELECTABLE_OPENAI_MODEL_LIST')) + SELECTED_GROQ_MODEL = ManagedProperty('SELECTED_GROQ_MODEL', type_=str, allowed=_allowed_in_populated('SELECTABLE_GROQ_MODEL_LIST')) SELECTED_LMSTUDIO_MODEL = ManagedProperty('SELECTED_LMSTUDIO_MODEL', type_=str, allowed=_allowed_in_populated('SELECTABLE_LMSTUDIO_MODEL_LIST')) SELECTED_OLLAMA_MODEL = ManagedProperty('SELECTED_OLLAMA_MODEL', type_=str, allowed=_allowed_in_populated('SELECTABLE_OLLAMA_MODEL_LIST')) @@ -815,6 +817,7 @@ class Config: self._SELECTABLE_PLAMO_MODEL_LIST = [] self._SELECTABLE_GEMINI_MODEL_LIST = [] self._SELECTABLE_OPENAI_MODEL_LIST = [] + self._SELECTABLE_GROQ_MODEL_LIST = [] self._SELECTABLE_LMSTUDIO_MODEL_LIST = [] self._SELECTABLE_OLLAMA_MODEL_LIST = [] @@ -939,6 +942,7 @@ class Config: "Plamo_API": None, "Gemini_API": None, "OpenAI_API": None, + "Groq_API": None, } self._USE_EXCLUDE_WORDS = True self._SELECTED_TRANSLATION_COMPUTE_DEVICE = copy.deepcopy(self.SELECTABLE_COMPUTE_DEVICE_LIST[0]) @@ -947,6 +951,7 @@ class Config: self._SELECTED_PLAMO_MODEL = None self._SELECTED_GEMINI_MODEL = None self._SELECTED_OPENAI_MODEL = None + self._SELECTED_GROQ_MODEL = None self._LMSTUDIO_URL = "http://127.0.0.1:1234/v1" self._SELECTED_LMSTUDIO_MODEL = None self._SELECTED_OLLAMA_MODEL = None @@ -1051,6 +1056,7 @@ class Config: ('SELECTED_PLAMO_MODEL', 'SELECTABLE_PLAMO_MODEL_LIST'), ('SELECTED_GEMINI_MODEL', 'SELECTABLE_GEMINI_MODEL_LIST'), ('SELECTED_OPENAI_MODEL', 'SELECTABLE_OPENAI_MODEL_LIST'), + ('SELECTED_GROQ_MODEL', 'SELECTABLE_GROQ_MODEL_LIST'), ('SELECTED_LMSTUDIO_MODEL', 'SELECTABLE_LMSTUDIO_MODEL_LIST'), ('SELECTED_OLLAMA_MODEL', 'SELECTABLE_OLLAMA_MODEL_LIST'), ] diff --git a/src-python/controller.py b/src-python/controller.py index 714b6a85..29d0896e 100644 --- a/src-python/controller.py +++ b/src-python/controller.py @@ -1900,6 +1900,103 @@ class Controller: } return response + @staticmethod + def getGroqAuthKey(*args, **kwargs) -> dict: + return {"status":200, "result":config.AUTH_KEYS["Groq_API"]} + + def setGroqAuthKey(self, data, *args, **kwargs) -> dict: + printLog("Set Groq Auth Key", data) + translator_name = "Groq_API" + try: + data = str(data) + if data.startswith("gsk-") and len(data) >= 40: + result = model.authenticationTranslatorGroqAuthKey(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_GROQ_MODEL_LIST = model.getTranslatorGroqModelList() + self.run(200, self.run_mapping["selectable_groq_model_list"], config.SELECTABLE_GROQ_MODEL_LIST) + if config.SELECTED_GROQ_MODEL not in config.SELECTABLE_GROQ_MODEL_LIST: + config.SELECTED_GROQ_MODEL = config.SELECTABLE_GROQ_MODEL_LIST[0] + model.setTranslatorGroqModel(model=config.SELECTED_GROQ_MODEL) + self.run(200, self.run_mapping["selected_groq_model"], config.SELECTED_GROQ_MODEL) + model.updateTranslatorGroqClient() + self.updateTranslationEngineAndEngineList() + response = {"status":200, "result":config.AUTH_KEYS[translator_name]} + else: + response = { + "status":400, + "result":{ + "message":"Authentication failure of Groq auth key", + "data": config.AUTH_KEYS[translator_name] + } + } + else: + response = { + "status":400, + "result":{ + "message":"Groq 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 delGroqAuthKey(self, *args, **kwargs) -> dict: + translator_name = "Groq_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 getGroqModelList(self, *args, **kwargs) -> dict: + return {"status":200, "result": config.SELECTABLE_GROQ_MODEL_LIST} + + def getGroqModel(self, *args, **kwargs) -> dict: + return {"status":200, "result":config.SELECTED_GROQ_MODEL} + + def setGroqModel(self, data, *args, **kwargs) -> dict: + printLog("Set Groq Model", data) + try: + data = str(data) + result = model.setTranslatorGroqModel(model=data) + if result is True: + config.SELECTED_GROQ_MODEL = data + model.setTranslatorGroqModel(model=config.SELECTED_GROQ_MODEL) + model.updateTranslatorGroqClient() + response = {"status":200, "result":config.SELECTED_GROQ_MODEL} + else: + response = { + "status":400, + "result":{ + "message":"Groq model is not valid", + "data": config.SELECTED_GROQ_MODEL + } + } + except Exception as e: + errorLogging() + response = { + "status":400, + "result":{ + "message":f"Error {e}", + "data": config.SELECTED_GROQ_MODEL + } + } + return response + def getTranslatorLMStudioConnection(self, *args, **kwargs) -> dict: return {"status":200, "result":model.getTranslatorLMStudioConnected()} diff --git a/src-python/mainloop.py b/src-python/mainloop.py index 20b4bf2f..2da84cbe 100644 --- a/src-python/mainloop.py +++ b/src-python/mainloop.py @@ -58,6 +58,8 @@ run_mapping = { "selected_gemini_model":"/run/selected_gemini_model", "selectable_openai_model_list":"/run/selectable_openai_model_list", "selected_openai_model":"/run/selected_openai_model", + "selectable_groq_model_list":"/run/selectable_groq_model_list", + "selected_groq_model":"/run/selected_groq_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", diff --git a/src-python/model.py b/src-python/model.py index 0283d7d7..a9cc7b51 100644 --- a/src-python/model.py +++ b/src-python/model.py @@ -249,6 +249,23 @@ class Model: self.ensure_initialized() self.translator.updateOpenAIClient() + def authenticationTranslatorGroqAuthKey(self, auth_key: str) -> bool: + result = self.translator.authenticationGroqAuthKey(auth_key, root_path=config.PATH_LOCAL) + return result + + def getTranslatorGroqModelList(self) -> list[str]: + self.ensure_initialized() + return self.translator.getGroqModelList() + + def setTranslatorGroqModel(self, model: str) -> bool: + self.ensure_initialized() + result = self.translator.setGroqModel(model=model) + return result + + def updateTranslatorGroqClient(self) -> None: + self.ensure_initialized() + self.translator.updateGroqClient() + def getTranslatorLMStudioConnected(self) -> bool: self.ensure_initialized() return self.translator.getLMStudioConnected() diff --git a/src-python/models/translation/languages/languages.yml b/src-python/models/translation/languages/languages.yml index cfde921e..66bdc612 100644 --- a/src-python/models/translation/languages/languages.yml +++ b/src-python/models/translation/languages/languages.yml @@ -769,3 +769,7 @@ LMStudio: Ollama: source: *openai_langs target: *openai_langs + +Groq_API: + source: *openai_langs + target: *openai_langs diff --git a/src-python/models/translation/prompt/translation_groq.yml b/src-python/models/translation/prompt/translation_groq.yml new file mode 100644 index 00000000..bc256c3d --- /dev/null +++ b/src-python/models/translation/prompt/translation_groq.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. diff --git a/src-python/models/translation/translation_groq.py b/src-python/models/translation/translation_groq.py new file mode 100644 index 00000000..04c57dbe --- /dev/null +++ b/src-python/models/translation/translation_groq.py @@ -0,0 +1,145 @@ +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, loadTranslationLanguages + from translation_utils import loadPromptConfig + translation_lang = loadTranslationLanguages(path=".", force=True) + +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="https://api.groq.com/openai/v1", + ) + client.models.list() + return True + except Exception: + return False + +def _get_available_text_models(api_key: str) -> list[str]: + """Extract only Groq models suitable for translation and chat applications. + """ + client = OpenAI( + api_key=api_key, + base_url="https://api.groq.com/openai/v1", + ) + res = client.models.list() + allowed_models = [] + + for model in res.data: + model_id = model.id + + # 除外対象のキーワード + exclude_keywords = [ + "whisper", # 音声認識 + "embedding", # 埋め込み + "image", # 画像生成 + "tts", # 音声合成 + "audio", # 音声系 + "search", # 検索補助モデル + "transcribe", # 音声→文字起こし + "diarize", # 話者分離 + "vision" # 画像入力系 + ] + + # 除外キーワードが含まれているモデルをスキップ + if any(kw in model_id.lower() for kw in exclude_keywords): + continue + + # テキスト処理用モデルのみ対象 + allowed_models.append(model_id) + + allowed_models.sort() + return allowed_models + +class GroqClient: + """Groq API Translation wrapper using OpenAI-compatible endpoint. + + Groq provides a fast LLM inference platform with an OpenAI-compatible API. + The API endpoint: https://api.groq.com/openai/v1 + """ + def __init__(self, root_path: str = None): + self.api_key = None + self.model = None + self.base_url = "https://api.groq.com/openai/v1" + + prompt_config = loadPromptConfig(root_path, "translation_groq.yml") + self.supported_languages = list(translation_lang["Groq_API"]["source"].keys()) + self.prompt_template = prompt_config["system_prompt"] + + self.groq_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.groq_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.groq_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 = "GROQ_API_KEY" + client = GroqClient() + 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")) diff --git a/src-python/models/translation/translation_lmstudio.py b/src-python/models/translation/translation_lmstudio.py index aa8509e8..16ff9545 100644 --- a/src-python/models/translation/translation_lmstudio.py +++ b/src-python/models/translation/translation_lmstudio.py @@ -11,7 +11,7 @@ except Exception: sys.path.append(os_path.dirname(os_path.abspath(__file__))) from translation_languages import translation_lang, loadTranslationLanguages from translation_utils import loadPromptConfig - loadTranslationLanguages(path=".", force=True) + translation_lang = loadTranslationLanguages(path=".", force=True) def _authentication_check(base_url: str | None = None) -> bool: """Check if the provided API key is valid by attempting to list models. diff --git a/src-python/models/translation/translation_ollama.py b/src-python/models/translation/translation_ollama.py index ae4cc860..543e603a 100644 --- a/src-python/models/translation/translation_ollama.py +++ b/src-python/models/translation/translation_ollama.py @@ -10,7 +10,7 @@ except Exception: sys.path.append(os_path.dirname(os_path.abspath(__file__))) from translation_languages import translation_lang, loadTranslationLanguages from translation_utils import loadPromptConfig - loadTranslationLanguages(path=".", force=True) + translation_lang = loadTranslationLanguages(path=".", force=True) def _authentication_check(base_url: str | None = None) -> bool: """Check authentication for Ollama API. diff --git a/src-python/models/translation/translation_openai.py b/src-python/models/translation/translation_openai.py index b7115e51..cc09a90e 100644 --- a/src-python/models/translation/translation_openai.py +++ b/src-python/models/translation/translation_openai.py @@ -9,8 +9,9 @@ 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_languages import translation_lang, loadTranslationLanguages from translation_utils import loadPromptConfig + translation_lang = loadTranslationLanguages(path=".", force=True) 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. diff --git a/src-python/models/translation/translation_plamo.py b/src-python/models/translation/translation_plamo.py index ea54fd61..68d68754 100644 --- a/src-python/models/translation/translation_plamo.py +++ b/src-python/models/translation/translation_plamo.py @@ -9,8 +9,9 @@ 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_languages import translation_lang, loadTranslationLanguages from translation_utils import loadPromptConfig + translation_lang = loadTranslationLanguages(path=".", force=True) BASE_URL = "https://api.platform.preferredai.jp/v1" diff --git a/src-python/models/translation/translation_translator.py b/src-python/models/translation/translation_translator.py index 175a84ba..71a69e64 100644 --- a/src-python/models/translation/translation_translator.py +++ b/src-python/models/translation/translation_translator.py @@ -15,6 +15,7 @@ try: from .translation_openai import OpenAIClient from .translation_lmstudio import LMStudioClient from .translation_ollama import OllamaClient + from .translation_groq import GroqClient except Exception: import sys sys.path.append(os_path.dirname(os_path.dirname(os_path.dirname(os_path.abspath(__file__))))) @@ -25,6 +26,7 @@ except Exception: from translation_openai import OpenAIClient from translation_lmstudio import LMStudioClient from translation_ollama import OllamaClient + from translation_groq import GroqClient import ctranslate2 import transformers @@ -50,6 +52,7 @@ class Translator: self.plamo_client: Optional[PlamoClient] = None self.gemini_client: Optional[GeminiClient] = None self.openai_client: Optional[OpenAIClient] = None + self.groq_client: Optional[GroqClient] = None self.lmstudio_client: LMStudioClient[LMStudioClient] = None self.ollama_client: OllamaClient[OllamaClient] = None self.ctranslate2_translator: Any = None @@ -176,6 +179,40 @@ class Translator: """Update the OpenAI client (fetch available models).""" self.openai_client.updateClient() + def authenticationGroqAuthKey(self, auth_key: str, root_path: str = None) -> bool: + """Authenticate Groq API with the provided key. + + Returns True on success, False on failure. + """ + self.groq_client = GroqClient(root_path=root_path) + if self.groq_client.setAuthKey(auth_key): + return True + else: + self.groq_client = None + return False + + def getGroqModelList(self) -> list[str]: + """Get available Groq models. + + Returns a list of model names, or an empty list on failure. + """ + if self.groq_client is None: + return [] + return self.groq_client.getModelList() + + def setGroqModel(self, model: str) -> bool: + """Change the Groq model used for translation. + + Returns True on success, False on failure. + """ + if self.groq_client is None: + return False + return self.groq_client.setModel(model) + + def updateGroqClient(self) -> None: + """Update the Groq client (fetch available models).""" + self.groq_client.updateClient() + def getLMStudioConnected(self) -> bool: """Get LM Studio connection status. @@ -409,6 +446,15 @@ class Translator: input_lang=source_language, output_lang=target_language, ) + case "Groq_API": + if self.groq_client is None: + result = False + else: + result = self.groq_client.translate( + message, + input_lang=source_language, + output_lang=target_language, + ) case "LMStudio": if self.lmstudio_client is None: result = False