From ca91ae4b337b06ca949b89b416a8f9c89ddcd516 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Wed, 29 Apr 2026 21:25:03 +0200 Subject: [PATCH] feat(perception): autonomous FSD ad marker learning with zero-latency structural persistence --- .../core/behaviors/resonance_evaluator.py | 4 ++ GramAddict/core/utils.py | 59 +++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/GramAddict/core/behaviors/resonance_evaluator.py b/GramAddict/core/behaviors/resonance_evaluator.py index 3890d23..f828348 100644 --- a/GramAddict/core/behaviors/resonance_evaluator.py +++ b/GramAddict/core/behaviors/resonance_evaluator.py @@ -68,6 +68,10 @@ class ResonanceEvaluatorPlugin(BehaviorPlugin): else: if vibe.get("is_ad"): logger.info("🛡️ [Resonance Oracle] Visually identified post as an Ad! Skipping...") + marker = vibe.get("ad_marker_text") + if marker and marker.strip(): + from GramAddict.core.utils import learn_ad_marker + learn_ad_marker(marker, ctx.context_xml) from GramAddict.core.utils import humanized_scroll humanized_scroll(ctx.device) return BehaviorResult(executed=True, should_skip=True) diff --git a/GramAddict/core/utils.py b/GramAddict/core/utils.py index 92b4cac..c745906 100644 --- a/GramAddict/core/utils.py +++ b/GramAddict/core/utils.py @@ -1,4 +1,6 @@ import logging +import json +import os import random from time import sleep @@ -95,6 +97,62 @@ def get_value(count, name, default=0): return default +_LEARNED_AD_MARKERS_FILE = os.path.join(os.getcwd(), "learned_ad_markers.json") +_LEARNED_AD_MARKERS_CACHE = None + +def get_learned_ad_markers() -> set: + global _LEARNED_AD_MARKERS_CACHE + if _LEARNED_AD_MARKERS_CACHE is not None: + return _LEARNED_AD_MARKERS_CACHE + + if os.path.exists(_LEARNED_AD_MARKERS_FILE): + try: + with open(_LEARNED_AD_MARKERS_FILE, "r") as f: + _LEARNED_AD_MARKERS_CACHE = set(json.load(f)) + except Exception as e: + logger.error(f"Failed to load learned ad markers: {e}") + _LEARNED_AD_MARKERS_CACHE = set() + else: + _LEARNED_AD_MARKERS_CACHE = set() + + return _LEARNED_AD_MARKERS_CACHE + +def learn_ad_marker(marker: str, xml_hierarchy: str): + global _LEARNED_AD_MARKERS_CACHE + if not marker or len(marker) > 30: + return + + marker = marker.strip().lower() + + # Structural verification: the VLM-suggested marker MUST exist as an exact node text/desc in the current UI! + import xml.etree.ElementTree as ET + try: + root = ET.fromstring(xml_hierarchy) + found_in_ui = False + for node in root.iter("node"): + text = node.attrib.get("text", "").strip().lower() + desc = node.attrib.get("content-desc", "").strip().lower() + if text == marker or desc == marker: + found_in_ui = True + break + + if not found_in_ui: + logger.debug(f"🧠 [Autonomous FSD] Rejected hallucinated Ad marker '{marker}' (not found as exact node match in UI).") + return + except Exception: + return + + markers = get_learned_ad_markers() + if marker not in markers and marker not in {"ad", "sponsored", "advertisement", "gesponsert", "anzeige", "werbung"}: + markers.add(marker) + logger.info(f"🧠 [Autonomous FSD] Verified and Learned new Ad marker: '{marker}'. Persisting for zero-latency detection.", extra={"color": f"{Style.BRIGHT}{Fore.GREEN}"}) + try: + with open(_LEARNED_AD_MARKERS_FILE, "w") as f: + json.dump(list(markers), f) + except Exception as e: + logger.error(f"Failed to save learned ad markers: {e}") + + def is_ad(xml_hierarchy: str, cognitive_stack: dict = None) -> bool: """ Checks if the current view contains an advertisement using autonomous learning. @@ -125,6 +183,7 @@ def is_ad(xml_hierarchy: str, cognitive_stack: dict = None) -> bool: # Standalone label patterns: match only when the text/desc IS the ad marker, # not when "ad" appears inside longer phrases like "Create messaging ad" AD_EXACT_LABELS = {"ad", "sponsored", "advertisement", "gesponsert", "anzeige", "werbung"} + AD_EXACT_LABELS.update(get_learned_ad_markers()) try: root = ET.fromstring(xml_hierarchy)