<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Damiano Bianda's Dev Notes]]></title><description><![CDATA[Damiano Bianda's Dev Notes]]></description><link>https://damianobianda.hashnode.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1593680282896/kNC7E8IR4.png</url><title>Damiano Bianda&apos;s Dev Notes</title><link>https://damianobianda.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Mon, 19 Oct 2026 12:53:21 GMT</lastBuildDate><atom:link href="https://damianobianda.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[AI fatigue is real. So is the tiny offline model doing my most private task]]></title><description><![CDATA[My feed has a rhythm now. Every morning: a new model, a new agent framework, a new benchmark chart with a bar that goes up and to the right. By lunch, three hot takes. By evening, a thread about how t]]></description><link>https://damianobianda.hashnode.dev/ai-fatigue-is-real-so-is-the-tiny-offline-model-doing-my-most-private-task</link><guid isPermaLink="true">https://damianobianda.hashnode.dev/ai-fatigue-is-real-so-is-the-tiny-offline-model-doing-my-most-private-task</guid><category><![CDATA[Artificial Intelligence]]></category><category><![CDATA[Python]]></category><category><![CDATA[self-hosted]]></category><dc:creator><![CDATA[Damiano Bianda]]></dc:creator><pubDate>Sun, 18 Oct 2026 08:51:36 GMT</pubDate><content:encoded><![CDATA[<p>My feed has a rhythm now. Every morning: a new model, a new agent framework, a new benchmark chart with a bar that goes up and to the right. By lunch, three hot takes. By evening, a thread about how the benchmark was wrong. I understand why "AI fatigue" is one of the most-reacted topics on dev.to this week. I feel it too.</p>
<p>So this is not a post about a new model. It is a post about the opposite experience: picking the smallest model that would run offline, giving it one boring job, and finding out that this is where the technology quietly works.</p>
<h2>The boring job</h2>
<p>I deal with Swiss administrative letters: payment orders from the debt-collection office, tax decisions, health-insurance adjustments. They are written in a formal register that even native speakers have to read twice. I wanted a tool that explains them in plain Italian.</p>
<p>The obvious path was an API. The obvious problem was the document itself. A Zahlungsbefehl contains your full name, address, the creditor, and exactly how much you owe. It is not something you upload to a server you know nothing about. So the real requirement was: it has to run entirely on my machine.</p>
<h2>The smallest thing that works</h2>
<p>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 <code>gemma3:1b</code> does the language work. One billion parameters, on CPU. No GPU, no queue, no per-token cost.</p>
<p>This is the part that surprised me, after months of reading that everything interesting needs a frontier model: for a bounded job, the small model is not a compromise. It is the correct size.</p>
<h2>What the model does, and what it never touches</h2>
<p>The split is the whole design.</p>
<p>The code - plain, inspectable Python - handles everything that must be exact. It parses the letter, pulls out the amounts, the deadlines, the reference numbers, the office that sent it. Deterministic extraction, the kind of thing a regular expression and a date parser have done reliably for decades.</p>
<p>The model never sees a number it could invent. It receives the extracted fields and the letter text, and it does the one thing it is genuinely good at: explaining, in calm everyday Italian, what this letter is and what the sender is asking for. The deadline in the output is the deadline the code extracted. The model can paraphrase a sentence; it cannot paraphrase a date into existence.</p>
<p>That boundary is what makes the output trustworthy enough to actually use.</p>
<h2>The one number I'll give you</h2>
<p>On my machine, on CPU, one measured run: about 40 seconds to explain one payment-order letter end to end. That is exactly one measurement on exactly one machine - your hardware will differ, and I will not pretend otherwise.</p>
<p>Is 40 seconds slow? For a chatbot, yes. For a letter that sat in a drawer for a week because I did not want to read it, it is nothing. The right question was never "is it fast". It was "is it fast enough for the job", and the answer is comfortably yes.</p>
<h2>What this taught me about the fatigue</h2>
<p>The fatigue, I think, comes from the framing that AI is one thing that should be everywhere, doing everything, all the time. That framing produces demos that impress and products that exhaust.</p>
<p>My experience was the inverse. One model, one billion parameters, one machine, one job with clear edges. No account, no upload, no subscription, nothing to cancel. The tool is boring, and boring is exactly what a letter from the debt-collection office deserves.</p>
<p>If you are tired of the discourse, this is my suggestion: pick one small, private, slightly annoying task of yours. Put the smallest local model on it. Keep the numbers in the code and the words in the model. It is a very calm way to find out what this technology is actually for.</p>
<hr />
<p><em>The tool described is real and personal. The 40-second figure is a single measurement on a single machine, stated as such.</em></p>
]]></content:encoded></item><item><title><![CDATA[The letter you can't paste into a chatbot: explaining official mail with a fully local 1B model]]></title><description><![CDATA[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 amou]]></description><link>https://damianobianda.hashnode.dev/the-letter-you-can-t-paste-into-a-chatbot-explaining-official-mail-with-a-fully-local-1b-model</link><guid isPermaLink="true">https://damianobianda.hashnode.dev/the-letter-you-can-t-paste-into-a-chatbot-explaining-official-mail-with-a-fully-local-1b-model</guid><category><![CDATA[Python]]></category><category><![CDATA[ollama]]></category><category><![CDATA[privacy]]></category><category><![CDATA[Artificial Intelligence]]></category><dc:creator><![CDATA[Damiano Bianda]]></dc:creator><pubDate>Sat, 17 Oct 2026 12:28:03 GMT</pubDate><content:encoded><![CDATA[<p>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?</p>
<p>That works, technically. It also means sending one of the most sensitive documents you own to a third-party server you know nothing about.</p>
<p>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.</p>
<h2>Boring on purpose</h2>
<p>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 <code>gemma3:1b</code> does the language work.</p>
<p>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.</p>
<h2>The split that makes a small model reliable</h2>
<p>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.</p>
<p>So the tool splits the job:</p>
<ul>
<li><strong>Code</strong> extracts the hard facts deterministically: amounts in CHF, dates, deadlines, reference numbers. Those are printed verbatim, exactly as found in the letter.</li>
<li><strong>The model</strong> 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.</li>
</ul>
<p>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.</p>
<h2>The integration is the smallest part</h2>
<p>Calling Ollama from plain Python is a POST and a JSON parse:</p>
<pre><code class="language-python">import json, urllib.request

def ask(prompt: str) -&gt; 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"]
</code></pre>
<p>With <code>"stream": false</code> the endpoint returns one JSON object with the full text in <code>response</code>. With streaming on you get one JSON object per line and concatenate the <code>response</code> fields, which is what I do in the real tool so the user sees the explanation appear instead of staring at a frozen terminal.</p>
<h2>What 40 seconds buys you</h2>
<p>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.</p>
<p>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.</p>
<h2>What I would tell anyone building the same thing</h2>
<ul>
<li>Pick the smallest model that explains your own test document correctly, not the largest one that fits in memory.</li>
<li>Keep money, dates and deadlines in deterministic code. Never ask a small model to read a number twice.</li>
<li>Write the prompt in the output language you want. Asking in simple Italian works even when the source letter is German administrative prose.</li>
<li>Stream the response. The perceived speed difference is bigger than the real one.</li>
<li>Treat privacy as a requirement from line one. It is much harder to retrofit than a faster model.</li>
</ul>
<p>The request in that script goes to <code>localhost:11434</code> and nowhere else. The day this tool needs a bigger model, it still will not need a network.</p>
]]></content:encoded></item><item><title><![CDATA[Call a local model from plain Python: no SDK, no dependencies]]></title><description><![CDATA[Most "use a local model" tutorials start with a virtualenv and a dozen packages. For a small tool you may not need any of them. If a local Ollama server is running, Python's standard library is enough]]></description><link>https://damianobianda.hashnode.dev/call-a-local-model-from-plain-python-no-sdk-no-dependencies</link><guid isPermaLink="true">https://damianobianda.hashnode.dev/call-a-local-model-from-plain-python-no-sdk-no-dependencies</guid><dc:creator><![CDATA[Damiano Bianda]]></dc:creator><pubDate>Fri, 16 Oct 2026 08:53:06 GMT</pubDate><content:encoded><![CDATA[<p>Most "use a local model" tutorials start with a virtualenv and a dozen packages. For a small tool you may not need any of them. If a local Ollama server is running, Python's standard library is enough to talk to it.</p>
<p>I used this approach in Sportello chiaro, a small helper I built to explain Swiss administrative letters in plain Italian. It is Python 3.10, standard library only, and about 306 lines in total. No web framework, no HTTP client package, no SDK.</p>
<h2>What Ollama gives you</h2>
<p>Ollama runs a local HTTP server on port 11434. Its API documentation describes a <code>POST /api/generate</code> endpoint, and a <code>"stream": false</code> option that returns one JSON object instead of a stream of chunks. The generated text is in the <code>response</code> field. That is all the plumbing you need.</p>
<h2>The call, in plain Python</h2>
<p>This is an illustrative example written for this article, not an excerpt from the project:</p>
<pre><code class="language-python">import json
import urllib.request

def ask_local_model(prompt, model="gemma3:1b"):
    body = json.dumps({"model": model, "prompt": prompt, "stream": False}).encode()
    req = urllib.request.Request(
        "http://localhost:11434/api/generate",
        data=body,
        headers={"Content-Type": "application/json"},
    )
    with urllib.request.urlopen(req, timeout=300) as resp:
        return json.loads(resp.read())["response"]
</code></pre>
<p>Two details matter. First, set a generous timeout: a small model on a CPU is slow. Second, keep <code>stream</code> off unless you actually want to print tokens as they arrive. Without streaming, the function is a plain "string in, string out" call that is easy to test.</p>
<h2>What a small model on a CPU is like</h2>
<p>In my own test of the project, Gemma 3 1B on a CPU took about 40 seconds to explain a Zahlungsbefehl (a Swiss payment order). That is not fast, but for a letter you read once it is acceptable, and nothing leaves the machine. For documents like these, privacy was the reason to go local in the first place.</p>
<p>Speed is the price. If you need answers in a second, a 1B model on a CPU is the wrong tool.</p>
<h2>Why drop the dependencies</h2>
<ul>
<li><strong>Nothing to install.</strong> If Python 3.10 and Ollama are there, it runs.</li>
<li><strong>Less to break.</strong> There is no package that changes its API under you.</li>
<li><strong>Easy to read.</strong> 306 lines can be read in one sitting, which matters when a tool handles personal letters.</li>
</ul>
<p>The standard library is not always the better choice. If you need retries, connection pooling or async calls, a proper client library pays for itself. For a one-shot call per document, <code>urllib</code> was enough.</p>
<h2>Takeaway</h2>
<p>Before adding a dependency for a local model, try the plain HTTP call. For a small, single-purpose tool it may be all you need.</p>
]]></content:encoded></item><item><title><![CDATA[Your TypeScript types disappear at runtime: validating data at the edges of a Nuxt app]]></title><description><![CDATA[TypeScript checks your code before it runs. Once it ships, the types are gone. The compiler cannot tell you what a third-party API actually returned at 3 AM on a Sunday.
I've written full-stack TypeSc]]></description><link>https://damianobianda.hashnode.dev/your-typescript-types-disappear-at-runtime-validating-data-at-the-edges-of-a-nuxt-app</link><guid isPermaLink="true">https://damianobianda.hashnode.dev/your-typescript-types-disappear-at-runtime-validating-data-at-the-edges-of-a-nuxt-app</guid><dc:creator><![CDATA[Damiano Bianda]]></dc:creator><pubDate>Thu, 15 Oct 2026 01:19:07 GMT</pubDate><content:encoded><![CDATA[<p>TypeScript checks your code before it runs. Once it ships, the types are gone. The compiler cannot tell you what a third-party API actually returned at 3 AM on a Sunday.</p>
<p>I've written full-stack TypeScript with Nuxt and Vue for about six years. The production bugs I remember most were rarely logic errors. Most came from one assumption: "the data has the shape I typed it as."</p>
<p>The examples below are small and made up for this article. They are not taken from a specific project.</p>
<h2>The problem: a type assertion is a promise, not a check</h2>
<pre><code class="language-ts">// server/api/user/[id].get.ts
interface User {
  id: number
  email: string
  plan: 'free' | 'pro'
}

export default defineEventHandler(async (event) =&gt; {
  const id = getRouterParam(event, 'id')
  const user = await $fetch(`https://api.example.com/users/${id}`) as User
  return { label: user.email.toLowerCase(), plan: user.plan }
})
</code></pre>
<p>This compiles. If the API one day returns <code>email: null</code> for accounts created through a different sign-up flow, <code>user.email.toLowerCase()</code> throws. The <code>as User</code> told the compiler to stop asking questions. It checked nothing.</p>
<h2>Fix: parse at the boundary with a schema</h2>
<p>A schema library such as Zod keeps the type and the runtime check in one place:</p>
<pre><code class="language-ts">import { z } from 'zod'

const UserSchema = z.object({
  id: z.number(),
  email: z.string().email().nullable(),
  plan: z.enum(['free', 'pro']),
})
type User = z.infer&lt;typeof UserSchema&gt;

export default defineEventHandler(async (event) =&gt; {
  const id = getRouterParam(event, 'id')
  const raw = await $fetch(`https://api.example.com/users/${id}`)
  const parsed = UserSchema.safeParse(raw)
  if (!parsed.success) {
    throw createError({
      statusCode: 502,
      statusMessage: 'Upstream returned an unexpected shape',
    })
  }
  const user: User = parsed.data
  return { label: user.email?.toLowerCase() ?? 'no email', plan: user.plan }
})
</code></pre>
<p>Two things change. The <code>null</code> case is now a type error you have to handle, not a surprise in production. And a bad upstream response fails loudly with a clear status, instead of a vague <code>TypeError</code> deep in your code.</p>
<h2>Where to validate, and where not to bother</h2>
<p>You don't need a schema around every object. Validate where data crosses a trust boundary:</p>
<ul>
<li><p>Responses from APIs you don't control</p>
</li>
<li><p>Request bodies and query params in your own server routes (<code>readValidatedBody</code> and <code>getValidatedQuery</code> in Nitro/H3 take a validator for this)</p>
</li>
<li><p><code>localStorage</code>, cookies and anything else the user can edit</p>
</li>
<li><p>Output of any model or service whose format isn't guaranteed</p>
</li>
</ul>
<p>Inside your own code, after the boundary, trust the types.</p>
<h2>A tradeoff to be aware of</h2>
<p>Schemas cost a little bundle size and a little time per parse. On the server this rarely matters. In a hot client path, parse once when the data arrives, not on every render.</p>
<p>Also, strict schemas can make you reject responses that were fine in practice. Decide per field: <code>.passthrough()</code> or <code>.strip()</code> for unknown keys, and be careful with strict enums on upstreams that add values without warning.</p>
<h2>Takeaway</h2>
<p><code>as SomeType</code> is documentation. A schema is a check. Use assertions for things you actually know, and parse the rest at the edge.</p>
]]></content:encoded></item><item><title><![CDATA[Let the model explain, let the code count: a local LLM helper for Swiss administrative letters]]></title><description><![CDATA[In Switzerland, a letter from a public office can start a clock. A Zahlungsbefehl (payment order) gives you 10 days to object. An Einsprache period is often 30. Miss the date and you lose a right. Man]]></description><link>https://damianobianda.hashnode.dev/let-the-model-explain-let-the-code-count-a-local-llm-helper-for-swiss-administrative-letters</link><guid isPermaLink="true">https://damianobianda.hashnode.dev/let-the-model-explain-let-the-code-count-a-local-llm-helper-for-swiss-administrative-letters</guid><dc:creator><![CDATA[Damiano Bianda]]></dc:creator><pubDate>Thu, 15 Oct 2026 01:17:41 GMT</pubDate><content:encoded><![CDATA[<p>In Switzerland, a letter from a public office can start a clock. A <em>Zahlungsbefehl</em> (payment order) gives you 10 days to object. An <em>Einsprache</em> period is often 30. Miss the date and you lose a right. Many people read these letters in a language that isn't their first.</p>
<p>I built a small local tool, "Sportello chiaro", to explain such a letter in plain Italian and point out the deadline. It's an MVP. I want to describe the one design decision that I think matters, and be clear about what is and isn't tested.</p>
<h2>The rule: the model never produces the critical facts</h2>
<p>A language model is good at explaining. It is not reliable at date arithmetic or at quoting the right number of days. So the deadline does not come from the model. It comes from code:</p>
<pre><code class="language-python">SERVED_RE = re.compile(
    r"(?:zugestellt am|zustellung am|zugestellt:|notifié le|notifiée le|reçu le)"
    r"\s*(\d{1,2})[.\/](\d{1,2})[.\/](\d{4})", re.I)
