Skip to content
Pace
Esc
navigateopen⌘Jpreview
On this page

Screen & vision

Screen & vision

How Pace sees the screen: cheap pixel diffing to know when to look, a local VLM to know what’s there, OCR/AX to ground text and structure, and a click-recovery loop for when a guess misses. Foundational frameworks (ScreenCaptureKit, the speculative planner race) are covered in new-things.md and referenced here, not re-explained.


Screen image diffing

  • What: A low-res grayscale fingerprint plus pixel-delta comparison that decides whether two screenshots differ “meaningfully” rather than pixel-identically.
  • Why here: The cheap pre-filter behind watch mode and the prewarm cache — avoids re-running the VLM or emitting an event for a blinking cursor or a clock tick.
  • Where: PaceScreenImageDiffer.swiftPaceScreenImageDiffer.fingerprint(...) and .diff(...), producing a PaceScreenVisualFingerprint / PaceScreenImageDiff.
  • Source: internal — no external spec.

Watch mode

  • What: An explicit watch-loop primitive that samples full-screen captures on an interval and emits typed events only past a diff threshold.
  • Why here: Powers “watch my screen” — PaceScreenWatchModeController.startWatching(onMeaningfulChange:) loops CompanionScreenCaptureUtility.captureAllScreensAsJPEG(), runs each capture through PaceScreenWatchChangeDetector, and only calls back (and journals) on a real PaceScreenWatchEvent.
  • Where: PaceScreenWatchMode.swiftPaceScreenWatchModeController, PaceScreenWatchEventCategory (.majorScreenChange / .contentUpdate / .focusedRegionChange).
  • Source: internal — no external spec.

Set-of-Mark click recovery

  • What: When a coordinate click misses, render numbered marks over candidate elements and ask the model which numbered mark corresponds to the intended target, instead of re-guessing raw coordinates.
  • Why here: Pace’s click-miss recovery path — PaceSetOfMarkClickRecovery.resolve(inputs:renderMarks:groundMark:) takes the failed click’s screenshot + element map, renders marks, asks the VLM to pick a mark index, and turns that back into a screen coordinate. Pure decision logic — rendering and the VLM round-trip are injected closures, so it’s unit-testable without AppKit or a live model.
  • Where: PaceSetOfMarkClickRecovery.swiftPaceSetOfMarkClickRecovery.resolve.
  • Source: https://arxiv.org/abs/2310.11441 (Set-of-Mark Prompting Unleashes Extraordinary Visual Grounding in GPT-4V)

Set-of-Mark renderer

  • What: The drawing half of Set-of-Mark — takes a JPEG and a list of boxes, draws numbered outlines + index labels onto it.
  • Why here: Separates “what to draw” from “what to decide” — PaceSetOfMarkRenderer.drawMarks(onJPEG:boxes:) is the pure rendering function PaceSetOfMarkClickRecovery injects as its renderMarks closure.
  • Where: PaceSetOfMarkRenderer.swiftPaceSetOfMarkRenderer.drawMarks(onJPEG:boxes:).
  • Source: internal — no external spec.

Set-of-Mark mark-reading measurement

  • What: Set-of-Mark itself — numbering candidate regions on a screenshot so a VLM answers “which numbered mark” instead of emitting raw coordinates — is the technique behind both the click-recovery entries above; this entry is specifically about measuring how accurately a given VLM reads the marks.
  • Why here: Pace shipped Set-of-Mark click recovery without ever measuring the one number that determines whether it helps — how often the VLM picks the right mark index. scripts/eval-vlm-mark-reading.py closes that gap: it generates 119 synthetic Set-of-Mark cases (12 layouts × 5–15 elements) using PaceSetOfMarkRenderer’s exact drawing convention, so the eval measures the real rendering path, not an approximation of it.
  • Where: scripts/eval-vlm-mark-reading.py (mark-reading accuracy on synthetic layouts) and scripts/eval-vlm-grounding.py (real-VLM fixture eval against an AX-blind baseline, capture-grounding-corpus.sh builds the fixture corpus). As of 2026-07-12 both scripts exist and run against LM Studio, but per PROJECT_STATUS.md, first numbers are still pending an actual LM Studio run — treat any specific accuracy figure as not yet measured until that entry is updated.
  • Source: https://arxiv.org/abs/2310.11441 (same paper as Set-of-Mark click recovery above).

