libellus
Generates typeset booklets for Vespers in the usus antiquior — Gregorian chant notation via Gregorio, Latin and an interlinear vernacular, ready to print and fold.
You describe a celebration in one YAML file. libellus resolves it against a
chant library, generates the psalm notation for whichever Liber Usualis tone you
name, renders LaTeX, compiles it, pads the page count to a multiple of four and
imposes the booklet.
It was written for a schola in Bremen and is used there for real; the two feasts
in feasts/ are booklets that have actually been sung from. It is licensed
0BSD — do anything
you like with it, no attribution required.
Contents
- What it produces
- Requirements
- Install
- Quickstart
- The feast spec
- The browser form
- Choosing a Psalter
- Latin-only booklets
- The Kurzfassung
- Draft booklets
- Bundling a feast
- Transcribing text to gabc
- Repository layout
- Working on libellus
- Releasing
- Bremen operations
- Credits and licence
What it produces
St. Lambert, Second Vespers — shown here as a Latin-only booklet.
Three PDFs per build: the booklet itself, a -pdfjam imposition for booklet
printing, and a -pdfjam-duplex variant that rotates every second page for
duplex printers without a binding-edge option.
A booklet contains the whole office in order — incipit, five antiphons each with
its psalm pointed to a chosen tone, capitulum, hymn, responsory (in the
monastic rite), versicle, the Magnificat with its antiphon, preces and Pater
noster where the rite has them, oration, conclusion, the seasonal Marian
antiphon, optional filler pages, and a back cover. Rubrics are red, postures and
who-sings-what are marked in the margins, and section dividers use
pgfornament motifs.
Requirements
libellus is a Python package, but the typesetting is done by external tools it
calls. You need all of these on PATH:
| tool | why |
|---|---|
LuaLaTeX with --shell-escape |
typesets the booklet |
Gregorio / gregoriotex |
sets the chant notation (ships with TeX Live) |
pgfornament, tikz, fontspec, … |
ornaments and layout (TeX Live) |
| Node.js or Bun | runs the psalm-tone engine, on every build |
pdfjam |
imposes the booklet (TeX Live) |
pdftk |
rotates alternate pages for duplex printing |
Fonts: EB Garamond, Charis SIL (the interlinear translation face) and
XITS (for ✠). All three ship with TeX Live — as the packages ebgaramond,
charissil and xits — so nothing needs installing at the system level, and
the booklet sets the same on Linux as on macOS.
A full TeX Live install covers everything but Node.js and pdftk. On Debian or
Ubuntu: apt install texlive-full nodejs pdftk-java. On macOS: MacTeX, then
brew install node pdftk-java.
Without LuaLaTeX on PATH, libellus build stops after staging and tells you
so; the staged folder has its own Makefile, so you can run make in it
wherever the tools do exist.
Install
pip install libellus
or, to work on it:
git clone https://github.com/OmarEjjeh/libellus
cd libellus
uv sync --all-groups
The package carries everything that is the tool: the per-rite skeletons, the chant library, the psalm-tone engine and the border artwork. It deliberately carries no German psalm translations — see Choosing a Psalter.
Quickstart
libellus runs in a working directory, which is where it looks for your
content — feast specs, pictures, and a Psalter — and where it writes build/.
Everything else comes from the installed package.
libellus build feasts/2026-09-18-lambertus.yaml
That stages build/2026-09-18-lambertus/ — the rendered TeX plus every asset it
references and a Makefile — compiles it, and imposes the result.
A minimal working directory looks like this:
my-parish/
├── feasts/
│ └── 2027-01-25-conversio-pauli.yaml
├── images/
│ └── conversio/back-cover.jpg
└── psalter/
└── my-translation/… ← see below
One caveat if you cloned this repository: the two feast specs in feasts/
reference the Einheitsübersetzung, which is not distributable and is not here
(ADR-0023). Out of the box you can build them only as
Latin-only booklets. Supply your own Psalter, or wait for
the public-domain German one — it is an open issue and the intended default.
The feast spec
One YAML file describes one celebration and is always self-contained: no includes, no inheritance. It is plain text, never LaTeX (ADR-0001/0002) — everything about typesetting lives in the templates, and everything about this celebration lives in the spec.
title: Sancti Lamberti
header: S. Lambertus, Ep. et Mart. # running header
rank: Semiduplex # closed vocabulary, ADR-0015
vesperae: II # First or Second Vespers
date: 2026-09-18 # when it is actually sung
rite: romanum-cum-precibus # which ordo
source: Antiphonale Romanum · mcmxlix # printed on the cover
antiphonae:
- gabc: chant/ant/omnes-sancti-quanta-passi.gabc
de: Alle Heiligen, wie viele Qualen haben sie erlitten…
psalmus: 109 # Vulgate numbering
tonus: 8G # mode + differentia, as the books print it
# … five in the Roman rite, four in the monastic
capitulum:
ref: Iesu Sirach 50, 5–10
versus:
- n: 5
text: Qui præváluit amplificáre civitátem…
de: Wie herrlich war er, umgeben vom Volk…
hymnus:
gabc: chant/hymni/sanctorum-meritis.gabc
de: ["…", "…"] # one entry per stanza
# … versiculus, magnificat, oratio, antiphona_bmv, back_cover
Key fields:
rite— the ordo, which fixes the sequence of elements:romanum-1962,monasticum, orromanum-cum-precibus(the Bremen group's hybrid). It also decides how many antiphons there are and whether a responsory is sung.gabc:— either a path into the chant library or Gregorio notation pasted inline (ADR-0005). Inline notation is materialised at build time and behaves exactly like a library file.tonus:— a tone label the way the books print it:8G,8 G,8G*,peregrinus. Mode plus differentia fully determine the ending, so the EUOUAE cue is derived rather than typed (ADR-0016/0017).psalmus:— any psalm 1–150 in Vulgate numbering, plus the Magnificat. Nothing is pre-generated: the notation is produced on demand for whatever(psalm, tone)you ask for.note:— on any proper, a provenance footnote (ADR-0011): where this transcription or translation came from. It prints as a real footnote and survives a Latin-only booklet.image:— a path in your working directory, or adata:URI embedded in the spec itself (ADR-0019).
Every error is reported in German, all of them at once, naming the field.
The browser form
form/formular.html is a single self-contained HTML file that composes and
loads feast specs in the browser, with an instant notation preview
(ADR-0003/0004). Open it directly — there is no server and no build step.
It reads its vocabularies (tone labels, ordinarium chants, chant and image paths, which psalms have German) from a JSON data island embedded in the file. Regenerate it whenever those change:
libellus export-form-data # --check only reports staleness
Choosing a Psalter
A Psalter is one translator's complete German for the sung verses: one directory holding a file per psalm plus the Magnificat.
psalter/
└── eu1980/
├── 109.yaml
├── 110.yaml
└── magnificat.yaml
# psalter/eu1980/109.yaml
psalmus: 109
verses:
1: So spricht der Herr zu meinem Herrn…
2: …
One line per sung verse, including both Gloria Patri verses, in Vulgate numbering and Vulgate verse boundaries — which differ from how modern translations divide and number the psalms, so a translation usually has to be re-cut editorially (ADR-0007).
A feast picks one with psalter_de: eu1980; without that field the first
available is used. No Psalter ships with libellus: the one Bremen prints from
is the Einheitsübersetzung, which is copyright Katholische Bibelanstalt and not
redistributable. So you either supply your own, or print
Latin-only.
A public-domain German psalter is wanted and not yet done — it is an open issue and the intended long-term default.
Latin-only booklets
For a community that sings the office in Latin alone, and the way to use
libellus with no Psalter at all:
latin_only: true
Every translation goes: the antiphons', capitulum's, hymn's, responsory's,
versicle's and oration's de, and the interlinear German beneath the verses.
Every de field becomes optional, so you simply leave them out.
The booklet's German rubrics and headings stay — „Schola", „Man steht", „Alle — beide Seiten Vers um Vers im Wechsel". They name the parts of the office and tell the congregation what to do; they are not a translation of anything sung. Provenance footnotes stay too, for the same reason. (Latin rubrics are a separate, larger job — see ADR-0012 on multi-locale support.)
There is deliberately no --latin-only flag: this is a standing property of a
community rather than a choice made per printing, and it relaxes what a valid
spec is, which the browser form has to be able to see (ADR-0025).
The Kurzfassung
For singers who already know the office: the first verse of each psalm and of the Magnificat in notation, every later verse as pointed Latin text with its German beneath, and one notated stanza of the hymn (ADR-0022).
libellus build --compact feasts/2026-09-18-lambertus.yaml
compact: true in the spec does the same, and --no-compact overrides it —
unlike a draft, because the full booklet for visitors and the short one for the
schola are both wanted the same evening. They build into separate folders
(build/<feast>-kurzfassung/), so both can be printed.
The Magnificat is the exception: „Magníficat" is one word and cannot carry the tone's cadence, so its first two verses share one system as the Liber prints them — verse 2 beneath verse 1, reciting notes hollow. Those systems are generated per tone and committed; after touching the generator, refresh them:
libellus magnificat-systems # --check only reports staleness
To proofread that notation against a printed Liber, build the correction sheet — all 33 tones at the booklet's own page size:
uv run scripts/magnificat-proof/generate.py
Draft booklets
To send a booklet round for corrections, build it as a draft — every page then
carries PRO MANUSCRIPTO and the time it was made, so nobody prints it by
mistake (ADR-0021):
libellus build --draft feasts/2026-09-18-lambertus.yaml
draft: true in the spec does the same permanently, and is what the form sets.
Either alone suffices; a final booklet means removing the field.
Bundling a feast
To hand a feast to someone else, pack it into a single file — every chant and picture embedded, no outside references (ADR-0019):
libellus bundle feasts/2026-07-10-benedictus.yaml
That file alone builds the booklet. It is an export, not an authoring format: mostly base64, so keep editing the readable spec. Note that a bundle does not embed the Psalter — the German verses still come from your working directory.
Repository layout
feasts/ the feast specs — the artifact you edit
form/formular.html self-contained browser form
images/<feast>/ pictures, one folder per celebration
psalter/<versio>/ German translations (not in the package)
src/libellus/
├── cli.py schema.py resolve.py render.py stage.py compile.py …
├── template/ per-ordo skeletons and shared partials
├── chant/ the chant library — all gabc lives here
│ ├── ant/ hymni/ vers/ resp/ propers by genre
│ ├── ordinarium/ the fixed parts
│ ├── psalmi/ magnificat/ incipit table, committed systems
├── images/borders/ gilded back-cover border tiles
└── psalm-library/ the vendored jgabc psalm-tone engine
docs/adr/ architecture decisions — read these
CONTEXT.md the domain glossary
archive/ retired offices, kept for reference
The pipeline is validate → resolve → render → stage → compile. Resolution
records every file the TeX will read, so staging can copy exactly those into a
self-contained folder; libellus/paths.py decides, per asset, whether it comes
from the package, the build cache or your working directory (ADR-0024).
Working on libellus
uv sync --all-groups
uv run playwright install chromium # for the form tests
uv run pytest
The suite needs no LaTeX: the build tests stop before compiling. It resolves its
German against a synthetic probe Psalter under tests/fixtures/psalter/, so
it gives the same answers with or without a real Psalter present, and never
asserts against copyrighted text.
Two things worth knowing before touching the templates:
- The layout is sensitive to the preamble's line structure. Adding even a LaTeX comment there has repaginated a booklet. When changing anything typographic, compare page-by-page renders before and after rather than trusting that a comment is inert.
- A LaTeX
%comment in a template still reaches the.tex. Use\#{ … }for a comment that should not be emitted, and remember that template conditionals run at render time while\if…runs at compile time.
Commit messages are Conventional Commits,
and that is load-bearing: commitizen derives the version and the changelog from
them.
Releasing
cz bump # writes the version + CHANGELOG.md, commits, tags
git push --follow-tags # pushing the tag is what publishes
Pushing the tag triggers release.yml, which builds the artifacts, installs the
wheel into a clean environment, sets a whole booklet from it, and only then
publishes to PyPI via Trusted Publishing. Releases are deliberate: nothing is
published by merging (ADR-0023).
While the feast-spec format settles, the project stays on 0.x — a breaking
change bumps the minor. Set git config push.followTags true once so a tag is
never left behind.
Bremen operations
Notes for whoever keeps the Bremen booklets going.
- Two clones.
libellus, plus the privatepsalter-eurepository checked out intopsalter/. Without the second one, only Latin-only booklets build. - The psalter is Einheitsübersetzung and must not be published: not in this
repository, not in the wheel, not in a bundle sent outside the parish.
psalter/is gitignored and CI fails if any of it reaches an artifact. HANDOFF.mdat the root is the running state of work in progress. It is untracked on purpose — it names people and quotes correspondence — and stays that way.- The Benedict feast pins
psalter_de: eu2016, the hand-made re-cuts its printed booklet was set from. Leave it pinned. - Adding a picture:
.gitignoredeliberately ignores*.pngand*.jpg, so a new image is invisible until you add it explicitly withgit add -f. If a feast will not build on a fresh clone, this is why. - Before printing: build without
--draft, check the page count is a multiple of four, and print the-pdfjam-duplexPDF on a duplex printer without a binding-edge option, or-pdfjamotherwise.
Credits and licence
The code and templates are 0BSD. The repository also carries third-party material under its own terms — GregoBase's CC0 chant, the public-domain Clementine psalter, Ben Bloomfield's Unlicense psalm-tone engine, the border artwork and the hymn translations. All of it is listed, with what the licence does and does not cover, in CREDITS.md.
Decisions live in
docs/adr/ and the
domain vocabulary in
CONTEXT.md. If
you are picking this up cold, read ADR-0001 and ADR-0024 first: what a feast
spec is, and where the files come from.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file libellus-0.1.0.tar.gz.
File metadata
- Download URL: libellus-0.1.0.tar.gz
- Upload date:
- Size: 3.0 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1829fc6728341aea707854fa26e7ae55c2f5aab790a5135edf5d9d315d52b8ff
|
|
| MD5 |
94e0177b3aa3b5afaf6a74ca218c15b0
|
|
| BLAKE2b-256 |
be57d120336a405221dcd6f43ea88c4d958fdd06b67c9a5d0db8fd3da7468d00
|
Provenance
The following attestation bundles were made for libellus-0.1.0.tar.gz:
Publisher:
release.yml on OmarEjjeh/libellus
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
libellus-0.1.0.tar.gz -
Subject digest:
1829fc6728341aea707854fa26e7ae55c2f5aab790a5135edf5d9d315d52b8ff - Sigstore transparency entry: 2312917780
- Sigstore integration time:
-
Permalink:
OmarEjjeh/libellus@e3874150f0fb2b9223e7064e8f8329a1f7ecde5a -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/OmarEjjeh
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@e3874150f0fb2b9223e7064e8f8329a1f7ecde5a -
Trigger Event:
push
-
Statement type:
File details
Details for the file libellus-0.1.0-py3-none-any.whl.
File metadata
- Download URL: libellus-0.1.0-py3-none-any.whl
- Upload date:
- Size: 2.8 MB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1476385a8eecfd8cf799ffbd200b7edb3ddbafb660dbc2d0a8b225798e2b2b8e
|
|
| MD5 |
c613f2e2cedf037e562dd144e37d9125
|
|
| BLAKE2b-256 |
9366f3e2f9006de8ce5be46520b27674d3b289f649994854a79dcd3d230f8814
|
Provenance
The following attestation bundles were made for libellus-0.1.0-py3-none-any.whl:
Publisher:
release.yml on OmarEjjeh/libellus
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
libellus-0.1.0-py3-none-any.whl -
Subject digest:
1476385a8eecfd8cf799ffbd200b7edb3ddbafb660dbc2d0a8b225798e2b2b8e - Sigstore transparency entry: 2312917785
- Sigstore integration time:
-
Permalink:
OmarEjjeh/libellus@e3874150f0fb2b9223e7064e8f8329a1f7ecde5a -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/OmarEjjeh
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@e3874150f0fb2b9223e7064e8f8329a1f7ecde5a -
Trigger Event:
push
-
Statement type: