DazedTL/modules/main.py

242 lines
No EOL
9.3 KiB
Python

import sys
import os
import traceback
from concurrent.futures import ThreadPoolExecutor, as_completed
from colorama import Fore
from tqdm import tqdm
from dotenv import load_dotenv
# This needs to be before the module imports as some of them currently try to read and use some of these values
# upon import, in which case if they are unset the script will crash before we can output these messages.
envMissing = False
load_dotenv()
for env in [
"api",
"key",
"organization",
"model",
"language",
"timeout",
"fileThreads",
"threads",
"width",
"listWidth",
]:
if os.getenv(env) is None or str(os.getenv(env))[:1] == "<":
tqdm.write(Fore.RED + f"Environment variable {env} is not set!")
envMissing = True
if envMissing:
tqdm.write(
Fore.RED
+ "Some of the required environment values may not be set correctly. You can set \
these values using an .env file, for an example see .env.example"
)
from modules.rpgmakermvmz import handleMVMZ, setSpeakerParseMode, finalizeSpeakerParse
from modules.rpgmakerace import handleACE
from modules.csv import handleCSV
from modules.tyrano import handleTyrano
from modules.kirikiri import handleKirikiri
from modules.json import handleJSON
from modules.lune import handleLune
from modules.nscript import handleOnscripter
from modules.wolf import handleWOLF
from modules.wolf2 import handleWOLF2
from modules.regex import handleRegex
from modules.text import handleText
from modules.renpy import handleRenpy
from modules.unity import handleUnity
from modules.images import handleImages
from modules.rpgmakerplugin import handlePlugin
from modules.srpg import handleSRPG
# For GPT4 rate limit will be hit if you have more than 1 thread.
# 1 Thread for each file. Controls how many files are worked on at once.
THREADS = int(os.getenv("fileThreads"))
# [Display name, file extension, handle function]
MODULES = [
["RPGMaker MV/MZ", ["json"], handleMVMZ],
["RPGMaker Plugins", ["js", "rb"], handlePlugin],
["RPGMaker ACE", ["yaml"], handleACE],
["CSV (From Translator++)", ["csv"], handleCSV],
["Tyrano", ["ks"], handleTyrano],
["Kirikiri", ["ks", "tjs", "ssd", "asd"], handleKirikiri],
["JSON", ["json"], handleJSON],
["Lune", ["json"], handleLune],
["NScript", ["txt"], handleOnscripter],
["Wolf", ["json"], handleWOLF],
["Wolf", ["txt"], handleWOLF2],
["Regex", ["txt", "json", "script", "csv"], handleRegex],
["Text", ["txt", "srt"], handleText],
["Renpy", ["rpy"], handleRenpy],
["Unity", ["txt"], handleUnity],
["SRPG Studio", ["json"], handleSRPG],
["Images", [""], handleImages],
]
# Info Message
tqdm.write(
Fore.CYAN
+ "-Dazed MTL Tool -"
+ Fore.RESET,
end="\n\n",
)
def main():
estimate = ""
speaker_parse = False # Deferred until after engine select
while estimate == "":
estimate = input("Select Mode:\n\n 1. Translate\n 2. Estimate\n")
match estimate:
case "1":
estimate = False
case "2":
estimate = True
case _:
estimate = ""
version = ""
while True:
tqdm.write("Select game engine:\n")
for position, module in enumerate(MODULES):
tqdm.write(f"{str(position + 1).rjust(2)}. {module[0]} (.{module[1]})")
version = input()
try:
version = int(version) - 1
except:
continue
if version in range(len(MODULES)):
break
totalCost = (
Fore.RED
+ "Translation module didn't return the total cost. Make sure the \
files to translate are in the /files folder and that you picked the right game engine."
)
# If translating RPGMaker MV/MZ (index 0) prompt for speaker parse mode
if version == 0 and not estimate:
sub = ""
while sub == "":
sub = input("RPGMaker MV/MZ options:\n\n 1. Standard Translate\n 2. Parse Speakers (collect speaker names only)\n")
match sub:
case "1":
speaker_parse = False
case "2":
speaker_parse = True
case _:
sub = ""
if speaker_parse:
setSpeakerParseMode(True)
# Open File (Threads) - recursively walk 'files' and preserve directory structure
with ThreadPoolExecutor(max_workers=THREADS) as executor:
futures = []
files_root = "files"
# Special-case: Images engine expects a folder, not a file; schedule per directory containing assets
if MODULES[version][0] == "Images":
for root, dirs, filenames in os.walk(files_root):
# Skip hidden/system directories
dirs[:] = [d for d in dirs if d not in {".git", "__pycache__"}]
# Skip the root 'files' itself to avoid processing everything twice
# We'll still allow scheduling for root if it contains assets
# Only schedule directories that contain potential assets
has_assets = any(fn.lower().endswith((".png", ".txt")) for fn in filenames)
if not has_assets:
continue
# Compute relative directory path and ensure translated mirror exists
rel_dir = os.path.relpath(root, files_root).replace(os.sep, "/")
if rel_dir == ".":
# Represent root as empty string so handler creates files under translated/ directly
rel_dir = ""
try:
target_dir = os.path.join("translated", rel_dir.replace("/", os.sep)) if rel_dir else "translated"
os.makedirs(target_dir, exist_ok=True)
except Exception:
pass
futures.append(
executor.submit(MODULES[version][2], rel_dir, estimate)
)
else:
# Gather all candidate files recursively
for root, dirs, filenames in os.walk(files_root):
# Skip hidden/system directories if any
dirs[:] = [d for d in dirs if d not in {".git", "__pycache__"}]
for fname in filenames:
if fname == ".gitkeep":
continue
abs_path = os.path.join(root, fname)
# Build relative path from 'files' root using POSIX-style separators so handlers can do 'files/' + rel
rel_path = os.path.relpath(abs_path, files_root)
rel_path_posix = rel_path.replace(os.sep, "/")
# Check extension match for the selected module version
for m in MODULES[version][1]:
if rel_path_posix.endswith(m):
# Ensure the corresponding directory exists under 'translated'
rel_dir = os.path.dirname(rel_path_posix)
if rel_dir:
try:
os.makedirs(os.path.join("translated", rel_dir.replace("/", os.sep)), exist_ok=True)
except Exception:
# Best-effort; handler may attempt write and fail if permissions are insufficient
pass
futures.append(
executor.submit(MODULES[version][2], rel_path_posix, estimate)
)
break # Avoid double-adding if multiple ext entries match
for future in as_completed(futures):
try:
totalCost = future.result()
except Exception as e:
tracebackLineNo = str(traceback.extract_tb(sys.exc_info()[2])[-1].lineno)
tqdm.write(Fore.RED + str(e) + "|" + tracebackLineNo + Fore.RESET)
# Finalize speaker parse mode by writing collected speakers to vocab
if speaker_parse:
finalizeSpeakerParse()
# Delete Tmp Files
if os.path.isfile("csv.tmp"):
os.remove("csv.tmp")
# Sweep any leftover temp files in translated/
try:
translated_dir = os.path.join("translated")
if os.path.isdir(translated_dir):
for fname in os.listdir(translated_dir):
if fname.endswith(".tmp"):
fpath = os.path.join(translated_dir, fname)
try:
os.remove(fpath)
except Exception:
# Best-effort cleanup; ignore files locked by other processes
pass
except Exception:
pass
# Finish
if totalCost != "Fail":
# if estimate is False:
# This is to encourage people to grab what's in /translated instead
# deleteFolderFiles("files")
tqdm.write(str(totalCost))
def deleteFolderFiles(folderPath):
for filename in os.listdir(folderPath):
file_path = os.path.join(folderPath, filename)
if file_path.endswith((".json", ".yaml", ".ks")):
os.remove(file_path)