diff --git a/app/app_core.py b/app/app_core.py index 1786254..393e36d 100644 --- a/app/app_core.py +++ b/app/app_core.py @@ -168,8 +168,8 @@ class AppCore(JaaCore): # log request / response text if self.rest_log_params is not None and (self.rest_log_params.translate_resp_text or self.rest_log_params.translate_req_text): - log_source = "\nSource:\n" + req.text if self.rest_log_params.translate_req_text else "" - log_translate = "\nResult:\n" + translate_text if self.rest_log_params.translate_resp_text else "" + log_source = "\n\tSource:\n" + req.text if self.rest_log_params.translate_req_text else "" + log_translate = "\n\tResult:\n" + translate_text if self.rest_log_params.translate_resp_text else "" logger.info(f"Translate text.{log_source}{log_translate}") return dto.TranslateResp(result=translate_text, parts=translate_parts, error=None) diff --git a/doc/ru/integrations/luna_translator.md b/doc/ru/integrations/luna_translator.md new file mode 100644 index 0000000..e69de29 diff --git a/doc/ru/integrations/renpy.md b/doc/ru/integrations/renpy.md new file mode 100644 index 0000000..e69de29 diff --git a/doc/ru/readme.md b/doc/ru/readme.md index 7f9c0d5..812a07a 100644 --- a/doc/ru/readme.md +++ b/doc/ru/readme.md @@ -22,6 +22,10 @@ [Перевод файлов](processing_files.md) +## Интеграции + +Интеграция с внешними приложениями. [Документация](../../integrations/readme.md). + ## API Проект поддерживает Swagger-UI, в котором можно увидеть все методы API. @@ -38,20 +42,21 @@ ## Структура проекта * `/app` - папка с python-файлами, которые используются плагинами и API-контроллером. -* `/cache` - папка для сохранения файла базы кэша по умолчанию. [Документация](doc/ru/options.md). +* `/cache` - папка для сохранения файла базы кэша по умолчанию. [Документация](options.md). * `/doc` - документация * `/files_processing` - директория для обработки/перевода файлов по умолчанию. в `/files_processing/in` помещаются файлы для обработки, в папке `/files_processing/out` создаются результаты обработки. - [Документация](doc/ru/processing_files.md). + [Документация](processing_files.md). +* integrations - данные по интеграции с другими приложениями. [Документация](integrations.md). * `/models` - папка по умолчанию для размещения моделей для перевода, таких как madlad-400 или nllb-200. - [Документация](doc/ru/translate_text.md). -* `/options` - папка с настройками сервиса и плагинов. [Документация](doc/ru/options.md). + [Документация](translate_text.md). +* `/options` - папка с настройками сервиса и плагинов. [Документация](options.md). * `/plugins` - папка с python-файлами плагинов, перевода и обработки файлов. * `/resources` - папка с файлами ресурсов проекта, такими, как файл конфигурация логов, файлы миграции базы данных кэша. * `/static` - папка с html, css, js файлами для веб-интерфейса. * `/test` - файлы unit-тестов для исходного кода. -* `compose.yaml` - файл с настройками для запуска Docker-контейнера. [Документация](doc/ru/install.md). -* `Dockerfile` - файл с настройками создания Docker-контейнера. [Документация](doc/ru/install.md). +* `compose.yaml` - файл с настройками для запуска Docker-контейнера. [Документация](install.md). +* `Dockerfile` - файл с настройками создания Docker-контейнера. [Документация](install.md). * `jaa.py` - библиотека управления плагинами. * `requirements.txt` - внешние зависимости проекта. diff --git a/integrations/luna-translator/readme.md b/integrations/luna-translator/readme.md new file mode 100644 index 0000000..3ed32d2 --- /dev/null +++ b/integrations/luna-translator/readme.md @@ -0,0 +1,45 @@ +# LunaTranslator + +## English + +Project site: https://docs.lunatranslator.org/en/ + +--- + +## Русский + +Сайт проекта: https://docs.lunatranslator.org/ru/ + +### Установка + +Для интеграции нужно: +* Открыть настройки `LunaTranslator`. +* Перейти в пункт `Перевод` +* Найти подраздел `Прочее`. +* Найти пункт `Пользовательский перевод`. +* Включить перевод. + +Далее можно или скопировать-вставить текст, или заменить файл, что более просто и исключит вероятность ошибок при вставке текста. + +**Вариант с заменой файла.** +* Перейти в папку `Путь_устанвоки_LunaTranslator\userconfig`. +* Скопировать файл из папки интеграции `selfbuild.py` в папку `Путь_устанвоки_LunaTranslator\userconfig`. + +**Вариант с копированием-вставкой текста** +* Нажать кнопку редактирования (кнопка с карандашом, после кнопки с кистью). +* Выбрать любой текстовый редактор для открытия файла (для windows можно использовать Блокнот). +* Скопировать содержимое файла `selfbuild.py` из папки интеграции в открытый ранее файл. +* Сохранить файл. + +### Параметры + +В начале файла можно увидеть параметры, под текстом `Configuration variables`. +* **llm_translate__translate_path** - url приложения llm-translate + `/translate`, по умолчанию `http://127.0.0.1:4990/translate`. +* **llm_translate__context** - дополнительный контекст для перевода. Может быть пустым. Нужно обратить внимание, что не все плагины для перевода llm-translate поддерживают контекст. +* **llm_translate__use_languages_from_luna_translate** - использовать параметры языков из LunaTranslator. +* **llm_translate__from_lang** - двухбуквенный код языка оригинала. +Если не выбран параметр `llm_translate__use_languages_from_luna_translate`, то будет использоваться значение из этого параметра. +Может быть пустым, тогда llm-translate сам подставит язык из своих параметров. +* **llm_translate__to_lang** - двухбуквенный код языка, на который нужно выполнить перевод. +Если не выбран параметр `llm_translate__use_languages_from_luna_translate`, то будет использоваться значение из этого параметра. +Может быть пустым, тогда llm-translate сам подставит язык из своих параметров. \ No newline at end of file diff --git a/integrations/luna-translator/selfbuild.py b/integrations/luna-translator/selfbuild.py new file mode 100644 index 0000000..8b57ad7 --- /dev/null +++ b/integrations/luna-translator/selfbuild.py @@ -0,0 +1,33 @@ +import requests +from translator.basetranslator import basetrans + +# ------------------------------------------------------------------------- +# Configuration variables +# ------------------------------------------------------------------------- +llm_translate__translate_path = "http://127.0.0.1:4990/translate" +llm_translate__context = "CONTEXT: This text (or word) for translation is the dialogue of characters in a computer game.\n" +llm_translate__use_languages_from_luna_translate = True +llm_translate__from_lang = "" +llm_translate__to_lang = "" +# ------------------------------------------------------------------------- + + +class TS(basetrans): + def translate(self, content: str): + req = { + "text": content, + "from_lang": self.srclang if llm_translate__use_languages_from_luna_translate else llm_translate__from_lang, + "to_lang": self.tgtlang if llm_translate__use_languages_from_luna_translate else llm_translate__to_lang, + "context": llm_translate__context, + } + try: + resp = requests.post(llm_translate__translate_path, json=req).json() + + if resp.get("result"): + return resp["result"] + elif resp.get("error"): + return resp["error"] + else: + return "Unknown error" + except Exception as e: + raise Exception("Unknown error, text:" + content + ", e: " + str(e)) diff --git a/integrations/readme.md b/integrations/readme.md new file mode 100644 index 0000000..96438da --- /dev/null +++ b/integrations/readme.md @@ -0,0 +1,17 @@ +# Integrations + +## English + +The folders contain instructions or scripts for integrating with external applications. + +A more detailed description, usage instructions, and available parameters are located in the folder for the specific integration. + +--- + +# Интеграции + +## Русский + +В папках находятся инструкции или скрипты для интеграции с внешними приложениями. + +Более подробное описание, способ использования, доступные параметры - находится в папке с конкретной интеграцией. diff --git a/integrations/renpy/_llm-translate-integration.rpy b/integrations/renpy/_llm-translate-integration.rpy new file mode 100644 index 0000000..401978e --- /dev/null +++ b/integrations/renpy/_llm-translate-integration.rpy @@ -0,0 +1,161 @@ +init 99 python: + import sys + + #-------------------------------------------------------------------------- + # Configuration variables + #-------------------------------------------------------------------------- + llm_translate__translate_path = "http://127.0.0.1:4990/translate" + llm_translate__preserve_original_text = True + llm_translate__translate_text_all = False # not recommended + llm_translate__translate_font_name = "DejaVuSans.ttf" + llm_translate__translate_font_size = 22 + llm_translate__translate_font_format_tag = "i" + llm_translate__replace_new_line_to_space = True + llm_translate__translate_text_delimiter = "\n" + llm_translate__from_lang = "" + llm_translate__to_lang = "" + llm_translate__context = "CONTEXT: This text for translation is the dialogue of characters in a computer game.\n" + #-------------------------------------------------------------------------- + + # module variables + llm_translate__translate_toggle_value = True # enable / disable translate + llm_translate__translated_text_dict = {} # cache of translated text + + + def llm_translate__fill_variables_values(src): + """ + Set values for variables in text. + :param src: text + :return: text with filled variables values + """ + s = src + s = renpy.substitutions.substitute(s, scope = None, force = True, translate = False) + if isinstance(s, (bytes, str)): + return s + return s[0] + + + # + def llm_translate__preprocess_text(src): + """ + Preprocess text - remove tags, fill variables values + :param src: text + :return: preprocessed text + """ + s = src + if llm_translate__replace_new_line_to_space: + s = s.replace("{p}", " ") + s = s.replace("\n", " ") + else: + s = s.replace("{p}", "\n") + + s = llm_translate__fill_variables_values(s) + s = renpy.translation.dialogue.notags_filter(s) #remove tags {} + + return s + + + def llm_translate__wrap_text_tag(text, tag, value = None): + """ + Formatting text with tags. + :param text: text + :param tag: tag + :param value: tag value (optional) + :return: text with tags + """ + if tag is None or tag == "": + return text + if value is None: + return "{" + tag + "}" + text + "{/" + tag + "}" + else: + return "{" + tag + "=" + str(value) + "}" + text + "{/" + tag + "}" + + # translate request with module requests - for python v3 + def llm_translate__request_python_v3(src, s): + """ + Request to translate text. + :param src: source text + :param s: preprocessed text + :return: translate result + """ + import requests + req = { + "text": s, + "llm_translate__from_lang": llm_translate__from_lang, + "llm_translate__to_lang": llm_translate__to_lang, + "context": llm_translate__context, + } + resp = requests.post(url=llm_translate__translate_path, json=req).json() + + if resp.get("result"): + translate = resp["result"].replace("\n", "{p}") + translate_with_tags = translate + translate_with_tags = llm_translate__wrap_text_tag(translate_with_tags, llm_translate__translate_font_format_tag) + translate_with_tags = llm_translate__wrap_text_tag(translate_with_tags, "font", llm_translate__translate_font_name) + translate_with_tags = llm_translate__wrap_text_tag(translate_with_tags, "size", llm_translate__translate_font_size) + + if llm_translate__preserve_original_text: + result = src + llm_translate__translate_text_delimiter + translate_with_tags + else: + result = translate_with_tags + return result + else: + return resp["error"] + + # + def llm_translate__translate_text(src): + """ + Main function to translate + :param src: text + :return: translate / original text and translate + """ + s = src + # text is empty or translate disable - return + if s is None or s == "" or not llm_translate__translate_toggle_value: + return s + + # preprocess text and again validate to empty + s = llm_translate__preprocess_text(s) + if s is None or s == "": + return src + + dict_translated_value = llm_translate__translated_text_dict.get(s, None) + if dict_translated_value is None: + translate_result = llm_translate__request_python_v3(src, s) + + llm_translate__translated_text_dict[s] = translate_result + else: + return dict_translated_value + + # enable or disable translate + def llm_translate__toggle_translate(): + global llm_translate__translate_toggle_value + value = not llm_translate__translate_toggle_value + llm_translate__translate_toggle_value = value + if not value: #clear cache + llm_translate__translated_text_dict.clear() + + # apply replace text + if llm_translate__translate_text_all: + config.replace_text = llm_translate__translate_text + else: + config.say_menu_text_filter = llm_translate__translate_text + + + # translate request with module requests - for python v3 + def llm_translate__request_python_v2(src, s): + return src + + +# button to enable or disable translate +screen toggle_tr_button(): + hbox: + style_prefix "quick" + xalign 0.0 + yalign 0.0 + + textbutton _("Tr: " + str(llm_translate__translate_toggle_value)) action Function(llm_translate__toggle_translate) + +init python: + config.overlay_screens.append("toggle_tr_button") + diff --git a/integrations/renpy/readme.md b/integrations/renpy/readme.md new file mode 100644 index 0000000..805c0ed --- /dev/null +++ b/integrations/renpy/readme.md @@ -0,0 +1,49 @@ +# Ren'py + +## English + +[Ren'Py](https://www.renpy.org/) - is a game engine for creating games, typically text-based novels. + +## Русский + +[Ren'Py](https://www.renpy.org/) - игровой движок для создания игр, как правило - текстовых новелл. + +Поддерживаются игры на Python 3, то есть с [версией Ren'Py 8.x.y](https://www.renpy.org/doc/html/implementation/two_and_three.html). + +**Внимание. Эта интеграция может привести к некорректной работе игры.** + +Некоторые игры могут основывать логику работы скрипта игры на тексте, который плагин переводит. +Исправить это не представляется возможным, делать специальную обработку под каждую игру слишком сложно. + +В таком случае можно или временно выключить плагин и включить его обратно, или использовать другие варианты, например [LunaTranslator](../luna-translator/readme.md). + +### Установка + +Необходимо добавить файл `_llm-translate-integration.rpy` в папку с игрой по следующему пути: +`Путь_устанвоки_игры\game`. + +Если установка прошла успешно, то в игре, в диалогах (не в главном меню), в верхнем левом углу появится кнопка Tr: True. +Нажатием на эту кнопку можно включить или выключить перевод. + +### Параметры + +В начале файла можно увидеть параметры, под текстом `Configuration variables`. + +* **llm_translate__translate_path** - url приложения llm-translate + `/translate`, по умолчанию `http://127.0.0.1:4990/translate`. +* **llm_translate__preserve_original_text** - сохранять оригинальный текст. Рекомендуется, потому что при работе интеграция удаляет форматирование текста - цвет, начертание и т.д. +* **llm_translate__translate_text_all** - переводить весь текст или только текст в диалогах. Перевод всего текста не рекомендуется, так как больше вероятности, что это может сломать игру. +* **llm_translate__translate_font_name** - шрифт перевода. Шрифт должен поддерживать символы языка, на который происходит перевод. +Шрифт должен находиться в папке `fonts` `Путь_устанвоки_игры\game\fonts` или быть установлен в системе (более надежный и простой вариант). +* **llm_translate__translate_font_size** - размер шрифта перевода. +* **llm_translate__translate_font_format_tag** - тэг текста перевода. По умолчанию `i` - курсив. +* **llm_translate__replace_new_line_to_space** = Заменять символ новой строки пробелом. Может быть полезно, если текста очень много и текст с переводом не помещается в окно вывода. +* **llm_translate__translate_text_delimiter** - символ-разделитель между исходным и переведенным текстом (если включен параметр `llm_translate__preserve_original_text`). +* **llm_translate__from_lang** - двухбуквенный код языка оригинала. + Если не выбран параметр `llm_translate__use_languages_from_luna_translate`, то будет использоваться значение из этого параметра. + Может быть пустым, тогда llm-translate сам подставит язык из своих параметров. +* **llm_translate__to_lang** - двухбуквенный код языка, на который нужно выполнить перевод. + Если не выбран параметр `llm_translate__use_languages_from_luna_translate`, то будет использоваться значение из этого параметра. + Может быть пустым, тогда llm-translate сам подставит язык из своих параметров. +* **llm_translate__context** - дополнительный контекст для перевода. Может быть пустым. Нужно обратить внимание, что не все плагины для перевода llm-translate поддерживают контекст. + + diff --git a/integrations/unity/readme.md b/integrations/unity/readme.md new file mode 100644 index 0000000..6bb7332 --- /dev/null +++ b/integrations/unity/readme.md @@ -0,0 +1,50 @@ +# xunity-autotranslator + +## English + +Project site: https://github.com/bbepis/XUnity.AutoTranslator + +## Русский + +Сайт проекта: https://github.com/bbepis/XUnity.AutoTranslator + +### Установка + +Установка на примере [BepInEx](https://github.com/bbepis/BepInEx). + +* Необходимо загрузить `xunity-autotranslator` и `BepInEx`, поместить их в папку с игрой, согласно инструкциям этих проектов. +* Запустить игру и закрыть. +* Если плагины для перевода были установлены правильно, то будет создан файл по пути +`папка_с_игрой/BepInEx/config/AutoTranslatorConfig.ini` +* Необходимо открыть этот файл `AutoTranslatorConfig.ini` в любом текстовом редакторе (для windows можно использовать Блокнот). +* Найти следующие параметры и задать им следующие значения: + +Для включения интеграции с llm-translate: +``` +[Service] +Endpoint=CustomTranslate +``` + +Для выбора языков - двухбуквенный код языка оригинала и языка, на который нужно выполнить перевод. +``` +[General] +Language=ru +FromLanguage=en +``` + +Для настройки url приложения llm-translate. +``` +[Custom] +Url=http://127.0.0.1:4990/translate/xunity-autotranslator +EnableShortDelay=False +DisableSpamChecks=True +``` + +В запросе можно передать контекст дял перевода, тогда параметр Url будет выглядеть примерно так: +``` +Url=http://127.0.0.1:4990/translate/xunity-autotranslator?context=Context - This text (or word) for translation is the dialogue of characters in a computer game. +``` + + + + diff --git a/main.py b/main.py index 0157167..70432a5 100644 --- a/main.py +++ b/main.py @@ -2,9 +2,9 @@ import sys from contextlib import asynccontextmanager import uvicorn -from fastapi import FastAPI, Request, status +from fastapi import FastAPI, Request, status, Query from fastapi.exceptions import RequestValidationError -from starlette.responses import JSONResponse +from starlette.responses import JSONResponse, Response from starlette.staticfiles import StaticFiles from app import dto, log, cuda @@ -169,6 +169,35 @@ async def translate_sugoi_like_post(text: str, from_lang: str = "", to_lang: str return dto.SugoiLikeGetResp(trans=translate.result) +@app.get("/translate/xunity-autotranslator") +async def translate_sugoi_like_post( text: str, from_lang: str = Query("", alias="from"), to: str = "", + context: str = None, translator_plugin: str = "") -> Response: + """ + Translate text. Request and response for xunity-autotranslator - https://github.com/bbepis/XUnity.AutoTranslator + + :param str text: text to translate. + + :param str from_lang (from in Request Param - from is reserved word in python): from language (2 symbols, like "en"). + May be empty (will be replaced to "default_from_lang" from options) + + :param str to: to language (2 symbols, like "en"). + May be empty (will be replaced to "default_to_lang" from options) + + :param str context: additional context to translate (if model has context support) + + :param str translator_plugin: plugin to use. If blank, default will be used. + If not initialized (not in "default_translate_plugin" and not in "init_on_start" from options - throw error) + + :return: translated text string + """ + + request = dto.TranslateCommonRequest(text=text, context=context, from_lang=from_lang, to_lang=to, + translator_plugin=translator_plugin) + translate = core.translate(request) + + return Response(content=translate.result, media_type="text/plain") + + @app.get("/process-files-list") async def process_files_list(recursive_sub_dirs: bool) -> dto.ProcessingFileDirListResp: """ diff --git a/plugins/plugin_file_media_nemo.py b/plugins/plugin_file_media_nemo.py index 32bea58..0eb80ed 100644 --- a/plugins/plugin_file_media_nemo.py +++ b/plugins/plugin_file_media_nemo.py @@ -1,16 +1,16 @@ import datetime import gc -import inspect import os import torch from nemo.collections.asr.models import ASRModel, EncDecRNNTModel from pydub import AudioSegment -from app import file_processor, cuda +from app import file_processor, cuda, log from app.app_core import AppCore from app.dto import ProcessingFileDirReq, ProcessingFileResp, FileProcessingPluginInitInfo, ProcessingFileStruct +logger = log.logger() plugin_name = os.path.basename(__file__)[:-3] # calculating modname model: EncDecRNNTModel | None = None @@ -98,14 +98,19 @@ def file_processing(core: AppCore, file_struct: ProcessingFileStruct, req: Proce audio.export(resampled_audio_file, format="wav", parameters=["-ar", "16000", "-ac", "1"]) try: - # need to get function params - canary model supports "source_lang", parapet model - doesn't - param_names = [name for name, _ in inspect.signature(model.transcribe).parameters.items()] - if "source_lang" in param_names and "target_lang" in param_names: + + try: # canary model supports "source_lang", parapet model - doesn't transcribe = model.transcribe(audio=[resampled_audio_file], source_lang=req.from_lang, target_lang=req.from_lang, timestamps=True, batch_size=options["batch_size"]) - else: - transcribe = model.transcribe(audio=[resampled_audio_file], - timestamps=True, batch_size=options["batch_size"]) + except Exception as possible_param_exc: + if "argument 'source_lang'" in str(possible_param_exc) or "argument 'target_lang'" in str(possible_param_exc): + logger.info("It seems the model " + options["model"] + " does not support source_lang / target_lang params. Result text wiil be in english. Try to repeat request without params.") + transcribe = model.transcribe(audio=[resampled_audio_file], + timestamps=True, batch_size=options["batch_size"]) + else: + log.log_exception("Error transcribe.", possible_param_exc) + transcribe = None + if not transcribe or not isinstance(transcribe, list) or not transcribe[0] or not hasattr(transcribe[0], 'timestamp') or not transcribe[0].timestamp or 'segment' not in transcribe[0].timestamp: return file_processor.get_processing_file_resp_error( file_in=file_struct.file_name_ext, path_in=file_struct.path_in, error_msg="Can't get transcribe") @@ -117,7 +122,7 @@ def file_processing(core: AppCore, file_struct: ProcessingFileStruct, req: Proce out_file_name = processed_file_name(core=core, file_struct=file_struct, req=req) with open(file_struct.path_file_out(out_file_name), "w") as f: f.write(srt_content) - if options["translate_after_processing"]: + if options["translate_after_processing"] and req.from_lang != req.to_lang: return translate_after_processing(core=core, req=req, file_name_ext=out_file_name) else: return file_processor.get_processing_file_resp_ok(file_struct=file_struct, file_out=out_file_name) diff --git a/plugins/plugin_file_media_whisper.py b/plugins/plugin_file_media_whisper.py index bb8e286..da107b2 100644 --- a/plugins/plugin_file_media_whisper.py +++ b/plugins/plugin_file_media_whisper.py @@ -97,7 +97,7 @@ def file_processing(core: AppCore, file_struct: ProcessingFileStruct, req: Proce writer = utils.get_writer('srt', file_struct.path_out) writer(transcribe, out_file_name, {}) - if options["translate_after_processing"]: + if options["translate_after_processing"] and req.from_lang != req.to_lang: return translate_after_processing(core=core, req=req, file_name_ext=out_file_name) else: return file_processor.get_processing_file_resp_ok(file_struct=file_struct, file_out=out_file_name) diff --git a/readme.md b/readme.md index b71173c..27728dd 100644 --- a/readme.md +++ b/readme.md @@ -17,6 +17,8 @@ The MIT License Поддерживается как перевод текста через веб-интерфейс или запросы к API, так и перевод файлов. +Поддерживаются интеграции с другими приложениями, такими, как [LunaTranslator](https://docs.lunatranslator.org/en/), игры на движке renpy. + Более подробно - в [документации](doc/ru/readme.md). Распространяется по MIT лицензии.