diff --git a/README.md b/README.md index 8cffbc1..af86efb 100644 --- a/README.md +++ b/README.md @@ -263,7 +263,6 @@ Open the DazedMTLTool folder in VSCode and ask Copilot to make changes to the tr - *"Add a new regex pattern to skip lines that start with //"* - *"Change the wordwrap logic to break on full-width punctuation"* -- *"Make this module also handle .yaml files"* - *"Explain what CODE 356 does in rpgmakermvmz.py"* Copilot can read the surrounding code and suggest context-aware edits — you just review and accept. This makes it easy to customize the tool for specific games without deep Python knowledge. @@ -308,7 +307,6 @@ Not every file needs to be tracked. Create a file called `.gitignore` in the gam !*.csv !*.ks !*.tjs -!*.yaml !*.rb !*.rvdata2 # Other useful files diff --git a/gui/config_tab.py b/gui/config_tab.py index aa429ba..8587b20 100644 --- a/gui/config_tab.py +++ b/gui/config_tab.py @@ -177,27 +177,21 @@ class ConfigTab(QWidget): nav_layout.addWidget(btn_mvmz) self.nav_buttons.append(btn_mvmz) - # RPG Maker Ace button - btn_ace = self.create_nav_button("🎲", "RPG Maker Ace") - btn_ace.clicked.connect(lambda: self.switch_page(2)) - nav_layout.addWidget(btn_ace) - self.nav_buttons.append(btn_ace) - # Wolf RPG button btn_wolf = self.create_nav_button("🐺", "Wolf RPG") - btn_wolf.clicked.connect(lambda: self.switch_page(3)) + btn_wolf.clicked.connect(lambda: self.switch_page(2)) nav_layout.addWidget(btn_wolf) self.nav_buttons.append(btn_wolf) # CSV button btn_csv = self.create_nav_button("📄", "CSV") - btn_csv.clicked.connect(lambda: self.switch_page(4)) + btn_csv.clicked.connect(lambda: self.switch_page(3)) nav_layout.addWidget(btn_csv) self.nav_buttons.append(btn_csv) # SRPG Studio button btn_srpg = self.create_nav_button("⚔️", "SRPG Studio") - btn_srpg.clicked.connect(lambda: self.switch_page(5)) + btn_srpg.clicked.connect(lambda: self.switch_page(4)) nav_layout.addWidget(btn_srpg) self.nav_buttons.append(btn_srpg) @@ -215,19 +209,15 @@ class ConfigTab(QWidget): self.mvmz_tab = RPGMakerTab("MVMZ") self.content_stack.addWidget(self.mvmz_tab) - # Page 3: RPG Maker Ace Engine - self.ace_tab = RPGMakerTab("ACE") - self.content_stack.addWidget(self.ace_tab) - - # Page 4: Wolf RPG Engine + # Page 3: Wolf RPG Engine self.wolf_tab = WolfTab() self.content_stack.addWidget(self.wolf_tab) - # Page 5: CSV Settings + # Page 4: CSV Settings self.csv_tab = CSVTab() self.content_stack.addWidget(self.csv_tab) - # Page 6: SRPG Studio Engine + # Page 5: SRPG Studio Engine self.srpg_tab = SRPGTab() self.content_stack.addWidget(self.srpg_tab) @@ -279,7 +269,6 @@ class ConfigTab(QWidget): """Refresh engine config pages from their module files when shown.""" engine_pages = { 1: getattr(self, "mvmz_tab", None), - 2: getattr(self, "ace_tab", None), } page = engine_pages.get(index) if page is not None and hasattr(page, "refresh_from_module"): @@ -882,7 +871,6 @@ class ConfigTab(QWidget): # Reset engine tabs self.mvmz_tab.reset_to_defaults() - self.ace_tab.reset_to_defaults() self.wolf_tab.reset_to_defaults() self.csv_tab.reset_to_defaults() diff --git a/gui/engine_config_tab.py b/gui/engine_config_tab.py index 457c1c1..a3ec84e 100644 --- a/gui/engine_config_tab.py +++ b/gui/engine_config_tab.py @@ -5,7 +5,6 @@ based on signals from the Translation tab (engine selection). Currently supports: - RPG Maker MV/MZ -- RPG Maker Ace - Wolf RPG Easily extensible: add new engine widget + mapping entry. @@ -31,7 +30,6 @@ class EngineConfigTab(QWidget): # Engine widgets self.mv_widget = RPGMakerTab("MVMZ") - self.ace_widget = RPGMakerTab("ACE") self.wolf_widget = WolfTab() # Placeholder when no engine selected @@ -42,7 +40,6 @@ class EngineConfigTab(QWidget): # Add to stack self.widget_map = { "mvmz": self.mv_widget, - "ace": self.ace_widget, "wolf": self.wolf_widget, } self.stack.addWidget(self.placeholder) # index 0 diff --git a/gui/rpgmaker_tab.py b/gui/rpgmaker_tab.py index a1f699f..ba92bc5 100644 --- a/gui/rpgmaker_tab.py +++ b/gui/rpgmaker_tab.py @@ -40,11 +40,7 @@ def create_section_label(text): class RPGMakerTab(QWidget): - """RPG Maker MV/MZ & Ace configuration tab. - - This widget now supports both MV/MZ and Ace engines. Pass engine="ACE" to - target rpgmakerace.py; otherwise it will default to MV/MZ (rpgmakermvmz.py). - """ + """RPG Maker MV/MZ configuration tab.""" # Default configuration values for RPG Maker MV/MZ DEFAULT_CONFIG = { @@ -187,8 +183,7 @@ class RPGMakerTab(QWidget): loaded_config = None if self.config_integration: - module_filename = "rpgmakermvmz.py" if self.engine != "ACE" else "rpgmakerace.py" - module_path = Path("modules") / module_filename + module_path = Path("modules") / "rpgmakermvmz.py" loaded_config = self.config_integration.read_current_config(module_path) # Use loaded module config if available, otherwise use canonical defaults @@ -237,8 +232,7 @@ class RPGMakerTab(QWidget): main_layout.setSpacing(12) # Title - title = "RPG Maker MV/MZ" if self.engine != "ACE" else "RPG Maker Ace" - title_label = QLabel(f"{title} Translation Settings") + title_label = QLabel("RPG Maker MV/MZ Translation Settings") title_label.setStyleSheet("font-size: 16px; font-weight: bold; color: #007acc;") main_layout.addWidget(title_label) @@ -297,11 +291,6 @@ class RPGMakerTab(QWidget): ) col1.addLayout(layout) - self.speakers408_cb, layout = self._create_checkbox_with_description( - "408 Speakers", "Detect speakers in comment blocks", "Apply speaker logic to comment blocks." - ) - col1.addLayout(layout) - col1.addStretch() # ==================== COLUMN 2: DIALOGUE ==================== @@ -516,8 +505,6 @@ class RPGMakerTab(QWidget): self.tlsystemvariables_cb.stateChanged.disconnect() self.tlsystemswitches_cb.stateChanged.disconnect() self.join408_cb.stateChanged.disconnect() - if self.engine == "ACE": - self.speakers408_cb.stateChanged.disconnect() # Main Codes self.code401_cb.stateChanged.disconnect() @@ -559,8 +546,6 @@ class RPGMakerTab(QWidget): self.tlsystemvariables_cb.stateChanged.connect(lambda: self.apply_to_module(show_messages=False)) self.tlsystemswitches_cb.stateChanged.connect(lambda: self.apply_to_module(show_messages=False)) self.join408_cb.stateChanged.connect(lambda: self.apply_to_module(show_messages=False)) - if self.engine == "ACE": - self.speakers408_cb.stateChanged.connect(lambda: self.apply_to_module(show_messages=False)) # Main Codes self.code401_cb.stateChanged.connect(lambda: self.apply_to_module(show_messages=False)) @@ -626,11 +611,6 @@ class RPGMakerTab(QWidget): "CODE111": self.code111_cb.isChecked(), "CODE108": self.code108_cb.isChecked(), } - - # Only include SPEAKERS408 for ACE engine - if self.engine == "ACE": - config["SPEAKERS408"] = self.speakers408_cb.isChecked() - return config def set_config(self, config): @@ -645,10 +625,6 @@ class RPGMakerTab(QWidget): self.tlsystemvariables_cb.setChecked(config.get("TLSYSTEMVARIABLES", False)) self.tlsystemswitches_cb.setChecked(config.get("TLSYSTEMSWITCHES", False)) self.join408_cb.setChecked(config.get("JOIN408", False)) - - # Only set SPEAKERS408 for ACE engine - if self.engine == "ACE": - self.speakers408_cb.setChecked(config.get("SPEAKERS408", False)) # Main Codes self.code401_cb.setChecked(config.get("CODE401", True)) @@ -760,11 +736,10 @@ class RPGMakerTab(QWidget): return is_valid def apply_to_module(self, show_messages=False): - """Apply current configuration to the correct RPG Maker module (MV/MZ or Ace).""" + """Apply current configuration to the RPG Maker MV/MZ module.""" try: config = self.get_config() - # Select module filename based on engine - module_filename = "rpgmakermvmz.py" if self.engine != "ACE" else "rpgmakerace.py" + module_filename = "rpgmakermvmz.py" module_path = Path(__file__).parent.parent / "modules" / module_filename if not module_path.exists(): @@ -815,12 +790,12 @@ class RPGMakerTab(QWidget): self.config_changed.emit() def load_from_module(self): - """Load configuration from the actual module file based on engine.""" + """Load configuration from the RPG Maker MV/MZ module file.""" if not self.config_integration: return try: - module_filename = "rpgmakermvmz.py" if self.engine != "ACE" else "rpgmakerace.py" + module_filename = "rpgmakermvmz.py" module_path = Path("modules") / module_filename config = self.config_integration.read_current_config(module_path) if config: @@ -841,8 +816,7 @@ class RPGMakerTab(QWidget): return False try: - module_filename = "rpgmakermvmz.py" if self.engine != "ACE" else "rpgmakerace.py" - module_path = Path("modules") / module_filename + module_path = Path("modules") / "rpgmakermvmz.py" config = self.config_integration.read_current_config(module_path) if not config: return False diff --git a/gui/translation_tab.py b/gui/translation_tab.py index 8916cdd..299b3b5 100644 --- a/gui/translation_tab.py +++ b/gui/translation_tab.py @@ -325,11 +325,10 @@ class TranslationWorker(QThread): threads = int(os.getenv("fileThreads", "1")) total_cost = "Fail" - # If we're doing Parse Speakers for RPGMaker MV/MZ or ACE, run handlers in-process so + # If we're doing Parse Speakers for RPGMaker MV/MZ, run handlers in-process so # speaker collection is shared in this process and finalizeSpeakerParse() can run once. module_name_lower = self.module_info[0].lower() if isinstance(self.module_info[0], str) else "" is_mvmz = "mv/mz" in module_name_lower - is_ace = "ace" in module_name_lower # Change to project directory for module execution old_cwd = os.getcwd() @@ -345,16 +344,12 @@ class TranslationWorker(QThread): pass try: - if self.parse_speakers and (is_mvmz or is_ace): + if self.parse_speakers and is_mvmz: # Run handlers sequentially in this worker process so globals are shared try: - if is_mvmz: - from modules.rpgmakermvmz import handleMVMZ as handler, setSpeakerParseMode, finalizeSpeakerParse, resetSpeakerState, TOKENS, calculateCost, MODEL - elif is_ace: - from modules.rpgmakerace import handleACE as handler, setSpeakerParseMode, finalizeSpeakerParse, resetSpeakerState, TOKENS, calculateCost, MODEL + from modules.rpgmakermvmz import handleMVMZ as handler, setSpeakerParseMode, finalizeSpeakerParse, resetSpeakerState, TOKENS, calculateCost, MODEL except Exception as e: - engine_name = "rpgmakermvmz" if is_mvmz else "rpgmakerace" - self.emit_log(f"❌ Could not import {engine_name} for speaker-parse: {e}") + self.emit_log(f"❌ Could not import rpgmakermvmz for speaker-parse: {e}") self.finished_signal.emit(False, str(e)) return @@ -1106,7 +1101,6 @@ class TranslationTab(QWidget): try: sys.path.append(str(self.project_root)) from modules.rpgmakermvmz import handleMVMZ - from modules.rpgmakerace import handleACE from modules.csv import handleCSV from modules.tyrano import handleTyrano from modules.kirikiri import handleKirikiri @@ -1126,7 +1120,6 @@ class TranslationTab(QWidget): self.modules = [ ["RPG Maker MV/MZ", [".json"], handleMVMZ], - ["RPG Maker Ace", [".yaml"], handleACE], ["CSV", [".csv"], handleCSV], ["Tyrano", [".ks"], handleTyrano], ["Kirikiri", [".ks"], handleKirikiri], @@ -1161,9 +1154,7 @@ class TranslationTab(QWidget): def _on_module_changed(self, text: str): lowered = text.lower() - if "ace" in lowered: - self.engine_changed.emit("ace") - elif "wolf" in lowered and "wolf rpg 2" not in lowered: + if "wolf" in lowered and "wolf rpg 2" not in lowered: self.engine_changed.emit("wolf") elif "mv/mz" in lowered: self.engine_changed.emit("mvmz") @@ -1176,8 +1167,8 @@ class TranslationTab(QWidget): self.mode_combo.addItem("Translate") self.mode_combo.addItem("Estimate") - # Add Parse Speakers for RPG Maker MV/MZ and RPG Maker Ace - if "mv/mz" in lowered or "ace" in lowered: + # Add Parse Speakers for RPG Maker MV/MZ + if "mv/mz" in lowered: self.mode_combo.addItem("Parse Speakers") # Restore previous selection if it still exists @@ -1336,7 +1327,7 @@ class TranslationTab(QWidget): try: selected_index = self.module_combo.currentIndex() if 0 <= selected_index < len(self.modules): - accepted_extensions = self.modules[selected_index][1] # List of extensions like [".json", ".yaml"] + accepted_extensions = self.modules[selected_index][1] # List of extensions like [".json"] except Exception: pass diff --git a/modules/main.py b/modules/main.py index 92a167d..af56461 100644 --- a/modules/main.py +++ b/modules/main.py @@ -34,7 +34,6 @@ these values using an .env file, for an example see .env.example" ) from modules.rpgmakermvmz import handleMVMZ, setSpeakerParseMode as setSpeakerParseMVMZ, finalizeSpeakerParse as finalizeSpeakerParseMVMZ -from modules.rpgmakerace import handleACE, setSpeakerParseMode as setSpeakerParseACE, finalizeSpeakerParse as finalizeSpeakerParseACE from modules.csv import handleCSV from modules.tyrano import handleTyrano from modules.kirikiri import handleKirikiri @@ -60,7 +59,6 @@ THREADS = int(os.getenv("fileThreads")) 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], @@ -123,13 +121,12 @@ def main(): files to translate are in the /files folder and that you picked the right game engine." ) - # If translating RPGMaker MV/MZ (index 0) or RPGMaker ACE (index 2) prompt for speaker parse mode + # If translating RPGMaker MV/MZ, prompt for speaker parse mode speaker_parse = False - if version in [0, 2] and not estimate: + if version == 0 and not estimate: sub = "" - engine_name = "RPGMaker MV/MZ" if version == 0 else "RPGMaker ACE" while sub == "": - sub = input(f"{engine_name} options:\n\n 1. Standard Translate\n 2. Parse Speakers (collect speaker names only)\n") + 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 @@ -138,10 +135,7 @@ files to translate are in the /files folder and that you picked the right game e case _: sub = "" if speaker_parse: - if version == 0: - setSpeakerParseMVMZ(True) - elif version == 2: - setSpeakerParseACE(True) + setSpeakerParseMVMZ(True) # Open File (Threads) - recursively walk 'files' and preserve directory structure # Prepare per-run log file so CLI runs also write to a run-specific history file @@ -259,10 +253,7 @@ files to translate are in the /files folder and that you picked the right game e # Finalize speaker parse mode by writing collected speakers to vocab if speaker_parse: - if version == 0: - finalizeSpeakerParseMVMZ() - elif version == 2: - finalizeSpeakerParseACE() + finalizeSpeakerParseMVMZ() # Delete Tmp Files if os.path.isfile("csv.tmp"): @@ -295,5 +286,5 @@ files to translate are in the /files folder and that you picked the right game e def deleteFolderFiles(folderPath): for filename in os.listdir(folderPath): file_path = os.path.join(folderPath, filename) - if file_path.endswith((".json", ".yaml", ".ks")): + if file_path.endswith((".json", ".ks")): os.remove(file_path) \ No newline at end of file diff --git a/modules/rpgmakerace.py b/modules/rpgmakerace.py deleted file mode 100644 index d35f163..0000000 --- a/modules/rpgmakerace.py +++ /dev/null @@ -1,3851 +0,0 @@ -# Libraries -import json -import os -import re -import util.dazedwrap as dazedwrap -import threading -import time -import traceback -import copy -from pathlib import Path -import shutil -from colorama import Fore -from dotenv import load_dotenv -from retry import retry -from tqdm import tqdm -from util.translation import TranslationConfig, translateAI as sharedtranslateAI, getPricingConfig, calculateCost, getPricingConfig, calculateCost -from ruamel.yaml import YAML - - -# Globals -MODEL = os.getenv("model") -TIMEOUT = int(os.getenv("timeout")) -LANGUAGE = os.getenv("language").capitalize() -PROMPT = Path("prompt.txt").read_text(encoding="utf-8") -VOCAB = Path("vocab.txt").read_text(encoding="utf-8") -LOCK = threading.Lock() -THREAD_CTX = threading.local() -WIDTH = int(os.getenv("width")) -LISTWIDTH = int(os.getenv("listWidth")) -NOTEWIDTH = int(os.getenv("noteWidth")) -MAXHISTORY = 10 -ESTIMATE = "" -TOKENS = [0, 0] -MISMATCH = [] # Lists files that throw a mismatch error (Length of GPT list response is wrong) -PBAR = None -FILENAME = None -TIMETOTAL = 0 # Total Time Taken for all translations -VOCAB_LOCK = threading.Lock() -PREFLIGHT_COUNT_MODE = False # When True, translateAI wrapper only counts units and never calls API - -# Speakers -NAMESLIST = [] -SPEAKER_PARSE_MODE = False -_speakerCache = {} -_speakerCacheLock = threading.Lock() -SPEAKER_COLLECTED = [] # Original speaker names collected during parse mode (untranslated) - -def clearSpeakerCache(): - """Clear the speaker cache between passes to ensure fresh translations""" - global _speakerCache - with _speakerCacheLock: - _speakerCache.clear() - -# Regex - Need to change this if you want to translate from/to other languages. Default is Japanese Regex -LANGREGEX = r"[一-龠ぁ-ゔァ-ヴーa-zA-Z0-9\uFF61-\uFF9F]+" - -# Get pricing configuration based on the model -PRICING_CONFIG = getPricingConfig(MODEL) -INPUTAPICOST = PRICING_CONFIG["inputAPICost"] -OUTPUTAPICOST = PRICING_CONFIG["outputAPICost"] -BATCHSIZE = PRICING_CONFIG["batchSize"] -FREQUENCY_PENALTY = PRICING_CONFIG["frequencyPenalty"] - -# tqdm Globals -BAR_FORMAT = "{l_bar}{bar:10}{r_bar}{bar:-10b}" -POSITION = 0 - -# Initialize Translation Config -TRANSLATION_CONFIG = TranslationConfig( - model=MODEL, - language=LANGUAGE, - prompt=PROMPT, - vocab=VOCAB, - langRegex=LANGREGEX, - batchSize=BATCHSIZE, - maxHistory=MAXHISTORY, - estimateMode=False # Will be set dynamically based on ESTIMATE -) -LEAVE = False - -# Config (Default) -# FIRSTLINESPEAKERS: Guess speaker from first line. -FIRSTLINESPEAKERS = False -# FACENAME101: Map face name -> speaker. -FACENAME101 = False -# BRFLAG: Newlines ->
. -BRFLAG = False -# FIXTEXTWRAP: Rewrap text to WIDTH/NOTEWIDTH. -FIXTEXTWRAP = True -# IGNORETLTEXT: Skip Translated Text. -IGNORETLTEXT = False -# TLSYSTEMVARIABLES: Translate System Variables. (Optional but sometimes necessary. Can break stuff.) -TLSYSTEMVARIABLES = False -# TLSYSTEMSWITCHES: Translate System Switches. (Optional. Translates switch names in System.json.) -TLSYSTEMSWITCHES = False -# Join 408 codes into a single string like 401. -JOIN408 = False -# SPEAKERS408: Process speakers in code 408 the same way as code 401. -SPEAKERS408 = False - -# Dialogue / Scroll / Choices (Main Codes) -CODE101 = True -CODE401 = True -CODE405 = True -CODE102 = True - -# Optional -CODE408 = True - -# Variables -CODE122 = True -CODE122_VAR_MIN = 0 # Minimum variable ID to translate (inclusive) -CODE122_VAR_MAX = 2000 # Maximum variable ID to translate (exclusive) - -# Plugins / Scripts -CODE355655 = False -CODE357 = False -CODE657 = False -CODE356 = False -CODE320 = False -CODE324 = False -CODE325 = False -CODE111 = False -CODE108 = False - - -def handleACE(filename, estimate): - global ESTIMATE, TOKENS, FILENAME - ESTIMATE = estimate - FILENAME = filename - # Also record per-thread filename to avoid cross-thread interference - try: - THREAD_CTX.filename = filename - except Exception: - pass - - # Translate - start = time.time() - translatedData = openFiles(filename) - - # Translate - # Skip writing output file during speaker-parse mode - if not estimate and not SPEAKER_PARSE_MODE: - try: - with open("translated/" + filename, "w", encoding="utf-8", newline="\n") as outFile: - yaml = YAML(pure=True) - yaml.width = 4096 - yaml.default_style = "'" - yaml.dump(translatedData[0], outFile) - except Exception: - traceback.print_exc() - return "Fail" - - # Print File - end = time.time() - tqdm.write(getResultString(translatedData, end - start, filename)) - with LOCK: - TOKENS[0] += translatedData[1][0] - TOKENS[1] += translatedData[1][1] - - # Print Total - totalString = getResultString(["", TOKENS, None], end - start, "TOTAL") - - # Print any errors on maps - if len(MISMATCH) > 0: - return totalString + Fore.RED + f"\nMismatch Errors: {MISMATCH}" + Fore.RESET - else: - return totalString - - -def openFiles(filename): - yaml = YAML(pure=True) # Need a yaml instance per thread. - yaml.width = 4096 - yaml.default_style = "'" - - with open("files/" + filename, "r", encoding="UTF-8") as f: - data = yaml.load(f) - # Map Files - if "Map" in filename and "MapInfos" not in filename: - translatedData = parseMap(data, filename) - - # CommonEvents Files - elif "CommonEvents" in filename: - translatedData = parseCommonEvents(data, filename) - - # Actor File - elif "Actors" in filename: - translatedData = parseNames(data, filename, "Actors") - - # Armor File - elif "Armors" in filename: - translatedData = parseNames(data, filename, "Armors") - - # Weapons File - elif "Weapons" in filename: - translatedData = parseNames(data, filename, "Weapons") - - # Classes File - elif "Classes" in filename: - translatedData = parseNames(data, filename, "Classes") - - # Enemies File - elif "Enemies" in filename: - translatedData = parseNames(data, filename, "Enemies") - - # Items File - elif "Items" in filename: - translatedData = parseNames(data, filename, "Items") - - # MapInfo File - elif "MapInfos" in filename: - translatedData = parseNames(data, filename, "MapInfos") - - # Skills File - elif "Skills" in filename: - translatedData = parseNames(data, filename, "Skills") - - # Troops File - elif "Troops" in filename: - translatedData = parseTroops(data, filename) - - # States File - elif "States" in filename: - translatedData = parseSS(data, filename) - - # System File - elif "System" in filename: - translatedData = parseSystem(data, filename) - - # Scenario File - elif "Scenario" in filename: - translatedData = parseScenario(data, filename) - - else: - raise NameError(filename + " Not Supported") - - return translatedData - - -def getResultString(translatedData, translationTime, filename): - global TIMETOTAL - # File Print String - cost = calculateCost(translatedData[1][0], translatedData[1][1], MODEL) - totalTokenstring = ( - Fore.YELLOW + "[Input: " + str(translatedData[1][0]) + "]" - "[Output: " - + str(translatedData[1][1]) - + "]" "[Cost: ${:,.4f}".format(cost) - + "]" - ) - if filename != "TOTAL": - timeString = Fore.BLUE + "[" + str(round(translationTime, 1)) + "s]" - TIMETOTAL += round(translationTime, 1) - else: - timeString = Fore.BLUE + "[" + str(round(TIMETOTAL, 1)) + "s]" - - if translatedData[2] is None: - # Success - return filename + ": " + totalTokenstring + timeString + Fore.GREEN + " \u2713 " + Fore.RESET - else: - # Fail - try: - raise translatedData[2] - except Exception as e: - traceback.print_exc() - errorString = str(e) + Fore.RED - return filename + ": " + totalTokenstring + timeString + Fore.RED + " \u2717 " + errorString + Fore.RESET - - -def saveProgress(data, filename): - """Atomically write current data to translated/filename to avoid progress loss. - Skips when running in estimate mode. - """ - try: - # Also skip progress saves during speaker-parse mode - if ESTIMATE or SPEAKER_PARSE_MODE: - return - os.makedirs("translated", exist_ok=True) - # Use a unique temp file name to avoid collisions across threads/processes - tmp_path = os.path.join( - "translated", - f"{filename}.{os.getpid()}.{threading.get_ident()}.tmp", - ) - final_path = os.path.join("translated", filename) - yaml = YAML(pure=True) - yaml.width = 4096 - yaml.default_style = "'" - with open(tmp_path, "w", encoding="utf-8", newline="\n") as outFile: - yaml.dump(data, outFile) - outFile.flush() - try: - os.fsync(outFile.fileno()) - except Exception: - # fsync may not be available on some platforms; ignore best-effort - pass - - # Replace atomically when possible, with retries to mitigate transient locks on Windows - attempts = 6 - delay = 0.1 - last_err = None - for attempt in range(attempts): - try: - os.replace(tmp_path, final_path) - last_err = None - break - except PermissionError as e: - last_err = e - # Try to relax permissions on target if it exists, then back off - try: - if os.path.exists(final_path): - os.chmod(final_path, 0o666) - except Exception: - pass - time.sleep(delay) - delay = min(1.0, delay * 2) - except Exception as e: - last_err = e - break - if last_err is not None: - # Fallback: try move via shutil (not guaranteed atomic), then raise on failure - try: - shutil.move(tmp_path, final_path) - except Exception: - # Ensure tmp is cleaned up if move failed - try: - if os.path.exists(tmp_path): - os.remove(tmp_path) - except Exception: - pass - raise last_err - except Exception: - # Best-effort; don't crash the translation if saving fails - traceback.print_exc() - - -def checkSave(data, filename, tokens): - """Save progress only if the given tokens reflect an actual translation. - tokens should be a [input_tokens, output_tokens] pair returned by a search/translate call. - """ - try: - # Never save progress to translated/ during speaker-parse mode - if SPEAKER_PARSE_MODE: - return - if not tokens: - return - if (isinstance(tokens, (list, tuple)) and len(tokens) >= 2 and (tokens[0] or tokens[1])): - saveProgress(data, filename) - except Exception: - # Don't let saving issues affect the translation flow - traceback.print_exc() - - -def update_vocab_section(category: str, pairs: list[tuple[str, str]]): - """Update or insert a section in vocab.txt for the given category with provided pairs. - Only writes when there's an actual translation (dst is non-empty and differs from src after normalization). - - category: e.g., "Items", "Weapons", etc. Section header will be "# {category}". - - pairs: list of (source, translated) strings. Duplicates by source are deduped (last wins). - The existing section is replaced entirely; other sections are preserved. - """ - try: - vocab_path = Path("vocab.txt") - - # Helper: normalized comparison to detect no-op translations - def _norm(s: str) -> str: - if s is None: - return "" - # Collapse whitespace and case-fold; leave punctuation to avoid over-matching - return re.sub(r"\s+", " ", str(s)).strip().casefold() - - # Filter and deduplicate by source term (last mapping wins) - dedup: dict[str, str] = {} - for src, dst in pairs: - if not src: - continue - # Skip when no destination or no actual change - if dst is None or _norm(dst) == "" or _norm(dst) == _norm(src): - continue - dedup[src] = dst - - # If nothing to add after filtering, skip touching the file - if not dedup: - return - - # Guard the read-modify-write with a dedicated lock to avoid races - with VOCAB_LOCK: - existing = vocab_path.read_text(encoding="utf-8") if vocab_path.exists() else "" - - lines = [f"{src} ({dst})" for src, dst in dedup.items()] - # Always terminate a section with a blank line to separate from next header - new_block = f"# {category}\n" + "\n".join(lines) - if not new_block.endswith("\n\n"): - if not new_block.endswith("\n"): - new_block += "\n" - new_block += "\n" - - # Regex to find the specific section starting at the header for this category - # and ending right before the next header (any number of '#') or EOF. - # - Handles headers like '#Category', '# Category', '## Category', etc. - # - Uses non-greedy matching for the body to avoid spanning multiple sections. - pattern = re.compile( - rf"^[\t ]*#+\s*{re.escape(category)}\s*$\r?\n.*?(?=^[\t ]*#|\Z)", - re.MULTILINE | re.DOTALL, - ) - if pattern.search(existing): - # Replace only the first matching section for this category. - updated = pattern.sub(lambda m: new_block, existing, count=1) - else: - updated = existing - if updated and not updated.endswith("\n\n"): - # Ensure a blank line before appending new section if file not empty - if not updated.endswith("\n"): - updated += "\n" - updated += "\n" - updated += new_block - - # Avoid writing if nothing changed - if updated == existing: - return - # Atomic write: write to unique temp and replace with retries on Windows - tmp_path = vocab_path.with_suffix(vocab_path.suffix + f".{os.getpid()}.{threading.get_ident()}.tmp") - tmp_path.write_text(updated, encoding="utf-8") - - attempts = 6 - delay = 0.1 - last_err = None - for attempt in range(attempts): - try: - os.replace(tmp_path, vocab_path) - last_err = None - break - except PermissionError as e: - last_err = e - # Try relaxing permissions then retry - try: - if vocab_path.exists(): - os.chmod(vocab_path, 0o666) - except Exception: - pass - time.sleep(delay) - delay = min(1.0, delay * 2) - except Exception as e: - last_err = e - break - if last_err is not None: - try: - shutil.move(str(tmp_path), str(vocab_path)) - except Exception: - try: - if tmp_path.exists(): - tmp_path.unlink(missing_ok=True) - except Exception: - pass - raise last_err - except Exception: - traceback.print_exc() - - -def parseMap(data, filename): - totalTokens = [0, 0] - events = data["events"] - global LOCK - - # --- Preflight: estimate exact progress total using the same translation batching --- - class _CountingBar: - def __init__(self): - self.n = 0 - def update(self, n=1): - try: - self.n += int(n) if n is not None else 1 - except Exception: - self.n += 1 - def write(self, *args, **kwargs): - pass - def refresh(self): - pass - - def _estimate_map_units(d, fname) -> int: - # Avoid deep copy - just count items directly - count = 0 - try: - # Count display name TL (1 unit if present) - if "Map" in fname and isinstance(d.get("display_name", None), str): - count += 1 - - # Notes and pages - count actual translatable items - evts = d.get("events", {}) or {} - for evt in evts.values(): - if not evt: - continue - note_val = evt.get("note") or "" - if not isinstance(note_val, str): - note_val = str(note_val) if note_val is not None else "" - - # Count note-based translations - if "" in note_val: - name_val = evt.get("name") or "" - if isinstance(name_val, str) and name_val: - count += 1 - - if "", note_val, re.DOTALL) - count += len(matches) - - if ".*", note_val) - count += len(matches) - if ".*", note_val) - count += len(matches) - - # Count commands in pages (rough estimate) - for page in (evt.get("pages", []) or []): - if page and "list" in page: - # Count translatable codes - for cmd in page.get("list", []): - if cmd and "c" in cmd: - code = cmd["c"] - # Count common translatable codes - if code in [401, 405, 102, 122, 408, 355, 655, 356, 357, 320, 324, 325, 111, 108, 657]: - count += 1 - - return count if count > 0 else 1 - except Exception: - return 1 - - - # Translate display_name for Map files - if "Map" in filename: - response = translateAI( - data["display_name"], - "Reply with only the " + LANGUAGE + " translation of the RPG location name", - False, - ) - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - data["display_name"] = response[0].replace('"', "") - - totalLines = _estimate_map_units(data, filename) - if not isinstance(totalLines, int) or totalLines <= 0: - # Fallback to naive count so a bar still renders - totalLines = 0 - for evt in events.values(): - if evt: - for page in (evt.get("pages", []) or []): - try: - totalLines += len(page.get("list", [])) - except Exception: - pass - global PBAR - - # Process each page synchronously with progress updates - with tqdm(total=totalLines, bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE, desc=filename) as pbar: - PBAR = pbar - for event in events.values(): - if event is not None and isinstance(event, dict): - # Normalize note to a safe string - note_val = event.get("note") or "" - if not isinstance(note_val, str): - note_val = str(note_val) if note_val is not None else "" - - # This translates ID of events. (May break the game) - if ".*") - if tok and isinstance(tok, (list, tuple)): - totalTokens[0] += tok[0] - totalTokens[1] += tok[1] - if ".*") - if tok and isinstance(tok, (list, tuple)): - totalTokens[0] += tok[0] - totalTokens[1] += tok[1] - - for page in event["pages"]: - if page is not None: - try: - totalTokensPage = searchCodes(page, pbar, [], filename) - totalTokens[0] += totalTokensPage[0] - totalTokens[1] += totalTokensPage[1] - except Exception as e: - traceback.print_exc() - return [data, totalTokens, e] - finally: - # Persist progress only if this page produced tokens - checkSave(data, filename, totalTokensPage) - return [data, totalTokens, None] - - -def translateNote(event, regex, wordwrap=False): - # Regex String - jaString = event.get("note") or "" - if not isinstance(jaString, str): - jaString = str(jaString) if jaString is not None else "" - match = re.findall(regex, jaString, re.DOTALL) - if match: - tokens = [0, 0] - i = 0 - while i < len(match): - initialJAString = match[i] - modifiedJAString = initialJAString - # Remove any textwrap - if wordwrap: - modifiedJAString = modifiedJAString.replace("\n", " ") - - # Translate - response = translateAI( - modifiedJAString, - "Reply with only the " + LANGUAGE + " translation.", - False, - ) - translatedText = response[0] - tokens[0] += response[1][0] - tokens[1] += response[1][1] - - # Textwrap - if wordwrap: - translatedText = dazedwrap.wrapText(translatedText, width=NOTEWIDTH) - translatedText = translatedText.replace('"', "") - - jaString = jaString.replace(initialJAString, translatedText) - event["note"] = jaString - i += 1 - return tokens - return [0, 0] - - -# For notes that can't have spaces. -def translateNoteOmitSpace(event, regex): - # Regex that only matches text inside LB. - jaString = event.get("note") or "" - if not isinstance(jaString, str): - jaString = str(jaString) if jaString is not None else "" - - match = re.findall(regex, jaString, re.DOTALL) - if match: - oldJAString = match[0] - # Remove any textwrap - jaString = re.sub(r"\n", " ", oldJAString) - - # Translate - response = translateAI( - jaString, - "Reply with the " + LANGUAGE + " translation of the location name.", - False, - ) - # Defend against unexpected response shapes - try: - translatedText = response[0] - token_info = response[1] if isinstance(response, (list, tuple)) and len(response) > 1 else [0, 0] - if not (isinstance(token_info, (list, tuple)) and len(token_info) >= 2): - token_info = [0, 0] - except Exception: - translatedText = str(response) if response is not None else "" - token_info = [0, 0] - - translatedText = translatedText.replace('"', "") - translatedText = translatedText.replace(" ", "_") - # Safely update the note if it exists and is a string - current_note = event.get("note") - if isinstance(current_note, str): - event["note"] = current_note.replace(oldJAString, translatedText) - return token_info - return [0, 0] - - -def parseCommonEvents(data, filename): - totalTokens = [0, 0] - global LOCK - - # --- Preflight: estimate exact progress total using same batching --- - class _CountingBar: - def __init__(self): - self.n = 0 - def update(self, n=1): - try: - self.n += int(n) if n is not None else 1 - except Exception: - self.n += 1 - def write(self, *args, **kwargs): - pass - def refresh(self): - pass - - def _estimate_units(pages, fname) -> int: - # Avoid deep copy - just count commands directly - count = 0 - try: - for page in pages: - if page is not None and "list" in page: - for cmd in page.get("list", []): - if cmd and "c" in cmd: - code = cmd["c"] - if code in [401, 405, 102, 122, 408, 355, 655, 356, 357, 320, 324, 325, 111, 108, 657]: - count += 1 - return count if count > 0 else 1 - except Exception: - return 1 - - totalLines = _estimate_units(data, filename) - if not isinstance(totalLines, int) or totalLines <= 0: - # Fallback to naive command count - totalLines = 0 - for page in data: - if page is not None: - try: - totalLines += len(page.get("list", [])) - except Exception: - pass - global PBAR - - with tqdm(total=totalLines, bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE, desc=filename) as pbar: - PBAR = pbar - for page in data: - if page is not None: - try: - totalTokensPage = searchCodes(page, pbar, [], filename) - totalTokens[0] += totalTokensPage[0] - totalTokens[1] += totalTokensPage[1] - except Exception as e: - traceback.print_exc() - return [data, totalTokens, e] - finally: - # Persist progress only if this page produced tokens - checkSave(data, filename, totalTokensPage) - return [data, totalTokens, None] - - -def parseTroops(data, filename): - totalTokens = [0, 0] - global LOCK - - # --- Preflight total using same code paths --- - class _CountingBar: - def __init__(self): - self.n = 0 - def update(self, n=1): - try: - self.n += int(n) if n is not None else 1 - except Exception: - self.n += 1 - def write(self, *args, **kwargs): - pass - def refresh(self): - pass - - def _estimate_units(troops, fname) -> int: - # Avoid deep copy - just count commands directly - count = 0 - try: - for troop in troops: - if troop is None: - continue - for page in (troop.get("pages", []) or []): - if page is not None and "list" in page: - for cmd in page.get("list", []): - if cmd and "c" in cmd: - code = cmd["c"] - if code in [401, 405, 102, 122, 408, 355, 655, 356, 357, 320, 324, 325, 111, 108, 657]: - count += 1 - return count if count > 0 else 1 - except Exception: - return 1 - - totalLines = _estimate_units(data, filename) - if not isinstance(totalLines, int) or totalLines <= 0: - totalLines = 0 - for troop in data: - if troop is not None: - for page in troop.get("pages", []) or []: - try: - totalLines += len(page.get("list", [])) - except Exception: - pass - global PBAR - - with tqdm(total=totalLines, bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE, desc=filename) as pbar: - PBAR = pbar - for troop in data: - if troop is not None: - for page in troop["pages"]: - if page is not None: - try: - totalTokensPage = searchCodes(page, pbar, [], filename) - totalTokens[0] += totalTokensPage[0] - totalTokens[1] += totalTokensPage[1] - except Exception as e: - traceback.print_exc() - return [data, totalTokens, e] - finally: - # Persist progress only if this page produced tokens - checkSave(data, filename, totalTokensPage) - return [data, totalTokens, None] - - -def parseNames(data, filename, context): - totalTokens = [0, 0] - - # --- Preflight: custom estimator that mirrors searchNames increments (incl. notes/messages) --- - class _CountingBar: - def __init__(self): - self.n = 0 - def update(self, n=1): - try: - self.n += int(n) if n is not None else 1 - except Exception: - self.n += 1 - def write(self, *args, **kwargs): - pass - def refresh(self): - pass - - def _estimate_names_units(entries, ctx, fname) -> int: - # Avoid deep copy - just count fields directly - count = 0 - try: - note_regexes = [ - (r"", False), - (r"", False), - (r"", False), - (r"", False), - (r"", False), - (r"", False), - (r"", False), - (r"", False), - (r"", True), - (r"", False), - (r"\n(.*)\n", False), - (r"", False), - (r"WATs:(.+?)>", False), - (r"ADTs?:(.+?)>", False), - (r"", False), - (r"", False), - (r"]+)", True), - (r"]+)", True), - (r"]+)", True), - (r"", True), - (r"", True), - (r"", False), - (r"<拡張説明:(.+?)>", False), - (r"\n(.+?)\n<", False), - (r"text:(.+)>", False), - ] - - for entry in entries: - if not entry: - continue - nm = entry.get("name") or "" - ds = entry.get("description") or "" - nn = entry.get("nickname") or "" - pf = entry.get("profile") or "" - if ctx == "Actors": - if nm: count += 1 - if nn: count += 1 - if pf: count += 1 - elif ctx in ["Armors", "Weapons", "Items"]: - if nm: count += 1 - if ds: count += 1 - elif ctx == "Skills": - if nm: count += 1 - if ds: count += 1 - for k in range(1,5): - if entry.get(f"message{k}"): count += 1 - elif ctx in ["Enemies", "Classes", "MapInfos"]: - if nm: count += 1 - - # Notes counting - note = entry.get("note") or "" - if isinstance(note, str) and note: - for regex, _ww in note_regexes: - try: - matches = re.findall(regex, note, re.DOTALL) - except Exception: - matches = [] - if regex.startswith(r" 0 else 1 - except Exception: - return 1 - - total_units = _estimate_names_units(data, context, filename) - if not isinstance(total_units, int) or total_units <= 0: - # Reasonable fallback: count visible fields/messages (no notes) - total_units = 0 - for entry in data: - if not entry: - continue - if entry.get("name"): total_units += 1 - if context in ["Armors", "Weapons", "Items", "Skills"] and entry.get("description"): total_units += 1 - if context == "Actors": - if entry.get("nickname"): total_units += 1 - if entry.get("profile"): total_units += 1 - if context == "Skills": - for k in range(1,5): - if entry.get(f"message{k}"): total_units += 1 - global PBAR - - with tqdm(total=total_units, bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE, desc=filename) as pbar: - PBAR = pbar - try: - # Thread the filename through so progress saves write to the right file - result = searchNames(data, pbar, context, filename) - totalTokens[0] += result[0] - totalTokens[1] += result[1] - except Exception as e: - traceback.print_exc() - return [data, totalTokens, e] - finally: - # Persist progress only if this names pass produced tokens - checkSave(data, filename, totalTokens) - return [data, totalTokens, None] - - -def parseSS(data, filename): - totalTokens = [0, 0] - - # --- Preflight using searchSS over deep copy --- - class _CountingBar: - def __init__(self): - self.n = 0 - def update(self, n=1): - try: - self.n += int(n) if n is not None else 1 - except Exception: - self.n += 1 - def write(self, *args, **kwargs): - pass - def refresh(self): - pass - - def _estimate_units(states, fname) -> int: - # Avoid deep copy - just count fields directly - count = 0 - try: - for st in states: - if not st: - continue - if st.get("name"): count += 1 - if st.get("description"): count += 1 - for n in range(1,5): - if st.get(f"message{n}"): count += 1 - return count if count > 0 else 1 - except Exception: - return 1 - - total_units = _estimate_units(data, filename) - if not isinstance(total_units, int) or total_units <= 0: - total_units = 0 - for st in data: - if not st: - continue - if st.get("name"): total_units += 1 - if st.get("description"): total_units += 1 - for n in range(1,5): - if st.get(f"message{n}"): total_units += 1 - global PBAR - - with tqdm(total=total_units, bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE, desc=filename) as pbar: - PBAR = pbar - for ss in data: - if ss is not None: - try: - result = searchSS(ss, pbar) - totalTokens[0] += result[0] - totalTokens[1] += result[1] - except Exception as e: - traceback.print_exc() - return [data, totalTokens, e] - finally: - # Persist progress only if this state produced tokens - checkSave(data, filename, result) - return [data, totalTokens, None] - - -def parseSystem(data, filename): - totalTokens = [0, 0] - - # --- Preflight: call searchSystem on deep copy to count increments --- - class _CountingBar: - def __init__(self): - self.n = 0 - def update(self, n=1): - try: - self.n += int(n) if n is not None else 1 - except Exception: - self.n += 1 - def write(self, *args, **kwargs): - pass - def refresh(self): - pass - - def _estimate_units(sysobj, fname) -> int: - # Avoid deep copy - just count fields directly - count = 0 - try: - for term in sysobj.get("terms", {}) or {}: - termList = sysobj["terms"][term] - if isinstance(termList, list): - count += len(termList) - gt = sysobj.get("game_title") - if isinstance(gt, str) and gt: - count += 1 - count += len(sysobj.get("variables", []) or []) - count += len(sysobj.get("switches", []) or []) - count += len(sysobj.get("weapon_types", []) or []) - count += len(sysobj.get("armor_types", []) or []) - count += len(sysobj.get("skill_types", []) or []) - count += len(sysobj.get("equip_types", []) or []) - return count if count > 0 else 1 - except Exception: - return 1 - - total_units = _estimate_units(data, filename) - if not isinstance(total_units, int) or total_units <= 0: - # Fallback: rough count - total_units = 0 - for term in data.get("terms", {}) or {}: - termList = data["terms"][term] - if isinstance(termList, list): - total_units += len(termList) - gt = data.get("game_title") - if isinstance(gt, str) and gt: - total_units += 1 - total_units += len(data.get("variables", []) or []) - total_units += len(data.get("weapon_types", []) or []) - total_units += len(data.get("armor_types", []) or []) - total_units += len(data.get("skill_types", []) or []) - total_units += len(data.get("equip_types", []) or []) - global PBAR - - with tqdm(total=total_units, bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE, desc=filename) as pbar: - PBAR = pbar - try: - result = searchSystem(data, pbar) - totalTokens[0] += result[0] - totalTokens[1] += result[1] - except Exception as e: - traceback.print_exc() - return [data, totalTokens, e] - finally: - # Persist only if system sections produced tokens - checkSave(data, filename, result) - return [data, totalTokens, None] - - -def parseScenario(data, filename): - totalTokens = [0, 0] - global LOCK - - # --- Preflight: run searchCodes on each page list --- - class _CountingBar: - def __init__(self): - self.n = 0 - def update(self, n=1): - try: - self.n += int(n) if n is not None else 1 - except Exception: - self.n += 1 - def write(self, *args, **kwargs): - pass - def refresh(self): - pass - - def _estimate_units(scenario, fname) -> int: - # Avoid deep copy - just count commands directly - count = 0 - try: - for key, lst in scenario.items(): - if lst is not None and "list" in lst: - for cmd in lst.get("list", []): - if cmd and "c" in cmd: - code = cmd["c"] - if code in [401, 405, 102, 122, 408, 355, 655, 356, 357, 320, 324, 325, 111, 108, 657]: - count += 1 - return count if count > 0 else 1 - except Exception: - return 1 - - totalLines = _estimate_units(data, filename) - if not isinstance(totalLines, int) or totalLines <= 0: - totalLines = 0 - for _, lst in data.items(): - try: - totalLines += len(lst or []) - except Exception: - pass - global PBAR - - with tqdm(total=totalLines, bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE, desc=filename) as pbar: - PBAR = pbar - for page in data.items(): - if page[1] is not None: - try: - totalTokensPage = searchCodes(page[1], pbar, [], filename) - totalTokens[0] += totalTokensPage[0] - totalTokens[1] += totalTokensPage[1] - except Exception as e: - traceback.print_exc() - return [data, totalTokens, e] - finally: - # Persist progress only if this page produced tokens - checkSave(data, filename, totalTokensPage) - return [data, totalTokens, None] - - -def searchNames(data, pbar, context, filename): - totalTokens = [0, 0] - nameList = [] - profileList = [] - nicknameList = [] - descriptionList = [] - # For Skills: collect messages across all entries for batch translation - messagesList = [] # List of tuples: (entry_idx, message_field, message_text, needs_taro) - # Collect name mappings for vocab per run - vocab_pairs: list[tuple[str, str]] = [] - vocab_enabled = context in ["Armors", "Weapons", "Items", "MapInfos", "Classes", "Enemies", "Skills"] - # For batching all note types - notesBatch = [] # List of (i, regex, match_text, note_type) - notesBatchMap = [] # List of (i, regex, match_text, note_type, groupidx) - i = 0 # Counter - j = 0 # Counter 2 - filling = False - mismatch = False - batchFull = False - - # Set the context of what we are translating - if "Actors" in context: - newContext = "Reply with only the " + LANGUAGE + " translation of the NPC name" - if "Armors" in context: - newContext = "Reply with only the " + LANGUAGE + " translation of the RPG equipment name" - if "Classes" in context: - newContext = "Reply with only the " + LANGUAGE + " translation of the RPG class name" - if "MapInfos" in context: - newContext = "Reply with only the " + LANGUAGE + " translation of the location name" - if "Enemies" in context: - newContext = "Reply with only the " + LANGUAGE + " translation of the enemy NPC name" - if "Weapons" in context: - newContext = "Reply with only the " + LANGUAGE + " translation of the RPG weapon name" - if "Items" in context: - newContext = "Reply with only the " + LANGUAGE + " translation of the RPG item name" - if "Skills" in context: - newContext = "Reply with only the " + LANGUAGE + " translation of the RPG skill name" - - # Names - with open("log/translations.txt", "a", encoding="utf-8") as file: - file.write(f"\n#{context}\n") - - # --- Batching pass: collect all note texts for all note types --- - note_regexes = [ - (r"", False), - (r"", False), - (r"", False), - (r"", False), - (r"", False), - (r"", False), - (r"", False), - (r"", False), - (r"", True), - (r"", False), - (r"\n(.*)\n", False), - (r"", False), - (r"WATs:(.+?)>", False), - (r"ADTs?:(.+?)>", False), - (r"", False), - (r"", False), - (r"]+)", True), - (r"]+)", True), - (r"]+)", True), - (r"", True), - (r"", True), - (r"", False), - (r"<拡張説明:(.+?)>", False), - (r"\n(.+?)\n<", False), - (r"text:(.+)>", False), - ] - # For each entry, collect all note matches - for idx, entry in enumerate(data): - if entry is None or "note" not in entry or not entry["note"]: - continue - note = entry["note"] - for regex, wordwrap in note_regexes: - matches = re.findall(regex, note, re.DOTALL) - # Special filter for to skip if 'Client' is in the match - if regex.startswith(r"= len(translatedNotesBatch): - break - translated = translatedNotesBatch[note_insert_idx] - if wordwrap: - translated = dazedwrap.wrapText(translated, width=NOTEWIDTH) - translated = translated.replace('"', "") - # Use a safe literal match for the replacement (no re.escape, just str.replace) - data[idx]["note"] = data[idx]["note"].replace(match_text, translated, 1) - note_insert_idx += 1 - - # --- For Skills: Batch translate all messages --- - if context in ["Skills"]: - messages_batch = [] - messages_map = [] # List of (entry_idx, message_field, needs_taro) - - for idx, entry in enumerate(data): - if entry is None: - continue - # Collect all message1-4 fields - for msg_num in range(1, 5): - msg_field = f"message{msg_num}" - if msg_field in entry and entry[msg_field]: - msg_text = entry[msg_field] - needs_taro = len(msg_text) > 0 and msg_text[0] in ["は", "を", "の", "に", "が"] - if needs_taro: - messages_batch.append("Taro" + msg_text) - else: - messages_batch.append(msg_text) - messages_map.append((idx, msg_field, needs_taro)) - - # Batch translate all messages - if messages_batch: - response = translateAI( - messages_batch, - "reply with only the gender neutral " + LANGUAGE + " translation of the action log. For messages starting with Taro, always start the sentence with Taro. For example, translate 'Taroを倒した!' as 'Taro was defeated!'", - False, - ) - translated_messages = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - - # Apply translations back to data - for msg_idx, (entry_idx, msg_field, needs_taro) in enumerate(messages_map): - if msg_idx < len(translated_messages): - translation = translated_messages[msg_idx] - if needs_taro: - translation = translation.replace("Taro", "") - data[entry_idx][msg_field] = translation - - # Update progress for messages - if pbar is not None: - pbar.refresh() - - # Now continue with the rest of the batching logic for names, descriptions, etc. - i = 0 - filling = False - batchFull = False - mismatch = False - while i < len(data) or filling == True: - if i < len(data): - # Empty Data - if data[i] is None or data[i]["name"] == "": - i += 1 - continue - # Filling up Batch - filling = True - if context in "Actors": - if len(nameList) < BATCHSIZE: - if data[i]["name"] != "": - nameList.append(data[i]["name"]) - if "nickname" in data[i] and data[i]["nickname"]: - nicknameList.append(data[i]["nickname"]) - if "profile" in data[i] and data[i]["profile"]: - profileList.append(data[i]["profile"].replace("\n", " ")) - i += 1 - else: - batchFull = True - if context in ["Armors", "Weapons", "Items"]: - if len(nameList) < BATCHSIZE: - nameList.append(data[i]["name"]) - if "description" in data[i] and data[i]["description"] != "": - description = data[i]["description"] - description = description.replace("\n", " ") - descriptionList.append(description) - i += 1 - else: - batchFull = True - if context in ["Skills"]: - if len(nameList) < BATCHSIZE: - nameList.append(data[i]["name"]) - if "description" in data[i] and data[i]["description"]: - descriptionList.append(data[i]["description"].replace("\n", " ")) - i += 1 - else: - batchFull = True - if context in ["Enemies", "Classes", "MapInfos"]: - if len(nameList) < BATCHSIZE: - nameList.append(data[i]["name"]) - i += 1 - else: - batchFull = True - - # Batch Full - if batchFull == True or i >= len(data): - k = j # Original Index - if context in "Actors": - # Track tokens for this batch - batchTokens = [0, 0] - # Name - response = translateAI(nameList, newContext) - translatedNameBatch = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - batchTokens[0] += response[1][0] - batchTokens[1] += response[1][1] - if pbar is not None and nameList: - pbar.refresh() - - # Nickname - if nicknameList: - response = translateAI(nicknameList, newContext) - translatedNicknameBatch = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - batchTokens[0] += response[1][0] - batchTokens[1] += response[1][1] - if pbar is not None: - pbar.refresh() - - # Profile - if profileList: - response = translateAI(profileList, "") - translatedProfileBatch = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - batchTokens[0] += response[1][0] - batchTokens[1] += response[1][1] - if pbar is not None: - pbar.refresh() - - # Set Data - if len(nameList) == len(translatedNameBatch): - j = k - while j < i: - # Empty Data - if data[j] is None or data[j]["name"] == "": - j += 1 - continue - else: - # Get Text - if data[j]["name"] != "": - with open("log/translations.txt", "a", encoding="utf-8") as file: - file.write(f'{data[j]["name"]} ({translatedNameBatch[0]})\n') - # Actors are excluded from vocab updates - data[j]["name"] = translatedNameBatch[0] - translatedNameBatch.pop(0) - if "nickname" in data[j] and data[j]["nickname"]: - data[j]["nickname"] = translatedNicknameBatch[0] - translatedNicknameBatch.pop(0) - if "profile" in data[j] and data[j]["profile"]: - data[j]["profile"] = dazedwrap.wrapText(translatedProfileBatch[0], LISTWIDTH) - translatedProfileBatch.pop(0) - - # If Batch is empty. Move on. - if len(translatedNameBatch) == 0: - nameList.clear() - profileList.clear() - nicknameList.clear() - batchFull = False - filling = False - j += 1 - # Persist after applying this batch only if we actually translated something in this batch - checkSave(data, filename, batchTokens) - else: - mismatch = True - - if context in ["Armors", "Weapons", "Items", "Skills"]: - # Track tokens for this batch - batchTokens = [0, 0] - # Name - response = translateAI(nameList, newContext) - translatedNameBatch = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - batchTokens[0] += response[1][0] - batchTokens[1] += response[1][1] - if pbar is not None and nameList: - pbar.refresh() - - # Description - if descriptionList: - response = translateAI( - descriptionList, - f"Reply with only the {LANGUAGE} translation of the text.", - True, - ) - translatedDescriptionBatch = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - batchTokens[0] += response[1][0] - batchTokens[1] += response[1][1] - if pbar is not None: - pbar.refresh() - - # Set Data - if len(nameList) == len(translatedNameBatch): - j = k - with open("log/translations.txt", "a", encoding="utf-8") as file: - while j < i: - # Empty Data - if data[j] is None or data[j]["name"] == "": - j += 1 - continue - else: - # Get Text - file.write(f"{data[j]['name']} ({translatedNameBatch[0]})\n") - if vocab_enabled: - try: - vocab_pairs.append((data[j]['name'], translatedNameBatch[0])) - except Exception: - pass - data[j]["name"] = translatedNameBatch[0] - translatedNameBatch.pop(0) - if "description" in data[j] and data[j]["description"] != "": - translatedDescriptionBatch[0] = dazedwrap.wrapText(translatedDescriptionBatch[0], LISTWIDTH) - data[j]["description"] = translatedDescriptionBatch[0] - translatedDescriptionBatch.pop(0) - - # If Batch is empty. Move on. - if len(translatedNameBatch) == 0: - nameList.clear() - descriptionList.clear() - batchFull = False - filling = False - j += 1 - # Persist after applying this batch only if we actually translated something in this batch - checkSave(data, filename, batchTokens) - else: - mismatch = True - if context in ["Enemies", "Classes", "MapInfos"]: - # Track tokens for this batch - batchTokens = [0, 0] - response = translateAI(nameList, newContext) - translatedNameBatch = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - batchTokens[0] += response[1][0] - batchTokens[1] += response[1][1] - if pbar is not None and nameList: - pbar.refresh() - - # Set Data - if len(nameList) == len(translatedNameBatch): - j = k - while j < i: - # Empty Data - if data[j] is None or data[j]["name"] == "": - j += 1 - continue - else: - with open("log/translations.txt", "a", encoding="utf-8") as file: - file.write(f'{data[j]["name"]} ({translatedNameBatch[0]})\n') - # Get Text - if vocab_enabled: - try: - vocab_pairs.append((data[j]["name"], translatedNameBatch[0])) - except Exception: - pass - data[j]["name"] = translatedNameBatch[0] - translatedNameBatch.pop(0) - - # If Batch is empty. Move on. - if len(translatedNameBatch) == 0: - nameList.clear() - batchFull = False - filling = False - j += 1 - # Persist after applying this batch only if we actually translated something in this batch - checkSave(data, filename, batchTokens) - else: - mismatch = True - - # Mismatch - if mismatch == True: - MISMATCH.append(nameList) - nameList.clear() - profileList.clear() - descriptionList.clear() - filling = False - mismatch = False - batchFull = False - - i += 1 - - # Update vocab section once per context after processing all names - if vocab_enabled and vocab_pairs: - update_vocab_section(context, vocab_pairs) - - return totalTokens - - -def searchCodes(page, pbar, jobList, filename): - if len(jobList) > 0: - list401 = jobList[0] - list122 = jobList[1] - list355655 = jobList[2] - list108 = jobList[3] - list356 = jobList[4] - list357 = jobList[5] - list324 = jobList[6] - list408 = jobList[7] - list325 = jobList[8] - setData = False - else: - list401 = [] - list122 = [] - list355655 = [] - list108 = [] - list356 = [] - list357 = [] - list324 = [] - list408 = [] - list325 = [] - setData = True - textHistory = [] - match = [] - totalTokens = [0, 0] - translatedText = "" - speaker = "" - speakerID = None - syncIndex = 0 - maxHistory = MAXHISTORY - VNameValue = None - global LOCK - global NAMESLIST - global MISMATCH - global PBAR - with LOCK: - PBAR = pbar - - # Begin Parsing File - try: - # Normal Format - if "list" in page: - codeList = page["list"] - - # Special Format (Scenario) - else: - codeList = page - - # Iterate through page - i = 0 - while i < len(codeList): - with LOCK: - # syncIndex will keep i in sync when it gets modified - if syncIndex > i: - i = syncIndex - if len(codeList) <= i: - break - - # Declare Varss - currentGroup = [] - nametag = "" - - ## Event Code: 401 Show Text - if "c" in codeList[i] and codeList[i]["c"] in [401, 405, -1] and (CODE401 or CODE405): - # Save Code and starting index (j) - code = codeList[i]["c"] - j = i - endtag = "" - instantLineFlag = False - - # Grab String - if len(codeList[i]["p"]) > 0: - jaString = codeList[i]["p"][0] - oldjaString = jaString - else: - codeList[i]["c"] = -1 - i += 1 - continue - - # # For Retarded Devs - # retardRegex = r'([\\]+[nN]\[[\\]+V\[\d*?\]\])' - # match = re.search(retardRegex, jaString) - # if match: - # if VNameValue == 1: - # jaString = re.sub(retardRegex, 'リッカ', jaString) - # if VNameValue == 2: - # jaString = re.sub(retardRegex, 'ミミ', jaString) - # if VNameValue == 3: - # jaString = re.sub(retardRegex, 'ヒトミ', jaString) - # if VNameValue == 4: - # jaString = re.sub(retardRegex, 'Taro', jaString) - # if VNameValue == 5: - # jaString = re.sub(retardRegex, '富士見', jaString) - - # Speaker Check - speakerList = [] - - # Remove any RPGMaker Code at start - ffMatch = re.search( - r"^((?:[\\]+[^cCnNiIkKvVrRlL]+\[[\d\w]+\])+)", - jaString, - ) - if ffMatch != None: - jaString = jaString.replace(ffMatch.group(0), "") - nametag += ffMatch.group(0) - - # m and z Codes - match = re.search(r"(.*?)[\\]+m\[\d+?\][\\]+z\[\d+?\]", jaString) - if match: - speakerList.append(match.group(1)) - if "\\c" in speakerList[0]: - speakerList = re.findall( - r"^[\\]+[cC]\[[^\]]+\]【(.+?)】[\\]+[cC]\[[^\]]+\](?:[\\]+[A-Za-z]+(?:\[[^\]]*\])?)*[\\]*$", - speakerList[0], - ) - - # Brackets (support multiple names like 【A】【B】) - # This now properly detects speakers after control codes are removed - if len(speakerList) == 0: - # Strip \sp, \l, \r control codes to check for bracketed speakers - testString = re.sub(r"^(?:[\\]+[splrSPLR]\[[^\]]*\]\s*)+", "", jaString) - - # Check if the line contains bracketed names 【name】 - startsWithBracket = re.match(r"^\s*【", testString) is not None - endsWithBracket = re.search( - r"(】\s*$|】\s*(?:[\\]+[A-Za-z]+(?:\[(?:[^\[\]]|\[[^\]]*\])*\])+\s*)$)", - testString, - ) is not None - - if startsWithBracket and endsWithBracket: - candidates = re.findall(r"【(.*?)】", testString) - if candidates: - candidates = [c.strip() for c in candidates] - if candidates: - speakerList = candidates - - # Colors (strict): require a bracketed name inside color codes to count as a speaker - # This prevents arbitrary color-wrapped sentences from being misclassified as speakers. - if len(speakerList) == 0: - speakerList = re.findall( - r"^[\\]+[cC]\[[^\]]+\]【(.+?)】[\\]+[cC]\[[^\]]+\](?:[\\]+[A-Za-z]+(?:\[[^\]]*\])?)*[\\]*$", - jaString, - ) - - # Colons - if len(speakerList) == 0: - speakerList = re.findall( - r"(.+):$", - jaString, - ) - - # First Line Speakers - if len(speakerList) == 0 and FIRSTLINESPEAKERS is True: - # Test Speaker - if ( - len(jaString) < 40 - and "c" in codeList[i + 1] - and codeList[i + 1]["c"] in [401, 405, -1] - and len(codeList[i + 1]["p"]) > 0 - and len(codeList[i + 1]["p"][0]) > 0 - ): - nextString = codeList[i + 1]["p"][0].strip() - - # Remove any RPGMaker Code at start - ffMatchNS = re.search( - r"^((?:[\\]+[^cCnNiIkKvVSsrRlL{}]+?\[[\d\w\W]+?\]?\])+)", - nextString, - ) - if ffMatchNS != None: - nextString = nextString.replace(ffMatchNS.group(1), "") - - # Remove other format codes - formatMatch = re.search(r"(^[\\]+[\W]+?)", nextString) - if formatMatch != None: - nextString = nextString.replace(formatMatch.group(1), "") - - # If next line starts with dialogue marker, current line is likely a speaker - if nextString and nextString[0] in [ - "「", - '"', - "(", - "(", - "*", - "[", - ]: - speakerList = re.findall(r".+", jaString) - - # Replace Speaker - if len(speakerList) != 0 and codeList[i + 1]["c"] in [401, 405, -1]: - # Single - if len(speakerList) == 1: - response = getSpeaker(speakerList[0]) - speaker = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - - # Multiple (Brackets) - elif len(speakerList) > 1: - jaStringUpdated = jaString - for idx, sp in enumerate(speakerList): - response = getSpeaker(sp) - tled = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - if not setData: - pattern = r"【\s*" + re.escape(sp) + r"\s*】" - jaStringUpdated = re.sub(pattern, lambda m: f"【{tled}】", jaStringUpdated) - # Back-compat: set 'speaker' to the first translated name - if idx == 0: - speaker = tled - - # Set Data - if not setData and len(speakerList) > 1: - codeList[i]["p"][0] = nametag + jaStringUpdated - elif not setData and len(speakerList) == 1: - codeList[i]["p"][0] = nametag + jaString.replace(speakerList[0], speaker) - nametag = "" - - # Iterate to next string - i += 1 - j = i - while codeList[i]["c"] in [-1]: - i += 1 - j = i - jaString = codeList[i]["p"][0] - - # Check if there is text to translate - if not re.search(r"\w+", jaString): - i += 1 - continue - - # Validate Japanese Text - if not re.search(LANGREGEX, jaString) and IGNORETLTEXT: - i += 1 - continue - - # Using this to keep track of 401's in a row. - currentGroup.append(jaString) - - # Join Up 401's into single string - if len(codeList) > i + 1: - while codeList[i + 1]["c"] in [401, 405, -1] and len(codeList[i]["p"]) > 0 and len(codeList[i + 1]["p"]) > 0 and not re.match(r"^(\s*[\\]+[aAbBdDeEfFgGhHjJlLmMoOpPqQrRsStTuUwWxXyYzZ]+\[[\w\d\[\]\\]+\])", codeList[i+1]["p"][0]): - if not setData: - codeList[i]["p"] = [] - codeList[i]["c"] = -1 - i += 1 - j = i - - jaString = codeList[i]["p"][0] - if jaString.strip(): - currentGroup.append(jaString) - - # Make sure not the end of the list. - if len(codeList) <= i + 1: - break - - # Format String - if len(currentGroup) > 0: - finalJAString = "\n".join(currentGroup) - oldjaString = finalJAString - - # Set Back - if not setData: - codeList[i]["p"] = [finalJAString] - - ### \\n - regex = r"([\\]+[kKnN][wWcCrRrEe]?[\[<](?:[\\]*\w\[\d+\])?(.*?)(?:[\\]*\w\[\d+\])?[>])" - match = re.search(regex, finalJAString) - - # Set Name - if match: - nametag = match.group(1) - speaker = match.group(2) - - # Translate Speaker - response = getSpeaker(speaker) - tledSpeaker = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - - # Set Nametag and Remove from Final String - finalJAString = finalJAString.replace(nametag, "") - nametag = nametag.replace(speaker, tledSpeaker) - speaker = tledSpeaker - - # Remove Extra Stuff bad for translation. - finalJAString = finalJAString.replace("゙", "") - finalJAString = finalJAString.replace("…", "...") - finalJAString = finalJAString.replace("。", ".") - finalJAString = re.sub(r"(\.{3}\.+)", "...", finalJAString) - finalJAString = finalJAString.replace(" ", "") - finalJAString = finalJAString.replace("「", '"') - finalJAString = finalJAString.replace("」", '"') - finalJAString = finalJAString.replace("\\,", ',') - - ### Remove format codes - # Furigana: \r or \rb [base,reading] -> keep reading/base per pattern - finalJAString = re.sub(r"[\\]+[rR][bB]?\[(.*?),.*?\]", r"\1", finalJAString) - - # Curly-brace furigana: {base|reading} -> keep base - finalJAString = re.sub(r"\{([^|{}]+)\|[^|{}]+?\}", r"\1", finalJAString) - - # Remove any RPGMaker Code at start - ffMatch = re.search( - r"^((?:[\\]+[^cCnNiIkKvVSsrRlL{}]+?\[[\d\w\W]+?\]?\])+)", - finalJAString, - ) - if ffMatch != None: - finalJAString = finalJAString.replace(ffMatch.group(1), "") - nametag = ffMatch.group(1) + nametag - - # Remove _ABL Codes - ffMatch = re.search(r"^(_ABL).*", finalJAString) - if ffMatch != None: - finalJAString = finalJAString.replace(ffMatch.group(1), "") - nametag += ffMatch.group(1) - - # Center Lines (We Nuke These) - if "\\CL" in finalJAString or "\\ac" in finalJAString or "\\#" in finalJAString: - finalJAString = finalJAString.replace("\\CL", "") - finalJAString = finalJAString.replace("\\ac", "") - finalJAString = finalJAString.replace("\\#", "") - - # Handle Formatting Codes - if "\\>" in finalJAString: - instantLineFlag = True - finalJAString = finalJAString.replace("\\>", "") - - # Check if Empty - if finalJAString == "": - if nametag and match: - codeList[j]["p"][0] = codeList[j]["p"][0].replace(match.group(2), tledSpeaker) - i += 1 - continue - - # Pass 1 (Grabbing Data) - if setData: - # Remove Textwrap - if FIXTEXTWRAP: - finalJAString = finalJAString.replace("\n", " ") - if "\\px[200]" in finalJAString: - finalJAString = finalJAString.replace("\\px[200]", "") - - # Append - if finalJAString != "": - if speaker == "" and finalJAString != "": - list401.append(finalJAString) - elif finalJAString != "": - list401.append(f"[{speaker}]: {finalJAString}") - else: - list401.append(speaker) - speaker = "" - match = [] - nametag = "" - currentGroup = [] - syncIndex = i + 1 - - # Keep textHistory list at length maxHistory - textHistory.append('"' + finalJAString + '"') - if len(textHistory) > maxHistory: - textHistory.pop(0) - - # Pass 2 (Setting Data) - else: - # Grab Translated String - if len(list401) > 0: - translatedText = list401[0] - - # Remove speaker - match = re.search(r'(^\[.+?\]\s?[|:]\s?)', translatedText) - if match: - translatedText = translatedText.replace(match.group(1), "") - - # Fix '- ' - translatedText = translatedText.replace("- ", "-") - - # Textwrap - if FIXTEXTWRAP is True: - finalJAString = re.sub(r"\n", " ", finalJAString) - finalJAString = finalJAString.replace("
", " ") - - if FIXTEXTWRAP is True and "_ABL" in nametag: - translatedText = dazedwrap.wrapText(translatedText, width=100) - elif FIXTEXTWRAP is True: - translatedText = dazedwrap.wrapText(translatedText, width=WIDTH) - - # Formatting Code - if instantLineFlag: - translatedText = translatedText.replace("\n", "\n\\>") - translatedText = f"\\>{translatedText}" - - # BR Flag - if BRFLAG is True: - translatedText = translatedText.replace("\n", "
") - - # px - if "\\px[200]" in nametag: - translatedText = translatedText.replace("\\px[200]", "") - translatedText = translatedText.replace("\n", "\n\\px[200]") - - # Add Nametag Back In - translatedText = nametag + translatedText - nametag = "" - - # Endtag - if endtag != "": - translatedText = translatedText + endtag - endtag = "" - - # Set Code - codeList[j]["c"] = code - - # Handle 405 - if codeList[j]["c"] == 405: - # 1. Split translatedText by newlines - lines = [line for line in translatedText.split('\n') if line.strip() != ""] - - # 2. Set the first string to codeList[j]["p"] - codeList[j]["p"] = [lines[0]] - - # 3. Make copies for each additional line and insert them - for idx, line in enumerate(lines[1:]): - new_item = copy.deepcopy(codeList[j]) - new_item["p"] = [line] - codeList.insert(j + idx + 1, new_item) - - # 4. Update syncIndex to the last modified/added position - syncIndex = j + len(lines) - - # Handle 401 - else: - codeList[j]["p"] = [translatedText] - codeList[j]["c"] = code - syncIndex = i + 1 - - # Reset - speaker = "" - match = [] - currentGroup = [] - list401.pop(0) - - ## Event Code: 122 [Set Variables] - if "c" in codeList[i] and codeList[i]["c"] == 122 and CODE122 is True: - # This is going to be the var being translated. - # Only translate variables within the specified range. - if codeList[i]["p"][0] not in list(range(CODE122_VAR_MIN, CODE122_VAR_MAX)): - i += 1 - continue - - jaString = codeList[i]["p"][4] - - # # For Retarded Devs - # VNameValue = jaString - # i += 1 - # continue - - # Validate String - if not isinstance(jaString, str): - i += 1 - continue - - # Definitely don't want to mess with files - if 'gameV' in jaString or '_' in jaString or '"[' in jaString: - i += 1 - continue - - # # Avoid anything not quoted - # if '\"' not in jaString: - # i += 1 - # continue - - # Validate Japanese Text - # if not re.search(LANGREGEX, jaString): - # i += 1 - # continue - - # Set String - matchedText = None - if len(re.findall(r"([\'\"\`])", jaString)) >= 2: - matchedText = re.search(r"[\'\"\`](.*)[\'\"\`]", jaString) - if matchedText and matchedText.group(1).strip(): - # Remove Textwrap - finalJAString = matchedText.group(1).replace("\\n", " ") - - # Pass 1 - if setData: - if finalJAString != "": - list122.append(finalJAString) - - # Pass 2 - else: - if len(list122) > 0: - # Grab and Replace - translatedText = list122[0] - translatedText = jaString.replace(jaString, translatedText) - - # Remove characters that may break scripts - charList = ['"', "\\n"] - for char in charList: - translatedText = translatedText.replace(char, "") - - # Force 4 Escapes - translatedText = re.sub(r'(? 0: - # Grab and Replace - translatedText = list357[0] - translatedText = jaString.replace(jaString, translatedText) - - # Remove characters that may break scripts - charList = ['"', "\\n"] - for char in charList: - translatedText = translatedText.replace(char, "") - - # Textwrap - # translatedText = dazedwrap.wrapText(translatedText, 80) - # translatedText = translatedText.replace("\n", "\\n") - # translatedText = re.sub(r"[\\]+c", r"\\\\c", translatedText) - translatedText = re.sub(r"[\\]+\*item", r"\\\\*item", translatedText) - - # Center Text - if acExist: - translatedText = f'\\ac {translatedText.replace('\n', '\n\\ac ')}' - - # Check and Set Font - if "fontSize" in codeList[i]["p"][3]: - if font: - codeList[i]["p"][3]["fontSize"] = font - - # Set - codeList[i]["p"][3][argVar] = f"{translatedText}" - list357.pop(0) - - # Map Plugins - headerMappings = { - "LL_InfoPopupWIndow": ("messageText", None), - "QuestSystem": ("DetailNote", None), - "BalloonInBattle": ("text", None), - "MNKR_CommonPopupCoreMZ": ("text", None), - "DestinationWindow": ("destination", None), - "_TMLogWindowMZ": ("text", None), - "TorigoyaMZ_NotifyMessage": ("message", None), - "SoR_GabWindow": ("arg1", None), - "DarkPlasma_CharacterText": ("text", None), - "DTextPicture": ("text", None), - "TextPicture": ("text", None), - "TRP_SkitMZ": ("name", None), - "LogWindow": ("text", None), - "BattleLogOutput": ("message", None), - "TorigoyaMZ_NotifyMessage_CommandMessage": ("message", None), - "NUUN_SaveScreen": ("AnyName", None), - } - - for key, (argVar, font) in headerMappings.items(): - if key in headerString: - translatePlugins(argVar, font) - - # AdvExtention plugin support (message event) - if headerString == "AdvExtentionllk" and len(codeList[i]["p"]) > 3: - try: - params_obj = codeList[i]["p"][3] - except Exception: - params_obj = None - - if isinstance(params_obj, dict): - # 1) Speaker comes from 'name', fallback to 'altName' if missing/empty - speaker_name = "" - if isinstance(params_obj.get("altName", None), str) and params_obj["altName"].strip(): - speaker_name = params_obj["altName"].strip() - if speaker_name: - response = getSpeaker(speaker_name) - params_obj["altName"] = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - speaker = response[0] - if isinstance(params_obj.get("name", None), str) and params_obj["name"].strip(): - speaker_name = params_obj["name"].strip() - if speaker_name: - response = getSpeaker(speaker_name) - params_obj["name"] = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - speaker = response[0] - speaker = "" - - # 2) Line comes from 'comment' if present, else 'text' - chosen_key = None - if isinstance(params_obj.get("comment", None), str) and params_obj["comment"].strip(): - chosen_key = "comment" - elif isinstance(params_obj.get("text", None), str): - chosen_key = "text" - - if chosen_key is not None: - jaString = params_obj.get(chosen_key, "") - if isinstance(jaString, str): - # Pass 1 (collect data) - if setData: - if FIXTEXTWRAP: - jaString = jaString.replace("\n", " ") - # Include speaker context like 401 does - if 'speaker' in locals() and isinstance(speaker, str) and speaker.strip(): - list357.append(f"[{speaker}]: {jaString}") - else: - list357.append(jaString) - # Pass 2 (apply translation) - else: - if len(list357) > 0: - translatedText = list357[0] - list357.pop(0) - - # Remove speaker prefix if present (same pattern used for 401) - m = re.search(r'(^\[.+?\]\s?[|:]\s?)', translatedText) - if m: - translatedText = translatedText.replace(m.group(1), "") - - if FIXTEXTWRAP: - translatedText = dazedwrap.wrapText(translatedText, width=WIDTH) - - params_obj[chosen_key] = translatedText - - if headerString == "LL_GalgeChoiceWindow": - ### Message Text First - jaString = codeList[i]["p"][3]["messageText"] - - # Remove any textwrap & TL - jaString = re.sub(r"\n", " ", jaString) - response = translateAI(jaString, "") - translatedText = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - - # Textwrap & Set - translatedText = dazedwrap.wrapText(translatedText, width=WIDTH) - codeList[i]["p"][3]["messageText"] = translatedText - - ### Choices - jaString = codeList[i]["p"][3]["choices"] - matchList = re.findall(r'"label[\\]*":[\\]*"(.*?)[\\]', jaString) - if matchList != None: - # Translate - question = codeList[i]["p"][3]["messageText"] - response = translateAI( - matchList, - f"Previous text for context: {question}\n", - True, - ) - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - translatedText = jaString - - # Replace Strings - for j in range(len(matchList)): - translatedText = translatedText.replace(matchList[j], response[0][j].replace('"', '')) - - # Set Data - codeList[i]["p"][3]["choices"] = translatedText - - ## Event Code: 657 [Picture Text] [Optional] - if "c" in codeList[i] and codeList[i]["c"] == 657 and CODE657 is True: - if "text" in codeList[i]["p"][0]: - jaString = codeList[i]["p"][0] - if not isinstance(jaString, str): - i += 1 - continue - - # Definitely don't want to mess with files - if "_" in jaString: - i += 1 - continue - - # If there isn't any Japanese in the text just skip - if not re.search(LANGREGEX, jaString): - i += 1 - continue - - # Remove outside text - startString = re.search(r"^[^一-龠ぁ-ゔァ-ヴー\<\>【】\\]+", jaString) - jaString = re.sub(r"^[^一-龠ぁ-ゔァ-ヴー\<\>【】\\]+", "", jaString) - endString = re.search(r"[^一-龠ぁ-ゔァ-ヴー\<\>【】。!?\\]+$", jaString) - jaString = re.sub(r"[^一-龠ぁ-ゔァ-ヴー\<\>【】。!?\\]+$", "", jaString) - if startString is None: - startString = "" - else: - startString = startString.group() - if endString is None: - endString = "" - else: - endString = endString.group() - - # Remove any textwrap - jaString = re.sub(r"\n", " ", jaString) - - # Translate - response = translateAI(jaString, "") - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - translatedText = response[0] - - # Remove characters that may break scripts - charList = [".", '"', "'"] - for char in charList: - translatedText = translatedText.replace(char, "") - - # Textwrap - translatedText = dazedwrap.wrapText(translatedText, width=WIDTH) - translatedText = startString + translatedText + endString - - # Set Data - codeList[i]["p"][0] = translatedText - - ## Event Code: 101 [Name] [Optional] - if "c" in codeList[i] and codeList[i]["c"] == 101 and CODE101 is True: - isVar = False - - # Grab String - jaString = "" - if len(codeList[i]["p"]) > 4: - jaString = codeList[i]["p"][4] - # Check for Var - elif len(codeList[i]["p"]) > 0: - jaString = codeList[i]["p"][0] - isVar = True - if not isinstance(jaString, str): - i += 1 - continue - - # Force Speaker using var - if "memerisu" in jaString.lower(): - speaker = "Memerisu" - i += 1 - continue - elif "thina" in jaString.lower(): - speaker = "Tina" - i += 1 - continue - elif "\\ap" in jaString: - speaker = re.search(r"[\\]+AP\[(.*?)\]", jaString).group(1) - i += 1 - continue - - # Get Speaker - match = re.search(r"^(?:[\\]+[cC]\[\d+?\])?([\w\s]+)", jaString) - if match: - jaString = match.group(1) - response = getSpeaker(jaString) - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - speaker = response[0] - - # Validate Speaker is not empty - if len(speaker) > 0: - if isVar == False: - codeList[i]["p"][4] = codeList[i]["p"][4].replace(jaString, speaker) - i += 1 - continue - else: - codeList[i]["p"][0] = codeList[i]["p"][0].replace(jaString, speaker) - isVar = False - i += 1 - continue - else: - speaker = "" - elif FACENAME101: - faceName = codeList[i]["p"][0] - if faceName == "Actor1_1": - speaker = "Sakura" - if faceName == "Actor2_1": - speaker = "Suzune" - if faceName == "Actor3_1": - speaker = "Kaji" - if faceName == "Actor4_1": - speaker = "Kirari" - if faceName == "Actor5_1": - speaker = "Onsen" - if faceName == "Actor6_1": - speaker = "Gufu" - if faceName == "Actor7_1": - speaker = "Kahimeru" - if faceName == "Actor10_1": - speaker = "Miuma" - if faceName == "Actor11_1": - speaker = "Nurari" - if faceName == "Actor12_1": - speaker = "Kokotsuzumi" - - ## Event Code: 355 or 655 Scripts [Optional] - if "c" in codeList[i] and (codeList[i]["c"] == 355 or codeList[i]["c"] == 655) and CODE355655 is True: - jaString = codeList[i]["p"][0] - - patterns = { - # "テキスト-": (r"テキスト-(.+)") - # "=": (r'=\s?(.*)",'), - # "var text": (r"var\stext\d+\s=\s\"(.+)\""), - # "logtxt = ": (r"logtxt\s=\s'(.+)'" - # ".setNickname": (r'.setNickname\(\\?"(.+?)\\?"\)' - # "_subject=": (r'_subject=(.+?)_' - # "text =": (r"text\s*=\s*'(.+[^\\])'"), - # "const text": (r'(const\stext\s?=\s?"(.+)";?)'), - # "ex_a_name": (r'ex_a_name\(\d+,"(.+)"\)'), - # "gameVariables.setValue": (r"\$gameVariables.setValue\(\d+,\s?'(.+)'\)"), - # "BattleManager._logWindow.push('addText'": (r"BattleManager._logWindow.push\('addText',\s'(.+)'\)"), - # "BattleManager._logWindow.addText": (r"BattleManager._logWindow.addText\('(.+)'\)"), - } - - for key, (regex) in patterns.items(): - if key in jaString: - match = re.search(regex, jaString) - if match: - # Check if the match contains actual text (not just numbers/special chars) - if not re.search(r'[a-zA-Z一-龠ぁ-ゔァ-ヴーa-zA-Z0-9\uFF61-\uFF9F]', match.group(1)): - continue - - # Pass 1 - if setData: - list355655.append(match.group(1)) - - # Pass 2 - else: - # Grab and Replace - translatedText = list355655[0] - list355655.pop(0) - - # Only escape if not already escaped - matchList = re.findall(r"(.+)'\s*[$+].+?'(.+)", translatedText) - if matchList: - for string in matchList[0]: - escapedMatch = re.sub(r"(? i + 1 - and "c" in codeList[i + 1] - and codeList[i + 1]["c"] in [408, -1] - and len(codeList[i + 1]["p"]) > 0 - and len(codeList[i + 1]["p"][0]) > 0 - ): - nextString = codeList[i + 1]["p"][0].strip() - - # Remove any RPGMaker Code at start - ffMatchNS = re.search( - r"^((?:[\\]+[^cCnNiIkKvVSsrRlL{}]+?\[[\d\w\W]+?\]?\])+)", - nextString, - ) - if ffMatchNS != None: - nextString = nextString.replace(ffMatchNS.group(1), "") - - # Remove other format codes - formatMatch = re.search(r"(^[\\]+[\W]+?)", nextString) - if formatMatch != None: - nextString = nextString.replace(formatMatch.group(1), "") - - # If next line starts with dialogue marker, current line is likely a speaker - if nextString and nextString[0] in [ - "「", - '"', - "(", - "(", - "*", - "[", - ]: - speakerList = re.findall(r".+", jaString) - - # Replace Speaker - if len(speakerList) != 0 and len(codeList) > i + 1 and codeList[i + 1]["c"] in [408, -1]: - # Single - if len(speakerList) == 1: - response = getSpeaker(speakerList[0]) - speaker = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - - # Multiple (Brackets) - elif len(speakerList) > 1: - jaStringUpdated = jaString - for idx, sp in enumerate(speakerList): - response = getSpeaker(sp) - tled = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - if not setData: - pattern = r"【\s*" + re.escape(sp) + r"\s*】" - jaStringUpdated = re.sub(pattern, lambda m: f"【{tled}】", jaStringUpdated) - # Back-compat: set 'speaker' to the first translated name - if idx == 0: - speaker = tled - - # Set Data - if not setData and len(speakerList) > 1: - codeList[i]["p"][0] = nametag + jaStringUpdated - elif not setData and len(speakerList) == 1: - codeList[i]["p"][0] = nametag + jaString.replace(speakerList[0], speaker) - nametag = "" - - # Iterate to next string - i += 1 - j = i - while codeList[i]["c"] in [-1]: - i += 1 - j = i - jaString = codeList[i]["p"][0] - - # Using this to keep track of 408's in a row. - currentGroup.append(jaString) - - # Join Up 408's into single string - if len(codeList) > i + 1 and JOIN408 is True: - while codeList[i + 1]["c"] in [408] and len(codeList[i]["p"]) > 0 and len(codeList[i + 1]["p"]) > 0 and not re.match(r"^(\s*[\\]+[aAbBdDeEfFgGhHjJlLmMoOpPqQrRsStTuUwWxXyYzZ]+\[[\w\d\[\]\\]+\])", codeList[i+1]["p"][0]): - if not setData: - codeList[i]["p"] = [] - codeList[i]["c"] = -1 - i += 1 - j = i - - jaString = codeList[i]["p"][0] - if jaString.strip(): - currentGroup.append(jaString) - - # Make sure not the end of the list. - if len(codeList) <= i + 1: - break - - # Format String - if len(currentGroup) > 0: - finalJAString = "\n".join(currentGroup) - - # Set Back - if not setData: - codeList[j]["p"] = [finalJAString] - - # Process speaker name tag if SPEAKERS408 enabled - if SPEAKERS408: - ### \\n - regex = r"([\\]+[kKnN][wWcCrRrEe]?[\[<](?:[\\]*\w\[\d+\])?(.*?)(?:[\\]*\w\[\d+\])?[>])" - matchSpeaker = re.search(regex, finalJAString) - - # Set Name - if matchSpeaker: - nametag = matchSpeaker.group(1) - speakerName = matchSpeaker.group(2) - - # Translate Speaker - response = getSpeaker(speakerName) - tledSpeaker = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - - # Set Nametag and Remove from Final String - finalJAString = finalJAString.replace(nametag, "") - nametag = nametag.replace(speakerName, tledSpeaker) - speaker = tledSpeaker - - # Pass 1 - if setData: - # Remove Textwrap - finalJAString = finalJAString.replace("\n", " ") - - # Append with or without speaker - if SPEAKERS408 and speaker != "" and finalJAString != "": - list408.append(f"[{speaker}]: {finalJAString}") - else: - list408.append(finalJAString) - - speaker = "" - currentGroup = [] - syncIndex = i + 1 - - # Pass 2 - else: - if len(list408) > 0: - translatedText = list408[0] - list408.pop(0) - - # Remove speaker if SPEAKERS408 enabled - if SPEAKERS408: - matchSpeaker = re.search(r'(^\[.+?\]\s?[|:]\s?)', translatedText) - if matchSpeaker: - translatedText = translatedText.replace(matchSpeaker.group(1), "") - - # Textwrap - translatedText = dazedwrap.wrapText(translatedText, width=WIDTH) - - # Add Nametag Back In if SPEAKERS408 enabled - if SPEAKERS408 and nametag: - translatedText = nametag + translatedText - nametag = "" - - # Set Data - codeList[j]["p"][0] = translatedText - - # Reset - speaker = "" - currentGroup = [] - syncIndex = i + 1 - - ## Event Code: 108 (Script) - if "c" in codeList[i] and (codeList[i]["c"] == 108) and CODE108 is True: - jaString = codeList[i]["p"][0] - - # If there isn't any Japanese in the text just skip - if not re.search(LANGREGEX, jaString): - i += 1 - continue - - # Translate - if "info:" in jaString: - regex = r"info:([^,]+)" - elif "ActiveMessage:" in jaString: - regex = r"?" - elif "event_text" in jaString: - regex = r"event_text\s*:\s*(.*)" - elif "Menu Name" in jaString: - regex = r"Menu\sName\s*:\s*(.*)>" - elif "text_indicator" in jaString: - regex = r"text_indicator\s?:\s?(.+)" - else: - i += 1 - continue - - # Need to remove outside code and put it back later - match = re.search(regex, jaString) - if match: - # Pass 1 - if setData: - list108.append(match.group(1)) - - # # Grab Next - # j = i - # while codeList[j + 1]["c"] == 408: - # j += 1 - # list108[0] = list108[0] + codeList[j]["p"][0].replace(">", "") - # codeList[j]["p"][0] = "" - # list108[0] = list108[0].replace("\n", " ") - - # Pass 2 - else: - # Grab and Replace - translatedText = list108[0] - list108.pop(0) - - # Textwrap - # if codeList[i + 1]["c"] == 408: - # translatedText = dazedwrap.wrapText(translatedText, WIDTH) - - # Remove characters that may break scripts - charList = ['"'] - for char in charList: - translatedText = translatedText.replace(char, "") - translatedText = translatedText.replace('"', '"') - translatedText = translatedText.replace(" ", "_") - translatedText = jaString.replace(match.group(1), translatedText) - - # Add > - if "ActiveMessage" in translatedText and ">" not in translatedText: - translatedText = translatedText + ">" - - # Set Data - codeList[i]["p"][0] = translatedText - - ## Event Code: 356 - if "c" in codeList[i] and codeList[i]["c"] == 356 and CODE356 is True: - jaString = codeList[i]["p"][0] - oldjaString = jaString - - # Grab Speaker - if "Tachie showName" in jaString: - matchList = re.findall(r"Tachie showName (.+)", jaString) - if len(matchList) > 0: - # Translate - response = translateAI( - matchList[0], - "Reply with the " + LANGUAGE + " translation of the NPC name.", - False, - ) - translatedText = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - - # Set Text - speaker = translatedText - speaker = speaker.replace(" ", " ") - codeList[i]["p"][0] = jaString.replace(matchList[0], speaker) - i += 1 - continue - - # Want to translate this script - if "D_TEXT " in jaString: - regex = r"D_TEXT\s*([^\s]+)\s?\d*" - elif "ShowInfo" in jaString: - regex = r"ShowInfo\s(.*)" - elif "PushGab" in jaString: - regex = r"PushGab\s(.*)" - elif "addLog" in jaString: - regex = r"addLog\s(.*)" - elif "DW_" in jaString: - regex = r"DW_.*\s\d+\s(.+)" - elif "CommonPopup" in jaString: - regex = r"CommonPopup\sadd\stext:(.*?)[\\]+}" - elif "AddCustomChoice" in jaString: - regex = r"AddCustomChoice\s\d+\s(.+)\s\d" - else: - regex = r"" - - # Remove any textwrap - jaString = re.sub(r"\n", "_", jaString) - - # Capture Arguments and text - textMatch = re.search(regex, jaString) - if textMatch and textMatch.group(0) != "": - text = textMatch.group(1) - - # Capture Speakers - match = re.search(r"[\\]+ow\[\d+\][\\]+c\[\d+\](.+)", text) - if match: - speakerJA = match.group(1) - - # Translate - response = getSpeaker(speakerJA) - translatedText = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - codeList[i]["p"][0] = jaString.replace(speakerJA, translatedText) - i += 1 - continue - else: - speaker = "" - - # Pass 1 - if setData: - text = text.replace("_", " ") - list356.append(text) - - # Pass 2 - else: - if len(list356) > 0: - # Grab - translatedText = list356[0] - - # Remove characters that may break scripts - charList = [".", '"'] - for char in charList: - translatedText = translatedText.replace(char, "") - - # Cant have spaces? - translatedText = translatedText.replace(" ", "_") - translatedText = translatedText.replace("__", "_") - - # Put Args Back - translatedText = jaString.replace(text, translatedText) - - # Set Data - codeList[i]["p"][0] = translatedText - list356.pop(0) - - if "namePop" in jaString: - # Support both "" and "namePop [num] text" formats - matchList = re.findall(r"]+)>", jaString) - if not matchList: - m = re.search(r"\bnamePop\b\s*(?:-?\d+)?\s*([^\r\n<>]+)", jaString) - if m: - matchList = [m.group(1).strip()] - if len(matchList) > 0: - # Translate - text = matchList[0] - response = translateAI(text, "Reply with the " + LANGUAGE + " Translation") - translatedText = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - - # Set Data - updated = jaString.replace(text, translatedText.replace(" ", "_")) - codeList[i]["p"][0] = updated - - if "LL_InfoPopupWIndowMV" in jaString: - matchList = re.findall(r"LL_InfoPopupWIndowMV\sshowWindow\s(.+?) .+", jaString) - if len(matchList) > 0: - text = matchList[0] - - # Pass 1: collect into batch - if setData: - # store without underscores for cleaner translation later - list356.append(text.replace("_", " ")) - - # Pass 2: apply translations from list356 - else: - if len(list356) > 0: - translatedText = list356[0] - list356.pop(0) - - # Replace spaces with underscores as original format expects - translatedText = translatedText.replace(" ", "_") - - # Put Args Back - translatedText = jaString.replace(text, translatedText) - - # Set Data - codeList[i]["p"][0] = translatedText - - if "OriginMenuStatus SetParam" in jaString: - matchList = re.findall(r"OriginMenuStatus\sSetParam\sparam[\d]\s(.*)", jaString) - if len(matchList) > 0: - # Translate - text = matchList[0] - response = translateAI(text, "Reply with the " + LANGUAGE + " Translation") - translatedText = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - - # Set Data - translatedText = translatedText.replace(" ", "_") - translatedText = jaString.replace(text, translatedText) - codeList[i]["p"][0] = translatedText - - # LL_GalgeChoiceWindowMV Message - if "LL_GalgeChoiceWindowMV setMessageText" in jaString: - ### Message Text First - match = re.search(r"LL_GalgeChoiceWindowMV setMessageText (.+)", jaString) - if match: - jaString = match.group(1) - - # Remove any textwrap & TL - jaString = re.sub(r"\n", " ", jaString) - response = translateAI(jaString, "") - translatedText = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - - # Textwrap & Replace Whitespace - translatedText = dazedwrap.wrapText(translatedText, width=WIDTH) - translatedText = translatedText.replace(" ", "_") - - # Replace and Set - translatedText = match.group(0).replace(match.group(1), translatedText) - codeList[i]["p"][0] = translatedText - - # LL_GalgeChoiceWindowMV Choices - if "LL_GalgeChoiceWindowMV setChoices": - match = re.search(r"LL_GalgeChoiceWindowMV setChoices (.+)", jaString) - if match: - jaString = match.group(1) - choiceList = jaString.split(",") - - # Translate - question = translatedText - response = translateAI( - choiceList, - f"Previous text for context: {question}\n", - True, - ) - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - choiceListTL = response[0] - translatedText = match.group(0) - - # Replace Strings - for j in range(len(choiceListTL)): - choiceListTL[j] = choiceListTL[j].replace(" ", "_") - translatedText = translatedText.replace(choiceList[j], choiceListTL[j]) - - # Set Data - codeList[i]["p"][0] = translatedText - - ### Event Code: 102 Show Choice - if "c" in codeList[i] and codeList[i]["c"] == 102 and CODE102 is True: - choiceList = [] - varList = [] - choiceIndexMap = [] # Track which original indices we're processing - - # Process each string in the parameters list - for choice in range(len(codeList[i]["p"][0])): - jaString = codeList[i]["p"][0][choice] - jaString = jaString.replace(" 。", ".") - - # Avoid Empty Strings - if not jaString.strip(): - continue - - # If and En Statements - ifVar = "" - ifList = re.findall(r"([ei][nf]\(.+?\)\)?\)?)", jaString) - if len(ifList) != 0: - for var in ifList: - jaString = jaString.replace(var, "") - ifVar += var - - # Store the formatting and cleaned string - varList.append(ifVar) - choiceList.append(jaString) - choiceIndexMap.append(choice) - - # Translate the list - if len(choiceList) > 0: - if len(textHistory) > 0: - response = translateAI( - choiceList, - f"Reply with the English translation of the dialogue choice.\n\nPrevious text for context: {str(textHistory)}\n", - True, - ) - else: - response = translateAI(choiceList, "Reply with the English translation of the dialogue choice.") - - translatedTextList = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - - # Check Mismatch and set translations - if len(translatedTextList) == len(choiceList): - for idx, translatedText in enumerate(translatedTextList): - originalIndex = choiceIndexMap[idx] - - # Apply formatting - if translatedText != "": - translatedText = varList[idx] + translatedText[0].upper() + translatedText[1:] - else: - translatedText = varList[idx] + translatedText - - # Set the translation back to the original position - codeList[i]["p"][0][originalIndex] = translatedText - else: - if filename not in MISMATCH: - MISMATCH.append(filename) - - ### Event Code: 111 Script - if "c" in codeList[i] and codeList[i]["c"] == 111 and CODE111 is True: - for j in range(len(codeList[i]["p"])): - jaString = codeList[i]["p"][j] - - # Check if String - if not isinstance(jaString, str): - continue - - # Only TL the Game Variable - if "$gameVariables" not in jaString: - continue - - # This is going to be the var being set. (IMPORTANT) - if "1045" not in jaString: - continue - - # Need to remove outside code and put it back later - matchList = re.findall(r"'(.*?)'", jaString) - - for match in matchList: - response = translateAI(match, "") - translatedText = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - - # Remove characters that may break scripts - charList = [".", '"', "'", "\\n"] - for char in charList: - translatedText = translatedText.replace(char, "") - - jaString = jaString.replace(match, translatedText) - - # Set Data - translatedText = jaString - codeList[i]["p"][j] = translatedText - - ### Event Code: 320 Set Variable - if "c" in codeList[i] and codeList[i]["c"] == 320 and CODE320 is True: - jaString = codeList[i]["p"][1] - if not isinstance(jaString, str): - i += 1 - continue - - # Definitely don't want to mess with files - if "■" in jaString or "_" in jaString: - i += 1 - continue - - # If there isn't any Japanese in the text just skip - if not re.search(LANGREGEX, jaString): - i += 1 - continue - - # Translate - response = getSpeaker(jaString) - translatedText = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - - # Remove characters that may break scripts - charList = [".", '"', "'", "\\n"] - for char in charList: - translatedText = translatedText.replace(char, "") - - # Set Data - codeList[i]["p"][1] = translatedText - - ### Event Code: 325 - if "c" in codeList[i] and codeList[i]["c"] == 325 and CODE325 is True: - # Expect parameters like [index, "text"] where parameters[1] is the string - if len(codeList[i]["p"]) <= 1: - i += 1 - continue - - jaString = codeList[i]["p"][1] - if not isinstance(jaString, str): - i += 1 - continue - - # Remove Textwrap - collectString = jaString.replace("\n", " ") - - # Pass 1: collect into batch - if setData: - list325.append(collectString) - - # Pass 2: apply translations from batch - else: - if len(list325) > 0: - translatedText = list325[0] - list325.pop(0) - - # Textwrap - translatedText = dazedwrap.wrapText(translatedText, width=WIDTH) - - # Set translated value back into parameters[1] - codeList[i]["p"][1] = "\\}\\}" + translatedText - - ### Event Code: 324 - if "c" in codeList[i] and codeList[i]["c"] == 324 and CODE324 is True: - # Expect parameters like [1, "text"] where index 1 is the string to translate - if len(codeList[i]["p"]) <= 1: - i += 1 - continue - - jaString = codeList[i]["p"][1] - if not isinstance(jaString, str): - i += 1 - continue - - # Remove any textwrap for collection - collectString = jaString.replace("\n", " ") - - # Pass 1: collect - if setData: - list324.append(collectString) - - # Pass 2: apply translations from list324 - else: - if len(list324) > 0: - translatedText = list324[0] - list324.pop(0) - - # Clean translation - for ch in ['"', "\\n"]: - translatedText = translatedText.replace(ch, "") - - # Textwrap to reasonable width - translatedText = dazedwrap.wrapText(translatedText, width=LISTWIDTH) - - # Set translated value back into parameters[1] - codeList[i]["p"][1] = translatedText - - # Iterate - i += 1 - - # EOF - list401TL = [] - list408TL = [] - list324TL = [] - list122TL = [] - list356TL = [] - list357TL = [] - list355655TL = [] - list108TL = [] - list325TL = [] - PBAR = pbar - - # 401 - if len(list401) > 0: - response = translateAI(list401, "") - list401TL = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - if len(list401TL) != len(list401): - with LOCK: - if filename not in MISMATCH: - MISMATCH.append(filename) - - # 122 - if len(list122) > 0: - response = translateAI(list122, "Keep your translation as brief as possible") - list122TL = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - if len(list122TL) != len(list122): - with LOCK: - if filename not in MISMATCH: - MISMATCH.append(filename) - - # 355/655 - if len(list355655) > 0: - response = translateAI(list355655, textHistory) - list355655TL = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - if len(list355655TL) != len(list355655): - with LOCK: - if filename not in MISMATCH: - MISMATCH.append(filename) - - # 108 - if len(list108) > 0: - response = translateAI(list108, "This text is a label. Use title capitalization and keep it brief.") - list108TL = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - if len(list108TL) != len(list108): - with LOCK: - if filename not in MISMATCH: - MISMATCH.append(filename) - - # 356 - if len(list356) > 0: - response = translateAI(list356, textHistory) - list356TL = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - if len(list356TL) != len(list356): - with LOCK: - if filename not in MISMATCH: - MISMATCH.append(filename) - - # 357 - if len(list357) > 0: - response = translateAI(list357, textHistory) - list357TL = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - if len(list357TL) != len(list357): - with LOCK: - if filename not in MISMATCH: - MISMATCH.append(filename) - - # 408 - if len(list408) > 0: - response = translateAI(list408, "") - list408TL = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - if len(list408TL) != len(list408): - with LOCK: - if filename not in MISMATCH: - MISMATCH.append(filename) - - # 324 - if len(list324) > 0: - # Generic short-text translation for parameter index 1 - response = translateAI(list324, "Reply with only the " + LANGUAGE + " translation of the text.") - list324TL = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - if len(list324TL) != len(list324): - with LOCK: - if filename not in MISMATCH: - MISMATCH.append(filename) - - # 325 - if len(list325) > 0: - # Use same short-text speaker-style translation as other name fields - response = translateAI(list325, "Reply with the " + LANGUAGE + " translation of the NPC name.") - list325TL = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - if len(list325TL) != len(list325): - with LOCK: - if filename not in MISMATCH: - MISMATCH.append(filename) - - # Start Pass 2 - if setData: - clearSpeakerCache() # Clear cache to ensure fresh speaker translations in Pass 2 - searchCodes( - page, - pbar, - [ - list401TL, - list122TL, - list355655TL, - list108TL, - list356TL, - list357TL, - list324TL, - list408TL, - list325TL, - ], - filename, - ) - - # Delete all -1 codes - codeListFinal = [] - for i in range(len(codeList)): - if "c" in codeList[i] and codeList[i]["c"] != -1: - codeListFinal.append(codeList[i]) - - # Normal Format - if "list" in page: - page["list"] = codeListFinal - - # Special Format (Scenario) - else: - page[:] = codeListFinal - except IndexError as e: - traceback.print_exc() - except Exception as e: - traceback.print_exc() - - return totalTokens - - -def searchSS(state, pbar): - totalTokens = [0, 0] - - # --- Batch collection for basic fields and messages --- - batch_texts = [] - batch_map = [] # [(field_type, field_name, needs_taro_prefix), ...] - - # Name - if "name" in state and state["name"]: - batch_texts.append(state["name"]) - batch_map.append(("name", "name", False)) - - # Description - if "description" in state and state["description"]: - batch_texts.append(state["description"]) - batch_map.append(("description", "description", False)) - - # Messages - collect all with Taro prefix handling - for msg_field in ["message1", "message2", "message3", "message4"]: - if msg_field in state and state[msg_field]: - msg_text = state[msg_field] - needs_taro = len(msg_text) > 0 and msg_text[0] in ["は", "を", "の", "に", "が"] - if needs_taro: - batch_texts.append("Taro" + msg_text) - else: - batch_texts.append(msg_text) - batch_map.append(("message", msg_field, needs_taro)) - - # --- Batch translate all basic fields --- - nameResponse = "" - descriptionResponse = "" - message1Response = "" - message2Response = "" - message3Response = "" - message4Response = "" - - if batch_texts: - response = translateAI( - batch_texts, - "reply with only the gender neutral " + LANGUAGE + " translation. For messages starting with Taro, always start the sentence with Taro. For example, translate 'Taroを倒した!' as 'Taro was defeated!'", - False, - ) - translated_batch = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - - # Map translations back to their fields - for idx, (field_type, field_name, needs_taro) in enumerate(batch_map): - if idx < len(translated_batch): - translation = translated_batch[idx] - if field_type == "name": - nameResponse = [translation, [0, 0]] - elif field_type == "description": - descriptionResponse = [translation, [0, 0]] - elif field_type == "message": - response_obj = [translation, [0, 0]] - if field_name == "message1": - message1Response = response_obj - elif field_name == "message2": - message2Response = response_obj - elif field_name == "message3": - message3Response = response_obj - elif field_name == "message4": - message4Response = response_obj - - # --- Batching pass: collect all note texts for all note types --- - note_regexes = [ - (r"]*)>", False), - (r"\n(.*)\n", False), - (r"", False), - (r"", False), - (r"(.+)", True), - ] - notesBatch = [] - notesBatchMap = [] - if "note" in state and state["note"]: - note = state["note"] - for regex, wordwrap in note_regexes: - matches = re.findall(regex, note, re.DOTALL) - for m in matches: - match_text = m if isinstance(m, str) else m[0] - notesBatch.append(match_text) - notesBatchMap.append((regex, match_text, wordwrap)) - - # --- Batch translate all notes --- - translatedNotesBatch = [] - if notesBatch: - response = translateAI(notesBatch, f"Reply with only the {LANGUAGE} translation of the note text.") - translatedNotesBatch = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - # Notes don't update progress - - # --- Insert translated notes back --- - note_insert_idx = 0 - if "note" in state and state["note"]: - for regex, match_text, wordwrap in notesBatchMap: - if note_insert_idx >= len(translatedNotesBatch): - break - translated = translatedNotesBatch[note_insert_idx] - if wordwrap: - translated = dazedwrap.wrapText(translated, width=NOTEWIDTH) - translated = translated.replace('"', "") - # Replace only the matched text in the note using a literal replacement - # Avoid re.sub here because replacement strings with backslashes (e.g., \I) - # are interpreted as escapes and can raise re.PatternError. - state["note"] = state["note"].replace(match_text, translated, 1) - note_insert_idx += 1 - - # Progress accounting for this state: name + description + messages present - if pbar is not None: - work_units = 0 - work_units += 1 if nameResponse != "" else 0 - work_units += 1 if descriptionResponse != "" else 0 - work_units += 1 if message1Response != "" else 0 - work_units += 1 if message2Response != "" else 0 - work_units += 1 if message3Response != "" else 0 - work_units += 1 if message4Response != "" else 0 - if work_units: - pbar.refresh() - - # Set Data - if "name" in state and nameResponse != "": - state["name"] = nameResponse[0].replace('"', "") - if "description" in state and descriptionResponse != "": - # Textwrap - translatedText = descriptionResponse[0] - translatedText = dazedwrap.wrapText(translatedText, width=LISTWIDTH) - state["description"] = translatedText.replace('"', "") - if "message1" in state and message1Response != "": - state["message1"] = message1Response[0].replace('"', "").replace("Taro", "") - if "message2" in state and message2Response != "": - state["message2"] = message2Response[0].replace('"', "").replace("Taro", "") - if "message3" in state and message3Response != "": - state["message3"] = message3Response[0].replace('"', "").replace("Taro", "") - if "message4" in state and message4Response != "": - state["message4"] = message4Response[0].replace('"', "").replace("Taro", "") - - return totalTokens - - -def searchSystem(data, pbar): - totalTokens = [0, 0] - context = "Reply with only the " + LANGUAGE + ' translation of the UI textbox."' - - # Title - batch as a single-item list - response = translateAI( - [data["game_title"]], - " Reply with the " + LANGUAGE + " translation of the game title name", - False, - ) - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - data["game_title"] = response[0][0].strip(".") - if pbar is not None: - pbar.refresh() - - # Terms - batch translate all term items - for term in data["terms"]: - if term != "messages": - termList = data["terms"][term] - term_values = [] - term_indices = [] - for i in range(len(termList)): - if termList[i] is not None: - term_values.append(termList[i]) - term_indices.append(i) - - if term_values: - response = translateAI(term_values, context) - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - tl_list = response[0] - - for n, idx in enumerate(term_indices[: len(tl_list)]): - termList[idx] = tl_list[n].replace('"', "").strip() - - if pbar is not None: - pbar.refresh() - - # Armor Types - batch translate all (check if exists) - if "armor_types" in data and data["armor_types"]: - armor_values = [data["armor_types"][i] for i in range(len(data["armor_types"]))] - if armor_values: - response = translateAI( - armor_values, - "Reply with only the " + LANGUAGE + " translation of the armor type", - False, - ) - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - tl_list = response[0] - for i in range(min(len(tl_list), len(data["armor_types"]))): - data["armor_types"][i] = tl_list[i].replace('"', "").strip() - if pbar is not None: - pbar.refresh() - - # Skill Types - batch translate all (check if exists) - if "skill_types" in data and data["skill_types"]: - skill_values = [data["skill_types"][i] for i in range(len(data["skill_types"]))] - if skill_values: - response = translateAI( - skill_values, - "Reply with only the " + LANGUAGE + " translation", - False, - ) - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - tl_list = response[0] - for i in range(min(len(tl_list), len(data["skill_types"]))): - data["skill_types"][i] = tl_list[i].replace('"', "").strip() - if pbar is not None: - pbar.refresh() - - # Equip Types - batch translate all (check if exists) - if "equip_types" in data and data["equip_types"]: - equip_values = [data["equip_types"][i] for i in range(len(data["equip_types"]))] - if equip_values: - response = translateAI( - equip_values, - "Reply with only the " + LANGUAGE + " translation of the equipment type. No disclaimers.", - False, - ) - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - tl_list = response[0] - for i in range(min(len(tl_list), len(data["equip_types"]))): - data["equip_types"][i] = tl_list[i].replace('"', "").strip() - if pbar is not None: - pbar.refresh() - - # Elements - batch translate all (skip empty) - element_values = [] - element_indices = [] - for i in range(len(data["elements"])): - if data["elements"][i]: # Skip empty strings - element_values.append(data["elements"][i]) - element_indices.append(i) - - if element_values: - response = translateAI( - element_values, - "Reply with only the " + LANGUAGE + " translation of the element type", - False, - ) - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - tl_list = response[0] - for n, idx in enumerate(element_indices[: len(tl_list)]): - data["elements"][idx] = tl_list[n].replace('"', "").strip() - if pbar is not None: - pbar.refresh() - - # Weapon Types - batch translate all (skip empty) - weapon_key = "weaponTypes" if "weaponTypes" in data else "weapon_types" - if weapon_key in data and data[weapon_key]: - weapon_values = [] - weapon_indices = [] - for i in range(len(data[weapon_key])): - if data[weapon_key][i]: # Skip empty strings - weapon_values.append(data[weapon_key][i]) - weapon_indices.append(i) - - if weapon_values: - response = translateAI( - weapon_values, - "Reply with only the " + LANGUAGE + " translation of the weapon type", - False, - ) - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - tl_list = response[0] - for n, idx in enumerate(weapon_indices[: len(tl_list)]): - data[weapon_key][idx] = tl_list[n].replace('"', "").strip() - if pbar is not None: - pbar.refresh() - - # Variables (Optional usually) — batch translate to reduce calls - if TLSYSTEMVARIABLES and "variables" in data and isinstance(data["variables"], list): - var_indices = [] - var_values = [] - for idx, val in enumerate(data["variables"]): - if isinstance(val, str) and val.strip(): - var_indices.append(idx) - var_values.append(val) - if var_values: - response = translateAI( - var_values, - 'Reply with only the ' + LANGUAGE + ' translation of the title', - True, - ) - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - tl_list = response[0] - # Assign back translations to corresponding indices - for n, idx in enumerate(var_indices[: len(tl_list)]): - data["variables"][idx] = tl_list[n].replace('"', '').strip() - if pbar is not None: - pbar.refresh() - - # Switches (Optional) — batch translate to reduce calls - if TLSYSTEMSWITCHES and "switches" in data and isinstance(data["switches"], list): - switch_indices = [] - switch_values = [] - for idx, val in enumerate(data["switches"]): - if isinstance(val, str) and val.strip(): - switch_indices.append(idx) - switch_values.append(val) - if switch_values: - response = translateAI( - switch_values, - 'Reply with only the ' + LANGUAGE + ' translation of the switch name', - True, - ) - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - tl_list = response[0] - # Assign back translations to corresponding indices - for n, idx in enumerate(switch_indices[: len(tl_list)]): - data["switches"][idx] = tl_list[n].replace('"', '').strip() - if pbar is not None: - pbar.refresh() - - # Messages — batch translate to reduce calls (check if exists) - if "messages" in data["terms"]: - messages = data["terms"]["messages"] - if messages: - msg_keys = [] - msg_values = [] - for key, value in messages.items(): - if isinstance(value, str) and value.strip(): - msg_keys.append(key) - msg_values.append(value) - - if msg_values: - response = translateAI( - msg_values, - "Reply with only the " - + LANGUAGE - + ' translation of the battle text.\nTranslate "常時ダッシュ" as "Always Dash"\nTranslate "次の%1まで" as Next %1.', - False, - ) - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - tl_list = response[0] - - # Remove characters that may break scripts - charList = [".", '"', "\\n"] - - # Assign back translations to corresponding keys - for n, key in enumerate(msg_keys[: len(tl_list)]): - translatedText = tl_list[n] - for char in charList: - translatedText = translatedText.replace(char, "") - messages[key] = translatedText - - if pbar is not None: - pbar.refresh() - - return totalTokens - -# Save some money and enter the character before translation -def getSpeaker(speaker: str): - """Return (and possibly collect) speaker name. - - Parse mode (SPEAKER_PARSE_MODE=True): - - Don't translate immediately. Collect unique originals in SPEAKER_COLLECTED. - - Return original so caller logic works; token cost is zero. - - Normal mode: translate immediately with caching. - """ - if speaker == "": - return ["", [0, 0]] - - # Preflight count mode: skip translation and caching entirely - if 'PREFLIGHT_COUNT_MODE' in globals() and PREFLIGHT_COUNT_MODE: - return [speaker, [0, 0]] - - if SPEAKER_PARSE_MODE: - with _speakerCacheLock: - if speaker in _speakerCache: - return [_speakerCache[speaker], [0, 0]] - if speaker not in SPEAKER_COLLECTED: - SPEAKER_COLLECTED.append(speaker) - return [speaker, [0, 0]] - - # Normal mode translation path - with _speakerCacheLock: - cached = _speakerCache.get(speaker) - if cached is not None: - return [cached, [0, 0]] - - try: - THREAD_CTX.in_speaker = True - except Exception: - pass - response = translateAI( - speaker, - "Reply with the " + LANGUAGE + " translation of the NPC name.", - False, - ) - try: - THREAD_CTX.in_speaker = False - except Exception: - pass - translated = response[0].title().replace("'S", "'s").replace("Speaker: ", "") - - if re.search(r"([a-zA-Z??])", translated) is None: - try: - THREAD_CTX.in_speaker = True - except Exception: - pass - response = translateAI( - speaker, - "Reply with the " + LANGUAGE + " translation of the NPC name.", - False, - ) - try: - THREAD_CTX.in_speaker = False - except Exception: - pass - translated = response[0].title().replace("'S", "'s") - - with _speakerCacheLock: - if speaker not in _speakerCache: - _speakerCache[speaker] = translated - NAMESLIST.append([speaker, translated]) - return [translated, response[1]] - -def translateAI(text, history, history_ctx=None): - """ - Legacy wrapper function for the new shared translation utility. - This maintains compatibility with existing code while using the new shared implementation. - """ - global PBAR, MISMATCH, FILENAME - - # Update config estimate mode based on global ESTIMATE - TRANSLATION_CONFIG.estimateMode = bool(ESTIMATE) - - # Call the new shared translation function - # Prefer thread-local filename for logging; fall back to global - try: - tl_filename = getattr(THREAD_CTX, "filename", FILENAME) - except Exception: - tl_filename = FILENAME - - # Speaker-parse mode: bypass all non-speaker translations to save tokens - if SPEAKER_PARSE_MODE and not getattr(THREAD_CTX, "in_speaker", False): - # Return original text unmodified with zero tokens - return [text, [0, 0]] - - # Preflight count mode: don't hit API; just simulate progress units - if 'PREFLIGHT_COUNT_MODE' in globals() and PREFLIGHT_COUNT_MODE: - try: - n = len(text) if isinstance(text, list) else 1 - except Exception: - n = 1 - if PBAR is not None: - try: - with LOCK: - PBAR.update(n) - except Exception: - pass - # Return original payload and zero tokens so totals aren't affected - return [text, [0, 0]] - - return sharedtranslateAI( - text=text, - history=history, - config=TRANSLATION_CONFIG, - filename=tl_filename, - pbar=PBAR, - lock=LOCK, - mismatchList=MISMATCH - ) - -def resetSpeakerState(): - """Clear all speaker-related globals so a fresh run doesn't carry over stale data.""" - global NAMESLIST, SPEAKER_COLLECTED - NAMESLIST = [] - SPEAKER_COLLECTED = [] - with _speakerCacheLock: - _speakerCache.clear() - -def setSpeakerParseMode(flag: bool): - """Enable/disable speaker-only parse mode.""" - global SPEAKER_PARSE_MODE - SPEAKER_PARSE_MODE = bool(flag) - -def finalizeSpeakerParse(): - """Batch translate collected speakers and write fresh # Speakers section.""" - if not SPEAKER_PARSE_MODE: - return - try: - # Step 1: batch translate any collected speakers not already translated - to_translate = [] - with _speakerCacheLock: - for s in SPEAKER_COLLECTED: - if s not in _speakerCache and s != "": - to_translate.append(s) - if to_translate: - try: - THREAD_CTX.in_speaker = True - except Exception: - pass - resp = translateAI( - to_translate, - "Reply with the " + LANGUAGE + " translation of the NPC name.", - True, - ) - try: - THREAD_CTX.in_speaker = False - except Exception: - pass - # Record token usage so it appears in the TOTAL string - try: - with LOCK: - TOKENS[0] += resp[1][0] - TOKENS[1] += resp[1][1] - except Exception: - pass - # Emit a one-time summary line for speaker translation using the same format - try: - cost = calculateCost(resp[1][0], resp[1][1], MODEL) - totalTokenstring = ( - Fore.YELLOW + "[Input: " + str(resp[1][0]) + "]" - "[Output: " - + str(resp[1][1]) - + "]" "[Cost: ${:,.4f}".format(cost) - + "]" - ) - tqdm.write("Speakers: " + totalTokenstring + Fore.GREEN + " \u2713 " + Fore.RESET) - except Exception: - pass - tl_list = resp[0] - with _speakerCacheLock: - for orig, tl in zip(to_translate, tl_list): - norm = tl.title().replace("'S", "'s").replace("Speaker: ", "") - if re.search(r"([a-zA-Z??])", norm) is None: - norm = tl # keep raw if heuristic fails - if orig not in _speakerCache: - _speakerCache[orig] = norm - NAMESLIST.append([orig, norm]) - - vocab_path = Path("vocab.txt") - if not vocab_path.exists(): - return - content = vocab_path.read_text(encoding="utf-8") - - seen = set() - lines = [] - for orig, tl in NAMESLIST: - if not orig or not tl: - continue - if orig in seen: - continue - seen.add(orig) - lines.append(f"{orig} ({tl})") - if not lines: - return - section_block = "# Speakers\n" + "\n".join(lines) + "\n\n" - - speakers_pattern = re.compile(r"^[\t ]*#+\s*Speakers\s*$\r?\n.*?(?=^[\t ]*#|\Z)", re.MULTILINE | re.DOTALL) - content = speakers_pattern.sub("", content) - - game_char_header = re.compile(r"^[\t ]*#\s*Game Characters\s*$", re.MULTILINE) - match_gc = game_char_header.search(content) - if match_gc: - subsequent_headers = list(re.finditer(r"^[\t ]*#\s+.*$", content[match_gc.end():], re.MULTILINE)) - if subsequent_headers: - insert_index = match_gc.end() + subsequent_headers[0].start() - else: - insert_index = len(content) - else: - insert_index = 0 - - before = content[:insert_index] - after = content[insert_index:] - if not before.endswith("\n\n"): - if not before.endswith("\n"): - before += "\n" - before += "\n" - new_content = before + section_block + after.lstrip("\n") - - tmp_path = vocab_path.with_suffix(vocab_path.suffix + f".{os.getpid()}.{threading.get_ident()}.tmp") - tmp_path.write_text(new_content, encoding="utf-8") - try: - os.replace(tmp_path, vocab_path) - except Exception: - try: - shutil.move(str(tmp_path), str(vocab_path)) - except Exception: - pass - except Exception: - traceback.print_exc() diff --git a/requirements.txt b/requirements.txt index 84cd0a6..ad0e17b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,6 @@ colorama>=0.4.6 openai>=2.8.0 python-dotenv>=1.0.0 retry>=0.9.2 -ruamel.yaml>=0.17.32 tiktoken>=0.8.0 tqdm>=4.65.0 jsbeautifier>=1.14.0 diff --git a/util/subprocess_runner.py b/util/subprocess_runner.py index cb2d575..7a33840 100644 --- a/util/subprocess_runner.py +++ b/util/subprocess_runner.py @@ -74,9 +74,6 @@ def run_handler(project_root, module_name, filename, estimate_only): if "RPG Maker MV/MZ" in module_name: from modules.rpgmakermvmz import handleMVMZ handler = handleMVMZ - elif "RPG Maker Ace" in module_name: - from modules.rpgmakerace import handleACE - handler = handleACE elif "CSV" in module_name: from modules.csv import handleCSV handler = handleCSV