"""Ähnlichkeit für Merge-KANDIDATEN — entscheidet nie selbst (das tut das Panel). sentence-transformers wenn vorhanden, sonst lexikalischer Jaccard-Fallback. Verglichen werden DEFINITIONEN, nie Titel (Lektion: Titel-Embeddings rauschen).""" import logging import re log = logging.getLogger("creator2.embedding") _modell = None _geladen = False def _lade_modell(): global _modell, _geladen if _geladen: return _modell _geladen = True try: from sentence_transformers import SentenceTransformer _modell = SentenceTransformer("sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2") log.info("Embedding-Modell geladen") except Exception: log.warning("kein Embedding-Modell — lexikalischer Jaccard-Fallback" " (findet Paraphrasen-Dubletten deutlich schlechter);" " Fix: pip install sentence-transformers") _modell = None return _modell _WORT = re.compile(r"[a-zäöüß0-9]+") def _tokens(text: str) -> set[str]: return set(_WORT.findall(text.lower())) def _jaccard(a: str, b: str) -> float: ta, tb = _tokens(a), _tokens(b) if not ta or not tb: return 0.0 return len(ta & tb) / len(ta | tb) def aehnlichkeit_matrix(texte: list[str]) -> list[list[float]]: """Paarweise Ähnlichkeit [0..1]. Symmetrisch, Diagonale 1.""" modell = _lade_modell() n = len(texte) if modell is not None: import numpy as np emb = modell.encode(texte, normalize_embeddings=True) return (emb @ emb.T).tolist() return [[1.0 if i == j else _jaccard(texte[i], texte[j]) for j in range(n)] for i in range(n)] def kandidaten_paare(texte: list[str], floor: float, fallback_floor: float | None = None) -> list[tuple[int, int, float]]: """Alle Index-Paare mit Ähnlichkeit ≥ Schwelle, absteigend sortiert. Im Jaccard-Fallback gilt fallback_floor: echte Paraphrasen messen dort nur ~0.3–0.5 — die Kosinus-Schwelle ließe sie alle durchrutschen (aak Lauf 8: 13 Panel-Calls bei 26 überlebenden Dubletten-Gruppen).""" m = aehnlichkeit_matrix(texte) if _modell is None and fallback_floor is not None: floor = fallback_floor paare = [(i, j, m[i][j]) for i in range(len(texte)) for j in range(i + 1, len(texte)) if m[i][j] >= floor] return sorted(paare, key=lambda p: -p[2])