</code></pre>
<p>The tool finds the notification date with this pattern, picks the number of days from the document type (10 for a Zahlungsbefehl or Strafbefehl, 30 for an Einsprache), and computes <code>end = served + timedelta(days=days)</code>.</p>
<p>The model then gets the letter and a system prompt that says, in short: use only information present in the letter, and do not invent dates, amounts or law articles. Its job is the readable explanation, in four fixed sections.</p>
<h2>Graceful failure</h2>
<p>The call to the local model is plain <code>urllib</code>, and it returns the text of the first choice. If the model call fails, the analysis falls back to the rule-based output and records the error. The user still sees the deadline and a response template, just without the prose.</p>
<h2>What is tested</h2>
<ul>
<li><p>Stack: Python 3.10 standard library only (<code>urllib</code>, <code>http.server</code>, <code>re</code>, <code>datetime</code>, <code>json</code>), about 300 lines. A CLI and a local web page on <code>127.0.0.1:8080</code>.</p>
</li>
<li><p>Model: Gemma 3 1B (Q4_K_M) through <code>llama-cpp-python</code> for my tests. The intended default is a larger model through Ollama, which I have <strong>not</strong> tested.</p>
</li>
<li><p>Six pytest tests, rules level only. One checks that a notification on 12.10 plus 10 days gives 22.10.2026. Another checks that a response template is selected. A third checks that an unrelated text gets low urgency.</p>
</li>
<li><p>One end-to-end run on one invented letter. About 40 seconds with the 1B model.</p>
</li>
</ul>
<h2>What is not done</h2>
<ul>
<li><p>No schema check on the model's output. The tool does not verify that the answer contains the four sections, or that the deadline in the prose matches the computed one. That's the next thing I'd add.</p>
</li>
<li><p>Weekends and public holidays are not handled in the deadline calculation.</p>
</li>
<li><p>No OCR and no PDF input. You paste text.</p>
</li>
<li><p>No users yet. The repository is not public.</p>
</li>
<li><p>It is not legal advice, and I wouldn't present it as such.</p>
</li>
</ul>
<h2>Why local</h2>
<p>These letters contain names, addresses and debts. Keeping the model on the user's machine means the text never leaves it. A small model is slower and less fluent, which is why the rules carry the parts that must be right.</p>
<h2>Takeaway</h2>
<p>When a task mixes language and facts, split it. Let code produce what must be exact and let the model do the explaining. Then say plainly which parts you have tested.</p>
]]></content:encoded></item></channel></rss>