<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mario</title>
    <description>The latest articles on DEV Community by Mario (@oierreaemme).</description>
    <link>https://dev.to/oierreaemme</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3938350%2Fa8f02e02-93fb-4369-a916-db70aa3bc1ab.jpg</url>
      <title>DEV Community: Mario</title>
      <link>https://dev.to/oierreaemme</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/oierreaemme"/>
    <language>en</language>
    <item>
      <title>Notari — voice notes that never leave your phone, structured by Gemma 4</title>
      <dc:creator>Mario</dc:creator>
      <pubDate>Sat, 23 May 2026 22:37:20 +0000</pubDate>
      <link>https://dev.to/oierreaemme/notari-voice-notes-that-never-leave-your-phone-structured-by-gemma-4-2dac</link>
      <guid>https://dev.to/oierreaemme/notari-voice-notes-that-never-leave-your-phone-structured-by-gemma-4-2dac</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/google-gemma-2026-05-06"&gt;Gemma 4 Challenge: Build with Gemma 4&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Notari&lt;/strong&gt; is an Android app that records a voice note, transcribes it, and turns it into a clean, structured Markdown note — &lt;strong&gt;entirely on-device&lt;/strong&gt;. The audio is held in RAM, never written to disk, and the app doesn't even request the &lt;code&gt;INTERNET&lt;/code&gt; permission.&lt;/p&gt;

&lt;p&gt;I keep voice memos: meeting decisions, half-formed ideas at 11pm, reminders I'll forget by the time I'm home. The two app categories that should solve this don't:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;System dictation&lt;/strong&gt; gives me a raw transcript with no structure. I never re-read it. It rots in a folder.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloud "AI voice notes"&lt;/strong&gt; structure beautifully, but they upload my audio — meeting decisions, personal reflections — to a server I don't control, against a privacy policy that can change.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I built the third option. Voice notes are the kind of content where privacy isn't a marketing veneer — it's a precondition for using the tool at all. If the app feels like it might leak, I won't dictate the thing that matters most. So the privacy guarantee had to be load-bearing, not optional.&lt;/p&gt;

&lt;p&gt;The pipeline is short on purpose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Mic ─▶ Android SpeechRecognizer ─▶ Gemma 4 E2B (LiteRT-LM) ─▶ JSON ─▶ Room
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Capture.&lt;/strong&gt; &lt;code&gt;SpeechRecognizer&lt;/code&gt; runs in continuous-listen mode so the user can pause naturally without the recognizer giving up. The OS owns the audio buffer; the app only ever sees the text Flow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structure.&lt;/strong&gt; The transcript is fed to Gemma 4 E2B running locally via Google AI Edge's LiteRT-LM runtime. The prompt is engineered to return a single JSON object — title, tags, dated &lt;code&gt;mentions[]&lt;/code&gt;, a Markdown body — and nothing else.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parse + persist.&lt;/strong&gt; A lenient Moshi parser tolerates trailing commas and unquoted keys. On parse failure we retry once with a stricter prompt; on a second failure the transcript is saved as plain text so the user never loses content. Notes land in Room as portable Markdown.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No step touches the network. The &lt;code&gt;INTERNET&lt;/code&gt; permission isn't declared in the merged manifest, and a CI gate fails the build if anyone ever adds it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/3U477zIH7FA"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Repository:&lt;/strong&gt; &lt;a href="https://github.com/oierreaemme/notari" rel="noopener noreferrer"&gt;https://github.com/oierreaemme/notari&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;APK (v1.0.0):&lt;/strong&gt; &lt;a href="https://github.com/oierreaemme/notari/releases/download/v1.0.0/notari-v1.0.0.apk" rel="noopener noreferrer"&gt;https://github.com/oierreaemme/notari/releases/download/v1.0.0/notari-v1.0.0.apk&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Architecture, ADRs, prompt evaluations:&lt;/strong&gt; see &lt;code&gt;docs/&lt;/code&gt; in the repo&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License:&lt;/strong&gt; Apache 2.0&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The privacy promise is verifiable. Run it in airplane mode. Inspect the manifest. Sniff the network. Nothing leaves the device — that's the whole point.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Used Gemma 4
&lt;/h2&gt;

