Files
local-translator/examples/extract_tomie_ui.py
T

70 lines
3.0 KiB
Python

"""Extract Naninovel DefaultUI TextAsset from Tomie into this workspace.
Only reads the game. Writes a JSON translation input and a source manifest.
Requires: python -m pip install -e ".[unity]"
"""
import argparse
import hashlib
import json
from pathlib import Path
def parse_dictionary(text):
result = []
seen = set()
for line_number, line in enumerate(text.splitlines(), 1):
if not line.strip() or line.lstrip().startswith((";", "#", "//")):
continue
key, separator, value = line.partition(":")
if not separator or not key.strip() or not value.strip():
continue
key = key.strip()
if key in seen:
raise ValueError(f"Duplicate dictionary key at line {line_number}: {key}")
seen.add(key)
result.append({"id": key, "text": value.lstrip(), "line": line_number})
return result
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("game", type=Path)
parser.add_argument("--output", type=Path, default=Path("data/tomie/input.json"))
parser.add_argument("--limit", type=int, default=15, help="0 extracts the full DefaultUI dictionary")
args = parser.parse_args()
if args.limit < 0:
parser.error("limit must be nonnegative")
try:
import UnityPy
except ImportError:
parser.error('Install optional extractor: python -m pip install -e ".[unity]"')
asset_path = args.game.resolve() / "TomieWGM_Data" / "data.unity3d"
output = args.output.resolve()
if output.is_relative_to(args.game.resolve()):
parser.error("Choose an output outside the game directory")
environment = UnityPy.load(str(asset_path))
assets = [obj for obj in environment.objects if obj.type.name == "TextAsset" and obj.read().m_Name == "DefaultUI"]
if len(assets) != 1:
parser.error(f"Expected exactly one DefaultUI TextAsset, found {len(assets)}")
obj = assets[0]
script = obj.read().m_Script
if isinstance(script, bytes):
script = script.decode("utf-8-sig")
entries = parse_dictionary(script)
selected = entries[:args.limit] if args.limit else entries
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps([{"id": row["id"], "text": row["text"]} for row in selected], ensure_ascii=False, indent=2), encoding="utf-8")
manifest = {
"game": str(args.game.resolve()), "source_file": str(asset_path), "asset_name": "DefaultUI",
"path_id": obj.path_id, "serialized_file": obj.assets_file.name,
"script_sha256": hashlib.sha256(script.encode("utf-8")).hexdigest(),
"total_entries": len(entries), "selected_entries": selected,
"note": "Read-only extraction. No patching, font replacement or changes to game files.",
}
output.with_suffix(".manifest.json").write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"Extracted {len(selected)}/{len(entries)} UI strings to {output}")
if __name__ == "__main__":
main()