fluid/stable/: fluid-wtp-ocr-0.4.2 metadata and description

Simple index

OCR functions for WTP logbook images

author Ameya Kirtane
author_email ameya.kirtane@fluidanalytics.ai
classifiers
  • Programming Language :: Python :: 3
  • Programming Language :: Python :: 3.10
  • Programming Language :: Python :: 3.11
  • Programming Language :: Python :: 3.12
  • Programming Language :: Python :: 3.13
  • Programming Language :: Python :: 3.14
description_content_type text/markdown
requires_dist
  • dotenv (>=0.9.9,<0.10.0)
  • google-cloud-documentai (>=3.14.0,<4.0.0)
  • google-genai (>=2.10.0,<3.0.0)
  • opencv-python (>=4.12.0.88,<6.0.0.0)
  • pydantic (>=2.13.4,<3.0.0)
requires_python >=3.10

Because this project isn't in the mirror_whitelist, no releases from root/pypi are included.

File Tox results History
fluid_wtp_ocr-0.4.2-py3-none-any.whl
Size
52 KB
Type
Python Wheel
Python
3
fluid_wtp_ocr-0.4.2.tar.gz
Size
40 KB
Type
Source
  • Replaced 1 time(s)
  • Uploaded to fluid/stable by fluid 2026-08-06 08:05:14

fluid-wtp-ocr

OCR library for Water Treatment Plant (WTP) logbook images. It sends page images to Google Cloud Document AI custom processors and converts the raw entity output into typed, structured Pydantic models.

Features

Requirements

Installation

poetry install

Or install the package directly:

pip install .

Configuration

Set the following environment variables before calling the OCR functions:

Variable Description
OCR_PROJECT_ID Google Cloud project ID
OCR_LOCATION Document AI processor region (for example asia-south1)
WTP_LEFT_PAGE_OCR_PROCESSOR_ID Document AI processor ID for the WTP left-page extractor (legacy path; production uses Gemini for this page, see below)
WTP_RIGHT_PAGE_OCR_PROCESSOR_ID Document AI processor ID for the WTP right-page extractor (legacy path; production uses Gemini for this page, see below)
WTP_COAGULATION_OCR_PROCESSOR_ID Document AI processor ID for the WTP coagulation extractor — this page still runs on Document AI in production
GEMINI_API_KEY Gemini API key, required by get_wtp_left_page_ocr_gemini/get_wtp_right_page_ocr_gemini in gemini_ocr/ — the RO left/right page path production actually uses

Usage

WTP left page

from fluid_wtp_ocr.get_structured_ocr import get_wtp_left_page_ocr

ocr_result = get_wtp_left_page_ocr(
    file_path="/path/to/logbook-page.pdf",
    mime_type="application/pdf",
)

print(ocr_result.time_elapsed)
print(ocr_result.result.date)

for row in ocr_result.result.log_rows or []:
    if row.sand_filter_entry and row.sand_filter_entry.ph:
        print(row.sand_filter_entry.ph.value)

WTP right page

from fluid_wtp_ocr.get_structured_ocr import get_wtp_right_page_ocr

ocr_result = get_wtp_right_page_ocr(
    file_path="/path/to/logbook-page.pdf",
    mime_type="application/pdf",
)

print(ocr_result.time_elapsed)
print(ocr_result.result.date)

WTP coagulation page

from fluid_wtp_ocr.get_structured_ocr import get_wtp_coagulation_log_ocr

ocr_result = get_wtp_coagulation_log_ocr(
    file_path="/path/to/logbook-page.pdf",
    mime_type="application/pdf",
)

print(ocr_result.time_elapsed)
print(ocr_result.result.date)

Supported MIME types depend on the input file (for example application/pdf, image/jpeg, image/png).

Output structure

get_wtp_left_page_ocr returns an OCRResult[WTPLeftPageOCR]:

OCRResult(
    result=WTPLeftPageOCR(...),
    time_elapsed=1.23,
)

WTPLeftPageOCR contains:

Each field on an entry is an OcrField[T]:

OcrField(
    raw_text="8.02",
    value=Decimal("8.02"),
    confidence=0.999,
    entity_id="4",
    normalized_text="8.02",
)

Supported left-page entry types

Stage Model Example fields
Sand filter SandFilterEntry time, pH, turbidity, free chlorine, m/p alkalinity
UF UfEntry time, turbidity
ACF AcfEntry time, pH, turbidity, m alkalinity, total hardness, total chlorine
Lead LeadEntry time, pH, turbidity, TDS
Lag LagEntry time, pH, TDS
RO RoEntry time, feed/permeate pH and TDS, turbidity

