Fix Speaker bug and add tests

This commit is contained in:
DazedAnon 2026-06-22 05:25:52 -05:00
parent 4dd9d2bbd4
commit 3dea0b8972
14 changed files with 139 additions and 77 deletions

View file

@ -14,6 +14,7 @@ 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.speaker_prefix import strip_speaker_prefix
import tempfile
# Globals
@ -330,8 +331,7 @@ def translateCSV(data, pbar, writeFile, writer, filename, translatedList):
translatedList.pop(0)
# Remove speaker prefix from translation if present
if speaker:
translatedText = re.sub(r"^\[?(.+?)\]?\s?[|:]\s?", "", translatedText)
translatedText = strip_speaker_prefix(translatedText)
# Add wordwrap
translatedText = dazedwrap.wrapText(translatedText, WIDTH)

View file

@ -13,6 +13,7 @@ from dotenv import load_dotenv
from retry import retry
from tqdm import tqdm
from util.translation import TranslationConfig, translateAI as sharedtranslateAI, getPricingConfig, calculateCost, getPricingConfig, calculateCost
from util.speaker_prefix import strip_speaker_prefix
import tempfile
# Globals
@ -912,10 +913,9 @@ def translateDialogueSrcTl(data, filename):
# Strip speaker prefix if the AI echoed it back
# Handles: [Speaker]: text | Speaker: text | Speaker(text) / CJK(text)
match = re.search(r'^\[.+?\]\s?[|:]\s?', translatedText)
if match:
translatedText = translatedText[match.end():]
else:
before_strip = translatedText
translatedText = strip_speaker_prefix(translatedText)
if translatedText == before_strip:
# Fallback: strip any leading Japanese/CJK name followed by ( or :
cjk_m = re.match(r'^[一-龠ぁ-ゔァ-ヴーa---\uFF61-\uFF9F]+\s*[\(:]\s*', translatedText)
if cjk_m:
@ -1149,10 +1149,9 @@ def translateJSON(data, filename, translatedList):
stringList = None
# Remove speaker prefix — handles [Name]: / Name: / CJK(text)
match = re.search(r'(^\[.+?\]\s?[|:]\s?)', translatedText)
if match:
translatedText = translatedText.replace(match.group(1), "")
else:
before_strip = translatedText
translatedText = strip_speaker_prefix(translatedText)
if translatedText == before_strip:
cjk_m = re.match(r'^[一-龠ぁ-ゔァ-ヴーa---\uFF61-\uFF9F]+\s*[\(:]\s*', translatedText)
if cjk_m:
translatedText = translatedText[cjk_m.end():]

View file

@ -13,6 +13,7 @@ from dotenv import load_dotenv
from retry import retry
from tqdm import tqdm
from util.translation import TranslationConfig, translateAI as sharedtranslateAI, getPricingConfig, calculateCost, getPricingConfig, calculateCost
from util.speaker_prefix import strip_speaker_prefix
import tempfile
# Globals
@ -324,7 +325,7 @@ def translateKiriKiri(data, pbar, filename, jobList):
translatedText = stringList[0]
stringList.pop(0)
# Remove Speaker label if present
translatedText = re.sub(r"\[.*?\]:\s", "", translatedText)
translatedText = strip_speaker_prefix(translatedText)
# Wrap and convert newlines to [r]
translatedText = dazedwrap.wrapText(translatedText, width=WIDTH)
translatedText = translatedText.replace("\n", "[r]")
@ -362,7 +363,7 @@ def translateKiriKiri(data, pbar, filename, jobList):
if len(stringList) > 0:
translatedText = stringList[0]
stringList.pop(0)
translatedText = re.sub(r"\[.*?\]:\s", "", translatedText)
translatedText = strip_speaker_prefix(translatedText)
translatedText = dazedwrap.wrapText(translatedText, width=WIDTH)
translatedText = translatedText.replace("\n", "[r]")
indent_match = re.match(r"^([ \t\u3000]+)", data[i])
@ -402,7 +403,7 @@ def translateKiriKiri(data, pbar, filename, jobList):
stringList.pop(0)
# Remove Speaker
translatedText = re.sub(r"\[.*?\]:\s", "", translatedText)
translatedText = strip_speaker_prefix(translatedText)
# Textwrap
translatedText = dazedwrap.wrapText(translatedText, width=WIDTH)

