112 lines
6.9 KiB
Python
112 lines
6.9 KiB
Python
#!/usr/bin/env python3
|
||
"""Арм SCOPED REPAIR: починка по УЖЕ НАЙДЕННОМУ спану — механизм, которого у нас нет.
|
||
|
||
`research/22:176` фиксирует раскол дословно: «Checker→typed-error→scoped repair | AiNiee
|
||
`ProofreadTask.py:250`; LinguaGacha retrans-by-keyword | у нас НЕТ (флагаем, не чиним)».
|
||
|
||
Что проверяется. Мои же замеры дали, что цену редакторского вызова держит РЕШЕНИЕ, а не формат:
|
||
арм `locate` (только найди, ничего не чини) стоил 2.14× от полного переписывания на deepseek-v4-pro.
|
||
Если так — починка по указанному месту обязана быть на порядок дешевле: модели не нужно ни искать,
|
||
ни держать в голове весь текст, ей дают одно предложение и тип ошибки.
|
||
|
||
Материал — те же 12 посаженных дефектов пробы 18 (§0.5): 6 несуществующих слов (к1) и 6 смысловых
|
||
инверсий (к2). Эталон известен посимвольно, поэтому починка скорится детерминированно, без судьи,
|
||
теми же регексами `fix_rx`/`bad_rx`, что валидированы ревью посылки (12/12 корректны).
|
||
|
||
⚠ Объявляется заранее: арм меряет починку при ИДЕАЛЬНОЙ детекции. Он НЕ отвечает, найдут ли гейты
|
||
эти дефекты сами — гейта на «несуществующее слово» и на смысловую инверсию у нас нет. Поэтому
|
||
результат читается как ВЕРХНЯЯ ГРАНИЦА схемы, а не как её цена в проде.
|
||
"""
|
||
from __future__ import annotations
|
||
import json
|
||
import re
|
||
import sys
|
||
import time
|
||
from pathlib import Path
|
||
|
||
REPO = Path("/home/ubuntu/projects/textmachine")
|
||
HERE = Path(__file__).resolve().parent
|
||
sys.path.insert(0, str(REPO / "eval" / "editor_contract"))
|
||
sys.path.insert(0, str(REPO / "eval" / "bank_arbitration"))
|
||
sys.path.insert(0, str(HERE))
|
||
|
||
import probe as Q # noqa: E402
|
||
import editor_wire_probe as P # noqa: E402
|
||
from inject_probe import pick_windows # noqa: E402
|
||
|
||
RAW = Q.RAW
|
||
TYPE = {"k1": "несуществующее слово (модель выдумала слово, которого в русском языке нет)",
|
||
"k2": "смысловое искажение против исходника (сказано обратное или иное, чем в оригинале)"}
|
||
|
||
|
||
def sentence_with(text: str, needle: str) -> str | None:
|
||
"""Предложение, содержащее посаженный фрагмент. Границы — по .!?… и переносу строки."""
|
||
i = text.find(needle)
|
||
if i < 0:
|
||
return None
|
||
starts = [m.end() for m in re.finditer(r"[.!?…]\s+|\n", text[:i])]
|
||
s = starts[-1] if starts else 0
|
||
m = re.search(r"[.!?…](\s|$)|\n", text[i + len(needle):])
|
||
e = i + len(needle) + (m.end() if m else 0)
|
||
return text[s:e].strip()
|
||
|
||
|
||
def plan() -> list[tuple]:
|
||
out = []
|
||
for k, (_, buf, _) in enumerate(pick_windows()):
|
||
planted, _ = P.planted_draft(k, P.draft_of(k))
|
||
for cls, old, new, bad_rx, fix_rx in P.DEFECTS[k]:
|
||
sent = sentence_with(planted, new)
|
||
if not sent:
|
||
print(f" ⚠ w{k}/{cls}: не выделил предложение вокруг посадки — пропуск")
|
||
continue
|
||
# ⚠ Селф-ревью 05.08: у части посадок дефект ДОБАВЛЯЕТ слово, не заменяя ключевое
|
||
# («мреязное СОКРОВИЩЕ природы»), поэтому `fix_rx` совпадает с испорченным предложением
|
||
# ЕЩЁ ДО починки. В таких клетках «восстановлен» не несёт информации — помечаем их и
|
||
# держим вне строгого знаменателя, иначе бездействие модели засчиталось бы как успех.
|
||
degenerate = bool(re.search(fix_rx, sent))
|
||
out.append((k, cls, sent, buf, bad_rx, fix_rx, old, degenerate))
|
||
return out
|
||
|
||
|
||
def main(models: list[str]) -> None:
|
||
pl = plan()
|
||
print(f"починок в плане: {len(pl)} (ожидается 12)")
|
||
tpl = HERE / "prompts" / "repair.md"
|
||
for m in models:
|
||
short = {"deepseek-v4-pro": "dp", "glm-5": "gl", "gpt-5.6-luna": "lu",
|
||
"grok-4.3": "gr"}[m]
|
||
nothink = Q.MODELS[m][5] in ("disable", "effort_none")
|
||
rows, spent = [], 0.0
|
||
for k, cls, sent, buf, bad_rx, fix_rx, old, degen in pl:
|
||
flagged = f"{TYPE[cls]}\n\nПроблемное предложение перевода:\n{sent}"
|
||
sys_msg, user = Q.render(tpl, buf, flagged)
|
||
rec = Q.call(m, [{"role": "system", "content": sys_msg},
|
||
{"role": "user", "content": user}],
|
||
f"rp-{short}-w{k}-{cls}", nothink)
|
||
spent += rec["cost_usd"]
|
||
got = rec["content"].strip()
|
||
rows.append(dict(window=k, cls=cls, degenerate=degen,
|
||
still=bool(re.search(bad_rx, got)),
|
||
restored=bool(re.search(fix_rx, got)),
|
||
cost=rec["cost_usd"], out_tokens=rec["completion_tokens"],
|
||
reasoning=rec["reasoning_chars"], chars=len(got)))
|
||
time.sleep(0.2)
|
||
inf = [r for r in rows if not r["degenerate"]]
|
||
snyat = sum(1 for r in rows if not r["still"])
|
||
strict = sum(1 for r in inf if not r["still"] and r["restored"])
|
||
print(f"\n{m}: СНЯТ дефект {snyat}/{len(rows)} · "
|
||
f"ВОССТАНОВЛЕН строго {strict}/{len(inf)} (клеток с информативным эталоном {len(inf)} "
|
||
f"из {len(rows)}) · ${spent:.6f}, ${spent / max(len(rows), 1):.6f} за починку")
|
||
for cls in ("k1", "k2"):
|
||
sub = [r for r in rows if r["cls"] == cls]
|
||
si = [r for r in sub if not r["degenerate"]]
|
||
if sub:
|
||
print(f" {cls}: снят {sum(1 for r in sub if not r['still'])}/{len(sub)}"
|
||
+ (f" · восстановлен {sum(1 for r in si if not r['still'] and r['restored'])}/{len(si)}" if si else ""))
|
||
(RAW / f"repair-{short}.json").write_text(json.dumps(rows, ensure_ascii=False, indent=1),
|
||
encoding="utf-8")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main(sys.argv[1:] or ["deepseek-v4-pro"])
|