Files
swp-02-aufbereitung/phase2.py
T
irrlichtandClaude Opus 5 cf89e012b2 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
2026-09-08 07:41:27 +02:00

98 lines
3.7 KiB
Python

#!/usr/bin/env python3
"""Phase 2 als Ganzes: alle Module in fester Reihenfolge.
python3 phase2.py --backfill
python3 phase2.py --slot 2026-09-07T08:00
python3 phase2.py --neueste # der juengste Slot in raw_items
python3 phase2.py --aufholen # jeder Slot, dem eine Kodierung fehlt
Das ist der Einstiegspunkt im Betrieb; die Module lassen sich weiter einzeln
aufrufen. Die Reihenfolge ist nicht beliebig: die regelbasierten Module sind
in Millisekunden fertig, das Embedding braucht Modell und Rechenzeit. Bricht
es ab, stehen die uebrigen Ergebnisse trotzdem.
Jedes Modul haelt seine eigene coder_version. Ein Fehlschlag in einem Modul
laesst die anderen unberuehrt - dafuer ist die Schichtung in codings da.
"""
import argparse
import sys
MODULE = ["dubletten", "revisionen", "embeddings"]
def slots_ohne_kodierung(conn):
"""Slots, in denen Items liegen, die noch keine Dublettenzeile haben.
Absichtlich am ersten Modul festgemacht und nicht an allen: laeuft das
Embedding einmal nicht durch, soll der Aufholvorgang nicht dauerhaft
dieselben Slots wiederholen. Fehlende Vektoren holt ein Backfill.
"""
return [r[0] for r in conn.execute(
"""select distinct r.slot from raw_items r
where not exists (select 1 from codings c
where c.raw_item_id = r.id and c.ebene = 'dublette')
order by 1""")]
def fuehre_aus(name, argumente):
modul = __import__(name)
print(f"\n--- {name} {' '.join(argumente)}")
try:
rueck = modul.main(argumente)
except Exception as fehler: # noqa: BLE001
# Ein Modul darf die uebrigen nicht mitreissen. Der Fehler steht im
# Protokoll und im Rueckgabewert, nicht in einem abgebrochenen Lauf.
print(f" FEHLER in {name}: {fehler.__class__.__name__}: {fehler}",
file=sys.stderr)
return 1
return rueck or 0
def main(argv=None):
p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
gruppe = p.add_mutually_exclusive_group(required=True)
gruppe.add_argument("--backfill", action="store_true", help="Gesamtbestand")
gruppe.add_argument("--slot", help="ein Slot, z.B. 2026-09-07T08:00")
gruppe.add_argument("--neueste", action="store_true",
help="der juengste Slot in raw_items")
gruppe.add_argument("--aufholen", action="store_true",
help="jeder Slot, dem eine Kodierung fehlt")
p.add_argument("--ohne", action="append", default=[], metavar="MODUL",
help="Modul auslassen, mehrfach angebbar")
p.add_argument("--trocken", action="store_true", help="rechnen, nichts schreiben")
a = p.parse_args(argv)
module = [m for m in MODULE if m not in a.ohne]
zusatz = ["--trocken"] if a.trocken else []
if a.backfill:
laeufe = [["--backfill"]]
elif a.slot:
laeufe = [["--slot", a.slot]]
else:
from db import verbindung
with verbindung() as conn:
if a.neueste:
zeile = conn.execute("select max(slot) from raw_items").fetchone()
slots = [zeile[0]] if zeile and zeile[0] else []
else:
slots = slots_ohne_kodierung(conn)
if not slots:
print("kein offener Slot")
return 0
print(f"{len(slots)} Slot(s): {slots[0]} bis {slots[-1]}")
laeufe = [["--slot", s.isoformat()] for s in slots]
schlecht = 0
for lauf in laeufe:
for name in module:
schlecht += fuehre_aus(name, lauf + zusatz)
if schlecht:
print(f"\n{schlecht} Modullauf/-laeufe mit Fehler", file=sys.stderr)
return 1 if schlecht else 0
if __name__ == "__main__":
sys.exit(main())