Merge branch 'main' of ssh://ssh.gitgud.io/kaosss/DazedMTLTool

This commit is contained in:
no 2026-06-12 21:15:11 +03:00
commit 060f02b72c
14 changed files with 3163 additions and 48 deletions

View file

@ -9,20 +9,21 @@
# - Leave this variable out to use the model's default setting.
GEMINI_THINKING_BUDGET=
# Set to "gemini" to use the Gemini API or "openai" for OpenAI (If empty it will default to openai.)
# Set to "gemini" to use the Gemini API or "openai" for OpenAI-compatible APIs (If empty it will default to openai.)
API_PROVIDER=openai
#API link, leave blank to use OpenAI API
# API URL, leave blank to use OpenAI API.
# Nvidia example: "https://integrate.api.nvidia.com/v1/"
api=""
#API key
# API key
key=""
#Oranization key, make something up for self hosted or other API
#Oranization key, make something up for self hosted or other API. If using Nvidia API, leave it blank or it can get wonky
organization=""
#LLM model name, use gpt-3.5-turbo-1106 or gpt-3.5-turbo or gpt-4-1106-preview for OpenAI API
#For text generation webui use gpt-3.5-turbo, for other API's consult their documentation
# LLM model name.
# Default below works for OpenAI; for Gemini/Nvidia set your provider model name.
model="gpt-4.1"
#The language to translate TO, Don't forget to change the prompt
@ -32,7 +33,10 @@ language="English"
timeout="120"
#The number of files to translate at the same time, 1 recommended for free or self hosted API or gpt-4
fileThreads="1"
fileThreads="5"
#Concurrent API calls per file (threads per file), 1 recommended for free or self hosted API or gpt-4
threads="1"
#The wordwrap of dialogue text
width="60"
@ -56,4 +60,8 @@ output_cost= 0.002
batchsize="10"
# Frequency penalty - adjust according to your needs
frequency_penalty= 0.2
frequency_penalty= 0.2
# TL Inspector (Step 8 playtest) — editor opened when clicking a source line in-game
# tlEditorCmd: 'auto' scans for VS Code / Cursor, or set an absolute path to any editor exe
tlEditorCmd='auto'

2
.gitignore vendored
View file

@ -1,3 +1,4 @@
.env
*.tmp
*.json
@ -19,3 +20,4 @@ __pycache__
!prompt.txt
!vocab_base.txt
!requirements.txt
!util/tl_inspector/TLInspector.js

View file

@ -5,6 +5,7 @@ An AI-powered game translation tool with a GUI. Translate RPG Maker, Ren'Py, Tyr
## Credits
- **[Sinflower](https://github.com/Sinflower)** — [RV2JSON](https://github.com/Sinflower/RV2JSON) — enables RPGMaker Ace games to be translated the same way as MV/MZ by converting rvdata2 files to JSON and back.
- **Sakura & Kao_SSS** — TL Inspector (`util/tl_inspector/`) — in-game translation source inspector and live-edit plugin for RPG Maker MV/MZ playtesting.
## Table of Contents
@ -109,9 +110,11 @@ This means Python wasn't added to your PATH. You have two options:
1. Inside the tool folder, find `.env.example` and make a copy of it named `.env`.
2. Open `.env` in any text editor (Notepad works fine) and fill in your API details:
- `api` — Your API base URL (for Nvidia use `https://integrate.api.nvidia.com/v1/`).
- `key` — Your API key.
- `organization` — Your organization key (make something up if using a self-hosted or non-OpenAI API).
- `API_PROVIDER` — Set to `openai` or `gemini` depending on your provider.
- `API_PROVIDER` — Use `openai` for OpenAI-compatible providers (including Nvidia), or `gemini` for Gemini.
- `model` — For Nvidia/custom OpenAI-compatible endpoints, enter the model name manually (example: `deepseek-ai/deepseek-v4-pro`).
3. The rest of the settings (wordwrap, batch size, etc.) can be left as defaults for now. You can tweak them later.
### 3. Launch the GUI

View file

@ -137,12 +137,27 @@ class ConfigTab(QWidget):
self.reset_to_defaults()
else:
self.load_from_env()
self._update_model_placeholder()
# Connect auto-save after initial load
self.connect_auto_save()
# Fetch latest models in the background once the UI is shown
QTimer.singleShot(0, lambda: self.fetch_models(silent=True))
def _is_nvidia_api_url(self, api_url: str) -> bool:
"""Return True when the configured URL points to Nvidia's OpenAI-compatible API."""
return "integrate.api.nvidia.com" in (api_url or "").strip().lower()
def _update_model_placeholder(self):
"""Show a manual model-entry hint only when Nvidia API is selected."""
line_edit = self.model_combo.lineEdit()
if not line_edit:
return
if self._is_nvidia_api_url(self.api_url_edit.text()):
line_edit.setPlaceholderText("Enter Nvidia model name (e.g., deepseek-ai/deepseek-v4-pro)")
else:
line_edit.setPlaceholderText("")
def init_ui(self):
"""Initialize the user interface with horizontal icon navigation at top."""
@ -326,11 +341,13 @@ class ConfigTab(QWidget):
("Claude (Anthropic)", "https://api.anthropic.com/v1"),
("Gemini", "https://generativelanguage.googleapis.com/v1beta/openai/"),
("DeepSeek", "https://api.deepseek.com/v1/"),
("Nvidia", "https://integrate.api.nvidia.com/v1/"),
]
for _name, _url in _url_presets:
_action = api_url_menu.addAction(_name)
_action.triggered.connect(lambda checked, u=_url: self.api_url_edit.setText(u))
api_url_preset_btn.setMenu(api_url_menu)
self.api_url_edit.textChanged.connect(self._update_model_placeholder)
api_url_layout.addWidget(self.api_url_edit)
api_url_layout.addWidget(api_url_preset_btn)

