From 6fb7ecbc52d9c1e98b2ea119b8ccb4d27826f649 Mon Sep 17 00:00:00 2001 From: misyaguziya <53165965+misyaguziya@users.noreply.github.com> Date: Sun, 7 Sep 2025 23:59:20 +0900 Subject: [PATCH 01/25] [Update] translation_utils.py: Refactor weight handling and improve error logging add translate model - jncraton/m2m100_418M-ct2-int8 - jncraton/m2m100_1.2B-ct2-int8 - OpenNMT/nllb-200-3.3B-ct2-int8 - OpenNMT/nllb-200-distilled-1.3B-ct2-int8 --- .../models/translation/translation_utils.py | 154 +++++++++--------- 1 file changed, 80 insertions(+), 74 deletions(-) diff --git a/src-python/models/translation/translation_utils.py b/src-python/models/translation/translation_utils.py index 457a65f1..ef0e5592 100644 --- a/src-python/models/translation/translation_utils.py +++ b/src-python/models/translation/translation_utils.py @@ -1,103 +1,109 @@ -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 -import hashlib import transformers -from utils import errorLogging +import ctranslate2 +from huggingface_hub import hf_hub_url, list_repo_files +from requests import get as requests_get +try: + from utils import errorLogging +except Exception: + import traceback + def errorLogging(): + print(traceback.format_exc()) ctranslate2_weights = { - "small": { # M2M-100 418M-parameter model - "url": "https://github.com/misyaguziya/VRCT-weights/releases/download/v1.0/m2m100_418m.zip", - "directory_name": "m2m100_418m", - "tokenizer": "facebook/m2m100_418M", - "hash": { - "model.bin": "e7c26a9abb5260abd0268fbe3040714070dec254a990b4d7fd3f74c5230e3acb", - "sentencepiece.model": "d8f7c76ed2a5e0822be39f0a4f95a55eb19c78f4593ce609e2edbc2aea4d380a", - "shared_vocabulary.txt": "bd440aa21b8ca3453fc792a0018a1f3fe68b3464aadddd4d16a4b72f73c86d8c", - } + "small": { + # "hf_repo": "jncraton/m2m100_418M-ct2-int8", + # "directory_name": "m2m100_418M-ct2-int8", + # "tokenizer": "facebook/m2m100_418M", + "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", }, - "large": { # M2M-100 1.2B-parameter model - "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", - } + "large": { + "hf_repo": "jncraton/m2m100_1.2B-ct2-int8", + "directory_name": "m2m100_1.2B-ct2-int8", + "tokenizer": "facebook/m2m100_1.2B", + }, + "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", + }, + "nllb-200-distilled-1.3B": { + "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", }, } -def calculate_file_hash(file_path, block_size=65536): - hash_object = hashlib.sha256() - - with open(file_path, 'rb') as file: - for block in iter(lambda: file.read(block_size), b''): - hash_object.update(block) - - return hash_object.hexdigest() - -def checkCTranslate2Weight(root, weight_type="small"): +def checkCTranslate2Weight(root: str, weight_type: str = "small"): weight_directory_name = ctranslate2_weights[weight_type]["directory_name"] - hash_data = ctranslate2_weights[weight_type]["hash"] - files = [ - "model.bin", - "sentencepiece.model", - "shared_vocabulary.txt" - ] - path = os_path.join(root, "weights", "ctranslate2") + path = os_path.join(root, "weights", "ctranslate2", weight_directory_name) + try: + # モデルロード可能かどうかで判定 + ctranslate2.Translator(path) + return True + except Exception: + return False - # check already downloaded - already_downloaded = False - if all(os_path.exists(os_path.join(path, weight_directory_name, file)) for file in files): - # check hash - for file in files: - original_hash = hash_data[file] - current_hash = calculate_file_hash(os_path.join(path, weight_directory_name, file)) - if original_hash != current_hash: - break - already_downloaded = True - return already_downloaded - -def downloadCTranslate2Weight(root, weight_type="small", callback=None, end_callback=None): - url = ctranslate2_weights[weight_type]["url"] - filename = "weight.zip" - path = os_path.join(root, "weights", "ctranslate2") +def downloadCTranslate2Weight(root: str, weight_type: str = "small", 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) - if checkCTranslate2Weight(root, weight_type) is False: + def downloadFile(url: str, file_path: str, func: Callable = None): try: - with tempfile.TemporaryDirectory() as tmp_path: - res = requests_get(url, stream=True) - file_size = int(res.headers.get('content-length', 0)) - total_chunk = 0 - with open(os_path.join(tmp_path, filename), 'wb') as file: - for chunk in res.iter_content(chunk_size=1024*2000): - file.write(chunk) - if isinstance(callback, Callable): - total_chunk += len(chunk) - callback(total_chunk/file_size) - - with ZipFile(os_path.join(tmp_path, filename)) as zf: - zf.extractall(path) + 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() - if isinstance(end_callback, Callable): + 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) + + if end_callback is not None: end_callback() -def downloadCTranslate2Tokenizer(path, weight_type="small"): +def downloadCTranslate2Tokenizer(path: str, weight_type: str = "small"): 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: os_makedirs(tokenizer_path, exist_ok=True) transformers.AutoTokenizer.from_pretrained(tokenizer, cache_dir=tokenizer_path) except Exception: errorLogging() tokenizer_path = os_path.join("./weights", "ctranslate2", directory_name, "tokenizer") - transformers.AutoTokenizer.from_pretrained(tokenizer, cache_dir=tokenizer_path) \ No newline at end of file + transformers.AutoTokenizer.from_pretrained(tokenizer, cache_dir=tokenizer_path) + +# テスト用コード(直接実行時のみ) +if __name__ == "__main__": + def progress_callback(percent): + print(f"Download progress: {percent*100:.2f}%") + + def end_callback(): + print("Download finished.") + + root = "./" # 必要に応じてパスを変更 + # for weight_type in ctranslate2_weights.keys(): + # print(f"Testing download for: {weight_type}") + # downloadCTranslate2Weight(root, weight_type, callback=progress_callback, end_callback=end_callback) + # result = checkCTranslate2Weight(root, weight_type) + # print(f"Model loadable: {result}") + # break + downloadCTranslate2Tokenizer(root, "small") From 83a61e2e8756faf6462ec26e8e3b8e62ae38a40e Mon Sep 17 00:00:00 2001 From: misyaguziya <53165965+misyaguziya@users.noreply.github.com> Date: Mon, 8 Sep 2025 16:27:47 +0900 Subject: [PATCH 02/25] [Update] translation: Add support for new translation models and improve weight handling --- src-python/config.py | 2 +- src-python/model.py | 2 + .../translation/translation_languages.py | 230 +++++++++++++++++- .../translation/translation_translator.py | 46 +++- .../models/translation/translation_utils.py | 49 ++-- 5 files changed, 294 insertions(+), 35 deletions(-) diff --git a/src-python/config.py b/src-python/config.py index 76638fbe..ce509dc8 100644 --- a/src-python/config.py +++ b/src-python/config.py @@ -1188,7 +1188,7 @@ class Config: 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._WHISPER_WEIGHT_TYPE = "base" self._AUTO_CLEAR_MESSAGE_BOX = True self._SEND_ONLY_TRANSLATED_MESSAGES = False diff --git a/src-python/model.py b/src-python/model.py index 333f1394..b4486bc0 100644 --- a/src-python/model.py +++ b/src-python/model.py @@ -190,6 +190,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, @@ -203,6 +204,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/src-python/models/translation/translation_languages.py b/src-python/models/translation/translation_languages.py index a697960b..5839e249 100644 --- a/src-python/models/translation/translation_languages.py +++ b/src-python/models/translation/translation_languages.py @@ -275,7 +275,7 @@ translation_lang["Papago"] = { "target":dict_papago_languages, } -dict_ctranslate2_languages = { +dict_m2m100_languages = { "English": "en", "Chinese Simplified": "zh", "Chinese Traditional":"zh", @@ -378,7 +378,229 @@ dict_ctranslate2_languages = { "Sundanese": "su" } -translation_lang["CTranslate2"] = { - "source":dict_ctranslate2_languages, - "target":dict_ctranslate2_languages, +translation_lang["m2m100_418M-ct2-int8"] = { + "source":dict_m2m100_languages, + "target":dict_m2m100_languages, +} + +translation_lang["m2m100_1.2B-ct2-int8"] = { + "source":dict_m2m100_languages, + "target":dict_m2m100_languages, +} + +dict_nllb_languages = { + "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", + "Modern 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" +} + +translation_lang["nllb-200-distilled-1.3B-ct2-int8"] = { + "source":dict_nllb_languages, + "target":dict_nllb_languages, +} + +translation_lang["nllb-200-3.3B-ct2-int8"] = { + "source":dict_nllb_languages, + "target":dict_nllb_languages, } \ 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 42eb828e..23a82d04 100644 --- a/src-python/models/translation/translation_translator.py +++ b/src-python/models/translation/translation_translator.py @@ -6,8 +6,15 @@ try: except Exception: 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 +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 import ctranslate2 import transformers @@ -63,13 +70,19 @@ class Translator(): def isLoadedCTranslate2Model(self): return self.is_loaded_ctranslate2_model - def translateCTranslate2(self, message, source_language, target_language): + def translateCTranslate2(self, message, source_language, target_language, weight_type): result = False if self.is_loaded_ctranslate2_model is True: 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)) @@ -78,7 +91,7 @@ class Translator(): return result @staticmethod - def getLanguageCode(translator_name, target_country, source_language, target_language): + def getLanguageCode(translator_name, weight_type, target_country, source_language, target_language): match translator_name: case "DeepL_API": if target_language == "English": @@ -91,16 +104,18 @@ class Translator(): target_language = "Portuguese European" else: target_language = "Portuguese Brazilian" + case "CTranslate2": + translator_name = weight_type case _: pass 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, source_language, target_language, target_country, message): + def translate(self, translator_name, weight_type, source_language, target_language, target_country, message): try: result = "" - 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: @@ -149,8 +164,23 @@ class Translator(): 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 ef0e5592..1bc8fed6 100644 --- a/src-python/models/translation/translation_utils.py +++ b/src-python/models/translation/translation_utils.py @@ -5,50 +5,50 @@ import transformers import ctranslate2 from huggingface_hub import hf_hub_url, list_repo_files from requests import get as requests_get + try: - from utils import errorLogging + from utils import errorLogging, getBestComputeType except Exception: - import traceback - def errorLogging(): - print(traceback.format_exc()) + 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 ctranslate2_weights = { - "small": { - # "hf_repo": "jncraton/m2m100_418M-ct2-int8", - # "directory_name": "m2m100_418M-ct2-int8", - # "tokenizer": "facebook/m2m100_418M", - "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", + "m2m100_418M-ct2-int8": { + "hf_repo": "jncraton/m2m100_418M-ct2-int8", + "directory_name": "m2m100_418M-ct2-int8", + "tokenizer": "facebook/m2m100_418M", }, - "large": { + "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", }, - "nllb-200-distilled-1.3B": { - "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", - }, } -def checkCTranslate2Weight(root: str, weight_type: str = "small"): +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: # モデルロード可能かどうかで判定 - ctranslate2.Translator(path) + compute_type = getBestComputeType("cpu", 0) + ctranslate2.Translator(path, compute_type=compute_type) return True except Exception: return False -def downloadCTranslate2Weight(root: str, weight_type: str = "small", callback: Callable = None, end_callback: Callable = None): +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"]) @@ -79,7 +79,7 @@ def downloadCTranslate2Weight(root: str, weight_type: str = "small", callback: C if end_callback is not None: end_callback() -def downloadCTranslate2Tokenizer(path: str, weight_type: str = "small"): +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") @@ -106,4 +106,9 @@ if __name__ == "__main__": # result = checkCTranslate2Weight(root, weight_type) # print(f"Model loadable: {result}") # break - downloadCTranslate2Tokenizer(root, "small") + # 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 From 7ffef350fedff6119a288da94e15df7c9d28178e Mon Sep 17 00:00:00 2001 From: misyaguziya <53165965+misyaguziya@users.noreply.github.com> Date: Mon, 8 Sep 2025 16:30:58 +0900 Subject: [PATCH 03/25] [Update] package.json: Add task-kill script to terminate VRCT-sidecar.exe before development builds [Add] task_kill.py: Implement script to forcefully terminate VRCT-sidecar.exe --- package.json | 5 +++-- task_kill.py | 12 ++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 task_kill.py 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/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 From f5ebcd7bd8c8b05e6a094562295d1f73a320f58e Mon Sep 17 00:00:00 2001 From: misyaguziya <53165965+misyaguziya@users.noreply.github.com> Date: Wed, 10 Sep 2025 17:52:42 +0900 Subject: [PATCH 04/25] [Update] translation: Add Plamo API support with authentication and translation functionality --- src-python/config.py | 1 + src-python/controller.py | 55 +++++++++++++ src-python/mainloop.py | 4 + src-python/model.py | 4 + .../translation/translation_languages.py | 38 +++++++++ .../models/translation/translation_plamo.py | 80 +++++++++++++++++++ .../translation/translation_translator.py | 27 ++++++- 7 files changed, 207 insertions(+), 2 deletions(-) create mode 100644 src-python/models/translation/translation_plamo.py diff --git a/src-python/config.py b/src-python/config.py index ce509dc8..aca3203c 100644 --- a/src-python/config.py +++ b/src-python/config.py @@ -1184,6 +1184,7 @@ class Config: self._OSC_PORT = 9000 self._AUTH_KEYS = { "DeepL_API": None, + "Plamo_API": None, } self._USE_EXCLUDE_WORDS = True self._SELECTED_TRANSLATION_COMPUTE_DEVICE = copy.deepcopy(self.SELECTABLE_COMPUTE_DEVICE_LIST[0]) diff --git a/src-python/controller.py b/src-python/controller.py index c34abaf8..514a105e 100644 --- a/src-python/controller.py +++ b/src-python/controller.py @@ -1431,6 +1431,50 @@ 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) == 32: + 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 + self.updateTranslationEngineAndEngineList() + response = {"status":200, "result":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]} + @staticmethod def getCtranslate2WeightType(*args, **kwargs) -> dict: return {"status":200, "result":config.CTRANSLATE2_WEIGHT_TYPE} @@ -2257,6 +2301,17 @@ class Controller: auth_keys = config.AUTH_KEYS auth_keys[engine] = None config.AUTH_KEYS = auth_keys + 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 + else: + # error update Auth key + auth_keys = config.AUTH_KEYS + auth_keys[engine] = None + config.AUTH_KEYS = auth_keys case _: if connected_network is True: config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = True diff --git a/src-python/mainloop.py b/src-python/mainloop.py index 0010b98a..0791b436 100644 --- a/src-python/mainloop.py +++ b/src-python/mainloop.py @@ -168,6 +168,10 @@ mapping = { "/set/data/deepl_auth_key": {"status": False, "variable":controller.setDeeplAuthKey}, "/delete/data/deepl_auth_key": {"status": False, "variable":controller.delDeeplAuthKey}, + "/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/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 b4486bc0..70f828e8 100644 --- a/src-python/model.py +++ b/src-python/model.py @@ -140,6 +140,10 @@ class Model: result = self.translator.authenticationDeepLAuthKey(auth_key) return result + def authenticationTranslatorPlamoAuthKey(self, auth_key): + result = self.translator.authenticationPlamoAuthKey(auth_key) + return result + def startLogger(self): os_makedirs(config.PATH_LOGS, exist_ok=True) file_name = os_path.join(config.PATH_LOGS, f"{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.log") diff --git a/src-python/models/translation/translation_languages.py b/src-python/models/translation/translation_languages.py index 5839e249..f3f334ae 100644 --- a/src-python/models/translation/translation_languages.py +++ b/src-python/models/translation/translation_languages.py @@ -603,4 +603,42 @@ translation_lang["nllb-200-distilled-1.3B-ct2-int8"] = { translation_lang["nllb-200-3.3B-ct2-int8"] = { "source":dict_nllb_languages, "target":dict_nllb_languages, +} + +dict_plamo_languages = { + "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", + "Simplified Chinese":"Simplified Chinese", + "Traditional Chinese":"Traditional Chinese" +} + +translation_lang["Plamo_API"] = { + "source":dict_plamo_languages, + "target":dict_plamo_languages, } \ 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..0d88e8bc --- /dev/null +++ b/src-python/models/translation/translation_plamo.py @@ -0,0 +1,80 @@ +from langchain_openai import ChatOpenAI + +class PlamoClient: + def __init__(self, api_key: str): + self.api_key = api_key + self.base_url = "https://api.platform.preferredai.jp/v1" + self.model = "plamo-2.0-prime" + self.supported_languages = """ + English + Japanese + Korean + French + German + Spanish + Portuguese + Russian + Italian + Dutch + Polish + Turkish + Arabic + Hindi + Thai + Vietnamese + Indonesian + Malay + Filipino + Swedish + Finnish + Danish + Norwegian + Romanian + Czech + Hungarian + Greek + Hebrew + Simplified Chinese + Traditional Chinese + """ + self.prompt_template = f""" + You are a translation assistant that uses the `plamo-translate` tool. + Translate the following text.Supported languages include:{self.supported_languages} + Translate the following text from {{input_lang}} to {{output_lang}}. + output only the translated text without any additional commentary. + """ + self.plamo_llm = ChatOpenAI( + base_url=self.base_url, + model=self.model, + streaming=True, + openai_api_key=self.api_key, + ) + + def translate_text(self, text: str, input_lang: str, output_lang: str): + messages = [ + { + "role": "system", + "content": self.prompt_template.format( + input_lang=input_lang, output_lang=output_lang + ), + }, + {"role": "user", "content": text}, + ] + + output = "" + for chunk in self.plamo_llm.stream(messages): + output += chunk.content + + return output[:-1] + + +if __name__ == "__main__": + text = """ + 毎朝コーヒーを入れるのがささやかな楽しみになってる + """ + input_lang = "Japanese" + output_lang = "English" + + plamo_client = PlamoClient(api_key="AUTH_KEY") + translated_text = plamo_client.translate_text(text, input_lang, output_lang) + print(translated_text) \ 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 23a82d04..212dc56c 100644 --- a/src-python/models/translation/translation_translator.py +++ b/src-python/models/translation/translation_translator.py @@ -9,12 +9,14 @@ except Exception: try: from .translation_languages import translation_lang from .translation_utils import ctranslate2_weights + from .translation_plamo import PlamoClient 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 import ctranslate2 import transformers @@ -27,6 +29,7 @@ warnings.filterwarnings("ignore") class Translator(): def __init__(self): self.deepl_client = None + self.plamo_client = None self.ctranslate2_translator = None self.ctranslate2_tokenizer = None self.is_loaded_ctranslate2_model = False @@ -36,13 +39,24 @@ class Translator(): result = True try: self.deepl_client = DeepLClient(authkey) - self.deepl_client.translate_text(" ", target_lang="EN-US") + self.deepl_client.translate_text("Hello World", target_lang="EN-US") except Exception: errorLogging() self.deepl_client = None result = False return result + def authenticationPlamoAuthKey(self, authkey): + result = True + try: + self.plamo_client = PlamoClient(authkey) + self.plamo_client.translate_text("Hello World", target_lang="English") + except Exception: + errorLogging() + self.plamo_client = None + result = False + return result + def changeCTranslate2Model(self, path, model_type, device="cpu", device_index=0): self.is_loaded_ctranslate2_model = False directory_name = ctranslate2_weights[model_type]["directory_name"] @@ -135,6 +149,15 @@ class Translator(): 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_text( + message, + input_lang=source_language, + output_lang=target_language, + ) case "Google": if self.is_enable_translators is True: result = other_web_Translator( @@ -170,7 +193,7 @@ class Translator(): errorLogging() result = False return result - + if __name__ == "__main__": translator = Translator() # test CTranslate2 model nllb-200-distilled-1.3B-ct2-int8 From d76a5b9e079f2b9b52fbeb247401a698248ce568 Mon Sep 17 00:00:00 2001 From: misyaguziya <53165965+misyaguziya@users.noreply.github.com> Date: Wed, 10 Sep 2025 17:59:31 +0900 Subject: [PATCH 05/25] [Update] requirements: Add langchain-openai to dependencies --- requirements.txt | 1 + requirements_cuda.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/requirements.txt b/requirements.txt index 244a9238..5bcbb597 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,6 +19,7 @@ websockets==15.0.1 huggingface_hub==0.32.2 hf-xet==1.1.2 setuptools==80.8.0 +langchain-openai==0.3.32 translators @ git+https://github.com/misyaguziya/translators@5.9.2.1 SpeechRecognition @ git+https://github.com/misyaguziya/custom_speech_recognition@3.10.4.1 tinyoscquery @ git+https://github.com/cyberkitsune/tinyoscquery@0.1.3 \ No newline at end of file diff --git a/requirements_cuda.txt b/requirements_cuda.txt index 4dadf192..a2b1ad0e 100644 --- a/requirements_cuda.txt +++ b/requirements_cuda.txt @@ -20,6 +20,7 @@ websockets==15.0.1 huggingface_hub==0.32.2 hf-xet==1.1.2 setuptools==80.8.0 +langchain-openai==0.3.32 translators @ git+https://github.com/misyaguziya/translators@5.9.2.1 SpeechRecognition @ git+https://github.com/misyaguziya/custom_speech_recognition@3.10.4.1 tinyoscquery @ git+https://github.com/cyberkitsune/tinyoscquery@0.1.3 \ No newline at end of file From bc269bc1fb013f957283947b781f42695ca25c37 Mon Sep 17 00:00:00 2001 From: misyaguziya <53165965+misyaguziya@users.noreply.github.com> Date: Thu, 11 Sep 2025 14:15:26 +0900 Subject: [PATCH 06/25] [Update] translation: Integrate Plamo API support with model selection and authentication features --- src-python/config.py | 19 ++++ src-python/controller.py | 58 ++++++++++-- src-python/mainloop.py | 2 + src-python/model.py | 6 +- .../models/translation/translation_plamo.py | 93 +++++++++++++++++-- .../translation/translation_translator.py | 12 +-- 6 files changed, 164 insertions(+), 26 deletions(-) diff --git a/src-python/config.py b/src-python/config.py index aca3203c..7fec780f 100644 --- a/src-python/config.py +++ b/src-python/config.py @@ -9,6 +9,7 @@ import torch from device_manager import device_manager from models.translation.translation_languages import translation_lang from models.translation.translation_utils import ctranslate2_weights +from models.translation.translation_plamo import _MODELS as plamo_models from models.transcription.transcription_languages import transcription_lang from models.transcription.transcription_whisper import _MODELS as whisper_models from utils import errorLogging, validateDictStructure @@ -119,6 +120,10 @@ class Config: def SELECTABLE_TRANSLATION_ENGINE_LIST(self): return self._SELECTABLE_TRANSLATION_ENGINE_LIST + @property + def SELECTABLE_PLAMO_MODEL_LIST(self): + return self._SELECTABLE_PLAMO_MODEL_LIST + @property def SELECTABLE_TRANSCRIPTION_ENGINE_LIST(self): return self._SELECTABLE_TRANSCRIPTION_ENGINE_LIST @@ -826,6 +831,18 @@ class Config: self._WHISPER_WEIGHT_TYPE = value self.saveConfig(inspect.currentframe().f_code.co_name, value) + @property + @json_serializable('PLAMO_MODEL') + def PLAMO_MODEL(self): + return self._PLAMO_MODEL + + @PLAMO_MODEL.setter + def PLAMO_MODEL(self, value): + if isinstance(value, str): + if value in self.SELECTABLE_PLAMO_MODEL_LIST: + self._PLAMO_MODEL = value + self.saveConfig(inspect.currentframe().f_code.co_name, value) + @property @json_serializable('AUTO_CLEAR_MESSAGE_BOX') def AUTO_CLEAR_MESSAGE_BOX(self): @@ -1043,6 +1060,7 @@ class Config: self._SELECTABLE_CTRANSLATE2_WEIGHT_TYPE_LIST = ctranslate2_weights.keys() self._SELECTABLE_WHISPER_WEIGHT_TYPE_LIST = whisper_models.keys() self._SELECTABLE_TRANSLATION_ENGINE_LIST = translation_lang.keys() + self._SELECTABLE_PLAMO_MODEL_LIST = plamo_models self._SELECTABLE_TRANSCRIPTION_ENGINE_LIST = list(transcription_lang[list(transcription_lang.keys())[0]].values())[0].keys() self._SELECTABLE_UI_LANGUAGE_LIST = ["en", "ja", "ko", "zh-Hant", "zh-Hans"] self._COMPUTE_MODE = "cuda" if torch.cuda.is_available() else "cpu" @@ -1191,6 +1209,7 @@ class Config: self._SELECTED_TRANSCRIPTION_COMPUTE_DEVICE = copy.deepcopy(self.SELECTABLE_COMPUTE_DEVICE_LIST[0]) self._CTRANSLATE2_WEIGHT_TYPE = "m2m100_418M-ct2-int8" self._WHISPER_WEIGHT_TYPE = "base" + self._PLAMO_MODEL = "plamo-2.0-prime" self._AUTO_CLEAR_MESSAGE_BOX = True self._SEND_ONLY_TRANSLATED_MESSAGES = False self._OVERLAY_SMALL_LOG = False diff --git a/src-python/controller.py b/src-python/controller.py index 514a105e..dc3418d5 100644 --- a/src-python/controller.py +++ b/src-python/controller.py @@ -1431,6 +1431,36 @@ class Controller: self.updateTranslationEngineAndEngineList() return {"status":200, "result":config.AUTH_KEYS[translator_name]} + def getPlamoModelList(self, *args, **kwargs) -> dict: + return {"status":200, "result": config.PLAMO_MODEL_LIST} + + def setPlamoModel(self, data, *args, **kwargs) -> dict: + printLog("Set Plamo Model", data) + try: + data = str(data) + result = model.authenticationTranslatorPlamoAuthKey(auth_key=config.AUTH_KEYS["Plamo_API"], model_name=data) + if result is True: + config.PLAMO_MODEL = data + response = {"status":200, "result":config.PLAMO_MODEL} + else: + response = { + "status":400, + "result":{ + "message":"Plamo model is not valid", + "data": config.PLAMO_MODEL + } + } + except Exception as e: + errorLogging() + response = { + "status":400, + "result":{ + "message":f"Error {e}", + "data": config.PLAMO_MODEL + } + } + return response + def getPlamoAuthKey(self, *args, **kwargs) -> dict: return {"status":200, "result":config.AUTH_KEYS["Plamo_API"]} @@ -1439,14 +1469,24 @@ class Controller: translator_name = "Plamo_API" try: data = str(data) - if len(data) == 32: - 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 - self.updateTranslationEngineAndEngineList() - response = {"status":200, "result":config.AUTH_KEYS[translator_name]} + if len(data) == 72: + result = model.authenticationTranslatorPlamoAuthKey(auth_key=data, model_name=config.PLAMO_MODEL) + 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 + 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, @@ -2305,7 +2345,7 @@ class Controller: 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: + if model.authenticationTranslatorPlamoAuthKey(auth_key=config.AUTH_KEYS[engine], model=config.PLAMO_MODEL) is True: config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = True else: # error update Auth key diff --git a/src-python/mainloop.py b/src-python/mainloop.py index 0791b436..0515a011 100644 --- a/src-python/mainloop.py +++ b/src-python/mainloop.py @@ -168,6 +168,8 @@ 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}, + "/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}, diff --git a/src-python/model.py b/src-python/model.py index 70f828e8..9c2394a1 100644 --- a/src-python/model.py +++ b/src-python/model.py @@ -136,12 +136,12 @@ class Model: del self.keyword_processor self.keyword_processor = KeywordProcessor() - def authenticationTranslatorDeepLAuthKey(self, auth_key): + def authenticationTranslatorDeepLAuthKey(self, auth_key: str) -> bool: result = self.translator.authenticationDeepLAuthKey(auth_key) return result - def authenticationTranslatorPlamoAuthKey(self, auth_key): - result = self.translator.authenticationPlamoAuthKey(auth_key) + def authenticationTranslatorPlamoAuthKey(self, auth_key: str, model: str) -> bool: + result = self.translator.authenticationPlamoAuthKey(auth_key, model=model) return result def startLogger(self): diff --git a/src-python/models/translation/translation_plamo.py b/src-python/models/translation/translation_plamo.py index 0d88e8bc..31046f41 100644 --- a/src-python/models/translation/translation_plamo.py +++ b/src-python/models/translation/translation_plamo.py @@ -1,10 +1,15 @@ from langchain_openai import ChatOpenAI +from pydantic import SecretStr + +_MODELS = [ + "plamo-2.0-prime" + ] class PlamoClient: - def __init__(self, api_key: str): + def __init__(self, api_key: str = "", model: str = "plamo-2.0-prime"): self.api_key = api_key self.base_url = "https://api.platform.preferredai.jp/v1" - self.model = "plamo-2.0-prime" + self.model = model self.supported_languages = """ English Japanese @@ -47,10 +52,55 @@ class PlamoClient: base_url=self.base_url, model=self.model, streaming=True, - openai_api_key=self.api_key, + api_key=SecretStr(self.api_key), ) - def translate_text(self, text: str, input_lang: str, output_lang: str): + def getListModels(self) -> list[str]: + return _MODELS + + def getAuthKey(self) -> str: + """現在のAuthKeyを取得する""" + return self.api_key + + def getModel(self) -> str: + """現在のモデルを取得する""" + return self.model + + def setAuthKey(self, api_key: str) -> bool: + """AuthKeyを設定し、成功したかどうかを返す""" + try: + self.api_key = api_key + self.plamo_llm = ChatOpenAI( + base_url=self.base_url, + model=self.model, + streaming=True, + api_key=SecretStr(self.api_key), + ) + return True + except Exception as e: + print(f"Error setting AuthKey: {e}") + return False + + def setModel(self, model: str) -> bool: + """モデルを設定し、成功したかどうかを返す""" + if model not in _MODELS: + print(f"Model {model} is not in the supported model list.") + return False + + try: + self.model = model + self.plamo_llm = ChatOpenAI( + base_url=self.base_url, + model=self.model, + streaming=True, + api_key=SecretStr(self.api_key), + ) + return True + except Exception as e: + print(f"Error setting model: {e}") + return False + + def translate(self, text: str, input_lang: str, output_lang: str) -> str: messages = [ { "role": "system", @@ -63,18 +113,45 @@ class PlamoClient: output = "" for chunk in self.plamo_llm.stream(messages): - output += chunk.content + if isinstance(chunk.content, str): + output += chunk.content + elif isinstance(chunk.content, list): + for item in chunk.content: + if isinstance(item, str): + output += item + elif isinstance(item, dict): + if "content" in item and isinstance(item["content"], str): + output += item["content"] return output[:-1] + def checkAuthKey(self) -> bool: + try: + self.setModel(self.model) + self.translate("Hello World", input_lang="English", output_lang="Japanese") + return True + except Exception: + return False if __name__ == "__main__": + AUTH_KEY = "AUTH_KEY" text = """ 毎朝コーヒーを入れるのがささやかな楽しみになってる """ input_lang = "Japanese" output_lang = "English" - plamo_client = PlamoClient(api_key="AUTH_KEY") - translated_text = plamo_client.translate_text(text, input_lang, output_lang) - print(translated_text) \ No newline at end of file + plamo_client = PlamoClient(api_key=AUTH_KEY) + + print("model list:", plamo_client.getListModels()) + print("AuthKey:", plamo_client.getAuthKey()) + print("Model:", plamo_client.getModel()) + print(f"set model: {plamo_client.setModel('plamo-2.0-prime')}") + print(f"set AuthKey: {plamo_client.setAuthKey(AUTH_KEY)}") + print(f"check AuthKey: {plamo_client.checkAuthKey()}") + + try: + translated_text = plamo_client.translate(text, input_lang, output_lang) + print(translated_text) + except Exception: + print("Invalid API key. Please check your credentials.") \ 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 212dc56c..e187d52a 100644 --- a/src-python/models/translation/translation_translator.py +++ b/src-python/models/translation/translation_translator.py @@ -35,10 +35,10 @@ class Translator(): self.is_loaded_ctranslate2_model = False self.is_enable_translators = ENABLE_TRANSLATORS - def authenticationDeepLAuthKey(self, authkey): + def authenticationDeepLAuthKey(self, auth_key: str) -> bool: result = True try: - self.deepl_client = DeepLClient(authkey) + self.deepl_client = DeepLClient(auth_key) self.deepl_client.translate_text("Hello World", target_lang="EN-US") except Exception: errorLogging() @@ -46,11 +46,11 @@ class Translator(): result = False return result - def authenticationPlamoAuthKey(self, authkey): + def authenticationPlamoAuthKey(self, auth_key: str, model: str) -> bool: result = True try: - self.plamo_client = PlamoClient(authkey) - self.plamo_client.translate_text("Hello World", target_lang="English") + self.plamo_client = PlamoClient(auth_key, model=model) + self.plamo_client.checkAuthKey() except Exception: errorLogging() self.plamo_client = None @@ -153,7 +153,7 @@ class Translator(): if self.plamo_client is None: result = False else: - result = self.plamo_client.translate_text( + result = self.plamo_client.translate( message, input_lang=source_language, output_lang=target_language, From 7bed19c81fe06edc66f98055e6c9b740e0a9e8a2 Mon Sep 17 00:00:00 2001 From: misyaguziya <53165965+misyaguziya@users.noreply.github.com> Date: Thu, 11 Sep 2025 18:02:12 +0900 Subject: [PATCH 07/25] [Update] controller: Change Plamo model list to selectable models --- src-python/controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-python/controller.py b/src-python/controller.py index dc3418d5..bbc32e13 100644 --- a/src-python/controller.py +++ b/src-python/controller.py @@ -1432,7 +1432,7 @@ class Controller: return {"status":200, "result":config.AUTH_KEYS[translator_name]} def getPlamoModelList(self, *args, **kwargs) -> dict: - return {"status":200, "result": config.PLAMO_MODEL_LIST} + return {"status":200, "result": config.SELECTABLE_PLAMO_MODEL_LIST} def setPlamoModel(self, data, *args, **kwargs) -> dict: printLog("Set Plamo Model", data) From 4e53584cffb1d70e43e2d79e4f5f8f5e14e635f4 Mon Sep 17 00:00:00 2001 From: misyaguziya <53165965+misyaguziya@users.noreply.github.com> Date: Thu, 11 Sep 2025 23:11:39 +0900 Subject: [PATCH 08/25] [Update] mainloop: Improve receiver loop to handle empty input and add stop message [Update] osc: Refactor OSC query service initialization to run in a separate thread --- src-python/mainloop.py | 20 +++++++++++++------- src-python/models/osc/osc.py | 20 +++++++++++--------- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/src-python/mainloop.py b/src-python/mainloop.py index 0515a011..b44469f9 100644 --- a/src-python/mainloop.py +++ b/src-python/mainloop.py @@ -358,8 +358,11 @@ class Main: self.main_loop = True def receiver(self) -> None: - while True: + while self.main_loop: received_data = sys.stdin.readline().strip() + if not received_data: + time.sleep(0.1) + continue received_data = json.loads(received_data) if received_data: @@ -441,11 +444,13 @@ if __name__ == "__main__": main.start() case "test": - for _ in range(100): - time.sleep(0.5) - endpoint = "/get/data/mic_host_list" - result, status = main.handleRequest(endpoint) - printResponse(status, endpoint, result) + endpoint = "/set/enable/translation" + result, status = main.handleRequest(endpoint) + printResponse(status, endpoint, result) + endpoint = "/run/send_message_box" + data = {"id":"123456", "message":"テスト"} + result, status = main.handleRequest(endpoint, data) + printResponse(status, endpoint, result) case "test_all": import time @@ -647,4 +652,5 @@ if __name__ == "__main__": result, status = main.handleRequest(endpoint, data) printResponse(status, endpoint, result) - time.sleep(0.5) \ No newline at end of file + time.sleep(0.5) + main.stop() \ No newline at end of file diff --git a/src-python/models/osc/osc.py b/src-python/models/osc/osc.py index a47f1f8d..3e56fc0e 100644 --- a/src-python/models/osc/osc.py +++ b/src-python/models/osc/osc.py @@ -118,15 +118,17 @@ class OSCHandler: self.osc_server = osc_server.ThreadingOSCUDPServer((self.osc_server_ip_address, self.osc_server_port), osc_dispatcher) Thread(target=self.oscServerServe, daemon=True).start() - while True: - try: - self.osc_query_service = OSCQueryService(self.osc_query_service_name, self.http_port, self.osc_server_port) - for filter, target in self.dict_filter_and_target.items(): - self.osc_query_service.advertise_endpoint(filter, access=OSCAccess.READWRITE_VALUE) - break - except Exception: - errorLogging() - sleep(1) + def startOSCQueryService(): + while True: + try: + self.osc_query_service = OSCQueryService(self.osc_query_service_name, self.http_port, self.osc_server_port) + for filter, target in self.dict_filter_and_target.items(): + self.osc_query_service.advertise_endpoint(filter, access=OSCAccess.READWRITE_VALUE) + break + except Exception: + errorLogging() + sleep(1) + Thread(target=startOSCQueryService, daemon=True).start() def oscServerServe(self) -> None: # ポーリング間隔を長くして(2秒から10秒に)CPUの使用率を削減 From e3555d204e3b307f851375a921c02c8da2e904c0 Mon Sep 17 00:00:00 2001 From: misyaguziya <53165965+misyaguziya@users.noreply.github.com> Date: Fri, 12 Sep 2025 14:40:35 +0900 Subject: [PATCH 09/25] [Update] requirements: Add langchain-google-genai to dependencies; [Add] translation_gemini: Implement GeminiClient for translation functionality --- requirements.txt | 1 + requirements_cuda.txt | 1 + .../models/translation/translation_gemini.py | 150 ++++++++++++++++++ 3 files changed, 152 insertions(+) create mode 100644 src-python/models/translation/translation_gemini.py diff --git a/requirements.txt b/requirements.txt index 5bcbb597..23f4abf6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,6 +20,7 @@ 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 translators @ git+https://github.com/misyaguziya/translators@5.9.2.1 SpeechRecognition @ git+https://github.com/misyaguziya/custom_speech_recognition@3.10.4.1 tinyoscquery @ git+https://github.com/cyberkitsune/tinyoscquery@0.1.3 \ No newline at end of file diff --git a/requirements_cuda.txt b/requirements_cuda.txt index a2b1ad0e..6ea366d4 100644 --- a/requirements_cuda.txt +++ b/requirements_cuda.txt @@ -21,6 +21,7 @@ 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 translators @ git+https://github.com/misyaguziya/translators@5.9.2.1 SpeechRecognition @ git+https://github.com/misyaguziya/custom_speech_recognition@3.10.4.1 tinyoscquery @ git+https://github.com/cyberkitsune/tinyoscquery@0.1.3 \ 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..51f43933 --- /dev/null +++ b/src-python/models/translation/translation_gemini.py @@ -0,0 +1,150 @@ +from langchain_google_genai import ChatGoogleGenerativeAI +from langchain_core.messages import HumanMessage + +_MODELS = [ + "gemini-2.5-pro", + "gemini-2.5-flash", + "gemini-2.5-flash-lite", + "gemini-2.0-flash", + "gemini-2.0-flash-lite", + "gemini-1.5-pro", + "gemini-1.5-flash-8b" + "gemini-1.5-flash", + ] + +class GeminiClient: + def __init__(self, api_key: str = "", model: str = "gemini-2.5-flash"): + self.api_key = api_key + self.model = model + self.prompt_template = """ + Please translate the following text from {input_lang} to {output_lang}. + Only provide the translated text as the output. + {text} + """ + self.gemini_llm = ChatGoogleGenerativeAI( + model=self.model, + api_key=self.api_key, + ) + + def getListModels(self) -> list[str]: + return _MODELS + + def getAuthKey(self) -> str: + """現在のAuthKeyを取得する""" + return self.api_key + + def getModel(self) -> str: + """現在のモデルを取得する""" + return self.model + + def setAuthKey(self, api_key: str) -> bool: + """AuthKeyを設定し、成功したかどうかを返す""" + try: + self.api_key = api_key + self.gemini_llm = ChatGoogleGenerativeAI( + model=self.model, + api_key=self.api_key, + ) + return True + except Exception as e: + print(f"Error setting AuthKey: {e}") + return False + + def setModel(self, model: str) -> bool: + """モデルを設定し、成功したかどうかを返す""" + try: + if model in _MODELS: + self.model = model + self.gemini_llm = ChatGoogleGenerativeAI( + model=self.model, + api_key=self.api_key, + ) + return True + else: + print(f"Model {model} is not supported.") + return False + except Exception as e: + print(f"Error setting model: {e}") + return False + + def translate(self, text: str, input_lang: str, output_lang: str) -> str: + messages = self.prompt_template.format( + input_lang=input_lang, + output_lang=output_lang, + text=text + ) + + output = self.gemini_llm.invoke([HumanMessage(content=messages)]) + return output.content + + def checkAuthKey(self) -> bool: + try: + self.setModel(self.model) + self.translate("Hello World", input_lang="English", output_lang="Japanese") + return True + except Exception: + return False + +if __name__ == "__main__": + AUTH_KEY = "AUTH_KEY" + text = """ + 毎朝コーヒーを入れるのがささやかな楽しみになってる + """ + input_lang = "Japanese" + output_lang = "English" + + gemini_client = GeminiClient(api_key=AUTH_KEY, model="gemini-1.5-flash") + + print("model list:", gemini_client.getListModels()) + print("AuthKey:", gemini_client.getAuthKey()) + # print("Model:", gemini_client.getModel()) + # print(f"set model: {gemini_client.setModel('gemini-2.5-flash')}") + # print(f"set AuthKey: {gemini_client.setAuthKey(AUTH_KEY)}") + # print(f"check AuthKey: {gemini_client.checkAuthKey()}") + + # try: + # translated_text = gemini_client.translate(text, input_lang, output_lang) + # print(translated_text) + # except Exception: + # print("Invalid API key. Please check your credentials.") + + + supported_languages = """ + English + Japanese + Korean + French + German + Spanish + Portuguese + Russian + Italian + Dutch + Polish + Turkish + Arabic + Hindi + Thai + Vietnamese + Indonesian + Malay + Filipino + Swedish + Finnish + Danish + Norwegian + Romanian + Czech + Hungarian + Greek + Hebrew + Simplified Chinese + Traditional Chinese + """ + + for lang in supported_languages.split("\n"): + if lang == "": + continue + print (f"Translating to {lang}:") + translated_text = gemini_client.translate(text, input_lang, lang) + print(f"Translated text: {translated_text}") From 224eaf3cef18dd68e873b6b6a22c6350057ed979 Mon Sep 17 00:00:00 2001 From: misyaguziya <53165965+misyaguziya@users.noreply.github.com> Date: Mon, 15 Sep 2025 17:42:09 +0900 Subject: [PATCH 10/25] [Add] translation_gemini: Integrate Gemini translation model and authentication; [Update] controller: Implement methods for managing Gemini models and auth keys; [Update] mainloop: Add routes for Gemini model and auth key management; [Update] translation: Enhance Translator class for Gemini API support; [Update] translation_languages: Add Gemini language mappings --- src-python/config.py | 20 +++++ src-python/controller.py | 84 +++++++++++++++++++ src-python/mainloop.py | 6 ++ src-python/model.py | 4 + .../models/translation/translation_gemini.py | 82 +++++++++++------- .../translation/translation_languages.py | 47 +++++++++++ .../translation/translation_translator.py | 23 +++++ 7 files changed, 234 insertions(+), 32 deletions(-) diff --git a/src-python/config.py b/src-python/config.py index 7fec780f..5b425a38 100644 --- a/src-python/config.py +++ b/src-python/config.py @@ -10,6 +10,7 @@ from device_manager import device_manager from models.translation.translation_languages import translation_lang from models.translation.translation_utils import ctranslate2_weights from models.translation.translation_plamo import _MODELS as plamo_models +from models.translation.translation_gemini import _MODELS as gemini_models from models.transcription.transcription_languages import transcription_lang from models.transcription.transcription_whisper import _MODELS as whisper_models from utils import errorLogging, validateDictStructure @@ -124,6 +125,10 @@ class Config: def SELECTABLE_PLAMO_MODEL_LIST(self): return self._SELECTABLE_PLAMO_MODEL_LIST + @property + def SELECTABLE_GEMINI_MODEL_LIST(self): + return self._SELECTABLE_GEMINI_MODEL_LIST + @property def SELECTABLE_TRANSCRIPTION_ENGINE_LIST(self): return self._SELECTABLE_TRANSCRIPTION_ENGINE_LIST @@ -843,6 +848,18 @@ class Config: self._PLAMO_MODEL = value self.saveConfig(inspect.currentframe().f_code.co_name, value) + @property + @json_serializable('GEMINI_MODEL') + def GEMINI_MODEL(self): + return self._GEMINI_MODEL + + @GEMINI_MODEL.setter + def GEMINI_MODEL(self, value): + if isinstance(value, str): + if value in self.SELECTABLE_GEMINI_MODEL_LIST: + self._GEMINI_MODEL = value + self.saveConfig(inspect.currentframe().f_code.co_name, value) + @property @json_serializable('AUTO_CLEAR_MESSAGE_BOX') def AUTO_CLEAR_MESSAGE_BOX(self): @@ -1061,6 +1078,7 @@ class Config: self._SELECTABLE_WHISPER_WEIGHT_TYPE_LIST = whisper_models.keys() self._SELECTABLE_TRANSLATION_ENGINE_LIST = translation_lang.keys() self._SELECTABLE_PLAMO_MODEL_LIST = plamo_models + self._SELECTABLE_GEMINI_MODEL_LIST = gemini_models self._SELECTABLE_TRANSCRIPTION_ENGINE_LIST = list(transcription_lang[list(transcription_lang.keys())[0]].values())[0].keys() self._SELECTABLE_UI_LANGUAGE_LIST = ["en", "ja", "ko", "zh-Hant", "zh-Hans"] self._COMPUTE_MODE = "cuda" if torch.cuda.is_available() else "cpu" @@ -1203,6 +1221,7 @@ class Config: self._AUTH_KEYS = { "DeepL_API": None, "Plamo_API": None, + "Gemini_API": None, } self._USE_EXCLUDE_WORDS = True self._SELECTED_TRANSLATION_COMPUTE_DEVICE = copy.deepcopy(self.SELECTABLE_COMPUTE_DEVICE_LIST[0]) @@ -1210,6 +1229,7 @@ class Config: self._CTRANSLATE2_WEIGHT_TYPE = "m2m100_418M-ct2-int8" self._WHISPER_WEIGHT_TYPE = "base" self._PLAMO_MODEL = "plamo-2.0-prime" + self._GEMINI_MODEL = "gemini-2.5-flash-lite" self._AUTO_CLEAR_MESSAGE_BOX = True self._SEND_ONLY_TRANSLATED_MESSAGES = False self._OVERLAY_SMALL_LOG = False diff --git a/src-python/controller.py b/src-python/controller.py index bbc32e13..abb4605c 100644 --- a/src-python/controller.py +++ b/src-python/controller.py @@ -1515,6 +1515,90 @@ class Controller: 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 setGeminiModel(self, data, *args, **kwargs) -> dict: + printLog("Set Gemini Model", data) + try: + data = str(data) + result = model.authenticationTranslatorGeminiAuthKey(auth_key=config.AUTH_KEYS["Gemini_API"], model_name=data) + if result is True: + config.GEMINI_MODEL = data + response = {"status":200, "result":config.GEMINI_MODEL} + else: + response = { + "status":400, + "result":{ + "message":"Gemini model is not valid", + "data": config.GEMINI_MODEL + } + } + except Exception as e: + errorLogging() + response = { + "status":400, + "result":{ + "message":f"Error {e}", + "data": config.GEMINI_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) >= 20: + result = model.authenticationTranslatorGeminiAuthKey(auth_key=data, model_name=config.GEMINI_MODEL) + 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 + 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]} + @staticmethod def getCtranslate2WeightType(*args, **kwargs) -> dict: return {"status":200, "result":config.CTRANSLATE2_WEIGHT_TYPE} diff --git a/src-python/mainloop.py b/src-python/mainloop.py index b44469f9..f6b52afa 100644 --- a/src-python/mainloop.py +++ b/src-python/mainloop.py @@ -174,6 +174,12 @@ mapping = { "/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}, + "/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/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 9c2394a1..e99aeaa5 100644 --- a/src-python/model.py +++ b/src-python/model.py @@ -144,6 +144,10 @@ class Model: result = self.translator.authenticationPlamoAuthKey(auth_key, model=model) return result + def authenticationTranslatorGeminiAuthKey(self, auth_key: str, model: str) -> bool: + result = self.translator.authenticationGeminiAuthKey(auth_key, model=model) + return result + def startLogger(self): os_makedirs(config.PATH_LOGS, exist_ok=True) file_name = os_path.join(config.PATH_LOGS, f"{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.log") diff --git a/src-python/models/translation/translation_gemini.py b/src-python/models/translation/translation_gemini.py index 51f43933..225f495a 100644 --- a/src-python/models/translation/translation_gemini.py +++ b/src-python/models/translation/translation_gemini.py @@ -1,10 +1,14 @@ +import logging from langchain_google_genai import ChatGoogleGenerativeAI from langchain_core.messages import HumanMessage +logger = logging.getLogger("langchain_google_genai") +logger.setLevel(logging.ERROR) + _MODELS = [ "gemini-2.5-pro", "gemini-2.5-flash", - "gemini-2.5-flash-lite", + "gemini-2.5-flash-lite", # default "gemini-2.0-flash", "gemini-2.0-flash-lite", "gemini-1.5-pro", @@ -13,7 +17,7 @@ _MODELS = [ ] class GeminiClient: - def __init__(self, api_key: str = "", model: str = "gemini-2.5-flash"): + def __init__(self, api_key: str = "", model: str = "gemini-2.5-flash-lite"): self.api_key = api_key self.model = model self.prompt_template = """ @@ -93,7 +97,7 @@ if __name__ == "__main__": input_lang = "Japanese" output_lang = "English" - gemini_client = GeminiClient(api_key=AUTH_KEY, model="gemini-1.5-flash") + gemini_client = GeminiClient(api_key=AUTH_KEY, model="gemini-2.5-flash-lite") print("model list:", gemini_client.getListModels()) print("AuthKey:", gemini_client.getAuthKey()) @@ -110,41 +114,55 @@ if __name__ == "__main__": supported_languages = """ - English - Japanese - Korean - French - German - Spanish - Portuguese - Russian - Italian - Dutch - Polish - Turkish Arabic - Hindi - Thai - Vietnamese - Indonesian - Malay - Filipino - Swedish - Finnish - Danish - Norwegian - Romanian - Czech - Hungarian - Greek - Hebrew + Bengali + Bulgarian Simplified Chinese Traditional Chinese + Croatian + Czech + Danish + Dutch + English + Estonian + Finnish + French + German + Greek + Hebrew + Hindi + Hungarian + Indonesian + Italian + Japanese + Korean + Latvian + Lithuanian + Norwegian + Polish + Portuguese + Romanian + Russian + Serbian + Slovak + Slovenian + Spanish + Swahili + Swedish + Thai + Turkish + Ukrainian + Vietnamese """ for lang in supported_languages.split("\n"): if lang == "": continue print (f"Translating to {lang}:") - translated_text = gemini_client.translate(text, input_lang, lang) - print(f"Translated text: {translated_text}") + try: + translated_text = gemini_client.translate(text, input_lang, lang) + print(f"Translated text: {translated_text}") + except Exception as e: + print(f"Error translating to {lang} api limit") + print(f"Error reason: {e}") + break \ 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 f3f334ae..de6ae5f8 100644 --- a/src-python/models/translation/translation_languages.py +++ b/src-python/models/translation/translation_languages.py @@ -641,4 +641,51 @@ dict_plamo_languages = { translation_lang["Plamo_API"] = { "source":dict_plamo_languages, "target":dict_plamo_languages, +} + +dict_gemini_languages = { + "Arabic": "Arabic", + "Bengali": "Bengali", + "Bulgarian": "Bulgarian", + "Simplified Chinese": "Simplified Chinese", + "Traditional Chinese": "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", + "Swahili": "Swahili", + "Swedish": "Swedish", + "Thai": "Thai", + "Turkish": "Turkish", + "Ukrainian": "Ukrainian", + "Vietnamese": "Vietnamese", +} + +translation_lang["Gemini_API"] = { + "source":dict_gemini_languages, + "target":dict_gemini_languages, } \ 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 e187d52a..93728570 100644 --- a/src-python/models/translation/translation_translator.py +++ b/src-python/models/translation/translation_translator.py @@ -10,6 +10,7 @@ try: from .translation_languages import translation_lang from .translation_utils import ctranslate2_weights from .translation_plamo import PlamoClient + from .translation_gemini import GeminiClient except Exception: import sys print(os_path.dirname(os_path.dirname(os_path.dirname(os_path.abspath(__file__))))) @@ -17,6 +18,7 @@ except Exception: from translation_languages import translation_lang from translation_utils import ctranslate2_weights from translation_plamo import PlamoClient + from translation_gemini import GeminiClient import ctranslate2 import transformers @@ -30,6 +32,7 @@ class Translator(): def __init__(self): self.deepl_client = None self.plamo_client = None + self.gemini_client = None self.ctranslate2_translator = None self.ctranslate2_tokenizer = None self.is_loaded_ctranslate2_model = False @@ -57,6 +60,17 @@ class Translator(): result = False return result + def authenticationGeminiAuthKey(self, auth_key: str, model: str) -> bool: + result = True + try: + self.gemini_client = GeminiClient(auth_key, model=model) + self.gemini_client.checkAuthKey() + except Exception: + errorLogging() + self.gemini_client = None + result = False + return result + def changeCTranslate2Model(self, path, model_type, device="cpu", device_index=0): self.is_loaded_ctranslate2_model = False directory_name = ctranslate2_weights[model_type]["directory_name"] @@ -158,6 +172,15 @@ class Translator(): 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 "Google": if self.is_enable_translators is True: result = other_web_Translator( From 7d0f63c1184a97eb7366275a9244d24759c6c0f2 Mon Sep 17 00:00:00 2001 From: misyaguziya <53165965+misyaguziya@users.noreply.github.com> Date: Tue, 14 Oct 2025 23:15:49 +0900 Subject: [PATCH 11/25] =?UTF-8?q?CTranslate2=E3=81=AB=E5=AF=BE=E5=BF=9C?= =?UTF-8?q?=E3=81=99=E3=82=8B=E7=BF=BB=E8=A8=B3=E8=A8=80=E8=AA=9E=E3=81=AE?= =?UTF-8?q?=E8=A8=AD=E5=AE=9A=E3=82=92=E8=BF=BD=E5=8A=A0=E3=81=97=E3=80=81?= =?UTF-8?q?=E7=BF=BB=E8=A8=B3=E3=83=AD=E3=82=B8=E3=83=83=E3=82=AF=E3=82=92?= =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-python/model.py | 13 ++++++++++--- .../models/translation/translation_languages.py | 9 +++++---- .../models/translation/translation_translator.py | 10 ++++++---- src-python/models/translation/translation_utils.py | 4 ++-- 4 files changed, 23 insertions(+), 13 deletions(-) diff --git a/src-python/model.py b/src-python/model.py index 494c72fb..3d5537a8 100644 --- a/src-python/model.py +++ b/src-python/model.py @@ -222,8 +222,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)) @@ -243,7 +247,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()) diff --git a/src-python/models/translation/translation_languages.py b/src-python/models/translation/translation_languages.py index 9a54430a..b08292aa 100644 --- a/src-python/models/translation/translation_languages.py +++ b/src-python/models/translation/translation_languages.py @@ -372,8 +372,9 @@ dict_m2m100_languages = { "Sundanese": "su" } -translation_lang["m2m100_418M-ct2-int8"] = {"source":dict_m2m100_languages, "target":dict_m2m100_languages} -translation_lang["m2m100_1.2B-ct2-int8"] = {"source":dict_m2m100_languages, "target":dict_m2m100_languages} +translation_lang["CTranslate2"] = {} +translation_lang["CTranslate2"]["m2m100_418M-ct2-int8"] = {"source":dict_m2m100_languages, "target":dict_m2m100_languages} +translation_lang["CTranslate2"]["m2m100_1.2B-ct2-int8"] = {"source":dict_m2m100_languages, "target":dict_m2m100_languages} dict_nllb_languages = { "Acehnese (Arabic script)": "ace_Arab", @@ -582,8 +583,8 @@ dict_nllb_languages = { "Zulu": "zul_Latn" } -translation_lang["nllb-200-distilled-1.3B-ct2-int8"] = {"source":dict_nllb_languages, "target":dict_nllb_languages} -translation_lang["nllb-200-3.3B-ct2-int8"] = {"source":dict_nllb_languages, "target":dict_nllb_languages} +translation_lang["CTranslate2"]["nllb-200-distilled-1.3B-ct2-int8"] = {"source":dict_nllb_languages, "target":dict_nllb_languages} +translation_lang["CTranslate2"]["nllb-200-3.3B-ct2-int8"] = {"source":dict_nllb_languages, "target":dict_nllb_languages} dict_plamo_languages = { "English": "English", diff --git a/src-python/models/translation/translation_translator.py b/src-python/models/translation/translation_translator.py index 8b240ffd..bc4d26f1 100644 --- a/src-python/models/translation/translation_translator.py +++ b/src-python/models/translation/translation_translator.py @@ -177,12 +177,14 @@ 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": - translator_name = weight_type + 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, weight_type: str, source_language: str, target_language: str, target_country: str, message: str) -> Any: diff --git a/src-python/models/translation/translation_utils.py b/src-python/models/translation/translation_utils.py index 688f131f..895a9680 100644 --- a/src-python/models/translation/translation_utils.py +++ b/src-python/models/translation/translation_utils.py @@ -95,8 +95,8 @@ def downloadCTranslate2Tokenizer(path: str, weight_type: str = "m2m100_418M-ct2- tokenizer = ctranslate2_weights[weight_type]["tokenizer"] tokenizer_path = os_path.join(path, "weights", "ctranslate2", directory_name, "tokenizer") try: - os_makedirs(tokenizer_cache, exist_ok=True) - transformers.AutoTokenizer.from_pretrained(tokenizer_name, cache_dir=tokenizer_cache) + os_makedirs(tokenizer_path, exist_ok=True) + transformers.AutoTokenizer.from_pretrained(tokenizer, cache_dir=tokenizer_path) except Exception: errorLogging() tokenizer_path = os_path.join("./weights", "ctranslate2", directory_name, "tokenizer") From f8466bd6e4a19d8ddc7434e22df7972f1e2ec191 Mon Sep 17 00:00:00 2001 From: misyaguziya <53165965+misyaguziya@users.noreply.github.com> Date: Wed, 15 Oct 2025 18:50:26 +0900 Subject: [PATCH 12/25] =?UTF-8?q?=E7=BF=BB=E8=A8=B3API=E3=81=AE=E8=AA=8D?= =?UTF-8?q?=E8=A8=BC=E3=83=AD=E3=82=B8=E3=83=83=E3=82=AF=E3=82=92=E6=94=B9?= =?UTF-8?q?=E5=96=84=E3=81=97=E3=80=81YAML=E3=83=95=E3=82=A1=E3=82=A4?= =?UTF-8?q?=E3=83=AB=E3=81=8B=E3=82=89=E3=83=97=E3=83=AD=E3=83=B3=E3=83=97?= =?UTF-8?q?=E3=83=88=E8=A8=AD=E5=AE=9A=E3=82=92=E8=AA=AD=E3=81=BF=E8=BE=BC?= =?UTF-8?q?=E3=82=80=E6=A9=9F=E8=83=BD=E3=82=92=E8=BF=BD=E5=8A=A0=E3=80=82?= =?UTF-8?q?=E6=96=B0=E3=81=97=E3=81=84=E3=83=95=E3=82=A9=E3=83=B3=E3=83=88?= =?UTF-8?q?=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB=E3=82=92=E8=BF=BD=E5=8A=A0?= =?UTF-8?q?=E3=81=97=E3=80=81=E3=83=87=E3=83=BC=E3=82=BF=E3=83=91=E3=82=B9?= =?UTF-8?q?=E3=82=92=E6=9B=B4=E6=96=B0=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend.spec | 9 +- backend_cuda.spec | 9 +- src-python/controller.py | 17 ++++ src-python/model.py | 4 +- .../overlay/fonts}/NotoSansJP-Regular.ttf | Bin .../overlay/fonts}/NotoSansKR-Regular.ttf | Bin .../overlay/fonts}/NotoSansSC-Regular.ttf | Bin .../overlay/fonts}/NotoSansTC-Regular.ttf | Bin src-python/models/overlay/overlay_image.py | 59 ++++--------- .../translation/prompt/translation_gemini.yml | 45 ++++++++++ .../translation/prompt/translation_plamo.yml | 39 +++++++++ .../models/translation/translation_gemini.py | 80 +++++++----------- .../translation/translation_languages.py | 8 +- .../models/translation/translation_plamo.py | 76 ++++++++--------- .../translation/translation_translator.py | 8 +- 15 files changed, 206 insertions(+), 148 deletions(-) rename {fonts => src-python/models/overlay/fonts}/NotoSansJP-Regular.ttf (100%) rename {fonts => src-python/models/overlay/fonts}/NotoSansKR-Regular.ttf (100%) rename {fonts => src-python/models/overlay/fonts}/NotoSansSC-Regular.ttf (100%) rename {fonts => src-python/models/overlay/fonts}/NotoSansTC-Regular.ttf (100%) create mode 100644 src-python/models/translation/prompt/translation_gemini.yml create mode 100644 src-python/models/translation/prompt/translation_plamo.yml diff --git a/backend.spec b/backend.spec index c5c33da0..b13ce288 100644 --- a/backend.spec +++ b/backend.spec @@ -5,7 +5,14 @@ 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/'), + ('.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..3ed416ac 100644 --- a/backend_cuda.spec +++ b/backend_cuda.spec @@ -5,7 +5,14 @@ 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/'), + ('.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/src-python/controller.py b/src-python/controller.py index b9eeca7a..2c33751d 100644 --- a/src-python/controller.py +++ b/src-python/controller.py @@ -2640,22 +2640,39 @@ 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], model=config.PLAMO_MODEL) is True: config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = True + printLog("Plamo API Key is valid") 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], model=config.GEMINI_MODEL) is True: + config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = True + printLog("Gemini API Key is valid") + 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 _: 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 3d5537a8..2cbc611c 100644 --- a/src-python/model.py +++ b/src-python/model.py @@ -199,11 +199,11 @@ class Model: return result def authenticationTranslatorPlamoAuthKey(self, auth_key: str, model: str) -> bool: - result = self.translator.authenticationPlamoAuthKey(auth_key, model=model) + result = self.translator.authenticationPlamoAuthKey(auth_key, model=model, root_path=config.PATH_LOCAL) return result def authenticationTranslatorGeminiAuthKey(self, auth_key: str, model: str) -> bool: - result = self.translator.authenticationGeminiAuthKey(auth_key, model=model) + result = self.translator.authenticationGeminiAuthKey(auth_key, model=model, root_path=config.PATH_LOCAL) return result def startLogger(self): 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/prompt/translation_gemini.yml b/src-python/models/translation/prompt/translation_gemini.yml new file mode 100644 index 00000000..478b2c5e --- /dev/null +++ b/src-python/models/translation/prompt/translation_gemini.yml @@ -0,0 +1,45 @@ +system_prompt: | + Please translate the following text from {input_lang} to {output_lang}. + Only provide the translated text as the output. + {text} + +supported_languages: | + Arabic + Bengali + Bulgarian + Simplified Chinese + Traditional Chinese + Croatian + Czech + Danish + Dutch + English + Estonian + Finnish + French + German + Greek + Hebrew + Hindi + Hungarian + Indonesian + Italian + Japanese + Korean + Latvian + Lithuanian + Norwegian + Polish + Portuguese + Romanian + Russian + Serbian + Slovak + Slovenian + Spanish + Swahili + Swedish + Thai + Turkish + Ukrainian + Vietnamese 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..cdb0b478 --- /dev/null +++ b/src-python/models/translation/prompt/translation_plamo.yml @@ -0,0 +1,39 @@ +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. + +supported_languages: | + English + Japanese + Korean + French + German + Spanish + Portuguese + Russian + Italian + Dutch + Polish + Turkish + Arabic + Hindi + Thai + Vietnamese + Indonesian + Malay + Filipino + Swedish + Finnish + Danish + Norwegian + Romanian + Czech + Hungarian + Greek + Hebrew + Simplified Chinese + Traditional Chinese diff --git a/src-python/models/translation/translation_gemini.py b/src-python/models/translation/translation_gemini.py index 225f495a..9311c2d3 100644 --- a/src-python/models/translation/translation_gemini.py +++ b/src-python/models/translation/translation_gemini.py @@ -1,6 +1,8 @@ import logging from langchain_google_genai import ChatGoogleGenerativeAI from langchain_core.messages import HumanMessage +import yaml +from os import path as os_path logger = logging.getLogger("langchain_google_genai") logger.setLevel(logging.ERROR) @@ -17,19 +19,39 @@ _MODELS = [ ] class GeminiClient: - def __init__(self, api_key: str = "", model: str = "gemini-2.5-flash-lite"): + def __init__(self, api_key: str = "", model: str = "gemini-2.5-flash-lite", root_path: str = None): self.api_key = api_key self.model = model - self.prompt_template = """ - Please translate the following text from {input_lang} to {output_lang}. - Only provide the translated text as the output. - {text} - """ + + # プロンプト設定をYAMLファイルから読み込む + prompt_config = self._load_prompt_config(root_path) + self.supported_languages = prompt_config["supported_languages"] + self.prompt_template = prompt_config["system_prompt"] + self.gemini_llm = ChatGoogleGenerativeAI( model=self.model, api_key=self.api_key, ) + def _load_prompt_config(self, 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) + def getListModels(self) -> list[str]: return _MODELS @@ -112,50 +134,8 @@ if __name__ == "__main__": # except Exception: # print("Invalid API key. Please check your credentials.") - - supported_languages = """ - Arabic - Bengali - Bulgarian - Simplified Chinese - Traditional Chinese - Croatian - Czech - Danish - Dutch - English - Estonian - Finnish - French - German - Greek - Hebrew - Hindi - Hungarian - Indonesian - Italian - Japanese - Korean - Latvian - Lithuanian - Norwegian - Polish - Portuguese - Romanian - Russian - Serbian - Slovak - Slovenian - Spanish - Swahili - Swedish - Thai - Turkish - Ukrainian - Vietnamese - """ - - for lang in supported_languages.split("\n"): + # 外部ファイルから読み込んだサポート言語を使用 + for lang in gemini_client.supported_languages.split("\n"): if lang == "": continue print (f"Translating to {lang}:") diff --git a/src-python/models/translation/translation_languages.py b/src-python/models/translation/translation_languages.py index b08292aa..8643c224 100644 --- a/src-python/models/translation/translation_languages.py +++ b/src-python/models/translation/translation_languages.py @@ -615,8 +615,8 @@ dict_plamo_languages = { "Hungarian": "Hungarian", "Greek": "Greek", "Hebrew": "Hebrew", - "Simplified Chinese":"Simplified Chinese", - "Traditional Chinese":"Traditional Chinese" + "Chinese Simplified":"Simplified Chinese", + "Chinese Traditional":"Traditional Chinese" } translation_lang["Plamo_API"] = {"source":dict_plamo_languages, "target":dict_plamo_languages} @@ -625,8 +625,8 @@ dict_gemini_languages = { "Arabic": "Arabic", "Bengali": "Bengali", "Bulgarian": "Bulgarian", - "Simplified Chinese": "Simplified Chinese", - "Traditional Chinese": "Traditional Chinese", + "Chinese Simplified": "Simplified Chinese", + "Chinese Traditional": "Traditional Chinese", "Croatian": "Croatian", "Czech": "Czech", "Danish": "Danish", diff --git a/src-python/models/translation/translation_plamo.py b/src-python/models/translation/translation_plamo.py index 31046f41..68192b6f 100644 --- a/src-python/models/translation/translation_plamo.py +++ b/src-python/models/translation/translation_plamo.py @@ -1,53 +1,23 @@ from langchain_openai import ChatOpenAI from pydantic import SecretStr +import yaml +from os import path as os_path _MODELS = [ "plamo-2.0-prime" ] class PlamoClient: - def __init__(self, api_key: str = "", model: str = "plamo-2.0-prime"): + def __init__(self, api_key: str = "", model: str = "plamo-2.0-prime", root_path: str = None): self.api_key = api_key self.base_url = "https://api.platform.preferredai.jp/v1" self.model = model - self.supported_languages = """ - English - Japanese - Korean - French - German - Spanish - Portuguese - Russian - Italian - Dutch - Polish - Turkish - Arabic - Hindi - Thai - Vietnamese - Indonesian - Malay - Filipino - Swedish - Finnish - Danish - Norwegian - Romanian - Czech - Hungarian - Greek - Hebrew - Simplified Chinese - Traditional Chinese - """ - self.prompt_template = f""" - You are a translation assistant that uses the `plamo-translate` tool. - Translate the following text.Supported languages include:{self.supported_languages} - Translate the following text from {{input_lang}} to {{output_lang}}. - output only the translated text without any additional commentary. - """ + + # プロンプト設定をYAMLファイルから読み込む + prompt_config = self._load_prompt_config(root_path) + self.supported_languages = prompt_config["supported_languages"] + self.prompt_template = prompt_config["system_prompt"] + self.plamo_llm = ChatOpenAI( base_url=self.base_url, model=self.model, @@ -55,6 +25,25 @@ class PlamoClient: api_key=SecretStr(self.api_key), ) + def _load_prompt_config(self, 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) + def getListModels(self) -> list[str]: return _MODELS @@ -78,13 +67,11 @@ class PlamoClient: ) return True except Exception as e: - print(f"Error setting AuthKey: {e}") return False def setModel(self, model: str) -> bool: """モデルを設定し、成功したかどうかを返す""" if model not in _MODELS: - print(f"Model {model} is not in the supported model list.") return False try: @@ -105,7 +92,9 @@ class PlamoClient: { "role": "system", "content": self.prompt_template.format( - input_lang=input_lang, output_lang=output_lang + supported_languages=self.supported_languages, + input_lang=input_lang, + output_lang=output_lang ), }, {"role": "user", "content": text}, @@ -130,7 +119,8 @@ class PlamoClient: self.setModel(self.model) self.translate("Hello World", input_lang="English", output_lang="Japanese") return True - except Exception: + except Exception as e: + print(f"Error checking AuthKey: {e}") return False if __name__ == "__main__": diff --git a/src-python/models/translation/translation_translator.py b/src-python/models/translation/translation_translator.py index bc4d26f1..aa0195f6 100644 --- a/src-python/models/translation/translation_translator.py +++ b/src-python/models/translation/translation_translator.py @@ -66,14 +66,14 @@ class Translator: result = False return result - def authenticationPlamoAuthKey(self, auth_key: str, model: str) -> bool: + def authenticationPlamoAuthKey(self, auth_key: str, model: str, root_path: str = None) -> bool: """Authenticate Plamo API with the provided key. Returns True on success, False on failure. """ result = True try: - self.plamo_client = PlamoClient(auth_key, model=model) + self.plamo_client = PlamoClient(auth_key, model=model, root_path=root_path) self.plamo_client.checkAuthKey() except Exception: errorLogging() @@ -81,14 +81,14 @@ class Translator: result = False return result - def authenticationGeminiAuthKey(self, auth_key: str, model: str) -> bool: + def authenticationGeminiAuthKey(self, auth_key: str, model: str, root_path: str = None) -> bool: """Authenticate Gemini API with the provided key. Returns True on success, False on failure. """ result = True try: - self.gemini_client = GeminiClient(auth_key, model=model) + self.gemini_client = GeminiClient(auth_key, model=model, root_path=root_path) self.gemini_client.checkAuthKey() except Exception: errorLogging() From 526fd4d5aa7bbd89d5f7f7536df6bbf004284e9a Mon Sep 17 00:00:00 2001 From: misyaguziya <53165965+misyaguziya@users.noreply.github.com> Date: Thu, 16 Oct 2025 18:09:08 +0900 Subject: [PATCH 13/25] =?UTF-8?q?=E7=BF=BB=E8=A8=B3=E3=83=90=E3=83=83?= =?UTF-8?q?=E3=82=AF=E3=82=A8=E3=83=B3=E3=83=89=E3=82=92=E6=8B=A1=E5=BC=B5?= =?UTF-8?q?=E3=83=BB=E3=83=AA=E3=83=95=E3=82=A1=E3=82=AF=E3=82=BF=E3=83=AA?= =?UTF-8?q?=E3=83=B3=E3=82=B0=EF=BC=9AOpenAI/Plamo/Gemini=20=E3=82=AF?= =?UTF-8?q?=E3=83=A9=E3=82=A4=E3=82=A2=E3=83=B3=E3=83=88=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0=E3=83=BB=E6=94=B9=E4=BF=AE=E3=81=97=E3=80=81=E3=83=97?= =?UTF-8?q?=E3=83=AD=E3=83=B3=E3=83=97=E3=83=88=E3=82=92=20YAML=20?= =?UTF-8?q?=E3=81=8B=E3=82=89=E8=AA=AD=E3=81=BF=E8=BE=BC=E3=82=80=E3=82=88?= =?UTF-8?q?=E3=81=86=E3=81=AB=E5=A4=89=E6=9B=B4=E3=80=82=E5=90=84=E3=82=AF?= =?UTF-8?q?=E3=83=A9=E3=82=A4=E3=82=A2=E3=83=B3=E3=83=88=E3=81=A7=E3=83=A2?= =?UTF-8?q?=E3=83=87=E3=83=AB=E4=B8=80=E8=A6=A7=E5=8F=96=E5=BE=97=E3=83=BB?= =?UTF-8?q?=E8=AA=8D=E8=A8=BC=E3=83=BB=E3=82=AF=E3=83=A9=E3=82=A4=E3=82=A2?= =?UTF-8?q?=E3=83=B3=E3=83=88=E6=9B=B4=E6=96=B0=E6=A9=9F=E8=83=BD=E3=82=92?= =?UTF-8?q?=E5=AE=9F=E8=A3=85=E3=81=97=E3=80=81Translator/Model=20?= =?UTF-8?q?=E5=B1=A4=E3=81=AE=E5=AF=BE=E5=BF=9C=E3=83=A1=E3=82=BD=E3=83=83?= =?UTF-8?q?=E3=83=89=E3=82=92=E8=BF=BD=E5=8A=A0=E3=80=82Controller=20?= =?UTF-8?q?=E3=81=A8=20mainloop=20=E3=81=AB=E3=83=97=E3=83=A9=E3=83=A2?= =?UTF-8?q?=E3=83=BB=E3=82=B8=E3=82=A7=E3=83=9F=E3=83=8B=E3=83=BBOpenAI=20?= =?UTF-8?q?=E3=81=AE=E8=AA=8D=E8=A8=BC/=E3=83=A2=E3=83=87=E3=83=AB?= =?UTF-8?q?=E6=93=8D=E4=BD=9C=E3=82=A8=E3=83=B3=E3=83=89=E3=83=9D=E3=82=A4?= =?UTF-8?q?=E3=83=B3=E3=83=88=E3=82=92=E8=BF=BD=E5=8A=A0=E3=83=BB=E6=95=B4?= =?UTF-8?q?=E5=82=99=E3=80=82config=20=E3=81=AE=E3=83=A2=E3=83=87=E3=83=AB?= =?UTF-8?q?/API=E8=A8=AD=E5=AE=9A=E3=82=92=E3=83=97=E3=83=AD=E3=83=91?= =?UTF-8?q?=E3=83=86=E3=82=A3=E5=8C=96=E3=81=97=E3=81=A6=E6=97=A2=E5=AE=9A?= =?UTF-8?q?=E5=80=A4=E3=82=92=20None=20=E3=81=AB=E5=A4=89=E6=9B=B4?= =?UTF-8?q?=E3=81=97=E3=80=81=E9=81=B8=E6=8A=9E=E8=82=A2=E3=83=AA=E3=82=B9?= =?UTF-8?q?=E3=83=88=E3=82=92=E5=88=9D=E6=9C=9F=E5=8C=96=E3=80=82translati?= =?UTF-8?q?on=5Flanguages=20=E3=81=AB=20OpenAI=20=E7=94=A8=E3=83=9E?= =?UTF-8?q?=E3=83=83=E3=83=94=E3=83=B3=E3=82=B0=E3=82=92=E8=BF=BD=E5=8A=A0?= =?UTF-8?q?=E3=80=82requirements=20=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB?= =?UTF-8?q?=E3=81=AE=E4=BE=9D=E5=AD=98=E8=A8=98=E8=BF=B0=E3=82=92=E8=AA=BF?= =?UTF-8?q?=E6=95=B4=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.txt | 2 + requirements_cuda.txt | 2 + src-python/config.py | 68 +++-- src-python/controller.py | 214 ++++++++++++---- src-python/mainloop.py | 9 + src-python/model.py | 51 +++- .../translation/prompt/translation_gemini.yml | 9 +- .../translation/prompt/translation_openai.yml | 49 ++++ .../models/translation/translation_gemini.py | 232 +++++++++--------- .../translation/translation_languages.py | 48 +++- .../models/translation/translation_openai.py | 148 +++++++++++ .../models/translation/translation_plamo.py | 225 ++++++++--------- .../translation/translation_translator.py | 122 +++++++-- 13 files changed, 842 insertions(+), 337 deletions(-) create mode 100644 src-python/models/translation/prompt/translation_openai.yml create mode 100644 src-python/models/translation/translation_openai.py diff --git a/requirements.txt b/requirements.txt index c5e81f2f..6cf0b473 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,6 +20,8 @@ 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 SudachiPy==0.6.10 SudachiDict-core==20250825 SudachiDict-full==20250825 diff --git a/requirements_cuda.txt b/requirements_cuda.txt index 2e41d2a4..b411a604 100644 --- a/requirements_cuda.txt +++ b/requirements_cuda.txt @@ -21,6 +21,8 @@ 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 SudachiPy==0.6.10 SudachiDict-core==20250825 SudachiDict-full==20250825 diff --git a/src-python/config.py b/src-python/config.py index 49df48d0..96376397 100644 --- a/src-python/config.py +++ b/src-python/config.py @@ -35,16 +35,6 @@ try: except Exception: # pragma: no cover - optional runtime whisper_models = {} # type: ignore -try: - from models.translation.translation_gemini import _MODELS as gemini_models -except Exception: # pragma: no cover - optional runtime - gemini_models = [] # type: ignore - -try: - from models.translation.translation_plamo import _MODELS as plamo_models -except Exception: # pragma: no cover - optional runtime - plamo_models = [] # type: ignore - from utils import errorLogging, validateDictStructure, getComputeDeviceList json_serializable_vars = {} @@ -173,14 +163,6 @@ class Config: def SELECTABLE_TRANSLATION_ENGINE_LIST(self): return self._SELECTABLE_TRANSLATION_ENGINE_LIST - @property - def SELECTABLE_PLAMO_MODEL_LIST(self): - return self._SELECTABLE_PLAMO_MODEL_LIST - - @property - def SELECTABLE_GEMINI_MODEL_LIST(self): - return self._SELECTABLE_GEMINI_MODEL_LIST - @property def SELECTABLE_TRANSCRIPTION_ENGINE_LIST(self): return self._SELECTABLE_TRANSCRIPTION_ENGINE_LIST @@ -329,6 +311,33 @@ 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 + # Save Json Data ## Main Window @property @@ -936,6 +945,18 @@ class Config: self._GEMINI_MODEL = value self.saveConfig(inspect.currentframe().f_code.co_name, value) + @property + @json_serializable('OPENAI_MODEL') + def OPENAI_MODEL(self): + return self._OPENAI_MODEL + + @OPENAI_MODEL.setter + def OPENAI_MODEL(self, value): + if isinstance(value, str): + if value in self.SELECTABLE_OPENAI_MODEL_LIST: + self._OPENAI_MODEL = value + self.saveConfig(inspect.currentframe().f_code.co_name, value) + @property @json_serializable('AUTO_CLEAR_MESSAGE_BOX') def AUTO_CLEAR_MESSAGE_BOX(self): @@ -1160,8 +1181,9 @@ class Config: self._SELECTABLE_TRANSCRIPTION_ENGINE_LIST = list(transcription_lang[first_key].values())[0].keys() except Exception: self._SELECTABLE_TRANSCRIPTION_ENGINE_LIST = [] - self._SELECTABLE_PLAMO_MODEL_LIST = plamo_models - self._SELECTABLE_GEMINI_MODEL_LIST = gemini_models + self._SELECTABLE_PLAMO_MODEL_LIST = [] + self._SELECTABLE_GEMINI_MODEL_LIST = [] + self._SELECTABLE_OPENAI_MODEL_LIST = [] self._SELECTABLE_UI_LANGUAGE_LIST = ["en", "ja", "ko", "zh-Hant", "zh-Hans"] self._COMPUTE_MODE = "cuda" if torch.cuda.is_available() else "cpu" self._SELECTABLE_COMPUTE_DEVICE_LIST = getComputeDeviceList() @@ -1313,13 +1335,15 @@ class Config: "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 = "m2m100_418M-ct2-int8" - self._PLAMO_MODEL = "plamo-2.0-prime" - self._GEMINI_MODEL = "gemini-2.5-flash-lite" + self._PLAMO_MODEL = None + self._GEMINI_MODEL = None + self._OPENAI_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 2c33751d..a6c099a9 100644 --- a/src-python/controller.py +++ b/src-python/controller.py @@ -1605,36 +1605,6 @@ class Controller: 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 setPlamoModel(self, data, *args, **kwargs) -> dict: - printLog("Set Plamo Model", data) - try: - data = str(data) - result = model.authenticationTranslatorPlamoAuthKey(auth_key=config.AUTH_KEYS["Plamo_API"], model_name=data) - if result is True: - config.PLAMO_MODEL = data - response = {"status":200, "result":config.PLAMO_MODEL} - else: - response = { - "status":400, - "result":{ - "message":"Plamo model is not valid", - "data": config.PLAMO_MODEL - } - } - except Exception as e: - errorLogging() - response = { - "status":400, - "result":{ - "message":f"Error {e}", - "data": config.PLAMO_MODEL - } - } - return response - def getPlamoAuthKey(self, *args, **kwargs) -> dict: return {"status":200, "result":config.AUTH_KEYS["Plamo_API"]} @@ -1644,13 +1614,19 @@ class Controller: try: data = str(data) if len(data) == 72: - result = model.authenticationTranslatorPlamoAuthKey(auth_key=data, model_name=config.PLAMO_MODEL) + 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() + # ここにrunが必要 + if config.PLAMO_MODEL not in config.SELECTABLE_PLAMO_MODEL_LIST: + config.PLAMO_MODEL = config.SELECTABLE_PLAMO_MODEL_LIST[0] + # ここにrunが必要 + model.updateTranslatorPlamoClient() self.updateTranslationEngineAndEngineList() response = {"status":200, "result":config.AUTH_KEYS[translator_name]} else: @@ -1689,23 +1665,27 @@ class Controller: 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 getPlamoModelList(self, *args, **kwargs) -> dict: + return {"status":200, "result": config.SELECTABLE_PLAMO_MODEL_LIST} - def setGeminiModel(self, data, *args, **kwargs) -> dict: - printLog("Set Gemini Model", data) + def getPlamoModel(self, *args, **kwargs) -> dict: + return {"status":200, "result":config.PLAMO_MODEL} + + def setPlamoModel(self, data, *args, **kwargs) -> dict: + printLog("Set Plamo Model", data) try: data = str(data) - result = model.authenticationTranslatorGeminiAuthKey(auth_key=config.AUTH_KEYS["Gemini_API"], model_name=data) + result = model.setTranslatorPlamoModel(model=data) if result is True: - config.GEMINI_MODEL = data - response = {"status":200, "result":config.GEMINI_MODEL} + config.PLAMO_MODEL = data + model.updateTranslatorPlamoClient() + response = {"status":200, "result":config.PLAMO_MODEL} else: response = { "status":400, "result":{ - "message":"Gemini model is not valid", - "data": config.GEMINI_MODEL + "message":"Plamo model is not valid", + "data": config.PLAMO_MODEL } } except Exception as e: @@ -1714,7 +1694,7 @@ class Controller: "status":400, "result":{ "message":f"Error {e}", - "data": config.GEMINI_MODEL + "data": config.PLAMO_MODEL } } return response @@ -1728,13 +1708,19 @@ class Controller: try: data = str(data) if len(data) >= 20: - result = model.authenticationTranslatorGeminiAuthKey(auth_key=data, model_name=config.GEMINI_MODEL) + 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() + # ここにrunが必要 + if config.GEMINI_MODEL not in config.SELECTABLE_GEMINI_MODEL_LIST: + config.GEMINI_MODEL = config.SELECTABLE_GEMINI_MODEL_LIST[0] + # ここにrunが必要 + model.updateTranslatorGeminiClient() self.updateTranslationEngineAndEngineList() response = {"status":200, "result":config.AUTH_KEYS[translator_name]} else: @@ -1773,6 +1759,118 @@ class Controller: 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.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.GEMINI_MODEL = data + model.updateTranslatorGeminiClient() + response = {"status":200, "result":config.GEMINI_MODEL} + else: + response = { + "status":400, + "result":{ + "message":"Gemini model is not valid", + "data": config.GEMINI_MODEL + } + } + except Exception as e: + errorLogging() + response = { + "status":400, + "result":{ + "message":f"Error {e}", + "data": config.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 len(data) >= 20: + 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 + self.updateTranslationEngineAndEngineList() + response = {"status":200, "result":config.AUTH_KEYS[translator_name]} + else: + response = { + "status":400, + "result":{ + "message":"OpenAI 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 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.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.OPENAI_MODEL = data + response = {"status":200, "result":config.OPENAI_MODEL} + else: + response = { + "status":400, + "result":{ + "message":"OpenAI model is not valid", + "data": config.OPENAI_MODEL + } + } + except Exception as e: + errorLogging() + response = { + "status":400, + "result":{ + "message":f"Error {e}", + "data": config.OPENAI_MODEL + } + } + return response + @staticmethod def getCtranslate2WeightType(*args, **kwargs) -> dict: return {"status":200, "result":config.CTRANSLATE2_WEIGHT_TYPE} @@ -2651,9 +2749,14 @@ class Controller: 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], model=config.PLAMO_MODEL) is True: + 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.PLAMO_MODEL not in config.SELECTABLE_PLAMO_MODEL_LIST: + config.PLAMO_MODEL = config.SELECTABLE_PLAMO_MODEL_LIST[0] + model.setTranslatorPlamoModel(config.PLAMO_MODEL) + model.updateTranslatorPlamoClient() else: # error update Auth key auth_keys = config.AUTH_KEYS @@ -2664,15 +2767,38 @@ class Controller: 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], model=config.GEMINI_MODEL) is True: + 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.GEMINI_MODEL not in config.SELECTABLE_GEMINI_MODEL_LIST: + config.GEMINI_MODEL = config.SELECTABLE_GEMINI_MODEL_LIST[0] + model.setTranslatorGeminiModel(config.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.OPENAI_MODEL not in config.SELECTABLE_OPENAI_MODEL_LIST: + config.OPENAI_MODEL = config.SELECTABLE_OPENAI_MODEL_LIST[0] + model.setTranslatorOpenAiModel(config.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 _: if connected_network is True: config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = True diff --git a/src-python/mainloop.py b/src-python/mainloop.py index 51215eb9..d8550687 100644 --- a/src-python/mainloop.py +++ b/src-python/mainloop.py @@ -175,17 +175,26 @@ mapping = { "/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}, + "/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 2cbc611c..926ccbb2 100644 --- a/src-python/model.py +++ b/src-python/model.py @@ -198,14 +198,57 @@ class Model: result = self.translator.authenticationDeepLAuthKey(auth_key) return result - def authenticationTranslatorPlamoAuthKey(self, auth_key: str, model: str) -> bool: - result = self.translator.authenticationPlamoAuthKey(auth_key, model=model, root_path=config.PATH_LOCAL) + def authenticationTranslatorPlamoAuthKey(self, auth_key: str) -> bool: + result = self.translator.authenticationPlamoAuthKey(auth_key, root_path=config.PATH_LOCAL) return result - def authenticationTranslatorGeminiAuthKey(self, auth_key: str, model: str) -> bool: - result = self.translator.authenticationGeminiAuthKey(auth_key, model=model, root_path=config.PATH_LOCAL) + 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 startLogger(self): self.ensure_initialized() os_makedirs(config.PATH_LOGS, exist_ok=True) diff --git a/src-python/models/translation/prompt/translation_gemini.yml b/src-python/models/translation/prompt/translation_gemini.yml index 478b2c5e..8c7b56fc 100644 --- a/src-python/models/translation/prompt/translation_gemini.yml +++ b/src-python/models/translation/prompt/translation_gemini.yml @@ -1,7 +1,10 @@ system_prompt: | - Please translate the following text from {input_lang} to {output_lang}. - Only provide the translated text as the output. - {text} + 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. supported_languages: | Arabic 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..802d0b1d --- /dev/null +++ b/src-python/models/translation/prompt/translation_openai.yml @@ -0,0 +1,49 @@ +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. + +supported_languages: | + Arabic + Bengali + Bulgarian + Catalan + Chinese Simplified + Chinese Traditional + Croatian + Czech + Danish + Dutch + English + Estonian + Finnish + French + German + Greek + Hebrew + Hindi + Hungarian + Indonesian + Italian + Japanese + Korean + Latvian + Lithuanian + Norwegian + Polish + Portuguese + Romanian + Russian + Serbian + Slovak + Slovenian + Spanish + Swahili + Swedish + Thai + Turkish + Ukrainian + Vietnamese diff --git a/src-python/models/translation/translation_gemini.py b/src-python/models/translation/translation_gemini.py index 9311c2d3..ee019c06 100644 --- a/src-python/models/translation/translation_gemini.py +++ b/src-python/models/translation/translation_gemini.py @@ -1,148 +1,138 @@ import logging +from google import genai from langchain_google_genai import ChatGoogleGenerativeAI -from langchain_core.messages import HumanMessage import yaml from os import path as os_path logger = logging.getLogger("langchain_google_genai") logger.setLevel(logging.ERROR) -_MODELS = [ - "gemini-2.5-pro", - "gemini-2.5-flash", - "gemini-2.5-flash-lite", # default - "gemini-2.0-flash", - "gemini-2.0-flash-lite", - "gemini-1.5-pro", - "gemini-1.5-flash-8b" - "gemini-1.5-flash", +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 + +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, api_key: str = "", model: str = "gemini-2.5-flash-lite", root_path: str = None): - self.api_key = api_key - self.model = model + def __init__(self, root_path: str = None): + self.api_key = None + self.model = None # プロンプト設定をYAMLファイルから読み込む - prompt_config = self._load_prompt_config(root_path) + prompt_config = _load_prompt_config(root_path) self.supported_languages = prompt_config["supported_languages"] 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 _load_prompt_config(self, 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) - - def getListModels(self) -> list[str]: - return _MODELS - - def getAuthKey(self) -> str: - """現在のAuthKeyを取得する""" - return self.api_key - - def getModel(self) -> str: - """現在のモデルを取得する""" - return self.model - - def setAuthKey(self, api_key: str) -> bool: - """AuthKeyを設定し、成功したかどうかを返す""" - try: - self.api_key = api_key - self.gemini_llm = ChatGoogleGenerativeAI( - model=self.model, - api_key=self.api_key, - ) - return True - except Exception as e: - print(f"Error setting AuthKey: {e}") - return False - - def setModel(self, model: str) -> bool: - """モデルを設定し、成功したかどうかを返す""" - try: - if model in _MODELS: - self.model = model - self.gemini_llm = ChatGoogleGenerativeAI( - model=self.model, - api_key=self.api_key, - ) - return True - else: - print(f"Model {model} is not supported.") - return False - except Exception as e: - print(f"Error setting model: {e}") - return False - def translate(self, text: str, input_lang: str, output_lang: str) -> str: - messages = self.prompt_template.format( - input_lang=input_lang, - output_lang=output_lang, - text=text - ) + system_prompt = self.prompt_template.format( + supported_languages=self.supported_languages, + input_lang=input_lang, + output_lang=output_lang + ) - output = self.gemini_llm.invoke([HumanMessage(content=messages)]) - return output.content + messages = [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": text} + ] - def checkAuthKey(self) -> bool: - try: - self.setModel(self.model) - self.translate("Hello World", input_lang="English", output_lang="Japanese") - return True - except Exception: - return False + 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" - text = """ - 毎朝コーヒーを入れるのがささやかな楽しみになってる - """ - input_lang = "Japanese" - output_lang = "English" - - gemini_client = GeminiClient(api_key=AUTH_KEY, model="gemini-2.5-flash-lite") - - print("model list:", gemini_client.getListModels()) - print("AuthKey:", gemini_client.getAuthKey()) - # print("Model:", gemini_client.getModel()) - # print(f"set model: {gemini_client.setModel('gemini-2.5-flash')}") - # print(f"set AuthKey: {gemini_client.setAuthKey(AUTH_KEY)}") - # print(f"check AuthKey: {gemini_client.checkAuthKey()}") - - # try: - # translated_text = gemini_client.translate(text, input_lang, output_lang) - # print(translated_text) - # except Exception: - # print("Invalid API key. Please check your credentials.") - - # 外部ファイルから読み込んだサポート言語を使用 - for lang in gemini_client.supported_languages.split("\n"): - if lang == "": - continue - print (f"Translating to {lang}:") - try: - translated_text = gemini_client.translate(text, input_lang, lang) - print(f"Translated text: {translated_text}") - except Exception as e: - print(f"Error translating to {lang} api limit") - print(f"Error reason: {e}") - break \ No newline at end of file + 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 8643c224..ab087e0c 100644 --- a/src-python/models/translation/translation_languages.py +++ b/src-python/models/translation/translation_languages.py @@ -663,4 +663,50 @@ dict_gemini_languages = { "Vietnamese": "Vietnamese", } -translation_lang["Gemini_API"] = {"source":dict_gemini_languages, "target":dict_gemini_languages} \ No newline at end of file +translation_lang["Gemini_API"] = {"source":dict_gemini_languages, "target":dict_gemini_languages} + +# OpenAI API (Chat Completions) - Gemini とほぼ同等の自然言語名を使用 +dict_openai_languages = { + "Arabic": "Arabic", + "Bengali": "Bengali", + "Bulgarian": "Bulgarian", + "Catalan": "Catalan", + "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", + "Swahili": "Swahili", + "Swedish": "Swedish", + "Thai": "Thai", + "Turkish": "Turkish", + "Ukrainian": "Ukrainian", + "Vietnamese": "Vietnamese", +} + +translation_lang["OpenAI_API"] = {"source": dict_openai_languages, "target": dict_openai_languages} \ 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..f461526c --- /dev/null +++ b/src-python/models/translation/translation_openai.py @@ -0,0 +1,148 @@ +from openai import OpenAI +from langchain_openai import ChatOpenAI +from pydantic import SecretStr +import yaml +from os import path as os_path + +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 + +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 を読み込む。 + """ + 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 = _load_prompt_config(root_path) + self.supported_languages = prompt_config["supported_languages"] + 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 index 68192b6f..81cc55bc 100644 --- a/src-python/models/translation/translation_plamo.py +++ b/src-python/models/translation/translation_plamo.py @@ -1,147 +1,126 @@ +from openai import OpenAI from langchain_openai import ChatOpenAI from pydantic import SecretStr import yaml from os import path as os_path -_MODELS = [ - "plamo-2.0-prime" - ] +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 + +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, api_key: str = "", model: str = "plamo-2.0-prime", root_path: str = None): - self.api_key = api_key - self.base_url = "https://api.platform.preferredai.jp/v1" - self.model = model + def __init__(self, root_path: str = None): + self.api_key = None + self.base_url = BASE_URL + self.model = None - # プロンプト設定をYAMLファイルから読み込む - prompt_config = self._load_prompt_config(root_path) + prompt_config = _load_prompt_config(root_path) self.supported_languages = prompt_config["supported_languages"] 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=True, + streaming=False, api_key=SecretStr(self.api_key), ) - def _load_prompt_config(self, 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) - - def getListModels(self) -> list[str]: - return _MODELS - - def getAuthKey(self) -> str: - """現在のAuthKeyを取得する""" - return self.api_key - - def getModel(self) -> str: - """現在のモデルを取得する""" - return self.model - - def setAuthKey(self, api_key: str) -> bool: - """AuthKeyを設定し、成功したかどうかを返す""" - try: - self.api_key = api_key - self.plamo_llm = ChatOpenAI( - base_url=self.base_url, - model=self.model, - streaming=True, - api_key=SecretStr(self.api_key), - ) - return True - except Exception as e: - return False - - def setModel(self, model: str) -> bool: - """モデルを設定し、成功したかどうかを返す""" - if model not in _MODELS: - return False - - try: - self.model = model - self.plamo_llm = ChatOpenAI( - base_url=self.base_url, - model=self.model, - streaming=True, - api_key=SecretStr(self.api_key), - ) - return True - except Exception as e: - print(f"Error setting model: {e}") - return 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": self.prompt_template.format( - supported_languages=self.supported_languages, - input_lang=input_lang, - output_lang=output_lang - ), - }, + {"role": "system", "content": system_prompt}, {"role": "user", "content": text}, ] - output = "" - for chunk in self.plamo_llm.stream(messages): - if isinstance(chunk.content, str): - output += chunk.content - elif isinstance(chunk.content, list): - for item in chunk.content: - if isinstance(item, str): - output += item - elif isinstance(item, dict): - if "content" in item and isinstance(item["content"], str): - output += item["content"] - - return output[:-1] - - def checkAuthKey(self) -> bool: - try: - self.setModel(self.model) - self.translate("Hello World", input_lang="English", output_lang="Japanese") - return True - except Exception as e: - print(f"Error checking AuthKey: {e}") - return False + 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 = "AUTH_KEY" - text = """ - 毎朝コーヒーを入れるのがささやかな楽しみになってる - """ - input_lang = "Japanese" - output_lang = "English" - - plamo_client = PlamoClient(api_key=AUTH_KEY) - - print("model list:", plamo_client.getListModels()) - print("AuthKey:", plamo_client.getAuthKey()) - print("Model:", plamo_client.getModel()) - print(f"set model: {plamo_client.setModel('plamo-2.0-prime')}") - print(f"set AuthKey: {plamo_client.setAuthKey(AUTH_KEY)}") - print(f"check AuthKey: {plamo_client.checkAuthKey()}") - - try: - translated_text = plamo_client.translate(text, input_lang, output_lang) - print(translated_text) - except Exception: - print("Invalid API key. Please check your credentials.") \ No newline at end of file + 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 aa0195f6..4ea4c0ba 100644 --- a/src-python/models/translation/translation_translator.py +++ b/src-python/models/translation/translation_translator.py @@ -12,6 +12,7 @@ try: from .translation_utils import ctranslate2_weights from .translation_plamo import PlamoClient from .translation_gemini import GeminiClient + from .translation_openai import OpenAIClient except Exception: import sys print(os_path.dirname(os_path.dirname(os_path.dirname(os_path.abspath(__file__))))) @@ -20,6 +21,7 @@ except Exception: from translation_utils import ctranslate2_weights from translation_plamo import PlamoClient from translation_gemini import GeminiClient + from translation_openai import OpenAIClient import ctranslate2 import transformers @@ -44,6 +46,7 @@ class Translator: 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.ctranslate2_translator: Any = None self.ctranslate2_tokenizer: Any = None self.is_loaded_ctranslate2_model: bool = False @@ -66,35 +69,107 @@ class Translator: result = False return result - def authenticationPlamoAuthKey(self, auth_key: str, model: str, root_path: str = None) -> bool: + 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. """ - result = True - try: - self.plamo_client = PlamoClient(auth_key, model=model, root_path=root_path) - self.plamo_client.checkAuthKey() - except Exception: - errorLogging() + self.plamo_client = PlamoClient(root_path=root_path) + if self.plamo_client.setAuthKey(auth_key): + return True + else: self.plamo_client = None - result = False - return result + return False - def authenticationGeminiAuthKey(self, auth_key: str, model: str, root_path: str = None) -> bool: + 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. """ - result = True - try: - self.gemini_client = GeminiClient(auth_key, model=model, root_path=root_path) - self.gemini_client.checkAuthKey() - except Exception: - errorLogging() - self.gemini_client = None - result = False - return result + 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 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. @@ -236,6 +311,15 @@ class Translator: 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 "Google": if self.is_enable_translators is True and other_web_Translator is not None: result = other_web_Translator( From a862805a0559a5919c2dfb5dd9a6e36f73ae4942 Mon Sep 17 00:00:00 2001 From: misyaguziya <53165965+misyaguziya@users.noreply.github.com> Date: Thu, 16 Oct 2025 18:53:45 +0900 Subject: [PATCH 14/25] =?UTF-8?q?OpenAI=E9=96=A2=E9=80=A3=E3=81=AE?= =?UTF-8?q?=E3=83=A1=E3=82=BD=E3=83=83=E3=83=89=E5=90=8D=E3=82=92=E7=B5=B1?= =?UTF-8?q?=E4=B8=80=E3=81=97=E3=80=81=E8=AA=8D=E8=A8=BC=E5=BE=8C=E3=81=AE?= =?UTF-8?q?=E3=83=A2=E3=83=87=E3=83=AB=E6=9B=B4=E6=96=B0=E5=87=A6=E7=90=86?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - メソッド名を OpenAi -> OpenAI に統一(get/set/del/get_model_list/get_model/set_model) - OpenAI 認証キー設定時にモデル一覧を取得して SELECTABLE_OPENAI_MODEL_LIST を更新、選択モデルが存在しなければ第一候補へフォールバックして OpenAI クライアントを再初期化する処理を追加 - model のメソッド名を setTranslatorOpenAiModel -> setTranslatorOpenAIModel に変更 - mainloop のエンドポイントマッピングを新しいメソッド名に合わせて更新 --- src-python/controller.py | 22 ++++++++++++++-------- src-python/mainloop.py | 12 ++++++------ src-python/model.py | 2 +- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src-python/controller.py b/src-python/controller.py index a6c099a9..e3b788a2 100644 --- a/src-python/controller.py +++ b/src-python/controller.py @@ -1794,10 +1794,10 @@ class Controller: return response @staticmethod - def getOpenAiAuthKey(*args, **kwargs) -> dict: + def getOpenAIAuthKey(*args, **kwargs) -> dict: return {"status":200, "result":config.AUTH_KEYS["OpenAI_API"]} - def setOpenAiAuthKey(self, data, *args, **kwargs) -> dict: + def setOpenAIAuthKey(self, data, *args, **kwargs) -> dict: printLog("Set OpenAI Auth Key", data) translator_name = "OpenAI_API" try: @@ -1808,6 +1808,12 @@ class Controller: 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() + # ここにrunが必要 + if config.OPENAI_MODEL not in config.SELECTABLE_OPENAI_MODEL_LIST: + config.OPENAI_MODEL = config.SELECTABLE_OPENAI_MODEL_LIST[0] + # ここにrunが必要 + model.updateTranslatorOpenAIClient() self.updateTranslationEngineAndEngineList() response = {"status":200, "result":config.AUTH_KEYS[translator_name]} else: @@ -1829,7 +1835,7 @@ class Controller: } return response - def delOpenAiAuthKey(self, *args, **kwargs) -> dict: + def delOpenAIAuthKey(self, *args, **kwargs) -> dict: translator_name = "OpenAI_API" auth_keys = config.AUTH_KEYS auth_keys[translator_name] = None @@ -1838,17 +1844,17 @@ class Controller: self.updateTranslationEngineAndEngineList() return {"status":200, "result":config.AUTH_KEYS[translator_name]} - def getOpenAiModelList(self, *args, **kwargs) -> dict: + def getOpenAIModelList(self, *args, **kwargs) -> dict: return {"status":200, "result": config.SELECTABLE_OPENAI_MODEL_LIST} - def getOpenAiModel(self, *args, **kwargs) -> dict: + def getOpenAIModel(self, *args, **kwargs) -> dict: return {"status":200, "result":config.OPENAI_MODEL} - def setOpenAiModel(self, data, *args, **kwargs) -> dict: + def setOpenAIModel(self, data, *args, **kwargs) -> dict: printLog("Set OpenAI Model", data) try: data = str(data) - result = model.setTranslatorOpenAiModel(model=data) + result = model.setTranslatorOpenAIModel(model=data) if result is True: config.OPENAI_MODEL = data response = {"status":200, "result":config.OPENAI_MODEL} @@ -2791,7 +2797,7 @@ class Controller: config.SELECTABLE_OPENAI_MODEL_LIST = model.getTranslatorOpenAIModelList() if config.OPENAI_MODEL not in config.SELECTABLE_OPENAI_MODEL_LIST: config.OPENAI_MODEL = config.SELECTABLE_OPENAI_MODEL_LIST[0] - model.setTranslatorOpenAiModel(config.OPENAI_MODEL) + model.setTranslatorOpenAIModel(config.OPENAI_MODEL) model.updateTranslatorOpenAIClient() else: # error update Auth key diff --git a/src-python/mainloop.py b/src-python/mainloop.py index d8550687..cfedaecd 100644 --- a/src-python/mainloop.py +++ b/src-python/mainloop.py @@ -188,12 +188,12 @@ mapping = { "/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}, + "/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}, "/get/data/convert_message_to_romaji": {"status": True, "variable":controller.getConvertMessageToRomaji}, "/set/enable/convert_message_to_romaji": {"status": True, "variable":controller.setEnableConvertMessageToRomaji}, diff --git a/src-python/model.py b/src-python/model.py index 926ccbb2..ba3fd1cf 100644 --- a/src-python/model.py +++ b/src-python/model.py @@ -240,7 +240,7 @@ class Model: self.ensure_initialized() return self.translator.getOpenAIModelList() - def setTranslatorOpenAiModel(self, model: str) -> bool: + def setTranslatorOpenAIModel(self, model: str) -> bool: self.ensure_initialized() result = self.translator.setOpenAIModel(model=model) return result From c18748f6bc7978e00f6dbb2f28e67f5a5cc0648d Mon Sep 17 00:00:00 2001 From: misyaguziya <53165965+misyaguziya@users.noreply.github.com> Date: Thu, 16 Oct 2025 21:55:27 +0900 Subject: [PATCH 15/25] =?UTF-8?q?=E7=BF=BB=E8=A8=B3=E3=83=A2=E3=83=87?= =?UTF-8?q?=E3=83=AB=E8=A8=AD=E5=AE=9A=E3=82=92=20SELECTED=5F*=20=E3=81=AB?= =?UTF-8?q?=E7=B5=B1=E4=B8=80=E3=81=97=E3=80=81Controller=20=E3=81=A8=20UI?= =?UTF-8?q?=20=E6=9B=B4=E6=96=B0=E3=81=AE=E9=80=A3=E6=90=BA=E3=82=92?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - config: PLAMO_MODEL/GEMINI_MODEL/OPENAI_MODEL を SELECTED_PLAMO_MODEL/SELECTED_GEMINI_MODEL/SELECTED_OPENAI_MODEL にリネーム - JSON シリアライズキーも SELECTED_* に変更 - 初期化フィールドを新キーに合わせて修正 - controller: すべての参照を新しい SELECTED_* プロパティへ置換 - モデル一覧取得時に run コールバックで selectable リストを通知 - 選択モデルを更新した際に run コールバックで選択状態を通知 - モデル更新・クライアント再構築処理の呼び出しを保持 - mainloop: GUI 更新用の run_mapping に selectable/selected のモデル関連エンドポイントを追加 --- src-python/config.py | 46 ++++++++++++------------- src-python/controller.py | 72 ++++++++++++++++++++-------------------- src-python/mainloop.py | 7 ++++ 3 files changed, 66 insertions(+), 59 deletions(-) diff --git a/src-python/config.py b/src-python/config.py index 96376397..ffbc8844 100644 --- a/src-python/config.py +++ b/src-python/config.py @@ -922,39 +922,39 @@ class Config: self.saveConfig(inspect.currentframe().f_code.co_name, value) @property - @json_serializable('PLAMO_MODEL') - def PLAMO_MODEL(self): - return self._PLAMO_MODEL + @json_serializable('SELECTED_PLAMO_MODEL') + def SELECTED_PLAMO_MODEL(self): + return self._SELECTED_PLAMO_MODEL - @PLAMO_MODEL.setter - def PLAMO_MODEL(self, value): + @SELECTED_PLAMO_MODEL.setter + def SELECTED_PLAMO_MODEL(self, value): if isinstance(value, str): if value in self.SELECTABLE_PLAMO_MODEL_LIST: - self._PLAMO_MODEL = value + self._SELECTED_PLAMO_MODEL = value self.saveConfig(inspect.currentframe().f_code.co_name, value) @property @json_serializable('GEMINI_MODEL') - def GEMINI_MODEL(self): - return self._GEMINI_MODEL + def SELECTED_GEMINI_MODEL(self): + return self._SELECTED_GEMINI_MODEL - @GEMINI_MODEL.setter - def GEMINI_MODEL(self, value): + @SELECTED_GEMINI_MODEL.setter + def SELECTED_GEMINI_MODEL(self, value): if isinstance(value, str): if value in self.SELECTABLE_GEMINI_MODEL_LIST: - self._GEMINI_MODEL = value + self._SELECTED_GEMINI_MODEL = value self.saveConfig(inspect.currentframe().f_code.co_name, value) @property - @json_serializable('OPENAI_MODEL') - def OPENAI_MODEL(self): - return self._OPENAI_MODEL + @json_serializable('SELECTED_OPENAI_MODEL') + def SELECTED_OPENAI_MODEL(self): + return self._SELECTED_OPENAI_MODEL - @OPENAI_MODEL.setter - def OPENAI_MODEL(self, value): + @SELECTED_OPENAI_MODEL.setter + def SELECTED_OPENAI_MODEL(self, value): if isinstance(value, str): if value in self.SELECTABLE_OPENAI_MODEL_LIST: - self._OPENAI_MODEL = value + self._SELECTED_OPENAI_MODEL = value self.saveConfig(inspect.currentframe().f_code.co_name, value) @property @@ -1181,9 +1181,6 @@ class Config: self._SELECTABLE_TRANSCRIPTION_ENGINE_LIST = list(transcription_lang[first_key].values())[0].keys() except Exception: self._SELECTABLE_TRANSCRIPTION_ENGINE_LIST = [] - self._SELECTABLE_PLAMO_MODEL_LIST = [] - self._SELECTABLE_GEMINI_MODEL_LIST = [] - self._SELECTABLE_OPENAI_MODEL_LIST = [] self._SELECTABLE_UI_LANGUAGE_LIST = ["en", "ja", "ko", "zh-Hant", "zh-Hans"] self._COMPUTE_MODE = "cuda" if torch.cuda.is_available() else "cpu" self._SELECTABLE_COMPUTE_DEVICE_LIST = getComputeDeviceList() @@ -1234,6 +1231,9 @@ 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 = [] # Save Json Data ## Main Window @@ -1341,9 +1341,9 @@ class Config: 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 = "m2m100_418M-ct2-int8" - self._PLAMO_MODEL = None - self._GEMINI_MODEL = None - self._OPENAI_MODEL = None + self._SELECTED_PLAMO_MODEL = None + self._SELECTED_GEMINI_MODEL = None + self._SELECTED_OPENAI_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 e3b788a2..f4c1e094 100644 --- a/src-python/controller.py +++ b/src-python/controller.py @@ -1622,10 +1622,10 @@ class Controller: config.AUTH_KEYS = auth_keys config.SELECTABLE_TRANSLATION_ENGINE_STATUS[translator_name] = True config.SELECTABLE_PLAMO_MODEL_LIST = model.getTranslatorPlamoModelList() - # ここにrunが必要 - if config.PLAMO_MODEL not in config.SELECTABLE_PLAMO_MODEL_LIST: - config.PLAMO_MODEL = config.SELECTABLE_PLAMO_MODEL_LIST[0] - # ここにrunが必要 + 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] + 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]} @@ -1669,7 +1669,7 @@ class Controller: return {"status":200, "result": config.SELECTABLE_PLAMO_MODEL_LIST} def getPlamoModel(self, *args, **kwargs) -> dict: - return {"status":200, "result":config.PLAMO_MODEL} + return {"status":200, "result":config.SELECTED_PLAMO_MODEL} def setPlamoModel(self, data, *args, **kwargs) -> dict: printLog("Set Plamo Model", data) @@ -1677,15 +1677,15 @@ class Controller: data = str(data) result = model.setTranslatorPlamoModel(model=data) if result is True: - config.PLAMO_MODEL = data + config.SELECTED_PLAMO_MODEL = data model.updateTranslatorPlamoClient() - response = {"status":200, "result":config.PLAMO_MODEL} + response = {"status":200, "result":config.SELECTED_PLAMO_MODEL} else: response = { "status":400, "result":{ "message":"Plamo model is not valid", - "data": config.PLAMO_MODEL + "data": config.SELECTED_PLAMO_MODEL } } except Exception as e: @@ -1694,7 +1694,7 @@ class Controller: "status":400, "result":{ "message":f"Error {e}", - "data": config.PLAMO_MODEL + "data": config.SELECTED_PLAMO_MODEL } } return response @@ -1716,10 +1716,10 @@ class Controller: config.AUTH_KEYS = auth_keys config.SELECTABLE_TRANSLATION_ENGINE_STATUS[translator_name] = True config.SELECTABLE_GEMINI_MODEL_LIST = model.getTranslatorGeminiModelList() - # ここにrunが必要 - if config.GEMINI_MODEL not in config.SELECTABLE_GEMINI_MODEL_LIST: - config.GEMINI_MODEL = config.SELECTABLE_GEMINI_MODEL_LIST[0] - # ここにrunが必要 + 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] + 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]} @@ -1763,7 +1763,7 @@ class Controller: return {"status":200, "result": config.SELECTABLE_GEMINI_MODEL_LIST} def getGeminiModel(self, *args, **kwargs) -> dict: - return {"status":200, "result":config.GEMINI_MODEL} + return {"status":200, "result":config.SELECTED_GEMINI_MODEL} def setGeminiModel(self, data, *args, **kwargs) -> dict: printLog("Set Gemini Model", data) @@ -1771,15 +1771,15 @@ class Controller: data = str(data) result = model.setTranslatorGeminiModel(model=data) if result is True: - config.GEMINI_MODEL = data + config.SELECTED_GEMINI_MODEL = data model.updateTranslatorGeminiClient() - response = {"status":200, "result":config.GEMINI_MODEL} + response = {"status":200, "result":config.SELECTED_GEMINI_MODEL} else: response = { "status":400, "result":{ "message":"Gemini model is not valid", - "data": config.GEMINI_MODEL + "data": config.SELECTED_GEMINI_MODEL } } except Exception as e: @@ -1788,7 +1788,7 @@ class Controller: "status":400, "result":{ "message":f"Error {e}", - "data": config.GEMINI_MODEL + "data": config.SELECTED_GEMINI_MODEL } } return response @@ -1809,10 +1809,10 @@ class Controller: config.AUTH_KEYS = auth_keys config.SELECTABLE_TRANSLATION_ENGINE_STATUS[translator_name] = True config.SELECTABLE_OPENAI_MODEL_LIST = model.getTranslatorOpenAIModelList() - # ここにrunが必要 - if config.OPENAI_MODEL not in config.SELECTABLE_OPENAI_MODEL_LIST: - config.OPENAI_MODEL = config.SELECTABLE_OPENAI_MODEL_LIST[0] - # ここにrunが必要 + 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] + 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]} @@ -1848,7 +1848,7 @@ class Controller: return {"status":200, "result": config.SELECTABLE_OPENAI_MODEL_LIST} def getOpenAIModel(self, *args, **kwargs) -> dict: - return {"status":200, "result":config.OPENAI_MODEL} + return {"status":200, "result":config.SELECTED_OPENAI_MODEL} def setOpenAIModel(self, data, *args, **kwargs) -> dict: printLog("Set OpenAI Model", data) @@ -1856,14 +1856,14 @@ class Controller: data = str(data) result = model.setTranslatorOpenAIModel(model=data) if result is True: - config.OPENAI_MODEL = data - response = {"status":200, "result":config.OPENAI_MODEL} + config.SELECTED_OPENAI_MODEL = data + response = {"status":200, "result":config.SELECTED_OPENAI_MODEL} else: response = { "status":400, "result":{ "message":"OpenAI model is not valid", - "data": config.OPENAI_MODEL + "data": config.SELECTED_OPENAI_MODEL } } except Exception as e: @@ -1872,7 +1872,7 @@ class Controller: "status":400, "result":{ "message":f"Error {e}", - "data": config.OPENAI_MODEL + "data": config.SELECTED_OPENAI_MODEL } } return response @@ -2759,9 +2759,9 @@ class Controller: config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = True printLog("Plamo API Key is valid") config.SELECTABLE_PLAMO_MODEL_LIST = model.getTranslatorPlamoModelList() - if config.PLAMO_MODEL not in config.SELECTABLE_PLAMO_MODEL_LIST: - config.PLAMO_MODEL = config.SELECTABLE_PLAMO_MODEL_LIST[0] - model.setTranslatorPlamoModel(config.PLAMO_MODEL) + 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 @@ -2777,9 +2777,9 @@ class Controller: config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = True printLog("Gemini API Key is valid") config.SELECTABLE_GEMINI_MODEL_LIST = model.getTranslatorGeminiModelList() - if config.GEMINI_MODEL not in config.SELECTABLE_GEMINI_MODEL_LIST: - config.GEMINI_MODEL = config.SELECTABLE_GEMINI_MODEL_LIST[0] - model.setTranslatorGeminiModel(config.GEMINI_MODEL) + 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 @@ -2795,9 +2795,9 @@ class Controller: config.SELECTABLE_TRANSLATION_ENGINE_STATUS[engine] = True printLog("OpenAI API Key is valid") config.SELECTABLE_OPENAI_MODEL_LIST = model.getTranslatorOpenAIModelList() - if config.OPENAI_MODEL not in config.SELECTABLE_OPENAI_MODEL_LIST: - config.OPENAI_MODEL = config.SELECTABLE_OPENAI_MODEL_LIST[0] - model.setTranslatorOpenAIModel(config.OPENAI_MODEL) + 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 diff --git a/src-python/mainloop.py b/src-python/mainloop.py index cfedaecd..e5dbc735 100644 --- a/src-python/mainloop.py +++ b/src-python/mainloop.py @@ -51,6 +51,13 @@ 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", + "mic_host_list":"/run/mic_host_list", "mic_device_list":"/run/mic_device_list", "speaker_device_list":"/run/speaker_device_list", From 965bee818a89fb2c243fb6feea7f53b3e8d31c7f Mon Sep 17 00:00:00 2001 From: misyaguziya <53165965+misyaguziya@users.noreply.github.com> Date: Fri, 17 Oct 2025 15:58:50 +0900 Subject: [PATCH 16/25] =?UTF-8?q?LM=20Studio=20=E3=81=A8=20Ollama=20?= =?UTF-8?q?=E3=81=AE=E7=BF=BB=E8=A8=B3=E3=82=AF=E3=83=A9=E3=82=A4=E3=82=A2?= =?UTF-8?q?=E3=83=B3=E3=83=88=E3=81=A8=E3=83=97=E3=83=AD=E3=83=B3=E3=83=97?= =?UTF-8?q?=E3=83=88=E3=82=92=E8=BF=BD=E5=8A=A0=E3=80=81requirements=20?= =?UTF-8?q?=E3=81=AB=20langchain-ollama=20=E3=82=92=E8=BF=BD=E8=A8=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - src-python/models/translation に LM Studio 用 (translation_lmstudio.py / translation_lmstudio.yml) を追加 - Ollama 用クライアント (translation_ollama.py / translation_ollama.yml) を追加 - 各クライアントでプロンプト YAML から system_prompt / supported_languages を読み込み、認証チェック・モデル一覧取得・モデル設定・クライアント更新・translate 呼び出しを実装 - requirements.txt と requirements_cuda.txt に langchain-ollama==0.3.10 を追記 --- requirements.txt | 1 + requirements_cuda.txt | 1 + .../prompt/translation_lmstudio.yml | 48 +++++++ .../translation/prompt/translation_ollama.yml | 48 +++++++ .../translation/translation_lmstudio.py | 124 ++++++++++++++++++ .../models/translation/translation_ollama.py | 115 ++++++++++++++++ 6 files changed, 337 insertions(+) create mode 100644 src-python/models/translation/prompt/translation_lmstudio.yml create mode 100644 src-python/models/translation/prompt/translation_ollama.yml create mode 100644 src-python/models/translation/translation_lmstudio.py create mode 100644 src-python/models/translation/translation_ollama.py diff --git a/requirements.txt b/requirements.txt index 6cf0b473..3d0a6670 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,6 +22,7 @@ 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 b411a604..60f0e9e1 100644 --- a/requirements_cuda.txt +++ b/requirements_cuda.txt @@ -23,6 +23,7 @@ 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/models/translation/prompt/translation_lmstudio.yml b/src-python/models/translation/prompt/translation_lmstudio.yml new file mode 100644 index 00000000..8c7b56fc --- /dev/null +++ b/src-python/models/translation/prompt/translation_lmstudio.yml @@ -0,0 +1,48 @@ +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. + +supported_languages: | + Arabic + Bengali + Bulgarian + Simplified Chinese + Traditional Chinese + Croatian + Czech + Danish + Dutch + English + Estonian + Finnish + French + German + Greek + Hebrew + Hindi + Hungarian + Indonesian + Italian + Japanese + Korean + Latvian + Lithuanian + Norwegian + Polish + Portuguese + Romanian + Russian + Serbian + Slovak + Slovenian + Spanish + Swahili + Swedish + Thai + Turkish + Ukrainian + Vietnamese 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..8c7b56fc --- /dev/null +++ b/src-python/models/translation/prompt/translation_ollama.yml @@ -0,0 +1,48 @@ +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. + +supported_languages: | + Arabic + Bengali + Bulgarian + Simplified Chinese + Traditional Chinese + Croatian + Czech + Danish + Dutch + English + Estonian + Finnish + French + German + Greek + Hebrew + Hindi + Hungarian + Indonesian + Italian + Japanese + Korean + Latvian + Lithuanian + Norwegian + Polish + Portuguese + Romanian + Russian + Serbian + Slovak + Slovenian + Spanish + Swahili + Swedish + Thai + Turkish + Ukrainian + Vietnamese diff --git a/src-python/models/translation/translation_lmstudio.py b/src-python/models/translation/translation_lmstudio.py new file mode 100644 index 00000000..6de2f615 --- /dev/null +++ b/src-python/models/translation/translation_lmstudio.py @@ -0,0 +1,124 @@ +from openai import OpenAI +from langchain_openai import ChatOpenAI +from pydantic import SecretStr +import yaml +from os import path as os_path + +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. + """ + 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 + +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 を読み込む。 + """ + 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 = _load_prompt_config(root_path) + self.supported_languages = prompt_config["supported_languages"] + 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__": + 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 diff --git a/src-python/models/translation/translation_ollama.py b/src-python/models/translation/translation_ollama.py new file mode 100644 index 00000000..22ce3e4f --- /dev/null +++ b/src-python/models/translation/translation_ollama.py @@ -0,0 +1,115 @@ +import requests +from langchain_ollama import ChatOllama +import yaml +from os import path as os_path + + +def _authentication_check(base_url: str | None = None) -> bool: + """Check authentication for Ollama API. + """ + try: + response = requests.get(f"{base_url}/api/ping") + 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. + """ + response = requests.get(f"{base_url}/api/tags") + models = response.json()["models"] + + allowed_models = [] + for model in models: + allowed_models.append(model["name"]) + + 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 を読み込む。 + """ + def __init__(self, root_path: str = None): + self.model = None + self.base_url = "http://localhost:11434" + + prompt_config = _load_prompt_config(root_path) + self.supported_languages = prompt_config["supported_languages"] + self.prompt_template = prompt_config["system_prompt"] + + self.openai_llm = None + + def getModelList(self) -> list[str]: + if _authentication_check(self.base_url): + 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 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 17/25] =?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): From 6596743b6bfb8fa9f763eca08530105abadbc879 Mon Sep 17 00:00:00 2001 From: misyaguziya <53165965+misyaguziya@users.noreply.github.com> Date: Sat, 18 Oct 2025 23:16:22 +0900 Subject: [PATCH 18/25] =?UTF-8?q?=E7=BF=BB=E8=A8=B3=E8=A8=80=E8=AA=9E?= =?UTF-8?q?=E5=AE=9A=E7=BE=A9=E3=82=92=20YAML=20=E3=81=AB=E7=A7=BB?= =?UTF-8?q?=E8=A1=8C=E3=81=97=E3=83=AD=E3=83=BC=E3=83=89=E5=87=A6=E7=90=86?= =?UTF-8?q?=E3=82=92=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 言語マッピングを src-python/models/translation/languages/languages.yml として追加 - translation_languages.py を全面リファクタ:YAML から読み込み、構造検証、スレッドロック、キャッシュ対応を導入 - config.py に loadTranslationLanguages のインポート/フォールバックを追加し、Config 初期化時に言語定義を読み込むよう変更 - PyYAML を requirements.txt / requirements_cuda.txt に追記 - PyInstaller spec (backend.spec / backend_cuda.spec) に言語ファイル配布先を追加 - translation_ollama.py の loadPromptConfig 呼び出しを修正(ファイル名を明示) --- backend.spec | 1 + backend_cuda.spec | 1 + requirements.txt | 1 + requirements_cuda.txt | 1 + src-python/config.py | 5 +- .../translation/languages/languages.yml | 771 +++++++++++++++++ .../translation/translation_languages.py | 815 +++--------------- .../models/translation/translation_ollama.py | 2 +- 8 files changed, 903 insertions(+), 694 deletions(-) create mode 100644 src-python/models/translation/languages/languages.yml diff --git a/backend.spec b/backend.spec index b13ce288..605c82fb 100644 --- a/backend.spec +++ b/backend.spec @@ -8,6 +8,7 @@ a = Analysis( 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/'), diff --git a/backend_cuda.spec b/backend_cuda.spec index 3ed416ac..8d80b6ec 100644 --- a/backend_cuda.spec +++ b/backend_cuda.spec @@ -8,6 +8,7 @@ a = Analysis( 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/'), diff --git a/requirements.txt b/requirements.txt index 3d0a6670..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 diff --git a/requirements_cuda.txt b/requirements_cuda.txt index 60f0e9e1..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 diff --git a/src-python/config.py b/src-python/config.py index 9cd41589..165919b2 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 @@ -1227,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 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/translation_languages.py b/src-python/models/translation/translation_languages.py index 69a594b8..eccd5665 100644 --- a/src-python/models/translation/translation_languages.py +++ b/src-python/models/translation/translation_languages.py @@ -1,713 +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_m2m100_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"] = {} -translation_lang["CTranslate2"]["m2m100_418M-ct2-int8"] = {"source":dict_m2m100_languages, "target":dict_m2m100_languages} -translation_lang["CTranslate2"]["m2m100_1.2B-ct2-int8"] = {"source":dict_m2m100_languages, "target":dict_m2m100_languages} + 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})" + ) -dict_nllb_languages = { - "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", - "Modern 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" -} +def loadTranslationLanguages(path: str, force: bool = False) -> Dict[str, Any]: + """Load translation language mappings from YAML file. -translation_lang["CTranslate2"]["nllb-200-distilled-1.3B-ct2-int8"] = {"source":dict_nllb_languages, "target":dict_nllb_languages} -translation_lang["CTranslate2"]["nllb-200-3.3B-ct2-int8"] = {"source":dict_nllb_languages, "target":dict_nllb_languages} + Args: + path: Path to the YAML file + force: If True, reload even if already loaded -dict_plamo_languages = { - "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" -} + Returns: + Dictionary of translation language mappings -translation_lang["Plamo_API"] = {"source":dict_plamo_languages, "target":dict_plamo_languages} + 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 -dict_gemini_languages = { - "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", - "Swahili": "Swahili", - "Swedish": "Swedish", - "Thai": "Thai", - "Turkish": "Turkish", - "Ukrainian": "Ukrainian", - "Vietnamese": "Vietnamese", -} + with _lock: + if _loaded and not force: + return translation_lang -translation_lang["Gemini_API"] = {"source":dict_gemini_languages, "target":dict_gemini_languages} + data = _load_languages(path, "languages/languages.yml") -dict_openai_languages = { - "Arabic": "Arabic", - "Bengali": "Bengali", - "Bulgarian": "Bulgarian", - "Catalan": "Catalan", - "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", - "Swahili": "Swahili", - "Swedish": "Swedish", - "Thai": "Thai", - "Turkish": "Turkish", - "Ukrainian": "Ukrainian", - "Vietnamese": "Vietnamese", -} + if not isinstance(data, dict): + raise ValueError( + "languages/languages.yml のルートはマッピング(dict)である必要があります。" + ) -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 + # 検証と正規化 + 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_ollama.py b/src-python/models/translation/translation_ollama.py index 8b99d328..7e5b5468 100644 --- a/src-python/models/translation/translation_ollama.py +++ b/src-python/models/translation/translation_ollama.py @@ -42,7 +42,7 @@ class OllamaClient: self.model = None self.base_url = "http://localhost:11434" - prompt_config = loadPromptConfig(root_path) + prompt_config = loadPromptConfig(root_path, "translation_ollama.yml") self.supported_languages = prompt_config["supported_languages"] self.prompt_template = prompt_config["system_prompt"] From e71bf17e1359025b5f8c46e2ae3a0267fad7c227 Mon Sep 17 00:00:00 2001 From: misyaguziya <53165965+misyaguziya@users.noreply.github.com> Date: Sun, 19 Oct 2025 22:07:58 +0900 Subject: [PATCH 19/25] =?UTF-8?q?=E5=90=84=E7=BF=BB=E8=A8=B3=E3=82=AF?= =?UTF-8?q?=E3=83=A9=E3=82=A4=E3=82=A2=E3=83=B3=E3=83=88=E3=81=A7=20suppor?= =?UTF-8?q?ted=5Flanguages=20=E3=82=92=20YAML=20=E3=81=8B=E3=82=89=20trans?= =?UTF-8?q?lation=5Flang=20=E3=81=AB=E7=A7=BB=E8=A1=8C=E3=83=BB=E5=8F=82?= =?UTF-8?q?=E7=85=A7=E3=81=99=E3=82=8B=E3=82=88=E3=81=86=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=E3=80=81=E3=83=97=E3=83=AD=E3=83=B3=E3=83=97=E3=83=88YAML?= =?UTF-8?q?=E3=81=AE=E9=87=8D=E8=A4=87=E3=81=97=E3=81=9F=20supported=5Flan?= =?UTF-8?q?guages=20=E3=83=96=E3=83=AD=E3=83=83=E3=82=AF=E3=82=92=E5=89=8A?= =?UTF-8?q?=E9=99=A4=E3=80=81LMStudio=20=E3=81=AE=E3=83=87=E3=83=95?= =?UTF-8?q?=E3=82=A9=E3=83=AB=E3=83=88=20URL=20=E3=82=92=20/v1=20=E3=81=AB?= =?UTF-8?q?=E5=A4=89=E6=9B=B4=E3=80=81mainloop=20=E3=81=AB=20LMStudio/Olla?= =?UTF-8?q?ma=20=E3=81=AE=E3=83=A2=E3=83=87=E3=83=AB=E9=96=A2=E9=80=A3?= =?UTF-8?q?=E3=82=A8=E3=83=B3=E3=83=89=E3=83=9D=E3=82=A4=E3=83=B3=E3=83=88?= =?UTF-8?q?=E3=81=A8=20run=5Fmapping=20=E3=82=92=E8=BF=BD=E5=8A=A0?= =?UTF-8?q?=E3=80=81Controller=20=E5=86=85=E3=81=AE=20Ollama=20=E8=A1=A8?= =?UTF-8?q?=E8=A8=98=E3=81=AEtypo=EF=BC=88Lollama=E2=86=92Ollama=EF=BC=89?= =?UTF-8?q?=E3=82=92=E4=BF=AE=E6=AD=A3=E3=81=97=E3=81=A6=E3=83=A1=E3=82=BD?= =?UTF-8?q?=E3=83=83=E3=83=89=E5=90=8D=E3=83=BB=E3=83=AD=E3=82=B0=E6=96=87?= =?UTF-8?q?=E3=82=92=E6=9B=B4=E6=96=B0=E3=80=81=E5=90=84=E3=82=AF=E3=83=A9?= =?UTF-8?q?=E3=82=A4=E3=82=A2=E3=83=B3=E3=83=88=E3=81=A7=20translation=5Fl?= =?UTF-8?q?anguages=20=E3=81=AE=E3=83=95=E3=82=A9=E3=83=BC=E3=83=AB?= =?UTF-8?q?=E3=83=90=E3=83=83=E3=82=AF=20import=20=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-python/config.py | 2 +- src-python/controller.py | 16 +++---- src-python/mainloop.py | 15 +++++++ .../translation/prompt/translation_gemini.yml | 45 +------------------ .../prompt/translation_lmstudio.yml | 45 +------------------ .../translation/prompt/translation_ollama.yml | 45 +------------------ .../translation/prompt/translation_openai.yml | 44 +----------------- .../translation/prompt/translation_plamo.yml | 36 +-------------- .../models/translation/translation_gemini.py | 4 +- .../translation/translation_lmstudio.py | 4 +- .../models/translation/translation_ollama.py | 4 +- .../models/translation/translation_openai.py | 4 +- .../models/translation/translation_plamo.py | 4 +- 13 files changed, 48 insertions(+), 220 deletions(-) diff --git a/src-python/config.py b/src-python/config.py index 165919b2..e1683534 100644 --- a/src-python/config.py +++ b/src-python/config.py @@ -1402,7 +1402,7 @@ 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._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" diff --git a/src-python/controller.py b/src-python/controller.py index 55968566..136f807c 100644 --- a/src-python/controller.py +++ b/src-python/controller.py @@ -1941,8 +1941,8 @@ class Controller: } return response - def checkTranslatorLOllamaConnection(self, *args, **kwargs) -> dict: - printLog("Check Translator Lollama Connection") + def checkTranslatorOllamaConnection(self, *args, **kwargs) -> dict: + printLog("Check Translator Ollama Connection") try: result = model.authenticationTranslatorOllama() if result is True: @@ -1951,7 +1951,7 @@ class Controller: response = { "status":400, "result":{ - "message":"Cannot connect to Lollama server", + "message":"Cannot connect to ollama server", "data": False } } @@ -1966,15 +1966,15 @@ class Controller: } return response - def getTranslatorLOllamaModelList(self, *args, **kwargs) -> dict: + def getTranslatorOllamaModelList(self, *args, **kwargs) -> dict: model_list = model.getTranslatorOllamaModelList() return {"status":200, "result": model_list} - def getTranslatorLOllamaModel(self, *args, **kwargs) -> dict: + def getTranslatorOllamaModel(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) + def setTranslatorOllamaModel(self, data, *args, **kwargs) -> dict: + printLog("Set Translator Ollama Model", data) try: data = str(data) result = model.setTranslatorOllamaModel(model=data) @@ -1985,7 +1985,7 @@ class Controller: response = { "status":400, "result":{ - "message":"Lollama model is not valid", + "message":"ollama model is not valid", "data": config.SELECTED_OLLAMA_MODEL } } diff --git a/src-python/mainloop.py b/src-python/mainloop.py index e5dbc735..4e77104b 100644 --- a/src-python/mainloop.py +++ b/src-python/mainloop.py @@ -57,6 +57,10 @@ 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_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", @@ -202,6 +206,17 @@ mapping = { "/set/data/openai_auth_key": {"status": True, "variable":controller.setOpenAIAuthKey}, "/delete/data/openai_auth_key": {"status": True, "variable":controller.delOpenAIAuthKey}, + "/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}, + + "/get/data/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}, + "/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/models/translation/prompt/translation_gemini.yml b/src-python/models/translation/prompt/translation_gemini.yml index 8c7b56fc..8dbe4927 100644 --- a/src-python/models/translation/prompt/translation_gemini.yml +++ b/src-python/models/translation/prompt/translation_gemini.yml @@ -2,47 +2,6 @@ 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. -supported_languages: | - Arabic - Bengali - Bulgarian - Simplified Chinese - Traditional Chinese - Croatian - Czech - Danish - Dutch - English - Estonian - Finnish - French - German - Greek - Hebrew - Hindi - Hungarian - Indonesian - Italian - Japanese - Korean - Latvian - Lithuanian - Norwegian - Polish - Portuguese - Romanian - Russian - Serbian - Slovak - Slovenian - Spanish - Swahili - Swedish - Thai - Turkish - Ukrainian - Vietnamese + 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 index 8c7b56fc..8dbe4927 100644 --- a/src-python/models/translation/prompt/translation_lmstudio.yml +++ b/src-python/models/translation/prompt/translation_lmstudio.yml @@ -2,47 +2,6 @@ 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. -supported_languages: | - Arabic - Bengali - Bulgarian - Simplified Chinese - Traditional Chinese - Croatian - Czech - Danish - Dutch - English - Estonian - Finnish - French - German - Greek - Hebrew - Hindi - Hungarian - Indonesian - Italian - Japanese - Korean - Latvian - Lithuanian - Norwegian - Polish - Portuguese - Romanian - Russian - Serbian - Slovak - Slovenian - Spanish - Swahili - Swedish - Thai - Turkish - Ukrainian - Vietnamese + 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 index 8c7b56fc..8dbe4927 100644 --- a/src-python/models/translation/prompt/translation_ollama.yml +++ b/src-python/models/translation/prompt/translation_ollama.yml @@ -2,47 +2,6 @@ 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. -supported_languages: | - Arabic - Bengali - Bulgarian - Simplified Chinese - Traditional Chinese - Croatian - Czech - Danish - Dutch - English - Estonian - Finnish - French - German - Greek - Hebrew - Hindi - Hungarian - Indonesian - Italian - Japanese - Korean - Latvian - Lithuanian - Norwegian - Polish - Portuguese - Romanian - Russian - Serbian - Slovak - Slovenian - Spanish - Swahili - Swedish - Thai - Turkish - Ukrainian - Vietnamese + 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 index 802d0b1d..8dbe4927 100644 --- a/src-python/models/translation/prompt/translation_openai.yml +++ b/src-python/models/translation/prompt/translation_openai.yml @@ -4,46 +4,4 @@ system_prompt: | {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. - -supported_languages: | - Arabic - Bengali - Bulgarian - Catalan - Chinese Simplified - Chinese Traditional - Croatian - Czech - Danish - Dutch - English - Estonian - Finnish - French - German - Greek - Hebrew - Hindi - Hungarian - Indonesian - Italian - Japanese - Korean - Latvian - Lithuanian - Norwegian - Polish - Portuguese - Romanian - Russian - Serbian - Slovak - Slovenian - Spanish - Swahili - Swedish - Thai - Turkish - Ukrainian - Vietnamese + 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 index cdb0b478..c0afe533 100644 --- a/src-python/models/translation/prompt/translation_plamo.yml +++ b/src-python/models/translation/prompt/translation_plamo.yml @@ -2,38 +2,6 @@ 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. -supported_languages: | - English - Japanese - Korean - French - German - Spanish - Portuguese - Russian - Italian - Dutch - Polish - Turkish - Arabic - Hindi - Thai - Vietnamese - Indonesian - Malay - Filipino - Swedish - Finnish - Danish - Norwegian - Romanian - Czech - Hungarian - Greek - Hebrew - Simplified Chinese - Traditional Chinese + 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 index fd20c941..1c8a0161 100644 --- a/src-python/models/translation/translation_gemini.py +++ b/src-python/models/translation/translation_gemini.py @@ -3,12 +3,14 @@ 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") @@ -56,7 +58,7 @@ class GeminiClient: # プロンプト設定をYAMLファイルから読み込む prompt_config = loadPromptConfig(root_path, "translation_gemini.yml") - self.supported_languages = prompt_config["supported_languages"] + self.supported_languages = list(translation_lang["Gemini_API"]["source"].keys()) self.prompt_template = prompt_config["system_prompt"] self.gemini_llm = None diff --git a/src-python/models/translation/translation_lmstudio.py b/src-python/models/translation/translation_lmstudio.py index 1827d887..34319075 100644 --- a/src-python/models/translation/translation_lmstudio.py +++ b/src-python/models/translation/translation_lmstudio.py @@ -3,11 +3,13 @@ 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: @@ -43,7 +45,7 @@ class LMStudioClient: self.base_url = base_url # None の場合は公式エンドポイント prompt_config = loadPromptConfig(root_path, "translation_lmstudio.yml") - self.supported_languages = prompt_config["supported_languages"] + self.supported_languages = list(translation_lang["LMStudio"]["source"].keys()) self.prompt_template = prompt_config["system_prompt"] self.openai_llm = None diff --git a/src-python/models/translation/translation_ollama.py b/src-python/models/translation/translation_ollama.py index 7e5b5468..2d56aaee 100644 --- a/src-python/models/translation/translation_ollama.py +++ b/src-python/models/translation/translation_ollama.py @@ -2,11 +2,13 @@ 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: @@ -43,7 +45,7 @@ class OllamaClient: self.base_url = "http://localhost:11434" prompt_config = loadPromptConfig(root_path, "translation_ollama.yml") - self.supported_languages = prompt_config["supported_languages"] + self.supported_languages = list(translation_lang["Ollama"]["source"].keys()) self.prompt_template = prompt_config["system_prompt"] self.openai_llm = None diff --git a/src-python/models/translation/translation_openai.py b/src-python/models/translation/translation_openai.py index 2741702e..b7115e51 100644 --- a/src-python/models/translation/translation_openai.py +++ b/src-python/models/translation/translation_openai.py @@ -3,11 +3,13 @@ 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: @@ -67,7 +69,7 @@ class OpenAIClient: self.base_url = base_url # None の場合は公式エンドポイント prompt_config = loadPromptConfig(root_path, "translation_openai.yml") - self.supported_languages = prompt_config["supported_languages"] + self.supported_languages = list(translation_lang["OpenAI_API"]["source"].keys()) self.prompt_template = prompt_config["system_prompt"] self.openai_llm = None diff --git a/src-python/models/translation/translation_plamo.py b/src-python/models/translation/translation_plamo.py index 112a962b..ea54fd61 100644 --- a/src-python/models/translation/translation_plamo.py +++ b/src-python/models/translation/translation_plamo.py @@ -3,11 +3,13 @@ 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" @@ -42,7 +44,7 @@ class PlamoClient: self.model = None prompt_config = loadPromptConfig(root_path, "translation_plamo.yml") - self.supported_languages = prompt_config["supported_languages"] + self.supported_languages = list(translation_lang["Plamo_API"]["source"].keys()) self.prompt_template = prompt_config["system_prompt"] self.plamo_llm = None From 83e72b37cc621162835fffdd69ba160c011bf421 Mon Sep 17 00:00:00 2001 From: misyaguziya <53165965+misyaguziya@users.noreply.github.com> Date: Mon, 20 Oct 2025 00:38:38 +0900 Subject: [PATCH 20/25] =?UTF-8?q?LMStudio=20=E6=8E=A5=E7=B6=9A=E3=83=81?= =?UTF-8?q?=E3=82=A7=E3=83=83=E3=82=AF=E3=81=A8=E3=83=A2=E3=83=87=E3=83=AB?= =?UTF-8?q?=E9=81=B8=E6=8A=9E=E5=87=A6=E7=90=86=E3=82=92=E8=BF=BD=E5=8A=A0?= =?UTF-8?q?=E3=83=BB=E5=BC=B7=E5=8C=96=E3=80=81mainloop=20=E3=81=AB?= =?UTF-8?q?=E3=82=A8=E3=83=B3=E3=83=89=E3=83=9D=E3=82=A4=E3=83=B3=E3=83=88?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0=E3=80=81LMStudio/Ollama=20=E3=81=AE=E3=83=A2?= =?UTF-8?q?=E3=83=87=E3=83=AB=E5=8F=96=E5=BE=97=E3=82=92=E4=BE=8B=E5=A4=96?= =?UTF-8?q?=E8=80=90=E6=80=A7=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Controller に checkTranslatorLMStudioConnection を追加し、接続成功時に SELECTABLE_TRANSLATION_ENGINE_STATUS や SELECTABLE_LMSTUDIO_MODEL_LIST を更新、選択モデルのフォールバック設定・クライアント更新・UI 更新呼び出しを行うよう実装 - setTranslatorLMStudioURL を接続成功時にモデルリスト/ステータス更新や選択モデルのフォールバック処理を行うよう拡張 - checkTranslatorOllamaConnection 成功時にも SELECTABLE_TRANSLATION_ENGINE_STATUS/モデルリスト更新、選択モデルフォールバック、クライアント更新、UI 更新呼び出しを追加 - mainloop に /get/data/lmstudio_connection エンドポイントを追加して LMStudio 接続チェックを公開 - translation_lmstudio.py と translation_ollama.py のモデル取得処理を try/except で保護し、例外時は空リストを返すよう修正(接続失敗やレスポンス異常に対する耐性向上) --- src-python/controller.py | 52 +++++++++++++++++++ src-python/mainloop.py | 1 + .../translation/translation_lmstudio.py | 12 +++-- .../models/translation/translation_ollama.py | 7 ++- 4 files changed, 66 insertions(+), 6 deletions(-) diff --git a/src-python/controller.py b/src-python/controller.py index 136f807c..f8ce770c 100644 --- a/src-python/controller.py +++ b/src-python/controller.py @@ -1877,16 +1877,59 @@ class Controller: } return response + def checkTranslatorLMStudioConnection(self, *args, **kwargs) -> dict: + printLog("Check Translator LMStudio Connection") + translator_name = "LMStudio" + try: + result = model.authenticationTranslatorLMStudio() + 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] + 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] + 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 = { @@ -1943,9 +1986,18 @@ class Controller: 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] + self.run(200, self.run_mapping["selected_ollama_model"], config.SELECTED_OLLAMA_MODEL) + model.updateTranslatorOllamaClient() + self.updateTranslationEngineAndEngineList() response = {"status":200, "result":True} else: response = { diff --git a/src-python/mainloop.py b/src-python/mainloop.py index 4e77104b..21e194bc 100644 --- a/src-python/mainloop.py +++ b/src-python/mainloop.py @@ -206,6 +206,7 @@ mapping = { "/set/data/openai_auth_key": {"status": True, "variable":controller.setOpenAIAuthKey}, "/delete/data/openai_auth_key": {"status": True, "variable":controller.delOpenAIAuthKey}, + "/get/data/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}, diff --git a/src-python/models/translation/translation_lmstudio.py b/src-python/models/translation/translation_lmstudio.py index 34319075..7751dc16 100644 --- a/src-python/models/translation/translation_lmstudio.py +++ b/src-python/models/translation/translation_lmstudio.py @@ -25,11 +25,15 @@ def _authentication_check(api_key: str, base_url: str | None = None) -> bool: 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. """ - client = OpenAI(api_key=api_key, base_url=base_url) - res = client.models.list() - allowed_models = [] + try: + client = OpenAI(api_key=api_key, base_url=base_url) + res = client.models.list() + models = res.data + except Exception: + models = [] - for model in res.data: + allowed_models = [] + for model in models: allowed_models.append(model.id) allowed_models.sort() diff --git a/src-python/models/translation/translation_ollama.py b/src-python/models/translation/translation_ollama.py index 2d56aaee..6dd1f2fb 100644 --- a/src-python/models/translation/translation_ollama.py +++ b/src-python/models/translation/translation_ollama.py @@ -26,8 +26,11 @@ def _authentication_check(base_url: str | None = None) -> bool: def _get_available_text_models(base_url: str | None = None) -> list[str]: """Extract available text models from Ollama. """ - response = requests.get(f"{base_url}/api/tags") - models = response.json()["models"] + try: + response = requests.get(f"{base_url}/api/tags") + models = response.json()["models"] + except Exception: + models = [] allowed_models = [] for model in models: From bcfbf5169641c0f8ead8fd7780a334f958934bfb Mon Sep 17 00:00:00 2001 From: misyaguziya <53165965+misyaguziya@users.noreply.github.com> Date: Mon, 20 Oct 2025 01:19:49 +0900 Subject: [PATCH 21/25] =?UTF-8?q?LMStudio=20=E8=AA=8D=E8=A8=BC=E5=91=BC?= =?UTF-8?q?=E3=81=B3=E5=87=BA=E3=81=97=E3=81=A7=20base=5Furl=20=E3=82=92?= =?UTF-8?q?=E6=98=8E=E7=A4=BA=E6=B8=A1=E3=81=97=E3=81=B8=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=20+=20=E3=83=89=E3=82=AD=E3=83=A5=E3=83=A1=E3=83=B3=E3=83=88?= =?UTF-8?q?=E6=95=B4=E5=82=99=EF=BC=88=E3=83=AD=E3=83=BC=E3=82=AB=E3=83=AB?= =?UTF-8?q?LLM/=E8=A8=80=E8=AA=9E=E3=83=9E=E3=83=83=E3=83=94=E3=83=B3?= =?UTF-8?q?=E3=82=B0/=E3=83=95=E3=82=A9=E3=83=B3=E3=83=88=E7=AD=89?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - controller: model.authenticationTranslatorLMStudio 呼び出しに base_url=config.LMSTUDIO_URL を明示的に渡すよう修正(LMStudio 接続判定で設定 URL を利用) - docs: 新規ドキュメントを追加・更新 - 追加: translation_gemini.md, translation_lmstudio.md, translation_ollama.md, translation_openai.md, translation_plamo.md - 更新: config.md, controller.md, mainloop.md, model.md, overlay.md, translation_languages.md, translation_translator.md, 仕様書.md(翻訳/モデル管理・エンドポイント・YAML 言語定義・フォント探索・VRAM フォールバック等の記載追加) - ドキュメントに記載した主な変更点 - LMStudio / Ollama のローカルLLM統合(モデルリスト/選択用プロパティ追加、接続確認エンドポイント) - CTranslate2 の言語定義を weight_type ネスト構造へ変更対応 - 外部 YAML による言語マッピング導入(loadTranslationLanguages) - フォント探索を PyInstaller バンドル(_internal/fonts/) を考慮して強化 - 認証後のモデルリスト自動更新・SELECTED_* プロパティ名統一、VRAM エラー検知時の自動フォールバック等の動作説明追加 (コードの振る舞いは既存処理に合わせた引数指定の修正とドキュメント反映が主体) --- src-python/controller.py | 4 +- src-python/docs/details/config.md | 59 +++++++++- src-python/docs/details/controller.md | 108 ++++++++++++++++-- src-python/docs/details/mainloop.md | 79 ++++++++++++- src-python/docs/details/model.md | 87 +++++++++++++- src-python/docs/details/overlay.md | 23 +++- src-python/docs/details/translation_gemini.md | 87 ++++++++++++++ .../docs/details/translation_languages.md | 53 ++++++++- .../docs/details/translation_lmstudio.md | 86 ++++++++++++++ src-python/docs/details/translation_ollama.md | 86 ++++++++++++++ src-python/docs/details/translation_openai.md | 85 ++++++++++++++ src-python/docs/details/translation_plamo.md | 86 ++++++++++++++ .../docs/details/translation_translator.md | 48 +++++++- src-python/docs/仕様書.md | 76 +++++++++++- 14 files changed, 943 insertions(+), 24 deletions(-) create mode 100644 src-python/docs/details/translation_gemini.md create mode 100644 src-python/docs/details/translation_lmstudio.md create mode 100644 src-python/docs/details/translation_ollama.md create mode 100644 src-python/docs/details/translation_openai.md create mode 100644 src-python/docs/details/translation_plamo.md diff --git a/src-python/controller.py b/src-python/controller.py index f8ce770c..b9d4bab6 100644 --- a/src-python/controller.py +++ b/src-python/controller.py @@ -1881,7 +1881,7 @@ class Controller: printLog("Check Translator LMStudio Connection") translator_name = "LMStudio" try: - result = model.authenticationTranslatorLMStudio() + 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() @@ -2984,7 +2984,7 @@ class Controller: 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: + 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() 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..66adb775 100644 --- a/src-python/docs/details/controller.md +++ b/src-python/docs/details/controller.md @@ -4,25 +4,77 @@ VRCTアプリケーションのビジネスロジックを制御するコントローラークラスです。UI層とモデル層の間に位置し、ユーザーの入力を適切な処理に変換し、結果を UI に返す役割を担います。全ての機能制御、設定管理、状態管理を一元的に行います。 +## 最近の更新 (2025-10-20) + +### 新規ローカルLLM翻訳エンジン統合 + +- LMStudio / Ollama への接続確認エンドポイント追加: `/get/data/lmstudio_connection`, `/get/data/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` を統合して互換言語のみ抽出 + +### デバイス自動選択改善 + +- マイク/スピーカー自動選択機能で更新前後に適切な停止/再開コールバックをチェーン設定 (energy チェック再起動含む) + +### 影響 + +| 項目 | 内容 | +|------|------| +| 翻訳柔軟性 | ローカルLLM (LMStudio/Ollama) によりネットワーク不要運用が可能 | +| 回復性 | VRAMエラー時自動フォールバックで異常終了を防止 | +| 拡張性 | モデルリスト自動更新により新モデル追加時の手動変更不要化 | +| 一貫性 | 統一された SELECTED_/SELECTABLE_ 命名とタブ別管理で UI 連携が簡易化 | +| 可読性 | 外部 YAML 言語定義によりコード側のハードコード削減 | + ## 主要機能 ### 機能制御 + - 翻訳機能の有効化・無効化 - 音声認識機能の制御 - VRオーバーレイの管理 - WebSocketサーバーの制御 ### 設定管理 + - アプリケーション設定の取得・更新 - デバイス設定の管理 - 言語・エンジン設定の制御 ### 状態管理 + - システム状態の監視 - エラー状態の管理 - 初期化プロセスの制御 ### 通信制御 + - OSC通信の管理 - WebSocket通信の制御 - 外部アプリケーション連携 @@ -30,6 +82,7 @@ VRCTアプリケーションのビジネスロジックを制御するコント ## クラス構造 ### Controller クラス + ```python class Controller: def __init__(self) -> None @@ -40,19 +93,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 +119,7 @@ class DownloadWhisper: ```python init() -> None ``` + - コントローラーの初期化 - 各コンポーネントの起動 - 初期設定の適用 @@ -71,6 +129,7 @@ setInitMapping(init_mapping: dict) -> None setRunMapping(run_mapping: dict) -> None setRun(run: Callable) -> None ``` + - エンドポイント・コールバック設定 ### 翻訳機能制御 @@ -79,23 +138,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 +167,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 +197,14 @@ setSpeakerThreshold(data) -> dict setEnableOverlaySmallLog(data) -> dict setEnableOverlayLargeLog(data) -> dict ``` + - VRオーバーレイの有効化 ```python setOverlaySmallLogSettings(data) -> dict setOverlayLargeLogSettings(data) -> dict ``` + - オーバーレイ設定の更新 ### WebSocket制御 @@ -144,12 +213,14 @@ setOverlayLargeLogSettings(data) -> dict setEnableWebSocketServer(data) -> dict setDisableWebSocketServer(data) -> dict ``` + - WebSocketサーバーの制御 ```python setWebSocketHost(data) -> dict setWebSocketPort(data) -> dict ``` + - WebSocket接続設定 ### システム管理 @@ -158,17 +229,20 @@ setWebSocketPort(data) -> dict updateSoftware(data) -> dict updateCudaSoftware(data) -> dict ``` + - ソフトウェアアップデート ```python downloadCtranslate2Weight(data) -> dict downloadWhisperWeight(data) -> dict ``` + - AIモデルのダウンロード ```python feedWatchdog(data) -> dict ``` + - ウォッチドッグの生存シグナル送信 ## 使用方法 @@ -233,6 +307,7 @@ result = controller.setEnableTranscriptionSend(None) ``` ### 成功レスポンス例 + ```python { "status": 200, @@ -241,6 +316,7 @@ result = controller.setEnableTranscriptionSend(None) ``` ### エラーレスポンス例 + ```python { "status": 400, @@ -248,19 +324,22 @@ result = controller.setEnableTranscriptionSend(None) } ``` -## 状態管理 +## 詳細状態管理 ### システム状態 + - 各機能の有効・無効状態 - デバイスの接続状態 - ネットワーク接続状態 -### エラー状態 +### エラー状態 + - デバイスエラー - 翻訳エンジンエラー - VRAMオーバーフローエラー ### 初期化状態 + - 段階的な初期化プロセス - 依存関係の解決状態 @@ -271,32 +350,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 +389,39 @@ speakerMessage(result: dict) -> None ## エラーハンドリング ### VRAM不足エラー + - 自動的にCTranslate2への切り替え - ユーザーへの適切な通知 ### デバイスエラー + - デバイス接続状態の監視 - 自動復旧機能 -### ネットワークエラー +### ネットワークエラー + - 接続状態の定期確認 - オフライン機能への切り替え ### 設定エラー + - 設定値の妥当性チェック - デフォルト値への復帰 ## パフォーマンス考慮事項 ### 遅延初期化 + - 必要な時点での機能初期化 - メモリ使用量の最適化 ### 非同期処理 + - バックグラウンドでの重い処理 - UI の応答性維持 ### キャッシュ機能 + - 設定値のキャッシュ - 翻訳結果のキャッシュ @@ -346,4 +438,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..e89909d9 100644 --- a/src-python/docs/details/mainloop.md +++ b/src-python/docs/details/mainloop.md @@ -4,19 +4,59 @@ 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 モデル取得通知と形式統一 + +### エンドポイントロックキー正規化 + +- `/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 +64,7 @@ VRCTアプリケーションのメインイベントループを管理するモ ## クラス構造 ### Main クラス + ```python class Main: def __init__(self, controller_instance: Controller, mapping_data: dict, worker_count: int = 3) @@ -36,46 +77,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 +136,21 @@ class Main: ```python receiver() -> None ``` + - 標準入力からのJSONリクエスト受信 - パースエラーの適切な処理 ```python handleRequest(endpoint: str, data: Any = None) -> tuple ``` + - エンドポイント処理の実行 - ステータスコードと結果の返却 ```python handler() -> None ``` + - ワーカースレッドのメイン処理 - キューからのリクエスト取得・処理 @@ -107,21 +159,25 @@ handler() -> None ```python startReceiver() -> None ``` + - レシーバースレッドの起動 ```python startHandler() -> None ``` + - ハンドラースレッドプールの起動 ```python start() -> None ``` + - 全スレッドの起動 ```python stop(wait: float = 2.0) -> None ``` + - 全スレッドの安全な停止 ## 使用方法 @@ -164,6 +220,7 @@ result, status = main_instance.handleRequest("/set/enable/translation", None) ## リクエスト形式 ### 入力形式 + ```json { "endpoint": "string", // 必須:処理対象のエンドポイント @@ -172,6 +229,7 @@ result, status = main_instance.handleRequest("/set/enable/translation", None) ``` ### 出力形式 + ```json { "status": 200, // HTTPステータスコード @@ -191,11 +249,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 +264,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 +303,13 @@ result, status = main_instance.handleRequest("/set/enable/translation", None) ## 設定項目 ### ワーカー数 + ```python DEFAULT_WORKER_COUNT = 3 # 並列処理スレッド数 ``` ### タイムアウト + - キュー待機タイムアウト: 0.5秒 - スレッド停止待機: 2.0秒 - 処理安定化待機: 0.2秒 @@ -256,14 +324,17 @@ DEFAULT_WORKER_COUNT = 3 # 並列処理スレッド数 ## パフォーマンス特性 ### スループット + - 複数ワーカーによる並列処理 - ノンブロッキングI/O ### レイテンシ + - キューイング遅延の最小化 - 排他制御による一時的な遅延あり ### メモリ使用量 + - リクエストキューのサイズ制限なし(要注意) - スレッドプールによる固定オーバーヘッド @@ -272,4 +343,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 ライセンス許容範囲内)。 From 49aafadd1c34f551e11c1e421e3f28cc0fd40398 Mon Sep 17 00:00:00 2001 From: misyaguziya <53165965+misyaguziya@users.noreply.github.com> Date: Mon, 20 Oct 2025 01:30:00 +0900 Subject: [PATCH 22/25] =?UTF-8?q?=E8=AA=8D=E8=A8=BC=E3=82=AD=E3=83=BC?= =?UTF-8?q?=E6=A4=9C=E8=A8=BC=E3=82=92=E4=BF=AE=E6=AD=A3=EF=BC=88Plamo/Gem?= =?UTF-8?q?ini/OpenAI=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Plamo API のキー判定を厳密化: 長さチェックを「==72」から「>=72」に変更して72文字以上を受け入れるように - Gemini API の最小長を20→39に引き上げ - OpenAI API キー判定を簡易長さチェックから厳格化: "sk-"で始まりかつ長さ>=164 を要求するように変更 - OpenAI のエラーメッセージを「長さが不正」から「無効」に変更して意図を明確化 --- src-python/controller.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src-python/controller.py b/src-python/controller.py index b9d4bab6..c33ffe54 100644 --- a/src-python/controller.py +++ b/src-python/controller.py @@ -1613,7 +1613,7 @@ class Controller: translator_name = "Plamo_API" try: data = str(data) - if len(data) == 72: + if len(data) >= 72: result = model.authenticationTranslatorPlamoAuthKey(auth_key=data) if result is True: key = data @@ -1707,7 +1707,7 @@ class Controller: translator_name = "Gemini_API" try: data = str(data) - if len(data) >= 20: + if len(data) >= 39: result = model.authenticationTranslatorGeminiAuthKey(auth_key=data) if result is True: key = data @@ -1802,7 +1802,7 @@ class Controller: translator_name = "OpenAI_API" try: data = str(data) - if len(data) >= 20: + if data.startswith("sk-") and len(data) >= 164: key = data auth_keys = config.AUTH_KEYS auth_keys[translator_name] = key @@ -1820,7 +1820,7 @@ class Controller: response = { "status":400, "result":{ - "message":"OpenAI auth key length is not correct", + "message":"OpenAI auth key is not valid", "data": config.AUTH_KEYS[translator_name] } } From 452f564c004707e9135e1a1017c1db95b66e77d8 Mon Sep 17 00:00:00 2001 From: misyaguziya <53165965+misyaguziya@users.noreply.github.com> Date: Mon, 20 Oct 2025 01:30:41 +0900 Subject: [PATCH 23/25] =?UTF-8?q?backend=5Ftest:=20LLM/API=E9=96=A2?= =?UTF-8?q?=E9=80=A3=E3=81=AE=20set/data=20=E3=83=86=E3=82=B9=E3=83=88?= =?UTF-8?q?=E6=8B=A1=E5=BC=B5=E3=81=A8=E3=83=87=E3=83=90=E3=83=83=E3=82=B0?= =?UTF-8?q?=E5=87=BA=E5=8A=9B=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - /set/data 系のテストに Plamo/Gemini/OpenAI/LMStudio/Ollama のモデル選択と認証キー投入を追加(/get/data/* でモデルリストを事前取得してランダム選択) - LMStudio の URL を正常/異常ケースでランダム投入するテストを追加 - Deepl 等の認証キーの期待ステータスを [200, 400] に更新(成功/失敗両方を許容) - set_data_endpoints の数をデバッグログ出力する行を追加 - transliteration 機能(convert_message_to_romaji / convert_message_to_hiragana)の enable/disable エントリをテスト用に追加 --- src-python/backend_test.py | 63 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 61 insertions(+), 2 deletions(-) diff --git a/src-python/backend_test.py b/src-python/backend_test.py index 8fea1092..7c91cb51 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] From 306f0af734e61a5bcf6a67ef37d5054876f35427 Mon Sep 17 00:00:00 2001 From: misyaguziya <53165965+misyaguziya@users.noreply.github.com> Date: Mon, 20 Oct 2025 03:48:41 +0900 Subject: [PATCH 24/25] =?UTF-8?q?controller:=20=E7=BF=BB=E8=A8=B3=E3=83=A2?= =?UTF-8?q?=E3=83=87=E3=83=AB=E9=81=B8=E6=8A=9E=E6=99=82=E3=81=AB=E3=83=A2?= =?UTF-8?q?=E3=83=87=E3=83=AB=E9=81=A9=E7=94=A8=E3=81=A8=E3=82=AF=E3=83=A9?= =?UTF-8?q?=E3=82=A4=E3=82=A2=E3=83=B3=E3=83=88=E6=9B=B4=E6=96=B0=E3=82=92?= =?UTF-8?q?=E7=A2=BA=E5=AE=9F=E3=81=AB=E8=A1=8C=E3=81=86=E3=82=88=E3=81=86?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3=20-=20=E3=83=87=E3=83=95=E3=82=A9=E3=83=AB?= =?UTF-8?q?=E3=83=88=E9=81=B8=E6=8A=9E=E6=99=82=E3=81=8A=E3=82=88=E3=81=B3?= =?UTF-8?q?=E6=98=8E=E7=A4=BA=E7=9A=84=E3=81=AA=E3=83=A2=E3=83=87=E3=83=AB?= =?UTF-8?q?=E8=A8=AD=E5=AE=9A=E6=99=82=E3=81=AB=20model.setTranslatorXMode?= =?UTF-8?q?l(...)=20=E3=82=92=E5=91=BC=E3=81=B3=E5=87=BA=E3=81=97=E3=80=81?= =?UTF-8?q?=E3=83=A2=E3=83=87=E3=83=AB=E7=8A=B6=E6=85=8B=E3=82=92=E7=A2=BA?= =?UTF-8?q?=E5=AE=9F=E3=81=AB=E5=8F=8D=E6=98=A0=20-=20OpenAI/LMStudio/Olla?= =?UTF-8?q?ma/Plamo/Gemini/Ollama=20=E3=81=A7=E3=82=AF=E3=83=A9=E3=82=A4?= =?UTF-8?q?=E3=82=A2=E3=83=B3=E3=83=88=E6=9B=B4=E6=96=B0=E3=82=92=E9=81=A9?= =?UTF-8?q?=E5=88=87=E3=81=AA=E7=AE=87=E6=89=80=E3=81=AB=E8=BF=BD=E5=8A=A0?= =?UTF-8?q?=E3=81=97=E3=81=A6=E6=95=B4=E5=90=88=E6=80=A7=E3=82=92=E5=90=91?= =?UTF-8?q?=E4=B8=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mainloop: LMStudio/Ollama 接続チェックのエンドポイントを run 系に変更、ローマ字変換関連を追加 - "/get/data/*_connection" → "/run/*_connection" に変更(LMStudio/Ollama) - ローマ字変換(convert_message_to_romaji) のマッピングを追加(取得/有効化/無効化) translation_ollama: 認証チェックのエンドポイント呼び出しを修正 - base_url + "/api/ping" ではなく base_url 直下に対して GET を行うよう変更(環境による応答差異に対応) --- src-python/controller.py | 14 ++++++++++++++ src-python/mainloop.py | 5 +++-- .../models/translation/translation_ollama.py | 2 +- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src-python/controller.py b/src-python/controller.py index c33ffe54..f12afec9 100644 --- a/src-python/controller.py +++ b/src-python/controller.py @@ -1625,6 +1625,7 @@ class Controller: 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() @@ -1678,6 +1679,7 @@ class Controller: 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: @@ -1719,6 +1721,7 @@ class Controller: 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() @@ -1772,6 +1775,7 @@ class Controller: 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: @@ -1812,6 +1816,7 @@ class Controller: 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() @@ -1857,6 +1862,8 @@ class Controller: 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 = { @@ -1888,6 +1895,7 @@ class Controller: 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() @@ -1927,6 +1935,7 @@ class Controller: 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() @@ -1964,6 +1973,8 @@ class Controller: 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 = { @@ -1995,6 +2006,7 @@ class Controller: 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() @@ -2032,6 +2044,8 @@ class Controller: 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 = { diff --git a/src-python/mainloop.py b/src-python/mainloop.py index 21e194bc..aa0cc386 100644 --- a/src-python/mainloop.py +++ b/src-python/mainloop.py @@ -206,18 +206,19 @@ mapping = { "/set/data/openai_auth_key": {"status": True, "variable":controller.setOpenAIAuthKey}, "/delete/data/openai_auth_key": {"status": True, "variable":controller.delOpenAIAuthKey}, - "/get/data/lmstudio_connection": {"status": True, "variable":controller.checkTranslatorLMStudioConnection}, + "/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}, - "/get/data/ollama_connection": {"status": True, "variable":controller.checkTranslatorOllamaConnection}, + "/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/models/translation/translation_ollama.py b/src-python/models/translation/translation_ollama.py index 6dd1f2fb..4152c953 100644 --- a/src-python/models/translation/translation_ollama.py +++ b/src-python/models/translation/translation_ollama.py @@ -15,7 +15,7 @@ def _authentication_check(base_url: str | None = None) -> bool: """Check authentication for Ollama API. """ try: - response = requests.get(f"{base_url}/api/ping") + response = requests.get(f"{base_url}") if response.status_code == 200: return True else: From 87d11c617c969cfd5ce1504eac078080394e67ca Mon Sep 17 00:00:00 2001 From: misyaguziya <53165965+misyaguziya@users.noreply.github.com> Date: Mon, 20 Oct 2025 12:23:16 +0900 Subject: [PATCH 25/25] =?UTF-8?q?backend=5Ftest/docs:=20LMStudio/Ollama=20?= =?UTF-8?q?run=20=E6=8E=A5=E7=B6=9A=E3=82=A8=E3=83=B3=E3=83=89=E3=83=9D?= =?UTF-8?q?=E3=82=A4=E3=83=B3=E3=83=88=E3=81=AE=E3=83=86=E3=82=B9=E3=83=88?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0=E3=81=A8=E3=83=89=E3=82=AD=E3=83=A5=E3=83=A1?= =?UTF-8?q?=E3=83=B3=E3=83=88=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - backend_test: /run/lmstudio_connection, /run/ollama_connection をテスト対象に追加(期待ステータス 200,400) - docs: controller.md / mainloop.md の LMStudio/Ollama 接続エンドポイント表記を /get/data→/run に修正 - docs: APIキー検証の厳格化と翻訳モデル選択時の適用確実化に関する説明を追記 --- src-python/backend_test.py | 6 ++++++ src-python/docs/details/controller.md | 13 ++++++++++++- src-python/docs/details/mainloop.md | 2 ++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src-python/backend_test.py b/src-python/backend_test.py index 7c91cb51..c43a622d 100644 --- a/src-python/backend_test.py +++ b/src-python/backend_test.py @@ -528,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/docs/details/controller.md b/src-python/docs/details/controller.md index 66adb775..3dd3e73b 100644 --- a/src-python/docs/details/controller.md +++ b/src-python/docs/details/controller.md @@ -8,7 +8,7 @@ VRCTアプリケーションのビジネスロジックを制御するコント ### 新規ローカルLLM翻訳エンジン統合 -- LMStudio / Ollama への接続確認エンドポイント追加: `/get/data/lmstudio_connection`, `/get/data/ollama_connection` +- 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`) @@ -38,6 +38,17 @@ VRCTアプリケーションのビジネスロジックを制御するコント - `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 チェック再起動含む) diff --git a/src-python/docs/details/mainloop.md b/src-python/docs/details/mainloop.md index e89909d9..02175264 100644 --- a/src-python/docs/details/mainloop.md +++ b/src-python/docs/details/mainloop.md @@ -11,6 +11,8 @@ VRCTアプリケーションのメインイベントループを管理するモ - 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` で音訳機能制御 ### エンドポイントロックキー正規化