Skip to main content

Я пытался сделать переводчик для блога. В итоге сделал QA-обвязку.

Практический разбор автоматизации переводов блога на русский и сербский: локальные модели, GPT-4.1 nano через API, сломанные сноски, утечки кириллицы и финальное решение — пусть инструменты защищают структуру, а финальный голос остаётся за редактурой.

· 9 мин чтения

Я начал с простого желания: у меня были английские статьи на 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. Модель казалась достаточно крупной для качественного результата и достаточно многоязычной, чтобы это выглядело правдоподобно.

Для русского языка результат часто бывал приемлемым. Для сербского на латинице — категорически нет.

Провалы не были драматичными в привычном понимании бенчмарков. Модель не генерировала откровенную бессмыслицу. Она выдавала текст, который был слишком похож на правильный, и тем самым опасен: славяноподобную прозу с примесью чешского, польского, хорватского или боснийского там, где требовался чистый сербский на латинице. Носитель языка или свободный спикер заметил бы это сразу. Я же не владею сербским настолько совершенно, чтобы надёжно вылавливать такие галлюцинации самостоятельно.

Тогда я попробовал классическую двухэтапную схему:

  1. Использовать OPUS-MT / Marian для сырого перевода.
  2. Взять крупную LLM для финальной полировки стиля.

На бумаге это выглядело красиво: специализированная NMT-модель (нейросетевой перевод) отвечает за грамматику, а LLM наводит лоск и стиль.

На практике граница размылась. На этапе NLLB коверкались названия брендов и ломался inline Markdown. А этап полировки, хоть и выправлял тон, начинал самовольно менять структуру текста. Один запуск добавил фантомный заголовок верхнего уровня, другой — переписал правила пунктуации, которые его эксплицитно просили сохранить. LLM вела себя не как механический редактор, а как вольный писатель.

Это полезно, когда вам нужен соавтор. Но слишком дорого, когда нужна просто точная заплатка.

Первый полезный baseline оказался скучным

Всё изменилось, когда я попробовал TranslateGemma.

Запуск translategemma:12b не выдавал шедевров литературы, но он идеально делал важную рутину: сохранял бренды, не трогал inline-код, удерживал структуру Markdown, не скатывался в «сербский салат» и работал быстро.

Ранний архив экспериментов в _meta/translation-experiments в итоге зафиксировал такие результаты:

ЗапускПайплайнРезультат
Aya 32BОдностадийная локальная LLMПриемлемо для RU, ненадёжно для сербского на латинице.
OPUS-MT -> Aya 32BNMT плюс полировкаВыше качество грамматики, но ломаются бренды и 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 4B29 с47 сБыстро, качество черновика вполне рабочее.
TranslateGemma 12B54 с89 сЛучший баланс качества и скорости по умолчанию.
TranslateGemma 27B536 с~870 сИногда точнее, но неоправданно медленно.
Gemma 3 12B47 с116 сХороший слог, но выше риск сломать форматирование.
Gemma 3 27B521 с~1700 с (на одном RS-блоке)Медленно, улучшения минимальны и нестабильны.
Aya 32B863 с~1880 сМедленно, высокий риск повредить структуру.
NLLB 1.3B2022 с (на коротком тесте)ДисквалифицированаНе справилась с 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) закрыл все хвосты по сербскому языку:

СтатьяЯзыкСтатусОбщее времяСамый медленный чанк
Часть 2RSdone72.9 сsection, 11.7 с
Часть 3RSdone66.8 сfootnotes, 11.8 с
Часть 5RSdone80.8 сfootnotes, 12.5 с
Часть 6RSdone112.6 сfootnotes, 18.6 с
Часть 7RSdone133.3 сsection, 33.4 с
Часть 8RSdone220.7 сsection, 72.9 с

В этот момент выбор конкретной модели окончательно уступил значимости системы, построенной вокруг неё. GPT-nano дешёвая, TranslateGemma — локальная и приватная. Но ни одна из них не избавляет от финальной редактуры. Обе пригодны как движки для черновиков, если обвязка умеет вовремя сообщить, что модель сломала структуру.

Неловкая правда: интерфейс ChatGPT всё ещё пишет лучше

Даже со всей автоматизацией, финальную полировку стиля по-прежнему эффективнее делать руками в веб-интерфейсе ChatGPT. И это не провал проекта, а нормальное разделение труда.

Мой локальный пайплайн пытался усидеть на двух стульях:

  1. Выдать структурно безопасный и точный Markdown.
  2. Сформировать готовую к публикации прозу в моём авторском стиле.

Первая задача — чисто инженерная. Вторая — глубоко редакторская.

Когда я пытался сгрузить обе задачи в один автоматический проход, система выдавала плохие компромиссы. Мощные «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.

Это выглядит куда менее «магически», чем сквозной перевод одной кнопкой, о котором я мечтал вначале. Но это первая система, результатам которой я могу доверять.