feat: Inhaltsinventar in SQLite — KI-Inhaltsangabe je Objekt (URL + Hash)

Vereinheitlicht den bisherigen JSON-Ledger zu EINEM abfragbaren SQLite-Store
pro Site (data/content_inventory.db): jedes geprüfte Objekt mit Inhalts-Hash,
URL(s), Sicherheits-Verdikt UND neutraler KI-Inhaltsangabe.

- baseline.py: load/save_ai_ledger jetzt SQLite-gestützt (Dict-Schnittstelle
  bleibt → Analyzer unverändert). Tabellen objects + object_urls. Einmalige
  Migration eines vorhandenen ai_ledger.json → SQLite (.migrated). UPSERT mit
  first_seen-Erhalt; object_urls transaktional ersetzt. Neu: query_inventory,
  export_inventory_csv (beide migrieren failsafe).
- ai_analyzer.py: Antwort-Schema + Prompt um neutrales Feld 'description'
  erweitert (im selben Call, keine Extrakosten); _make_entry speichert es.
- __main__.py: neues Kommando 'inventory' (Übersicht, --search, --kind, --csv).
- Doku: README + Bedienungsanleitung.

Audio/Video sind im Schema (kind) vorbereitet (Phase 2: Extractor + Modalitäten).
bredelar.info real migriert: 245 Objekte (200 Bilder, 45 Texte).

Tests: 266 grün (+6: Round-Trip mit description, Migration, first_seen-Erhalt,
invalidate, Suche/CSV-Export, description landet im Ledger).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-06-13 05:12:19 +02:00
commit 989f5a933f
7 changed files with 262 additions and 22 deletions

View file

@ -58,12 +58,13 @@ _RESPONSE_SCHEMA = {
"schema": {
"type": "object",
"properties": {
"description": {"type": "string"},
"category": {"type": "string", "enum": _CATEGORIES},
"severity": {"type": "string", "enum": ["none", "low", "medium", "high"]},
"confidence": {"type": "number"},
"explanation": {"type": "string"},
},
"required": ["category", "severity", "confidence", "explanation"],
"required": ["description", "category", "severity", "confidence", "explanation"],
"additionalProperties": False,
},
},
@ -483,6 +484,7 @@ def _make_entry(kind: str, url: str, verdict: dict, model: str) -> dict:
return {
"kind": kind,
"url": url,
"description": verdict.get("description", ""),
"category": verdict.get("category", "clean"),
"severity": verdict.get("severity", "none"),
"confidence": float(verdict.get("confidence", 0.0)),
@ -566,7 +568,9 @@ _SYSTEM_PROMPT = (
"einen Werbe- oder Spam-Text nicht unproblematisch — ein Angreifer kann sie "
"gezielt einstreuen. Bewerte den Gesamtcharakter des Inhalts.\n"
"Gib 'clean' zurück, wenn der Inhalt zum Thema passt und unproblematisch ist. "
"Antworte ausschließlich im vorgegebenen JSON-Format mit deutscher Begründung."
"Gib im Feld 'description' eine knappe, neutrale Inhaltsangabe (12 Sätze, deutsch): "
"was ist auf dem Bild zu sehen bzw. worum geht es im Text — sachlich, unabhängig vom "
"Sicherheitsurteil. Antworte ausschließlich im vorgegebenen JSON-Format."
)