Files
local-translator/README.md
T

25 KiB
Raw Blame History

Local Translator

Локальный HTTP API для перевода игровых строк через LM Studio, OpenRouter и другие серверы с совместимым POST /chat/completions. Python 3.11+. По умолчанию перевод English → Russian, сервер слушает только 127.0.0.1:8787.

Запуск на Windows

  1. Установите Python 3.11+ и LM Studio. Скачайте модель, загрузите её и запустите сервер в Developer → Start Server.
  2. Дважды щёлкните start.bat. При первом запуске создаётся .venv, скачиваются зависимости из PyPI и копируется config.example.toml в config.toml.
  3. Документация с формами запросов: http://127.0.0.1:8787/docs. Состояние приложения: http://127.0.0.1:8787/health.

После установки зависимостей локальный перевод не требует интернета. Чтобы обновить зависимости после изменения pyproject.toml, выполните .venv\Scripts\python.exe -m pip install -e ..

В config.toml задайте providers.lmstudio.model — точный ID из GET /providers/lmstudio/models, а не произвольное имя файла. Пустое значение допустимо, если /models возвращает ровно одну модель. При нескольких моделях приложение потребует явный выбор. Можно переопределить модель в отдельном проекте.

Автозапуск LM Studio

В config.toml, внутри [providers.lmstudio]:

autostart = true
startup_timeout = 90
# Необязательно: точное имя уже скачанной модели, известное `lms ls`.
load_model = ""

Нужен доступный в PATH официальный lms CLI. Если выбранный ID уже доступен через API, работающий сервер используется сразу. Иначе приложение выполняет lms daemon up, затем lms server start --port 1234 --bind 127.0.0.1, при заданном load_model — lms load ... --context-length 8192, с --identifier, если задан model. Окно загрузки меняется через load_context_length. После этого приложение ждёт доступности выбранного ID в /models до startup_timeout. GUI открывать не требуется. Это запуск локального движка; модель автоматически не скачивается. Внешний CLI запускается без видимого окна на Windows.

Если автозапуск не удался, API всё равно доступен для управления проектами. Причина видна в консоли и health.startup_errors (результат именно последней попытки запуска, а не текущая готовность провайдера). Запустите LM Studio вручную и повторите неудачное задание.

Официальные инструкции: сервер LM Studio, CLI, совместимый API.

Первый перевод из Python

В другой консоли из каталога проекта:

python examples/translate_game.py --input examples/strings.json --output data/result.json

Скрипт не требует сторонних библиотек. Создаёт проект с контекстом и небольшим примером словаря, отправляет строки пачками по 10 и сохраняет полный результат. Вход — JSON-массив:

[
  {"id": "intro.001", "text": "Welcome, {player_name}!"},
  {"id": "intro.002", "text": "Bring me the Moonstone."}
]

ID задаёт ваш скрипт извлечения; если их нет в игре, используйте порядковые номера в виде строк. ID уникальны внутри одного задания. Связь с файлами и обратную вставку обеспечивает клиентский скрипт.

Для создания проекта из своего JSON используйте --project-file examples/tomie_project.json. Существующий проект:

python examples/translate_game.py --project PROJECT_ID --input examples/strings.json --output data/result.json

Рядом с результатом сохраняется result.json.job.json с ID задания. Повтор той же команды продолжит наблюдать это задание после остановки клиента. --retry повторяет только неудачные строки. --new-job --project PROJECT_ID создаёт новое задание с текущими настройками проекта. Состояние предыдущего задания доступно в API. При создании нового проекта можно указать --context "описание игры" и --target Russian.

API

Полная схема: /docs и /openapi.json. Ключ модели не передаётся в запросах — используется переменная окружения, имя которой задано в конфигурации провайдера.

Создать проект

POST /projects

{
  "name": "My RPG",
  "source_language": "English",
  "target_language": "Russian",
  "context": "Мрачная фэнтези RPG. Реплики короткие, речь естественная.",
  "instructions": "Сохраняй грубость и стиль оригинала. Обращение на ты.",
  "glossary": {"Moonstone": "Лунный камень"},
  "provider": "lmstudio",
  "model": null,
  "disabled_rules": [],
  "protection_rules": []
}

Ответ содержит id и revision. Языки можно опустить: по умолчанию English → Russian. Контекст и словарь добавляются к каждой пачке. История разговоров между пачками не накапливается; словарь редактируется явно, автоматического извлечения терминов пока нет. Если вся строка в точности совпадает с ключом словаря (регистр учитывается), используется заданное значение без генерации моделью. Это обеспечивает точный перевод коротких кнопок и сокращений; защита синтаксиса проверяется и в этом случае. У таких строк attempts: 0. Для терминов внутри более длинных строк словарь служит инструкцией модели, без гарантии точного соблюдения.

GET /projects, GET /projects/{id} — просмотр. PUT /projects/{id} — полная замена настроек, без полей id и revision; для изменения одного значения сначала получите проект и отправьте все его настройки. Каждое обновление увеличивает revision.

Перевести одну строку или пачку

POST /projects/{id}/translate

{
  "items": [
    {"id": "1", "text": "Hello, {player}!"},
    {"id": "2", "text": "You have %d seconds."}
  ],
  "batch_size": 10,
  "max_batch_chars": 12000,
  "retries": 2,
  "use_cache": true
}

Один элемент — перевод одной строки. Одна пачка — один запрос к модели с общим контекстом. Порядок результата соответствует входу, даже если модель переставила записи в JSON. retries: 2 означает максимум три попытки для каждой строки; успешно переведённые строки повторно не отправляются. Ответ модели проверяется до сохранения перевода.

Сервер ждёт результат до 30 секунд: 200 с готовым заданием или 202 с текущим состоянием и Location: /jobs/{id}. После 202 опрашивайте этот адрес. Завершение задания с ошибкой тоже возвращает объект задания — проверяйте status и состояние каждой строки, одного HTTP-кода недостаточно.

Долговечные задания

  • POST /projects/{id}/jobs — сразу возвращает 202 и ID. Предпочтительный вариант для скриптов.
  • GET /jobs/{id} — прогресс и результаты всех строк.
  • GET /projects/{id}/jobs?limit=50&offset=0 — история заданий, новые первыми.
  • POST /jobs/{id}/retry — повторить незавершённые/неудачные строки после ошибки. Сбрасывает их счётчик попыток. Готовые строки сохраняются.

Статусы задания: queued, running, completed, completed_with_errors, failed. Статусы строк: pending, completed, failed. В items возвращаются id, source, translation, status, error, attempts, cached, position. При общей ошибке подключения/выбора модели задание получает failed, ещё не обработанные строки могут остаться pending.

Задание хранит снимок контекста, словаря, правил и настроек провайдера. Обновление проекта не меняет уже созданные задания. Повтор старого задания использует старые настройки; для перевода по новым настройкам создайте новое.

SQLite сохраняет каждый успешный результат. После перезапуска незавершённые задания возвращаются в очередь, готовые строки не теряются. Запрос, прерванный непосредственно во время генерации или до записи результата, может быть отправлен модели повторно; это не гарантия ровно одного вызова. Счётчик попыток также сохраняется. После исчерпания попыток нужен явный /retry.

Кэш разделён по проекту, его ревизии, настройкам провайдера, выбранной модели и версии промпта. Изменение настроек не удаляет историю, но прежний кэш больше не используется. use_cache: false принудительно выполняет новый перевод. Замена весов модели под тем же ID автоматически не распознаётся: обновите проект или отключите кэш.

Один процесс последовательно обрабатывает задания, чтобы не перегружать локальную GPU. Пачки группируются по числу строк и сумме символов, а не по точному числу токенов. Предел одной строки — 30 000 символов; она не разрезается даже при превышении max_batch_chars. На задание допускается 1–500 строк, суммарно до 1 000 000 символов; более крупные входы делите на задания. Размер реального контекста модели настраивается в LM Studio. При переполнении уменьшите пачку/контекст или увеличьте окно модели.

Плейсхолдеры и правила

GET /protection-rules возвращает имена и регулярные выражения встроенных правил:

  • braces: {player}, {0}, {{value}};
  • dollar: ${quest}, $variable;
  • printf: %s, %1$s, %02d, %NAME%, %%;
  • tags: <color=red>, </color>, HTML/XML-подобные теги;
  • square: [wait] и другие конструкции в квадратных скобках;
  • escapes: текстовые \n, \t, \u1234 и подобные последовательности;
  • newlines: настоящие LF/CRLF/CR;
  • entities: &amp;, &#123;;
  • controls: табуляция и другие управляющие символы.

До отправки модели фрагменты заменяются уникальными маркерами. После ответа проверяются их количество и порядок, восстанавливаются исходные байт-в-байт значения строковых фрагментов, проверяется отсутствие новых распознаваемых конструкций. Повреждённый ответ не сохраняется как успешный перевод. Строгий порядок также запрещает переставлять плейсхолдеры ради грамматики.

В проекте можно отключать встроенные правила и добавлять свои Python-regex:

{
  "disabled_rules": ["square"],
  "protection_rules": [
    {"name": "engine_codes", "pattern": "@[A-Z_]+@"},
    {"name": "rpg_maker", "pattern": "\\\\[A-Za-z]+\\[[0-9]+\\]"}
  ]
}

Это часть полного объекта проекта. Пользовательские правила имеют приоритет над встроенными при совпадении начала фрагмента. При перекрытии защищается первое совпадение; вложенные языки/ICU/сложные шаблоны требуют правил для конкретного движка. square намеренно широкое: если в скобках обычный текст, отключите его. Универсального распознавания всех игровых форматов нет. Защита проверяет структуру, но не смысл или качество перевода.

HY-MT2 и выбор модели

По умолчанию локальный провайдер использует prompt_style = "hy_mt": один user-промпт с контекстом, словарём и JSON. temperature=0.7, top_p=0.6, top_k=20, repetition_penalty=1.05, max_tokens=4096 соответствуют рекомендациям Tencent для 7B. Для обычных instruction-моделей поставьте prompt_style = "generic"; неподдерживаемые необязательные параметры удалите из TOML. Параметры и инструкции — карточка Tencent.

Для RTX 3060 12 GB и 32 GB RAM разумно начать с HY-MT2-7B Q6_K, сравнив с Q4_K_M на своих строках. Файлы официальных квантовок: Q4_K_M — 4,62 GB, Q6_K — 6,16 GB, Q8_0 — 7,98 GB. Это размеры весов, не общий расход VRAM: контекст и runtime требуют дополнительную память. Стартовая проба — пачка 10 коротких строк и окно 8192, затем настройка по фактическому расходу LM Studio. Это ориентир, не измеренный тест на этой GPU. Официальные GGUF.

Tencent не обещает отсутствие цензуры в карточке HY-MT2. Промпт просит сохранять грубость и откровенность, однако отсутствие отказов и смягчений нужно проверять на репликах игры. Дополнительный кандидат для сравнения — Huihui Qwen3-8B abliterated, заявленный автором как модель с удалением отказов. Такая модификация сама по себе не доказывает качество EN→RU. Автоматической проверки отказов по смыслу в API нет.

Реальный перевод HY-MT2 через LM Studio пока не входит в автоматические проверки: тесты используют имитатор OpenAI-совместимого провайдера.

Пример: Tomie Wants to Get Married

Для версии Expansion-v1.3801-Win подготовлен отдельный экстрактор словаря интерфейса Naninovel DefaultUI, хранящегося в Unity TextAsset внутри TomieWGM_Data/data.unity3d.

.venv\Scripts\python.exe -m pip install -e ".[unity]"
.venv\Scripts\python.exe examples/extract_tomie_ui.py "D:\Torrents\Pgames\Tomie_Wants_to_Get_Married_Expansion-v1.3801-Win" --limit 15
.venv\Scripts\python.exe examples/translate_game.py --project-file examples/tomie_project.json --input data/tomie/input.json --output data/tomie/result.json

