# atrium-digital-convert — the published environment contract (atrium-project#60). # # Copy to `.env` and edit. Do not commit `.env` (it is gitignored). # # This file is the COMPLETE ledger: every variable the api image reads, at the # default that is in the CODE. The operator-facing subset across all services is # the table in the hub's docs/k8s_deployment.md; this file's layout is fixed by # docs/templates/env.example.template in ufal/atrium-project. # # TAG LEGEND: [required] raises at startup without it · [secret] never a literal # anywhere committed, use a k8s Secret · [operator] an ordinary deployment knob · # [algorithmic] changes what the service COMPUTES, not how it is deployed · # [build] read by compose/Dockerfile or baked at build time · [limit] a limit or # setting (atrium-project#53): reported in GET /info, listed in service/README.md § Limits # # HOW THIS FILE REACHES THE PROCESS # docker compose → delivered. docker-compose.yaml's `digital-convert-api` service # declares `env_file: .env`, so everything below is injected into # the container. PORT is ALSO named in its `environment:` block, # because compose itself needs it for the `ports:` mapping. # Kubernetes → NOT delivered. Use the manifest's own env: block. See # ufal/atrium-project docs/k8s_deployment.md. # python -m service.api # → NOT delivered. Nothing here calls load_dotenv(); export the # values yourself: set -a; . ./.env; set +a # # BLANK IS NOT UNIVERSALLY "UNSET". ALLOWED_ORIGINS is the trap: a blank value parses # to an EMPTY origin list and blocks every browser request, where omitting the line # gives you `*` — so it ships commented below, not blank. # ── §0 Required before first start ───────────────────────────────────────────── # Nothing. The converter needs no key, no model and no network. /describe's adjacent # stages (§3) are optional: without them it answers from the converter alone. # ── §1 HTTP surface ───────────────────────────────────────────────────────────── # [operator] Port the api service binds, and the port service/healthcheck.py probes # inside the container. In Kubernetes, change `containerPort` in the same edit. # (atrium-project#58) PORT=8000 # [operator] Bind address. Leave at 0.0.0.0. service/healthcheck.py always probes # 127.0.0.1 by design and never reads HOST, so 127.0.0.1 here gives a container that # reports HEALTHY to every probe while being unreachable from outside it. This is the # one value whose failure mode is silent. # HOST=0.0.0.0 # [operator] CSV of CORS origins. Code default `*` (every origin). Ships commented so an # operator who leaves it alone gets the real code default; uncomment to narrow it. # ALLOWED_ORIGINS=* # [limit] Canonical upload limit in megabytes, per uploaded part (the file and # document_json). 250 is THIS repo's built-in (tool_limits.py, `upload_limit(250)`), as AMČR # runs it: born-digital PDFs with embedded images run larger than OCR text, and the AMČR samples # (atrium-digital-convert#5) reach 156 MB. Larger originals (up to about 1.5 GB) need this raised # per deployment and are not yet tested for memory, concurrency and timeouts. The upload is held in memory; a conversion's own # peak follows the document's text, not its size (the server peaked at 783 MB on those 15 # reports, 560 MB for the 156 MB one), so budget about 1 GB plus this limit per # MAX_CONCURRENT_JOBS slot. Over it → 413 limit_exceeded. # Default: 250. MAX_UPLOAD_MB=250 # [deprecated] Do not set. A byte-valued fallback kept working for one release for # callers that already exported it (strategy §4.5); MAX_UPLOAD_MB wins whenever both # are present. Listed commented-out so the contract is complete. # MAX_UPLOAD_BYTES= # [operator] Path the container's own HEALTHCHECK probes. Change it only alongside # the route itself. # HEALTHCHECK_PATH=/health # ── §2 Lifecycle ──────────────────────────────────────────────────────────────── # [operator] Seconds uvicorn waits for in-flight requests after SIGTERM. A conversion # takes seconds; a /describe with stages configured can take as long as the stages # answer (STAGE_TIMEOUT_S per call, two calls): about 5 minutes for a 500-page report with # both stages (atrium-digital-convert#5). A shutdown cuts a /describe still running after # this. Raise it together with the deployment's grace period (terminationGracePeriodSeconds # in k8s, see docs/k8s_deployment.md in the hub). GRACEFUL_SHUTDOWN_S=20 # [operator] Root logger level for the `python -m service.api` start path. All # services honour it identically (atrium-project#61). LOG_LEVEL=INFO # [operator] Filesystem auto-reload. Development only — never in a deployment. # RELOAD=false # ── §3 Backing services (atrium-project#63) ───────────────────────────────────── # The adjacent stages POST /describe calls (POST /reformat never calls them). Unset → # the stage is reported `not_configured` and the assessment rests on the converter. # In the AMČR deployment Temporal chains the stages and these stay unset. # [operator] Base URL of an atrium-page-classification service (`/predict_document`). # PAGE_CLASSIFICATION_URL=http://page-classification:8000 # [operator] Base URL of an atrium-ocr-postprocess service (`/score_record`; an older one # is asked per page through `/process`). # OCR_POSTPROCESS_URL=http://ocr-postprocess:8000 # [operator] The headless LibreOffice binary that converts legacy DOC/XLS. Unset → the # first of `soffice`, `libreoffice` on PATH (the -digital and -api images ship it). # Without one, DOC/XLS are refused with 501 (cause dependency_missing). # LIBREOFFICE_BIN=soffice # ── §4 Image / provenance ──────────────────────────────────────────────────────── # [build] Image tag used by docker-compose: on release, para_config.txt's [tool] version # WITHOUT the leading `v` (para_config.txt says `v1.1.0-beta`, GHCR carries # `1.1.0-beta` -- atrium-project#69, roadmap B3). Read only by docker-compose.yaml. ATRIUM_VERSION=dev # [build] Baked at build time; stamped into paradata as runner_ref. ATRIUM_RUNNER_REF=dev # [build] Override either only if you publish under another name. # ATRIUM_RUNNER_IMAGE=ghcr.io/ufal/atrium-digital-convert-api:dev # ATRIUM_RUNNER_REPO=https://github.com/ufal/atrium-digital-convert # [build] The uid docker-compose.yaml runs the containers as (`user: ${ATRIUM_UID:-10001}:0`); # no Python here reads it. On Linux set it to your own uid -- `echo "ATRIUM_UID=$(id -u)" >> .env` # -- so the container can write ./data, which a clone owns as you. (atrium-project#69, B6) # ATRIUM_UID=10001 # [build] Correlation id copied verbatim into paradata `request_id`. Read ONCE from # the process environment at import (atrium_paradata.py's module-level constants), # so it identifies a batch run, not an individual HTTP request. # ATRIUM_REQUEST_ID= # [operator] The IRI (e.g. a ROR id) of the organisation that operates this deployment, # recorded as the `agent` of every run's CreateAction (atrium-project#71). Unset, no agent # is recorded; never set it to a processing account, which only reads and calls back. # ATRIUM_RUN_AGENT= # ── §5 Service knobs — operator ────────────────────────────────────────────────── # Limits and settings (atrium-project#53) — every one of this service except MAX_UPLOAD_MB # (§1), declared in tool_limits.py, reported with its current value in GET /info # (`limits`; `limits_meta` names the variable and where the value came from), and listed # in service/README.md's `## Limits` table. A malformed value stops the service at # startup, naming the variable. # # [limit] Pages one document may have. Over it → 422 limit_exceeded. Default: 2000. MAX_PAGES=2000 # [limit] Conversions the service runs at once; one more → 429 busy with Retry-After. # Default: 2. MAX_CONCURRENT_JOBS=2 # [limit] Seconds headless LibreOffice may take to convert one DOC/XLS. Over it → 422 # limit_exceeded. Default: 120. LIBREOFFICE_TIMEOUT_S=120 # [limit] Seconds /describe waits for one call to an adjacent stage; over it the stage is # reported `unavailable` and the request still succeeds. page-classification took up to 202 s # on a 529-page AMČR report (atrium-digital-convert#5); 360 is that × 1.5. Default: 360. STAGE_TIMEOUT_S=360 # [limit] POLICY: share of a PDF's text-bearing pages whose text layer is a prior OCR run at # or above which the whole document is refused (422 ocr_text_layer); below it those pages # are converted and flagged needs_ocr. Default: 0.5. OCR_LAYER_DOCUMENT_SHARE=0.5 # [limit] POLICY (/describe): a page whose text layer decodes goes to OCR instead of NLP # when the quality model calls more than this share of its prose lines Trash (outside tables, # 20+ characters, mostly letters, not a table of contents; 5 of them at least). Default: 0.5. ROUTE_TRASH_SHARE=0.5 # [limit] POLICY: a page whose text layer decodes is flagged needs_ocr in the record (garbled) # and routed to OCR by /describe when at least this share of the words of its body text # (outside the running header and footer) lose a letter to a glyph without a Unicode mapping # (U+FFFD). Default: 0.05. ROUTE_DAMAGED_WORD_SHARE=0.05 # [limit] POLICY: a PDF page whose only text is its running header or footer (a page number, # a stamp) is `none` with needs_ocr when its images cover at least this share of the page: a # scan the PDF's producer numbered. Below it the page stays `digital`. Default: 0.85. FURNITURE_ONLY_IMAGE_COVERAGE=0.85 # [limit] POLICY: a PDF line in bold at the body size is a heading (one level below the # size-based ones) when it reads as a title — short, no trailing comma, not a list item, at most # two bold lines in a row, not the same bold line on 3+ pages — while bold body-size text is at # most this share of the document's body text. 0 switches the rule off. Default: 0.25. BOLD_HEADING_MAX_SHARE=0.25 # [limit] The vendored reader's caps (text_formats.py, from atrium-ocr-postprocess — same # names and defaults as there). Most a repeated ODF cell/row is expanded. Default: 100. ATRIUM_TEXT_INGEST_ODF_REPEAT_CAP=100 # [limit] Most objects of one PDF page the shared reader scans to judge its text layer. # Default: 20000. ATRIUM_TEXT_INGEST_PDF_OBJECT_CAP=20000 # ── §6 Completeness ledger — algorithmic, deprecated, unreachable ──────────────── # [algorithmic] page-classification model revision /describe asks for (`all` = the ensemble). # PAGE_CLASSIFICATION_VERSION=all # [algorithmic] Categories per page page-classification returns to /describe. # PAGE_CLASSIFICATION_TOPN=3