Repository navigation
Releases: ufal/atrium-ocr-postprocess
Release list
GHA fixes + shared scripts update
Patch release on top of v1.15.0-beta, and the S2 pilot candidate (atrium-project#77).
Fixed
- A
document_json/document_recordwhosepagesorlinesis not a list of objects is refused with 422invalid_recordon/processand/score_record; it used to end in a 500 (atrium-project#72). The batch reports the same error instead of anAttributeError.
Changed
- The
invalid_recorddescription inopenapi.jsonnames this case; no endpoint, field or status changes. - CI: the hub's workflows are pinned to the commit with the shared fix, including the Trivy database retry.
Images this release certifies (images.json, atrium-project#81):
ghcr.io/ufal/atrium-ocr-postprocess:1.15.1-beta=sha256:e1ed70c817400d6d2509db7a5f09532a7e246b2385ac931c17b62909b9a6665cghcr.io/ufal/atrium-ocr-postprocess-api:1.15.1-beta=sha256:6928575c68679fcff4c76b13f04003ccae7b4f847aeb529a747c6695280e2a5c
Full Changelog: v1.15.0-beta...v1.15.1-beta
API-CLI alignment of results
v1.15.0-beta: the batch and the service agree at the edges
What changed (atrium-project#72)
- The batch's LayoutReader method keeps an ALTO page without
WIDTH/HEIGHTand a word without coordinates, as the service does. Before, it dropped them. - A page that runs out of GPU memory even at the smallest LayoutReader window keeps the ALTO's own order, as in the service. Before, the batch skipped the page.
- The service reads its perplexity model from
[CLASSIFY] MODEL_NAMEinsetup/config.txt, as the batch does;GPT2_MODEL_NAMEstill comes first. The shipped config names the same model as before (Qwen2.5-0.5B). - The batch record's page
quality_scoreis rounded to four decimals, as the service's. - Both web frontends compute "Avg Quality" from
ClearandNoisylines only, as the API does. /infolists every line field (original_lang,orig_lang_score,page,page_labeladded); the spec's field descriptions are corrected.
Compatibility
- The API is unchanged apart from the version. Batch outputs now include lines and pages that were dropped before.
Upgrade notes
- Pin
1.15.0-beta. Changing[CLASSIFY] MODEL_NAMEnow changes the service's model too, unlessGPT2_MODEL_NAMEis set.
Images this release certifies (images.json, atrium-project#81):
ghcr.io/ufal/atrium-ocr-postprocess:1.15.0-beta=sha256:7c4fb7eceda555a4e6d996e40ad93fd2250061c68fdfa542853c3925d029b88cghcr.io/ufal/atrium-ocr-postprocess-api:1.15.0-beta=sha256:8090aace5db566ca6b24e3fa15a85912fe2423422869632c6d0eb0c0a5588ebc
Full Changelog: v1.14.0-beta...v1.15.0-beta
API-CLI parity fixes
v1.14.0-beta: the same lines and page score from the command line and the service
What changed (atrium-project#72)
- Both sides now keep blank lines as
Emptylines, numbered with the rest, so the service'sline_nummatches the batch's for the same page.- In ALTO, blank lines are the paragraph breaks that the text extraction writes.
- In plain text and documents, blank lines are kept only with
[TEXT_INGEST] KEEP_BLANK_LINES = true. The service now reads this setting too. The default staysfalse.
reading_order=altonow reads the page with alto-tools, as--method alto-toolsdoes: a blank line before each text block, and a hyphenated line joined to the next.alto_tools.py(Apache-2.0) is now part of the service image and bundle.- A page's
quality_scorefrom/process,/score_recordandscore_record.pyis now the mean of itsClearandNoisylines, as in the batch page statistics. It used to includeTrashlines. docs/usage.mdexplains what counts as a blank line.
Compatibility
- The API fields are unchanged. The spec differs only in its version and two descriptions.
- An ALTO upload's
cleaned_linesnow includesEmptylines, so the lines after them get higherline_numvalues. A record written by the service gets those rows too. - Plain-text and document uploads give the same lines as before unless
KEEP_BLANK_LINESis set. - A page's
quality_scorechanges if the page hasTrashlines. A page with onlyTrashlines now has no score. - With
reading_order=alto, an ALTO file in a namespace that alto-tools does not know now gets a 422.
Upgrade notes
- Pin
1.14.0-beta. - A client that expects only lines with text should skip lines whose
categoryisEmpty.
Images this release certifies (images.json, atrium-project#81):
ghcr.io/ufal/atrium-ocr-postprocess:1.14.0-beta=sha256:a2f00af5d09b439c9ef9b031297e46ed540ef9c11ac4426ac5226ee395b32d8aghcr.io/ufal/atrium-ocr-postprocess-api:1.14.0-beta=sha256:880b025c57ada6194dcd109ae4a2ad6b75b013f9adf7d79a7c1dca495154a4ce
Full Changelog: v1.13.1-beta...v1.14.0-beta
API-CLI alignment and citation updated
v1.13.1-beta: the release v1.13.0-beta did not get
What changed
- The v1.13.0-beta tag pushed its images, but no GitHub release was made: the service bundle was missing
record_scoring.py. It ships now, andtests/test_release_bundle.pyruns the release's bundle check on every push. .github/production-image.jsonno longer listsscore_record.py(it is the command line, which the service does not reach), so Workflow Policy Lint passes again.- Everything from v1.13.0-beta is included:
- text with form feeds and multi-page JSON come back page by page from
/process; - the opt-in
reading_order=altoandinclude_record; score_record.pyon the command line;- one split-word merge shared by the command line and the service.
- text with form feeds and multi-page JSON come back page by page from
Compatibility
- The API is unchanged apart from the version.
Upgrade notes
-
Pin
1.13.1-beta. The1.13.0-betaimages exist but have no release notes.
Images this release certifies (images.json, atrium-project#81): -
ghcr.io/ufal/atrium-ocr-postprocess:1.13.1-beta=sha256:c0ac7d12c3bca2f3461b71e4cebf603d53afa909e4768b47df8cdcc752b21674 -
ghcr.io/ufal/atrium-ocr-postprocess-api:1.13.1-beta=sha256:d6e4d68011a0552f6f3cda5aaf2be608eab3ddab475313dae655e86ad56ca2c2
Full Changelog: v1.12.1-beta...v1.13.1-beta
Minor changes and work on issues as pre-pilot preparation
Note
Same code as v1.12.0-beta, whose images were never published (its Docker run stopped at the test step). This release publishes ghcr.io/ufal/atrium-ocr-postprocess:1.12.1-beta and ghcr.io/ufal/atrium-ocr-postprocess-api:1.12.1-beta, and moves latest from 1.11.0-beta.
What's new
tests/test_categorization_walkthrough.pycompares the walkthrough table cell by cell, not its alignment.
What changed since v1.11.0-beta is in the notes of v1.12.0-beta.
Compatibility
The spec changes only its version. Thresholds, weights and batch output are those of v1.12.0-beta.
Full Changelog: v1.12.0-beta...v1.12.1-beta
Pre-production fixes and test added
Production-readiness release. Thresholds, weights and word lists are unchanged and batch output is identical, so DOC_LINE_CATEG_1210 stays valid.
Changed for API callers
/processand/score_recordnow run the CPU pre-filter first, as the batch pipeline does. Numeric, code-like and blank lines come backNon-textorEmpty(score 0.0, languageN/A) without a model call; every other line is scored on its repaired text. Such lines that were answeredClear,NoisyorTrashbefore change category. A page's score averagesClear,NoisyandTrashlines only.PPL_BATCH_LINESdefaults to 32. A.envcopied from an older.env.examplestill sets 128.- GPU work is serialised per process, and
GET /health?deep=truefails while no model is loaded. - Paradata names the perplexity model and
layoutlmv3when they ran, so the recorded licence no longer understates an API run./score_recordtakes its page and line caps from the configuration. - A JSON or text upload without
pageonto a multi-page record is skipped with a warning instead of landing on page 1.
Reliability
- A perplexity batch that runs out of GPU memory is split and retried; only a line that fails on its own scores 99999. Values are unchanged.
- The ordering of
CATEG_TRASH_SCORE_MAX<CATEG_NOISY_SCORE_MAX,SHORT_PPL_CAP<PERPLEXITY_THRESHOLD_MAXand the positive weight sum is checked at startup: the service refuses to start andclassify_TEXT.pyexits before loading a model. - Per-document CSVs are written as
<doc>.csv.partand renamed when complete, so a failed re-classification keeps the previous file. A leftover.partis safe to delete. - Exit status is non-zero when
run_pipeline.pyaborts a stage, whenclassify_TEXT.pyloses its GPU or fails every document, and whenpage_split.pyfinds no input directory. Orchestrators that treated these as success will now see them. - Config files are read as UTF-8, and the Qwen-scale warning no longer fires on every run.
Packaging
- API-only installs list pandas and tqdm, and the torch spec matches the requirements.
- The security scan targets the API image.
setup/setup_api_server.shfetches FastText at the Dockerfile's pinned revision and checks its SHA-256.
Documentation and tests (#5)
docs/categorization_logic.mdis rebuilt from the code: order of evaluation, overrides with what blocks each, language tiers, the four document passes, foreign-language limits, worked examples.- README Step 4 is restructured.
tests/test_categorization_walkthrough.pyfollows a line through every step and pins the known limits.
Warning
No container images were published for this tag (its Docker run stopped at the test step). Use v1.12.1-beta: the same code, with images.
Full Changelog: v1.11.0-beta...v1.12.0-beta
Minor refinements + sidecar from David
What's new
LOWPPL_CLEAR_MAX50 → 35 (#3). A line of three or more words now takes the language-model shortcut toClearonly below perplexity 35. From 35 to 50 it goes through the score routing like any other line.- Over both collections it moves 62,859 lines (0.09 %) in 16,687 documents: Noisy → Clear 37,062, Clear → Noisy 12,342, Noisy → Trash 9,272, Clear → Trash 3,900, Trash → Noisy 279, Trash → Clear 4. The band changes on 1,128 of 1,226,349 pages.
- On the labelled sets it fixes 63 lines and breaks none: 60 of @david-spacil's and 3 of the 2,067 gold (510 → 507 errors). @DanaKriv's lines do not move.
- This closes the pilot's parameter work. The witness floor and the shape witness stay off.
- No band for a page without text lines (batch path).
aggregate_STAT.pygave a page made only ofEmpty/Non-textlines the bandClearin its document record. It now omitsquality_bandthere, as the service already did. In the July collections that was 48,544 pages (4 %). The page-stats CSV has no band column and is unchanged. tools/corpus_change.py(#3, stages 16–18):reachre-scores whole collections in memory under several configurations and writes only the changed lines, with labels joined and page aggregates (--pages,--page-ref).pagecheckapplies the stage-17 rule to its summary.recatre-scores a collection into a new folder, sharded and resumable (--expect-missing).diffcompares two re-scored trees, lines and pages, with an independent recount (--verify). It pairs lines by(page_num, line_num), so a document stored in another order is counted, not refused.
tools/ab_constant_eval.py(both copies): the exact McNemar test no longer overflows when more than 1,023 lines disagree.- Labels (
tools/gold/, seeGOLD.md):sidecars/floor_2026-10-05_keys.csv: @david-spacil's floor annotation, keys and labels only, on 141,292 lines of the July batch;- one gold label corrected:
sektlll(CTX201703691149:287) isNoisy, notTrash.
- Docs:
README.mdon whatavg_quality_scoreleaves out, anddocs/categorization_logic.mdon the new threshold. The pre-commit size limit is raised to 6,024 KB for the sidecar.
Compatibility
- Categories change for lines of three or more words with a perplexity from 35 to 50, and, through the page rules, for a few lines next to them. Re-scoring a stored batch with this version moves about 0.09 % of its lines.
- In document records from the batch path, pages without text lines no longer carry
quality_band. Nothing else in records, the API oropenapi.jsonchanges.
The ARUP and ARUB collections are re-scored with this version as DOC_LINE_CATEG_1210, with page stats (#3, stage 18). They are delivered separately, not as release assets.
Full Changelog: v1.10.0-beta...v1.11.0-beta
Edits for shared scripts and quality summary
What's new
quality_summaryin the record (ufal/atrium-project#73 R6). Every write ofpagesorlines(/process,/score_record, batch stages) sets page and line totals, the mean, median, min and max of the pages'quality_score, and the lines percateg. Numbers only: nothing routes or refuses on it.- A malformed ALTO upload is a 422 (
cause: malformed) instead of an empty 200. tools/corpus_change.pymeasures a categorisation arm over whole collections (#3, stage 16).- Tests for the W4 OCR hand-off and the port guard; shared modules of 9 October.
Compatibility
Additive for records. A client that sent broken ALTO and accepted an empty result now gets a 422.
Full Changelog: v1.9.1-beta...v1.10.0-beta
API edits for digital-convert repo
A page that digital-convert cannot vouch for now comes back into the same record (atrium-digital-convert#4 W4). digital-convert flags such a page needs_ocr, the pipeline runs ATR on it, and this service merges the page's ALTO into digital-convert's record: that page only, with every born-digital page left as it was. The spec stays compatible: every change is additive.
POST /process: the OCR hand-off (atrium-digital-convert#4 W4)
- In: the ATR ALTO of one page (
file), digital-convert's record (document_json), andpage, a new optional form field: the record's own key for that page (ii,A-1, …).- Without
page, the ALTO'sPHYSICAL_IMG_NRis read as a physical position and mapped through the record'spages[].page_index. A number is never matched to a label.
- Without
- Refused before any model runs (422):
- a page the record lacks, or does not flag
needs_ocr; - a multi-page ALTO with
page; pagewithout a record, or with a document upload;pageon a born-digital record that flags nothing.
- a page the record lacks, or does not flag
- Written into the record:
- the page's
lines[]are replaced by the classified lines (text,categ,quality_score,lang), in the record's page order; - its
pages[]row getsquality_score,quality_bandandocr.engine(the engine the ALTO names, e.g.ocr:pero, else the configured ALTO origin); - its
page_index,canvas,needs_ocr,text_layerandcategorystay; source,contentand every other page are untouched.- Both stamps read
contribution: "ocr-handoff".
- the page's
- Out: the usual response, plus
ocr_handoff:page,lines_replaced,lines_written. - A JSON or text upload names no page of its own: on a record that flags pages, it needs
page, and without it nothing is written. - Chain the calls: send each flagged page with the record the previous call returned.
Record writes (document_hook.write_document_block())
- On a record that flags pages:
- only those pages' rows are written, keyed by the record's own labels (
handoff_pages,record_page_key); - their lines are replaced;
- a page row is limited to its quality and
ocr(HANDOFF_PAGE_FIELDS); content,tablesand rows of other pages are held back, with one warning.
- only those pages' rows are written, keyed by the record's own labels (
- The batch stages make the same write.
- A
sourcewhose origin names another originator than the record's is no longer written onto it. An OCR of the original's pages is another file, and it used to cause conflict warnings.
Shared modules (hub-canonical, re-vendored)
atrium_document.py:- the hand-off is per page: a row on a page the record does not flag is refused, and so is a wholesale
set_block(); DocumentRecord.replace_page_rows();OCR_HANDOFF, whichmerge_document_records()accepts while a page is flagged.
- the hand-off is per page: a row on a page the record does not flag is refused, and so is a wholesale
- Record schema (still
1.0, additive):pages[].text_layer, a closed enum, written by digital-convert. atrium_service.py: the reason codesource_digest_mismatch(422), now in every spec.service/openapi.jsonregenerated:/processgainspage;ProcessResponsegainsocr_handoff(OcrHandoff);- the record schema gains
text_layer.
Upgrade notes
- Additive: an existing call behaves as before, with two exceptions:
- an ALTO sent with a born-digital record that flags pages: its page is now written, page by page, where before rows were added under the ALTO's own label;
- an OCR file's
sourceis no longer written onto a record whosesourcedescribes another file.
- To re-acquire a page, pass its key as
page. ThePHYSICAL_IMG_NRmapping covers ALTO that numbers pages by their physical position.
Assets
service_bundle.zip, openapi.json and openapi.json.sha256 (sha256sum -c openapi.json.sha256).
Full Changelog: v1.9.0-beta...v1.9.1-beta
API change for digital-convert repo + minor algo and param optim edits
Born-digital lines get the same quality answer as OCR lines (atrium-digital-convert#4, W3): the common line-quality model now scores an existing record in place. The release also carries the #3 parameter work since v1.8.0-beta. The spec stays compatible: /score_record is a new endpoint, and nothing existing changed.
POST /score_record (atrium-digital-convert#4 W3)
- In:
document_json(required; any record, born-digital or OCR) and an optionalpages: the record's own page keys, comma-separated (i,ii,3). A key the record lacks is 422. - Scored: each
lines[]row with text, exactly as it is: no re-reading, splitting or reordering, so every answer maps back onto its row. The scorer is the same as on every other path, batched byPPL_BATCH_LINES. - Skipped and counted:
- lines that carry atrium-digital-convert's decode verdict (
categGarbageorInverted): their text layer does not decode, so the verdict stands; - lines without text.
- lines that carry atrium-digital-convert's decode verdict (
- Written:
lines[].categ/quality_score/langandpages[].quality_score/quality_band, on rows that already exist. Nevertext,bboxorsource, and never a new row. - Out:
cleaned_lines: theAltoLinefields, plus the record'sline.pageholds the record's page key (e.g.iv), not a 1-based index;pages[]:lines_scored,skipped_decode_verdict,skipped_empty,quality_score,quality_band;limits_applied;document_json: scored, or returned as sent when nothing was scored;paradata: the run'sCreateAction.
- Limits:
MAX_UPLOAD_MB(the record part),ATRIUM_TEXT_INGEST_MAX_PAGES(pages scored) andATRIUM_TEXT_INGEST_MAX_LINES_PER_PAGE(lines on one page), each413 limit_exceeded. A record that cannot be opened, or has nodoc_id, is422 invalid_record. - atrium-digital-convert's
POST /describecalls it whenOCR_POSTPROCESS_URLis set. Against an older version of this service, it falls back to/process, one page at a time. Those scores reach the/describereport only; the record is not written.
Record writes
document_hook.write_scores()is a scoring-only write path:- only the scoring fields above; any other field is refused before a file is opened;
- existing rows only;
- lines with a decode verdict are left alone;
- the result is validated before it is written.
- Re-vendored
atrium_document.py(SCORING_FIELDS). On a record another tool originated (a born-digital one), such a merge is a scoring co-contribution: noted, not refused, and stampedcontribution: "scoring". The record stays the converter's, and the fan-in accepts it. Any other write onto a foreign record is still refused unless a page asks for OCR (needs_ocr). This tool's own OCR records are unchanged. DIGITAL_CONVERT_KINDSgains ODT, ODS, XLSX and RTF, which atrium-digital-convertv1.1.0-betareads with this repository'stext_formats.py. This changes only the wording of the text-split report note.
Categorisation (#3)
- Witness floor (
rule_short_garbage_witness_floor), new and shipped off. With[TEXT_UTILS] SHORT_GARBAGE_WITNESS_FLOOR = true, a short line the cascade ratedClearbecomesNoisywhen the shape witness finds garbage in it.- It is off in
setup/config.txtand in the code, so categories do not change for any input. - The stage-13 figures are in the
setup/config.txtcomment, anddocs/categorization_logic.mddescribes the rule. Switching it on is the maintainers' call.
- It is off in
tools/ab_constant_eval.pyreports gold-Clearlines sent to another class (Clear-demoted). Its discordant dump gainsref_pred/arm_predandclear_lossrows. Developer tools only.
Note
setup/setup_api_server.sh now uses the virtualenv directory venv-ocr (it was venv-alto). An existing installation gets a fresh environment on the next run.
Assets
service_bundle.zip, openapi.json and openapi.json.sha256 (sha256sum -c openapi.json.sha256).
Full Changelog: v1.8.0-beta...v1.9.0-beta