perf: URL→Byte-Hash-Erinnerung — bekannte Bilder nicht erneut laden/analysieren

Bisher: Die Analyse identischer Bilder war bereits dedupliziert (Ledger nach
Byte-Hash indiziert). Aber jede Bild-URL wurde bei jedem Scan ERNEUT
heruntergeladen, um ihren Hash für Gruppierung/Abdeckung zu berechnen — auch
wenn die Bytes längst bekannt und analysiert waren.

Neu: persistente URL→Hash-Map im Ledger (url_hashes). Eine bereits gesehene
Bild-URL wird nicht erneut geladen; Abdeckung umfasst nur noch nie gesehene
URLs. Duplikate (gleiche Bytes an mehreren URLs) werden je URL höchstens einmal
geholt und nie doppelt analysiert. Geänderte/geflaggte Bilder werden weiterhin
IMMER (erneut) geprüft.

Spart Bandbreite/Zeit und schont den überwachten Server (Fetch nur für Neues).
Byte-Änderungen bereits bekannter Bilder auf stabilen Seiten deckt weiterhin
check-assets ab (Integritäts-Layer).

Tests: 258 grün (+1: Duplikat-URLs einmal geladen, einmal analysiert, danach
kein erneuter Fetch).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-06-13 04:17:31 +02:00
commit 4db5817013
2 changed files with 48 additions and 15 deletions

View file

@ -230,6 +230,32 @@ class TestImages:
if e.get("kind") == "image"}
assert len(analysed) == 5 # nach 3 Scans sind alle 5 Bilder abgedeckt
def test_duplicate_image_urls_fetched_once_analyzed_once(self, tmp_path, monkeypatch):
"""Gleiches Bild an mehreren URLs: nur EINE Analyse; danach kein erneuter Fetch
(Byte-Hash gemerkt und geprüft)."""
monkeypatch.setenv("OPENROUTER_API_KEY", "test")
bm = BaselineManager(tmp_path)
cfg = self._cfg_img()
imgs = [{"url": "https://x.de/a.jpg", "class": "internal"},
{"url": "https://x.de/b.jpg", "class": "internal"}]
snap = _snap("kurz", img=imgs)
same = {"https://x.de/a.jpg": {"sha256": "DUP", "size": 1, "error": None},
"https://x.de/b.jpg": {"sha256": "DUP", "size": 1, "error": None}}
with patch("scanner.ai_analyzer.fetch_asset_hashes", return_value=same), \
patch("scanner.ai_analyzer._openrouter_chat", return_value=_verdict()):
res1 = run_ai_analysis(cfg, bm, snap, {})
assert res1["api_calls"] == 1 # gleiche Bytes → nur EINE Analyse
led = bm.load_ai_ledger()
assert set(led["url_hashes"]) == {"https://x.de/a.jpg", "https://x.de/b.jpg"}
# zweiter Scan: beide URLs bekannt → gar kein Fetch, keine Analyse mehr
with patch("scanner.ai_analyzer.fetch_asset_hashes") as fetch2, \
patch("scanner.ai_analyzer._openrouter_chat") as chat2:
res2 = run_ai_analysis(cfg, bm, snap, {})
fetch2.assert_not_called()
chat2.assert_not_called()
assert res2["api_calls"] == 0
def test_image_backlog_unlimited_covers_all_in_one_scan(self, tmp_path, monkeypatch):
"""max_images_per_scan=0 → voller Bildbestand in EINEM Scan (Default)."""
monkeypatch.setenv("OPENROUTER_API_KEY", "test")