"""utility.py Static clasess and functions for general use """ # Standard Library import random from pathlib import Path # Pip import argostranslate.package import argostranslate.translate import torch from cedict_utils.cedict import CedictEntry, CedictParser from qwen_tts import Qwen3TTSModel # Local from .constants import ( CCCEDICT_PATH, DICTATION_TYPE, DICT_TYPE, GWEN_TTS, INPUT, LANGUAGES, OUTPUT, PHRASES_TYPE, RESOURCES, ) # Static Clases class TRANS: """Static Class for Argos translate""" UPDATED = False PACKAGES = [] @staticmethod def create_translator(from_code, to_code): """Download and install Argos Translate package""" print(f"Create translator from {from_code} to {to_code}") if from_code == to_code: return if not TRANS.UPDATED: argostranslate.package.update_package_index() TRANS.PACKAGES = argostranslate.package.get_available_packages() TRANS.UPDATED = True packages = tuple( filter( lambda x: x.from_code == from_code or x.to_code == to_code, TRANS.PACKAGES, ) ) print(f"available packages {packages[:5]}") packages_to_install = [] for in_package in packages: if in_package.from_code == from_code: if in_package.to_code == to_code: # Single package between 2 languages print( f"Installing package {in_package.from_code}" f"->{in_package.to_code}" ) packages_to_install.append(in_package) break for out_package in packages: if out_package.to_code == to_code: if in_package.to_code == out_package.from_code: print( f"Installing in_package {in_package.from_code}" f"->{in_package.to_code}" ) print( f"Installing out_package {out_package.from_code}" f"->{out_package.to_code}" ) packages_to_install.append(in_package) packages_to_install.append(out_package) break for package in packages_to_install: print(f"instaling package {package}") argostranslate.package.install_from_path(package.download()) class TranslatedEntry: """Holder class for CCCEDIT entry translated to `language_id`""" def __init__(self, entry: CedictEntry, language_id: str): self.entry = entry self.language_id = language_id self._translated_meanings = [] @property def simplified(self): """Entry simplified""" return self.entry.simplified @property def traditional(self): """Entry traditional""" return self.entry.traditional @property def pinyin(self): """Entry piying""" return self.entry.pinyin @property def meanings(self): """Entry translated meaning list""" for meaning in self.entry.meanings: if self.language_id != LANGUAGES.EN: print(f"translating from {LANGUAGES.EN} to {self.language_id}") print(f"-> {meaning}") trans_meaning = argostranslate.translate.translate( meaning, LANGUAGES.EN, self.language_id ) else: trans_meaning = meaning self._translated_meanings.append(trans_meaning) return self._translated_meanings class CCCEDICT: """Static Class for the CCCEDIT dictionary""" PARSER = None ENTRIES = [] DICTIONARY_LIST = {} @staticmethod def create_cedict( language_id: str = LANGUAGES.EN, ) -> dict[str, list[TranslatedEntry]]: """Creates a create_cedict dictionary object""" if not CCCEDICT.PARSER: CCCEDICT.PARSER = CedictParser() CCCEDICT.PARSER.read_file(CCCEDICT_PATH) CCCEDICT.ENTRIES = CCCEDICT.PARSER.parse() if language_id not in CCCEDICT.DICTIONARY_LIST: TRANS.create_translator(LANGUAGES.EN, language_id) dictionary = {} for entry in CCCEDICT.ENTRIES: trans_entry = TranslatedEntry(entry, language_id) if entry.simplified not in dictionary: dictionary[entry.simplified] = [trans_entry] else: dictionary[entry.simplified].append(trans_entry) CCCEDICT.DICTIONARY_LIST[language_id] = dictionary else: dictionary = CCCEDICT.DICTIONARY_LIST[language_id] return dictionary class TTS: """Static class for the the TTS engine""" MODEL = None DEVICE = None DEFAULTS = { "language": "Chinese", "instruct": "语速缓慢而审慎,每个音节的语调都拿捏得恰到好处,宛如教授在指导新生。", "instruct": "Speak in a slow pace and enuntiate every word, as a teacher to a learning student", } VOICES = ["Vivian", "Serena", "Uncle_Fu", "Dylan", "Eric"] @staticmethod def create_tts(): """Creates a TTS engine""" if TTS.DEVICE is None: # Automatically detect the best available device if torch.cuda.is_available(): TTS.DEVICE = "cuda:0" elif torch.backends.mps.is_available(): TTS.DEVICE = "mps" else: TTS.DEVICE = "cpu" if TTS.MODEL is None: TTS.MODEL = Qwen3TTSModel.from_pretrained( GWEN_TTS, device_map=TTS.DEVICE, dtype=torch.bfloat16, # attn_implementation="flash_attention_2", ) @staticmethod def generate(text: str): """Generates a Waw using the defaulst values""" print(f"starting to generate {text}") speaker = random.choice(TTS.VOICES) audio = TTS.MODEL.generate_custom_voice( text=text, **TTS.DEFAULTS, speaker=speaker ) print(f"finish to generate {text}") return audio # Clases class ProcessFolder: """Class that represents a folder to processs diferent input files has direfent process_files depending on language """ def __init__(self, input_folder: Path, language_id: str = None): self.input_folder = input_folder self._language_id = language_id self.absolute_input_folder = INPUT / self.input_folder # process file type self.out_folder = OUTPUT / input_folder self.out_folder.mkdir(parents=True, exist_ok=True) self.resources = RESOURCES / input_folder self.resources.mkdir(parents=True, exist_ok=True) @property def output_name(self): """Posible name for the output file, still missing the filetype""" if self.language_id is None: raise ValueError("Not a valid language selected") return self.out_folder / f"{self.input_folder.stem}.{self.language_id}.temp" @property def input_files(self): input_files = [] for file in self.absolute_input_folder.glob(f"*.txt"): for file_type in (DICT_TYPE, PHRASES_TYPE, DICTATION_TYPE): if file_type in file.suffixes: input_files.append( ProcessFile( file.relative_to(INPUT), language_id=self.language_id ) ) return input_files @property def language_id(self): """language for this trasnlation process""" return self._language_id @language_id.setter def language_id(self, value): self._language_id = value def __str__(self): return ( f"Proccess {self.input_folder}" f"(out: {self.out_folder}, res: {self.resources})" ) class ProcessFile: """Class that represents a file to processs diferent input files has direfent process_files depending on language """ def __init__(self, input_file: Path, language_id: str = None): self.input_file = input_file self._language_id = language_id # process file type self.out_folder = OUTPUT / input_file.parent self.out_folder.mkdir(parents=True, exist_ok=True) resources = RESOURCES / input_file self.resources = resources.parent / resources.stem self.resources.mkdir(parents=True, exist_ok=True) @property def file_type(self): if DICTATION_TYPE in self.input_file.suffixes: return DICTATION_TYPE elif DICT_TYPE in self.input_file.suffixes: return DICT_TYPE elif PHRASES_TYPE in self.input_file.suffixes: return PHRASES_TYPE else: raise ValueError("File type not recognized") @property def absolute_input_file(self): """Absolute input file""" return INPUT / self.input_file @property def language_id(self): """language for this trasnlation proccess""" return self._language_id @language_id.setter def language_id(self, value): self._language_id = value @property def output_name(self): """Posible name for the output file, still missing the filetype""" if self.language_id is None: raise ValueError("Not a valid language selected") return self.out_folder / f"{self.input_file.stem}.{self.language_id}.temp" @property def dictionary_resource_file(self): """The path for the resource tsv for dictionary files""" return self.resources / f"dictionary.{self.language_id}.tsv" @property def relative_dictionary_resource_file(self): """The path for the resource tsv for dictionary files""" path = self.resources / f"dictionary.{self.language_id}.tsv" return path.relative_to(RESOURCES) @property def available_dictionary_languages(self): """for a Dictionary file loads the avaliable proceced languages""" return [lan.suffixes[0][1:] for lan in self.resources.glob("dictionary.*.tsv")] def __str__(self): return f"Proccess:{self.input_file}" class TranslationResult: """Result of a translated process""" def __init__( self, language_id: str, translated: str, line: str, audio_path: Path, ): self.language_id = language_id self.translated = translated self.line = line self.audio_path = audio_path class DictionaryResult: """Result of a dictionaty process""" def __init__( self, n: str, language_id: str, simplified: str, traditional: str, pinyin: str, meaning: str, audio_path: Path, ): self.n = n self.language_id = language_id self.simplified = simplified self.traditional = traditional self.pinyin = pinyin self.meaning = meaning self.audio_path = audio_path