&lt;p&gt;I chose &lt;strong&gt;Gemma 4 E2B&lt;/strong&gt; (Effective-2B, INT4-quantized, ~1.5 GB on disk) running locally via LiteRT-LM. Three reasons made E2B the right fit — not E4B, not a cloud model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It fits.&lt;/strong&gt; ~1.5 GB in INT4 loads inside a 4 GB-RAM phone's budget alongside the rest of the app. The larger Gemma 4 variant exceeded what I was willing to ship in an APK for a personal capture tool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's strong enough for structured generation.&lt;/strong&gt; The task isn't open-ended reasoning — it's "given this transcript, return a fixed-schema JSON object". E2B does this reliably across six languages once the prompt is tuned for it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LiteRT-LM ships a maintained Kotlin binding.&lt;/strong&gt; &lt;code&gt;com.google.ai.edge.litertlm:litertlm-android&lt;/code&gt; reads &lt;code&gt;.litertlm&lt;/code&gt; files directly, supports GPU and CPU backends, and exposes the &lt;code&gt;Engine&lt;/code&gt; / &lt;code&gt;Session&lt;/code&gt; API the rest of the app is built around.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The app is opinionated: it transforms the transcript faithfully, never paraphrases meaning, and never invents dates, names, or facts. That guarantee is enforced by the prompt and verified by adversarial fixtures in &lt;code&gt;core/inference/src/test/resources/prompt-eval/&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The JSON-first contract
&lt;/h3&gt;

&lt;p&gt;The model is asked for one thing: a JSON object matching a fixed schema. No prose, no Markdown fences, no "Sure! Here's the structured note:" preamble.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"language"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;bcp47&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;short, no trailing punctuation&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;lowercase-kebab&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mentions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"surface_form"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;datetime span&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"iso_resolved"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;ISO-8601 or null&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"body_markdown"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;Markdown&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The prompt is versioned in &lt;code&gt;core/inference/src/main/assets/prompts/structure_note_vN.txt&lt;/code&gt; and referenced from &lt;code&gt;AssetPromptLoader.ACTIVE_PROMPT&lt;/code&gt;. Every change is a versioned, file-based change with a corresponding ADR. The active version is &lt;strong&gt;v10&lt;/strong&gt;, evolved through ten rounds of real-corpus testing — and the evolution itself is most of what I learned about the model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;v2&lt;/strong&gt; condensed the few-shot examples after E2B started over-mimicking long examples.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;v3&lt;/strong&gt; added a &lt;code&gt;CURRENT TIMESTAMP&lt;/code&gt; block so the model could resolve "tomorrow at 3pm" to a real ISO instant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;v4&lt;/strong&gt; fixed four E2B-specific failure modes (confusing &lt;code&gt;mentions[]&lt;/code&gt; with named entities, dropping checkboxes for spoken commitments, collapsing enumerations into prose, never using headings on multi-topic notes). The fix in every case was changing the framing from "you may" to "REQUIRED".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;v6&lt;/strong&gt; added orthographic cleanup rules (fix false starts and obvious mis-hearings without changing meaning).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;v7&lt;/strong&gt; slimmed the prompt back down — removed verbose formatting-whitespace rules that were eating the cold-start prefill budget.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;v8&lt;/strong&gt; added a FINAL CHECKLIST before generation and a headings-preserve-prose rule, then had to be trimmed again when the extra ~1000 characters pushed cold-start over budget on a Pixel 6a.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;v10&lt;/strong&gt; fixed the most important bug of the whole project (Pillar 4): E2B was occasionally emitting the &lt;em&gt;content&lt;/em&gt; of the worked examples as if it were the user's note. The fix cut the examples from ten to three short, low-salience ones, replaced specific names and ticket numbers with bland placeholders, and added a blunt anti-copy guard right before the transcript. v10 also moved the language lock from the bare BCP-47 code to the language name ("English"), which stopped mixed-language titles and tags.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Robust parsing — the model will be sloppy
&lt;/h3&gt;

&lt;p&gt;Even with a strict prompt, real E2B output has variance: trailing commas, occasional Markdown fences, an extra explanation after the closing brace. The parser strips any leading or trailing Markdown code fences, trims everything before the first &lt;code&gt;{&lt;/code&gt; and after the last balanced &lt;code&gt;}&lt;/code&gt;, and hands the cleaned slice to Moshi configured as &lt;strong&gt;lenient&lt;/strong&gt;. If that fails, we retry once with a stricter &lt;code&gt;RETURN JSON ONLY. NO OTHER TEXT.&lt;/code&gt; preamble; if &lt;em&gt;that&lt;/em&gt; fails, we fall back to saving the raw transcript as a plain-text note. The user always keeps their content.&lt;/p&gt;

&lt;h3&gt;
  
  
  Audio non-persistence — the privacy backbone
&lt;/h3&gt;

&lt;p&gt;The most important thing this app does is &lt;em&gt;not&lt;/em&gt; write audio to disk. Ever. &lt;code&gt;SpeechRecognizer&lt;/code&gt; owns the buffer; the app only ever sees a &lt;code&gt;Flow&amp;lt;TranscriptChunk&amp;gt;&lt;/code&gt; of strings. When the user stops, &lt;code&gt;awaitClose&lt;/code&gt; calls &lt;code&gt;recognizer.destroy()&lt;/code&gt; and the buffer goes with it. There is no &lt;code&gt;.wav&lt;/code&gt;, &lt;code&gt;.m4a&lt;/code&gt;, &lt;code&gt;.aac&lt;/code&gt;, or &lt;code&gt;.tmp&lt;/code&gt; file in the app's data directory at any point. The check is one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adb shell run-as com.voicenotemd.debug find /data/data/com.voicenotemd.debug &lt;span class="nt"&gt;-type&lt;/span&gt; f
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output lists the Room database, the DataStore settings, and the model files — and nothing audio. I verified this live during, before, and after a recording.&lt;/p&gt;

&lt;h3&gt;
  
  
  Backend probing — GPU first, CPU fallback
&lt;/h3&gt;