View file

@ -13,6 +13,7 @@ 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.speaker_prefix import strip_speaker_prefix
import tempfile
# Globals
@ -291,7 +292,7 @@ def translateRegex(data, filename, translatedList):
stringList = None
# Remove speaker prefix from translation
translatedText = re.sub(r"^\[?(.+?)\]?\s?[|:]\s?", "", translatedText)
translatedText = strip_speaker_prefix(translatedText)
# Textwrap
translatedText = dazedwrap.wrapText(translatedText, width=WIDTH)
@ -383,7 +384,7 @@ def translateRegex(data, filename, translatedList):
stringList = None
# Remove speaker
translatedText = re.sub(r"^\[?(.+?)\]?\s?[|:]\s?", "", translatedText)
translatedText = strip_speaker_prefix(translatedText)
# Escape Quotes
translatedText = re.sub(r'(?<!\\)"', r"", translatedText)

View file

@ -13,6 +13,7 @@ from dotenv import load_dotenv
from retry import retry
from tqdm import tqdm
from util.translation import TranslationConfig, translateAI as sharedtranslateAI, getPricingConfig, calculateCost, getPricingConfig, calculateCost
from util.speaker_prefix import strip_speaker_prefix
import tempfile
# Globals
@ -251,9 +252,7 @@ def translateRenpy(data, filename, translatedList):
translatedList = None
# Remove speaker
if speaker != "":
matchSpeakerList = re.findall(r"^\[?(.+?)\]?\s?[|:]\s?", translatedText)
translatedText = re.sub(r"^\[?(.+?)\]?\s?[|:]\s?", "", translatedText)
translatedText = strip_speaker_prefix(translatedText)
# Escape Quotes
translatedText = re.sub(r'[\\]*(")', '\\"', translatedText)

View file

