Inspiration

A prescription is the single most important instruction a patient receives for their own body, and a large share of patients cannot act on it. The handwriting is illegible, the form is in a language they do not read, or they have low literacy or low vision.

We started from a real example: Cuba's pre-printed RECETA MEDICA form (MINSAP model 53-05-04). The printed scaffolding is machine-readable, but the drug name, dose and frequency are handwritten in blue ballpoint. Off-the-shelf OCR reads Clotrimazol (500 mg) as Mletzina 20. The patients most affected are the ones with the least access to an interpreter, pharmacist counselling, or a family member who can translate.

What it does

RxBridge turns a photo of a prescription into a spoken, structured medication schedule.

  1. Photograph the prescription from a phone. It's a single-screen, installable, mobile-first web app (108 kB first load).
  2. Drug, dose, frequency, duration are extracted and shown as a pictogram day-grid (Morning / Midday / Evening / Night, a pill glyph and a large numeral per dose), so a patient who cannot read a word still knows how many pills to take and when.
  3. Read aloud in the patient's own language. The summary is spoken with the browser's Web Speech API, so no cloud speech call is needed.
  4. Every value is attributed. For each drug, dose, frequency and duration the API names the exact source tokens that produced it, their mean confidence, and which field came out weakest, so a pharmacist sees why a value was read, not just a number.
  5. It never invents medical advice. It only reads back the prescription. If a line can't be read it is left out, never guessed, and an unreadable photo returns a friendly "unreadable" result, never a crash.

Verified end to end on a real printed prescription: 20/20 OCR lines at 0.87-0.99 confidence, 3/3 drugs extracted with dose, frequency, day-grid times and duration.

How we built it

phone photo
 -> [1] docTR            detect + recognise text (in memory: no disk write, no cloud call)
 -> [2] DistilBERT       token-classification slot tagger, trained by us
                         DRUG / DOSE / UNIT / ROUTE / FREQ / DURATION
 -> [3] Pydantic         BIO spans -> validated PrescriptionSchedule (69 contract tests)
 -> [4] normalise        "twice daily" -> [morning, night]; q8h, BID/TID/QID/PRN; EN/ES/FR
 -> [5] Next.js PWA      pictogram day-grid + Web Speech read-aloud
  • docTR for detection and recognition, PyTorch / transformers for the DistilBERT tagger, FastAPI for the extraction service, Next.js 15 / React 19 / TypeScript for the PWA, Pydantic v2 for the validated contract with exported JSON Schema, pytest (96 tests) and Playwright for end-to-end tests, all trained and served on 4x RTX 5060 Ti.
  • Two honest fallbacks keep it usable. If the learned tagger emits an invalid span, a rule-based lexicon retries and the response reports which one answered. Unreadable input is a result, not a 500.

Measuring that the model reads rather than memorises

A metric can be high for the wrong reason, so we trained the same DistilBERT tagger twice on an identical split of 12,000 prescription sentences: once with the real labels, once with every label replaced by a random draw from the label distribution.

model entity-level F1 (seqeval, n = 960 held out)
trained on real labels 0.9367
trained on shuffled labels (control) 0.0000

The control collapses to exactly zero, so the 0.937 measures the token-to-label mapping, not an artifact of the data generator.

Handwriting in ten languages: a fine-tuned edge VLM and a benchmark that can say no

Handwriting is the hard part, so we fine-tuned LiquidAI/LFM2.5-VL-3B, a vision-language model small enough for edge devices, with a rank-16 LoRA on line-level handwriting in ten languages and six scripts (German, French, Vietnamese, Persian, Arabic, Russian, English, Thai, Hindi, Urdu). Then we built the thing that matters more than the model: a held-out benchmark (benchmark.py) that never touches training data, is never used for model selection, and grades every language against a fixed bar of CER 0.25. It's a verifier, so it's allowed to say FAIL.

It did, three times, and each time it pointed at a real bug in our pipeline:

  1. Corpus imbalance. Two languages were 92% of the gradient, so we capped by characters per language and shuffled globally.
  2. A leak in our own harness. The generation budget was sized from the reference length of the answer being graded. We removed it, re-measured, and published the corrected numbers.
  3. A missing stop token. The training labels never contained <|im_end|>, so the model learned to transcribe a line correctly and then keep going (Thai, Hindi and Urdu had ~40% exact lines but huge error). One line fixed it: the validation stop rate went from 54% to 100%.

Final result on the held-out test set, with zero-shot and fine-tuned on the identical harness and budget:

language zero-shot CER fine-tuned CER
German 0.124 0.067 (pass)
French 0.115 0.109 (pass)
Thai 0.988 0.163 (pass)
Vietnamese 0.377 0.262
Hindi 0.551 0.535
English 0.585 0.582
Arabic 0.796 0.792
Persian 0.798 0.801
Russian 0.934 0.923
Urdu 1.028 1.045
languages passing 2 / 10 3 / 10
mean CER 0.630 0.528

The fine-tuned model beats zero-shot in 8 of 10 languages, Thai error falls 6x, and a third language now clears the bar. It does not yet pass all ten, and we say so: the paragraph-form languages (Russian, Persian, Arabic, English) have references of 1,000-3,700 characters that a fixed 128-token budget can't cover, and a pre-declared supplementary run at 1,024 tokens is reported separately in eval/SUPPLEMENTARY_1024.md. That's why the handwriting model stays out of the production pipeline until the benchmark passes for a language: the verifier, not our optimism, decides what ships. Every number is reproducible from the repo (python3 benchmark.py --adapter ...).

Challenges we ran into

  • Handwriting. docTR reads printed scaffolding at ~0.99 confidence and fails on cursive. Zero-shot LiquidAI/LFM2.5-VL-3B read the blue-ink form exactly where docTR failed, but also hallucinated on a red-ink form, so we fine-tuned it and refused to ship it into the pipeline until a benchmark says it's reliable.
  • Our own benchmark caught our own bugs. It caught a corpus imbalance (two languages were 92% of the gradient), a harness budget that leaked the reference length into generation, and a missing end-of-sequence token in the training labels that taught the model never to stop. Each one is written up, with numbers, in submission/EVIDENCE.md.
  • Privacy. Real prescription text is never persisted: images are decoded in memory, and committed benchmark files are scrubbed to {language, CER} only.

Accomplishments that we're proud of

  • A complete, working path from phone photo to spoken schedule, with attribution on every field.
  • A tagger result backed by a shuffled-label control, not just a number.
  • A multilingual benchmark that's strict enough to report FAIL, which we kept honest even when it made our headline worse.

What we learned

Measure the signal, not the artifact. Every time a number looked good or bad for a reason we didn't understand, the cause was in the pipeline (data mix, decoding budget, a missing stop token), not in the model. Building the verifier first is what let us find them.

What's next for RxBridge

  • Promote the fine-tuned handwriting model into the pipeline for the languages where the benchmark passes, and keep it out where it doesn't.
  • Pharmacist-review mode built on the per-field attribution.
  • Fully on-device recognition: LFM2.5-VL-3B is small enough for edge hardware.

Target users and social impact

Primary: patients who cannot read their own prescription: low-literacy adults, elderly patients handed a prescription in a language they don't read, language-barrier patients who'd otherwise need a family member to translate dose and timing, and low-vision patients who can see a pictogram and hear speech but not small print. Secondary: community pharmacists and clinic staff confirming that a patient understood the label before dispensing.

Medication errors from unreadable instructions are a documented cause of preventable harm. RxBridge doesn't replace the doctor or pharmacist. It makes sure the patient can actually hear what they were told.

Built With

Share this project:

Updates

Submission history