From 92990207386926ee6bd3b88e5a44c9eff2d96435 Mon Sep 17 00:00:00 2001 From: Wolfang Torres Date: Mon, 29 Jun 2026 00:05:46 +0800 Subject: [PATCH] adds support for comppleted files --- src/anki_hsk_creator/__main__.py | 29 +++++++-- src/anki_hsk_creator/anki_generation.py | 85 ++++++++++++++++++++++++- src/anki_hsk_creator/api.py | 22 +++++++ src/anki_hsk_creator/constants.py | 3 +- src/anki_hsk_creator/proccessor.py | 61 ++++++++++++------ src/anki_hsk_creator/utility.py | 58 +++++++++++++---- 6 files changed, 216 insertions(+), 42 deletions(-) diff --git a/src/anki_hsk_creator/__main__.py b/src/anki_hsk_creator/__main__.py index 0d6a62a..8d089ac 100644 --- a/src/anki_hsk_creator/__main__.py +++ b/src/anki_hsk_creator/__main__.py @@ -9,13 +9,21 @@ from .api import ( is_file, list_input_files, pre_process_a_dictionary_file, + proccess_a_completed_file, process_a_dictation_file, process_a_dictionary_file, process_a_phrases_file, select_file, select_folder, ) -from .constants import DICTATION_TYPE, DICT_TYPE, INPUT, LANGUAGES, PHRASES_TYPE +from .constants import ( + COMPLETED_TYPE, + DICTATION_TYPE, + DICT_TYPE, + INPUT, + LANGUAGES, + PHRASES_TYPE, +) from .utility import ProcessFile, ProcessFolder @@ -24,7 +32,7 @@ def cli_choose_work_type() -> str: option = None while not option in ["s", "f", "e"]: option = input( - "Please select the work option:\n" "s: single file, f:folder, e:exit\n" + "Please select the work option:\n" "s:single file, f:folder, e:exit\n" ) return option @@ -66,7 +74,7 @@ def cli_select_folder() -> ProcessFolder: print(f"Selected {selected}") s = None while s not in ("yes", "y", "no", "n"): - s = input("if this the folder? (yes/no, go inside)") + s = input("if this the folder? (yes / no, go inside) : ") if s in ("yes", "y"): in_folder = selected else: @@ -100,9 +108,10 @@ def cli_select_language(languages: list = None) -> str: for language_id, language in avaliable_languages: if languages and language_id in languages: print(f"{language_id} - {language}") + print("all - All languages") s = None - while not s or s not in LANGUAGES.AvailableLanguages: - lan_codes = [lan_id for lan_id, lan in avaliable_languages] + while not s or s not in LANGUAGES.AvailableLanguages + ("all",): + lan_codes = [lan_id for lan_id, lan in avaliable_languages] + ["all"] s = input(f"Please select the language {', '.join(lan_codes)}: ") return s @@ -137,8 +146,16 @@ def main(): language_id = cli_select_language() print(f"processing file {input_file} with language {language_id}") process_a_dictation_file(input_file, language_id) + elif COMPLETED_TYPE in input_file.input_file.suffixes: + language_id = input_file.input_file.suffixes[1][1:] + print(f"processing file {input_file} with language {language_id}") + proccess_a_completed_file(input_file, language_id) elif option == "f": - for language_id in LANGUAGES.AvailableLanguages: + language_id = cli_select_language() + langs = ( + LANGUAGES.AvailableLanguages if language_id == "all" else [language_id] + ) + for language_id in langs: in_folder = cli_select_folder() print(f"Selected: {in_folder} with language {language_id}") for dirpath, dirnames, filenames in tuple( diff --git a/src/anki_hsk_creator/anki_generation.py b/src/anki_hsk_creator/anki_generation.py index 1982926..f7da4f6 100644 --- a/src/anki_hsk_creator/anki_generation.py +++ b/src/anki_hsk_creator/anki_generation.py @@ -13,7 +13,13 @@ from pinyin_tone_converter.pinyin_tone_converter import PinyinToneConverter # Local from .constants import DICTATION_TYPE, DICT_TYPE, PHRASES_TYPE -from .utility import DictionaryResult, ProcessFile, ProcessFolder, TranslationResult +from .utility import ( + DictionaryResult, + ProcessFile, + ProcessFolder, + SimpleResult, + TranslationResult, +) # Constants @@ -137,9 +143,86 @@ HSK_MODEL = Model( css=CSS, ) +SIMPLE_HSK_MODEL = Model( + 2819647620, + "Simple HSK Model", + fields=[ + {"name": "Meaning"}, + {"name": "Pinyin"}, + {"name": "Simplified"}, + {"name": "Audio"}, + ], + templates=[ + { + "name": "Card 1", + "qfmt": ( + "{{Pinyin}}" + "
{{Meaning}}" + "
{{Audio}}" + "
Simplified: {{type:Simplified}}" + ), + "afmt": ( + "{{FrontSide}}
{{Simplified}}
" + ), + }, + { + "name": "Card 2", + "qfmt": ( + "
{{Simplified}}
" "
Pinyin: {{type:Pinyin}}" + ), + "afmt": ( + "{{FrontSide}}
{{Pinyin}}" + "
{{Meaning}}
{{Audio}}" + ), + }, + { + "name": "Card 3", + "qfmt": ("{{Audio}}" "
Simplified: {{type:Simplified}}"), + "afmt": ( + "{{FrontSide}}
{{Pinyin}}" + "
{{Simplified}}
" + ), + }, + ], + css=CSS, +) + # Proccess +def output_anki_completed( + process_file: ProcessFile, results: list[SimpleResult] +) -> Path: + """Creates an anki file for a completed file""" + final_file = ( + process_file.output_name.parent / f"{process_file.input_file.stem}.apkg" + ) + deck_name = "::".join(process_file.input_file.parts[:-1] + (final_file.stem,)) + deck = Deck( + random.randrange(1 << 30, 1 << 31), + deck_name, + f"Deck for {process_file.input_file.stem}, " + "created in https://www.wolfang.info.ve/hskankicreator/", + ) + audios = [] + for result in results: + note = Note( + model=SIMPLE_HSK_MODEL, + fields=[ + result.meaning, + PinyinToneConverter().convert_text(result.pinyin), + result.character, + f"[sound:{result.audio_path.name}]", + ], + ) + deck.add_note(note) + audios.append(result.audio_path) + package = Package(deck) + package.media_files = audios + package.write_to_file(final_file) + return final_file + + def output_anki_dictation( process_file: ProcessFile, results: list[DictionaryResult] ) -> Path: diff --git a/src/anki_hsk_creator/api.py b/src/anki_hsk_creator/api.py index ffe9a11..5100835 100644 --- a/src/anki_hsk_creator/api.py +++ b/src/anki_hsk_creator/api.py @@ -9,12 +9,14 @@ from pathlib import Path # Local from . import DATA_FOLDER from .anki_generation import ( + output_anki_completed, output_anki_dictation, output_anki_dictionary, output_anki_package, output_anki_phrase, ) from .constants import ( + COMPLETED_TYPE, DICTATION_TYPE, DICT_TYPE, INPUT, @@ -24,6 +26,7 @@ from .constants import ( RESOURCES, ) from .proccessor import ( + completed_process, dictation_process, dictionary_bulk_process, dictionary_pre_process, @@ -249,6 +252,18 @@ def process_a_phrases_file(process_file: ProcessFile, language_id: str) -> Path: return output_anki_phrase(process_file, results) +def proccess_a_completed_file(process_file: ProcessFile, language_id: str) -> Path: + """Process a completed file""" + process_file.language_id = language_id + TTS.create_tts() + with process_file.absolute_input_file.open( + "r", encoding="utf8", newline="\n" + ) as file: + text_lines = [line.strip() for line in file.readlines() if line.strip()] + results = completed_process(text_lines, process_file) + return output_anki_completed(process_file, results) + + def folder_proccess(process_folder: ProcessFolder, language_id: str): process_folder.language_id = language_id TTS.create_tts() @@ -278,4 +293,11 @@ def folder_proccess(process_folder: ProcessFolder, language_id: str): ) as file: text_lines = [line.strip() for line in file.readlines() if line.strip()] results[process_file] = translator_process(text_lines, process_file) + elif process_file.file_type is COMPLETED_TYPE: + process_file.language_id = process_file.input_file.suffixes[1][1:] + with process_file.absolute_input_file.open( + "r", encoding="utf8", newline="\n" + ) as file: + text_lines = [line.strip() for line in file.readlines() if line.strip()] + results[process_file] = completed_process(text_lines, process_file) return output_anki_package(process_folder, results) diff --git a/src/anki_hsk_creator/constants.py b/src/anki_hsk_creator/constants.py index b3cbf0e..e7414e4 100644 --- a/src/anki_hsk_creator/constants.py +++ b/src/anki_hsk_creator/constants.py @@ -22,6 +22,7 @@ GWEN_TTS = GWEN_FOLDER / "Qwen3-TTS-12Hz-0.6B-CustomVoice" PHRASES_TYPE = ".phrases" DICT_TYPE = ".dictionary" DICTATION_TYPE = ".dictation" +COMPLETED_TYPE = ".completed" class LANGUAGES: @@ -35,7 +36,7 @@ class LANGUAGES: TR = "tr" TH = "th" JP = "jp" - AvailableLanguages = (EN, ES, FR, RU, TR, TH, JP) + AvailableLanguages = (ES, EN, FR, RU, TR, TH, JP) LanguageNames = { EN: "English", ES: "Spanish", diff --git a/src/anki_hsk_creator/proccessor.py b/src/anki_hsk_creator/proccessor.py index 11e1d37..30b3fb6 100644 --- a/src/anki_hsk_creator/proccessor.py +++ b/src/anki_hsk_creator/proccessor.py @@ -10,7 +10,14 @@ import soundfile as sf # Local from .constants import LANGUAGES -from .utility import CCCEDICT, TTS, DictionaryResult, ProcessFile, TranslationResult +from .utility import ( + CCCEDICT, + TTS, + DictionaryResult, + ProcessFile, + SimpleResult, + TranslationResult, +) # Constants @@ -68,7 +75,26 @@ def translator_process( return results +def completed_process( + text_lines: list[str], process_file: ProcessFile +) -> list[TranslationResult]: + """Process a complete file, not translation necesary""" + results = [] + dictionary = CCCEDICT.create_cedict() + for n, line in enumerate(text_lines): + char, meaning = line.split(maxsplit=1) + entries = dictionary.get(char) + pinying = entries[0].pinyin + audio_path = process_file.resources / f"{char}.wav" + if not audio_path.is_file(): + wavs, sr = TTS.generate(f"{char}。") + sf.write(audio_path, wavs[0], sr) + results.append(SimpleResult(char, pinying, meaning, audio_path)) + return results + + def dictionary_bulk_process(words_list: list[str], process_file: ProcessFile): + dictionary_en = CCCEDICT.create_cedict(LANGUAGES.EN) dictionary = CCCEDICT.create_cedict(process_file.language_id) with process_file.dictionary_resource_file.open( "w", encoding="utf8", newline="" @@ -81,30 +107,25 @@ def dictionary_bulk_process(words_list: list[str], process_file: ProcessFile): for words in words_list: word = words.split()[0] hint = " ".join(words.split()[1:]) if len(words.split()) > 1 else None - if entries := dictionary.get(word): - if len(entries) > 1: - print(f"\nWARNING: {word} has multiple meanings:") + entries_en = dictionary_en.get(word) + entries = dictionary.get(word) + if entries: pos_meanings = ( [ - entry - for entry in entries - for meaning in entry.meanings - if hint in meaning + meaning + for entry, entry_en in zip(entries, entries_en) + for meaning, meaning_en in zip( + entry.meanings, entry_en.meanings + ) + if hint in meaning_en or hint in meaning ] if hint - else [] + else [meaning for entry in entries for meaning in entry.meanings] ) - pos_entries = pos_meanings or entries - meanings = [] - for entry in pos_entries: - for meaning in entry.meanings: - if hint and hint in meaning: - meanings.append(meaning) - if not meanings: - meanings = [ - meaning for entry in pos_entries for meaning in entry.meanings - ] - meanings_text = meanings[0] + meanings_text = "\n".join( + f"{n}: {meaning}" for n, meaning in enumerate(pos_meanings) + ) + entry = entries[0] tsv_writer.writerow( { "n": number, diff --git a/src/anki_hsk_creator/utility.py b/src/anki_hsk_creator/utility.py index b5f460e..4c137a5 100644 --- a/src/anki_hsk_creator/utility.py +++ b/src/anki_hsk_creator/utility.py @@ -18,6 +18,7 @@ from qwen_tts import Qwen3TTSModel # Local from .constants import ( CCCEDICT_PATH, + COMPLETED_TYPE, DICTATION_TYPE, DICT_TYPE, GWEN_TTS, @@ -55,6 +56,7 @@ class TRANS: ) print(f"available packages {packages[:5]}") packages_to_install = [] + ready = False for in_package in packages: if in_package.from_code == from_code: if in_package.to_code == to_code: @@ -64,7 +66,12 @@ class TRANS: f"->{in_package.to_code}" ) packages_to_install.append(in_package) + ready = True break + for in_package in packages: + if ready: + break + if in_package.from_code == from_code: for out_package in packages: if out_package.to_code == to_code: if in_package.to_code == out_package.from_code: @@ -78,6 +85,7 @@ class TRANS: ) packages_to_install.append(in_package) packages_to_install.append(out_package) + ready = True break for package in packages_to_install: print(f"instaling package {package}") @@ -110,16 +118,17 @@ class TranslatedEntry: @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) + if not self._translated_meanings: + 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 @@ -227,7 +236,7 @@ class ProcessFolder: 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): + for file_type in (DICT_TYPE, PHRASES_TYPE, DICTATION_TYPE, COMPLETED_TYPE): if file_type in file.suffixes: input_files.append( ProcessFile( @@ -264,9 +273,18 @@ class ProcessFile: # 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 resources(self): + resources = RESOURCES / self.input_file + if self.file_type is COMPLETED_TYPE: + resources = ( + resources.parent / f"{self.input_file.name.split(".")[0]}.dictionary" + ) + else: + resources = resources.parent / resources.stem + resources.mkdir(parents=True, exist_ok=True) + return resources @property def file_type(self): @@ -276,6 +294,8 @@ class ProcessFile: return DICT_TYPE elif PHRASES_TYPE in self.input_file.suffixes: return PHRASES_TYPE + elif COMPLETED_TYPE in self.input_file.suffixes: + return COMPLETED_TYPE else: raise ValueError("File type not recognized") @@ -320,6 +340,16 @@ class ProcessFile: return f"Proccess:{self.input_file}" +class SimpleResult: + """Simplied result class""" + + def __init__(self, character: str, pinyin: str, meaning: str, audio_path: Path): + self.character = character + self.pinyin = pinyin + self.meaning = meaning + self.audio_path = audio_path + + class TranslationResult: """Result of a translated process"""