OCR Fallback Boundary¶
TouchPilot's primary local screen-understanding path is the Android Accessibility service. Some surfaces (Canvas/SurfaceView rendering, custom views, games, intentionally obfuscated screens) expose weak or empty Accessibility trees. The OCR fallback boundary defined here exists so the rest of the agent has one place to ask "is what I am seeing usable, and if not, can OCR help?" without making OCR or a local VLM a Milestone 3 dependency.
Pipeline¶
text
Accessibility observation
─► screen context builder
─► ContextQualityDetector
─► Strong ─► summarize / suggest directly
─► Weak ─► OcrFallback.attempt(...)
─► Recognized: surface as untrusted text, do not act on it
─► Unavailable: respond honestly that the screen cannot be read
─► NotAttempted: same conservative copy as Unavailable
─► Empty ─► refuse to guess
Package¶
All boundary types live under dev.touchpilot.app.screen.ocr:
ObservedScreenSignals— small, platform-free value type carrying the counts the detector needs (total nodes, visible text count, clickable count, input field count, max depth, package name).ContextQualityDetector— returnsContextQuality.Strong,ContextQuality.Weak(reason), orContextQuality.Empty. Thresholds are injectable.WeakReason—NO_VISIBLE_TEXT,NO_CLICKABLE_NODES,SHALLOW_TREE,MOSTLY_EMPTY.OcrFallback— interface withattempt(OcrRequest): OcrFallbackResult.OcrFallbackResult— sealed:NotAttempted,Unavailable(reason),Recognized(text, confidence).NoOpOcrFallback— default boundary that always returnsUnavailable.WeakContextResponse— generates the honest copy callers should emit for weak/empty contexts.
The detector takes counts, not Android types, so it runs as a JVM unit test and
so a future caller can derive signals from either an AccessibilityNodeInfo
walk or a higher-level screen context model (for example the one being added in
#39).
Local-only constraint¶
OCR and VLM implementations behind this interface MUST run fully on device. The
project direction (see ROADMAP.md, LOCAL_INFERENCE.md) is 100% local for
core product behavior, and OCR is no exception. Cloud OCR is not part of this
boundary and should not be added behind it.
A future LiteRT, ExecuTorch, or llama.cpp-backed OCR/VLM runtime can implement
OcrFallback once the runtime is selected. Until then, the bundled
implementation is NoOpOcrFallback, which returns Unavailable and lets the
agent respond honestly instead of guessing.
Safety¶
- OCR output is untrusted noise. Any
Recognized.textMUST pass through the existingSensitiveTextRedactorbefore being written to logs or traces, and MUST NOT be used as a tool argument without passing through validation, the active skill allowlist, and the safety policy — the same pipeline that protects against any other untrusted command source. - Weak context must not cause unsafe guessing or blind tapping. The
WeakContextResponsecopy is intentionally conservative and includes "I will not act on it without confirmation" when recognized text is surfaced. - Empty context produces a refusal rather than a guess.
What is intentionally out of scope here¶
- Wiring the detector into the live
AccessibilityServiceobservation path — that belongs with the screen context builder (#40). - Choosing an OCR runtime and shipping a real
OcrFallbackimplementation — deferred to the local OCR/VLM milestone. - Capturing screen pixels for OCR — also deferred, and gated behind the same local-only and policy constraints when it lands.