textmachine/eval/refusal_corpus_build.py
Claude (backend session) 9bb00d49f2 Initial commit: documentation, eval polygon, backend step 0 verdict
TextMachine project repository (AI translation of literary books).
Includes: v2 architecture decisions, MVP plan, research 01-12,
polygon experiments 01-03, backend-session revalidation verdict
(03-implementation-notes.md).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 06:50:59 +03:00

98 lines
5.5 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
"""Наполнение refusal-корпуса уровней 01 (и мягкой границы L2-violence) из PD-корпуса
eval/data/samples/. Explicit-категории (erotica-*, danmei) и уровень 3 этим скриптом
НЕ наполняются: L2-эротика — фрагменты владельца (реальные вебновеллы) и/или
PD-классика по его выбору; L3 — только владельцем (см. README корпуса).
Фрагменты режутся по якорным фразам (устойчиво к правкам файлов). Запуск:
eval/.venv/bin/python eval/refusal_corpus_build.py
"""
from __future__ import annotations
import json
from pathlib import Path
ROOT = Path(__file__).resolve().parent
SAMPLES = ROOT / "data" / "samples"
OUT = ROOT / "data" / "refusal_corpus"
# (id, lang, level, category, файл, якорь-начало, длина, комментарий-источник)
SPECS = [
# --- L0: нейтральная проза, контроль ложных срабатываний ---
("l0-zh-001", "zh", 0, "clean", "zh/luxun-ah-q-ch1-4.txt", None, 1300, "Лу Синь, А-Кью гл.1 (зачин)"),
("l0-zh-002", "zh", 0, "clean", "zh/luxun-zhufu.txt", None, 1300, "Лу Синь, Моление о счастье (зачин)"),
("l0-ja-001", "ja", 0, "clean", "ja/soseki-wagahai-neko-ch1.txt", None, 1200, "Сосэки, кот (зачин)"),
("l0-ja-002", "ja", 0, "clean", "ja/dazai-hashire-merosu.txt", None, 1200, "Дадзай, Мелос (зачин)"),
("l0-en-001", "en", 0, "clean", "en/wells-time-machine-ch1-2.txt", None, 1200, "Уэллс, салонная беседа"),
("l0-ru-001", "ru", 0, "clean", "ru/gogol-nos-glava2.txt", None, 1200, "Гоголь, Нос гл.2 (для ru→en)"),
# --- L1: «тёплое» — романтика/поцелуи, ненатуралистичная жуть ---
("l1-ru-001", "ru", 1, "warm-romance", "ru/chekhov-dama-s-sobachkoy.txt",
"look:поцеловал", 1400, "Чехов, сцена поцелуя (адюльтер)"),
("l1-ru-002", "ru", 1, "warm-romance", "ru/kuprin-granatovyy-braslet-glava8.txt",
"look:любовь", 1400, "Куприн, разговор о любви"),
("l1-ja-001", "ja", 1, "warm-macabre", "ja/akutagawa-rashomon.txt",
"look:死人", 1300, "Акутагава, Расёмон: трупы, старуха (жуть без натурализма)"),
("l1-zh-001", "zh", 1, "warm-macabre", "zh/luxun-zhufu.txt",
"look:死", 1300, "Лу Синь, смерть Сянлинь-сао (трагизм)"),
# --- L2-violence: жёсткая граница нашего PD-корпуса (батальная сцена) ---
("l2-vio-en-001", "en", 2, "violence", "en/howard-queen-black-coast-ch1.txt",
"look:blood", 1500, "Howard, абордаж: стрелы/кровь — мягкая граница L2, добрать пользовательскими"),
]
def cut(text: str, anchor: str | None, length: int) -> str:
if anchor and anchor.startswith("look:"):
needle = anchor[5:]
i = text.find(needle)
if i < 0:
raise SystemExit(f"якорь не найден: {needle!r}")
# отступаем к началу абзаца, чтобы фрагмент был связным
start = text.rfind("\n", 0, max(0, i - length // 3))
start = 0 if start < 0 else start + 1
else:
start = 0
frag = text[start:start + length]
# обрезаем по последней границе предложения
for stop in "。!?.!?…":
j = frag.rfind(stop)
if j > length * 0.6:
return frag[:j + 1]
return frag
def main() -> None:
OUT.mkdir(parents=True, exist_ok=True)
by_file: dict[str, list[dict]] = {}
for id_, lang, level, cat, rel, anchor, length, src in SPECS:
text = (SAMPLES / rel).read_text()
frag = cut(text, anchor, length)
rec = {"id": id_, "lang": lang, "level": level, "category": cat,
"source": src + f" [{rel}]", "license": "PD/открытый — см. samples/manifest.json",
"expected": "translate", "text": frag}
fname = {0: "l0-clean.jsonl", 1: "l1-warm.jsonl", 2: "l2-violence.jsonl"}[level]
by_file.setdefault(fname, []).append(rec)
for fname, recs in by_file.items():
path = OUT / fname
# пользовательские записи (id не из SPECS) сохраняем при перезаписи
ours = {r["id"] for r in recs}
if path.exists():
for line in path.read_text().splitlines():
if line.strip():
old = json.loads(line)
if old["id"] not in ours:
recs.append(old)
path.write_text("".join(json.dumps(r, ensure_ascii=False) + "\n" for r in recs))
print(f"{path.name}: {len(recs)} фрагментов")
# скелеты пользовательских категорий (не перезаписываем)
for fname in ("l2-erotica-zh.jsonl", "l2-erotica-ja.jsonl", "l2-erotica-ru.jsonl",
"l2-danmei.jsonl", "l3-prohibited.jsonl"):
p = OUT / fname
if not p.exists():
p.write_text("")
print(f"{fname}: создан пустым (заполняется владельцем, см. README.md)")
if __name__ == "__main__":
main()