Architecture

Image/PDF
   │
   ▼
Google Document AI (custom processor)
   │
   ▼
RawOcrDocument          ← entities + full OCR text
   │
   ▼
OcrParser (per processor)
   │
   ▼
OCRResult[LogbookOCR]   ← typed domain models

Processors and parsers are registered by name:

To add a new document type, define a processor config, implement an OcrParser subclass, and register both in those maps.

Ground-truth regression testing

We keep a manually-reviewed set of real logbook pages (RO and coagulation) as a regression benchmark: whenever a prompt, model, or parser changes, re-run OCR against this set and compare the output to the reviewer-confirmed values, instead of eyeballing a handful of pages in the app.

Getting the fixture bundle

The dataset is exported from fluid-backend with the export_wtp_ocr_fixtures management command (see its docstring there for filtering by date/org/facility) and shared as a .zip — currently attached to the ENG-601 Linear issue. Download it somewhere outside this repo (a gitignored fixtures/ folder here works fine) — the zip contains real facility images and must never be committed.

Each record in the bundle has the source images, meta.json, and (when a reviewer has worked it) expected/review.json — the human-corrected values, which is what this tool treats as ground truth. expected/response_payload.json is only the previous OCR run, not a verified answer, so it's never scored against — it's there for your own before/after reference if you want it.

Running it

poetry run python scripts/run_ground_truth_eval.py \
  --fixtures fixtures/wtp_ocr_fixtures_20260728_061651.zip \
  --record-type ro \
  --report-csv ro_eval.csv \
  --save-baseline

Needs the same environment as normal OCR calls: GEMINI_API_KEY, OCR_PROJECT_ID, OCR_LOCATION, WTP_COAGULATION_OCR_PROCESSOR_ID (see Configuration above). Run once per record type (ro or coagulation) against the matching fixture zip.

Only records with a submitted review are scored by default — an in-progress or unreviewed record has no confirmed answer to compare against. Pass --include-unsubmitted-reviews to score them anyway.

To try it on a couple of records instead of the whole bundle (each --record-id is a live Gemini/Document AI call, so this keeps a quick sanity check cheap), pass --record-id — it's the fixture's records/<this> directory name, also in the CSV's record_id column. Two records per type with solid review coverage from the 2026-07-28/29 fixture bundles:

poetry run python scripts/run_ground_truth_eval.py \
  --fixtures fixtures/wtp_ocr_fixtures_20260728_061651.zip --record-type ro \
  --record-id 2025-04-10_ro_5782e5b1 --record-id 2025-10-13_ro_7cdbbe62 \
  --report-csv ro_smoke.csv

poetry run python scripts/run_ground_truth_eval.py \
  --fixtures fixtures/wtp_ocr_fixtures_20260729_121945.zip --record-type coagulation \
  --record-id 2026-06-29_coagulation_8c23bd55 --record-id 2026-06-17_coagulation_749a2b7b \
  --report-csv coag_smoke.csv

Reading the results

The console prints a per-record accuracy and error breakdown, then an aggregate by record type, and (if a prior --save-baseline run exists) how accuracy moved since then. --report-csv writes one row per compared field — open it in Sheets/Excel: each row is record, page, table, field, ground_truth_value, predicted_value, category, so you can filter down to just the rows that aren't match and go through them.

Every non-matching field is put into one of these categories:

Category Meaning
missed_field Ground truth has a value; OCR extracted nothing
hallucinated_value OCR extracted a value; ground truth has none
incorrect_transcription OCR read the wrong value
incorrect_normalization The raw OCR reading was actually correct, but the normalized/final value doesn't match — the bug is in normalization, not extraction
formatting_mismatch Same value, different formatting (punctuation/spacing) than ground truth
wrong_row_or_column_mapping A ground-truth value shows up unchanged somewhere else in the record — a row or table mix-up, not a reading error

date_check is a separate, informational-only row per record (never counted toward accuracy) comparing the extracted page date to the upload's confirmed log_date — a mismatch here is common and doesn't by itself mean the page date was misread; it can just mean the two dates were never expected to agree.

A record's accuracy is matched fields / all comparable fields — blank-on-both-sides fields aren't counted. Row alignment between OCR output and ground truth uses each entry's time field when present (tolerant of small time differences); tables without a time column, or where OCR badly over/under-counts rows, fall back to a positional match and will show elevated missed_field/hallucinated_value even when the OCR mostly got individual values right — read the CSV for those tables rather than trusting the aggregate percentage alone.

Development

Install dev dependencies:

poetry install --with dev

Run tests:

poetry run pytest

Lint:

poetry run ruff check .