Initial local translation API with persistent projects and batch jobs
This commit is contained in:
commit
13492929c6
21 files changed
+1624
No files matched your search
@@ -0,0 +1,69 @@
|
||||
"""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()
|
||||
Reference in new issue
Block a user