Files
tudien/bin/convert_all.py
2026-10-02 22:54:53 +08:00

960 lines
38 KiB
Python
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Script to build all dictionaries with all formats
Usage:
./bin/convert_all.py --input_folder=./dict --output_folder=./output --extension=tab
"""
import argparse
import glob
import os
import re
import shlex
import shutil
import subprocess
from fileinput import filename
from pathlib import Path
from dict_summary import parse_toml_file
from iso_language_codes import language_name
# List of dictionary formats to generate using pyglossary
DICT_FORMATS = [
# DictType, foldername, file ending, need zip
# DictType is the pyglossary write-format name
("Yomichan", "yomitan", "yomitan.zip", False),
("Epub2", "epub", "epub", False),
("Kobo", "kobo", "kobo.zip", False),
("Stardict", "stardict", "ifo", True),
("DictOrg", "dictd", "index", True),
("Aard2Slob", "aard", "slob", False),
]
# List of directory formats and their file patterns
DIR_FORMATS = [
# (Dir, File pattern)
("stardict", "*.stardict.zip"),
("epub", "*.epub"),
("kobo", "*.kobo.zip"),
("lingvo", "*.dsl.dz"),
("kindle", "*.mobi"),
("dictd", "*.dictd.zip"),
("yomitan", "*.yomitan.zip"),
("mdict", "*.mdx"),
("pleco", "*.pleco.zip"),
("aard", "*.slob"),
("dic", "*.dic"),
# ("xdxf", "*.xdxf.zip"),
]
def execute_shell(cmd_line, message="", printout=True):
"""
Executes a shell command and handles any errors that occur during execution.
Parameters:
cmd_line (str): The command line string to be executed.
message (str): An optional message to include in error output if the command fails.
printout (bool): If True, prints the command line before execution.
Returns:
bool: True if the command executes successfully, False if an error occurs.
""" # noqa: D401
try:
if printout:
print(cmd_line)
# subprocess.run(shlex.split(cmd_line), check=True)
subprocess.call(cmd_line, shell=True)
return True
except subprocess.CalledProcessError as e:
if message:
print(f"Error {message}: {e}")
else:
print(f"Error executing shell: {e}")
return False
DEBUG_FLAG = False
def escape_forbidden_chars(text, forbidden_chars=r" (){}[]$*?^|<>\\"):
"""
Escapes forbidden characters in a given text.
Args:
text (str): The input string to process.
forbidden_chars (str): A string containing characters to escape (default: common special chars).
Returns:
str: The text with forbidden characters escaped.
"""
# Create a regex pattern to match any of the forbidden characters
pattern = f"[{re.escape(forbidden_chars)}]"
# Escape each match by prefixing it with a backslash
escaped_text = re.sub(pattern, r"\\\g<0>", text)
return escaped_text
# Example usage
def gen_mdict_target(filepath, filebase, output_folder, dataName, dataDescription):
"""
Generate MDict source files (*.title.html, *.description.html, *.txt) and build an MDX dictionary.
This function creates the necessary metadata and definition files for building an MDict (.mdx) file
from a tab-separated input text file. Each valid line in the input must contain exactly one tab:
`<headword>\t<definition>`. The function then calls the `mdict` command-line tool to produce the MDX file.
Parameters:
filepath (str): Path to the input text file containing dictionary entries.
Each line must have one headword and one definition separated by a tab.
filebase (str): Base filename (without extension) used for generated output files.
output_folder (str): Directory where all output files will be created.
dataName (str): Title of the dictionary, written to `<filebase>.title.html`.
dataDescription (str): Description of the dictionary, written to `<filebase>.description.html`.
Output Files Generated (in output_folder):
<filebase>.title.html Contains the dictionary title.
<filebase>.description.html Contains the dictionary description.
<filebase>.txt Contains formatted entries for MDict (headword + definition).
<filebase>.mdx Final MDict dictionary output (built by `mdict` CLI).
Behavior:
• Skips empty lines.
• Skips lines without exactly one tab, printing a warning for each skipped line.
• Writes entries to the .txt file in the MDict expected format:
headword
definition
</>
• Executes the `mdict` tool to build the `.mdx` file.
Returns:
Result of the `execute_shell()` call that runs the MDX generation command.
"""
title_filepath = os.path.join(output_folder, filebase + ".title.html")
with open(title_filepath, "w", encoding="utf-8") as file:
file.write(dataName)
desc_filepath = os.path.join(output_folder, filebase + ".description.html")
with open(desc_filepath, "w", encoding="utf-8") as file:
file.write(dataDescription)
def_filepath = os.path.join(output_folder, filebase + ".txt")
dict_filepath = os.path.join(output_folder, filebase + ".mdx")
print(f"{filepath} - {output_folder} - {dict_filepath}")
with open(def_filepath, "w", encoding="utf-8") as outfile:
with open(filepath, "r", encoding="utf-8") as file:
for line in file:
line = line.strip() # remove trailing newline
if not line:
continue
if line.count("\t") != 1:
print(f"Warning: line in {filepath} has no tab or multiple tabs, skipping: {line!r}")
continue
# assert(line.count("\t") == 1)
headword, definition = line.split("\t") # split by tab
outfile.write(f"{headword}\n{definition}\n</>\n")
cmd_line = (
f"mdict --title {title_filepath} --description {desc_filepath} -a {def_filepath} {dict_filepath}" # noqa: E501
)
return execute_shell(cmd_line=cmd_line, message=f"generating MDict MDX")
import re
def split_long_text_file(
input_path,
output_path=None,
max_len=800
):
item_pattern = re.compile(r'(?=\b\d+\. )')
ascii_word = re.compile(r'[A-Za-z0-9_]')
zh_punct = '。;,'
output_lines = []
def split_by_length(text):
chunks = []
while len(text) > max_len:
cut = -1
# 1. Try space (English)
cut = text.rfind(' ', 0, max_len)
# 2. Try Chinese punctuation
if cut == -1:
for p in zh_punct:
pos = text.rfind(p, 0, max_len)
if pos > cut:
cut = pos + 1 # include punctuation
# 3. Smart fallback
if cut == -1:
cut = max_len
# Avoid breaking ASCII words / tags
if (
cut < len(text)
and ascii_word.match(text[cut - 1])
and ascii_word.match(text[cut])
):
forward = re.search(r'\s', text[cut:cut + 100])
if forward:
cut += forward.start()
chunks.append(text[:cut].rstrip())
text = text[cut:].lstrip()
if text:
chunks.append(text)
return chunks
with open(input_path, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
if not line:
continue
items = item_pattern.split(line)
for item in items:
item = item.strip()
if not item:
continue
output_lines.extend(split_by_length(item))
if output_path:
with open(output_path, 'w', encoding='utf-8') as f:
f.write('\n'.join(output_lines))
return output_lines
def main() -> None:
"""Main entry point for converting dictionary files into multiple formats.
This function processes dictionary files and their metadata to convert them into various
dictionary formats including StarDict, EPUB, Kobo, Lingvo, Kindle, DictD, Yomitan, and MDict.
Args:
None, but accepts command line arguments:
-i, --input_folder: Input folder containing dictionary files (default: 'dict')
-o, --output_folder: Output folder for converted dictionaries (default: 'output')
-e, --extension: File extension for input dictionary files (default: 'tab')
-m, --metadata: File extension for input metadata files (default: 'toml')
-f, --filter: Optional comma-separated list of dictionary keys to filter
Returns:
None
The function performs the following operations:
1. Parses command line arguments
2. Searches for dictionary and metadata files
3. Creates necessary output directories
4. Converts dictionaries to multiple formats:
- Kindle (MOBI)
- Pleco (TXT)
- Lingvo (DSL)
- MDict (MDX)
- Other formats specified in DICT_FORMATS
5. Creates ZIP archives of converted dictionaries
Note:
Requires various external dependencies and shell commands for dictionary conversion.
Temporary files are cleaned up during processing to save space.
"""
parser = argparse.ArgumentParser(description="Convert all dictionaries in a folder")
parser.add_argument("-i", "--input_folder", default="dict", help="Input folder containing .tsv and .dfo files")
parser.add_argument("-o", "--output_folder", default="output", help="Output folder containing dictionary files")
parser.add_argument("-e", "--extension", default="tab", help="Filename extention for input dictionary files. Default is .tab")
parser.add_argument("-m", "--metadata", default="toml", help="Filename extention for input metadata for dictionary. Default is .dfo")
parser.add_argument("-f", "--filter", help="Filter only dictionary entries with matching keys (seperated by comma)")
args = parser.parse_args()
input_folder = Path(escape_forbidden_chars(args.input_folder))
output_folder = Path(escape_forbidden_chars(args.output_folder))
extension = args.extension
metadata = args.metadata
dict_filters = args.filter.split(",") if args.filter is not None else []
print(f"Arguments: {args}")
metafilelist = []
datafilelist = []
metafilelist, datafilelist = search_data_files(input_folder, extension, metadata, dict_filters)
# dirs = ["stardict", "epub", "kobo", "lingvo", "kindle", "dictd", "yomitan", "mdict"]
output_kindle = output_folder / "kindle"
output_mdict = output_folder / "mdict"
dict_kindle = input_folder / "kindle"
# cmd_line = f"mkdir -p {input_folder}/kindle"
# execute_shell(cmd_line=cmd_line, message="Creating directory")
# subprocess.call(f'rm -r {output_folder}/*', shell=True)
dict_kindle.mkdir(parents=True, exist_ok=True)
# cmd_line = f"rm -r {output_folder}/*"
# execute_shell(cmd_line=cmd_line, message="Remove existing file in {output_folder}")
shutil.rmtree(output_folder, ignore_errors=True)
# cmd_line = f"rm -r {input_folder}/kindle/*"
# execute_shell(cmd_line=cmd_line, message="Remove existing file in kindle {input_folder}/kindle")
shutil.rmtree(output_kindle, ignore_errors=True)
for dir, _ in DIR_FORMATS:
sub_path = Path(output_folder / dir)
sub_path.mkdir(parents=True, exist_ok=True)
# cmd_line = f"mkdir -p {output_folder}/{dir}"
# execute_shell(cmd_line=cmd_line, message="Creating directory")
# subprocess.call(f'mkdir -p {output_folder}/{dir}', shell=True)
# use_only_these = {'Tu-dien-ThienChuu-TranVanChanh'}
for filepath, datafile in zip(metafilelist, datafilelist):
_, filename = os.path.split(filepath)
filebase, fileext = os.path.splitext(filename)
data = parse_toml_file(filepath)
if not data:
continue
# Add quote to wrap long filename/path
#datafile = datafile.replace(" ", "\\ ") # safe = shlex.quote(str(datafile))
dataCreator = data["Owner_Editor"].replace(" ", "\\ ")
if not dataCreator:
dataCreator = "Panthera Tigris".replace(" ", "\\ ")
dataTarget = data["Target"]
dataSource = data["Source"]
dataDescription = data["Description"]
dataFullSource = language_name(data["Source"])
dataFullTarget = language_name(data["Target"])
dataName = escape_forbidden_chars(data["Name"])
INFLECTION_DIR = "./bin/inflections"
INFLECTION_NONE = "inflections-none.tab"
# Generare HTML file for Kindle dictionary
if "Inflections" in data and data["Inflections"]:
inflections = f'"{INFLECTION_DIR}/{data["Inflections"]}"'
else:
inflections = f"{INFLECTION_DIR}/{INFLECTION_NONE}"
# Generate other dictionary formats using PyGlossary
for write_format, folder, ext, needzip in DICT_FORMATS:
_, filename = os.path.split(filepath)
filebase, _ = os.path.splitext(filename)
build_dict_many(output_folder, ext, datafile, filebase, dataTarget, dataSource, dataName, write_format, folder, needzip)
# Must run after dictd was created
build_dict_pocketbook(output_folder, filebase)
# cmd_line = f"ls -llR {output_folder}"
# execute_shell(cmd_line=cmd_line, message=f"Listing all files in {output_folder}")
build_dict_mobi(input_folder, output_folder, datafile, filebase, inflections, dataCreator, dataTarget, dataSource, dataName)
build_dict_pleco_txt(input_folder, output_folder, filebase)
build_dict_lingvo(input_folder, output_folder, datafile, filebase, dataCreator, dataFullTarget, dataFullSource, dataName)
build_dict_mdict(output_folder, datafile, filebase, dataDescription, dataName)
# if DEBUG_FLAG:
# continue
# Deletes src data file to save space
delete_file(f"{filebase}.txt", input_folder)
delete_file(f"{filebase}.{extension}", input_folder)
# cmd_line = f"rm {input_folder}/{filebase}.txt" # Still keeps .tab file for counting lines later
# execute_shell(cmd_line=cmd_line, message=f"Removes {filebase} input files to save space")
# cmd_line = f"ls -llR {output_folder}"
# execute_shell(cmd_line=cmd_line, message=f"Listing all files in {output_folder}")
for dir, format in DIR_FORMATS:
cmd_line = f"zip -9 -j {output_folder}/all-{dir}.zip {output_folder}/{format}"
execute_shell(cmd_line=cmd_line, message=f"Zipping all {dir}-format dicts in {output_folder}")
def delete_file(filename, input_folder=""):
"""
Delete a single file if it exists.
Parameters
----------
filename : str
Name of the file to delete.
input_folder : str, optional
Path to the folder containing the file (default is current directory).
Notes
-----
If the file does not exist, the function does nothing.
"""
file_path = Path(input_folder) / filename
if file_path.exists():
file_path.unlink()
def delete_file_pattern(filename, pattern, dir_path=""):
"""
Delete all files in a directory that match a filename pattern.
Parameters
----------
filename : str
Base name of the files to match.
pattern : str
Pattern appended to filename (e.g., "*.txt" or "_backup.*").
dir_path : str, optional
Directory to search for matching files (default is current directory).
Notes
-----
Only files are deleted. Subdirectories are ignored.
"""
dir = Path(dir_path)
for f in dir.glob(f"{filename}{pattern}"):
if f.is_file():
f.unlink()
def build_dict_many(output_folder, extension, datafile, filebase, dataTarget, dataSource, dataName, write_format, folder, needzip):
"""
Generate a dictionary file from a tab-delimited data source using pyglossary,
optionally compress the result into a ZIP archive, and move or clean up
intermediate files.
This function builds command lines for pyglossary and common shell utilities
(zip, rm, mv) and delegates execution to `execute_shell`. It is intended to
create a converted dictionary file at:
<output_folder>/<folder>/<filebase>.<extension>
or, when `needzip` is True, to create a ZIP at:
<output_folder>/<filebase>.<folder>.zip
Parameters
----------
output_folder : str
Path to the base output directory where resulting files or ZIPs will be
placed.
extension : str
File extension for the generated file (used in the output filename).
datafile : str
Path to the input data file (expected Tabfile format) to be converted.
filebase : str
Base name (without extension) for the generated file or ZIP.
dataTarget : str
Target language code passed to pyglossary via --target-lang.
dataSource : str
Source language code passed to pyglossary via --source-lang.
dataName : str
Dictionary name passed to pyglossary via --name.
pyglossary : str
Path to the pyglossary executable or a placeholder parameter. Note:
the current implementation does not use this parameter directly when
building the command line (pyglossary is invoked by literal name).
write_format : str
Output format name passed to pyglossary via --write-format.
folder : str
Subfolder inside `output_folder` where the non-zipped output is placed.
needzip : bool
If True, compress the generated output files into a ZIP and remove the
temporary generated files; if False, move the generated file(s) into
`output_folder`.
Returns
-------
None
Side effects
------------
- Executes shell commands via `execute_shell`:
- Runs pyglossary to convert the input file to the requested format.
- If `needzip` is True:
* Creates a ZIP containing the generated files and deletes the
temporary files produced in the subfolder.
Otherwise:
* Moves the produced file(s) from the subfolder into `output_folder`.
- Creates, moves, and deletes files on the filesystem.
Errors and exceptions
---------------------
- Any exception or error produced by `execute_shell` (or underlying shell
commands) will propagate to the caller.
- The function currently interpolates many values directly into shell
command strings. For safety, ensure that inputs are trusted or properly
sanitized to avoid shell injection vulnerabilities. Only `out_path` is
quoted with `shlex.quote` in the current implementation.
Notes
-----
- The function assumes `pyglossary` and standard Unix utilities (`zip`, `rm`,
`mv`) are available in PATH, or that `execute_shell` knows how to locate them.
- `pyglossary` parameter is accepted but not inserted into constructed
command lines in the current implementation; update the command
construction if you need to use a non-default pyglossary executable.
"""
out_path = os.path.join(output_folder, folder, f"{filebase}.{extension}")
cmd_line = (
f"pyglossary --ui=none --read-format=Tabfile --write-format={write_format} "
f"--source-lang={dataSource} --target-lang={dataTarget} --name={dataName} {datafile} {shlex.quote(out_path)}"
)
execute_shell(cmd_line=cmd_line, message=f"generating {write_format}")
if needzip:
out_path = os.path.join(output_folder, f"{folder}/{filebase}.*")
zip_path = os.path.join(output_folder, f"{filebase}.{folder}.zip")
cmd_line = f"zip -j -9 {zip_path} {out_path}"
execute_shell(cmd_line=cmd_line, message=f"creating zip file for {write_format} in {output_folder}")
if folder not in ("dictd"): # keep dictd files for Pocketbook conversion
out_dir = Path(output_folder) / folder
for f in out_dir.glob(f"{filebase}.*"):
if f.is_file():
f.unlink()
# cmd_line = f"rm {out_path}"
# execute_shell(cmd_line=cmd_line, message=f"Remove temp file for {filebase} in {output_folder}")
else:
# cmd_line = f"mv {out_path} {output_folder}"
# execute_shell(cmd_line=cmd_line, message=f"moving output {out_path} to output folder {output_folder}")
Path(out_path).replace(Path(output_folder) / f"{filebase}.{extension}")
def build_dict_mobi(input_folder, output_folder, datafile, filebase, inflections, dataCreator, dataTarget, dataSource, dataName):
"""
Build a Kindle-compatible .mobi dictionary from intermediate data files.
This function orchestrates the creation of a Kindle dictionary by:
1. Running a conversion script (tab2opf.py) to produce an OPF+HTML set in a temporary
"kindle" subdirectory under the provided input folder.
2. Invoking the mobigen.exe tool (via wine) to generate a .mobi file from the
generated OPF file.
3. Moving the resulting .mobi file to the specified output folder.
4. Cleaning up generated HTML files for the processed file base.
Note: this function executes external commands and relies on several external
binaries and scripts being present and executable (python ./bin/tab2opf.py,
wine, ./bin/mobigen/mobigen.exe). It prints command lines it runs and uses
subprocess and an execute_shell helper for execution.
Args:
input_folder (str): Path to the input folder containing source materials.
A "kindle" subdirectory under this folder will be used as the temporary
output directory for OPF/HTML files.
output_folder (str): Destination directory where the final .mobi file will
be moved.
datafile (str): Path to the source data file passed as the final argument to
tab2opf.py.
filebase (str): Base filename (without extension) used to name the generated
OPF/HTML/mobi artefacts.
inflections (str): Value passed to tab2opf.py's --inflection option that
controls whether/which inflections to include.
dataCreator (str): Creator/author name passed to tab2opf.py (also used as
publisher in the current command).
dataTarget (str): Target language/code passed to tab2opf.py’s --target option.
dataSource (str): Source language/code passed to tab2opf.py’s --source option.
dataName (str): Title/name passed to tab2opf.py’s --title option and used as
metadata in the generated OPF.
Returns:
None
Side effects:
- Creates (and later removes) HTML files in {input_folder}/kindle.
- Creates an OPF file and a .mobi file for the given filebase.
- Moves the .mobi file into the provided output_folder.
- Prints executed command lines to stdout.
Errors and exceptions:
- External command failures may raise subprocess exceptions or fail silently
depending on how subprocess.run/subprocess.call are invoked. The function
also depends on the behavior of execute_shell for cleanup; any errors
raised by that helper will propagate.
- Because command lines are assembled from input parameters, passing
untrusted input may lead to shell injection risks (note that some calls
use shlex.split while others invoke a shell). Sanitize or validate inputs
before calling this function in untrusted contexts.
Example:
build_dict_mobi(
input_folder="/path/to/workspace",
output_folder="/path/to/output",
datafile="/path/to/data.tsv",
filebase="my_dictionary",
inflections="yes",
dataCreator="Example Author",
dataTarget="en",
dataSource="la",
dataName="Latin–English Dictionary"
)
"""
html_dir = f"kindle"
html_out_dir = f"{input_folder}/{html_dir}"
cmd_line = f"python ./bin/tab2opf.py --title={dataName} --source={dataSource} --target={dataTarget} --inflection={inflections} --outdir={html_out_dir} --creator={dataCreator} --publisher={dataCreator} {datafile}" # noqa: E501
print(cmd_line)
subprocess.run(shlex.split(cmd_line))
# Generate .mobi dictionary from opf+html file
out_path = f"{html_out_dir}/{filebase}.opf".replace(" ", "\\ ")
cmd_line = f"wine ./bin/mobigen/mobigen.exe -unicode -s0 {out_path}"
print(cmd_line)
subprocess.run(shlex.split(cmd_line))
cmd_line = f"mv {html_out_dir}/{filebase}.mobi {output_folder}/"
print(cmd_line)
subprocess.call(cmd_line, shell=True)
# cmd_line = f"rm {htmlOutDir}/{filebase}*.html"
# execute_shell(cmd_line=cmd_line, message=f"Removes html files for {filebase}")
delete_file_pattern(filebase, "*.html", html_out_dir)
def build_dict_pocketbook(output_folder, filebase):
"""
Builds an Pocketbook from dictd file. This MUST RUN AFTER dictd was created
Args:
output_folder (str): Directory path where the output dictionary will be saved
datafile (str): Path to input data file
filebase (str): Base filename to use for generated dictionary
dataDescription (str): Description of the dictionary data
dataName (str): Name of the dictionary
The function:
1. Generates an XDXF datafile using makedict tool
2. Use Pocketbook LanguageFilesPocketbookConverter to convert XDXF to Pocketbook .dic format
3. Renames output file
Requires:
- gen_mdict_target() function to generate the dictionary
- execute_shell() function to run shell commands
"""
# Generare Pocketbook dictionary
out_dictdir = os.path.join(output_folder, "dic")
out_xdxfdir = os.path.join(output_folder, "xdxf")
# cmd_line = f"mkdir {out_dictdir}"
# print(cmd_line)
# subprocess.run(shlex.split(cmd_line))
dictd_base = f"{output_folder}/dictd/{filebase}"
# Generare xdxf intermediate dictionary format
cmd_line = f"makedict -i dictd -o xdxf {dictd_base}.index --work-dir {out_dictdir}"
print(cmd_line)
subprocess.run(shlex.split(cmd_line))
cmd_line = f"rm {dictd_base}.index {dictd_base}.dict"
print(cmd_line)
subprocess.run(shlex.split(cmd_line))
converter = "wine LanguageFilesPocketbookConverter/converter.exe"
lang_data = "./LanguageFilesPocketbookConverter/en/"
old_xdxf_file = f"{out_dictdir}/{filebase}/dict.xdxf"
new_xdxf_file = f"{out_dictdir}/{filebase}.xdxf"
old_dic_file = f"{out_dictdir}/{filebase}.dic"
cmd_line = f"mv {old_xdxf_file} {new_xdxf_file}"
print(cmd_line)
subprocess.run(shlex.split(cmd_line))
# Split the lines by item number, to overcome a limitation of `converter`
split_long_text_file(new_xdxf_file, new_xdxf_file)
cmd_line = f"{converter} {new_xdxf_file} {lang_data}"
print(cmd_line)
subprocess.run(shlex.split(cmd_line))
cmd_line = f"rm {new_xdxf_file}"
print(cmd_line)
subprocess.run(shlex.split(cmd_line))
cmd_line = f"mv {old_dic_file} {output_folder}/"
print(cmd_line)
subprocess.run(shlex.split(cmd_line))
# cmd_line = f"mv {old_dic_file} {new_dic_file}"
# print(cmd_line)
# subprocess.run(shlex.split(cmd_line))
# cmd_line = f"zip -j -m -9 {output_folder}/{filebase}.xdxf.zip {out_xdxfdir}/{filebase}.xdxf"
# execute_shell(cmd_line=cmd_line, message=f"Making Pleco dict file for {filebase}")
# cmd_line = f"rm {output_folder}/mdict/{filebase}*"
# execute_shell(cmd_line=cmd_line, message=f"Removes temp mdict files in {output_folder}/mdict")
# delete_file_pattern(filebase, "*", f"{output_folder}/mdict/")
def build_dict_mdict(output_folder, datafile, filebase, dataDescription, dataName):
"""
Builds an MDict from a data file.
Args:
output_folder (str): Directory path where the output dictionary will be saved
datafile (str): Path to input data file
filebase (str): Base filename to use for generated dictionary
dataDescription (str): Description of the dictionary data
dataName (str): Name of the dictionary
The function:
1. Generates an MDict in a temporary folder
2. Moves .mdx files to the output folder
3. Cleans up temporary files
Requires:
- gen_mdict_target() function to generate the dictionary
- execute_shell() function to run shell commands
"""
# Generare Mdict dictionary
out_dictdir = os.path.join(output_folder, "mdict")
gen_mdict_target(datafile, filebase, out_dictdir, dataName, dataDescription)
cmd_line = f"mv {out_dictdir}/*.mdx {output_folder}/"
execute_shell(cmd_line=cmd_line, message=f"moving output {output_folder} to output folder {output_folder}")
# cmd_line = f"rm {output_folder}/mdict/{filebase}*"
# execute_shell(cmd_line=cmd_line, message=f"Removes temp mdict files in {output_folder}/mdict")
delete_file_pattern(filebase, "*", f"{output_folder}/mdict/")
def build_dict_pleco_txt(input_folder, output_folder, filebase):
"""
Create a Pleco-compatible zipped dictionary file (.pleco.zip) from a plaintext
dictionary file located in input_folder.
This function looks for a file named "{filebase}.txt" in input_folder. If that
file exists, it runs a shell zip command to create a zip archive named
"{filebase}.pleco.zip" in output_folder containing the original .txt file.
The zip is created with the -j option (junk the path) so the archive contains
only the .txt file, not its parent directories.
Parameters
----------
input_folder : str
Path to the folder that contains the source "{filebase}.txt" file.
output_folder : str
Path to the folder where the resulting "{filebase}.pleco.zip" archive
should be written.
filebase : str
Base filename (without extension) used to compose the source ".txt" and
destination ".pleco.zip" filenames.
Returns
-------
None
Side effects
------------
- If "{input_folder}/{filebase}.txt" exists, a shell command is executed via
execute_shell to create the zip archive at "{output_folder}/{filebase}.pleco.zip".
- If the source .txt file does not exist, the function does nothing.
Exceptions
----------
Any exceptions raised by execute_shell or underlying system calls (e.g.,
permission errors, missing external zip utility) will propagate to the caller.
Example
-------
build_dict_pleco_txt("/path/to/input", "/path/to/output", "mydict")
# If "/path/to/input/mydict.txt" exists, creates "/path/to/output/mydict.pleco.zip"
"""
# Zip to create Pleco dictionary
pleco_dict_file = f"{input_folder}/{filebase}.txt"
if os.path.exists(pleco_dict_file):
cmd_line = f"zip -j -m -9 {output_folder}/{filebase}.pleco.zip {input_folder}/{filebase}.txt"
execute_shell(cmd_line=cmd_line, message=f"Making Pleco dict file for {pleco_dict_file}")
def build_dict_lingvo(input_folder, output_folder, datafile, filebase, dataCreator, dataFullTarget, dataFullSource, dataName):
"""
Generate a Lingvo (.dsl) dictionary by invoking an external tab2dsl Ruby tool and move the generated archive to the output folder.
This function constructs and runs shell commands to:
1. Call the Ruby script ./dsl-tools/tab2dsl/tab2dsl.rb to convert a tabular dictionary file into a Lingvo DSL file.
2. Move the resulting compressed output (.dz) from the temporary output location into the provided output folder.
Notes:
- The temporary DSL output path is constructed as "<output_folder>/lingvo/<filebase>.dsl" with spaces escaped.
- The function relies on an available `execute_shell` helper to run shell commands; any exceptions raised by that helper will propagate.
- The Ruby script must exist at ./dsl-tools/tab2dsl/tab2dsl.rb and be executable in the running environment.
- The parameter `dataCreator` is accepted but not used by this function.
Parameters
----------
input_folder : str
Path to the folder containing input resources (not directly used by the current implementation but kept for API consistency).
output_folder : str
Destination folder where the final .dz archive will be moved.
datafile : str
Path to the tab-separated dictionary file that will be converted by the tab2dsl tool.
filebase : str
Base name used for the generated DSL file (the function creates "lingvo/{filebase}.dsl" under output_folder).
dataCreator : str
Metadata field intended to represent the creator of the data. Note: currently not used by the function.
dataFullTarget : str
Target language code/name passed to the tab2dsl tool via --to-lang.
dataFullSource : str
Source language code/name passed to the tab2dsl tool via --from-lang.
dataName : str
Dictionary name passed to the tab2dsl tool via --dict-name.
Returns
-------
None
The function performs side effects (file creation and movement) and does not return a value.
Raises
------
Exception
Propagates any exception raised by the underlying `execute_shell` calls (e.g., subprocess errors, OSError),
indicating failure of the external tool invocation or filesystem operations.
Examples
--------
# Typical usage
build_dict_lingvo(
input_folder="/path/to/input",
output_folder="/path/to/output",
datafile="/path/to/input/data.tsv",
filebase="my_dictionary",
dataCreator="Example Creator",
dataFullTarget="ru",
dataFullSource="en",
dataName="My Dictionary"
"""
# Generare Lingvo dictionary
out_path = os.path.join(output_folder, f"lingvo/{filebase}.dsl").replace(" ", "\\ ")
cmd_line = (
f"ruby ./dsl-tools/tab2dsl/tab2dsl.rb --from-lang {dataFullSource} --to-lang {dataFullTarget} --dict-name {dataName} --output {out_path} {datafile}" # noqa: E501
)
execute_shell(cmd_line=cmd_line, message=f"generating DSL/Longvo")
cmd_line = f"mv {out_path}.dz {output_folder}"
execute_shell(cmd_line=cmd_line, message=f"moving output {out_path} to output folder {output_folder}")
def search_data_files(input_folder, extension, metadata, dict_filters):
"""
Search for matching metadata and data files in a specified directory.
This function looks for pairs of metadata and dictionary data files in the input folder,
optionally filtering the results based on specified dictionary names.
Args:
input_folder (str): Path to the directory containing the data files
extension (str): File extension for the dictionary data files
metadata (str): File extension for the metadata files
dict_filters (list): Optional list of strings to filter dictionary names. Only dictionaries
containing any of these strings in their names will be included.
Returns:
tuple: A pair of lists (metafilelist, datafilelist) containing paths to matching
metadata and data files. For compressed files (.bz2), they will be decompressed
and the path to the decompressed file will be returned.
Notes:
- The function handles both regular and bzip2 compressed (.bz2) data files
- Files are matched based on their base names (without extensions)
- When dict_filters is provided, only dictionaries with names containing any
of the filter strings will be included
- Compressed files are automatically decompressed during processing
"""
if input_folder:
metafilelist = sorted(input_folder.glob(f"*.{metadata}"), reverse=True)
datafilelist = sorted(input_folder.glob(f"*.{extension}"), reverse=True)
zippdatafilelist = sorted(input_folder.glob(f"*.bz2"), reverse=True)
meta_dict = {}
data_dict = {}
print(f"Len of metafilelist: {len(metafilelist)}")
print(f"Len of datafilelist: {len(datafilelist)}")
print(f"Len of compressed datafilelist: {len(zippdatafilelist)}")
bothdatalist = datafilelist + zippdatafilelist # Lists of data files including compressed ones
# Keep only pairs of metadata and dict data files
meta_dict = {full_stem(filepath): filepath for filepath in metafilelist}
data_dict = {full_stem(filepath): filepath for filepath in bothdatalist}
common_keys = sorted(list(meta_dict.keys() & data_dict.keys()))
print(common_keys)
metafilelist.clear()
datafilelist.clear()
for key in common_keys:
include_dict = False
if dict_filters:
for filter in dict_filters:
if filter in key:
include_dict = True
break
if not include_dict:
print(f"Excluding this dictionary: {key}")
continue
metafilelist.append(meta_dict[key])
datafile = data_dict[key]
# If datafile is a .bz2
if datafile.suffix == ".bz2":
cmd_line = f'bzip2 -d "{str(datafile)}"' # Add -k to keep the original file
execute_shell(cmd_line=cmd_line, message=f"bunzip data file")
datafilelist.append(datafile.with_suffix("")) # Remove .bz2 suffix
else:
datafilelist.append(datafile)
print(f"Len of checked datafilelist: {len(datafilelist)}")
return metafilelist,datafilelist
def full_stem(path):
"""
Return the filename without any extensions for files with multiple suffixes (e.g., .tar.gz).
Files with a single suffix behave as path.stem
Parameters
----------
path : str or Path
The path to the file.
Returns
-------
str
The filename with all suffixes removed.
"""
p = Path(path)
name = p.name
for _ in p.suffixes:
name = Path(name).with_suffix("")
return str(name)
if __name__ == "__main__":
""""
Main entry point
"""
main()