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:
parent
369a3b8774
commit
4db5817013
2 changed files with 48 additions and 15 deletions
|
|
@ -193,6 +193,7 @@ def run_ai_analysis(cfg: dict, bm, snap: dict, diff: dict) -> dict:
|
|||
try:
|
||||
ledger = bm.load_ai_ledger()
|
||||
entries = ledger.setdefault("entries", {})
|
||||
url_hashes = ledger.setdefault("url_hashes", {}) # URL→Byte-Hash-Erinnerung (Bilder)
|
||||
dirty = False
|
||||
# Ledger wird zwischengesichert (nach Textphase + im finally), damit bereits
|
||||
# berechnete Verdikte einen späteren Hang/Abbruch überleben und nicht erneut
|
||||
|
|
@ -206,7 +207,7 @@ def run_ai_analysis(cfg: dict, bm, snap: dict, diff: dict) -> dict:
|
|||
bm.save_ai_ledger(ledger)
|
||||
if ai_cfg.get("image", {}).get("enabled", True):
|
||||
dirty |= _analyze_images(cfg, ai_cfg, api_key, snap, diff,
|
||||
entries, result, router, executor)
|
||||
entries, result, router, executor, url_hashes)
|
||||
# Audio/Video: vorbereitet, default aus (siehe _collect_audio/video_candidates).
|
||||
finally:
|
||||
if dirty:
|
||||
|
|
@ -359,8 +360,13 @@ def _analyze_text(cfg, ai_cfg, api_key, snap, diff, entries, result, router, exe
|
|||
# Image analysis
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _analyze_images(cfg, ai_cfg, api_key, snap, diff, entries, result, router, executor) -> bool:
|
||||
"""Prüft neue/geänderte Bilder (parallel). Returns True wenn Ledger verändert."""
|
||||
def _analyze_images(cfg, ai_cfg, api_key, snap, diff, entries, result, router, executor,
|
||||
url_hashes) -> bool:
|
||||
"""Prüft neue/geänderte Bilder (parallel). Returns True wenn Ledger verändert.
|
||||
|
||||
url_hashes ist die persistente URL→Byte-Hash-Erinnerung: eine bereits gesehene
|
||||
Bild-URL wird NICHT erneut heruntergeladen (kein redundanter Fetch, keine doppelte
|
||||
Analyse). Nur nie gesehene Bilder sowie geänderte/geflaggte werden geholt."""
|
||||
img_cfg = ai_cfg.get("image", {})
|
||||
models = _models_for(img_cfg)
|
||||
site_context = ai_cfg.get("site_context", "")
|
||||
|
|
@ -378,17 +384,14 @@ def _analyze_images(cfg, ai_cfg, api_key, snap, diff, entries, result, router, e
|
|||
if e.get("kind") == "image" and e.get("category", "clean") != "clean"
|
||||
and not e.get("dismissed") and e.get("url") in current_img_urls
|
||||
}
|
||||
# Noch nie analysierte Bilder (URL nicht im Ledger): bauen über mehrere Scans die
|
||||
# VOLLE Abdeckung auf — sonst bliebe jedes Bild jenseits des ersten Budget-Fensters
|
||||
# ein dauerhafter blinder Fleck (z. B. ein eingeschleustes strafbares Logo auf einer
|
||||
# ansonsten stabilen Seite). Bytes-Änderungen bekannter Bilder deckt check-assets ab.
|
||||
known_img_urls = {e["url"] for e in entries.values() if e.get("kind") == "image"}
|
||||
coverage_urls = current_img_urls - known_img_urls
|
||||
|
||||
# Geänderte/geflaggte Bilder: IMMER prüfen (Echtzeit-Schutz, nie gedeckelt).
|
||||
# Bestands-Abdeckung: Drossel optional; 0 = unbegrenzt (volle Abdeckung im ersten Scan).
|
||||
# Geänderte/geflaggte Bilder: IMMER (erneut) prüfen (Echtzeit-Schutz, nie gedeckelt).
|
||||
# Abdeckung: nur noch NIE gesehene URLs (nicht in url_hashes) — schon bekannte Bilder
|
||||
# werden nicht erneut geladen (Hashwert gemerkt). Volle Abdeckung baut sich so EINMALIG
|
||||
# auf; Duplikate (gleiche Bytes an mehreren URLs) werden je URL höchstens einmal geholt
|
||||
# und nie doppelt analysiert (Ledger ist nach Byte-Hash indiziert). Drossel 0 = unbegrenzt.
|
||||
priority = sorted((changed_img_urls | flagged_img_urls) & current_img_urls)
|
||||
coverage = sorted(coverage_urls - set(priority))
|
||||
coverage = sorted((current_img_urls - set(url_hashes)) - set(priority))
|
||||
backlog_limit = img_cfg.get("max_images_per_scan", 0)
|
||||
if backlog_limit and backlog_limit > 0:
|
||||
coverage = coverage[:backlog_limit]
|
||||
|
|
@ -401,12 +404,17 @@ def _analyze_images(cfg, ai_cfg, api_key, snap, diff, entries, result, router, e
|
|||
hashes = fetch_asset_hashes(fetch_urls, timeout=cfg.get("request_timeout", 15))
|
||||
|
||||
# Nach Bild-Hash (Bytes) gruppieren: identische Bilder werden nur einmal klassifiziert.
|
||||
# Zugleich URL→Hash merken (auch für Duplikate), damit künftige Scans sie überspringen.
|
||||
dirty = False
|
||||
fp_assets: dict[str, list[str]] = {}
|
||||
order_fps: list[str] = []
|
||||
for url in fetch_urls:
|
||||
fp = hashes.get(url, {}).get("sha256")
|
||||
if not fp:
|
||||
continue # nicht abrufbar — überspringen
|
||||
continue # nicht abrufbar — überspringen (nicht merken → später erneut versuchen)
|
||||
if url_hashes.get(url) != fp:
|
||||
url_hashes[url] = fp # neu gesehen oder Bytes geändert
|
||||
dirty = True
|
||||
result["checked"] += 1
|
||||
if fp not in fp_assets:
|
||||
fp_assets[fp] = []
|
||||
|
|
@ -425,7 +433,6 @@ def _analyze_images(cfg, ai_cfg, api_key, snap, diff, entries, result, router, e
|
|||
tasks.append((fp, _image_task(fp_assets[fp][0], site_context, models, ai_cfg, api_key, router)))
|
||||
|
||||
classified = _classify_batch(tasks, executor)
|
||||
dirty = False
|
||||
for fp in miss_fps:
|
||||
res = classified.get(fp)
|
||||
rep_url = fp_assets[fp][0]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue