Turn an image of a civil-registry or parish record into validated structured JSON.
tabellio: the Roman / medieval scribe who officially drafted legal acts.
The 'vieux tabellion' Georges Brassens addresses in Supplique pour être enterré sur la plage de Sète (1966):
Trempe dans l'encre bleue du Golfe du Lion,
Trempe, trempe ta plume, ô mon vieux tabellion,
Et de ta plus belle écriture,
Note ce qu'il faudrait qu'il advînt de mon corps…
This library reads back what that pen wrote.
- BYOK — you pass your own VLM API key; it is never stored, never logged.
- Provider-agnostic — Gemini, OpenAI, NVIDIA NIM, Anthropic, or a local Ollama model. None required at install.
- No embedded model, no training, no storage. Reliability comes from a strict Pydantic schema and post-extraction consistency rules, not from trusting the model.
- No silent resolution. Original spelling is kept, every date carries a
qualifier(exact / about / calculated / …), every field aconfidence. - Verbatim transcription too. The full diplomatic text of the act,
[?]for an illegible word — asact.transcriptionin full mode, or on its own withoutput_mode="transcription". - GEDCOM 7 out of the box.
tabellio.to_gedcom(act)— schema built to map cleanly to GEDCOM. - Any language or script. Text is transcribed in the language of the act
(Latin, French, German, …) and never translated; only the schema's
type/rolevocabulary is fixed English. The detected language is recorded onact.language.
pip install "tabellio[gemini]" # or [openai] / [nim] / [anthropic] / [ollama] / [all]Requires Python 3.11+.
A parish burial act from 1757, a public-domain register page,
examples/sample_act.jpg:
export TABELLIO_PROVIDER=...
export TABELLIO_KEY=...
for mode in full simple transcription; do
python -m tabellio examples/sample_act.jpg --output "$mode"
done
python -m tabellio examples/sample_act.jpg --format gedcom| Run | Output | What you get |
|---|---|---|
--output full |
sample_act.full.json |
every field, raw spelling, confidence, date qualifier, transcription, warnings |
--output simple |
sample_act.simple.json |
type / date / location / persons only |
--output transcription |
sample_act.transcription.json |
the verbatim text + detected language |
--format gedcom |
sample_act.gedcom |
a GEDCOM 7.0 document — SOUR + INDI (deceased + witnesses + officiant) + BURI / derived DEAT events |
import tabellio
act = tabellio.parse(
"register-page.jpg",
provider="gemini",
api_key="...", # your key, BYOK
act_type_hint="baptism", # optional
)
print(act.type) # ActType.BAPTISM
print(act.date.raw) # "le douziesme jour de may mil sept cens trois"
print(act.date.iso) # "1703-05-12"
for p in act.persons:
print(p.role, p.given.value if p.given else None, p.surname.raw if p.surname else None)
print(act.warnings) # consistency + low-confidence flagsparse() returns an Act Pydantic model. Unread
fields are null with a note — never guessed.
for mode in ("full", "simple", "transcription"):
print(tabellio.parse("register-page.jpg", output_mode=mode))output_mode="full"(default) — richAct: raw spelling, per-fieldconfidence, datequalifier,transcription, validationwarnings.act.dateis the date of the act; the subject's real birth / death (often a different day — "né la veille", "décédé hier") lands onpersons[i].birth_date/death_datewithqualifier: "calculated"+ a note when deduced.validatewarns if a baptism or burial has no such date on its subject.output_mode="simple"— a bareActSummary:type,date(ISO string ornull),location,persons(role / given / surname). Nothing else — no confidence, no raw, no qualifiers, nolanguage, no life-event dates, no transcription. Shortest prompt, fewest tokens.output_mode="transcription"— aTranscription:{text, language}— the verbatim text of the act ([?]/[illegible]for gaps) and its detected language, nothing else. Still JSON, just two fields.
tabellio.parse(
img,
act_type_hint="burial", # --hint
act_language_hint="fr", # --lang
context="The deceased is Joseph BOMMAL, buried 21 December 1757.", # --context
)All three are optional. act_type_hint / act_language_hint are guesses the
model verifies against the image. context is free text of whatever you already
know — names, a date, a place. It is used only to disambiguate hard-to-read
passages: it never overrides a clear reading, raw stays faithful to the page,
and in full mode the model adds a note wherever it followed the image
against your context.
act = tabellio.parse("register-page.jpg") # the one network call
print(act.model_dump_json(indent=2)) # JSON
print(tabellio.to_gedcom(act)) # a complete GEDCOM 7.0 documentto_gedcom(act) writes one SOUR record for the act, one INDI per person it
names, and a FAM for the couple or parent-child link the act states —
BIRT / BAPM / MARR / DEAT / BURI events with DATE / PLAC / AGE,
ASSO + ROLE for witnesses and godparents, QUAY from confidence. It
transcribes one act; it does not merge a tree — your genealogy software does
that on import. Output is conformance-checked against the gedcom7 library in
the test suite.
CLI: python -m tabellio examples/sample_act.jpg --format gedcom.
The schema is GEDCOM-mappable by design: GenDate.qualifier
(about/before/after/calculated → ABT/BEF/AFT/CAL),
GenDate.calendar (julian, french_republican), Person.name_particle
(SPFX), Person.name_suffix (NSFX).
Three optional environment variables, each a fallback for the matching
parse() argument. An explicit argument always wins. tabellio reads them from
os.environ — it does not load a .env file; your application manages its own
environment.
| Variable | Fills | Default |
|---|---|---|
TABELLIO_PROVIDER |
provider= |
gemini |
TABELLIO_KEY |
api_key= |
— (required except ollama) |
TABELLIO_MODEL |
model= |
the provider's default |
The key is never logged and never stored.
export TABELLIO_PROVIDER=gemini
export TABELLIO_KEY=... # in your shell
python -m tabellio examples/sample_act.jpg [--hint baptism] [--lang fr] [--context "..."] \
[--output simple|transcription] [--format gedcom] [-v]Prints the result as JSON — Act, ActSummary or Transcription per
--output — on stdout, warnings on stderr; --format gedcom prints a GEDCOM 7
document instead. The key is only read from the environment, never a CLI
argument.
provider= |
Extra | Default model | Cost |
|---|---|---|---|
gemini |
tabellio[gemini] |
gemini-3.6-flash |
free tier, no card — best on acts |
mistral |
tabellio[mistral] |
mistral-small-latest |
paid (mistral-medium-latest for harder hands) |
openai |
tabellio[openai] |
gpt-4o |
paid |
anthropic |
tabellio[anthropic] |
claude-sonnet-4 |
paid |
nim |
tabellio[nim] (pulls Pillow) |
meta/llama-3.2-11b-vision-instruct |
free, but slow / weak here |
ollama |
tabellio[ollama] |
llama3.2-vision |
local, no key |
For a no-cost run, gemini (its AI Studio free tier needs no credit card).
openai / anthropic are OpenAI/Anthropic-compatible, so third-party gateways
(Groq, OpenRouter, GitHub Models — some with free tiers) work via
provider="openai", base_url=... with no extra code.
Set TABELLIO_MODEL (or pass model=) to switch model — quality vs speed is
your call. Keyword options pass straight through: timeout= (seconds, default
120), base_url=, host=, max_tokens=.
NIM caveat. On the hosted free endpoint (integrate.api.nvidia.com) the 90B
vision model routinely times out; the default is therefore
meta/llama-3.2-11b-vision-instruct, which answers in seconds but is a small
model — it hallucinates names and dates on old cursive and returns poor results
here. Use --output simple with it, and treat gemini as the real path.
The endpoint also rejects inline images larger than ~180 KB: tabellio re-encodes
an oversized image in memory (lower JPEG quality, then downscale, long edge
kept ≥ 1200 px) before sending — the source file is never touched — and logs a
warning; pass shrink=False to get an error instead. NVCF asset upload is not
implemented.
Local / general-purpose VLMs hallucinate plausible names and dates on old cursive. The
ollamaprovider exists for convenience and testing, not for production genealogy.
In: extraction of a single record to schema, ambiguity flags, confidence surfacing, provider-agnostic adapter, JSON and GEDCOM 7 output.
Out: embedded/trained model, home-grown HTR, storage, accounts, billing, GEDCOM import, building or merging a family tree, image segmentation / deskew, translation of any transcribed text.
make install # uv sync --all-extras
make lint # ruff check + format --check
make test # pytestTo run python -m tabellio against a real provider while developing, export
TABELLIO_KEY (and optionally TABELLIO_PROVIDER / TABELLIO_MODEL) in your
shell, or use your own direnv / dotenv — the repo ships none.
The examples/ directory holds a sample act and its extracted output —
fictional or public-domain only, see examples/README.md.
