diff --git a/GramAddict/core/behaviors/obstacle_guard.py b/GramAddict/core/behaviors/obstacle_guard.py index cd29e67..d58489e 100644 --- a/GramAddict/core/behaviors/obstacle_guard.py +++ b/GramAddict/core/behaviors/obstacle_guard.py @@ -47,21 +47,27 @@ class ObstacleGuardPlugin(BehaviorPlugin): logger.warning("⚠️ [ObstacleGuard] System permission dialog detected. Dismissing with BACK...") ctx.device.press("back") sleep(1.5 * ctx.sleep_mod) - return BehaviorResult(executed=True, should_skip=True) + res = BehaviorResult(executed=True, should_skip=True) + res.skip_type = "no_scroll" + return res # ── Foreign App Takeover (e.g. browser opened, wrong app in foreground) ── if situation == SituationType.OBSTACLE_FOREIGN_APP: logger.warning("⚠️ [ObstacleGuard] Foreign app detected. Pressing BACK to recover...") ctx.device.press("back") sleep(1.5 * ctx.sleep_mod) - return BehaviorResult(executed=True, should_skip=True) + res = BehaviorResult(executed=True, should_skip=True) + res.skip_type = "no_scroll" + return res # ── On-Screen Keyboard (e.g. hallucinated click on comment field) ── if situation == SituationType.OBSTACLE_KEYBOARD: logger.warning("⚠️ [ObstacleGuard] On-screen Keyboard is open. Pressing BACK to dismiss...") ctx.device.press("back") sleep(1.0 * ctx.sleep_mod) - return BehaviorResult(executed=True, should_skip=True) + res = BehaviorResult(executed=True, should_skip=True) + res.skip_type = "no_scroll" + return res # ── Instagram Modal / Overlay (survey, "Not Now" prompt, creation flow) ── if situation == SituationType.OBSTACLE_MODAL: @@ -88,6 +94,8 @@ class ObstacleGuardPlugin(BehaviorPlugin): else: ctx.shared_state["consecutive_marker_misses"] = misses + 1 - return BehaviorResult(executed=True, should_skip=True) # Restart loop for same post or next + res = BehaviorResult(executed=True, should_skip=True) # Restart loop for same post or next + res.skip_type = "no_scroll" + return res return BehaviorResult(executed=False) diff --git a/GramAddict/core/bot_flow.py b/GramAddict/core/bot_flow.py index febfc44..5c6a599 100644 --- a/GramAddict/core/bot_flow.py +++ b/GramAddict/core/bot_flow.py @@ -1134,6 +1134,9 @@ def _run_zero_latency_feed_loop( elif skip_type == "fast": _humanized_scroll(device, is_skip=True) sleep(1.0 * sleep_mod) + elif skip_type == "no_scroll": + logger.debug("⏭️ Skipping post without scrolling (e.g., dismissing obstacle).") + sleep(1.0 * sleep_mod) else: _humanized_scroll(device, is_skip=False) sleep(1.5 * sleep_mod) diff --git a/GramAddict/core/situational_awareness.py b/GramAddict/core/situational_awareness.py index 9cfac9b..5371c1c 100644 --- a/GramAddict/core/situational_awareness.py +++ b/GramAddict/core/situational_awareness.py @@ -347,8 +347,21 @@ class SituationalAwarenessEngine: "com.apple.android.inputmethod", } if any(pkg in keyboard_pkgs for pkg in packages): - logger.info("📱 [SAE Perceive] On-screen Keyboard explicitly detected. Treating as obstacle.") - return SituationType.OBSTACLE_KEYBOARD + # Differentiate between actual EditText focus and layout-induced phantom keyboard signals + is_actual_keyboard = False + if 'focused="true"' in xml_dump and "EditText" in xml_dump: + if re.search(r'class="[^"]*EditText"[^>]*focused="true"', xml_dump) or re.search( + r'focused="true"[^>]*class="[^"]*EditText"', xml_dump + ): + is_actual_keyboard = True + + if is_actual_keyboard: + logger.info( + "📱 [SAE Perceive] On-screen Keyboard explicitly detected (EditText focused). Treating as obstacle." + ) + return SituationType.OBSTACLE_KEYBOARD + else: + logger.debug("📱 [SAE Perceive] Phantom keyboard signal detected (No focused EditText). Ignoring.") # ── Foreign Environment Detection (package-based) ── # If the main app package is completely absent from the UI hierarchy, diff --git a/GramAddict/core/telepathic_engine.py b/GramAddict/core/telepathic_engine.py index a794f5b..841c321 100644 --- a/GramAddict/core/telepathic_engine.py +++ b/GramAddict/core/telepathic_engine.py @@ -85,6 +85,18 @@ class TelepathicEngine: filtered_candidates.append(c) candidates = filtered_candidates + # 2.5 Structural Sanity Check (Filter out traps before VLM sees them) + safe_candidates = [] + for c in candidates: + if self._structural_sanity_check(c, intent_description): + safe_candidates.append(c) + + if not safe_candidates: + logger.warning(f"All candidates failed structural sanity check for intent: '{intent_description}'") + return None + + candidates = safe_candidates + # 3. Resolve intent against candidates best_node = self._resolver.resolve(intent_description, candidates, device=device) @@ -224,13 +236,18 @@ class TelepathicEngine: def _is_instagram_context(self, xml_string: str) -> bool: return "com.instagram.android" in xml_string - def _structural_sanity_check(self, node: dict, intent_description: str, screen_height: int = 2400) -> bool: + def _structural_sanity_check(self, node: SpatialNode, intent_description: str, screen_height: int = 2400) -> bool: """ Structural guard to ensure nodes are in valid locations for their intents. """ intent = intent_description.lower() - y = node.get("y", 0) - semantic = (node.get("semantic_string", "") or "").lower() + y = node.center_y + semantic = ((node.text or "") + " " + (node.content_desc or "") + " " + (node.resource_id or "")).lower() + + # 0. EXTREME GUARD: NEVER pick an input field (EditText) or reply box unless explicitly asked to type/reply. + if "edittext" in (node.class_name or "").lower() or "reply" in semantic: + if "type" not in intent and "reply" not in intent and "message" not in intent and "search" not in intent: + return False # 1. Post Username Guard if "post username" in intent or "author username" in intent: @@ -277,7 +294,7 @@ class TelepathicEngine: # 6. Block massive layout containers UNLESS specifically looking for feed/post MAX_CONTAINER_AREA = 500000 - area = node.get("area", 0) + area = node.area is_feed_or_post = "feed" in intent or "post" in intent is_grid_item = "grid" in intent or "list" in intent diff --git a/tests/unit/test_telepathic_container_filtering.py b/tests/unit/test_telepathic_container_filtering.py index fa58501..cb3f6f3 100644 --- a/tests/unit/test_telepathic_container_filtering.py +++ b/tests/unit/test_telepathic_container_filtering.py @@ -11,15 +11,15 @@ def test_media_intent_rejects_grid_containers(): engine = TelepathicEngine() screen_height = 2400 + from GramAddict.core.perception.spatial_parser import SpatialNode + # Mock node representing a massive RecyclerView containing the entire grid # Area is 1080 * 2400 = 2592000 > MAX_CONTAINER_AREA (500000) - massive_grid_container = { - "semantic_string": "id context: 'swipeable nav view pager inner recycler view'", - "area": 2592000, - "y": 1200, - "class_name": "androidx.recyclerview.widget.RecyclerView", - "resource_id": "com.instagram.android:id/swipeable_nav_view_pager_inner_recycler_view", - } + massive_grid_container = SpatialNode( + bounds=(0, 0, 1080, 2400), + class_name="androidx.recyclerview.widget.RecyclerView", + resource_id="com.instagram.android:id/swipeable_nav_view_pager_inner_recycler_view", + ) # Intent intent = "first image post in profile grid"