This commit is contained in:
team3
2026-06-30 00:14:18 +02:00
parent 3e3559aa8f
commit c794fcaccf
152 changed files with 9485 additions and 9583 deletions

115
backend/readability.py Normal file
View File

@@ -0,0 +1,115 @@
"""Deterministic readability gate for guide sections.
A small German complexity model (DistilBERT, GermEval 2022, scale 17) rates the
readability of the prose. guide.py feeds sections that are too hard into the existing
read-exam/revision loop — no prompt, no guessing.
Optional: if `transformers`/`torch` are missing or the model won't load, the gate is
silently disabled (the backend keeps running unchanged). CPU is enough; the caller
wraps the scoring in `asyncio.to_thread` (blocking model inference).
"""
import logging
import re
from config import (
READABILITY_ACTIVE, READABILITY_HARD, READABILITY_HARD_SHARE, READABILITY_MAX, READABILITY_MODEL,
)
log = logging.getLogger("creator.readability")
_model_cache = None # (tokenizer, model, torch) — singleton
_load_attempt = False # already tried to load?
# Strip markup → plain prose (code does not count toward readability).
_CODE_FENCE = re.compile(r"```.*?```", re.DOTALL)
_COMMENT = re.compile(r"<!--.*?-->", re.DOTALL)
_INLINE_CODE = re.compile(r"`[^`]*`")
_LINK = re.compile(r"\[([^\]]*)\]\([^)]*\)")
_MD_MARK = re.compile(r"^[ \t]*([#>]+|[-*+]\s)|[*_~|]", re.MULTILINE)
_WS = re.compile(r"\s+")
_SENTENCE = re.compile(r"(?<=[.!?])\s+")
def _model():
"""Load the model once. None = gate off (disabled or load error)."""
global _model_cache, _load_attempt
if _load_attempt:
return _model_cache
_load_attempt = True
if not READABILITY_ACTIVE:
return None
try:
import torch
from transformers import AutoModelForSequenceClassification, AutoTokenizer
tok = AutoTokenizer.from_pretrained(READABILITY_MODEL)
model = AutoModelForSequenceClassification.from_pretrained(READABILITY_MODEL)
model.eval()
_model_cache = (tok, model, torch)
log.info("readability model loaded: %s (num_labels=%d)", READABILITY_MODEL, model.config.num_labels)
except Exception as e:
log.warning("readability gate disabled (model not loadable): %s", e)
_model_cache = None
return _model_cache
def _prose(md: str) -> str:
"""Strip markdown/code → plain prose for scoring."""
t = _CODE_FENCE.sub(" ", md)
t = _COMMENT.sub(" ", t)
t = _INLINE_CODE.sub(" ", t)
t = _LINK.sub(r"\1", t)
t = _MD_MARK.sub(" ", t)
return _WS.sub(" ", t).strip()
def _sentences(text: str) -> list[str]:
"""Split prose into sentences; discard very short fragments."""
return [s.strip() for s in _SENTENCE.split(text) if len(s.strip()) >= 15]
def _scores(sentences: list[str]) -> list[float]:
"""Complexity per sentence (17). Regression (num_labels=1) or expectation over classes."""
tok, model, torch = _model_cache
values: list[float] = []
n = model.config.num_labels
for i in range(0, len(sentences), 16):
batch = sentences[i:i + 16]
enc = tok(batch, return_tensors="pt", truncation=True, max_length=256, padding=True)
with torch.no_grad():
logits = model(**enc).logits
if n == 1:
vals = logits.reshape(-1).tolist()
else:
probs = torch.softmax(logits, dim=-1)
levels = torch.arange(1, n + 1, dtype=probs.dtype)
vals = (probs * levels).sum(-1).reshape(-1).tolist()
values.extend(vals)
return values
def rate_sections(md_by_num: dict[int, str]) -> dict[int, str]:
"""{num: section_md} → {num: hint} only for sections that are too hard.
Empty dict if the gate is off. Blocking (CPU) — call inside to_thread.
"""
if _model() is None:
return {}
out: dict[int, str] = {}
for num, md in md_by_num.items():
sentences = _sentences(_prose(md or ""))
if len(sentences) < 2: # almost only code / too short → skip
continue
values = _scores(sentences)
if not values:
continue
mean = sum(values) / len(values)
hard = sum(1 for w in values if w > READABILITY_HARD) / len(values)
# Too hard = high mean OR too many hard individual sentences (outlier nests).
if mean > READABILITY_MAX or hard >= READABILITY_HARD_SHARE:
# German revision hint fed to the (German-writing) writer agent — kept German on purpose.
out[num] = (
f"Zu schwer lesbar (Ø {mean:.1f}/7, {hard * 100:.0f}% harte Sätze): "
"kürzere Sätze, einfachere Wörter, weniger Schachtelsätze, mehr Examples."
)
return out