Add auto-advance: click Next, scroll to top, capture next page

After each capture the controller asks the vision model to locate a 'Next'
control (returned as normalized screen coordinates), clicks it via OS input,
scrolls to top, and continues the loop. Configurable via SCREEN_LEADS_AUTO_NEXT
/ _NEXT_LOAD_PAUSE / _MAX_AUTO_NEXT, with a per-run safety cap.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-27 07:56:13 +05:30
parent c1eef4c6aa
commit 511e36b660
6 changed files with 93 additions and 0 deletions
+5
View File
@@ -7,6 +7,11 @@ ANTHROPIC_API_KEY=sk-ant-...
# Capture tuning
# SCREEN_LEADS_START_DELAY=5 # grace period to focus the browser (0 = off)
# SCREEN_LEADS_SCROLL_STEPS=6
# Auto-advance through paginated results
# SCREEN_LEADS_AUTO_NEXT=true # click a "Next" control after each capture
# SCREEN_LEADS_NEXT_LOAD_PAUSE=2.5 # seconds to wait for the next page to load
# SCREEN_LEADS_MAX_AUTO_NEXT=25 # safety cap on auto-advances per run
# SCREEN_LEADS_SCROLL_AMOUNT=800
# SCREEN_LEADS_SCROLL_PAUSE=0.8
# SCREEN_LEADS_WATCH_INTERVAL=2.0
+15
View File
@@ -40,3 +40,18 @@ def scroll_down(amount: int = SCROLL_AMOUNT) -> None:
pg = _pyautogui()
pg.scroll(-abs(amount))
time.sleep(SCROLL_PAUSE)
def click_at_fraction(x_frac: float, y_frac: float) -> None:
"""Click at a position given as fractions (0..1) of the logical screen.
Fractions are resolution- and Retina-independent: pyautogui reports the
logical screen size, and the capture covers the same primary display, so a
fraction of the screenshot maps directly to a fraction of the screen.
"""
pg = _pyautogui()
w, h = pg.size()
x = int(max(0.0, min(1.0, x_frac)) * w)
y = int(max(0.0, min(1.0, y_frac)) * h)
pg.moveTo(x, y, duration=0.2)
pg.click()
+36
View File
@@ -88,3 +88,39 @@ def classify_page(screenshot_path: str) -> dict[str, Any]:
output_config={"format": {"type": "json_schema", "schema": _CLASSIFY_SCHEMA}},
)
return _first_json(response)
_NEXT_SCHEMA = {
"type": "object",
"properties": {
"found": {"type": "boolean"},
"x": {"type": "number"}, # 0..1 fraction of image width (button centre)
"y": {"type": "number"}, # 0..1 fraction of image height
"label": {"type": "string"},
},
"required": ["found", "x", "y", "label"],
"additionalProperties": False,
}
_NEXT_PROMPT = (
"Look at this screenshot for a control that advances to the NEXT item or page — "
"for example a button or link labelled 'Next', 'Next result', 'See next profile', "
"or a right-facing pagination arrow ('>' / '' / ''). Ignore 'Back'/'Previous' "
"and unrelated arrows. If such a control is clearly visible, set found=true and give "
"its CENTRE position as fractions of the image: x = left→right (0.01.0), "
"y = top→bottom (0.01.0), plus its visible label. If none is visible, "
"return found=false, x=0, y=0, label=\"\"."
)
def find_next_button(screenshot_path: str) -> dict[str, Any]:
response = _client().messages.create(
model=ANTHROPIC_MODEL,
max_tokens=256,
messages=[{
"role": "user",
"content": [_image_block(screenshot_path), {"type": "text", "text": _NEXT_PROMPT}],
}],
output_config={"format": {"type": "json_schema", "schema": _NEXT_SCHEMA}},
)
return _first_json(response)
+6
View File
@@ -45,6 +45,12 @@ SCROLL_AMOUNT = int(_env("SCREEN_LEADS_SCROLL_AMOUNT", "800")) # pyautogui scro
SCROLL_PAUSE = float(_env("SCREEN_LEADS_SCROLL_PAUSE", "0.8")) # settle time (s)
WATCH_INTERVAL = float(_env("SCREEN_LEADS_WATCH_INTERVAL", "2.0")) # loop tick (s)
# Auto-advance: after capturing a profile, look for a "Next" control, click it,
# scroll back to the top, and keep going — walking paginated results hands-free.
AUTO_NEXT = _env("SCREEN_LEADS_AUTO_NEXT", "true").lower() in ("1", "true", "yes", "on")
NEXT_LOAD_PAUSE = float(_env("SCREEN_LEADS_NEXT_LOAD_PAUSE", "2.5")) # wait after click (s)
MAX_AUTO_NEXT = int(_env("SCREEN_LEADS_MAX_AUTO_NEXT", "25")) # safety cap per run
# What to do when the front tab is not an enabled site's target page.
# "stop" -> end the run (the requested behaviour)
# "wait" -> pause and keep polling until a valid page appears
+28
View File
@@ -44,6 +44,7 @@ class CaptureController:
self.last_event = ""
self.captured_urls: set[str] = set()
self.leads_captured = 0
self._advances = 0
self._run_id: Optional[int] = None
# --- introspection -------------------------------------------------------
@@ -55,6 +56,8 @@ class CaptureController:
"leads_captured": self.leads_captured,
"captured_urls": len(self.captured_urls),
"on_invalid_page": config.ON_INVALID_PAGE,
"auto_next": config.AUTO_NEXT,
"advances": self._advances,
}
def _set(self, state: State, event: str = "") -> None:
@@ -71,6 +74,7 @@ class CaptureController:
self._pause.clear()
self.reason = ""
self.leads_captured = 0
self._advances = 0
self.captured_urls.clear()
self._run_id = database.start_run()
self._set(State.RUNNING, "started")
@@ -149,6 +153,9 @@ class CaptureController:
if url:
self.captured_urls.add(url)
self.last_event = f"saved: {lead.full_name or url}"
# Auto-advance: click a Next control, scroll to top, capture again.
if config.AUTO_NEXT and not self._stop.is_set() and self._advance_next():
continue
time.sleep(config.WATCH_INTERVAL)
@@ -181,6 +188,27 @@ class CaptureController:
database.upsert_lead(lead)
return lead
def _advance_next(self) -> bool:
"""Look for a 'Next' control on screen; if found, click it and scroll to
top so the loop captures the next page. Returns True if it advanced."""
if self._advances >= config.MAX_AUTO_NEXT:
self.last_event = f"auto-next cap ({config.MAX_AUTO_NEXT}) reached"
return False
try:
shot = screenshot.capture(prefix="next")
info = extractor.find_next_button(shot)
if not info.get("found"):
return False
self.last_event = f"clicking '{info.get('label') or 'Next'}' → next page"
scroller.click_at_fraction(float(info.get("x", 0)), float(info.get("y", 0)))
time.sleep(config.NEXT_LOAD_PAUSE)
scroller.scroll_to_top()
self._advances += 1
return True
except Exception as e:
self.last_event = f"next-button step skipped: {e}"
return False
# module-level singleton shared by the API
controller = CaptureController()
+3
View File
@@ -147,6 +147,9 @@ python main.py</code></pre>
<tr><td><code>ANTHROPIC_API_KEY</code></td><td></td><td>Required. Claude API key.</td></tr>
<tr><td><code>SCREEN_LEADS_MODEL</code></td><td>claude-opus-4-8</td><td>Vision model (use claude-sonnet-5 to cut cost).</td></tr>
<tr><td><code>SCREEN_LEADS_START_DELAY</code></td><td>5</td><td>Grace seconds before first capture (0 = off).</td></tr>
<tr><td><code>SCREEN_LEADS_AUTO_NEXT</code></td><td>true</td><td>After each capture, click a "Next" control, scroll to top, and continue.</td></tr>
<tr><td><code>SCREEN_LEADS_NEXT_LOAD_PAUSE</code></td><td>2.5</td><td>Seconds to wait for the next page to load after clicking Next.</td></tr>
<tr><td><code>SCREEN_LEADS_MAX_AUTO_NEXT</code></td><td>25</td><td>Safety cap on auto-advances per run.</td></tr>
<tr><td><code>SCREEN_LEADS_SCROLL_STEPS</code></td><td>6</td><td>Screenshots per profile.</td></tr>
<tr><td><code>SCREEN_LEADS_SCROLL_AMOUNT</code></td><td>800</td><td>Scroll distance per step.</td></tr>
<tr><td><code>SCREEN_LEADS_SCROLL_PAUSE</code></td><td>0.8</td><td>Settle time (s) after each scroll.</td></tr>