View file

@ -222,6 +222,45 @@ def _sanitise_output(text: str) -> str:
return text
def _extract_claude_text(response) -> str:
"""Collect text from all text blocks; skip thinking/tool blocks."""
parts = []
for block in getattr(response, "content", None) or []:
if getattr(block, "type", None) == "text":
t = getattr(block, "text", None)
if t:
parts.append(t)
elif hasattr(block, "text") and block.text:
# Older SDK blocks without explicit type
parts.append(block.text)
text = "".join(parts).strip()
if not text:
stop = getattr(response, "stop_reason", None)
raise ValueError(
f"Empty response from API (stop_reason={stop!r}). "
"Try again or use a different model."
)
return text
def _extract_openai_text(response) -> str:
"""Extract assistant message text from an OpenAI-compatible completion."""
choices = getattr(response, "choices", None) or []
if not choices:
raise ValueError("Empty response from API (no choices returned).")
message = choices[0].message
text = getattr(message, "content", None)
if text is None and hasattr(message, "refusal") and message.refusal:
raise ValueError(f"Model refused: {message.refusal}")
if not text or not str(text).strip():
finish = getattr(choices[0], "finish_reason", None)
raise ValueError(
f"Empty response from API (finish_reason={finish!r}). "
"Try again or use a different model."
)
return str(text).strip()
def _call_llm(messages: list, model: str, api_url: str, api_key: str,
is_claude: bool, timeout: int) -> tuple:
"""Call the LLM. Returns (text, input_tokens, output_tokens)."""
@ -237,7 +276,7 @@ def _call_llm(messages: list, model: str, api_url: str, api_key: str,
messages=user_msgs,
timeout=timeout,
)
text = response.content[0].text.strip()
text = _extract_claude_text(response)
in_tok = getattr(response.usage, "input_tokens", 0) or 0
out_tok = getattr(response.usage, "output_tokens", 0) or 0
return text, in_tok, out_tok
@ -254,7 +293,7 @@ def _call_llm(messages: list, model: str, api_url: str, api_key: str,
temperature=0,
timeout=timeout,
)
text = response.choices[0].message.content.strip()
text = _extract_openai_text(response)
in_tok = getattr(response.usage, "prompt_tokens", 0) or 0
out_tok = getattr(response.usage, "completion_tokens", 0) or 0
return text, in_tok, out_tok

View file

@ -282,16 +282,15 @@ class TranslationWorker(QThread):
# Check for required environment variables
required_envs = ["api", "key", "model", "language", "timeout", "fileThreads", "threads", "width", "listWidth"]
env_missing = False
for env in required_envs:
if os.getenv(env) is None or str(os.getenv(env))[:1] == "<":
self.emit_log(f"❌ Environment variable {env} is not set!")
env_missing = True
if env_missing:
self.emit_log("❌ Some required environment variables are not set. Check your .env file.")
self.finished_signal.emit(False, "Environment variables missing")
missing_envs = [
env for env in required_envs
if os.getenv(env) is None or str(os.getenv(env))[:1] == "<"
]
if missing_envs:
names = ", ".join(missing_envs)
self.emit_log(f"❌ Missing required environment variable(s): {names}")
self.emit_log(" Check your .env file (see .env.example).")
self.finished_signal.emit(False, f"Missing env: {names}")
return
# Get files to process

View file

