Wolf support for skills prep
This commit is contained in:
parent
b91d5b9acd
commit
26132a02aa
4 changed files with 769 additions and 626 deletions
|
|
@ -123,9 +123,93 @@ Do **not** include:
|
||||||
|
|
||||||
<!-- engine:wolf -->
|
<!-- engine:wolf -->
|
||||||
|
|
||||||
### Wolf file strategy (stub)
|
### Wolf file strategy
|
||||||
|
|
||||||
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.
|
WolfDawn extractions live under `files/` as JSON lists of `{source, text}` entries.
|
||||||
|
Analyse `source` only.
|
||||||
|
|
||||||
|
**DB / system (read in full first):**
|
||||||
|
- `DataBase.project.json`, `CDataBase.project.json`, `SysDatabase.project.json` - richest small source of character/actor names, classes, factions, lore titles
|
||||||
|
- `CommonEvent.dat.json` - common events (dialogue + system text)
|
||||||
|
- `Game.dat.json` - game/system strings (title, terms)
|
||||||
|
- `Evtext.json` - external event text when present
|
||||||
|
|
||||||
|
**Maps (grep / sample, do not read huge files sequentially):**
|
||||||
|
- `<Map>.mps.json` - per-map events; main story dialogue; often very large
|
||||||
|
- Prefer grep of `source` for speaker patterns and recurring proper nouns
|
||||||
|
- Scan lowest-numbered maps first - early maps usually carry the most story
|
||||||
|
|
||||||
|
**Exclude from glossary analysis:**
|
||||||
|
- `names.json` - item / skill / enemy value names (translated separately in Step 3 Names; do **not** list them in the glossary)
|
||||||
|
|
||||||
|
--- attach the extracted JSON in files/ here before continuing ---
|
||||||
|
|
||||||
|
### Speakers analysis (for `speakers` block)
|
||||||
|
|
||||||
|
WolfDawn already tags who speaks. High-confidence nameplates (`literal_line1`) are always reshaped - nothing to decide.
|
||||||
|
|
||||||
|
The only flag is whether **low-confidence** first-line guesses (`speaker_src = literal_line1_lowconf`) are real speaker names for this game:
|
||||||
|
- short first line with **no** preceding face window
|
||||||
|
- might be a nameplate, or the start of dialogue / narration
|
||||||
|
|
||||||
|
Inspect a sample of those entries in maps / CommonEvent:
|
||||||
|
- `ENABLE` if low-confidence first lines are overwhelmingly real speaker names
|
||||||
|
- `SKIP` if many are dialogue or narration (reshaping would mislabel lines)
|
||||||
|
|
||||||
|
Do **not** emit RPG Maker flags (`INLINE401SPEAKERS`, `FIRSTLINESPEAKERS`, `FACENAME101`).
|
||||||
|
|
||||||
|
**Wolf `speakers` schema (use this instead of the shared RPG Maker flag list):**
|
||||||
|
|
||||||
|
```
|
||||||
|
Patterns detected:
|
||||||
|
- ...
|
||||||
|
|
||||||
|
Flag decisions:
|
||||||
|
LOWCONF_FIRSTLINE : ENABLE|SKIP - <one-line reason>
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
- ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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; note player-chosen names (variable / input).
|
||||||
|
- Worldbuilding: factions, lore locations (mentioned in dialogue but not map labels), unique systems/titles.
|
||||||
|
- Exclude: skill / item / weapon / armour names from `names.json`, generic RPG words, unnamed NPCs.
|
||||||
|
|
||||||
|
### Quirks rules (for `translation_quirks` block)
|
||||||
|
|
||||||
|
Find translation-only quirks, for example:
|
||||||
|
- Battle log / system messages with a fixed person or register
|
||||||
|
- Global dialect (archaic narration, cute speech markers game-wide)
|
||||||
|
- Recurring item/skill description style
|
||||||
|
- Unusual honorific habits
|
||||||
|
|
||||||
|
Exclude: wrap geometry, inject layout, `names.json` harvest, speaker LOWCONF checkbox, character name lists.
|
||||||
|
|
||||||
|
Output as short imperative bullets suitable to paste into `skills/quirks.md`.
|
||||||
|
|
||||||
|
### Game skill rules (for `game_skill` block)
|
||||||
|
|
||||||
|
Produce the per-game translation skill saved at `skills/game.md`.
|
||||||
|
DazedMTLTool **merges this file into the translation system prompt** (before quirks).
|
||||||
|
|
||||||
|
**Translation Frame only** (one compact line per field; evidence-based):
|
||||||
|
- `世界観 (Theme / setting)` - genre, world type, core atmosphere
|
||||||
|
- `時代感 (Era / technology level)` - medieval / modern / sci-fi / historical / etc.
|
||||||
|
- `文体方針 (Register policy)` - overall English style
|
||||||
|
- `固有名詞方針 (Naming policy)` - invented names, titles, ranks, honorifics (high-level only)
|
||||||
|
- `神話・伝承 (Myth / folklore basis)` - **omit unless** evidence supports a specific tradition
|
||||||
|
|
||||||
|
Do **not** include voice-rules pointers, tool-boundary essays, file inventories, per-character register (glossary), or quirks bullets.
|
||||||
|
|
||||||
|
**Paths:**
|
||||||
|
- Game skill (API): ``skills/game.md``
|
||||||
|
- Quirks (API): ``skills/quirks.md``
|
||||||
|
- Optional custom API overlays: other ``skills/*.md`` except those two
|
||||||
|
|
||||||
<!-- /engine:wolf -->
|
<!-- /engine:wolf -->
|
||||||
|
|
||||||
|
|
|
||||||
587
gui/setup_skills_editors.py
Normal file
587
gui/setup_skills_editors.py
Normal file
|
|
@ -0,0 +1,587 @@
|
||||||
|
"""Shared Vocab / Quirks / Game skills editors for Workflow Setup steps."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
|
||||||
|
from PyQt5.QtCore import Qt, QSize
|
||||||
|
from PyQt5.QtGui import QFont
|
||||||
|
from PyQt5.QtWidgets import (
|
||||||
|
QHBoxLayout,
|
||||||
|
QInputDialog,
|
||||||
|
QLabel,
|
||||||
|
QMessageBox,
|
||||||
|
QPushButton,
|
||||||
|
QStackedWidget,
|
||||||
|
QTabBar,
|
||||||
|
QTabWidget,
|
||||||
|
QTextEdit,
|
||||||
|
QVBoxLayout,
|
||||||
|
QWidget,
|
||||||
|
)
|
||||||
|
|
||||||
|
from util.skills import (
|
||||||
|
custom_skill_path_for_game,
|
||||||
|
game_skill_path_for_game,
|
||||||
|
list_custom_skill_paths,
|
||||||
|
migrate_game_skill_text,
|
||||||
|
quirks_path_for_game,
|
||||||
|
sanitize_custom_skill_stem,
|
||||||
|
)
|
||||||
|
from util.vocab import read_game_vocab, write_game_vocab
|
||||||
|
|
||||||
|
|
||||||
|
class _PlainPasteTextEdit(QTextEdit):
|
||||||
|
"""QTextEdit that always pastes as plain text."""
|
||||||
|
|
||||||
|
def insertFromMimeData(self, source): # noqa: N802
|
||||||
|
self.insertPlainText(source.text())
|
||||||
|
|
||||||
|
|
||||||
|
def _make_btn(text: str, color: str = "#007acc") -> QPushButton:
|
||||||
|
"""Match workflow_tab button styling (flat dark + coloured outline)."""
|
||||||
|
btn = QPushButton()
|
||||||
|
try:
|
||||||
|
c = color.lstrip("#")
|
||||||
|
if len(c) == 3:
|
||||||
|
c = c[0] * 2 + c[1] * 2 + c[2] * 2
|
||||||
|
r, g, b = int(c[0:2], 16), int(c[2:4], 16), int(c[4:6], 16)
|
||||||
|
is_flat = max(r, g, b) < 115
|
||||||
|
except Exception:
|
||||||
|
r = g = b = 0
|
||||||
|
is_flat = False
|
||||||
|
_PAD = "padding:6px 14px;"
|
||||||
|
_icon_color = "#cccccc"
|
||||||
|
if is_flat:
|
||||||
|
btn.setStyleSheet(
|
||||||
|
f"QPushButton{{background-color:#2d2d30;color:#cccccc;"
|
||||||
|
f"border:1px solid #555555;{_PAD}"
|
||||||
|
f"border-radius:4px;font-size:12px;font-weight:bold;"
|
||||||
|
f"font-family:'Segoe UI','Segoe UI Emoji','Apple Color Emoji',sans-serif;}}"
|
||||||
|
f"QPushButton:hover{{background-color:#3e3e42;}}"
|
||||||
|
f"QPushButton:pressed{{background-color:#007acc;color:white;}}"
|
||||||
|
f"QPushButton:disabled{{background-color:#404040;color:#666666;border-color:#444444;}}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
rt = min(255, r + 80)
|
||||||
|
gt = min(255, g + 80)
|
||||||
|
bt = min(255, b + 80)
|
||||||
|
text_color = f"#{rt:02x}{gt:02x}{bt:02x}"
|
||||||
|
_icon_color = text_color
|
||||||
|
base = 0x2d
|
||||||
|
rh = min(255, int(base + (r - base) * 0.18))
|
||||||
|
gh = min(255, int(base + (g - base) * 0.18))
|
||||||
|
bh = min(255, int(base + (b - base) * 0.18))
|
||||||
|
hover_bg = f"#{rh:02x}{gh:02x}{bh:02x}"
|
||||||
|
rb = min(255, r + 35)
|
||||||
|
gb = min(255, g + 35)
|
||||||
|
bb = min(255, b + 35)
|
||||||
|
hover_accent = f"#{rb:02x}{gb:02x}{bb:02x}"
|
||||||
|
btn.setStyleSheet(
|
||||||
|
f"QPushButton{{background-color:#2d2d30;color:{text_color};"
|
||||||
|
f"border:1px solid {color};{_PAD}"
|
||||||
|
f"border-radius:4px;font-size:12px;font-weight:bold;"
|
||||||
|
f"font-family:'Segoe UI','Segoe UI Emoji','Apple Color Emoji',sans-serif;}}"
|
||||||
|
f"QPushButton:hover{{background-color:{hover_bg};border-color:{hover_accent};"
|
||||||
|
f"color:{hover_accent};}}"
|
||||||
|
f"QPushButton:pressed{{background-color:#1a1a1a;}}"
|
||||||
|
f"QPushButton:disabled{{background-color:#2d2d30;color:#555555;border-color:#444444;}}"
|
||||||
|
)
|
||||||
|
from gui import qt_icons
|
||||||
|
|
||||||
|
qt_icons.apply_button_icon(btn, text, color=_icon_color)
|
||||||
|
if not btn.icon().isNull():
|
||||||
|
btn.setIconSize(QSize(16, 16))
|
||||||
|
return btn
|
||||||
|
|
||||||
|
|
||||||
|
class SetupSkillsEditors(QWidget):
|
||||||
|
"""Tabbed Vocab / Quirks / Game skills editors for Workflow Setup.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
game_root_fn:
|
||||||
|
Callable returning the current game root path (may be empty).
|
||||||
|
log_fn:
|
||||||
|
Callable used for status / error messages.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
parent: QWidget | None = None,
|
||||||
|
*,
|
||||||
|
game_root_fn: Callable[[], str],
|
||||||
|
log_fn: Callable[[str], None],
|
||||||
|
):
|
||||||
|
super().__init__(parent)
|
||||||
|
self._game_root_fn = game_root_fn
|
||||||
|
self._log = log_fn
|
||||||
|
self._custom_skill_editors: dict[str, QTextEdit] = {}
|
||||||
|
self.vocab_editor: QTextEdit | None = None
|
||||||
|
self.quirks_editor: QTextEdit | None = None
|
||||||
|
self.game_skill_editor: QTextEdit | None = None
|
||||||
|
self._build_ui()
|
||||||
|
|
||||||
|
def _build_ui(self) -> None:
|
||||||
|
root = QVBoxLayout(self)
|
||||||
|
root.setContentsMargins(0, 0, 0, 0)
|
||||||
|
root.setSpacing(0)
|
||||||
|
|
||||||
|
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;}"
|
||||||
|
)
|
||||||
|
|
||||||
|
editors.addTab(
|
||||||
|
self._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(
|
||||||
|
self._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",
|
||||||
|
)
|
||||||
|
|
||||||
|
game_skills_page = QWidget()
|
||||||
|
gs_layout = QVBoxLayout(game_skills_page)
|
||||||
|
gs_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
gs_layout.setSpacing(0)
|
||||||
|
|
||||||
|
self._game_skills_bar = QTabBar()
|
||||||
|
self._game_skills_bar.setDocumentMode(True)
|
||||||
|
self._game_skills_bar.setExpanding(False)
|
||||||
|
self._game_skills_bar.setDrawBase(False)
|
||||||
|
self._game_skills_bar.setMinimumHeight(30)
|
||||||
|
self._game_skills_bar.setStyleSheet(
|
||||||
|
"QTabBar::tab{background:#2d2d30;color:#9d9d9d;padding:6px 12px;"
|
||||||
|
"border:1px solid #3c3c3c;border-bottom:none;margin-right:2px;}"
|
||||||
|
"QTabBar::tab:selected{background:#1e1e1e;color:#e0e0e0;}"
|
||||||
|
"QTabBar::tab:hover{color:#ffffff;}"
|
||||||
|
)
|
||||||
|
self._game_skills_stack = QStackedWidget()
|
||||||
|
self._game_skills_stack.setStyleSheet("background:#1e1e1e;")
|
||||||
|
self._game_skills_bar.currentChanged.connect(
|
||||||
|
self._game_skills_stack.setCurrentIndex
|
||||||
|
)
|
||||||
|
|
||||||
|
strip = QWidget()
|
||||||
|
strip.setStyleSheet("background-color:#252526;")
|
||||||
|
strip_l = QHBoxLayout(strip)
|
||||||
|
strip_l.setContentsMargins(8, 6, 10, 0)
|
||||||
|
strip_l.setSpacing(6)
|
||||||
|
strip_l.addWidget(self._game_skills_bar, 0, Qt.AlignBottom)
|
||||||
|
|
||||||
|
add_btn = QPushButton("+ Add custom")
|
||||||
|
add_btn.setCursor(Qt.PointingHandCursor)
|
||||||
|
add_btn.setFixedHeight(30)
|
||||||
|
add_btn.setToolTip(
|
||||||
|
"Create a custom skills/*.md overlay.\n"
|
||||||
|
"Merged into the translation system prompt - can hurt quality."
|
||||||
|
)
|
||||||
|
add_btn.setStyleSheet(
|
||||||
|
"QPushButton{background-color:#2d2d30;color:#4ec9b0;"
|
||||||
|
"border:1px solid #3c3c3c;border-bottom:none;"
|
||||||
|
"border-top-left-radius:3px;border-top-right-radius:3px;"
|
||||||
|
"padding:0 12px;font-size:12px;font-weight:bold;}"
|
||||||
|
"QPushButton:hover{background-color:#3a3a3a;color:#7ee0c8;"
|
||||||
|
"border-color:#4ec9b0;}"
|
||||||
|
"QPushButton:pressed{background-color:#1e1e1e;}"
|
||||||
|
)
|
||||||
|
add_btn.clicked.connect(self._add_custom_skill)
|
||||||
|
strip_l.addWidget(add_btn, 0, Qt.AlignBottom)
|
||||||
|
strip_l.addStretch(1)
|
||||||
|
gs_layout.addWidget(strip)
|
||||||
|
gs_layout.addWidget(self._game_skills_stack, 1)
|
||||||
|
|
||||||
|
self._add_game_skill_page(
|
||||||
|
"game",
|
||||||
|
self._editor_page(
|
||||||
|
"<game>/skills/game.md",
|
||||||
|
"Paste the game_skill Translation Frame. Merged into the translation "
|
||||||
|
"system prompt (before quirks) when this game folder is selected.",
|
||||||
|
self._save_game_skill,
|
||||||
|
self._reload_game_skill,
|
||||||
|
"game_skill_editor",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
editors.addTab(game_skills_page, "Game skills")
|
||||||
|
root.addWidget(editors, 1)
|
||||||
|
|
||||||
|
def _editor_page(
|
||||||
|
self,
|
||||||
|
path_hint: str,
|
||||||
|
tip: str,
|
||||||
|
save_slot,
|
||||||
|
reload_slot,
|
||||||
|
attr: str,
|
||||||
|
) -> QWidget:
|
||||||
|
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(
|
||||||
|
"QTextEdit{background-color:#1e1e1e;color:#d4d4d4;"
|
||||||
|
"border:none;padding:8px;"
|
||||||
|
"selection-background-color:#264f78;}"
|
||||||
|
)
|
||||||
|
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
|
||||||
|
|
||||||
|
def reload_all(self) -> None:
|
||||||
|
"""Reload vocab, quirks, game skill, and custom skill tabs from disk."""
|
||||||
|
self._reload_vocab()
|
||||||
|
self._reload_quirks()
|
||||||
|
self._reload_game_skill()
|
||||||
|
self._reload_custom_skills()
|
||||||
|
|
||||||
|
def _game_root(self) -> str:
|
||||||
|
return (self._game_root_fn() or "").strip()
|
||||||
|
|
||||||
|
def _game_root_or_warn(self) -> str | None:
|
||||||
|
root = self._game_root()
|
||||||
|
if not root:
|
||||||
|
self._log("⚠ Select a game folder in Step 0 first.")
|
||||||
|
return None
|
||||||
|
return root
|
||||||
|
|
||||||
|
def _reload_vocab(self) -> None:
|
||||||
|
if self.vocab_editor is None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
self.vocab_editor.setPlainText(read_game_vocab())
|
||||||
|
except Exception as exc:
|
||||||
|
self._log(f"❌ Could not load vocab.txt: {exc}")
|
||||||
|
|
||||||
|
def _save_vocab(self) -> None:
|
||||||
|
if self.vocab_editor is None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
write_game_vocab(self.vocab_editor.toPlainText())
|
||||||
|
self._log("✅ vocab.txt saved (base terms from vocab_base.txt appended).")
|
||||||
|
except Exception as exc:
|
||||||
|
self._log(f"❌ Could not save vocab.txt: {exc}")
|
||||||
|
|
||||||
|
def _reload_quirks(self) -> None:
|
||||||
|
if self.quirks_editor is None:
|
||||||
|
return
|
||||||
|
path = quirks_path_for_game(self._game_root())
|
||||||
|
if not path or not path.is_file():
|
||||||
|
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) -> None:
|
||||||
|
root = self._game_root_or_warn()
|
||||||
|
if not root or self.quirks_editor is None:
|
||||||
|
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) -> None:
|
||||||
|
if self.game_skill_editor is None:
|
||||||
|
return
|
||||||
|
path = game_skill_path_for_game(self._game_root())
|
||||||
|
if not path or not path.is_file():
|
||||||
|
self.game_skill_editor.setPlainText("")
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
raw = path.read_text(encoding="utf-8")
|
||||||
|
fixed = migrate_game_skill_text(raw)
|
||||||
|
self.game_skill_editor.setPlainText(fixed)
|
||||||
|
if fixed != raw:
|
||||||
|
path.write_text(fixed.rstrip() + "\n", encoding="utf-8")
|
||||||
|
self._log(
|
||||||
|
f"↺ Updated legacy sections in {path.name} for API use"
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
self._log(f"❌ Could not load game skill: {exc}")
|
||||||
|
|
||||||
|
def _save_game_skill(self) -> None:
|
||||||
|
root = self._game_root_or_warn()
|
||||||
|
if not root or self.game_skill_editor is None:
|
||||||
|
return
|
||||||
|
path = game_skill_path_for_game(root)
|
||||||
|
try:
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
text = migrate_game_skill_text(self.game_skill_editor.toPlainText())
|
||||||
|
self.game_skill_editor.setPlainText(text)
|
||||||
|
path.write_text(text.rstrip() + "\n", encoding="utf-8")
|
||||||
|
self._log(f"✅ Saved {path}")
|
||||||
|
except Exception as exc:
|
||||||
|
self._log(f"❌ Could not save game skill: {exc}")
|
||||||
|
|
||||||
|
def _add_game_skill_page(self, title: str, page: QWidget) -> int:
|
||||||
|
idx = self._game_skills_stack.addWidget(page)
|
||||||
|
self._game_skills_bar.addTab(title)
|
||||||
|
return idx
|
||||||
|
|
||||||
|
def _select_game_skill_tab(self, title: str) -> None:
|
||||||
|
for i in range(self._game_skills_bar.count()):
|
||||||
|
if self._game_skills_bar.tabText(i) == title:
|
||||||
|
self._game_skills_bar.setCurrentIndex(i)
|
||||||
|
return
|
||||||
|
|
||||||
|
def _custom_skill_editor_page(self, stem: str) -> QWidget:
|
||||||
|
page = QWidget()
|
||||||
|
vl = QVBoxLayout(page)
|
||||||
|
vl.setContentsMargins(8, 8, 8, 8)
|
||||||
|
vl.setSpacing(6)
|
||||||
|
|
||||||
|
tip = QLabel(
|
||||||
|
"Custom skill - merged into the translation system prompt. "
|
||||||
|
"Extra or conflicting rules can hurt quality."
|
||||||
|
)
|
||||||
|
tip.setWordWrap(True)
|
||||||
|
tip.setStyleSheet("color:#c9a227;font-size:12px;")
|
||||||
|
vl.addWidget(tip)
|
||||||
|
|
||||||
|
path_lbl = QLabel(f"<game>/skills/{stem}.md")
|
||||||
|
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(
|
||||||
|
"QTextEdit{background-color:#1e1e1e;color:#d4d4d4;"
|
||||||
|
"border:none;padding:8px;"
|
||||||
|
"selection-background-color:#264f78;}"
|
||||||
|
)
|
||||||
|
self._custom_skill_editors[stem] = ed
|
||||||
|
vl.addWidget(ed, 1)
|
||||||
|
|
||||||
|
row = QHBoxLayout()
|
||||||
|
row.setSpacing(8)
|
||||||
|
save_btn = _make_btn("💾 Save", "#3a7a3a")
|
||||||
|
save_btn.setFixedWidth(110)
|
||||||
|
save_btn.clicked.connect(lambda _=False, s=stem: self._save_custom_skill(s))
|
||||||
|
row.addWidget(save_btn)
|
||||||
|
|
||||||
|
reload_btn = _make_btn("↺ Reload", "#555")
|
||||||
|
reload_btn.setFixedWidth(110)
|
||||||
|
reload_btn.clicked.connect(
|
||||||
|
lambda _=False, s=stem: self._reload_one_custom_skill(s)
|
||||||
|
)
|
||||||
|
row.addWidget(reload_btn)
|
||||||
|
|
||||||
|
delete_btn = _make_btn("🗑 Delete", "#8b0000")
|
||||||
|
delete_btn.setFixedWidth(110)
|
||||||
|
delete_btn.setToolTip("Delete this custom skill file from the game folder")
|
||||||
|
delete_btn.clicked.connect(lambda _=False, s=stem: self._delete_custom_skill(s))
|
||||||
|
row.addWidget(delete_btn)
|
||||||
|
row.addStretch()
|
||||||
|
vl.addLayout(row)
|
||||||
|
return page
|
||||||
|
|
||||||
|
def _reload_custom_skills(self) -> None:
|
||||||
|
bar = getattr(self, "_game_skills_bar", None)
|
||||||
|
stack = getattr(self, "_game_skills_stack", None)
|
||||||
|
if bar is None or stack is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
while bar.count() > 1:
|
||||||
|
bar.removeTab(1)
|
||||||
|
while stack.count() > 1:
|
||||||
|
w = stack.widget(1)
|
||||||
|
stack.removeWidget(w)
|
||||||
|
if w is not None:
|
||||||
|
w.deleteLater()
|
||||||
|
self._custom_skill_editors = {}
|
||||||
|
|
||||||
|
for path in list_custom_skill_paths(self._game_root()):
|
||||||
|
stem = path.stem
|
||||||
|
page = self._custom_skill_editor_page(stem)
|
||||||
|
self._add_game_skill_page(stem, page)
|
||||||
|
try:
|
||||||
|
self._custom_skill_editors[stem].setPlainText(
|
||||||
|
path.read_text(encoding="utf-8")
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
self._log(f"❌ Could not load skills/{stem}.md: {exc}")
|
||||||
|
|
||||||
|
def _reload_one_custom_skill(self, stem: str) -> None:
|
||||||
|
path = custom_skill_path_for_game(self._game_root(), stem)
|
||||||
|
ed = self._custom_skill_editors.get(stem)
|
||||||
|
if ed is None:
|
||||||
|
return
|
||||||
|
if not path or not path.is_file():
|
||||||
|
ed.setPlainText("")
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
ed.setPlainText(path.read_text(encoding="utf-8"))
|
||||||
|
except Exception as exc:
|
||||||
|
self._log(f"❌ Could not load skills/{stem}.md: {exc}")
|
||||||
|
|
||||||
|
def _save_custom_skill(self, stem: str) -> None:
|
||||||
|
root = self._game_root_or_warn()
|
||||||
|
if not root:
|
||||||
|
return
|
||||||
|
path = custom_skill_path_for_game(root, stem)
|
||||||
|
ed = self._custom_skill_editors.get(stem)
|
||||||
|
if path is None or ed is None:
|
||||||
|
self._log("❌ Invalid custom skill.")
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_text(ed.toPlainText().rstrip() + "\n", encoding="utf-8")
|
||||||
|
self._log(f"✅ Saved {path}")
|
||||||
|
except Exception as exc:
|
||||||
|
self._log(f"❌ Could not save skills/{stem}.md: {exc}")
|
||||||
|
|
||||||
|
def _add_custom_skill(self) -> None:
|
||||||
|
root = self._game_root_or_warn()
|
||||||
|
if not root:
|
||||||
|
return
|
||||||
|
|
||||||
|
warn = QMessageBox(self)
|
||||||
|
warn.setIcon(QMessageBox.Warning)
|
||||||
|
warn.setWindowTitle("Custom skill - quality warning")
|
||||||
|
warn.setTextFormat(Qt.RichText)
|
||||||
|
warn.setText(
|
||||||
|
"<b>Custom skills are merged into the translation system prompt.</b><br><br>"
|
||||||
|
"Extra or poorly written skills can <b>distract the model and hurt "
|
||||||
|
"translation quality</b> (conflicting rules, prompt bloat, diluted quirks).<br><br>"
|
||||||
|
"Prefer <code>quirks.md</code> for voice rules and <code>game.md</code> "
|
||||||
|
"for the Translation Frame. Add a custom skill only if you need a rare, tightly scoped "
|
||||||
|
"overlay - <b>at your own risk</b>."
|
||||||
|
)
|
||||||
|
warn.setStandardButtons(QMessageBox.Cancel | QMessageBox.Ok)
|
||||||
|
warn.button(QMessageBox.Ok).setText("I understand - continue")
|
||||||
|
warn.setStyleSheet(
|
||||||
|
"QMessageBox{background-color:#252526;}"
|
||||||
|
"QLabel{color:#d4d4d4;font-size:13px;min-width:420px;}"
|
||||||
|
)
|
||||||
|
if warn.exec_() != QMessageBox.Ok:
|
||||||
|
return
|
||||||
|
|
||||||
|
name, ok = QInputDialog.getText(
|
||||||
|
self,
|
||||||
|
"New custom skill",
|
||||||
|
"Skill file name (letters, numbers, ._- ; saved as <game>/skills/<name>.md):",
|
||||||
|
)
|
||||||
|
if not ok:
|
||||||
|
return
|
||||||
|
stem = sanitize_custom_skill_stem(name)
|
||||||
|
if not stem:
|
||||||
|
QMessageBox.warning(
|
||||||
|
self,
|
||||||
|
"Invalid name",
|
||||||
|
"Use a short name like battle-log or honorifics_extra.\n"
|
||||||
|
"Reserved: quirks, game, translation.",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
if stem in self._custom_skill_editors:
|
||||||
|
QMessageBox.information(
|
||||||
|
self, "Already exists", f"Custom skill '{stem}' is already open."
|
||||||
|
)
|
||||||
|
self._select_game_skill_tab(stem)
|
||||||
|
return
|
||||||
|
|
||||||
|
path = custom_skill_path_for_game(root, stem)
|
||||||
|
if path is None:
|
||||||
|
return
|
||||||
|
if path.is_file():
|
||||||
|
self._reload_custom_skills()
|
||||||
|
self._select_game_skill_tab(stem)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_text(
|
||||||
|
f"# {stem}\n\n"
|
||||||
|
"# Keep this short and specific. Conflicting rules hurt quality.\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
self._log(f"❌ Could not create skills/{stem}.md: {exc}")
|
||||||
|
return
|
||||||
|
|
||||||
|
page = self._custom_skill_editor_page(stem)
|
||||||
|
idx = self._add_game_skill_page(stem, page)
|
||||||
|
self._game_skills_bar.setCurrentIndex(idx)
|
||||||
|
self._reload_one_custom_skill(stem)
|
||||||
|
self._log(f"✅ Created custom skill {path}")
|
||||||
|
|
||||||
|
def _delete_custom_skill(self, stem: str) -> None:
|
||||||
|
root = self._game_root_or_warn()
|
||||||
|
if not root:
|
||||||
|
return
|
||||||
|
path = custom_skill_path_for_game(root, stem)
|
||||||
|
reply = QMessageBox.question(
|
||||||
|
self,
|
||||||
|
"Delete custom skill",
|
||||||
|
f"Delete skills/{stem}.md from the game folder?",
|
||||||
|
QMessageBox.Yes | QMessageBox.No,
|
||||||
|
QMessageBox.No,
|
||||||
|
)
|
||||||
|
if reply != QMessageBox.Yes:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
if path and path.is_file():
|
||||||
|
path.unlink()
|
||||||
|
self._log(f"🗑 Deleted {path}")
|
||||||
|
except Exception as exc:
|
||||||
|
self._log(f"❌ Could not delete skills/{stem}.md: {exc}")
|
||||||
|
return
|
||||||
|
self._reload_custom_skills()
|
||||||
|
|
@ -6,8 +6,8 @@ Mirrors the RPGMaker WorkflowTab, driven by the vendored WolfDawn ``wolf`` CLI
|
||||||
|
|
||||||
Step 0 Project - select game folder; import wolf_json/ into files/ (like RPGMaker)
|
Step 0 Project - select game folder; import wolf_json/ into files/ (like RPGMaker)
|
||||||
Step 1 Pre-process - optional dazedformat + gameupdate/ copy before translating
|
Step 1 Pre-process - optional dazedformat + gameupdate/ copy before translating
|
||||||
Step 2 Glossary - build vocab.txt (characters / worldbuilding terms) before
|
Step 2 Setup - Project Setup skill + vocab / quirks / game skills editors
|
||||||
translating so the AI keeps names and voice consistent
|
(build glossary and API overlays before translating)
|
||||||
Step 3 Names - translate names.json (Phase 0). WolfDawn safe entries
|
Step 3 Names - translate names.json (Phase 0). WolfDawn safe entries
|
||||||
are translated per-name; refs and verify names are skipped.
|
are translated per-name; refs and verify names are skipped.
|
||||||
Harvests short name terms into vocab.txt.
|
Harvests short name terms into vocab.txt.
|
||||||
|
|
@ -99,7 +99,8 @@ from util.wolfdawn import db_classify as wolf_db
|
||||||
from util.wolfdawn import wrap_search as wolf_ws
|
from util.wolfdawn import wrap_search as wolf_ws
|
||||||
import util.dazedwrap as dazedwrap
|
import util.dazedwrap as dazedwrap
|
||||||
|
|
||||||
from util.paths import PROJECT_ROOT
|
from gui.setup_skills_editors import SetupSkillsEditors
|
||||||
|
from util.paths import PROJECT_ROOT, VOCAB_PATH
|
||||||
from util.project_scanner import (
|
from util.project_scanner import (
|
||||||
detect_wolf_layout,
|
detect_wolf_layout,
|
||||||
find_wolf_text_archives,
|
find_wolf_text_archives,
|
||||||
|
|
@ -111,7 +112,7 @@ from util.project_scanner import (
|
||||||
wolf_repair_nested_data_dir,
|
wolf_repair_nested_data_dir,
|
||||||
wolf_unpack_out_dir,
|
wolf_unpack_out_dir,
|
||||||
)
|
)
|
||||||
from util.vocab import read_game_vocab, write_game_vocab
|
from util.skills import load_project_setup
|
||||||
|
|
||||||
# Workflow-level label for the non-batch (live) translation path. The Translation
|
# Workflow-level label for the non-batch (live) translation path. The Translation
|
||||||
# tab's own mode is called "Translate"; _workflow_mode_text() maps to that.
|
# tab's own mode is called "Translate"; _workflow_mode_text() maps to that.
|
||||||
|
|
@ -128,93 +129,6 @@ PHASE_NAMES_KINDS = {"names"}
|
||||||
PHASE_DB_KINDS = {"db"}
|
PHASE_DB_KINDS = {"db"}
|
||||||
PHASE_MAPS_EVENTS_KINDS = {"map", "common", "gamedat", "txt", "txt-dir"}
|
PHASE_MAPS_EVENTS_KINDS = {"map", "common", "gamedat", "txt", "txt-dir"}
|
||||||
|
|
||||||
# Glossary-discovery prompt for Copilot / Cursor, tailored to WolfDawn's extracted
|
|
||||||
# JSON (source/text pairs staged in files/). Produces the same two-section format
|
|
||||||
# the shared vocab.txt expects, so the AI output pastes straight into the editor.
|
|
||||||
_WOLF_GLOSSARY_PROMPT = (
|
|
||||||
"You are an expert Japanese WOLF RPG Editor game analyst building a translation glossary.\n"
|
|
||||||
"\n"
|
|
||||||
"<task>\n"
|
|
||||||
"Extract named characters and lore-specific worldbuilding terms from this game's extracted "
|
|
||||||
"text. 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 the extracted JSON in files/ here before continuing ---\n"
|
|
||||||
"\n"
|
|
||||||
"<data_format>\n"
|
|
||||||
"The files/ folder holds WolfDawn extractions as JSON. Each file is a list of entries with a\n"
|
|
||||||
"'source' field (the original Japanese) and a 'text' field (initially empty/identical).\n"
|
|
||||||
"Analyse the 'source' fields only. The files are:\n"
|
|
||||||
" - DataBase.project.json, CDataBase.project.json, SysDatabase.project.json — databases:\n"
|
|
||||||
" richest, small source of character/actor names, classes, factions, and lore titles.\n"
|
|
||||||
" - CommonEvent.dat.json — common events: dialogue and system text.\n"
|
|
||||||
" - Game.dat.json — game/system strings (title, terms).\n"
|
|
||||||
" - <Map>.mps.json — per-map events: the main story dialogue (can be large).\n"
|
|
||||||
" - Evtext.json — external event text, when present.\n"
|
|
||||||
" - names.json — item/skill/enemy value names (translated separately in Step 3; do NOT list them).\n"
|
|
||||||
"</data_format>\n"
|
|
||||||
"\n"
|
|
||||||
"<file_strategy>\n"
|
|
||||||
"Map files can be extremely large. Do NOT read them sequentially — you will hit context "
|
|
||||||
"limits. Instead:\n"
|
|
||||||
"1. Read the DataBase.project.json files IN FULL first — small and the best source of names.\n"
|
|
||||||
"2. For large map files, SEARCH (grep) the 'source' fields instead of reading sequentially. "
|
|
||||||
"Prioritise dialogue because it is the best evidence for character voice:\n"
|
|
||||||
" - Speaker patterns such as 【Name】, [Name], Name「…」, Name:, and <Name> at the start of a line\n"
|
|
||||||
" - Katakana clusters or kanji compound proper nouns that recur across dialogue\n"
|
|
||||||
" Scan the lowest-numbered maps first — early maps have the most story content.\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 an em dash or en dash. "
|
|
||||||
"The translation tool only recognises the plain hyphen.\n"
|
|
||||||
"- Descriptions must be entirely in English. Refer to other characters by English name only.\n"
|
|
||||||
"- Never give two spelling options (e.g. 'Sylfia / Sylphia' is wrong). Commit to one translation.\n"
|
|
||||||
"\n"
|
|
||||||
"# Game Characters — rules:\n"
|
|
||||||
"- Discover named characters from the databases and dialogue. Skip unnamed NPCs, generic "
|
|
||||||
"enemies, and narration-only entries.\n"
|
|
||||||
"- For each character include: gender, role, speech register, and personality. Note if the "
|
|
||||||
"name is player-chosen (a hero/actor whose name comes from a variable or input prompt).\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/armour names (translated via names.json in "
|
|
||||||
"Step 3). Skip generic RPG words. 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"
|
|
||||||
"シロ (Shiro) - Female; protagonist; player-named; speaks in a flustered, cute register "
|
|
||||||
"with feminine speech markers\n"
|
|
||||||
"ゼクス (Zex) - Male; antagonist; cold and commanding; uses an 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 late-game dialogue; "
|
|
||||||
"not a named map location\n"
|
|
||||||
"裁定者 (Arbiter) - Title held by the ruling council; lore-specific rank with no "
|
|
||||||
"real-world equivalent\n"
|
|
||||||
"</example>\n"
|
|
||||||
"</output_format>\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# Speaker-format prompt for a repo-aware AI. WolfDawn already detects and tags who
|
# Speaker-format prompt for a repo-aware AI. WolfDawn already detects and tags who
|
||||||
# speaks on each line, so the only decision left is whether its LOW-confidence
|
# speaks on each line, so the only decision left is whether its LOW-confidence
|
||||||
# first-line guesses are really speaker names for this game. The AI inspects the
|
# first-line guesses are really speaker names for this game. The AI inspects the
|
||||||
|
|
@ -620,7 +534,7 @@ class WolfWorkflowTab(QWidget):
|
||||||
_tab_defs = [
|
_tab_defs = [
|
||||||
("0 Project", self._build_step0),
|
("0 Project", self._build_step0),
|
||||||
("1 Pre-process", self._build_step1_preprocess),
|
("1 Pre-process", self._build_step1_preprocess),
|
||||||
("2 Glossary", self._build_step2_glossary),
|
("2 Setup", self._build_step2_setup),
|
||||||
("3 Names", self._build_step3_names),
|
("3 Names", self._build_step3_names),
|
||||||
("4 Database", self._build_step4_database),
|
("4 Database", self._build_step4_database),
|
||||||
("5 Maps/Events", self._build_step5_maps_events),
|
("5 Maps/Events", self._build_step5_maps_events),
|
||||||
|
|
@ -1142,6 +1056,8 @@ class WolfWorkflowTab(QWidget):
|
||||||
|
|
||||||
self._save_setting("last_game_folder", folder)
|
self._save_setting("last_game_folder", folder)
|
||||||
self._game_root = folder
|
self._game_root = folder
|
||||||
|
if hasattr(self, "setup_editors"):
|
||||||
|
self.setup_editors.reload_all()
|
||||||
self.detected_label.setText("Scanning…")
|
self.detected_label.setText("Scanning…")
|
||||||
self.detected_label.setStyleSheet(
|
self.detected_label.setStyleSheet(
|
||||||
"color:#9d9d9d;font-size:13px;padding:4px 8px;"
|
"color:#9d9d9d;font-size:13px;padding:4px 8px;"
|
||||||
|
|
@ -1767,80 +1683,97 @@ class WolfWorkflowTab(QWidget):
|
||||||
|
|
||||||
self._run_task(task, on_done=on_done)
|
self._run_task(task, on_done=on_done)
|
||||||
|
|
||||||
# ── Step 2: Glossary ───────────────────────────────────────────────────────
|
# ── Step 2: Setup ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def _build_step2_glossary(self, layout: QVBoxLayout):
|
def _build_step2_setup(self, layout: QVBoxLayout):
|
||||||
layout.addWidget(_make_section_label("Step 2 · Glossary (build before translating)"))
|
layout.addWidget(_make_section_label("Step 2 · Setup (Project Setup + Skills)"))
|
||||||
layout.addWidget(self._desc(
|
layout.addWidget(self._desc(
|
||||||
"vocab.txt is the project-wide glossary used by every translation batch to keep "
|
"Copy Project Setup into Cursor/Copilot with files/ open. Paste glossary into Vocab, "
|
||||||
"character names, speech register, honorifics, and lore terms consistent. Build it "
|
"quirks into Quirks, game_skill into Game skills. Speakers advice (LOWCONF_FIRSTLINE) "
|
||||||
"before translating (Steps 4-5) so the AI already knows every character and term. "
|
"is applied via the Step 5 checkbox - keep that step self-contained."
|
||||||
"Copy the prompt below into Cursor or Copilot Chat with the extracted files/ JSON "
|
|
||||||
"open, let it analyse the game, then paste its output into the editor and save."
|
|
||||||
))
|
))
|
||||||
prompt_btn = _make_btn("📋 Copy glossary prompt for Copilot / Cursor", "#5a3a7a")
|
|
||||||
prompt_btn.clicked.connect(self._copy_wolf_glossary_prompt)
|
|
||||||
layout.addWidget(prompt_btn)
|
|
||||||
|
|
||||||
fmt = QLabel(
|
actions = QHBoxLayout()
|
||||||
"Format: Japanese (English) - description\n"
|
actions.setSpacing(8)
|
||||||
"Example: シロ (Shiro) - Female; protagonist; flustered, cute register"
|
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."
|
||||||
)
|
)
|
||||||
fmt.setFont(QFont("Consolas", 9))
|
copy_btn.clicked.connect(self._copy_project_setup_prompt)
|
||||||
fmt.setWordWrap(True)
|
actions.addWidget(copy_btn)
|
||||||
fmt.setStyleSheet(
|
actions.addStretch()
|
||||||
"color:#569cd6;background-color:#1a1e2a;border:1px solid #2a3a5a;"
|
layout.addLayout(actions)
|
||||||
"padding:5px 10px;border-radius:4px;font-size:12px;border-left:3px solid #2a6a9a;"
|
|
||||||
)
|
|
||||||
layout.addWidget(fmt)
|
|
||||||
|
|
||||||
self.vocab_editor = QTextEdit()
|
|
||||||
self.vocab_editor.setMinimumHeight(120)
|
|
||||||
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;selection-background-color:#264f78;}"
|
|
||||||
)
|
|
||||||
self._reload_vocab()
|
|
||||||
layout.addWidget(self.vocab_editor, 1)
|
|
||||||
|
|
||||||
vrow = QHBoxLayout()
|
|
||||||
save_btn = _make_btn("💾 Save vocab.txt", "#3a7a3a")
|
|
||||||
save_btn.clicked.connect(self._save_vocab)
|
|
||||||
vrow.addWidget(save_btn)
|
|
||||||
reload_btn = _make_btn("↺ Reload", "#555")
|
|
||||||
reload_btn.clicked.connect(self._reload_vocab)
|
|
||||||
vrow.addWidget(reload_btn)
|
|
||||||
vrow.addStretch()
|
|
||||||
layout.addLayout(vrow)
|
|
||||||
|
|
||||||
note = self._desc(
|
note = self._desc(
|
||||||
"You don't need to add item / skill / enemy value names here: Phase 0 translates "
|
"Do not list names.json item/skill/enemy values in the glossary - Phase 0 "
|
||||||
"WolfDawn safe entries from names.json and harvests them into vocab.txt. "
|
"(Step 3) harvests those. Focus Vocab on characters and worldbuilding."
|
||||||
"Foundation DB also merges short labels (map names, titles) after Step 4. "
|
|
||||||
"Focus this glossary on characters and worldbuilding terms."
|
|
||||||
)
|
)
|
||||||
layout.addWidget(note)
|
layout.addWidget(note)
|
||||||
|
|
||||||
def _copy_wolf_glossary_prompt(self):
|
self.setup_editors = SetupSkillsEditors(
|
||||||
try:
|
self,
|
||||||
QApplication.clipboard().setText(_WOLF_GLOSSARY_PROMPT)
|
game_root_fn=lambda: self.folder_edit.text().strip() or self._game_root,
|
||||||
self._log("📋 Glossary prompt copied. Paste it into Cursor/Copilot with files/ open.")
|
log_fn=self._log,
|
||||||
except Exception as exc:
|
)
|
||||||
self._log(f"❌ Could not copy glossary prompt: {exc}")
|
layout.addWidget(self.setup_editors, 1)
|
||||||
|
self.setup_editors.reload_all()
|
||||||
|
|
||||||
def _reload_vocab(self):
|
def _copy_project_setup_prompt(self):
|
||||||
|
"""Copy the Wolf Project Setup skill, optionally prepending known vocab speakers."""
|
||||||
try:
|
try:
|
||||||
self.vocab_editor.setPlainText(read_game_vocab())
|
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 already listed in vocab.txt.\n"
|
||||||
|
"For the glossary block '# Game Characters', prefer entries for these names, "
|
||||||
|
"then cross-check DataBase*.project.json and dialogue for other major names.\n"
|
||||||
|
"\n"
|
||||||
|
+ speaker_lines
|
||||||
|
+ "\n</known_speakers>\n"
|
||||||
|
)
|
||||||
|
prompt = load_project_setup("wolf", prepend=prepend)
|
||||||
|
QApplication.clipboard().setText(prompt)
|
||||||
|
self._log("📋 Project Setup skill copied. Paste it into Cursor/Copilot with files/ open.")
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
self._log(f"❌ Could not load vocab.txt: {exc}")
|
self._log(f"❌ Could not load Project Setup skill: {exc}")
|
||||||
|
|
||||||
def _save_vocab(self):
|
def _read_vocab_speakers(self) -> list[tuple[str, str]]:
|
||||||
|
"""Parse '# Speakers' (or '# Game Characters') from vocab.txt as (orig, tl) pairs."""
|
||||||
|
vocab_path = VOCAB_PATH
|
||||||
|
if not vocab_path.exists():
|
||||||
|
return []
|
||||||
try:
|
try:
|
||||||
write_game_vocab(self.vocab_editor.toPlainText())
|
content = vocab_path.read_text(encoding="utf-8")
|
||||||
self._log("✅ vocab.txt saved (base terms from vocab_base.txt appended).")
|
except Exception:
|
||||||
except Exception as exc:
|
return []
|
||||||
self._log(f"❌ Could not save vocab.txt: {exc}")
|
|
||||||
|
results = []
|
||||||
|
for header in (r"Speakers", r"Game Characters"):
|
||||||
|
m = re.search(
|
||||||
|
rf"^[ ]*#\s*{header}\s*$\r?\n(.*?)(?=^[ ]*#|\Z)",
|
||||||
|
content,
|
||||||
|
re.MULTILINE | re.DOTALL,
|
||||||
|
)
|
||||||
|
if not m:
|
||||||
|
continue
|
||||||
|
for line in m.group(1).splitlines():
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
pm = re.match(r"^(.+?)\s+\((.+?)\)", line)
|
||||||
|
if pm:
|
||||||
|
pair = (pm.group(1), pm.group(2))
|
||||||
|
if pair not in results:
|
||||||
|
results.append(pair)
|
||||||
|
if results:
|
||||||
|
break
|
||||||
|
return results
|
||||||
|
|
||||||
# ── Step 4: Database ───────────────────────────────────────────────────────
|
# ── Step 4: Database ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -3912,7 +3845,7 @@ class WolfWorkflowTab(QWidget):
|
||||||
short_names = (
|
short_names = (
|
||||||
"Project",
|
"Project",
|
||||||
"Prep",
|
"Prep",
|
||||||
"Glossary",
|
"Setup",
|
||||||
"Names",
|
"Names",
|
||||||
"Database",
|
"Database",
|
||||||
"Maps",
|
"Maps",
|
||||||
|
|
|
||||||
|
|
@ -21,17 +21,8 @@ import threading
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from util.paths import VOCAB_PATH
|
from util.paths import VOCAB_PATH
|
||||||
from util.skills import (
|
from util.skills import load_project_setup
|
||||||
custom_skill_path_for_game,
|
|
||||||
game_skill_path_for_game,
|
|
||||||
list_custom_skill_paths,
|
|
||||||
load_project_setup,
|
|
||||||
migrate_game_skill_text,
|
|
||||||
quirks_path_for_game,
|
|
||||||
sanitize_custom_skill_stem,
|
|
||||||
)
|
|
||||||
from util.vocab import BASE_SEPARATOR as _SHARED_BASE_SEPARATOR
|
from util.vocab import BASE_SEPARATOR as _SHARED_BASE_SEPARATOR
|
||||||
from util.vocab import read_game_vocab, write_game_vocab
|
|
||||||
|
|
||||||
import jsbeautifier
|
import jsbeautifier
|
||||||
|
|
||||||
|
|
@ -46,7 +37,6 @@ from PyQt5.QtWidgets import (
|
||||||
QGridLayout,
|
QGridLayout,
|
||||||
QGroupBox,
|
QGroupBox,
|
||||||
QHBoxLayout,
|
QHBoxLayout,
|
||||||
QInputDialog,
|
|
||||||
QLabel,
|
QLabel,
|
||||||
QLineEdit,
|
QLineEdit,
|
||||||
QListWidget,
|
QListWidget,
|
||||||
|
|
@ -57,9 +47,7 @@ from PyQt5.QtWidgets import (
|
||||||
QSizePolicy,
|
QSizePolicy,
|
||||||
QSpinBox,
|
QSpinBox,
|
||||||
QSplitter,
|
QSplitter,
|
||||||
QStackedWidget,
|
|
||||||
QStyle,
|
QStyle,
|
||||||
QTabBar,
|
|
||||||
QTabWidget,
|
QTabWidget,
|
||||||
QTextEdit,
|
QTextEdit,
|
||||||
QToolButton,
|
QToolButton,
|
||||||
|
|
@ -68,6 +56,8 @@ from PyQt5.QtWidgets import (
|
||||||
QAbstractItemView,
|
QAbstractItemView,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from gui.setup_skills_editors import SetupSkillsEditors
|
||||||
|
|
||||||
from gui.translation_tab import (
|
from gui.translation_tab import (
|
||||||
BATCH_MODE_LABEL,
|
BATCH_MODE_LABEL,
|
||||||
BATCH_MODE_BENEFIT_NOTE,
|
BATCH_MODE_BENEFIT_NOTE,
|
||||||
|
|
@ -622,13 +612,6 @@ def _make_icon_btn(icon_text: str, tooltip: str = "") -> QPushButton:
|
||||||
# Helpers
|
# Helpers
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
class _PlainPasteTextEdit(QTextEdit):
|
|
||||||
"""QTextEdit that always pastes as plain text to avoid spurious newlines."""
|
|
||||||
|
|
||||||
def insertFromMimeData(self, source): # noqa: N802
|
|
||||||
self.insertPlainText(source.text())
|
|
||||||
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
# Main widget
|
# Main widget
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
@ -1716,153 +1699,13 @@ class WorkflowTab(QWidget):
|
||||||
layout.addWidget(top)
|
layout.addWidget(top)
|
||||||
self._populate_speaker_flags()
|
self._populate_speaker_flags()
|
||||||
|
|
||||||
# ---- Editors (tabbed to save vertical space) -----------------------
|
self.setup_editors = SetupSkillsEditors(
|
||||||
editors = QTabWidget()
|
self,
|
||||||
editors.setObjectName("setupEditors")
|
game_root_fn=lambda: self.folder_edit.text().strip(),
|
||||||
editors.setDocumentMode(False)
|
log_fn=self._log,
|
||||||
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(
|
layout.addWidget(self.setup_editors, 1)
|
||||||
"QTabWidget::pane{border:1px solid #3c3c3c;background:#1e1e1e;}"
|
self.setup_editors.reload_all()
|
||||||
)
|
|
||||||
_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",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Game skills: built-in game.md + optional custom overlays
|
|
||||||
game_skills_page = QWidget()
|
|
||||||
gs_layout = QVBoxLayout(game_skills_page)
|
|
||||||
gs_layout.setContentsMargins(0, 0, 0, 0)
|
|
||||||
gs_layout.setSpacing(0)
|
|
||||||
|
|
||||||
self._custom_skill_editors: dict[str, QTextEdit] = {}
|
|
||||||
self._game_skills_bar = QTabBar()
|
|
||||||
self._game_skills_bar.setDocumentMode(True)
|
|
||||||
self._game_skills_bar.setExpanding(False)
|
|
||||||
self._game_skills_bar.setDrawBase(False)
|
|
||||||
self._game_skills_bar.setMinimumHeight(30)
|
|
||||||
self._game_skills_bar.setStyleSheet(
|
|
||||||
"QTabBar::tab{background:#2d2d30;color:#9d9d9d;padding:6px 12px;"
|
|
||||||
"border:1px solid #3c3c3c;border-bottom:none;margin-right:2px;}"
|
|
||||||
"QTabBar::tab:selected{background:#1e1e1e;color:#e0e0e0;}"
|
|
||||||
"QTabBar::tab:hover{color:#ffffff;}"
|
|
||||||
)
|
|
||||||
self._game_skills_stack = QStackedWidget()
|
|
||||||
self._game_skills_stack.setStyleSheet("background:#1e1e1e;")
|
|
||||||
self._game_skills_bar.currentChanged.connect(self._game_skills_stack.setCurrentIndex)
|
|
||||||
|
|
||||||
strip = QWidget()
|
|
||||||
strip.setStyleSheet("background-color:#252526;")
|
|
||||||
strip_l = QHBoxLayout(strip)
|
|
||||||
strip_l.setContentsMargins(8, 6, 10, 0)
|
|
||||||
strip_l.setSpacing(6)
|
|
||||||
strip_l.addWidget(self._game_skills_bar, 0, Qt.AlignBottom)
|
|
||||||
|
|
||||||
add_btn = QPushButton("+ Add custom")
|
|
||||||
add_btn.setCursor(Qt.PointingHandCursor)
|
|
||||||
add_btn.setFixedHeight(30)
|
|
||||||
add_btn.setToolTip(
|
|
||||||
"Create a custom skills/*.md overlay.\n"
|
|
||||||
"Merged into the translation system prompt - can hurt quality."
|
|
||||||
)
|
|
||||||
add_btn.setStyleSheet(
|
|
||||||
"QPushButton{background-color:#2d2d30;color:#4ec9b0;"
|
|
||||||
"border:1px solid #3c3c3c;border-bottom:none;"
|
|
||||||
"border-top-left-radius:3px;border-top-right-radius:3px;"
|
|
||||||
"padding:0 12px;font-size:12px;font-weight:bold;}"
|
|
||||||
"QPushButton:hover{background-color:#3a3a3a;color:#7ee0c8;"
|
|
||||||
"border-color:#4ec9b0;}"
|
|
||||||
"QPushButton:pressed{background-color:#1e1e1e;}"
|
|
||||||
)
|
|
||||||
add_btn.clicked.connect(self._add_custom_skill)
|
|
||||||
strip_l.addWidget(add_btn, 0, Qt.AlignBottom)
|
|
||||||
strip_l.addStretch(1)
|
|
||||||
gs_layout.addWidget(strip)
|
|
||||||
gs_layout.addWidget(self._game_skills_stack, 1)
|
|
||||||
|
|
||||||
self._add_game_skill_page(
|
|
||||||
"game",
|
|
||||||
_editor_page(
|
|
||||||
"<game>/skills/game.md",
|
|
||||||
"Paste the game_skill Translation Frame. Merged into the translation "
|
|
||||||
"system prompt (before quirks) when this game folder is selected.",
|
|
||||||
self._save_game_skill,
|
|
||||||
self._reload_game_skill,
|
|
||||||
"game_skill_editor",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
editors.addTab(game_skills_page, "Game skills")
|
|
||||||
layout.addWidget(editors, 1)
|
|
||||||
|
|
||||||
self._reload_vocab()
|
|
||||||
self._reload_quirks()
|
|
||||||
self._reload_game_skill()
|
|
||||||
self._reload_custom_skills()
|
|
||||||
|
|
||||||
|
|
||||||
def _run_parse_speakers(self):
|
def _run_parse_speakers(self):
|
||||||
|
|
@ -3196,9 +3039,8 @@ class WorkflowTab(QWidget):
|
||||||
return
|
return
|
||||||
|
|
||||||
self._save_setting("last_game_folder", folder)
|
self._save_setting("last_game_folder", folder)
|
||||||
self._reload_quirks()
|
if hasattr(self, "setup_editors"):
|
||||||
self._reload_game_skill()
|
self.setup_editors.reload_all()
|
||||||
self._reload_custom_skills()
|
|
||||||
self.detected_label.setText("Scanning…")
|
self.detected_label.setText("Scanning…")
|
||||||
self.detected_label.setStyleSheet(
|
self.detected_label.setStyleSheet(
|
||||||
"color:#9d9d9d;font-size:13px;padding:4px 8px;"
|
"color:#9d9d9d;font-size:13px;padding:4px 8px;"
|
||||||
|
|
@ -3556,292 +3398,6 @@ class WorkflowTab(QWidget):
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
self._log(f"❌ Could not load Project Setup skill: {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:
|
|
||||||
raw = path.read_text(encoding="utf-8")
|
|
||||||
fixed = migrate_game_skill_text(raw)
|
|
||||||
self.game_skill_editor.setPlainText(fixed)
|
|
||||||
if fixed != raw:
|
|
||||||
path.write_text(fixed.rstrip() + "\n", encoding="utf-8")
|
|
||||||
self._log(
|
|
||||||
f"↺ Updated legacy quirk path in {path.name} → skills/quirks.md"
|
|
||||||
)
|
|
||||||
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)
|
|
||||||
text = migrate_game_skill_text(self.game_skill_editor.toPlainText())
|
|
||||||
self.game_skill_editor.setPlainText(text)
|
|
||||||
path.write_text(text.rstrip() + "\n", encoding="utf-8")
|
|
||||||
self._log(f"✅ Saved {path}")
|
|
||||||
except Exception as exc:
|
|
||||||
self._log(f"❌ Could not save game skill: {exc}")
|
|
||||||
|
|
||||||
def _add_game_skill_page(self, title: str, page: QWidget) -> int:
|
|
||||||
"""Add a page to the Game skills bar + stack. Returns the new index."""
|
|
||||||
idx = self._game_skills_stack.addWidget(page)
|
|
||||||
self._game_skills_bar.addTab(title)
|
|
||||||
return idx
|
|
||||||
|
|
||||||
def _select_game_skill_tab(self, title: str) -> None:
|
|
||||||
for i in range(self._game_skills_bar.count()):
|
|
||||||
if self._game_skills_bar.tabText(i) == title:
|
|
||||||
self._game_skills_bar.setCurrentIndex(i)
|
|
||||||
return
|
|
||||||
|
|
||||||
def _custom_skill_editor_page(self, stem: str) -> QWidget:
|
|
||||||
"""Editor page for one custom skill file under <game>/skills/<stem>.md."""
|
|
||||||
page = QWidget()
|
|
||||||
vl = QVBoxLayout(page)
|
|
||||||
vl.setContentsMargins(8, 8, 8, 8)
|
|
||||||
vl.setSpacing(6)
|
|
||||||
|
|
||||||
tip = QLabel(
|
|
||||||
"Custom skill - merged into the translation system prompt. "
|
|
||||||
"Extra or conflicting rules can hurt quality."
|
|
||||||
)
|
|
||||||
tip.setWordWrap(True)
|
|
||||||
tip.setStyleSheet("color:#c9a227;font-size:12px;")
|
|
||||||
vl.addWidget(tip)
|
|
||||||
|
|
||||||
path_lbl = QLabel(f"<game>/skills/{stem}.md")
|
|
||||||
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(
|
|
||||||
"QTextEdit{background-color:#1e1e1e;color:#d4d4d4;"
|
|
||||||
"border:none;padding:8px;"
|
|
||||||
"selection-background-color:#264f78;}"
|
|
||||||
)
|
|
||||||
self._custom_skill_editors[stem] = ed
|
|
||||||
vl.addWidget(ed, 1)
|
|
||||||
|
|
||||||
row = QHBoxLayout()
|
|
||||||
row.setSpacing(8)
|
|
||||||
save_btn = _make_btn("💾 Save", "#3a7a3a")
|
|
||||||
save_btn.setFixedWidth(110)
|
|
||||||
save_btn.clicked.connect(lambda _=False, s=stem: self._save_custom_skill(s))
|
|
||||||
row.addWidget(save_btn)
|
|
||||||
|
|
||||||
reload_btn = _make_btn("↺ Reload", "#555")
|
|
||||||
reload_btn.setFixedWidth(110)
|
|
||||||
reload_btn.clicked.connect(lambda _=False, s=stem: self._reload_one_custom_skill(s))
|
|
||||||
row.addWidget(reload_btn)
|
|
||||||
|
|
||||||
delete_btn = _make_btn("🗑 Delete", "#8b0000")
|
|
||||||
delete_btn.setFixedWidth(110)
|
|
||||||
delete_btn.setToolTip("Delete this custom skill file from the game folder")
|
|
||||||
delete_btn.clicked.connect(lambda _=False, s=stem: self._delete_custom_skill(s))
|
|
||||||
row.addWidget(delete_btn)
|
|
||||||
row.addStretch()
|
|
||||||
vl.addLayout(row)
|
|
||||||
return page
|
|
||||||
|
|
||||||
def _reload_custom_skills(self):
|
|
||||||
"""Rebuild custom skill tabs from <game>/skills/*.md (excluding built-ins)."""
|
|
||||||
bar = getattr(self, "_game_skills_bar", None)
|
|
||||||
stack = getattr(self, "_game_skills_stack", None)
|
|
||||||
if bar is None or stack is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Remove existing custom tabs (index 0 is always game.md).
|
|
||||||
while bar.count() > 1:
|
|
||||||
bar.removeTab(1)
|
|
||||||
while stack.count() > 1:
|
|
||||||
w = stack.widget(1)
|
|
||||||
stack.removeWidget(w)
|
|
||||||
if w is not None:
|
|
||||||
w.deleteLater()
|
|
||||||
self._custom_skill_editors = {}
|
|
||||||
|
|
||||||
root = self.folder_edit.text().strip()
|
|
||||||
for path in list_custom_skill_paths(root):
|
|
||||||
stem = path.stem
|
|
||||||
page = self._custom_skill_editor_page(stem)
|
|
||||||
self._add_game_skill_page(stem, page)
|
|
||||||
try:
|
|
||||||
self._custom_skill_editors[stem].setPlainText(
|
|
||||||
path.read_text(encoding="utf-8")
|
|
||||||
)
|
|
||||||
except Exception as exc:
|
|
||||||
self._log(f"❌ Could not load skills/{stem}.md: {exc}")
|
|
||||||
|
|
||||||
def _reload_one_custom_skill(self, stem: str):
|
|
||||||
root = self.folder_edit.text().strip()
|
|
||||||
path = custom_skill_path_for_game(root, stem)
|
|
||||||
ed = self._custom_skill_editors.get(stem)
|
|
||||||
if ed is None:
|
|
||||||
return
|
|
||||||
if not path or not path.is_file():
|
|
||||||
ed.setPlainText("")
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
ed.setPlainText(path.read_text(encoding="utf-8"))
|
|
||||||
except Exception as exc:
|
|
||||||
self._log(f"❌ Could not load skills/{stem}.md: {exc}")
|
|
||||||
|
|
||||||
def _save_custom_skill(self, stem: str):
|
|
||||||
root = self._game_root_or_warn()
|
|
||||||
if not root:
|
|
||||||
return
|
|
||||||
path = custom_skill_path_for_game(root, stem)
|
|
||||||
ed = self._custom_skill_editors.get(stem)
|
|
||||||
if path is None or ed is None:
|
|
||||||
self._log("❌ Invalid custom skill.")
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
path.write_text(ed.toPlainText().rstrip() + "\n", encoding="utf-8")
|
|
||||||
self._log(f"✅ Saved {path}")
|
|
||||||
except Exception as exc:
|
|
||||||
self._log(f"❌ Could not save skills/{stem}.md: {exc}")
|
|
||||||
|
|
||||||
def _add_custom_skill(self):
|
|
||||||
root = self._game_root_or_warn()
|
|
||||||
if not root:
|
|
||||||
return
|
|
||||||
|
|
||||||
warn = QMessageBox(self)
|
|
||||||
warn.setIcon(QMessageBox.Warning)
|
|
||||||
warn.setWindowTitle("Custom skill - quality warning")
|
|
||||||
warn.setTextFormat(Qt.RichText)
|
|
||||||
warn.setText(
|
|
||||||
"<b>Custom skills are merged into the translation system prompt.</b><br><br>"
|
|
||||||
"Extra or poorly written skills can <b>distract the model and hurt "
|
|
||||||
"translation quality</b> (conflicting rules, prompt bloat, diluted quirks).<br><br>"
|
|
||||||
"Prefer <code>quirks.md</code> for voice rules and <code>game.md</code> "
|
|
||||||
"for the Translation Frame. Add a custom skill only if you need a rare, tightly scoped "
|
|
||||||
"overlay - <b>at your own risk</b>."
|
|
||||||
)
|
|
||||||
warn.setStandardButtons(QMessageBox.Cancel | QMessageBox.Ok)
|
|
||||||
warn.button(QMessageBox.Ok).setText("I understand - continue")
|
|
||||||
warn.setStyleSheet(
|
|
||||||
"QMessageBox{background-color:#252526;}"
|
|
||||||
"QLabel{color:#d4d4d4;font-size:13px;min-width:420px;}"
|
|
||||||
)
|
|
||||||
if warn.exec_() != QMessageBox.Ok:
|
|
||||||
return
|
|
||||||
|
|
||||||
name, ok = QInputDialog.getText(
|
|
||||||
self,
|
|
||||||
"New custom skill",
|
|
||||||
"Skill file name (letters, numbers, ._- ; saved as <game>/skills/<name>.md):",
|
|
||||||
)
|
|
||||||
if not ok:
|
|
||||||
return
|
|
||||||
stem = sanitize_custom_skill_stem(name)
|
|
||||||
if not stem:
|
|
||||||
QMessageBox.warning(
|
|
||||||
self,
|
|
||||||
"Invalid name",
|
|
||||||
"Use a short name like battle-log or honorifics_extra.\n"
|
|
||||||
"Reserved: quirks, game, translation.",
|
|
||||||
)
|
|
||||||
return
|
|
||||||
if stem in self._custom_skill_editors:
|
|
||||||
QMessageBox.information(
|
|
||||||
self, "Already exists", f"Custom skill '{stem}' is already open."
|
|
||||||
)
|
|
||||||
self._select_game_skill_tab(stem)
|
|
||||||
return
|
|
||||||
|
|
||||||
path = custom_skill_path_for_game(root, stem)
|
|
||||||
if path is None:
|
|
||||||
return
|
|
||||||
if path.is_file():
|
|
||||||
# File exists on disk but tab missing - just reload tabs.
|
|
||||||
self._reload_custom_skills()
|
|
||||||
self._select_game_skill_tab(stem)
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
path.write_text(
|
|
||||||
f"# {stem}\n\n"
|
|
||||||
"# Keep this short and specific. Conflicting rules hurt quality.\n",
|
|
||||||
encoding="utf-8",
|
|
||||||
)
|
|
||||||
except Exception as exc:
|
|
||||||
self._log(f"❌ Could not create skills/{stem}.md: {exc}")
|
|
||||||
return
|
|
||||||
|
|
||||||
page = self._custom_skill_editor_page(stem)
|
|
||||||
idx = self._add_game_skill_page(stem, page)
|
|
||||||
self._game_skills_bar.setCurrentIndex(idx)
|
|
||||||
self._reload_one_custom_skill(stem)
|
|
||||||
self._log(f"✅ Created custom skill {path}")
|
|
||||||
|
|
||||||
def _delete_custom_skill(self, stem: str):
|
|
||||||
root = self._game_root_or_warn()
|
|
||||||
if not root:
|
|
||||||
return
|
|
||||||
path = custom_skill_path_for_game(root, stem)
|
|
||||||
reply = QMessageBox.question(
|
|
||||||
self,
|
|
||||||
"Delete custom skill",
|
|
||||||
f"Delete skills/{stem}.md from the game folder?",
|
|
||||||
QMessageBox.Yes | QMessageBox.No,
|
|
||||||
QMessageBox.No,
|
|
||||||
)
|
|
||||||
if reply != QMessageBox.Yes:
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
if path and path.is_file():
|
|
||||||
path.unlink()
|
|
||||||
self._log(f"🗑 Deleted {path}")
|
|
||||||
except Exception as exc:
|
|
||||||
self._log(f"❌ Could not delete skills/{stem}.md: {exc}")
|
|
||||||
return
|
|
||||||
self._reload_custom_skills()
|
|
||||||
|
|
||||||
def _read_vocab_speakers(self) -> list[tuple[str, str]]:
|
def _read_vocab_speakers(self) -> list[tuple[str, str]]:
|
||||||
"""Parse the '# Speakers' section from vocab.txt and return (orig, tl) pairs."""
|
"""Parse the '# Speakers' section from vocab.txt and return (orig, tl) pairs."""
|
||||||
vocab_path = VOCAB_PATH
|
vocab_path = VOCAB_PATH
|
||||||
|
|
@ -3873,23 +3429,6 @@ class WorkflowTab(QWidget):
|
||||||
results.append((pm.group(1), pm.group(2)))
|
results.append((pm.group(1), pm.group(2)))
|
||||||
return results
|
return results
|
||||||
|
|
||||||
def _reload_vocab(self):
|
|
||||||
try:
|
|
||||||
self.vocab_editor.setPlainText(read_game_vocab())
|
|
||||||
except Exception as exc:
|
|
||||||
self._log(f"❌ Could not load vocab.txt: {exc}")
|
|
||||||
|
|
||||||
def _save_vocab(self):
|
|
||||||
try:
|
|
||||||
write_game_vocab(self.vocab_editor.toPlainText())
|
|
||||||
self._log("✅ vocab.txt saved (base terms from vocab_base.txt appended).")
|
|
||||||
except Exception as exc:
|
|
||||||
self._log(f"❌ Could not save vocab.txt: {exc}")
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────
|
|
||||||
# Step 2 – Actor substitution
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────
|
||||||
# Step 3 – Speaker detection
|
# Step 3 – Speaker detection
|
||||||
# ─────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue