89 lines
4.6 KiB
Python
89 lines
4.6 KiB
Python
#!/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))
|