Foundation DB should write to glossary
This commit is contained in:
parent
563d09db89
commit
0df4dbf3ed
7 changed files with 360 additions and 20 deletions
|
|
@ -12,7 +12,8 @@ Mirrors the RPGMaker WorkflowTab, driven by the vendored WolfDawn ``wolf`` CLI
|
||||||
are translated per-name; refs and verify names are skipped.
|
are translated per-name; refs and verify names are skipped.
|
||||||
Harvests short name terms into vocab.txt.
|
Harvests short name terms into vocab.txt.
|
||||||
Step 4 Database - discover content layout; translate foundation DB sheets
|
Step 4 Database - discover content layout; translate foundation DB sheets
|
||||||
(items, skills, descriptions) before narrative custom sheets
|
(items, skills, descriptions) before narrative custom sheets;
|
||||||
|
harvests short label fields (map names, titles) into vocab.txt
|
||||||
Step 5 Maps/Events - .mps maps, CommonEvent, Game.dat, Evtext; speaker handling
|
Step 5 Maps/Events - .mps maps, CommonEvent, Game.dat, Evtext; speaker handling
|
||||||
Step 6 Precheck - dry-run selected JSON for safety-guard skips; edit those
|
Step 6 Precheck - dry-run selected JSON for safety-guard skips; edit those
|
||||||
lines before writing binaries
|
lines before writing binaries
|
||||||
|
|
@ -1784,7 +1785,8 @@ class WolfWorkflowTab(QWidget):
|
||||||
|
|
||||||
note = self._desc(
|
note = self._desc(
|
||||||
"You don't need to add item / skill / enemy value names here: Phase 0 translates "
|
"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 "
|
"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."
|
"Focus this glossary on characters and worldbuilding terms."
|
||||||
)
|
)
|
||||||
layout.addWidget(note)
|
layout.addWidget(note)
|
||||||
|
|
@ -1883,7 +1885,8 @@ class WolfWorkflowTab(QWidget):
|
||||||
layout.addWidget(self._subheading("Translate database"))
|
layout.addWidget(self._subheading("Translate database"))
|
||||||
layout.addWidget(self._desc(
|
layout.addWidget(self._desc(
|
||||||
"Run after Step 3 (names) so vocab.txt stays consistent. Only checked sheets "
|
"Run after Step 3 (names) so vocab.txt stays consistent. Only checked sheets "
|
||||||
"are sent to the model. Re-running skips lines already translated."
|
"are sent to the model. Re-running skips lines already translated. Short "
|
||||||
|
"foundation labels (map names, titles) are merged into vocab.txt."
|
||||||
))
|
))
|
||||||
self._add_tl_mode_selector(layout)
|
self._add_tl_mode_selector(layout)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,9 +24,10 @@ names.json safety: WolfDawn tags every value name with a static ``safety`` badge
|
||||||
(``safe``, ``refs``, or ``verify``). For ``kind == "names"`` documents, only
|
(``safe``, ``refs``, or ``verify``). For ``kind == "names"`` documents, only
|
||||||
entries whose badge is ``safe`` are translated; ``refs`` and ``verify`` names and
|
entries whose badge is ``safe`` are translated; ``refs`` and ``verify`` names and
|
||||||
legacy entries without a badge keep ``text == source``. After translating
|
legacy entries without a badge keep ``text == source``. After translating
|
||||||
names.json, translatable entries are harvested into ``vocab.txt`` (grouped by
|
names.json, short name-like entries are harvested into ``vocab.txt`` (grouped by
|
||||||
``note``, with bilingual headers) so later phases keep item / skill / term names
|
``note``, with bilingual headers). After foundation DB sheets are translated,
|
||||||
consistent.
|
short label fields (map names, titles, etc.) are merged into the same glossary
|
||||||
|
so later phases keep place / term names consistent without overwriting names.json.
|
||||||
|
|
||||||
Database sheet filter (Step 4): optional ``wolfDbIncludeGroups`` or
|
Database sheet filter (Step 4): optional ``wolfDbIncludeGroups`` or
|
||||||
``wolfDbIncludeTiers`` in ``.env`` limit which ``kind: db`` lines are collected.
|
``wolfDbIncludeTiers`` in ``.env`` limit which ``kind: db`` lines are collected.
|
||||||
|
|
@ -353,6 +354,10 @@ def parseDocument(data, filename):
|
||||||
# so a resume still seeds vocab. Mirrors the RPGMaker DB-first strategy.
|
# so a resume still seeds vocab. Mirrors the RPGMaker DB-first strategy.
|
||||||
if is_names and not ESTIMATE:
|
if is_names and not ESTIMATE:
|
||||||
_harvest_names_to_vocab(data)
|
_harvest_names_to_vocab(data)
|
||||||
|
# Foundation DB label fields (map names, titles) merge into vocab.txt without
|
||||||
|
# replacing entries already seeded from names.json.
|
||||||
|
elif data.get("kind") == "db" and not ESTIMATE:
|
||||||
|
_harvest_db_to_vocab(data)
|
||||||
|
|
||||||
return [data, totalTokens, None]
|
return [data, totalTokens, None]
|
||||||
|
|
||||||
|
|
@ -389,6 +394,25 @@ def _harvest_names_to_vocab(data):
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
|
|
||||||
|
def _harvest_db_to_vocab(data):
|
||||||
|
"""Merge short foundation DB labels into vocab.txt, grouped by sheet.
|
||||||
|
|
||||||
|
Uses merge mode so names.json-seeded entries for the same section stay put.
|
||||||
|
Descriptions and narrative dialogue are filtered out.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
include_tiers, include_groups = wolf_db.load_db_filter_config()
|
||||||
|
by_sheet = wolf_db.collect_db_vocab_pairs(
|
||||||
|
data,
|
||||||
|
include_tiers=include_tiers,
|
||||||
|
include_groups=include_groups,
|
||||||
|
)
|
||||||
|
for type_name, pairs in by_sheet.items():
|
||||||
|
wolf_vocab.update_vocab_section(type_name, pairs, merge=True)
|
||||||
|
except Exception:
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
|
||||||
def getResultString(translatedData, translationTime, filename):
|
def getResultString(translatedData, translationTime, filename):
|
||||||
"""Format the per-file / total cost + status line for the console log."""
|
"""Format the per-file / total cost + status line for the console log."""
|
||||||
cost = calculateCost(translatedData[1][0], translatedData[1][1], MODEL)
|
cost = calculateCost(translatedData[1][0], translatedData[1][1], MODEL)
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,20 @@ class TestUpdateVocabSection(unittest.TestCase):
|
||||||
self.assertIn("ヒール (Cure)", text)
|
self.assertIn("ヒール (Cure)", text)
|
||||||
self.assertNotIn("ヒール (Heal)", text)
|
self.assertNotIn("ヒール (Heal)", text)
|
||||||
|
|
||||||
|
def test_merge_keeps_existing_and_adds_new(self):
|
||||||
|
vocab.update_vocab_section("Map Setting · マップ設定", [("礼拝堂", "Chapel")])
|
||||||
|
vocab.update_vocab_section(
|
||||||
|
"Map Setting · マップ設定",
|
||||||
|
[("礼拝堂", "Chapel Hall"), ("大通り", "Main Street")],
|
||||||
|
merge=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
text = self.vocab_path.read_text(encoding="utf-8")
|
||||||
|
self.assertIn("礼拝堂 (Chapel)", text)
|
||||||
|
self.assertNotIn("礼拝堂 (Chapel Hall)", text)
|
||||||
|
self.assertIn("大通り (Main Street)", text)
|
||||||
|
self.assertEqual(text.count("# Map Setting · マップ設定"), 1)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
||||||
|
|
@ -238,5 +238,93 @@ class TestDbProfile(unittest.TestCase):
|
||||||
self.assertFalse(checks[groups[1].key])
|
self.assertFalse(checks[groups[1].key])
|
||||||
|
|
||||||
|
|
||||||
|
class TestDbVocabHarvest(unittest.TestCase):
|
||||||
|
def test_map_name_is_candidate(self):
|
||||||
|
line = {"fieldName": "マップ名", "source": "礼拝堂", "text": "Chapel"}
|
||||||
|
self.assertTrue(
|
||||||
|
wdb.is_db_vocab_harvest_candidate(
|
||||||
|
line,
|
||||||
|
type_name="Map Setting · マップ設定",
|
||||||
|
tier=wdb.TIER_UNKNOWN,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_description_is_not_candidate(self):
|
||||||
|
line = {
|
||||||
|
"fieldName": "Description · 説明",
|
||||||
|
"source": "味方一人のHPを回復",
|
||||||
|
"text": "Restores one ally's HP",
|
||||||
|
}
|
||||||
|
self.assertFalse(
|
||||||
|
wdb.is_db_vocab_harvest_candidate(
|
||||||
|
line,
|
||||||
|
type_name="Skill · 技能",
|
||||||
|
tier=wdb.TIER_FOUNDATION,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_usage_message_with_name_placeholder_is_not_candidate(self):
|
||||||
|
line = {
|
||||||
|
"fieldName": "Usage Message [Battle] (Name~ · 使用時文章[戦闘](人名~",
|
||||||
|
"source": "の攻撃!",
|
||||||
|
"text": "'s attack!",
|
||||||
|
}
|
||||||
|
self.assertFalse(
|
||||||
|
wdb.is_db_vocab_harvest_candidate(
|
||||||
|
line,
|
||||||
|
type_name="Skill · 技能",
|
||||||
|
tier=wdb.TIER_FOUNDATION,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_narrative_sheet_is_not_candidate(self):
|
||||||
|
line = {"fieldName": "マップ名", "source": "礼拝堂", "text": "Chapel"}
|
||||||
|
self.assertFalse(
|
||||||
|
wdb.is_db_vocab_harvest_candidate(
|
||||||
|
line,
|
||||||
|
type_name="■イベント(セルリア)",
|
||||||
|
tier=wdb.TIER_NARRATIVE,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_collect_pairs_groups_by_sheet(self):
|
||||||
|
doc = {
|
||||||
|
"file": "SysDatabase.project",
|
||||||
|
"kind": "db",
|
||||||
|
"groups": [
|
||||||
|
{
|
||||||
|
"typeName": "Map Setting · マップ設定",
|
||||||
|
"lines": [
|
||||||
|
{
|
||||||
|
"fieldName": "マップ名",
|
||||||
|
"source": "礼拝堂",
|
||||||
|
"text": "Chapel",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldName": "Description · 説明",
|
||||||
|
"source": "静かな礼拝堂",
|
||||||
|
"text": "A quiet chapel",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"typeName": "■イベント(セルリア)",
|
||||||
|
"lines": [
|
||||||
|
{
|
||||||
|
"fieldName": "現在の行動",
|
||||||
|
"source": "礼拝堂で黙とう中",
|
||||||
|
"text": "Praying in the chapel",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
pairs = wdb.collect_db_vocab_pairs(doc)
|
||||||
|
self.assertEqual(
|
||||||
|
pairs,
|
||||||
|
{"Map Setting · マップ設定": [("礼拝堂", "Chapel")]},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
||||||
|
|
@ -46,22 +46,22 @@ class _WolfTranslateHarness:
|
||||||
self.captured.append(copy.deepcopy(text))
|
self.captured.append(copy.deepcopy(text))
|
||||||
return _mock_translate(text, history, history_ctx)
|
return _mock_translate(text, history, history_ctx)
|
||||||
|
|
||||||
def capture_vocab(category, pairs):
|
def capture_vocab(category, pairs, merge=False):
|
||||||
self.vocab_writes.append((category, list(pairs)))
|
self.vocab_writes.append((category, list(pairs), merge))
|
||||||
|
|
||||||
orig_t = wd.translateAI
|
orig_t = wd.translateAI
|
||||||
orig_estimate = wd.ESTIMATE
|
orig_estimate = wd.ESTIMATE
|
||||||
orig_wrap = wd.WRAP
|
|
||||||
orig_ignore = wd.IGNORETLTEXT
|
orig_ignore = wd.IGNORETLTEXT
|
||||||
orig_update = wd.wolf_vocab.update_vocab_section
|
orig_update = wd.wolf_vocab.update_vocab_section
|
||||||
orig_labels = wd.wolf_names.derive_db_labels
|
orig_labels = wd.wolf_names.derive_db_labels
|
||||||
|
orig_db_filter = wd.wolf_db.load_db_filter_config
|
||||||
wd.translateAI = translate
|
wd.translateAI = translate
|
||||||
wd.ESTIMATE = estimate
|
wd.ESTIMATE = estimate
|
||||||
wd.WRAP = False # keep write-back byte-faithful; wrapping tested separately
|
|
||||||
wd.IGNORETLTEXT = ignore_tl_text
|
wd.IGNORETLTEXT = ignore_tl_text
|
||||||
# Never touch the real glossary / DB files during tests.
|
# Never touch the real glossary / DB files during tests.
|
||||||
wd.wolf_vocab.update_vocab_section = capture_vocab
|
wd.wolf_vocab.update_vocab_section = capture_vocab
|
||||||
wd.wolf_names.derive_db_labels = lambda _p: {}
|
wd.wolf_names.derive_db_labels = lambda _p: {}
|
||||||
|
wd.wolf_db.load_db_filter_config = lambda: (frozenset(), frozenset())
|
||||||
try:
|
try:
|
||||||
data_copy = copy.deepcopy(data)
|
data_copy = copy.deepcopy(data)
|
||||||
result = wd.parseDocument(data_copy, filename)
|
result = wd.parseDocument(data_copy, filename)
|
||||||
|
|
@ -69,10 +69,10 @@ class _WolfTranslateHarness:
|
||||||
finally:
|
finally:
|
||||||
wd.translateAI = orig_t
|
wd.translateAI = orig_t
|
||||||
wd.ESTIMATE = orig_estimate
|
wd.ESTIMATE = orig_estimate
|
||||||
wd.WRAP = orig_wrap
|
|
||||||
wd.IGNORETLTEXT = orig_ignore
|
wd.IGNORETLTEXT = orig_ignore
|
||||||
wd.wolf_vocab.update_vocab_section = orig_update
|
wd.wolf_vocab.update_vocab_section = orig_update
|
||||||
wd.wolf_names.derive_db_labels = orig_labels
|
wd.wolf_names.derive_db_labels = orig_labels
|
||||||
|
wd.wolf_db.load_db_filter_config = orig_db_filter
|
||||||
|
|
||||||
|
|
||||||
MAP_DOC = {
|
MAP_DOC = {
|
||||||
|
|
@ -198,7 +198,7 @@ class TestTranslationWriteback(unittest.TestCase):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
harness.vocab_writes,
|
harness.vocab_writes,
|
||||||
[
|
[
|
||||||
("Weapon · 武器", [("剣", "EN_剣")]),
|
("Weapon · 武器", [("剣", "EN_剣")], False),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -233,7 +233,7 @@ class TestTranslationWriteback(unittest.TestCase):
|
||||||
finally:
|
finally:
|
||||||
wd.wolf_vocab.remove_vocab_section = orig_remove
|
wd.wolf_vocab.remove_vocab_section = orig_remove
|
||||||
self.assertIsNone(err)
|
self.assertIsNone(err)
|
||||||
self.assertEqual(harness.vocab_writes, [("Weapon · 武器", [("ダガー", "EN_ダガー")])])
|
self.assertEqual(harness.vocab_writes, [("Weapon · 武器", [("ダガー", "EN_ダガー")], False)])
|
||||||
self.assertEqual(harness.vocab_remove_writes, ["├■プロフィール"])
|
self.assertEqual(harness.vocab_remove_writes, ["├■プロフィール"])
|
||||||
|
|
||||||
def test_txtdir_translates(self):
|
def test_txtdir_translates(self):
|
||||||
|
|
@ -451,9 +451,94 @@ class TestTranslationWriteback(unittest.TestCase):
|
||||||
self.assertEqual(captured, [])
|
self.assertEqual(captured, [])
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
harness.vocab_writes,
|
harness.vocab_writes,
|
||||||
[("Weapon · 武器", [("剣", "Sword")])],
|
[("Weapon · 武器", [("剣", "Sword")], False)],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_db_foundation_labels_harvest_to_vocab(self):
|
||||||
|
doc = {
|
||||||
|
"file": "SysDatabase.project",
|
||||||
|
"kind": "db",
|
||||||
|
"groups": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"typeName": "Map Setting · マップ設定",
|
||||||
|
"lines": [
|
||||||
|
{
|
||||||
|
"row": 0,
|
||||||
|
"field": 0,
|
||||||
|
"fieldName": "マップ名",
|
||||||
|
"source": "礼拝堂",
|
||||||
|
"text": "礼拝堂",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 1,
|
||||||
|
"field": 0,
|
||||||
|
"fieldName": "マップ名",
|
||||||
|
"source": "大通り",
|
||||||
|
"text": "大通り",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"row": 0,
|
||||||
|
"field": 1,
|
||||||
|
"fieldName": "Description · 説明",
|
||||||
|
"source": "静かな礼拝堂",
|
||||||
|
"text": "静かな礼拝堂",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"typeName": "■イベント(セルリア)",
|
||||||
|
"lines": [
|
||||||
|
{
|
||||||
|
"row": 0,
|
||||||
|
"field": 0,
|
||||||
|
"fieldName": "現在の行動",
|
||||||
|
"source": "礼拝堂で黙とう中",
|
||||||
|
"text": "礼拝堂で黙とう中",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
harness = _WolfTranslateHarness()
|
||||||
|
(_data, _t, err), _c = harness.run(doc, "SysDatabase.project.json")
|
||||||
|
self.assertIsNone(err)
|
||||||
|
self.assertEqual(
|
||||||
|
harness.vocab_writes,
|
||||||
|
[
|
||||||
|
(
|
||||||
|
"Map Setting · マップ設定",
|
||||||
|
[("礼拝堂", "EN_礼拝堂"), ("大通り", "EN_大通り")],
|
||||||
|
True,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_db_harvest_skipped_in_estimate(self):
|
||||||
|
doc = {
|
||||||
|
"file": "SysDatabase.project",
|
||||||
|
"kind": "db",
|
||||||
|
"groups": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"typeName": "Map Setting · マップ設定",
|
||||||
|
"lines": [
|
||||||
|
{
|
||||||
|
"row": 0,
|
||||||
|
"field": 0,
|
||||||
|
"fieldName": "マップ名",
|
||||||
|
"source": "礼拝堂",
|
||||||
|
"text": "礼拝堂",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
harness = _WolfTranslateHarness()
|
||||||
|
harness.run(doc, "SysDatabase.project.json", estimate=True)
|
||||||
|
self.assertEqual(harness.vocab_writes, [])
|
||||||
|
|
||||||
|
|
||||||
import re # noqa: E402
|
import re # noqa: E402
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,26 @@ def _norm(s: str) -> str:
|
||||||
return re.sub(r"\s+", " ", str(s)).strip().casefold()
|
return re.sub(r"\s+", " ", str(s)).strip().casefold()
|
||||||
|
|
||||||
|
|
||||||
def update_vocab_section(category: str, pairs) -> None:
|
_SECTION_PAIR_RE = re.compile(
|
||||||
|
r"^(.+?)\s+\((.+)\)\s*$",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_section_pairs(section_body: str) -> dict[str, str]:
|
||||||
|
"""Parse ``src (dst)`` lines from a vocab section body (no header)."""
|
||||||
|
pairs: dict[str, str] = {}
|
||||||
|
for raw in section_body.splitlines():
|
||||||
|
line = raw.strip()
|
||||||
|
if not line or line.startswith("#"):
|
||||||
|
continue
|
||||||
|
m = _SECTION_PAIR_RE.match(line)
|
||||||
|
if not m:
|
||||||
|
continue
|
||||||
|
pairs[m.group(1)] = m.group(2)
|
||||||
|
return pairs
|
||||||
|
|
||||||
|
|
||||||
|
def update_vocab_section(category: str, pairs, *, merge: bool = False) -> None:
|
||||||
"""Insert or replace a ``# {category}`` section in the game-specific vocab.
|
"""Insert or replace a ``# {category}`` section in the game-specific vocab.
|
||||||
|
|
||||||
Mirrors the RPGMaker auto-glossary behaviour (translated DB names feed
|
Mirrors the RPGMaker auto-glossary behaviour (translated DB names feed
|
||||||
|
|
@ -74,6 +93,8 @@ def update_vocab_section(category: str, pairs) -> None:
|
||||||
- ``pairs``: iterable of ``(source, translated)``. Deduped by source (last
|
- ``pairs``: iterable of ``(source, translated)``. Deduped by source (last
|
||||||
wins); no-ops (empty translation or unchanged after normalisation) are
|
wins); no-ops (empty translation or unchanged after normalisation) are
|
||||||
dropped. When nothing survives filtering the file is left untouched.
|
dropped. When nothing survives filtering the file is left untouched.
|
||||||
|
- ``merge``: when True, keep existing entries for this category and only add
|
||||||
|
sources that are not already present (names.json stays authoritative).
|
||||||
"""
|
"""
|
||||||
dedup: dict[str, str] = {}
|
dedup: dict[str, str] = {}
|
||||||
for src, dst in pairs:
|
for src, dst in pairs:
|
||||||
|
|
@ -97,16 +118,26 @@ def update_vocab_section(category: str, pairs) -> None:
|
||||||
game_part = existing
|
game_part = existing
|
||||||
base_part = ""
|
base_part = ""
|
||||||
|
|
||||||
block_lines = [f"{src} ({dst})" for src, dst in dedup.items()]
|
|
||||||
new_block = f"# {category}\n" + "\n".join(block_lines) + "\n\n"
|
|
||||||
|
|
||||||
# Match this category's section up to the next '#' header or end of the
|
# Match this category's section up to the next '#' header or end of the
|
||||||
# game portion. Handles '#Cat', '# Cat', '## Cat', etc.
|
# game portion. Handles '#Cat', '# Cat', '## Cat', etc.
|
||||||
pattern = re.compile(
|
pattern = re.compile(
|
||||||
rf"^[\t ]*#+\s*{re.escape(category)}\s*$\r?\n.*?(?=^[\t ]*#|\Z)",
|
rf"^([\t ]*#+\s*{re.escape(category)}\s*$\r?\n)(.*?)(?=^[\t ]*#|\Z)",
|
||||||
re.MULTILINE | re.DOTALL,
|
re.MULTILINE | re.DOTALL,
|
||||||
)
|
)
|
||||||
if pattern.search(game_part):
|
match = pattern.search(game_part)
|
||||||
|
if merge and match:
|
||||||
|
merged = _parse_section_pairs(match.group(2))
|
||||||
|
for src, dst in dedup.items():
|
||||||
|
if src not in merged:
|
||||||
|
merged[src] = dst
|
||||||
|
dedup = merged
|
||||||
|
if not dedup:
|
||||||
|
return
|
||||||
|
|
||||||
|
block_lines = [f"{src} ({dst})" for src, dst in dedup.items()]
|
||||||
|
new_block = f"# {category}\n" + "\n".join(block_lines) + "\n\n"
|
||||||
|
|
||||||
|
if match:
|
||||||
new_game = pattern.sub(lambda _m: new_block, game_part, count=1)
|
new_game = pattern.sub(lambda _m: new_block, game_part, count=1)
|
||||||
else:
|
else:
|
||||||
new_game = game_part.rstrip("\n")
|
new_game = game_part.rstrip("\n")
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,28 @@ DIALOGUE_FIELDS_RE = re.compile(
|
||||||
r"セリフ|コメント|相手|行為|台詞|会話|メッセージ_",
|
r"セリフ|コメント|相手|行為|台詞|会話|メッセージ_",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Foundation DB lines that are short display labels (map names, term titles),
|
||||||
|
# not descriptions / battle messages / dialogue. Used when seeding vocab.txt.
|
||||||
|
_DB_VOCAB_FIELD_RE = re.compile(
|
||||||
|
r"マップ名|"
|
||||||
|
r"属性名|"
|
||||||
|
r"用語名|"
|
||||||
|
r"コマンド名|"
|
||||||
|
r"(?:^|·\s*)タイトル(?:$|[((\s])|"
|
||||||
|
r"(?:^|·\s*)Name(?:\s*·|$)|"
|
||||||
|
r"(?:^|·\s*)Title(?:\s*·|$)|"
|
||||||
|
r"(?:^|·\s*)名称(?:$|[((\s·])|"
|
||||||
|
r"(?:^|·\s*)称号(?!.*コメント)|"
|
||||||
|
r"(?:^|·\s*)好きなもの(?!.*コメント)",
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
_DB_VOCAB_SKIP_FIELD_RE = re.compile(
|
||||||
|
r"説明|Description|文章|メッセージ|Message|セリフ|コメント|Comment|"
|
||||||
|
r"行為|相手|台詞|会話",
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
_DB_VOCAB_HARVEST_MAX_LEN = 72
|
||||||
|
|
||||||
ARCHETYPE_CLASSIC = "classic_rpg"
|
ARCHETYPE_CLASSIC = "classic_rpg"
|
||||||
ARCHETYPE_DB_HEAVY = "simulation_db_heavy"
|
ARCHETYPE_DB_HEAVY = "simulation_db_heavy"
|
||||||
ARCHETYPE_HYBRID = "hybrid"
|
ARCHETYPE_HYBRID = "hybrid"
|
||||||
|
|
@ -458,6 +480,79 @@ def load_db_filter_config() -> tuple[frozenset[str], frozenset[str]]:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def is_db_vocab_harvest_candidate(
|
||||||
|
line: dict[str, Any],
|
||||||
|
*,
|
||||||
|
type_name: str = "",
|
||||||
|
tier: str | None = None,
|
||||||
|
) -> bool:
|
||||||
|
"""True when a foundation DB line should seed ``vocab.txt``.
|
||||||
|
|
||||||
|
Only short label-like fields on foundation/system/unknown sheets qualify.
|
||||||
|
Descriptions, battle messages, and narrative dialogue stay out.
|
||||||
|
"""
|
||||||
|
resolved = tier
|
||||||
|
if resolved is None:
|
||||||
|
resolved = classify_group_tier(type_name, [line])
|
||||||
|
if resolved not in FOUNDATION_TIERS:
|
||||||
|
return False
|
||||||
|
field_name = str(line.get("fieldName") or "")
|
||||||
|
if _DB_VOCAB_SKIP_FIELD_RE.search(field_name):
|
||||||
|
return False
|
||||||
|
if not _DB_VOCAB_FIELD_RE.search(field_name):
|
||||||
|
return False
|
||||||
|
if DIALOGUE_FIELDS_RE.search(field_name):
|
||||||
|
return False
|
||||||
|
src = str(line.get("source") or "")
|
||||||
|
if not src.strip():
|
||||||
|
return False
|
||||||
|
if "\n" in src or "\r" in src:
|
||||||
|
return False
|
||||||
|
if len(src) > _DB_VOCAB_HARVEST_MAX_LEN:
|
||||||
|
return False
|
||||||
|
if len(re.findall(r"[。!?]", src)) >= 2:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def collect_db_vocab_pairs(
|
||||||
|
doc: dict[str, Any],
|
||||||
|
*,
|
||||||
|
include_tiers: frozenset[str] | None = None,
|
||||||
|
include_groups: frozenset[str] | None = None,
|
||||||
|
) -> dict[str, list[tuple[str, str]]]:
|
||||||
|
"""Return ``{typeName: [(source, text), ...]}`` for harvestable DB terms.
|
||||||
|
|
||||||
|
Respects the same sheet filter used for translation when *include_tiers* /
|
||||||
|
*include_groups* are provided. Only foundation-tier sheets contribute.
|
||||||
|
"""
|
||||||
|
if doc.get("kind") != "db":
|
||||||
|
return {}
|
||||||
|
json_file = json_file_from_doc(doc)
|
||||||
|
tiers = include_tiers if include_tiers is not None else frozenset()
|
||||||
|
groups = include_groups if include_groups is not None else frozenset()
|
||||||
|
by_sheet: dict[str, list[tuple[str, str]]] = {}
|
||||||
|
for group in doc.get("groups") or []:
|
||||||
|
type_name = str(group.get("typeName") or "")
|
||||||
|
lines = group.get("lines") or []
|
||||||
|
if not group_matches_filter(json_file, type_name, tiers, groups):
|
||||||
|
continue
|
||||||
|
tier = classify_group_tier(type_name, lines)
|
||||||
|
if tier not in FOUNDATION_TIERS:
|
||||||
|
continue
|
||||||
|
pairs: list[tuple[str, str]] = []
|
||||||
|
for line in lines:
|
||||||
|
if not is_db_vocab_harvest_candidate(line, type_name=type_name, tier=tier):
|
||||||
|
continue
|
||||||
|
src, dst = line.get("source"), line.get("text")
|
||||||
|
if not isinstance(src, str) or not isinstance(dst, str):
|
||||||
|
continue
|
||||||
|
pairs.append((src, dst))
|
||||||
|
if pairs:
|
||||||
|
by_sheet[type_name] = pairs
|
||||||
|
return by_sheet
|
||||||
|
|
||||||
|
|
||||||
def group_matches_filter(
|
def group_matches_filter(
|
||||||
json_file: str,
|
json_file: str,
|
||||||
type_name: str,
|
type_name: str,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue