<?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: memeshe</title>
    <description>The latest articles on DEV Community by memeshe (@memeshe).</description>
    <link>https://dev.to/memeshe</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2192841%2F630ae06c-38ab-46af-83fa-afba1fed4a69.jpg</url>
      <title>DEV Community: memeshe</title>
      <link>https://dev.to/memeshe</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/memeshe"/>
    <language>en</language>
    <item>
      <title>Whisper invented a cooking ad over six seconds of silence. So I built a memorial book that refuses to.</title>
      <dc:creator>memeshe</dc:creator>
      <pubDate>Wed, 28 Oct 2026 04:10:43 +0000</pubDate>
      <link>https://dev.to/memeshe/whisper-invented-a-cooking-ad-over-six-seconds-of-silence-so-i-built-a-memorial-book-that-refuses-b56</link>
      <guid>https://dev.to/memeshe/whisper-invented-a-cooking-ad-over-six-seconds-of-silence-so-i-built-a-memorial-book-that-refuses-b56</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week4-2026-10-26"&gt;Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In a fifteen-second recording donated to a public corpus, a woman is saying something ordinary in Vietnamese. When&lt;br&gt;
she stops talking, one of the two speech models I ran over it hears nothing at all — and the other thanks an&lt;br&gt;
audience that isn't there: &lt;em&gt;"Cảm ơn các bạn đã chào tạm biệt và hẹn gặp lại trong video tiếp theo."&lt;/em&gt; Thanks for&lt;br&gt;
watching, see you in the next video.&lt;/p&gt;

&lt;p&gt;She never made a video. That sentence is why this project exists. The recording I actually care about is the one&lt;br&gt;
nobody has made yet: a grandmother naming which ancestors get a giỗ, and on which lunar day, while someone&lt;br&gt;
finally thinks to press record. Her pauses will be longer than that woman's.&lt;/p&gt;
&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Sổ Giỗ&lt;/strong&gt; is a family memorial book that computes itself, and admits what it did not hear.&lt;/p&gt;

&lt;p&gt;You record the one person in the family who knows the death anniversaries — in Vietnamese, on a phone, at a&lt;br&gt;
kitchen table, for as long as they are willing to talk. Two different open speech models, both running on your&lt;br&gt;
own laptop with the network cable out, transcribe that recording independently. Where they agree, the words&lt;br&gt;
become quotable. Where they disagree, the page shows both readings and marks the span. Where one of them&lt;br&gt;
produced words over a stretch the other heard as silence, the page strikes the sentence out and labels it&lt;br&gt;
&lt;strong&gt;invented&lt;/strong&gt; — and that sentence is never allowed to become a date.&lt;/p&gt;

&lt;p&gt;From the words that survive, a small open model (also local) proposes claims: who the giỗ is for, on which&lt;br&gt;
lunar day and month, what goes on the tray, in which order. Every claim must carry the words that produced it&lt;br&gt;
and the second in the recording they were said at, or it is refused. Then a deterministic Vietnamese lunar&lt;br&gt;
engine — 200 lines, no dependencies — turns the surviving lunar dates into the next ten years of giỗ, and a&lt;br&gt;
durable workflow holds each one until the day arrives, a year at a time, surviving restarts.&lt;/p&gt;

&lt;p&gt;One sentence: &lt;strong&gt;the model is allowed to suggest, and only code decides — and the thing code guarantees is the&lt;br&gt;
date, not the prose.&lt;/strong&gt;&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/memeshee" rel="noopener noreferrer"&gt;
        memeshee
      &lt;/a&gt; / &lt;a href="https://github.com/memeshee/so-gio" rel="noopener noreferrer"&gt;
        so-gio
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Sổ Giỗ — the family memorial book whose ears admit what they didn't hear. Two local speech models, a deterministic Vietnamese lunar engine, and a guard that refuses to invent.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Bàn thờ&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;Sổ Giỗ — the family memorial book whose ears admit what they didn't hear.&lt;/p&gt;
&lt;p&gt;Two local speech models transcribe the same Vietnamese recording. Only the words both of them produced may
become a claim. A deterministic lunar engine turns those claims into the next ten years of giỗ dates, and a
durable workflow holds each one until the day arrives. Everything the models were unsure about is shown
marked, and never allowed to become a date.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Live demo: &lt;a href="https://gio.uconfidant.tech/" rel="nofollow noopener noreferrer"&gt;https://gio.uconfidant.tech/&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;The problem, and who has it&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;A Vietnamese household's ritual knowledge — which ancestor's giỗ falls on which lunar date, what goes on the
tray, in what order the eldest lights the incense — lives in one elderly head. Recording her is the easy part.
Turning the recording into something a family can rely on for the next forty years is not, for three reasons:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Speech models invent text&lt;/strong&gt;…&lt;/li&gt;
&lt;/ol&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/memeshee/so-gio" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


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

&lt;p&gt;&lt;a href="https://memeshee.github.io/so-gio/" rel="noopener noreferrer"&gt;memeshee.github.io/so-gio&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What to do, in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the page. The top shows the recording it read — duration, both model names, and how fast each one ran
on an 8-core CPU with no GPU.&lt;/li&gt;
&lt;li&gt;Scroll to &lt;strong&gt;what both models heard&lt;/strong&gt;. Each line has a timestamp chip. Those are the only lines allowed to
become claims.&lt;/li&gt;
&lt;li&gt;Look for the struck-through lines. Those are words one model produced over silence the other heard. There
is at least one; the audio is public, so you can listen and decide for yourself.&lt;/li&gt;
&lt;li&gt;Then the claims that survived, each with its chip, and the ones the guard refused, each with its reason.&lt;/li&gt;
&lt;li&gt;Then ten years of dates, computed, with the same quote behind each one.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The demo audio is a &lt;strong&gt;Google FLEURS &lt;code&gt;vi_vn&lt;/code&gt; recording (CC-BY-4.0)&lt;/strong&gt;, not a private family tape — deliberately,&lt;br&gt;
so that you can check every claim against the same public audio I used. The demo runs on a static page because&lt;br&gt;
a page about a family's private recording should not need a server to be alive in order to be read.&lt;/p&gt;
&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;MIT. Python 3.12, no GPU, one command per stage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/memeshee/so-gio &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;so-gio
python data/extract/build_dataset.py        &lt;span class="c"&gt;# 24 authored passages, 65 gold claims, every quote verified&lt;/span&gt;
python &lt;span class="nt"&gt;-m&lt;/span&gt; core.oracle                        &lt;span class="c"&gt;# calendar vs USNO + 17 published Vietnamese dates&lt;/span&gt;
python pipeline.py &lt;span class="nt"&gt;--audio&lt;/span&gt; corpus/audio/fleurs_vi_001.wav   &lt;span class="c"&gt;# the whole thing, end to end&lt;/span&gt;
python &lt;span class="nt"&gt;-m&lt;/span&gt; unittest discover &lt;span class="nt"&gt;-s&lt;/span&gt; tests         &lt;span class="c"&gt;# 19 tests on the parts that decide what a family is told&lt;/span&gt;
python &lt;span class="nb"&gt;eval&lt;/span&gt;/run_eval.py                      &lt;span class="c"&gt;# every number in this post&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;data/models/&lt;/code&gt; and the audio are gitignored; the models are downloaded on first run&lt;br&gt;
(&lt;code&gt;faster-whisper/small&lt;/code&gt; and &lt;code&gt;vinai/PhoWhisper-small&lt;/code&gt;, ~1.2 GB together).&lt;/p&gt;
&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;Four layers, and the interesting decisions are all about which one is allowed to speak.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Two ears, not one.&lt;/strong&gt; Arm A is OpenAI Whisper &lt;code&gt;small&lt;/code&gt; through CTranslate2 (int8), the model most people&lt;br&gt;
would actually run. Arm B is &lt;code&gt;vinai/PhoWhisper-small&lt;/code&gt;, trained on Vietnamese (VLSP), a different architecture&lt;br&gt;
family and a different corpus. Both get the &lt;em&gt;identical&lt;/em&gt; 16 kHz float32 samples, decoded once by ffmpeg, so any&lt;br&gt;
disagreement is attributable to the models and not to two different audio decoders.&lt;/p&gt;

