From 039370f4e27850beecf87fae4efb30923774a4ad Mon Sep 17 00:00:00 2001 From: dazedanon Date: Mon, 16 Mar 2026 00:25:56 -0500 Subject: [PATCH] Fix inline --- gui/workflow_tab.py | 117 +++++++++++++++++++++++++++------------- modules/rpgmakermvmz.py | 78 ++++++++------------------- util/translation.py | 7 +++ 3 files changed, 111 insertions(+), 91 deletions(-) diff --git a/gui/workflow_tab.py b/gui/workflow_tab.py index 036062a..ed80f70 100644 --- a/gui/workflow_tab.py +++ b/gui/workflow_tab.py @@ -1266,6 +1266,8 @@ class WorkflowTab(QWidget): layout.addWidget(cb_box) + self._populate_speaker_flags() + # ── Step 4: Translation ───────────────────────────────────────────────── def _build_step4_translation(self, layout: QVBoxLayout): @@ -1641,19 +1643,6 @@ class WorkflowTab(QWidget): p2_inner.addLayout(p2_row) layout.addWidget(p2_box) - # Shared manual sync button (also happens automatically before each phase run) - sync_row = QHBoxLayout() - sync_row.setContentsMargins(0, 6, 0, 0) - sync_all_btn = _make_btn("🔄 Sync translated/ → files/", "#3a4a6a") - sync_all_btn.setToolTip( - "Copy translated files back into files/ so the next phase starts from the latest output.\n" - "This also happens automatically at the start of each phase run." - ) - sync_all_btn.clicked.connect(self._copy_translated_to_files) - sync_row.addWidget(sync_all_btn) - sync_row.addStretch() - layout.addLayout(sync_row) - # Pre-populate all Phase 2 checkboxes from current module state self._populate_p2_checkboxes() @@ -2100,6 +2089,24 @@ class WorkflowTab(QWidget): except Exception as exc: self._log(f"❌ Could not update .env: {exc}") + def _populate_speaker_flags(self): + """Read current module config and pre-tick speaker flag checkboxes.""" + try: + from gui.config_integration import ConfigIntegration + cur = ConfigIntegration().read_current_config() + flag_map = { + "INLINE401SPEAKERS": self.spk_inline_cb, + "FIRSTLINESPEAKERS": self.spk_firstline_cb, + "FACENAME101": self.spk_face_cb, + } + for key, cb in flag_map.items(): + if key in cur: + cb.blockSignals(True) + cb.setChecked(bool(cur[key])) + cb.blockSignals(False) + except Exception: + pass + def _apply_speaker_flags(self): cfg = { "INLINE401SPEAKERS": self.spk_inline_cb.isChecked(), @@ -2128,18 +2135,25 @@ class WorkflowTab(QWidget): # ───────────────────────────────────────────────────────────────────────── def _run_phase(self, phase): - # For every phase after Phase 0, automatically sync translated/ → files/ - # so the next phase always starts from the latest translated output. - if phase != 0: - translated_dir = Path("translated") - files_dir = Path("files") - has_translated = ( - translated_dir.exists() - and any(translated_dir.glob("*.json")) - ) - if has_translated: - self._log("🔄 Auto-syncing translated/ → files/ before phase run…") - self._copy_translated_to_files() + # Ask user if they want to sync translated/ → files/ before running this phase + from PyQt5.QtWidgets import QMessageBox + transl_dir = Path("translated") + files_dir = Path("files") + if transl_dir.exists() and any(transl_dir.glob("*.json")): + active = {fp.name for fp in files_dir.glob("*.json")} if files_dir.exists() else set() + overlap = [fp for fp in transl_dir.glob("*.json") if not active or fp.name in active] + if overlap: + reply = QMessageBox.question( + None, + "Sync before phase?", + f"translated/ contains {len(overlap)} file(s) that match files/.\n\n" + "Sync translated/ → files/ before running this phase?\n" + "Yes = overwrite files/ with translated versions\n" + "No = use existing files/ as-is", + QMessageBox.Yes | QMessageBox.No, + ) + if reply == QMessageBox.Yes: + self._do_copy_translated_to_files() if phase == 0: config = PHASE0_CONFIG @@ -2317,11 +2331,38 @@ class WorkflowTab(QWidget): # Step 5 – Export to game # ───────────────────────────────────────────────────────────────────────── - def _copy_translated_to_files(self): - """Copy translated/ files back into files/ (only matching names).""" + def _do_copy_translated_to_files(self): + """Silently copy translated/ files back into files/ (only matching names). Returns count copied.""" import shutil - files_dir = Path("files") - transl_dir = Path("translated") + files_dir = Path("files") + transl_dir = Path("translated") + + if not transl_dir.exists(): + self._log("⚠ translated/ folder not found — nothing to sync.") + return 0 + + active = {fp.name for fp in files_dir.glob("*.json")} if files_dir.exists() else set() + to_copy = [fp for fp in transl_dir.glob("*.json") if not active or fp.name in active] + + if not to_copy: + self._log("⚠ No matching files found in translated/ to sync.") + return 0 + + files_dir.mkdir(exist_ok=True) + copied = 0 + for src in to_copy: + dst = files_dir / src.name + shutil.copy2(src, dst) + copied += 1 + + self._log(f"✅ Synced {copied} file(s) from translated/ → files/") + return copied + + def _copy_translated_to_files(self): + """Prompt user then copy translated/ files back into files/ (only matching names).""" + from PyQt5.QtWidgets import QMessageBox + files_dir = Path("files") + transl_dir = Path("translated") if not transl_dir.exists(): self._log("⚠ translated/ folder not found — nothing to sync.") @@ -2334,14 +2375,18 @@ class WorkflowTab(QWidget): self._log("⚠ No matching files found in translated/ to sync.") return - files_dir.mkdir(exist_ok=True) - copied = 0 - for src in to_copy: - dst = files_dir / src.name - shutil.copy2(src, dst) - copied += 1 + reply = QMessageBox.question( + None, + "Sync translated/ → files/", + f"This will overwrite {len(to_copy)} file(s) in files/ with their translated versions.\n\n" + "Choose Yes to sync, or No to keep files/ as-is.", + QMessageBox.Yes | QMessageBox.No, + ) + if reply != QMessageBox.Yes: + self._log("⏭ Sync skipped — using existing files/ as-is.") + return - self._log(f"✅ Synced {copied} file(s) from translated/ → files/") + self._do_copy_translated_to_files() def _export_active_files(self): """Export only translated files whose names match what is in files/.""" diff --git a/modules/rpgmakermvmz.py b/modules/rpgmakermvmz.py index ddb806f..56a0a5e 100644 --- a/modules/rpgmakermvmz.py +++ b/modules/rpgmakermvmz.py @@ -1881,17 +1881,16 @@ def searchCodes(page, pbar, jobList, filename): if inlineFmtMatch: speakerList = [inlineFmtMatch.group(1).strip()] - # Inline Japanese quote speakers (e.g. "トルテ「dialogue」" or "エルミナ「first line...") + # Inline speaker detection — Name「/Name: "/Name: (/[Name] "/[Name] ( if len(speakerList) == 0 and INLINE401SPEAKERS: - inlineQuoteDetect = re.match(r"^([^\s「」。、!?…\\\n]{1,20})「", jaString) - if inlineQuoteDetect: - speakerList = [inlineQuoteDetect.group(1).strip()] - - # Inline curly-quote speakers (e.g. "Elmina: \u201cdialogue" or "Elmina\u201cdialogue") - if len(speakerList) == 0 and INLINE401SPEAKERS: - inlineCurlyDetect = re.match(r'^([^\s「」。、!?…\\\n\u201c\u201d"::\[\]]{1,20})(?:[::]\s*)?[\u201c"]', jaString) - if inlineCurlyDetect: - speakerList = [inlineCurlyDetect.group(1).strip()] + inlineSpeakerMatch = re.match( + r'^(?:\[([^\]]{1,30})\]\s*|([^\s「」。、!?…\\\n“”"(:\[\]]{1,20})(?:[:::]\s*)?(?=[「“"(]))(.*)', + jaString, re.DOTALL + ) + if inlineSpeakerMatch: + speakerList = [(inlineSpeakerMatch.group(1) or inlineSpeakerMatch.group(2)).strip()] + else: + inlineSpeakerMatch = None # First Line Speakers if len(speakerList) == 0 and FIRSTLINESPEAKERS is True: @@ -1928,14 +1927,18 @@ def searchCodes(page, pbar, jobList, filename): # Replace Speaker if len(speakerList) != 0: - # Check if speaker+dialogue are on same line (【speaker】dialogue) + # Check if speaker+dialogue are on same line sameLineMatch = re.match(r"^\s*【([^】]+)】(.+)", jaString, re.DOTALL) - # Check if speaker+dialogue are on same line via Japanese quote (Name「dialogue) - inlineQuoteMatch = re.match(r"^([^\s「」。、!?…\\\n]{1,20})「(.*)", jaString, re.DOTALL) if INLINE401SPEAKERS else None - # Check if speaker+dialogue are on same line via curly quote (Name: "dialogue or Name"dialogue) - inlineCurlyMatch = re.match(r'^([^\s「」。、!?…\\\n\u201c\u201d"::\[\]]{1,20})(?:[::]\s*)?([\u201c"])(.*)', jaString, re.DOTALL) if INLINE401SPEAKERS else None - - if sameLineMatch and len(speakerList) == 1: + if inlineSpeakerMatch and len(speakerList) == 1: + # Strip speaker prefix, keep everything after as dialogue + response = getSpeaker(speakerList[0]) + speaker = response[0] + totalTokens[0] += response[1][0] + totalTokens[1] += response[1][1] + jaString = inlineSpeakerMatch.group(3) + if not setData: + nametag = f"[{speaker}]\n" + nametag + elif sameLineMatch and len(speakerList) == 1: # Translate speaker response = getSpeaker(speakerList[0]) speaker = response[0] @@ -1944,31 +1947,6 @@ def searchCodes(page, pbar, jobList, filename): # Remove speaker bracket from jaString, let dialogue get translated jaString = sameLineMatch.group(2) # Store the translated bracket to add back later - if not setData: - nametag = f"【{speaker}】" + nametag - # Don't skip to next line - continue with current line - elif inlineQuoteMatch and len(speakerList) == 1: - # Inline quote format: strip "Name「" prefix entirely, keep raw dialogue - response = getSpeaker(speakerList[0]) - speaker = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - # Remove speaker name from jaString, preserve 「...」 for standard quote conversion - jaString = "「" + inlineQuoteMatch.group(2) - # Format as [Speaker]\n for output - if not setData: - nametag = f"[{speaker}]\n" + nametag - # Don't skip to next line - continue with current line - elif inlineCurlyMatch and len(speakerList) == 1: - # Curly/straight-quote format: strip "Name: \u201c" or "Name"" prefix, keep dialogue - response = getSpeaker(speakerList[0]) - speaker = response[0] - totalTokens[0] += response[1][0] - totalTokens[1] += response[1][1] - # Remove speaker name, preserve whichever opening quote was matched - openQuote = inlineCurlyMatch.group(2) - jaString = openQuote + inlineCurlyMatch.group(3) - # Format as [Speaker]\n for output if not setData: nametag = f"[{speaker}]\n" + nametag # Don't skip to next line - continue with current line @@ -2232,19 +2210,9 @@ def searchCodes(page, pbar, jobList, filename): # Handle 401 else: - lines = translatedText.split('\n') - if len(lines) > 1: - codeList[j]["parameters"] = [lines[0]] - codeList[j]["code"] = code - for idx, line in enumerate(lines[1:]): - new_item = copy.deepcopy(codeList[j]) - new_item["parameters"] = [line] - codeList.insert(j + idx + 1, new_item) - syncIndex = j + len(lines) - else: - codeList[j]["parameters"] = [translatedText] - codeList[j]["code"] = code - syncIndex = i + 1 + codeList[j]["parameters"] = [translatedText] + codeList[j]["code"] = code + syncIndex = i + 1 # Reset speaker = "" diff --git a/util/translation.py b/util/translation.py index fa8255b..f40f33a 100644 --- a/util/translation.py +++ b/util/translation.py @@ -6,6 +6,7 @@ Centralized translation function used across all modules. import os import re import json +import unicodedata import tiktoken import openai import anthropic @@ -76,6 +77,12 @@ def protect_script_codes(text): }) text = text.translate(quote_norm_table) + # Convert half-width katakana (U+FF61–U+FF9F) to full-width katakana so the + # AI recognises them as Japanese text and translates them correctly. + # NFKC is applied only to matched half-width kana spans to avoid altering + # intentional fullwidth Latin/digit characters elsewhere in the string. + text = re.sub(r'[\uFF61-\uFF9F]+', lambda m: unicodedata.normalize('NFKC', m.group(0)), text) + replacements = {} protected_text = text counter = 0