From 145d5ddc0c63524c30007e107a44a49befef13ad Mon Sep 17 00:00:00 2001 From: dazedanon Date: Sun, 15 Mar 2026 19:27:29 -0500 Subject: [PATCH] Improvements --- gui/workflow_tab.py | 63 +++++++++++++++++++++++++++++++++++++++-- modules/rpgmakermvmz.py | 2 +- util/translation.py | 44 ++++++++++++++++++++++------ 3 files changed, 96 insertions(+), 13 deletions(-) diff --git a/gui/workflow_tab.py b/gui/workflow_tab.py index c8f380e..9d66ea8 100644 --- a/gui/workflow_tab.py +++ b/gui/workflow_tab.py @@ -672,11 +672,20 @@ class WorkflowTab(QWidget): " - Speech register and personality notes — how they speak, their tone, any nicknames, " "whether their name is player-chosen, etc.\n" "\n" + "⚠️ IMPORTANT: In all descriptions, ALWAYS refer to other characters by their " + "ENGLISH name, never their Japanese name. " + "For example, write \"her sister Ruin was kidnapped\" not " + "\"her sister ルイン was kidnapped\". " + "The description text must be entirely in English with no Japanese characters " + "except inside the leading Japanese (English) name tag.\n" + "\n" "Format — the header line, then one entry per line:\n" "# Game Characters\n" "\u30b7\u30ed (Shiro) - Female; protagonist; player-controlled (Actors.json ID 1); " - "speaks in a flustered, cute register with feminine speech markers; nickname \u30d0\u30ab\u732b\u3002\n" - "\u30af\u30ed\u30cd (Kurone) - Female; antagonist; cold and terse; speaks in short cutting sentences\n" + "speaks in a flustered, cute register with feminine speech markers; " + "her partner is Kurone\n" + "\u30af\u30ed\u30cd (Kurone) - Female; antagonist; cold and terse; speaks in short cutting sentences; " + "pursues Shiro throughout the story\n" "\n" "Rules:\n" " - Named characters only — no generic enemy types or unnamed NPCs.\n" @@ -1212,6 +1221,10 @@ class WorkflowTab(QWidget): self.import_btn.setEnabled(False) self.import_btn.clicked.connect(self._import_files) import_row.addWidget(self.import_btn) + clear_translated_btn = _make_btn("🗑 Clear translated/", "#8b0000") + clear_translated_btn.setToolTip("Delete all files inside the translated/ folder") + clear_translated_btn.clicked.connect(self._clear_translated) + import_row.addWidget(clear_translated_btn) import_row.addStretch() layout.addLayout(import_row) @@ -1820,6 +1833,43 @@ class WorkflowTab(QWidget): self._worker = worker worker.start() + def _clear_translated(self): + translated_dir = Path("translated") + items_to_delete = [ + item for item in translated_dir.iterdir() + if item.name != ".gitkeep" + ] if translated_dir.exists() else [] + if not items_to_delete: + self._log("ℹ translated/ is already empty — nothing to clear.") + return + reply = QMessageBox.warning( + self, + "Clear translated/ folder", + "This will permanently delete all files inside the translated/ folder.\n\nAre you sure?", + QMessageBox.Yes | QMessageBox.Cancel, + QMessageBox.Cancel, + ) + if reply != QMessageBox.Yes: + return + deleted = 0 + errors = [] + for item in items_to_delete: + try: + if item.is_file(): + item.unlink() + deleted += 1 + elif item.is_dir(): + import shutil + shutil.rmtree(item) + deleted += 1 + except Exception as exc: + errors.append(f"{item.name}: {exc}") + if errors: + self._log(f"⚠ {len(errors)} error(s) while clearing translated/:") + for e in errors[:10]: + self._log(f" {e}") + self._log(f"✅ Cleared {deleted} item(s) from translated/") + def _on_import_done(self, count: int, errors: list): self.import_btn.setEnabled(True) if errors: @@ -2299,10 +2349,17 @@ class WorkflowTab(QWidget): if not game_data: return + translated_dir = Path("translated") + active_set = set(active) + exportable = [ + fp for fp in translated_dir.glob("*.json") + if fp.name in active_set and fp.name != ".gitkeep" + ] if translated_dir.exists() else [] + reply = QMessageBox.question( self, "Export Active Files to Game", - f"Export {len(active)} file(s) matching files/ contents into:\n{game_data}\n\n" + f"Export {len(exportable)} file(s) into:\n{game_data}\n\n" "Make a backup first if needed. Continue?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No, diff --git a/modules/rpgmakermvmz.py b/modules/rpgmakermvmz.py index e58443e..5ac40ab 100644 --- a/modules/rpgmakermvmz.py +++ b/modules/rpgmakermvmz.py @@ -79,7 +79,7 @@ LEAVE = False # FIRSTLINESPEAKERS: Guess speaker from first line. FIRSTLINESPEAKERS = False # INLINE401SPEAKERS: Extract speaker from "Name「dialogue」" inline format on 401 lines. -INLINE401SPEAKERS = False +INLINE401SPEAKERS = True # FACENAME101: Map face name -> speaker. FACENAME101 = False # Face name -> speaker mapping for FACENAME101. diff --git a/util/translation.py b/util/translation.py index b352abe..a837440 100644 --- a/util/translation.py +++ b/util/translation.py @@ -57,7 +57,25 @@ def protect_script_codes(text): """ if not text or not isinstance(text, str): return text, {} - + + # Normalize curly/smart quotes to ASCII equivalents BEFORE building the JSON + # payload. When these characters appear inside a JSON string value the AI + # tends to treat them as regular ASCII double-quotes, which makes the value + # appear empty (e.g. `"スキルを"リセットする` → AI sees empty + stray text). + # This mirrors the identical normalization already applied to the AI's OUTPUT + # inside extractTranslation's translation_table. + quote_norm_table = str.maketrans({ + '\u201C': "'", # " left double quotation mark + '\u201D': "'", # " right double quotation mark + '\uFF02': "'", # " fullwidth quotation mark + '\u2018': "'", # ' left single quotation mark + '\u2019': "'", # ' right single quotation mark + '\u201B': "'", # ‛ single high-reversed-9 quotation mark + '\u02BC': "'", # ʼ modifier letter apostrophe + '\uFF07': "'", # ' fullwidth apostrophe + }) + text = text.translate(quote_norm_table) + replacements = {} protected_text = text counter = 0 @@ -1042,8 +1060,9 @@ def cleanTranslatedText(translatedText, language): "〜": "~", "ッ": "", "。": ".", - "「": '\"', - "」": '\"', + # Note: 「 and 」 are NOT replaced here — replacing them with ASCII " would + # corrupt raw JSON strings before extraction. They are handled per-line + # in _clean_extracted_line() after JSON parsing. "—": "―", "】": "]", "【": "[", @@ -1069,8 +1088,11 @@ def cleanTranslatedText(translatedText, language): def elongateCharacters(text): """Replace ー sequences with elongated characters""" - # Define a pattern to match one character followed by two or more ー characters - pattern = r"(?<=(.))ー{2,}" + # Define a pattern to match one character followed by two or more ー characters. + # The lookbehind is restricted to non-ー Japanese/CJK characters so that: + # - standalone ー separators (e.g. "ーーーーーーーーーー") are left untouched + # - ー sequences preceded by a JSON quote or other non-Japanese char are not corrupted + pattern = r"(?<=([\u3040-\u309F\u30A0-\u30FB\u30FD-\u30FF\u4E00-\u9FEF\uFF61-\uFF9F]))ー{2,}" # Define a replacement function that elongates the captured character def repl(match): @@ -1588,10 +1610,14 @@ def translateAI(text, history, fullPromptFlag, config, filename=None, pbar=None, else: # Set translations (line count matches, placeholders valid, and content is good) # Strip "Placeholder Text" from individual lines (AI placeholder for untranslatable input) - final_translations = [ - line.replace("Placeholder Text", "").strip() if isinstance(line, str) else line - for line in extracted - ] + # Also apply the 「→" / 」→" replacements here per-line (safe now that JSON is parsed) + def _clean_extracted_line(line): + if not isinstance(line, str): + return line + line = line.replace("Placeholder Text", "").strip() + line = line.replace("「", '"').replace("」", '"') + return line + final_translations = [_clean_extracted_line(line) for line in extracted] else: # Single string: validate placeholders placeholder_valid, missing, extra = validate_placeholders(protected_items, cleaned_text, all_replacements[0])