mirror of
https://github.com/illian64/llm-translate.git
synced 2026-07-09 17:28:38 +00:00
init
This commit is contained in:
commit
8557624008
29 changed files with 2560 additions and 0 deletions
11
.gitignore
vendored
Normal file
11
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
.idea
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
|
||||
cache.db
|
||||
|
||||
/.idea/
|
||||
/options/
|
||||
/models/
|
||||
venv
|
||||
73
app.py
Normal file
73
app.py
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
from contextlib import asynccontextmanager
|
||||
from fastapi import FastAPI
|
||||
import uvicorn
|
||||
import logging
|
||||
|
||||
from starlette.staticfiles import StaticFiles
|
||||
|
||||
from app.app_core import AppCore
|
||||
from app.cuda import cuda_info
|
||||
from app.dto import TranslateReq
|
||||
from app.struct import Request
|
||||
from app.properties import Properties
|
||||
|
||||
|
||||
core: AppCore
|
||||
logger = logging.getLogger('uvicorn')
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(fast_api: FastAPI):
|
||||
cuda_info()
|
||||
logger.info("Starting llm-translate")
|
||||
global core
|
||||
core = AppCore()
|
||||
core.init_with_plugins()
|
||||
|
||||
yield
|
||||
logger.info("Stopping llm-translate")
|
||||
|
||||
|
||||
app = FastAPI(lifespan=lifespan)
|
||||
properties = Properties()
|
||||
|
||||
|
||||
@app.get("/translate")
|
||||
async def translate_get(text: str, from_lang: str = "", to_lang: str = "", translator_plugin: str = ""):
|
||||
"""
|
||||
Return translation
|
||||
|
||||
:param str text: text to translate
|
||||
|
||||
:param str from_lang: from language (2 symbols, like "en").
|
||||
May be empty (will be replaced to "default_from_lang" from options)
|
||||
|
||||
:param str to_lang: to language (2 symbols, like "en").
|
||||
May be empty (will be replaced to "default_to_lang" from options)
|
||||
|
||||
:param str translator_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)
|
||||
|
||||
:param str api_key: api key for access (if service setup in security mode with api keys)
|
||||
|
||||
:return: dict (result: text)
|
||||
"""
|
||||
|
||||
request = Request(text, from_lang, to_lang, translator_plugin)
|
||||
|
||||
return core.translate(request)
|
||||
|
||||
|
||||
@app.post("/translate")
|
||||
async def translate_post(req: TranslateReq):
|
||||
request = Request(req.text, req.from_lang, req.to_lang, req.translator_plugin)
|
||||
return core.translate(request)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
log_config = uvicorn.config.LOGGING_CONFIG
|
||||
log_config["formatters"]["access"]["fmt"] = "%(asctime)s %(levelname)s %(message)s"
|
||||
log_config["formatters"]["default"]["fmt"] = "%(asctime)s %(levelname)s %(message)s"
|
||||
|
||||
app.mount('/', StaticFiles(directory='static', html=True), name='static')
|
||||
uvicorn.run(app, host="127.0.0.1", port=properties.port, log_level="info", log_config=log_config, use_colors=False)
|
||||
0
app/__init__.py
Normal file
0
app/__init__.py
Normal file
154
app/app_core.py
Normal file
154
app/app_core.py
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
import logging
|
||||
import traceback
|
||||
|
||||
from app import text_splitter
|
||||
from app.cache import Cache
|
||||
from app.dto import TranslateResp
|
||||
from app.struct import TranslateStruct, TranslationParams, TextSplitParams, TextProcessParams, Request, Part, \
|
||||
CacheParams
|
||||
from app.text_processor import pre_process
|
||||
from jaa import JaaCore
|
||||
|
||||
logger = logging.getLogger('uvicorn')
|
||||
version = "0.1.0"
|
||||
|
||||
|
||||
class AppCore(JaaCore):
|
||||
def __init__(self):
|
||||
JaaCore.__init__(self)
|
||||
|
||||
self.default_translate_plugin = ""
|
||||
self.init_on_start = ""
|
||||
|
||||
self.translation_params = TranslationParams("", "")
|
||||
self.text_split_params: TextSplitParams = None
|
||||
self.text_process_params: TextProcessParams = None
|
||||
self.cache_params: CacheParams = None
|
||||
|
||||
self.translators: dict = {}
|
||||
self.initialized_translator_engines = dict()
|
||||
self.cache: Cache = None
|
||||
|
||||
def process_plugin_manifest(self, modname, manifest):
|
||||
if "translate" in manifest: # process commands
|
||||
for cmd in manifest["translate"].keys():
|
||||
self.translators[cmd] = manifest["translate"][cmd]
|
||||
|
||||
return manifest
|
||||
|
||||
def init_with_plugins(self):
|
||||
self.init_plugins(["core"])
|
||||
self.cache = Cache(self.cache_params)
|
||||
|
||||
logger.info("Default translator: %s", self.default_translate_plugin)
|
||||
|
||||
self.init_translator_engine(self.default_translate_plugin)
|
||||
|
||||
init_on_start_list = self.init_on_start.replace(" ", "").split(",")
|
||||
for translator in init_on_start_list:
|
||||
if translator != "":
|
||||
self.init_translator_engine(translator)
|
||||
|
||||
logger.info("Found translation engines: %s", ", ".join(str(key) for key in self.translators.keys()))
|
||||
|
||||
def init_translator_engine(self, translator_engine: str):
|
||||
if translator_engine in self.initialized_translator_engines:
|
||||
# already inited
|
||||
return
|
||||
|
||||
try:
|
||||
logger.info("Try to init translation plugin '%s'...", translator_engine)
|
||||
modname = self.translators[translator_engine][0](self)
|
||||
self.initialized_translator_engines[translator_engine] = modname
|
||||
logger.info("Success init translation plugin: '%s'.", translator_engine)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error init translation plugin '%s'...", translator_engine, e)
|
||||
|
||||
def get_plugin_options(self, translator_engine: str):
|
||||
modname = self.initialized_translator_engines[translator_engine]
|
||||
return self.plugin_options(modname)
|
||||
|
||||
def get_translation_params(self, translator_engine: str):
|
||||
options = self.get_plugin_options(translator_engine)
|
||||
if options['translation_params_struct']:
|
||||
return options['translation_params_struct']
|
||||
else:
|
||||
return self.translation_params
|
||||
|
||||
def get_text_split_params(self, translator_engine: str):
|
||||
options = self.get_plugin_options(translator_engine)
|
||||
if options['text_split_params_struct']:
|
||||
return options['text_split_params_struct']
|
||||
else:
|
||||
return self.text_split_params
|
||||
|
||||
def get_text_process_params(self, translator_engine: str):
|
||||
options = self.get_plugin_options(translator_engine)
|
||||
if options['text_process_params_struct']:
|
||||
return options['text_process_params_struct']
|
||||
else:
|
||||
return self.text_process_params
|
||||
|
||||
def translate(self, req: Request):
|
||||
if req.text == '':
|
||||
return TranslateResp(result='', parts=[], error=None)
|
||||
|
||||
try:
|
||||
if not req.translator_plugin or req.translator_plugin == "":
|
||||
req.translator_plugin = self.default_translate_plugin
|
||||
|
||||
if req.translator_plugin not in self.initialized_translator_engines:
|
||||
raise ValueError("This translate_plugin not in initialized: " + req.translator_plugin)
|
||||
|
||||
if req.from_lang == "":
|
||||
req.from_lang = self.get_translation_params(req.translator_plugin).default_from_lang
|
||||
|
||||
if req.to_lang == "":
|
||||
req.to_lang = self.get_translation_params(req.translator_plugin).default_to_lang
|
||||
|
||||
processed_text: str
|
||||
if self.get_text_process_params(req.translator_plugin).apply_for_request:
|
||||
processed_text: str = pre_process(self.get_text_process_params(req.translator_plugin), req.text)
|
||||
else:
|
||||
processed_text = req.text
|
||||
|
||||
text_parts: list[Part] = text_splitter.split_text(processed_text,
|
||||
self.get_text_split_params(req.translator_plugin),
|
||||
req.from_lang)
|
||||
self.cache_read(req, text_parts)
|
||||
|
||||
translate_struct = TranslateStruct(req=req, processed_text=processed_text, parts=text_parts)
|
||||
|
||||
translate_struct: TranslateStruct = self.translators[req.translator_plugin][1](self, translate_struct)
|
||||
self.cache_write(req, translate_struct.parts)
|
||||
|
||||
(translate_text, translate_parts) = text_splitter.join_text(translate_struct.parts)
|
||||
|
||||
if self.text_process_params.apply_for_response:
|
||||
translate_text: str = pre_process(self.text_process_params, translate_text)
|
||||
else:
|
||||
translate_text = req.text
|
||||
|
||||
return TranslateResp(result=translate_text, parts=translate_parts, error=None)
|
||||
except ValueError as ve:
|
||||
return TranslateResp(result=None, parts=None, error=ve.args[0])
|
||||
except Exception as e:
|
||||
traceback.print_tb(e.__traceback__, limit=10)
|
||||
return TranslateResp(result=None, parts=None, error=getattr(e, 'message', repr(e)))
|
||||
|
||||
def cache_read(self, req: Request, parts: list[Part]):
|
||||
if self.cache_params.enabled and req.translator_plugin not in self.cache_params.disable_for_plugins:
|
||||
for part in parts:
|
||||
cached_translate = self.cache.get(req, part.text)
|
||||
if cached_translate:
|
||||
part.cache_found = True
|
||||
part.translate = cached_translate
|
||||
else:
|
||||
part.cache_found = False
|
||||
|
||||
def cache_write(self, req: Request, parts: list[Part]):
|
||||
if self.cache_params.enabled and req.translator_plugin not in self.cache_params.disable_for_plugins:
|
||||
for part in parts:
|
||||
if not part.cache_found:
|
||||
self.cache.put(req, part.text, part.translate)
|
||||
75
app/cache.py
Normal file
75
app/cache.py
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import logging
|
||||
import sqlite3
|
||||
|
||||
from app.struct import CacheParams, Request
|
||||
|
||||
logger = logging.getLogger('uvicorn')
|
||||
|
||||
|
||||
class Cache:
|
||||
cache_table_name = "cache_translate"
|
||||
params: CacheParams
|
||||
connection: sqlite3.Connection
|
||||
|
||||
def __init__(self, params: CacheParams):
|
||||
self.params = params
|
||||
self.connection = self.get_connection()
|
||||
self.init()
|
||||
|
||||
def get_connection(self):
|
||||
return sqlite3.connect(self.params.file)
|
||||
|
||||
def init(self):
|
||||
if not self.params.enabled:
|
||||
return None
|
||||
|
||||
cursor = self.connection.cursor()
|
||||
cursor.execute(
|
||||
"SELECT name FROM sqlite_master WHERE type='table' AND name='{0}'".format(self.cache_table_name))
|
||||
table_exists = cursor.fetchall()
|
||||
|
||||
if len(table_exists) == 0:
|
||||
logger.info("Init cache table: %s, file db: %s", self.cache_table_name, self.params.file)
|
||||
create_table = """
|
||||
CREATE TABLE IF NOT EXISTS {0}
|
||||
(key TEXT NOT NULL, created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
from_lang TEXT NOT NULL, to_lang TEXT NOT NULL, plugin TEXT NOT NULL, value TEXT NOT NULL)
|
||||
""".format(self.cache_table_name)
|
||||
create_idx_translate_cols = ('CREATE UNIQUE INDEX IF NOT EXISTS idx_translate_cols '
|
||||
'ON {0} (key, from_lang, to_lang, plugin)').format(self.cache_table_name)
|
||||
create_idx_created = ('CREATE INDEX IF NOT EXISTS idx_created '
|
||||
'ON {0} (created)').format(self.cache_table_name)
|
||||
|
||||
with cursor:
|
||||
cursor.execute(create_table)
|
||||
cursor.execute(create_idx_translate_cols)
|
||||
cursor.execute(create_idx_created)
|
||||
else:
|
||||
if (self.params.expire_days > 0):
|
||||
delete_expired_values = "DELETE FROM {0} WHERE created < date('now', '-{1} day')".format(
|
||||
self.cache_table_name, self.params.expire_days)
|
||||
cursor.execute(delete_expired_values)
|
||||
|
||||
self.connection.commit()
|
||||
|
||||
def get(self, req: Request, text: str):
|
||||
select = "SELECT value FROM {0} WHERE key = ? AND from_lang = ? AND to_lang = ? AND plugin = ?".format(
|
||||
self.cache_table_name)
|
||||
cursor = self.connection.cursor()
|
||||
cursor.execute(select, (text, req.from_lang, req.to_lang, req.translator_plugin))
|
||||
value = cursor.fetchone()
|
||||
if value:
|
||||
return value[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
def put(self, req: Request, text: str, value: str):
|
||||
try:
|
||||
insert_connection = self.get_connection()
|
||||
cursor = insert_connection.cursor()
|
||||
cursor.execute('INSERT INTO {0} (KEY, from_lang, to_lang, plugin, VALUE) VALUES (?, ?, ?, ?, ?)'.format(
|
||||
self.cache_table_name),(text, req.from_lang, req.to_lang, req.translator_plugin, value))
|
||||
insert_connection.commit()
|
||||
insert_connection.close()
|
||||
except Exception as e:
|
||||
logger.error("Error save cache entry, text = %s, req = %s, error=%s", text, req, e)
|
||||
32
app/cuda.py
Normal file
32
app/cuda.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import logging
|
||||
|
||||
import torch
|
||||
|
||||
logger = logging.getLogger('uvicorn')
|
||||
|
||||
|
||||
def cuda_info():
|
||||
cuda_is_available = torch.cuda.is_available()
|
||||
device_count = torch.cuda.device_count()
|
||||
current_device = torch.cuda.current_device()
|
||||
|
||||
logger.info("CUDA info: is available={0}, device count={1}, current_device={2}. Devices:"
|
||||
.format(cuda_is_available, device_count, current_device))
|
||||
|
||||
for i in range(device_count):
|
||||
logger.info("GPU #%d: %s", i, torch.cuda.get_device_name(i))
|
||||
|
||||
|
||||
def get_device(options: dict):
|
||||
cuda_opt = options["cuda"]
|
||||
if cuda_opt:
|
||||
return "cuda"
|
||||
else:
|
||||
return "cpu"
|
||||
|
||||
def get_device_with_gpu_num(options: dict):
|
||||
cuda_opt = options["cuda"]
|
||||
if cuda_opt:
|
||||
return "cuda:{0}".format(options["cuda_device_index"])
|
||||
else:
|
||||
return "cpu"
|
||||
25
app/dto.py
Normal file
25
app/dto.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
from dataclasses import dataclass
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class TranslateReq(BaseModel):
|
||||
text: str
|
||||
from_lang: str | None = ""
|
||||
to_lang: str | None = ""
|
||||
translator_plugin: str | None = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class TranslatePart:
|
||||
text: str
|
||||
translate: str
|
||||
paragraph_end: bool
|
||||
|
||||
|
||||
@dataclass
|
||||
class TranslateResp:
|
||||
result: str | None
|
||||
parts: list[TranslatePart] | None
|
||||
error: str | None
|
||||
|
||||
368
app/lang_dict.py
Normal file
368
app/lang_dict.py
Normal file
|
|
@ -0,0 +1,368 @@
|
|||
lang_2_chars_to_name: dict = {
|
||||
'ab': 'abkhazian',
|
||||
'aa': 'afar',
|
||||
'af': 'afrikaans',
|
||||
'sq': 'albanian',
|
||||
'am': 'amharic',
|
||||
'ar': 'arabic',
|
||||
'hy': 'armenian',
|
||||
'as': 'assamese',
|
||||
'ay': 'aymara',
|
||||
'az': 'azerbaijani',
|
||||
'ba': 'bashkir',
|
||||
'eu': 'basque',
|
||||
'bn': 'bengali',
|
||||
'dz': 'bhutani',
|
||||
'bh': 'bihari',
|
||||
'bi': 'bislama',
|
||||
'br': 'breton',
|
||||
'bg': 'bulgarian',
|
||||
'my': 'burmese',
|
||||
'be': 'byelorussian',
|
||||
'km': 'cambodian',
|
||||
'ca': 'catalan',
|
||||
'zh': 'chinese',
|
||||
'co': 'corsican',
|
||||
'hr': 'croatian',
|
||||
'cs': 'czech',
|
||||
'da': 'danish',
|
||||
'nl': 'dutch',
|
||||
'en': 'english',
|
||||
'eo': 'esperanto',
|
||||
'et': 'estonian',
|
||||
'fo': 'faeroese',
|
||||
'fj': 'fiji',
|
||||
'fi': 'finnish',
|
||||
'fr': 'french',
|
||||
'fy': 'frisian',
|
||||
'gd': 'gaelic',
|
||||
'gl': 'galician',
|
||||
'ka': 'georgian',
|
||||
'de': 'german',
|
||||
'el': 'greek',
|
||||
'kl': 'greenlandic',
|
||||
'gn': 'guarani',
|
||||
'gu': 'gujarati',
|
||||
'ha': 'hausa',
|
||||
'iw': 'hebrew',
|
||||
'hi': 'hindi',
|
||||
'hu': 'hungarian',
|
||||
'is': 'icelandic',
|
||||
'in': 'indonesian',
|
||||
'ia': 'interlingua',
|
||||
'ie': 'interlingue',
|
||||
'ik': 'inupiak',
|
||||
'ga': 'irish',
|
||||
'it': 'italian',
|
||||
'ja': 'japanese',
|
||||
'jw': 'javanese',
|
||||
'kn': 'kannada',
|
||||
'ks': 'kashmiri',
|
||||
'kk': 'kazakh',
|
||||
'rw': 'kinyarwanda',
|
||||
'ky': 'kirghiz',
|
||||
'rn': 'kirundi',
|
||||
'ko': 'korean',
|
||||
'ku': 'kurdish',
|
||||
'lo': 'laothian',
|
||||
'la': 'latin',
|
||||
'lv': 'latvian',
|
||||
'ln': 'lingala',
|
||||
'lt': 'lithuanian',
|
||||
'mk': 'macedonian',
|
||||
'mg': 'malagasy',
|
||||
'ms': 'malay',
|
||||
'ml': 'malayalam',
|
||||
'mt': 'maltese',
|
||||
'mi': 'maori',
|
||||
'mr': 'marathi',
|
||||
'mo': 'moldavian',
|
||||
'mn': 'mongolian',
|
||||
'na': 'nauru',
|
||||
'ne': 'nepali',
|
||||
'no': 'norwegian',
|
||||
'oc': 'occitan',
|
||||
'or': 'oriya',
|
||||
'om': 'oromo',
|
||||
'ps': 'pashto',
|
||||
'fa': 'persian',
|
||||
'pl': 'polish',
|
||||
'pt': 'portuguese',
|
||||
'pa': 'punjabi',
|
||||
'qu': 'quechua',
|
||||
'rm': 'rhaeto-romance',
|
||||
'ro': 'romanian',
|
||||
'ru': 'russian',
|
||||
'sm': 'samoan',
|
||||
'sg': 'sangro',
|
||||
'sa': 'sanskrit',
|
||||
'sr': 'serbian',
|
||||
'sh': 'serbo-croatian',
|
||||
'st': 'sesotho',
|
||||
'tn': 'setswana',
|
||||
'sn': 'shona',
|
||||
'sd': 'sindhi',
|
||||
'si': 'singhalese',
|
||||
'ss': 'siswati',
|
||||
'sk': 'slovak',
|
||||
'sl': 'slovenian',
|
||||
'so': 'somali',
|
||||
'es': 'spanish',
|
||||
'su': 'sudanese',
|
||||
'sw': 'swahili',
|
||||
'sv': 'swedish',
|
||||
'tl': 'tagalog',
|
||||
'tg': 'tajik',
|
||||
'ta': 'tamil',
|
||||
'tt': 'tatar',
|
||||
'te': 'tegulu',
|
||||
'th': 'thai',
|
||||
'bo': 'tibetan',
|
||||
'ti': 'tigrinya',
|
||||
'to': 'tonga',
|
||||
'ts': 'tsonga',
|
||||
'tr': 'turkish',
|
||||
'tk': 'turkmen',
|
||||
'tw': 'twi',
|
||||
'uk': 'ukrainian',
|
||||
'ur': 'urdu',
|
||||
'uz': 'uzbek',
|
||||
'vi': 'vietnamese',
|
||||
'vo': 'volapuk',
|
||||
'cy': 'welsh',
|
||||
'wo': 'wolof',
|
||||
'xh': 'xhosa',
|
||||
'ji': 'yiddish',
|
||||
'yo': 'yoruba',
|
||||
'zu': 'zulu',
|
||||
}
|
||||
|
||||
|
||||
def get_lang_by_2_chars_code(code: str):
|
||||
result = lang_2_chars_to_name.get(code)
|
||||
if result:
|
||||
return result
|
||||
else:
|
||||
return code
|
||||
|
||||
|
||||
nllb_200_langlist_str = """
|
||||
ace_Arab | Acehnese (Arabic script)
|
||||
ace_Latn | Acehnese (Latin script)
|
||||
acm_Arab | Mesopotamian Arabic
|
||||
acq_Arab | Ta’izzi-Adeni Arabic
|
||||
aeb_Arab | Tunisian Arabic
|
||||
afr_Latn | Afrikaans
|
||||
ajp_Arab | South Levantine Arabic
|
||||
aka_Latn | Akan
|
||||
als_Latn | Tosk Albanian
|
||||
amh_Ethi | Amharic
|
||||
apc_Arab | North Levantine Arabic
|
||||
arb_Arab | Modern Standard Arabic
|
||||
arb_Latn | Modern Standard Arabic (Romanized)
|
||||
ars_Arab | Najdi Arabic
|
||||
ary_Arab | Moroccan Arabic
|
||||
arz_Arab | Egyptian Arabic
|
||||
asm_Beng | Assamese
|
||||
ast_Latn | Asturian
|
||||
awa_Deva | Awadhi
|
||||
ayr_Latn | Central Aymara
|
||||
azb_Arab | South Azerbaijani
|
||||
azj_Latn | North Azerbaijani
|
||||
bak_Cyrl | Bashkir
|
||||
bam_Latn | Bambara
|
||||
ban_Latn | Balinese
|
||||
bel_Cyrl | Belarusian
|
||||
bem_Latn | Bemba
|
||||
ben_Beng | Bengali
|
||||
bho_Deva | Bhojpuri
|
||||
bjn_Arab | Banjar (Arabic script)
|
||||
bjn_Latn | Banjar (Latin script)
|
||||
bod_Tibt | Standard Tibetan
|
||||
bos_Latn | Bosnian
|
||||
bug_Latn | Buginese
|
||||
bul_Cyrl | Bulgarian
|
||||
cat_Latn | Catalan
|
||||
ceb_Latn | Cebuano
|
||||
ces_Latn | Czech
|
||||
cjk_Latn | Chokwe
|
||||
ckb_Arab | Central Kurdish
|
||||
crh_Latn | Crimean Tatar
|
||||
cym_Latn | Welsh
|
||||
dan_Latn | Danish
|
||||
deu_Latn | German
|
||||
dik_Latn | Southwestern Dinka
|
||||
dyu_Latn | Dyula
|
||||
dzo_Tibt | Dzongkha
|
||||
ell_Grek | Greek
|
||||
eng_Latn | English
|
||||
epo_Latn | Esperanto
|
||||
est_Latn | Estonian
|
||||
eus_Latn | Basque
|
||||
ewe_Latn | Ewe
|
||||
fao_Latn | Faroese
|
||||
fij_Latn | Fijian
|
||||
fin_Latn | Finnish
|
||||
fon_Latn | Fon
|
||||
fra_Latn | French
|
||||
fur_Latn | Friulian
|
||||
fuv_Latn | Nigerian Fulfulde
|
||||
gaz_Latn | West Central Oromo
|
||||
gla_Latn | Scottish Gaelic
|
||||
gle_Latn | Irish
|
||||
glg_Latn | Galician
|
||||
grn_Latn | Guarani
|
||||
guj_Gujr | Gujarati
|
||||
hat_Latn | Haitian Creole
|
||||
hau_Latn | Hausa
|
||||
heb_Hebr | Hebrew
|
||||
hin_Deva | Hindi
|
||||
hne_Deva | Chhattisgarhi
|
||||
hrv_Latn | Croatian
|
||||
hun_Latn | Hungarian
|
||||
hye_Armn | Armenian
|
||||
ibo_Latn | Igbo
|
||||
ilo_Latn | Ilocano
|
||||
ind_Latn | Indonesian
|
||||
isl_Latn | Icelandic
|
||||
ita_Latn | Italian
|
||||
jav_Latn | Javanese
|
||||
jpn_Jpan | Japanese
|
||||
kab_Latn | Kabyle
|
||||
kac_Latn | Jingpho
|
||||
kam_Latn | Kamba
|
||||
kan_Knda | Kannada
|
||||
kas_Arab | Kashmiri (Arabic script)
|
||||
kas_Deva | Kashmiri (Devanagari script)
|
||||
kat_Geor | Georgian
|
||||
kaz_Cyrl | Kazakh
|
||||
kbp_Latn | Kabiyè
|
||||
kea_Latn | Kabuverdianu
|
||||
khk_Cyrl | Halh Mongolian
|
||||
khm_Khmr | Khmer
|
||||
kik_Latn | Kikuyu
|
||||
kin_Latn | Kinyarwanda
|
||||
kir_Cyrl | Kyrgyz
|
||||
kmb_Latn | Kimbundu
|
||||
kmr_Latn | Northern Kurdish
|
||||
knc_Arab | Central Kanuri (Arabic script)
|
||||
knc_Latn | Central Kanuri (Latin script)
|
||||
kon_Latn | Kikongo
|
||||
kor_Hang | Korean
|
||||
lao_Laoo | Lao
|
||||
lij_Latn | Ligurian
|
||||
lim_Latn | Limburgish
|
||||
lin_Latn | Lingala
|
||||
lit_Latn | Lithuanian
|
||||
lmo_Latn | Lombard
|
||||
ltg_Latn | Latgalian
|
||||
ltz_Latn | Luxembourgish
|
||||
lua_Latn | Luba-Kasai
|
||||
lug_Latn | Ganda
|
||||
luo_Latn | Luo
|
||||
lus_Latn | Mizo
|
||||
lvs_Latn | Standard Latvian
|
||||
mag_Deva | Magahi
|
||||
mai_Deva | Maithili
|
||||
mal_Mlym | Malayalam
|
||||
mar_Deva | Marathi
|
||||
min_Arab | Minangkabau (Arabic script)
|
||||
min_Latn | Minangkabau (Latin script)
|
||||
mkd_Cyrl | Macedonian
|
||||
mlt_Latn | Maltese
|
||||
mni_Beng | Meitei (Bengali script)
|
||||
mos_Latn | Mossi
|
||||
mri_Latn | Maori
|
||||
mya_Mymr | Burmese
|
||||
nld_Latn | Dutch
|
||||
nno_Latn | Norwegian Nynorsk
|
||||
nob_Latn | Norwegian Bokmål
|
||||
npi_Deva | Nepali
|
||||
nso_Latn | Northern Sotho
|
||||
nus_Latn | Nuer
|
||||
nya_Latn | Nyanja
|
||||
oci_Latn | Occitan
|
||||
ory_Orya | Odia
|
||||
pag_Latn | Pangasinan
|
||||
pan_Guru | Eastern Panjabi
|
||||
pap_Latn | Papiamento
|
||||
pbt_Arab | Southern Pashto
|
||||
pes_Arab | Western Persian
|
||||
plt_Latn | Plateau Malagasy
|
||||
pol_Latn | Polish
|
||||
por_Latn | Portuguese
|
||||
prs_Arab | Dari
|
||||
quy_Latn | Ayacucho Quechua
|
||||
ron_Latn | Romanian
|
||||
run_Latn | Rundi
|
||||
rus_Cyrl | Russian
|
||||
sag_Latn | Sango
|
||||
san_Deva | Sanskrit
|
||||
sat_Olck | Santali
|
||||
scn_Latn | Sicilian
|
||||
shn_Mymr | Shan
|
||||
sin_Sinh | Sinhala
|
||||
slk_Latn | Slovak
|
||||
slv_Latn | Slovenian
|
||||
smo_Latn | Samoan
|
||||
sna_Latn | Shona
|
||||
snd_Arab | Sindhi
|
||||
som_Latn | Somali
|
||||
sot_Latn | Southern Sotho
|
||||
spa_Latn | Spanish
|
||||
srd_Latn | Sardinian
|
||||
srp_Cyrl | Serbian
|
||||
ssw_Latn | Swati
|
||||
sun_Latn | Sundanese
|
||||
swe_Latn | Swedish
|
||||
swh_Latn | Swahili
|
||||
szl_Latn | Silesian
|
||||
tam_Taml | Tamil
|
||||
taq_Latn | Tamasheq (Latin script)
|
||||
taq_Tfng | Tamasheq (Tifinagh script)
|
||||
tat_Cyrl | Tatar
|
||||
tel_Telu | Telugu
|
||||
tgk_Cyrl | Tajik
|
||||
tgl_Latn | Tagalog
|
||||
tha_Thai | Thai
|
||||
tir_Ethi | Tigrinya
|
||||
tpi_Latn | Tok Pisin
|
||||
tsn_Latn | Tswana
|
||||
tso_Latn | Tsonga
|
||||
tuk_Latn | Turkmen
|
||||
tum_Latn | Tumbuka
|
||||
tur_Latn | Turkish
|
||||
twi_Latn | Twi
|
||||
tzm_Tfng | Central Atlas Tamazight
|
||||
uig_Arab | Uyghur
|
||||
ukr_Cyrl | Ukrainian
|
||||
umb_Latn | Umbundu
|
||||
urd_Arab | Urdu
|
||||
uzn_Latn | Northern Uzbek
|
||||
vec_Latn | Venetian
|
||||
vie_Latn | Vietnamese
|
||||
war_Latn | Waray
|
||||
wol_Latn | Wolof
|
||||
xho_Latn | Xhosa
|
||||
ydd_Hebr | Eastern Yiddish
|
||||
yor_Latn | Yoruba
|
||||
yue_Hant | Yue Chinese
|
||||
zho_Hans | Chinese (Simplified)
|
||||
zho_Hant | Chinese (Traditional)
|
||||
zsm_Latn | Standard Malay
|
||||
zul_Latn | Zulu
|
||||
"""
|
||||
|
||||
|
||||
# SEE LANGUAGE CODES in transformers_fb_nllb200distilled600M_text.md
|
||||
# need to extend this dict from nllb_200_langlist_str
|
||||
lang_2_chars_to_nllb_lang: dict = {
|
||||
'zh': 'zho_Hant',
|
||||
'en': 'eng_Latn',
|
||||
'fr': 'fra_Latn',
|
||||
'de': 'deu_Latn',
|
||||
'it': 'ita_Latn',
|
||||
'ja': 'jpn_Jpan',
|
||||
'ru': 'rus_Cyrl',
|
||||
'es': 'spa_Latn',
|
||||
}
|
||||
11
app/properties.py
Normal file
11
app/properties.py
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import json
|
||||
|
||||
|
||||
class Properties:
|
||||
def __init__(self, file="properties.json"):
|
||||
with open(file) as file:
|
||||
data = json.load(file)
|
||||
|
||||
self.port = data['port']
|
||||
|
||||
|
||||
168
app/struct.py
Normal file
168
app/struct.py
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
from dataclasses import dataclass, field
|
||||
|
||||
|
||||
# dict_field: dict = field(default_factory=lambda: {})
|
||||
@dataclass
|
||||
class Request:
|
||||
text: str
|
||||
from_lang: str | None
|
||||
to_lang: str | None
|
||||
translator_plugin: str | None
|
||||
|
||||
|
||||
@dataclass
|
||||
class Sentence:
|
||||
text: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class Part:
|
||||
text: str
|
||||
translate: str
|
||||
paragraph_end: bool
|
||||
cache_found: bool
|
||||
|
||||
def need_to_translate(self):
|
||||
return not self.cache_found and self.text and self.text != ""
|
||||
|
||||
def __init__(self, text: str, paragraph_end: bool):
|
||||
self.text = text
|
||||
self.translate = ""
|
||||
self.paragraph_end = paragraph_end
|
||||
self.cache_found = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class TranslateStruct:
|
||||
req: Request
|
||||
processed_text: str
|
||||
parts: list[Part]
|
||||
|
||||
|
||||
@dataclass
|
||||
class TranslationParams:
|
||||
default_from_lang: str
|
||||
default_to_lang: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class TextSplitParams:
|
||||
split_by_paragraphs_and_length: bool
|
||||
split_by_sentences_and_length: bool
|
||||
split_expected_length: int
|
||||
|
||||
split_by_paragraphs_only: bool
|
||||
split_by_sentences_only: bool
|
||||
|
||||
# pysbd (default) / blingfire
|
||||
sentence_splitter: str
|
||||
|
||||
def split_enabled(self):
|
||||
return (self.split_by_paragraphs_only or self.split_by_paragraphs_and_length
|
||||
or self.split_by_sentences_and_length or self.split_by_sentences_only)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TextProcessParams:
|
||||
apply_for_request: bool
|
||||
apply_for_response: bool
|
||||
|
||||
replace_non_standard_new_lines_chars: bool
|
||||
replace_not_text_chars: bool
|
||||
allowed_chars_ignoring_replace: set
|
||||
replace_not_text_target_char: str
|
||||
|
||||
remove_identical_characters: bool
|
||||
remove_identical_characters_extra_chars: str
|
||||
remove_identical_characters_max_repeats: int
|
||||
|
||||
remove_multiple_spaces: bool
|
||||
replace_text_from_to: dict
|
||||
|
||||
|
||||
@dataclass
|
||||
class CacheParams:
|
||||
enabled: bool
|
||||
file: str
|
||||
disable_for_plugins: list[str]
|
||||
expire_days: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class TranslateProgress:
|
||||
unit: str
|
||||
ascii: bool
|
||||
desc: str
|
||||
|
||||
|
||||
tp: TranslateProgress = TranslateProgress(unit="part", ascii=True, desc="translate parts: ")
|
||||
|
||||
|
||||
def read_plugin_params(manifest: dict):
|
||||
manifest["options"]["translation_params_struct"] = read_translation_params(manifest)
|
||||
manifest["options"]["text_split_params_struct"] = read_text_split_params(manifest)
|
||||
manifest["options"]["text_process_params_struct"] = read_text_process_params(manifest)
|
||||
|
||||
|
||||
def read_translation_params(manifest: dict):
|
||||
options = manifest["options"]
|
||||
if "translation_params" not in options:
|
||||
return None
|
||||
|
||||
return TranslationParams(
|
||||
default_from_lang=options["translation_params"]["default_from_lang"],
|
||||
default_to_lang=options["translation_params"]["default_to_lang"]
|
||||
)
|
||||
|
||||
|
||||
def read_text_split_params(manifest: dict):
|
||||
options = manifest["options"]
|
||||
|
||||
if "text_split_params" not in options:
|
||||
return None
|
||||
|
||||
return TextSplitParams(
|
||||
split_by_paragraphs_and_length=options["text_split_params"].get("split_by_paragraphs_and_length", False),
|
||||
split_by_sentences_and_length=options["text_split_params"].get("split_by_sentences_and_length", False),
|
||||
split_expected_length=options["text_split_params"].get("split_expected_length", 1000),
|
||||
|
||||
split_by_paragraphs_only=options["text_split_params"].get("split_by_paragraphs_only", False),
|
||||
split_by_sentences_only=options["text_split_params"].get("split_by_sentences_only", False),
|
||||
|
||||
sentence_splitter=options["text_split_params"].get("sentence_splitter", "default"),
|
||||
)
|
||||
|
||||
|
||||
def read_text_process_params(manifest: dict):
|
||||
options = manifest["options"]
|
||||
|
||||
if "text_processing_params" not in options:
|
||||
return None
|
||||
|
||||
return TextProcessParams(
|
||||
apply_for_request=options["text_processing_params"].get("apply_for_request", True),
|
||||
apply_for_response=options["text_processing_params"].get("apply_for_response", True),
|
||||
|
||||
replace_non_standard_new_lines_chars=options["text_processing_params"].get("replace_non_standard_new_lines_chars", True),
|
||||
replace_not_text_chars=options["text_processing_params"].get("replace_not_text_chars", False),
|
||||
allowed_chars_ignoring_replace=options["text_processing_params"].get("allowed_chars_ignoring_replace", " .,<>:;\"'-–…?!#@№$%+/\\^&[]=*()«»—\r\t\n"),
|
||||
replace_not_text_target_char=options["text_processing_params"].get("replace_not_text_target_char", " "),
|
||||
|
||||
remove_identical_characters=options["text_processing_params"].get("remove_identical_characters", False),
|
||||
remove_identical_characters_extra_chars=options["text_processing_params"].get("remove_identical_characters_extra_chars", ""),
|
||||
remove_identical_characters_max_repeats=options["text_processing_params"].get("remove_identical_characters_max_repeats", 3),
|
||||
|
||||
remove_multiple_spaces=options["text_processing_params"].get("remove_multiple_spaces", True),
|
||||
replace_text_from_to=options["text_processing_params"].get("replace_text_from_to", {}),
|
||||
)
|
||||
|
||||
|
||||
def read_cache_params(manifest: dict):
|
||||
options = manifest["options"]
|
||||
|
||||
return CacheParams(
|
||||
enabled=options["cache_params"]["enabled"],
|
||||
file=options["cache_params"]["file"],
|
||||
disable_for_plugins=options["cache_params"]["disable_for_plugins"],
|
||||
expire_days=options["cache_params"]["expire_days"],
|
||||
)
|
||||
73
app/text_processor.py
Normal file
73
app/text_processor.py
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import logging
|
||||
import re
|
||||
|
||||
from app.struct import TextProcessParams
|
||||
|
||||
|
||||
logger = logging.getLogger('uvicorn')
|
||||
|
||||
|
||||
def pre_process(params: TextProcessParams, original_text: str):
|
||||
processed_text = replace_text_from_to(original_text, params.replace_text_from_to)
|
||||
|
||||
if params.replace_non_standard_new_lines_chars:
|
||||
processed_text = replace_non_standard_new_lines_chars(processed_text)
|
||||
|
||||
if params.remove_multiple_spaces:
|
||||
processed_text = remove_multiple_spaces(processed_text)
|
||||
|
||||
if params.replace_not_text_chars:
|
||||
processed_text = replace_not_text_chars(
|
||||
original_text, params.allowed_chars_ignoring_replace, params.replace_not_text_target_char)
|
||||
|
||||
if params.remove_identical_characters:
|
||||
processed_text = remove_identical_characters(
|
||||
processed_text, params.remove_identical_characters_max_repeats,
|
||||
params.remove_identical_characters_extra_chars)
|
||||
|
||||
return processed_text
|
||||
|
||||
|
||||
def replace_not_text_chars(text: str, allowed_chars_ignoring_replace: set, replace_not_text_target_char: str):
|
||||
result = ""
|
||||
replaced_chars = []
|
||||
for char in text:
|
||||
if char.isalpha() or char.isdigit() or char in allowed_chars_ignoring_replace:
|
||||
result = result + char
|
||||
else:
|
||||
result = result + replace_not_text_target_char
|
||||
replaced_chars.append(char)
|
||||
|
||||
if len(replaced_chars) > 0:
|
||||
replaced_chars_set = set(replaced_chars)
|
||||
logger.info("Replaced chars in text {0}: {1}".format(text, replaced_chars_set))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def replace_non_standard_new_lines_chars(text: str):
|
||||
return text.replace("\r\n", "\n").replace("\n\r", "\n").replace("\r", "\n")
|
||||
|
||||
|
||||
def remove_identical_characters(text: str,
|
||||
remove_identical_characters_max_repeats: int,
|
||||
remove_identical_characters_extra_chars: str):
|
||||
chars = 'A-zА-яЁё' + remove_identical_characters_extra_chars
|
||||
regexp = re.compile(r'([%s])\1{%d,}' % (chars, remove_identical_characters_max_repeats))
|
||||
return re.sub(regexp, r'\1' * remove_identical_characters_max_repeats, text)
|
||||
|
||||
|
||||
def remove_multiple_spaces(text: str):
|
||||
while ' ' in text:
|
||||
text = text.replace(' ', ' ')
|
||||
|
||||
return text
|
||||
|
||||
|
||||
def replace_text_from_to(text: str, from_to: dict | None):
|
||||
if from_to and len(from_to) > 0:
|
||||
for key, value in from_to.items():
|
||||
text = text.replace(key, value)
|
||||
|
||||
return text
|
||||
|
||||
128
app/text_splitter.py
Normal file
128
app/text_splitter.py
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
import pysbd
|
||||
from blingfire import text_to_sentences
|
||||
|
||||
from app.dto import TranslatePart
|
||||
from app.struct import TextSplitParams, Part
|
||||
|
||||
|
||||
def is_arr_fin(arr: list, i):
|
||||
return len(arr) - 1 == i
|
||||
|
||||
|
||||
def split_by_sentences(text: str, split_params: TextSplitParams, language="en"):
|
||||
if split_params.sentence_splitter == 'blingfire':
|
||||
return text_to_sentences(text).splitlines(False)
|
||||
else:
|
||||
seg = pysbd.Segmenter(language=language, clean=False)
|
||||
result = []
|
||||
for s in seg.segment(text):
|
||||
result.append(s.strip())
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def split_text(processed_text: str, split_params: TextSplitParams, language="en"):
|
||||
result: list[Part] = []
|
||||
|
||||
# no split
|
||||
if not split_params.split_enabled():
|
||||
part = Part(processed_text, False)
|
||||
result.append(part)
|
||||
|
||||
return result
|
||||
else:
|
||||
# split by paragraphs
|
||||
paragraphs = processed_text.splitlines(False)
|
||||
|
||||
if split_params.split_by_paragraphs_and_length:
|
||||
parts_in_paragraph: list[list[str]] = []
|
||||
part_length = 0
|
||||
# current_part = 0
|
||||
part_in_paragraph: list[str] = []
|
||||
for i, paragraph in enumerate(paragraphs):
|
||||
if part_length == 0 or part_length + len(paragraph) <= split_params.split_expected_length:
|
||||
part_in_paragraph.append(paragraph)
|
||||
part_length += len(paragraph)
|
||||
else:
|
||||
parts_in_paragraph.append(part_in_paragraph)
|
||||
part_in_paragraph = [paragraph]
|
||||
part_length = len(paragraph)
|
||||
|
||||
parts_in_paragraph.append(part_in_paragraph)
|
||||
|
||||
for part_in_paragraph in parts_in_paragraph:
|
||||
result.append(Part("\n".join(part_in_paragraph), True))
|
||||
|
||||
return result
|
||||
|
||||
if split_params.split_by_paragraphs_only:
|
||||
for i, paragraph in enumerate(paragraphs):
|
||||
part = Part(paragraph, not is_arr_fin(paragraphs, i))
|
||||
result.append(part)
|
||||
|
||||
return result
|
||||
|
||||
# split by sentences
|
||||
sentences_in_paragraph: list[list[str]] = []
|
||||
for paragraph in paragraphs:
|
||||
sentences = split_by_sentences(paragraph, split_params, language)
|
||||
sentences_in_paragraph.append(sentences)
|
||||
|
||||
if split_params.split_by_sentences_and_length:
|
||||
sentences_part = ""
|
||||
arr_fin = False
|
||||
for i, sentences_list in enumerate(sentences_in_paragraph):
|
||||
if len(sentences_list) == 0:
|
||||
sentences_part = sentences_part + "\n"
|
||||
for j, sentence in enumerate(sentences_list):
|
||||
if not sentence:
|
||||
sentences_part = sentences_part + "\n"
|
||||
pass
|
||||
if sentence == "" or len(sentences_part) + len(sentence) <= split_params.split_expected_length:
|
||||
if is_arr_fin(sentences_in_paragraph, i) and is_arr_fin(sentences_list, j):
|
||||
sentences_part = sentences_part + sentence
|
||||
elif j == len(sentences_list) - 1:
|
||||
sentences_part = sentences_part + sentence + "\n"
|
||||
else:
|
||||
sentences_part = sentences_part + sentence + " "
|
||||
else:
|
||||
part = Part(sentences_part.strip(), arr_fin)
|
||||
result.append(part)
|
||||
if is_arr_fin(sentences_in_paragraph, i) and is_arr_fin(sentences_list, j):
|
||||
sentences_part = sentence
|
||||
elif is_arr_fin(sentences_list, j):
|
||||
sentences_part = sentence + "\n"
|
||||
else:
|
||||
sentences_part = sentence + " "
|
||||
arr_fin = is_arr_fin(sentences_list, j)
|
||||
|
||||
result.append(Part(sentences_part, False))
|
||||
|
||||
return result
|
||||
|
||||
if split_params.split_by_sentences_only:
|
||||
for i, sentences_list in enumerate(sentences_in_paragraph):
|
||||
if len(sentences_list) == 0:
|
||||
result.append(Part("", True))
|
||||
for j, sentence in enumerate(sentences_list):
|
||||
result.append(Part(sentence, is_arr_fin(sentences_list, j) and not is_arr_fin(sentences_in_paragraph, i)))
|
||||
|
||||
return result
|
||||
|
||||
raise ValueError("Incorrect split params - can't split: " + str(split_params))
|
||||
|
||||
|
||||
def join_text(parts: list[Part]):
|
||||
result = ""
|
||||
translate_parts: list[TranslatePart] = []
|
||||
for i, part in enumerate(parts):
|
||||
translate_part = TranslatePart(text=part.text, translate=part.translate, paragraph_end=part.paragraph_end)
|
||||
translate_parts.append(translate_part)
|
||||
result = result + part.translate
|
||||
if not is_arr_fin(parts, i):
|
||||
if part.paragraph_end:
|
||||
result = result + "\n"
|
||||
else:
|
||||
result = result + " "
|
||||
|
||||
return result, translate_parts
|
||||
347
jaa.py
Normal file
347
jaa.py
Normal file
|
|
@ -0,0 +1,347 @@
|
|||
"""
|
||||
Jaa.py Plugin Framework
|
||||
Author: Janvarev Vladislav
|
||||
|
||||
Jaa.py - minimalistic one-file plugin framework with no dependencies.
|
||||
Main functions:
|
||||
- run all plugins files from "plugins" folder, base on filename
|
||||
- save each plugin options in "options" folder in JSON text files for further editing
|
||||
|
||||
- Plugins
|
||||
must located in plugins/ folder
|
||||
must have "start(core)" function, that returns manifest dict
|
||||
manifest must contain keys "name" and "version"
|
||||
can contain "default_options"
|
||||
- if contain - options will be saved in "options" folder and reload instead next time
|
||||
- if contain - "start_with_options(core, manifest)" function will run with manifest with "options" key
|
||||
manifest will be processed in "process_plugin_manifest" function if you override it
|
||||
|
||||
- Options (for plugins)
|
||||
are saved under "options" folder in JSON format
|
||||
created at first run plugin with "default_options"
|
||||
updated when plugin change "version"
|
||||
|
||||
- Example usage:
|
||||
class VoiceAssCore(JaaCore): # class must override JaaCore
|
||||
def __init__(self):
|
||||
JaaCore.__init__(self, __file__)
|
||||
...
|
||||
|
||||
main = VoiceAssCore()
|
||||
main.init_plugins(["core"]) # 1 param - first plugins to be initialized
|
||||
# Good if you need some "core" options/plugin to be loaded before others
|
||||
# not necessary starts with "plugin_" prefix
|
||||
|
||||
also can be run like
|
||||
|
||||
main.init_plugins()
|
||||
|
||||
- Requirements
|
||||
Python 3.5+ (due to dict mix in final_options calc), can be relaxed
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
|
||||
# here we trying to use termcolor to highlight plugin info and errors during load
|
||||
try:
|
||||
from termcolor import cprint
|
||||
except Exception as e:
|
||||
# not found? making a stub!
|
||||
def cprint(p,color=None):
|
||||
if color == None:
|
||||
print(p)
|
||||
else:
|
||||
print(str(color).upper(),p)
|
||||
|
||||
version = "2.2.1"
|
||||
|
||||
class JaaCore:
|
||||
def __init__(self, root_file = __file__):
|
||||
self.jaaPluginPrefix = "plugin_"
|
||||
self.jaaVersion = version
|
||||
self.jaaRootFolder = os.path.dirname(root_file)
|
||||
self.jaaOptionsPath = os.path.join(self.jaaRootFolder, "options")
|
||||
self.jaaShowTracebackOnPluginErrors = False
|
||||
cprint("JAA.PY v{0} class created!".format(version), "blue")
|
||||
|
||||
# ------------- plugins -----------------
|
||||
def init_plugins(self, list_first_plugins = []):
|
||||
self.plugin_manifests = {}
|
||||
|
||||
# 1. run first plugins first!
|
||||
for modname in list_first_plugins:
|
||||
self.init_plugin(modname)
|
||||
|
||||
# 2. run all plugins from plugins folder
|
||||
pluginpath = os.path.join(self.jaaRootFolder, "plugins")
|
||||
files = [f for f in os.listdir(pluginpath) if os.path.isfile(os.path.join(pluginpath, f))]
|
||||
|
||||
for fil in files:
|
||||
# print fil[:-3]
|
||||
if fil.startswith(self.jaaPluginPrefix) and fil.endswith(".py"):
|
||||
modfile = fil[:-3]
|
||||
self.init_plugin(modfile)
|
||||
|
||||
|
||||
|
||||
def init_plugin(self, modname):
|
||||
# import
|
||||
try:
|
||||
mod = self.import_plugin("plugins."+modname)
|
||||
except Exception as e:
|
||||
self.print_error("JAA PLUGIN ERROR: {0} error on load: {1}".format(modname, str(e)))
|
||||
return False
|
||||
|
||||
# run start function
|
||||
try:
|
||||
res = mod.start(self)
|
||||
except Exception as e:
|
||||
self.print_error("JAA PLUGIN ERROR: {0} error on start: {1}".format(modname, str(e)))
|
||||
return False
|
||||
|
||||
# if plugin has an options
|
||||
if "default_options" in res:
|
||||
try:
|
||||
# saved options try to read
|
||||
saved_options = {}
|
||||
try:
|
||||
with open(os.path.join(self.jaaOptionsPath, modname+'.json'), 'r', encoding="utf-8") as f:
|
||||
s = f.read()
|
||||
saved_options = json.loads(s)
|
||||
#print("Saved options", saved_options)
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
res["default_options"]["v"] = res["version"]
|
||||
|
||||
|
||||
# only string needs Python 3.5
|
||||
final_options = {**res["default_options"], **saved_options}
|
||||
|
||||
# if no option found or version is differ from mod version
|
||||
if len(saved_options) == 0 or saved_options["v"] != res["version"]:
|
||||
final_options["v"] = res["version"]
|
||||
self.save_plugin_options(modname, final_options)
|
||||
|
||||
res["options"] = final_options
|
||||
|
||||
try:
|
||||
res2 = mod.start_with_options(self, res)
|
||||
if res2 != None:
|
||||
res = res2
|
||||
except Exception as e:
|
||||
self.print_error("JAA PLUGIN ERROR: {0} error on start_with_options processing: {1}".format(modname, str(e)))
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
self.print_error("JAA PLUGIN ERROR: {0} error on options processing: {1}".format(modname, str(e)))
|
||||
return False
|
||||
|
||||
|
||||
# processing plugin manifest
|
||||
try:
|
||||
# set up name and version
|
||||
plugin_name = res["name"]
|
||||
plugin_version = res["version"]
|
||||
|
||||
|
||||
self.process_plugin_manifest(modname, res)
|
||||
|
||||
except Exception as e:
|
||||
print("JAA PLUGIN ERROR: {0} error on process startup options: {1}".format(modname, str(e)))
|
||||
return False
|
||||
|
||||
self.plugin_manifests[modname] = res
|
||||
|
||||
self.on_succ_plugin_start(modname, plugin_name, plugin_version)
|
||||
return True
|
||||
|
||||
def on_succ_plugin_start(self, modname, plugin_name, plugin_version):
|
||||
cprint("JAA PLUGIN: {1} {2} ({0}) started!".format(modname, plugin_name, plugin_version))
|
||||
|
||||
def print_error(self, p):
|
||||
import traceback
|
||||
cprint(p, "red")
|
||||
if self.jaaShowTracebackOnPluginErrors:
|
||||
traceback.print_exc()
|
||||
|
||||
def import_plugin(self, module_name):
|
||||
import sys
|
||||
|
||||
__import__(module_name)
|
||||
|
||||
if module_name in sys.modules:
|
||||
return sys.modules[module_name]
|
||||
return None
|
||||
|
||||
def save_plugin_options(self, modname, options):
|
||||
# check folder exists
|
||||
if not os.path.exists(self.jaaOptionsPath):
|
||||
os.makedirs(self.jaaOptionsPath)
|
||||
|
||||
str_options = json.dumps(options, sort_keys=True, indent=4, ensure_ascii=False)
|
||||
with open(os.path.join(self.jaaOptionsPath, modname+'.json'), 'w', encoding="utf-8") as f:
|
||||
f.write(str_options)
|
||||
f.close()
|
||||
|
||||
# process manifest must be overrided in inherit class
|
||||
def process_plugin_manifest(self, modname, manifest):
|
||||
print("JAA PLUGIN: {0} manifest dummy procession (override 'process_plugin_manifest' function)".format(modname))
|
||||
return
|
||||
|
||||
def plugin_manifest(self, pluginname):
|
||||
if pluginname in self.plugin_manifests:
|
||||
return self.plugin_manifests[pluginname]
|
||||
return {}
|
||||
|
||||
def plugin_options(self, pluginname):
|
||||
manifest = self.plugin_manifest(pluginname)
|
||||
if "options" in manifest:
|
||||
return manifest["options"]
|
||||
return None
|
||||
|
||||
# ------------ gradio stuff --------------
|
||||
def gradio_save(self, pluginname):
|
||||
print("Saving options for {0}!".format(pluginname))
|
||||
self.save_plugin_options(pluginname, self.plugin_options(pluginname))
|
||||
|
||||
def gradio_upd(self, pluginname, option, val):
|
||||
options = self.plugin_options(pluginname)
|
||||
|
||||
# special case
|
||||
if isinstance(options[option], (list, dict)) and isinstance(val, str):
|
||||
try:
|
||||
options[option] = json.loads(val)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
pass
|
||||
else:
|
||||
options[option] = val
|
||||
print(option, val, options)
|
||||
|
||||
def gradio_render_settings_interface(self, title: str="Settings manager", required_fields_to_show_plugin: list=["default_options"]):
|
||||
import gradio as gr
|
||||
|
||||
with gr.Blocks() as gr_interface:
|
||||
gr.Markdown("# {0}".format(title))
|
||||
for pluginname in self.plugin_manifests:
|
||||
manifest = self.plugin_manifests[pluginname]
|
||||
|
||||
# calculate if we show plugin
|
||||
is_show_plugin = False
|
||||
if len(required_fields_to_show_plugin) == 0:
|
||||
is_show_plugin = True
|
||||
else:
|
||||
for k in required_fields_to_show_plugin:
|
||||
if manifest.get(k) is not None:
|
||||
is_show_plugin = True
|
||||
|
||||
if is_show_plugin:
|
||||
with gr.Tab(pluginname):
|
||||
gr.Markdown("## {0} v{1}".format(manifest["name"], manifest["version"]))
|
||||
if manifest.get("description") is not None:
|
||||
gr.Markdown(manifest.get("description"))
|
||||
|
||||
if manifest.get("url") is not None:
|
||||
gr.Markdown("**URL:** [{0}]({0})".format(manifest.get("url")))
|
||||
|
||||
|
||||
if "options" in manifest:
|
||||
options = manifest["options"]
|
||||
if len(options) > 1: # not only v
|
||||
text_button = gr.Button("Save options".format(pluginname))
|
||||
#options_int_list = []
|
||||
for option in options:
|
||||
|
||||
#gr.Label(label=option)
|
||||
if option != "v":
|
||||
val = options[option]
|
||||
label = option
|
||||
|
||||
if manifest.get("options_label") is not None:
|
||||
if manifest.get("options_label").get(option) is not None:
|
||||
label = option+": "+manifest.get("options_label").get(option)
|
||||
|
||||
|
||||
if isinstance(val, (bool, )):
|
||||
gr_elem = gr.Checkbox(value=val, label=label)
|
||||
elif isinstance(val, (dict, list)):
|
||||
gr_elem = gr.Textbox(value=json.dumps(val, ensure_ascii=False), label=label)
|
||||
else:
|
||||
gr_elem = gr.Textbox(value=val, label=label)
|
||||
|
||||
def handler(x, pluginname=pluginname, option=option):
|
||||
self.gradio_upd(pluginname, option, x)
|
||||
|
||||
gr_elem.change(handler, gr_elem, None)
|
||||
|
||||
def handler_save(pluginname=pluginname):
|
||||
self.gradio_save(pluginname)
|
||||
|
||||
text_button.click(handler_save, inputs=None, outputs=None)
|
||||
else:
|
||||
gr.Markdown("_No options for this plugin_")
|
||||
|
||||
return gr_interface
|
||||
|
||||
|
||||
def load_options(options_file=None, py_file=None, default_options={}):
|
||||
# 1. calculating options filename
|
||||
if options_file == None:
|
||||
if py_file == None:
|
||||
raise Exception('JAA: Options or PY file is not defined, cant calc options filename')
|
||||
else:
|
||||
options_file = py_file[:-3]+'.json'
|
||||
|
||||
# 2. try to read saved options
|
||||
saved_options = {}
|
||||
try:
|
||||
with open(options_file, 'r', encoding="utf-8") as f:
|
||||
s = f.read()
|
||||
saved_options = json.loads(s)
|
||||
#print("Saved options", saved_options)
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
# 3. calculating final options
|
||||
|
||||
# only string needs Python 3.5
|
||||
final_options = {**default_options, **saved_options}
|
||||
|
||||
# 4. calculating hash from def options to check - is file rewrite needed?
|
||||
import hashlib
|
||||
hash = hashlib.md5((json.dumps(default_options, sort_keys=True)).encode('utf-8')).hexdigest()
|
||||
|
||||
# 5. if no option file found or hash was from other default options
|
||||
if len(saved_options) == 0 or not ("hash" in saved_options.keys()) or saved_options["hash"] != hash:
|
||||
final_options["hash"] = hash
|
||||
#self.save_plugin_options(modname, final_options)
|
||||
|
||||
# saving in file
|
||||
str_options = json.dumps(final_options, sort_keys=True, indent=4, ensure_ascii=False)
|
||||
with open(options_file, 'w', encoding="utf-8") as f:
|
||||
f.write(str_options)
|
||||
f.close()
|
||||
|
||||
return final_options
|
||||
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
Copyright (c) 2021 Janvarev Vladislav
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the “Software”), to deal
|
||||
in the Software without restriction, including without limitation the rights to use,
|
||||
copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
|
||||
and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or
|
||||
substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||
FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
77
plugins/core.py
Normal file
77
plugins/core.py
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
from app.app_core import AppCore
|
||||
from app.struct import TranslationParams, read_text_split_params, \
|
||||
read_text_process_params, read_translation_params, read_cache_params
|
||||
|
||||
|
||||
def start(core: AppCore):
|
||||
manifest = {
|
||||
"name": "Core plugin",
|
||||
"version": "1.0",
|
||||
|
||||
# this is DEFAULT options
|
||||
# ACTUAL options is in options/<plugin_name>.json after first run
|
||||
"default_options": {
|
||||
"default_translate_plugin": "lm_studio", # default translation engine. Will be auto inited on start
|
||||
"init_on_start": "", # additional list of engines, that must be init on start, separated by ","
|
||||
|
||||
"translation_params": {
|
||||
"default_from_lang": "en", # default from language
|
||||
"default_to_lang": "ru", # default to language
|
||||
},
|
||||
|
||||
"text_split_params": {
|
||||
"split_by_paragraphs_and_length": True,
|
||||
"split_by_sentences_and_length": False,
|
||||
"split_expected_length": 1000,
|
||||
|
||||
"split_by_paragraphs_only": False,
|
||||
"split_by_sentences_only": False,
|
||||
|
||||
"sentence_splitter": "default"
|
||||
},
|
||||
|
||||
"text_processing_params": {
|
||||
"apply_for_request": True, # apply processing params for text to translate
|
||||
"apply_for_response": True, # apply processing params for result text
|
||||
|
||||
"replace_non_standard_new_lines_chars": True,
|
||||
"replace_not_text_chars": False,
|
||||
# some models has issues with special chars (for example { or }) in text. this option replace all non-digit / non text / non-allowed (allowed_chars_for_replace) chars
|
||||
"allowed_chars_ignoring_replace": " .,<>:;\"'-–…?!#@№$%+/\\^&[]=*()«»—\r\t\n",
|
||||
# allowed chars for replace with replace_not_text_chars
|
||||
"replace_not_text_target_char": " ", # replace not allowed char to this char
|
||||
|
||||
# replace more than N char consecutive, for example: aaaa -> aaa, bbbbbbb -> bbb
|
||||
"remove_identical_characters": True,
|
||||
"remove_identical_characters_extra_chars": "",
|
||||
"remove_identical_characters_max_repeats": 3,
|
||||
|
||||
"remove_multiple_spaces": True, # replace two or more space to one
|
||||
"replace_text_from_to": { # additional replace variants, from : to
|
||||
},
|
||||
},
|
||||
|
||||
"cache_params": {
|
||||
"enabled": True, # enable/disable translate cache
|
||||
"file": "cache.db", # path to cache file
|
||||
"disable_for_plugins": ["no_translate"], # list of plugin names without cache
|
||||
"expire_days": 0, # 0 - without expire
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
return manifest
|
||||
|
||||
|
||||
def start_with_options(core: AppCore, manifest: dict):
|
||||
options = manifest["options"]
|
||||
|
||||
core.default_translate_plugin = options["default_translate_plugin"]
|
||||
core.init_on_start = options["init_on_start"]
|
||||
|
||||
core.translation_params = read_translation_params(manifest)
|
||||
core.text_split_params = read_text_split_params(manifest)
|
||||
core.text_process_params = read_text_process_params(manifest)
|
||||
core.cache_params = read_cache_params(manifest)
|
||||
|
||||
return manifest
|
||||
99
plugins/plugin_kobold_cpp.py
Normal file
99
plugins/plugin_kobold_cpp.py
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
import os
|
||||
|
||||
from app import struct
|
||||
from app.app_core import AppCore
|
||||
from app.lang_dict import get_lang_by_2_chars_code
|
||||
from app.struct import TranslateStruct
|
||||
import requests
|
||||
|
||||
modname = os.path.basename(__file__)[:-3] # calculating modname
|
||||
|
||||
|
||||
# start function
|
||||
def start(core: AppCore):
|
||||
manifest = { # plugin settings
|
||||
"name": "KoboldCpp Translator", # name
|
||||
"version": "1.0", # version
|
||||
|
||||
"default_options": {
|
||||
"custom_url": "http://127.0.0.1:5001", #
|
||||
"prompt": "\n### Instruction:\nYou are professional translator. Translate this text from {0} to {1}. Don't add any notes or any additional info in your answer, write only translate. Text: {2}\n### Response:\n"
|
||||
},
|
||||
"translate": {
|
||||
"kobold_cpp": (init, translate) # 1 function - init, 2 - translate
|
||||
}
|
||||
}
|
||||
|
||||
return manifest
|
||||
|
||||
|
||||
def start_with_options(core: AppCore, manifest: dict):
|
||||
struct.read_plugin_params(manifest)
|
||||
pass
|
||||
|
||||
|
||||
def init(core: AppCore):
|
||||
return modname
|
||||
|
||||
|
||||
def translate(core: AppCore, ts: TranslateStruct):
|
||||
options = core.plugin_options(modname)
|
||||
|
||||
from_lang_name = get_lang_by_2_chars_code(ts.req.from_lang)
|
||||
to_lang_name = get_lang_by_2_chars_code(ts.req.to_lang)
|
||||
|
||||
# prompt = options["prompt"].format(from_lang_name, to_lang_name)
|
||||
url = options['custom_url'] + "/api/v1/generate"
|
||||
|
||||
for part in ts.parts:
|
||||
if part.need_to_translate():
|
||||
prompt = options["prompt"].format(from_lang_name, to_lang_name, part.text)
|
||||
length: int
|
||||
min_length = 512
|
||||
if len(part.text) * 2 < min_length:
|
||||
length = min_length
|
||||
else:
|
||||
length = len(part.text) * 2
|
||||
req = {
|
||||
"n": 1,
|
||||
"max_context_length": 4096,
|
||||
"max_length": length,
|
||||
"rep_pen": 1.07,
|
||||
"temperature": 0.01,
|
||||
"top_p": 0.92,
|
||||
"top_k": 100,
|
||||
"top_a": 0,
|
||||
"typical": 1,
|
||||
"tfs": 1,
|
||||
"rep_pen_range": 360,
|
||||
"rep_pen_slope": 0.7,
|
||||
"sampler_order": [6,0,1,3,4,2,5],
|
||||
"memory": "",
|
||||
"trim_stop": True,
|
||||
"genkey": "KCPP9747",
|
||||
"min_p": 0,
|
||||
"dynatemp_range": 0,
|
||||
"dynatemp_exponent": 1,
|
||||
"smoothing_factor": 0,
|
||||
"nsigma": 0,
|
||||
"banned_tokens": [],
|
||||
"render_special": False,
|
||||
"logprobs": False,
|
||||
"presence_penalty": 0,
|
||||
"logit_bias": {},
|
||||
"prompt": prompt,
|
||||
"quiet": True,
|
||||
"stop_sequence": ["### Instruction:", "### Response:"],
|
||||
"use_default_badwordsids": False,
|
||||
"bypass_eos": False
|
||||
}
|
||||
|
||||
response = requests.post(url, json=req)
|
||||
|
||||
if response.status_code != 200:
|
||||
raise ValueError("Response status {0} for request by url {1}".format(response.status_code, url))
|
||||
|
||||
content: str = response.json()["results"][0]['text']
|
||||
part.translate = content.strip()
|
||||
|
||||
return ts
|
||||
70
plugins/plugin_lm_studio.py
Normal file
70
plugins/plugin_lm_studio.py
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import os
|
||||
|
||||
import requests
|
||||
from tqdm import tqdm
|
||||
|
||||
from app import struct
|
||||
from app.app_core import AppCore
|
||||
from app.lang_dict import get_lang_by_2_chars_code
|
||||
from app.struct import TranslateStruct, tp
|
||||
|
||||
modname = os.path.basename(__file__)[:-3] # calculating modname
|
||||
|
||||
|
||||
# start function
|
||||
def start(core: AppCore):
|
||||
manifest = {
|
||||
"name": "LM-Studio Translator", # name
|
||||
"version": "1.0", # version
|
||||
|
||||
"default_options": {
|
||||
"custom_url": "http://localhost:1234", #
|
||||
"prompt": "You are professional translator. Translate text from {0} to {1}. Don't add any notes or any additional info in your answer, write only translate. Text: ",
|
||||
"prompt_postfix": ""
|
||||
},
|
||||
|
||||
"translate": {
|
||||
"lm_studio": (init, translate) # 1 function - init, 2 - translate
|
||||
}
|
||||
}
|
||||
|
||||
return manifest
|
||||
|
||||
|
||||
def start_with_options(core: AppCore, manifest: dict):
|
||||
struct.read_plugin_params(manifest)
|
||||
pass
|
||||
|
||||
|
||||
def init(core: AppCore):
|
||||
return modname
|
||||
|
||||
|
||||
def translate(core: AppCore, ts: TranslateStruct):
|
||||
options = core.plugin_options(modname)
|
||||
|
||||
from_lang_name = get_lang_by_2_chars_code(ts.req.from_lang)
|
||||
to_lang_name = get_lang_by_2_chars_code(ts.req.to_lang)
|
||||
|
||||
prompt = options["prompt"].format(from_lang_name, to_lang_name)
|
||||
url = options['custom_url'] + "/v1/chat/completions"
|
||||
|
||||
for part in tqdm(ts.parts, unit=tp.unit, ascii=tp.ascii, desc=tp.desc):
|
||||
if part.need_to_translate():
|
||||
req = {
|
||||
"messages": [
|
||||
{"role": "system", "content": prompt + options["prompt_postfix"]},
|
||||
{"role": "user", "content": part.text}
|
||||
],
|
||||
"temperature": 0.0
|
||||
}
|
||||
|
||||
response = requests.post(url, json=req)
|
||||
|
||||
if response.status_code != 200:
|
||||
raise ValueError("Response status {0} for request by url {1}".format(response.status_code, url))
|
||||
|
||||
content: str = response.json()["choices"][0]['message']['content']
|
||||
part.translate = content.replace("<think>\n\n</think>\n\n", "").strip()
|
||||
|
||||
return ts
|
||||
100
plugins/plugin_madlab_ctranslate2.py
Normal file
100
plugins/plugin_madlab_ctranslate2.py
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
import os
|
||||
|
||||
import ctranslate2
|
||||
import transformers
|
||||
from ctranslate2 import Translator
|
||||
from tqdm import tqdm
|
||||
from transformers import PreTrainedTokenizerBase
|
||||
|
||||
from app import cuda, struct
|
||||
from app.app_core import AppCore
|
||||
from app.struct import TranslateStruct, tp
|
||||
|
||||
modname = os.path.basename(__file__)[:-3]
|
||||
|
||||
model: Translator
|
||||
tokenizer: PreTrainedTokenizerBase
|
||||
|
||||
|
||||
def start(core: AppCore):
|
||||
manifest = { # plugin settings
|
||||
"name": "Madlab CTranslate2", # name
|
||||
"version": "1.0", # version
|
||||
|
||||
"translate": {
|
||||
"madlab_ctranslate2": (init, translate) # 1 function - init, 2 - translate
|
||||
},
|
||||
|
||||
"default_options": {
|
||||
"model": "models/madlad400-10b-mt-bfloat16", # key model
|
||||
"tokenizer": "jbochi/madlad400-10b-mt", # transformers.AutoTokenizer
|
||||
"compute_type": "bfloat16",
|
||||
"cuda": True, # false if you want to run on CPU, true - if on CUDA
|
||||
"cuda_device_index": 0, # GPU index (if you have more than one GPU)
|
||||
"max_batch_size": 16, # batch processing requests, increase need to more memory
|
||||
|
||||
"text_split_params": {
|
||||
"split_by_sentences_only": True,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
return manifest
|
||||
|
||||
|
||||
def start_with_options(core: AppCore, manifest:dict):
|
||||
struct.read_plugin_params(manifest)
|
||||
|
||||
return manifest
|
||||
|
||||
|
||||
def init(core:AppCore):
|
||||
options = core.plugin_options(modname)
|
||||
|
||||
global model
|
||||
global tokenizer
|
||||
|
||||
model = ctranslate2.Translator(options["model"],
|
||||
device=cuda.get_device(options), device_index=options["cuda_device_index"])
|
||||
tokenizer = transformers.AutoTokenizer.from_pretrained(options["tokenizer"])
|
||||
|
||||
return modname
|
||||
|
||||
|
||||
def translate(core: AppCore, ts: TranslateStruct):
|
||||
options = core.plugin_options(modname)
|
||||
|
||||
# # implementation 1: one part - one batch
|
||||
# for part in ts.parts:
|
||||
# if not part.text or part.text == "":
|
||||
# part.translate = ""
|
||||
# else:
|
||||
# input_text = "<2" + ts.req.to_lang + ">" + part.text
|
||||
# tokens = tokenizer.convert_ids_to_tokens(tokenizer.encode(input_text))
|
||||
# translate_results = model.translate_batch([tokens])
|
||||
# output_tokens = translate_results[0].hypotheses[0]
|
||||
# decoded_text = tokenizer.decode(tokenizer.convert_tokens_to_ids(output_tokens))
|
||||
# part.translate = decoded_text
|
||||
|
||||
# implementation 2: all parts - one batch. It's faster, but depends on amount of batches.
|
||||
tokens_list = []
|
||||
for part in tqdm(ts.parts, unit=tp.unit, ascii=tp.ascii, desc=tp.desc):
|
||||
if part.need_to_translate():
|
||||
input_text = "<2" + ts.req.to_lang + ">" + part.text
|
||||
tokens = tokenizer.convert_ids_to_tokens(tokenizer.encode(input_text))
|
||||
tokens_list.append(tokens)
|
||||
|
||||
translate_results = model.translate_batch(
|
||||
tokens_list, max_batch_size=options["max_batch_size"], beam_size=1, return_scores=False, disable_unk=False)
|
||||
|
||||
i = 0
|
||||
for part in ts.parts:
|
||||
if part.text and part.text != "":
|
||||
output_tokens = translate_results[i].hypotheses[0]
|
||||
decoded_text = tokenizer.decode(tokenizer.convert_tokens_to_ids(output_tokens))
|
||||
part.translate = decoded_text
|
||||
i += 1
|
||||
else:
|
||||
part.translate = ""
|
||||
|
||||
return ts
|
||||
81
plugins/plugin_nllb_200.py
Normal file
81
plugins/plugin_nllb_200.py
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
# nllb plugin
|
||||
# author: Vladislav Janvarev
|
||||
|
||||
# from https://github.com/facebookresearch/fairseq/tree/nllb
|
||||
import os
|
||||
|
||||
from tqdm import tqdm
|
||||
from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
|
||||
|
||||
from app import struct, cuda
|
||||
from app.app_core import AppCore
|
||||
from app.lang_dict import lang_2_chars_to_nllb_lang
|
||||
from app.struct import TranslateStruct, tp
|
||||
|
||||
modname = os.path.basename(__file__)[:-3] # calculating modname
|
||||
|
||||
model = None
|
||||
tokenizers:dict = {}
|
||||
|
||||
|
||||
def start(core: AppCore):
|
||||
manifest = { # plugin settings
|
||||
"name": "NLLB 200 Translate", # name
|
||||
"version": "1.0", # version
|
||||
|
||||
"translate": {
|
||||
"nllb_200": (init, translate) # 1 function - init, 2 - translate
|
||||
},
|
||||
|
||||
"default_options": {
|
||||
"model": "facebook/nllb-200-distilled-600M", # key model
|
||||
"cuda": True,
|
||||
"cuda_device_index": 0,
|
||||
|
||||
"text_split_params": {
|
||||
"split_by_sentences_only": True,
|
||||
}
|
||||
},
|
||||
}
|
||||
return manifest
|
||||
|
||||
|
||||
def start_with_options(core: AppCore, manifest: dict):
|
||||
struct.read_plugin_params(manifest)
|
||||
|
||||
return manifest
|
||||
|
||||
|
||||
def init(core: AppCore):
|
||||
options = core.plugin_options(modname)
|
||||
|
||||
global model
|
||||
model = AutoModelForSeq2SeqLM.from_pretrained(options["model"]).to(cuda.get_device_with_gpu_num(options))
|
||||
|
||||
return modname
|
||||
|
||||
|
||||
def translate(core: AppCore, ts: TranslateStruct):
|
||||
options = core.plugin_options(modname)
|
||||
|
||||
from_lang = lang_2_chars_to_nllb_lang[ts.req.from_lang]
|
||||
to_lang = lang_2_chars_to_nllb_lang[ts.req.to_lang]
|
||||
cuda_device = cuda.get_device_with_gpu_num(options)
|
||||
|
||||
if tokenizers.get(from_lang) is None:
|
||||
tokenizers[from_lang] = AutoTokenizer.from_pretrained(options["model"], src_lang=from_lang)
|
||||
tokenizer = tokenizers[from_lang]
|
||||
|
||||
for part in tqdm(ts.parts, unit=tp.unit, ascii=tp.ascii, desc=tp.desc):
|
||||
if part.need_to_translate():
|
||||
inputs = tokenizer(part.text, return_tensors="pt").to(cuda_device)
|
||||
|
||||
translated_tokens = model.generate(
|
||||
**inputs,
|
||||
forced_bos_token_id=tokenizer.convert_tokens_to_ids(to_lang),
|
||||
max_length=int(len(part.text) * 5)
|
||||
)
|
||||
decoded = tokenizer.batch_decode(translated_tokens, skip_special_tokens=True)[0]
|
||||
part.translate = decoded
|
||||
|
||||
return ts
|
||||
86
plugins/plugin_nllb_200_ctranslate2.py
Normal file
86
plugins/plugin_nllb_200_ctranslate2.py
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import os
|
||||
|
||||
import ctranslate2
|
||||
from ctranslate2 import Translator
|
||||
from tqdm import tqdm
|
||||
from transformers import AutoTokenizer
|
||||
|
||||
from app import cuda, struct
|
||||
from app.app_core import AppCore
|
||||
from app.lang_dict import lang_2_chars_to_nllb_lang
|
||||
from app.struct import TranslateStruct, tp
|
||||
|
||||
modname = os.path.basename(__file__)[:-3]
|
||||
|
||||
model: Translator
|
||||
tokenizers:dict = {}
|
||||
|
||||
|
||||
def start(core: AppCore):
|
||||
manifest = { # plugin settings
|
||||
"name": "NLLB 200 CTranslate2", # name
|
||||
"version": "1.0", # version
|
||||
|
||||
"translate": {
|
||||
"nllb_200_ctranslate2": (init, translate) # 1 function - init, 2 - translate
|
||||
},
|
||||
|
||||
"default_options": {
|
||||
"model": "models/nllb-200-3.3B-ct2-float16", # model
|
||||
"compute_type": "bfloat16",
|
||||
"cuda": True, # false if you want to run on CPU, true - if on CUDA
|
||||
"cuda_device_index": 0, # GPU index (if you have more than one GPU)
|
||||
"max_batch_size": 16,
|
||||
|
||||
"text_split_params": {
|
||||
"split_by_sentences_only": True,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
return manifest
|
||||
|
||||
|
||||
def start_with_options(core: AppCore, manifest:dict):
|
||||
struct.read_plugin_params(manifest)
|
||||
|
||||
return manifest
|
||||
|
||||
|
||||
def init(core:AppCore):
|
||||
options = core.plugin_options(modname)
|
||||
|
||||
global model
|
||||
|
||||
model = ctranslate2.Translator(options["model"],
|
||||
device=cuda.get_device(options), device_index=options["cuda_device_index"])
|
||||
|
||||
return modname
|
||||
|
||||
|
||||
def translate(core: AppCore, ts: TranslateStruct):
|
||||
options = core.plugin_options(modname)
|
||||
|
||||
from_lang = lang_2_chars_to_nllb_lang[ts.req.from_lang]
|
||||
to_lang = lang_2_chars_to_nllb_lang[ts.req.to_lang]
|
||||
if tokenizers.get(from_lang) is None:
|
||||
tokenizers[from_lang] = AutoTokenizer.from_pretrained(options["model"], src_lang=from_lang)
|
||||
tokenizer = tokenizers[from_lang]
|
||||
|
||||
# translate_batch not optimal, but there are problems with try to implement batch processing like madlab_ctranslate2
|
||||
for part in tqdm(ts.parts, unit=tp.unit, ascii=tp.ascii, desc=tp.desc):
|
||||
if part.need_to_translate():
|
||||
input_text = part.text
|
||||
tokens = tokenizer.convert_ids_to_tokens(tokenizer.encode(input_text))
|
||||
|
||||
translate_results = model.translate_batch(
|
||||
[tokens], max_batch_size=options["max_batch_size"], beam_size=1, return_scores=False, disable_unk=False,
|
||||
target_prefix=[[to_lang]], batch_type="tokens"
|
||||
)
|
||||
output_tokens = translate_results[0].hypotheses[0]
|
||||
decoded_text = tokenizer.decode(tokenizer.convert_tokens_to_ids(output_tokens))
|
||||
if to_lang in decoded_text:
|
||||
decoded_text = decoded_text.replace(to_lang, "").lstrip()
|
||||
part.translate = decoded_text
|
||||
|
||||
return ts
|
||||
34
plugins/plugin_no_translate.py
Normal file
34
plugins/plugin_no_translate.py
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# No Translate dummy plugin
|
||||
# author: Vladislav Janvarev
|
||||
|
||||
import os
|
||||
|
||||
from app.app_core import AppCore
|
||||
from app.struct import TranslateStruct
|
||||
|
||||
modname = os.path.basename(__file__)[:-3] # calculating modname
|
||||
|
||||
|
||||
# start function
|
||||
def start(core: AppCore):
|
||||
manifest = { # plugin settings
|
||||
"name": "No Translate dummy plugin", # name
|
||||
"version": "1.0", # version
|
||||
|
||||
"translate": {
|
||||
"no_translate": (init, translate) # 1 function - init, 2 - translate
|
||||
}
|
||||
}
|
||||
|
||||
return manifest
|
||||
|
||||
|
||||
def init(core: AppCore):
|
||||
return modname
|
||||
|
||||
|
||||
def translate(core: AppCore, ts: TranslateStruct):
|
||||
for part in ts.parts:
|
||||
part.translate = part.text
|
||||
|
||||
return ts
|
||||
3
properties.json
Normal file
3
properties.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"port": 4990
|
||||
}
|
||||
11
requirements.txt
Normal file
11
requirements.txt
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
uvicorn
|
||||
uvicorn[standard]
|
||||
fastapi
|
||||
termcolor
|
||||
|
||||
transformers
|
||||
ctranslate2
|
||||
|
||||
blingfire
|
||||
pysbd
|
||||
|
||||
3
requirements_pytorch.txt
Normal file
3
requirements_pytorch.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
Open https://pytorch.org/get-started/locally/ and select your variant, for example:
|
||||
|
||||
pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu128
|
||||
1
static/chota.min.css
vendored
Normal file
1
static/chota.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
static/favicon.ico
Normal file
BIN
static/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
70
static/index.html
Normal file
70
static/index.html
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>LLM translate</title>
|
||||
<script type="application/javascript" src="index.js"></script>
|
||||
<link rel="stylesheet" href="chota.min.css">
|
||||
<style>
|
||||
.loader {
|
||||
border: 4px solid #f3f3f3; /* Light grey */
|
||||
border-top: 4px solid #2a82b6; /* Blue */
|
||||
border-radius: 50%;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
animation: spin 2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="top" class="container" role="document">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h5>LLM Translate</h5>
|
||||
</div>
|
||||
<div class="col">
|
||||
<span id="errorText" class="text-error"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<select name="from_lang" id="from_lang_select"></select>
|
||||
</div>
|
||||
<div class="col">
|
||||
<button id="trigger" class="button primary icon" type="submit">
|
||||
 Translate <div id="progress" class="loader" style="display: none;"></div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<select name="to_lang" id="to_lang_select"></select>
|
||||
</div>
|
||||
<div class="col">
|
||||
<input id="plugin" value="" placeholder="Use translator plugin (optional)"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<textarea id="text" rows="20"></textarea>
|
||||
</div>
|
||||
<div class="col" aria-busy="true">
|
||||
<textarea id="text_result" rows="20" ></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
203
static/index.js
Normal file
203
static/index.js
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
async function translateText() {
|
||||
const elProgress = document.getElementById('progress');
|
||||
const trigger = document.getElementById('trigger');
|
||||
const errorText = document.getElementById('errorText');
|
||||
|
||||
const text = document.getElementById('text').value;
|
||||
const fromLang = document.getElementById('from_lang_select').value;
|
||||
const toLang = document.getElementById('to_lang_select').value;
|
||||
const plugin = document.getElementById('plugin').value;
|
||||
|
||||
trigger.disabled = true;
|
||||
elProgress.style.display = 'inline';
|
||||
|
||||
const reqBody = JSON.stringify({
|
||||
text: text, from_lang: fromLang, to_lang: toLang,
|
||||
translator_plugin: plugin
|
||||
});
|
||||
const reqParam = {
|
||||
method: 'POST',
|
||||
body: reqBody,
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
}
|
||||
try {
|
||||
const response = await fetch(`/translate`, reqParam);
|
||||
const data = await response.json();
|
||||
if (data.error) {
|
||||
errorText.innerHTML = data.error;
|
||||
return "";
|
||||
} else {
|
||||
const translation = data.result;
|
||||
document.getElementById('text_result').value = translation;
|
||||
|
||||
errorText.innerHTML = ""
|
||||
return translation;
|
||||
}
|
||||
} catch (error) {
|
||||
errorText.innerHTML = error.message;
|
||||
console.error(error.message);
|
||||
} finally {
|
||||
elProgress.style.display = 'none';
|
||||
trigger.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
window.onload = () => {
|
||||
const trigger = document.getElementById('trigger');
|
||||
trigger.onmouseup = () => {
|
||||
translateText();
|
||||
};
|
||||
|
||||
const langDict = {
|
||||
'en': 'english',
|
||||
'ru': 'russian',
|
||||
'ab': 'abkhazian',
|
||||
'aa': 'afar',
|
||||
'af': 'afrikaans',
|
||||
'sq': 'albanian',
|
||||
'am': 'amharic',
|
||||
'ar': 'arabic',
|
||||
'hy': 'armenian',
|
||||
'as': 'assamese',
|
||||
'ay': 'aymara',
|
||||
'az': 'azerbaijani',
|
||||
'ba': 'bashkir',
|
||||
'eu': 'basque',
|
||||
'bn': 'bengali',
|
||||
'dz': 'bhutani',
|
||||
'bh': 'bihari',
|
||||
'bi': 'bislama',
|
||||
'br': 'breton',
|
||||
'bg': 'bulgarian',
|
||||
'my': 'burmese',
|
||||
'be': 'byelorussian',
|
||||
'km': 'cambodian',
|
||||
'ca': 'catalan',
|
||||
'zh': 'chinese',
|
||||
'co': 'corsican',
|
||||
'hr': 'croatian',
|
||||
'cs': 'czech',
|
||||
'da': 'danish',
|
||||
'nl': 'dutch',
|
||||
'eo': 'esperanto',
|
||||
'et': 'estonian',
|
||||
'fo': 'faeroese',
|
||||
'fj': 'fiji',
|
||||
'fi': 'finnish',
|
||||
'fr': 'french',
|
||||
'fy': 'frisian',
|
||||
'gd': 'gaelic',
|
||||
'gl': 'galician',
|
||||
'ka': 'georgian',
|
||||
'de': 'german',
|
||||
'el': 'greek',
|
||||
'kl': 'greenlandic',
|
||||
'gn': 'guarani',
|
||||
'gu': 'gujarati',
|
||||
'ha': 'hausa',
|
||||
'iw': 'hebrew',
|
||||
'hi': 'hindi',
|
||||
'hu': 'hungarian',
|
||||
'is': 'icelandic',
|
||||
'in': 'indonesian',
|
||||
'ia': 'interlingua',
|
||||
'ie': 'interlingue',
|
||||
'ik': 'inupiak',
|
||||
'ga': 'irish',
|
||||
'it': 'italian',
|
||||
'ja': 'japanese',
|
||||
'jw': 'javanese',
|
||||
'kn': 'kannada',
|
||||
'ks': 'kashmiri',
|
||||
'kk': 'kazakh',
|
||||
'rw': 'kinyarwanda',
|
||||
'ky': 'kirghiz',
|
||||
'rn': 'kirundi',
|
||||
'ko': 'korean',
|
||||
'ku': 'kurdish',
|
||||
'lo': 'laothian',
|
||||
'la': 'latin',
|
||||
'lv': 'latvian',
|
||||
'ln': 'lingala',
|
||||
'lt': 'lithuanian',
|
||||
'mk': 'macedonian',
|
||||
'mg': 'malagasy',
|
||||
'ms': 'malay',
|
||||
'ml': 'malayalam',
|
||||
'mt': 'maltese',
|
||||
'mi': 'maori',
|
||||
'mr': 'marathi',
|
||||
'mo': 'moldavian',
|
||||
'mn': 'mongolian',
|
||||
'na': 'nauru',
|
||||
'ne': 'nepali',
|
||||
'no': 'norwegian',
|
||||
'oc': 'occitan',
|
||||
'or': 'oriya',
|
||||
'om': 'oromo',
|
||||
'ps': 'pashto',
|
||||
'fa': 'persian',
|
||||
'pl': 'polish',
|
||||
'pt': 'portuguese',
|
||||
'pa': 'punjabi',
|
||||
'qu': 'quechua',
|
||||
'rm': 'rhaeto-romance',
|
||||
'ro': 'romanian',
|
||||
'sm': 'samoan',
|
||||
'sg': 'sangro',
|
||||
'sa': 'sanskrit',
|
||||
'sr': 'serbian',
|
||||
'sh': 'serbo-croatian',
|
||||
'st': 'sesotho',
|
||||
'tn': 'setswana',
|
||||
'sn': 'shona',
|
||||
'sd': 'sindhi',
|
||||
'si': 'singhalese',
|
||||
'ss': 'siswati',
|
||||
'sk': 'slovak',
|
||||
'sl': 'slovenian',
|
||||
'so': 'somali',
|
||||
'es': 'spanish',
|
||||
'su': 'sudanese',
|
||||
'sw': 'swahili',
|
||||
'sv': 'swedish',
|
||||
'tl': 'tagalog',
|
||||
'tg': 'tajik',
|
||||
'ta': 'tamil',
|
||||
'tt': 'tatar',
|
||||
'te': 'tegulu',
|
||||
'th': 'thai',
|
||||
'bo': 'tibetan',
|
||||
'ti': 'tigrinya',
|
||||
'to': 'tonga',
|
||||
'ts': 'tsonga',
|
||||
'tr': 'turkish',
|
||||
'tk': 'turkmen',
|
||||
'tw': 'twi',
|
||||
'uk': 'ukrainian',
|
||||
'ur': 'urdu',
|
||||
'uz': 'uzbek',
|
||||
'vi': 'vietnamese',
|
||||
'vo': 'volapuk',
|
||||
'cy': 'welsh',
|
||||
'wo': 'wolof',
|
||||
'xh': 'xhosa',
|
||||
'ji': 'yiddish',
|
||||
'yo': 'yoruba',
|
||||
'zu': 'zulu',
|
||||
};
|
||||
|
||||
const fromLangSelect = document.getElementById('from_lang_select');
|
||||
const toLangSelect = document.getElementById('to_lang_select');
|
||||
|
||||
for (const [key, value] of Object.entries(langDict)) {
|
||||
fromLangSelect.innerHTML += "<option value='" + key + "'>" + value + "</option>";
|
||||
toLangSelect.innerHTML += "<option value='" + key + "'>" + value + "</option>";
|
||||
}
|
||||
fromLangSelect.value = 'en';
|
||||
toLangSelect.value = 'ru';
|
||||
}
|
||||
44
test/test_text_processor.py
Normal file
44
test/test_text_processor.py
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
from unittest import TestCase
|
||||
|
||||
from app import text_processor
|
||||
|
||||
allowed_chars_ignoring_replace = set(" .,<>:;\"'-…?!#@№$%+/\\^&[]=*()—\r\t\n")
|
||||
|
||||
|
||||
class TextProcessorTest(TestCase):
|
||||
def test_replace_not_text_chars_eng_rus_digits(self):
|
||||
text = "Южно-эфиопский грач увёл мышь за хобот на съезд ящериц. Quick wafting zephyrs vex bold Jim. 1234567890"
|
||||
processed = text_processor.replace_not_text_chars(text, allowed_chars_ignoring_replace, "|")
|
||||
self.assertEqual(text, processed)
|
||||
|
||||
def test_replace_not_text_chars_with_japan_german_chars(self):
|
||||
text = "こんにちわ / Ü ü, Ö ö, Ä ä, ẞ ß"
|
||||
processed = text_processor.replace_not_text_chars(text, allowed_chars_ignoring_replace, "|")
|
||||
self.assertEqual(text, processed)
|
||||
|
||||
def test_replace_not_text_chars_with_ignored_chars(self):
|
||||
text = "{ }"
|
||||
processed = text_processor.replace_not_text_chars(text, allowed_chars_ignoring_replace, "|")
|
||||
self.assertEqual("| |", processed)
|
||||
|
||||
def test_trim_duplicate_characters(self):
|
||||
text = "soooooome textttttt оёёёёёёё 12222223 qÖÖÖÖÖÖöööööwe"
|
||||
processed = text_processor.remove_identical_characters(text, 3, "Ö")
|
||||
self.assertEqual("sooome texttt оёёё 12222223 qÖÖÖöööööwe", processed)
|
||||
|
||||
text = "soooooome textttttt оёёёёёёё 12222223 qÖÖÖÖÖÖöööööwe"
|
||||
processed = text_processor.remove_identical_characters(text, 4, "Ö")
|
||||
self.assertEqual("soooome textttt оёёёё 12222223 qÖÖÖÖöööööwe", processed)
|
||||
|
||||
def test_remove_multiple_spaces(self):
|
||||
text = "q w e r t y"
|
||||
processed = text_processor.remove_multiple_spaces(text)
|
||||
self.assertEqual("q w e r t y", processed)
|
||||
|
||||
def test_replace_text_from_to(self):
|
||||
text = "qwe 123 asd zxc"
|
||||
processed = text_processor.replace_text_from_to(text, None)
|
||||
self.assertEqual(text, processed)
|
||||
|
||||
processed = text_processor.replace_text_from_to(text, {"123": "456", "zxc": "789"})
|
||||
self.assertEqual("qwe 456 asd 789", processed)
|
||||
213
test/test_text_splitter.py
Normal file
213
test/test_text_splitter.py
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
import unittest
|
||||
|
||||
from app import text_splitter
|
||||
from app.struct import TextSplitParams, Part
|
||||
|
||||
s1 = "Text one."
|
||||
s2 = "Text two."
|
||||
s3 = "Text three."
|
||||
s4 = "Text four."
|
||||
s5 = "Text five."
|
||||
s6 = "Text six."
|
||||
s7 = "Text seven."
|
||||
s8 = "Text eight."
|
||||
s9 = "Text nine."
|
||||
|
||||
split_txt1 = "Some text. Mr. John Johnson Jr. was born in the U.S.A but earned his Ph.D. in Israel before joining Nike Inc. as an engineer. He also worked at craigslist.org as a business analyst. Some text one. Some text two."
|
||||
split_txt2 = "Some sentence. Mr. Holmes... This is a new sentence! And This is another one.. Hi "
|
||||
|
||||
|
||||
def get_params():
|
||||
return TextSplitParams(
|
||||
split_expected_length=20, sentence_splitter="def",
|
||||
split_by_paragraphs_only=False, split_by_paragraphs_and_length=False,
|
||||
split_by_sentences_only=False, split_by_sentences_and_length=False)
|
||||
|
||||
|
||||
def get_text(*sentences):
|
||||
return " ".join(sentences).replace("\n ", "\n")
|
||||
|
||||
|
||||
class ReqProcessorTest(unittest.TestCase):
|
||||
def test_split_by_sentences_blingfire(self):
|
||||
params = get_params()
|
||||
params.sentence_splitter = "blingfire"
|
||||
|
||||
split = text_splitter.split_by_sentences(split_txt1, params)
|
||||
exp = ['Some text. Mr. John Johnson Jr. was born in the U.S.A but earned his Ph.D. in Israel before joining Nike Inc. as an engineer.',
|
||||
'He also worked at craigslist.org as a business analyst.',
|
||||
'Some text one.',
|
||||
'Some text two.']
|
||||
self.assertEqual(exp, split)
|
||||
|
||||
split = text_splitter.split_by_sentences(split_txt2, params)
|
||||
exp = ['Some sentence.',
|
||||
'Mr.',
|
||||
'Holmes...',
|
||||
'This is a new sentence!',
|
||||
'And This is another one..',
|
||||
'Hi ']
|
||||
self.assertEqual(exp, split)
|
||||
|
||||
def test_split_by_sentences_pysbd(self):
|
||||
params = get_params()
|
||||
|
||||
split = text_splitter.split_by_sentences(split_txt1, params)
|
||||
exp = ['Some text.',
|
||||
'Mr. John Johnson Jr. was born in the U.S.A but earned his Ph.D. in Israel before joining Nike Inc. as an engineer.',
|
||||
'He also worked at craigslist.org as a business analyst.',
|
||||
'Some text one.',
|
||||
'Some text two.']
|
||||
self.assertEqual(exp, split)
|
||||
|
||||
split = text_splitter.split_by_sentences(split_txt2, params)
|
||||
exp = ['Some sentence.',
|
||||
'Mr. Holmes...',
|
||||
'This is a new sentence!',
|
||||
'And This is another one.',
|
||||
'.',
|
||||
'Hi']
|
||||
self.assertEqual(exp, split)
|
||||
|
||||
def test_no_split(self):
|
||||
params = get_params()
|
||||
|
||||
text = get_text(s1, s2, s3, s4, s5)
|
||||
split = text_splitter.split_text(text, params)
|
||||
exp = [Part(get_text(s1, s2, s3, s4, s5), False)]
|
||||
|
||||
self.assertEqual(exp, split)
|
||||
|
||||
def test_split_by_few_paragraphs_and_length__long_text_with_one_paragraph(self):
|
||||
params = get_params()
|
||||
params.split_by_paragraphs_and_length = True
|
||||
|
||||
text = get_text(s1, s2, s3, s4, s5)
|
||||
split = text_splitter.split_text(text, params)
|
||||
exp = [Part(get_text(s1, s2, s3, s4, s5), True)]
|
||||
|
||||
self.assertEqual(exp, split)
|
||||
|
||||
def test_split_by_few_paragraphs_and_length__short_text_with_one_paragraph(self):
|
||||
params = get_params()
|
||||
params.split_by_paragraphs_and_length = True
|
||||
|
||||
text = get_text(s1, s2)
|
||||
split = text_splitter.split_text(text, params)
|
||||
exp = [Part(get_text(s1, s2), True)]
|
||||
|
||||
self.assertEqual(exp, split)
|
||||
|
||||
def test_split_by_few_paragraphs_and_length__text_with_few_paragraphs_01(self):
|
||||
params = get_params()
|
||||
params.split_by_paragraphs_and_length = True
|
||||
# text with few paragraphs
|
||||
text = get_text(s1 + "\n\n", s2 + "\n", s3, s4 + "\n", s5, s6, s7, s8 + "\n", s9 + "\n", s1)
|
||||
split = text_splitter.split_text(text, params)
|
||||
exp = [Part(get_text(s1 + "\n\n", s2), True),
|
||||
Part(get_text(s3, s4), True),
|
||||
Part(get_text(s5, s6, s7, s8), True),
|
||||
Part(get_text(s9 + "\n", s1), True),]
|
||||
|
||||
self.assertEqual(exp, split)
|
||||
|
||||
def test_split_by_few_paragraphs_and_length__text_with_few_paragraphs_02(self):
|
||||
params = get_params()
|
||||
params.split_by_paragraphs_and_length = True
|
||||
|
||||
text = get_text(s1 + "\n", s2 + "\n", s3 + "\n", s4 + "\n", s5)
|
||||
split = text_splitter.split_text(text, params)
|
||||
exp = [Part(get_text(s1 + "\n", s2), True),
|
||||
Part(get_text(s3), True),
|
||||
Part(get_text(s4 + "\n", s5), True),]
|
||||
|
||||
self.assertEqual(exp, split)
|
||||
|
||||
def test_split_by_paragraphs_only_01(self):
|
||||
params = get_params()
|
||||
params.split_by_paragraphs_only = True
|
||||
# text with few paragraphs
|
||||
text = get_text(s1 + "\n\n", s2 + "\n", s3, s4 + "\n", s5, s6)
|
||||
split = text_splitter.split_text(text, params)
|
||||
exp = [Part(get_text(s1), True),
|
||||
Part(get_text(""), True),
|
||||
Part(get_text(s2), True),
|
||||
Part(get_text(s3, s4), True),
|
||||
Part(get_text(s5, s6), False)]
|
||||
|
||||
self.assertEqual(exp, split)
|
||||
|
||||
def test_split_by_paragraphs_only_02(self):
|
||||
params = get_params()
|
||||
params.split_by_paragraphs_only = True
|
||||
# text with few paragraphs
|
||||
split = text_splitter.split_text(s1, params)
|
||||
exp = [Part(get_text(s1), True),
|
||||
Part(get_text(""), True),
|
||||
Part(get_text(s2), True),
|
||||
Part(get_text(s3, s4), True),
|
||||
Part(get_text(s5, s6), True)]
|
||||
|
||||
self.assertEqual([Part(s1, False)], split)
|
||||
|
||||
def test_split_by_few_sentences_and_length_01(self):
|
||||
params = get_params()
|
||||
params.split_by_sentences_and_length = True
|
||||
params.split_expected_length = 25
|
||||
# text with few paragraphs
|
||||
text = get_text(s1 + "\n\n", s2 + "\n", s3, s4 + "\n", s5, s6, s7, s8 + "\n", s9 + "\n", s1)
|
||||
split = text_splitter.split_text(text, params)
|
||||
exp = [Part(get_text(s1 + "\n\n", s2), True),
|
||||
Part(get_text(s3, s4), True),
|
||||
Part(get_text(s5, s6), False),
|
||||
Part(get_text(s7, s8), True),
|
||||
Part(get_text(s9 + "\n", s1), False),]
|
||||
|
||||
self.assertEqual(exp, split)
|
||||
|
||||
def test_split_by_few_sentences_and_length_02(self):
|
||||
params = get_params()
|
||||
params.split_by_sentences_and_length = True
|
||||
params.split_expected_length = 40
|
||||
# text with few paragraphs
|
||||
text = get_text(s1 + "\n\n", s2 + "\n", s3, s4 + "\n", s5, s6, s7, s8 + "\n", s9 + "\n", s1)
|
||||
split = text_splitter.split_text(text, params)
|
||||
exp = [Part(get_text(s1 + "\n\n", s2 + "\n", s3), False),
|
||||
Part(get_text(s4 + "\n", s5, s6), False),
|
||||
Part(get_text(s7, s8 + "\n", s9), True),
|
||||
Part(get_text(s1), False),]
|
||||
|
||||
self.assertEqual(exp, split)
|
||||
|
||||
def test_split_by_few_sentences_and_length_03(self):
|
||||
params = get_params()
|
||||
params.split_by_sentences_and_length = True
|
||||
split = text_splitter.split_text(s1, params)
|
||||
|
||||
self.assertEqual([Part(s1, False)], split)
|
||||
|
||||
def test_split_by_sentences_only_01(self):
|
||||
params = get_params()
|
||||
params.split_by_sentences_only = True
|
||||
split = text_splitter.split_text(s1, params)
|
||||
self.assertEqual([Part(s1, False)], split)
|
||||
|
||||
def test_split_by_sentences_only_02(self):
|
||||
params = get_params()
|
||||
params.split_by_sentences_only = True
|
||||
# text with few paragraphs
|
||||
text = get_text(s1 + "\n\n", s2 + "\n", s3, s4 + "\n", s5, s6, s7, s8 + "\n", s9 + "\n", s1)
|
||||
split = text_splitter.split_text(text, params)
|
||||
exp = [Part(s1, True),
|
||||
Part("", True),
|
||||
Part(s2, True),
|
||||
Part(s3, False),
|
||||
Part(s4, True),
|
||||
Part(s5, False),
|
||||
Part(s6, False),
|
||||
Part(s7, False),
|
||||
Part(s8, True),
|
||||
Part(s9, True),
|
||||
Part(s1, False),]
|
||||
|
||||
self.assertEqual(exp, split)
|
||||
Loading…
Add table
Add a link
Reference in a new issue