&lt;p&gt;Why two: &lt;em&gt;Careless Whisper&lt;/em&gt; (FAccT 2024) measured roughly &lt;strong&gt;1% of Whisper segments wholly invented, 38% of&lt;br&gt;
those harmful, and the rate rising with the duration of non-vocal audio&lt;/strong&gt;. A tired 90-year-old is mostly&lt;br&gt;
pauses, so the failure lands on the recording you cannot re-make. One model cannot tell you when it did this;&lt;br&gt;
two models disagreeing can.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The guard is the product.&lt;/strong&gt; A claim survives only if (a) its quote is a normalised substring of what both&lt;br&gt;
arms produced, (b) that quote can be located at a timestamp, and (c) any lunar date in it is resolvable by the&lt;br&gt;
calendar. Failing any of those, the claim is &lt;em&gt;refused with a reason&lt;/em&gt; rather than corrected. The refusals are&lt;br&gt;
printed on the page and counted in the post, because "we invented nothing" is only credible next to the list&lt;br&gt;
of things we refused to invent. In the demo run, &lt;strong&gt;zero kept claims were uncited&lt;/strong&gt;, by construction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The calendar is not a model.&lt;/strong&gt; &lt;code&gt;core/lunar.py&lt;/code&gt; implements the definitions rather than copying a code&lt;br&gt;
path: a month begins on the astronomical new moon in &lt;strong&gt;UTC+7&lt;/strong&gt;; lunar month 11 is the one containing the&lt;br&gt;
December solstice; a year has a leap month when 13 new moons fall between two month-11s, and the leap month is&lt;br&gt;
the first with no major solar term (a solar longitude that is a multiple of 30°), repeating the previous&lt;br&gt;
month's number. New moons come from Meeus chapter 49 with the full periodic series and a ΔT conversion to UT.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Durability is Temporal, and the ritual repeats.&lt;/strong&gt; One &lt;code&gt;GioWorkflow&lt;/code&gt; per person sleeps on a durable timer&lt;br&gt;
until the next giỗ, sends the reminder through an activity with a retry policy, waits for an acknowledgement,&lt;br&gt;
and re-arms for the following year. A giỗ is not a notification; it is an annual obligation, and the only&lt;br&gt;
honest way to show software will hold it is to kill the process holding it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;=== 14. verdict ===
killed worker pid 2554542 with SIGKILL while the timer was pending;
a different process (pid 2554888) woke up and delivered the reminder:
    {"activity_id": "1", "attempt": 3, "solar_date": "2027-04-16", "worker_pid": 2554888,
     "body_vi": "Hôm nay là 10/03 âm lịch — ngày giỗ của Cụ Nguyễn Thị Đào (2027-04-16).
                 Con cháu chuẩn bị hương, hoa và mâm cơm cúng."}
PASS — the timer lived in the server, not in the killed process.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;attempt: 3&lt;/code&gt; because the activity fails twice on purpose, to show the retry policy earning its place. The full&lt;br&gt;
transcript, including the workflow input and every state transition, is in &lt;code&gt;scripts/prove_durability.out&lt;/code&gt;.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;The calendar is right.&lt;/strong&gt; Every giỗ date in this post comes from &lt;code&gt;core/lunar.py&lt;/code&gt;, and I graded it against two&lt;br&gt;
external oracles rather than against itself:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What&lt;/th&gt;
&lt;th&gt;Oracle&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;New-moon instants&lt;/td&gt;
&lt;td&gt;USNO moon-phase API, 37 new moons across 2025–2027&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;37/37 within 2 minutes&lt;/strong&gt;, worst delta &lt;strong&gt;84.2 s&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lunar → solar dates&lt;/td&gt;
&lt;td&gt;17 published Vietnamese dates (Tết 2020–2036, Ông Táo, Giỗ Tổ Hùng Vương, Rằm tháng Bảy, Trung Thu 2026)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;17/17 exact&lt;/strong&gt;, all round-trip&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Structural invariants&lt;/td&gt;
&lt;td&gt;41 lunar years, 2020–2060&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0 violations&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The Rằm tháng Bảy case is the one that matters: in 2026 that new moon falls at &lt;strong&gt;00:36 on 13 August in UTC+7&lt;/strong&gt; —&lt;br&gt;
36 minutes past midnight. Anchor the calendar to UTC+8 instead and the same margin lands on the wrong day the&lt;br&gt;
first time a new moon falls a few minutes earlier.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The two models are not equally good, which is why there are two.&lt;/strong&gt; Syllable-level WER against the published&lt;br&gt;
FLEURS reference transcripts, 18 clips, 204 s of audio, CPU only:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Arm&lt;/th&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Mean WER&lt;/th&gt;
&lt;th&gt;Realtime factor&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;whisper&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;faster-whisper/small&lt;/code&gt; int8&lt;/td&gt;
&lt;td&gt;0.221&lt;/td&gt;
&lt;td&gt;0.52×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;phowhisper&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;vinai/PhoWhisper-small&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.140&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.21×&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The Vietnamese-specific model is 36% more accurate and 2.5× slower. Neither number decides anything; what&lt;br&gt;
decides is that they make &lt;em&gt;different&lt;/em&gt; mistakes, and the difference is the product.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What they invented in silence.&lt;/strong&gt; I spliced verified digital silence (peak &lt;strong&gt;-91 dBFS&lt;/strong&gt;) into real recordings&lt;br&gt;
at real speech pauses and asked each model to transcribe only the silent window. The reference for every one of&lt;br&gt;
those windows is nothing at all. Model output, quoted exactly:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Window&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;whisper-small&lt;/code&gt; said&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;phowhisper-small&lt;/code&gt; said&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;5.6 s of silence&lt;/td&gt;
&lt;td&gt;
&lt;em&gt;"Cảm ơn các bạn đã chào tạm biệt và hẹn gặp lại trong video tiếp theo."&lt;/em&gt; — 16 syllables&lt;/td&gt;
&lt;td&gt;
&lt;em&gt;"điều này từ thành công của tài xế hạng cao hơn."&lt;/em&gt; — 11 syllables&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6.5 s of silence&lt;/td&gt;
&lt;td&gt;
&lt;em&gt;"Hãy subscribe cho kênh Ghiền Mì Gõ Để không bỏ lỡ những video hấp dẫn"&lt;/em&gt; — 15 syllables&lt;/td&gt;
&lt;td&gt;
&lt;em&gt;"một trong những thay đổi đều được xem là một tài liệu sản phẩm."&lt;/em&gt; — 14 syllables&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7.2 s of silence&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;nothing&lt;/strong&gt; — correct&lt;/td&gt;
&lt;td&gt;
&lt;em&gt;"một trong những thay tòa soạn nhất là các tàu ngầm vũ trang sức và tính mạng."&lt;/em&gt; — 17 syllables&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Nineteen and a quarter seconds of verified nothing; &lt;strong&gt;73 syllables of invented Vietnamese&lt;/strong&gt;, at 96.6 invented&lt;br&gt;
words per minute from one arm and 130.9 from the other. Both arms do it, on different windows, which is the whole&lt;br&gt;
argument for running two: sample one silence and you would conclude that Whisper is safe and PhoWhisper is not —&lt;br&gt;
the third window says the opposite of the first two.&lt;/p&gt;

&lt;p&gt;The narrowing matters as much as the finding. I ran two more conditions: silences spliced &lt;strong&gt;into&lt;/strong&gt; continuous&lt;br&gt;
speech at 15 s total, and 5 s tails appended after the last word at 30 s total. Both arms produced &lt;strong&gt;zero&lt;/strong&gt;&lt;br&gt;
invented syllables on all of them. So this is not "Whisper invents whenever it hears quiet" — it invents when a&lt;br&gt;
silent stretch arrives as a clean block with nothing around it, which is precisely what a long pause at the end&lt;br&gt;
of a recording, a gap where the tape was stopped, or a trimmed file looks like. The measurement that would have&lt;br&gt;
flattered the product (one big scary percentage) is not the one I am reporting.&lt;/p&gt;

&lt;p&gt;Read the second row again. Given six and a half seconds of &lt;em&gt;nothing&lt;/em&gt;, Whisper asked the listener to subscribe to&lt;br&gt;
a named Vietnamese YouTube cooking channel. This is the failure the product exists to catch, and it is not&lt;br&gt;
subtle: it is a commercial playing in the pause where a grandmother stopped talking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can the two-model method catch it?&lt;/strong&gt; Here I have to publish a loss. The catch rule compares segment&lt;br&gt;
timestamps: if one arm placed words where the other had no segment, flag it invented. Across 24 (arm × silence)&lt;br&gt;
pairs, five had real invention and it flagged &lt;strong&gt;one — 20%&lt;/strong&gt;. The mechanism is not mysterious: 14 segments sat&lt;br&gt;
more than half inside detected non-speech, and &lt;strong&gt;13 of those were suppressed&lt;/strong&gt; because the other arm had emitted&lt;br&gt;
one long segment spanning the whole window, so "nothing was said here" was never true.&lt;br&gt;
The fix was to stop inferring invention from timestamps and start &lt;em&gt;probing&lt;/em&gt; it: take each detected non-speech&lt;br&gt;
window, hand the models that window alone, and treat whatever comes back as invented, because there is no speech&lt;br&gt;
in the input. That is &lt;code&gt;probe_silences()&lt;/code&gt;, and it is now wired into the pipeline rather than sitting in an&lt;br&gt;
evaluation script, which it was for one embarrassing afternoon. It cannot miss by construction, which is exactly&lt;br&gt;
what the in-transcript rule cannot say. And no invented syllable ever reached the quotable set under either&lt;br&gt;
method — &lt;code&gt;agreed_spans&lt;/code&gt; only keeps syllables both arms produced — which is why the guard, not the detector, is&lt;br&gt;
the load-bearing part.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Extraction, six arms.&lt;/strong&gt; 8 held-out passages, 65 gold claims, every arm scored through the same guard:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Arm&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;th&gt;Precision&lt;/th&gt;
&lt;th&gt;Recall&lt;/th&gt;
&lt;th&gt;F1&lt;/th&gt;
&lt;th&gt;Uncited kept&lt;/th&gt;
&lt;th&gt;s/item&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tinker_tuned&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Qwen3.5-4B&lt;/code&gt; + rank-16 LoRA, Tinker&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.958&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.875&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.915&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;7.8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tinker_base&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;same model, no adapter&lt;/td&gt;
&lt;td&gt;0.802&lt;/td&gt;
&lt;td&gt;0.667&lt;/td&gt;
&lt;td&gt;0.728&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;2.6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;qwen2.5:3b-instruct&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;local, Ollama, 3B&lt;/td&gt;
&lt;td&gt;0.625&lt;/td&gt;
&lt;td&gt;0.476&lt;/td&gt;
&lt;td&gt;0.540&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;92.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;regex&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;hand-written lunar-date pattern&lt;/td&gt;
&lt;td&gt;0.857&lt;/td&gt;
&lt;td&gt;0.286&lt;/td&gt;
&lt;td&gt;0.429&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0.001&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;regex_all_dates&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;the degenerate "every date is a giỗ" arm&lt;/td&gt;
&lt;td&gt;0.194&lt;/td&gt;
&lt;td&gt;0.286&lt;/td&gt;
&lt;td&gt;0.231&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0.001&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gemma3:1b-it-qat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;local, Ollama, 1B&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.000&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.000&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.000&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;36.3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two rows deserve their own sentence. &lt;strong&gt;A 1B model scores zero&lt;/strong&gt; — not badly, zero: it produces well-formed&lt;br&gt;
claims that are not the ones in the text, so "just run a small model locally" is the assumption this table&lt;br&gt;
kills. And a regex gets 0.857 precision at 0.286 recall, because a pattern is never wrong about a date it can&lt;br&gt;
see and blind to every date phrased differently. What wins is neither; it is a model trained for this one narrow&lt;br&gt;
job. Two things I would rather not print: the fine-tune costs &lt;strong&gt;3× the latency&lt;/strong&gt;, and 16 training passages is&lt;br&gt;
small enough that a loss collapsing to ~0.00002 by step 40 is memorisation as much as learning.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Went Wrong
&lt;/h2&gt;

&lt;p&gt;Three failures worth publishing, because each one would have shipped silently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A reasoning model hid its own answer.&lt;/strong&gt; &lt;code&gt;Qwen3.5-4B&lt;/code&gt; given an ordinary generation prompt emits&lt;br&gt;
&lt;code&gt;Thinking Process:&lt;/code&gt; and reasons in prose before any JSON. The strict parser found no claims, and the first&lt;br&gt;
scoring run read as a total failure of the model arm. The fix is one argument — render &lt;em&gt;both&lt;/em&gt; arms through the&lt;br&gt;
chat template with &lt;code&gt;enable_thinking=False&lt;/code&gt; — and it is not cosmetic: without it the tuned arm is scored against&lt;br&gt;
a baseline that was never allowed to answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Segment-level grounding threw away everything.&lt;/strong&gt; My first version of "only the words both models agree on"&lt;br&gt;
required a whole ASR segment to be contained in the other arm's transcript. The two arms agree on ~84% of&lt;br&gt;
syllables and disagree somewhere in almost every sentence, so the filter returned &lt;strong&gt;zero quotable spans&lt;/strong&gt; on a&lt;br&gt;
clean recording. The fix was to align the two transcripts syllable by syllable and keep the runs they agree on.&lt;br&gt;
Same principle, one level finer — and the difference between a product and a demo that outputs nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The leap month was numbered like the month after it.&lt;/strong&gt; My first version gave &lt;code&gt;tháng 4 nhuận&lt;/code&gt; the number 5, and&lt;br&gt;
it still round-tripped its own tests — consistently wrong in both directions. Only the 17 &lt;em&gt;published&lt;/em&gt; dates&lt;br&gt;
caught it. That is the whole argument for an external oracle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;Because this artefact has to outlive every company in this post.&lt;/p&gt;

&lt;p&gt;A giỗ calendar is a document about dead people that a household will consult for forty years. If it lives in a&lt;br&gt;
subscription, it has a churn date. If the lunar conversion is a call to somebody's API, the family's ritual&lt;br&gt;
calendar is now a dependency of a vendor's pricing decision. And if the only copy of grandmother's recording is&lt;br&gt;
on a server that grants itself a perpetual licence over uploaded content — which is what the default cloud&lt;br&gt;
transcription offering asks for — then the family has handed over the one thing that cannot be re-recorded.&lt;/p&gt;

&lt;p&gt;Concretely, three things were only possible because the weights are open:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Diffing two models requires owning two models.&lt;/strong&gt; You cannot run a disagreement check against two rented
endpoints you are not allowed to inspect, at a price a household will pay per recording.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The failure mode is the product feature.&lt;/strong&gt; I could point a camera at the invention because both models sit
on the same disk as the audio and the whole pipeline runs with &lt;code&gt;nmcli networking off&lt;/code&gt;. A closed pipeline
gives you a transcript and no way to ask what it heard that wasn't there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The engine is inspectable and forkable.&lt;/strong&gt; Vietnamese lunar practice differs by region and by family; some
households hold the giỗ on the solar date, some use the leap month, some move it when it lands badly. A tool
for this must be readable, editable, and runnable in 2066 by someone who has never heard of me. Two hundred
lines of arithmetic and a text file travel better than an SDK.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And one honest concession: a hosted frontier model writes a warmer remembrance paragraph than a 4B model on a&lt;br&gt;
six-year-old laptop. Where a bigger model genuinely wins in this build is prose, not dates — which is exactly&lt;br&gt;
why the architecture puts the guarantee in the code and treats the writing as decoration the family can&lt;br&gt;
replace.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Agent Session
&lt;/h2&gt;

&lt;p&gt;This build was done with a coding agent (Hermes, running the model this IDE was talking to) driving the repo,&lt;br&gt;
the tests, and the calendar validation, with the human making the calls that mattered: which product, which&lt;br&gt;
category claims, what to refuse. DevRelay's transcript parser does not cover this harness, so rather than&lt;br&gt;
embed a session it cannot read, here is what the agent actually did: wrote 19 tests and the oracle before the&lt;br&gt;
features, ran the calendar against USNO until the worst new-moon delta was under two minutes, caught the&lt;br&gt;
reasoning-model trap by reading a probe rather than the docs, and produced every table in this post with a&lt;br&gt;
script that can be re-run.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Spirit of It
&lt;/h2&gt;

&lt;p&gt;An ancestor who is fed is not a ghost that haunts.&lt;/p&gt;

&lt;p&gt;That line is not mine. It is the closing thought of a Vietnamese-American physician writing about her&lt;br&gt;
grandmother's death feast, and it is the best sentence about AI I have read this year:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Throughout the year, at the family altar, the spirit is offered fruit, rice, water and tea. It's not&lt;br&gt;
uncommon to see a can or two of Heineken. … Without the offerings, the spirit becomes a 'hungry ghost,' that&lt;br&gt;
haunts the family."&lt;br&gt;
— Christine Nguyen, MD&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The ritual is a maintenance contract between the living and the dead, renewed annually, with food. Nothing in it&lt;br&gt;
requires a machine to speak as anybody. The ghost in the machine, in this build, is confabulation — and the&lt;br&gt;
whole product is a way of pointing at it rather than pretending it is a feature.&lt;/p&gt;

&lt;p&gt;What landed, for me, was smaller and more uncomfortable. The demo recording is public, so you can do what I did:&lt;br&gt;
listen to the six seconds of silence where one model asked you to subscribe to a cooking channel. Then imagine&lt;br&gt;
that silence is not a corpus clip. Imagine it is the pause after your grandmother says she is tired and would&lt;br&gt;
like to stop now. Every family has six seconds like that, somewhere in the middle of the recording that was&lt;br&gt;
supposed to keep her.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;p&gt;Five categories, each one carrying a real part of the build rather than a logo.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Tinker&lt;/strong&gt; — &lt;code&gt;Qwen/Qwen3.5-4B&lt;/code&gt; with a rank-16 LoRA trained on Tinker for one task: copy a
verbatim span into a structured claim, or emit nothing. Scored on 8 held-out passages against the same base
model, same prompt, same &lt;code&gt;enable_thinking=False&lt;/code&gt; position, plus a regex baseline and a degenerate
always-a-date arm. The comparison table is in &lt;em&gt;What I Measured&lt;/em&gt;, including the run where it lost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Gemma&lt;/strong&gt; — &lt;code&gt;gemma3:1b-it-qat&lt;/code&gt; through Ollama is the &lt;em&gt;on-device, no-fine-tune&lt;/em&gt; arm: what a family
gets with zero training and zero setup. It is the weakest arm on extraction, which is the point of printing
it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Temporal&lt;/strong&gt; — one &lt;code&gt;GioWorkflow&lt;/code&gt; per deceased relative, sleeping on a durable timer until the next
giỗ, sending the reminder through an activity with a retry policy, waiting for an acknowledgement, then
re-arming for the next year. The proof is a transcript, not a claim: the worker was killed with &lt;code&gt;SIGKILL&lt;/code&gt;
mid-timer and a different operating-system process woke up and delivered the reminder. Temporal's brief asks
for software that "survives failures, retries flaky tool calls, and picks up where it left off" — this is that,
at a one-year scale, which is the scale a ritual actually runs on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Sentry Agent Tracing&lt;/strong&gt; — one root transaction per pipeline run (&lt;code&gt;core/telemetry.py&lt;/code&gt;), with a
span for each ASR arm carrying latency, realtime factor and segment count; a span for the extraction call with
its prompt and completion tokens; a span for the guard carrying kept and refused counts plus &lt;strong&gt;one span per
refusal&lt;/strong&gt; with the rule that fired; and a per-attempt span in the Temporal activity, so the delivery that
finally succeeded shows up as three attempts with the retry backoff visible between them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The design decision is the part worth judging. Tracing a product whose entire premise is that a private&lt;br&gt;
  recording never leaves the house means the trace must not contain the recording. So the module exposes no&lt;br&gt;
  helper that accepts free text — every call site passes a number, a duration or an enum. &lt;code&gt;send_default_pii&lt;/code&gt; is&lt;br&gt;
  forced off, request bodies are never attached, and any string that could have come from audio goes through&lt;br&gt;
  &lt;code&gt;redact()&lt;/code&gt;, which keeps length, syllable count and a fingerprint and throws away the words. A contributor who&lt;br&gt;
  wants to log a transcript has to edit that file, which is exactly the kind of change a reviewer sees. Verified&lt;br&gt;
  by watching an envelope leave the process: &lt;code&gt;Sending envelope [envelope with 1 items (transaction)]&lt;br&gt;
  project:4512199446757376 host:o1068235.ingest.us.sentry.io&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;And the honest limit: tracing did not find my bug. The bug that mattered — an invention detector that caught&lt;br&gt;
  &lt;strong&gt;one of five&lt;/strong&gt; hallucinations — was found by measuring against ground truth, and it is in &lt;em&gt;What I Measured&lt;/em&gt;&lt;br&gt;
  with the numbers attached. Sentry's contribution here is the refusal ledger and the retry trail: the thing that&lt;br&gt;
  makes "we invented nothing" and "it fired after the crash" auditable after the fact rather than asserted.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of a .tech Domain&lt;/strong&gt; — &lt;code&gt;gio.uconfidant.tech&lt;/code&gt;, claimed through Hacktoberfest. The demo behind it is a
static build, on purpose: a page about a private recording must not depend on anybody's free tier staying
awake. Of the 36 live demo links among this series' winners, &lt;strong&gt;five are already dead&lt;/strong&gt; — every one of them a
suspended free instance. A prebuilt HTML page with a &lt;code&gt;.nojekyll&lt;/code&gt; costs nothing to serve, loads in one request,
prints correctly, and the link a judge clicks three weeks after the deadline is the same bytes I tested today.
Live at &lt;a href="https://gio.uconfidant.tech/" rel="noopener noreferrer"&gt;gio.uconfidant.tech&lt;/a&gt; behind a Let's Encrypt certificate issued this
afternoon, with the &lt;code&gt;.github.io&lt;/code&gt; hostname as the fallback GitHub redirects to it automatically.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Every number above was produced by a script in the repository, and every published date is quoted with the&lt;br&gt;
source I checked it against. If you find a wrong giỗ date in here, open an issue — the calendar is the one part&lt;br&gt;
of this that has to be right.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Leave It: I fine-tuned a 4B model to defer 73 of 731 GitHub issues, then killed its worker with -9</title>
      <dc:creator>memeshe</dc:creator>
      <pubDate>Fri, 23 Oct 2026 16:37:18 +0000</pubDate>
      <link>https://dev.to/memeshe/leave-it-i-fine-tuned-a-4b-model-to-defer-73-of-731-github-issues-then-killed-its-worker-with-9-30on</link>
      <guid>https://dev.to/memeshe/leave-it-i-fine-tuned-a-4b-model-to-defer-73-of-731-github-issues-then-killed-its-worker-with-9-30on</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week3-2026-10-19"&gt;Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;#devchallenge&lt;/code&gt; &lt;code&gt;#hf26challenge&lt;/code&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Leave It&lt;/strong&gt; is a morning filter for a backlog I have decided not to read.&lt;/p&gt;

&lt;p&gt;I point it at a stream of items — for this post, 731 real GitHub issues from&lt;br&gt;
&lt;code&gt;vercel/next.js&lt;/code&gt;, &lt;code&gt;rust-lang/rust&lt;/code&gt;, &lt;code&gt;python/cpython&lt;/code&gt; and &lt;code&gt;microsoft/vscode&lt;/code&gt;. For each one,&lt;br&gt;
a fine-tuned 4B open-weight model answers a single question: &lt;strong&gt;does this need a maintainer&lt;br&gt;
today?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Items it answers &lt;code&gt;WAIT&lt;/code&gt; for do not get shown to me. They do not get deleted either. Each&lt;br&gt;
one is held by a durable Temporal fuse, so if nobody ever engages with it, it stays&lt;br&gt;
buried; if someone does, it surfaces immediately. The morning surface is &lt;strong&gt;one card&lt;/strong&gt;,&lt;br&gt;
not a list, and definitely not an inbox with 731 rows in it.&lt;/p&gt;

&lt;p&gt;The whole product is one sentence: &lt;em&gt;the tool's job is to earn me the right to skip&lt;br&gt;
something, and then to make sure being wrong about that is cheap.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The number, up front.&lt;/strong&gt; On the held-out window it defers &lt;strong&gt;73 of the 731 items&lt;/strong&gt; (10.0% of the stream) while wrongly deferring 11 of the 235 items that genuinely needed a maintainer within 24 hours. The same model with the same prompt and no fine-tune defers 49 (6.7%) at AUC 0.516, which is a coin flip. The best classical arm reaches 10.7%.&lt;/p&gt;
&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;The demo is a static page generated from the same artefacts the evaluation reads:&lt;br&gt;
&lt;strong&gt;&lt;a href="https://memeshee.github.io/leaveit/" rel="noopener noreferrer"&gt;https://memeshee.github.io/leaveit/&lt;/a&gt;&lt;/strong&gt; (mirror: &lt;a href="https://leaveit-demo.vercel.app/" rel="noopener noreferrer"&gt;https://leaveit-demo.vercel.app/&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;There is deliberately no dynamic backend. A previous hackathon entry of mine scored&lt;br&gt;
badly because its demo was a free-tier web service that had spun down by judging time&lt;br&gt;
and returned a 404 — the rules require "a demo of the project (a deployed link or&lt;br&gt;
video)", and a dead link fails a required element no matter how good the code is. So&lt;br&gt;
this page is a single self-contained HTML file with zero external requests, rebuilt by&lt;br&gt;
&lt;code&gt;app/build_site.py&lt;/code&gt; from &lt;code&gt;data/&lt;/code&gt;, &lt;code&gt;eval/&lt;/code&gt; and the ledger. It cannot 404 an asset it does&lt;br&gt;
not have.&lt;/p&gt;

&lt;p&gt;The interesting half of the demo is not visual, though. It is this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;t+0s  three seals opened: A fuse 24s, C fuse 16s, B fuse 40s (B is the rescue case)
       sealed A  vercel/next.js#100381      p=0.15 fuse=24s  wf=seal-A-1792773234
       sealed B  vercel/next.js#100518      p=0.18 fuse=40s  wf=seal-B-1792773234
       sealed C  vercel/next.js#100398      p=0.20 fuse=16s  wf=seal-C-1792773234
t+9s  kill -9 the worker (pid 1453470)
t+9s  worker is dead: no process exists that could run an activity
t+9s  3 workflows still Running on the server (they were not living in the worker)
t+11s  deliver 'on_attention' to seal B while NO worker process exists
       signal delivered to seal-B-1792773234 (worker alive: no)
t+16s  restart the worker
t+40s  fuses fired; ledger folded from the append-only log:
       LEDGER (append-only, folded):
       vercel/next.js#100398        LAPSED    p=0.20  fuse expired after 16s; nobody ever engaged
       vercel/next.js#100381        LAPSED    p=0.15  fuse expired after 24s; nobody ever engaged
       vercel/next.js#100518        RESCUED   p=0.18  a maintainer commented while the worker was dead
       MORNING CARD:
       One thing survived the fuse: vercel/next.js#100381 (sealed at P=0.15, nobody touched it for 0s past the fuse)
