textmachine/eval/dovodka/material_ja.py

173 lines
10 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
"""Д6 — МАТЕРИАЛ ПАРЫ ja→ru: гардрейл, гейт прав, отбор единиц. $0.
Источник: `~/books/isekai_majutsushi_jp.txt` (файл владельца, вне git — правило Р8). Площадка
`novel18.syosetu.com`, то есть срез ЗАВЕДОМО взрослый; аннотация книги сама называет 洗脳 и 暴力.
⚠⚠ ГАРДРЕЙЛ ПЕРВЫМ, ДО ЛЮБОГО ДРУГОГО ДЕЙСТВИЯ. Гардрейл владельца (вынесен в `eval/README.md`):
**18+ уровень 3 — несовершеннолетние в сексуальном контексте — не обрабатывать и не анализировать
ни в каком виде.** Здесь он реализован механически: чанк, где ПЛОТНО совпадают маркеры возраста и
явные сексуальные маркеры, помечается и материал целиком уходит в СТОП с пингом владельцу. Ни один
такой чанк не печатается и дальше по конвейеру не идёт — печатается только его номер и счёт
маркеров. Детектор существует, чтобы ИСКЛЮЧАТЬ, и его срабатывание — не повод читать внимательнее.
Дальше — та же лестница, что у эксп-22 §0.2, и по тем же причинам:
1. гейт ПРАВ (маршрутизация по договорам провайдеров, не по поведению модели): у `deepseek` и
`zai` класс `sexually-explicit` — FORBIDDEN, поэтому верхний дециль по плотности эротических
маркеров снимается ДО отбора единиц;
2. отбор единиц детерминированный: дедуп по подписи, ключ артефакта — хеш источника, сортировка
по (длина, uid), из середины распределения длин;
3. манифест: единицы прикалываются к файлу, иначе докачка материала молча сдвинет уже
купленный замер (ровно это случилось с эксп-22 и чинилось в паке 23).
Запуск: eval/.venv/bin/python eval/dovodka/material_ja.py [--units]
"""
from __future__ import annotations
import hashlib
import json
import re
import sys
from pathlib import Path
SRC = Path.home() / "books" / "isekai_majutsushi_jp.txt"
OUT = Path.home() / "books" / "dovodka"
MANIFEST = OUT / "manifest-ja.json"
TARGET_CHARS, MIN_CHARS, N_UNITS = 1100, 700, 9 # ja плотнее zh на знак: цель короче
# ── маркеры гардрейла ─────────────────────────────────────────────────────────────────────────
AGE = re.compile(r"(小学生|中学生|高校生|幼女|幼児|少女|少年|児童|子供|子ども|ロリ|"
r"[0-9-9一二三四五六七八九十]{1,2}\s*[歳才])")
SEX = re.compile(r"(性行為|セックス|挿入|射精|愛撫|陰茎|陰部|膣|乳房|裸体|全裸|絶頂|喘|"
r"犯[すされし]|強姦|レイプ|淫|媚薬|発情)")
# Эротическая плотность для ГЕЙТА ПРАВ — шире, чем гардрейл: это про договоры провайдеров.
EROTIC = re.compile(r"(キス|口づけ|抱[きく]しめ|裸|胸|尻|太もも|下着|色気|艶|情事|房事|"
r"性的|欲情|快感|喘|淫|媚薬|発情|愛撫|挿入|射精|セックス|性行為)")
def text() -> str:
if not SRC.exists():
raise SystemExit(f"⛔ материала нет: {SRC}")
return SRC.read_text(encoding="utf-8", errors="replace")
def chapters() -> list[dict]:
"""Главы по разделителю площадки; заголовок остаётся в теле — он часть материала."""
raw = text()
parts = re.split(r"\n=+\n", raw)
out = []
for i, p in enumerate(parts):
p = p.strip()
if len(p) >= MIN_CHARS:
out.append(dict(idx=i, text=p))
return out
def chunks() -> list[dict]:
"""Абзацные чанки; абзац не рвётся, хвост главы прирастает к предыдущему (правило эксп-22)."""
out = []
for ch in chapters():
made, buf, size = [], [], 0
for para in ch["text"].split("\n"):
para = para.strip()
if not para:
continue
buf.append(para)
size += len(para)
if size >= TARGET_CHARS:
made.append(dict(text="\n".join(buf), chapter=ch["idx"]))
buf, size = [], 0
if size:
tail = "\n".join(buf)
if size >= MIN_CHARS or not made:
made.append(dict(text=tail, chapter=ch["idx"]))
else:
made[-1]["text"] += "\n" + tail
out += made
return out
def uid_of(s: str) -> str:
return hashlib.sha1(s.encode("utf-8")).hexdigest()[:10] # noqa: S324
def guardrail(cs: list[dict]) -> list[int]:
"""Индексы чанков КЛАССА, КОТОРЫЙ НЕ ОБРАБАТЫВАЕТСЯ. Содержимое не печатается никогда."""
hits = []
for i, c in enumerate(cs):
t = c["text"]
a, s = len(AGE.findall(t)), len(SEX.findall(t))
if a and s and (a + s) >= 4:
hits.append(i)
return hits
def main() -> int:
cs = chunks()
print(f"Д6 МАТЕРИАЛ ja→ru · источник {SRC.name} · знаков {len(text())} · чанков {len(cs)}")
print("\n=== ГАРДРЕЙЛ 18+ УРОВНЯ 3 (проверяется ПЕРВЫМ) ===")
bad = guardrail(cs)
if bad:
print(f"⛔ СТОП. Чанков с плотным совпадением возрастных и явных сексуальных маркеров: "
f"{len(bad)} из {len(cs)} (номера: {bad[:12]}{'' if len(bad) > 12 else ''}).")
print(" Содержимое НЕ печатается и дальше не идёт. Материал непригоден по гардрейлу\n"
" владельца; замена без его слова НЕ ищется (промт: «непригоден по пробе → СТОП\n"
" и пинг»).")
if "--owner-sanctioned-exclude" not in sys.argv:
print("\n Детектор КОНСЕРВАТИВЕН намеренно: 少年/少女/子供 — частотные слова японской\n"
" прозы, и совпасть с эротическим маркером они могут безобидно. Развести это\n"
" можно только ЧТЕНИЕМ помеченных мест, а читать их правило запрещает — значит\n"
" решает не сессия. Обход существует и требует слова владельца:\n"
" --owner-sanctioned-exclude помеченные чанки выбрасываются механически,\n"
" остальные 181 идут в отбор; в отчёт пишется,\n"
" сколько и почему выброшено.\n"
" Прекращаю обработку источника.")
return 1
print(f"\n ⚠ ОБХОД ПО СЛОВУ ВЛАДЕЛЬЦА: {len(bad)} помеченных чанков выброшены "
"механически, без чтения.")
cs = [c for i, c in enumerate(cs) if i not in set(bad)]
print(f" осталось чанков: {len(cs)}")
else:
print(f"[OK ] срабатываний нет: 0 из {len(cs)} чанков")
print("\n=== ГЕЙТ ПРАВ (верхний дециль эротической плотности снимается ДО отбора) ===")
dens = sorted(((len(EROTIC.findall(c["text"])) / max(1, len(c["text"])) * 1000, i)
for i, c in enumerate(cs)), reverse=True)
cut = max(1, len(cs) // 10)
dropped = {i for _, i in dens[:cut]}
print(f"снято чанков {len(dropped)} из {len(cs)} (дециль); плотность верхнего "
f"{dens[0][0]:.2f}/1000 знаков, порога дециля {dens[cut - 1][0]:.2f}, медиана "
f"{dens[len(dens) // 2][0]:.2f}")
pool = [c for i, c in enumerate(cs) if i not in dropped]
print("\n=== ОТБОР ЕДИНИЦ (детерминированный, методика эксп-21 §0) ===")
uniq: dict[str, dict] = {}
for c in pool:
u = uid_of(c["text"])
uniq.setdefault(u, dict(uid=u, source=c["text"], chapter=c["chapter"]))
ordered = sorted(uniq.values(), key=lambda x: (len(x["source"]), x["uid"]))
mid = len(ordered) // 2
lo = max(0, mid - N_UNITS // 2)
units = ordered[lo:lo + N_UNITS]
ls = [len(u["source"]) for u in units]
print(f"пул {len(ordered)} уникальных · выбрано {len(units)} из середины распределения длин")
print(f"знаков {min(ls)}{max(ls)}, медиана {sorted(ls)[len(ls) // 2]} · "
f"глав {len({u['chapter'] for u in units})}")
for u in units:
print(f" {u['uid']} гл.{u['chapter']:<4d} знаков {len(u['source']):5d}")
if "--units" in sys.argv:
OUT.mkdir(parents=True, exist_ok=True)
MANIFEST.write_text(json.dumps(
dict(source=SRC.name, n_chunks=len(cs), dropped_rights=len(dropped),
ja=dict(uids=[u["uid"] for u in units])), ensure_ascii=False, indent=1),
encoding="utf-8")
print(f"\nманифест → {MANIFEST} (единицы приколоты; докачка их больше не сдвинет)")
else:
print("\n(манифест НЕ записан — прогнать с флагом --units, когда состав утверждён)")
return 0
if __name__ == "__main__":
sys.exit(main())