textmachine/eval/bank_arbitration/crosslang_bank.py

182 lines
8.1 KiB
Python
Raw Permalink 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
"""§F: кросс-пары. F1 — zh→en терминолог-проход по голд-ростеру (близость стратегий к ru-банку
владельца). F2 — en→zh на Empire of the Dawn (Кристофф): майнинг термов + терминолог-проход + перевод
двух окон (deepseek-low и glm-5). Сырьё → ~/books/gu-zhenren/bank-arbitration/ (общий durable пака).
"""
from __future__ import annotations
import argparse
import collections
import json
import os
import re
import time
import zipfile
from pathlib import Path
from dotenv import load_dotenv
from openai import OpenAI
REPO = Path("/home/ubuntu/projects/textmachine")
HERE = Path(__file__).resolve().parent
RAW = Path.home() / "books" / "gu-zhenren" / "bank-arbitration"
KRISTOFF = Path.home() / "books" / "Kristoff_Jay_-_Empire_of_the_Vampire_3_-_Empire_of_the_Dawn.epub"
load_dotenv(REPO / "eval" / ".env")
import sys # noqa: E402
sys.path.insert(0, str(HERE))
import consilium_probe as cp # noqa: E402 (ростер, батчер, call, цены, клиенты)
TERM_EN_SYS = """You are the terminologist of a publishing-grade translation from Chinese into English.
Book: «蛊真人» (a cultivation webnovel). You are given the book's term list; for each term — contexts
from the SOURCE and renderings the draft translators already proposed (in Russian — a parallel edition;
treat them as evidence of meaning, not as English candidates). You see the whole book at once; pick ONE
English rendering per term, consistent across the whole book. Transcribe personal and place names in
pinyin; translate titles, ranks and realia by MEANING. Keep families of related terms sharing a source
morpheme consistent in English. Dictionary form, no explanations.
Answer format: one line per term, exactly two fields separated by a TAB: source term, English rendering."""
TERM_ZH_SYS = """你是一部出版级奇幻小说英译中项目的术语专家。小说:《Empire of the Dawn》(Jay Kristoff,
吸血鬼史诗奇幻)。给你这本书的术语表:每个术语附有原文语境。你能看到整本书,请为每个术语选定ONE个
统一的中文译名,全书一致。人名用约定俗成的音译;头衔、组织、事物按意义翻译,要求文学性、符合奇幻
小说的中文语感。同源术语的译名要保持词根呼应。只给词典形式,不要解释。
回答格式:每行一个术语,两个字段,用制表符分隔:原文术语、中文译名。"""
TRANSLATE_ZH_SYS = """你是一位专业文学翻译家,把英文小说译成中文。这是 Jay Kristoff 的吸血鬼史诗奇幻
《Empire of the Dawn》。要求:逐句翻译,不删不增;中文要有文学性,句式自然,避免翻译腔;对话用中文
小说的标点习惯。人名音译。只输出译文,不要任何前言、注释或 markdown。"""
def epub_text() -> str:
out = []
with zipfile.ZipFile(KRISTOFF) as z:
for n in sorted(z.namelist()):
if n.endswith((".xhtml", ".html")):
html = z.read(n).decode("utf-8", "ignore")
txt = re.sub(r"<[^>]+>", " ", html)
txt = re.sub(r"&[a-z]+;", " ", txt)
out.append(re.sub(r"\s+", " ", txt))
return "\n".join(out)
def mine_en_terms(text: str, top=14):
"""Рекуррентные капитализированные термы: имена + двусловные (The X / X Y)."""
stop = {"The", "But", "And", "She", "His", "Her", "They", "That", "This", "What", "When", "With",
"For", "Not", "You", "All", "Was", "Had", "Chapter", "Then", "Now", "There", "One", "It's",
"Book", "Part", "From", "Like", "Still", "Yet", "So", "In", "On", "At", "He", "We", "If",
"God", "Oui", "Non", "Mademoiselle", "Monsieur"}
words = collections.Counter(re.findall(r"\b[A-Z][a-zà-ÿé]+(?:\s[A-Z][a-zà-ÿé]+)?\b", text))
cand = [(w, n) for w, n in words.most_common(400)
if n >= 15 and w.split()[0] not in stop and len(w) > 3]
# предпочесть двусловные и явные имена/реалии
seen, out = set(), []
for w, n in cand:
head = w.split()[0]
if head in seen:
continue
seen.add(head)
out.append((w, n))
if len(out) == top:
break
return out
def kwic(text, term, per=3, width=60):
out, frm = [], 0
while len(out) < per:
i = text.find(term, frm)
if i < 0:
break
out.append(text[max(0, i - width):i + len(term) + width])
frm = i + len(term)
return out
def run_f1(models=("deepseek-v4-flash",)):
roster = cp.build_roster()
batches = cp.batches_of(roster)
total = 0.0
for model in models:
cl = cp.client_for(model)
short = {"deepseek-v4-flash": "ds", "glm-5": "glm"}[model]
for i, b in enumerate(batches):
user = "Термины книги:\n\n" + "\n\n".join(cp.block_of(c) for c in b)
rec = cp.call(cl, model, TERM_EN_SYS, user, f"xen-{short}-b{i}",
"low" if model.startswith("deepseek") else None, model == "glm-5")
total += rec["cost_usd"]
time.sleep(1)
return total
# F2-ростер: майнер дал имена; реалии добраны РУКАМИ по частотам (раскрыто в отчёте). Семья
# *-blood (coldblood/highblood/paleblood) — намеренный тест корневой связности в zh.
TERMS_F2 = ["Dior", "Gabriel", "Phoebe", "Celene", "Voss", "silversaint", "coldblood", "highblood",
"paleblood", "Forever King", "Grail", "San Michon", "aegis", "duskdancer", "famille"]
def run_f2():
text = epub_text()
print(f"epub text: {len(text)} chars")
terms = [(t, text.count(t)) for t in TERMS_F2]
print("terms:", terms)
blocks = []
for t, n in terms:
blocks.append(f"### {t}\nfreq: {n}\n" + "\n".join(f"ctx: {c}" for c in kwic(text, t)))
user = "Terms:\n\n" + "\n\n".join(blocks)
total = 0.0
for model, short in [("deepseek-v4-flash", "ds"), ("glm-5", "glm")]:
cl = cp.client_for(model)
rec = cp.call(cl, model, TERM_ZH_SYS, user, f"xzh-term-{short}",
"low" if model.startswith("deepseek") else None, model == "glm-5")
total += rec["cost_usd"]
time.sleep(1)
# два окна прозы (~900 симв.), разнесённые
paras = [p for p in text.split("\n") if len(p) > 400]
wins = [paras[3][:1200], paras[len(paras) // 2][:1200]]
for k, w in enumerate(wins):
for model, short in [("deepseek-v4-flash", "ds"), ("glm-5", "glm")]:
cl = cp.client_for(model)
rec = cp.call(cl, model, TRANSLATE_ZH_SYS, "翻译下面的段落:\n\n" + w, f"xzh-prose-w{k}-{short}",
"low" if model.startswith("deepseek") else None, model == "glm-5")
total += rec["cost_usd"]
time.sleep(1)
return total
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--part", default="all", choices=["f1", "f2", "all", "mine"])
a = ap.parse_args()
if a.part == "mine":
print(mine_en_terms(epub_text()))
return
total = 0.0
if a.part in ("f1", "all"):
total += run_f1()
if a.part in ("f2", "all"):
total += run_f2()
print(f"TOTAL crosslang: ${total:.6f}")
if __name__ == "__main__":
main()
def score_f1():
"""Джойн en-консолидаций с голдом: печать пар (src, ru владельца, en свежий) для таблицы §F."""
import glob
en = {}
for f in sorted(glob.glob(str(RAW / "xen-ds-b*.json"))):
for ln in json.load(open(f, encoding="utf-8"))["content"].split("\n"):
if "\t" in ln:
s, d = ln.split("\t", 1)
en[s.strip()] = d.strip()
gold = [json.loads(l) for l in open(HERE / "gold" / "gold.jsonl", encoding="utf-8")]
n = 0
for r in gold:
if not r.get("in_bank"):
continue
s = r["bank_src"]
if s in en:
n += 1
print(f"{s}\t{r['gold_dst']}\t{en[s]}")
print(f"# joined: {n}")