t+40s  server-side run history: 3/3 seals Completed
t+40s  done: 3 seals, 1 kill -9, 1 signal delivered to nobody, 0 fuses lost
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Read the t+11s line again. A signal was delivered to a workflow at a moment when &lt;strong&gt;no&lt;br&gt;
worker process existed on the machine&lt;/strong&gt;. On restart, the seal that received it was&lt;br&gt;
rescued and the two that received nothing fired exactly when they were promised to.&lt;br&gt;
That is the entire reason a &lt;code&gt;sleep()&lt;/code&gt; is not good enough for this product.&lt;/p&gt;
&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/memeshee" rel="noopener noreferrer"&gt;
        memeshee
      &lt;/a&gt; / &lt;a href="https://github.com/memeshee/leaveit" rel="noopener noreferrer"&gt;
        leaveit
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A morning filter for a backlog you have decided not to read. Fine-tuned open-weight model + durable Temporal fuses.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Leave It&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;A morning filter for a backlog you have decided not to read.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You point it at a stream of items. A fine-tuned 4B open-weight model answers one question per
item — &lt;strong&gt;does this need a human today?&lt;/strong&gt; — as a single token. Everything it defers is held by
a durable Temporal fuse rather than dropped, so the decision to ignore something survives a
crash, a reboot, or a closed laptop lid.&lt;/p&gt;
&lt;p&gt;Live demo: &lt;strong&gt;&lt;a href="https://memeshee.github.io/leaveit/" rel="nofollow noopener noreferrer"&gt;https://memeshee.github.io/leaveit/&lt;/a&gt;&lt;/strong&gt; (mirror: &lt;a href="https://leaveit-demo.vercel.app/" rel="nofollow noopener noreferrer"&gt;https://leaveit-demo.vercel.app/&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;Built for the &lt;a href="https://dev.to/challenges/hacktoberfest-week3-2026-10-19" rel="nofollow"&gt;Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady&lt;/a&gt;.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;The claim, and the number behind it&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Evaluated on a &lt;strong&gt;held-out future window&lt;/strong&gt; — items created after everything the model trained
on. Every arm is scored at the same operating point: a &lt;strong&gt;5% miss budget&lt;/strong&gt;, meaning at most 5%
of the items that genuinely needed a maintainer within 24 hours may be deferred.&lt;/p&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;arm&lt;/th&gt;
&lt;th&gt;deferred&lt;/th&gt;
&lt;th&gt;of pool&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;/table&gt;&lt;/div&gt;…&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/memeshee/leaveit" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;MIT. One command per stage, and the numbers in this post are regenerated by those&lt;br&gt;
commands rather than typed into it.&lt;/p&gt;
&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;
&lt;h3&gt;
  
  
  1. The label had to be "a maintainer looked", not "somebody replied"
&lt;/h3&gt;

&lt;p&gt;My first instinct was to label an item &lt;code&gt;NOW&lt;/code&gt; if anyone commented on it. That is wrong in&lt;br&gt;
exactly the way that matters. In large repositories, other users pile onto an issue with&lt;br&gt;
"+1", "me too", "same here". That is engagement. It is not attention. A model trained on&lt;br&gt;
it learns to predict crowd noise.&lt;/p&gt;

&lt;p&gt;So the fetch pulls real issues over GitHub GraphQL, including per-comment timestamps and&lt;br&gt;
each commenter's &lt;code&gt;authorAssociation&lt;/code&gt;, and the label is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;needs_today&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;MEMBER&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;OWNER&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;COLLABORATOR&lt;/span&gt; &lt;span class="n"&gt;commented&lt;/span&gt; &lt;span class="n"&gt;within&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="n"&gt;hours&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="n"&gt;was&lt;/span&gt; &lt;span class="n"&gt;closed&lt;/span&gt; &lt;span class="n"&gt;within&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="n"&gt;hours&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details in the dataset that I would defend in court:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Censoring is not a label.&lt;/strong&gt; An item created three hours ago has not "not been
handled". It has not happened yet. Those rows are dropped, not labelled &lt;code&gt;WAIT&lt;/code&gt;. My
first pass counted them and produced a test set that was 58.7% "ignorable" against a
training set that was 31.2% — I was measuring the clock, not the model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The split is temporal.&lt;/strong&gt; I train on May–October and test on the newest 5 days,
because that is how the thing is used: always on items that did not exist when it was
trained. A random split would have flattered every arm, including the bad ones.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That leaves &lt;strong&gt;2,924 training items and 731 held-out items&lt;/strong&gt;, of which 235 genuinely&lt;br&gt;
needed a maintainer within 24 hours and &lt;strong&gt;496 (67.9%) genuinely could wait&lt;/strong&gt;. That last&lt;br&gt;
number is the ceiling: a perfect ranker can defer 67.9% of the stream, and no ranker can&lt;br&gt;
do better.&lt;/p&gt;
&lt;h3&gt;
  
  
  2. One policy for every arm, and it is a miss budget, not accuracy
&lt;/h3&gt;

&lt;p&gt;Accuracy is the wrong headline here and I refuse to print it. An arm that answers "needs&lt;br&gt;
you" to all 731 items is 32% "accurate" on this test set and saves me precisely nothing.&lt;/p&gt;

&lt;p&gt;So every arm — fine-tune, base model, logistic regression, dumb heuristics — goes through&lt;br&gt;
the same function in &lt;code&gt;eval/policy.py&lt;/code&gt;. Items are deferred from highest defer-score down,&lt;br&gt;
and the arm stops at a &lt;strong&gt;miss budget&lt;/strong&gt;: at most 5% of the 235 items that genuinely needed&lt;br&gt;
someone may end up deferred. Within that budget, we count how much of the stream never&lt;br&gt;
reached me.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;arm&lt;/th&gt;
&lt;th&gt;deferred&lt;/th&gt;
&lt;th&gt;of the stream&lt;/th&gt;
&lt;th&gt;wrongly deferred&lt;/th&gt;
&lt;th&gt;AUC&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;tfidf + hand features&lt;/td&gt;
&lt;td&gt;78&lt;/td&gt;
&lt;td&gt;10.7%&lt;/td&gt;
&lt;td&gt;11 of 235&lt;/td&gt;
&lt;td&gt;0.663&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fine-tuned 4B (Tinker LoRA)&lt;/td&gt;
&lt;td&gt;73&lt;/td&gt;
&lt;td&gt;10.0%&lt;/td&gt;
&lt;td&gt;11 of 235&lt;/td&gt;
&lt;td&gt;0.665&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tfidf + logistic regression&lt;/td&gt;
&lt;td&gt;57&lt;/td&gt;
&lt;td&gt;7.8%&lt;/td&gt;
&lt;td&gt;11 of 235&lt;/td&gt;
&lt;td&gt;0.634&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;base 4B, same prompt&lt;/td&gt;
&lt;td&gt;49&lt;/td&gt;
&lt;td&gt;6.7%&lt;/td&gt;
&lt;td&gt;11 of 235&lt;/td&gt;
&lt;td&gt;0.516&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;text length only&lt;/td&gt;
&lt;td&gt;28&lt;/td&gt;
&lt;td&gt;3.8%&lt;/td&gt;
&lt;td&gt;11 of 235&lt;/td&gt;
&lt;td&gt;0.426&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;already has a label&lt;/td&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;td&gt;3.1%&lt;/td&gt;
&lt;td&gt;11 of 235&lt;/td&gt;
&lt;td&gt;0.393&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;em&gt;perfect ranker (ceiling)&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;&lt;em&gt;496&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;&lt;em&gt;67.9%&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;&lt;em&gt;11 of 235&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;&lt;em&gt;1.000&lt;/em&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two things in that table are worth saying out loud rather than burying:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The fine-tune does not dominate the classical arm.&lt;/strong&gt; AUC 0.665 versus 0.663 is a tie
dressed up as a result, and the best word-count classifier actually defers 5 more items
at exactly the 5% budget. It wins at the tight budget (64 deferred at 3% versus 44) and
at the loose one (152 at 10% versus 129). If you want "a small model beats TF-IDF
everywhere", this is not that post.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The base model is a coin flip.&lt;/strong&gt; AUC 0.516 against a baseline of 0.500. Against the
baseline the category actually specifies — the same model, same prompt, no LoRA — the
fine-tune moves deferral from 49 items to 73 at the same miss budget, which is what the
fine-tune is for.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  3. What thinking costs
&lt;/h3&gt;

&lt;p&gt;The Tinker brief asks for an improvement in "performance, latency, or cost", so I measured&lt;br&gt;
the other two axes as well, on the same 40 items, with the base model allowed to use its&lt;br&gt;
own reasoning:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;arm&lt;/th&gt;
&lt;th&gt;verdict produced&lt;/th&gt;
&lt;th&gt;tokens generated per item&lt;/th&gt;
&lt;th&gt;tokens for 40 items&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;base 4B, thinking enabled&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;15 / 40&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;696&lt;/td&gt;
&lt;td&gt;27,837&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fine-tuned 4B, verdict as first token&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;40 / 40&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Left to its own devices, the reasoning model spent the entire 700-token budget on 25 of 40&lt;br&gt;
items and &lt;strong&gt;never produced a verdict at all&lt;/strong&gt;, and on the 15 it did answer it was 20%&lt;br&gt;
accurate. The fine-tune turns the same weights into a one-token classifier: 348× fewer&lt;br&gt;
tokens and a parseable answer every time.&lt;/p&gt;