&lt;p&gt;LiteRT-LM supports both &lt;code&gt;Backend.GPU()&lt;/code&gt; and &lt;code&gt;Backend.CPU()&lt;/code&gt;. GPU is faster on decode, but GPU init fails on some devices (the Pixel 6a's Mali-G78 in my testing). The session factory probes GPU and recovers to CPU:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nf"&gt;runCatching&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;engineFactory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Backend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;GPU&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;recoverCatching&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;engineFactory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Backend&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CPU&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getOrThrow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the reference Pixel 6a (CPU fallback) a 1000-character note structures in ~50-60s; on a device that gets the GPU path it's ~15-25s.&lt;/p&gt;

&lt;h3&gt;
  
  
  Engine lifecycle — keeping 1.5 GB livable
&lt;/h3&gt;

&lt;p&gt;The engine is ~1.5 GB resident — most of a 4 GB device's budget. &lt;code&gt;LiteRtLmGemmaSession&lt;/code&gt; implements &lt;code&gt;ComponentCallbacks2&lt;/code&gt; and releases the engine on &lt;code&gt;onTrimMemory(TRIM_MEMORY_BACKGROUND)&lt;/code&gt;, reloading lazily. To hide cold-start, &lt;code&gt;warmUp()&lt;/code&gt; is fire-and-forget from &lt;code&gt;CaptureViewModel.init&lt;/code&gt; — by the time the user has tapped the mic and started talking, the engine is already loading.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multilingual handling
&lt;/h3&gt;

&lt;p&gt;The prompt detects the input language and produces the title, tags, body, and datetime surface forms in that language. Datetimes resolve against the device timezone, so &lt;em&gt;"domani alle 15"&lt;/em&gt;, &lt;em&gt;"tomorrow at 3pm"&lt;/em&gt;, and &lt;em&gt;"mañana a las 3"&lt;/em&gt; all produce real ISO instants. Supported at v1: English, Italian, Spanish, French, German, Portuguese. The UI is English-only in v1 — UI localization is a roadmap item.&lt;/p&gt;

&lt;h3&gt;
  
  
  Interoperability — your notes are not hostages
&lt;/h3&gt;

&lt;p&gt;Every note is, by construction, a portable Markdown file with YAML frontmatter (&lt;code&gt;Note.toMarkdownWithFrontmatter()&lt;/code&gt;). Drop it into an Obsidian vault, a Logseq graph, or any folder you sync — tags, resolved datetimes, headings, and checkboxes all carry with it. The privacy promise isn't just "we don't send your data", it's "your data was always yours".&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned about Gemma 4 E2B
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Framing matters more than I expected.&lt;/strong&gt; Going from "use checkboxes for tasks" to "REQUIRED: every &lt;em&gt;I need to / must&lt;/em&gt; is a &lt;code&gt;- [ ]&lt;/code&gt; checkbox" was the single largest quality jump. E2B respects directives far more reliably than permissions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Few-shot examples are tokens, not magic — and they can leak.&lt;/strong&gt; Early prompts had eight to ten examples; E2B over-mimicked their length &lt;em&gt;and&lt;/em&gt;, worse, sometimes copied their content into the user's note. Cutting to three short, low-salience examples fixed both the bloat and the leakage. This was the scariest bug of the project precisely because it violated the core "transform, don't invent" promise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Schema is the strongest hint.&lt;/strong&gt; An inline schema block plus three worked examples beats every "be sure to return valid JSON" instruction. The strict-retry pass works because it isn't asking for new content — just restating the schema with louder caps locks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It can do temporal reasoning if you give it the time.&lt;/strong&gt; Without &lt;code&gt;CURRENT TIMESTAMP&lt;/code&gt; in the prompt, every relative date came back &lt;code&gt;null&lt;/code&gt;. With it, ~95% of relative dates resolve correctly across the six languages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It can't be a fact source.&lt;/strong&gt; Anything that requires recall — "the dentist I always go to" — is hallucination territory. The contract is &lt;em&gt;transform&lt;/em&gt;, never &lt;em&gt;augment&lt;/em&gt;, and I verify it with adversarial fixtures.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Latency is real but tameable.&lt;/strong&gt; ~60s on a Pixel 6a (CPU) sounds long until you remember the user &lt;em&gt;just spent 60 seconds dictating&lt;/em&gt;. Pre-warming the engine and showing a clear progress affordance turns it into "I see something happening" rather than "is this frozen?". On the GPU path it's ~15-25s.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;Three upgrades I deliberately cut from v1 to ship within the competition window:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gemma audio-native ASR.&lt;/strong&gt; Replace &lt;code&gt;SpeechRecognizer&lt;/code&gt; with Gemma 4 E2B's multimodal audio input so transcription, language detection, and structuring all happen in one forward pass.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool calls for calendar.&lt;/strong&gt; With Gemma function-calling, a resolved &lt;code&gt;mentions[]&lt;/code&gt; could surface an on-device "Add to calendar" affordance via &lt;code&gt;Intent.ACTION_INSERT&lt;/code&gt;. Still no network.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ask your past notes — local RAG.&lt;/strong&gt; A small on-device embedding model (INT8, well under 200 MB) would make the corpus searchable by meaning. Embeddings live in Room next to the notes; queries never leave the device. This is roadmap and not v1 because RAG needs careful citation handling to keep the "transform, don't augment" promise.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Notari was built solo across the two weeks of the Gemma 4 Challenge. The model file is downloaded once, manually, from Google AI — no analytics, no telemetry, no surprises. The name takes its cue from the Latin&lt;/em&gt; notarius &lt;em&gt;— the historically trusted recorder of spoken statements. That, in two syllables, is the product.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>gemmachallenge</category>
      <category>gemma</category>
    </item>
  </channel>
</rss>
