Files
creator2/backend/embedding.py
2026-07-10 15:43:11 +02:00

68 lines
2.4 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.
"""Ä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.30.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])