Reworking prompts/skills
This commit is contained in:
parent
e991f27cbf
commit
fe31c71130
16 changed files with 1050 additions and 540 deletions
|
|
@ -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
|
||||
|
|
|
|||
2
TODO.md
2
TODO.md
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
165
data/skills/project_setup.md
Normal file
165
data/skills/project_setup.md
Normal 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.
|
||||
|
|
@ -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]:`
|
||||
18
gui/main.py
18
gui/main.py
|
|
@ -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
247
gui/skills_tab.py
Normal 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}")
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 1–2 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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -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
21
util/skills/__init__.py
Normal 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
63
util/skills/contexts.py
Normal 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
62
util/skills/setup.py
Normal 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
66
util/skills/system.py
Normal 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
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue