You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Two weeks ago I shipped #353 — persistent lastSeen cache for image find via a PNG ancillary chunk. Idea was honest: the second time you look for the same image, you already paid the bill once; the second lookup should not pay it again. Measured cold-start speedup on repeated find(): ~×6.5.
Then I ran a notepad session with the word "Espagne" on screen and called findText("Espagne") eight times in a row. Each call took roughly six seconds. Eight calls, forty-eight seconds. The text never moved between calls of the same test step, and Tesseract was scanning the entire 1920×1080 screen every single time, just to confirm something it had already located one second earlier.
The obvious question came next: why do image lookups have a lastSeen cache but text lookups don't? The data shape is the same — a bounding box, a screen resolution stamp. The fallback semantics are the same — if the cached ROI no longer contains the target, scan the full region and update. The only thing #353 has that this cannot reuse is the PNG chunk as a carrier: text has no PNG file to attach to.
Full design story and measured numbers live in the design discussion linked in the badges — Discussion #400. This issue tracks the implementation.
Proposed solution
A new class org.sikuli.support.TesseractLastSeen — persistent per-text last-seen cache, JSON sidecar catalog, mirroring the image mechanism from #353.
Storage: tesseract-lastseen.json in the JVM working directory (user.dir). Colocated with the user's test project so it can be committed, shared across machines, and survives across runs. Deliberately out of ~/.oculix/: it is a project asset, not a per-user setting.
Key: (text, search-region context) pair. The same text sought in different sub-regions (notepad.findText("Save") vs chrome.findText("Save")) gets independent slots and never overwrites the other.
Flow:
On first lookup: full region scan (unchanged behaviour), bounding box of the returned Match is persisted under the (text, context) key.
On subsequent lookups for the same pair: only the cached box is rescanned — typically <50 ms vs >1000 ms for full screen.
If the cached region no longer contains the text (UI shifted, element moved): caller falls back to a full scan and the stale entry is invalidated.
Public API — three static methods on TesseractLastSeen:
RegiongetRegion(RegionsearchRegion, Stringtext); // returns cached ROI or nullvoidput(RegionsearchRegion, Stringtext, Matchmatch); // persist bounding boxvoidinvalidate(RegionsearchRegion, Stringtext); // drop stale entry
Injection site: single funnel in Region.doFind — the private method through which every text-finding API converges (findText, existsText, waitText, hasText, click(text), doubleClick(text), rightClick(text), hover(text), find(text), dragDrop(text, ...)). One modification at the funnel = full coverage across all public entry points, zero duplication.
Safety:
All reads/writes guarded by a single intra-process lock.
Writes are atomic (write tmp + ATOMIC_MOVE) so a crash mid-persistence never leaves corrupt JSON.
Any IO/parse failure is swallowed and the OCR call proceeds as if no cache existed — the cache layer must never break the user's automation.
Screen-resolution-aware: entries captured on 1920×1080 are ignored when the current screen is a different size, so cached coordinates never point off-screen on a different setup.
Zero padding on purpose: cached box is exact, any UI shift >0 px triggers a fallback. Deterministic beats permissive.
Store in ~/.oculix/ as a per-user setting — the cache is inherently tied to a specific project (its screens, its UI, its resolutions). Colocating it in user.dir lets teams commit it with the test project, which turns a per-user optimization into a team-shared one.
Padded cached box for tolerance to small UI shifts — considered and rejected. Padding introduces ambiguity ("did the cache match or did the fallback?"). Determinism first: exact box, any shift triggers a full rescan and updates.
TTL-based invalidation — considered for future iteration but not in v1. The current invalidation (miss → full rescan → update) is sufficient for the reported use case. TTL can be added as an option later without breaking the wire format (FORMAT_VERSION field is reserved).
Where would this live?
API (Screen / Region / Pattern / Match)
PaddleOCR / Tesseract
API impact
Yes — new public API (backward-compatible). Adds org.sikuli.support.TesseractLastSeen as a utility class with three static methods. No existing method signature changes. The injection at Region.doFind is transparent: callers of findText, click(text), etc. see faster repeated lookups, nothing else.
Use case / impact
Anyone running OCR-heavy scripts on the same UI repeatedly — CI test suites, kiosk automation, screen readers, RPA workflows that revisit the same fields many times per run.
Measured cases in the PoC:
Notepad "Espagne" ×8 lookups: 48 s → <400 ms after warm-up (×120).
Full-screen findText on a typical desktop: >1000 ms cold → <50 ms warm (~×25 low bound).
Full design walkthrough, PoC measurements, and open design questions live in the design discussion linked in the badges — Discussion #400. Raimund's feedback there already covers the future dimensions (per-project scope, config surface via OCR.options, oculix-text-cache.json as future filename, cache-all-occurrences as future option). This issue tracks the v1 landing.
Format version is a first-class field in the JSON schema so future evolutions (TTL, multi-occurrence, padded boxes as opt-in) can be added without breaking existing catalogs.
Problem / motivation
Two weeks ago I shipped #353 — persistent
lastSeencache for image find via a PNG ancillary chunk. Idea was honest: the second time you look for the same image, you already paid the bill once; the second lookup should not pay it again. Measured cold-start speedup on repeatedfind(): ~×6.5.Then I ran a notepad session with the word "Espagne" on screen and called
findText("Espagne")eight times in a row. Each call took roughly six seconds. Eight calls, forty-eight seconds. The text never moved between calls of the same test step, and Tesseract was scanning the entire 1920×1080 screen every single time, just to confirm something it had already located one second earlier.The obvious question came next: why do image lookups have a lastSeen cache but text lookups don't? The data shape is the same — a bounding box, a screen resolution stamp. The fallback semantics are the same — if the cached ROI no longer contains the target, scan the full region and update. The only thing #353 has that this cannot reuse is the PNG chunk as a carrier: text has no PNG file to attach to.
Full design story and measured numbers live in the design discussion linked in the badges — Discussion #400. This issue tracks the implementation.
Proposed solution
A new class
org.sikuli.support.TesseractLastSeen— persistent per-text last-seen cache, JSON sidecar catalog, mirroring the image mechanism from #353.Storage:
tesseract-lastseen.jsonin the JVM working directory (user.dir). Colocated with the user's test project so it can be committed, shared across machines, and survives across runs. Deliberately out of~/.oculix/: it is a project asset, not a per-user setting.Key:
(text, search-region context)pair. The same text sought in different sub-regions (notepad.findText("Save")vschrome.findText("Save")) gets independent slots and never overwrites the other.Flow:
Matchis persisted under the(text, context)key.Public API — three static methods on
TesseractLastSeen:Injection site: single funnel in
Region.doFind— the private method through which every text-finding API converges (findText,existsText,waitText,hasText,click(text),doubleClick(text),rightClick(text),hover(text),find(text),dragDrop(text, ...)). One modification at the funnel = full coverage across all public entry points, zero duplication.Safety:
ATOMIC_MOVE) so a crash mid-persistence never leaves corrupt JSON.Alternatives considered
~/.oculix/as a per-user setting — the cache is inherently tied to a specific project (its screens, its UI, its resolutions). Colocating it inuser.dirlets teams commit it with the test project, which turns a per-user optimization into a team-shared one.FORMAT_VERSIONfield is reserved).Where would this live?
API impact
Yes — new public API (backward-compatible). Adds
org.sikuli.support.TesseractLastSeenas a utility class with three static methods. No existing method signature changes. The injection atRegion.doFindis transparent: callers offindText,click(text), etc. see faster repeated lookups, nothing else.Use case / impact
findTexton a typical desktop: >1000 ms cold → <50 ms warm (~×25 low bound).Additional context
Full design walkthrough, PoC measurements, and open design questions live in the design discussion linked in the badges — Discussion #400. Raimund's feedback there already covers the future dimensions (per-project scope, config surface via
OCR.options,oculix-text-cache.jsonas future filename, cache-all-occurrences as future option). This issue tracks the v1 landing.Format version is a first-class field in the JSON schema so future evolutions (TTL, multi-occurrence, padded boxes as opt-in) can be added without breaking existing catalogs.
🦎