diff --git a/modules/rpgmakerace.py b/modules/rpgmakerace.py
index 66b472e..600c673 100644
--- a/modules/rpgmakerace.py
+++ b/modules/rpgmakerace.py
@@ -6,15 +6,14 @@ import util.dazedwrap as dazedwrap
import threading
import time
import traceback
-import tiktoken
import copy
-# Removed concurrent.futures usage for simplicity; running synchronously
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
+from util.translation import TranslationConfig, translateAI as sharedtranslateAI, getPricingConfig, calculateCost, getPricingConfig, calculateCost
from ruamel.yaml import YAML
@@ -26,17 +25,25 @@ PROMPT = Path("prompt.txt").read_text(encoding="utf-8")
VOCAB = Path("vocab.txt").read_text(encoding="utf-8")
THREADS = int(os.getenv("threads"))
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]
-NAMESLIST = []
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()
+
+# Speakers
+NAMESLIST = []
+SPEAKER_PARSE_MODE = False
+_speakerCache = {}
+_speakerCacheLock = threading.Lock()
+SPEAKER_COLLECTED = [] # Original speaker names collected during parse mode (untranslated)
# 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]+"
@@ -51,7 +58,6 @@ FREQUENCY_PENALTY = PRICING_CONFIG["frequencyPenalty"]
# tqdm Globals
BAR_FORMAT = "{l_bar}{bar:10}{r_bar}{bar:-10b}"
POSITION = 0
-LEAVE = False
# Initialize Translation Config
TRANSLATION_CONFIG = TranslationConfig(
@@ -67,32 +73,39 @@ TRANSLATION_CONFIG = TranslationConfig(
LEAVE = False
# Config (Default)
+# FIRSTLINESPEAKERS: Guess speaker from first line.
FIRSTLINESPEAKERS = False
+# FACENAME101: Map face name -> speaker.
FACENAME101 = False
-NAMES = 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
# Dialogue / Scroll / Choices (Main Codes)
+CODE101 = True
CODE401 = True
CODE405 = True
CODE102 = True
# Optional
-CODE101 = True
CODE408 = False
# Variables
CODE122 = False
-# Other
+# Plugins / Scripts
CODE355655 = False
CODE357 = False
CODE657 = False
CODE356 = False
CODE320 = False
CODE324 = False
+CODE325 = False
CODE111 = False
CODE108 = False
@@ -101,13 +114,19 @@ 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
- if not estimate:
+ # 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)
@@ -143,7 +162,7 @@ def openFiles(filename):
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:
+ if "Map" in filename and filename != "MapInfos.json":
translatedData = parseMap(data, filename)
# CommonEvents Files
@@ -234,31 +253,190 @@ def getResultString(translatedData, translationTime, filename):
return filename + ": " + totalTokenstring + timeString + Fore.RED + " \u2717 " + errorString + Fore.RESET
-def save_progress_yaml(data, filename):
- """Atomically write current YAML data to translated/filename; skip in estimate mode."""
+def saveProgress(data, filename):
+ """Atomically write current data to translated/filename to avoid progress loss.
+ Skips when running in estimate mode.
+ """
try:
- if ESTIMATE:
+ # Also skip progress saves during speaker-parse mode
+ if ESTIMATE or SPEAKER_PARSE_MODE:
return
os.makedirs("translated", exist_ok=True)
- tmp_path = os.path.join("translated", f"{filename}.tmp")
+ # 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)
- os.replace(tmp_path, final_path)
+ 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 maybe_save_progress_yaml(data, filename, tokens):
- """Save YAML progress only when tokens indicate actual translation work."""
+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]):
- save_progress_yaml(data, filename)
+ 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()
@@ -280,33 +458,79 @@ def parseMap(data, filename):
totalTokens[1] += response[1][1]
data["display_name"] = response[0].replace('"', "")
- # Process each page synchronously and persist after each
- with tqdm(bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE) as pbar:
- pbar.desc = filename
- for key in events:
- if key is not None:
+ # Get total for progress bar (sum of all command list lengths across pages)
+ for event in events:
+ if event:
+ note_val = event.get("note") or ""
+ if not isinstance(note_val, str):
+ note_val = str(note_val) if note_val is not None else ""
+ if "" in note_val:
+ # Translate event name when flagged with
+ name_val = event.get("name") or ""
+ if isinstance(name_val, str) and name_val:
+ response = translateAI(
+ name_val,
+ "Reply with only the " + LANGUAGE + " translation of the RPG location name",
+ False,
+ )
+ totalTokens[0] += response[1][0]
+ totalTokens[1] += response[1][1]
+ event["name"] = response[0].replace('"', "")
+ if "", False)
+ totalTokens[0] += tokensResponse[0]
+ totalTokens[1] += tokensResponse[1]
+ for page in event["pages"]:
+ totalLines += len(page["list"])
+ 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:
+ if event is not None:
+ # 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 ".*")
- totalTokens[0] += response[0]
- totalTokens[1] += response[1]
- for page in events[key]["pages"]:
+ 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:
- tt = searchCodes(page, pbar, [], filename)
- totalTokens[0] += tt[0]
- totalTokens[1] += tt[1]
+ 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:
- maybe_save_progress_yaml(data, filename, tt)
+ # 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["note"]
+ 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]
@@ -343,7 +567,9 @@ def translateNote(event, regex, wordwrap=False):
# For notes that can't have spaces.
def translateNoteOmitSpace(event, regex):
# Regex that only matches text inside LB.
- jaString = event["note"]
+ 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:
@@ -357,12 +583,23 @@ def translateNoteOmitSpace(event, regex):
"Reply with the " + LANGUAGE + " translation of the location name.",
False,
)
- translatedText = response[0]
+ # 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(" ", "_")
- event["note"] = event["note"].replace(oldJAString, translatedText)
- return response[1]
+ # 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]
@@ -375,20 +612,22 @@ def parseCommonEvents(data, filename):
for page in data:
if page is not None:
totalLines += len(page["list"])
+ global PBAR
- with tqdm(bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE) as pbar:
- pbar.desc = filename
+ 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:
- tt = searchCodes(page, pbar, [], filename)
- totalTokens[0] += tt[0]
- totalTokens[1] += tt[1]
+ 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:
- maybe_save_progress_yaml(data, filename, tt)
+ # Persist progress only if this page produced tokens
+ checkSave(data, filename, totalTokensPage)
return [data, totalTokens, None]
@@ -401,83 +640,97 @@ def parseTroops(data, filename):
for troop in data:
if troop is not None:
for page in troop["pages"]:
- totalLines += len(page["list"]) + 1 # The +1 is because each page has a name.
+ # Progress measured by number of commands in each page's list
+ totalLines += len(page["list"])
+ global PBAR
- with tqdm(bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE) as pbar:
- pbar.desc = filename
+ 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:
- tt = searchCodes(page, pbar, [], filename)
- totalTokens[0] += tt[0]
- totalTokens[1] += tt[1]
+ 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:
- save_progress_yaml(data, filename)
+ # Persist progress only if this page produced tokens
+ checkSave(data, filename, totalTokensPage)
return [data, totalTokens, None]
def parseNames(data, filename, context):
totalTokens = [0, 0]
- # Precompute total work units for progress bar (exclude notes)
- def count_work_units(entries, ctx):
+ # Precompute total work units for progress bar
+ def count_work_units(data, context):
total = 0
- for entry in entries:
+
+ for entry in data:
if not entry:
continue
+
+ # Names and associated fields
name = entry.get("name", "")
desc = entry.get("description", "")
nickname = entry.get("nickname", "")
- if ctx == "Actors":
+ profile = entry.get("profile", "")
+
+ if context == "Actors":
if name:
total += 1
if nickname:
total += 1
- if desc:
+ if profile:
total += 1
- elif ctx in ["Armors", "Weapons", "Items"]:
+ elif context in ["Armors", "Weapons", "Items"]:
if name:
total += 1
if desc:
total += 1
- elif ctx == "Skills":
+ elif context == "Skills":
if name:
total += 1
if desc:
total += 1
+ # Messages translated individually in searchNames
for n in range(1, 5):
- if entry.get(f"message{n}"):
+ msg = entry.get(f"message{n}")
+ if msg:
total += 1
- elif ctx in ["Enemies", "Classes", "MapInfos"]:
+ elif context in ["Enemies", "Classes", "MapInfos"]:
if name:
total += 1
+
return total
total_units = count_work_units(data, context)
+ global PBAR
- with tqdm(total=total_units, bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE) as pbar:
- pbar.desc = filename
+ with tqdm(total=total_units, bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE, desc=filename) as pbar:
+ PBAR = pbar
try:
- result = searchNames(data, pbar, context)
+ # 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:
- maybe_save_progress_yaml(data, filename, result)
+ # Persist progress only if this names pass produced tokens
+ checkSave(data, filename, totalTokens)
return [data, totalTokens, None]
def parseSS(data, filename):
totalTokens = [0, 0]
- # Precompute total units: name, description, message1..4 (exclude notes)
+ # Precompute total units (ignore notes): name, description, message1..4 presence
def count_work_units(states):
total = 0
for st in states:
@@ -493,9 +746,10 @@ def parseSS(data, filename):
return total
total_units = count_work_units(data)
+ global PBAR
- with tqdm(total=total_units, bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE) as pbar:
- pbar.desc = filename
+ 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:
@@ -506,7 +760,8 @@ def parseSS(data, filename):
traceback.print_exc()
return [data, totalTokens, e]
finally:
- maybe_save_progress_yaml(data, filename, result)
+ # Persist progress only if this state produced tokens
+ checkSave(data, filename, result)
return [data, totalTokens, None]
@@ -530,67 +785,24 @@ def parseSystem(data, filename):
total += len(sys.get("weapon_types", []) or [])
total += len(sys.get("armor_types", []) or [])
total += len(sys.get("skill_types", []) or [])
+ total += len(sys.get("equip_types", []) or [])
return total
total_units = count_work_units(data)
+ global PBAR
- with tqdm(total=total_units, bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE) as pbar:
- pbar.desc = filename
- input_log = {}
- output_log = {}
+ with tqdm(total=total_units, bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE, desc=filename) as pbar:
+ PBAR = pbar
try:
- # Helper to flatten lists to Line1, Line2, ...
- def flatten_lines(prefix, values, target):
- for idx, v in enumerate(values):
- target[f"{prefix}Line{idx+1}"] = v
-
- # Capture pre-translation values
- if "terms" in data:
- for key, value in data["terms"].items():
- if isinstance(value, list):
- flatten_lines(key, value, input_log)
- else:
- input_log[key] = value
- for key in ["gameTitle", "game_title", "armor_types", "skill_types", "weapon_types"]:
- if key in data:
- val = data[key]
- if isinstance(val, list):
- flatten_lines(key, val, input_log)
- else:
- input_log[key] = val
- # Run translation
result = searchSystem(data, pbar)
totalTokens[0] += result[0]
totalTokens[1] += result[1]
- # Capture post-translation values
- if "terms" in data:
- for key, value in data["terms"].items():
- if isinstance(value, list):
- flatten_lines(key, value, output_log)
- else:
- output_log[key] = value
- for key in ["gameTitle", "game_title", "armor_types", "skill_types", "weapon_types"]:
- if key in data:
- val = data[key]
- if isinstance(val, list):
- flatten_lines(key, val, output_log)
- else:
- output_log[key] = val
except Exception as e:
traceback.print_exc()
return [data, totalTokens, e]
finally:
- maybe_save_progress_yaml(data, filename, result)
- # Log translation history for system.yaml in established line-by-line format
- try:
- with open("log/translationHistory.txt", "a", encoding="utf-8") as log_file:
- log_file.write("Input:\n")
- log_file.write(json.dumps(input_log, ensure_ascii=False, indent=4))
- log_file.write("\nOutput:\n")
- log_file.write(json.dumps(output_log, ensure_ascii=False, indent=4))
- log_file.write("\n")
- except Exception:
- pass
+ # Persist only if system sections produced tokens
+ checkSave(data, filename, result)
return [data, totalTokens, None]
@@ -602,30 +814,39 @@ def parseScenario(data, filename):
# Get total for progress bar
for page in data.items():
totalLines += len(page[1])
+ global PBAR
- with tqdm(bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE) as pbar:
- pbar.desc = filename
+ 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:
- tt = searchCodes(page[1], pbar, [], filename)
- totalTokens[0] += tt[0]
- totalTokens[1] += tt[1]
+ 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:
- maybe_save_progress_yaml(data, filename, tt)
+ # Persist progress only if this page produced tokens
+ checkSave(data, filename, totalTokensPage)
return [data, totalTokens, None]
-def searchNames(data, pbar, context):
+def searchNames(data, pbar, context, filename):
totalTokens = [0, 0]
nameList = []
profileList = []
nicknameList = []
descriptionList = []
- noteList = []
+ # 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
@@ -653,13 +874,133 @@ def searchNames(data, pbar, context):
# 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":
@@ -668,18 +1009,8 @@ def searchNames(data, pbar, context):
nameList.append(data[i]["name"])
if "nickname" in data[i] and data[i]["nickname"]:
nicknameList.append(data[i]["nickname"])
- if "description" in data[i] and data[i]["description"]:
- profileList.append(data[i]["description"].replace("\n", " "))
-
- # Notes
- if "")
- totalTokens[0] += tokensResponse[0]
- totalTokens[1] += tokensResponse[1]
- if "PE拡張" in data[i]["note"]:
- tokensResponse = translateNote(data[i], r"")
- totalTokens[0] += tokensResponse[0]
- totalTokens[1] += tokensResponse[1]
+ if "profile" in data[i] and data[i]["profile"]:
+ profileList.append(data[i]["profile"].replace("\n", " "))
i += 1
else:
batchFull = True
@@ -690,80 +1021,6 @@ def searchNames(data, pbar, context):
description = data[i]["description"]
description = description.replace("\n", " ")
descriptionList.append(description)
- if "")
- totalTokens[0] += tokensResponse[0]
- totalTokens[1] += tokensResponse[1]
- if "")
- totalTokens[0] += tokensResponse[0]
- totalTokens[1] += tokensResponse[1]
- if "")
- totalTokens[0] += tokensResponse[0]
- totalTokens[1] += tokensResponse[1]
- if "")
- totalTokens[0] += tokensResponse[0]
- totalTokens[1] += tokensResponse[1]
- if "")
- totalTokens[0] += tokensResponse[0]
- totalTokens[1] += tokensResponse[1]
- if "")
- totalTokens[0] += tokensResponse[0]
- totalTokens[1] += tokensResponse[1]
- if "")
- totalTokens[0] += tokensResponse[0]
- totalTokens[1] += tokensResponse[1]
- if "Switch Shop Description" in data[i]["note"]:
- tokensResponse = translateNote(data[i], r"\n(.*)\n")
- totalTokens[0] += tokensResponse[0]
- totalTokens[1] += tokensResponse[1]
- if "")
- totalTokens[0] += tokensResponse[0]
- totalTokens[1] += tokensResponse[1]
- if "")
- totalTokens[0] += tokensResponse[0]
- totalTokens[1] += tokensResponse[1]
- if "")
- totalTokens[0] += tokensResponse[0]
- totalTokens[1] += tokensResponse[1]
- if "")
- totalTokens[0] += tokensResponse[0]
- totalTokens[1] += tokensResponse[1]
- if "")
- totalTokens[0] += tokensResponse[0]
- totalTokens[1] += tokensResponse[1]
- if "sub_1" in data[i]["note"]:
- tokensResponse = translateNote(data[i], r"]+)", True)
- totalTokens[0] += tokensResponse[0]
- totalTokens[1] += tokensResponse[1]
- if "sub_2" in data[i]["note"]:
- tokensResponse = translateNote(data[i], r"]+)", True)
- totalTokens[0] += tokensResponse[0]
- totalTokens[1] += tokensResponse[1]
- if "sub_3" in data[i]["note"]:
- tokensResponse = translateNote(data[i], r"]+)", True)
- totalTokens[0] += tokensResponse[0]
- totalTokens[1] += tokensResponse[1]
- if "infowindow" in data[i]["note"]:
- tokensResponse = translateNote(data[i], r"", True)
- totalTokens[0] += tokensResponse[0]
- totalTokens[1] += tokensResponse[1]
- if "ExtendDesc" in data[i]["note"]:
- tokensResponse = translateNote(data[i], r"", True)
- totalTokens[0] += tokensResponse[0]
- totalTokens[1] += tokensResponse[1]
-
-
i += 1
else:
batchFull = True
@@ -772,84 +1029,12 @@ def searchNames(data, pbar, context):
nameList.append(data[i]["name"])
if "description" in data[i] and data[i]["description"]:
descriptionList.append(data[i]["description"].replace("\n", " "))
-
- # Messages
- number = 1
- while number < 5:
- if f"message{number}" in data[i] and data[i][f"message{number}"]:
- if data[i][f"message{number}"][0] in ["は", "を", "の", "に", "が"]:
- msgResponse = translateAI(
- "Taro" + data[i][f"message{number}"],
- "reply with only the gender neutral "
- + LANGUAGE
- + " translation of the action log. Always start the sentence with Taro. For example, Translate 'Taroを倒した!' as 'Taro was defeated!'",
- False,
- )
- data[i][f"message{number}"] = msgResponse[0].replace("Taro", "")
- totalTokens[0] += msgResponse[1][0]
- totalTokens[1] += msgResponse[1][1]
- if pbar is not None:
- pbar.refresh()
- number += 1
-
- else:
- msgResponse = translateAI(
- data[i][f"message{number}"],
- "reply with only the gender neutral " + LANGUAGE + " translation",
- False,
- )
- data[i][f"message{number}"] = msgResponse[0]
- totalTokens[0] += msgResponse[1][0]
- totalTokens[1] += msgResponse[1][1]
- if pbar is not None:
- pbar.refresh()
- number += 1
- else:
- number += 1
-
- # Notes
- if "")
- totalTokens[0] += tokensResponse[0]
- totalTokens[1] += tokensResponse[1]
- if "")
- totalTokens[0] += tokensResponse[0]
- totalTokens[1] += tokensResponse[1]
- if "<拡張説明:" in data[i]["note"]:
- tokensResponse = translateNote(data[i], r"<拡張説明:(.+?)>")
- totalTokens[0] += tokensResponse[0]
- totalTokens[1] += tokensResponse[1]
- if "" in data[i]["note"]:
- tokensResponse = translateNote(data[i], r"\n(.+?)\n<")
- totalTokens[0] += tokensResponse[0]
- totalTokens[1] += tokensResponse[1]
-
i += 1
else:
batchFull = True
if context in ["Enemies", "Classes", "MapInfos"]:
if len(nameList) < BATCHSIZE:
nameList.append(data[i]["name"])
-
- # Notes
- if "note" in data[i]:
- if "")
- totalTokens[0] += tokensResponse[0]
- totalTokens[1] += tokensResponse[1]
- if "")
- totalTokens[0] += tokensResponse[0]
- totalTokens[1] += tokensResponse[1]
- if "図鑑特徴" in data[i]["note"]:
- tokensResponse = translateNote(data[i], r"<図鑑特徴:(.+?)>")
- totalTokens[0] += tokensResponse[0]
- totalTokens[1] += tokensResponse[1]
- if "図鑑説明" in data[i]["note"]:
- tokensResponse = translateNote(data[i], r"<図鑑説明:(.+?)>")
- totalTokens[0] += tokensResponse[0]
- totalTokens[1] += tokensResponse[1]
i += 1
else:
batchFull = True
@@ -858,11 +1043,15 @@ def searchNames(data, pbar, context):
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, True)
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()
@@ -872,6 +1061,8 @@ def searchNames(data, pbar, context):
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()
@@ -881,6 +1072,8 @@ def searchNames(data, pbar, context):
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()
@@ -897,13 +1090,14 @@ def searchNames(data, pbar, context):
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 "description" in data[j] and data[j]["description"]:
- data[j]["description"] = dazedwrap.wrapText(translatedProfileBatch[0], LISTWIDTH)
+ 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.
@@ -914,15 +1108,21 @@ def searchNames(data, pbar, context):
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, True)
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()
@@ -936,6 +1136,8 @@ def searchNames(data, pbar, context):
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()
@@ -950,7 +1152,12 @@ def searchNames(data, pbar, context):
continue
else:
# Get Text
- file.write(f'{data[j]['name']} ({translatedNameBatch[0]})\n')
+ 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"] != "":
@@ -965,13 +1172,19 @@ def searchNames(data, pbar, context):
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, True)
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()
@@ -987,6 +1200,11 @@ def searchNames(data, pbar, context):
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)
@@ -996,6 +1214,8 @@ def searchNames(data, pbar, context):
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
@@ -1011,6 +1231,10 @@ def searchNames(data, pbar, context):
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
@@ -1022,7 +1246,9 @@ def searchCodes(page, pbar, jobList, filename):
list108 = jobList[3]
list356 = jobList[4]
list357 = jobList[5]
- list408 = jobList[6]
+ list324 = jobList[6]
+ list408 = jobList[7]
+ list325 = jobList[8]
setData = False
else:
list401 = []
@@ -1031,7 +1257,9 @@ def searchCodes(page, pbar, jobList, filename):
list108 = []
list356 = []
list357 = []
+ list324 = []
list408 = []
+ list325 = []
setData = True
textHistory = []
match = []
@@ -1040,7 +1268,6 @@ def searchCodes(page, pbar, jobList, filename):
speaker = ""
speakerID = None
syncIndex = 0
- CLFlag = False
maxHistory = MAXHISTORY
VNameValue = None
global LOCK
@@ -1109,50 +1336,58 @@ def searchCodes(page, pbar, jobList, filename):
# Speaker Check
speakerList = []
+ # Remove any RPGMaker Code at start
+ ffMatch = re.search(
+ r"^((?:[\\]+[^cCnNiIkKvV]+\[[\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]\[[\d]+\]【?(.+?)】?[\\]+[Cc]\[[\d]\]\\?\\?$",
+ r"^[\\]+[cC]\[\d+\]【?(.+?)】?[\\]+[cC]\[\d+\](?:[\\]+[A-Za-z]+(?:\[[^\]]*\])?)*[\\]*$",
speakerList[0],
)
- # Brackets
+ # Brackets (support multiple names like 【A】【B】)
if len(speakerList) == 0:
- speakerList = re.findall(r"^(?:[\\]+[rlRL]\[[\w\d\-]+\])?【(.*?)】$|^(?:[\\]+[rlRL]\[[\w\d\-]+\])?【(.*?)】[\\]*[a-zA-Z]*\[.*\]$", jaString)
- if speakerList:
- if speakerList[0][0]:
- speakerList = [speakerList[0][0]]
- else:
- speakerList = [speakerList[0][1]]
+ # Only consider bracketed names when the line starts with '【' and
+ # ends with either '】' or trailing variable/control codes like \n[2], \FF[\w[3]], etc.
+ startsWithBracket = re.match(r"^\s*【", jaString) is not None
+ endsWithBracket = re.search(
+ r"(】\s*|(?:[\\]+[A-Za-z]+(?:\[(?:[^\[\]]|\[[^\]]*\])*\])+\s*)$)",
+ jaString,
+ ) is not None
+
+ if startsWithBracket and endsWithBracket:
+ candidates = re.findall(r"【(.*?)】", jaString)
+ if candidates:
+ candidates = [c.strip() for c in candidates]
+ if candidates:
+ speakerList = candidates
# Colors
if len(speakerList) == 0:
speakerList = re.findall(
- r"^[\\]+[cC]\[[\d]+\]【?(.+?)】?[\\]+[Cc]\[?[\d]?\]?\\?\\?$",
+ r"^[\\]+[cC]\[\d+\]【?(.+?)】?[\\]+[cC]\[\d+\](?:[\\]+[A-Za-z]+(?:\[[^\]]*\])?)*[\\]*$",
jaString,
)
# Colons
if len(speakerList) == 0:
speakerList = re.findall(
- r"[\\]*[cC]?\[?\d*\]?(.+):$",
+ r"(.+):$",
jaString,
)
# First Line Speakers
if len(speakerList) == 0 and FIRSTLINESPEAKERS is True:
- # Remove any RPGMaker Code at start (including \r[...] and \l[...] patterns)
- ffMatch = re.search(
- r"^((?:[\\]+[^cCnNiIkKvV]+\[[\d\w]+\])+|(?:[\\]+[rlRL]\[[\w\d\-]+\]))",
- jaString,
- )
- if ffMatch != None:
- jaString = jaString.replace(ffMatch.group(0), "")
- nametag += ffMatch.group(0)
-
# Test Speaker
if (
len(jaString) < 40
@@ -1162,6 +1397,18 @@ def searchCodes(page, pbar, jobList, filename):
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"^((?:[\\]+[^cCnNiIkKvVSs{}]+?\[[\d\w\W]+?\]?\])+)",
+ nextString,
+ )
+ formatMatch = re.search(r"(^[\\]+[\W]+?)", nextString)
+ if ffMatchNS != None:
+ nextString = nextString.replace(ffMatchNS.group(1), "")
+ if formatMatch != None:
+ nextString = nextString.replace(formatMatch.group(1), "")
+
if nextString and nextString[0] in [
"「",
'"',
@@ -1174,21 +1421,33 @@ def searchCodes(page, pbar, jobList, filename):
# Replace Speaker
if len(speakerList) != 0 and codeList[i + 1]["c"] in [401, 405, -1]:
- # Get Speaker
- response = getSpeaker(speakerList[0])
- speaker = response[0]
- totalTokens[0] += response[1][0]
- totalTokens[1] += response[1][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:
- # Check if there's a \r[...] or \l[...] code at the beginning
- codePrefix = re.search(r"^([\\]+[rlRL]\[[\w\d\-]+\])", jaString)
- if codePrefix:
- # Replace only the speaker name, preserving the code and brackets
- codeList[i]["p"][0] = jaString.replace(f"【{speakerList[0]}】", f"【{speaker}】")
- else:
- codeList[i]["p"][0] = nametag + jaString.replace(speakerList[0], speaker)
+ 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
@@ -1199,13 +1458,6 @@ def searchCodes(page, pbar, jobList, filename):
j = i
jaString = codeList[i]["p"][0]
- # Replace Symbols
- jaString = jaString.replace("…", "...")
- jaString = jaString.replace("。", ".")
- jaString = jaString.replace("・", ".")
- jaString = jaString.replace("「", '"')
- jaString = jaString.replace("」", '"')
-
# Check if there is text to translate
if not re.search(r"\w+", jaString):
i += 1
@@ -1221,7 +1473,7 @@ def searchCodes(page, pbar, jobList, filename):
# 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 not re.match(r"^(\s*[\\]+[aAbBdDeEfFgGhHjJlLmMoOpPqQrRsStTuUwWxXyYzZ]+\[[\w\d\[\]\\]+\])", codeList[i+1]["p"][0]):
+ 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
@@ -1246,7 +1498,7 @@ def searchCodes(page, pbar, jobList, filename):
codeList[i]["p"] = [finalJAString]
### \\n
- regex = r"([\\]+[kKnN][wWcCrRrEe]?[\[<](.*?)[>])"
+ regex = r"([\\]+[kKnN][wWcCrRrEe]?[\[<](?:[\\]*\w\[\d+\])?(.*?)(?:[\\]*\w\[\d+\])?[>])"
match = re.search(regex, finalJAString)
# Set Name
@@ -1276,15 +1528,15 @@ def searchCodes(page, pbar, jobList, filename):
finalJAString = finalJAString.replace("\\,", ',')
### Remove format codes
- # Furigana
- rcodeMatch = re.findall(r"([\\]+[r][b]?\[.*?,(.*?)\])", finalJAString)
- if len(rcodeMatch) > 0:
- for match in rcodeMatch:
- finalJAString = finalJAString.replace(match[0], match[1])
+ # 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"^((?:[\\]+[^cCnNiIkKvV{}]+\[[\d\w]+\])+)",
+ r"^((?:[\\]+[^cCnNiIkKvVSs{}]+?\[[\d\w\W]+?\]?\])+)",
finalJAString,
)
if ffMatch != None:
@@ -1297,13 +1549,11 @@ def searchCodes(page, pbar, jobList, filename):
finalJAString = finalJAString.replace(ffMatch.group(1), "")
nametag += ffMatch.group(1)
- # Center Lines
- if "\\CL" in finalJAString or "\\ac" in finalJAString:
- finalJAString = finalJAString.replace("\\CL ", "")
+ # 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("\\ac", "")
- CLFlag = True
+ finalJAString = finalJAString.replace("\\#", "")
# Handle Formatting Codes
if "\\>" in finalJAString:
@@ -1312,7 +1562,7 @@ def searchCodes(page, pbar, jobList, filename):
# Check if Empty
if finalJAString == "":
- if nametag:
+ if nametag and match:
codeList[j]["p"][0] = codeList[j]["p"][0].replace(match.group(2), tledSpeaker)
i += 1
continue
@@ -1382,14 +1632,6 @@ def searchCodes(page, pbar, jobList, filename):
translatedText = translatedText.replace("\\px[200]", "")
translatedText = translatedText.replace("\n", "\n\\px[200]")
- ### Add Var Strings
- # CL Flag
- if CLFlag:
- translatedText = "\\ac " + translatedText
- translatedText = translatedText.replace("\n", "\n\\ac ")
- translatedText = re.sub(r"[\\]+?ac\s+", r"\\ac ", translatedText)
- CLFlag = False
-
# Add Nametag Back In
translatedText = nametag + translatedText
nametag = ""
@@ -1578,12 +1820,79 @@ def searchCodes(page, pbar, jobList, filename):
"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"]
@@ -1616,7 +1925,7 @@ def searchCodes(page, pbar, jobList, filename):
# Replace Strings
for j in range(len(matchList)):
- translatedText = translatedText.replace(matchList[j], response[0][j])
+ translatedText = translatedText.replace(matchList[j], response[0][j].replace('"', ''))
# Set Data
codeList[i]["p"][3]["choices"] = translatedText
@@ -1754,21 +2063,28 @@ def searchCodes(page, pbar, jobList, filename):
jaString = codeList[i]["p"][0]
patterns = {
- "テキスト-": (r"テキスト-(.+)")
+ # "テキスト-": (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+,'(.+)'\)"),
+ # "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))
@@ -1797,40 +2113,33 @@ def searchCodes(page, pbar, jobList, filename):
## Event Code: 408 (Script)
if "c" in codeList[i] and (codeList[i]["c"] == 408) and CODE408 is True:
- # Save starting index
- j = i
- jaString = codeList[i]["p"][0] if len(codeList[i]["p"]) > 0 else ""
-
- # Join consecutive 408 codes
- combinedText = jaString
- if len(codeList) > i + 1:
- while i + 1 < len(codeList) and "c" in codeList[i + 1] and codeList[i + 1]["c"] == 408:
- i += 1
- if len(codeList[i]["p"]) > 0:
- combinedText += " " + codeList[i]["p"][0]
- # Mark as -1 only in Pass 2
- if not setData:
- codeList[i]["p"] = []
- codeList[i]["c"] = -1
+ jaString = codeList[i]["p"][0]
+ match = re.search(r"(.+)", jaString)
+ if match:
+ # Remove Textwrap
+ jaString = codeList[i]["p"][0]
+ ojaString = jaString
+ jaString = jaString.replace("\n", " ")
- # Pass 1
- if setData:
- if combinedText.strip(): # Only add non-empty text
- list408.append(combinedText)
-
- # Pass 2
- else:
- if len(list408) > 0:
+ # If there isn't any Japanese in the text just skip
+ if not re.search(LANGREGEX, jaString):
+ i += 1
+ continue
+
+ # Pass 1
+ if setData:
+ list408.append(jaString)
+
+ # Pass 2
+ else:
translatedText = list408[0]
list408.pop(0)
# Textwrap
- translatedText = dazedwrap.wrapText(translatedText, width=WIDTH)
+ translatedText = dazedwrap.wrapText(translatedText, width=LISTWIDTH)
- # Set Data - ensure list has at least one element
- if len(codeList[j]["p"]) == 0:
- codeList[j]["p"] = [""]
- codeList[j]["p"][0] = f"{translatedText}"
+ # Set Data
+ codeList[i]["p"][0] = codeList[i]["p"][0].replace(ojaString, translatedText)
## Event Code: 108 (Script)
if "c" in codeList[i] and (codeList[i]["c"] == 108) and CODE108 is True:
@@ -1843,13 +2152,15 @@ def searchCodes(page, pbar, jobList, filename):
# Translate
if "info:" in jaString:
- regex = r"info:(.*)"
+ 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
@@ -1922,7 +2233,7 @@ def searchCodes(page, pbar, jobList, filename):
# Want to translate this script
if "D_TEXT " in jaString:
- regex = r"D_TEXT\s(.+)\s?.*"
+ regex = r"D_TEXT\s*([^\s]+)\s?\d*"
elif "ShowInfo" in jaString:
regex = r"ShowInfo\s(.*)"
elif "PushGab" in jaString:
@@ -1930,7 +2241,7 @@ def searchCodes(page, pbar, jobList, filename):
elif "addLog" in jaString:
regex = r"addLog\s(.*)"
elif "DW_" in jaString:
- regex = r"DW_.*?\s(.*)"
+ regex = r"DW_.*\s\d+\s(.+)"
elif "CommonPopup" in jaString:
regex = r"CommonPopup\sadd\stext:(.*?)[\\]+}"
elif "AddCustomChoice" in jaString:
@@ -1946,9 +2257,25 @@ def searchCodes(page, pbar, jobList, filename):
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("_", " ")
+ text = text.replace("_", " ")
list356.append(text)
# Pass 2
@@ -1974,7 +2301,12 @@ def searchCodes(page, pbar, jobList, filename):
list356.pop(0)
if "namePop" in jaString:
- matchList = re.findall(r"" 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]
@@ -1984,23 +2316,33 @@ def searchCodes(page, pbar, jobList, filename):
totalTokens[1] += response[1][1]
# Set Data
- translatedText = jaString.replace(text, translatedText)
- codeList[i]["p"][0] = translatedText
+ 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:
- # Translate
text = matchList[0]
- response = translateAI(text, "Reply with the " + LANGUAGE + " Translation", False)
- 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
+ # 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)
@@ -2070,12 +2412,15 @@ def searchCodes(page, pbar, jobList, filename):
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 jaString == "":
+ if not jaString.strip():
continue
# If and En Statements
@@ -2085,50 +2430,43 @@ def searchCodes(page, pbar, jobList, filename):
for var in ifList:
jaString = jaString.replace(var, "")
ifVar += var
+
+ # Store the formatting and cleaned string
varList.append(ifVar)
-
- # Append to List
choiceList.append(jaString)
+ choiceIndexMap.append(choice)
- # Translate
- 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,
- )
- translatedTextList = response[0]
- totalTokens[0] += response[1][0]
- totalTokens[1] += response[1][1]
- else:
- response = translateAI(choiceList, "Reply with the English translation of the dialogue choice.", True)
+ # 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.", True)
+
translatedTextList = response[0]
totalTokens[0] += response[1][0]
totalTokens[1] += response[1][1]
- # Check Mismatch
- if len(translatedTextList) == len(choiceList):
- for choice in range(len(codeList[i]["p"][0])):
- jaString = codeList[i]["p"][0][choice]
- jaString = jaString.replace(" 。", ".")
-
- # Avoid Empty Strings
- if jaString == "":
- continue
-
- translatedText = translatedTextList[choice]
-
- # Set Data
- totalTokens[0] += response[1][0]
- totalTokens[1] += response[1][1]
- if translatedText != "":
- translatedText = varList[choice] + translatedText[0].upper() + translatedText[1:]
- else:
- translatedText = varList[choice] + translatedText
- codeList[i]["p"][0][choice] = translatedText
- else:
- if filename not in MISMATCH:
- MISMATCH.append(filename)
+ # 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:
@@ -2201,18 +2539,86 @@ def searchCodes(page, pbar, jobList, filename):
# 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)
+ i += 1
+
+ # 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
- else:
- i += 1
+ i += 1
# EOF
list401TL = []
list408TL = []
+ list324TL = []
list122TL = []
list356TL = []
list357TL = []
list355655TL = []
list108TL = []
+ list325TL = []
PBAR = pbar
# 401
@@ -2250,7 +2656,7 @@ def searchCodes(page, pbar, jobList, filename):
# 108
if len(list108) > 0:
- response = translateAI(list108, textHistory, True)
+ response = translateAI(list108, "This text is a label. Use title capitalization and keep it brief.", True)
list108TL = response[0]
totalTokens[0] += response[1][0]
totalTokens[1] += response[1][1]
@@ -2292,12 +2698,46 @@ def searchCodes(page, pbar, jobList, filename):
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.", True)
+ 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.", True)
+ 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:
searchCodes(
page,
pbar,
- [list401TL, list122TL, list355655TL, list108TL, list356TL, list357TL, list408TL],
+ [
+ list401TL,
+ list122TL,
+ list355655TL,
+ list108TL,
+ list356TL,
+ list357TL,
+ list324TL,
+ list408TL,
+ list325TL,
+ ],
filename,
)
@@ -2325,173 +2765,139 @@ def searchCodes(page, pbar, jobList, filename):
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
- nameResponse = (
- translateAI(
- state["name"],
- "Reply with only the " + LANGUAGE + " translation of the RPG Skill name.",
- False,
- )
- if "name" in state
- else ""
- )
-
+ if "name" in state and state["name"]:
+ batch_texts.append(state["name"])
+ batch_map.append(("name", "name", False))
+
# Description
- descriptionResponse = (
- translateAI(
- state["description"],
- "Reply with only the " + LANGUAGE + " translation of the description.",
- False,
- )
- if "description" in state
- else ""
- )
-
- # Messages
+ 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 = ""
- message4Response = ""
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
- if "message1" in state:
- if len(state["message1"]) > 0 and state["message1"][0] in [
- "は",
- "を",
- "の",
- "に",
- "が",
- ]:
- message1Response = translateAI(
- "Taro" + state["message1"],
- "reply with only the gender neutral "
- + LANGUAGE
- + " translation of the action log. Always start the sentence with Taro. For example,\
-Translate 'Taroを倒した!' as 'Taro was defeated!'",
- False,
- )
- else:
- message1Response = translateAI(
- state["message1"],
- "reply with only the gender neutral " + LANGUAGE + " translation",
- False,
- )
+ # --- 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))
- if "message2" in state:
- if len(state["message2"]) > 0 and state["message2"][0] in [
- "は",
- "を",
- "の",
- "に",
- "が",
- ]:
- message2Response = translateAI(
- "Taro" + state["message2"],
- "reply with only the gender neutral "
- + LANGUAGE
- + " translation of the action log. Always start the sentence with Taro. For example,\
-Translate 'Taroを倒した!' as 'Taro was defeated!'",
- False,
- )
- else:
- message2Response = translateAI(
- state["message2"],
- "reply with only the gender neutral " + LANGUAGE + " translation",
- False,
- )
+ # --- Batch translate all notes ---
+ translatedNotesBatch = []
+ if notesBatch:
+ response = translateAI(notesBatch, f"Reply with only the {LANGUAGE} translation of the note text.", True)
+ translatedNotesBatch = response[0]
+ totalTokens[0] += response[1][0]
+ totalTokens[1] += response[1][1]
+ # Notes don't update progress
- if "message3" in state:
- if len(state["message3"]) > 0 and state["message3"][0] in [
- "は",
- "を",
- "の",
- "に",
- "が",
- ]:
- message3Response = translateAI(
- "Taro" + state["message3"],
- "reply with only the gender neutral "
- + LANGUAGE
- + " translation of the action log. Always start the sentence with Taro. For example,\
-Translate 'Taroを倒した!' as 'Taro was defeated!'",
- False,
- )
- else:
- message3Response = translateAI(
- state["message3"],
- "reply with only the gender neutral " + LANGUAGE + " translation",
- False,
- )
+ # --- 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
- if "message4" in state:
- if len(state["message4"]) > 0 and state["message4"][0] in [
- "は",
- "を",
- "の",
- "に",
- "が",
- ]:
- message4Response = translateAI(
- "Taro" + state["message4"],
- "reply with only the gender neutral "
- + LANGUAGE
- + " translation of the action log. Always start the sentence with Taro. For example,\
-Translate 'Taroを倒した!' as 'Taro was defeated!'",
- False,
- )
- else:
- message4Response = translateAI(
- state["message4"],
- "reply with only the gender neutral " + LANGUAGE + " translation",
- False,
- )
-
- # Translate State Notes
- if "help" in state["note"]:
- noteResponse = translateNote(state, r"]*)>")
- totalTokens[0] += noteResponse[0]
- totalTokens[1] += noteResponse[1]
- if "STATE_HELP" in state["note"]:
- noteResponse = translateNote(state, r"\n(.*)\n")
- totalTokens[0] += noteResponse[0]
- totalTokens[1] += noteResponse[1]
- if "ShowHoverState" in state["note"]:
- noteResponse = translateNote(state, r"")
- totalTokens[0] += noteResponse[0]
- totalTokens[1] += noteResponse[1]
- if "")
- totalTokens[0] += noteResponse[0]
- totalTokens[1] += noteResponse[1]
-
- # Count totalTokens
- totalTokens[0] += nameResponse[1][0] if nameResponse != "" else 0
- totalTokens[1] += nameResponse[1][1] if nameResponse != "" else 0
- totalTokens[0] += descriptionResponse[1][0] if descriptionResponse != "" else 0
- totalTokens[1] += descriptionResponse[1][1] if descriptionResponse != "" else 0
- totalTokens[0] += message1Response[1][0] if message1Response != "" else 0
- totalTokens[1] += message1Response[1][1] if message1Response != "" else 0
- totalTokens[0] += message2Response[1][0] if message2Response != "" else 0
- totalTokens[1] += message2Response[1][1] if message2Response != "" else 0
- totalTokens[0] += message3Response[1][0] if message3Response != "" else 0
- totalTokens[1] += message3Response[1][1] if message3Response != "" else 0
- totalTokens[0] += message4Response[1][0] if message4Response != "" else 0
- totalTokens[1] += message4Response[1][1] if message4Response != "" else 0
+ # 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:
+ if "name" in state and nameResponse != "":
state["name"] = nameResponse[0].replace('"', "")
- if "description" in state:
+ if "description" in state and descriptionResponse != "":
# Textwrap
translatedText = descriptionResponse[0]
translatedText = dazedwrap.wrapText(translatedText, width=LISTWIDTH)
state["description"] = translatedText.replace('"', "")
- if "message1" in state:
+ if "message1" in state and message1Response != "":
state["message1"] = message1Response[0].replace('"', "").replace("Taro", "")
- if "message2" in state:
+ if "message2" in state and message2Response != "":
state["message2"] = message2Response[0].replace('"', "").replace("Taro", "")
- if "message3" in state:
+ if "message3" in state and message3Response != "":
state["message3"] = message3Response[0].replace('"', "").replace("Taro", "")
- if "message4" in state:
+ if "message4" in state and message4Response != "":
state["message4"] = message4Response[0].replace('"', "").replace("Taro", "")
return totalTokens
@@ -2501,153 +2907,256 @@ def searchSystem(data, pbar):
totalTokens = [0, 0]
context = "Reply with only the " + LANGUAGE + ' translation of the UI textbox."'
- # Title (handle both 'gameTitle' and 'game_title', skip if missing)
- game_title = data.get("gameTitle")
- if game_title is None:
- game_title = data.get("game_title")
- if game_title is not None:
- response = translateAI(
- game_title,
- " Reply with the " + LANGUAGE + " translation of the game title name",
- False,
- )
- totalTokens[0] += response[1][0]
- totalTokens[1] += response[1][1]
- # Write back to whichever key existed
- if "gameTitle" in data:
- data["gameTitle"] = response[0].strip(".")
- else:
- data["game_title"] = response[0].strip(".")
+ # 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
+ # Terms - batch translate all term items
for term in data["terms"]:
if term != "messages":
termList = data["terms"][term]
- for i in range(len(termList)): # Last item is a messages object
+ term_values = []
+ term_indices = []
+ for i in range(len(termList)):
if termList[i] is not None:
- response = translateAI(termList[i], context, False)
- totalTokens[0] += response[1][0]
- totalTokens[1] += response[1][1]
- termList[i] = response[0].replace('"', "").strip()
+ term_values.append(termList[i])
+ term_indices.append(i)
+
+ if term_values:
+ response = translateAI(term_values, context, False)
+ 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
- for i in range(len(data["armor_types"])):
+ # Armor Types - batch translate all
+ armor_values = [data["armor_types"][i] for i in range(len(data["armor_types"]))]
+ if armor_values:
response = translateAI(
- data["armor_types"][i],
+ armor_values,
"Reply with only the " + LANGUAGE + " translation of the armor type",
False,
)
totalTokens[0] += response[1][0]
totalTokens[1] += response[1][1]
- data["armor_types"][i] = response[0].replace('"', "").strip()
+ 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
- for i in range(len(data["skill_types"])):
+ # Skill Types - batch translate all
+ skill_values = [data["skill_types"][i] for i in range(len(data["skill_types"]))]
+ if skill_values:
response = translateAI(
- data["skill_types"][i],
+ skill_values,
"Reply with only the " + LANGUAGE + " translation",
False,
)
totalTokens[0] += response[1][0]
totalTokens[1] += response[1][1]
- data["skill_types"][i] = response[0].replace('"', "").strip()
+ 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
- for i in range(len(data["weapon_types"])):
+ # Equip Types - batch translate all
+ equip_values = [data["equip_types"][i] for i in range(len(data["equip_types"]))]
+ if equip_values:
response = translateAI(
- data["weapon_types"][i],
+ 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]
- data["weapon_types"][i] = response[0].replace('"', "").strip()
+ 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()
- # # Variables (Optional ususally)
- # for i in range(len(data['variables'])):
- # response = translateAI(data['variables'][i], 'Reply with only the '+ LANGUAGE +' translation of the title', False)
- # totalTokens[0] += response[1][0]
- # totalTokens[1] += response[1][1]
- # data['variables'][i] = response[0].replace('\"', '').strip()
+ # 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()
- # Messages and lists (handle both string and list values, log as Line1, Line2, ...)
- messages = data["terms"]
- for key, value in messages.items():
- if isinstance(value, list):
- new_list = []
- for idx, item in enumerate(value):
- if isinstance(item, str):
- response = translateAI(
- item,
- f"Reply with only the {LANGUAGE} translation of the battle text.\nTranslate '常時ダッシュ' as 'Always Dash'\nTranslate '次の%1まで' as Next %1.",
- False,
- )
- translatedText = response[0]
- charList = [".", '"', "\\n"]
- for char in charList:
- translatedText = translatedText.replace(char, "")
- totalTokens[0] += response[1][0]
- totalTokens[1] += response[1][1]
- new_list.append(translatedText)
- else:
- new_list.append(item)
- # For logging: create Line1, Line2, ... keys for this list
- messages[key] = new_list
- elif isinstance(value, str):
+ # Weapon Types - batch translate all (skip empty)
+ weapon_values = []
+ weapon_indices = []
+ for i in range(len(data["weaponTypes"])):
+ if data["weaponTypes"][i]: # Skip empty strings
+ weapon_values.append(data["weaponTypes"][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["weaponTypes"][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(
- value,
- f"Reply with only the {LANGUAGE} translation of the battle text.\nTranslate '常時ダッシュ' as 'Always Dash'\nTranslate '次の%1まで' as Next %1.",
- False,
+ var_values,
+ 'Reply with only the ' + LANGUAGE + ' translation of the title',
+ True,
)
- translatedText = response[0]
- charList = [".", '"', "\\n"]
- for char in charList:
- translatedText = translatedText.replace(char, "")
totalTokens[0] += response[1][0]
totalTokens[1] += response[1][1]
- messages[key] = translatedText
- else:
- messages[key] = value
+ 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()
+
+ # Messages — batch translate to reduce calls
+ 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):
- match speaker:
- case "ファイン":
- return ["Fine", [0, 0]]
- case "":
- return ["", [0, 0]]
- case _:
- # Find Speaker
- for i in range(len(NAMESLIST)):
- if speaker == NAMESLIST[i][0]:
- return [NAMESLIST[i][1], [0, 0]]
+def getSpeaker(speaker: str):
+ """Return (and possibly collect) speaker name.
- # Translate and Store Speaker
- response = translateAI(
- f"{speaker}",
- "Reply with the " + LANGUAGE + " translation of the NPC name.",
- False,
- )
- response[0] = response[0].title()
- response[0] = response[0].replace("'S", "'s")
- response[0] = response[0].replace("Speaker: ", "")
+ 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.
- # Retry if name doesn't translate for some reason
- if re.search(r"([a-zA-Z??])", response[0]) == None:
- response = translateAI(
- f"{speaker}",
- "Reply with the " + LANGUAGE + " translation of the NPC name.",
- False,
- )
- response[0] = response[0].title()
- response[0] = response[0].replace("'S", "'s")
+ Normal mode: translate immediately with caching.
+ """
+ if speaker == "":
+ return ["", [0, 0]]
- speakerList = [speaker, response[0]]
- NAMESLIST.append(speakerList)
- return response
- 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, fullPromptFlag):
"""
@@ -2660,13 +3169,136 @@ def translateAI(text, history, fullPromptFlag):
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]]
+
return sharedtranslateAI(
text=text,
history=history,
fullPromptFlag=fullPromptFlag,
config=TRANSLATION_CONFIG,
- filename=FILENAME,
+ filename=tl_filename,
pbar=PBAR,
lock=LOCK,
mismatchList=MISMATCH
)
+
+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()