Fix the contamination scorer to accept Russian renderings of names and titles, and add a refill mode for cells truncated by the reasoning budget
This commit is contained in:
parent
b9f5ed0e3f
commit
ac16170ec4
1 changed files with 76 additions and 11 deletions
|
|
@ -36,6 +36,8 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import importlib.util
|
import importlib.util
|
||||||
|
import json
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
@ -74,6 +76,12 @@ if MONEY.M22.BUY.OUT != MONEY.OUT: # ассерт,
|
||||||
# получала 0–46 знаков. Ответ здесь нужен в две строки, думать модель будет много — кап должен
|
# получала 0–46 знаков. Ответ здесь нужен в две строки, думать модель будет много — кап должен
|
||||||
# вмещать размышление ЦЕЛИКОМ, иначе пустота будет означать «не успел сказать», а не «не знаю».
|
# вмещать размышление ЦЕЛИКОМ, иначе пустота будет означать «не успел сказать», а не «не знаю».
|
||||||
MAX_OUT = 32000
|
MAX_OUT = 32000
|
||||||
|
# ⚠ Кап ДО-КУПКИ усечённых клеток. У `deepseek-v4-pro` две клетки английской книги вернулись
|
||||||
|
# пустыми с `finish=length`: размышление съело все 32000 и на две строки ответа не осталось.
|
||||||
|
# Пустая клетка — не «книга чиста», а ОТСУТСТВИЕ данных (правило шапки), и решение о покупке
|
||||||
|
# несущей оси по 3 клеткам из 5 принимать нельзя. Усечённые уводятся в карантин `.LENGTH.json`
|
||||||
|
# (деньги остаются видимы кассе) и покупаются заново с этим капом. Девиация объявлена.
|
||||||
|
MAX_OUT_REFILL = 60000
|
||||||
# Глушить thinking у DeepSeek ЗАПРЕЩЕНО (эхо-мина, гардрейл проекта). Конфигурацию на модель
|
# Глушить thinking у DeepSeek ЗАПРЕЩЕНО (эхо-мина, гардрейл проекта). Конфигурацию на модель
|
||||||
# даёт `roster.call_kwargs` — своих `reasoning_effort` здесь не ставим НИКОГДА.
|
# даёт `roster.call_kwargs` — своих `reasoning_effort` здесь не ставим НИКОГДА.
|
||||||
SYS = ("Тебе дают отрывок художественного текста, в котором одно имя собственное заменено на "
|
SYS = ("Тебе дают отрывок художественного текста, в котором одно имя собственное заменено на "
|
||||||
|
|
@ -88,15 +96,17 @@ RESIDENTS = ("deepseek-v4-flash", "deepseek-v4-pro")
|
||||||
# Книги, которые фаза Д покупает, плюс обязательный положительный контроль.
|
# Книги, которые фаза Д покупает, плюс обязательный положительный контроль.
|
||||||
BOOKS_D = {
|
BOOKS_D = {
|
||||||
"enkan-ja": dict(path=Path.home() / "books" / "enkan_no_hate_ja.txt", lang="ja",
|
"enkan-ja": dict(path=Path.home() / "books" / "enkan_no_hate_ja.txt", lang="ja",
|
||||||
titles=["円環の果てに", "enkan", "円環"],
|
titles=["円環の果てに", "enkan", "円環", "круг", "предел круга"],
|
||||||
note="ja-ось Д6; первая публикация 2026-07-30, 18 глав — НЕ мерена ни разу"),
|
note="ja-ось Д6; первая публикация 2026-07-30, 18 глав — НЕ мерена ни разу"),
|
||||||
"kristoff-en": dict(path=Path.home() / "books" /
|
"kristoff-en": dict(path=Path.home() / "books" /
|
||||||
"Kristoff_Jay_-_Empire_of_the_Vampire_3_-_Empire_of_the_Dawn.epub",
|
"Kristoff_Jay_-_Empire_of_the_Vampire_3_-_Empire_of_the_Dawn.epub",
|
||||||
lang="en", titles=["empire of the dawn", "empire of the vampire",
|
lang="en", titles=["empire of the dawn", "empire of the vampire",
|
||||||
"кристофф", "kristoff"],
|
"кристофф", "kristoff", "империя вампиров",
|
||||||
|
"империя рассвета", "империя зари"],
|
||||||
note="en-ось Д3; luna узнала СЕРИЮ 1 раз из 5 — жильцами не мерена"),
|
note="en-ось Д3; luna узнала СЕРИЮ 1 раз из 5 — жильцами не мерена"),
|
||||||
"jinpingmei": dict(path=Path.home() / "books" / "jinpingmei" / "jinpingmei-ctext-zhs.txt",
|
"jinpingmei": dict(path=Path.home() / "books" / "jinpingmei" / "jinpingmei-ctext-zhs.txt",
|
||||||
lang="zh", titles=["金瓶梅", "золотой лотос", "jin ping mei"],
|
lang="zh", titles=["金瓶梅", "золотой лотос", "jin ping mei", "цзинь пин мэй",
|
||||||
|
"цветы сливы", "ланьлин", "насмешник"],
|
||||||
note="ПОЛОЖИТЕЛЬНЫЙ КОНТРОЛЬ — обязателен на каждом жильце"),
|
note="ПОЛОЖИТЕЛЬНЫЙ КОНТРОЛЬ — обязателен на каждом жильце"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -104,10 +114,60 @@ BOOKS_D = {
|
||||||
# Книга → фаза кассы. Положительный контроль относим к ФД-B: он покупается один раз, а прогон
|
# Книга → фаза кассы. Положительный контроль относим к ФД-B: он покупается один раз, а прогон
|
||||||
# Д6 возьмёт те же теги из кэша за $0.
|
# Д6 возьмёт те же теги из кэша за $0.
|
||||||
PHASE = {"kristoff-en": "ФД-B", "jinpingmei": "ФД-B", "enkan-ja": "ФД-C"}
|
PHASE = {"kristoff-en": "ФД-B", "jinpingmei": "ФД-B", "enkan-ja": "ФД-C"}
|
||||||
|
|
||||||
|
# ⚠ СЧЁТЧИК СВЕРЯЛ НЕ ТО. Найдено ЧТЕНИЕМ всех 30 ответов 14.08 — прибор печатал числа, которые
|
||||||
|
# не соответствуют содержимому клеток. Причина: сверка шла подстрокой со списком названий на
|
||||||
|
# языке оригинала, а модели отвечают ПО-РУССКИ. Цена ошибки была двусторонней и обе стороны
|
||||||
|
# опасны: контроль pro на «Цзинь пин мэй» — 5 верных ответов из 5 — печатался как 1/5 (то есть
|
||||||
|
# проба выглядела бы неработающей и аннулировала бы жильца), а на английской книге ТОЧНОЕ
|
||||||
|
# попадание «Империя вампиров, Джей Кристофф» + «Габриэль де Леон» при вырезанном `Gabriel`
|
||||||
|
# печаталось как клоуз-промах, потому что `Gabriel` не подстрока «Габриэль». Второе страшнее:
|
||||||
|
# так контаминация несущей оси спряталась бы под нулём.
|
||||||
|
# ПОРОГИ НЕ ТРОНУТЫ (узнавание ≥3/5, клоуз ≥2/5) — чинится только сопоставление.
|
||||||
|
_RU2LAT = {"щ": "sch", "ш": "sh", "ч": "ch", "ж": "zh", "ю": "yu", "я": "ya", "ё": "yo",
|
||||||
|
"э": "e", "х": "h", "ц": "c", "а": "a", "б": "b", "в": "v", "г": "g", "д": "d",
|
||||||
|
"е": "e", "з": "z", "и": "i", "й": "y", "к": "k", "л": "l", "м": "m", "н": "n",
|
||||||
|
"о": "o", "п": "p", "р": "r", "с": "s", "т": "t", "у": "u", "ф": "f", "ы": "y",
|
||||||
|
"ь": "", "ъ": ""}
|
||||||
|
|
||||||
|
|
||||||
|
def _lat(s: str) -> str:
|
||||||
|
return "".join(_RU2LAT.get(c, c) for c in s.lower())
|
||||||
|
|
||||||
|
|
||||||
|
def same_name(gold: str, ans: str) -> bool:
|
||||||
|
"""Назвал ли ответ то же имя, что вырезано. Терпит русскую передачу латинского имени.
|
||||||
|
|
||||||
|
⚠ Не «похожие строки», а именно передача: сравниваются ЛАТИНСКИЕ скелеты, и порог сходства
|
||||||
|
держится высоким, чтобы «Габриэль»≈`Gabriel` проходило, а «Габриэль»≈`Gabrio` — нет.
|
||||||
|
"""
|
||||||
|
import difflib # noqa: PLC0415
|
||||||
|
g, a = gold.strip().lower(), (ans or "").strip().lower()
|
||||||
|
if not g or not a or _is_refusal(a):
|
||||||
|
return False
|
||||||
|
if g in a: # точное вхождение (в т.ч. CJK)
|
||||||
|
return True
|
||||||
|
gl = _lat(g)
|
||||||
|
return any(difflib.SequenceMatcher(None, gl, _lat(tok)).ratio() >= 0.75
|
||||||
|
for tok in re.findall(r"[\w\u0400-\u04ff]+", a))
|
||||||
|
|
||||||
|
|
||||||
|
def _is_refusal(a: str) -> bool:
|
||||||
|
"""⚠ «не знаю» ОТВЕТОМ, а не оговоркой. Проверка подстрокой отбрасывала верный ответ
|
||||||
|
«金瓶梅, автор Ланьлинский насмешник (не знаю точно)»: книга названа правильно, неуверенность
|
||||||
|
относится к автору. Так контроль занижался, а занижённый контроль аннулирует жильца."""
|
||||||
|
return a.startswith("не знаю") or a.strip("«»\"'. ") == "не знаю"
|
||||||
|
|
||||||
|
|
||||||
|
def same_work(titles: list[str], ans: str) -> bool:
|
||||||
|
a = (ans or "").strip().lower()
|
||||||
|
if not a or _is_refusal(a):
|
||||||
|
return False
|
||||||
|
return any(t.lower() in a for t in titles)
|
||||||
PREFIX = {"ФД-B": "dv-b", "ФД-C": "dv-c"}
|
PREFIX = {"ФД-B": "dv-b", "ФД-C": "dv-c"}
|
||||||
|
|
||||||
|
|
||||||
def probe(res: str, book: str, dry: bool) -> dict:
|
def probe(res: str, book: str, dry: bool, refill: bool = False) -> dict:
|
||||||
"""Проба одного жильца на одной книге. Возвращает счёт узнавания и клоуза."""
|
"""Проба одного жильца на одной книге. Возвращает счёт узнавания и клоуза."""
|
||||||
m = BOOKS_D[book]
|
m = BOOKS_D[book]
|
||||||
text = CT.read_book(m["path"])
|
text = CT.read_book(m["path"])
|
||||||
|
|
@ -120,9 +180,17 @@ def probe(res: str, book: str, dry: bool) -> dict:
|
||||||
tg = f"{PREFIX[ph]}-contam-{res.replace('.', '')}-{book}-{i}"
|
tg = f"{PREFIX[ph]}-contam-{res.replace('.', '')}-{book}-{i}"
|
||||||
if dry:
|
if dry:
|
||||||
continue
|
continue
|
||||||
|
cap = MAX_OUT
|
||||||
|
f = MONEY.OUT / f"{tg}.json"
|
||||||
|
if refill and f.exists():
|
||||||
|
rec0 = json.loads(f.read_text(encoding="utf-8"))
|
||||||
|
if rec0.get("finish") == "length" and not (rec0.get("content") or "").strip():
|
||||||
|
f.rename(MONEY.OUT / f"{tg}.LENGTH.json")
|
||||||
|
cap = MAX_OUT_REFILL
|
||||||
|
print(f" до-купка {tg}: усечённая клетка в карантин, кап {cap}")
|
||||||
msgs = [{"role": "system", "content": SYS}, {"role": "user", "content": prime}]
|
msgs = [{"role": "system", "content": SYS}, {"role": "user", "content": prime}]
|
||||||
r = MONEY.purchase(led, tg, res, ED.client(res),
|
r = MONEY.purchase(led, tg, res, ED.client(res),
|
||||||
ROSTER.call_kwargs(res, msgs, max_out=MAX_OUT))
|
ROSTER.call_kwargs(res, msgs, max_out=cap))
|
||||||
if r.get("skipped"):
|
if r.get("skipped"):
|
||||||
raise SystemExit(f"⛔ ГАРД ОТКАЗАЛ на {tg} — СТОП и вопрос владельцу")
|
raise SystemExit(f"⛔ ГАРД ОТКАЗАЛ на {tg} — СТОП и вопрос владельцу")
|
||||||
ans = r.get("content") or ""
|
ans = r.get("content") or ""
|
||||||
|
|
@ -130,11 +198,8 @@ def probe(res: str, book: str, dry: bool) -> dict:
|
||||||
empty += 1
|
empty += 1
|
||||||
continue
|
continue
|
||||||
work, name = CT.parse(ans)
|
work, name = CT.parse(ans)
|
||||||
if work and "НЕ ЗНАЮ" not in work.upper() and any(t.lower() in work.lower()
|
rec_hits += same_work(m["titles"], work)
|
||||||
for t in m["titles"]):
|
cloze_hits += same_name(gold, name)
|
||||||
rec_hits += 1
|
|
||||||
if name and "НЕ ЗНАЮ" not in name.upper() and gold and gold.lower() in name.lower():
|
|
||||||
cloze_hits += 1
|
|
||||||
return dict(recognition=rec_hits, cloze=cloze_hits, empty=empty, n=len(ps))
|
return dict(recognition=rec_hits, cloze=cloze_hits, empty=empty, n=len(ps))
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -158,7 +223,7 @@ def main() -> int:
|
||||||
res_tbl: dict[tuple[str, str], dict] = {}
|
res_tbl: dict[tuple[str, str], dict] = {}
|
||||||
for res in RESIDENTS:
|
for res in RESIDENTS:
|
||||||
for b in books:
|
for b in books:
|
||||||
res_tbl[(res, b)] = probe(res, b, dry=False)
|
res_tbl[(res, b)] = probe(res, b, dry=False, refill="--refill" in sys.argv)
|
||||||
|
|
||||||
print(f"\n{'жилец':20s}{'книга':14s}{'узнавание':>11s}{'клоуз':>8s}{'пусто':>7s} вердикт")
|
print(f"\n{'жилец':20s}{'книга':14s}{'узнавание':>11s}{'клоуз':>8s}{'пусто':>7s} вердикт")
|
||||||
bad = 0
|
bad = 0
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue