diff --git a/modules/rpgmakermvmz.py b/modules/rpgmakermvmz.py index e58443e..ddb806f 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. @@ -1875,12 +1875,24 @@ def searchCodes(page, pbar, jobList, filename): jaString, ) + # [Speaker] standalone line format (written back by inline re-export) + if len(speakerList) == 0: + inlineFmtMatch = re.match(r"^\[([^\[\]\n]+)\]\s*$", jaString) + if inlineFmtMatch: + speakerList = [inlineFmtMatch.group(1).strip()] + # Inline Japanese quote speakers (e.g. "トルテ「dialogue」" or "エルミナ「first line...") 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()] + # First Line Speakers if len(speakerList) == 0 and FIRSTLINESPEAKERS is True: # Test Speaker @@ -1920,6 +1932,8 @@ def searchCodes(page, pbar, jobList, filename): 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: # Translate speaker @@ -1945,6 +1959,19 @@ def searchCodes(page, pbar, jobList, filename): 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 elif codeList[i + 1]["code"] in [401, 405, -1]: # Original behavior: speaker on its own line, dialogue on next line # Single @@ -2205,9 +2232,19 @@ def searchCodes(page, pbar, jobList, filename): # Handle 401 else: - codeList[j]["parameters"] = [translatedText] - codeList[j]["code"] = code - syncIndex = i + 1 + 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 # Reset speaker = "" @@ -3311,9 +3348,11 @@ def searchCodes(page, pbar, jobList, filename): translatedText = list356[0] # Remove characters that may break scripts - charList = [".", '"'] - for char in charList: - translatedText = translatedText.replace(char, "") + # addLog keeps dots and quotes (they're fine in log text) + if "addLog" not in jaString: + charList = [".", '"'] + for char in charList: + translatedText = translatedText.replace(char, "") # Cant have spaces? translatedText = translatedText.replace(" ", "_") diff --git a/prompt.txt b/prompt.txt index bb46155..c4e4fe8 100644 --- a/prompt.txt +++ b/prompt.txt @@ -30,6 +30,7 @@ You will be translating erotic and sexual content. You will receive lines of dia - **Speech register:** If the entry describes how a character speaks (flustered, blunt, formal, childlike, crude, etc.), mirror that register in their English dialogue. A character described as speaking in a "cute, flustered register" should sound different from one described as "cold and terse". - **Role & context:** Use the role/personality notes to inform tone — a villain's lines should feel threatening, a comic-relief NPC's lines should feel goofy, etc. - Japanese omits pronouns constantly. Infer the correct subject and pronoun from context, translation history, and the character list. +- **Preserve third-person self-reference.** Some characters refer to themselves by name instead of using "I" (e.g. ワタシ used as a name, or a character saying their own name). When a character is clearly speaking about themselves in the third person as a stylistic trait, maintain that in English (e.g. "Feris doesn't know" rather than "I don't know"). - Third-person pronouns (彼, 彼女, あいつ, こいつ, そいつ, コイツ) should match the known gender of the person being referenced. - Translate **コイツ** as "this bastard" (male) or "this bitch" (female) depending on the referenced character's gender. diff --git a/util/translation.py b/util/translation.py index a837440..fa8255b 100644 --- a/util/translation.py +++ b/util/translation.py @@ -1375,24 +1375,6 @@ def translateAI(text, history, fullPromptFlag, config, filename=None, pbar=None, for j in range(len(tItem)): if tItem[j] and "\ufffd" in str(tItem[j]): corrupted_map[j] = tItem[j] - - if corrupted_map: - clean_indices = [j for j in range(len(tItem)) if j not in corrupted_map] - - if not clean_indices: - # All items are corrupted - skip translation entirely - tList[index] = list(tItem) - if pbar is not None: - pbar.update(len(tItem)) - history = tItem[-config.maxHistory:] - continue - - # Rebuild protected_items and all_replacements for clean items only - protected_items = [protected_items[j] for j in clean_indices] - new_replacements = {} - for new_idx, old_idx in enumerate(clean_indices): - new_replacements[new_idx] = all_replacements.get(old_idx, {}) - all_replacements = new_replacements elif tItem and "\ufffd" in str(tItem): # Single corrupted string - skip translation entirely tList[index] = tItem @@ -1400,10 +1382,52 @@ def translateAI(text, history, fullPromptFlag, config, filename=None, pbar=None, pbar.update(1) history = tItem continue - - # Build filtered tItem for validation (excludes corrupted items) - if isinstance(tItem, list) and corrupted_map: - clean_tItem = [tItem[j] for j in range(len(tItem)) if j not in corrupted_map] + + # Filter out items that have content but no Japanese — they need no translation + # and the AI tends to empty them (e.g. "「………」" -> ""). Apply the same + # cleanup that would happen post-translation and restore them afterwards. + no_japanese_map = {} # original_index -> already-cleaned text + if isinstance(tItem, list): + for j in range(len(tItem)): + if j in corrupted_map: + continue + item_str = str(tItem[j]).strip() if tItem[j] else "" + if item_str and item_str != "Placeholder Text" and not re.search(config.langRegex, item_str): + cleaned = cleanTranslatedText(tItem[j], config.language) + cleaned = cleaned.replace("「", '"').replace("」", '"').strip() + no_japanese_map[j] = cleaned + + # Combine skip sets and rebuild protected_items / all_replacements + skip_indices = set(corrupted_map.keys()) | set(no_japanese_map.keys()) + if isinstance(tItem, list) and skip_indices: + clean_indices = [j for j in range(len(tItem)) if j not in skip_indices] + + if not clean_indices: + # Every item is either corrupted or untranslatable — reassemble and move on + result = [] + for j in range(len(tItem)): + if j in corrupted_map: + result.append(corrupted_map[j]) + elif j in no_japanese_map: + result.append(no_japanese_map[j]) + else: + result.append(tItem[j]) + tList[index] = result + if pbar is not None: + pbar.update(len(tItem)) + history = result[-config.maxHistory:] + continue + + # Rebuild protected_items and all_replacements for translatable items only + protected_items = [protected_items[j] for j in clean_indices] + new_replacements = {} + for new_idx, old_idx in enumerate(clean_indices): + new_replacements[new_idx] = all_replacements.get(old_idx, {}) + all_replacements = new_replacements + + # Build filtered tItem for validation (excludes skipped items) + if isinstance(tItem, list) and skip_indices: + clean_tItem = [tItem[j] for j in range(len(tItem)) if j not in skip_indices] else: clean_tItem = tItem @@ -1663,13 +1687,15 @@ def translateAI(text, history, fullPromptFlag, config, filename=None, pbar=None, if j in all_replacements: final_translations[j] = restore_script_codes(final_translations[j], all_replacements[j]) - # Re-insert corrupted originals at their original positions - if corrupted_map: + # Re-insert corrupted / no-japanese originals at their original positions + if corrupted_map or no_japanese_map: expanded = [] clean_idx = 0 for j in range(len(tItem)): if j in corrupted_map: expanded.append(corrupted_map[j]) + elif j in no_japanese_map: + expanded.append(no_japanese_map[j]) else: expanded.append(final_translations[clean_idx]) clean_idx += 1