Reworking prompts/skills

This commit is contained in:
DazedAnon 2026-07-11 11:33:14 -05:00
parent e991f27cbf
commit fe31c71130
16 changed files with 1050 additions and 540 deletions

View file

@ -176,8 +176,8 @@ Format: Japanese name, English name in parentheses, then gender.
> **Note:** A very large vocab file can increase API costs and potentially reduce quality. Focus on the most important characters and terms.
### data/prompt.txt
This is the system prompt sent to the AI. A default `data/prompt.txt` is included and works well for most games. You generally don't need to edit it unless you want to customize the translation style.
### data/skills/system.md
This is the system prompt skill sent to the AI on every translation call. A default `data/skills/system.md` is included and works well for most games. You generally don't need to edit it unless you want to customize the translation style. Per-game voice rules go in `<game>/skills/quirks.md` instead.
---
@ -230,7 +230,7 @@ How it works (all engine modules are supported automatically):
1. **Pass 1 (collect)** — files are processed normally, but instead of calling the API each
request is queued to `log/batch_requests.json`. Requests are byte-identical to live ones:
the static `prompt.txt` block is cached with a 1h TTL, matched vocab and translation
the static `data/skills/system.md` block is cached with a 1h TTL, matched vocab and translation
history ride along per request, and structured output enforces the exact line count.
Speaker/variable names still translate live during this pass (they get embedded into the
dialogue payloads, so both passes must resolve them identically) — they're a tiny share

View file

@ -2,7 +2,7 @@
## Custom game prompt
- Keep the default prompt (`prompt.txt`) as the base for every game.
- Keep the default prompt (`data/skills/system.md`) as the base for every game.
- Add an optional **per-game custom prompt** in the UI that merges with the default (custom rules layered on top of shared instructions, not a full replacement unless explicitly chosen).
- Surface this in settings / project setup so each game folder can carry its own tone, terminology notes, or content warnings without forking the global prompt.

View file