Vision OCR client

  • What: A wrapper around Apple’s Vision text-recognition request, run at the accurate recognition level, returning text plus bounding boxes.
  • Why here: Grounds “what does that text say” / “click the button labeled X” turns in real on-screen text rather than relying on the VLM’s element-map guesses alone — fully on-device, ~50-200 ms per screenshot.
  • Where: PaceVisionOCRClient.swiftPaceVisionOCRClient.recognizeText(...), built on VNRecognizeTextRequest.
  • Source: https://developer.apple.com/documentation/vision/vnrecognizetextrequest

OCR data detector

  • What: Runs Foundation’s NSDataDetector over OCR’d screen text to pull out typed entities — phone numbers, email addresses, URLs, dates, postal addresses.
  • Why here: Turns raw OCR text into actionable entities for turns like “add this date to my calendar” or “open that link” without an LLM call to extract them — the same trick TextEdit uses for its data-detector highlights, fully on-device.
  • Where: PaceOCRDataDetector.swiftPaceDetectedEntity, PaceOCRDataDetector.detectEntities(in:).
  • Source: https://developer.apple.com/documentation/foundation/nsdatadetector

OCR language resolver

  • What: A pure helper that intersects the user’s Locale.preferredLanguages with Vision’s supported OCR recognition languages, falling back to en-US if the intersection is empty.
  • Why here: Without it, OCR silently mis-reads non-English UIs (umlauts, kana, etc.) because Vision defaults to en-US; both language lists are injected, so this stays actor-free and unit-testable without a live Locale or Vision request.
  • Where: PaceVisionOCRLanguageResolver.swiftPaceVisionOCRLanguageResolver.resolveRecognitionLanguages(preferredLanguagesFromLocale:supportedLanguagesFromVision:).
  • Source: internal — no external spec.

Ambient context store

  • What: A permission-free snapshot of the user’s current context — frontmost app name/bundle ID, window title, a lightweight AX-tree summary, and clipboard change count/timestamp.
  • Why here: Cheap situational grounding the planner gets on every turn without a screenshot or VLM call — PaceAmbientContextStore polls NSWorkspace.shared.frontmostApplication plus a background AX read and publishes a PaceAmbientContextSnapshot that renders into the planner prompt.
  • Where: PaceAmbientContextStore.swiftPaceAmbientContextStore, PaceAmbientContextSnapshot.
  • Source: internal — no external spec.

Screen context service

  • What: The per-screen VLM analysis cache and AX+OCR+VLM coordinator, keyed by screen/display identity plus a pixel hash of the captured image.
  • Why here: Avoids paying VLM latency twice for the same unchanged screen — PaceScreenContextService.prewarmScreenContext(reason:) kicks off analysis speculatively at push-to-talk press (.pushToTalkPress) and at deeplink chat (.deepLinkChat); a pixel-hash match against PaceCachedScreenAnalysis reuses the cached element map instead of re-calling the VLM.
  • Where: PaceScreenContextService.swiftPaceScreenContextService, PaceCachedScreenAnalysis, PaceScreenContextPrewarmReason.
  • Source: internal — no external spec.

Local VLM element maps

  • What: The client that sends a screenshot to the local VLM (LM Studio, OpenAI-compatible chat-completions) and parses back a structured element map — label, role, bounding box, and optional verbatim text per UI element.
  • Why here: This is what makes screen-action turns groundable in real coordinates instead of hallucinated ones — LocalVLMClient.analyzeScreenshot(...) builds the vision chat-completion request, and groundMarkedClickTarget(...) is the same client’s Set-of-Mark grounding call. Both go through the loopback-only endpoint guard, never leaving the Mac.
  • Where: LocalVLMClient.swiftLocalVLMClient (conforms to PaceScreenAnalysisClient), LocalVLMScreenElement, LocalVLMScreenAnalysis.
  • Source: internal — loopback OpenAI-compatible endpoint; see LM Studio entry in new-things.md.

VLM-skip heuristic

  • What: A rule-based check on the transcript text that decides whether a turn is likely to reference the screen at all, before paying VLM latency.
  • Why here: Pure-knowledge turns (“what’s the capital of France?”) don’t need a screenshot — PaceTagParsers.transcriptIsLikelyScreenReferential gates the VLM call so those turns skip straight to the text-only planner path; AlwaysRunLocalVLMRegardlessOfTranscript=true overrides it for testing.
  • Where: PaceTagParsers.swiftPaceTagParsers.transcriptIsLikelyScreenReferential(_:).
  • Source: internal — no external spec.

See also README.md for the full learning-roadmap index.

Was this page helpful?