Pipelines¶
What the tools do, end to end: what goes in, what each stage does, what comes out, and what the point of it is.
Scope
Every workflow that runs through page-classification or the translator is described in full. The five that belong only to alto-postprocess, nlp-enrich and llm-enrich are listed under Other workflows.
This page follows the tools together. Each tool's own workflow — its steps, formats and licence, told the way its SSH Open Marketplace and Galaxy records tell it — is on Workflows.
The workflows at a glance:
| # | Workflow | In one line |
|---|---|---|
| W1 | Scanned / OCR document pipeline | a scanned page in, a searchable, enriched, documented record out |
| W2 | Born-digital pipeline | the same, for PDFs and DOCX files that already have a text layer |
| W3 | Digital → OCR re-origination | handing a born-digital document back to OCR when its text layer is unusable |
| W4 | Containerised service / API | any tool, over HTTP, one document per request |
| W5 | Agent Skill | a coding agent using a tool through its service |
| W6 | E2E smoke | the automated test that runs the whole chain |
| W7 | Vocabulary harvesting | building the term list that keeps translations consistent |
| W8 | Training and evaluation | making and scoring a page-classification model |
| W9 | Parameter optimisation | tuning alto-postprocess's line-categorisation rules |
| W10 | Document-understanding benchmark | comparing models on sampled documents |
| W11 | Format adaptation | bringing PDF, office files, PAGE XML, hOCR, … in: as line tables (alto-postprocess) or as TEITOK (flexiconv) |
| W12 | Annotation round trip | turning PDFs into a labelled training set, and corrections back into it |
| W13 | RO-Crate export | packaging finished records for a repository or catalogue |
How the stages connect¶
A pipeline diagram with five boxes in a row suggests five file handoffs. The files actually move differently:
- page-classification informs a routing decision. Its categories tell a person or a
script which pages go to OCR, HTR, table or image extraction; the next stage,
alto-postprocess, works from the OCR output and does not read
page_categories. - alto-postprocess is where the files fan out. It writes per-page ALTO (
PAGE_ALTO/) for the translator and a per-line table (DOC_LINE_CATEG/) for nlp-enrich and llm-enrich. - The translator's output is an end product.
TRANSLATED/holds English editions for readers; no later stage reads it. - nlp-enrich reads
DOC_LINE_CATEG/and the originalALTO/, and writesTEITOK/, which llm-enrich reads.
So there are three file handoffs — PAGE_ALTO/, DOC_LINE_CATEG/ and TEITOK/ — and one
thing that does run through every stage in order: the record. This page draws both
layers.
Layer 1 — the file DAG¶
It fans out from alto-postprocess and terminates at the translator.
flowchart LR
SCAN[Scanned page image] --> PC[page-classification]
PC -. informs a human<br/>routing decision .-> ALTO[alto-postprocess]
OCR[ALTO XML from OCR] --> ALTO
ALTO --> PA[PAGE_ALTO/]
ALTO --> DLC[DOC_LINE_CATEG/]
PA --> TR[translator]
TR --> TXL[TRANSLATED/]
TXL --> END(((end product)))
DLC --> NLP[nlp-enrich]
OCR --> NLP
DLC --> LLM[llm-enrich]
NLP --> TT[TEITOK/]
TT --> LLM
Layer 2 — the record accretion chain¶
Each stage takes --document-json in, writes only the block it owns, deep-copies
everything else, and stamps assembled.blocks[<block>] with its program, run_id and
paradata_ref. Licences are merged through para_licenses, so the record carries the
resolved licence of the whole chain rather than of the last writer.
flowchart LR
P1["1_pc.json<br/>page_categories"] --> P2["2_alto.json<br/>pages · content · lines · tables"]
P2 --> P3["3_translate.json<br/>translations"]
P3 --> P4["4_nlp.json<br/>entities"]
P4 --> P5["5_llm.json<br/>enrichment"]
Who may write what is declared once, in the hub-canonical BLOCK_OWNERS:
| Block | Owner |
|---|---|
pages, content, lines, tables |
alto-postprocess or digital-convert, decided by source.origin |
page_categories |
page-classification |
translations |
translator |
entities |
nlp-enrich |
enrichment, forms |
llm-enrich |
That table authorises writes. The read-time answer to "who wrote this block in this
record" is assembled.blocks[<block>].program, and for a field-split block
provenance.contributors[] — the stamp names only the most recent writer.
The workflows¶
W1 — Scanned / OCR document pipeline¶
Purpose. Take a scanned archival page and end with a searchable, enriched, FAIR record of the document it belongs to.
Inputs. A page image, and — for every stage after the first — the ALTO XML produced by OCR.
Stages.
| # | Stage | Reads | Writes | Block |
|---|---|---|---|---|
| 1 | page-classification | the page image | a Top-N CSV | page_categories |
| 2 | alto-postprocess | ALTO/ |
PAGE_ALTO/, DOC_LINE_CATEG/ |
pages, content, lines, tables |
| 3 | translator | PAGE_ALTO/<doc>/<doc>-N.alto.xml |
TRANSLATED/ |
translations |
| 4 | nlp-enrich | DOC_LINE_CATEG/ + the original ALTO/ |
TEITOK/ |
entities |
| 5 | llm-enrich | DOC_LINE_CATEG/, TEITOK/ |
enrichment output | enrichment |
alto-postprocess also takes OCR output that is not ALTO — PAGE XML, hOCR, ABBYY FineReader XML,
DjVuXML, Tesseract TSV, OCR JSON, a PDF's OCR layer — through its text-lines and json-keys
methods (W11). Those give the same DOC_LINE_CATEG/ table, but no
PAGE_ALTO/ for the translator and no page layout for nlp-enrich's TEITOK, which takes its
boxes from the ALTO file (or from a flexiconv conversion). Its ALTO methods split ALTO v3 files
only; v2 and v4 go through text-lines. The formats, their standards and what is kept of each are
in alto-postprocess's
input formats reference.
Outputs. The translated document, the TEITOK XML, and one atrium_document record that
has accreted every stage's block — which atrium_rocrate.py can then map, granularity
intact, into an RO-Crate.
What the user actually gets. A routing decision for each page (stage 1), a cleaned positional text layer with per-line quality scores (stage 2), an English reading of the document (stage 3), linguistic annotation and named entities (stage 4), and vocabulary-linked enrichment (stage 5) — with a provenance record that says which program, at which version, produced each of them.
Running the whole chain
Each stage runs as its own container, and the stages are joined by the files above and
the record passed with --document-json / --document-json-out. The
E2E workflow (W6) is the reference sequence of
docker run invocations; the two stages documented here are reproduced in
page-classification → Guide
and translator → Guide.
W4 — Containerised service / API workflow¶
Purpose. Use a tool without installing it, over HTTP, one document at a time — the mode a web frontend, an Agent Skill or another service uses.
Inputs. One file per request, uploaded as multipart, optionally with a baseline
document_json to accrete onto.
Stages. Every one of the five tools publishes an atrium-<tool>-api image from the same
Dockerfile as its batch image, and every one exposes the same meta-contract:
| Endpoint | Guarantee |
|---|---|
GET /info |
service, version, endpoints (the live route set), limits — plus per-tool extras |
GET /health |
200 while the process is alive |
GET /health?deep=true |
503 with detail when a dependency is degraded, or when draining |
GET /ready |
503 starting → 200 ready → 503 draining on SIGTERM |
The tool-specific endpoints for the two repositories documented here:
| Tool | Endpoints | Upload cap |
|---|---|---|
| page-classification | POST /predict_image, POST /predict_document |
10 MB, 50 PDF pages |
| translator | POST /translate |
50 MB, because ALTO XML goes in and ALTO XML comes out |
Outputs. JSON for the classifier; for the translator, the document itself as an XML
attachment, or multipart/mixed when a record was supplied. The translator is the one
service that does not answer with a JSON envelope, by design — it composes with curl -o.
What the user actually gets. A stateless, horizontally scalable stage: nothing is
retained between requests, so a Kubernetes Deployment can scale on queue depth. Error
codes are harmonised across all five services — 413 too large, 400 or 415 a wrong
content type, 422 unusable input, 500 a processing failure, 503 warming up or
draining — so a client can treat them uniformly — with the one caveat that FastAPI's detail is a string
for a service-raised error and a list of validation objects when FastAPI rejects the request
first.
A clean shutdown exits 143 (128 + SIGTERM) after draining for GRACEFUL_SHUTDOWN_S.
That is success, not failure.
See Operations for deployment, and Agent skills for the clients that sit on top of these endpoints.
W5 — Agent-Skill workflow¶
Purpose. Let an AI agent use a tool the way a person uses a web service — find it, ask what it offers, send a file, read the answer — without importing the tool's code or installing its dependencies.
Inputs. Files on the agent's side — page images or PDFs for the classifier, ALTO pages or AMCR records for the translator — and a service to send them to: a local one, or a hosted one named by a single environment variable.
Stages. Each agent-skill branch ships a SKILL.md and a standard-library-only client;
the agent follows the same five steps with either tool.
| # | Stage | page-classification | translator |
|---|---|---|---|
| 1 | Find a service | $ATRIUM_PC_URL, else a local one started with scripts/server.sh |
$ATRIUM_TR_URL, else a local one started with scripts/server.sh |
| 2 | Ask what it offers | atrium_classify.py --info — models, categories, limits |
atrium_translate.py --info — capabilities and limits |
| 3 | Send the file | /predict_image or /predict_document, by suffix; the client checks 10 MB and 50 pages first |
/translate, .xml only; the client checks 50 MB first |
| 4 | Read the answer | FILE, PAGE, RANK, LABEL, SCORE as a table, CSV or JSON |
the translated file (page.alto.xml → page_en.alto.xml), plus the updated record when a baseline was sent with it |
| 5 | Decide what next | the exit code | the exit code |
Outputs. Whatever the service returns, saved or printed by the client — and an exit code
that is the agent's whole decision table: 0 done; 1 a file not found or nothing produced;
2 nothing listening, so start the server and retry once; 3 an HTTP error after three
attempts ten seconds apart, so read /health?deep=true and the logs.
What the user actually gets. The same instructions work against a laptop and against a hosted endpoint: switching is one variable, so when the services are hosted no skill has to change. The agent never holds a model or a GPU — the classifier's weights live in the service container, and the translator's model runs at LINDAT.
W6 — E2E smoke: the integration contract¶
Purpose. Prove that the five tools still compose. This is the only place the whole chain runs, and it is therefore the de-facto specification of the pipeline.
Inputs. One synthetic single-page Czech document, CTX000000003 — ALTO v3,
LANG="cs", one page, two lines. There is no committed page image: the
page-classification stage renders one from the ALTO at run time, keeping the whole smoke
test anchored to a single fixture.
Stages. Five docker run invocations, each mounting one shared workspace and threading
the record forward: 1_pc.json → 2_alto.json → 3_translate.json → 4_nlp.json →
5_llm.json. Triggered on push to main, on dispatch, and on a cron every third day.
The DOC_LINE_CATEG bridge. The E2E config sets SKIP_CLASSIFY = true for
alto-postprocess, and the hub commits that stage's real output as a fixture instead —
DOC_LINE_CATEG/CTX000000003.csv, in alto-postprocess's 37-column CSV_HEADER format, one
Clear line and one Non-text line.
The reason is hardware: alto-postprocess's line-categorisation step (langID_classify.py)
hard-requires CUDA, and GitHub-hosted runners have no GPU. Skipping the stage and
committing its output is what lets the remaining four stages be exercised at all — stages 4
and 5 read the bridge file directly. It is a pinned fixture, unlike the vocabulary, because
the assertions depend on its content.
How to read a green run
Each stage runs a published image, selected by the workflow's image-tag input,
together with files checked out from each tool repository's default branch;
the hub's own files come from its v1 tag. A green run therefore says that the
default-branch configuration works with that image tag.
Assertions check formats and contracts, never model quality. Every job boundary in the workflow is one cross-repository interface, which is the point.
W7 — Vocabulary harvesting & review (the translator's half)¶
Purpose. Make domain terms translate the same way every time. An archaeological term such as (polo)zemnice has one agreed English rendering, pit house; a general translation model does not know that, and may render it differently in every document. W7 builds the list of agreed pairs and enforces it at translation time.
Inputs. Two public vocabularies, both served from aiscr.cz: the AMCR heslář — the
controlled keyword lists of the Archaeological Map of the Czech Republic — over OAI-PMH, and the
TEATER thesaurus over GraphQL. No credentials.
Stages. load_vocab.py does 1–3 once; the translator does 4–6 on every run given
--vocabulary.
| # | Stage | What actually happens |
|---|---|---|
| 1 | Harvest AMCR | ListRecords with metadataPrefix=oai_amcr&set=heslo against https://api.aiscr.cz/2.2/oai, following resumption tokens and sleeping --delay (0.3 s) between pages. Each heslo block gives its Czech term and its heslo_en sibling when both are non-empty. A network or XML error ends the harvest and keeps what was collected so far |
| 2 | Harvest TEATER | Introspect the schema at https://teater.aiscr.cz/api/graphql. exportAll returns the export's URL (as the API's internal http://localhost:8080/api/export, rewritten to the public host); the download is a JSON categories tree, and every concept with a Czech and an English name gives a pair keyed by its TEATER id. A label shared by two concepts keeps the last. Only if that yields nothing does the fallback run: search(value: "", limit: 99999) in Czech and in English, joined on id. Every TEATER request is spaced by --delay |
| 3 | Merge and write | TEATER first, then AMCR over it, so AMCR wins a collision. Keys are lower-cased and rows sorted, so a re-harvest gives a stable diff. Five columns: source_lemma, target_translation, source, source_id, uri. Nothing harvested → exit 2, and the existing file is left alone; --skip-amcr together with --skip-teater is refused as a usage error |
| 4 | Load | processors/vocab.py reads the 2-column form or the 5-column one, with or without a header row. A later duplicate key wins. An unreadable file prints a warning and loads nothing — the run carries on without a vocabulary |
| 5 | Tag and protect | Before each translation call: multi-word phrases, longest first, case-insensitive, whole words only — every occurrence gets its own sentinel; then single words by UDPipe lemma (only for languages with a UDPipe model), skipping plural tokens so the model can inflect the English. Each match becomes an Xtermzzz<N>z sentinel; after translation it is restored — exact, then case-insensitive, then fuzzy — as the agreed term |
| 6 | Account | Paradata records vocabulary_protected_terms per document plus a total, and the run's licence gains udpipe2_engine, udpipe2_models, amcr_vocab and teater_data |
Outputs. data_samples/vocabulary.csv (the default --out), and translated documents in
which every protected term carries its agreed rendering.
What the user actually gets. Consistent terminology across every translated document, with a count of protected terms per document in the paradata, and a glossary whose every row points back at the thesaurus concept it came from. A translation run with the vocabulary resolves to CC BY-NC-SA 4.0, determined by CUBBITT and the UDPipe models — the vocabulary adds two CC BY-NC 4.0 sources, but a CUBBITT run is already the more restrictive licence.
Worth knowing. The same Czech label can recur across AMCR's keyword lists, so the number
of pairs is smaller than the number of AMCR records. The glossary is written in the five-column
form; a two-column source_lemma,target_translation file loads just as well. Harvest with TLS
verification on — if a certificate chain is incomplete, add the missing intermediate through
REQUESTS_CA_BUNDLE rather than disabling verification.
The review half belongs to nlp-enrich, which builds the reviewed SKOS vocabulary from the same
two sources with its own harvester. The files are prefix-compatible by design — these five columns
are the first five of nlp-enrich's *_flat.csv — but they are separate harvests, each from the
live sources at the time it was run. See
SKOS → The translator and its glossary.
W8 — Training and evaluation (page-classification)¶
Purpose. Fine-tune, cross-validate and score a page classifier — the only workflow in the ecosystem that produces a model rather than consuming one.
Inputs. A directory tree of page images, one folder per category — training reads the label list from the sorted sub-directory names. Optionally an explicit folds CSV.
Stages.
- Split — deterministic periodic sampling with a randomised offset, not a shuffle;
80/10/10, or read from
--folds_csvto reproduce a model's original split exactly, with pages absent from the CSV excluded. - Train —
--train, three epochs at 5e-5, batch 8,load_best_model_at_endon accuracy. Augmentation is colour jitter, sharpness and Gaussian blur at 50 % each, and deliberately no rotation or flipping: page orientation is part of what is being read. - Evaluate —
--evalproduces a Top-N CSV with aTRUEcolumn, a raw per-class CSV and a confusion-matrix plot at 300 dpi. - Average —
--average -ap <pattern>averages fold weights;--bestaverages the five published models' probabilities at inference time. These are different operations.
Outputs. Model weights, {stamp}_{rev}_FOLD_{n}_DATASETS.txt recording the split
actually used, evaluation CSVs, confusion matrices, and paradata whose resolved licence is
CC BY-NC 4.0 rather than MIT — because training logs the LINDAT dataset, which
setup/para_config.txt declares as CC BY-NC 4.0.
What the user actually gets. A reproducible model. The fold rule
splitN ↔ foldN ↔ seed 420+(N−1) and REVISION_BEST_FOLDS mean a published revision can be
retrained on the split it originally saw — which is how the v*.4 generation was trained on
the licensed subset and compared page by page with the generation before it; see
page-classification → History.
Full flag reference: page-classification → Reference.
W12 — Annotation round trip (page-classification's half)¶
Purpose. Turn a pile of PDFs into a labelled training set, and feed what a reviewer finds back into it. Every page-classification model was trained on a set built this way.
Inputs. PDFs, and a person with a spreadsheet. No annotation tool is named or shipped — labels are typed into a CSV.
The one fact to hold on to: folder names are the labels. Training and evaluation read a
directory tree with one folder per category, and take the category list from the sorted names of
its sub-directories (collect_images() in utils.py); files and hidden entries at that level are
ignored. A mistyped folder name is a new class, and an empty folder is one too.
Stages. The scripts live in data_scripts/unix/ (.sh) and data_scripts/windows/ (.bat),
with the same flags in both, and in supplementary/scripts/.
| # | Stage | Script | What actually happens |
|---|---|---|---|
| 1 | PDF → page images | pdf2png.sh · pdf2png.bat |
Unix: pdftoppm, one job per core, 300 dpi PNG by default, page numbers zero-padded (doc-001.png); .pdf and .PDF are both found. The PDFs are kept unless --delete (/delete) is given. Windows: ImageMagick + Ghostscript, one file at a time, unpadded (doc-1.png) |
| 2 | Gather one-pagers | move_single.sh · .bat (optional) |
Every folder holding exactly one image is emptied into ./onepagers/ and removed; -n previews it. The default target is relative to where you run it, while sort.sh looks for onepagers/ inside its input directory — run it from there, or pass -t |
| 3 | Annotate | a spreadsheet | A CSV with a header row and at least FILE,PAGE,CLASS, one row per page. Further columns are ignored, and PAGE may be zero-padded (08). sort.sh finds the columns by header name; sort.bat reads the first three by position. The README suggests keeping the categories roughly equal in size |
| 4 | Route into label folders | sort.sh · sort.bat |
Each row's page is copied — or --moved — into <out>/<CLASS>/, trying no padding, then 2-, 3- and 4-digit padding, in the document's folder or, when it has none, in onepagers/. A label folder is made only once a page is found for it; a row whose PAGE is not a number or whose CLASS is empty or holds / or \ is reported and skipped. sort.sh also takes CLASS-1/CATEGORY, so a result table routes as-is. --dry-run (/n) previews it |
| 5 | Train and evaluate | run.py --train · --eval |
W8. --folds_csv fixes the split: it needs a PNG column holding the exact file name and foldN columns of train / dev / test; pages missing from it are left out |
| 6 | Review | the --raw and EVAL tables |
Raw tables are sorted by per-class probability, so the ambiguous pages gather where a reviewer looks; EVAL tables carry a TRUE column beside the prediction |
| 7 | Correct | a file manager | Mislabelled pages are moved between label folders — the tree is the source of truth, not the CSV |
| 8 | Re-sync the CSV | filtering.py |
Keeps each row whose (file, page, class) still exists in the tree and relabels a row whose page now sits in exactly one other label folder; a page that is gone, or in several other folders, is dropped and reported. Reads CLASS, else CLASS-1, else CATEGORY; writes <stem>_filtered.csv beside the input. --no-relabel only drops |
| 9 | Account for the split | result/stats/unused.sh · dataset_stat.sh |
Which annotated pages no fold used, and per-set, per-category counts, read from the *_FOLD_*_DATASETS.txt split records |
What the loop still leaves to you. The scripts refuse what would misroute a page — a PAGE
that is not a number, a label that is not a single folder name — and report it; they cannot tell a
wrong label from a right one:
- A mistyped
CLASSis a new category.sort.shroutesTEXtinto its own folder, and training then counts twelve classes. Check the label list against the eleven in SKOS → How page-classification uses it before sorting;collect_images()prints a note when the folders and the declared labels differ. filtering.pykeys on the<name>-<page>.pngconvention. A PNG renamed outside it is not indexed — the script warns and counts it — so its row is reported as missing.sort.batreads by position, unlikesort.sh: on Windows keepFILE, PAGE, CLASSas the first three columns.
Helpers around the loop. averaging.py averages several result tables — only pages present in
every input survive, and a wide per-model table counts one vote per model, which makes it a
majority vote; per_doc_split.py splits a result table into one CSV per document;
result_analysis.sh scores a directory of *_EVAL.csv files; downscale.py resizes a label tree.
Outputs. A label tree ready for W8, a CSV that agrees with it, and split records that say which page was used where.
What the user actually gets. The published training dataset — 48,499 page images from 37,328
documents, cited as hdl.handle.net/20.500.12800/1-6184 — is this loop's output. The v*.4
generation is one complete round trip: the set was reduced to its licensed subset and the five
models retrained on the same folds (W8).
W13 — RO-Crate export / FAIR publication¶
Purpose. Hand a finished record to a repository or catalogue in a form it can read without knowing anything about ATRIUM.
Inputs. Records that have already been through the pipeline and, for a run crate, the run's paradata file.
Stages. One, run as its own step over finished records. The exporter, atrium_rocrate.py, is
vendored into every tool:
python atrium_rocrate.py --document CTX000000003.document.json --out-dir crate/
python atrium_rocrate.py --run 1.document.json 2.document.json --paradata run.json --out-dir crate/
Outputs. ro-crate-metadata.json — RO-Crate 1.1, sorted and byte-stable, written atomically.
A run crate holds one sub-crate per document and also declares the Process Run Crate profile.
What the user actually gets. A catalogue-readable description with a separate creator and date for every block, so "who classified the pages" and "who translated them" have separate answers, and the record's computed licence. Per-page detail — which page carries which category, the language pair of a translation — stays in the record, which can travel inside the crate. The full mapping is under RO-Crate export → What happens to these two tools' blocks.
Other workflows¶
These run through alto-postprocess, nlp-enrich and llm-enrich only; each tool's own README describes them, and the stable core of each tool's workflow is on its Workflows page — alto-postprocess, nlp-enrich, llm-enrich.
| # | Workflow | What it is |
|---|---|---|
| W2 | Born-digital pipeline | Two stages, not six — a digital-born PDF or DOCX already has a text layer, so digital-convert originates the positional plane directly and origin-consistency refuses a second originator |
| W3 | Digital → OCR re-origination | How a digital-born document that turns out to need OCR is re-authorised, through needs_ocr: true — the single exception that lets two originators coexist |
| W9 | Parameter optimisation / rule coverage | alto-postprocess's sweep over its categorisation rules |
| W10 | Document-understanding benchmark | llm-enrich's stratified sampling and model comparison |
| W11 | Format adaptation | Two routes for documents that are not ALTO. alto-postprocess's --method text-lines reads them directly — PDF text layers, DOCX/ODT/XLSX/PPTX, EPUB, RTF, HTML, the OCR exports PAGE XML, hOCR, ABBYY FineReader XML, DjVuXML and Tesseract TSV, TEI/TEITOK, JSON, CSV, plain text — into the same per-line tables (DOC_LINE_CATEG/) nlp-enrich and llm-enrich read, with a truthful source.origin per format; it keeps text, not coordinates. nlp-enrich's api_flexiconv.sh converts them (PDF, DOCX, PAGE XML, hOCR, …) into TEITOK; nlp-enrich then annotates them (FLEXICONV_ANNOTATE, or a converted file uploaded to /enrich) and llm-enrich reads them. flexiconv never writes ALTO |
Sources¶
Written from the tree at the refs below. This table records provenance, not a build instruction.
| Source | What was taken from it |
|---|---|
atrium-project/.github/workflows/e2e-pipeline-smoke.yml |
W1's stage table, W6 in full, SKIP_CLASSIFY = true |
atrium-project/fixtures/e2e/README.md |
the fixture description |
atrium-project/docs/templates/shared/atrium_document.py |
BLOCK_OWNERS |
atrium-project/docs/document_schema.md |
the write/read contract and the accretion rules |
atrium-project/docs/skills_catalog.md, docs/k8s_deployment.md |
W4's endpoint and limit tables |
atrium-page-classification @ vit adee922 — run.py, setup/config.txt, setup/para_config.txt, model_registry.py, data_scripts/{unix,windows}/*, supplementary/scripts/*, result/stats/*.sh, utils.py, README.md § Data preparation |
W8 and W12 |
atrium-translator @ master 71feaef — main.py, service/api.py, load_vocab.py, processors/{vocab,translator,lemmatizer}.py, data_samples/vocabulary.csv |
W1 stage 3, W4, W7 |
both tools' agent-skill branches |
W5 |
atrium-project/docs/templates/shared/atrium_rocrate.py |
W13 |
atrium-alto-postprocess @ test 2e2794d — text_formats.py (READERS), page_split.py (split_alto_xml), docs/text_inputs.md |
W11's alto-postprocess route; the note under W1's stage table |