#!/usr/bin/env python3 """ДЕТЕРМИНИРОВАННЫЙ ПОСТ-ПРОЦЕССОР ЧЕРНОВИКА. $0 по ключам провайдеров. Зачем. Оба слепых читателя независимо назвали главным источником непечатного текста НЕ перевод, а механический слой: прямая речь в кавычках вместо тире, служебная преамбула модели, протёкшая markdown-разметка, авторский текст под тире. Если дешёвая детерминированная правка поверх черновика закрывает существенную часть разрыва «черновик против редактора» (+1.88), то второй проход покупает меньше, чем кажется, и часть запланированных покупок уточняет цену того, что не нужно. Проверка стоит только голосов судей — сам пост-процессор бесплатен. Правила закрытые и проверяемые. Каждое чинит класс, названный читателями ЦИТАТОЙ, и ни одно не трогает содержание перевода: пост-процессор не переставляет слова, не сливает абзацы и не правит лексику. Он снимает ровно то, что является браком вывода, а не браком перевода. ⚠ Чего он НЕ делает и почему: слипшиеся абзацы не разделяются (границу хода детерминированно не установить, а неверная разбивка хуже слипшейся), непереведённые остатки не переводятся (это работа модели), калька не правится (это суждение). Запуск: --selfcheck проверка правил на входах с известным ответом, $0 --emit породить арм F_post из черновиков, $0 """ from __future__ import annotations import re import sys from pathlib import Path REPO = Path("/home/ubuntu/projects/textmachine") sys.path.insert(0, str(REPO / "eval" / "role_topology")) # Служебная преамбула модели перед текстом. Поймана слепым чтением дословно: # «Вот перевод фрагмента на русский язык в соответствии с вашими требованиями:». _RE_PREAMBLE = re.compile( r"^\s*(?:вот\s+)?(?:перевод|отредактированн\w+\s+текст|результат)[^\n:]{0,80}:\s*\n+", re.I) _RE_MD_HEAD = re.compile(r"^\s{0,3}#{1,6}\s+", re.M) # ### Раздел 17 _RE_MD_BOLD = re.compile(r"\*\*(.+?)\*\*", re.S) # **Глава 19** _RE_MD_ITAL = re.compile(r"(? str: """Убрать служебную преамбулу и markdown-разметку, протёкшую в тело книги.""" t = _RE_PREAMBLE.sub("", text) t = _RE_MD_HEAD.sub("", t) t = _RE_MD_BOLD.sub(r"\1", t) t = _RE_MD_ITAL.sub(r"\1", t) t = _RE_HR.sub("", t) return _RE_MULTI_BLANK.sub("\n\n", t).strip() def dialogue_dash(text: str) -> str: """Реплику, стоящую отдельным абзацем в кавычках, оформить русским тире. ⚠ Правило намеренно узкое: трогается только строка, которая ЦЕЛИКОМ является закавыченной репликой (возможно со словами автора после закрывающей кавычки). Кавычки ВНУТРИ абзаца не трогаются — там они могут быть цитатой, названием или мыслью, и различить это детерминированно нельзя. Узкое правило чинит меньше, но не портит. """ def repl(m: re.Match) -> str: body, tail = m.group(1), (m.group(2) or "") if len(body) < 2: return m.group(0) return f"— {body}{tail}" return _RE_QUOTED_LINE.sub(repl, text) def process(text: str) -> str: return dialogue_dash(strip_service(text)) CASES: list[tuple[str, str, str]] = [ ("служебная преамбула", "Вот перевод фрагмента на русский язык в соответствии с вашими требованиями:\n\nОн вышел.", "Он вышел."), ("markdown-заголовок", "### Раздел семнадцатый\n\nОн вышел.", "Раздел семнадцатый\n\nОн вышел."), ("markdown-жирный", "**Глава 19: Цикада!**\n\nТекст.", "Глава 19: Цикада!\n\nТекст."), ("реплика в ёлочках", '«Время летит. Садись», — сказал он.', "— Время летит. Садись, — сказал он."), ("реплика в лапках", '"Спасибо", — ответил племянник.', "— Спасибо, — ответил племянник."), ("кавычки ВНУТРИ абзаца не трогаются", 'Он вспомнил «Тысячу и одну ночь» и усмехнулся.', 'Он вспомнил «Тысячу и одну ночь» и усмехнулся.'), ("уже тире — не трогается", "— Время летит. Садись, — сказал он.", "— Время летит. Садись, — сказал он."), ("чистый текст не меняется", "Он вышел за дверь. Небо было серым.", "Он вышел за дверь. Небо было серым."), ] def selfcheck() -> int: bad = 0 for name, src, want in CASES: got = process(src) ok = got == want bad += not ok print(f"[{'OK ' if ok else 'ПРОВАЛ'}] {name}" + ("" if ok else f"\n получено: {got!r}\n ждали: {want!r}")) print(f"\n{'ПРАВИЛА ЧИСТЫ' if not bad else f'ПРОВАЛОВ: {bad}'} ({len(CASES) - bad}/{len(CASES)})") return bad def emit() -> None: """Породить выходы арма F_post: пост-процессор поверх ТОГО ЖЕ черновика, что правят армы.""" import json # noqa: PLC0415 import bakeoff as BO # noqa: PLC0415 OUT = Path.home() / "books" / "role-topology" changed = 0 for u in BO.units(): src = BO.text_of("F", u) dst = process(src) (OUT / f"bo2-F_post-{u['uid']}.json").write_text( json.dumps(dict(tag=f"bo2-F_post-{u['uid']}", arm="F_post", uid=u["uid"], model="$0-postproc", cost_usd=0.0, content=dst, changed=dst != src, delta_chars=len(dst) - len(src)), ensure_ascii=False), encoding="utf-8") changed += dst != src print(f"единиц {len(BO.units())} · пост-процессор изменил {changed}") if __name__ == "__main__": a = sys.argv[1:] or ["--selfcheck"] if a[0] == "--selfcheck": sys.exit(1 if selfcheck() else 0) elif a[0] == "--emit": emit() else: print(__doc__)