Я пытался сделать переводчик для блога. В итоге сделал QA-обвязку.
Практический разбор автоматизации переводов блога на русский и сербский: локальные модели, GPT-4.1 nano через API, сломанные сноски, утечки кириллицы и финальное решение — пусть инструменты защищают структуру, а финальный голос остаётся за редактурой.
Я начал с простого желания: у меня были английские статьи на dzarlax.dev, и я хотел получить их русские и сербские версии, не превращая каждую публикацию в проект на выходные.
На первый взгляд всё выглядело автоматизируемым. Исходник — в Markdown. На сайте уже настроена мультиязычная маршрутизация. Текст я пишу в Obsidian vault, а структура статей максимально предсказуема:
Plaintext
articles/<slug>/index.md
articles/<slug>/index.ru.md
articles/<slug>/index.rs.md
Поэтому изначальный вопрос казался чисто техническим: какую модель перевода выбрать?
Оказалось, это был неправильный вопрос. Настоящий звучал так: какие части перевода стоит автоматизировать, а какие должны остаться редакторскими?
Финальный ответ оказался не героическим end-to-end переводчиком, а штукой гораздо менее романтичной: генератором черновиков в обвязке из детерминированного QA. Модель переводит. Обвязка защищает Markdown, ссылки, сноски, inline-код, языковые ограничения, лимиты прогонов и статус ревью. Финальный проход по тексту я делаю в приложении ChatGPT и в редакторе, потому что именно там текст и доводится до приличного состояния.
Вот как я к этому пришёл.
Переводчик, который, как мне казалось, мне нужен
Первая версия пайплайна была очевидной: отправить статью в достаточно сильную локальную LLM, снабдив её глоссарием и инструкцией по ToV (тону голоса). У меня уже была развёрнута Ollama, где я мог запустить aya-expanse:32b. Модель казалась достаточно крупной для качественного результата и достаточно многоязычной, чтобы это выглядело правдоподобно.
Для русского языка результат часто бывал приемлемым. Для сербского на латинице — категорически нет.
Провалы не были драматичными в привычном понимании бенчмарков. Модель не генерировала откровенную бессмыслицу. Она выдавала текст, который был слишком похож на правильный, и тем самым опасен: славяноподобную прозу с примесью чешского, польского, хорватского или боснийского там, где требовался чистый сербский на латинице. Носитель языка или свободный спикер заметил бы это сразу. Я же не владею сербским настолько совершенно, чтобы надёжно вылавливать такие галлюцинации самостоятельно.
Тогда я попробовал классическую двухэтапную схему:
- Использовать OPUS-MT / Marian для сырого перевода.
- Взять крупную LLM для финальной полировки стиля.
На бумаге это выглядело красиво: специализированная NMT-модель (нейросетевой перевод) отвечает за грамматику, а LLM наводит лоск и стиль.
На практике граница размылась. На этапе NLLB коверкались названия брендов и ломался inline Markdown. А этап полировки, хоть и выправлял тон, начинал самовольно менять структуру текста. Один запуск добавил фантомный заголовок верхнего уровня, другой — переписал правила пунктуации, которые его эксплицитно просили сохранить. LLM вела себя не как механический редактор, а как вольный писатель.
Это полезно, когда вам нужен соавтор. Но слишком дорого, когда нужна просто точная заплатка.
Первый полезный baseline оказался скучным
Всё изменилось, когда я попробовал TranslateGemma.
Запуск translategemma:12b не выдавал шедевров литературы, но он идеально делал важную рутину: сохранял бренды, не трогал inline-код, удерживал структуру Markdown, не скатывался в «сербский салат» и работал быстро.
Ранний архив экспериментов в _meta/translation-experiments в итоге зафиксировал такие результаты:
| Запуск | Пайплайн | Результат |
|---|---|---|
| Aya 32B | Одностадийная локальная LLM | Приемлемо для RU, ненадёжно для сербского на латинице. |
| OPUS-MT -> Aya 32B | NMT плюс полировка | Выше качество грамматики, но ломаются бренды и Markdown, модель выдумывает структуру. |
| TranslateGemma 12B | Одностадийная модель перевода | Лучший структурный baseline, достаточно быстрый для реального использования. |
| TranslateGemma 12B -> Aya 32B | Перевод плюс полировка | Регресс по структуре и пунктуации, ненадёжная интеграция терминов из глоссария. |
Неожиданный вывод: TranslateGemma победила не потому, что она лучший писатель. Она оказалась лучшим производителем черновиков. Это более узкая, утилитарная задача, и её гораздо проще сделать предсказуемой.
На этом этапе я всё ещё надеялся добавить вторую модель для финальной полировки, но полноценный бенчмарк окончательно похоронил эту идею.
Бенчмарк ответил на другой вопрос
Я провёл широкое сравнение на двух разноплановых статьях:
- Короткий пост с обилием брендов:
sleeping-terminal-1000-dollars. - Длинный лонгрид из серии Health Dashboard со сложной терминологией:
part-4-readiness-stopped-being-one-number.
В тестировании участвовали TranslateGemma (разных размеров), Gemma 3, Aya 32B и NLLB. Проверки были намеренно приземлёнными: валидность заголовков, сохранность inline-кода и брендовых токенов, правильность длинных тире и форматирования чисел, отсутствие утечек кириллицы в сербский текст и явных следов других славянских языков.
Одной таблицы задержек хватило, чтобы избавить меня от иллюзий:
| Модель | Короткая статья (RU+RS) | Длинная статья (RU+RS) | Примечания |
|---|---|---|---|
| TranslateGemma 4B | 29 с | 47 с | Быстро, качество черновика вполне рабочее. |
| TranslateGemma 12B | 54 с | 89 с | Лучший баланс качества и скорости по умолчанию. |
| TranslateGemma 27B | 536 с | ~870 с | Иногда точнее, но неоправданно медленно. |
| Gemma 3 12B | 47 с | 116 с | Хороший слог, но выше риск сломать форматирование. |
| Gemma 3 27B | 521 с | ~1700 с (на одном RS-блоке) | Медленно, улучшения минимальны и нестабильны. |
| Aya 32B | 863 с | ~1880 с | Медленно, высокий риск повредить структуру. |
| NLLB 1.3B | 2022 с (на коротком тесте) | Дисквалифицирована | Не справилась с Markdown-разметкой. |
Конечно, были заманчивые находки. Gemma 3 иногда подбирала более точные сербские синонимы, а TranslateGemma 27B требовала меньше правок по глоссарию. Но каждое такое улучшение приносило новую регрессию: то американское форматирование валюты проскочит, то английский корень обрастёт сербским суффиксом, то заметно вырастет время прогона.
Главный итог бенчмарка: я понял, что автоматизировать не нужно.
В глоссарии были контекстные правила. Например: оставлять термин readiness на английском, если речь идёт о конкретном параметре (recovery score), но переводить в остальных случаях. Ни одна модель не смогла соблюдать это стабильно. Вывод стал очевиден: это не проблема качества перевода. Это чисто редакторское решение, требующее локального контекста. Его должен принимать человек.
Отсюда родилось первое правило проектирования системы:
Не тратьте инженерный бюджет на автоматизацию оценочных решений, которые человек всё равно обязан перепроверить.
Как массовый запуск сломался по старинке
Следующая проблема оказалась не лингвистической, а механической.
Когда я запустил массовый перевод всех восьми частей серии Health Dashboard, контекстный лимит вывода (output tokens) оказался маловат для длинных текстов. Модель не выбросила ошибку — она просто молча оборвала генерацию на полуслове.
Паттерн повреждений выглядел специфически:
- Исчезали определения сносок (потому что в Markdown они обычно идут в самом конце файла).
- Пропадали финальные разделы длинных статей.
- «Срезались» закрывающие блоки кода.
- В сербский латинский вывод внезапно просачивалась кириллица.
Этот сбой помог сформулировать второе правило:
Перевод не становится валидным только потому, что текст гладко читается. Он валиден только тогда, когда пройдены структурные проверки.
Это заставило меня переписать пайплайн: внедрить чанкинг (деление на куски), защиту плейсхолдеров, логику повторных попыток (retries) и лимиты.
Теперь перед отправкой текста модели скрипт изолирует fenced code blocks, inline-код и ссылки на сноски. Инструмент логирует профили выполнения и имеет жёсткие ограничения вроде TRANSLATION_MAX_MODEL_CALLS и TRANSLATION_MAX_CHUNK_RETRIES. Цикл повторов без лимита — это гарантированный инцидент с биллингом, пусть и с очень красивыми логами.
Quality gate тоже перестал быть субъективным «vibe check». Скрипт жёстко роняет пайплайн, если пропал хотя бы один плейсхолдер, сноска осталась без определения или в сербском тексте обнаружилась кириллица вне разрешённых блоков цитирования.
GPT-4-nano делает экономику скучной
После экспериментов с локальными моделями я попробовал openai/gpt-4.1-nano через OpenRouter.
Первый предпродуктовый запуск охватил почти все статьи (кроме уже опубликованного поста про терминал) сразу на два языка. Результаты получились неидеальными, но крайне информативными:
| Метрика | Результат |
|---|---|
| Всего задач в очереди | 18 |
| Успешно выполнено | 9 |
| Упало или прервано | 9 |
| Фактическая стоимость API | $0.048265 |
| Общее время успешных задач | 30.8 мин |
Самая важная цифра здесь — не копеечная стоимость, хотя она и сняла финансовый вопрос. Важно то, что упавшие задачи ломались уже после перевода, на этапе локального structural QA.
Большинство русских статей прошло без проблем. Сербские же раз за разом спотыкались о валидацию: «Serbian Latin output contains Cyrillic outside code blocks». В одной из русских статей модель потеряла плейсхолдер сноски. Это был сигнал не отказываться от API, а дорабатывать обвязку.
В следующих итерациях появились более строгие инструкции для retry-промптов, автоматическая нормализация сербских символов, точечные повторные попытки для сломанных плейсхолдеров, а также артефакты пречека в _meta/translation-precheck и изолированный debug-вывод, чтобы не мусорить в рабочих папках.
Чистый запуск пайплайна (strict-v2) закрыл все хвосты по сербскому языку:
| Статья | Язык | Статус | Общее время | Самый медленный чанк |
|---|---|---|---|---|
| Часть 2 | RS | done | 72.9 с | section, 11.7 с |
| Часть 3 | RS | done | 66.8 с | footnotes, 11.8 с |
| Часть 5 | RS | done | 80.8 с | footnotes, 12.5 с |
| Часть 6 | RS | done | 112.6 с | footnotes, 18.6 с |
| Часть 7 | RS | done | 133.3 с | section, 33.4 с |
| Часть 8 | RS | done | 220.7 с | section, 72.9 с |
В этот момент выбор конкретной модели окончательно уступил значимости системы, построенной вокруг неё. GPT-nano дешёвая, TranslateGemma — локальная и приватная. Но ни одна из них не избавляет от финальной редактуры. Обе пригодны как движки для черновиков, если обвязка умеет вовремя сообщить, что модель сломала структуру.
Неловкая правда: интерфейс ChatGPT всё ещё пишет лучше
Даже со всей автоматизацией, финальную полировку стиля по-прежнему эффективнее делать руками в веб-интерфейсе ChatGPT. И это не провал проекта, а нормальное разделение труда.
Мой локальный пайплайн пытался усидеть на двух стульях:
- Выдать структурно безопасный и точный Markdown.
- Сформировать готовую к публикации прозу в моём авторском стиле.
Первая задача — чисто инженерная. Вторая — глубоко редакторская.
Когда я пытался сгрузить обе задачи в один автоматический проход, система выдавала плохие компромиссы. Мощные «rewrite-модели» делали предложения живыми, но калечили синтаксис разметки и искажали термины. Модели-переводчики сохраняли структуру, но на выходе получался текст, который читался как добросовестный машинный перевод.
Детерминированный скрипт легко найдёт потерянную сноску. Но он никогда не почувствует, звучит ли заголовок естественно — так, как его написал бы живой человек.
Поэтому оптимальным воркфлоу стала не сквозная автоматизация, а чёткое разделение обязанностей:
Plaintext
English canonical article
└─> draft translation (автоматика)
└─> structural QA (автоматика)
└─> ChatGPT app editorial pass (человек + LLM-редактор)
└─> post-edit structural QA (автоматика)
└─> draft:false + mark-reviewed
В этой схеме приложение ChatGPT выступает в роли стилистического редактора, а репозиторий гарантирует, что после всех правок текст можно безопасно встроить обратно на сайт.
Из чего состоит финальный инструмент
Текущий blog-translator — это не «волшебная кнопка», а строгая инженерная обвязка. Он умеет:
- Генерировать первичные черновики для
index.ru.mdиindex.rs.md. - Экранировать и защищать синтаксис Markdown перед отправкой в LLM.
- Нарезать длинные статьи на чанки и собирать профили по каждому куску.
- Контролировать лимиты вызовов и бюджетировать ретраи.
- Архивировать логи в
_meta/translation-runsи_meta/translation-experiments. - Вести учёт состояний (переведено / проверено / устарело) в файле
_meta/translation-registry.json. - Гонять детерминированные QA-тесты по статьям и страницам серий.
Самый свежий и полезный компонент системы — translation-qa.py. Он умышленно сделан скучным. Скрипт проверяет frontmatter, статус черновика, соответствие ссылок и сносок английскому оригиналу, урлы, code fences, битые теги форматирования, проверяет сербский текст на вшивую кириллицу и запускает git diff --check.
Он также учитывает, что обычные статьи и страницы серий имеют разную структуру:
Bash
python bin/translation-qa.py --vault /path/to/blog --slug part-8-asking-before-telling --lang rs
python bin/translation-qa.py --vault /path/to/blog --kind series --slug health-dashboard
python bin/translation-qa.py --vault /path/to/blog --kind all --expect-draft false
Что бы я точно автоматизировал снова?
Я бы без сомнений автоматизировал генерацию первого черновика. Защиту блоков кода, изоляцию инлайн-тегов, валидацию сносок, ссылок, run-манифесты, подсчёт стоимости и трекинг статусов — всё это критически важно и должно работать на скриптах.
Но я бы больше никогда не пытался заставить одну и ту же модель отвечать за финальный авторский голос.
Урок на будущее простой: используйте LLM там, где они дёшевы, быстры и эффективны, но обязательно стройте вокруг них детерминированные защитные системы (quality gates), заточенные под реальные и неизбежные режимы отказов этих моделей.
Для моего блога это означает, что каноническим источником правды остаётся английский текст. Русский и сербский языки стартуют как автоматические черновики. Код ловит структурные поломки, а мы с ChatGPT наводим финальный лоск в тексте. Затем скрипты перепроверяют результат ещё раз, прежде чем перевод получит статус reviewed.
Это выглядит куда менее «магически», чем сквозной перевод одной кнопкой, о котором я мечтал вначале. Но это первая система, результатам которой я могу доверять.