WordDetector model
Training, export and demo code for the WordDetector word-detection model. The WordDetectorNN model was originally created by Harald Scheidl and is reimplemented here with some best practices and integrated into Xournal++ HTR according to the ADRs (in particular ADR 006 and ADR 007).
The source lives under
xournalpp_htr/training/word_detector/.
Structure (ADR 006)
This is no longer a standalone uv project; it is part of the main package.
| File | Purpose | Deps |
|---|---|---|
config.py |
Hydra structured config (single source of truth for all constants) | — |
network.py |
WordDetectorNet architecture + training loss |
training-word-detector |
dataset.py |
IAM dataset loading + ground-truth encoding | training-word-detector |
train.py |
Training entrypoint (Hydra CLI) | training-word-detector |
export.py |
ONNX + config.json export, HF Hub upload |
training-word-detector |
infer.py |
Local torch inference from a .pth checkpoint |
training-word-detector |
demo.py |
Local Gradio demo (run locally, not a HF Space, ADR 007) | training-word-detector |
utils.py |
Git-hash, JSON encoder, example-image list | training-word-detector |
test_best_model.ipynb |
Inspect a trained checkpoint offline | training-word-detector, dev |
run_training.sh |
Hyperparameter sweep (grid search) | training-word-detector |
run_training.eval.sh |
Find the best model from a sweep | — |
Generic geometry and the map decoder/clustering/metrics live in
xournalpp_htr/training/shared/ (base deps only, importable in the lean
inference install). The HF-Hub-backed inference class lives in
xournalpp_htr/inference_models.py.
GPU training setup (step-by-step)
Prerequisites: a Linux machine with an NVIDIA GPU, CUDA drivers installed
(nvidia-smi should work), and uv installed (pip install uv).
1. Clone and install the base package
git clone https://github.com/PellelNitram/xournalpp_htr.git
cd xournalpp_htr
bash INSTALL_LINUX.sh
The install script downloads the HTRPipeline models via wget from Dropbox.
If this fails (e.g. corporate proxy blocking SSL), download models.zip on
another machine and copy it into
external/htr_pipeline/HTRPipeline/htr_pipeline/models/, then unzip -o models.zip.
After that, copy the ONNX/JSON files into the venv:
mkdir -p .venv/lib/python3.11/site-packages/htr_pipeline/models/
cp external/htr_pipeline/HTRPipeline/htr_pipeline/models/*.onnx \
external/htr_pipeline/HTRPipeline/htr_pipeline/models/*.json \
.venv/lib/python3.11/site-packages/htr_pipeline/models/
2. Install the training extra (with CUDA PyTorch)
uv sync --extra training-word-detector
This installs PyTorch with CUDA support (cu128 index configured in
pyproject.toml). Verify GPU access:
uv run python -c "import torch; print(torch.cuda.is_available(), torch.cuda.get_device_name(0))"
If the CUDA version doesn't match your driver, update the pytorch-cu128
index URL in pyproject.toml to the appropriate version (e.g. cu121,
cu124) and re-run uv sync --extra training-word-detector.
3. Verify the installation
make tests-not-slow
All tests should pass except test_run_htr::test_main which requires the
xournalpp desktop application (not needed for training). No datasets are
required for this step.
4. Authenticate with HuggingFace
Required for downloading the training dataset and (later) uploading the exported model:
hf auth login
5. Download the training dataset
hf download PellelNitram/xournalpp_htr_IAM_DB --repo-type dataset
The first run caches the dataset under ~/.cache/huggingface/. Subsequent
runs resolve the cache instantly.
6. Train
Single training run (uses Hydra for configuration):
uv run python -m xournalpp_htr.training.word_detector.train \
training.epoch_max=200 training.batch_size=32 training.learning_rate=0.001
To enable train-time data augmentations (off by default):
uv run python -m xournalpp_htr.training.word_detector.train \
augmentation.enabled=true training.epoch_max=200
Show all configurable parameters and their defaults:
uv run python -m xournalpp_htr.training.word_detector.train --cfg job
Or run the full hyperparameter sweep:
cd xournalpp_htr/training/word_detector
bash run_training.sh
Results are written to experiments/experiment1/lr<LR>_bs<BS>/. Each run
produces best_model.pth, best_model.json (best val F1, epoch),
TensorBoard logs in summary_writer/, and config.yaml.
The script also contains experiment2, an augmentation ablation study
that compares augmentation.enabled=false vs true across three data
splits. Results go to experiments/experiment2/aug<BOOL>_seed<SEED>/.
Monitor training with TensorBoard (forward port 6006 if remote):
tensorboard --logdir experiments/ --port 6006
The first training run builds a dataset_cache.pickle from the HF-cached
raw files. Subsequent runs load directly from this pickle, skipping
both the HF validation and image preprocessing.
7. Evaluate the sweep
bash run_training.eval.sh
Reports the F1 score for each completed run and prints the best model path.
8. Inspect the best model
Use the best model path from the previous step. Visually check it with the Gradio demo:
uv run python -m xournalpp_htr.training.word_detector.demo \
--model-path experiments/experiment1/<best_run>/best_model.pth \
--device auto --share
--share exposes a temporary public URL (useful on headless machines).
9. Export to ONNX
uv run python -m xournalpp_htr.training.word_detector.export \
--checkpoint experiments/experiment1/<best_run>/best_model.pth \
--output-dir exports/
Produces exports/model.onnx and exports/config.json.
10. Validate the ONNX export
Run the notebook to compare PyTorch and ONNX predictions side by side.
The notebook expects best_model.pth in the word_detector/ directory:
cp experiments/experiment1/<best_run>/best_model.pth best_model.pth
uv sync --extra dev # adds jupyter
uv run jupyter nbconvert --to notebook --execute test_best_model.ipynb \
--output test_best_model_executed.ipynb
11. Upload to HuggingFace Hub
Once satisfied with the model quality:
uv run python -m xournalpp_htr.training.word_detector.export \
--checkpoint experiments/experiment1/<best_run>/best_model.pth \
--output-dir exports/ --upload
Requires write access to PellelNitram/xournalpp-htr-word-detector.
Inference
Once model.onnx + config.json are on the Hub, inference is uniform across
all custom models (no transformers dependency):
from xournalpp_htr.inference_models import WordDetectorModel
model = WordDetectorModel.from_pretrained() # or revision="v1.2.0"
boxes = model.detect(grayscale_image) # list[BoundingBox]
WordDetector is detection-only: it produces word bounding boxes but no
transcription/labels. It is therefore not exposed as a compute_predictions
pipeline (ADR 003), which contracts word-level boxes and transcriptions. The
WordDetectorModel class is the integration point; wiring it into a full
pipeline waits on a recognition stage that adds labels.
Best model
Experiment 3, augmentation on, seed 42 — F1 0.8896.
Checkpoint: experiments/experiment3/augtrue_seed42/best_model.pth.
Configuration: bs=10, lr=0.001, augmentation enabled, epoch_max=10000
(early stopping with patience 50).
Experiments
A log of training experiments run on this model. Each entry should capture what was tried, why, and what the outcome was, so that future work can reproduce or build on the result.
Suggested entry template:
### <date> — <short title>
- **Goal:** what question this experiment is meant to answer.
- **Setup:** dataset split, config overrides, code revision (commit hash).
- **Command:** the exact training/eval command used.
- **Results:** key metrics (val F1, etc.), path to artefacts under
`experiments/`, TensorBoard run name.
- **Conclusion:** what was learned and what to try next.
2026-06-01 — Experiment 3: augmentation under original training regime
- Goal: does augmentation help when training closer to the original WordDetectorNN regime (bs=10, unbounded epochs with early stopping)? Experiment 2 showed augmentation hurting under our regime (bs=32, epoch_max=100), but the original repo always uses augmentation with a much smaller batch size and no epoch cap.
- Setup: IAM-DB, 80/20 random split, lr=0.001, bs=10, epoch_max=10000
(effectively unbounded, relies on patience_max=50), input 448x448.
Augmentation on vs off, 3 seeds each (42, 43, 44). Code revision
948f48f. - Command:
bash run_training.sh(experiment3 function). - Results:
| Augmentation | Seed 42 | Seed 43 | Seed 44 | Mean F1 |
|---|---|---|---|---|
| Off | 0.8750 | 0.8786 | 0.8799 | 0.8779 |
| On | 0.8896 | 0.8819 | 0.8787 | 0.8834 |
Augmented runs trained longer before early stopping (139–416 epochs vs 95–165 epochs), indicating augmentation adds useful diversity the model can exploit given enough training time.
Artefacts: experiments/experiment3/aug{true,false}_seed{42,43,44}/.
- Conclusion: under the original-like regime (bs=10, unbounded epochs), augmentation helps (+0.55 pp mean F1). Both conditions also outperform experiment 2 overall (mean F1 ~0.88 vs ~0.86), confirming that the smaller batch size with longer training is beneficial. The recommended final training configuration is bs=10, augmentation on, unbounded epochs with early stopping (patience 50).
2026-05-28 — Experiment 2: augmentation ablation
- Goal: does train-time data augmentation improve word detector performance?
- Setup: IAM-DB, 80/20 random split, lr=0.001, bs=32, epoch_max=100,
patience_max=50, input 448x448. Augmentation on vs off, 3 seeds each
(42, 43, 44). Code revision
58a26e4. - Command:
bash run_training.sh(experiment2 function). - Results:
| Augmentation | Seed 42 | Seed 43 | Seed 44 | Mean F1 |
|---|---|---|---|---|
| Off | 0.8596 | 0.8681 | 0.8590 | 0.8622 |
| On | 0.8571 | 0.8574 | 0.8609 | 0.8584 |
Artefacts: experiments/experiment2/aug{true,false}_seed{42,43,44}/.
- Conclusion: augmentation slightly hurts performance (~0.4 pp lower mean F1). However, our training regime differs from the original WordDetectorNN (bs=10, unbounded epochs with early stopping, first-20-sample val split, 350x350 input). Experiment 3 will test augmentation under an original-like regime (bs=10, unbounded epochs) to see if augmentation helps there.
Current status
Everything from the original WordDetectorNN model is reimplemented, including
train-time data augmentations (geometric and photometric, matching
githubharald/WordDetectorNN). Augmentations are off by default and can be
enabled via augmentation.enabled=true. The bentham sample should be
re-checked for correctness as it currently fails badly.
Outlook
- Use PIL images everywhere instead of numpy to keep track of channel order.
- Revisit
PyTorchModelHubMixinonce the architecture stabilises (ADR 006).