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 -->
|
||||
|
||||
### 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 -->
|
||||
|
||||
|
|
|
|||
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 1 Pre-process - optional dazedformat + gameupdate/ copy before translating
|
||||
Step 2 Glossary - build vocab.txt (characters / worldbuilding terms) before
|
||||
translating so the AI keeps names and voice consistent
|
||||
Step 2 Setup - Project Setup skill + vocab / quirks / game skills editors
|
||||
(build glossary and API overlays before translating)
|
||||
Step 3 Names - translate names.json (Phase 0). WolfDawn safe entries
|
||||
are translated per-name; refs and verify names are skipped.
|
||||
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
|
||||
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 (
|
||||
detect_wolf_layout,
|
||||
find_wolf_text_archives,
|
||||
|
|
@ -111,7 +112,7 @@ from util.project_scanner import (
|
|||
wolf_repair_nested_data_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
|
||||
# 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_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
|
||||
# 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
|
||||
|
|
@ -620,7 +534,7 @@ class WolfWorkflowTab(QWidget):
|
|||
_tab_defs = [
|
||||
("0 Project", self._build_step0),
|
||||
("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),
|
||||
("4 Database", self._build_step4_database),
|
||||
("5 Maps/Events", self._build_step5_maps_events),
|
||||
|
|
@ -1142,6 +1056,8 @@ class WolfWorkflowTab(QWidget):
|
|||
|
||||
self._save_setting("last_game_folder", folder)
|
||||
self._game_root = folder
|
||||
if hasattr(self, "setup_editors"):
|
||||
self.setup_editors.reload_all()
|
||||
self.detected_label.setText("Scanning…")
|
||||
self.detected_label.setStyleSheet(
|
||||
"color:#9d9d9d;font-size:13px;padding:4px 8px;"
|
||||
|
|
@ -1767,80 +1683,97 @@ class WolfWorkflowTab(QWidget):
|
|||
|
||||
self._run_task(task, on_done=on_done)
|
||||
|
||||
# ── Step 2: Glossary ───────────────────────────────────────────────────────
|
||||
# ── Step 2: Setup ──────────────────────────────────────────────────────────
|
||||
|
||||
def _build_step2_glossary(self, layout: QVBoxLayout):
|
||||
layout.addWidget(_make_section_label("Step 2 · Glossary (build before translating)"))
|
||||
def _build_step2_setup(self, layout: QVBoxLayout):
|
||||
layout.addWidget(_make_section_label("Step 2 · Setup (Project Setup + Skills)"))
|
||||
layout.addWidget(self._desc(
|
||||
"vocab.txt is the project-wide glossary used by every translation batch to keep "
|
||||
"character names, speech register, honorifics, and lore terms consistent. Build it "
|
||||
"before translating (Steps 4-5) so the AI already knows every character and term. "
|
||||
"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."
|
||||
"Copy Project Setup into Cursor/Copilot with files/ open. Paste glossary into Vocab, "
|
||||
"quirks into Quirks, game_skill into Game skills. Speakers advice (LOWCONF_FIRSTLINE) "
|
||||
"is applied via the Step 5 checkbox - keep that step self-contained."
|
||||
))
|
||||
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(
|
||||
"Format: Japanese (English) - description\n"
|
||||
"Example: シロ (Shiro) - Female; protagonist; flustered, cute register"
|
||||
actions = QHBoxLayout()
|
||||
actions.setSpacing(8)
|
||||
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))
|
||||
fmt.setWordWrap(True)
|
||||
fmt.setStyleSheet(
|
||||
"color:#569cd6;background-color:#1a1e2a;border:1px solid #2a3a5a;"
|
||||
"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)
|
||||
copy_btn.clicked.connect(self._copy_project_setup_prompt)
|
||||
actions.addWidget(copy_btn)
|
||||
actions.addStretch()
|
||||
layout.addLayout(actions)
|
||||
|
||||
note = self._desc(
|
||||
"You don't need to add item / skill / enemy value names here: Phase 0 translates "
|
||||
"WolfDawn safe entries from names.json and harvests them into vocab.txt. "
|
||||
"Foundation DB also merges short labels (map names, titles) after Step 4. "
|
||||
"Focus this glossary on characters and worldbuilding terms."
|
||||
"Do not list names.json item/skill/enemy values in the glossary - Phase 0 "
|
||||
"(Step 3) harvests those. Focus Vocab on characters and worldbuilding."
|
||||
)
|
||||
layout.addWidget(note)
|
||||
|
||||
def _copy_wolf_glossary_prompt(self):
|
||||
try:
|
||||
QApplication.clipboard().setText(_WOLF_GLOSSARY_PROMPT)
|
||||
self._log("📋 Glossary prompt copied. Paste it into Cursor/Copilot with files/ open.")
|
||||
except Exception as exc:
|
||||
self._log(f"❌ Could not copy glossary prompt: {exc}")
|
||||
self.setup_editors = SetupSkillsEditors(
|
||||
self,
|
||||
game_root_fn=lambda: self.folder_edit.text().strip() or self._game_root,
|
||||
log_fn=self._log,
|
||||
)
|
||||
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:
|
||||
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:
|
||||
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:
|
||||
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}")
|
||||
content = vocab_path.read_text(encoding="utf-8")
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
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 ───────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -3912,7 +3845,7 @@ class WolfWorkflowTab(QWidget):
|
|||
short_names = (
|
||||
"Project",
|
||||
"Prep",
|
||||
"Glossary",
|
||||
"Setup",
|
||||
"Names",
|
||||
"Database",
|
||||
"Maps",
|
||||
|
|
|
|||
|
|
@ -21,17 +21,8 @@ import threading
|
|||
from pathlib import Path
|
||||
|
||||
from util.paths import VOCAB_PATH
|
||||
from util.skills import (
|
||||
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.skills import load_project_setup
|
||||
from util.vocab import BASE_SEPARATOR as _SHARED_BASE_SEPARATOR
|
||||
from util.vocab import read_game_vocab, write_game_vocab
|
||||
|
||||
import jsbeautifier
|
||||
|
||||
|
|
@ -46,7 +37,6 @@ from PyQt5.QtWidgets import (
|
|||
QGridLayout,
|
||||
QGroupBox,
|
||||
QHBoxLayout,
|
||||
QInputDialog,
|
||||
QLabel,
|
||||
QLineEdit,
|
||||
QListWidget,
|
||||
|
|
@ -57,9 +47,7 @@ from PyQt5.QtWidgets import (
|
|||
QSizePolicy,
|
||||
QSpinBox,
|
||||
QSplitter,
|
||||
QStackedWidget,
|
||||
QStyle,
|
||||
QTabBar,
|
||||
QTabWidget,
|
||||
QTextEdit,
|
||||
QToolButton,
|
||||
|
|
@ -68,6 +56,8 @@ from PyQt5.QtWidgets import (
|
|||
QAbstractItemView,
|
||||
)
|
||||
|
||||
from gui.setup_skills_editors import SetupSkillsEditors
|
||||
|
||||
from gui.translation_tab import (
|
||||
BATCH_MODE_LABEL,
|
||||
BATCH_MODE_BENEFIT_NOTE,
|
||||
|
|
@ -622,13 +612,6 @@ def _make_icon_btn(icon_text: str, tooltip: str = "") -> QPushButton:
|
|||
# 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
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
|
@ -1716,153 +1699,13 @@ class WorkflowTab(QWidget):
|
|||
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;}"
|
||||
self.setup_editors = SetupSkillsEditors(
|
||||
self,
|
||||
game_root_fn=lambda: self.folder_edit.text().strip(),
|
||||
log_fn=self._log,
|
||||
)
|
||||
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",
|
||||
)
|
||||
|
||||
# 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()
|
||||
layout.addWidget(self.setup_editors, 1)
|
||||
self.setup_editors.reload_all()
|
||||
|
||||
|
||||
def _run_parse_speakers(self):
|
||||
|
|
@ -3196,9 +3039,8 @@ class WorkflowTab(QWidget):
|
|||
return
|
||||
|
||||
self._save_setting("last_game_folder", folder)
|
||||
self._reload_quirks()
|
||||
self._reload_game_skill()
|
||||
self._reload_custom_skills()
|
||||
if hasattr(self, "setup_editors"):
|
||||
self.setup_editors.reload_all()
|
||||
self.detected_label.setText("Scanning…")
|
||||
self.detected_label.setStyleSheet(
|
||||
"color:#9d9d9d;font-size:13px;padding:4px 8px;"
|
||||
|
|
@ -3556,292 +3398,6 @@ class WorkflowTab(QWidget):
|
|||
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:
|
||||
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]]:
|
||||
"""Parse the '# Speakers' section from vocab.txt and return (orig, tl) pairs."""
|
||||
vocab_path = VOCAB_PATH
|
||||
|
|
@ -3873,23 +3429,6 @@ class WorkflowTab(QWidget):
|
|||
results.append((pm.group(1), pm.group(2)))
|
||||
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
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
|
|
|
|||
Loading…
Reference in a new issue