&lt;p&gt;I am explicitly &lt;strong&gt;not&lt;/strong&gt; claiming a latency win. My median wall-clock numbers came out 7.08s&lt;br&gt;
for the tuned arm against 4.03s for the base arm, which is almost certainly queue and&lt;br&gt;
session overhead rather than model time, and I would rather leave a favourable claim on the&lt;br&gt;
table than publish a measurement I do not trust.&lt;/p&gt;
&lt;h3&gt;
  
  
  4. The fine-tune (Tinker)
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Qwen/Qwen3.5-4B&lt;/code&gt; on Tinker, LoRA rank 16, 2,924 items, ~300 steps at batch 8, learning&lt;br&gt;
rate 1e-4. The verdict is a &lt;strong&gt;single token&lt;/strong&gt;, and the score is not a sampled opinion — it&lt;br&gt;
is the softmax over two logprobs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;lp_now&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;compute_logprobs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt_ids&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;tok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NOW&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)])[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;lp_wait&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;compute_logprobs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt_ids&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;tok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WAIT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)])[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;p_now&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lp_now&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lp_now&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lp_wait&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One forward pass per item, no sampling noise, and a real probability that can be ranked&lt;br&gt;
and thresholded. Sampling five times and counting votes would have been easier and worse.&lt;/p&gt;

&lt;p&gt;This is where Tinker earned its place. The category brief asks for "a specific task and a&lt;br&gt;
clear improvement over a baseline", and the baseline is the &lt;em&gt;same model, same prompt, same&lt;br&gt;
decoding position, no LoRA&lt;/em&gt; — which isolates the fine-tune rather than comparing it to&lt;br&gt;
nothing.&lt;/p&gt;
&lt;h3&gt;
  
  
  5. The fuse is a Temporal timer, not a &lt;code&gt;sleep()&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;workflow.wait_condition(lambda: self._rescued, timeout=timedelta(seconds=fuse))&lt;/code&gt; inside a&lt;br&gt;
Temporal workflow gives me one wait with two ways out: an &lt;code&gt;on_attention&lt;/code&gt; signal or the&lt;br&gt;
timer. The timer lives on the server, so it does not die with my laptop. The signal is&lt;br&gt;
buffered by the server, so it is not lost if I am not running. Both of those are&lt;br&gt;
demonstrated above rather than asserted.&lt;/p&gt;

&lt;p&gt;And the deterministic layer decides what the model's number is &lt;em&gt;allowed&lt;/em&gt; to mean:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;nothing is deleted, moved, or hidden forever;&lt;/li&gt;
&lt;li&gt;every deferral gets a fuse;&lt;/li&gt;
&lt;li&gt;one outside signal unseals immediately;&lt;/li&gt;
&lt;li&gt;the ledger is append-only, so any verdict can be re-read and argued with.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The model supplies a probability. Code decides consequences. I do not want a language&lt;br&gt;
model to be the last thing standing between me and losing an issue.&lt;/p&gt;
&lt;h2&gt;
  
  
  What went wrong
&lt;/h2&gt;

&lt;p&gt;I am keeping this section because the failures were the actual work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My loss was 0.0000 for the first two hundred steps, and I almost shipped it.&lt;/strong&gt; The&lt;br&gt;
weights tensor has to align with the &lt;em&gt;labels&lt;/em&gt;, not the inputs. I wrote:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;full&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;full&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;
&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="c1"&gt;# wrong: w[:-1] drops the only 1.0, so every supervised weight was 0
&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every weight was zero. The model was training on nothing, and because Tinker reports&lt;br&gt;
&lt;code&gt;loss:sum&lt;/code&gt; already weighted, the metric was a perfectly plausible &lt;code&gt;0.0000&lt;/code&gt; instead of an&lt;br&gt;
error. The fix is one character of slicing — &lt;code&gt;w[1:], not w[:-1]&lt;/code&gt; — and the loss at&lt;br&gt;
initialisation became 0.736, which is &lt;code&gt;ln(2)&lt;/code&gt;: dead-on chance for a coin flip, exactly&lt;br&gt;
what an untrained model should score. &lt;strong&gt;If your loss starts at zero, you are not&lt;br&gt;
learning something impressive, you are not learning anything.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"LATER" is two tokens.&lt;/strong&gt; I wanted the verdict vocabulary to be &lt;code&gt;NOW&lt;/code&gt; / &lt;code&gt;LATER&lt;/code&gt;, which&lt;br&gt;
reads better in a blog post than &lt;code&gt;NOW&lt;/code&gt; / &lt;code&gt;WAIT&lt;/code&gt;. It tokenises to &lt;code&gt;[43, 18577]&lt;/code&gt;, which&lt;br&gt;
makes the two-logprob comparison meaningless. The verdict had to become &lt;code&gt;WAIT&lt;/code&gt; because&lt;br&gt;
the tokenizer said so, not because I preferred it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Qwen3.5-4B is a reasoning model, and it wanted to think first.&lt;/strong&gt; With a normal&lt;br&gt;
generation prompt it emitted &lt;code&gt;"Thinking Process:\n\n1."&lt;/code&gt; before any verdict, which put my&lt;br&gt;
scored token nowhere near the answer position: the initial loss was &lt;strong&gt;17.5 nats per&lt;br&gt;
token&lt;/strong&gt; (p ≈ 2.5e-8). The fix was &lt;code&gt;apply_chat_template(..., enable_thinking=False)&lt;/code&gt;,&lt;br&gt;
which produces a clean single-token &lt;code&gt;NOW&lt;/code&gt;. I apply the same setting to the base arm, so&lt;br&gt;
the comparison stays fair — the only difference between arms is the LoRA.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The base model, given one token and no thinking, is essentially at chance.&lt;/strong&gt; AUC 0.516.&lt;br&gt;
Not "slightly worse than the fine-tune" — 0.5 is a coin flip. Almost every bit of ranking&lt;br&gt;
signal in this product comes from the fine-tune, which is the least flattering possible&lt;br&gt;
finding for a post that is mostly about a fine-tune, and it is the true one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;Because the thing being trained is &lt;strong&gt;my judgement about my own attention&lt;/strong&gt;, and that is&lt;br&gt;
not a resource I want to rent back.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The decision is mine, and it stays on my disk.&lt;/strong&gt; The ledger is an append-only local
file. Deferring an item is recorded with the probability that produced it, so six weeks
later I can find every item I was wrong about and see what the model believed at the
time. That audit loop is impossible if the scoring happens inside someone's API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The training set is my behaviour.&lt;/strong&gt; A closed model cannot be fine-tuned on which
items &lt;em&gt;I&lt;/em&gt; actually opened. Today the fine-tune learns one maintainer's stream in a few
minutes on Tinker; the same pipeline pointed at a personal inbox is the same code with
a different &lt;code&gt;decisions.csv&lt;/code&gt;. With a hosted model I would be sending my reading habits
out to be trained on someone else's roadmap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It costs nothing per decision.&lt;/strong&gt; 731 items × 2 forward passes, on a laptop-class
model, with no card on file. A tool that charges per item is a tool I ration, and a tool
I ration is a tool that does not do the boring middle of the job.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I can swap the model.&lt;/strong&gt; The whole interface is one probability. Swapping in a 1B for
speed, or a bigger model for the disagreements, is a config line — which is the point of
the two-logprob design. &lt;code&gt;compute_logprobs&lt;/code&gt; is not a Qwen feature; it is an open-weights
feature.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And one honest concession: a 397B model, allowed to reason for a paragraph per item, would&lt;br&gt;
probably beat this 4B on the hard cases. It costs about a thousand times as much per&lt;br&gt;
decision and cannot run with no internet, which is exactly the trade this category is&lt;br&gt;
about. I would rather have a 4B that I can run on the train.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Lazy Payoff
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;73 of 731 real issues never reached me&lt;/strong&gt;, at a cost of 11 mis-deferrals out of the 235 that actually needed a human within 24 hours.&lt;/li&gt;
&lt;li&gt;The filter is a &lt;em&gt;ranking&lt;/em&gt;, so the budget is a dial: the same model defers 64 items at a 3% miss budget, 73 at 5%, and 152 at 10%. The published numbers are the 5% point.&lt;/li&gt;
&lt;li&gt;A perfect ranker could defer 67.9% of this stream; we reach 10.0%. Useful, and with headroom left.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The more useful payoff is not the count of issues. It is that the morning surface is one&lt;br&gt;
card. My backlog does not shrink into a longer list that I scroll and feel bad about; the&lt;br&gt;
731 items are 731 decisions already made, most of them "no", each one written down with a&lt;br&gt;
reason and a fuse. The laziness being rewarded is not "the AI does my work" — it is&lt;br&gt;
"somebody, finally, did the triage, and it was a 4B model that cost nothing and cannot&lt;br&gt;
send my data anywhere".&lt;/p&gt;

&lt;h2&gt;
  
  
  What this does not do
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Three repositories, one 5-day held-out window.&lt;/strong&gt; This is a demonstration, not a
benchmark. The 67.9% ceiling is a property of these four repos, and a repository with a
different maintainer cadence will move it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The held-out window is 71% microsoft/vscode&lt;/strong&gt; (518 of 731 items) while training skews to
next.js, rust and cpython (956/881/818 against 269 for vscode). A temporal split over a
fast-moving repo set drifts in composition, so the headline number is dominated by one
project's triage rhythm. I would want a per-repo breakdown before trusting this on a new
repository, and I have not done one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"A maintainer touched it" is not "it mattered."&lt;/strong&gt; The label is a proxy. Some ignored
issues genuinely mattered and some handled ones did not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A mis-deferral is the expensive error.&lt;/strong&gt; The policy is asymmetric on purpose: the 5%
budget buys patience rather than quiet. At the 3% budget the filter defers less and
earns less.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One queue, one shape.&lt;/strong&gt; The extraction step assumes item-shaped text. It is not
general triage and I would not pretend otherwise.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  My Agent Session
&lt;/h2&gt;

&lt;p&gt;I built this with an agent and the reasoning sits in the artefacts rather than in a&lt;br&gt;
transcript: &lt;code&gt;eval/policy.py&lt;/code&gt; is the judge, &lt;code&gt;app/workflows.py&lt;/code&gt; is the guarantee, and every&lt;br&gt;
number above is regenerated by &lt;code&gt;app/build_site.py&lt;/code&gt; from &lt;code&gt;data/&lt;/code&gt; and &lt;code&gt;eval/&lt;/code&gt;. The &lt;code&gt;WAIT&lt;/code&gt;&lt;br&gt;
token, the &lt;code&gt;w[1:]&lt;/code&gt; fix and the &lt;code&gt;enable_thinking=False&lt;/code&gt; fix are all visible in the repo's&lt;br&gt;
history as they were found.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Tinker&lt;/strong&gt; — the fine-tuned arm is &lt;code&gt;Qwen/Qwen3.5-4B&lt;/code&gt; LoRA on Tinker, scored
against the same base model with the same prompt and decoding position. The improvement
over that baseline is the entire result.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Temporal&lt;/strong&gt; — every deferral is a &lt;code&gt;SealWorkflow&lt;/code&gt; with a durable
&lt;code&gt;wait_condition&lt;/code&gt; timer plus an &lt;code&gt;on_attention&lt;/code&gt; signal; the demo kills the worker with
&lt;code&gt;kill -9&lt;/code&gt; mid-fuse and delivers a signal while no worker exists.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Confidant: a 1B Gemma drafts a therapist's session notes on her own laptop, and nothing leaves the room</title>
      <dc:creator>memeshe</dc:creator>
      <pubDate>Tue, 13 Oct 2026 18:19:38 +0000</pubDate>
      <link>https://dev.to/memeshe/confidant-a-1b-gemma-drafts-a-therapists-session-notes-on-her-own-laptop-and-nothing-leaves-the-5fnp</link>
      <guid>https://dev.to/memeshe/confidant-a-1b-gemma-drafts-a-therapists-session-notes-on-her-own-laptop-and-nothing-leaves-the-5fnp</guid>
      <description>&lt;p&gt;It is 9pm, and a solo therapist still owes the day a set of notes. She has two options. Type them — about twenty minutes every day, after the last client. Or pay $69–114 a month for a cloud "AI scribe" that records the session, which her clients did not consent to.&lt;/p&gt;

&lt;p&gt;I built the third option. &lt;strong&gt;Confidant&lt;/strong&gt; lets her speak a twenty-second recap between clients; a small open-weights Gemma model running on her own laptop drafts the structured note — Subjective, Objective, Assessment, Plan, next focus — and a deterministic guardrail checks every sentence against the words she actually said. A sentence whose words cannot be found in the recap is never filed. It is shown struck through, under "refused by the guardrail".&lt;/p&gt;

&lt;p&gt;This is my entry for the Hacktoberfest Open-Source AI Challenge, Week 2: Side Hustle. It runs on a laptop with 8 threads, 6.7 GB of RAM, no GPU, no CUDA. Every number below was measured on that machine, and every claim maps to a file, a command, or a run in the repo.&lt;/p&gt;

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

&lt;p&gt;Confidant is a local-first session-note drafter for one-person practices: therapists, counsellors, physiotherapists, social workers.&lt;/p&gt;

&lt;p&gt;The flow is short. She presses record and speaks a messy recap the way she would to a colleague — "Maya came in flat today, said the journaling stopped two weeks ago, we practised the grounding exercise, she'll try it twice before Thursday, and I want to watch the sleep thing next time." Speech-to-text runs locally through faster-whisper. The transcript goes to Gemma 3 1B in Ollama on the same machine, which drafts the note in five fields — the S/O/A/P shape clinicians already write, plus a next-session focus. Each sentence carries the exact transcript span that supports it; hovering a line highlights that span in the side rail. She edits inline, signs, and moves on. The interface is a document, not a dashboard — the note is the product.&lt;/p&gt;

&lt;p&gt;Three decisions do most of the work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The note is a view over the transcript.&lt;/strong&gt; Every claim stores a verbatim quote from the recap. Grounding is a data-model invariant, not a prompt instruction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unsupported sentences are refused, not softened.&lt;/strong&gt; The guardrail in &lt;code&gt;daemon/src/validator.rs&lt;/code&gt; normalises a sentence's quote and searches for it in the transcript. If it cannot find it, the sentence never reaches the filed note. The model's failure mode is "this line is missing, and here is what it tried to write" — never a plausible clinical sentence that nobody said.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Risk flags are rules, not model output.&lt;/strong&gt; A small, auditable rule set over her own words decides the risk chips. The type the note generator returns has no risk field at all: the model is never asked for a risk level.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What it does not do: no diagnosis, no advice, no EHR integration, no multi-user sync, no mobile app, no cloud. The Rust daemon (Axum) binds &lt;code&gt;127.0.0.1&lt;/code&gt; only, and mutating routes require a run token injected into the page the daemon serves, so a random website cannot post a transcript into the local store. The store is SQLite on her disk. Audio is deleted after transcription — the delete happens before the transcription result is even parsed. Export is Markdown or a print-ready sheet; history stays in the local database.&lt;/p&gt;

&lt;p&gt;There is one honest escape hatch: remote mode, opt-in, for hardware that cannot hold a model. It sends the transcript to any OpenAI-compatible endpoint and says so loudly in the UI. It is off by default. In local mode, &lt;code&gt;pipeline::assert_policy&lt;/code&gt; refuses — a hard error, not a warning — any Ollama base URL that is not loopback, because that single misconfiguration would make the privacy claim false.&lt;/p&gt;

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

&lt;p&gt;Live demo: &lt;a href="https://uconfidant.tech" rel="noopener noreferrer"&gt;https://uconfidant.tech&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The demo on Render is read-only. It replays real recorded runs of the pipeline on the synthetic corpus, so you can watch the whole flow without installing anything: recap in, drafted note out, and at least one sentence struck through where the guardrail refused it. Underneath the replay is one arm of the evaluation table from the next section — the no-model baseline over the full corpus, rendered from the committed results file, the same one a reader can open.&lt;/p&gt;

&lt;p&gt;The two-minute walkthrough is the same product recorded on this laptop — the paste, the CPU draft, the guardrail refusing one sentence it could not trace, then the deployed page answering &lt;code&gt;403&lt;/code&gt; on every write:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/Qu9942_FO8I" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;The recording is also committed as &lt;code&gt;docs/confidant-demo.mp4&lt;/code&gt; (&lt;a href="https://raw.githubusercontent.com/memeshee/confidant/main/docs/confidant-demo.mp4" rel="noopener noreferrer"&gt;direct download&lt;/a&gt;), if you would rather download it than stream.&lt;/p&gt;

&lt;p&gt;The local demo is the one that matters, and it works with the wifi off:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the app the daemon serves, and press &lt;em&gt;Record instead&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Talk for twenty seconds about a session the way you would to a colleague.&lt;/li&gt;
&lt;li&gt;Stop. The note drafts on the machine; each line shows the span it came from.&lt;/li&gt;
&lt;li&gt;Find the struck-through line — that is the guardrail refusing something the model invented.&lt;/li&gt;
&lt;li&gt;Export to print-ready HTML. The footer shows &lt;code&gt;0.0000 USD/note&lt;/code&gt;, the tokens/sec, and the model name.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The landing page and the docs live at &lt;a href="https://uconfidant.tech" rel="noopener noreferrer"&gt;https://uconfidant.tech&lt;/a&gt;, the read-only demo deployment itself — one URL, no separate marketing site.&lt;/p&gt;

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

&lt;p&gt;Repo: &lt;a href="https://github.com/memeshee/confidant" rel="noopener noreferrer"&gt;https://github.com/memeshee/confidant&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Everything is in there: the Rust daemon (&lt;code&gt;daemon/src/&lt;/code&gt;), the statically exported Next.js UI (&lt;code&gt;ui/&lt;/code&gt;), the prompts shared by the product and the eval harness (&lt;code&gt;prompts/&lt;/code&gt;), the synthetic corpus and the harness (&lt;code&gt;eval/&lt;/code&gt;), and the footprint documents (&lt;code&gt;docs/&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Three commands are worth running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo &lt;span class="nb"&gt;test&lt;/span&gt;                                   &lt;span class="c"&gt;# 20 tests: guardrail, provider policy, exports, store, line parser, eval-table determinism&lt;/span&gt;
python3 &lt;span class="nb"&gt;eval&lt;/span&gt;/run_eval.py &lt;span class="nt"&gt;--arm&lt;/span&gt; rules-only    &lt;span class="c"&gt;# the no-model baseline&lt;/span&gt;
scripts/cleanup.sh                           &lt;span class="c"&gt;# dry run: lists everything this build added to a machine&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;cargo test&lt;/code&gt; is at 20 passing tests, including one that refuses a fabricated medication dose. &lt;code&gt;scripts/cleanup.sh&lt;/code&gt; is dry-run verified: 4.3 GB removable, of which 958 MB is the model, and one command reverses everything this build added to a machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;The daemon is Rust (Axum + tokio + rusqlite) and serves the statically exported Next.js UI plus the local API on &lt;code&gt;127.0.0.1&lt;/code&gt;. Inference is Ollama on loopback; transcription is faster-whisper in a sidecar process; the store is SQLite. The prompts are compiled into the daemon and read by the eval harness from the same files, so the published numbers measure the shipped configuration, not a copy of it.&lt;/p&gt;

&lt;p&gt;Then the measurements, because "runs on your machine" is a claim until it has a number attached:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gemma 3 1B (QAT, 1.0 GB on disk, Ollama).&lt;/strong&gt; The shipped default. On this CPU it drafts a note in one to two minutes: the recorded run below took 136 s for a 250-word recap at 3.6 tokens/sec, a shorter one came back in 74 s, and across the corpus the raw arm averages 2.9 tokens/sec. The output is thin — a handful of sentences — and it is the default anyway, because it is the one that runs with the wifi off.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemma 4B (3.2 GB weights + 0.85 GB vision tower).&lt;/strong&gt; 81 seconds just to load, then 0.26 tokens/sec. A note would take about four minutes. I deleted it from disk. Whatever the download page implies, it is not a small-laptop model, and I would rather say that than ship it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemma 3 27B (open weights, hosted via Nebius Token Factory, opt-in remote mode).&lt;/strong&gt; 16 sentences in 9.9 s, all 16 traceable, 0 refused — 549 prompt tokens + 505 completion tokens. That is the trade in full: better notes, faster, and the transcript leaves the machine. Hosted endpoints also do not enforce JSON schemas (I probed), so the remote path uses a flat line protocol parsed in Rust instead of the schema the local path uses.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The evaluation has five measured rows: a rules-only baseline (deterministic sentence-splitting by trigger words, no model), the local 1B raw and the same 1B behind the guardrail, and the hosted 27B both raw over the full corpus and behind the guardrail. The corpus is synthetic and frozen: &lt;strong&gt;90 recaps, 760 annotated facts and 427 planted distractors&lt;/strong&gt; — recaps and fact lists written for this project, no real client data anywhere — and the harness is &lt;code&gt;eval/run_eval.py&lt;/code&gt;; results are committed under &lt;code&gt;eval/results/&lt;/code&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Arm&lt;/th&gt;
&lt;th&gt;Fact recall&lt;/th&gt;
&lt;th&gt;Facts in right section&lt;/th&gt;
&lt;th&gt;Distractor rate&lt;/th&gt;
&lt;th&gt;Unsupported claims&lt;/th&gt;
&lt;th&gt;Median latency&lt;/th&gt;
&lt;th&gt;Cost per note&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;rules-only baseline — 90 recaps&lt;/td&gt;
&lt;td&gt;99.2%&lt;/td&gt;
&lt;td&gt;40.0%&lt;/td&gt;
&lt;td&gt;11.0%&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;td&gt;~0 ms&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;local Gemma 1B, raw — 8 recaps&lt;/td&gt;
&lt;td&gt;37.5%&lt;/td&gt;
&lt;td&gt;25.0%&lt;/td&gt;
&lt;td&gt;2.6%&lt;/td&gt;
&lt;td&gt;69.0%&lt;/td&gt;
&lt;td&gt;201.6 s&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;local Gemma 1B &lt;strong&gt;+ guardrail&lt;/strong&gt; — 8 recaps&lt;/td&gt;
&lt;td&gt;20.8%&lt;/td&gt;
&lt;td&gt;6.9%&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;td&gt;74.2 s&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hosted Gemma 27B, raw — 90 recaps&lt;/td&gt;
&lt;td&gt;72.8%&lt;/td&gt;
&lt;td&gt;45.9%&lt;/td&gt;
&lt;td&gt;19.7%&lt;/td&gt;
&lt;td&gt;1.5%&lt;/td&gt;
&lt;td&gt;5.1 s&lt;/td&gt;
&lt;td&gt;provider-priced&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hosted Gemma 27B &lt;strong&gt;+ guardrail&lt;/strong&gt; — 8 recaps&lt;/td&gt;
&lt;td&gt;87.5%&lt;/td&gt;
&lt;td&gt;47.2%&lt;/td&gt;
&lt;td&gt;13.2%&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;td&gt;12.4 s&lt;/td&gt;
&lt;td&gt;provider-priced&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Fact recall: annotated facts whose keywords appear anywhere in the note. Facts in right section: the same facts landing in the section they belong to — the metric that separates a note from a copy. Distractor rate: planted, plausible-but-absent statements the note wrote anyway. Unsupported claims: note sentences whose quote cannot be found in the transcript; the guardrail arms read 0% because refusing exactly those is what the guardrail does — that column measures the guardrail, not luck. Brevity (note words ÷ transcript words, committed in the results): rules-only 0.99, local 1B 0.44, hosted 27B 0.68 raw and 0.72 behind the guardrail — a note, not a transcript. Every cell is a real run committed under &lt;code&gt;eval/results/&lt;/code&gt;, median over the recaps noted per row.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The row that matters most is the third one.&lt;/strong&gt; Behind the guardrail, the 1B keeps about two sentences a note and refuses 1.75 more per note on top of that; three of those eight recaps filed an empty note, because nearly everything the model wrote failed the trace check. That is the honest cost of the shipped default being the model that fits: at 1B the guardrail is what stands between the clinician and a mostly-invented note, and its answer is often "nothing to file". On hardware that can hold a bigger model, the same code path turns 87.5% recall at 0% unsupported — which is why the model picker exists and why the hosted arm is measured through the same guardrail rather than around it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The first version of this table was wrong, and the way it was wrong is worth reporting.&lt;/strong&gt; My first metric was fact recall: does the note contain the facts. The rules-only baseline scored well over 98% — near-perfect, for a system with no model in it. That was implausible enough that I opened a result, and there it was: the baseline dumps the transcript into every section, so every keyword trivially matched. It was not writing a note; it was copying one. I rewrote the metrics to measure where each fact lands (section placement) and how long the note is relative to the transcript (brevity). The baseline immediately read as what it is: 40% of facts in the right section, and a note exactly as long as the transcript (0.99×) — while the hosted 27B's note is 0.68×, which is what a note looks like. A baseline that looks perfect is usually a measurement bug, not a miracle, and a recall-only metric would have let the no-model arm win the table.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;For this product the answer is structural, not philosophical.&lt;/p&gt;

&lt;p&gt;A cloud scribe cannot be this product. Its architecture requires shipping a client's words to somebody else's server — the one thing this product promises never happens. That is a consent problem, not a pricing problem: her clients did not agree to be recorded, and no amount of vendor diligence changes that. The local, open-weights model is the only version of this product that can exist.&lt;/p&gt;

&lt;p&gt;Open matters for the rest of it too:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;She can see what the model was told.&lt;/strong&gt; The prompts are files in the repo, not a vendor's secret.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;She can re-run my numbers.&lt;/strong&gt; The harness, the corpus, and the results are in the repo. Nobody has to trust this table.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The guardrail is code, not a policy page.&lt;/strong&gt; It runs locally, on her machine, and it is unit-tested. Auditable refusal beats promised moderation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;$0.0000 per note&lt;/strong&gt; is the difference between a tool that exists for a solo practice and one that does not. A per-note meter is a subscription for someone who already pays one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is a smaller version of this, too. The model sizes that make the product possible — a 1B that fits in a gigabyte — exist because open weights were released and measured in public. The 4B measurement above (81 seconds to load, 0.26 tokens/sec, deleted) is the kind of thing that only gets said out loud in an open ecosystem; a closed vendor's size chart does not include it.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Agent Session
&lt;/h2&gt;

&lt;p&gt;I built this as a series of numbered increments, one at a time, with an agent doing the work in each, under a rule that no increment counts until it ends in a real run. The session log — timestamps, decisions, and what broke — is committed at &lt;code&gt;docs/CONFIDANT-PLAN.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The rule earned itself. The unit tests were green while the router was broken: axum 0.8 changed path parameters from &lt;code&gt;:id&lt;/code&gt; to &lt;code&gt;{id}&lt;/code&gt;, and the daemon panicked when the first real request hit a parameterised route. Unit tests never touch the router, so nothing in the test suite could have caught it. Fixing it meant renaming the path parameters; the daemon has served &lt;code&gt;/api/health&lt;/code&gt; in local mode since.&lt;/p&gt;

&lt;p&gt;The agent also produced one measurement I threw away: a thread-scaling benchmark run while the 4B model was still resident in RAM. Same machine, different load — not valid evidence. The clean re-run is committed at &lt;code&gt;eval/results/bench-threads-clean.log&lt;/code&gt;. That habit — discard the contaminated number, re-run, commit the artifact — is most of what makes the numbers here worth reading.&lt;/p&gt;

&lt;p&gt;The standing rule for the build was never invent evidence. It is why the metric bug above got fixed instead of hidden, why the third row of that table stays in the post although it flatters nothing, and why every cell is a run I can point at instead of a number I liked the look of. The decisions that mattered stayed with me: what ships as the default, and what the product refuses to do. The post itself was drafted with agent assistance, and every number in it was checked against an artifact before it went in.&lt;/p&gt;

&lt;h2&gt;
  
  
  My First User
&lt;/h2&gt;

&lt;p&gt;No clinician was available within the five-day window, so this section carries no quote and no testimonial; the evidence that someone would actually pay for this is the measured evaluation instead — the baseline that files the transcript as a note, the 1B that keeps about two sentences a note once the guardrail has refused the rest, and the 27B comparison that produces sixteen. All of it is re-runnable from the repo with &lt;code&gt;python3 eval/run_eval.py&lt;/code&gt;. A fabricated quote would have been easy to write, and it would have made every other number in this post suspect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Best Use of Gemma.&lt;/strong&gt; Gemma 3 1B QAT is the shipped default runtime, running on CPU with no GPU: on this laptop it drafts a note in one to two minutes (3.6–4.8 tokens/sec; median 74 s end to end behind the guardrail across the corpus block above). Its honest limits are in that table too — at 1B, raw, 69% of its sentences cannot be traced to the recap, and the guardrail refuses most of them — which is exactly why the same product, pointed at Gemma 3 27B (open weights, hosted, opt-in), reads 87.5% recall at 0% unsupported through the same guardrail. The model picker lists whatever is installed on the machine, with real sizes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best Use of Render.&lt;/strong&gt; The public read-only demo of real recorded runs, plus the evaluation table, is hosted on Render at &lt;a href="https://uconfidant.tech" rel="noopener noreferrer"&gt;https://uconfidant.tech&lt;/a&gt; (fallback: confidant-demo.onrender.com; service &lt;code&gt;srv-db73srid0e5s73avf6i0&lt;/code&gt;, free plan, Docker runtime). Every mutating route answers 403 there — it is a replay, not a live service.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best Use of a .tech Domain.&lt;/strong&gt; The project's landing page and docs live on a .tech domain claimed through Hacktoberfest: &lt;a href="https://uconfidant.tech" rel="noopener noreferrer"&gt;https://uconfidant.tech&lt;/a&gt;. The domain points at the read-only Render deployment, so the landing page is the product demo itself — TLS issued, DNS on the registrar's own nameservers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not claimed, and why — now with runs behind the refusal.&lt;/strong&gt; Sentry tracing is wired, and I checked the claim instead of asserting it: pointing the daemon at a local sink (&lt;code&gt;eval/sentry_envelope_sink.py&lt;/code&gt;) shows the exact envelope it posts — &lt;code&gt;note.draft&lt;/code&gt;, spans &lt;code&gt;{cost_usd: 0.0, duration_ms: 73844, output_tokens: 347}&lt;/code&gt; and &lt;code&gt;{transcript_chars: 600}&lt;/code&gt;. Metadata only; no clinical text, verifiable without an account. It stays unclaimed because metadata tracing is not what the category asks for. Tinker is wired too: a 40-step LoRA on &lt;code&gt;Qwen/Qwen3.5-4B&lt;/code&gt; over the note-structuring task, trained on the deterministic floor's outputs and scored on six held-out recaps with the same scorer the committed eval uses (&lt;code&gt;eval/results/tinker-lora-20261014-002815.json&lt;/code&gt;). The loss fell from 0.082 to 0.0015 per supervised token — and the tuned arm still did not dominate the floor: two runs of the same config landed on either side of it on section placement (2.17 and 3.17 of 7 against the floor's 2.50), while producing unsupported claims (0.33 and 0.5 per note) that the floor never produces at all. A first run that loses is the honest state of the art here; filing it as a win would be the lie this whole project exists to avoid.&lt;/p&gt;

&lt;p&gt;Confidant is a small thing: one therapist, one laptop, one recap, and one note that can prove where every sentence came from. That it can exist at all is a property of open models. That it refuses to write what she did not say is the property I care about most.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
    </item>
    <item>
      <title>FieldDay: the allergy-smart grass window</title>
      <dc:creator>memeshe</dc:creator>
      <pubDate>Tue, 06 Oct 2026 11:24:19 +0000</pubDate>
      <link>https://dev.to/memeshe/fieldday-the-allergy-smart-grass-window-30jn</link>
      <guid>https://dev.to/memeshe/fieldday-the-allergy-smart-grass-window-30jn</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week1-2026-10-05"&gt;Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;FieldDay tells allergy families the single best hour to go outside today — then gets out of the way. Parents of sneezy kids currently check three apps (weather, AQI, pollen) and guess; guess wrong and the evening is miserable. FieldDay fuses forecast + pollen + YOUR symptom history into one 0–100 score per daylight hour, explains the pick in warm parent-language, and fits in one mobile screen. Sixty seconds, then screen off — that's the whole point of Touch Grass.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9wh7aqnpjfj91vk8htsy.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9wh7aqnpjfj91vk8htsy.jpg" alt=" " width="800" height="342"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The whole point: more days like this. (Rick Obst, CC BY 2.0, via Wikimedia Commons)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu8yw6xhqk5do9up5cxbj.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu8yw6xhqk5do9up5cxbj.jpg" alt=" " width="760" height="1014"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Beautiful — and exactly what triggers the sneezes. (Peter Salanki, CC BY 2.0, via Wikimedia Commons)&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Live:&lt;/strong&gt; &lt;a href="https://fieldday-r66v.onrender.com" rel="noopener noreferrer"&gt;https://fieldday-r66v.onrender.com&lt;/a&gt; (free Render tier, PWA — installable, works from the home screen)&lt;/p&gt;

&lt;p&gt;Tested live Oct 6, Hanoi, straight from the production API: best window 08:00 local (score 76/100, personal flare risk 0.227 via TabPFN on 48 rows), worst 35/100 later in the week. Every hour is labeled with the model that scored it. Outdoor verification with photos lands this week — updating this post.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://github.com/memeshee/fieldday" rel="noopener noreferrer"&gt;https://github.com/memeshee/fieldday&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;FastAPI + vanilla JS, one-click deploy via &lt;code&gt;render.yaml&lt;/code&gt;, 6 smoke tests with CI.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;Open-source AI is the core, not a garnish:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;TabPFN (Prior Labs tabular foundation model)&lt;/strong&gt; predicts personal flare risk fitted on YOUR symptom log — not a generic county pollen number. Your browser gets a random anonymous ID, so your taps train your forecast and nobody else's. Lightweight &lt;code&gt;tabpfn-client&lt;/code&gt; so Render's free tier never OOMs; self-hosters can swap in local &lt;code&gt;tabpfn&lt;/code&gt; with zero code changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemma (open weights)&lt;/strong&gt; writes the coach note. Chain: local Ollama &lt;code&gt;gemma3:1b&lt;/code&gt; → hosted Gemma via Gemini API → labeled template fallback. The UI always names which source wrote the note — no silent fallbacks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free keyless data:&lt;/strong&gt; Open-Meteo forecast + pollen, geocoding included.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Hardest part: latency. v1 called TabPFN once per daylight hour — 7s × 24, guaranteed timeout. Fixed by fitting once and batch-predicting all hours in one call, then caching the predictor keyed on the training-data fingerprint: 37s cold → 1.3s warm in prod. Second hardest: Open-Meteo 429-throttles Render's shared IP, which once blanked the whole demo — now retry + 30-min upstream cache + an honestly-labeled MET Norway backup feed. Third: labeling every AI output with its provenance (&lt;code&gt;risk_source&lt;/code&gt;, coach source), including when it's "just" the baseline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;Symptom logs are health-adjacent — they must never train someone else's closed model or require an account. Open weights + a tabular foundation model give a personal forecast with no account, no tracking, no per-call price tag: TabPFN inference sees anonymized rows and never trains on them, Gemma runs local-first. $0 to run, any model swappable. Closed APIs would make this a privacy compromise and a billing meter; open makes it a tool parents can trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Agent Session
&lt;/h2&gt;

&lt;p&gt;Built pair-programming with an AI agent (Hermes) — roughly 280 tool iterations across one session: real TabPFN verification, predictor cache, per-user logs, hosted-Gemma chain, PWA shell, upstream-resilience fallback, this post. No DevRelay link (session ran outside DevRelay); the full build log lives in the repo's commit history.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Best Use of TabPFN&lt;/li&gt;
&lt;li&gt;Best Use of Gemma&lt;/li&gt;
&lt;li&gt;Best Use of Render&lt;/li&gt;
&lt;li&gt;Overall&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
    </item>
    <item>
      <title>My Mum Keeps Getting Scam Texts. I Built Her a Telegram Shield.</title>
      <dc:creator>memeshe</dc:creator>
      <pubDate>Sun, 04 Oct 2026 19:26:19 +0000</pubDate>
      <link>https://dev.to/memeshe/my-mum-keeps-getting-scam-texts-i-built-her-a-telegram-shield-1j1a</link>
      <guid>https://dev.to/memeshe/my-mum-keeps-getting-scam-texts-i-built-her-a-telegram-shield-1j1a</guid>
      <description>&lt;h1&gt;
  
  
  My Mum Keeps Getting Scam Texts. I Built Her a Telegram Shield.
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;This is a submission for the Hacktoberfest Weekend Challenge: Build for a Friend.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live:&lt;/strong&gt; &lt;a href="https://t.me/scamsshield_bot" rel="noopener noreferrer"&gt;t.me/scamsshield_bot&lt;/a&gt; · &lt;a href="https://scamshield-g14e.onrender.com" rel="noopener noreferrer"&gt;Dashboard&lt;/a&gt; · &lt;a href="https://github.com/memeshee/scamshield" rel="noopener noreferrer"&gt;Repo&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The friend
&lt;/h2&gt;

&lt;p&gt;My mum gets parcel-scam SMSes in Thai almost weekly — "พัสดุถูกระงับ, ยืนยันตัวตนด่วน" with a &lt;code&gt;bit.ly&lt;/code&gt; link, fake bank warnings, the works. She forwards them to me, I tell her "don't click that," and the cycle repeats. She's not going to learn to read headers. So I built the thing that reads them for her: &lt;strong&gt;ScamShield&lt;/strong&gt;. She forwards any suspicious message to a Telegram bot, and in seconds gets back a verdict card — SCAM / SUSPICIOUS / LIKELY SAFE — with reasons in Thai first, English second, and what to do next.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it actually does
&lt;/h2&gt;

&lt;p&gt;Send anything — an SMS forward, a link, a wallet address — and the bot runs a triage pipeline:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Signal extraction&lt;/strong&gt; — links, crypto addresses, phone numbers, urgency phrases ("ด่วน", "airdrop", "verify now"), impersonation hits (banks, Binance, Shopee, "ตำรวจ"…).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live web grounding (SerpApi)&lt;/strong&gt; — the suspicious domain gets searched for scam/fraud reports, so the verdict cites the web, not vibes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On-chain probe&lt;/strong&gt; — wallet addresses are checked against public RPCs: contract code (drainer pattern) vs plain wallet vs fresh/empty.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Score&lt;/strong&gt; — transparent point rules, capped at 100. ≥70 SCAM, ≥40 SUSPICIOUS, else LIKELY SAFE.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explanation by an open-weight model (Gemma)&lt;/strong&gt; — two short sentences, Thai then English, written for a non-technical reader. The model explains; the rules decide. If the model is unreachable, the verdict still lands, honestly labelled rule-based.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Real outputs from the pipeline (cards exactly as the bot sends them):&lt;/p&gt;

&lt;p&gt;🚨 Thai parcel scam → SCAM 75&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;🚨 อันตราย: น่าจะเป็นมิจฉาชีพ (SCAM, 75/100)

เหตุผล / Why:
• pressure tactics: ด่วน, ระงับ, ยืนยันตัวตน
• impersonates: พัสดุ
• suspicious link (shortener / risky domain)

ลิงก์ / Links:
`http://bit.ly/kerry-th-99`

เว็บพูดถึง / Web:
• bit.ly url scan | Free Url Scanner &amp;amp;amp; Phishing Detection — checkphish.bolster.ai/…

🤖 ข้อความนี้ดูน่าสงสัยเพราะใช้คำเร่งรีบและลิงก์ที่ดูไม่น่าเชื่อถือ อย่ากดลิงก์หรือกรอกข้อมูลส่วนตัวเด็ดขาด
This message is suspicious because it uses urgent language and an untrustworthy link. Do not click the link or enter any personal information.
_(model: gemma-4-26b-a4b-it)_

ทำอย่างไร / What to do:
• อย่าโอน อย่าให้ OTP — Do not send money or OTP
• ส่งมาให้ลูกดูก่อนเสมอ
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;✅ Family check-in → LIKELY SAFE 0&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✅ ดูปลอดภัย (แต่ยังระวังไว้) (LIKELY SAFE, 0/100)

เหตุผล / Why:
• no pressure tactics, links, or crypto addresses found

🤖 ข้อความนี้ดูปลอดภัยเพราะเป็นแค่การถามไถ่ทั่วไปและไม่มีลิงก์แปลกๆ ให้กดครับ
This message looks safe because it is just a casual question and has no suspicious links.
_(model: gemma-4-26b-a4b-it)_

ทำอย่างไร / What to do:
• ไม่มีสัญญาณอันตราย แต่ถ้าไม่แน่ใจถามลูกก่อน
• ส่งมาให้ลูกดูก่อนเสมอ
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;There's also a live dashboard (latest verdicts stream in every 2 seconds, zero API credits burned on page views) and a &lt;code&gt;/health&lt;/code&gt; endpoint the host watches.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why open innovation is what makes this work
&lt;/h2&gt;

&lt;p&gt;The prompt asks where open beats closed for this build. Three places, all load-bearing:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The explainer is an open-weight model behind a swappable plug.&lt;/strong&gt; The verdict text my mum reads is written by Gemma through a plain OpenAI-compatible endpoint. This weekend that endpoint was, in turn: a big hosted Gemma (too slow, 35s+), a smaller one (fast, but it thinks out loud — it wraps &lt;em&gt;everything&lt;/em&gt; in &lt;code&gt;&amp;lt;thought&amp;gt;&lt;/code&gt; planning blocks), and finally the smaller one plus a 15-line answer-extractor that recovers the Thai/English sentences from inside the thinking. I could do that iteration in an evening &lt;em&gt;because&lt;/em&gt; the model, the weights lineage, and the wire format are all inspectable and interchangeable. A closed black-box API gives you one behavior, take it or leave it. Next weekend the same code can point at Ollama on a laptop and nothing else changes — try that migration with a proprietary agent stack.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The score is readable by her kid.&lt;/strong&gt; Every point comes from a named rule in one file (&lt;code&gt;triage.py&lt;/code&gt;), and the card lists each reason. When the bot says 75, I can point at exactly which three signals added up. That transparency is the whole product for a family: trust isn't a brand, it's showing your work. Closed-scoring "AI fraud APIs" return a number and a shrug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. It costs $0 to run and nobody's data is the product.&lt;/strong&gt; Free-tier hosting, 250 free SerpApi searches a month (only suspicious domains trigger one), public RPCs, an open model. A tool that protects your family shouldn't need a subscription or a data pipeline into someone's ad machine to survive.&lt;/p&gt;

&lt;p&gt;What open &lt;em&gt;didn't&lt;/em&gt; do: the score itself is still hand-written rules, and I'm saying so instead of dressing it up. The model explains; the rules decide; the receipts are real links and real RPC responses. A demo that fakes intelligence is worse than no demo.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it's built
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;python-telegram-bot&lt;/code&gt; (polling worker inside the FastAPI lifespan) + FastAPI + &lt;code&gt;httpx&lt;/code&gt;, on Render's free tier from a &lt;code&gt;render.yaml&lt;/code&gt;. Telegram handlers have timeouts, an error handler, and a 120s triage budget so one slow provider can't wedge the bot. Every verdict is saved to SQLite and streamed to the dashboard. Sentry watches errors and performance. Nothing exotic — the exotic part is that it exists in my mum's chat app instead of a slide deck.&lt;/p&gt;

&lt;p&gt;Built with: &lt;strong&gt;Render&lt;/strong&gt; (hosting) · &lt;strong&gt;Gemma&lt;/strong&gt; (open-weight explanations) · &lt;strong&gt;SerpApi&lt;/strong&gt; (live search grounding) · &lt;strong&gt;Sentry&lt;/strong&gt; (error + performance monitoring).&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/memeshee/scamshield" rel="noopener noreferrer"&gt;memeshee/scamshield&lt;/a&gt; — MIT, contributions welcome, especially Thai scam-pattern rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handover
&lt;/h2&gt;

&lt;p&gt;The bot is live in my mum's Telegram now. Her verdict on the verdicts lands here as an update — the real judging criterion was always whether &lt;em&gt;she&lt;/em&gt; trusts it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: this write-up was drafted with an AI assistant; every verdict, link, and number in it was run and verified by the author against the live code.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>hf26challenge</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
