Merge branch 'main' of ssh://ssh.gitgud.io/kaosss/DazedMTLTool
This commit is contained in:
commit
060f02b72c
14 changed files with 3163 additions and 48 deletions
24
.env.example
24
.env.example
|
|
@ -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
2
.gitignore
vendored
|
|
@ -1,3 +1,4 @@
|
|||
|
||||
.env
|
||||
*.tmp
|
||||
*.json
|
||||
|
|
@ -19,3 +20,4 @@ __pycache__
|
|||
!prompt.txt
|
||||
!vocab_base.txt
|
||||
!requirements.txt
|
||||
!util/tl_inspector/TLInspector.js
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
112
util/tl_inspector/TLInspector.bat
Normal file
112
util/tl_inspector/TLInspector.bat
Normal 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
|
||||
2279
util/tl_inspector/TLInspector.js
Normal file
2279
util/tl_inspector/TLInspector.js
Normal file
File diff suppressed because it is too large
Load diff
16
util/tl_inspector/__init__.py
Normal file
16
util/tl_inspector/__init__.py
Normal 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
135
util/tl_inspector/config.py
Normal 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)
|
||||
155
util/tl_inspector/installer.py
Normal file
155
util/tl_inspector/installer.py
Normal 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."
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue