Initial local translation API with persistent projects and batch jobs

This commit is contained in:
mrleo1nid committed 2026-10-23 13:31:13 +03:00
commit 13492929c6
21 files changed
+1624

No files matched your search

+223
View File
@@ -0,0 +1,223 @@
# 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]`:
```toml
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](https://lmstudio.ai/docs/developer/core/server), [CLI](https://lmstudio.ai/docs/cli), [совместимый API](https://lmstudio.ai/docs/developer/openai-compat).
## Первый перевод из Python
В другой консоли из каталога проекта:
```powershell
python examples/translate_game.py --input examples/strings.json --output data/result.json
```
Скрипт не требует сторонних библиотек. Создаёт проект с контекстом и небольшим примером словаря, отправляет строки пачками по 10 и сохраняет полный результат. Вход — JSON-массив:
```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`. Существующий проект:
```powershell
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`
```json
{
"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`
```json
{
"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:
```json
{
"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](https://huggingface.co/tencent/Hy-MT2-7B).
Для 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](https://huggingface.co/tencent/Hy-MT2-7B-GGUF/tree/main).
Tencent не обещает отсутствие цензуры в карточке HY-MT2. Промпт просит сохранять грубость и откровенность, однако отсутствие отказов и смягчений нужно проверять на репликах игры. Дополнительный кандидат для сравнения — [Huihui Qwen3-8B abliterated](https://huggingface.co/huihui-ai/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`.
```powershell
.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:
```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:
```bash
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 сам это не настраивает.
## Проверки и ограничения первой версии
```powershell
.venv\Scripts\python.exe -m unittest discover -s tests -v
```
Проверяются обработка пачек, защита синтаксиса, пользовательские правила, словарь, кэш и ревизии, ошибки ответов модели, повтор только неудачных строк и восстановление очереди. Провайдер в тестах подменяется HTTP-имитатором. Windows-батник и работающая LM Studio проверены также в реальном прогоне. Установка systemd на Linux пока не проверялась.
Используйте один процесс приложения: файловая блокировка запрещает двум экземплярам обрабатывать одну БД. Сервер запускается на loopback, без отдельной авторизации и CORS. В SQLite сохраняются исходные строки, контекст и переводы. Для резервного копирования остановите сервер и скопируйте `data/` вместе с `config.toml`.
Пока нет веб-интерфейса управления, потоковой выдачи токенов, отмены заданий, автоматического пополнения словаря, точного подсчёта токенов и адаптеров игровых файлов. Интерактивная `/docs` предназначена для работы с API.