#!/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" "исходный_терминтвой_переводтип\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))