Revisionsmodul, Embeddings und Container

Drei Schritte, die zusammen Phase 2 lauffaehig machen.

Revisionen (migrations/p2-004, revisionen.py)
    Ingest schreibt jede geaenderte Fassung eines Items nach
    raw_item_versionen fort. Daraus faellt ein Befund ab, den die
    Spezifikation nicht vorsehen konnte: welches Haus seine Schlagzeile
    nachtraeglich umschreibt, und wie. 206 von 3407 Items, acht
    Einstufungen von 'formal' bis 'neufassung'. Kein Modell, kein Lexikon,
    kein Netzzugriff - nur ein Wortvergleich zweier Fassungen.

    Fortschreibungen sind ueber ticker_markiert und kopf_stabil abziehbar,
    absichtlich als zwei Merkmale statt einer Kategorie: das eine ist eine
    Selbstauskunft des Hauses, das andere eine Beobachtung am Satzbau.

Embeddings (embeddings.py)
    multilingual-e5-large, 1024 Dimensionen, MIT. Jede Zeile wird auf feste
    128 Token aufgefuellt, nicht nur bis zur laengsten des Stapels - sonst
    haengt ein Vektor davon ab, mit welchen Nachbarn er kodiert wurde, und
    ein Slot-Lauf liefert andere letzte Stellen als ein Backfill.
    Nachgewiesen: 64/64 bitgleich einzeln wie im vollen Stapel, groesste
    Abweichung 0.00e+00, und im Betrieb 26 Slot-Vektoren unveraendert
    gegen den 3407er-Backfill.

    Darum transformers statt sentence-transformers: letzteres kapselt genau
    diese Stelle weg.

Container (Containerfile, phase2.py, deploy/)
    Debian trixie wie der Postgres-Container, torch aus dem CPU-Index.
    Die regelbasierten Module liefern darin fuer alle 3407 Items bitgleich
    dieselbe Nutzlast wie auf der Entwicklungsmaschine - Portabilitaet
    gemessen, nicht behauptet.

version.py bekommt den Schluessel 'revision'. Weil der Hash ueber den ganzen
Komponentensatz laeuft, aendert das auch die Version des Dublettenmoduls,
obwohl an dessen Verfahren nichts anders ist. Das ist die in version.py
beschriebene Semantik der Spezifikation, kein Versehen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NupWEBPzsyohVPG47HsTz8
This commit is contained in:
irrlicht
2026-09-08 07:41:27 +02:00
co-authored by Claude Opus 5
parent 50d497925f
commit cf89e012b2
16 changed files with 1980 additions and 7 deletions
+46 -7
View File
@@ -49,19 +49,29 @@ _RESSORT_MAX_WOERTER = 4
_REST_MIN_WOERTER = 5
def _ressort_abtrennen(text):
def ressort_teilen(text):
"""(kopf, rest) - kopf ist None, wenn kein Ressortkuerzel zu erkennen ist.
Das Modul revisionen braucht beide Teile getrennt: ob ein Haus die
Ressortmarke oder die Schlagzeile selbst geaendert hat, sind zwei sehr
verschiedene Vorgaenge.
"""
treffer = _RESSORT.match(text)
if not treffer:
return text
return None, text
kopf, rest = treffer.group("kopf"), treffer.group("rest")
if len(kopf.split()) > _RESSORT_MAX_WOERTER:
return text
return None, text
if len(rest.split()) < _REST_MIN_WOERTER:
return text
return rest
return None, text
return kopf, rest
def normalisiere(*teile, ressort_abtrennen=False):
def _ressort_abtrennen(text):
return ressort_teilen(text)[1]
def normalisiere(*teile, ressort_abtrennen=False, pluskasten=True):
"""Vergleichstext aus einem oder mehreren Feldern.
Reihenfolge ist bedeutsam: Tags vor Entities, NFKC vor der
@@ -71,6 +81,12 @@ def normalisiere(*teile, ressort_abtrennen=False):
ressort_abtrennen gilt nur fuer Titel. Im Teaser steht der Doppelpunkt
fuer gewoehnlich mitten im Satz.
pluskasten=False laesst "++ ... ++" stehen. Beim Dublettenvergleich ist
der Kasten Beiwerk, das die Haeuser verschieden setzen. Bei den
Revisionen ist er der Text: die tagesschau fuehrt ihren Liveticker
vollstaendig darin ("++ Liveticker zur Wahl: ... ++"), und mit Entfernung
bliebe von der Schlagzeile nichts uebrig, was sich vergleichen liesse.
"""
text = " ".join(t for t in teile if t)
if not text:
@@ -80,13 +96,36 @@ def normalisiere(*teile, ressort_abtrennen=False):
text = unicodedata.normalize("NFKC", text)
text = text.translate(_ZEICHEN)
text = text.casefold()
text = _PLUSKASTEN.sub(" ", text)
if pluskasten:
text = _PLUSKASTEN.sub(" ", text)
if ressort_abtrennen:
text = _ressort_abtrennen(text)
text = _KEIN_WORT.sub(" ", text)
return _MEHRFACH_LEER.sub(" ", text).strip()
def bereinige(*teile):
"""Text fuer neuronale Encoder: aufgeraeumt, aber nicht zerlegt.
normalisiere() macht Text vergleichbar, indem es ihn abschleift -
Kleinschreibung, keine Satzzeichen, keine Ressortmarke. Fuer einen
Encoder ist das Zerstoerung: er ist auf natuerlichem Text trainiert,
Grossschreibung unterscheidet im Deutschen Wortarten, und die
Anfuehrungszeichen um ein Zitat sind Bedeutung.
Entfernt wird deshalb nur, was kein Text ist: Auszeichnung, Entities,
uneinheitliche Unicode-Varianten, ueberzaehliger Leerraum.
"""
text = " ".join(t for t in teile if t)
if not text:
return ""
text = _TAGS.sub(" ", text)
text = html.unescape(text)
text = unicodedata.normalize("NFKC", text)
text = text.translate(_ZEICHEN)
return _MEHRFACH_LEER.sub(" ", text).strip()
def woerter(text):
return text.split()