DEV Community

Damiano
Damiano

Posted on Originally published at damianobianda.hashnode.dev

The letter you can't paste into a chatbot: explaining official mail with a fully local 1B model

A Zahlungsbefehl is not the kind of letter you photograph and upload somewhere. It is a formal payment order from the Swiss debt-collection office: your full name, your address, the creditor, the amount, a deadline. It arrives in German (or French), in dense administrative language, and for a lot of people in Ticino the first instinct in 2026 is to paste it into a chatbot and ask: what is this, and how worried should I be?

That works, technically. It also means sending one of the most sensitive documents you own to a third-party server you know nothing about.

I wanted the explanation without the upload. So I built a small tool that explains Swiss administrative letters in plain Italian, entirely on my own machine. This post is about the constraints that shaped it, because they are the same constraints I keep seeing in the current wave of small-model projects.

Boring on purpose

The whole tool is about 306 lines of Python 3.10, standard library only. No pip install, no SDK, no API key, no account. It talks to Ollama over HTTP on localhost, where gemma3:1b does the language work.

Why a 1B model on CPU, when bigger is one download away? Because the letter is the point. A payment order, a tax decision, a health-insurance letter: these are documents people should be able to explain without a data-processing agreement. Local is not a compromise here. It is the feature.

The split that makes a small model reliable

Small models are good at paraphrasing and bad at arithmetic. A 1B model will occasionally mangle a number it just read. A regex will not.

So the tool splits the job:

  • Code extracts the hard facts deterministically: amounts in CHF, dates, deadlines, reference numbers. Those are printed verbatim, exactly as found in the letter.
  • The model gets the letter text and one job: explain what this document is, what the office wants, and what the reader should do next, in simple Italian.

The model never gets to invent the amount or the deadline. It explains; the code counts. That division is what makes a 1B model usable for something that matters.

The integration is the smallest part

Calling Ollama from plain Python is a POST and a JSON parse:

import json, urllib.request

def ask(prompt: str) -> str:
    req = urllib.request.Request(
        "http://localhost:11434/api/generate",
        data=json.dumps({
            "model": "gemma3:1b",
            "prompt": prompt,
            "stream": False,
        }).encode(),
        headers={"Content-Type": "application/json"},
    )
    with urllib.request.urlopen(req) as r:
        return json.load(r)["response"]
Enter fullscreen mode Exit fullscreen mode

With "stream": false the endpoint returns one JSON object with the full text in response. With streaming on you get one JSON object per line and concatenate the response fields, which is what I do in the real tool so the user sees the explanation appear instead of staring at a frozen terminal.

What 40 seconds buys you

On CPU, explaining one Zahlungsbefehl end to end takes about 40 seconds with gemma3:1b. That number would look embarrassing in a benchmark table. In this tool it is irrelevant: letters arrive one at a time, and 40 seconds is still faster than finding a German-speaking friend at 21:00 on a Sunday.

That is my one measurement, on my machine, on my test letter, and I am reporting it as exactly that. But it changed how I think about the current small-model benchmarks. The question is rarely "how many tokens per second" and almost always "is the latency inside the budget of the task". A batch job, a letter explainer, an offline assistant: seconds are fine. Autocomplete: they are not. Measure your task, not the leaderboard.

What I would tell anyone building the same thing

  • Pick the smallest model that explains your own test document correctly, not the largest one that fits in memory.
  • Keep money, dates and deadlines in deterministic code. Never ask a small model to read a number twice.
  • Write the prompt in the output language you want. Asking in simple Italian works even when the source letter is German administrative prose.
  • Stream the response. The perceived speed difference is bigger than the real one.
  • Treat privacy as a requirement from line one. It is much harder to retrofit than a faster model.

The request in that script goes to localhost:11434 and nowhere else. The day this tool needs a bigger model, it still will not need a network.

Top comments (1)

Collapse
 
omyvnss profile image
Om Yaduvanshi •

the split is the whole post. letting a 1b model paraphrase is fine; letting it read a deadline twice is where it invents one. and reporting 40 seconds as exactly one measurement on one machine is how you write a number people can trust.