Skip to content
Back to projects

2026

NetTune

  • Python
  • OpenAI Vision
  • Raspberry Pi
  • Samba
  • SQLite
  • python-docx
  • pytest

Built for a shopping centre's security and maintenance team, and in use there. Staff scan the day's handwritten log sheets into a shared folder on a Raspberry Pi. Every sheet is read twice, independently, by an OpenAI vision model, then dated, put in shift order and filtered for routine. An email says when a batch is ready. A member of the team checks each sheet beside its lines in a web dashboard, and one click writes the centre's monthly Word report. A hard monthly budget is checked before every API call; at about five pages a day it costs roughly €1.20 a month.

One batch, end to end

A colleague scans the day's handwritten sheets into a shared folder on a Raspberry Pi. Each page of the PDF is rasterised at 300 dpi and read twice, independently, by OpenAI's vision model.

The readings are compared line by line, and every date is checked against the rest of the batch. Lines that pass both checks go straight into the monthly report. Anything doubtful branches off, in teal, to a person in the review dashboard, who confirms or corrects it before the report is written.

scan.pdfSamba sharerasterise300 dpiread ×2OpenAI visionconsensusagree?datesbatch checkreviewa personMEMORIA.docxmonthly reportdisagreeodd date25/25 pages3 to check175 linesscan.pdfSamba sharerasterise300 dpiread ×2OpenAI visionconsensusagree?datesbatch checkreviewa personMEMORIA.docxmonthly reportdisagreeodd date25/25 pages3 to check175 lines

Why every page is read twice

A model reading cursive can be confidently wrong, and a single reading gives no way to tell. So every page is read twice, independently, and the two readings are compared word by word, each word under its pair.

Where they agree, the line is trusted. Where one word differs, here "air" against "copper", the whole line is flagged with both readings, so a person checks exactly that line instead of the whole sheet. It is not infallible: both readings can make the same mistake, which is why a person still sees every sheet.

pass 1: Checked the air conditioning on floor 1, no incidents. pass 2: Checked the copper conditioning on floor 1, no incidents.

consensus: 8 of 9 words agree · "air" / "copper" differ → line flagged ⚠ for a person

The date that didn't fit

A scanned batch is one session of consecutive days, which gives a second check for free: every page's date is compared with its neighbours.

A page dated months away from the pages around it has been misread, even when both readings agree on it. It is lifted out and flagged instead of being filed in the wrong month.

batch.pdf · 1 July25 July

page 16 reads 16-04-2026 in both readings; its neighbours are 15-07 and 17-07 → misread, flagged ⚠

What the Pi prints while it works

The service runs on the Raspberry Pi under systemd. It watches the share, waits for a scan to finish copying, skips anything it has already processed, reads and checks every page, and files the result in that month's folder.

Once no new scan has arrived for a couple of minutes, one email per month tells the team how many sheets are in and how many lines need checking, with a link to the dashboard.

09:14:02 watcher new scan: incoming_scans/batch.pdf (25 pages)
09:14:05 pdf rasterised 25 pages at 300 dpi
09:14:31 read p01–p25: 2 readings each, 50 calls
09:14:58 consensus p07: 1 line disagrees ("air" / "copper")
09:14:58 consensus p20: 1 line disagrees ("09:40" / "09:10")
09:15:01 dates p16 reads 16-04-2026, batch is 01-07…25-07 → flagged
09:15:02 routine 41 routine checks omitted from the report
09:15:02 spend €0.14 this batch · €1.06 / €5.00 this month
09:17:03 notify email sent: July ready, 3 lines to check

Never over budget

Every API call is checked against a hard monthly cap before it is made. When the cap is reached, scans simply wait in the folder until the month rolls over: nothing is lost and nothing is overspent.

At about five pages a day, a month costs roughly €1.20 of a €5 cap. Each bar is one day's spend against that day's share of the cap.

month so far €1.21 of €5.00 cap · [#####...............]

Handwritten cursive cannot be transcribed perfectly, and the failures are silent. On a real sheet, the word “aire” (air) was read as “cobre” (copper) by five of six passes across two models, confidently and unanimously, even with both words in the prompt’s glossary. A single reading that looks right is not evidence that it is right.

That changed the design. No reading is trusted on its own: every page is read twice and any disagreement goes to a person with both readings (figure 2), and every date is checked against its batch (figure 3). The date check earned its place on a real 25-page July batch, where it caught a sheet read unanimously as 16-04-2026, something comparing readings could never have caught.