Fix inline

This commit is contained in:
dazedanon 2026-03-16 00:25:56 -05:00
parent 6248752366
commit 039370f4e2
3 changed files with 111 additions and 91 deletions

View file

@ -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/."""

View file

@ -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 = ""

View file

@ -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+FF61U+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