@ -0,0 +1,165 @@
# DazedMTLTool — Project Setup
You are analysing a Japanese game project to produce configuration artifacts for DazedMTLTool.
Work in the game repository. Scan files; do not invent content you did not see.
---
## Task
In **one pass**, discover everything needed for translation setup and emit the labeled output blocks below.
Do **not** translate the game. Do **not** rewrite formatting codes, wrap widths, or tool pipeline settings.
Default: emit **all four** blocks.
Regenerate mode: if the user asks for only one block (`glossary`, `speakers`, `translation_quirks`, or `game_skill`), emit **only** that block using the same schema and exclusivity rules.
---
## Ownership (no duplication)
| Block | Owns | Must NOT include |
|-------|------|------------------|
| `glossary` | Named characters (gender, role, **per-character** speech register) + worldbuilding terms | Global dialect / person rules; honorific policy; speaker-format flags; formatting |
| `speakers` | Tool flag ENABLE/SKIP + short evidence | Character bios; quirks; full glossary |
| `translation_quirks` | Cross-cutting voice rules (battle-log person, global dialect, item-description style, **unusual** honorific habits) | Per-character register; "always keep -san" (tool base prompt already does); codes/wrap/line counts; speaker flags |
| `game_skill` | Game identity, which files matter, pointer to `skills/quirks.md`, what the tool owns | Verbatim quirks list; verbatim glossary; restating formatting/honorific base policy |
Hard rules:
1. Per-character voice → `glossary` only.
2. Category-wide / cross-cutting voice → `translation_quirks` only.
3. Default honorifics policy is owned by the tool base prompt - only note **unusual** honorific habits in quirks.
4. `game_skill` **references** `skills/quirks.md`; do not reprint those rules.
5. `speakers` is config, not lore.
---
## Shared scan strategy
Map / event files can be huge. Do **not** read them sequentially end-to-end.
1. Read small DB files in full first (richest, always small).
2. For large event/map files: **search/grep**; sample early story maps; stop when patterns stabilize.
3. One scan feeds all four blocks - do not rescan from scratch per block.
<!-- engine:rpgmaker -->
### RPG Maker file strategy
**DB (read in full):**
- `Actors.json` (mandatory for major characters, actor IDs, `\\N[n]` mappings)
- `Classes.json`, `Troops.json`, `Skills.json`, `Items.json`, `Armors.json`, `Weapons.json`, `States.json`, `System.json`
**Events (grep / sample):**
- `CommonEvents.json`, `Troops.json`, `Map001.json``Map010.json` (early maps first)
- Prefer code `401` dialogue + nearby `101` speaker/name params; `405` scrolling text when present
- Speaker markup evidence: `【Name】`, `[Name]`, `Name`, `\\n<Name>`, `\\k<Name>`, colour-wrapped name lines
--- attach your game data files / open the game repo before continuing ---
### Speakers analysis (for `speakers` block)
Code `101` opens the text window. Code `401` is a dialogue line. Multiple `401`s form one message box.
**Always-on formats (NO FLAG needed):**
- `101` param[4] name
- `\\n<Name>` or `\\k<Name>` (angle brackets)
- `【Name】` alone or with dialogue
- `[Name]` alone or with dialogue
- `\\c[N]Name\\c[0]` colour-wrapped name on its own `401` line
- `Name` full-width colon name line
**Critical:** `\\N[X]` / `\\n[X]` (square brackets + number) are actor variable codes, NOT speaker formats. Do not count them as always-on speaker hits.
**Flags (only when some speakers lack an always-on format):**
- `INLINE401SPEAKERS` — name immediately before `「` on a `401` line (e.g. `エレナ「今日は…`)
- `FIRSTLINESPEAKERS` — first `401` is a short plain name (< 40 chars, has Japanese); next line starts with `「` `"`` `` `*` `[`; often empty face on `101`
- `FACENAME101` — last resort only when no always-on and neither flag above; face filename in `101` param[0], empty param[4]. If ENABLE, list every unique face filename from `101` param[0].
### Glossary rules (for `glossary` block)
- Separator: plain hyphen-minus `-` only (never em/en dash).
- Descriptions entirely in English; refer to other characters by English name.
- Commit to one spelling - never `Sylfia / Sylphia`.
- Characters: gender, role, speech register, personality, player-chosen name (Actors.json ID 1).
- Real named actors get full `# Game Characters` entries, not only `\\N[n]` placeholders.
- Worldbuilding: factions, lore locations, unique systems/titles - exclude skill/item/weapon/armour names and generic RPG words.
### Quirks rules (for `translation_quirks` block)
Find translation-only quirks, for example:
- Battle log / system messages consistently 3rd person (or other fixed person)
- Global dialect (old-timer speech, archaic narration)
- Recurring item/skill description style
- Unusual honorific habits (who uses what with whom)
Exclude: formatting codes, wrap, line counts, speaker flags, character name lists.
Output as short imperative bullets suitable to paste into `skills/quirks.md`.
### Game skill rules (for `game_skill` block)
Produce a durable IDE skill for this title saved at `skills/translation.md`:
- Short game identity (tone/genre/content notes)
- Which files matter for dialogue/DB in this repo
- Explicit pointer: follow `skills/quirks.md` for voice rules (authoritative)
- What not to change (codes, formatting - tool-owned via base prompt)
- Do **not** reprint quirks or the glossary
<!-- /engine:rpgmaker -->
<!-- engine:wolf -->
### Wolf file strategy (stub)
When analysing a WolfDawn / WOLF project later: use `wolf_json/` extracts (`names.json`, DB sheets, map/common scenes). Prefer foundation DB + narrative sheets; grep large map dumps. Same four-block ownership rules apply.
<!-- /engine:wolf -->
---
## Output format
Emit **only** labeled fenced code blocks (plus a one-line heading before each block for navigation).
No essay outside the blocks.
### Block `glossary`
Label the fence language as `glossary`. Inside:
```
# Game Characters
Japanese (English) - description
# Worldbuilding Terms
Japanese (English) - description
```
### Block `speakers`
Label the fence language as `speakers`. Inside:
```
Patterns detected:
- ...
Flag decisions:
INLINE401SPEAKERS : ENABLE|SKIP — <one-line reason>
FIRSTLINESPEAKERS : ENABLE|SKIP — <one-line reason>
FACENAME101 : ENABLE|SKIP — <one-line reason>
Examples:
- ...
Face filenames (only if FACENAME101 ENABLE):
- ...
```
### Block `translation_quirks`
Label the fence language as `translation_quirks`. Inside: imperative bullet list only (no preamble).
### Block `game_skill`
Label the fence language as `game_skill`. Inside: full markdown for `skills/translation.md` (identity, file map, quirks pointer, tool boundaries).
If known speakers were prepended in a `<known_speakers>` block above this skill, prefer those names in the glossary, then cross-check Actors.json for other major named actors.

View file

@ -30,6 +30,7 @@ You will be translating erotic and sexual content. You will receive lines of dia
- **Speech register:** If the entry describes how a character speaks (flustered, blunt, formal, childlike, crude, etc.), mirror that register in their English dialogue. A character described as speaking in a "cute, flustered register" should sound different from one described as "cold and terse".
- **Role & context:** Use the role/personality notes to inform tone — a villain's lines should feel threatening, a comic-relief NPC's lines should feel goofy, etc.
- Japanese omits pronouns constantly. Infer the correct subject and pronoun from context, translation history, and the character list.
- **Person / perspective consistency (1st / 2nd / 3rd):** Keep the grammatical person consistent with the source and with surrounding lines. Before writing English, verify what perspective the Japanese line actually uses - do not assume first-person dialogue when the line is third-person narration, second-person address (お前 / 君 / あなた), or impersonal UI text. Match "I / you / he / she / they" (and possessives) to that verified perspective; do not flip person mid-scene unless the Japanese does.
- **Preserve third-person self-reference.** Some characters refer to themselves by name instead of using "I" (e.g. ワタシ used as a name, or a character saying their own name). When a character is clearly speaking about themselves in the third person as a stylistic trait, maintain that in English (e.g. "Feris doesn't know" rather than "I don't know").
- Third-person pronouns (彼, 彼女, あいつ, こいつ, そいつ, コイツ) should match the known gender of the person being referenced.
- Translate **コイツ** as "this bastard" (male) or "this bitch" (female) depending on the referenced character's gender.
@ -38,7 +39,9 @@ You will be translating erotic and sexual content. You will receive lines of dia
## Honorifics and Names
- Preserve Japanese honorifics in the translation: -san, -kun, -chan, -senpai, -sensei, -sama, -dono, etc.
- **Always preserve and enforce Japanese honorifics** in the English translation: -san, -kun, -chan, -senpai, -sensei, -sama, -dono, etc.
- Do **not** drop honorifics to make the English sound more "natural" unless the source Japanese also lacks them.
- If `# Game Characters` (or game quirks) specifies how a character addresses someone, follow that address pattern.
- The `=` or `` character in a Japanese name marks a foreign or nickname component. Wrap that part in parentheses.
- Example: `バンカー=ベット``Bunker (Bet)`
- Always translate speaker tags to English: `[クロネ]:``[Kurone]:`

View file

@ -595,6 +595,7 @@ from gui.config_tab import ConfigTab
from gui.translation_tab import TranslationTab
from gui.workflow_tab import WorkflowTab
from gui.wolf_workflow_tab import WolfWorkflowTab
from gui.skills_tab import SkillsTab
from gui.batch_tab import BatchTab
class DazedMTLGUI(QMainWindow):
@ -838,9 +839,16 @@ class DazedMTLGUI(QMainWindow):
sidebar_layout.addWidget(btn_batches)
self.nav_buttons.append(btn_batches)
# Configuration button (fourth)
# Skills button (fourth)
btn_skills = self.create_nav_button("📚", "Skills")
btn_skills.setToolTip("Skills — edit system prompt, Project Setup, and translation contexts")
btn_skills.clicked.connect(lambda: self.switch_page(3))
sidebar_layout.addWidget(btn_skills)
self.nav_buttons.append(btn_skills)
# Configuration button (fifth)
btn_config = self.create_nav_button("⚙️", "Configuration")
btn_config.clicked.connect(lambda: self.switch_page(3))
btn_config.clicked.connect(lambda: self.switch_page(4))
sidebar_layout.addWidget(btn_config)
self.nav_buttons.append(btn_config)
@ -906,7 +914,11 @@ class DazedMTLGUI(QMainWindow):
self.batch_tab = BatchTab(self)
self.content_stack.addWidget(self.batch_tab)
# Configuration Tab (index 3)
# Skills Tab (index 3)
self.skills_tab = SkillsTab(self)
self.content_stack.addWidget(self.skills_tab)
# Configuration Tab (index 4)
self.config_tab = ConfigTab()
self.config_tab.config_changed.connect(self.on_config_changed)
self.content_stack.addWidget(self.config_tab)

247
gui/skills_tab.py Normal file
View file

@ -0,0 +1,247 @@
"""Skills Tab - edit tool-level skills and translation contexts under data/."""
from __future__ import annotations
import json
from pathlib import Path
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QFont
from PyQt5.QtWidgets import (
QHBoxLayout,
QLabel,
QMessageBox,
QPushButton,
QTabWidget,
QTextEdit,
QVBoxLayout,
QWidget,
)
from util.paths import PROMPT_PATH, SKILLS_DIR, TRANSLATION_CONTEXTS_PATH
from util.skills.contexts import reload_contexts
class _PlainPasteTextEdit(QTextEdit):
"""QTextEdit that always pastes as plain text."""
def insertFromMimeData(self, source): # noqa: N802
self.insertPlainText(source.text())
_EDITOR_STYLE = (
"QTextEdit{background-color:#1e1e1e;color:#d4d4d4;"
"border:1px solid #3c3c3c;border-radius:4px;padding:10px;"
"selection-background-color:#264f78;}"
)
_HINT_STYLE = "color:#9d9d9d;font-size:13px;"
_PATH_STYLE = "color:#569cd6;font-size:12px;font-family:Consolas,monospace;"
class SkillsTab(QWidget):
"""Edit ``data/skills/*.md`` and ``data/translation_contexts.json``."""
def __init__(self, parent=None):
super().__init__(parent)
self._pages: dict[str, dict] = {}
self._build_ui()
self.reload_all()
def _build_ui(self):
root = QVBoxLayout(self)
root.setContentsMargins(20, 16, 20, 16)
root.setSpacing(10)
title = QLabel("Skills")
title.setStyleSheet("color:#ffffff;font-size:18px;font-weight:bold;")
root.addWidget(title)
intro = QLabel(
"Tool-level AI instructions live here. "
"Per-game quirks and the game IDE skill are edited in Workflow Step 3 "
"(they travel with the game folder)."
)
intro.setWordWrap(True)
intro.setStyleSheet(_HINT_STYLE)
root.addWidget(intro)
self.tabs = QTabWidget()
self.tabs.setStyleSheet(
"QTabWidget::pane{border:1px solid #3c3c3c;border-radius:4px;top:-1px;}"
"QTabBar::tab{background:#2d2d30;color:#cccccc;padding:8px 14px;"
"border:1px solid #3c3c3c;border-bottom:none;margin-right:2px;}"
"QTabBar::tab:selected{background:#1e1e1e;color:#ffffff;"
"border-top:2px solid #007acc;}"
)
root.addWidget(self.tabs, 1)
self._add_file_page(
key="system",
tab_title="System",
path=PROMPT_PATH,
hint=(
"Runtime system prompt for every translation API call. "
"Honorifics, formatting, and universal quality rules live here. "
"Do not put game-specific voice quirks here - use <game>/skills/quirks.md."
),
is_json=False,
)
self._add_file_page(
key="project_setup",
tab_title="Project Setup",
path=SKILLS_DIR / "project_setup.md",
hint=(
"Clipboard skill copied from Workflow. "
"Instructs the IDE to emit glossary, speakers, quirks, and game_skill blocks."
),
is_json=False,
)
self._add_file_page(
key="contexts",
tab_title="Contexts",
path=TRANSLATION_CONTEXTS_PATH,
hint=(
"Per-call history templates used by RPG Maker / WolfDawn "
"(names.npc, database.item, events.label_108, …). "
"Edit carefully - keys must match what the engine modules look up."
),
is_json=True,
)
btn_row = QHBoxLayout()
reload_btn = QPushButton("↺ Reload All")
reload_btn.setFixedWidth(140)
reload_btn.clicked.connect(self.reload_all)
btn_row.addWidget(reload_btn)
btn_row.addStretch()
open_btn = QPushButton("Open skills folder")
open_btn.setFixedWidth(160)
open_btn.clicked.connect(self._open_skills_folder)
btn_row.addWidget(open_btn)
root.addLayout(btn_row)
def _add_file_page(
self,
*,
key: str,
tab_title: str,
path: Path,
hint: str,
is_json: bool,
):
page = QWidget()
layout = QVBoxLayout(page)
layout.setContentsMargins(12, 12, 12, 12)
layout.setSpacing(8)
path_lbl = QLabel(str(path))
path_lbl.setStyleSheet(_PATH_STYLE)
path_lbl.setTextInteractionFlags(Qt.TextSelectableByMouse)
layout.addWidget(path_lbl)
hint_lbl = QLabel(hint)
hint_lbl.setWordWrap(True)
hint_lbl.setStyleSheet(_HINT_STYLE)
layout.addWidget(hint_lbl)
editor = _PlainPasteTextEdit()
editor.setFont(QFont("Consolas", 10))
editor.setStyleSheet(_EDITOR_STYLE)
layout.addWidget(editor, 1)
row = QHBoxLayout()
save_btn = QPushButton("💾 Save")
save_btn.setFixedWidth(120)
save_btn.clicked.connect(lambda _=False, k=key: self._save_page(k))
row.addWidget(save_btn)
reload_btn = QPushButton("↺ Reload")
reload_btn.setFixedWidth(120)
reload_btn.clicked.connect(lambda _=False, k=key: self._reload_page(k))
row.addWidget(reload_btn)
status = QLabel("")
status.setStyleSheet("color:#6a9955;font-size:12px;")
row.addWidget(status)
row.addStretch()
layout.addLayout(row)
self.tabs.addTab(page, tab_title)
self._pages[key] = {
"path": path,
"editor": editor,
"status": status,
"is_json": is_json,
}
def reload_all(self):
for key in self._pages:
self._reload_page(key)
def _reload_page(self, key: str):
page = self._pages[key]
path: Path = page["path"]
editor: QTextEdit = page["editor"]
status: QLabel = page["status"]
try:
if not path.is_file():
editor.setPlainText("")
status.setText(f"Missing: {path.name}")
status.setStyleSheet("color:#f0ad4e;font-size:12px;")
return
editor.setPlainText(path.read_text(encoding="utf-8"))
status.setText("Loaded")
status.setStyleSheet("color:#6a9955;font-size:12px;")
except Exception as exc:
status.setText(f"Load failed: {exc}")
status.setStyleSheet("color:#f44747;font-size:12px;")
def _save_page(self, key: str):
page = self._pages[key]
path: Path = page["path"]
editor: QTextEdit = page["editor"]
status: QLabel = page["status"]
text = editor.toPlainText()
if page["is_json"]:
try:
parsed = json.loads(text)
text = json.dumps(parsed, ensure_ascii=False, indent=2) + "\n"
editor.setPlainText(text.rstrip("\n") + "\n")
except json.JSONDecodeError as exc:
QMessageBox.warning(
self,
"Invalid JSON",
f"Cannot save {path.name}:\n{exc}",
)
status.setText("Invalid JSON")
status.setStyleSheet("color:#f44747;font-size:12px;")
return
try:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(text if text.endswith("\n") else text + "\n", encoding="utf-8")
if key == "contexts":
reload_contexts()
status.setText(f"Saved {path.name}")
status.setStyleSheet("color:#6a9955;font-size:12px;")
except Exception as exc:
QMessageBox.warning(self, "Save failed", str(exc))
status.setText(f"Save failed: {exc}")
status.setStyleSheet("color:#f44747;font-size:12px;")
def _open_skills_folder(self):
folder = SKILLS_DIR
folder.mkdir(parents=True, exist_ok=True)
import os
import subprocess
import sys
try:
if sys.platform.startswith("win"):
os.startfile(str(folder)) # type: ignore[attr-defined]
elif sys.platform == "darwin":
subprocess.run(["open", str(folder)], check=False)
else:
subprocess.run(["xdg-open", str(folder)], check=False)
except Exception as exc:
QMessageBox.information(self, "Skills folder", f"{folder}\n\n{exc}")

View file

@ -3018,6 +3018,20 @@ class TranslationTab(QWidget):
except Exception:
pass
# Per-game quirks overlay: Workflow Step 0 folder → DAZED_GAME_ROOT
try:
game_root = ""
if self.settings:
game_root = str(
self.settings.value("last_game_folder", "") or ""
).strip()
if game_root and Path(game_root).is_dir():
os.environ["DAZED_GAME_ROOT"] = game_root
else:
os.environ.pop("DAZED_GAME_ROOT", None)
except Exception:
pass
# Try to create a hard link at legacy location so modules that
# still write to log/translationHistory.txt end up in this file.
# This will be created when the run_log_path file is first written to

View file

@ -5,13 +5,12 @@ Provides a guided, step-by-step interface:
Step 0 Select game project folder and import data files into files/
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)
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 and/or Forge playtest plugins
Step 2 Setup: speaker flags, Project Setup skill, vocab / quirks / game skill
Step 3 Translation: Phase 0 (DB), Phase 1 (dialogue), Phase 1b (111 cache)
Step 4 Translation Phase 2 (risky codes)
Step 5 Translate visible strings in js/plugins.js (or Ace scripts)
Step 6 Export translated/ back to the game folder
Step 7 Install TL Inspector and/or Forge playtest plugins
"""
from __future__ import annotations
@ -23,6 +22,11 @@ import threading
from pathlib import Path
from util.paths import VOCAB_PATH
from util.skills import (
game_skill_path_for_game,
load_project_setup,
quirks_path_for_game,
)
from util.vocab import BASE_SEPARATOR as _SHARED_BASE_SEPARATOR
from util.vocab import read_game_vocab, write_game_vocab
@ -619,13 +623,12 @@ class WorkflowTab(QWidget):
_tab_defs = [
("0 Project", self._build_step0),
("1 Pre-process", self._build_step1_preprocess),
("2 Speaker", self._build_step2_speaker),
("3 Glossary", self._build_step3_glossary),
("4 TL Phase 1", self._build_step4_translation),
("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),
("2 Setup", self._build_step2_setup),
("3 TL Phase 1", self._build_step4_translation),
("4 TL Phase 2", self._build_step5_tl_phase2),
("5 Plugins.js", self._build_step6_plugins_js),
("6 Export", self._build_step7_export),
("7 Playtest", self._build_step8_playtest),
]
self._step_labels = [label for label, _ in _tab_defs]
@ -793,21 +796,20 @@ class WorkflowTab(QWidget):
short_names = (
"Project",
"Prep",
"Speaker",
"Glossary",
"Setup",
"Phase1",
"Phase2",
"Plugins",
"Export",
"Playtest",
)
# Step 6 label swaps for Ace scripts.
if idx == 6 and len(self._step_labels) > 6:
raw = self._step_labels[6]
# Step 5 label swaps for Ace scripts.
if idx == 5 and len(self._step_labels) > 5:
raw = self._step_labels[5]
if "Script" in raw:
name = "Scripts"
else:
name = short_names[6]
name = short_names[5]
else:
name = short_names[idx] if 0 <= idx < len(short_names) else str(idx)
mark = "" if done else ""
@ -868,9 +870,9 @@ class WorkflowTab(QWidget):
if previous_index == 0 and index != 0:
self._auto_import_if_needed()
if index == 5:
if index == 4:
self._populate_p2_checkboxes()
if index == 8:
if index == 7:
self._refresh_playtest_status()
self._load_playtest_settings()
@ -1024,226 +1026,6 @@ class WorkflowTab(QWidget):
# ── Copilot prompt templates ────────────────────────────────────────────
_SPEAKER_PROMPT = (
"You are an expert RPGMaker MV/MZ analyst helping configure a Japanese game translation tool.\n"
"\n"
"<task>\n"
"Examine the game's event files (Map*.json, CommonEvents.json, Troops.json) and determine "
"which speaker name format(s) the game uses. Output flag recommendations based on what you find.\n"
"</task>\n"
"\n"
"<context>\n"
"Code 101 opens the text window. Code 401 is a dialogue line. Multiple 401s in a row form "
"one message box. The translation tool needs to know how character names appear in the event "
"data so it can extract and translate them correctly.\n"
"</context>\n"
"\n"
"--- attach your game files here before continuing ---\n"
"\n"
"<always_on_formats>\n"
"The following formats are detected and translated automatically — no flag is needed. "
"If the game uses any of these, do NOT enable a flag for lines matching that exact pattern. "
"However, continue scanning to check whether other speakers use a flag-requiring pattern.\n"
"\n"
"## How dialogue commands work\n"
"\n"
"Code 101 opens the text window. Code 401 is a dialogue line. "
"Multiple 401s in a row form one message box.\n"
"\n"
"## IMPORTANT — Always-on formats (NO FLAG NEEDED, never enable any flag for these)\n"
"\n"
"The following formats are detected and translated automatically. "
"If the game uses ANY of these, do NOT enable any flag for lines matching that pattern. "
"However, **DO NOT STOP** — continue checking whether other speakers in the same game "
"use a different format that requires a flag.\n"
"\n"
"\n"
" 101 param[4] name { \"code\": 101, \"parameters\": [\"\", 0, 2, 2, \"るな\"] }\n"
" \\\\n<Name> or \\\\k<Name> escape code with ANGLE BRACKETS anywhere inside a 401 line\n"
" 【Name】 alone on a line, or 【Name】dialogue on the same line\n"
" [Name] alone on a line, or [Name]dialogue on the same line\n"
" \\\\c[N]Name\\\\c[0] color-wrapped name on its own 401 line\n"
" Name line ending with a full-width colon\n"
"\n"
"## CRITICAL — \\\\N[X] / \\\\n[X] (square bracket + number) is NOT a speaker format\n"
"\n"
" \\\\N[ActorID] and \\\\n[ActorID] (e.g. \\\\N[1], \\\\n[2]) are RPGMaker actor variable\n"
" substitution codes. They expand to an actor's name at runtime, but the translation tool\n"
" handles them purely as text substitution during translation — they are NEVER treated as\n"
" a speaker name tag.\n"
"\n"
" A 401 line containing ONLY \\\\N[X], or narration text that embeds \\\\N[X] (e.g.\n"
" \"ローターが\\\\N[1]の乳首に装着される!\"), does NOT match any always-on speaker format.\n"
" Do NOT count \\\\N[X] / \\\\n[X] codes as always-on speaker detection hits.\n"
"\n"
" If a game's ONLY speaker indicator is a standalone \\\\N[X] line followed by dialogue,\n"
" check whether FIRSTLINESPEAKERS would catch it (the standalone line must be < 40 chars\n"
" AND contain at least one Japanese character — a bare \\\\N[1] with no Japanese text does\n"
" NOT qualify for FIRSTLINESPEAKERS either).\n"
"</always_on_formats>\n"
"\n"
"<flags>\n"
"Only enable these when at least some speakers do NOT use an always-on format.\n"
"\n"
"<flag name=\"INLINE401SPEAKERS\">\n"
"The speaker name is embedded at the start of a 401 line directly before 「, "
"with no intervening markup or brackets.\n"
"<example>\n"
"{ \"code\": 401, \"parameters\": [\"エレナ「今日は晴れですね。\"] }\n"
"</example>\n"
"Enable if the game has dialogue lines matching this pattern.\n"
"</flag>\n"
"\n"
"<flag name=\"FIRSTLINESPEAKERS\">\n"
"The very first 401 in a message group is a short standalone name (under 40 chars), "
"and the following 401 starts with 「 \" ( * [. This commonly appears for NPCs even "
"in games where the protagonist uses an always-on format. The 101 command for these "
"lines typically has an empty face image (parameters[0] == \"\").\n"
"<example>\n"
"{ \"code\": 101, \"parameters\": [\"\", 0, 0, 2] }\n"
"{ \"code\": 401, \"parameters\": [\"衛兵さん\"] } ← plain name, no color or brackets\n"
"{ \"code\": 401, \"parameters\": [\"「……起きたか」\"] }\n"
"</example>\n"
"Enable if ANY speakers in the game use this pattern, even if others use an always-on format.\n"
"</flag>\n"
"\n"
"<flag name=\"FACENAME101\" priority=\"last_resort\">\n"
"Enable ONLY when: (a) the game has no always-on format, AND (b) neither INLINE401SPEAKERS "
"nor FIRSTLINESPEAKERS applies, AND (c) the 101 code has a face image filename in "
"parameters[0] while parameters[4] is empty.\n"
"<example>\n"
"{ \"code\": 101, \"parameters\": [\"face_alice\", 0, 0, 2, \"\"] }\n"
"</example>\n"
"If you recommend this, you MUST list every unique face filename found in parameters[0] "
"of code 101 across all attached files.\n"
"</flag>\n"
"</flags>\n"
"\n"
"<instructions>\n"
"Follow these steps in order:\n"
"\n"
"1. Scan ALL 101→401 blocks across the sample files.\n"
" (a) List every always-on pattern found — these need no flag.\n"
" (b) List every flag-requiring pattern found separately.\n"
" A game may use both simultaneously (e.g. protagonist via \\\\c[N]Name\\\\c[0], "
"NPCs via plain standalone name lines). Do not stop after finding one pattern.\n"
"\n"
"2. For each flag-requiring pattern from step 1:\n"
" - INLINE401SPEAKERS → ENABLE if name「 inline pattern exists\n"
" - FIRSTLINESPEAKERS → ENABLE if plain standalone name line exists\n"
" Both may be enabled together if both patterns exist.\n"
"\n"
"3. Only if steps 12 found no flag-requiring patterns AND no always-on format "
"→ consider FACENAME101.\n"
"</instructions>\n"
"\n"
"<output_format>\n"
"Provide exactly four sections:\n"
"\n"
"1. Patterns detected — one entry per speaker type (protagonist, NPCs, signs/narration)\n"
"2. Flag decisions:\n"
" INLINE401SPEAKERS : ENABLE / SKIP — <one-line reason>\n"
" FIRSTLINESPEAKERS : ENABLE / SKIP — <one-line reason>\n"
" FACENAME101 : ENABLE / SKIP — <one-line reason>\n"
"3. A short concrete example from the actual files for each detected pattern\n"
"4. (Only if FACENAME101 is ENABLE) Every unique face filename from parameters[0] of code 101\n"
"</output_format>\n"
)
_PROMPT_GLOSSARY = (
"You are an expert Japanese RPGMaker game analyst building a translation glossary.\n"
"\n"
"<task>\n"
"Extract named characters and lore-specific worldbuilding terms from this game's data files. "
"Produce a structured glossary in the exact format specified below. It will be loaded directly "
"into a translation tool, so strict format compliance is required.\n"
"</task>\n"
"\n"
"--- attach your game data files here before continuing ---\n"
"\n"
"<file_strategy>\n"
"Map files and CommonEvents.json can be extremely large. Do NOT read them sequentially — "
"you will hit context limits. Use this strategy:\n"
"\n"
"1. Read these small DB files IN FULL first — richest source of names, always small:\n"
" Actors.json is mandatory for major character vocab. Use it as the canonical source "
"for actor IDs, Japanese names, nicknames, profiles, and \\N[n] name mappings.\n"
" Then read Classes.json, Troops.json, Skills.json, Items.json,\n"
" Armors.json, Weapons.json, States.json, System.json\n"
"\n"
"2. For large files (CommonEvents.json, Map*.json), SEARCH (grep) instead of reading "
"sequentially. Prioritise dialogue commands because they are the best evidence for "
"character voice:\n"
" - Code 401 dialogue lines, plus nearby code 101 speaker/name parameters\n"
" - Code 405 scrolling text when present\n"
" - Speaker patterns such as 【Name】, [Name], Name, \\\\n<Name>, and \\\\k<Name>\n"
" - Capitalised katakana clusters or kanji compound proper nouns in dialogue/parameters fields\n"
" Scan Map001.json through Map010.json at most — early maps have the most story content.\n"
"\n"
"3. Stop once you stop finding new names or terms. Do not pad the output.\n"
"</file_strategy>\n"
"\n"
"<rules>\n"
"Apply to both sections:\n"
"- Separator: use a plain hyphen-minus (-). Never use — or . "
"The translation tool only recognises the plain hyphen.\n"
"- Descriptions must be entirely in English. Refer to other characters by English name only "
"(write 'her sister Ruin was kidnapped', not 'her sister ルイン was kidnapped').\n"
"- Never give two spelling options (e.g. 'Sylfia / Sylphia' is wrong). Commit to one translation.\n"
"\n"
"# Game Characters — rules:\n"
"- If a <known_speakers> list was provided, output entries for those names, then cross-check "
"Actors.json for major named actors that should also be included. Skip unnamed NPCs, generic "
"enemies, and narration-only entries.\n"
"- If no list was provided, discover named characters from the files, but still skip "
"unnamed NPCs and generic enemy types.\n"
"- For each character include: gender, role, speech register, personality, and whether "
"the name is player-chosen (check Actors.json ID 1).\n"
"- Any actor in Actors.json with a real name should get a full # Game Characters entry, "
"not only a \\N[n] placeholder mapping. If events reference \\N[3] or [EnglishName], "
"resolve it through Actors.json ID 3 and preserve the full character context.\n"
"- Event references are supporting evidence for personality/speech, but they must not "
"replace actor-database discovery. Do not miss major characters just because they appear "
"primarily in Actors.json.\n"
"\n"
"# Worldbuilding Terms — rules:\n"
"- Include: faction/organisation names, locations mentioned in dialogue but not on maps, "
"unique magic systems, lore titles, recurring in-universe concepts.\n"
"- Exclude: skill names, item names, weapon names, armour names (the tool handles those). "
"Skip generic RPG words (\u30dd\u30fc\u30b7\u30e7\u30f3, \u30ec\u30d9\u30eb, \u30b9\u30c6\u30fc\u30bf\u30b9, etc.). "
"Do not repeat character names here.\n"
"</rules>\n"
"\n"
"<output_format>\n"
"Return the ENTIRE glossary inside ONE fenced code block (a ``` block), with nothing before "
"or after it, so it can be copied in a single click and pasted straight into the tool.\n"
"Inside the code block, output EXACTLY two sections with these headers. Do not add any "
"preamble, explanation, or text outside the entries.\n"
"\n"
"# Game Characters\n"
"# Worldbuilding Terms\n"
"\n"
"Entry format: Japanese (English) - description\n"
"\n"
"<example>\n"
"# Game Characters\n"
"アリア (Aria) - Female; protagonist; player-chosen name (Actors.json ID 1); "
"speaks cheerfully in casual feminine speech; nicknamed アリアちゃん by her sister\n"
"ゼクス (Zex) - Male; antagonist; cold and commanding; addresses others with contempt; "
"uses archaic formal register\n"
"カナエ (Kanae) - Female; NPC shopkeeper; warm and motherly; ends sentences with わね\n"
"\n"
"# Worldbuilding Terms\n"
"虚無の穴 (Void Rift) - Dimensional tear referenced repeatedly in Act 2 NPC dialogue; "
"not a named map location\n"
"鋼の誓約 (Iron Vow) - Sacred oath-binding ritual unique to the knightly order; "
"appears in story cutscenes\n"
"裁定者 (Arbiter) - Title held by the ruling council; lore-specific rank with no "
"real-world equivalent\n"
"</example>\n"
"</output_format>\n"
)
_WRAP_PROMPT = (
"You are an expert RPGMaker MV/MZ configuration analyst.\n"
"\n"
@ -1789,116 +1571,201 @@ class WorkflowTab(QWidget):
"QCheckBox{border:none;background-color:transparent;}"
)
def _build_step3_glossary(self, layout: QVBoxLayout):
layout.addWidget(_make_section_label("Step 3 — Vocab / Glossary"))
def _build_step2_setup(self, layout: QVBoxLayout):
"""Combined speaker flags + Project Setup + vocab/quirks/game-skill editors."""
layout.addWidget(_make_section_label("Step 2 — Setup (Speakers + Glossary)"))
# ---- 3a: Parse Speakers ---------------------------------------------
parse_title = QLabel("3a — Parse Speakers")
parse_title.setStyleSheet("color:#4ec9b0;font-size:13px;font-weight:bold;")
layout.addWidget(parse_title)
# ---- Compact action bar --------------------------------------------
top = QWidget()
top.setObjectName("tbox")
top.setStyleSheet(self._task_box_style())
top_l = QVBoxLayout(top)
top_l.setContentsMargins(10, 8, 10, 8)
top_l.setSpacing(6)
parse_box = QWidget()
parse_box.setObjectName("tbox")
parse_box.setStyleSheet(self._task_box_style())
parse_inner = QVBoxLayout(parse_box)
parse_inner.setContentsMargins(10, 8, 10, 8)
parse_inner.setSpacing(4)
parse_hint = QLabel(
"Scan all event files (Maps, CommonEvents, Troops) for speaker names and "
"batch-translate them, then write them into the # Speakers section of vocab.txt.\n"
"Run this before the full translation so the AI already knows every character name."
hint = QLabel(
"1) Set speaker flags · 2) Parse Speakers into vocab · "
"3) Copy Project Setup → IDE → paste glossary / quirks / game skill below"
)
parse_hint.setWordWrap(True)
parse_hint.setStyleSheet("color:#9d9d9d;font-size:13px;")
parse_inner.addWidget(parse_hint)
hint.setWordWrap(True)
hint.setStyleSheet("color:#9d9d9d;font-size:12px;")
top_l.addWidget(hint)
parse_row = QHBoxLayout()
parse_speakers_btn = _make_btn("🔍 Parse Speakers", "#5a3a7a")
parse_speakers_btn.setFixedWidth(180)
parse_speakers_btn.setToolTip(
"Sets mode to 'Parse Speakers', selects all event files, and starts the run.\n"
"No text is translated — only speaker names are collected and written to vocab.txt."
from gui import qt_icons
actions = QHBoxLayout()
actions.setSpacing(8)
import_btn = QPushButton()
qt_icons.apply_button_icon(import_btn, "↓ Import → files/", color="#4da8f0")
import_btn.setStyleSheet(
"QPushButton{background-color:#2d2d30;color:#4da8f0;border:1px solid #007acc;"
"padding:0px 10px;border-radius:4px;font-size:12px;font-weight:bold;"
"font-family:'Segoe UI',sans-serif;}"
"QPushButton:hover{background-color:#1a2d3a;border-color:#1a9aff;color:#7ac8ff;}"
"QPushButton:pressed{background-color:#0a1a2a;}"
"QPushButton:disabled{background-color:#2d2d30;color:#555555;border-color:#444444;}"
)
parse_speakers_btn.clicked.connect(self._run_parse_speakers)
parse_row.addWidget(parse_speakers_btn)
parse_row.addStretch()
parse_inner.addLayout(parse_row)
import_btn.setFixedHeight(32)
import_btn.setEnabled(False)
import_btn.clicked.connect(lambda _checked=False: self._import_files())
self._register_import_button(import_btn)
actions.addWidget(import_btn)
layout.addWidget(parse_box)
# ---- Copilot / Cursor prompt helpers --------------------------------
prompt_box_title = QLabel("3b — AI Prompt Helpers (Copilot / Cursor)")
prompt_box_title.setStyleSheet("color:#4ec9b0;font-size:13px;font-weight:bold;")
layout.addWidget(prompt_box_title)
prompt_box = QWidget()
prompt_box.setObjectName("tbox")
prompt_box.setStyleSheet(self._task_box_style())
pb_inner = QVBoxLayout(prompt_box)
pb_inner.setContentsMargins(10, 8, 10, 8)
pb_inner.setSpacing(4)
prompt_hint = QLabel(
"Copy the prompt and paste it into Copilot Chat or Cursor with your game files open. "
"Paste the AI's output back into vocab.txt."
clear_translated_btn = QPushButton()
qt_icons.apply_button_icon(clear_translated_btn, "✕ Clear TL", color="#cc4444")
clear_translated_btn.setStyleSheet(
"QPushButton{background-color:#2d2d30;color:#cc4444;border:1px solid #8b0000;"
"padding:0px 10px;border-radius:4px;font-size:12px;font-weight:bold;"
"font-family:'Segoe UI',sans-serif;}"
"QPushButton:hover{background-color:#3a2020;border-color:#cc2222;color:#ff6666;}"
"QPushButton:pressed{background-color:#4a1010;}"
)
prompt_hint.setWordWrap(True)
prompt_hint.setStyleSheet("color:#9d9d9d;font-size:13px;")
pb_inner.addWidget(prompt_hint)
clear_translated_btn.setFixedHeight(32)
clear_translated_btn.setToolTip("Delete all files inside the translated/ folder")
clear_translated_btn.clicked.connect(self._clear_translated)
actions.addWidget(clear_translated_btn)
glossary_row = QHBoxLayout()
copy_glossary_btn = _make_btn("📋 Copy Prompt for Copilot", "#555")
copy_glossary_btn.setFixedWidth(220)
copy_glossary_btn.setToolTip("Copy the full glossary discovery prompt to clipboard")
copy_glossary_btn.clicked.connect(self._copy_glossary_prompt)
glossary_row.addWidget(copy_glossary_btn)
glossary_row.addStretch()
pb_inner.addLayout(glossary_row)
layout.addWidget(prompt_box)
# ---- vocab.txt editor -----------------------------------------------
vocab_title = QLabel("3c — vocab.txt editor")
vocab_title.setStyleSheet("color:#4ec9b0;font-size:13px;font-weight:bold;")
layout.addWidget(vocab_title)
format_hint = QLabel(
"Format: Japanese (English) - Gender; role; speech register / personality notes\n"
"Example: シロ (Shiro) - Female; protagonist; speaks in a flustered, cute register with feminine speech markers"
parse_btn = _make_btn("🔍 Parse Speakers", "#5a3a7a")
parse_btn.setFixedWidth(160)
parse_btn.setFixedHeight(32)
parse_btn.setToolTip(
"Collect speaker names from event files into vocab.txt # Speakers "
"(no dialogue translation)."
)
format_hint.setFont(QFont("Consolas", 9))
format_hint.setStyleSheet(
"color:#569cd6;background-color:#1a1e2a;border:1px solid #2a3a5a;"
"padding:5px 10px;border-radius:4px;font-size:13px;"
"border-left:3px solid #2a6a9a;"
)
layout.addWidget(format_hint)
parse_btn.clicked.connect(self._run_parse_speakers)
actions.addWidget(parse_btn)
self.vocab_editor = _PlainPasteTextEdit()
self.vocab_editor.setMinimumHeight(80)
self.vocab_editor.setMaximumHeight(160)
self.vocab_editor.setFont(QFont("Consolas", 9))
self.vocab_editor.setStyleSheet(
"QTextEdit{background-color:#252526;color:#d4d4d4;"
"border:1px solid #3c3c3c;border-radius:4px;padding:8px;"
copy_btn = _make_btn("📋 Copy Project Setup", "#555")
copy_btn.setFixedWidth(190)
copy_btn.setFixedHeight(32)
copy_btn.setToolTip(
"Clipboard skill for the game repo IDE. Returns glossary, speakers, "
"translation_quirks, and game_skill code blocks."
)
copy_btn.clicked.connect(self._copy_project_setup_prompt)
actions.addWidget(copy_btn)
actions.addStretch()
top_l.addLayout(actions)
flags_row = QHBoxLayout()
flags_row.setSpacing(14)
flags_lbl = QLabel("Flags:")
flags_lbl.setStyleSheet("color:#4ec9b0;font-size:12px;font-weight:bold;")
flags_row.addWidget(flags_lbl)
self.spk_inline_cb = QCheckBox("INLINE401SPEAKERS")
self.spk_inline_cb.setToolTip("Speaker name inline before 「 in the 401 text")
self.spk_inline_cb.stateChanged.connect(self._apply_speaker_flags)
flags_row.addWidget(self.spk_inline_cb)
self.spk_firstline_cb = QCheckBox("FIRSTLINESPEAKERS")
self.spk_firstline_cb.setToolTip("First 401 line is a short speaker name")
self.spk_firstline_cb.stateChanged.connect(self._apply_speaker_flags)
flags_row.addWidget(self.spk_firstline_cb)
self.spk_face_cb = QCheckBox("FACENAME101")
self.spk_face_cb.setToolTip("Speaker inferred from 101 face-image filename")
self.spk_face_cb.stateChanged.connect(self._apply_speaker_flags)
flags_row.addWidget(self.spk_face_cb)
flags_row.addStretch()
top_l.addLayout(flags_row)
layout.addWidget(top)
self._populate_speaker_flags()
# ---- Editors (tabbed to save vertical space) -----------------------
editors = QTabWidget()
editors.setObjectName("setupEditors")
editors.setDocumentMode(False)
tab_bar = editors.tabBar()
tab_bar.setExpanding(False)
tab_bar.setMinimumHeight(34)
tab_bar.setStyleSheet(
"QTabBar::tab{background:#333337;color:#c0c0c0;min-width:90px;min-height:28px;"
"padding:6px 16px;border:1px solid #3c3c3c;border-bottom:none;"
"margin-right:3px;font-size:12px;font-weight:bold;}"
"QTabBar::tab:selected{background:#1e1e1e;color:#4ec9b0;"
"border-top:2px solid #4ec9b0;}"
"QTabBar::tab:hover{color:#ffffff;}"
)
editors.setStyleSheet(
"QTabWidget::pane{border:1px solid #3c3c3c;background:#1e1e1e;}"
)
_ed_ss = (
"QTextEdit{background-color:#1e1e1e;color:#d4d4d4;"
"border:none;padding:8px;"
"selection-background-color:#264f78;}"
)
def _editor_page(path_hint: str, tip: str, save_slot, reload_slot, attr: str):
page = QWidget()
vl = QVBoxLayout(page)
vl.setContentsMargins(8, 8, 8, 8)
vl.setSpacing(6)
tip_lbl = QLabel(tip)
tip_lbl.setWordWrap(True)
tip_lbl.setStyleSheet("color:#9d9d9d;font-size:12px;")
vl.addWidget(tip_lbl)
path_lbl = QLabel(path_hint)
path_lbl.setStyleSheet("color:#569cd6;font-size:11px;font-family:Consolas,monospace;")
vl.addWidget(path_lbl)
ed = _PlainPasteTextEdit()
ed.setMinimumHeight(160)
ed.setFont(QFont("Consolas", 9))
ed.setStyleSheet(_ed_ss)
setattr(self, attr, ed)
vl.addWidget(ed, 1)
row = QHBoxLayout()
row.setSpacing(8)
save_btn = _make_btn("💾 Save", "#3a7a3a")
save_btn.setFixedWidth(110)
save_btn.clicked.connect(save_slot)
row.addWidget(save_btn)
reload_btn = _make_btn("↺ Reload", "#555")
reload_btn.setFixedWidth(110)
reload_btn.clicked.connect(reload_slot)
row.addWidget(reload_btn)
row.addStretch()
vl.addLayout(row)
return page
editors.addTab(
_editor_page(
"data/vocab.txt (game section)",
"Paste the glossary code block. Format: Japanese (English) - notes",
self._save_vocab,
self._reload_vocab,
"vocab_editor",
),
"Vocab",
)
editors.addTab(
_editor_page(
"<game>/skills/quirks.md",
"Paste the translation_quirks block. Merged onto the system prompt at translate time.",
self._save_quirks,
self._reload_quirks,
"quirks_editor",
),
"Quirks",
)
editors.addTab(
_editor_page(
"<game>/skills/translation.md",
"Paste the game_skill block. IDE companion skill for this game repo.",
self._save_game_skill,
self._reload_game_skill,
"game_skill_editor",
),
"Game skill",
)
layout.addWidget(editors, 1)
self._reload_vocab()
layout.addWidget(self.vocab_editor, 1)
self._reload_quirks()
self._reload_game_skill()
row = QHBoxLayout()
row.setSpacing(8)
save_vocab = _make_btn("💾 Save vocab.txt", "#3a7a3a")
save_vocab.setFixedWidth(180)
save_vocab.clicked.connect(self._save_vocab)
row.addWidget(save_vocab)
reload_vocab = _make_btn("↺ Reload", "#555")
reload_vocab.setFixedWidth(110)
reload_vocab.clicked.connect(self._reload_vocab)
row.addWidget(reload_vocab)
row.addStretch()
layout.addLayout(row)
def _run_parse_speakers(self):
"""Configure Translation tab for Parse Speakers mode and auto-start."""
@ -1948,102 +1815,6 @@ class WorkflowTab(QWidget):
# ── Step 2: Speaker Detection ───────────────────────────────────────────
def _build_step2_speaker(self, layout: QVBoxLayout):
layout.addWidget(_make_section_label("Step 2 — Speaker Format Detection"))
# Import button row
_IMP_W = 220
_IMP_H = 36
_IMP_SS = (
"QPushButton{{"
"background-color:{bg};color:white;border:none;"
"padding:0px;border-radius:4px;font-size:12px;font-weight:bold;"
"font-family:'Segoe UI',sans-serif;}}"
"QPushButton:hover{{background-color:{hover};}}"
"QPushButton:pressed{{background-color:{press};}}"
"QPushButton:disabled{{background-color:#404040;color:#666666;}}"
)
import_row = QHBoxLayout()
import_row.setSpacing(8)
import_row.setAlignment(Qt.AlignVCenter)
from gui import qt_icons
import_btn = QPushButton()
qt_icons.apply_button_icon(import_btn, "↓ Import Selected → files/", color="#4da8f0")
import_btn.setStyleSheet(
"QPushButton{background-color:#2d2d30;color:#4da8f0;border:1px solid #007acc;"
"padding:0px;border-radius:4px;font-size:12px;font-weight:bold;"
"font-family:'Segoe UI',sans-serif;}"
"QPushButton:hover{background-color:#1a2d3a;border-color:#1a9aff;color:#7ac8ff;}"
"QPushButton:pressed{background-color:#0a1a2a;}"
"QPushButton:disabled{background-color:#2d2d30;color:#555555;border-color:#444444;}"
)
import_btn.setFixedSize(_IMP_W, _IMP_H)
import_btn.setEnabled(False)
import_btn.clicked.connect(lambda _checked=False: self._import_files())
self._register_import_button(import_btn)
import_row.addWidget(import_btn)
clear_translated_btn = QPushButton()
qt_icons.apply_button_icon(clear_translated_btn, "✕ Clear translated/", color="#cc4444")
clear_translated_btn.setStyleSheet(
"QPushButton{background-color:#2d2d30;color:#cc4444;border:1px solid #8b0000;"
"padding:0px;border-radius:4px;font-size:12px;font-weight:bold;"
"font-family:'Segoe UI',sans-serif;}"
"QPushButton:hover{background-color:#3a2020;border-color:#cc2222;color:#ff6666;}"
"QPushButton:pressed{background-color:#4a1010;}"
)
clear_translated_btn.setFixedSize(_IMP_W, _IMP_H)
clear_translated_btn.setToolTip("Delete all files inside the translated/ folder")
clear_translated_btn.clicked.connect(self._clear_translated)
import_row.addWidget(clear_translated_btn)
import_row.addStretch()
layout.addLayout(import_row)
hint = QLabel(
"Copy the prompt below, open Copilot (or any AI), attach a few of the game's "
"Map*.json / CommonEvents.json files, paste the prompt, and ask it which "
"speaker flags to enable. Then tick the matching boxes and click Apply."
)
hint.setWordWrap(True)
hint.setStyleSheet("color:#9d9d9d;font-size:13px;padding-bottom:6px;")
layout.addWidget(hint)
copy_row = QHBoxLayout()
copy_btn = _make_btn("📋 Copy Prompt for Copilot", "#555")
copy_btn.setFixedWidth(220)
copy_btn.setToolTip("Copies a prompt explaining all speaker formats — paste into Copilot with game files")
copy_btn.clicked.connect(self._copy_speaker_prompt)
copy_row.addWidget(copy_btn)
copy_row.addStretch()
layout.addLayout(copy_row)
# ---- Flag checkboxes ------------------------------------------------
cb_box_title = QLabel("Speaker flags")
cb_box_title.setStyleSheet("color:#4ec9b0;font-size:13px;font-weight:bold;")
layout.addWidget(cb_box_title)
cb_box = QWidget()
cb_box.setObjectName("tbox")
cb_box.setStyleSheet(self._task_box_style())
cb_inner = QVBoxLayout(cb_box)
cb_inner.setContentsMargins(10, 8, 10, 8)
cb_inner.setSpacing(3)
self.spk_inline_cb = QCheckBox("INLINE401SPEAKERS — speaker name inline before 「 in the 401 text")
self.spk_inline_cb.stateChanged.connect(self._apply_speaker_flags)
cb_inner.addWidget(self.spk_inline_cb)
self.spk_firstline_cb = QCheckBox("FIRSTLINESPEAKERS — first 401 line is a short speaker name")
self.spk_firstline_cb.stateChanged.connect(self._apply_speaker_flags)
cb_inner.addWidget(self.spk_firstline_cb)
self.spk_face_cb = QCheckBox("FACENAME101 — speaker inferred from 101 face-image filename")
self.spk_face_cb.stateChanged.connect(self._apply_speaker_flags)
cb_inner.addWidget(self.spk_face_cb)
layout.addWidget(cb_box)
self._populate_speaker_flags()
# ── Step 4: Translation ─────────────────────────────────────────────────
def _add_tl_mode_selector(self, layout: QVBoxLayout):
@ -2113,7 +1884,7 @@ class WorkflowTab(QWidget):
def _build_step4_translation(self, layout: QVBoxLayout):
layout.addWidget(_make_section_label("Step 4 — TL Phase 1"))
layout.addWidget(_make_section_label("Step 3 — TL Phase 1"))
self._add_tl_mode_selector(layout)
@ -2265,7 +2036,7 @@ class WorkflowTab(QWidget):
def _build_step5_tl_phase2(self, layout: QVBoxLayout):
layout.addWidget(_make_section_label("Step 5 — TL Phase 2"))
layout.addWidget(_make_section_label("Step 4 — TL Phase 2"))
self._step5_mode_hint = QLabel(f"Translation mode: {BATCH_MODE_LABEL}")
self._step5_mode_hint.setStyleSheet("color:#8fbc8f;font-size:12px;margin-bottom:4px;")
@ -2571,7 +2342,7 @@ class WorkflowTab(QWidget):
# ── Step 5: plugins.js / Ace scripts Translation ───────────────────────
def _build_step6_plugins_js(self, layout: QVBoxLayout):
self._step6_section_label = _make_section_label("Step 6 — Translate plugins.js")
self._step6_section_label = _make_section_label("Step 5 — Translate plugins.js")
layout.addWidget(self._step6_section_label)
self._step6_hint = QLabel(
@ -2612,7 +2383,7 @@ class WorkflowTab(QWidget):
# ── Step 6: Export ──────────────────────────────────────────────────────
def _build_step7_export(self, layout: QVBoxLayout):
layout.addWidget(_make_section_label("Step 7 — Export to Game"))
layout.addWidget(_make_section_label("Step 6 — Export to Game"))
hint = QLabel(
"Copy translated files back into the game's data folder to patch the game in-place."
)
@ -2643,7 +2414,7 @@ class WorkflowTab(QWidget):
# ── Step 8: Playtest (TL Inspector) ─────────────────────────────────────
def _build_step8_playtest(self, layout: QVBoxLayout):
self._step8_section_label = _make_section_label("Step 8 — Playtest Tools")
self._step8_section_label = _make_section_label("Step 7 — Playtest Tools")
layout.addWidget(self._step8_section_label)
self._step8_main_hint = QLabel(
@ -3257,6 +3028,8 @@ class WorkflowTab(QWidget):
self._detected_on_show = True # new folder chosen — treat as already-shown
self._ask_clear_old_files()
self._detect_folder()
self._reload_quirks()
self._reload_game_skill()
def _ask_clear_old_files(self):
"""Prompt the user to clear /files and /translated to avoid stale data conflicts."""
@ -3310,18 +3083,18 @@ class WorkflowTab(QWidget):
w = getattr(self, attr, None)
if w is not None:
w.setVisible(not is_ace)
# Tab / strip label
step6_label = "6 Scripts" if is_ace else "6 Plugins.js"
self._step_tabs.setTabText(6, step6_label)
if hasattr(self, "_step_labels") and len(self._step_labels) > 6:
self._step_labels[6] = step6_label
if hasattr(self, "_step_buttons") and len(self._step_buttons) > 6:
self._step_buttons[6].setToolTip(step6_label)
# Step 6 section header
# Tab / strip label (Plugins step is index 5 after Setup merge)
step6_label = "5 Scripts" if is_ace else "5 Plugins.js"
self._step_tabs.setTabText(5, step6_label)
if hasattr(self, "_step_labels") and len(self._step_labels) > 5:
self._step_labels[5] = step6_label
if hasattr(self, "_step_buttons") and len(self._step_buttons) > 5:
self._step_buttons[5].setToolTip(step6_label)
# Step 5 section header
lbl = getattr(self, "_step6_section_label", None)
if lbl is not None:
lbl.setText("Step 6 — Translate Ace Scripts (.rb)" if is_ace
else "Step 6 — Translate plugins.js")
lbl.setText("Step 5 — Translate Ace Scripts (.rb)" if is_ace
else "Step 5 — Translate plugins.js")
# Hint text
hint = getattr(self, "_step6_hint", None)
if hint is not None:
@ -3352,18 +3125,18 @@ 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; hidden for Ace)
# Step 7 — TL Inspector (MV/MZ only; hidden for Ace)
show_playtest = not is_ace
if hasattr(self, "_step_tabs") and self._step_tabs.count() > 8:
if hasattr(self, "_step_tabs") and self._step_tabs.count() > 7:
if hasattr(self._step_tabs, "setTabVisible"):
self._step_tabs.setTabVisible(8, show_playtest)
self._step_tabs.setTabVisible(7, show_playtest)
else:
self._step_tabs.setTabEnabled(8, show_playtest)
if is_ace and self._step_tabs.currentIndex() == 8:
self._goto_step(7)
if hasattr(self, "_step_buttons") and len(self._step_buttons) > 8:
self._step_buttons[8].setVisible(show_playtest)
self._step_buttons[8].setEnabled(show_playtest)
self._step_tabs.setTabEnabled(7, show_playtest)
if is_ace and self._step_tabs.currentIndex() == 7:
self._goto_step(6)
if hasattr(self, "_step_buttons") and len(self._step_buttons) > 7:
self._step_buttons[7].setVisible(show_playtest)
self._step_buttons[7].setEnabled(show_playtest)
self._refresh_step_strip()
box = getattr(self, "_step8_playtest_box", None)
install_both_btn = getattr(self, "_install_both_btn", None)
@ -3718,24 +3491,81 @@ class WorkflowTab(QWidget):
_BASE_SEPARATOR = _SHARED_BASE_SEPARATOR
def _copy_glossary_prompt(self):
"""Copy the glossary prompt to clipboard, injecting known speakers from vocab.txt."""
speakers = self._read_vocab_speakers()
if speakers:
speaker_lines = "\n".join(f" {orig} ({tl})" for orig, tl in speakers)
known_block = (
"<known_speakers>\n"
"These character names were extracted from the game files by the Parse Speakers tool.\n"
"For the '# Game Characters' section, output entries for ONLY these names.\n"
"Skip any unnamed NPCs, generic enemies, or narration-only entries.\n"
"\n"
+ speaker_lines
+ "\n</known_speakers>\n\n"
)
final_prompt = known_block + self._PROMPT_GLOSSARY
else:
final_prompt = self._PROMPT_GLOSSARY
self._copy_to_clipboard(final_prompt, "Glossary prompt copied.")
def _copy_project_setup_prompt(self):
"""Copy the Project Setup skill, optionally prepending known speakers."""
try:
speakers = self._read_vocab_speakers()
prepend = ""
if speakers:
speaker_lines = "\n".join(f" {orig} ({tl})" for orig, tl in speakers)
prepend = (
"<known_speakers>\n"
"These character names were extracted from the game files by the Parse Speakers tool.\n"
"For the glossary block '# Game Characters', prefer entries for these names, "
"then cross-check Actors.json for other major named actors.\n"
"\n"
+ speaker_lines
+ "\n</known_speakers>\n"
)
prompt = load_project_setup("rpgmaker", prepend=prepend)
self._copy_to_clipboard(prompt, "Project Setup skill copied.")
except Exception as exc:
self._log(f"❌ Could not load Project Setup skill: {exc}")
def _game_root_or_warn(self) -> str | None:
root = self.folder_edit.text().strip()
if not root:
self._log("⚠ Select a game folder in Step 0 first.")
return None
return root
def _reload_quirks(self):
root = self.folder_edit.text().strip()
path = quirks_path_for_game(root)
if not path or not path.is_file():
if hasattr(self, "quirks_editor"):
self.quirks_editor.setPlainText("")
return
try:
self.quirks_editor.setPlainText(path.read_text(encoding="utf-8"))
except Exception as exc:
self._log(f"❌ Could not load skills/quirks.md: {exc}")
def _save_quirks(self):
root = self._game_root_or_warn()
if not root:
return
path = quirks_path_for_game(root)
try:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(self.quirks_editor.toPlainText().rstrip() + "\n", encoding="utf-8")
self._log(f"✅ Saved {path}")
except Exception as exc:
self._log(f"❌ Could not save skills/quirks.md: {exc}")
def _reload_game_skill(self):
root = self.folder_edit.text().strip()
path = game_skill_path_for_game(root)
if not path or not path.is_file():
if hasattr(self, "game_skill_editor"):
self.game_skill_editor.setPlainText("")
return
try:
self.game_skill_editor.setPlainText(path.read_text(encoding="utf-8"))
except Exception as exc:
self._log(f"❌ Could not load game skill: {exc}")
def _save_game_skill(self):
root = self._game_root_or_warn()
if not root:
return
path = game_skill_path_for_game(root)
try:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(self.game_skill_editor.toPlainText().rstrip() + "\n", encoding="utf-8")
self._log(f"✅ Saved {path}")
except Exception as exc:
self._log(f"❌ Could not save game skill: {exc}")
def _read_vocab_speakers(self) -> list[tuple[str, str]]:
"""Parse the '# Speakers' section from vocab.txt and return (orig, tl) pairs."""
@ -3790,8 +3620,8 @@ class WorkflowTab(QWidget):
# ─────────────────────────────────────────────────────────────────────────
def _copy_speaker_prompt(self):
QApplication.clipboard().setText(self._SPEAKER_PROMPT)
self._log("Speaker format prompt copied to clipboard.")
# Legacy alias — Project Setup covers speakers analysis.
self._copy_project_setup_prompt()
def _copy_wrap_prompt(self):
QApplication.clipboard().setText(self._WRAP_PROMPT)

View file

@ -15,14 +15,15 @@ from retry import retry
from tqdm import tqdm
from util.translation import TranslationConfig, translateAI as sharedtranslateAI, getPricingConfig, calculateCost, getPricingConfig, calculateCost, get_var_translation, set_var_translations_batch, convert_corner_brackets
from util.speakers import SPEAKER_BRACKET_INNER, strip_speaker_prefix
from util.skills import ctx, load_system_prompt
from util.paths import VOCAB_PATH
# Globals
MODEL = os.getenv("model")
TIMEOUT = int(os.getenv("timeout"))
LANGUAGE = os.getenv("language").capitalize()
from util.paths import PROMPT_PATH, VOCAB_PATH
PROMPT = PROMPT_PATH.read_text(encoding="utf-8")
PROMPT = load_system_prompt()
VOCAB = VOCAB_PATH.read_text(encoding="utf-8")
LOCK = threading.Lock()
THREAD_CTX = threading.local()
@ -240,7 +241,7 @@ def _pat355655_captured_text(match):
def handleMVMZ(filename, estimate):
global ESTIMATE, TOKENS, FILENAME, MISMATCH
global ESTIMATE, TOKENS, FILENAME, MISMATCH, PROMPT
ESTIMATE = estimate
FILENAME = filename
MISMATCH = [] # Reset per-file; prevents cross-file contamination in CLI mode
@ -250,6 +251,10 @@ def handleMVMZ(filename, estimate):
except Exception:
pass
# Reload base prompt + optional per-game quirks (DAZED_GAME_ROOT).
PROMPT = load_system_prompt()
TRANSLATION_CONFIG.prompt = PROMPT
# Translate
start = time.time()
translatedData = openFiles(filename)
@ -979,7 +984,7 @@ def parseMap(data, filename):
if "Map" in filename:
response = translateAI(
data["displayName"],
"Reply with only the " + LANGUAGE + " translation of the RPG location name",
ctx("names.location"),
False,
)
totalTokens[0] += response[1][0]
@ -1097,7 +1102,7 @@ def _normalize_sg_desc(text: str) -> str:
# Translate
response = translateAI(
modifiedJAString,
"Reply with only the " + LANGUAGE + " translation.",
ctx("database.generic"),
False,
)
translatedText = response[0]
@ -1132,7 +1137,7 @@ def translateNoteOmitSpace(event, regex):
# Translate
response = translateAI(
jaString,
"Reply with the " + LANGUAGE + " translation of the location name.",
ctx("names.location_short"),
False,
)
# Defend against unexpected response shapes
@ -1181,7 +1186,7 @@ def translateLBNames(events):
names_to_translate = [item[1] for item in lb_events]
response = translateAI(
names_to_translate,
"Reply with only the " + LANGUAGE + " translation of the name.",
ctx("names.generic"),
True,
)
translated_names = response[0] if isinstance(response[0], list) else [response[0]]
@ -1670,21 +1675,21 @@ def searchNames(data, pbar, context, filename):
# Set the context of what we are translating
if "Actors" in context:
newContext = "Reply with only the " + LANGUAGE + " translation of the NPC name"
newContext = ctx("names.npc_only")
if "Armors" in context:
newContext = "Reply with only the " + LANGUAGE + " translation of the RPG equipment name"
newContext = ctx("database.armor")
if "Classes" in context:
newContext = "Reply with only the " + LANGUAGE + " translation of the RPG class name"
newContext = ctx("database.class")
if "MapInfos" in context:
newContext = "Reply with only the " + LANGUAGE + " translation of the location name"
newContext = ctx("names.location")
if "Enemies" in context:
newContext = "Reply with only the " + LANGUAGE + " translation of the enemy NPC name"
newContext = ctx("names.enemy")
if "Weapons" in context:
newContext = "Reply with only the " + LANGUAGE + " translation of the RPG weapon name"
newContext = ctx("database.weapon")
if "Items" in context:
newContext = "Reply with only the " + LANGUAGE + " translation of the RPG item name"
newContext = ctx("database.item")
if "Skills" in context:
newContext = "Reply with only the " + LANGUAGE + " translation of the RPG skill name"
newContext = ctx("database.skill")
# Names
with open("log/translations.txt", "a", encoding="utf-8") as file:
@ -1753,7 +1758,7 @@ def searchNames(data, pbar, context, filename):
# --- Batch translate all notes ---
translatedNotesBatch = []
if notesBatch:
response = translateAI(notesBatch, f"Reply with only the {LANGUAGE} translation of the note text.")
response = translateAI(notesBatch, ctx("database.note"))
translatedNotesBatch = response[0]
totalTokens[0] += response[1][0]
totalTokens[1] += response[1][1]
@ -2002,7 +2007,7 @@ def searchNames(data, pbar, context, filename):
if descriptionList:
response = translateAI(
descriptionList,
f"Reply with only the {LANGUAGE} translation of the text.",
ctx("events.generic_text"),
True,
)
translatedDescriptionBatch = response[0]
@ -2954,7 +2959,7 @@ def searchCodes(page, pbar, jobList, filename):
question = codeList[i]["parameters"][3]["messageText"]
response = translateAI(
matchList,
f"Previous text for context: {question}\n",
ctx("events.plugin_with_context", context=question),
True,
)
totalTokens[0] += response[1][0]
@ -3716,7 +3721,7 @@ def searchCodes(page, pbar, jobList, filename):
# Translate
response = translateAI(
matchList[0],
"Reply with the " + LANGUAGE + " translation of the NPC name.",
ctx("names.npc"),
False,
)
translatedText = response[0]
@ -3812,7 +3817,7 @@ def searchCodes(page, pbar, jobList, filename):
if len(matchList) > 0:
# Translate
text = matchList[0]
response = translateAI(text, "Reply with the " + LANGUAGE + " Translation")
response = translateAI(text, ctx("events.generic"))
translatedText = response[0]
totalTokens[0] += response[1][0]
totalTokens[1] += response[1][1]
@ -3851,7 +3856,7 @@ def searchCodes(page, pbar, jobList, filename):
if len(matchList) > 0:
# Translate
text = matchList[0]
response = translateAI(text, "Reply with the " + LANGUAGE + " Translation")
response = translateAI(text, ctx("events.generic"))
translatedText = response[0]
totalTokens[0] += response[1][0]
totalTokens[1] += response[1][1]
@ -3894,7 +3899,7 @@ def searchCodes(page, pbar, jobList, filename):
question = translatedText
response = translateAI(
choiceList,
f"Previous text for context: {question}\n",
ctx("events.plugin_with_context", context=question),
True,
)
totalTokens[0] += response[1][0]
@ -3949,11 +3954,11 @@ def searchCodes(page, pbar, jobList, filename):
if len(textHistory) > 0:
response = translateAI(
choiceList,
f"Reply with the English translation of the dialogue choice.\n\nPrevious text for context: {str(textHistory)}\n",
ctx("events.choice_with_context", context=str(textHistory)),
True,
)
else:
response = translateAI(choiceList, "Reply with the English translation of the dialogue choice.")
response = translateAI(choiceList, ctx("events.choice"))
translatedTextList = response[0]
totalTokens[0] += response[1][0]
@ -4143,7 +4148,7 @@ def searchCodes(page, pbar, jobList, filename):
# 122
if len(list122) > 0:
response = translateAI(list122, "Keep your translation as brief as possible")
response = translateAI(list122, ctx("events.code122_brief"))
list122TL = response[0]
totalTokens[0] += response[1][0]
totalTokens[1] += response[1][1]
@ -4168,7 +4173,7 @@ def searchCodes(page, pbar, jobList, filename):
# 108
if len(list108) > 0:
response = translateAI(list108, "This text is a label. Use title capitalization and keep it brief.")
response = translateAI(list108, ctx("events.label_108"))
list108TL = response[0]
totalTokens[0] += response[1][0]
totalTokens[1] += response[1][1]
@ -4224,7 +4229,7 @@ def searchCodes(page, pbar, jobList, filename):
# 324
if len(list324) > 0:
# Generic short-text translation for parameter index 1
response = translateAI(list324, "Reply with only the " + LANGUAGE + " translation of the text.")
response = translateAI(list324, ctx("events.generic_text"))
list324TL = response[0]
totalTokens[0] += response[1][0]
totalTokens[1] += response[1][1]
@ -4236,7 +4241,7 @@ def searchCodes(page, pbar, jobList, filename):
# 325
if len(list325) > 0:
# Use same short-text speaker-style translation as other name fields
response = translateAI(list325, "Reply with the " + LANGUAGE + " translation of the NPC name.")
response = translateAI(list325, ctx("names.npc"))
list325TL = response[0]
totalTokens[0] += response[1][0]
totalTokens[1] += response[1][1]
@ -4388,7 +4393,7 @@ def searchSS(state, pbar):
# --- Batch translate all notes ---
translatedNotesBatch = []
if notesBatch:
response = translateAI(notesBatch, f"Reply with only the {LANGUAGE} translation of the note text.")
response = translateAI(notesBatch, ctx("database.note"))
translatedNotesBatch = response[0]
totalTokens[0] += response[1][0]
totalTokens[1] += response[1][1]
@ -4444,7 +4449,7 @@ def searchSS(state, pbar):
def searchSystem(data, pbar):
totalTokens = [0, 0]
context = "Reply with only the " + LANGUAGE + ' translation of the UI textbox."'
context = ctx("database.ui_term")
# Title - batch as a single-item list
title_src = _system_scalar_source(data, "gameTitle")
@ -4452,7 +4457,7 @@ def searchSystem(data, pbar):
if not (IGNORETLTEXT and not re.search(LANGREGEX, title_src)):
response = translateAI(
[title_src],
" Reply with the " + LANGUAGE + " translation of the game title name",
ctx("database.game_title"),
False,
)
totalTokens[0] += response[1][0]
@ -4507,7 +4512,7 @@ def searchSystem(data, pbar):
if armor_values:
response = translateAI(
armor_values,
"Reply with only the " + LANGUAGE + " translation of the armor type",
ctx("database.type_armor"),
False,
)
totalTokens[0] += response[1][0]
@ -4534,7 +4539,7 @@ def searchSystem(data, pbar):
if skill_values:
response = translateAI(
skill_values,
"Reply with only the " + LANGUAGE + " translation",
ctx("database.generic"),
False,
)
totalTokens[0] += response[1][0]
@ -4561,7 +4566,7 @@ def searchSystem(data, pbar):
if equip_values:
response = translateAI(
equip_values,
"Reply with only the " + LANGUAGE + " translation of the equipment type. No disclaimers.",
ctx("database.type_equipment"),
False,
)
totalTokens[0] += response[1][0]
@ -4591,7 +4596,7 @@ def searchSystem(data, pbar):
if element_values:
response = translateAI(
element_values,
"Reply with only the " + LANGUAGE + " translation of the element type",
ctx("database.element"),
False,
)
totalTokens[0] += response[1][0]
@ -4621,7 +4626,7 @@ def searchSystem(data, pbar):
if weapon_values:
response = translateAI(
weapon_values,
"Reply with only the " + LANGUAGE + " translation of the weapon type",
ctx("database.type_weapon"),
False,
)
totalTokens[0] += response[1][0]
@ -4650,7 +4655,7 @@ def searchSystem(data, pbar):
if var_values:
response = translateAI(
var_values,
'Reply with only the ' + LANGUAGE + ' translation of the title',
ctx("database.actor_title"),
True,
)
totalTokens[0] += response[1][0]
@ -4680,7 +4685,7 @@ def searchSystem(data, pbar):
if switch_values:
response = translateAI(
switch_values,
'Reply with only the ' + LANGUAGE + ' translation of the switch name',
ctx("database.switch"),
True,
)
totalTokens[0] += response[1][0]
@ -4712,9 +4717,7 @@ def searchSystem(data, pbar):
if msg_values:
response = translateAI(
msg_values,
"Reply with only the "
+ LANGUAGE
+ ' translation of the battle text.\nTranslate "常時ダッシュ" as "Always Dash"\nTranslate "次の%1まで" as Next %1.',
ctx("database.battle_messages"),
False,
)
totalTokens[0] += response[1][0]
@ -4805,7 +4808,7 @@ def getSpeaker(speaker: str):
pass
response = translateAI(
speaker,
"Reply with the " + LANGUAGE + " translation of the NPC name.",
ctx("names.npc"),
False,
)
try:
@ -4822,7 +4825,7 @@ def getSpeaker(speaker: str):
pass
response = translateAI(
speaker,
"Reply with the " + LANGUAGE + " translation of the NPC name.",
ctx("names.npc"),
False,
)
try:
@ -5033,7 +5036,7 @@ def finalizeSpeakerParse():
pass
resp = translateAI(
to_translate,
"Reply with the " + LANGUAGE + " translation of the NPC name.",
ctx("names.npc"),
True,
)
try:

View file

@ -58,7 +58,8 @@ import traceback
from colorama import Fore
from tqdm import tqdm
from util.paths import PROMPT_PATH, VOCAB_PATH
from util.paths import VOCAB_PATH
from util.skills import ctx, load_system_prompt
from util.translation import (
TranslationConfig,
translateAI as sharedtranslateAI,
@ -78,7 +79,7 @@ MODEL = os.getenv("model")
TIMEOUT = int(os.getenv("timeout"))
LANGUAGE = os.getenv("language").capitalize()
PROMPT = PROMPT_PATH.read_text(encoding="utf-8")
PROMPT = load_system_prompt()
VOCAB = VOCAB_PATH.read_text(encoding="utf-8")
LOCK = threading.Lock()
MAXHISTORY = 10
@ -143,7 +144,7 @@ def _batch_phase() -> str:
def handleWolfDawn(filename, estimate):
"""Entry point used by the CLI/GUI dispatchers. Returns a summary string or 'Fail'."""
global ESTIMATE, TOKENS, FILENAME, SPEAKER_CONFIG, VOCAB
global ESTIMATE, TOKENS, FILENAME, SPEAKER_CONFIG, VOCAB, PROMPT
ESTIMATE = estimate
FILENAME = filename
# Re-read workflow-configured settings so edits made this session take effect
@ -153,6 +154,9 @@ def handleWolfDawn(filename, estimate):
# that an earlier Phase 0 (names) harvested into vocab.txt.
VOCAB = VOCAB_PATH.read_text(encoding="utf-8")
TRANSLATION_CONFIG.vocab = VOCAB
# Reload base prompt + optional per-game quirks (DAZED_GAME_ROOT).
PROMPT = load_system_prompt()
TRANSLATION_CONFIG.prompt = PROMPT
start = time.time()
translatedData = openFiles(filename)
@ -361,7 +365,7 @@ def getSpeaker(speaker: str):
response = translateAI(
speaker,
"Reply with the " + LANGUAGE + " translation of the NPC name.",
ctx("names.npc"),
)
translated = _normalize_speaker_name(
response[0] if isinstance(response[0], str) else str(response[0])
@ -371,7 +375,7 @@ def getSpeaker(speaker: str):
if re.search(r"([a-zA-Z?])", translated) is None:
response = translateAI(
speaker,
"Reply with the " + LANGUAGE + " translation of the NPC name.",
ctx("names.npc"),
)
translated = _normalize_speaker_name(
response[0] if isinstance(response[0], str) else str(response[0])

View file

@ -8,11 +8,19 @@ PROJECT_ROOT = Path(__file__).resolve().parent.parent
DATA_DIR = PROJECT_ROOT / "data"
VOCAB_PATH = DATA_DIR / "vocab.txt"
VOCAB_BASE_PATH = DATA_DIR / "vocab_base.txt"
PROMPT_PATH = DATA_DIR / "prompt.txt"
SKILLS_DIR = DATA_DIR / "skills"
# Runtime translation system skill (formerly data/prompt.txt).
PROMPT_PATH = SKILLS_DIR / "system.md"
LEGACY_PROMPT_PATH = DATA_DIR / "prompt.txt"
LAST_UPDATE_SHA_PATH = DATA_DIR / "last_update_sha.txt"
ENV_PATH = PROJECT_ROOT / ".env"
ICON_PATH = PROJECT_ROOT / "assets" / "icon.png"
ENGINE_ICONS_DIR = PROJECT_ROOT / "assets" / "engine_icons"
TRANSLATION_CONTEXTS_PATH = DATA_DIR / "translation_contexts.json"
# Per-game quirks skill (API overlay). Legacy flat file still migrated on load.
GAME_QUIRKS_RELATIVE = Path("skills") / "quirks.md"
LEGACY_QUIRKS_FILENAME = "translation_quirks.txt"
GAME_SKILL_RELATIVE = Path("skills") / "translation.md"
_ROOT_DATA_FILES = (
"vocab.txt",
@ -32,7 +40,19 @@ def migrate_root_data_files() -> None:
src.rename(dst)
def migrate_prompt_to_skills() -> None:
"""Move legacy ``data/prompt.txt`` to ``data/skills/system.md`` when needed."""
SKILLS_DIR.mkdir(parents=True, exist_ok=True)
if PROMPT_PATH.is_file():
return
for legacy in (LEGACY_PROMPT_PATH, PROJECT_ROOT / "prompt.txt"):
if legacy.is_file():
legacy.rename(PROMPT_PATH)
return
migrate_root_data_files()
migrate_prompt_to_skills()
def ensure_vocab_file() -> None:

21
util/skills/__init__.py Normal file
View file

@ -0,0 +1,21 @@
"""Skill and prompt loaders (editable files under ``data/skills/``)."""
from __future__ import annotations
from util.skills.contexts import ctx, reload_contexts
from util.skills.setup import load_project_setup, skills_dir
from util.skills.system import (
game_skill_path_for_game,
load_system_prompt,
quirks_path_for_game,
)
__all__ = [
"ctx",
"game_skill_path_for_game",
"load_project_setup",
"load_system_prompt",
"quirks_path_for_game",
"reload_contexts",
"skills_dir",
]

63
util/skills/contexts.py Normal file
View file

@ -0,0 +1,63 @@
"""Load per-call translation history templates from data/translation_contexts.json."""
from __future__ import annotations
import json
import os
from typing import Any
from util.paths import TRANSLATION_CONTEXTS_PATH
_cache: dict[str, Any] | None = None
_cache_mtime: float | None = None
def _load() -> dict[str, Any]:
global _cache, _cache_mtime
path = TRANSLATION_CONTEXTS_PATH
if not path.is_file():
raise FileNotFoundError(f"Translation contexts file missing: {path}")
mtime = path.stat().st_mtime
if _cache is not None and _cache_mtime == mtime:
return _cache
data = json.loads(path.read_text(encoding="utf-8"))
if not isinstance(data, dict):
raise ValueError(f"translation_contexts.json must be an object: {path}")
_cache = data
_cache_mtime = mtime
return data
def ctx(path: str, *, language: str | None = None, **kwargs: Any) -> str:
"""Return a formatted history string for ``section.key``.
``{language}`` defaults to the ``language`` env / argument (capitalized).
Extra kwargs fill other ``{placeholders}`` (e.g. ``context``).
"""
if not path or "." not in path:
raise KeyError(f"Context path must be 'section.key', got {path!r}")
section_name, key = path.split(".", 1)
data = _load()
section = data.get(section_name)
if not isinstance(section, dict):
raise KeyError(f"Unknown translation context section: {section_name!r}")
template = section.get(key)
if not isinstance(template, str) or not template.strip():
raise KeyError(f"Unknown translation context key: {path!r}")
lang = language or os.getenv("language", "English")
lang = str(lang).capitalize()
values = {"language": lang, **kwargs}
try:
return template.format(**values)
except KeyError as exc:
raise KeyError(
f"Missing placeholder {exc.args[0]!r} for context {path!r}"
) from exc
def reload_contexts() -> None:
"""Clear the in-memory cache so the next ctx() re-reads from disk."""
global _cache, _cache_mtime
_cache = None
_cache_mtime = None

62
util/skills/setup.py Normal file
View file

@ -0,0 +1,62 @@
"""Load editable clipboard skills from data/skills/."""
from __future__ import annotations
from pathlib import Path
from util.paths import SKILLS_DIR
_ENGINE_MARKERS = {
"rpgmaker": ("<!-- engine:rpgmaker -->", "<!-- /engine:rpgmaker -->"),
"wolf": ("<!-- engine:wolf -->", "<!-- /engine:wolf -->"),
}
def _read_skill_file(name: str) -> str:
path = SKILLS_DIR / name
if not path.is_file():
raise FileNotFoundError(f"Skill file missing: {path}")
return path.read_text(encoding="utf-8")
def _extract_engine_section(text: str, engine: str) -> str:
"""Keep shared body + the requested engine block; drop other engine blocks."""
start_tag, end_tag = _ENGINE_MARKERS.get(engine, (None, None))
if not start_tag:
return text
result_parts: list[str] = []
pos = 0
while pos < len(text):
next_starts = [
(text.find(marker[0], pos), eng, marker)
for eng, marker in _ENGINE_MARKERS.items()
if text.find(marker[0], pos) != -1
]
if not next_starts:
result_parts.append(text[pos:])
break
next_starts.sort(key=lambda x: x[0])
idx, eng, (s_tag, e_tag) = next_starts[0]
result_parts.append(text[pos:idx])
end_idx = text.find(e_tag, idx)
if end_idx == -1:
raise ValueError(f"Unclosed engine block {s_tag} in skill file")
block_body = text[idx + len(s_tag) : end_idx]
if eng == engine:
result_parts.append(block_body.strip("\n") + "\n")
pos = end_idx + len(e_tag)
return "".join(result_parts).strip() + "\n"
def load_project_setup(engine: str = "rpgmaker", *, prepend: str = "") -> str:
"""Load the Project Setup clipboard skill for *engine*."""
raw = _read_skill_file("project_setup.md")
body = _extract_engine_section(raw, engine)
if prepend:
return prepend.rstrip() + "\n\n" + body
return body
def skills_dir() -> Path:
return SKILLS_DIR

66
util/skills/system.py Normal file
View file

@ -0,0 +1,66 @@
"""Load the static system prompt and optional per-game quirks overlay."""
from __future__ import annotations
import os
from pathlib import Path
from util.paths import (
GAME_QUIRKS_RELATIVE,
GAME_SKILL_RELATIVE,
LEGACY_QUIRKS_FILENAME,
PROMPT_PATH,
)
def quirks_path_for_game(game_root: str | Path | None) -> Path | None:
"""Return ``<game_root>/skills/quirks.md`` when *game_root* is set.
Also migrates a legacy ``translation_quirks.txt`` in the game root if present.
"""
if not game_root:
return None
root = Path(game_root)
preferred = root / GAME_QUIRKS_RELATIVE
legacy = root / LEGACY_QUIRKS_FILENAME
if not preferred.is_file() and legacy.is_file():
preferred.parent.mkdir(parents=True, exist_ok=True)
try:
legacy.rename(preferred)
except OSError:
# Fall back to reading the legacy path if rename fails.
return legacy
return preferred
def game_skill_path_for_game(game_root: str | Path | None) -> Path | None:
"""Return ``<game_root>/skills/translation.md`` when *game_root* is set."""
if not game_root:
return None
return Path(game_root) / GAME_SKILL_RELATIVE
def load_system_prompt(game_root: str | Path | None = None) -> str:
"""Load ``data/skills/system.md`` and optionally append per-game quirks.
Quirks path resolution order:
1. Explicit *game_root* argument
2. ``DAZED_GAME_ROOT`` environment variable
"""
base = ""
if PROMPT_PATH.is_file():
base = PROMPT_PATH.read_text(encoding="utf-8")
root = game_root or os.getenv("DAZED_GAME_ROOT") or ""
root = str(root).strip()
quirks_file = quirks_path_for_game(root) if root else None
if quirks_file and quirks_file.is_file():
quirks = quirks_file.read_text(encoding="utf-8").strip()
if quirks:
return (
base.rstrip()
+ "\n\n## Game-Specific Translation Quirks\n\n"
+ quirks
+ "\n"
)
return base

View file

@ -1477,8 +1477,8 @@ class TranslationConfig:
# Load prompt and vocab files if not provided
if prompt is None:
try:
from util.paths import PROMPT_PATH, VOCAB_PATH
self.prompt = PROMPT_PATH.read_text(encoding="utf-8")
from util.skills import load_system_prompt
self.prompt = load_system_prompt()
except FileNotFoundError:
self.prompt = ""
else:
@ -1945,7 +1945,7 @@ def createContext(config, subbedText, formatType, history=None):
cache invalidation caused by changing vocabulary matches.
Cached in static_system:
- prompt.txt content
- data/skills/system.md content (plus optional game quirks overlay)
Dynamic in vocab_text:
- only vocab terms found in the current batch text
@ -2061,7 +2061,7 @@ def buildClaudeRequest(system, user, history, formatType, model, numLines=None,
def translateText(system, user, history, penalty, formatType, model, numLines=None, vocab_text=""):
"""Send translation request to the selected API.
system: Static system prompt (prompt.txt). Cached by Claude.
system: Static system prompt (data/skills/system.md). Cached by Claude.
vocab_text: Per-batch vocabulary (dynamic, never cached to avoid cache busting).
"""
# Ensure system content is not empty
@ -2830,7 +2830,7 @@ def translateAI(text, history, config, filename=None, pbar=None, lock=None, mism
continue
# Create context — static_system is the stable prompt.txt content;
# Create context — static_system is the stable system.md content;
# vocab_text is the per-batch matched vocabulary (dynamic).
static_system, vocab_text, user = createContext(config, subbedT, formatType, history)