Speaker Handling
This commit is contained in:
parent
10eb68503a
commit
e0989112d7
5 changed files with 115 additions and 37 deletions
|
|
@ -326,7 +326,7 @@ Open the **Workflow** tab and choose **Wolf RPG (WolfDawn)** from the engine sel
|
||||||
|------|--------|
|
|------|--------|
|
||||||
| **0 Project** | Select the game folder, **Unpack** the `.wolf` archives into a loose `Data/` folder, then **Extract text** (maps, common events, databases, `Game.dat`, external event text, and the project-wide name glossary). Everything is staged into `files/` for translation; `names.json` is translated later (Step 3) and the Step 2 bulk run skips it. Extraction also snapshots the pristine (untranslated) binaries into `wolf_json/originals/` so Step 4 can inject idempotently. **Set up git tracking** by copying the `gameupdate/` folder (updater scripts, `patch.sh`/`patch.ps1`, and a `.gitignore` that excludes the original game files) into the game root, so the translation project can be tracked with git and later shipped as a git-based patch players apply themselves. Finally, **Format extracted JSON** (dazedformat) normalises `wolf_json/` and `files/` to the same layout the translator writes back (`json.dump`, indent 4) — run it once and commit it as your baseline so later injects produce clean, line-level git diffs instead of reformatting whole files. |
|
| **0 Project** | Select the game folder, **Unpack** the `.wolf` archives into a loose `Data/` folder, then **Extract text** (maps, common events, databases, `Game.dat`, external event text, and the project-wide name glossary). Everything is staged into `files/` for translation; `names.json` is translated later (Step 3) and the Step 2 bulk run skips it. Extraction also snapshots the pristine (untranslated) binaries into `wolf_json/originals/` so Step 4 can inject idempotently. **Set up git tracking** by copying the `gameupdate/` folder (updater scripts, `patch.sh`/`patch.ps1`, and a `.gitignore` that excludes the original game files) into the game root, so the translation project can be tracked with git and later shipped as a git-based patch players apply themselves. Finally, **Format extracted JSON** (dazedformat) normalises `wolf_json/` and `files/` to the same layout the translator writes back (`json.dump`, indent 4) — run it once and commit it as your baseline so later injects produce clean, line-level git diffs instead of reformatting whole files. |
|
||||||
| **1 Glossary** | Build `vocab.txt` before translating: copy the WOLF-tailored prompt into Cursor/Copilot with the extracted `files/` JSON, let it discover character names, speech registers, and lore terms, then paste the result into the in-tab editor and save (the shared `vocab.txt` is used by every translation batch to keep names and voice consistent). Item/skill/enemy value names (`names.json`) are handled later in Step 3, not here. |
|
| **1 Glossary** | Build `vocab.txt` before translating: copy the WOLF-tailored prompt into Cursor/Copilot with the extracted `files/` JSON, let it discover character names, speech registers, and lore terms, then paste the result into the in-tab editor and save (the shared `vocab.txt` is used by every translation batch to keep names and voice consistent). Item/skill/enemy value names (`names.json`) are handled later in Step 3, not here. |
|
||||||
| **2 Translate** | Pick the **Translation mode** (Normal or Batch - Batch uses the Anthropic Batches API, ~50% cheaper, Claude only), then run the `Wolf RPG (WolfDawn)` module over `files/`. Under **Speaker handling**, toggle which speaker formats are reshaped: WolfDawn tags who speaks on each line, and for lines where the name is baked into the first line (a nameplate) the translator reshapes them into the `[Speaker]: line` convention the prompt understands (translating the speaker tag), then restores WOLF's native `Speaker⏎line` layout on inject. Turn a format off if a game's first lines are not really speaker names. Under **Text wrap width**, set whether/where translated dialogue re-wraps to fit WOLF's message box (same `dazedwrap` engine the RPG Maker workflow uses, saved to `.env` as `wolfWrap` / `wolfWidth`); only the dialogue body is wrapped - a speaker's nameplate line is always kept separate so the `⏎` after a name is never folded into the text. Only the `text` fields are filled in; `source` is preserved so injection can verify each line. |
|
| **2 Translate** | Pick the **Translation mode** (Normal or Batch - Batch uses the Anthropic Batches API, ~50% cheaper, Claude only), then run the `Wolf RPG (WolfDawn)` module over `files/`. Under **Speaker handling**, speaker attribution is automatic - WolfDawn already tags who speaks on each line, so lines with a real nameplate (a face window precedes the name) are always reshaped into the `[Speaker]: line` convention the prompt understands (translating the speaker tag), then restored to WOLF's native `Speaker⏎line` layout on inject. The only judgement call is WolfDawn's low-confidence guesses (a short first line with no face window that may or may not be a name); copy the **speaker-format prompt** to have the repo-aware AI inspect `files/` and recommend ENABLE/DISABLE, then set the single low-confidence checkbox to match (there is no toggle for the reliable nameplates - they need no configuration). Under **Text wrap width**, set whether/where translated dialogue re-wraps to fit WOLF's message box (same `dazedwrap` engine the RPG Maker workflow uses, saved to `.env` as `wolfWrap` / `wolfWidth`); only the dialogue body is wrapped - a speaker's nameplate line is always kept separate so the `⏎` after a name is never folded into the text. Only the `text` fields are filled in; `source` is preserved so injection can verify each line. |
|
||||||
| **3 Names** | Curate `names.json` (item/skill/enemy/variable value names). **Translating all of them breaks the game** - many WOLF value names double as logic keys (compared by value, used as variable/file/event names), so by default none are translated. WolfDawn tags each name with its WOLF database **category** (the `note` field), so instead of judging thousands of names you pick which *categories* are safe. Copy the **names-safety prompt** into a repo-aware AI (Cursor/Copilot with `files/` open); it groups names by category, checks how each is used, and returns a JSON list of the safe categories in a code block. Paste that list and click **Apply** to tick the matching rows (or tick them by hand - each row shows the category, its name count, and an example), then **Save** to `data/wolf_safe_notes.json`. Finally **Translate safe names** - the WolfDawn module only translates entries whose category you approved and leaves every other name identical to the source; `names.json`'s structure is unchanged. Conservative by design: nothing changes until you opt categories in. |
|
| **3 Names** | Curate `names.json` (item/skill/enemy/variable value names). **Translating all of them breaks the game** - many WOLF value names double as logic keys (compared by value, used as variable/file/event names), so by default none are translated. WolfDawn tags each name with its WOLF database **category** (the `note` field), so instead of judging thousands of names you pick which *categories* are safe. Copy the **names-safety prompt** into a repo-aware AI (Cursor/Copilot with `files/` open); it groups names by category, checks how each is used, and returns a JSON list of the safe categories in a code block. Paste that list and click **Apply** to tick the matching rows (or tick them by hand - each row shows the category, its name count, and an example), then **Save** to `data/wolf_safe_notes.json`. Finally **Translate safe names** - the WolfDawn module only translates entries whose category you approved and leaves every other name identical to the source; `names.json`'s structure is unchanged. Conservative by design: nothing changes until you opt categories in. |
|
||||||
| **4 Inject** | Write the translations back into the game's `Data/` binaries **and** refresh the git-tracked `wolf_json/` with the translated JSON, so the game project's git diff shows both the JSON source and the injected binaries. Injection always reads from a **pristine snapshot** of the original binaries (`wolf_json/originals/`, captured during Step 0 extraction and rebuilt from the `.wolf`/`.wolf.bak` archives if missing) and writes the result into the live `Data/` file. This keeps injection **idempotent** - WolfDawn locates each Japanese source string in the original, so re-injecting after fixing a few lines actually updates the binary instead of silently no-op'ing (the old in-place inject broke once a file had already been translated). Toggle `--en-punct` (convert Japanese punctuation to ASCII) and `--allow-code-drift` (relax the inline-code guard) as needed, and use **Check name consistency** to catch names translated differently across files. **Quick inject** lists the files you've translated so far (present in `translated/`) so you can tick just a few, inject them straight into `Data/`, and review the git diff / test in-game - ideal for iterating. **Inject all translations** writes every document, including the safe name values from Step 3, across `Data/`. |
|
| **4 Inject** | Write the translations back into the game's `Data/` binaries **and** refresh the git-tracked `wolf_json/` with the translated JSON, so the game project's git diff shows both the JSON source and the injected binaries. Injection always reads from a **pristine snapshot** of the original binaries (`wolf_json/originals/`, captured during Step 0 extraction and rebuilt from the `.wolf`/`.wolf.bak` archives if missing) and writes the result into the live `Data/` file. This keeps injection **idempotent** - WolfDawn locates each Japanese source string in the original, so re-injecting after fixing a few lines actually updates the binary instead of silently no-op'ing (the old in-place inject broke once a file had already been translated). Toggle `--en-punct` (convert Japanese punctuation to ASCII) and `--allow-code-drift` (relax the inline-code guard) as needed, and use **Check name consistency** to catch names translated differently across files. **Quick inject** lists the files you've translated so far (present in `translated/`) so you can tick just a few, inject them straight into `Data/`, and review the git diff / test in-game - ideal for iterating. **Inject all translations** writes every document, including the safe name values from Step 3, across `Data/`. |
|
||||||
| **5 Package** | Either run from the loose `Data/` folder (backs up `Data.wolf` → `Data.wolf.bak`), or **Repack** a fresh `Data.wolf`, inheriting the original archive's encryption. |
|
| **5 Package** | Either run from the loose `Data/` folder (backs up `Data.wolf` → `Data.wolf.bak`), or **Repack** a fresh `Data.wolf`, inheriting the original archive's encryption. |
|
||||||
|
|
|
||||||
|
|
@ -244,6 +244,47 @@ _WOLF_NAMES_PROMPT = (
|
||||||
"</output_format>\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
|
||||||
|
# extracted text and returns a single ENABLE/DISABLE recommendation.
|
||||||
|
_WOLF_SPEAKER_PROMPT = (
|
||||||
|
"You are an expert Japanese WOLF RPG Editor translator helping configure a translation tool.\n"
|
||||||
|
"\n"
|
||||||
|
"<background>\n"
|
||||||
|
"WolfDawn extracts each line with a 'speaker' and a 'speaker_src' tag describing how it "
|
||||||
|
"detected the speaker. Two tags carry the speaker name baked into the FIRST line of the "
|
||||||
|
"'source' text (the rest is the dialogue body):\n"
|
||||||
|
" - literal_line1 : HIGH confidence - a face/name window precedes the line, so the "
|
||||||
|
"first line really is a nameplate. The tool always reshapes these; nothing to decide.\n"
|
||||||
|
" - literal_line1_lowconf : LOW confidence - a short first line with NO preceding face "
|
||||||
|
"window. WolfDawn guesses it is a speaker name, but it might actually be the start of the "
|
||||||
|
"dialogue or narration.\n"
|
||||||
|
"When a first-line format is trusted, the tool reshapes 'Name\\nbody' into '[Name]: body' for "
|
||||||
|
"translation, then restores 'Name\\nbody' on inject (byte-safe either way).\n"
|
||||||
|
"</background>\n"
|
||||||
|
"\n"
|
||||||
|
"--- attach the extracted JSON in files/ here (maps / CommonEvent) before continuing ---\n"
|
||||||
|
"\n"
|
||||||
|
"<task>\n"
|
||||||
|
"Decide whether the LOW-confidence first-line guesses (speaker_src = literal_line1_lowconf) "
|
||||||
|
"should be treated as speaker names for THIS game. Inspect a good sample of those entries in "
|
||||||
|
"files/: look at the first line of each such 'source' and judge whether it is genuinely a "
|
||||||
|
"character/speaker label as opposed to real dialogue or narration.\n"
|
||||||
|
" - ENABLE if the low-confidence first lines are overwhelmingly real speaker names.\n"
|
||||||
|
" - DISABLE if many are actually dialogue/narration (reshaping them would mislabel lines).\n"
|
||||||
|
"(The high-confidence nameplates are always handled; you are only ruling on the guesses.)\n"
|
||||||
|
"</task>\n"
|
||||||
|
"\n"
|
||||||
|
"<output_format>\n"
|
||||||
|
"Return ONLY a fenced code block containing the recommendation and a one-line reason, e.g.\n"
|
||||||
|
"```\n"
|
||||||
|
"LOWCONF_FIRSTLINE: ENABLE - low-confidence first lines are short character names (市民, 兵士...).\n"
|
||||||
|
"```\n"
|
||||||
|
"Use exactly ENABLE or DISABLE after the colon.\n"
|
||||||
|
"</output_format>\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class _WolfTaskWorker(QThread):
|
class _WolfTaskWorker(QThread):
|
||||||
"""Run a blocking WolfDawn task callable off the UI thread.
|
"""Run a blocking WolfDawn task callable off the UI thread.
|
||||||
|
|
@ -998,41 +1039,53 @@ class WolfWorkflowTab(QWidget):
|
||||||
layout.addWidget(open_btn)
|
layout.addWidget(open_btn)
|
||||||
|
|
||||||
def _add_speaker_options(self, layout: QVBoxLayout):
|
def _add_speaker_options(self, layout: QVBoxLayout):
|
||||||
"""Toggles for which speaker formats are reshaped into '[Speaker]: line'."""
|
"""Speaker handling: nameplates are automatic; the low-confidence guess is
|
||||||
|
an AI-recommended per-game setting."""
|
||||||
from util import speakers as wolf_speakers
|
from util import speakers as wolf_speakers
|
||||||
|
|
||||||
layout.addWidget(_make_hr())
|
layout.addWidget(_make_hr())
|
||||||
layout.addWidget(self._subheading("Speaker handling"))
|
layout.addWidget(self._subheading("Speaker handling"))
|
||||||
layout.addWidget(self._desc(
|
layout.addWidget(self._desc(
|
||||||
"WolfDawn tags who is speaking on each line. For lines where the name is baked "
|
"WolfDawn already detects who speaks on each line, so this is automatic: lines "
|
||||||
"into the first line (a nameplate), the translator reshapes them into the "
|
"with a real nameplate (a face window precedes the name) are always reshaped into "
|
||||||
"\"[Speaker]: line\" format the prompt understands, translates the speaker tag, then "
|
"the \"[Speaker]: line\" format the prompt understands, translated, and restored to "
|
||||||
"restores WOLF's native \"Speaker⏎line\" layout on inject. Turn the formats off if a "
|
"WOLF's native \"Speaker⏎line\" layout on inject. The only judgement call is whether "
|
||||||
"game's first lines are not really speaker names."
|
"to trust WolfDawn's low-confidence guesses (a short first line with no face window "
|
||||||
|
"that might be a name - or might just be the start of dialogue)."
|
||||||
|
))
|
||||||
|
layout.addWidget(self._desc(
|
||||||
|
"Ask the AI managing the game repo to decide: the prompt has it inspect the "
|
||||||
|
"extracted files/ and recommend whether low-confidence first-line names should be "
|
||||||
|
"reshaped for this game. Then set the box below to match."
|
||||||
))
|
))
|
||||||
|
|
||||||
|
prompt_btn = _make_btn("📋 Copy speaker-format prompt for Copilot / Cursor", "#5a3a7a")
|
||||||
|
prompt_btn.clicked.connect(self._copy_wolf_speaker_prompt)
|
||||||
|
layout.addWidget(prompt_btn)
|
||||||
|
|
||||||
cfg = wolf_speakers.load_config()
|
cfg = wolf_speakers.load_config()
|
||||||
|
|
||||||
self._speaker_hi_cb = QCheckBox(
|
|
||||||
"High-confidence nameplates (a face window precedes the line)"
|
|
||||||
)
|
|
||||||
self._speaker_hi_cb.setChecked(bool(cfg.get("literal_line1", True)))
|
|
||||||
self._speaker_hi_cb.stateChanged.connect(self._save_speaker_options)
|
|
||||||
layout.addWidget(self._speaker_hi_cb)
|
|
||||||
|
|
||||||
self._speaker_lo_cb = QCheckBox(
|
self._speaker_lo_cb = QCheckBox(
|
||||||
"Low-confidence first-line guesses (short first line, no preceding face window)"
|
"Reshape low-confidence first-line names (enable if the AI says this game uses them)"
|
||||||
)
|
)
|
||||||
self._speaker_lo_cb.setChecked(bool(cfg.get("literal_line1_lowconf", True)))
|
self._speaker_lo_cb.setChecked(bool(cfg.get("literal_line1_lowconf", True)))
|
||||||
self._speaker_lo_cb.stateChanged.connect(self._save_speaker_options)
|
self._speaker_lo_cb.stateChanged.connect(self._save_speaker_options)
|
||||||
layout.addWidget(self._speaker_lo_cb)
|
layout.addWidget(self._speaker_lo_cb)
|
||||||
|
|
||||||
|
def _copy_wolf_speaker_prompt(self):
|
||||||
|
try:
|
||||||
|
QApplication.clipboard().setText(_WOLF_SPEAKER_PROMPT)
|
||||||
|
self._log(
|
||||||
|
"📋 Speaker-format prompt copied. Paste it into Cursor/Copilot with files/ open; "
|
||||||
|
"set the low-confidence box to match its recommendation."
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
self._log(f"❌ Could not copy speaker prompt: {exc}")
|
||||||
|
|
||||||
def _save_speaker_options(self):
|
def _save_speaker_options(self):
|
||||||
from util import speakers as wolf_speakers
|
from util import speakers as wolf_speakers
|
||||||
|
|
||||||
try:
|
try:
|
||||||
wolf_speakers.save_config({
|
wolf_speakers.save_config({
|
||||||
"literal_line1": self._speaker_hi_cb.isChecked(),
|
|
||||||
"literal_line1_lowconf": self._speaker_lo_cb.isChecked(),
|
"literal_line1_lowconf": self._speaker_lo_cb.isChecked(),
|
||||||
})
|
})
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
|
|
||||||
|
|
@ -36,8 +36,10 @@ Speakers: WolfDawn tags each line with ``speaker`` / ``speaker_src``. For the
|
||||||
first-line formats (``literal_line1`` / ``literal_line1_lowconf``) the speaker
|
first-line formats (``literal_line1`` / ``literal_line1_lowconf``) the speaker
|
||||||
name is baked into line 1 of ``source``. Those lines are reshaped into the shared
|
name is baked into line 1 of ``source``. Those lines are reshaped into the shared
|
||||||
``[Speaker]: line`` convention (which the prompt already translates) and restored
|
``[Speaker]: line`` convention (which the prompt already translates) and restored
|
||||||
to WOLF's native ``Speaker\nline`` layout on write-back. See ``util.speakers``;
|
to WOLF's native ``Speaker\nline`` layout on write-back. See ``util.speakers``.
|
||||||
which formats are reshaped is configurable from the workflow.
|
Detection is WolfDawn's, so the reliable nameplate (``literal_line1``) is always
|
||||||
|
reshaped; only the low-confidence guess (``literal_line1_lowconf``) is gated by a
|
||||||
|
per-game, AI-recommended setting from the workflow.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
@ -129,12 +131,13 @@ TRANSLATION_CONFIG = TranslationConfig(
|
||||||
|
|
||||||
def handleWolfDawn(filename, estimate):
|
def handleWolfDawn(filename, estimate):
|
||||||
"""Entry point used by the CLI/GUI dispatchers. Returns a summary string or 'Fail'."""
|
"""Entry point used by the CLI/GUI dispatchers. Returns a summary string or 'Fail'."""
|
||||||
global ESTIMATE, TOKENS, FILENAME, SAFE_NOTES
|
global ESTIMATE, TOKENS, FILENAME, SAFE_NOTES, SPEAKER_CONFIG
|
||||||
ESTIMATE = estimate
|
ESTIMATE = estimate
|
||||||
FILENAME = filename
|
FILENAME = filename
|
||||||
# Re-read the safe note categories so edits made in the workflow this session
|
# Re-read workflow-configured settings so edits made this session take effect
|
||||||
# take effect even when translation runs in-process (module import is cached).
|
# even when translation runs in-process (the module import is cached).
|
||||||
SAFE_NOTES = wolf_names.load_safe_notes()
|
SAFE_NOTES = wolf_names.load_safe_notes()
|
||||||
|
SPEAKER_CONFIG = wolf_speakers.load_config()
|
||||||
|
|
||||||
start = time.time()
|
start = time.time()
|
||||||
translatedData = openFiles(filename)
|
translatedData = openFiles(filename)
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,8 @@ sys.path.insert(0, str(ROOT))
|
||||||
|
|
||||||
from util import speakers as ws # noqa: E402
|
from util import speakers as ws # noqa: E402
|
||||||
|
|
||||||
ALL_ON = {"literal_line1": True, "literal_line1_lowconf": True}
|
ALL_ON = {"literal_line1_lowconf": True}
|
||||||
ALL_OFF = {"literal_line1": False, "literal_line1_lowconf": False}
|
ALL_OFF = {"literal_line1_lowconf": False}
|
||||||
|
|
||||||
|
|
||||||
class TestSplitSource(unittest.TestCase):
|
class TestSplitSource(unittest.TestCase):
|
||||||
|
|
@ -30,6 +30,11 @@ class TestSplitSource(unittest.TestCase):
|
||||||
def test_disabled_format_returns_none(self):
|
def test_disabled_format_returns_none(self):
|
||||||
self.assertIsNone(ws.split_source("市民\nおはよう", "literal_line1_lowconf", ALL_OFF))
|
self.assertIsNone(ws.split_source("市民\nおはよう", "literal_line1_lowconf", ALL_OFF))
|
||||||
|
|
||||||
|
def test_highconf_always_on_ignores_config(self):
|
||||||
|
# The reliable nameplate is reshaped regardless of config (no toggle).
|
||||||
|
out = ws.split_source("セルリア\nふふふ", "literal_line1", ALL_OFF)
|
||||||
|
self.assertEqual(out, ("", "セルリア", "ふふふ"))
|
||||||
|
|
||||||
def test_non_firstline_src_returns_none(self):
|
def test_non_firstline_src_returns_none(self):
|
||||||
for src in ("narration", "ui", "choice", "string_var", ""):
|
for src in ("narration", "ui", "choice", "string_var", ""):
|
||||||
self.assertIsNone(ws.split_source("市民\nおはよう", src, ALL_ON))
|
self.assertIsNone(ws.split_source("市民\nおはよう", src, ALL_ON))
|
||||||
|
|
@ -94,10 +99,10 @@ class TestConfigIO(unittest.TestCase):
|
||||||
try:
|
try:
|
||||||
with tempfile.TemporaryDirectory() as td:
|
with tempfile.TemporaryDirectory() as td:
|
||||||
ws.CONFIG_PATH = Path(td) / "wolf_speakers.json"
|
ws.CONFIG_PATH = Path(td) / "wolf_speakers.json"
|
||||||
ws.save_config({"literal_line1": False, "literal_line1_lowconf": True})
|
ws.save_config({"literal_line1_lowconf": False})
|
||||||
loaded = ws.load_config()
|
self.assertFalse(ws.load_config()["literal_line1_lowconf"])
|
||||||
self.assertFalse(loaded["literal_line1"])
|
ws.save_config({"literal_line1_lowconf": True})
|
||||||
self.assertTrue(loaded["literal_line1_lowconf"])
|
self.assertTrue(ws.load_config()["literal_line1_lowconf"])
|
||||||
finally:
|
finally:
|
||||||
ws.CONFIG_PATH = orig
|
ws.CONFIG_PATH = orig
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -348,19 +348,30 @@ def _has_japanese(text: str) -> bool:
|
||||||
#
|
#
|
||||||
# When translating we reshape those into ``[Speaker]: body`` (the prompt already
|
# When translating we reshape those into ``[Speaker]: body`` (the prompt already
|
||||||
# knows to translate the tag) and, on write-back, restore WOLF's native
|
# knows to translate the tag) and, on write-back, restore WOLF's native
|
||||||
# ``Speaker\nbody`` layout so injection stays byte-faithful. Which formats are
|
# ``Speaker\nbody`` layout so injection stays byte-faithful.
|
||||||
# reshaped is configurable (``data/wolf_speakers.json``) so the workflow can
|
#
|
||||||
# toggle the high-confidence nameplate and the low-confidence guess separately.
|
# WolfDawn does the detection, so there is nothing to configure for the reliable
|
||||||
|
# format: ``literal_line1`` is a real nameplate (a face window precedes the line),
|
||||||
|
# always reshaped, no toggle. Only ``literal_line1_lowconf`` is a heuristic guess
|
||||||
|
# (short line 1, no face window) that can occasionally be wrong; whether to trust
|
||||||
|
# it is a per-game call, so it is the single configurable format
|
||||||
|
# (``data/wolf_speakers.json``), recommended by the workflow's speaker-format AI
|
||||||
|
# prompt rather than guessed by hand.
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
# speaker_src values whose name is baked into line 1 of the source text.
|
# speaker_src values whose name is baked into line 1 of the source text.
|
||||||
FIRSTLINE_SRCS = ("literal_line1", "literal_line1_lowconf")
|
FIRSTLINE_SRCS = ("literal_line1", "literal_line1_lowconf")
|
||||||
|
|
||||||
# Default: reshape both first-line formats. WolfDawn already gates the
|
# Reliable nameplate - always reshaped, not user-configurable.
|
||||||
# low-confidence one (short line 1, no control codes, no sentence punctuation),
|
ALWAYS_ON_SRCS = ("literal_line1",)
|
||||||
# so it is safe enough to enable by default.
|
|
||||||
|
# Heuristic guess - reshaping is gated by config (per-game, AI-recommended).
|
||||||
|
CONFIGURABLE_SRCS = ("literal_line1_lowconf",)
|
||||||
|
|
||||||
|
# Default: also reshape the low-confidence guess. WolfDawn already gates it (short
|
||||||
|
# line 1, no control codes, no sentence punctuation), so it is safe enough on by
|
||||||
|
# default; the speaker-format prompt can advise turning it off for a given game.
|
||||||
DEFAULT_CONFIG = {
|
DEFAULT_CONFIG = {
|
||||||
"literal_line1": True,
|
|
||||||
"literal_line1_lowconf": True,
|
"literal_line1_lowconf": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -394,8 +405,14 @@ def save_config(config: dict) -> None:
|
||||||
|
|
||||||
|
|
||||||
def is_firstline_enabled(speaker_src: str, config: dict | None = None) -> bool:
|
def is_firstline_enabled(speaker_src: str, config: dict | None = None) -> bool:
|
||||||
"""True if *speaker_src* is a first-line format that is enabled in *config*."""
|
"""True if *speaker_src* is a first-line format that should be reshaped.
|
||||||
if speaker_src not in FIRSTLINE_SRCS:
|
|
||||||
|
The reliable nameplate (``literal_line1``) is always on; the low-confidence
|
||||||
|
guess is gated by *config* (per-game, AI-recommended).
|
||||||
|
"""
|
||||||
|
if speaker_src in ALWAYS_ON_SRCS:
|
||||||
|
return True
|
||||||
|
if speaker_src not in CONFIGURABLE_SRCS:
|
||||||
return False
|
return False
|
||||||
cfg = config if config is not None else DEFAULT_CONFIG
|
cfg = config if config is not None else DEFAULT_CONFIG
|
||||||
return bool(cfg.get(speaker_src, DEFAULT_CONFIG.get(speaker_src, False)))
|
return bool(cfg.get(speaker_src, DEFAULT_CONFIG.get(speaker_src, False)))
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue