загружаю
This commit is contained in:
+256
@@ -0,0 +1,256 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
r"""
|
||||
Универсальный скрипт для локализации CatSystem2 (Shift-JIS).
|
||||
2 в 1: Извлечение в .nsv и обратная сборка в .txt с поддержкой поля translated_text.
|
||||
Поддерживает аргументы: --txt2nsv и --nsv2txt
|
||||
|
||||
Формат хранения — NSV (Newline-Separated Values, см. nsv.py) вместо JSON:
|
||||
компактнее на диске, лучше диффы в git. LLM (в translate.py) никогда не видит
|
||||
NSV-экранирование — оно разбирается и собирается заново только здесь и в
|
||||
nsv.py; наружу и в промпт всегда идут обычные Python-строки.
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
from nsv_io import load_nsv_file, save_nsv_file
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent
|
||||
SRC_ENCODING = "cp932" # Shift-JIS
|
||||
|
||||
PURE_CONTROL_TOKENS = {"\\r", "\\p", "\\n", "\\@"}
|
||||
|
||||
|
||||
def is_text_line(raw_line: str) -> bool:
|
||||
stripped = raw_line.strip()
|
||||
if not stripped or stripped in PURE_CONTROL_TOKENS:
|
||||
return False
|
||||
if "[" in raw_line or "「" in raw_line or "」" in raw_line:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def parse_speaker_and_text(raw_line: str):
|
||||
"""Строгий разбор формата CatSystem2: 'Имя\\tТекст' или '\\tТекст' (без имени).
|
||||
|
||||
Разделитель между именем и текстом — ровно один таб, ничего больше.
|
||||
Если строка начинается с таба — имени нет, текст идёт с отступом.
|
||||
"""
|
||||
if raw_line.startswith("\t"):
|
||||
return None, raw_line[1:]
|
||||
|
||||
if "\t" in raw_line:
|
||||
speaker, text = raw_line.split("\t", 1)
|
||||
return speaker, text
|
||||
|
||||
# Строка без единого таба под критерии is_text_line не должна попадать,
|
||||
# но на случай мусорных данных не теряем её молча.
|
||||
return None, raw_line
|
||||
|
||||
|
||||
def extract_entries(lines):
|
||||
entries = []
|
||||
pending_pcm = None
|
||||
pcm_re = re.compile(r"^\s*pcm\s+(\S+)\s*$")
|
||||
|
||||
for idx, raw in enumerate(lines):
|
||||
line_no = idx + 1
|
||||
stripped = raw.strip()
|
||||
|
||||
pcm_match = pcm_re.match(stripped)
|
||||
if pcm_match:
|
||||
pending_pcm = pcm_match.group(1)
|
||||
continue
|
||||
|
||||
if is_text_line(raw):
|
||||
speaker, text = parse_speaker_and_text(raw)
|
||||
entries.append({
|
||||
"line": line_no,
|
||||
"speaker": speaker,
|
||||
"pcm": pending_pcm,
|
||||
"text": text,
|
||||
"translated_text": "",
|
||||
})
|
||||
pending_pcm = None
|
||||
|
||||
return entries
|
||||
|
||||
|
||||
def txt_to_nsv():
|
||||
print("\n--- РЕЖИМ 1: ИЗВЛЕЧЕНИЕ В .nsv ---")
|
||||
src_dir = BASE_DIR / (input("Папка с исходными .txt [txt]: ").strip() or "txt")
|
||||
dst_dir = BASE_DIR / (input("Папка для .nsv [translate]: ").strip() or "translate")
|
||||
|
||||
if not src_dir.is_dir():
|
||||
print(f"Ошибка: папка не найдена: {src_dir}")
|
||||
return
|
||||
|
||||
dst_dir.mkdir(exist_ok=True)
|
||||
txt_files = sorted(src_dir.glob("*.txt"))
|
||||
ok_count, fail_count = 0, 0
|
||||
|
||||
for txt_file in txt_files:
|
||||
nsv_file = dst_dir / (txt_file.stem + ".nsv")
|
||||
try:
|
||||
# errors="replace" здесь осознанно: если в исходном декомпилированном
|
||||
# .txt попадётся байт, нечитаемый в cp932, лучше получить "?" и продолжить,
|
||||
# чем упасть на самом первом шаге пайплайна. Итоговый .nsv всё равно
|
||||
# редактируется человеком/LLM дальше, ошибка будет заметна визуально.
|
||||
with open(txt_file, "r", encoding=SRC_ENCODING, errors="replace", newline="") as f:
|
||||
lines = [ln.rstrip("\r\n") for ln in f]
|
||||
|
||||
entries = extract_entries(lines)
|
||||
save_nsv_file(nsv_file, entries)
|
||||
|
||||
ok_count += 1
|
||||
print(f"-> {txt_file.name} (строк: {len(entries)})")
|
||||
except Exception as e:
|
||||
fail_count += 1
|
||||
print(f"[!] Ошибка {txt_file.name}: {e}")
|
||||
|
||||
print(f"\nГотово. Успешно: {ok_count}, Ошибок: {fail_count}\n")
|
||||
|
||||
|
||||
def fix_llm_symbols(text: str) -> str:
|
||||
"""Заменяет символы от LLM, от которых падает Shift-JIS"""
|
||||
replacements = {
|
||||
'ё': 'е', 'Ё': 'Е',
|
||||
'—': '―', '–': '-',
|
||||
'«': '"', '»': '"',
|
||||
'“': '"', '”': '"',
|
||||
'‘': "'", '’': "'",
|
||||
'…': '...',
|
||||
'\u200b': '',
|
||||
'\u00a0': ' ',
|
||||
'\u2014': '―',
|
||||
'\u2013': '-',
|
||||
}
|
||||
for bad, good in replacements.items():
|
||||
text = text.replace(bad, good)
|
||||
return text
|
||||
|
||||
|
||||
def nsv_to_txt():
|
||||
print("\n--- РЕЖИМ 2: СБОРКА ИЗ .nsv В TXT ---")
|
||||
|
||||
print("Какое поле с текстом использовать?")
|
||||
print(" 1 - Использовать базовое поле 'text'")
|
||||
print(" 2 - Использовать поле 'translated_text' (с откатом к 'text')")
|
||||
|
||||
while True:
|
||||
mode_input = input("Ваш выбор [2]: ").strip()
|
||||
if not mode_input:
|
||||
mode = 2
|
||||
break
|
||||
elif mode_input in ("1", "2"):
|
||||
mode = int(mode_input)
|
||||
break
|
||||
else:
|
||||
print("Пожалуйста, введите 1 или 2.")
|
||||
|
||||
txt_input = input("Папка с ОРИГИНАЛЬНЫМИ .txt [txt]: ").strip() or "txt"
|
||||
nsv_input = input("Папка с ПЕРЕВЕДЕННЫМИ .nsv [translate]: ").strip() or "translate"
|
||||
out_input = input("Куда сохранить готовые .txt [txt_translated]: ").strip() or "txt_translated"
|
||||
|
||||
txt_dir = BASE_DIR / txt_input
|
||||
nsv_dir = BASE_DIR / nsv_input
|
||||
out_dir = BASE_DIR / out_input
|
||||
|
||||
if not nsv_dir.is_dir() or not txt_dir.is_dir():
|
||||
print("Ошибка: папки с исходниками или переводами не найдены.")
|
||||
return
|
||||
|
||||
out_dir.mkdir(exist_ok=True)
|
||||
nsv_files = sorted(nsv_dir.glob("*.nsv"))
|
||||
ok_count, fail_count = 0, 0
|
||||
|
||||
for nsv_file in nsv_files:
|
||||
txt_file = txt_dir / (nsv_file.stem + ".txt")
|
||||
out_file = out_dir / txt_file.name
|
||||
temp_txt_file = out_dir / f"~temp_{txt_file.name}"
|
||||
|
||||
if not txt_file.exists():
|
||||
print(f"[!] Нет оригинального txt для {nsv_file.name}, пропускаю...")
|
||||
continue
|
||||
|
||||
try:
|
||||
shutil.copy2(txt_file, temp_txt_file)
|
||||
|
||||
with open(temp_txt_file, "r", encoding=SRC_ENCODING, errors="replace", newline="") as f:
|
||||
orig_lines = f.readlines()
|
||||
|
||||
entries = load_nsv_file(nsv_file)
|
||||
|
||||
for entry in entries:
|
||||
idx = entry["line"] - 1
|
||||
if idx >= len(orig_lines):
|
||||
continue
|
||||
|
||||
if mode == 2:
|
||||
text_raw = entry.get("translated_text") or entry.get("text", "")
|
||||
else:
|
||||
text_raw = entry.get("text", "")
|
||||
|
||||
text = fix_llm_symbols(text_raw)
|
||||
speaker = entry.get("speaker")
|
||||
|
||||
orig_line = orig_lines[idx]
|
||||
newline_chars = "\r\n" if orig_line.endswith("\r\n") else "\n"
|
||||
clean_orig = orig_line.rstrip("\r\n")
|
||||
|
||||
if speaker:
|
||||
orig_lines[idx] = f"{speaker}\t{text}{newline_chars}"
|
||||
else:
|
||||
m = re.match(r"^(\s+)", clean_orig)
|
||||
sep = m.group(1) if m else " "
|
||||
orig_lines[idx] = f"{sep}{text}{newline_chars}"
|
||||
|
||||
# errors="strict": если после fix_llm_symbols остался символ,
|
||||
# не представимый в cp932, лучше упасть с понятной ошибкой сейчас,
|
||||
# чем молча получить "?" в игре после компиляции.
|
||||
with open(out_file, "w", encoding=SRC_ENCODING, errors="strict", newline="") as f:
|
||||
f.writelines(orig_lines)
|
||||
|
||||
ok_count += 1
|
||||
print(f"<- {nsv_file.name} собран успешно")
|
||||
|
||||
except Exception as e:
|
||||
fail_count += 1
|
||||
print(f"[!] Ошибка {nsv_file.name}: {e}")
|
||||
|
||||
finally:
|
||||
if temp_txt_file.exists():
|
||||
try:
|
||||
temp_txt_file.unlink()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
print(f"\nГотово. Успешно: {ok_count}, Ошибок: {fail_count}\n")
|
||||
|
||||
|
||||
def main():
|
||||
if "--txt2nsv" in sys.argv or "--txt2json" in sys.argv:
|
||||
txt_to_nsv()
|
||||
return
|
||||
elif "--nsv2txt" in sys.argv or "--json2txt" in sys.argv:
|
||||
nsv_to_txt()
|
||||
return
|
||||
|
||||
print("=== Утилита локализации CatSystem2 ===")
|
||||
print("1. Разобрать игру (TXT -> NSV)")
|
||||
print("2. Собрать перевод (NSV -> TXT)")
|
||||
choice = input("Ваш выбор (1 или 2): ").strip()
|
||||
|
||||
if choice == '1':
|
||||
txt_to_nsv()
|
||||
elif choice == '2':
|
||||
nsv_to_txt()
|
||||
else:
|
||||
print("Неверный выбор. До свидания!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user