textmachine/eval/exp16/banknote.py

89 lines
4.6 KiB
Python
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.

#!/usr/bin/env python3
"""exp16 — banknote-v1 in-band footnote channel: format, instruction, parser (research/20 §B3). miner-v1.
The translator, after the translation, may emit a versioned separator + tab-delimited term lines for
NEW terms only (not in the injected glossary block). Code slices the block off BEFORE any gate/editor
(here: the polygon reads the parsed artifact; the editor never sees the block). Parser is tolerant of a
truncated final line (flags banknote_truncated) and flags malformed residual (banknote_parse_fail).
Separator ⟦TM-BANK-v1⟧ is chosen to (a) not occur in natural text, (b) NOT be the words
'Примечание/Сноска/Комментарий' which the backend sanitizer's trailing-note class reserves (§B3-1).
"""
from __future__ import annotations
import re
SEP = "⟦TM-BANK-v1⟧"
MAX_LINES = 12 # max banknote lines budgeted per chunk (§B3-5 max_tokens sizing)
# instruction appended to the translator system prompt for the banknote (config-b) arm.
BANKNOTE_INSTRUCTION = (
"\n\nПОСЛЕ полного перевода, если в этом фрагменте встретились НОВЫЕ имена собственные, "
"топонимы или уникальные термины мира книги (которых нет в уже данном тебе глоссарии, если он "
"был), добавь В САМОМ КОНЦЕ ответа ОТДЕЛЬНЫЙ блок ровно в таком формате:\n"
f"{SEP}\n"
"исходный_термин<TAB>твой_перевод<TAB>тип\n"
"где тип — одно из: name (имя), place (топоним), title (титул/ранг), term (термин). Каждый термин "
"на своей строке, поля разделены символом табуляции. Не более " + str(MAX_LINES) + " строк, только "
"самые важные новые термины. Если новых терминов нет — НЕ добавляй ни блок, ни разделитель. "
"Сам перевод не должен содержать этот блок внутри себя — только в самом конце."
)
_TYPE_OK = {"name", "place", "title", "term", "nickname"}
def split_banknote(model_output: str):
"""Slice the banknote block off the raw model output. Returns (clean_translation, raw_block_or_'')."""
idx = model_output.find(SEP)
if idx < 0:
return model_output.rstrip(), ""
clean = model_output[:idx].rstrip()
block = model_output[idx + len(SEP):].strip("\n")
return clean, block
def parse_banknote(block: str, truncated_generation: bool = False):
"""Parse the tab-delimited block. Returns (entries, flags).
entries: [{'src','dst','type'}]. flags: {'n_lines','parse_fail','truncated'}."""
entries, bad = [], 0
flags = {"n_banknote_lines": 0, "banknote_parse_fail": False, "banknote_truncated": False}
if not block.strip():
return entries, flags
lines = [ln for ln in block.split("\n") if ln.strip()]
for i, ln in enumerate(lines):
# tolerate the LAST line being truncated by generation length (§B3-5)
parts = re.split(r"\t| {2,}|\s*\|\s*", ln.strip())
parts = [p.strip() for p in parts if p.strip()]
if len(parts) < 2:
if i == len(lines) - 1 and truncated_generation:
flags["banknote_truncated"] = True
continue
bad += 1
continue
src, dst = parts[0], parts[1]
typ = parts[2].lower() if len(parts) >= 3 else "term"
if typ not in _TYPE_OK:
typ = "term"
# src should contain a Han char (this is a zh->ru channel); else malformed
if not re.search(r"[㐀-鿿]", src):
bad += 1
continue
entries.append({"src": src, "dst": dst, "type": typ})
flags["n_banknote_lines"] = len(entries)
if bad > 0:
flags["banknote_parse_fail"] = True
return entries, flags
if __name__ == "__main__":
demo = ("Перевод фрагмента... последнее предложение.\n\n"
f"{SEP}\n方源\tФан Юань\tname\n蛊师\tгу-мастер\ttitle\n青茅山\tгора Цинмао\tplace")
clean, block = split_banknote(demo)
ents, flags = parse_banknote(block)
print("clean tail:", repr(clean[-40:]))
print("entries:", ents)
print("flags:", flags)
# truncated last line
demo2 = f"текст\n{SEP}\n方源\tФан Юань\tname\n"
_, b2 = split_banknote(demo2)
print("\ntruncated:", parse_banknote(b2, truncated_generation=True))