В словаре найдено 61 значение. По умолчанию извлекаются первые 15; --limit 0 извлекает все. В manifest рядом с входным JSON сохраняются исходные ключи, номера строк, ID Unity-объекта и хеш исходного текста. Экстрактор только читает файлы игры и запрещает вывод внутрь её каталога. Обратная упаковка и замена шрифтов пока не реализованы.

На этой машине выполнен реальный прогон первых 15 строк через HY-MT2-7B Q4_K_M в LM Studio на RTX 3060 с окном 8192: все строки обработаны, цветовые теги сохранены. Проверены повторное использование кэша и сохранность задания после перезапуска API через батник. Проект содержит контекст игры и словарь интерфейсных терминов. Качество диалогов и поведение модели на откровенных сценах этим тестом не оценивались.

Здесь проверяется только DefaultUI: тексты сценариев и динамические реплики не охвачены. Основной контент использует Unity/Naninovel, для полноценной локализации понадобится отдельный разбор сценариев, пользовательского интерфейса и поддержки кириллицы шрифтами.

Другие провайдеры

Пример OpenRouter уже есть в config.example.toml. Задайте модель и OPENROUTER_API_KEY в окружении процесса, затем создайте проект с provider: "openrouter". В PowerShell:

$env:OPENROUTER_API_KEY = "ваш ключ"
.\start.bat

Для другого сервера добавьте [providers.my_server] с base_url, model, при необходимости api_key_env. В base_url входит /v1 или другой префикс, но не /chat/completions. После изменения TOML перезапустите приложение. Сервис поддерживает HTTP OpenAI Chat Completions, а не произвольные нативные API всех поставщиков. Структурированный ответ запрашивается промптом, без обязательного response_format, чтобы работать с разными серверами.

Linux и systemd

Для ручного запуска: bash start.sh (нужны Python 3.11+, модуль venv и доступ к PyPI при первой установке).

Для системного сервиса пример рассчитан на /opt/local-translator и пользователя translator. Подготовьте каталог с проектом и виртуальное окружение до запуска systemd:

sudo useradd --system --home-dir /opt/local-translator --shell /usr/sbin/nologin translator
sudo python3 -m venv /opt/local-translator/.venv
sudo /opt/local-translator/.venv/bin/python -m pip install /opt/local-translator
sudo cp /opt/local-translator/config.example.toml /opt/local-translator/config.toml
sudo install -d -o translator -g translator /opt/local-translator/data
sudo cp /opt/local-translator/deploy/local-translator.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now local-translator
journalctl -u local-translator -f

Если пользователь/каталог уже существуют, адаптируйте команды. Путь к БД должен оставаться внутри доступного сервису data/ либо нужно изменить ReadWritePaths. Ключи провайдеров можно положить в /etc/local-translator.env (OPENROUTER_API_KEY=..., права 600, владелец root).

Пример systemd предполагает уже работающий локальный сервер модели или внешний провайдер. LM Studio, запущенная под вашим desktop-пользователем, доступна сервису через loopback. Автозапуск lms из этого защищённого systemd unit требует отдельной настройки пользователя, HOME, PATH и прав на модели; для него удобнее пользовательский unit с собственным LM Studio/llmster. Системный unit сам это не настраивает.

Проверки и ограничения первой версии

.venv\Scripts\python.exe -m unittest discover -s tests -v

Проверяются обработка пачек, защита синтаксиса, пользовательские правила, словарь, кэш и ревизии, ошибки ответов модели, повтор только неудачных строк и восстановление очереди. Провайдер в тестах подменяется HTTP-имитатором. Windows-батник и работающая LM Studio проверены также в реальном прогоне. Установка systemd на Linux пока не проверялась.

Используйте один процесс приложения: файловая блокировка запрещает двум экземплярам обрабатывать одну БД. Сервер запускается на loopback, без отдельной авторизации и CORS. В SQLite сохраняются исходные строки, контекст и переводы. Для резервного копирования остановите сервер и скопируйте data/ вместе с config.toml.

Пока нет веб-интерфейса управления, потоковой выдачи токенов, отмены заданий, автоматического пополнения словаря, точного подсчёта токенов и адаптеров игровых файлов. Интерактивная /docs предназначена для работы с API.