WolfDawn namewrap
This commit is contained in:
parent
c781f66ae7
commit
c56fae3141
6 changed files with 310 additions and 1 deletions
|
|
@ -65,6 +65,15 @@ listWidth="100"
|
|||
#The wordwrap of items and help text
|
||||
noteWidth="75"
|
||||
|
||||
# WolfDawn names.json (Step 3): re-wrap selected note categories with dazedwrap
|
||||
#wolfNameWrap="false"
|
||||
#wolfNameWrapWidth="60"
|
||||
#wolfNameWrapNotes='["├■プロフィール","説明"]'
|
||||
|
||||
# WolfDawn dialogue (Step 4 Phase 2): re-wrap translated event text
|
||||
#wolfWrap="true"
|
||||
#wolfWidth="55"
|
||||
|
||||
#CSV delimiter character (comma, semicolon, or tab)
|
||||
csvDelimiter=","
|
||||
|
||||
|
|
|
|||
|
|
@ -2006,6 +2006,8 @@ class WolfWorkflowTab(QWidget):
|
|||
refresh_btn.clicked.connect(self._refresh_names_summary)
|
||||
layout.addWidget(refresh_btn)
|
||||
|
||||
self._add_name_wrap_options(layout)
|
||||
|
||||
layout.addWidget(_make_hr())
|
||||
layout.addWidget(self._subheading("Translate names (Phase 0)"))
|
||||
layout.addWidget(self._desc(
|
||||
|
|
@ -2044,6 +2046,154 @@ class WolfWorkflowTab(QWidget):
|
|||
summary = wolf_names.format_name_safety_summary(data)
|
||||
where = src.parent.name if src else WORK_DIR_NAME
|
||||
self.names_summary_label.setText(f"{summary}\nSource: {where}/{NAMES_JSON}")
|
||||
self._refresh_name_wrap_categories()
|
||||
|
||||
def _add_name_wrap_options(self, layout: QVBoxLayout):
|
||||
"""Per-category dazedwrap for names.json (Step 3)."""
|
||||
layout.addWidget(_make_hr())
|
||||
layout.addWidget(self._subheading("Category word wrap (dazedwrap)"))
|
||||
layout.addWidget(self._desc(
|
||||
"Re-wrap translated name values in selected categories (profile blurbs, "
|
||||
"descriptions, and other multi-line fields) to a character width so English "
|
||||
"fits in-game UI. Short row labels are usually left alone. Applies on the "
|
||||
"next Phase 0 run."
|
||||
))
|
||||
|
||||
wrap_env = self._read_env_values(["wolfNameWrap", "wolfNameWrapWidth", "wolfNameWrapNotes"])
|
||||
self._name_wrap_enable_cb = QCheckBox("Apply dazedwrap to selected categories")
|
||||
enabled = str(wrap_env.get("wolfNameWrap", "false")).strip().lower() == "true"
|
||||
self._name_wrap_enable_cb.setChecked(enabled)
|
||||
self._name_wrap_enable_cb.stateChanged.connect(self._apply_name_wrap_config)
|
||||
layout.addWidget(self._name_wrap_enable_cb)
|
||||
|
||||
width_row = QHBoxLayout()
|
||||
width_lbl = QLabel("Wrap width (characters):")
|
||||
width_lbl.setStyleSheet("color:#cccccc;font-size:12px;background:transparent;")
|
||||
self._name_wrap_width_spin = QSpinBox()
|
||||
self._name_wrap_width_spin.setRange(20, 300)
|
||||
try:
|
||||
self._name_wrap_width_spin.setValue(int(wrap_env.get("wolfNameWrapWidth") or 60))
|
||||
except ValueError:
|
||||
self._name_wrap_width_spin.setValue(60)
|
||||
self._name_wrap_width_spin.setFixedWidth(70)
|
||||
self._name_wrap_width_spin.valueChanged.connect(self._apply_name_wrap_config)
|
||||
width_row.addWidget(width_lbl)
|
||||
width_row.addWidget(self._name_wrap_width_spin)
|
||||
width_row.addStretch()
|
||||
layout.addLayout(width_row)
|
||||
|
||||
self._name_wrap_notes_list = QListWidget()
|
||||
self._name_wrap_notes_list.setMaximumHeight(180)
|
||||
self._name_wrap_notes_list.setStyleSheet(
|
||||
"QListWidget{background-color:#252526;border:1px solid #3a3a3a;border-radius:4px;"
|
||||
"color:#cccccc;font-size:12px;padding:2px;}"
|
||||
"QListWidget::item{padding:3px 4px;}"
|
||||
"QListWidget::item:selected{background:#264f78;color:#ffffff;}"
|
||||
)
|
||||
self._name_wrap_notes_list.itemChanged.connect(self._apply_name_wrap_config)
|
||||
layout.addWidget(self._name_wrap_notes_list)
|
||||
|
||||
list_row = QHBoxLayout()
|
||||
refresh_cats_btn = _make_btn("↻ Refresh categories", "#3a3a3a")
|
||||
refresh_cats_btn.clicked.connect(self._refresh_name_wrap_categories)
|
||||
sel_all_btn = _make_btn("Select all", "#3a3a3a")
|
||||
sel_all_btn.clicked.connect(lambda: self._set_name_wrap_checks(True))
|
||||
sel_none_btn = _make_btn("Select none", "#3a3a3a")
|
||||
sel_none_btn.clicked.connect(lambda: self._set_name_wrap_checks(False))
|
||||
list_row.addWidget(refresh_cats_btn)
|
||||
list_row.addWidget(sel_all_btn)
|
||||
list_row.addWidget(sel_none_btn)
|
||||
list_row.addStretch()
|
||||
layout.addLayout(list_row)
|
||||
|
||||
self._saved_name_wrap_notes = wolf_names.parse_name_wrap_notes(
|
||||
wrap_env.get("wolfNameWrapNotes", "")
|
||||
)
|
||||
|
||||
def _set_name_wrap_checks(self, checked: bool):
|
||||
if not hasattr(self, "_name_wrap_notes_list"):
|
||||
return
|
||||
state = Qt.Checked if checked else Qt.Unchecked
|
||||
self._name_wrap_notes_list.blockSignals(True)
|
||||
for i in range(self._name_wrap_notes_list.count()):
|
||||
item = self._name_wrap_notes_list.item(i)
|
||||
if item.flags() & Qt.ItemIsUserCheckable:
|
||||
item.setCheckState(state)
|
||||
self._name_wrap_notes_list.blockSignals(False)
|
||||
self._apply_name_wrap_config()
|
||||
|
||||
def _refresh_name_wrap_categories(self):
|
||||
if not hasattr(self, "_name_wrap_notes_list"):
|
||||
return
|
||||
saved = getattr(self, "_saved_name_wrap_notes", frozenset())
|
||||
wrap_env = self._read_env_values(["wolfNameWrapNotes"])
|
||||
if wrap_env.get("wolfNameWrapNotes"):
|
||||
saved = wolf_names.parse_name_wrap_notes(wrap_env["wolfNameWrapNotes"])
|
||||
self._saved_name_wrap_notes = saved
|
||||
|
||||
self._name_wrap_notes_list.blockSignals(True)
|
||||
self._name_wrap_notes_list.clear()
|
||||
data, _src = self._load_names_document()
|
||||
if not isinstance(data, dict):
|
||||
item = QListWidgetItem("Run Step 0 (Extract text) first to list categories.")
|
||||
item.setFlags(Qt.ItemIsEnabled)
|
||||
self._name_wrap_notes_list.addItem(item)
|
||||
self._name_wrap_notes_list.blockSignals(False)
|
||||
return
|
||||
|
||||
notes = wolf_names.collect_name_notes(data)
|
||||
if not notes:
|
||||
item = QListWidgetItem("No categories found in names.json.")
|
||||
item.setFlags(Qt.ItemIsEnabled)
|
||||
self._name_wrap_notes_list.addItem(item)
|
||||
self._name_wrap_notes_list.blockSignals(False)
|
||||
return
|
||||
|
||||
db_labels = wolf_names.derive_db_labels("files")
|
||||
for note in notes:
|
||||
label = wolf_names.note_header(note, db_labels)
|
||||
item = QListWidgetItem(label)
|
||||
item.setData(Qt.UserRole, note)
|
||||
item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
|
||||
item.setCheckState(Qt.Checked if note in saved else Qt.Unchecked)
|
||||
self._name_wrap_notes_list.addItem(item)
|
||||
self._name_wrap_notes_list.blockSignals(False)
|
||||
|
||||
def _selected_name_wrap_notes(self) -> list[str]:
|
||||
if not hasattr(self, "_name_wrap_notes_list"):
|
||||
return []
|
||||
notes: list[str] = []
|
||||
for i in range(self._name_wrap_notes_list.count()):
|
||||
item = self._name_wrap_notes_list.item(i)
|
||||
if not (item.flags() & Qt.ItemIsUserCheckable):
|
||||
continue
|
||||
if item.checkState() == Qt.Checked:
|
||||
note = item.data(Qt.UserRole)
|
||||
if note:
|
||||
notes.append(str(note))
|
||||
return notes
|
||||
|
||||
def _apply_name_wrap_config(self, *args):
|
||||
"""Persist names.json category wrap settings to .env."""
|
||||
if not hasattr(self, "_name_wrap_enable_cb"):
|
||||
return
|
||||
notes = self._selected_name_wrap_notes()
|
||||
enabled = self._name_wrap_enable_cb.isChecked() and bool(notes)
|
||||
updates = {
|
||||
"wolfNameWrap": "true" if enabled else "false",
|
||||
"wolfNameWrapWidth": str(self._name_wrap_width_spin.value()),
|
||||
"wolfNameWrapNotes": json.dumps(notes, ensure_ascii=False),
|
||||
}
|
||||
self._saved_name_wrap_notes = frozenset(notes)
|
||||
if self._write_env_values(updates):
|
||||
if enabled:
|
||||
self._log(
|
||||
"Name category wrap updated: "
|
||||
f"width={updates['wolfNameWrapWidth']}, "
|
||||
f"categories={len(notes)}"
|
||||
)
|
||||
else:
|
||||
self._log("Name category wrap disabled.")
|
||||
|
||||
def _names_json_path(self) -> Path | None:
|
||||
"""Locate names.json for reading its note categories (files/ then wolf_json/)."""
|
||||
|
|
@ -2149,6 +2299,7 @@ class WolfWorkflowTab(QWidget):
|
|||
# Index 3 == Names: refresh the per-name safety summary.
|
||||
if idx == 3:
|
||||
self._refresh_names_summary()
|
||||
self._refresh_name_wrap_categories()
|
||||
# Index 5 == Inject: keep the quick-inject picker in sync with translated/.
|
||||
elif idx == 5:
|
||||
self._refresh_inject_list()
|
||||
|
|
|
|||
|
|
@ -32,6 +32,11 @@ Wrapping is applied to the dialogue *body* only - a speaker's nameplate line (th
|
|||
Configurable from the workflow (``.env``: ``wolfWrap`` / ``wolfWidth``); a width of
|
||||
0 (or ``wolfWrap=false``) writes the model output back verbatim.
|
||||
|
||||
names.json category wrap (Step 3): selected ``note`` categories can be re-wrapped
|
||||
with dazedwrap at a dedicated width (``.env``: ``wolfNameWrap``,
|
||||
``wolfNameWrapWidth``, ``wolfNameWrapNotes``). Other name categories keep the
|
||||
model output verbatim.
|
||||
|
||||
Speakers: WolfDawn tags each line with ``speaker`` / ``speaker_src``. For the
|
||||
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
|
||||
|
|
@ -100,6 +105,15 @@ try:
|
|||
except ValueError:
|
||||
WRAPWIDTH = 0
|
||||
|
||||
# names.json: optional per-category dazedwrap (Step 3 workflow UI).
|
||||
NAME_WRAP_ENABLED, NAME_WRAP_WIDTH, NAME_WRAP_NOTES = wolf_names.load_name_wrap_config()
|
||||
|
||||
|
||||
def reload_name_wrap_config() -> None:
|
||||
"""Re-read names.json category wrap settings from ``.env``."""
|
||||
global NAME_WRAP_ENABLED, NAME_WRAP_WIDTH, NAME_WRAP_NOTES
|
||||
NAME_WRAP_ENABLED, NAME_WRAP_WIDTH, NAME_WRAP_NOTES = wolf_names.load_name_wrap_config()
|
||||
|
||||
# Pricing / batching from the configured model
|
||||
PRICING_CONFIG = getPricingConfig(MODEL)
|
||||
INPUTAPICOST = PRICING_CONFIG["inputAPICost"]
|
||||
|
|
@ -133,6 +147,7 @@ def handleWolfDawn(filename, estimate):
|
|||
# Re-read workflow-configured settings so edits made this session take effect
|
||||
# even when translation runs in-process (the module import is cached).
|
||||
SPEAKER_CONFIG = wolf_speakers.load_config()
|
||||
reload_name_wrap_config()
|
||||
# Reload the glossary so a later phase (DB text / dialogue) picks up names
|
||||
# that an earlier Phase 0 (names) harvested into vocab.txt.
|
||||
VOCAB = VOCAB_PATH.read_text(encoding="utf-8")
|
||||
|
|
@ -213,6 +228,18 @@ def _wrap_body(body):
|
|||
return dazedwrap.wrapText(body, WRAPWIDTH)
|
||||
|
||||
|
||||
def _wrap_names_entry(text, entry):
|
||||
"""Apply dazedwrap to a names.json entry when its ``note`` is selected in Step 3."""
|
||||
if not NAME_WRAP_ENABLED or NAME_WRAP_WIDTH <= 0:
|
||||
return text
|
||||
if not isinstance(text, str) or not text:
|
||||
return text
|
||||
note = str(entry.get("note", ""))
|
||||
if note not in NAME_WRAP_NOTES:
|
||||
return text
|
||||
return dazedwrap.wrapText(text, NAME_WRAP_WIDTH)
|
||||
|
||||
|
||||
def _wrap_plain(text, is_firstline):
|
||||
"""Wrap a non-reshaped entry, protecting a nameplate first line if present.
|
||||
|
||||
|
|
@ -289,7 +316,9 @@ def parseDocument(data, filename):
|
|||
for (entry, prefix, has_speaker, is_firstline), text in zip(plans, translated):
|
||||
if not isinstance(text, str):
|
||||
continue
|
||||
if has_speaker:
|
||||
if is_names:
|
||||
entry["text"] = _wrap_names_entry(text, entry)
|
||||
elif has_speaker:
|
||||
speaker_en, body_en = wolf_speakers.parse_prefixed(text)
|
||||
if speaker_en is not None:
|
||||
# Wrap only the body; the name stays on its own line.
|
||||
|
|
|
|||
|
|
@ -87,6 +87,33 @@ class TestNoteHeader(unittest.TestCase):
|
|||
self.assertEqual(wn.note_header("■MOBセリフ"), "■MOBセリフ")
|
||||
|
||||
|
||||
class TestCollectNameNotes(unittest.TestCase):
|
||||
DOC = {
|
||||
"kind": "names",
|
||||
"names": [
|
||||
{"source": "a", "note": "武器"},
|
||||
{"source": "b", "note": "技能"},
|
||||
{"source": "c", "note": "武器"},
|
||||
{"source": "d"},
|
||||
],
|
||||
}
|
||||
|
||||
def test_collects_unique_sorted_notes(self):
|
||||
self.assertEqual(wn.collect_name_notes(self.DOC), ["技能", "武器"])
|
||||
|
||||
def test_parse_json_notes(self):
|
||||
self.assertEqual(
|
||||
wn.parse_name_wrap_notes('["武器","├■プロフィール"]'),
|
||||
frozenset({"武器", "├■プロフィール"}),
|
||||
)
|
||||
|
||||
def test_parse_comma_separated_notes(self):
|
||||
self.assertEqual(
|
||||
wn.parse_name_wrap_notes("武器,技能"),
|
||||
frozenset({"武器", "技能"}),
|
||||
)
|
||||
|
||||
|
||||
class TestDeriveDbLabels(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self._tmp = tempfile.TemporaryDirectory()
|
||||
|
|
|
|||
|
|
@ -388,6 +388,62 @@ class TestWrapping(unittest.TestCase):
|
|||
self.assertEqual(line0.split("\n", 1)[0], "EN_市民")
|
||||
|
||||
|
||||
class TestNameCategoryWrap(unittest.TestCase):
|
||||
"""dazedwrap for selected names.json note categories (Step 3)."""
|
||||
|
||||
def setUp(self):
|
||||
self._orig = (wd.NAME_WRAP_ENABLED, wd.NAME_WRAP_WIDTH, wd.NAME_WRAP_NOTES)
|
||||
|
||||
def tearDown(self):
|
||||
wd.NAME_WRAP_ENABLED, wd.NAME_WRAP_WIDTH, wd.NAME_WRAP_NOTES = self._orig
|
||||
|
||||
def _set(self, enabled, width, notes):
|
||||
wd.NAME_WRAP_ENABLED = enabled
|
||||
wd.NAME_WRAP_WIDTH = width
|
||||
wd.NAME_WRAP_NOTES = frozenset(notes)
|
||||
|
||||
def test_wrap_names_entry_only_selected_notes(self):
|
||||
self._set(True, 10, {"├■プロフィール"})
|
||||
long_text = "alpha beta gamma delta epsilon"
|
||||
entry = {"note": "├■プロフィール"}
|
||||
wrapped = wd._wrap_names_entry(long_text, entry)
|
||||
self.assertGreater(wrapped.count("\n"), 0)
|
||||
entry_other = {"note": "武器"}
|
||||
self.assertEqual(wd._wrap_names_entry(long_text, entry_other), long_text)
|
||||
|
||||
def test_wrap_names_entry_disabled_is_noop(self):
|
||||
self._set(False, 10, {"├■プロフィール"})
|
||||
long_text = "alpha beta gamma delta epsilon"
|
||||
entry = {"note": "├■プロフィール"}
|
||||
self.assertEqual(wd._wrap_names_entry(long_text, entry), long_text)
|
||||
|
||||
def test_names_translation_wraps_selected_category(self):
|
||||
doc = {
|
||||
"kind": "names",
|
||||
"names": [
|
||||
{"source": "剣", "text": "剣", "note": "武器", "safety": "safe"},
|
||||
{
|
||||
"source": "alpha beta gamma delta epsilon zeta",
|
||||
"text": "alpha beta gamma delta epsilon zeta",
|
||||
"note": "├■プロフィール",
|
||||
"safety": "safe",
|
||||
},
|
||||
],
|
||||
}
|
||||
self._set(True, 8, {"├■プロフィール"})
|
||||
orig_wrap = wd.WRAP
|
||||
wd.WRAP = True
|
||||
wd.WRAPWIDTH = 4
|
||||
try:
|
||||
(data, _t, err), _c = _WolfTranslateHarness().run(doc, "names.json")
|
||||
finally:
|
||||
wd.WRAP = orig_wrap
|
||||
self.assertIsNone(err)
|
||||
names = {n["note"]: n["text"] for n in data["names"]}
|
||||
self.assertEqual(names["武器"], "EN_剣")
|
||||
self.assertGreater(names["├■プロフィール"].count("\n"), 0)
|
||||
|
||||
|
||||
class TestOpenFiles(unittest.TestCase):
|
||||
def test_rejects_unknown_kind(self):
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
|
|
|
|||
|
|
@ -189,3 +189,40 @@ def note_header(note: str, db_labels: dict[str, str] | None = None) -> str:
|
|||
if english and english != note:
|
||||
return f"{english}{_LABEL_SEP}{note}"
|
||||
return note
|
||||
|
||||
|
||||
def collect_name_notes(data: dict[str, Any]) -> list[str]:
|
||||
"""Return sorted unique ``note`` values from a names.json document."""
|
||||
notes: set[str] = set()
|
||||
for entry in parse_names_entries(data):
|
||||
note = str(entry.get("note", "")).strip()
|
||||
if note:
|
||||
notes.add(note)
|
||||
return sorted(notes)
|
||||
|
||||
|
||||
def parse_name_wrap_notes(raw: str) -> frozenset[str]:
|
||||
"""Parse ``wolfNameWrapNotes`` from ``.env`` (JSON array or comma-separated)."""
|
||||
text = (raw or "").strip()
|
||||
if not text:
|
||||
return frozenset()
|
||||
try:
|
||||
parsed = json.loads(text)
|
||||
if isinstance(parsed, list):
|
||||
return frozenset(str(item).strip() for item in parsed if str(item).strip())
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
return frozenset(part.strip() for part in text.split(",") if part.strip())
|
||||
|
||||
|
||||
def load_name_wrap_config() -> tuple[bool, int, frozenset[str]]:
|
||||
"""Return ``(enabled, width, notes)`` from workflow ``.env`` settings."""
|
||||
import os
|
||||
|
||||
enabled = os.getenv("wolfNameWrap", "false").strip().lower() == "true"
|
||||
try:
|
||||
width = int(os.getenv("wolfNameWrapWidth") or 0)
|
||||
except ValueError:
|
||||
width = 0
|
||||
notes = parse_name_wrap_notes(os.getenv("wolfNameWrapNotes", ""))
|
||||
return enabled, width, notes
|
||||
|
|
|
|||
Loading…
Reference in a new issue