Files
vesti/openspec/changes/post-translate/proposal.md
T
kpa39l 4a166fa9d6 feat(web): перевод поста на русский локальной моделью (openspec post-translate)
Кнопка «🌐 Перевести» в карточках кандидатов и отобранных переводит пост
локальной моделью (qwen3:8b-nothink), результат в posts.translated_text
(идемпотентный ALTER при старте). Метка «Оригинальный пост» стала закладкой,
при готовом переводе рядом появляется закладка «Перевод» (активна по умолчанию),
переключение на vanilla JS. Повторный клик не тратит токены: перевод в БД.
call_ollama из classifier не подошла (парсит ответ как JSON) — прямой запрос
к Ollama /v1/chat/completions. POST /posts/{id}/translate, лимит 6000 символов.
2026-09-18 05:40:59 +00:00

4.2 KiB
Raw Blame History

post-translate

Перевод поста на русский локальной моделью (кнопка «Перевести» в карточках кандидатов и отобранных).

Why

Многие посты в кандидатах приходят на английском (LWN, bleepingcomputer, omarchy и т.п.). Чтобы прочитать и отобрать новость, модератору нужен перевод на русский прямо в карточке. Сейчас поста нет: есть только оригинал на языке источника.

Ограничение: перевод должен быть ПО ЗАПРОСУ (кнопка), потому что массовый перевод всех кандидатов будет жечь токены локальной модели без пользы (модератор смотрит далеко не все посты). Это соответствует существующему паттерну: классификация и пересказ тоже запускаются только по кнопке.

What Changes

  • БД: колонка posts.translated_text TEXT (через ALTER TABLE при старте, идемпотентно, как это делает classifier/classify.py для direction/summary).
  • web/app.py:
    • helper _translate_text(text) → переводит текст через qwen3:8b-nothink (Ollama /v1, та же call_ollama, что у rewrite/reclassify). Текст обрезается до 6000 символов (перевод длинного текста + ответ упирается в контекст локальной модели быстрее, чем классификация). Возвращает str, не падает — при ошибке кидает исключение.
    • POST /posts/{post_id}/translate (src, group_by, direction, own, q — как у rewrite): читает пост, переводит, сохраняет в translated_text БД, редирект обратно с selected=post_id. Если translated_text уже есть — не тратит токены, сразу редирект (повторный клик — бесплатный).
    • render candidates/selected — уже передаёт selected (sqlite row), колонка подтянется автоматически (SELECT *).
  • web/templates/candidates.html и selected.html:
    • Заголовок «Оригинальный пост» → становится кликабельной закладкой/аккордеоном (bootstrap collapse, без JS-файлов — как остальной проект).
    • Рядом с ним кнопка «Перевести» — POST-форма на /posts/{id}/translate.
    • Если translated_text есть — вторая закладка «Перевод» с текстом перевода.
    • Активная закладка по умолчанию: «Перевод», если перевод уже есть, иначе «Оригинал».

Why Not

  • Не переводим автоматически при крауле — жечь токены на все посты (включая неотобранные).
  • Не делаем отдельную страницу/ход в БД «переводы» — достаточно одной колонки, перевод — атрибут поста, а не сущность.
  • Не используем внешний API перевода (DeepL и т.п.) — локальная модель уже есть, бесплатна и подходит для новостного текста.

Impact

  • Веб-морда: в карточках кандидатов и отобранных появляется перевод на русский по кнопке.
  • Рестарт vesti-web.service после выката (колонка добавляется идемпотентно).
  • Никаких изменений в pipeline/publisher/краулере — перевод чисто для модерации, не для публикации.