128 lines
8 KiB
Python
128 lines
8 KiB
Python
#!/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"(?<!\*)\*(?!\s)(.+?)(?<!\s)\*(?!\*)", re.S)
|
||
_RE_HR = re.compile(r"^\s*(?:---+|===+|\*\s*\*\s*\*)\s*$", re.M)
|
||
# Реплика, целиком взятая в кавычки на отдельной строке: «…» или "…" в начале абзаца.
|
||
_RE_QUOTED_LINE = re.compile(r'^[ \t]*[«"„]\s*(.+?)\s*[»"“]([ \t]*[,.—-].*)?$', re.M)
|
||
_RE_MULTI_BLANK = re.compile(r"\n{3,}")
|
||
|
||
|
||
def strip_service(text: str) -> 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__)
|