idk lol keep going

This commit is contained in:
dazedanon 2026-03-12 21:50:59 -05:00
parent ccdbbf6e62
commit b3981dfd35
2 changed files with 240 additions and 229 deletions

View file

@ -128,50 +128,6 @@ class _ImportWorker(QThread):
self.done.emit(0, [str(exc)])
class _ActorSubstituteWorker(QThread):
done = pyqtSignal(dict)
log = pyqtSignal(str)
def __init__(self, mode: str): # "substitute" or "restore"
super().__init__()
self.mode = mode
def run(self):
try:
from util.actor_substitutor import (
load_actor_map,
substitute_in_files,
restore_in_translated,
)
actor_map = load_actor_map()
if not actor_map:
self.done.emit({"error": "No actor map found. Make sure files/Actors.json exists."})
return
if self.mode == "substitute":
self.log.emit("Substituting \\n[X] → actor names in files/ …")
stats = substitute_in_files(actor_map=actor_map)
else:
self.log.emit("Restoring actor names → \\n[X] in translated/ …")
stats = restore_in_translated(actor_map=actor_map)
self.done.emit(stats)
except Exception as exc:
self.done.emit({"error": str(exc)})
class _SpeakerDetectWorker(QThread):
done = pyqtSignal(dict)
log = pyqtSignal(str)
def run(self):
try:
from util.speaker_detector import detect_speaker_format
self.log.emit("Scanning files/ for speaker patterns …")
result = detect_speaker_format()
self.done.emit(result)
except Exception as exc:
self.done.emit({"error": str(exc)})
class _ExportWorker(QThread):
done = pyqtSignal(int, list)
log = pyqtSignal(str)
@ -384,7 +340,6 @@ class WorkflowTab(QWidget):
self._data_path: str | None = None
self._engine: str = "MVMZ"
self._file_items: list[dict] = []
self._detect_result: dict | None = None
self._worker = None # active background QThread
# Pre-process paths (auto-populated after folder detection)
self._plugins_js_path: str = ""
@ -422,8 +377,6 @@ class WorkflowTab(QWidget):
vbox.addWidget(_make_hr())
self._build_step1(vbox)
vbox.addWidget(_make_hr())
self._build_step2(vbox)
vbox.addWidget(_make_hr())
self._build_step4(vbox)
vbox.addWidget(_make_hr())
self._build_step6(vbox)
@ -541,6 +494,60 @@ class WorkflowTab(QWidget):
# ── Copilot prompt templates ────────────────────────────────────────────
_SPEAKER_PROMPT = (
"You are helping me configure a Japanese RPGMaker MV/MZ translation tool.\n"
"\n"
"Look at a sample of the game's event files (Map*.json, CommonEvents.json, "
"Troops.json) and determine which speaker name format the game uses.\n"
"\n"
"## How dialogue commands work\n"
"\n"
"Code 101 opens the text window. Code 401 is a dialogue line. "
"Multiple 401s in a row form one message box.\n"
"\n"
"## The formats to identify\n"
"\n"
"### Format A — 101 name in param[4] (NO FLAG NEEDED, handled automatically)\n"
"The 101 code has a non-empty string at parameters[4].\n"
"Example:\n"
" { \"code\": 101, \"parameters\": [\"\", 0, 2, 2, \"るな\"] }\n"
" { \"code\": 401, \"parameters\": [\"「おはよう!\"] }\n"
"→ No flag needed. Do not set anything.\n"
"\n"
"### Format B — INLINE401SPEAKERS\n"
"The speaker name is embedded at the start of the 401 text directly before 「.\n"
"Example:\n"
" { \"code\": 401, \"parameters\": [\"エレナ「今日は晴れですね。\"] }\n"
"→ Set INLINE401SPEAKERS = True\n"
"\n"
"### Format C — FIRSTLINESPEAKERS\n"
"The first 401 in a group is a short name (<40 chars), and the next 401 "
"starts with 「 \" ( * [.\n"
"Example:\n"
" { \"code\": 401, \"parameters\": [\"アリス\"] }\n"
" { \"code\": 401, \"parameters\": [\"「こんにちは。\"] }\n"
"→ Set FIRSTLINESPEAKERS = True\n"
"\n"
"### Format D — FACENAME101\n"
"The 101 code has a face image filename in parameters[0] and parameters[4] is empty "
"or missing. The speaker name must be inferred from the face filename.\n"
"Example:\n"
" { \"code\": 101, \"parameters\": [\"face_alice\", 0, 0, 2, \"\"] }\n"
"→ Set FACENAME101 = True\n"
"\n"
"### Always-on (NO FLAG NEEDED, handled automatically)\n"
" - \\\\n<Name> or \\\\k<Name> codes embedded in 401 text\n"
" - 【Name】 alone on a line or 【Name】dialogue on the same line\n"
"\n"
"---\n"
"\n"
"Please examine a sample of the event commands in the game files attached and tell me:\n"
" 1. Which format(s) are present\n"
" 2. Exactly which flags to set True (only list flags that should be True):\n"
" INLINE401SPEAKERS, FIRSTLINESPEAKERS, FACENAME101\n"
" 3. A short concrete example from the files confirming the pattern\n"
)
_PROMPT_GLOSSARY = (
"You are helping me build a complete translation glossary for a Japanese RPGMaker game.\n"
"\n"
@ -883,7 +890,7 @@ class WorkflowTab(QWidget):
glossary_row.addWidget(glossary_label, 1)
copy_glossary_btn = _make_btn("📋 Copy Prompt", "#555")
copy_glossary_btn.setToolTip("Copy the full glossary discovery prompt to clipboard")
copy_glossary_btn.clicked.connect(lambda: self._copy_to_clipboard(self._PROMPT_GLOSSARY, "Glossary prompt copied."))
copy_glossary_btn.clicked.connect(self._copy_glossary_prompt)
glossary_row.addWidget(copy_glossary_btn)
pb_inner.addLayout(glossary_row)
@ -927,79 +934,64 @@ class WorkflowTab(QWidget):
# ── Step 2: Actor Variables ─────────────────────────────────────────────
def _build_step2(self, layout: QVBoxLayout):
layout.addWidget(_make_section_label("Step 4 — Actor Variable Substitution"))
hint = QLabel(
"Replaces \\n[X] RPGMaker name variables with actual actor names "
"so the AI has proper context. After translation the variables are "
"restored — rename-able protagonists continue to work at runtime."
)
hint.setWordWrap(True)
hint.setStyleSheet("color:#888;font-size:10px;padding-bottom:4px;")
layout.addWidget(hint)
self.actor_status = QLabel("Click Scan to preview substitutions.")
self.actor_status.setWordWrap(True)
self.actor_status.setStyleSheet("color:#aaa;font-size:10px;padding:4px 0;")
layout.addWidget(self.actor_status)
row = QHBoxLayout()
scan_actors = _make_btn("🔍 Scan \\n[X] usage", "#555")
scan_actors.setToolTip("Scan files/ and show which actor IDs are referenced")
scan_actors.clicked.connect(self._scan_actor_vars)
row.addWidget(scan_actors)
row.addStretch()
layout.addLayout(row)
row2 = QHBoxLayout()
self.sub_btn = _make_btn("▶ Substitute \\n[X] → Names (pre-translation)", "#007acc")
self.sub_btn.setToolTip("Modifies files/ in-place. Run before translating.")
self.sub_btn.clicked.connect(self._substitute_actors)
row2.addWidget(self.sub_btn)
self.restore_btn = _make_btn("↩ Restore Names → \\n[X] (post-translation)", "#7a4a7a")
self.restore_btn.setToolTip("Modifies translated/ in-place. Run after translating.")
self.restore_btn.clicked.connect(self._restore_actors)
row2.addWidget(self.restore_btn)
layout.addLayout(row2)
# ── Step 3: Speaker Detection ───────────────────────────────────────────
def _build_step3(self, layout: QVBoxLayout):
layout.addWidget(_make_section_label("Step 2 — Speaker Format Detection"))
hint = QLabel(
"Scans map files to determine how speaker names are embedded in dialogue "
"and automatically sets the correct INLINE401SPEAKERS / FIRSTLINESPEAKERS "
"/ FACENAME101 flags."
"Copy the prompt below, open Copilot (or any AI), attach a few of the game's "
"Map*.json / CommonEvents.json files, paste the prompt, and ask it which "
"speaker flags to enable. Then tick the matching boxes and click Apply."
)
hint.setWordWrap(True)
hint.setStyleSheet("color:#888;font-size:10px;padding-bottom:4px;")
hint.setStyleSheet("color:#888;font-size:10px;padding-bottom:6px;")
layout.addWidget(hint)
self.speaker_result = QLabel("Run the scan to auto-detect speaker format.")
self.speaker_result.setWordWrap(True)
self.speaker_result.setStyleSheet(
"color:#aaa;font-size:10px;padding:4px 0;"
"background:#252526;border:1px solid #444;padding:6px;border-radius:2px;"
copy_row = QHBoxLayout()
copy_btn = _make_btn("📋 Copy Prompt for Copilot", "#555")
copy_btn.setToolTip("Copies a prompt explaining all speaker formats — paste into Copilot with game files")
copy_btn.clicked.connect(self._copy_speaker_prompt)
copy_row.addWidget(copy_btn)
copy_row.addStretch()
layout.addLayout(copy_row)
# ---- Flag checkboxes ------------------------------------------------
_cb_style = "color:#ccc;font-size:10px;"
cb_box = QGroupBox("Speaker flags")
cb_box.setStyleSheet(
"QGroupBox{color:#aaa;border:1px solid #3a3a3a;border-radius:3px;"
"margin-top:6px;font-size:10px;padding:6px;background:#1e1e1e;}"
"QGroupBox::title{padding:0 4px;}"
)
layout.addWidget(self.speaker_result)
cb_inner = QVBoxLayout(cb_box)
cb_inner.setSpacing(3)
row = QHBoxLayout()
scan_spk = _make_btn("🔍 Scan Speaker Format", "#555")
scan_spk.clicked.connect(self._detect_speakers)
row.addWidget(scan_spk)
self.spk_inline_cb = QCheckBox("INLINE401SPEAKERS — speaker name inline before 「 in the 401 text")
self.spk_inline_cb.setStyleSheet(_cb_style)
cb_inner.addWidget(self.spk_inline_cb)
self.apply_speaker_btn = _make_btn("✔ Apply to RPGMaker Settings", "#3a7a3a")
self.apply_speaker_btn.setEnabled(False)
self.apply_speaker_btn.clicked.connect(self._apply_speaker_settings)
row.addWidget(self.apply_speaker_btn)
row.addStretch()
layout.addLayout(row)
self.spk_firstline_cb = QCheckBox("FIRSTLINESPEAKERS — first 401 line is a short speaker name")
self.spk_firstline_cb.setStyleSheet(_cb_style)
cb_inner.addWidget(self.spk_firstline_cb)
self.spk_face_cb = QCheckBox("FACENAME101 — speaker inferred from 101 face-image filename")
self.spk_face_cb.setStyleSheet(_cb_style)
cb_inner.addWidget(self.spk_face_cb)
apply_row = QHBoxLayout()
apply_spk = _make_btn("✔ Apply", "#3a7a3a")
apply_spk.setToolTip("Write the selected flags to RPGMaker settings")
apply_spk.clicked.connect(self._apply_speaker_flags)
apply_row.addStretch()
apply_row.addWidget(apply_spk)
cb_inner.addLayout(apply_row)
layout.addWidget(cb_box)
# ── Step 4: Translation ─────────────────────────────────────────────────
def _build_step4(self, layout: QVBoxLayout):
layout.addWidget(_make_section_label("Step 5 — Translation"))
layout.addWidget(_make_section_label("Step 4 — Translation"))
p1_box = QGroupBox("Phase 1 Safe Codes (dialogue + choices)")
p1_box.setStyleSheet(
@ -1056,7 +1048,7 @@ class WorkflowTab(QWidget):
# ── Step 6: Export ──────────────────────────────────────────────────────
def _build_step6(self, layout: QVBoxLayout):
layout.addWidget(_make_section_label("Step 6 — Export to Game"))
layout.addWidget(_make_section_label("Step 5 — Export to Game"))
hint = QLabel(
"Copy all files from translated/ back into the game's data folder "
"to patch the game in-place."
@ -1246,6 +1238,47 @@ class WorkflowTab(QWidget):
_BASE_SEPARATOR = "# ── Base Vocabulary (auto-appended from vocab_base.txt — do not edit below) ──\n"
def _copy_glossary_prompt(self):
"""Build the glossary prompt, injecting any already-parsed speakers."""
prompt = self._PROMPT_GLOSSARY
# Extract # Speakers entries from current vocab.txt
speakers = []
try:
vocab_text = Path("vocab.txt").read_text(encoding="utf-8")
in_speakers = False
for line in vocab_text.splitlines():
stripped = line.strip()
if stripped == "# Speakers":
in_speakers = True
continue
if in_speakers:
if stripped.startswith("#"):
break # next section
if stripped:
speakers.append(stripped)
except Exception:
pass
if speakers:
speaker_block = (
"\n\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
"ALREADY-IDENTIFIED SPEAKERS\n"
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
"The speaker parser has already found these names in the game files.\n"
"For genuine named characters in this list, add them to # Game Characters\n"
"with gender, role, and speech register.\n"
"Skip entries that are clearly NOT real characters — generic labels (e.g. 男, 女),\n"
"placeholders (e.g. , ???), or system strings.\n"
"```\n"
+ "\n".join(speakers)
+ "\n```"
)
prompt = prompt + speaker_block
self._copy_to_clipboard(prompt, "Glossary prompt copied."
+ (f" ({len(speakers)} speaker(s) included.)" if speakers else ""))
def _reload_vocab(self):
vocab_path = Path("vocab.txt")
try:
@ -1276,139 +1309,36 @@ class WorkflowTab(QWidget):
# Step 2 Actor substitution
# ─────────────────────────────────────────────────────────────────────────
def _scan_actor_vars(self):
try:
from util.actor_substitutor import scan_variables, load_actor_map
counts = scan_variables("files")
actor_map = load_actor_map()
if not counts:
self.actor_status.setText("No \\n[X] variables found in files/")
return
lines = []
for aid, cnt in sorted(counts.items()):
name = actor_map.get(aid, "???")
lines.append(f" \\n[{aid}] → {name} ({cnt} occurrence(s))")
self.actor_status.setText("\n".join(lines))
self._log("Actor variable scan complete.")
except Exception as exc:
self._log(f"❌ Scan error: {exc}")
def _substitute_actors(self):
reply = QMessageBox.question(
self,
"Substitute Actor Variables",
"This will modify files/ in-place, replacing \\n[X] codes with actor names.\n\n"
"Back up files/ first if needed. Continue?",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.Yes,
)
if reply != QMessageBox.Yes:
return
self.sub_btn.setEnabled(False)
w = _ActorSubstituteWorker("substitute")
w.log.connect(self._log)
w.done.connect(self._on_actor_done)
self._worker = w
w.start()
def _restore_actors(self):
reply = QMessageBox.question(
self,
"Restore Actor Variables",
"This will modify translated/ in-place, replacing actor names back with \\n[X] codes.\n\n"
"Continue?",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.Yes,
)
if reply != QMessageBox.Yes:
return
self.restore_btn.setEnabled(False)
w = _ActorSubstituteWorker("restore")
w.log.connect(self._log)
w.done.connect(self._on_actor_done)
self._worker = w
w.start()
def _on_actor_done(self, stats: dict):
self.sub_btn.setEnabled(True)
self.restore_btn.setEnabled(True)
if "error" in stats:
self._log(f"{stats['error']}")
return
mods = stats.get("files_modified", 0)
total = stats.get("total_substitutions", 0)
found = stats.get("variables_found", {})
errors = stats.get("errors", [])
self._log(
f"{total} substitution(s) across {mods} file(s). "
f"Actors: {', '.join(f'ID {k}={v}' for k, v in found.items()) or 'none'}"
)
for e in errors:
self._log(f"{e}")
# ─────────────────────────────────────────────────────────────────────────
# Step 3 Speaker detection
# ─────────────────────────────────────────────────────────────────────────
def _detect_speakers(self):
self.apply_speaker_btn.setEnabled(False)
self.speaker_result.setText("Scanning…")
w = _SpeakerDetectWorker()
w.log.connect(self._log)
w.done.connect(self._on_speaker_done)
self._worker = w
w.start()
def _copy_speaker_prompt(self):
QApplication.clipboard().setText(self._SPEAKER_PROMPT)
self._log("Speaker format prompt copied to clipboard.")
def _on_speaker_done(self, result: dict):
if "error" in result:
self.speaker_result.setText(f"❌ Error: {result['error']}")
self._log(f"❌ Speaker detection error: {result['error']}")
return
self._detect_result = result
scores = result.get("scores", {})
best = result.get("best_mode", "NONE")
conf = result.get("confidence", "low")
note = result.get("note", "")
conf_color = {"high": "#4ec9b0", "medium": "#dcdcaa", "low": "#f48771"}.get(conf, "#ccc")
score_str = " | ".join(f"{k}: {v}" for k, v in scores.items())
self.speaker_result.setText(
f"Best mode: {best} ({conf} confidence)\n"
f"Scores — {score_str}\n"
f"{note}"
)
self._log(f"Speaker detection: {best} ({conf}) — {note}")
if best != "NONE":
self.apply_speaker_btn.setEnabled(True)
def _apply_speaker_settings(self):
if not self._detect_result:
return
cfg = self._detect_result.get("recommended_config", {})
def _apply_speaker_flags(self):
cfg = {
"INLINE401SPEAKERS": self.spk_inline_cb.isChecked(),
"FIRSTLINESPEAKERS": self.spk_firstline_cb.isChecked(),
"FACENAME101": self.spk_face_cb.isChecked(),
}
try:
from gui.config_integration import ConfigIntegration
ci = ConfigIntegration()
ci.update_rpgmaker_config(cfg)
ConfigIntegration().update_rpgmaker_config(cfg)
self._log(
f"✅ Applied speaker settings: "
"✅ Speaker flags applied: "
+ ", ".join(f"{k}={v}" for k, v in cfg.items())
)
# Refresh the RPGMaker tab if accessible
self._refresh_rpgmaker_tab(cfg)
try:
if self.parent_window and hasattr(self.parent_window, "config_tab"):
ct = self.parent_window.config_tab
if hasattr(ct, "rpgmaker_tab") and ct.rpgmaker_tab:
ct.rpgmaker_tab.set_config(cfg)
except Exception:
pass
except Exception as exc:
self._log(f"❌ Could not apply settings: {exc}")
def _refresh_rpgmaker_tab(self, cfg: dict):
"""Push the new config to the RPGMaker tab widget so it refreshes live."""
try:
if self.parent_window and hasattr(self.parent_window, "config_tab"):
ct = self.parent_window.config_tab
if hasattr(ct, "rpgmaker_tab") and ct.rpgmaker_tab:
ct.rpgmaker_tab.set_config(cfg)
except Exception:
pass
self._log(f"❌ Could not apply speaker flags: {exc}")
# ─────────────────────────────────────────────────────────────────────────
# Step 4 Translation phases

View file

@ -43,6 +43,11 @@ _speakerCache = {}
_speakerCacheLock = threading.Lock()
SPEAKER_COLLECTED = [] # Original speaker names collected during parse mode (untranslated)
# Actor variable substitution (\n[X] -> name before AI, name -> \n[X] after)
_ACTOR_MAP_CACHE: dict | None = None
_ACTOR_MAP_CACHE_LOCK = threading.Lock()
_VAR_ACTOR_RE = re.compile(r"\\n\[(\d+)\]", re.IGNORECASE)
# Regex - Need to change this if you want to translate from/to other languages. Default is Japanese Regex
LANGREGEX = r"[\u3000-\u303F\u3040-\u309F\u30A0-\u30FF\u31F0-\u31FF\u3400-\u4DBF\u4E00-\u9FFF\uF900-\uFAFF\uFF61-\uFF9F]+"
@ -4184,6 +4189,40 @@ def getSpeaker(speaker: str):
NAMESLIST.append([speaker, translated])
return [translated, response[1]]
def _get_actor_map() -> dict:
"""Lazily load actor_id -> name from Actors.json (translated/ preferred)."""
global _ACTOR_MAP_CACHE
with _ACTOR_MAP_CACHE_LOCK:
if _ACTOR_MAP_CACHE is not None:
return _ACTOR_MAP_CACHE
for candidate in (Path("translated/Actors.json"), Path("files/Actors.json")):
if candidate.is_file():
try:
data = json.loads(candidate.read_text(encoding="utf-8-sig"))
m: dict = {}
for entry in data:
if not entry or not isinstance(entry, dict):
continue
aid = entry.get("id")
name = (entry.get("name") or "").strip()
if aid is not None and name:
m[int(aid)] = name
if m:
_ACTOR_MAP_CACHE = m
return m
except Exception:
continue
_ACTOR_MAP_CACHE = {}
return {}
def resetActorMapCache():
"""Invalidate the cached actor map so it reloads on next use."""
global _ACTOR_MAP_CACHE
with _ACTOR_MAP_CACHE_LOCK:
_ACTOR_MAP_CACHE = None
def translateAI(text, history, fullPromptFlag):
"""
Legacy wrapper function for the new shared translation utility.
@ -4221,7 +4260,31 @@ def translateAI(text, history, fullPromptFlag):
# Return original payload and zero tokens so totals aren't affected
return [text, [0, 0]]
return sharedtranslateAI(
# ── Actor variable substitution ──────────────────────────────────────────
# Replace \n[X] codes with actor names before sending to AI so the model
# sees real character names. Track what was swapped so we can restore
# the variable codes in the translated output.
actor_map = _get_actor_map()
reverse: dict[str, str] = {} # lower_name -> "\\n[X]"
def _sub(s: str) -> str:
if not isinstance(s, str) or not actor_map:
return s
def _repl(m: re.Match) -> str:
aid = int(m.group(1))
name = actor_map.get(aid)
if name:
reverse[name.lower()] = f"\\n[{aid}]"
return name
return m.group(0)
return _VAR_ACTOR_RE.sub(_repl, s)
if isinstance(text, list):
text = [_sub(s) for s in text]
else:
text = _sub(text)
result = sharedtranslateAI(
text=text,
history=history,
fullPromptFlag=fullPromptFlag,
@ -4232,6 +4295,24 @@ def translateAI(text, history, fullPromptFlag):
mismatchList=MISMATCH
)
# ── Restore \n[X] codes in translated output ───────────────────────────
if reverse:
_restore_pat = re.compile(
"|".join(re.escape(n) for n in sorted(reverse, key=len, reverse=True)),
re.IGNORECASE,
)
def _restore(s: str) -> str:
if not isinstance(s, str):
return s
return _restore_pat.sub(lambda m: reverse[m.group(0).lower()], s)
if isinstance(result[0], list):
result[0] = [_restore(s) for s in result[0]]
elif isinstance(result[0], str):
result[0] = _restore(result[0])
return result
def resetSpeakerState():
"""Clear all speaker-related globals so a fresh run doesn't carry over stale data."""
global NAMESLIST, SPEAKER_COLLECTED