Extraction of statistical and vocab-controlled keywords from text-containing documents
See the codeKeywords of archival documents, of two kinds kept apart β and every keyword says which method produced it, with its score and rank:
One service, POST /extract_keywords, takes the ATRIUM document record after nlp-enrich and answers with the
statistical keywords of the document and of its pages, and the record with its controlled keywords written in
(the enrichment block). This repository is where both kinds live: it was assembled on
1 October 2026 from the keyword extraction of atrium-nlp-enrich
(keywords.py) and of atrium-llm-enrich (the LLM engine and the vocabulary code), after the meeting of
30 September (atrium-keyword-extract#1; atrium-project#72). nlp-enrich keeps only the LINDAT calls (UDPipe,
NameTag) and what is built from them.
[!NOTE] State of this branch. Both kinds run in the service. The controlled kind calls a language model in an inference service β OpenRouter, or a local Ollama β configured by
LLM_BACKEND; a deployment without one answerskind=controlledwith 501 and reports itskippedunderkind=both. Its quality is not evaluated yet: the evaluation rubric (D1) of atrium-keyword-extract#2 was decided on 8 October 2026, but the annotated corpus and the scorer (D2) do not exist yet, so treat its output as research output. Since v1.2.0-beta the statistical keywords are also written into the record, as itskeywordsblock (atrium-project#73).
keywords.pyservice/README.md is the reference: the endpoints, the options, the response, the limits,
the errors and the OpenAPI contract. In short:
python -m service.api # PORT / HOST; default 0.0.0.0:8000
curl -s -X POST localhost:8000/extract_keywords \
-F "document_json=@CTX000000001.document.json;type=application/json" \
-F kind=statistical -F method=keybert -F num_keywords=10
# both kinds, with the controlled one's backend configured
OPENROUTER_API_KEY=sk-... OPENROUTER_MODEL=openai/gpt-4o-mini python -m service.api
curl -s -X POST localhost:8000/extract_keywords \
-F "document_json=@CTX000000001.document.json;type=application/json" -F kind=both
The record is read: its lines[].text per page (lines labelled Trash, Garbage, Inverted or Empty are left out), and for the
legacy method its lines[].lemma and lines[].upos, which nlp-enrich
writes. In the pipeline it comes after nlp-enrich (and after digital-convert on the born-digital route), and its
keywords can be projected into the TEITOK header by nlp-enrich's /project_record.
| Method | Needs | Score |
|---|---|---|
keybert | the embedding model, a GPU if available | cosine similarity, [0, 1] |
yake | nothing (CPU) | inverted YAKE score, normalised to [0, 1] |
legacy | lines[].lemma / lines[].upos | an occurrence count |
Scores are compared only within one method.
git clone https://github.com/ufal/atrium-keyword-extract.git
cd atrium-keyword-extract
python -m venv venv && source venv/bin/activate
pip install -r requirements.txt # KeyBERT, YAKE, the shared contract
pip install -r service/requirements.txt # + the service (fastapi, uvicorn)
KeyBERT downloads its embedding model (paraphrase-multilingual-MiniLM-L12-v2) from the Hugging Face Hub on first
use; set HF_HOME to keep the cache. A GPU is used when torch finds one. The environment of the service is
.env.example.
keywords.py[!NOTE] The command-line form of the statistical kind, for a whole collection at once. It reads the CoNLL-U files nlp-enrich writes (or its TEITOK files) and writes CSV tables; the service reads the document record instead. Both run the same three methods.
Extract keywords π from your documents by running keywords.py on a directory of CoNLL-U files.
The keyword extraction script uses a three-tier configuration hierarchy (from highest to lowest priority):
-m yake, -w 3) always override everything else.kw_config.txt (the [DEFAULTS] section) is read automatically if placed next to the script.This means if you configure your settings in kw_config.txt, you can simply run:
python3 keywords.py
| Flag value | Method | Dependencies | Score semantics | Best for |
|---|---|---|---|---|
legacy | Original KER β NOUN/PROPN/ADJ lemma frequency | none (stdlib only) | raw occurrence count | reproducing original ATRIUM results |
yake | YAKE β unsupervised statistical, CPU-only (AGPL-3.0) | pip install yake | normalised inverse YAKE score, [0, 1] | fast CPU runs, no model download |
keybert (default) | KeyBERT β embedding-based, GPU-accelerated | pip install keybert sentence-transformers | cosine similarity, [0, 1] | highest semantic quality, GPU recommended |
You can override any kw_config.txt setting via the command line:
python3 keywords.py -i <input_dir> -m <method> -l <lang> -w <integer> \
-n <integer> -d <output_dir> -o <output_file>.csv
All available flags:
| Flag | Long form | Default in kw_config.txt | Description |
|---|---|---|---|
-i | --input_dir | data_samples/UDP | CoNLL-U directory to process |
-m | --method | keybert | Backend: legacy, yake, or keybert |
-l | --lang | cs | Language code for YAKE stopwords (cs, en, de, β¦). Ignored by legacy and keybert |
-w | --max_words | 3 | Maximum words per keyword phrase (n-gram upper bound) |
-n | --num_keywords | 20 | Number of keywords to extract per document |
-d | --per_doc_out_dir | data_samples/KW_PER_DOC | Output directory for per-document CSV files |
-o | --output_file | keywords_summary.csv | Master keywords CSV |
--keybert-model | paraphrase-multilingual-MiniLM-L12-v2 | Sentence-Transformer model name (KeyBERT only) | |
--no-mmr | (False) | Disable Maximal Marginal Relevance diversification (KeyBERT only) | |
--diversity | 0.5 | MMR diversity parameter, 0 = max relevance β 1 = max diversity (KeyBERT only) | |
--workers | 0 (Auto / CPU count) | Parallel worker processes. Auto-forced to 1 for KeyBERT + GPU |
Examples:
YAKE β Czech, up to 3-word phrases, 20 keywords per document
python3 keywords.py -i OUTPUT_DIR/UDP -m yake -l cs -w 3 -n 20 \
-o keywords_summary.csv -d KW_PER_DOC
KeyBERT β multilingual model, GPU-accelerated (the default)
python3 keywords.py -i OUTPUT_DIR/UDP -m keybert -w 3 -n 20 \
--keybert-model paraphrase-multilingual-MiniLM-L12-v2 \
-o keywords_summary.csv -d KW_PER_DOC
Legacy KER β (English/Czech) original ATRIUM lemma-frequency approach, no extra dependencies
python3 keywords.py -i OUTPUT_DIR/UDP -m legacy -n 20 \
-o keywords_summary.csv -d KW_PER_DOC
[!WARNING] For KeyBERT with a GPU, the script automatically forces
--workers 1to prevent competing CUDA context initialisation across subprocesses. On CPU, any worker count is safe.
UDP/), or *.teitok.xml files.keywords_summary.csv).KW_PER_DOC/).KW_PER_DOC/
βββ <docname1>_keywords.csv
βββ <docname2>_keywords.csv
βββ ...
Each per-document file contains two columns β keyword and score β sorted
by score in descending order. The master summary uses the same column structure
as the original pipeline (document_id, kw-1, score-1, kw-2, score-2, β¦).
legacy β raw lemma count; higher = more frequent in the document. Examples in directory: KW_PER_DOC_L π and summary file
kw_summary_l.csv π.
| Score range | Interpretation |
|---|---|
| 1β5 | Common functional nouns, low informativeness |
| 5β20 | Topic-representative vocabulary |
| > 20 | Dominant terms, likely named entities or domain headings |
yake β normalised inverse YAKE score, [0, 1] per document. Examples in directory: KW_PER_DOC_Y π and summary file
kw_summary_y.csv π.
| Score range | Semantic category | Interpretation |
|---|---|---|
| 0.0β0.2 | Noise floor | Common words, low local relevance |
| 0.2β0.6 | Context layer | General vocabulary defining the broad topic |
| 0.6β0.9 | Topic layer | Specific nouns and verbs central to the text |
| 0.9β1.0 | Entity layer | Rare terms, neologisms, named entities |
keybert β cosine similarity to document centroid, [0, 1]. Examples in directory: KW_PER_DOC_KB π and summary file
kw_summary_kb.csv π.
| Score range | Interpretation |
|---|---|
| < 0.3 | Weakly related phrases |
| 0.3β0.6 | Contextually relevant terms |
| > 0.6 | Highly representative keyphrases |
The controlled kind maps each line onto the AMΔR keyword lists and the TEATER thesaurus with a language model whose
answer is constrained to the vocabulary's terms (a JSON schema whose category is an enum of its 4711 terms and the
meta-text answer NerelevantnΓ (meta-text)). One
prompt serves every path: prompts/system_prompt.txt, whose blocks the PROMPT_* flags
of llm_config.txt select β prompts/RUNBOOK.md explains each β followed by the vocabulary
and the examples; python3 prompt_template.py --preview prints it. Each answer carries the AMΔR/TEATER records behind
the chosen term (teater_category_ids), and a homonym the vocabulary build qualified (zΓ‘mek (sΓdlo elity)) comes
back as its plain label.
| Path | Model | Reads | Writes |
|---|---|---|---|
the service, kind=controlled|both | an inference service: OpenRouter, Ollama | the record's lines | the record's enrichment block, entities[].pid |
openrouter_client.py, ollama_client.py (batch) | the same | records, CSV, TEITOK (per line); .md/.txt (per document) | <doc_id>_enriched.json, the record with --document-json-out |
llm_run.py (research, GPU) | local weights, vLLM or transformers | CSV, TEITOK | <doc_id>_enriched.json |
The first two share llm_client_shared.py, which needs no GPU stack (requirements_remote.txt); the third is the
bake-off path of atrium-keyword-extract#2 (llm_utils.py, requirements_llm.txt, the llm Docker target).
pip install -r requirements_remote.txt
export OPENROUTER_API_KEY=sk-...
python openrouter_client.py --input CTX000000001.document.json --model openai/gpt-4o-mini \
--document-json-out CTX000000001.kw.document.json
python ollama_client.py --input data_samples/DOC_LINE_CATEG --model qwen2.5:14b
The research run on a GPU node:
pip install -r requirements.txt -r requirements_llm.txt # vLLM brings its own torch pin
$EDITOR llm_config.txt # MODEL_KEY; INPUT_DIR (line CSVs or *.teitok.xml), OUTPUT_DIR, PARADATA_DIR
python llm_run.py --download # where huggingface.co is reachable: MODEL_KEY into the HF cache
python llm_run.py # or: python llm_run.py <another config>
A GPU node without internet access (HF_HUB_OFFLINE=1) loads a model only from the Hugging Face cache, so
fetch it first on a machine that has access, into the same HF_HOME (--download KEY ... takes several keys).
It fetches what a run reads (configs, tokenizer, weights; not README or LICENSE), skips files already cached
(huggingface_hub then reports 0 B), and ends with what the cache holds. A run whose model files are not all
cached stops before loading and names what is missing.
It writes <OUTPUT_DIR>_<model>/<doc_id>_enriched.json per document (and <doc_id>_enriched.abort.json after ten
consecutive errors), skips a document whose output already exists, so a stopped job resumes (delete an aborted
document's two files to redo it), and writes a paradata record naming the vocabulary build and the sha256 of the
prompt it used. The whole prompt is about 58k tokens: a model served with a smaller window, including the large vLLM
models at their default MAX_MODEL_LEN of 16384, sees only a prefix of the vocabulary, and the paradata records how
many terms were cut. vLLM computes the prompt's shared part once and reuses it (prefix caching), and so does
BACKEND=transformers with REUSE_PROMPT_CACHE=true; without it every line re-reads the whole prompt.
PREFILL_CHUNK_SIZE keeps the transformers prefill within a 24 GB card. Hybrid models
(Qwen3.5, Qwen3.6) need flash-linear-attention, which requirements_llm.txt lists: without it their
linear-attention layers run on a reference kernel that is far slower and does not fit. The llm image carries no
sample inputs, and whatever it writes outside /data goes with the container, so point INPUT_DIR, OUTPUT_DIR
and PARADATA_DIR under /data in the llm_config.txt the compose file mounts.
The vocabulary:
vocab_sources.py, vocab_build.py, vocab_review.py, vocab_manager.py β harvest the AMΔR and TEATER vocabularies,
build the nested union, review it; the built files are in data_samples/vocab (see its
RUNBOOK). The vocabularies are CC0; the build is to be published as a versioned
release asset for the translator and the end-to-end test.corpus_review.py β the vocabulary-gap review used for the TEATER experiment.The LLM engine and the vocabulary code existed twice, in nlp-enrich and in llm-enrich, and partly diverged. This
repository holds the one copy: nlp-enrich's came with the repository, and llm-enrich's (atrium-digital-convert
31534d5) was merged into it on 2026-10-07 β its remote clients, its service path and its later engine fixes.
The code is MIT. The licence of a run's output is computed from the components it used, as declared in
para_config.txt, and the most restrictive one wins:
| Component | Licence | When |
|---|---|---|
| KER (legacy) | MIT | method=legacy |
| KeyBERT, sentence-transformers, the embedding model | MIT / Apache-2.0 | method=keybert |
| YAKE | AGPL-3.0 | method=yake β a run that uses it is declared AGPL-3.0 |
| AMΔR and TEATER vocabularies | CC0 | the vocabulary build, the controlled kind |
The language model of the controlled kind has no row: its terms depend on the model a deployment chooses
(OPENROUTER_MODEL, OLLAMA_MODEL, MODEL_KEY), whose id every run records in its paradata. The source document's
own licence applies to its text.
| Target | Image | What it is |
|---|---|---|
api | ghcr.io/ufal/atrium-keyword-extract-api | the service, both kinds β the production image (no LLM weights) |
base | ghcr.io/ufal/atrium-keyword-extract | the batch CLI (keywords.py) |
llm | ghcr.io/ufal/atrium-keyword-extract-llm | the research LLM batch run, GPU |
docker compose --profile api up # the service on :8000
docker compose run --rm kw -i /data/UDP -m yake # the batch CLI
docker compose -f docker-compose.yaml -f docker-compose.gpu.yaml --profile llm run --rm kw-llm
The images of atrium-nlp-enrich (its -llm target) and atrium-llm-enrich that carried these stages before
1 October 2026 stay published for consumers pinned to them and receive no new tags.
See CONTRIBUTING.md. For support write to lutsai.k@gmail.com, responsible for this repository.
Β©οΈ 2026 UFAL & ATRIUM
Extraction of statistical and vocab-controlled keywords from text-containing documents
See the codeKeywords of archival documents, of two kinds kept apart β and every keyword says which method produced it, with its score and rank:
One service, POST /extract_keywords, takes the ATRIUM document record after nlp-enrich and answers with the
statistical keywords of the document and of its pages, and the record with its controlled keywords written in
(the enrichment block). This repository is where both kinds live: it was assembled on
1 October 2026 from the keyword extraction of atrium-nlp-enrich
(keywords.py) and of atrium-llm-enrich (the LLM engine and the vocabulary code), after the meeting of
30 September (atrium-keyword-extract#1; atrium-project#72). nlp-enrich keeps only the LINDAT calls (UDPipe,
NameTag) and what is built from them.
[!NOTE] State of this branch. Both kinds run in the service. The controlled kind calls a language model in an inference service β OpenRouter, or a local Ollama β configured by
LLM_BACKEND; a deployment without one answerskind=controlledwith 501 and reports itskippedunderkind=both. Its quality is not evaluated yet: the evaluation rubric (D1) of atrium-keyword-extract#2 was decided on 8 October 2026, but the annotated corpus and the scorer (D2) do not exist yet, so treat its output as research output. Since v1.2.0-beta the statistical keywords are also written into the record, as itskeywordsblock (atrium-project#73).
keywords.pyservice/README.md is the reference: the endpoints, the options, the response, the limits,
the errors and the OpenAPI contract. In short:
python -m service.api # PORT / HOST; default 0.0.0.0:8000
curl -s -X POST localhost:8000/extract_keywords \
-F "document_json=@CTX000000001.document.json;type=application/json" \
-F kind=statistical -F method=keybert -F num_keywords=10
# both kinds, with the controlled one's backend configured
OPENROUTER_API_KEY=sk-... OPENROUTER_MODEL=openai/gpt-4o-mini python -m service.api
curl -s -X POST localhost:8000/extract_keywords \
-F "document_json=@CTX000000001.document.json;type=application/json" -F kind=both
The record is read: its lines[].text per page (lines labelled Trash, Garbage, Inverted or Empty are left out), and for the
legacy method its lines[].lemma and lines[].upos, which nlp-enrich
writes. In the pipeline it comes after nlp-enrich (and after digital-convert on the born-digital route), and its
keywords can be projected into the TEITOK header by nlp-enrich's /project_record.
| Method | Needs | Score |
|---|---|---|
keybert | the embedding model, a GPU if available | cosine similarity, [0, 1] |
yake | nothing (CPU) | inverted YAKE score, normalised to [0, 1] |
legacy | lines[].lemma / lines[].upos | an occurrence count |
Scores are compared only within one method.
git clone https://github.com/ufal/atrium-keyword-extract.git
cd atrium-keyword-extract
python -m venv venv && source venv/bin/activate
pip install -r requirements.txt # KeyBERT, YAKE, the shared contract
pip install -r service/requirements.txt # + the service (fastapi, uvicorn)
KeyBERT downloads its embedding model (paraphrase-multilingual-MiniLM-L12-v2) from the Hugging Face Hub on first
use; set HF_HOME to keep the cache. A GPU is used when torch finds one. The environment of the service is
.env.example.
keywords.py[!NOTE] The command-line form of the statistical kind, for a whole collection at once. It reads the CoNLL-U files nlp-enrich writes (or its TEITOK files) and writes CSV tables; the service reads the document record instead. Both run the same three methods.
Extract keywords π from your documents by running keywords.py on a directory of CoNLL-U files.
The keyword extraction script uses a three-tier configuration hierarchy (from highest to lowest priority):
-m yake, -w 3) always override everything else.kw_config.txt (the [DEFAULTS] section) is read automatically if placed next to the script.This means if you configure your settings in kw_config.txt, you can simply run:
python3 keywords.py
| Flag value | Method | Dependencies | Score semantics | Best for |
|---|---|---|---|---|
legacy | Original KER β NOUN/PROPN/ADJ lemma frequency | none (stdlib only) | raw occurrence count | reproducing original ATRIUM results |
yake | YAKE β unsupervised statistical, CPU-only (AGPL-3.0) | pip install yake | normalised inverse YAKE score, [0, 1] | fast CPU runs, no model download |
keybert (default) | KeyBERT β embedding-based, GPU-accelerated | pip install keybert sentence-transformers | cosine similarity, [0, 1] | highest semantic quality, GPU recommended |
You can override any kw_config.txt setting via the command line:
python3 keywords.py -i <input_dir> -m <method> -l <lang> -w <integer> \
-n <integer> -d <output_dir> -o <output_file>.csv
All available flags:
| Flag | Long form | Default in kw_config.txt | Description |
|---|---|---|---|
-i | --input_dir | data_samples/UDP | CoNLL-U directory to process |
-m | --method | keybert | Backend: legacy, yake, or keybert |
-l | --lang | cs | Language code for YAKE stopwords (cs, en, de, β¦). Ignored by legacy and keybert |
-w | --max_words | 3 | Maximum words per keyword phrase (n-gram upper bound) |
-n | --num_keywords | 20 | Number of keywords to extract per document |
-d | --per_doc_out_dir | data_samples/KW_PER_DOC | Output directory for per-document CSV files |
-o | --output_file | keywords_summary.csv | Master keywords CSV |
--keybert-model | paraphrase-multilingual-MiniLM-L12-v2 | Sentence-Transformer model name (KeyBERT only) | |
--no-mmr | (False) | Disable Maximal Marginal Relevance diversification (KeyBERT only) | |
--diversity | 0.5 | MMR diversity parameter, 0 = max relevance β 1 = max diversity (KeyBERT only) | |
--workers | 0 (Auto / CPU count) | Parallel worker processes. Auto-forced to 1 for KeyBERT + GPU |
Examples:
YAKE β Czech, up to 3-word phrases, 20 keywords per document
python3 keywords.py -i OUTPUT_DIR/UDP -m yake -l cs -w 3 -n 20 \
-o keywords_summary.csv -d KW_PER_DOC
KeyBERT β multilingual model, GPU-accelerated (the default)
python3 keywords.py -i OUTPUT_DIR/UDP -m keybert -w 3 -n 20 \
--keybert-model paraphrase-multilingual-MiniLM-L12-v2 \
-o keywords_summary.csv -d KW_PER_DOC
Legacy KER β (English/Czech) original ATRIUM lemma-frequency approach, no extra dependencies
python3 keywords.py -i OUTPUT_DIR/UDP -m legacy -n 20 \
-o keywords_summary.csv -d KW_PER_DOC
[!WARNING] For KeyBERT with a GPU, the script automatically forces
--workers 1to prevent competing CUDA context initialisation across subprocesses. On CPU, any worker count is safe.
UDP/), or *.teitok.xml files.keywords_summary.csv).KW_PER_DOC/).KW_PER_DOC/
βββ <docname1>_keywords.csv
βββ <docname2>_keywords.csv
βββ ...
Each per-document file contains two columns β keyword and score β sorted
by score in descending order. The master summary uses the same column structure
as the original pipeline (document_id, kw-1, score-1, kw-2, score-2, β¦).
legacy β raw lemma count; higher = more frequent in the document. Examples in directory: KW_PER_DOC_L π and summary file
kw_summary_l.csv π.
| Score range | Interpretation |
|---|---|
| 1β5 | Common functional nouns, low informativeness |
| 5β20 | Topic-representative vocabulary |
| > 20 | Dominant terms, likely named entities or domain headings |
yake β normalised inverse YAKE score, [0, 1] per document. Examples in directory: KW_PER_DOC_Y π and summary file
kw_summary_y.csv π.
| Score range | Semantic category | Interpretation |
|---|---|---|
| 0.0β0.2 | Noise floor | Common words, low local relevance |
| 0.2β0.6 | Context layer | General vocabulary defining the broad topic |
| 0.6β0.9 | Topic layer | Specific nouns and verbs central to the text |
| 0.9β1.0 | Entity layer | Rare terms, neologisms, named entities |
keybert β cosine similarity to document centroid, [0, 1]. Examples in directory: KW_PER_DOC_KB π and summary file
kw_summary_kb.csv π.
| Score range | Interpretation |
|---|---|
| < 0.3 | Weakly related phrases |
| 0.3β0.6 | Contextually relevant terms |
| > 0.6 | Highly representative keyphrases |
The controlled kind maps each line onto the AMΔR keyword lists and the TEATER thesaurus with a language model whose
answer is constrained to the vocabulary's terms (a JSON schema whose category is an enum of its 4711 terms and the
meta-text answer NerelevantnΓ (meta-text)). One
prompt serves every path: prompts/system_prompt.txt, whose blocks the PROMPT_* flags
of llm_config.txt select β prompts/RUNBOOK.md explains each β followed by the vocabulary
and the examples; python3 prompt_template.py --preview prints it. Each answer carries the AMΔR/TEATER records behind
the chosen term (teater_category_ids), and a homonym the vocabulary build qualified (zΓ‘mek (sΓdlo elity)) comes
back as its plain label.
| Path | Model | Reads | Writes |
|---|---|---|---|
the service, kind=controlled|both | an inference service: OpenRouter, Ollama | the record's lines | the record's enrichment block, entities[].pid |
openrouter_client.py, ollama_client.py (batch) | the same | records, CSV, TEITOK (per line); .md/.txt (per document) | <doc_id>_enriched.json, the record with --document-json-out |
llm_run.py (research, GPU) | local weights, vLLM or transformers | CSV, TEITOK | <doc_id>_enriched.json |
The first two share llm_client_shared.py, which needs no GPU stack (requirements_remote.txt); the third is the
bake-off path of atrium-keyword-extract#2 (llm_utils.py, requirements_llm.txt, the llm Docker target).
pip install -r requirements_remote.txt
export OPENROUTER_API_KEY=sk-...
python openrouter_client.py --input CTX000000001.document.json --model openai/gpt-4o-mini \
--document-json-out CTX000000001.kw.document.json
python ollama_client.py --input data_samples/DOC_LINE_CATEG --model qwen2.5:14b
The research run on a GPU node:
pip install -r requirements.txt -r requirements_llm.txt # vLLM brings its own torch pin
$EDITOR llm_config.txt # MODEL_KEY; INPUT_DIR (line CSVs or *.teitok.xml), OUTPUT_DIR, PARADATA_DIR
python llm_run.py --download # where huggingface.co is reachable: MODEL_KEY into the HF cache
python llm_run.py # or: python llm_run.py <another config>
A GPU node without internet access (HF_HUB_OFFLINE=1) loads a model only from the Hugging Face cache, so
fetch it first on a machine that has access, into the same HF_HOME (--download KEY ... takes several keys).
It fetches what a run reads (configs, tokenizer, weights; not README or LICENSE), skips files already cached
(huggingface_hub then reports 0 B), and ends with what the cache holds. A run whose model files are not all
cached stops before loading and names what is missing.
It writes <OUTPUT_DIR>_<model>/<doc_id>_enriched.json per document (and <doc_id>_enriched.abort.json after ten
consecutive errors), skips a document whose output already exists, so a stopped job resumes (delete an aborted
document's two files to redo it), and writes a paradata record naming the vocabulary build and the sha256 of the
prompt it used. The whole prompt is about 58k tokens: a model served with a smaller window, including the large vLLM
models at their default MAX_MODEL_LEN of 16384, sees only a prefix of the vocabulary, and the paradata records how
many terms were cut. vLLM computes the prompt's shared part once and reuses it (prefix caching), and so does
BACKEND=transformers with REUSE_PROMPT_CACHE=true; without it every line re-reads the whole prompt.
PREFILL_CHUNK_SIZE keeps the transformers prefill within a 24 GB card. Hybrid models
(Qwen3.5, Qwen3.6) need flash-linear-attention, which requirements_llm.txt lists: without it their
linear-attention layers run on a reference kernel that is far slower and does not fit. The llm image carries no
sample inputs, and whatever it writes outside /data goes with the container, so point INPUT_DIR, OUTPUT_DIR
and PARADATA_DIR under /data in the llm_config.txt the compose file mounts.
The vocabulary:
vocab_sources.py, vocab_build.py, vocab_review.py, vocab_manager.py β harvest the AMΔR and TEATER vocabularies,
build the nested union, review it; the built files are in data_samples/vocab (see its
RUNBOOK). The vocabularies are CC0; the build is to be published as a versioned
release asset for the translator and the end-to-end test.corpus_review.py β the vocabulary-gap review used for the TEATER experiment.The LLM engine and the vocabulary code existed twice, in nlp-enrich and in llm-enrich, and partly diverged. This
repository holds the one copy: nlp-enrich's came with the repository, and llm-enrich's (atrium-digital-convert
31534d5) was merged into it on 2026-10-07 β its remote clients, its service path and its later engine fixes.
The code is MIT. The licence of a run's output is computed from the components it used, as declared in
para_config.txt, and the most restrictive one wins:
| Component | Licence | When |
|---|---|---|
| KER (legacy) | MIT | method=legacy |
| KeyBERT, sentence-transformers, the embedding model | MIT / Apache-2.0 | method=keybert |
| YAKE | AGPL-3.0 | method=yake β a run that uses it is declared AGPL-3.0 |
| AMΔR and TEATER vocabularies | CC0 | the vocabulary build, the controlled kind |
The language model of the controlled kind has no row: its terms depend on the model a deployment chooses
(OPENROUTER_MODEL, OLLAMA_MODEL, MODEL_KEY), whose id every run records in its paradata. The source document's
own licence applies to its text.
| Target | Image | What it is |
|---|---|---|
api | ghcr.io/ufal/atrium-keyword-extract-api | the service, both kinds β the production image (no LLM weights) |
base | ghcr.io/ufal/atrium-keyword-extract | the batch CLI (keywords.py) |
llm | ghcr.io/ufal/atrium-keyword-extract-llm | the research LLM batch run, GPU |
docker compose --profile api up # the service on :8000
docker compose run --rm kw -i /data/UDP -m yake # the batch CLI
docker compose -f docker-compose.yaml -f docker-compose.gpu.yaml --profile llm run --rm kw-llm
The images of atrium-nlp-enrich (its -llm target) and atrium-llm-enrich that carried these stages before
1 October 2026 stay published for consumers pinned to them and receive no new tags.
See CONTRIBUTING.md. For support write to lutsai.k@gmail.com, responsible for this repository.
Β©οΈ 2026 UFAL & ATRIUM