feat(translation): default to batch only for native Claude

This commit is contained in:
DazedAnon 2026-07-27 16:59:11 -05:00
parent 5afd2c0fee
commit 4f1fb7bce7
7 changed files with 150 additions and 23 deletions

View file

@ -1180,6 +1180,10 @@ class DazedMTLGUI(QMainWindow):
config = self.config_tab.get_config()
font_scale = config.get("font_scale", 1.0)
self.apply_font_scaling(font_scale)
for tab in (self.translation_tab, self.workflow_tab, self.wolf_workflow_tab):
refresh_mode = getattr(tab, "refresh_default_translation_mode", None)
if callable(refresh_mode):
refresh_mode()
except Exception as e:
print(f"Warning: Could not apply configuration changes: {e}")

View file

@ -22,7 +22,7 @@ import re
from importlib import import_module
from colorama import Fore
from tqdm import tqdm
from dotenv import load_dotenv
from dotenv import dotenv_values, load_dotenv
from PyQt5.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QGroupBox,
QTextEdit, QMessageBox, QListWidget, QListWidgetItem,
@ -34,7 +34,7 @@ from PyQt5.QtCore import Qt, QTimer, pyqtSignal, QThread, QMutex, QProcess, QEve
from PyQt5.QtGui import QFont, QColor, QBrush
from gui.log_viewer import LogViewer
from gui import qt_icons
from util.paths import APP_NAME, ORG_NAME
from util.paths import APP_NAME, ORG_NAME, PROJECT_ROOT
def _strip_ansi(text):
@ -75,6 +75,22 @@ BATCH_COLLECT_LIVE_CHARGE_NOTE = (
"only after you confirm the estimate."
)
_CONFIG_UNSET = object()
def default_translation_mode(model=_CONFIG_UNSET, api_url=_CONFIG_UNSET) -> str:
"""Choose Batch only when the configured model supports native Claude batches."""
if model is _CONFIG_UNSET or api_url is _CONFIG_UNSET:
env = dotenv_values(PROJECT_ROOT / ".env") if (PROJECT_ROOT / ".env").exists() else {}
if model is _CONFIG_UNSET:
model = env.get("model", os.getenv("model", ""))
if api_url is _CONFIG_UNSET:
api_url = env.get("api", os.getenv("api", ""))
from util.translation import isClaudeNative
return BATCH_MODE_LABEL if isClaudeNative(str(model or ""), api_url) else "Translate"
TRANSLATION_MODULE_SPECS = (
("RPG Maker MV/MZ", (".json",), "modules.rpgmakermvmz", "handleMVMZ"),
@ -940,6 +956,8 @@ class TranslationTab(QWidget):
self._batch_ui_phase = None
self._batch_consume_started = False
self._batch_tab_index = -1
self._mode_user_selected = False
self._last_default_translation_mode = None
self.setup_ui()
self.setup_module_list()
@ -1627,6 +1645,7 @@ class TranslationTab(QWidget):
self.mode_combo.addItem(BATCH_MODE_LABEL)
self.mode_combo.setFixedWidth(300)
self.mode_combo.currentTextChanged.connect(self._on_mode_changed)
self.mode_combo.activated.connect(self._mark_mode_user_selected)
trans_form.addRow(mode_label, self.mode_combo)
self.batch_mode_note = QLabel(
@ -1662,7 +1681,7 @@ class TranslationTab(QWidget):
button_layout.addStretch()
layout.addLayout(button_layout)
self.mode_combo.setCurrentIndex(self.mode_combo.findText(BATCH_MODE_LABEL))
self.refresh_default_translation_mode(force=True)
left_widget.setLayout(layout)
# Right side - translation history log viewer
@ -1748,8 +1767,8 @@ class TranslationTab(QWidget):
if index >= 0:
self.mode_combo.setCurrentIndex(index)
else:
batch_idx = self.mode_combo.findText(BATCH_MODE_LABEL)
self.mode_combo.setCurrentIndex(batch_idx if batch_idx >= 0 else 0)
default_idx = self.mode_combo.findText(default_translation_mode())
self.mode_combo.setCurrentIndex(default_idx if default_idx >= 0 else 0)
# Refresh file list to show only files matching the selected module's extensions
self.refresh_file_lists()
@ -1843,6 +1862,20 @@ class TranslationTab(QWidget):
elif mode_text == "Parse Speakers":
self.translate_button.setText("Parse Speakers")
def _mark_mode_user_selected(self, _index: int):
self._mode_user_selected = True
def refresh_default_translation_mode(self, force=False):
"""Refresh the provider-aware default without overriding an active choice."""
default_mode = default_translation_mode()
if not force and default_mode == self._last_default_translation_mode:
return
self._last_default_translation_mode = default_mode
if default_mode == "Translate" or force or not self._mode_user_selected:
index = self.mode_combo.findText(default_mode)
if index >= 0:
self.mode_combo.setCurrentIndex(index)
def _switch_progress_tab(self, index):
"""Switch Batch/Files views; index 0 = batch overview, 1 = per-file list."""
self._batch_tab_index = index if self.progress_tab_row.isVisible() else -1

View file

@ -83,6 +83,7 @@ from gui.translation_tab import (
BATCH_COLLECT_LIVE_CHARGE_NOTE,
BATCH_MODE_BENEFIT_NOTE,
BATCH_MODE_LABEL,
default_translation_mode,
)
from gui.workflow_tab import (
_GAMEUPDATE_COPY_SKIP_NAMES,
@ -186,6 +187,9 @@ class WolfWorkflowTab(QWidget):
self._pending_import_signature: tuple[str, ...] | None = None
self._syncing_file_checks: bool = False
self._gameupdate_path: str = ""
self._tl_mode_user_selected = False
self._last_default_translation_mode = None
self._tl_mode_combos = []
self._init_ui()
@ -2182,17 +2186,28 @@ class WolfWorkflowTab(QWidget):
lbl = QLabel("Translation mode:")
lbl.setStyleSheet("color:#cccccc;font-size:12px;font-weight:bold;background:transparent;")
self._tl_mode_combo = QComboBox()
self._tl_mode_combos.append(self._tl_mode_combo)
self._tl_mode_combo.addItem(_TL_NORMAL_LABEL)
self._tl_mode_combo.addItem(BATCH_MODE_LABEL)
self._tl_mode_combo.setFixedWidth(220)
self._tl_mode_combo.setToolTip(
"Normal translates live; Batch uses the Anthropic Batches API (~50% cheaper, Claude only)."
)
saved = self._setting("tl_mode", BATCH_MODE_LABEL) or BATCH_MODE_LABEL
default_mode = default_translation_mode()
self._last_default_translation_mode = default_mode
automatic = BATCH_MODE_LABEL if default_mode == BATCH_MODE_LABEL else _TL_NORMAL_LABEL
saved = self._setting("tl_mode", None)
self._tl_mode_user_selected = saved in (_TL_NORMAL_LABEL, BATCH_MODE_LABEL)
if saved not in (_TL_NORMAL_LABEL, BATCH_MODE_LABEL):
saved = automatic
elif saved == BATCH_MODE_LABEL and automatic != BATCH_MODE_LABEL:
saved = automatic
self._tl_mode_user_selected = False
idx = self._tl_mode_combo.findText(str(saved))
if idx >= 0:
self._tl_mode_combo.setCurrentIndex(idx)
self._tl_mode_combo.currentTextChanged.connect(self._on_tl_mode_changed)
self._tl_mode_combo.activated.connect(self._mark_tl_mode_selected)
row.addWidget(lbl)
row.addWidget(self._tl_mode_combo)
row.addStretch()
@ -2202,21 +2217,40 @@ class WolfWorkflowTab(QWidget):
self._batch_note.setWordWrap(True)
self._batch_note.setStyleSheet("color:#8fbc8f;font-size:11px;background:transparent;")
layout.addWidget(self._batch_note)
self._on_tl_mode_changed(self._tl_mode_combo.currentText())
self._on_tl_mode_changed(self._tl_mode_combo.currentText(), save=False)
def _on_tl_mode_changed(self, mode_text: str):
def _on_tl_mode_changed(self, mode_text: str, save=True):
is_batch = mode_text == BATCH_MODE_LABEL
if hasattr(self, "_batch_note"):
self._batch_note.setVisible(is_batch)
self._save_setting("tl_mode", mode_text)
if save:
self._save_setting("tl_mode", mode_text)
def _workflow_mode_text(self) -> str:
"""Map the selector to the Translation tab's own mode label."""
saved = self._setting("tl_mode", BATCH_MODE_LABEL) or BATCH_MODE_LABEL
if saved == BATCH_MODE_LABEL:
automatic = BATCH_MODE_LABEL if default_translation_mode() == BATCH_MODE_LABEL else _TL_NORMAL_LABEL
selected = self._setting("tl_mode", automatic) or automatic
if selected == BATCH_MODE_LABEL and automatic == BATCH_MODE_LABEL:
return BATCH_MODE_LABEL
return "Translate"
def _mark_tl_mode_selected(self, _index: int):
self._tl_mode_user_selected = True
def refresh_default_translation_mode(self, force=False):
"""Apply the provider-aware mode after configuration changes."""
default_mode = default_translation_mode()
if not force and default_mode == self._last_default_translation_mode:
return
self._last_default_translation_mode = default_mode
mode = BATCH_MODE_LABEL if default_mode == BATCH_MODE_LABEL else _TL_NORMAL_LABEL
if default_mode == "Translate" or force or not self._tl_mode_user_selected:
for combo in self._tl_mode_combos:
index = combo.findText(mode)
if index >= 0:
combo.setCurrentIndex(index)
self._save_setting("tl_mode", mode)
def _navigate_to_translation(
self,
only: str | None = None,

View file

@ -63,6 +63,7 @@ from gui.translation_tab import (
BATCH_MODE_LABEL,
BATCH_MODE_BENEFIT_NOTE,
BATCH_COLLECT_LIVE_CHARGE_NOTE,
default_translation_mode,
)
WORKFLOW_TL_NORMAL_LABEL = "Normal Translate"
@ -778,6 +779,8 @@ class WorkflowTab(QWidget):
self._last_import_signature: tuple[str, ...] | None = None
self._pending_import_signature: tuple[str, ...] | None = None
self._release_zip_btn: QPushButton | None = None
self._tl_mode_user_selected = False
self._last_default_translation_mode = None
self._init_ui()
@ -1654,6 +1657,7 @@ class WorkflowTab(QWidget):
"Batch uses the Anthropic Batches API (50% off, Claude only)."
)
self._tl_mode_combo.currentTextChanged.connect(self._on_workflow_tl_mode_changed)
self._tl_mode_combo.activated.connect(self._mark_workflow_tl_mode_selected)
mode_row.addWidget(mode_lbl)
mode_row.addWidget(self._tl_mode_combo)
mode_row.addStretch()
@ -1672,9 +1676,7 @@ class WorkflowTab(QWidget):
mode_inner.addWidget(self._batch_mode_warning)
layout.addWidget(mode_box)
batch_idx = self._tl_mode_combo.findText(BATCH_MODE_LABEL)
if batch_idx >= 0:
self._tl_mode_combo.setCurrentIndex(batch_idx)
self.refresh_default_translation_mode(force=True)
self._on_workflow_tl_mode_changed(self._tl_mode_combo.currentText())
def _on_workflow_tl_mode_changed(self, mode_text: str):
@ -1694,6 +1696,23 @@ class WorkflowTab(QWidget):
combo = getattr(self, "_tl_mode_combo", None)
return combo is not None and combo.currentText() == BATCH_MODE_LABEL
def _mark_workflow_tl_mode_selected(self, _index: int):
self._tl_mode_user_selected = True
def refresh_default_translation_mode(self, force=False):
"""Refresh the workflow mode when the configured provider changes."""
default_mode = default_translation_mode()
if not force and default_mode == self._last_default_translation_mode:
return
self._last_default_translation_mode = default_mode
workflow_mode = (
BATCH_MODE_LABEL if default_mode == BATCH_MODE_LABEL else WORKFLOW_TL_NORMAL_LABEL
)
if default_mode == "Translate" or force or not self._tl_mode_user_selected:
index = self._tl_mode_combo.findText(workflow_mode)
if index >= 0:
self._tl_mode_combo.setCurrentIndex(index)
def _workflow_mode_text(self) -> str:
return BATCH_MODE_LABEL if self._workflow_batch_mode() else "Translate"
@ -1853,7 +1872,9 @@ class WorkflowTab(QWidget):
self._add_step_header(layout, "Step 4 — TL Phase 2", 4)
self._step5_mode_hint = QLabel(f"Translation mode: {BATCH_MODE_LABEL}")
initial_mode = getattr(self, "_tl_mode_combo", None)
initial_mode_text = initial_mode.currentText() if initial_mode is not None else WORKFLOW_TL_NORMAL_LABEL
self._step5_mode_hint = QLabel(f"Translation mode: {initial_mode_text}")
self._step5_mode_hint.setStyleSheet("color:#8fbc8f;font-size:12px;margin-bottom:4px;")
layout.addWidget(self._step5_mode_hint)
if hasattr(self, "_tl_mode_combo"):

View file

@ -148,13 +148,13 @@ TLSYSTEMSWITCHES = False
JOIN408 = False
# Dialogue / Scroll / Choices (Main Codes)
CODE101 = False
CODE401 = False
CODE405 = False
CODE102 = False
CODE101 = True
CODE401 = True
CODE405 = True
CODE102 = True
# Optional
CODE408 = False
CODE408 = True
# Variables
CODE122 = False

View file

@ -11,7 +11,12 @@ os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
try:
from PyQt5.QtWidgets import QApplication
from gui.translation_tab import TRANSLATION_MODULE_SPECS, TranslationTab
from gui.translation_tab import (
BATCH_MODE_LABEL,
TRANSLATION_MODULE_SPECS,
TranslationTab,
default_translation_mode,
)
_HAS_QT = True
except Exception: # pragma: no cover - PyQt5 not installed
@ -53,6 +58,36 @@ class TranslationEngineDropdownTests(unittest.TestCase):
self.assertEqual(actual, expected)
self.assertTrue(all(callable(module[2]) for module in tab.modules))
def test_normal_translate_is_default_for_non_claude_models(self) -> None:
self.assertEqual(
default_translation_mode("gpt-5.2", "https://api.openai.com/v1"),
"Translate",
)
def test_batch_translate_is_default_for_native_claude(self) -> None:
self.assertEqual(
default_translation_mode("claude-sonnet-4-6", "https://api.anthropic.com/v1"),
BATCH_MODE_LABEL,
)
def test_claude_through_custom_provider_defaults_to_normal(self) -> None:
self.assertEqual(
default_translation_mode("claude-sonnet-4-6", "https://openrouter.ai/api/v1"),
"Translate",
)
def test_translation_tab_applies_detected_default(self) -> None:
with patch("gui.translation_tab.default_translation_mode", return_value="Translate"):
normal_tab = TranslationTab()
self.assertEqual(normal_tab.mode_combo.currentText(), "Translate")
with patch(
"gui.translation_tab.default_translation_mode",
return_value=BATCH_MODE_LABEL,
):
claude_tab = TranslationTab()
self.assertEqual(claude_tab.mode_combo.currentText(), BATCH_MODE_LABEL)
if __name__ == "__main__":
unittest.main()

View file

@ -102,7 +102,7 @@ def isClaudeModel(model):
return bool(model) and any(x in model.lower() for x in ("claude", "sonnet", "haiku", "opus"))
def isClaudeNative(model):
def isClaudeNative(model, api_url=None):
"""True when this model routes to the native Anthropic SDK.
Mirrors the routing check in translateText: the model must look like Claude
@ -110,7 +110,7 @@ def isClaudeNative(model):
other custom URL (e.g. DeepSeek, OpenAI proxy) uses the OpenAI-compatible
path even for Claude-named models.
"""
live_api = os.getenv("api", "").strip()
live_api = os.getenv("api", "").strip() if api_url is None else str(api_url).strip()
return isClaudeModel(model) and (not live_api or "anthropic" in live_api.lower())