diff --git a/gui/rpgmaker_tab.py b/gui/rpgmaker_tab.py index 6db1ab2..0eb5d02 100644 --- a/gui/rpgmaker_tab.py +++ b/gui/rpgmaker_tab.py @@ -256,6 +256,13 @@ class RPGMakerTab(QWidget): self.join408_cb.setToolTip("Join 408 codes into a single string") left_column.addWidget(self.join408_cb) + self.speakers408_cb = QCheckBox("Process Speakers in 408") + self.speakers408_cb.setToolTip("Apply speaker detection and processing to code 408 (same as code 401)") + left_column.addWidget(self.speakers408_cb) + # Only show SPEAKERS408 for ACE engine (rpgmakerace.py) + if self.engine != "ACE": + self.speakers408_cb.hide() + left_column.addSpacing(15) # Main Codes @@ -379,6 +386,8 @@ class RPGMakerTab(QWidget): self.fixtextwrap_cb.stateChanged.disconnect() self.ignoretltext_cb.stateChanged.disconnect() self.join408_cb.stateChanged.disconnect() + if self.engine == "ACE": + self.speakers408_cb.stateChanged.disconnect() # Main Codes self.code401_cb.stateChanged.disconnect() @@ -415,6 +424,8 @@ class RPGMakerTab(QWidget): self.fixtextwrap_cb.stateChanged.connect(lambda: self.apply_to_module(show_messages=False)) self.ignoretltext_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)) @@ -473,6 +484,10 @@ 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 @@ -485,6 +500,10 @@ class RPGMakerTab(QWidget): self.fixtextwrap_cb.setChecked(config.get("FIXTEXTWRAP", True)) self.ignoretltext_cb.setChecked(config.get("IGNORETLTEXT", 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)) diff --git a/modules/rpgmakerace.py b/modules/rpgmakerace.py index 713ec93..99e9ceb 100644 --- a/modules/rpgmakerace.py +++ b/modules/rpgmakerace.py @@ -36,6 +36,7 @@ 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 = [] @@ -44,6 +45,12 @@ _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]+" @@ -73,7 +80,7 @@ LEAVE = False # Config (Default) # FIRSTLINESPEAKERS: Guess speaker from first line. -FIRSTLINESPEAKERS = False +FIRSTLINESPEAKERS = True # FACENAME101: Map face name -> speaker. FACENAME101 = False # BRFLAG: Newlines ->
. @@ -85,7 +92,9 @@ IGNORETLTEXT = False # TLSYSTEMVARIABLES: Translate System Variables. (Optional but sometimes necessary. Can break stuff.) TLSYSTEMVARIABLES = False # Join 408 codes into a single string like 401. -JOIN408 = False +JOIN408 = True +# SPEAKERS408: Process speakers in code 408 the same way as code 401. +SPEAKERS408 = True # Dialogue / Scroll / Choices (Main Codes) CODE101 = True @@ -94,7 +103,7 @@ CODE405 = True CODE102 = True # Optional -CODE408 = False +CODE408 = True # Variables CODE122 = False @@ -444,10 +453,101 @@ def update_vocab_section(category: str, pairs: list[tuple[str, str]]): def parseMap(data, filename): totalTokens = [0, 0] - totalLines = 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: + dcopy = copy.deepcopy(d) + bar = _CountingBar() + global PREFLIGHT_COUNT_MODE + saved_preflight = PREFLIGHT_COUNT_MODE + global PBAR + saved_pbar = PBAR + PREFLIGHT_COUNT_MODE = True + PBAR = bar + try: + # Count display name TL (1 unit if present) + if "Map" in fname and isinstance(dcopy.get("display_name", None), str): + try: + translateAI( + dcopy["display_name"], + "Reply with only the " + LANGUAGE + " translation of the RPG location name", + False, + ) + except Exception: + pass + + # Notes and pages + evts = dcopy.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 "" + + # name translation + if "" in note_val: + name_val = evt.get("name") or "" + if isinstance(name_val, str) and name_val: + try: + translateAI( + name_val, + "Reply with only the " + LANGUAGE + " translation of the RPG location name", + False, + ) + except Exception: + pass + + # + if "", False) + except Exception: + pass + + # , , handled before page processing in real run + if ".*") + except Exception: + pass + if ".*") + except Exception: + pass + + for page in (evt.get("pages", []) or []): + try: + searchCodes(page, bar, [], fname) + except Exception: + pass + + return getattr(bar, "n", 0) or 0 + finally: + PREFLIGHT_COUNT_MODE = saved_preflight + PBAR = saved_pbar + # Translate display_name for Map files if "Map" in filename: response = translateAI( @@ -459,30 +559,17 @@ def parseMap(data, filename): totalTokens[1] += response[1][1] data["display_name"] = response[0].replace('"', "") - # Get total for progress bar (sum of all command list lengths across pages) - for event in events.values(): - if event and isinstance(event, dict): - 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"]) + 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 @@ -606,13 +693,52 @@ def translateNoteOmitSpace(event, regex): def parseCommonEvents(data, filename): totalTokens = [0, 0] - totalLines = 0 global LOCK - # Get total for progress bar - for page in data: - if page is not None: - totalLines += len(page["list"]) + # --- 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: + dcopy = copy.deepcopy(pages) + bar = _CountingBar() + global PREFLIGHT_COUNT_MODE, PBAR + saved_flag = PREFLIGHT_COUNT_MODE + saved_pbar = PBAR + PREFLIGHT_COUNT_MODE = True + PBAR = bar + try: + for page in dcopy: + if page is not None: + try: + searchCodes(page, bar, [], fname) + except Exception: + pass + return getattr(bar, "n", 0) or 0 + finally: + PREFLIGHT_COUNT_MODE = saved_flag + PBAR = saved_pbar + + 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: @@ -634,15 +760,55 @@ def parseCommonEvents(data, filename): def parseTroops(data, filename): totalTokens = [0, 0] - totalLines = 0 global LOCK - # Get total for progress bar - for troop in data: - if troop is not None: - for page in troop["pages"]: - # Progress measured by number of commands in each page's list - totalLines += len(page["list"]) + # --- 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: + tcopy = copy.deepcopy(troops) + bar = _CountingBar() + global PREFLIGHT_COUNT_MODE, PBAR + saved_flag = PREFLIGHT_COUNT_MODE + saved_pbar = PBAR + PREFLIGHT_COUNT_MODE = True + PBAR = bar + try: + for troop in tcopy: + if troop is None: + continue + for page in (troop.get("pages", []) or []): + if page is not None: + try: + searchCodes(page, bar, [], fname) + except Exception: + pass + return getattr(bar, "n", 0) or 0 + finally: + PREFLIGHT_COUNT_MODE = saved_flag + PBAR = saved_pbar + + 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: @@ -667,49 +833,138 @@ def parseTroops(data, filename): def parseNames(data, filename, context): totalTokens = [0, 0] - # Precompute total work units for progress bar - def count_work_units(data, context): - total = 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: + ecopy = copy.deepcopy(entries) + bar = _CountingBar() + global PREFLIGHT_COUNT_MODE, PBAR + saved_flag = PREFLIGHT_COUNT_MODE + saved_pbar = PBAR + PREFLIGHT_COUNT_MODE = True + PBAR = bar + try: + # Counts + name_cnt = 0 + desc_cnt = 0 + profile_cnt = 0 + nickname_cnt = 0 + msg_cnt = 0 + notes_cnt = 0 + + 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 ecopy: + 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: name_cnt += 1 + if nn: nickname_cnt += 1 + if pf: profile_cnt += 1 + elif ctx in ["Armors", "Weapons", "Items"]: + if nm: name_cnt += 1 + if ds: desc_cnt += 1 + elif ctx == "Skills": + if nm: name_cnt += 1 + if ds: desc_cnt += 1 + for k in range(1,5): + if entry.get(f"message{k}"): msg_cnt += 1 + elif ctx in ["Enemies", "Classes", "MapInfos"]: + if nm: name_cnt += 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" int: + scopy = copy.deepcopy(states) + bar = _CountingBar() + global PREFLIGHT_COUNT_MODE, PBAR + saved_flag = PREFLIGHT_COUNT_MODE + saved_pbar = PBAR + PREFLIGHT_COUNT_MODE = True + PBAR = bar + try: + for ss in scopy: + if ss is not None: + try: + searchSS(ss, bar) + except Exception: + pass + return getattr(bar, "n", 0) or 0 + finally: + PREFLIGHT_COUNT_MODE = saved_flag + PBAR = saved_pbar + + 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 += 1 - if st.get("description"): - total += 1 - for n in range(1, 5): - if st.get(f"message{n}"): - total += 1 - return total - - total_units = count_work_units(data) + 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: @@ -769,27 +1052,54 @@ def parseSS(data, filename): def parseSystem(data, filename): totalTokens = [0, 0] - # Precompute total units for Ace schema - def count_work_units(sys): - total = 0 - # Terms: sum list lengths of each term list - for term in sys.get("terms", {}): - termList = sys["terms"][term] - if isinstance(termList, list): - total += len(termList) - # game_title might be a string; count as 1 if non-empty - gt = sys.get("game_title") - if isinstance(gt, str) and gt: - total += 1 - # variables is a list - total += len(sys.get("variables", []) or []) - 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 + # --- 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 - total_units = count_work_units(data) + def _estimate_units(sysobj, fname) -> int: + scopy = copy.deepcopy(sysobj) + bar = _CountingBar() + global PREFLIGHT_COUNT_MODE, PBAR + saved_flag = PREFLIGHT_COUNT_MODE + saved_pbar = PBAR + PREFLIGHT_COUNT_MODE = True + PBAR = bar + try: + try: + searchSystem(scopy, bar) + except Exception: + pass + return getattr(bar, "n", 0) or 0 + finally: + PREFLIGHT_COUNT_MODE = saved_flag + PBAR = saved_pbar + + 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: @@ -809,12 +1119,50 @@ def parseSystem(data, filename): def parseScenario(data, filename): totalTokens = [0, 0] - totalLines = 0 global LOCK - # Get total for progress bar - for page in data.items(): - totalLines += len(page[1]) + # --- 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: + scopy = copy.deepcopy(scenario) + bar = _CountingBar() + global PREFLIGHT_COUNT_MODE, PBAR + saved_flag = PREFLIGHT_COUNT_MODE + saved_pbar = PBAR + PREFLIGHT_COUNT_MODE = True + PBAR = bar + try: + for key, lst in scopy.items(): + if lst is not None: + try: + searchCodes(lst, bar, [], fname) + except Exception: + pass + return getattr(bar, "n", 0) or 0 + finally: + PREFLIGHT_COUNT_MODE = saved_flag + PBAR = saved_pbar + + 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: @@ -1352,32 +1700,35 @@ def searchCodes(page, pbar, jobList, filename): speakerList.append(match.group(1)) if "\\c" in speakerList[0]: speakerList = re.findall( - r"^[\\]+[cC]\[\d+\]【?(.+?)】?[\\]+[cC]\[\d+\](?:[\\]+[A-Za-z]+(?:\[[^\]]*\])?)*[\\]*$", + 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】 - # After control codes are stripped, check for pattern - startsWithBracket = re.match(r"^\s*【", jaString) is not None + startsWithBracket = re.match(r"^\s*【", testString) is not None endsWithBracket = re.search( r"(】\s*$|】\s*(?:[\\]+[A-Za-z]+(?:\[(?:[^\[\]]|\[[^\]]*\])*\])+\s*)$)", - jaString, + testString, ) is not None if startsWithBracket and endsWithBracket: - candidates = re.findall(r"【(.*?)】", jaString) + candidates = re.findall(r"【(.*?)】", testString) if candidates: candidates = [c.strip() for c in candidates] if candidates: speakerList = candidates - # Colors + # 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]\[\d+\]【?(.+?)】?[\\]+[cC]\[\d+\](?:[\\]+[A-Za-z]+(?:\[[^\]]*\])?)*[\\]*$", + r"^[\\]+[cC]\[[^\]]+\]【(.+?)】[\\]+[cC]\[[^\]]+\](?:[\\]+[A-Za-z]+(?:\[[^\]]*\])?)*[\\]*$", jaString, ) @@ -2121,10 +2472,144 @@ def searchCodes(page, pbar, jobList, filename): 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", " ") + # Save starting index + j = i + nametag = "" + + # Speaker Check (if SPEAKERS408 is enabled) + if SPEAKERS408: + 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 + matchSpeaker = re.search(r"(.*?)[\\]+m\[\d+?\][\\]+z\[\d+?\]", jaString) + if matchSpeaker: + speakerList.append(matchSpeaker.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】) + 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 + 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 len(codeList) > 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: @@ -2143,22 +2628,78 @@ def searchCodes(page, pbar, jobList, filename): 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 - jaString = jaString.replace("\n", " ") - list408.append(jaString) + 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: - translatedText = list408[0] - list408.pop(0) + if len(list408) > 0: + translatedText = list408[0] + list408.pop(0) - # Textwrap - translatedText = dazedwrap.wrapText(translatedText, width=WIDTH) + # Remove speaker if SPEAKERS408 enabled + if SPEAKERS408: + matchSpeaker = re.search(r'(^\[.+?\]\s?[|:]\s?)', translatedText) + if matchSpeaker: + translatedText = translatedText.replace(matchSpeaker.group(1), "") - # Set Data - codeList[i]["p"][0] = codeList[i]["p"][0].replace(ojaString, translatedText) + # 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: @@ -2743,6 +3284,7 @@ def searchCodes(page, pbar, jobList, filename): # Start Pass 2 if setData: + clearSpeakerCache() # Clear cache to ensure fresh speaker translations in Pass 2 searchCodes( page, pbar, @@ -2961,53 +3503,56 @@ def searchSystem(data, pbar): if pbar is not None: pbar.refresh() - # Armor Types - batch translate all - 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() + # 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 - 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() + # 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 - 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() + # 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 = [] @@ -3032,26 +3577,28 @@ def searchSystem(data, pbar): pbar.refresh() # 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() + 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): @@ -3076,40 +3623,41 @@ def searchSystem(data, pbar): 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] + # 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) - # 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() + 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 @@ -3199,6 +3747,21 @@ def translateAI(text, history, fullPromptFlag): # 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, diff --git a/modules/rpgmakermvmz.py b/modules/rpgmakermvmz.py index 6a390d8..445c096 100644 --- a/modules/rpgmakermvmz.py +++ b/modules/rpgmakermvmz.py @@ -34,6 +34,7 @@ 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 = [] @@ -433,10 +434,107 @@ def update_vocab_section(category: str, pairs: list[tuple[str, str]]): def parseMap(data, filename): totalTokens = [0, 0] - totalLines = 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): + # Silent during preflight + pass + def refresh(self): + pass + + def _estimate_map_units(d, fname) -> int: + # Use a deep copy to avoid mutating real data during estimation + dcopy = copy.deepcopy(d) + bar = _CountingBar() + # Temporarily enable preflight-count mode and route progress updates to our counter + global PREFLIGHT_COUNT_MODE + saved_preflight = PREFLIGHT_COUNT_MODE + PREFLIGHT_COUNT_MODE = True + global PBAR + saved_pbar = PBAR + PBAR = bar + try: + # Count display name TL (1 unit if present) + if "Map" in fname and isinstance(dcopy.get("displayName", None), str): + try: + translateAI( + dcopy["displayName"], + "Reply with only the " + LANGUAGE + " translation of the RPG location name", + False, + ) + except Exception: + pass + + # Notes and pages + evts = dcopy.get("events", []) or [] + for evt in evts: + 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 "" + + # name translation + if "" in note_val: + name_val = evt.get("name") or "" + if isinstance(name_val, str) and name_val: + try: + translateAI( + name_val, + "Reply with only the " + LANGUAGE + " translation of the RPG location name", + False, + ) + except Exception: + pass + + # + if "", False) + except Exception: + pass + + # , , handled before page processing in real run + if ".*") + except Exception: + pass + if ".*") + except Exception: + pass + + # Other note tags handled during main loop + # We'll just invoke the normal search pass which will batch and call translateAI + for page in (evt.get("pages", []) or []): + try: + searchCodes(page, bar, [], fname) + except Exception: + # Ignore counting errors to avoid blocking + pass + + return getattr(bar, "n", 0) or 0 + finally: + PREFLIGHT_COUNT_MODE = saved_preflight + PBAR = saved_pbar + # Translate displayName for Map files if "Map" in filename: response = translateAI( @@ -448,30 +546,18 @@ def parseMap(data, filename): totalTokens[1] += response[1][1] data["displayName"] = response[0].replace('"', "") - # 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"]) + # Compute accurate total using preflight (includes speakers, choices, groups, and notes) + 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 event in events: + if event: + for page in event.get("pages", []) or []: + try: + totalLines += len(page.get("list", [])) + except Exception: + pass global PBAR # Process each page synchronously with progress updates @@ -595,13 +681,52 @@ def translateNoteOmitSpace(event, regex): def parseCommonEvents(data, filename): totalTokens = [0, 0] - totalLines = 0 global LOCK - # Get total for progress bar - for page in data: - if page is not None: - totalLines += len(page["list"]) + # --- 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: + dcopy = copy.deepcopy(pages) + bar = _CountingBar() + global PREFLIGHT_COUNT_MODE, PBAR + saved_flag = PREFLIGHT_COUNT_MODE + saved_pbar = PBAR + PREFLIGHT_COUNT_MODE = True + PBAR = bar + try: + for page in dcopy: + if page is not None: + try: + searchCodes(page, bar, [], fname) + except Exception: + pass + return getattr(bar, "n", 0) or 0 + finally: + PREFLIGHT_COUNT_MODE = saved_flag + PBAR = saved_pbar + + 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: @@ -623,15 +748,55 @@ def parseCommonEvents(data, filename): def parseTroops(data, filename): totalTokens = [0, 0] - totalLines = 0 global LOCK - # Get total for progress bar - for troop in data: - if troop is not None: - for page in troop["pages"]: - # Progress measured by number of commands in each page's list - totalLines += len(page["list"]) + # --- 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: + tcopy = copy.deepcopy(troops) + bar = _CountingBar() + global PREFLIGHT_COUNT_MODE, PBAR + saved_flag = PREFLIGHT_COUNT_MODE + saved_pbar = PBAR + PREFLIGHT_COUNT_MODE = True + PBAR = bar + try: + for troop in tcopy: + if troop is None: + continue + for page in (troop.get("pages", []) or []): + if page is not None: + try: + searchCodes(page, bar, [], fname) + except Exception: + pass + return getattr(bar, "n", 0) or 0 + finally: + PREFLIGHT_COUNT_MODE = saved_flag + PBAR = saved_pbar + + 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: @@ -656,49 +821,138 @@ def parseTroops(data, filename): def parseNames(data, filename, context): totalTokens = [0, 0] - # Precompute total work units for progress bar - def count_work_units(data, context): - total = 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: + ecopy = copy.deepcopy(entries) + bar = _CountingBar() + global PREFLIGHT_COUNT_MODE, PBAR + saved_flag = PREFLIGHT_COUNT_MODE + saved_pbar = PBAR + PREFLIGHT_COUNT_MODE = True + PBAR = bar + try: + # Count names/descriptions/profile/nickname + name_cnt = 0 + desc_cnt = 0 + profile_cnt = 0 + nickname_cnt = 0 + msg_cnt = 0 + notes_cnt = 0 + + 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 ecopy: + 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: name_cnt += 1 + if nn: nickname_cnt += 1 + if pf: profile_cnt += 1 + elif ctx in ["Armors", "Weapons", "Items"]: + if nm: name_cnt += 1 + if ds: desc_cnt += 1 + elif ctx == "Skills": + if nm: name_cnt += 1 + if ds: desc_cnt += 1 + for k in range(1,5): + if entry.get(f"message{k}"): msg_cnt += 1 + elif ctx in ["Enemies", "Classes", "MapInfos"]: + if nm: name_cnt += 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" int: + scopy = copy.deepcopy(states) + bar = _CountingBar() + global PREFLIGHT_COUNT_MODE, PBAR + saved_flag = PREFLIGHT_COUNT_MODE + saved_pbar = PBAR + PREFLIGHT_COUNT_MODE = True + PBAR = bar + try: + for ss in scopy: + if ss is not None: + try: + searchSS(ss, bar) + except Exception: + pass + return getattr(bar, "n", 0) or 0 + finally: + PREFLIGHT_COUNT_MODE = saved_flag + PBAR = saved_pbar + + 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 += 1 - if st.get("description"): - total += 1 - for n in range(1, 5): - if st.get(f"message{n}"): - total += 1 - return total - - total_units = count_work_units(data) + 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: @@ -758,29 +1040,53 @@ def parseSS(data, filename): def parseSystem(data, filename): totalTokens = [0, 0] - # Precompute total units across system sections (exclude notes; count strings) - def count_work_units(sys): - total = 0 - # Title - if sys.get("gameTitle"): - total += 1 - # Terms (excluding 'messages' object) - terms = sys.get("terms", {}) - for term_key, term_list in terms.items(): - if term_key == "messages": - continue - if isinstance(term_list, list): - total += sum(1 for x in term_list if x is not None) - # Armor, Skill, Equip types - total += len(sys.get("armorTypes", []) or []) - total += len(sys.get("skillTypes", []) or []) - total += len(sys.get("equipTypes", []) or []) - # Messages - messages = terms.get("messages", {}) or {} - total += len(messages) - return total + # --- 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 - total_units = count_work_units(data) + def _estimate_units(sysobj, fname) -> int: + scopy = copy.deepcopy(sysobj) + bar = _CountingBar() + global PREFLIGHT_COUNT_MODE, PBAR + saved_flag = PREFLIGHT_COUNT_MODE + saved_pbar = PBAR + PREFLIGHT_COUNT_MODE = True + PBAR = bar + try: + try: + searchSystem(scopy, bar) + except Exception: + pass + return getattr(bar, "n", 0) or 0 + finally: + PREFLIGHT_COUNT_MODE = saved_flag + PBAR = saved_pbar + + total_units = _estimate_units(data, filename) + if not isinstance(total_units, int) or total_units <= 0: + # Fallback: rough count of strings + total_units = 0 + if data.get("gameTitle"): total_units += 1 + terms = data.get("terms", {}) or {} + for k,v in terms.items(): + if k == "messages": + continue + if isinstance(v, list): + total_units += sum(1 for x in v if x is not None) + total_units += len(data.get("armorTypes", []) or []) + total_units += len(data.get("skillTypes", []) or []) + total_units += len(data.get("equipTypes", []) or []) + total_units += len((terms.get("messages", {}) or {})) global PBAR with tqdm(total=total_units, bar_format=BAR_FORMAT, position=POSITION, leave=LEAVE, desc=filename) as pbar: @@ -800,12 +1106,50 @@ def parseSystem(data, filename): def parseScenario(data, filename): totalTokens = [0, 0] - totalLines = 0 global LOCK - # Get total for progress bar - for page in data.items(): - totalLines += len(page[1]) + # --- 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: + scopy = copy.deepcopy(scenario) + bar = _CountingBar() + global PREFLIGHT_COUNT_MODE, PBAR + saved_flag = PREFLIGHT_COUNT_MODE + saved_pbar = PBAR + PREFLIGHT_COUNT_MODE = True + PBAR = bar + try: + for key, lst in scopy.items(): + if lst is not None: + try: + searchCodes(lst, bar, [], fname) + except Exception: + pass + return getattr(bar, "n", 0) or 0 + finally: + PREFLIGHT_COUNT_MODE = saved_flag + PBAR = saved_pbar + + 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: @@ -3192,6 +3536,21 @@ def translateAI(text, history, fullPromptFlag): # 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,