@ -14,6 +14,7 @@ from dotenv import load_dotenv
from retry import retry
from tqdm import tqdm
from util.translation import TranslationConfig, translateAI as sharedtranslateAI, getPricingConfig, calculateCost, getPricingConfig, calculateCost, get_var_translation, set_var_translations_batch
from util.speaker_prefix import SPEAKER_BRACKET_INNER, strip_speaker_prefix
# Globals
MODEL = os.getenv("model")
@ -2288,7 +2289,7 @@ def searchCodes(page, pbar, jobList, filename):
# [Speaker] standalone line format (written back by inline re-export)
if len(speakerList) == 0:
inlineFmtMatch = re.match(r"^\[([^\[\]\n]+)\]\s*$", speakerWork)
inlineFmtMatch = re.match(rf"^\[({SPEAKER_BRACKET_INNER})\]\s*$", speakerWork, re.IGNORECASE)
if inlineFmtMatch:
speakerList = [inlineFmtMatch.group(1).strip()]
@ -2559,9 +2560,7 @@ def searchCodes(page, pbar, jobList, filename):
translatedText = list401[0]
# Remove speaker prefix if present
match = re.search(r'(^\[(.+?)\]\s?[|:]\s?)', translatedText)
if match:
translatedText = translatedText.replace(match.group(1), "")
translatedText = strip_speaker_prefix(translatedText)
# Remove 。 that appears after ... in AI output
translatedText = re.sub(r'\.\.\.(。)+', '...', translatedText)
@ -2884,9 +2883,7 @@ def searchCodes(page, pbar, jobList, filename):
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), "")
translatedText = strip_speaker_prefix(translatedText)
if FIXTEXTWRAP:
translatedText = dazedwrap.wrapText(translatedText, width=WIDTH)
@ -3282,7 +3279,7 @@ def searchCodes(page, pbar, jobList, filename):
translatedText = list355655[0]
list355655.pop(0)
# Strip speaker prefix if present
translatedText = re.sub(r'^\[.*?\]\s*[|:]\s*', '', translatedText)
translatedText = strip_speaker_prefix(translatedText)
# Replace double quotes to avoid breaking the JSON/JS syntax
translatedText = translatedText.replace('\\"', "'")
translatedText = translatedText.replace('"', "'")
@ -3542,7 +3539,7 @@ def searchCodes(page, pbar, jobList, filename):
translatedText = list355655[0]
list355655.pop(0)
# Strip speaker prefix if present
translatedText = re.sub(r'^\[.*?\]\s*[|:]\s*', '', translatedText)
translatedText = strip_speaker_prefix(translatedText)
# Replace double quotes to avoid breaking JS syntax
translatedText = translatedText.replace('\\"', "'")
translatedText = translatedText.replace('"', "'")
@ -4949,15 +4946,15 @@ def translateAI(text, history, history_ctx=None):
return name
return m.group(0)
speaker_prefix = re.match(
r"^(?P<open>\s*\[)(?P<speaker>(?:\\n\[\d+\]|[^\]\n])+)(?P<close>\]\s*[|:]\s*)",
tag_match = re.match(
rf"^(?P<open>\s*\[)(?P<speaker>{SPEAKER_BRACKET_INNER})(?P<close>\]\s*[|:]\s*)",
s,
re.IGNORECASE,
)
if speaker_prefix:
speaker = _VAR_ACTOR_RE.sub(_display_actor_name, speaker_prefix.group("speaker"))
body = _VAR_ACTOR_RE.sub(_repl, s[speaker_prefix.end():])
return f"{speaker_prefix.group('open')}{speaker}{speaker_prefix.group('close')}{body}"
if tag_match:
speaker = _VAR_ACTOR_RE.sub(_display_actor_name, tag_match.group("speaker"))
body = _VAR_ACTOR_RE.sub(_repl, s[tag_match.end():])
return f"{tag_match.group('open')}{speaker}{tag_match.group('close')}{body}"
return _VAR_ACTOR_RE.sub(_repl, s)

View file

@ -14,6 +14,7 @@ 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.speaker_prefix import strip_speaker_prefix
# Globals
MODEL = os.getenv("model")
@ -501,10 +502,7 @@ def translateBookmark(data, filename, translatedDataList=None, pbar=None):
else:
if dataList:
translated = dataList[0]
if speaker:
match = re.search(r'(^\[.+?\]\s?[|:]\s?)', translated)
if match:
translated = translated.replace(match.group(1), "")
translated = strip_speaker_prefix(translated)
translated = dazedwrap.wrapText(translated, width=WIDTH)
command["data"][i] = translated
dataList.pop(0)
@ -1389,10 +1387,7 @@ def translateRecollection(data, filename, translatedDataList=None, pbar=None):
translated = dataList[0]
# Remove speaker
if speaker:
match = re.search(r'(^\[.+?\]\s?[|:]\s?)', translated)
if match:
translated = translated.replace(match.group(1), "")
translated = strip_speaker_prefix(translated)
# Textwrap
translated = dazedwrap.wrapText(translated, width=WIDTH)
@ -1843,10 +1838,7 @@ def translateMap(data, filename, translatedDataList=None, pbar=None):
else:
if dataList:
translated = dataList[0]
if speaker:
match = re.search(r'(^\[.+?\]\s?[|:]\s?)', translated)
if match:
translated = translated.replace(match.group(1), "")
translated = strip_speaker_prefix(translated)
translated = dazedwrap.wrapText(translated, width=WIDTH)
command["data"][i] = translated
dataList.pop(0)
@ -1919,10 +1911,7 @@ def translateMap(data, filename, translatedDataList=None, pbar=None):
else:
if dataList:
translated = dataList[0]
if speaker:
match = re.search(r'(^\[.+?\]\s?[|:]\s?)', translated)
if match:
translated = translated.replace(match.group(1), "")
translated = strip_speaker_prefix(translated)
translated = dazedwrap.wrapText(translated, width=WIDTH)
command["data"][i] = translated
dataList.pop(0)

View file

@ -13,6 +13,7 @@ from dotenv import load_dotenv
from retry import retry
from tqdm import tqdm
from util.translation import TranslationConfig, translateAI as sharedtranslateAI, getPricingConfig, calculateCost, getPricingConfig, calculateCost
from util.speaker_prefix import SPEAKER_TAG_RE, extract_dialogue_after_speaker, strip_speaker_prefix
import tempfile
# Globals
@ -208,27 +209,20 @@ def translateTxt(data, filename, translatedList):
global LOCK, ESTIMATE, FILENAME, PBAR, MISMATCH
i = 0
# Regex
lineTextRegex = r"(?:^\[.+?\]:)?(.+)"
speakerTextRegex = r"^\[(.+?)\]"
while i < len(data):
# Speaker
match = re.search(speakerTextRegex, data[i])
match = SPEAKER_TAG_RE.match(data[i])
if match:
# Get Speaker
speakerData = getSpeaker(match.group(1))
speaker = speakerData[0]
tokens[0] += speakerData[1][0]
tokens[1] += speakerData[1][1]
data[i] = data[i].replace(match.group(1), speaker)
data[i] = data[i].replace(match.group(1), speaker, 1)
# Dialogue
match = re.search(lineTextRegex, data[i])
jaString = None
if match:
# Set String
jaString = match.group(1)
jaString = extract_dialogue_after_speaker(data[i])
if jaString is not None:
# Pass 1
if not translatedList:
@ -254,7 +248,7 @@ def translateTxt(data, filename, translatedList):
stringList = None
# Remove speaker
translatedText = re.sub(r"(^\[.+?\]\s?[|:]\s?)", "", translatedText)
translatedText = strip_speaker_prefix(translatedText)
# # Textwrap
# translatedText = dazedwrap.wrapText(translatedText, width=WIDTH)

View file

@ -13,6 +13,7 @@ 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.speaker_prefix import strip_speaker_prefix
import tempfile
# OpenAI initialization centralized in util/translation.py
@ -320,9 +321,7 @@ def translateTyrano(data, filename, translatedList):
stringList = None
# Remove speaker
if speaker != "":
matchSpeakerList = re.findall(r"^\[?(.+?)\]?\s?[|:]\s?", translatedText)
translatedText = re.sub(r"^\[?(.+?)\]?\s?[|:]\s?", "", translatedText)
translatedText = strip_speaker_prefix(translatedText)
# Avoid Crashes
translatedText = translatedText.replace("[", "(")

View file

@ -14,6 +14,7 @@ from dotenv import load_dotenv
from retry import retry
from tqdm import tqdm
from util.translation import TranslationConfig, translateAI as sharedtranslateAI, getPricingConfig, calculateCost, getPricingConfig, calculateCost
from util.speaker_prefix import strip_speaker_prefix
# OpenAI initialization centralized in util/translation.py
@ -391,9 +392,7 @@ def searchCodes(events, pbar, jobList, filename):
translatedText = stringList[0]
# Remove speaker
matchSpeakerList = re.findall(r"^(\[.+?\]\s?[|:]\s?)\s?", translatedText)
if len(matchSpeakerList) > 0:
translatedText = translatedText.replace(matchSpeakerList[0], "")
translatedText = strip_speaker_prefix(translatedText)
# Textwrap
if FIXTEXTWRAP is True:
@ -667,9 +666,7 @@ def searchCodes(events, pbar, jobList, filename):
translatedText = list300[0]
# Remove speaker
matchSpeakerList = re.findall(r"^(\[.+?\]\s?[|:]\s?)\s?", translatedText)
if len(matchSpeakerList) > 0:
translatedText = translatedText.replace(matchSpeakerList[0], "")
translatedText = strip_speaker_prefix(translatedText)
# Textwrap
if FIXTEXTWRAP is True:

View file

@ -14,6 +14,7 @@ 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.speaker_prefix import strip_speaker_prefix
import tempfile
# OpenAI initialization centralized in util/translation.py
@ -323,9 +324,7 @@ def translateWOLF(data, translatedList, pbar, filename):
translatedList = None
# Remove speaker
matchSpeakerList = re.findall(r"^(\[.+?\]\s?[|:]\s?)\s?", translatedText)
if len(matchSpeakerList) > 0:
translatedText = translatedText.replace(matchSpeakerList[0], "")
translatedText = strip_speaker_prefix(translatedText)
# Textwrap
translatedText = dazedwrap.wrapText(translatedText, width=WIDTH)

View file

@ -11,6 +11,7 @@ from colorama import Fore
from tqdm import tqdm
import util.dazedwrap as dazedwrap
from util.speaker_prefix import strip_speaker_prefix
from util.translation import (
TranslationConfig,
calculateCost,
@ -302,10 +303,9 @@ def translateYuris(data, filename):
def stripSpeakerPrefix(translated_text):
"""Same behavior as modules/json.translateJSON: strip [Name]: prefix from TL lines."""
match = re.search(r"(^\[.+?\]\s?[|:]\s?)", translated_text)
if match:
translated_text = translated_text.replace(match.group(1), "")
else:
before_strip = translated_text
translated_text = strip_speaker_prefix(translated_text)
if translated_text == before_strip:
cjk_m = re.match(
r"^[一-龠ぁ-ゔァ-ヴーa---\uFF61-\uFF9F]+\s*[\(:]\s*",
translated_text,

View file

@ -0,0 +1,44 @@
import unittest
from util.speaker_prefix import (
SPEAKER_TAG_RE,
extract_dialogue_after_speaker,
strip_speaker_prefix,
)
class SpeakerPrefixTests(unittest.TestCase):
def test_strip_plain_speaker(self):
self.assertEqual(strip_speaker_prefix("[Kurone]: Hello"), "Hello")
def test_strip_color_coded_speaker(self):
line = r"[\C[10]Hp Drink\C[0]]: Received item!"
self.assertEqual(strip_speaker_prefix(line), "Received item!")
def test_strip_fullwidth_colon(self):
line = r"[\C[10]HPドリンク\C[0]]:をもらった!"
self.assertEqual(strip_speaker_prefix(line), "をもらった!")
def test_tag_captures_color_coded_speaker(self):
line = r"[\C[10]Hp Drink\C[0]]: 【\c[10]HPドリンク\c[0]】をもらった!"
m = SPEAKER_TAG_RE.match(line)
self.assertIsNotNone(m)
self.assertEqual(m.group(1), r"\C[10]Hp Drink\C[0]")
def test_extract_dialogue_skips_color_speaker(self):
line = r"[\C[10]Hp Drink\C[0]]: 【\c[10]HPドリンク\c[0]】をもらった!"
self.assertEqual(
extract_dialogue_after_speaker(line),
"\\c[10]HPドリンク\\c[0]】をもらった!",
)
def test_actor_variable_in_speaker(self):
line = r"[\n[1]]: Actor line"
m = SPEAKER_TAG_RE.match(line)
self.assertIsNotNone(m)
self.assertEqual(m.group(1), r"\n[1]")
self.assertEqual(extract_dialogue_after_speaker(line), "Actor line")
if __name__ == "__main__":
unittest.main()

43
util/speaker_prefix.py Normal file
View file

@ -0,0 +1,43 @@
"""Shared [Speaker]: prefix parsing for dialogue lines.
Handles RPG Maker control codes inside speaker brackets, e.g.
``[\\C[10]Hp Drink\\C[0]]: dialogue`` where inner ``[10]`` must not end the match early.
"""
from __future__ import annotations
import re
# Inner text of [Speaker] — allows \C[n], \c[n], \i[n], \n[actorId], etc.
# The control-code branch (\X[...]) keeps the inner [..] from ending the bracket early.
SPEAKER_BRACKET_INNER = r"(?:\\[A-Za-z]+\[[^\]]*\]|[^\]\n])+"
# A bare [Speaker]: / [Speaker]| / [Speaker] prefix.
_PREFIX_PATTERN = rf"^\[{SPEAKER_BRACKET_INNER}\]\s*[|:]\s*"
SPEAKER_PREFIX_RE = re.compile(_PREFIX_PATTERN, re.IGNORECASE)
SPEAKER_TAG_RE = re.compile(
rf"^\[({SPEAKER_BRACKET_INNER})\]",
re.IGNORECASE,
)
# Dialogue body after an optional [Speaker]: prefix (text.py lineTextRegex, etc.)
SPEAKER_PREFIX_OPTIONAL_RE = re.compile(
rf"(?:{_PREFIX_PATTERN})?(.+)",
re.IGNORECASE,
)
def strip_speaker_prefix(text: str) -> str:
"""Remove a leading ``[Speaker]:`` / ``[Speaker]|`` / ``[Speaker]`` prefix."""
if not text:
return text
return SPEAKER_PREFIX_RE.sub("", text, count=1)
def extract_dialogue_after_speaker(text: str) -> str | None:
"""Return dialogue body after an optional ``[Speaker]:`` prefix, or ``None``."""
if not text:
return None
m = SPEAKER_PREFIX_OPTIONAL_RE.match(text)
return m.group(1) if m else None