Repository navigation
feat(ocr): a local Tesseract OCR service (deploy/ocr) - #7184
Conversation
Tesseract 5.5 with eng/deu/fra/ita/ind + osd baked into the image, behind a
stdlib HTTP server implementing the MeshWeaver.Ocr HttpService contract
(POST /ocr?languages=… → {text, languages, engine}). Language detection by
osd orientation + per-paragraph distinctive function words; hints restrict or
extend the set. Bounded body, pixels, deadline and concurrency; no egress.
The image build runs offline tests and a real-engine smoke on committed
German+Indonesian fixtures with negative controls; ocr-image.yml adds a
red-without-German control, the real CMD over HTTP, a chart render check,
and on main pushes tesseract-5.5-<sha> + the declared tesseract-5.5 line.
The helm chart renders nothing unless enabled; the portal chart passes
Ocr__Engine / Ocr__HttpService__Endpoint only when set.
Docs: Architecture/OcrService (topic map), End Vision G10/G11 updated.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
The negative control aborts CI on its expected result, and request buffering, transparency handling, and image-tag validation need correction.
4 open issues
What changed in this PR
Adds a local Tesseract OCR service as the server counterpart to the MeshWeaver.Ocr HTTP client. Deployment remains disabled until explicitly enabled for an instance.
Changes:
- Adds image recognition, orientation correction, language detection, and HTTP request limits.
- Adds container tests, image publishing, and a standalone Helm chart with restricted networking.
- Exposes optional portal configuration and documents deployment and release integration.
| File | Description |
|---|---|
src/MeshWeaver.Documentation/Data/Architecture/ReleaseEditions.md |
Lists the OCR release channel. |
src/MeshWeaver.Documentation/Data/Architecture/OcrService.md |
Documents the service contract and enablement. |
src/MeshWeaver.Documentation/Data/Architecture/DocumentIngestionEndVision.md |
Updates OCR delivery status. |
src/MeshWeaver.Documentation/Data/Architecture/DockerHubInCi.md |
Includes the OCR base-image build. |
src/MeshWeaver.Documentation/Data/Architecture.md |
Links the OCR architecture page. |
deploy/ocr/README.md |
Describes API, testing, and installation. |
deploy/ocr/helm/values.yaml |
Defines disabled-by-default deployment settings. |
deploy/ocr/helm/templates/service.yaml |
Exposes a cluster-internal service. |
deploy/ocr/helm/templates/networkpolicy.yaml |
Restricts ingress and denies egress. |
deploy/ocr/helm/templates/deployment.yaml |
Configures the hardened pod and probes. |
deploy/ocr/helm/templates/_helpers.tpl |
Defines chart names and labels. |
deploy/ocr/helm/Chart.yaml |
Declares OCR chart metadata. |
deploy/ocr/fixtures/make_fixtures.py |
Generates multilingual smoke fixtures. |
deploy/ocr/Dockerfile |
Packages Tesseract and runs build-time tests. |
deploy/ocr/app/test_server.py |
Tests decisions and HTTP limits offline. |
deploy/ocr/app/smoke.py |
Exercises real OCR with negative controls. |
deploy/ocr/app/server.py |
Implements HTTP endpoints and concurrency control. |
deploy/ocr/app/ocr.py |
Implements recognition and language identification. |
deploy/helm/templates/memex-portal/config.yaml |
Emits optional OCR client settings. |
.github/workflows/ocr-image.yml |
Builds, checks, and publishes OCR images. |
.github/release-channels.json |
Registers the moving OCR service tag. |
Review effort: Balanced
Test Results 27 files ±0 27 suites ±0 52m 29s ⏱️ +28s Results for commit 66719cc. ± Comparison against base commit ee791b6. This pull request removes 69 and adds 37 tests. Note that renamed tests count towards both.♻️ This comment has been updated with latest results. |
Pull request was converted to draft
…the negative control capturable - ocr-image.yml: the negative-control step ran under Actions' `bash -e`, so the smoke's expected non-zero exit aborted the step before `code=$?` (the PR's only red). The status is now captured through `||`, and the step demands an ASSERTION red: the German check's FAIL line, the smoke's own "<n> failure(s)" equal to the exit code, and no docker 125/126/127. - server.py: a request takes one of OCR_MAX_ADMITTED places (chart limits.maxAdmitted, default 8) BEFORE its body is read; none free -> 503, body unread. Buffered uploads are bounded by maxAdmitted x maxBytes instead of by the number of callers. - ocr.py: images with alpha (RGBA/LA/PA, or a tRNS key) are composited onto white instead of dropping alpha, which turned black-on-transparent text into one black page. - chart: image.tag must match tesseract-<line>-<hex sha>; the CI chart check now also refuses tesseract-5.5, latest and other moving tags. Regression tests (each red on the previous code): test_transparency_is_composited_onto_white (RGBA, LA, P+tRNS), test_admission_refuses_beyond_the_bound_without_reading_the_body, test_admission_below_concurrency_refuses_to_start, and the real-engine smoke check "transparent page (text only in alpha)". Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
| run: | | ||
| set -euo pipefail | ||
| short="${GITHUB_SHA::7}" | ||
| docker push "$ACR/ocr:${LINE}-${short}" |
There was a problem hiding this comment.
Valid. A rerun or workflow_dispatch at the same main commit would overwrite the tag the header calls immutable. Replied rather than pushed in this round, because nothing can pull a divergent build yet: the chart renders nothing by default (enabled: false), so no portal pulls the image until an estate enables it. I have not checked the private estate overlays. The write-once guard has three possible shapes: skip the build when the immutable tag already exists, telling an absent tag apart from a failed lookup; lock the tag after its first push; or bind deployments to the digest. It needs to land before any estate enables the chart. Tracked as follow-up: rbuergi/Feedback/ocr-image-write-once-and-upload-deadline-2026-10-25
| cfg.admission.release() | ||
|
|
||
| def _read_admitted(self, url, size: int): | ||
| data = self.rfile.read(size) |
There was a problem hiding this comment.
Correct: OCR_READ_TIMEOUT_SECONDS bounds inactivity, not total upload time, so a caller that trickles bytes holds its admission place. Replied rather than pushed in this round. The service is cluster-internal: the chart's NetworkPolicy admits ingress only from pods in the named allowedNamespaces, and an optional bearer key sits on top. So only an in-cluster client the policy already admits could exhaust the places, and an ordinary portal upload over the cluster network does not trickle. The fix is an absolute deadline started before buffering, enforced across incremental reads, that closes the connection and releases admission on expiry (504), with a test. Tracked as follow-up: rbuergi/Feedback/ocr-image-write-once-and-upload-deadline-2026-10-25
Every conflict resolved so the deletion wins: the chart files main changed (#7143, #7159, #7184, #7170, #7119; all ported to MeshWeaver.Deploy by Deploy#4), the chart-reading guards main moved onto RepoPaths (#7111), and the Memex.Portal.Shared files main moved to MeshWeaver.Plugins (#7125). Also deleted: the build-host chart files main added after this branch was cut (frozen, in Deploy now) and memex-local.yml (#7119), whose Linux gate is Deploy's homebrew.yml linux job. Kept from main: instances.json's re-measured MeshWeaver.Deploy row. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>


What
A local OCR service,
deploy/ocr, built the waydeploy/whisperanddeploy/s1-extractare. It becomes the fleet's default OCR engine, the server half of theHttpServiceengine inMeshWeaver.Ocr.Pairs with Refs Systemorph/MeshWeaver.Plugins#4204 (the client). This PR implements that branch's contract exactly:
POST /ocr?languages=de,idwith the image bytes, answered by{text, languages, engine}.deploy/ocr/is Tesseract 5.5 (Debian trixie) behind a stdlib HTTP server. Its only Python dependency is Pillow.eng,deu,fra,ita,ind, plusosd.osdturns the page upright and names the script, then each paragraph is identified by distinctive function words. A word two languages share counts for neither./healthzand/readyz..github/workflows/ocr-image.ymlis modelled ons1-extract-image.yml. On a PR it builds, and the build runs the tests. It also runs a negative control, the image's own CMD over HTTP, and a chart render check. Onmainit pushesmeshweaver.azurecr.io/ocr:tesseract-5.5-<sha>and:tesseract-5.5by OIDC and reads both back. The line is declared in.github/release-channels.json, so retention never prunes it.deploy/ocr/helmis disabled by default:enabled: falserenders nothing. When enabled, an immutable tag is required.Ocr__EngineandOcr__HttpService__Endpointare rendered only when set. An instance's record can then turn the client on. When they are absent, the client answers NotConfigured.Doc/Architecture/OcrService, listed in the topic map;DocumentIngestionEndVisionG10/G11 and its evidence paragraph updated;ReleaseEditionsandDockerHubInCiupdated.Evidence (local, colima docker)
The image builds, and the build is the test:
test_server.py: 17 tests OK, offline with a fake engine.smoke.py: 12/12 PASS against the real Tesseract 5.5.0 onfixtures/de-id.png.['de','id'].languages=de,idreads both.Negative controls:
fixtures/en.png. All six words are missing, andlanguages ['en'] != ['de','id'].{"text": "", "languages": []}.languages=enreports neitherdenorid.OCR_DEFAULT_LANGUAGES=en,id, so German cannot be read, exits 2:missing 'Gebäude'; languages ['id'] != ['de','id'], ×2.Other checks:
--network none --read-onlythe smoke passes (exit 0).helmwas not run locally. The CI step renders the chart three ways.Enabling it on an instance (owner-gated, not in this PR)
These steps run in the estate's deployment repository, following the whisper precedent. The detailed steps are in
Doc/Architecture/OcrService→ Enabling it on an instance:enabled: true, the immutable tag and the portal namespace;Hosting/ChartReleaserecordDeployments/<env>-ocr, and get theApplyChartapproved;extraPortalConfigtoOcr__Engine=HttpServiceandOcr__HttpService__Endpoint=http://ocr:8080/ocr, then roll;Nothing needs a recycle: no per-node hub serves this.
🤖 Generated with Claude Code