Compare commits
14 Commits
5334f3aefc
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af0934ea5a | ||
|
|
8efebfb5ed | ||
|
|
b30c8de0b4 | ||
|
|
04fd4cef89 | ||
|
|
977bb422c0 | ||
|
|
9299020738 | ||
|
|
5c78c2dad6 | ||
|
|
9437d5ed7a | ||
|
|
7cb0894abd | ||
|
|
777f4b4de1 | ||
|
|
5896f0db66 | ||
|
|
d153a441ed | ||
|
|
df64a5fc15 | ||
|
|
2cf2a1a5a9 |
@@ -24,3 +24,7 @@ indent_style = tab
|
||||
[*.{js,css,scss,html,xml}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.{dictionary,dictation,phrases}.txt]
|
||||
indent_style = tab
|
||||
indent_size = 8
|
||||
|
||||
10
README.md
10
README.md
@@ -14,9 +14,17 @@ creates anki hsk decks from a list of words
|
||||
## Installation
|
||||
|
||||
```console
|
||||
git clone https://github.com/resemble-ai/chatterbox
|
||||
install qwen models https://github.com/QwenLM/Qwen3-TTS
|
||||
modelscope download --model Qwen/Qwen3-TTS-12Hz-0.6B-CustomVoice --local_dir ./Qwen3-TTS-12Hz-0.6B-CustomVoice
|
||||
git clone https://gitea.wolfang.info.ve/wolfang/anki-hsk-creator
|
||||
install cuda 13.2 https://developer.nvidia.com/cuda-downloads
|
||||
install pythorch for cuda 13.2 https://pytorch.org/get-started/locally/
|
||||
git clone https://gitea.wolfang.info.ve/wolfang/anki-hsk-creator-data
|
||||
set local .env file
|
||||
HF_TOKEN =
|
||||
DATA_FOLDER =
|
||||
GWEN_FOLDER =
|
||||
ARGOS_DEVICE_TYPE = auto
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
@@ -30,12 +30,12 @@ dependencies = [
|
||||
"pinyin-tone-converter",
|
||||
"genanki",
|
||||
"argostranslate",
|
||||
"chatterbox-tts",
|
||||
"torch",
|
||||
"torchaudio",
|
||||
"soundfile",
|
||||
"torchcodec",
|
||||
"python-dotenv",
|
||||
|
||||
"qwen-tts",
|
||||
# "flash-attn"
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
# SPDX-FileCopyrightText: 2026-present Wolfang Torres <wolfang.torres@gmail.com>
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
__version__ = "0.1.3"
|
||||
__version__ = "0.2.4"
|
||||
|
||||
@@ -18,3 +18,6 @@ HF_TOKEN = os.environ.get("HF_TOKEN")
|
||||
default_path = Path.home() / "anki-hsk-creator-data"
|
||||
DATA_FOLDER = Path(os.environ.get("DATA_FOLDER", default_path))
|
||||
DATA_FOLDER.mkdir(exist_ok=True, parents=True)
|
||||
default_gwen = Path.home() / "Gwen"
|
||||
GWEN_FOLDER = Path(os.environ.get("GWEN_FOLDER", default_gwen))
|
||||
GWEN_FOLDER.mkdir(exist_ok=True, parents=True)
|
||||
|
||||
@@ -5,15 +5,36 @@ from pathlib import Path
|
||||
|
||||
# Local
|
||||
from .api import (
|
||||
folder_proccess,
|
||||
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 DICT_TYPE, LANGUAGES, PHRASES_TYPE
|
||||
from .utility import ProcessFile
|
||||
from .constants import (
|
||||
COMPLETED_TYPE,
|
||||
DICTATION_TYPE,
|
||||
DICT_TYPE,
|
||||
INPUT,
|
||||
LANGUAGES,
|
||||
PHRASES_TYPE,
|
||||
)
|
||||
from .utility import ProcessFile, ProcessFolder
|
||||
|
||||
|
||||
def cli_choose_work_type() -> str:
|
||||
"""entry point for interactive interface"""
|
||||
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"
|
||||
)
|
||||
return option
|
||||
|
||||
|
||||
def cli_select_files() -> ProcessFile:
|
||||
@@ -37,6 +58,30 @@ def cli_select_files() -> ProcessFile:
|
||||
return input_file
|
||||
|
||||
|
||||
def cli_select_folder() -> ProcessFolder:
|
||||
"""Loops until it finds a valid input_folder"""
|
||||
print("Select data folder:")
|
||||
in_folder = None
|
||||
level = Path()
|
||||
while not in_folder:
|
||||
files = list_input_files(level)
|
||||
for n, file in enumerate(files):
|
||||
print(f"{n+1} - {file}")
|
||||
s = None
|
||||
while not s or not s.isnumeric() or not 1 <= int(s) <= len(files):
|
||||
s = input(f"Please select the file [1-{len(files)}]: ")
|
||||
selected = files[int(s) - 1]
|
||||
print(f"Selected {selected}")
|
||||
s = None
|
||||
while s not in ("yes", "y", "no", "n"):
|
||||
s = input("if this the folder? (yes / no, go inside) : ")
|
||||
if s in ("yes", "y"):
|
||||
in_folder = selected
|
||||
else:
|
||||
level = selected
|
||||
return select_folder(in_folder)
|
||||
|
||||
|
||||
def cli_select_dictionay_tsv() -> bool:
|
||||
"""If a dictionary file is selected, ask if the user wants to proccess it"""
|
||||
s = None
|
||||
@@ -63,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
|
||||
|
||||
@@ -73,23 +119,61 @@ def cli_select_language(languages: list = None) -> str:
|
||||
def main():
|
||||
"""CLI interface for the module"""
|
||||
while True:
|
||||
option = cli_choose_work_type()
|
||||
if option == "e":
|
||||
break
|
||||
elif option == "s":
|
||||
input_file = cli_select_files()
|
||||
if DICT_TYPE in input_file.input_file.suffixes:
|
||||
if DICT_TYPE is input_file.file_type:
|
||||
dict_selected = cli_select_dictionay_tsv()
|
||||
if dict_selected:
|
||||
language_id = cli_select_language()
|
||||
print(
|
||||
f"pre-processing {DICT_TYPE} file {input_file} with language {language_id}"
|
||||
)
|
||||
pre_process_a_dictionary_file(input_file, language_id)
|
||||
else:
|
||||
print(
|
||||
f"Processing {DICT_TYPE} file {input_file} with language {language_id}"
|
||||
)
|
||||
language_id = cli_select_language(
|
||||
input_file.available_dictionary_languages
|
||||
)
|
||||
process_a_dictionary_file(input_file, language_id)
|
||||
elif PHRASES_TYPE in input_file.input_file.suffixes:
|
||||
elif PHRASES_TYPE is input_file.file_type:
|
||||
language_id = cli_select_language()
|
||||
print(
|
||||
f"processing file {input_file.input_file} with language {language_id}"
|
||||
f"processing {PHRASES_TYPE} file {input_file} with language {language_id}"
|
||||
)
|
||||
process_a_phrases_file(input_file, language_id)
|
||||
elif DICTATION_TYPE is input_file.file_type:
|
||||
language_id = cli_select_language()
|
||||
print(
|
||||
f"processing {DICTATION_TYPE} file {input_file} with language {language_id}"
|
||||
)
|
||||
process_a_dictation_file(input_file, language_id)
|
||||
elif COMPLETED_TYPE is input_file.file_type:
|
||||
language_id = input_file.input_file.suffixes[1][1:]
|
||||
print(
|
||||
f"processing {COMPLETED_TYPE} file {input_file} with language {language_id}"
|
||||
)
|
||||
proccess_a_completed_file(input_file, language_id)
|
||||
else:
|
||||
print(f"File {input_file} is not recognised")
|
||||
elif option == "f":
|
||||
language_id = cli_select_language()
|
||||
langs = (
|
||||
LANGUAGES.AvailableLanguages if language_id == "all" else [language_id]
|
||||
)
|
||||
in_folder = cli_select_folder()
|
||||
for language_id in langs:
|
||||
print(f"Selected: {in_folder} with language {language_id}")
|
||||
for dirpath, dirnames, filenames in tuple(
|
||||
in_folder.absolute_input_folder.walk()
|
||||
)[1:]:
|
||||
if not dirnames and filenames:
|
||||
folder = select_folder(dirpath.relative_to(INPUT))
|
||||
folder_proccess(folder, language_id=language_id)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -12,7 +12,14 @@ from genanki import Deck, Model, Note, Package
|
||||
from pinyin_tone_converter.pinyin_tone_converter import PinyinToneConverter
|
||||
|
||||
# Local
|
||||
from .utility import DictionaryResult, ProcessFile, TranslationResult
|
||||
from .constants import COMPLETED_TYPE, DICTATION_TYPE, DICT_TYPE, PHRASES_TYPE
|
||||
from .utility import (
|
||||
DictionaryResult,
|
||||
ProcessFile,
|
||||
ProcessFolder,
|
||||
SimpleResult,
|
||||
TranslationResult,
|
||||
)
|
||||
|
||||
# Constants
|
||||
|
||||
@@ -136,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": (
|
||||
"<strong>{{Pinyin}}</strong>"
|
||||
"<br>{{Meaning}}"
|
||||
"<br>{{Audio}}"
|
||||
"<br>Simplified: {{type:Simplified}}"
|
||||
),
|
||||
"afmt": (
|
||||
"{{FrontSide}}<hr id='answer''><div class='simple'>{{Simplified}}</div>"
|
||||
),
|
||||
},
|
||||
{
|
||||
"name": "Card 2",
|
||||
"qfmt": (
|
||||
"<div class='simple'>{{Simplified}}</div>" "<br>Pinyin: {{type:Pinyin}}"
|
||||
),
|
||||
"afmt": (
|
||||
"{{FrontSide}}<hr id='answer'><strong>{{Pinyin}}</strong>"
|
||||
"<br>{{Meaning}}<br>{{Audio}}"
|
||||
),
|
||||
},
|
||||
{
|
||||
"name": "Card 3",
|
||||
"qfmt": ("{{Audio}}" "<br>Simplified: {{type:Simplified}}"),
|
||||
"afmt": (
|
||||
"{{FrontSide}}<hr id='answer'><strong>{{Pinyin}}</strong>"
|
||||
"<br><div class='simple'>{{Simplified}}</div>"
|
||||
),
|
||||
},
|
||||
],
|
||||
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:
|
||||
@@ -153,7 +237,6 @@ def output_anki_dictation(
|
||||
f"Deck for {final_file.name}, "
|
||||
"created in https://www.wolfang.info.ve/hskankicreator/",
|
||||
)
|
||||
package = Package(deck)
|
||||
audios = []
|
||||
for result in results:
|
||||
note = Note(
|
||||
@@ -166,6 +249,7 @@ def output_anki_dictation(
|
||||
)
|
||||
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
|
||||
@@ -236,3 +320,79 @@ def output_anki_phrase(
|
||||
package.media_files = audios
|
||||
package.write_to_file(final_file)
|
||||
return final_file
|
||||
|
||||
|
||||
def output_anki_package(
|
||||
process_folder: ProcessFolder, results: dict[ProcessFile, list[TranslationResult]]
|
||||
):
|
||||
final_file = process_folder.output_name.with_suffix(".apkg")
|
||||
decks = []
|
||||
audios = []
|
||||
for process_file, results in results.items():
|
||||
deck_name = "::".join(
|
||||
process_file.input_file.parts[:-1] + (process_file.output_name.stem,)
|
||||
)
|
||||
deck = Deck(
|
||||
random.randrange(1 << 30, 1 << 31),
|
||||
deck_name,
|
||||
f"Deck for {process_file.input_file}, "
|
||||
"created in https://www.wolfang.info.ve/hskankicreator/",
|
||||
)
|
||||
if process_file.file_type is DICTATION_TYPE:
|
||||
for result in results:
|
||||
note = Note(
|
||||
model=DICTATION_MODEL,
|
||||
fields=[
|
||||
result.translated,
|
||||
result.line,
|
||||
f"[sound:{result.audio_path.name}]",
|
||||
],
|
||||
)
|
||||
deck.add_note(note)
|
||||
audios.append(result.audio_path)
|
||||
elif process_file.file_type is DICT_TYPE:
|
||||
for result in results:
|
||||
note = Note(
|
||||
model=HSK_MODEL,
|
||||
fields=[
|
||||
# "\n ".join(f"{n+1}. {m}" for n, m in enumerate(result.meanings)),
|
||||
result.meaning,
|
||||
PinyinToneConverter().convert_text(result.pinyin),
|
||||
result.simplified,
|
||||
result.traditional,
|
||||
f"[sound:{result.audio_path.name}]",
|
||||
],
|
||||
)
|
||||
deck.add_note(note)
|
||||
audios.append(result.audio_path)
|
||||
elif process_file.file_type is PHRASES_TYPE:
|
||||
for result in results:
|
||||
note = Note(
|
||||
model=PHRASE_MODEL,
|
||||
fields=[
|
||||
result.translated,
|
||||
result.line,
|
||||
f"[sound:{result.audio_path.name}]",
|
||||
],
|
||||
)
|
||||
deck.add_note(note)
|
||||
audios.append(result.audio_path)
|
||||
elif process_file.file_type is COMPLETED_TYPE:
|
||||
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)
|
||||
decks.append(deck)
|
||||
if decks:
|
||||
package = Package(decks)
|
||||
package.media_files = audios
|
||||
package.write_to_file(final_file)
|
||||
return final_file
|
||||
|
||||
@@ -4,16 +4,20 @@ Interface for managuing and procesing files
|
||||
"""
|
||||
|
||||
# Standard Library
|
||||
import re
|
||||
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,
|
||||
@@ -23,12 +27,14 @@ from .constants import (
|
||||
RESOURCES,
|
||||
)
|
||||
from .proccessor import (
|
||||
completed_process,
|
||||
dictation_process,
|
||||
dictionary_bulk_process,
|
||||
dictionary_pre_process,
|
||||
dictionary_process,
|
||||
translator_process,
|
||||
)
|
||||
from .utility import CCCEDICT, TRANS, TTS, ProcessFile
|
||||
from .utility import CCCEDICT, TRANS, TTS, ProcessFile, ProcessFolder
|
||||
|
||||
# interface
|
||||
|
||||
@@ -80,6 +86,14 @@ def select_file(file_path: Path) -> ProcessFile:
|
||||
raise ValueError(f"{file_path} is not a file")
|
||||
|
||||
|
||||
def select_folder(file_path: Path) -> ProcessFile:
|
||||
"""Given a relative path from `list_input_files`, return a ProcessFile"""
|
||||
if (INPUT / file_path).is_dir():
|
||||
return ProcessFolder(file_path)
|
||||
else:
|
||||
raise ValueError(f"{file_path} is not a folder")
|
||||
|
||||
|
||||
def create_folder(file_path: Path) -> ProcessFile:
|
||||
"""Creates a folder in a file_path"""
|
||||
input_folder = INPUT / file_path
|
||||
@@ -221,7 +235,9 @@ def process_a_dictation_file(process_file: ProcessFile, language_id: str) -> Pat
|
||||
with process_file.absolute_input_file.open(
|
||||
"r", encoding="utf8", newline="\n"
|
||||
) as file:
|
||||
text_lines = [line.strip() for line in file.read().split("。")]
|
||||
text = file.read().strip()
|
||||
result = re.split(r"[!?。;]*", text)
|
||||
text_lines = [line.strip() for line in result if line.strip()]
|
||||
results = dictation_process(text_lines, process_file)
|
||||
return output_anki_dictation(process_file, results)
|
||||
|
||||
@@ -234,6 +250,73 @@ def process_a_phrases_file(process_file: ProcessFile, language_id: str) -> Path:
|
||||
with process_file.absolute_input_file.open(
|
||||
"r", encoding="utf8", newline="\n"
|
||||
) as file:
|
||||
text_lines = [line.strip() for line in file.readlines()]
|
||||
text_lines = [line.strip() for line in file.readlines() if line.strip()]
|
||||
results = translator_process(text_lines, process_file)
|
||||
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
|
||||
print(f"Proccesing folder {process_folder} with language {language_id}")
|
||||
final_file = process_folder.output_name.with_suffix(".apkg")
|
||||
if final_file.is_file():
|
||||
print("File already proccessed")
|
||||
return final_file
|
||||
TTS.create_tts()
|
||||
TRANS.create_translator(LANGUAGES.CN, language_id)
|
||||
CCCEDICT.create_cedict(language_id)
|
||||
results = {}
|
||||
for process_file in process_folder.input_files:
|
||||
print(f"Proccessing {process_file}")
|
||||
try:
|
||||
if process_file.file_type is DICT_TYPE:
|
||||
with process_file.absolute_input_file.open(
|
||||
"r", encoding="utf8", newline="\n"
|
||||
) as file:
|
||||
words_list = [word.strip() for word in file.readlines() if word]
|
||||
dictionary_bulk_process(words_list, process_file)
|
||||
results[process_file] = dictionary_process(process_file)
|
||||
elif process_file.file_type is DICTATION_TYPE:
|
||||
with process_file.absolute_input_file.open(
|
||||
"r", encoding="utf8", newline="\n"
|
||||
) as file:
|
||||
text_lines = [
|
||||
line.strip() for line in file.read().split("。") if line.strip()
|
||||
]
|
||||
results[process_file] = dictation_process(text_lines, process_file)
|
||||
elif process_file.file_type is PHRASES_TYPE:
|
||||
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] = translator_process(text_lines, process_file)
|
||||
elif process_file.file_type is COMPLETED_TYPE:
|
||||
completed_language_id = process_file.input_file.suffixes[1][1:]
|
||||
if completed_language_id != language_id:
|
||||
continue
|
||||
process_file.language_id = completed_language_id
|
||||
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)
|
||||
except (AttributeError, AssertionError):
|
||||
print(f"Error procesing {process_file} with language {language_id}")
|
||||
continue
|
||||
return output_anki_package(process_folder, results)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import importlib.resources
|
||||
|
||||
# Local
|
||||
from . import DATA_FOLDER
|
||||
from . import DATA_FOLDER, GWEN_FOLDER
|
||||
|
||||
# Resources
|
||||
CCCEDICT_PATH = importlib.resources.files("anki_hsk_creator").joinpath("cedict_ts.u8")
|
||||
@@ -16,16 +16,14 @@ OUTPUT = DATA_FOLDER / "output"
|
||||
OUTPUT.mkdir(exist_ok=True, parents=True)
|
||||
RESOURCES = DATA_FOLDER / "resources"
|
||||
RESOURCES.mkdir(exist_ok=True, parents=True)
|
||||
GWEN_TTS = GWEN_FOLDER / "Qwen3-TTS-12Hz-0.6B-CustomVoice"
|
||||
|
||||
# File Types
|
||||
PHRASES_TYPE = ".phrases"
|
||||
DICT_TYPE = ".dictionary"
|
||||
DICTATION_TYPE = ".dictation"
|
||||
COMPLETED_TYPE = ".completed"
|
||||
|
||||
# time dalays
|
||||
WORD = 0.1
|
||||
COMMA = 0.5
|
||||
POINT = 1.0
|
||||
|
||||
class LANGUAGES:
|
||||
"""Available laguages for translation"""
|
||||
@@ -37,8 +35,11 @@ class LANGUAGES:
|
||||
RU = "ru"
|
||||
TR = "tr"
|
||||
TH = "th"
|
||||
|
||||
AvailableLanguages = (EN, ES, FR, RU, TR, TH)
|
||||
JA = "ja"
|
||||
KO = "ko"
|
||||
PB = "pb"
|
||||
VI = "vi"
|
||||
AvailableLanguages = (ES, EN, FR, RU, TR, TH, JA, KO, PB, VI)
|
||||
LanguageNames = {
|
||||
EN: "English",
|
||||
ES: "Spanish",
|
||||
@@ -46,4 +47,8 @@ class LANGUAGES:
|
||||
RU: "Russian",
|
||||
TR: "Turkish",
|
||||
TH: "Thai",
|
||||
JA: "Japanese",
|
||||
KO: "Korean",
|
||||
PB: "Portuguese (Brazil)",
|
||||
VI: "Vietnamese",
|
||||
}
|
||||
|
||||
@@ -2,18 +2,26 @@
|
||||
|
||||
# Standard Library
|
||||
import csv
|
||||
import uuid
|
||||
|
||||
# Pip
|
||||
import argostranslate.translate
|
||||
import torchaudio
|
||||
import soundfile as sf
|
||||
|
||||
# Local
|
||||
from .constants import LANGUAGES, WORD, COMMA
|
||||
from .utility import CCCEDICT, TTS, DictionaryResult, ProcessFile, TranslationResult
|
||||
from .constants import LANGUAGES
|
||||
from .utility import (
|
||||
CCCEDICT,
|
||||
TTS,
|
||||
DictionaryResult,
|
||||
ProcessFile,
|
||||
SimpleResult,
|
||||
TranslationResult,
|
||||
)
|
||||
|
||||
# Constants
|
||||
|
||||
FIELDNAMES = ["simplified", "traditional", "pinyin", "meaning"]
|
||||
FIELDNAMES = ["n", "simplified", "traditional", "pinyin", "meaning"]
|
||||
DIALECT = "excel-tab"
|
||||
|
||||
# Results Classes
|
||||
@@ -25,16 +33,15 @@ def dictation_process(
|
||||
"""Process for Dictation translation"""
|
||||
results = []
|
||||
for n, line in enumerate(text_lines):
|
||||
line = line.strip()
|
||||
line = " ".join(line.split())
|
||||
audio_line = f"[pause:{WORD}s]".join(line)
|
||||
audio_line = audio_line.replace(",", f",[pause:{COMMA}s]")
|
||||
audio_path = process_file.resources / f"N{n:03n}.wav"
|
||||
audio_line = line.strip()
|
||||
posible_name = tuple(process_file.resources.glob(f"N{n:03n}-*.wav"))
|
||||
if posible_name:
|
||||
audio_path = posible_name[0]
|
||||
else:
|
||||
audio_path = process_file.resources / f"N{n:03n}-{uuid.uuid1()}.wav"
|
||||
if not audio_path.exists():
|
||||
audio = TTS.MODEL.generate(
|
||||
f"{line}。", language_id=LANGUAGES.CN, **TTS.DEFAULTS
|
||||
)
|
||||
torchaudio.save(audio_path, audio, TTS.MODEL.sr)
|
||||
wavs, sr = TTS.generate(f"{audio_line}。")
|
||||
sf.write(audio_path, wavs[0], sr)
|
||||
translated = argostranslate.translate.translate(
|
||||
line, LANGUAGES.CN, process_file.language_id
|
||||
)
|
||||
@@ -50,16 +57,15 @@ def translator_process(
|
||||
"""Process for phases or sentence translation"""
|
||||
results = []
|
||||
for n, line in enumerate(text_lines):
|
||||
line = line.strip()
|
||||
line = " ".join(line.split())
|
||||
audio_line = "[pause:0.1s]".join(line)
|
||||
audio_line = audio_line.replace(",", ",[pause:0.5s]")
|
||||
audio_path = process_file.resources / f"N{n:03n}.wav"
|
||||
audio_line = line.strip()
|
||||
posible_name = tuple(process_file.resources.glob(f"N{n:03n}-*.wav"))
|
||||
if posible_name:
|
||||
audio_path = posible_name[0]
|
||||
else:
|
||||
audio_path = process_file.resources / f"N{n:03n}-{uuid.uuid1()}.wav"
|
||||
if not audio_path.exists():
|
||||
audio = TTS.MODEL.generate(
|
||||
f"{audio_line}。", language_id=LANGUAGES.CN, **TTS.DEFAULTS
|
||||
)
|
||||
torchaudio.save(audio_path, audio, TTS.MODEL.sr)
|
||||
wavs, sr = TTS.generate(f"{audio_line}。")
|
||||
sf.write(audio_path, wavs[0], sr)
|
||||
translated = argostranslate.translate.translate(
|
||||
line, LANGUAGES.CN, process_file.language_id
|
||||
)
|
||||
@@ -69,6 +75,80 @@ 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=""
|
||||
) as resource_file:
|
||||
tsv_writer = csv.DictWriter(
|
||||
resource_file, dialect=DIALECT, fieldnames=FIELDNAMES
|
||||
)
|
||||
tsv_writer.writeheader()
|
||||
number = 1
|
||||
for words in words_list:
|
||||
word = words.split()[0]
|
||||
hint = words.split(maxsplit=1)[1] if len(words.split()) > 1 else None
|
||||
entries_en = dictionary_en.get(word)
|
||||
entries = dictionary.get(word)
|
||||
if entries:
|
||||
all_meanings = [
|
||||
meaning for entry in entries for meaning in entry.meanings
|
||||
]
|
||||
pos_meanings = [
|
||||
meaning
|
||||
for entry, entry_en in zip(entries, entries_en)
|
||||
for meaning, meaning_en in zip(entry.meanings, entry_en.meanings)
|
||||
if hint and (hint in meaning_en or hint in meaning)
|
||||
]
|
||||
pos_meanings = pos_meanings or all_meanings
|
||||
if not pos_meanings:
|
||||
raise ValueError(
|
||||
f"Not menaing found for hint {hint}, {all_meanings}"
|
||||
)
|
||||
meanings_text = "\n".join(
|
||||
f"{n+1}: {meaning}" for n, meaning in enumerate(pos_meanings)
|
||||
)
|
||||
pos_entries = []
|
||||
for meaning in pos_meanings:
|
||||
for ent in entries:
|
||||
if meaning in ent.meanings:
|
||||
pos_entries.append(ent)
|
||||
entry = pos_entries[0]
|
||||
tsv_writer.writerow(
|
||||
{
|
||||
"n": number,
|
||||
"simplified": entry.simplified,
|
||||
"traditional": entry.traditional,
|
||||
"pinyin": entry.pinyin,
|
||||
"meaning": meanings_text,
|
||||
}
|
||||
)
|
||||
else:
|
||||
print("============================================")
|
||||
print(f"===================>ERROR: {word} not found")
|
||||
print("============================================")
|
||||
number += 1
|
||||
|
||||
|
||||
def dictionary_pre_process(words_list: list[str], process_file: ProcessFile):
|
||||
"""Pre Process dictionary files into a intermediary resources file"""
|
||||
dictionary = CCCEDICT.create_cedict(process_file.language_id)
|
||||
@@ -79,18 +159,32 @@ def dictionary_pre_process(words_list: list[str], process_file: ProcessFile):
|
||||
resource_file, dialect=DIALECT, fieldnames=FIELDNAMES
|
||||
)
|
||||
tsv_writer.writeheader()
|
||||
n = 1
|
||||
for words in words_list:
|
||||
word = words.split()[0]
|
||||
pinyin = " ".join(words.split()[1:]) if len(words.split()) > 1 else None
|
||||
hint = " ".join(words.split()[1:]) if len(words.split()) > 1 else None
|
||||
if entries := dictionary.get(word):
|
||||
if pinyin is not None:
|
||||
entries = list(filter(lambda x: x.pinyin == pinyin, entries))
|
||||
# if pinyin is not None:
|
||||
# filtered_entries = tuple(filter(lambda x: x.pinyin == pinyin, entries))
|
||||
if len(entries) > 1:
|
||||
print(f"\nWARNING: {word} has multiple meanings:")
|
||||
for entry in entries:
|
||||
pos_meanings = (
|
||||
[
|
||||
entry
|
||||
for entry in entries
|
||||
for meaning in entry.meaning
|
||||
if hint in meaning
|
||||
]
|
||||
if hint
|
||||
else []
|
||||
)
|
||||
pos_entries = pos_meanings or entries
|
||||
for entry in pos_entries:
|
||||
for meaning in entry.meanings:
|
||||
if pos_meanings and hint in meaning or not pos_meanings:
|
||||
tsv_writer.writerow(
|
||||
{
|
||||
"n": n,
|
||||
"simplified": entry.simplified,
|
||||
"traditional": entry.traditional,
|
||||
"pinyin": entry.pinyin,
|
||||
@@ -103,12 +197,14 @@ def dictionary_pre_process(words_list: list[str], process_file: ProcessFile):
|
||||
print("============================================")
|
||||
tsv_writer.writerow(
|
||||
{
|
||||
"n": number,
|
||||
"simplified": word,
|
||||
"traditional": None,
|
||||
"pinyin": None,
|
||||
"meaning": None,
|
||||
}
|
||||
)
|
||||
n += 1
|
||||
|
||||
|
||||
def dictionary_process(process_file: ProcessFile) -> list[DictionaryResult]:
|
||||
@@ -121,11 +217,8 @@ def dictionary_process(process_file: ProcessFile) -> list[DictionaryResult]:
|
||||
for line in reader:
|
||||
audio_path = process_file.resources / f"{line['pinyin']}.wav"
|
||||
if not audio_path.exists():
|
||||
audio = TTS.MODEL.generate(
|
||||
f"{line['simplified']}。", language_id=LANGUAGES.CN, **TTS.DEFAULTS
|
||||
)
|
||||
torchaudio.save(audio_path, audio, TTS.MODEL.sr)
|
||||
print(line)
|
||||
wavs, sr = TTS.generate(f"{line['simplified']}。")
|
||||
sf.write(audio_path, wavs[0], sr)
|
||||
result = DictionaryResult(
|
||||
**line, audio_path=audio_path, language_id=process_file.language_id
|
||||
)
|
||||
|
||||
@@ -5,6 +5,7 @@ Static clasess and functions for general use
|
||||
"""
|
||||
|
||||
# Standard Library
|
||||
import random
|
||||
from pathlib import Path
|
||||
|
||||
# Pip
|
||||
@@ -12,10 +13,21 @@ import argostranslate.package
|
||||
import argostranslate.translate
|
||||
import torch
|
||||
from cedict_utils.cedict import CedictEntry, CedictParser
|
||||
from chatterbox.mtl_tts import ChatterboxMultilingualTTS
|
||||
from qwen_tts import Qwen3TTSModel
|
||||
|
||||
# Local
|
||||
from .constants import CCCEDICT_PATH, INPUT, LANGUAGES, OUTPUT, RESOURCES
|
||||
from .constants import (
|
||||
CCCEDICT_PATH,
|
||||
COMPLETED_TYPE,
|
||||
DICTATION_TYPE,
|
||||
DICT_TYPE,
|
||||
GWEN_TTS,
|
||||
INPUT,
|
||||
LANGUAGES,
|
||||
OUTPUT,
|
||||
PHRASES_TYPE,
|
||||
RESOURCES,
|
||||
)
|
||||
|
||||
# Static Clases
|
||||
|
||||
@@ -42,21 +54,38 @@ class TRANS:
|
||||
TRANS.PACKAGES,
|
||||
)
|
||||
)
|
||||
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:
|
||||
# Single package between 2 languages
|
||||
print(
|
||||
f"Installing package {in_package.from_code}"
|
||||
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:
|
||||
print(
|
||||
f"Check in_package {in_package.from_code} {in_package.to_code}"
|
||||
f"Installing in_package {in_package.from_code}"
|
||||
f"->{in_package.to_code}"
|
||||
)
|
||||
print(
|
||||
f"Check out_package {out_package.from_code} {out_package.to_code}"
|
||||
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)
|
||||
ready = True
|
||||
break
|
||||
for package in packages_to_install:
|
||||
print(f"instaling package {package}")
|
||||
argostranslate.package.install_from_path(package.download())
|
||||
@@ -88,13 +117,13 @@ class TranslatedEntry:
|
||||
@property
|
||||
def meanings(self):
|
||||
"""Entry translated meaning list"""
|
||||
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
|
||||
)
|
||||
print(f"{meaning}-> {trans_meaning}")
|
||||
else:
|
||||
trans_meaning = meaning
|
||||
self._translated_meanings.append(trans_meaning)
|
||||
@@ -137,7 +166,12 @@ class TTS:
|
||||
|
||||
MODEL = None
|
||||
DEVICE = None
|
||||
DEFAULTS = {"cfg_weight": 0.2, "exaggeration": 0.8}
|
||||
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():
|
||||
@@ -145,20 +179,86 @@ class TTS:
|
||||
if TTS.DEVICE is None:
|
||||
# Automatically detect the best available device
|
||||
if torch.cuda.is_available():
|
||||
TTS.DEVICE = "cuda"
|
||||
TTS.DEVICE = "cuda:0"
|
||||
elif torch.backends.mps.is_available():
|
||||
TTS.DEVICE = "mps"
|
||||
else:
|
||||
TTS.DEVICE = "cpu"
|
||||
if TTS.MODEL is None:
|
||||
TTS.MODEL = ChatterboxMultilingualTTS.from_pretrained(
|
||||
device=TTS.DEVICE, t3_model="v3"
|
||||
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, COMPLETED_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
|
||||
|
||||
@@ -171,9 +271,31 @@ 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):
|
||||
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
|
||||
elif COMPLETED_TYPE in self.input_file.suffixes:
|
||||
return COMPLETED_TYPE
|
||||
else:
|
||||
raise ValueError("File type not recognized")
|
||||
|
||||
@property
|
||||
def absolute_input_file(self):
|
||||
@@ -182,7 +304,7 @@ class ProcessFile:
|
||||
|
||||
@property
|
||||
def language_id(self):
|
||||
"""language for this trasnlation process"""
|
||||
"""language for this trasnlation proccess"""
|
||||
return self._language_id
|
||||
|
||||
@language_id.setter
|
||||
@@ -194,6 +316,8 @@ class ProcessFile:
|
||||
"""Posible name for the output file, still missing the filetype"""
|
||||
if self.language_id is None:
|
||||
raise ValueError("Not a valid language selected")
|
||||
if self.file_type is COMPLETED_TYPE:
|
||||
return self.out_folder / f"{self.input_file.stem}.temp"
|
||||
return self.out_folder / f"{self.input_file.stem}.{self.language_id}.temp"
|
||||
|
||||
@property
|
||||
@@ -212,6 +336,19 @@ class ProcessFile:
|
||||
"""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 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"""
|
||||
@@ -234,6 +371,7 @@ class DictionaryResult:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
n: str,
|
||||
language_id: str,
|
||||
simplified: str,
|
||||
traditional: str,
|
||||
@@ -241,6 +379,7 @@ class DictionaryResult:
|
||||
meaning: str,
|
||||
audio_path: Path,
|
||||
):
|
||||
self.n = n
|
||||
self.language_id = language_id
|
||||
self.simplified = simplified
|
||||
self.traditional = traditional
|
||||
|
||||
Reference in New Issue
Block a user