Updating stuff
This commit is contained in:
parent
2e17604718
commit
601c64fe9f
9 changed files with 449 additions and 34 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -18,7 +18,7 @@ __pycache__
|
|||
|
||||
!assets/*.png
|
||||
!.pre-commit-config.yaml
|
||||
!data/prompt.txt
|
||||
!data/skills/**
|
||||
!data/vocab_base.txt
|
||||
!requirements.txt
|
||||
!util/tl_inspector/TLInspector.js
|
||||
|
|
|
|||
10
TODO.md
10
TODO.md
|
|
@ -1,19 +1,13 @@
|
|||
# TODO
|
||||
|
||||
## Custom game prompt
|
||||
|
||||
- Keep the default prompt (`data/skills/system.md`) as the base for every game.
|
||||
- Add an optional **per-game custom prompt** in the UI that merges with the default (custom rules layered on top of shared instructions, not a full replacement unless explicitly chosen).
|
||||
- Surface this in settings / project setup so each game folder can carry its own tone, terminology notes, or content warnings without forking the global prompt.
|
||||
|
||||
## Local translation cache (game retranslate)
|
||||
|
||||
- Persist every successful translation locally so a full retranslate from scratch can reuse prior results instead of paying again.
|
||||
- **Cache key:** hash the translation **payload** (source Japanese text sent to the model) plus target language — same approach as the existing global cache in `util/translation.py` (`get_cache_key`, `log/translation_cache.json`).
|
||||
- **Cache key:** hash the translation **payload** (source Japanese text sent to the model) plus target language - same approach as the existing global cache in `util.translation` (`get_cache_key`, `log/translation_cache.json`).
|
||||
Payload keys naturally dedupe identical lines across files and survive re-extract / re-import as long as the source string is unchanged.
|
||||
- **Scope:** consider a per-game cache file under the game project (e.g. `<game_root>/translation_cache.json` or alongside `wolf_json/`) in addition to (or merged with) the tool-wide `log/translation_cache.json`, so caches travel with the game folder and are easy to back up.
|
||||
- **Write path:** on every successful translate (live and batch consume), store `{cache_key: translation}` before or as results land in `translated/`.
|
||||
- **Read path:** before calling the API, check cache; on hit, skip the request and apply the cached translation (respecting file-specific re-expansion where the payload is Japanese-only).
|
||||
- **Retranslate workflow:** when the user re-imports fresh JSON and runs Translate again, cache hits should fill `translated/` automatically for unchanged source lines; only new or edited source text should incur API cost.
|
||||
- **UI (later):** cache stats (hits / misses / estimated savings), export/import, clear cache for one game, optional “prefer cache” toggle.
|
||||
- **UI (later):** cache stats (hits / misses / estimated savings), export/import, clear cache for one game, optional "prefer cache" toggle.
|
||||
- **Invalidation:** optional metadata (model id, prompt version) in the key or entry if we need to avoid reusing stale translations after prompt or model changes.
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ Hard rules:
|
|||
1. Per-character voice → `glossary` only.
|
||||
2. Category-wide / cross-cutting voice → `translation_quirks` only.
|
||||
3. Default honorifics policy is owned by the tool base prompt - only note **unusual** honorific habits in quirks.
|
||||
4. `game_skill` **references** `skills/quirks.md`; do not reprint those rules.
|
||||
4. `game_skill` **references** `skills/quirks.md` only; do not reprint those rules or cite legacy names like `translation_quirks.txt`.
|
||||
5. `speakers` is config, not lore.
|
||||
|
||||
---
|
||||
|
|
@ -101,10 +101,15 @@ Output as short imperative bullets suitable to paste into `skills/quirks.md`.
|
|||
Produce a durable IDE skill for this title saved at `skills/translation.md`:
|
||||
- Short game identity (tone/genre/content notes)
|
||||
- Which files matter for dialogue/DB in this repo
|
||||
- Explicit pointer: follow `skills/quirks.md` for voice rules (authoritative)
|
||||
- A **Voice rules** section that points only at ``skills/quirks.md`` (authoritative)
|
||||
- What not to change (codes, formatting - tool-owned via base prompt)
|
||||
- Do **not** reprint quirks or the glossary
|
||||
|
||||
**Path names (required):**
|
||||
- Voice quirks file: ``skills/quirks.md`` (never ``translation_quirks.txt`` or any other legacy name)
|
||||
- This IDE skill file: ``skills/translation.md``
|
||||
- Optional user overlays (if mentioned): other ``skills/*.md`` files except ``quirks.md`` / ``translation.md``
|
||||
|
||||
<!-- /engine:rpgmaker -->
|
||||
|
||||
<!-- engine:wolf -->
|
||||
|
|
@ -160,6 +165,28 @@ Label the fence language as `translation_quirks`. Inside: imperative bullet list
|
|||
|
||||
### Block `game_skill`
|
||||
|
||||
Label the fence language as `game_skill`. Inside: full markdown for `skills/translation.md` (identity, file map, quirks pointer, tool boundaries).
|
||||
Label the fence language as `game_skill`. Inside: full markdown for `skills/translation.md`.
|
||||
|
||||
Required skeleton (fill in the blanks; keep the Voice rules path exactly):
|
||||
|
||||
```markdown
|
||||
# <Game title> - Translation skill
|
||||
|
||||
## Identity
|
||||
<1-3 sentences: tone, genre, content notes>
|
||||
|
||||
## Files that matter
|
||||
- <dialogue / events paths for this repo>
|
||||
- <database paths>
|
||||
|
||||
## Voice rules
|
||||
Follow `skills/quirks.md` as the authoritative cross-cutting voice guide for this title.
|
||||
Do not invent alternate quirk filenames.
|
||||
|
||||
## Tool boundaries
|
||||
- DazedMTLTool owns formatting codes, wrap, line counts, and honorifics defaults via its base system skill.
|
||||
- Per-character register lives in the glossary (`vocab.txt`), not here.
|
||||
- Do not reprint quirks or glossary entries in this file.
|
||||
```
|
||||
|
||||
If known speakers were prepended in a `<known_speakers>` block above this skill, prefer those names in the glossary, then cross-check Actors.json for other major named actors.
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ You will be translating erotic and sexual content. You will receive lines of dia
|
|||
|
||||
- **Always preserve and enforce Japanese honorifics** in the English translation: -san, -kun, -chan, -senpai, -sensei, -sama, -dono, etc.
|
||||
- Do **not** drop honorifics to make the English sound more "natural" unless the source Japanese also lacks them.
|
||||
- If `# Game Characters` (or game quirks) specifies how a character addresses someone, follow that address pattern.
|
||||
- If `# Game Characters` (or `<game>/skills/quirks.md`) specifies how a character addresses someone, follow that address pattern.
|
||||
- The `=` or `=` character in a Japanese name marks a foreign or nickname component. Wrap that part in parentheses.
|
||||
- Example: `バンカー=ベット` → `Bunker (Bet)`
|
||||
- Always translate speaker tags to English: `[クロネ]:` → `[Kurone]:`
|
||||
|
|
|
|||
|
|
@ -59,8 +59,8 @@ class SkillsTab(QWidget):
|
|||
|
||||
intro = QLabel(
|
||||
"Tool-level AI instructions live here. "
|
||||
"Per-game quirks and the game IDE skill are edited in Workflow Step 3 "
|
||||
"(they travel with the game folder)."
|
||||
"Per-game quirks, the game IDE skill, and optional custom skills are edited "
|
||||
"in Workflow Step 2 (they travel with the game folder)."
|
||||
)
|
||||
intro.setWordWrap(True)
|
||||
intro.setStyleSheet(_HINT_STYLE)
|
||||
|
|
|
|||
|
|
@ -22,9 +22,13 @@ 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.vocab import BASE_SEPARATOR as _SHARED_BASE_SEPARATOR
|
||||
from util.vocab import read_game_vocab, write_game_vocab
|
||||
|
|
@ -42,6 +46,7 @@ from PyQt5.QtWidgets import (
|
|||
QGridLayout,
|
||||
QGroupBox,
|
||||
QHBoxLayout,
|
||||
QInputDialog,
|
||||
QLabel,
|
||||
QLineEdit,
|
||||
QListWidget,
|
||||
|
|
@ -52,7 +57,9 @@ from PyQt5.QtWidgets import (
|
|||
QSizePolicy,
|
||||
QSpinBox,
|
||||
QSplitter,
|
||||
QStackedWidget,
|
||||
QStyle,
|
||||
QTabBar,
|
||||
QTabWidget,
|
||||
QTextEdit,
|
||||
QToolButton,
|
||||
|
|
@ -437,8 +444,10 @@ _STEP_HELP: dict[int, str] = {
|
|||
"• <code>speakers</code> → apply flags above<br>"
|
||||
"• <code>translation_quirks</code> → Quirks tab "
|
||||
"(<code>skills/quirks.md</code>)<br>"
|
||||
"• <code>game_skill</code> → Game skill tab "
|
||||
"• <code>game_skill</code> → Game skills → translation tab "
|
||||
"(<code>skills/translation.md</code>)<br><br>"
|
||||
"Optional: <b>+</b> on Game skills adds custom <code>skills/*.md</code> "
|
||||
"overlays (merged into the API prompt - can hurt quality; use sparingly).<br><br>"
|
||||
"Use one game folder per translation run so quirks stay in the prompt cache."
|
||||
),
|
||||
3: (
|
||||
|
|
@ -1858,21 +1867,77 @@ class WorkflowTab(QWidget):
|
|||
),
|
||||
"Quirks",
|
||||
)
|
||||
editors.addTab(
|
||||
|
||||
# Game skills: built-in translation.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(
|
||||
"translation",
|
||||
_editor_page(
|
||||
"<game>/skills/translation.md",
|
||||
"Paste the game_skill block. IDE companion skill for this game repo.",
|
||||
"Paste the game_skill block. IDE companion skill for this game repo "
|
||||
"(not injected into the API prompt).",
|
||||
self._save_game_skill,
|
||||
self._reload_game_skill,
|
||||
"game_skill_editor",
|
||||
),
|
||||
"Game skill",
|
||||
)
|
||||
|
||||
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):
|
||||
|
|
@ -3122,8 +3187,6 @@ class WorkflowTab(QWidget):
|
|||
self._detected_on_show = True # new folder chosen — treat as already-shown
|
||||
self._ask_clear_old_files()
|
||||
self._detect_folder()
|
||||
self._reload_quirks()
|
||||
self._reload_game_skill()
|
||||
|
||||
def _ask_clear_old_files(self):
|
||||
"""Prompt the user to clear /files and /translated to avoid stale data conflicts."""
|
||||
|
|
@ -3224,6 +3287,9 @@ class WorkflowTab(QWidget):
|
|||
return
|
||||
|
||||
self._save_setting("last_game_folder", folder)
|
||||
self._reload_quirks()
|
||||
self._reload_game_skill()
|
||||
self._reload_custom_skills()
|
||||
self.detected_label.setText("Scanning…")
|
||||
self.detected_label.setStyleSheet(
|
||||
"color:#9d9d9d;font-size:13px;padding:4px 8px;"
|
||||
|
|
@ -3620,7 +3686,14 @@ class WorkflowTab(QWidget):
|
|||
self.game_skill_editor.setPlainText("")
|
||||
return
|
||||
try:
|
||||
self.game_skill_editor.setPlainText(path.read_text(encoding="utf-8"))
|
||||
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}")
|
||||
|
||||
|
|
@ -3631,11 +3704,235 @@ class WorkflowTab(QWidget):
|
|||
path = game_skill_path_for_game(root)
|
||||
try:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(self.game_skill_editor.toPlainText().rstrip() + "\n", encoding="utf-8")
|
||||
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 translation).
|
||||
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>translation.md</code> "
|
||||
"for IDE guidance. 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, 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
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ TRANSLATION_CONTEXTS_PATH = DATA_DIR / "translation_contexts.json"
|
|||
GAME_QUIRKS_RELATIVE = Path("skills") / "quirks.md"
|
||||
LEGACY_QUIRKS_FILENAME = "translation_quirks.txt"
|
||||
GAME_SKILL_RELATIVE = Path("skills") / "translation.md"
|
||||
# Built-in skill filenames under <game>/skills/ (not user-custom overlays).
|
||||
GAME_SKILL_RESERVED_NAMES = frozenset({"quirks.md", "translation.md"})
|
||||
|
||||
_ROOT_DATA_FILES = (
|
||||
"vocab.txt",
|
||||
|
|
|
|||
|
|
@ -5,17 +5,25 @@ from __future__ import annotations
|
|||
from util.skills.contexts import ctx, reload_contexts
|
||||
from util.skills.setup import load_project_setup, skills_dir
|
||||
from util.skills.system import (
|
||||
custom_skill_path_for_game,
|
||||
game_skill_path_for_game,
|
||||
list_custom_skill_paths,
|
||||
load_system_prompt,
|
||||
migrate_game_skill_text,
|
||||
quirks_path_for_game,
|
||||
sanitize_custom_skill_stem,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"ctx",
|
||||
"custom_skill_path_for_game",
|
||||
"game_skill_path_for_game",
|
||||
"list_custom_skill_paths",
|
||||
"load_project_setup",
|
||||
"load_system_prompt",
|
||||
"migrate_game_skill_text",
|
||||
"quirks_path_for_game",
|
||||
"reload_contexts",
|
||||
"sanitize_custom_skill_stem",
|
||||
"skills_dir",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -3,15 +3,19 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from util.paths import (
|
||||
GAME_QUIRKS_RELATIVE,
|
||||
GAME_SKILL_RELATIVE,
|
||||
GAME_SKILL_RESERVED_NAMES,
|
||||
LEGACY_QUIRKS_FILENAME,
|
||||
PROMPT_PATH,
|
||||
)
|
||||
|
||||
_SKILL_NAME_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$")
|
||||
|
||||
|
||||
def quirks_path_for_game(game_root: str | Path | None) -> Path | None:
|
||||
"""Return ``<game_root>/skills/quirks.md`` when *game_root* is set.
|
||||
|
|
@ -40,10 +44,83 @@ def game_skill_path_for_game(game_root: str | Path | None) -> Path | None:
|
|||
return Path(game_root) / GAME_SKILL_RELATIVE
|
||||
|
||||
|
||||
def load_system_prompt(game_root: str | Path | None = None) -> str:
|
||||
"""Load ``data/skills/system.md`` and optionally append per-game quirks.
|
||||
def migrate_game_skill_text(text: str) -> str:
|
||||
"""Rewrite legacy quirk path names inside a game skill markdown body."""
|
||||
if not text:
|
||||
return text
|
||||
# Common stale pointers from older Project Setup output.
|
||||
updated = text.replace("translation_quirks.txt", "skills/quirks.md")
|
||||
updated = updated.replace("`translation_quirks.md`", "`skills/quirks.md`")
|
||||
# Bare relative quirks.md without skills/ prefix (keep skills/quirks.md intact).
|
||||
updated = re.sub(
|
||||
r"(?<!skills/)(?<![\w./-])quirks\.md(?![\w.-])",
|
||||
"skills/quirks.md",
|
||||
updated,
|
||||
)
|
||||
# Avoid double skills/skills/
|
||||
updated = updated.replace("skills/skills/quirks.md", "skills/quirks.md")
|
||||
return updated
|
||||
|
||||
Quirks path resolution order:
|
||||
|
||||
def game_skills_dir(game_root: str | Path | None) -> Path | None:
|
||||
"""Return ``<game_root>/skills`` when *game_root* is set."""
|
||||
if not game_root:
|
||||
return None
|
||||
return Path(game_root) / "skills"
|
||||
|
||||
|
||||
def sanitize_custom_skill_stem(name: str) -> str | None:
|
||||
"""Return a safe skill stem (no ``.md``) or None if invalid/reserved."""
|
||||
raw = (name or "").strip()
|
||||
if not raw:
|
||||
return None
|
||||
if raw.lower().endswith(".md"):
|
||||
raw = raw[:-3].strip()
|
||||
if not _SKILL_NAME_RE.match(raw):
|
||||
return None
|
||||
filename = f"{raw}.md"
|
||||
if filename.lower() in {n.lower() for n in GAME_SKILL_RESERVED_NAMES}:
|
||||
return None
|
||||
return raw
|
||||
|
||||
|
||||
def custom_skill_path_for_game(
|
||||
game_root: str | Path | None, name: str
|
||||
) -> Path | None:
|
||||
"""Return ``<game_root>/skills/<name>.md`` for a valid custom skill name."""
|
||||
stem = sanitize_custom_skill_stem(name)
|
||||
skills = game_skills_dir(game_root)
|
||||
if not stem or skills is None:
|
||||
return None
|
||||
return skills / f"{stem}.md"
|
||||
|
||||
|
||||
def list_custom_skill_paths(game_root: str | Path | None) -> list[Path]:
|
||||
"""List user custom skill markdown files under ``<game>/skills/``.
|
||||
|
||||
Excludes built-in ``quirks.md`` and ``translation.md``.
|
||||
"""
|
||||
skills = game_skills_dir(game_root)
|
||||
if skills is None or not skills.is_dir():
|
||||
return []
|
||||
reserved = {n.lower() for n in GAME_SKILL_RESERVED_NAMES}
|
||||
out: list[Path] = []
|
||||
for path in sorted(skills.glob("*.md"), key=lambda p: p.name.lower()):
|
||||
if path.name.lower() in reserved:
|
||||
continue
|
||||
if path.is_file():
|
||||
out.append(path)
|
||||
return out
|
||||
|
||||
|
||||
def load_system_prompt(game_root: str | Path | None = None) -> str:
|
||||
"""Load ``data/skills/system.md`` plus optional per-game overlays.
|
||||
|
||||
Overlay order:
|
||||
1. ``skills/quirks.md`` (voice quirks)
|
||||
2. Any other ``skills/*.md`` except ``translation.md`` (custom skills)
|
||||
|
||||
Path resolution order for the game root:
|
||||
1. Explicit *game_root* argument
|
||||
2. ``DAZED_GAME_ROOT`` environment variable
|
||||
"""
|
||||
|
|
@ -53,14 +130,24 @@ def load_system_prompt(game_root: str | Path | None = None) -> str:
|
|||
|
||||
root = game_root or os.getenv("DAZED_GAME_ROOT") or ""
|
||||
root = str(root).strip()
|
||||
quirks_file = quirks_path_for_game(root) if root else None
|
||||
if not root:
|
||||
return base
|
||||
|
||||
parts = [base.rstrip()] if base.strip() else []
|
||||
|
||||
quirks_file = quirks_path_for_game(root)
|
||||
if quirks_file and quirks_file.is_file():
|
||||
quirks = quirks_file.read_text(encoding="utf-8").strip()
|
||||
if quirks:
|
||||
return (
|
||||
base.rstrip()
|
||||
+ "\n\n## Game-Specific Translation Quirks\n\n"
|
||||
+ quirks
|
||||
+ "\n"
|
||||
)
|
||||
return base
|
||||
parts.append("## Game-Specific Translation Quirks\n\n" + quirks)
|
||||
|
||||
for path in list_custom_skill_paths(root):
|
||||
body = path.read_text(encoding="utf-8").strip()
|
||||
if not body:
|
||||
continue
|
||||
title = path.stem.replace("_", " ").replace("-", " ").strip() or path.stem
|
||||
parts.append(f"## Custom Game Skill: {title}\n\n{body}")
|
||||
|
||||
if not parts:
|
||||
return base
|
||||
return "\n\n".join(parts) + "\n"
|
||||
|
|
|
|||
Loading…
Reference in a new issue