@ -7,9 +7,11 @@ Provides a guided, step-by-step interface:
Step 1 (Optional) Pre-process game files
Step 2 Auto-detect speaker format and apply to module settings
Step 3 Build glossary: parse speakers, then enrich with AI prompt
Step 4 Translation: Phase 0 (DB), Phase 1 (dialogue), Phase 1b (111 cache), Phase 2 (risky)
Step 5 Translate visible strings in js/plugins.js
Step 6 Export translated/ back to the game folder
Step 4 Translation: Phase 0 (DB), Phase 1 (dialogue), Phase 1b (111 cache)
Step 5 Translation Phase 2 (risky codes)
Step 6 Translate visible strings in js/plugins.js (or Ace scripts)
Step 7 Export translated/ back to the game folder
Step 8 Install TL Inspector for playtesting and live in-game edits (MV/MZ)
"""
from __future__ import annotations
@ -27,6 +29,7 @@ from PyQt5.QtGui import QFont
from PyQt5.QtWidgets import (
QApplication,
QCheckBox,
QComboBox,
QFileDialog,
QFrame,
QGridLayout,
@ -560,6 +563,7 @@ class WorkflowTab(QWidget):
("5 TL Phase 2", self._build_step5_tl_phase2),
("6 Plugins.js", self._build_step6_plugins_js),
("7 Export", self._build_step7_export),
("8 Playtest", self._build_step8_playtest),
]
for tab_label, builder in _tab_defs:
@ -714,6 +718,9 @@ class WorkflowTab(QWidget):
if index == 5:
self._populate_p2_checkboxes()
if index == 8:
self._refresh_tl_inspector_status()
self._load_tli_editor_settings()
def _register_import_button(self, button: QPushButton) -> None:
self._import_buttons.append(button)
@ -2398,6 +2405,335 @@ class WorkflowTab(QWidget):
row.addStretch()
layout.addLayout(row)
# ── Step 8: Playtest (TL Inspector) ─────────────────────────────────────
def _build_step8_playtest(self, layout: QVBoxLayout):
self._step8_section_label = _make_section_label("Step 8 — Playtest with TL Inspector")
layout.addWidget(self._step8_section_label)
hint = QLabel(
"Install <b>TL Inspector</b> into your RPG Maker MV/MZ game for playtesting. "
"Press <b>F10</b> in-game to see which source file each line of text comes from, "
"open it in VSCode, or <b>edit the text live</b> and save directly to the JSON file."
)
hint.setWordWrap(True)
hint.setTextFormat(Qt.RichText)
hint.setStyleSheet("color:#9d9d9d;font-size:13px;padding-bottom:4px;")
layout.addWidget(hint)
credits = QLabel("Idea by Sakura · Plugin by Kao_SSS")
credits.setStyleSheet("color:#6a6a6a;font-size:11px;font-style:italic;padding-bottom:6px;")
layout.addWidget(credits)
box = QWidget()
box.setObjectName("tbox")
box.setStyleSheet(self._task_box_style())
inner = QVBoxLayout(box)
inner.setContentsMargins(10, 8, 10, 8)
inner.setSpacing(6)
self._tli_status_label = QLabel("Status: (detect a project folder first)")
self._tli_status_label.setWordWrap(True)
self._tli_status_label.setStyleSheet("color:#7a7a7a;font-size:13px;")
inner.addWidget(self._tli_status_label)
editor_title = QLabel("Editor settings")
editor_title.setStyleSheet("color:#4ec9b0;font-size:12px;font-weight:bold;padding-top:4px;")
inner.addWidget(editor_title)
_TLI_LABEL_W = 80
_TLI_BTN_W = 88
_tli_lbl_style = "color:#9d9d9d;font-size:12px;"
tli_grid = QGridLayout()
tli_grid.setHorizontalSpacing(6)
tli_grid.setVerticalSpacing(6)
tli_grid.setColumnStretch(1, 1)
editor_lbl = QLabel("Editor:")
editor_lbl.setFixedWidth(_TLI_LABEL_W)
editor_lbl.setStyleSheet(_tli_lbl_style)
editor_lbl.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
tli_grid.addWidget(editor_lbl, 0, 0)
self._tli_editor_combo = QComboBox()
self._tli_editor_combo.currentIndexChanged.connect(self._on_tli_editor_combo_changed)
tli_grid.addWidget(self._tli_editor_combo, 0, 1)
detect_btn = _make_btn("Detect", "#4a4a4a")
detect_btn.setFixedWidth(_TLI_BTN_W)
detect_btn.setToolTip("Scan this PC for VS Code, Insiders, or Cursor")
detect_btn.clicked.connect(self._detect_tli_editors)
tli_grid.addWidget(detect_btn, 0, 2)
self._tli_editor_custom = QLineEdit()
self._tli_editor_custom.setPlaceholderText("Path to editor executable (when Custom is selected)")
self._tli_editor_custom.setEnabled(False)
tli_grid.addWidget(self._tli_editor_custom, 1, 1)
browse_editor_btn = _make_btn("Browse…", "#4a4a4a")
browse_editor_btn.setFixedWidth(_TLI_BTN_W)
browse_editor_btn.clicked.connect(self._browse_tli_editor)
tli_grid.addWidget(browse_editor_btn, 1, 2)
self._tli_detect_label = QLabel("")
self._tli_detect_label.setWordWrap(True)
self._tli_detect_label.setStyleSheet("color:#6a6a6a;font-size:11px;")
tli_grid.addWidget(self._tli_detect_label, 2, 1, 1, 2)
cfg_btn_wrap = QWidget()
cfg_btn_row = QHBoxLayout(cfg_btn_wrap)
cfg_btn_row.setContentsMargins(0, 0, 0, 0)
cfg_btn_row.setSpacing(8)
save_tli_btn = _make_btn("✔ Save settings", "#3a5a7a")
save_tli_btn.setFixedWidth(140)
save_tli_btn.setToolTip("Write editor settings to .env (used on Install / Apply)")
save_tli_btn.clicked.connect(self._save_tli_editor_settings)
cfg_btn_row.addWidget(save_tli_btn)
apply_tli_btn = _make_btn("↻ Apply to game", "#3a5a7a")
apply_tli_btn.setFixedWidth(140)
apply_tli_btn.setToolTip("Update the installed TLInspector.js in your game folder")
apply_tli_btn.clicked.connect(self._apply_tli_editor_settings)
cfg_btn_row.addWidget(apply_tli_btn)
cfg_btn_row.addStretch()
tli_grid.addWidget(cfg_btn_wrap, 3, 1, 1, 2)
inner.addLayout(tli_grid)
tips = QLabel(
"<ul style='margin:4px 0;padding-left:18px;color:#9d9d9d;font-size:12px;'>"
"<li>Run this <b>after exporting</b> translations so the game files are up to date.</li>"
"<li>Overlay <b>save to file</b> reloads instantly; VSCode saves reload once the file is stable on disk</li>"
"<li><b>F9</b> — force reload database + current map</li>"
"<li><b>F10</b> — toggle the inspector panel</li>"
"<li>Click a source location to open in VSCode; click <b>edit</b> to change text in-game</li>"
"<li>Remove the plugin before shipping a release build</li>"
"</ul>"
)
tips.setWordWrap(True)
tips.setTextFormat(Qt.RichText)
inner.addWidget(tips)
btn_row = QHBoxLayout()
btn_row.setSpacing(8)
_BTN_W = 200
self._tli_install_btn = _make_btn("⬇ Install TL Inspector", "#3a7a3a")
self._tli_install_btn.setFixedWidth(_BTN_W)
self._tli_install_btn.clicked.connect(self._install_tl_inspector)
btn_row.addWidget(self._tli_install_btn)
self._tli_uninstall_btn = _make_btn("⬆ Uninstall TL Inspector", "#7a3a3a")
self._tli_uninstall_btn.setFixedWidth(_BTN_W)
self._tli_uninstall_btn.clicked.connect(self._uninstall_tl_inspector)
btn_row.addWidget(self._tli_uninstall_btn)
btn_row.addStretch()
inner.addLayout(btn_row)
layout.addWidget(box)
self._step8_playtest_box = box
self._populate_tli_editor_combo()
self._load_tli_editor_settings()
def _populate_tli_editor_combo(self, select: str | None = None):
"""Fill editor dropdown with auto-detect, found editors, and custom."""
from util.tl_inspector.config import detect_editors
combo = self._tli_editor_combo
combo.blockSignals(True)
combo.clear()
combo.addItem("Auto-detect (recommended)", "auto")
for label, path in detect_editors():
combo.addItem(f"{label}{path}", str(path))
combo.addItem("Custom path…", "__custom__")
want = select
if want is None:
try:
from util.tl_inspector.config import load_config
want = load_config().get("editorCmd", "auto")
except Exception:
want = "auto"
idx = combo.findData(want) if want else 0
if idx >= 0:
combo.setCurrentIndex(idx)
elif want and want != "auto":
custom_idx = combo.findData("__custom__")
combo.setCurrentIndex(custom_idx if custom_idx >= 0 else 0)
self._tli_editor_custom.setText(want)
else:
combo.setCurrentIndex(0)
combo.blockSignals(False)
self._on_tli_editor_combo_changed()
self._update_tli_detect_label()
def _update_tli_detect_label(self):
from util.tl_inspector.config import detect_editors, detect_primary_editor
found = detect_editors()
primary = detect_primary_editor()
if primary:
extra = f" ({len(found)} found)" if len(found) > 1 else ""
self._tli_detect_label.setText(f"Detected on this PC: {primary}{extra}")
else:
self._tli_detect_label.setText(
"No VS Code / Cursor found — install one or choose Custom path."
)
def _load_tli_editor_settings(self):
"""Load TL Inspector editor settings from .env into Step 8 controls."""
try:
from util.tl_inspector.config import load_config
cfg = load_config()
except Exception:
cfg = {"editorCmd": "auto"}
self._populate_tli_editor_combo(select=cfg.get("editorCmd", "auto"))
def _resolve_tli_config(self) -> dict:
"""Build config dict from Step 8 editor controls."""
mode = self._tli_editor_combo.currentData()
if mode == "__custom__":
editor = self._tli_editor_custom.text().strip() or "auto"
elif mode:
editor = str(mode)
else:
editor = "auto"
return {"editorCmd": editor, "workspaceFolder": "auto"}
def _on_tli_editor_combo_changed(self, _index: int | None = None):
custom = self._tli_editor_combo.currentData() == "__custom__"
self._tli_editor_custom.setEnabled(custom)
def _detect_tli_editors(self):
try:
from util.tl_inspector.config import load_config
current = load_config().get("editorCmd", "auto")
except Exception:
current = "auto"
self._populate_tli_editor_combo(select=current)
self._log("🔍 Scanned for VS Code / Cursor installations.")
def _browse_tli_editor(self):
start = self._tli_editor_custom.text() or self._setting("last_tli_editor", "")
path, _ = QFileDialog.getOpenFileName(
self,
"Select Editor Executable",
start,
"Executables (*.exe);;All Files (*)",
)
if not path:
return
self._save_setting("last_tli_editor", path)
custom_idx = self._tli_editor_combo.findData("__custom__")
if custom_idx >= 0:
self._tli_editor_combo.setCurrentIndex(custom_idx)
self._tli_editor_custom.setText(path)
def _save_tli_editor_settings(self):
cfg = self._resolve_tli_config()
try:
from util.tl_inspector.config import save_config
save_config(cfg)
self._log(f"✅ TL Inspector settings saved — editor={cfg['editorCmd']}")
except Exception as exc:
self._log(f"❌ Could not save TL Inspector settings: {exc}")
def _apply_tli_editor_settings(self):
game_root = self.folder_edit.text().strip()
if not game_root:
self._log("⚠ No game folder set. Complete Step 0 first.")
return
cfg = self._resolve_tli_config()
try:
from util.tl_inspector.config import save_config
from util.tl_inspector.installer import apply_config
save_config(cfg)
ok, msg = apply_config(Path(game_root), cfg)
except Exception as exc:
self._log(f"❌ Could not apply TL Inspector settings: {exc}")
return
self._log(("" if ok else "") + msg)
def _refresh_tl_inspector_status(self):
"""Update Step 8 status label from the current game folder."""
label = getattr(self, "_tli_status_label", None)
if label is None:
return
game_root = self.folder_edit.text().strip()
if not game_root:
label.setText("Status: no game folder set — complete Step 0 first.")
label.setStyleSheet("color:#7a7a7a;font-size:13px;")
return
try:
from util.tl_inspector.installer import status
st = status(Path(game_root))
except Exception as exc:
label.setText(f"Status: error — {exc}")
label.setStyleSheet("color:#f48771;font-size:13px;")
return
if not st.get("ok"):
label.setText(f"Status: {st.get('message', 'unsupported')}")
label.setStyleSheet("color:#e9a12a;font-size:13px;")
return
engine = st.get("engine", "?")
msg = st.get("message", "")
parts = [f"RPG Maker {engine}", msg]
if st.get("declared") and st.get("plugin_file"):
detail = "plugin declared in plugins.js and file present"
elif st.get("declared"):
detail = "declared in plugins.js (plugin file missing)"
elif st.get("plugin_file"):
detail = "plugin file present (not declared in plugins.js)"
else:
detail = "not installed"
label.setText(f"Status: {' · '.join(parts)}{detail}")
color = "#6a9a6a" if st.get("declared") and st.get("plugin_file") else "#9d9d9d"
label.setStyleSheet(f"color:{color};font-size:13px;")
def _install_tl_inspector(self):
game_root = self.folder_edit.text().strip()
if not game_root:
self._log("⚠ No game folder set. Complete Step 0 first.")
return
cfg = self._resolve_tli_config()
try:
from util.tl_inspector.config import save_config
from util.tl_inspector.installer import install
save_config(cfg)
ok, msg = install(Path(game_root), cfg=cfg)
except Exception as exc:
self._log(f"❌ TL Inspector install failed: {exc}")
return
self._log(("" if ok else "") + msg)
self._refresh_tl_inspector_status()
def _uninstall_tl_inspector(self):
game_root = self.folder_edit.text().strip()
if not game_root:
self._log("⚠ No game folder set. Complete Step 0 first.")
return
reply = QMessageBox.question(
self,
"Uninstall TL Inspector",
"Remove TLInspector from plugins.js and delete the plugin file?",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No,
)
if reply != QMessageBox.Yes:
return
try:
from util.tl_inspector.installer import uninstall
ok, msg = uninstall(Path(game_root))
except Exception as exc:
self._log(f"❌ TL Inspector uninstall failed: {exc}")
return
self._log(("" if ok else "") + msg)
self._refresh_tl_inspector_status()
# ─────────────────────────────────────────────────────────────────────────
# Step 0 Project Folder logic
# ─────────────────────────────────────────────────────────────────────────
@ -2501,6 +2837,13 @@ class WorkflowTab(QWidget):
"Copy a prompt that instructs Copilot/Cursor to translate only "
"visible player-facing strings in plugins.js, using vocab.txt as a glossary."
)
# Step 8 — TL Inspector (MV/MZ only)
if hasattr(self, "_step_tabs") and self._step_tabs.count() > 8:
self._step_tabs.setTabEnabled(8, not is_ace)
box = getattr(self, "_step8_playtest_box", None)
if box is not None:
box.setEnabled(not is_ace)
self._refresh_tl_inspector_status()
def _detect_folder(self):
folder = self.folder_edit.text().strip()

View file

@ -10,27 +10,27 @@ from dotenv import load_dotenv
# This needs to be before the module imports as some of them currently try to read and use some of these values
# upon import, in which case if they are unset the script will crash before we can output these messages.
envMissing = False
load_dotenv()
for env in [
"api",
"key",
"model",
"language",
"timeout",
"fileThreads",
"threads",
"width",
"listWidth",
]:
if os.getenv(env) is None or str(os.getenv(env))[:1] == "<":
tqdm.write(Fore.RED + f"Environment variable {env} is not set!")
envMissing = True
if envMissing:
_missing_envs = [
env for env in [
"api",
"key",
"model",
"language",
"timeout",
"fileThreads",
"threads",
"width",
"listWidth",
]
if os.getenv(env) is None or str(os.getenv(env))[:1] == "<"
]
if _missing_envs:
names = ", ".join(_missing_envs)
tqdm.write(
Fore.RED
+ "Some of the required environment values may not be set correctly. You can set \
these values using an .env file, for an example see .env.example"
+ f"Missing required environment variable(s): {names}. "
+ "Set them in a .env file (see .env.example)."
)
from modules.rpgmakermvmz import handleMVMZ, setSpeakerParseMode as setSpeakerParseMVMZ, finalizeSpeakerParse as finalizeSpeakerParseMVMZ

View file

@ -0,0 +1,112 @@
@echo off
rem ============================================================================
rem TLInspector installer / uninstaller (RPG Maker MV & MZ)
rem Idea by Sakura · Plugin by Kao_SSS
rem Put this file and TLInspector.js in the GAME ROOT (next to the .exe /
rem index.html), then double-click this file.
rem - Not installed yet -> installs it (moves the plugin into the plugins
rem folder and declares it at the end of plugins.js).
rem - Already installed -> asks whether to remove it.
rem ============================================================================
chcp 65001 >nul
setlocal
set "TLI_ROOT=%~dp0"
powershell -NoProfile -ExecutionPolicy Bypass -Command "$l=Get-Content -LiteralPath '%~f0'; $i=[Array]::IndexOf($l,'#:PSSTART'); Invoke-Expression (($l[($i+1)..($l.Count-1)]) -join [char]10)"
endlocal
exit /b
#:PSSTART
$ErrorActionPreference = 'Stop'
try { [Console]::OutputEncoding = [Text.Encoding]::UTF8 } catch {}
function PauseExit { [void](Read-Host "`nPress Enter to exit"); exit }
$root = $env:TLI_ROOT
Write-Host "============================================"
Write-Host " TLInspector installer"
Write-Host " Idea by Sakura · Plugin by Kao_SSS"
Write-Host "============================================"
# --- Detect engine / locate plugins.js (MV uses www\, MZ does not) ----------
$mvJs = Join-Path $root 'www\js\plugins.js'
$mzJs = Join-Path $root 'js\plugins.js'
if (Test-Path -LiteralPath $mvJs) {
$engine = 'MV'; $pluginsJs = $mvJs; $pluginsDir = Join-Path $root 'www\js\plugins'
} elseif (Test-Path -LiteralPath $mzJs) {
$engine = 'MZ'; $pluginsJs = $mzJs; $pluginsDir = Join-Path $root 'js\plugins'
} else {
Write-Host ""
Write-Host "ERROR: No RPG Maker MV/MZ game found here." -ForegroundColor Red
Write-Host "Could not find 'www\js\plugins.js' (MV) or 'js\plugins.js' (MZ)."
Write-Host "Place this installer and TLInspector.js in the game ROOT folder"
Write-Host "(the one containing the .exe / index.html) and run it again."
PauseExit
}
Write-Host ("Detected RPG Maker {0}" -f $engine)
Write-Host ("plugins list: {0}" -f $pluginsJs)
$target = Join-Path $pluginsDir 'TLInspector.js'
$srcRoot = Join-Path $root 'TLInspector.js'
$utf8 = New-Object System.Text.UTF8Encoding($false) # no BOM
$content = [IO.File]::ReadAllText($pluginsJs)
$nl = if ($content -match "`r`n") { "`r`n" } else { "`n" }
$declared = [regex]::IsMatch($content, '"name"\s*:\s*"TLInspector"')
$fileThere = Test-Path -LiteralPath $target
# --- Already installed -> offer to remove -----------------------------------
if ($declared -or $fileThere) {
Write-Host ""
Write-Host "TLInspector is currently INSTALLED." -ForegroundColor Yellow
$ans = Read-Host "Remove it? (Y/N)"
if ($ans -match '^(y|yes)$') {
if ($declared) {
$kept = ($content -split "`r?`n") | Where-Object { $_ -notmatch '"name"\s*:\s*"TLInspector"' }
$newText = ($kept -join $nl)
# drop a now-dangling comma before the closing ];
$newText = [regex]::Replace($newText, ',(\s*)\];', '$1];')
[IO.File]::WriteAllText($pluginsJs, $newText, $utf8)
Write-Host "Removed the TLInspector entry from plugins.js"
}
if ($fileThere) {
Remove-Item -LiteralPath $target -Force
Write-Host ("Deleted {0}" -f $target)
}
Write-Host ""
Write-Host "Uninstalled." -ForegroundColor Green
} else {
Write-Host "Cancelled - nothing changed."
}
PauseExit
}
# --- Not installed -> install -----------------------------------------------
if (-not (Test-Path -LiteralPath $srcRoot)) {
Write-Host ""
Write-Host "ERROR: TLInspector.js was not found next to this installer." -ForegroundColor Red
Write-Host ("Expected: {0}" -f $srcRoot)
Write-Host "Copy TLInspector.js into the game root and run this again."
PauseExit
}
if (-not (Test-Path -LiteralPath $pluginsDir)) {
New-Item -ItemType Directory -Path $pluginsDir -Force | Out-Null
}
Move-Item -LiteralPath $srcRoot -Destination $target -Force
Write-Host ("Moved plugin -> {0}" -f $target)
# Insert the declaration as the last entry, just before the closing ];
$entry = ' { "name": "TLInspector", "status": true, "description": "TL source inspector", "parameters": {} }'
$idx = $content.LastIndexOf('];')
if ($idx -lt 0) {
Write-Host ""
Write-Host "ERROR: could not find the end of the plugin list ( ]; ) in plugins.js." -ForegroundColor Red
Write-Host "The plugin file was moved; please add this line before the ']' yourself:"
Write-Host $entry
PauseExit
}
$before = $content.Substring(0, $idx).TrimEnd()
$after = $content.Substring($idx) # starts at ];
$sep = if ($before.EndsWith(',')) { $nl } else { ',' + $nl }
$newText = $before + $sep + $entry + $nl + ' ' + $after
[IO.File]::WriteAllText($pluginsJs, $newText, $utf8)
Write-Host "Declared TLInspector at the end of plugins.js"
Write-Host ""
Write-Host "Installed! Start the game, press F10 to open the inspector, and use edit to save text changes in-game." -ForegroundColor Green
PauseExit

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,16 @@
"""TL Inspector install helpers for RPG Maker MV/MZ (Idea: Sakura · Plugin: Kao_SSS)."""
from util.tl_inspector.config import detect_editors, load_config, save_config
from util.tl_inspector.installer import apply_config, bundled_plugin_path, detect_engine, install, status, uninstall
__all__ = [
"apply_config",
"bundled_plugin_path",
"detect_editors",
"detect_engine",
"install",
"load_config",
"save_config",
"status",
"uninstall",
]

135
util/tl_inspector/config.py Normal file
View file

@ -0,0 +1,135 @@
"""TL Inspector configuration — .env storage, editor detection, JS patching."""
from __future__ import annotations
import json
import os
import re
import sys
from pathlib import Path
from dotenv import dotenv_values
ENV_EDITOR = "tlEditorCmd"
CFG_KEYS = ("editorCmd", "workspaceFolder")
DEFAULTS = {
"editorCmd": "auto",
"workspaceFolder": "auto",
}
_PKG_ROOT = Path(__file__).resolve().parent
BUNDLED_PLUGIN = _PKG_ROOT / "TLInspector.js"
ENV_PATH = Path(".env")
def detect_editors() -> list[tuple[str, Path]]:
"""Return installed code editors as (label, executable path) pairs."""
out: list[tuple[str, Path]] = []
seen: set[str] = set()
def add(label: str, path: Path) -> None:
key = str(path).lower()
if key in seen or not path.is_file():
return
seen.add(key)
out.append((label, path))
if sys.platform == "win32":
bases = [
os.environ.get("LOCALAPPDATA"),
os.environ.get("ProgramFiles"),
os.environ.get("ProgramFiles(x86)"),
]
for raw in bases:
if not raw:
continue
base = Path(raw)
add("VS Code", base / "Programs" / "Microsoft VS Code" / "Code.exe")
add("VS Code", base / "Microsoft VS Code" / "Code.exe")
add(
"VS Code Insiders",
base / "Programs" / "Microsoft VS Code Insiders" / "Code - Insiders.exe",
)
add("Cursor", base / "Programs" / "cursor" / "Cursor.exe")
add("Cursor", base / "cursor" / "Cursor.exe")
elif sys.platform == "darwin":
add(
"VS Code",
Path("/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code"),
)
add("Cursor", Path("/Applications/Cursor.app/Contents/MacOS/Cursor"))
else:
for path in (
Path("/usr/bin/code"),
Path("/usr/share/code/bin/code"),
Path("/snap/bin/code"),
Path("/usr/bin/cursor"),
):
label = "Cursor" if "cursor" in path.name else "VS Code"
add(label, path)
return out
def detect_primary_editor() -> Path | None:
editors = detect_editors()
return editors[0][1] if editors else None
def load_config(env_path: Path | None = None) -> dict:
path = env_path or ENV_PATH
env = dotenv_values(path) if path.is_file() else {}
cfg = dict(DEFAULTS)
editor = (env.get(ENV_EDITOR) or "").strip()
if editor:
cfg["editorCmd"] = editor
return cfg
def save_config(cfg: dict, env_path: Path | None = None) -> None:
path = env_path or ENV_PATH
text = path.read_text(encoding="utf-8") if path.is_file() else ""
updates = {
ENV_EDITOR: cfg.get("editorCmd", "auto"),
}
for key, val in updates.items():
quoted = f"'{val}'"
pattern = rf"^({re.escape(key)}\s*=\s*)(?:'[^']*'|\"[^\"]*\"|[^\n]*)"
text, n = re.subn(pattern, rf"\g<1>{quoted}", text, count=1, flags=re.MULTILINE)
if n == 0:
text = text.rstrip("\n") + f"\n{key}={quoted}\n"
if not path.exists():
path.touch()
path.write_text(text, encoding="utf-8")
def _js_literal(value) -> str:
if value is None:
return "null"
if isinstance(value, bool):
return "true" if value else "false"
if isinstance(value, (int, float)):
return str(value)
return json.dumps(str(value))
def patch_cfg_block(js: str, overrides: dict) -> str:
for key, val in overrides.items():
if key not in CFG_KEYS:
continue
lit = _js_literal(val)
pattern = rf"({re.escape(key)}:\s*)(?:'[^']*'|\"[^\"]*\"|true|false|null|\d+)"
js, count = re.subn(pattern, rf"\g<1>{lit}", js, count=1)
if count == 0:
raise ValueError(f"Could not patch CFG.{key} in TLInspector.js")
return js
def prepare_plugin_js(source: Path | None = None, cfg: dict | None = None) -> str:
src = source or BUNDLED_PLUGIN
text = src.read_text(encoding="utf-8")
effective = {**DEFAULTS, **(cfg or load_config())}
effective["workspaceFolder"] = "auto"
return patch_cfg_block(text, effective)

View file

@ -0,0 +1,155 @@
"""Install / uninstall TLInspector into an RPG Maker MV or MZ game folder.
Credits: Idea by Sakura · Plugin by Kao_SSS
"""
from __future__ import annotations
import re
from pathlib import Path
PLUGIN_NAME = "TLInspector"
PLUGIN_ENTRY = (
' { "name": "TLInspector", "status": true, '
'"description": "TL source inspector", "parameters": {} }'
)
_PKG_ROOT = Path(__file__).resolve().parent
DEFAULT_PLUGIN_SRC = _PKG_ROOT / "TLInspector.js"
def bundled_plugin_path() -> Path:
return DEFAULT_PLUGIN_SRC
def detect_engine(game_root: Path) -> tuple[str, Path, Path] | None:
"""Return (engine, plugins_js, plugins_dir) or None if not MV/MZ."""
root = Path(game_root)
mv_js = root / "www" / "js" / "plugins.js"
mz_js = root / "js" / "plugins.js"
if mv_js.is_file():
return "MV", mv_js, root / "www" / "js" / "plugins"
if mz_js.is_file():
return "MZ", mz_js, root / "js" / "plugins"
return None
def _read_plugins_js(plugins_js: Path) -> tuple[str, str]:
content = plugins_js.read_text(encoding="utf-8")
nl = "\r\n" if "\r\n" in content else "\n"
return content, nl
def _is_declared(content: str) -> bool:
return bool(re.search(r'"name"\s*:\s*"TLInspector"', content))
def status(game_root: Path) -> dict:
"""Return install state for the game folder."""
info = detect_engine(game_root)
if info is None:
return {
"ok": False,
"engine": None,
"installed": False,
"declared": False,
"plugin_file": None,
"message": "No RPG Maker MV/MZ game found (missing plugins.js).",
}
engine, plugins_js, plugins_dir = info
target = plugins_dir / f"{PLUGIN_NAME}.js"
content, _ = _read_plugins_js(plugins_js)
declared = _is_declared(content)
file_there = target.is_file()
return {
"ok": True,
"engine": engine,
"installed": declared or file_there,
"declared": declared,
"plugin_file": file_there,
"plugins_js": str(plugins_js),
"target": str(target),
"message": (
"Installed"
if declared and file_there
else "Partially installed"
if declared or file_there
else "Not installed"
),
}
def install(game_root: Path, source_js: Path | None = None, cfg: dict | None = None) -> tuple[bool, str]:
"""Copy TLInspector.js into the game and declare it in plugins.js."""
from util.tl_inspector.config import load_config, prepare_plugin_js
info = detect_engine(game_root)
if info is None:
return False, "No RPG Maker MV/MZ game found at that path."
engine, plugins_js, plugins_dir = info
src = Path(source_js) if source_js else DEFAULT_PLUGIN_SRC
if not src.is_file():
return False, f"TLInspector.js not found: {src}"
target = plugins_dir / f"{PLUGIN_NAME}.js"
content, nl = _read_plugins_js(plugins_js)
plugins_dir.mkdir(parents=True, exist_ok=True)
effective_cfg = cfg if cfg is not None else load_config()
target.write_text(prepare_plugin_js(src, effective_cfg), encoding="utf-8")
if not _is_declared(content):
idx = content.rfind("];")
if idx < 0:
return False, "Could not find plugin list end ( ]; ) in plugins.js."
before = content[:idx].rstrip()
after = content[idx:]
sep = nl if before.endswith(",") else "," + nl
content = before + sep + PLUGIN_ENTRY + nl + " " + after
plugins_js.write_text(content, encoding="utf-8", newline="")
return True, f"TLInspector installed for RPG Maker {engine}. Press F10 in-game to open."
def uninstall(game_root: Path) -> tuple[bool, str]:
"""Remove TLInspector from plugins.js and delete the plugin file."""
info = detect_engine(game_root)
if info is None:
return False, "No RPG Maker MV/MZ game found at that path."
_, plugins_js, plugins_dir = info
target = plugins_dir / f"{PLUGIN_NAME}.js"
content, nl = _read_plugins_js(plugins_js)
if _is_declared(content):
kept = [
line for line in re.split(r"\r?\n", content)
if not re.search(r'"name"\s*:\s*"TLInspector"', line)
]
new_text = nl.join(kept)
new_text = re.sub(r",(\s*)\];", r"\1];", new_text)
plugins_js.write_text(new_text, encoding="utf-8", newline="")
if target.is_file():
target.unlink()
return True, "TLInspector uninstalled."
def apply_config(game_root: Path, cfg: dict | None = None) -> tuple[bool, str]:
"""Rewrite an installed TLInspector.js with current editor settings."""
from util.tl_inspector.config import load_config, prepare_plugin_js
info = detect_engine(game_root)
if info is None:
return False, "No RPG Maker MV/MZ game found at that path."
_, _, plugins_dir = info
target = plugins_dir / f"{PLUGIN_NAME}.js"
if not target.is_file():
return False, "TLInspector is not installed in this game folder."
effective_cfg = cfg if cfg is not None else load_config()
target.write_text(prepare_plugin_js(DEFAULT_PLUGIN_SRC, effective_cfg), encoding="utf-8")
return True, "TL Inspector editor settings applied to the installed plugin."

View file

@ -88,6 +88,13 @@ def _write_request_debug_log(provider, request_payload, usage):
except Exception:
pass
def _normalize_openai_base_url(url: str) -> str:
"""Ensure OpenAI SDK global base_url has a trailing slash."""
_url = (url or "").strip()
if _url and not _url.endswith("/"):
_url += "/"
return _url
# Tracks which distinct batch sizes have already been cache-written during this estimate run.
# Each unique numLines value maps to a distinct output_config schema → one write per size.
# Persisted to disk so sequential GUI subprocesses share state.
@ -329,17 +336,17 @@ def validate_translation_content(original_items, translated_items, langRegex):
return is_valid, invalid_indices, reasons
# Load .env, strip accidental whitespace, set base URL / org / API key.
# Handles the Gemini compatibility layer as a special case.
# Gemini uses its compatibility endpoint only when no custom API URL is set.
load_dotenv()
api_provider = os.getenv("API_PROVIDER", "openai").lower()
env_api = os.getenv("api", "").strip()
if api_provider == "gemini":
# Use Google Generative Language compatibility endpoint when running Gemini
if api_provider == "gemini" and not env_api:
# Use Google Generative Language compatibility endpoint only as fallback.
openai.base_url = "https://generativelanguage.googleapis.com/v1beta/openai/"
openai.organization = None
else:
if env_api:
openai.base_url = env_api
openai.base_url = _normalize_openai_base_url(env_api)
# Support both 'organization' (gui/.env.example) and legacy 'org' names
org = os.getenv("organization") or os.getenv("org")
if org:
@ -1182,10 +1189,10 @@ def translateText(system, user, history, penalty, formatType, model, numLines=No
_live_api = os.getenv("api", "").strip()
_live_key = os.getenv("key", "").strip()
_live_provider = os.getenv("API_PROVIDER", "openai").lower()
if _live_provider == "gemini":
if _live_provider == "gemini" and not _live_api:
openai.base_url = "https://generativelanguage.googleapis.com/v1beta/openai/"
elif _live_api:
openai.base_url = _live_api
openai.base_url = _normalize_openai_base_url(_live_api)
if _live_key:
openai.api_key = _live_key