Thirty years of test results, discharge summaries, ultrasound reports. Some of it printed by a clinic that no longer exists, in a country you no longer live in, in a language your current doctor does not read. Some of it a photograph of a page, taken in a corridor, because that was the only copy.
You have all of it, and you can find none of it.
Epicrisis Companion turns that box into an archive on your own computer: every value exactly as it was printed, in the language it was printed in, next to the range printed beside it, one click from the original page. Self-hosted, free for you and your family, and — plainly — not a medical device.
Every picture below is the built-in demo — three archives of people who do not exist, drawn as scanned forms in five languages and then transcribed, checked and indexed for real, with no model called and nothing leaving the machine. See all of it, page by page →
For yourself. The appointment is in an hour and the doctor asks how long your haemoglobin has been at that level. Three years, and here are the eleven measurements, in the units each of the three laboratories used.
For your father with type 2 diabetes. Fifteen years of glycated haemoglobin in one line — before it was found, the two years it was bad, the year treatment started, and every year since. This is the picture no single form contains, and the one every endocrinologist asks for.
The shaded band is the reference range printed on the form beside that value, drawn around the value it belongs to. Where a form printed no range there is no band: the range on the form before it says nothing about a value printed without one. It steps because laboratories differ, not because anything was calculated. The rows below the chart are Russian where his clinic was Russian, and English after he moved.
For your grandmother. Her archive is small, on paper, in the language of another decade, and nobody but the family will ever type it up. Twenty documents are still twenty documents you can hand to a doctor as a chart instead of a plastic bag.
And for all three at once, without them ever mixing. One instance, one archive per person, one index file each. Switching between them is a box at the top of the page.
You point it at a folder. It never writes to that folder — it reads.
Every document under the date printed on it: laboratory panels, imaging reports, consultations, the letter a clinic sent on.
Open one and it is the form: names as printed, values as printed, units, the range the laboratory printed beside each value, the mark it put in the margin. The banner says plainly that a model transcribed this and no person has checked it. Correct a line by hand and the correction outlives every later re-reading.
And the page itself is one click away, served from your own disk. Nothing here asks you to take its word for anything.
Creatinine measured in µmol/L in one country and mg/dL in another. Most software would convert one into the other and draw a single line. Epicrisis draws two charts, because the moment a number is converted it is no longer the number printed on the page a doctor can ask you to show.
Blood, urine and stool are kept apart, by a tab. The same printed word — Protein, Glucose, Leukocytes — is a different measurement in blood and in urine, and a chart that puts them on one line lies quietly for years.
And where the archive has only a word — "not detected", "straw" — nothing is drawn and the page says so.
Models misread pages, so the archive does not trust its own reading. Checks that run without a model at all compare the stored number with the printed text, and find ranges printed backwards, dates nobody could settle, corners nobody could read, and the same blood draw filed under three file names. Copies come as a group with one already chosen; you change it only if the choice was wrong. Nothing is changed for you.
Read and in daily use in Russian, Ukrainian, English, Spanish and Greek — including Greek capitals that lose their accents, headings spaced o u t, and tables printed sideways.
![]() |
![]() |
Гемоглобін, Гемоглобин, Hemoglobina, Haemoglobin, Αιμοσφαιρίνη are one test. A model
proposes the grouping, a second and deliberately different model reads it again, a reference is
consulted where that is allowed — and then you approve it, once, for good. That is what makes
a fifteen-year chart possible at all.
For one page, yes — and it will read it better than any rule. For an archive it is a different job, and the difference is structural rather than a matter of prompting.
| A model reading your files | Epicrisis | |
|---|---|---|
| Scale | Thousands of pages do not fit in a context window | Read once, stored, indexed; the whole archive answers in milliseconds |
| Repeatability | Ask twice, get two answers | The reading is a file on your disk; tomorrow's answer is today's |
| Provenance | A number in a chat | Every value carries its file, page and date, and links to the scan |
| Your corrections | Die with the conversation | Stored apart from the model's output, keyed to the printed line, reapplied after every re-read |
| Catching mistakes | You have to notice | Deterministic checks find them; a second, different model reads again and disagreements are shown |
| Units and specimens | Quietly converted, quietly merged | Never converted silently; split by unit and by specimen, and where one test printed at two scales is drawn as one history, every moved point says so |
| Vocabulary | Regrouped differently every time | One vocabulary, approved once, in five languages |
| Several people | One pile | One archive per person, one index file each, by construction |
| With no model at all | Nothing works | Search, charts, index, checks and the whole dashboard |
The model is used for the one thing it is genuinely better at: reading a page. Everything built on top of that reading is ordinary code you can inspect — which is why this archive keeps working when the models change.
| Documents read | 479 |
| Values kept as printed | 5 355 |
| Years covered | 1989–2026 |
| Languages | Russian, Ukrainian, English, Spanish, Greek |
| Institutions as printed | over 200 |
| Tests in the vocabulary | 492 approved groups of spellings |
| Test suite | 1571 tests, no network, no model |
These are real medical records of real people, and whose they are is nobody's business. Nothing
from them appears in this repository: every screenshot here comes from uv run epicrisis demo, which
invents its own people.
- Self-hosted. Your computer or your own server. No account, no service, no telemetry.
- The dashboard listens on
127.0.0.1only. From anywhere else, through an SSH tunnel. - Your scans are read-only. Nothing is copied, moved or renamed. Everything the program
derives sits under
data/. Most of it can be deleted and rebuilt: the index in seconds (uv run epicrisis index), the checks in seconds (uv run epicrisis validate), the walk of the folder in minutes (uv run epicrisis inventory). The readings themselves — what each page is and what was printed on it — can only be made again by paying a model to read the documents again, and it will read them a little differently. And six things underdata/are your own work, which nothing can rebuild: your corrections (corrections.jsonl), your verdicts on findings (judgements.jsonl), the indicators you approved (indicators.json), the doctors and clinics you said were one (people.json), the earlier readings kept when a later one displaced them (replaced/), and your conversations (chats/). Copy those somewhere:uv run epicrisis backup <folder>puts exactly them, and nothing else, in one place.people.jsonlives inside each archive's own folder, because a file beside the instance is a file every archive can see. For one version it sat beside the instance; if yours still has adata/people.json, it is carried into the archives that print those names at every start of the dashboard and byuv run epicrisis people, and both say what moved. A group that no archive here names stays in that file rather than being thrown away, and the Archive status page says how many are waiting. Do not delete it: nothing makes that work again. - Everything that goes wrong is written down, and none of what the archive holds.
data/journal.jsonlis the file to read when something behaved oddly an hour ago: one line per failure and per decision nothing else records, with the time, the type of the fault, the module and line it came from, the file named relative todata/, and the exit code. It keeps no message of any exception —OSErrorcarries the name of the file it failed on, and here a file name carries a surname — no title, no value, no doctor, no laboratory, no spelling of any test, no search, nothing asked of a model and nothing it answered. What it says about an archive is that archive's random id, and the ids are random for exactly this reason. It is meant to be readable out loud: paste the whole file into a bug report and you have given nothing away, and a test builds an archive of recognisable strings, breaks it in every way, and proves it. - Each person's archive is a separate database. A folder belongs to one owner, two owners' folders may not contain one another, and ids are random, because folder names carry surnames. A question asked of one archive cannot reach another's values.
- Nothing is sent to a model until you agree once, in writing, on a page that shows you what would be sent. The steps that need no model never ask. The model runs under your own subscription or key, one isolated call per document, carrying that document's pages and nothing else.
- If you let an assistant read the archive over the network, it gets read-only tools and
three locks in front of them:
- an unguessable secret path — without it, the server answers as if nothing were there;
- a private tunnel (Tailscale Funnel) that admits only the connector's own network;
- and a six-digit code from your authenticator (RFC 6238):
unlockreturns a pass good for four hours, every tool refuses without it,lock_archiveends it early. The secret behind that code is generated on your server, read once into your phone, and never travels through a conversation. - The access log keeps who called and which tool — never the question, never the answer. A log of a medical archive that holds the questions is a second copy of the archive.
A secret path and a private tunnel say where a request came from and nothing at all about who sent it. The code from a phone is the only part a stranger cannot copy out of an address bar.
Setting that up, in order — the first two on the server, as root:
sudo $(which uv) run epicrisis mcp-secret # the secret that stands in the served path
sudo $(which uv) run epicrisis mcp-lock init # prints one line for your authenticator, once
uv run epicrisis mcp --http --secret-file /etc/epicrisis/mcp-token --public-host <name.ts.net>
tailscale funnel 8051 # or `tailscale serve` to keep it inside your own network--public-host is the name the tunnel answers on. Without it a request arriving through the
tunnel carries a name this server does not know itself by, and is refused before it reaches
anything.
Then turn the lock on under Settings → Over the network, which also says what a code opens,
for how long, and what to do if you lose the phone it is in. Behind tailscale serve rather than
Funnel, every device of your own tailnet reaches it whatever --allow-from says — the private
networks are always allowed — so there the path and the code are the whole of it. The address
filter is the weakest of the three in any case: it says where a request came from and nothing
about who sent it.
And nothing of yours goes out with the code. This repository is published from a machine that holds a real archive, so a check runs before every push:
uv run python tools/nothing-of-yours.py --data-dir data # 0 = nothing of yours is in here
git config core.hooksPath .githooks # and before every push, from now onIt reads the data directory — the people, the institutions that treated them, the folders and
files the scans live in and their hashes — and looks for every one of those in each tracked file
and in every commit ever made, because a file deleted today is still published in the history. It
also looks for this server's own secrets, whole and hashed, and for keys and tokens by their
shape. Nothing it finds is printed: a phrase out of an archive is exactly what must not be
written into a terminal or an issue, so it says what kind of thing matched and in which file.
Names published deliberately — an author's own, in a licence and a copyright line — go in
published-on-purpose.txt, which is a person saying so once, in writing.
Python 3.14 and uv.
This project asks uv to use a Python already on the machine rather than fetching one, so
uv python install 3.14 will not satisfy it. Install 3.14 the way your system installs
Pythons — brew install python@3.14 on macOS, your distribution's package or the installer
from python.org elsewhere.
One system library as well: libmagic, which tells a file's kind from its first bytes rather
than from its name. Debian and Ubuntu have it already or install it with apt install libmagic1;
on macOS it is brew install libmagic; on Windows install python-magic-bin into the environment.
Without it every command says so and names it, rather than failing on its own imports.
Then:
git clone https://github.com/bermana-net/epicrisis && cd epicrisis
uv sync
uv run epicrisis demo --into /tmp/demo # three invented archives, no model called
uv run epicrisis serve --data-dir /tmp/demo/dataThen, when you are ready:
uv run epicrisis sources add ~/scans --owner "Your name"
uv run epicrisis serve # say yes on /consent: nothing goes to a model before you do
uv run epicrisis update # every step, skipping what is doneIn that order. Nothing reaches a model until you have read, once, what would be sent and
agreed to it, and that screen is in the dashboard. Run update before it and the steps that
need a model are skipped — it finishes, says so, and leaves you with an archive that has been
listed and not read.
What update runs, in order: the inventory of the folder; then, behind that agreement, reading
what each page is, transcribing the values, searching for a date where the form printed none,
and reading what a table was measured in; then the checks, which need no model; then the index.
The last two run whether you agreed or not.
The steps that read documents need a model, and there are two ways to give it one: the
claude command installed for the account this runs as, signed in with your own Claude
subscription, or ANTHROPIC_API_KEY in a .env file beside the data folder with the engine
changed on the Settings page. Without one of them the other steps still work and the dashboard
says which is missing. Nothing is sent before you read, once, what would go and say yes.
Scans on a disk of their own: add the folder by typing it — uv run epicrisis sources add /mnt/scans --owner "Your name" — which takes a folder anywhere. The folder picker on the page
stays inside your home and this instance's own folder, because the dashboard has no login; to
have it offer your disks too, set EPICRISIS_ARCHIVE_ROOT to them before starting, several
separated by :. An archive is only ever read, wherever it is, and nothing is copied out of it.
Other commands:
uv run epicrisis check-indicators # a second model over the test vocabulary
uv run epicrisis mcp # read-only tools for an assistant, over stdio
uv run epicrisis mcp --http --secret-file /etc/epicrisis/mcp-token --public-host <tunnel host>
uv run pytest -n 4 # 1571 tests, no network, no modelStatus: working and in daily use by its author and their family; not yet used by anyone else. Data files and interfaces may still change without migration.
Epicrisis is not a medical device. It is not for diagnosis, treatment or any clinical decision. It stores and shows what your documents say; it does not interpret them, does not decide what is normal, and does not advise.
One door exists, and naming it is part of the rule. The Settings page of an instance has three modes, and the strictest is the default: as printed, where the application compares nothing and the model is asked to quote rather than explain; may also read the values, where the model may say what a value means and how it moved, and the application still computes nothing; and no limits set here, where the model answers under no rule of this program's — and where the application itself will compare a number with the range printed beside it on its own form and count what fell outside. That last mode is the owner's own choice on their own instance, it is off until they choose it, and everything this program computes about "outside the range" lives in it.
A transcription from a scan can be wrong. That is why every value in this program is one click from the page it came from, and why the page — not the program — is the authority.
Free for a person and their family, for good. Keeping and reading your own medical records, your household's, and those of relatives or friends you help without being paid — that is written into the licence itself, not promised in a README. So is reading the code, changing it, publishing your changes and running it to try it out.
A clinic, a laboratory, an insurer, an employer or anyone offering this program to other people needs a commercial licence: COMMERCIAL.md.
The licence is the Business Source License 1.1, which is source-available rather than open source, with one thing written into it: every version becomes Apache 2.0 four years after it is published.
Sending a patch: CONTRIBUTING.md. Where things live in the code, and where a new thing goes: ARCHITECTURE.md.
Your data, your machine, your call.
Copyright © 2026 Artem Berman · www.bermana.net











