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

50 lines
4.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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/краулере — перевод чисто для модерации,
не для публикации.