feat(translation): default to batch only for native Claude
This commit is contained in:
parent
5afd2c0fee
commit
4f1fb7bce7
7 changed files with 150 additions and 23 deletions
|
|
@ -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}")
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue