<?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: UugaaaBugaaa</title>
    <description>The latest articles on DEV Community by UugaaaBugaaa (@uugaaabugaaa).</description>
    <link>https://dev.to/uugaaabugaaa</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%2F4160498%2F2ec3282b-ceb9-4e0e-a776-c28221a7a8d6.jpg</url>
      <title>DEV Community: UugaaaBugaaa</title>
      <link>https://dev.to/uugaaabugaaa</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/uugaaabugaaa"/>
    <language>en</language>
    <item>
      <title>I'm too lazy to check my emails and Classroom stuff, so I built Hermes</title>
      <dc:creator>UugaaaBugaaa</dc:creator>
      <pubDate>Fri, 23 Oct 2026 21:28:05 +0000</pubDate>
      <link>https://dev.to/uugaaabugaaa/im-too-lazy-to-check-my-emails-and-classroom-stuff-so-i-built-hermes-ifc</link>
      <guid>https://dev.to/uugaaabugaaa/im-too-lazy-to-check-my-emails-and-classroom-stuff-so-i-built-hermes-ifc</guid>
      <description>&lt;p&gt;I was registered for the two previous challenges too, but I was too lazy (&lt;em&gt;binge watching Black Clover :)&lt;/em&gt;) to submit anything. When I saw this challenge and read the word &lt;strong&gt;"LAZY"&lt;/strong&gt;, I knew this one was for me. I got up with a marker in my hand, stared at the whiteboard thinking about what I needed, looked at my phone, and realised I had missed my class (&lt;em&gt;online, because my college flooded ;-;&lt;/em&gt;), which led me to this idea: what if I build a system that takes in all the stuff from my emails (both personal and college) and Google Classroom, generates an audio summary of it (so I can build this and then completely ignore it)? &lt;strong&gt;THEN I STARTED OVERTHINKING&lt;/strong&gt; whether this was a good idea, and finally I gave up and said, &lt;strong&gt;"I need this, so let's make this thing."&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;Hermes is a local-first academic triage engine and opportunity radar for one very distractible student (me :)). &lt;strong&gt;There is no chat box and no prompt.&lt;/strong&gt; It starts with FastAPI, polls Google Classroom, Gmail, and opportunity feeds, throws out the noise, extracts real deadlines, checks the web when an opportunity date is missing, and writes everything to SQLite. The dashboard reads that state.&lt;/p&gt;

&lt;p&gt;The loop has five visible outcomes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Panic&lt;/strong&gt; for cutoffs under 6 hours.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Approaching&lt;/strong&gt; for the next 24 hours.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chill&lt;/strong&gt; for anything beyond that (exactly, who cares).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watchlist&lt;/strong&gt; for items whose cutoff has not been announced (no guessing dates).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Opportunity matches&lt;/strong&gt; for hackathons, fellowships, and internships scored against my stack (gotta grab those achievements).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A local &lt;code&gt;llama-server&lt;/code&gt; runs &lt;code&gt;gemma-3-4b-it&lt;/code&gt; at Q4_K_M. The model extracts structure, it does not get to invent a timestamp. A 60-second English briefing turns the radar into audio through ElevenLabs. If the API key is missing, the dashboard switches to browser speech. &lt;strong&gt;The app still works.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo &amp;amp; Telemetry Receipts
&lt;/h2&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%2Feuhb10qqm5yg3gmatc1m.png" 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%2Feuhb10qqm5yg3gmatc1m.png" alt="Hermes dashboard showing 0 Panic, 1 Approaching, 15 Chill, 15 Watchlist items, 10 Scout matches, and live local telemetry: gemma-3-4b-it at 42.6 tok/s on 4.34 of 6.00 GB VRAM." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The task rows in that image come from my local Hermes instance. The Render demo uses the mock judge dataset, so its counts will differ. &lt;strong&gt;The telemetry strip is not hardcoded.&lt;/strong&gt; It is fetched from &lt;code&gt;GET /api/telemetry/gemma&lt;/code&gt;, which probes the local llama-server and &lt;code&gt;nvidia-smi&lt;/code&gt; on every cache miss. I removed the old hardcoded "6 GB VRAM" and "50 tok/s" labels because a screenshot should not be the source of truth.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live demo:&lt;/strong&gt; &lt;a href="https://athena-hermes.onrender.com" rel="noopener noreferrer"&gt;athena-hermes.onrender.com&lt;/a&gt;. The free instance runs the cloud sandbox profile, so its telemetry strip will show the heuristic fallback instead of the local GPU numbers.&lt;/p&gt;

&lt;p&gt;These are readings from the machine sitting under my desk:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Measurement&lt;/th&gt;
&lt;th&gt;Reading&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Model weights&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;gemma-3-4b-it&lt;/code&gt;, Q4_K_M, 2.48 GB on disk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPU&lt;/td&gt;
&lt;td&gt;NVIDIA RTX 4050 Laptop GPU, 6.00 GB VRAM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VRAM in use&lt;/td&gt;
&lt;td&gt;4.08–4.34 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Generation&lt;/td&gt;
&lt;td&gt;42–60 tok/s; the dashboard probe showed 42.6 tok/s in this screenshot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prompt evaluation&lt;/td&gt;
&lt;td&gt;109.5–132.6 tok/s on short probes; ~517 tok/s on long extraction prompts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;First token&lt;/td&gt;
&lt;td&gt;48–113 ms depending on prefix cache state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dashboard probe latency&lt;/td&gt;
&lt;td&gt;234.9 ms in this screenshot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full email extraction&lt;/td&gt;
&lt;td&gt;~2.9–3.3 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Briefing script&lt;/td&gt;
&lt;td&gt;~4.1–4.2 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQLite dashboard read&lt;/td&gt;
&lt;td&gt;0.3–0.4 ms cold; WAL enabled, &lt;code&gt;busy_timeout=5000&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tests&lt;/td&gt;
&lt;td&gt;63 passed in 0.43–0.50 s with &lt;code&gt;ResourceWarning&lt;/code&gt; treated as an error&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two details make those numbers less like a launch slide and more like a measurement:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The telemetry endpoint has a 10-second cache, so opening the dashboard does not hammer the GPU.&lt;/li&gt;
&lt;li&gt;The test command is &lt;code&gt;uv run pytest -q -W error::ResourceWarning&lt;/code&gt;. &lt;strong&gt;A green suite with leaked SQLite handles is not green.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The run in the screenshot shows 16 active items, 15 unconfirmed items on the Watchlist, and 10 ranked opportunities. The counts are not fixed, they move with the sync and the current date.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Open Innovation Matters
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Student privacy:
&lt;/h3&gt;

&lt;p&gt;Emails containing OTP's, Bank statements, some information regarding my socials and college mails screaming "DO NOT SHARE" shouldn't be on more than one server (I mean google).&lt;/p&gt;

&lt;p&gt;Hermes keeps extraction on the laptop. Gmail and Classroom tokens stay on disk. Email bodies go to &lt;code&gt;llama-server&lt;/code&gt; over &lt;code&gt;127.0.0.1&lt;/code&gt;, not to an inference vendor. The database is local SQLite. When I enable ElevenLabs, only the generated briefing script leaves the machine, never the raw email. I can disable voice entirely and let the browser speak the same script.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WE KNOW WHAT HAPPENS WHEN EMAILS ARE LEAKED.&lt;/strong&gt; Student data should not leave the student's machine just because a model is convenient.&lt;/p&gt;

&lt;h3&gt;
  
  
  The token bill is zero
&lt;/h3&gt;

&lt;p&gt;A hosted model charges per token. Forty emails a day, every day, add up even before retries, classification mistakes, and the occasional 1,500-character body. Gemma 3 4B runs on open weights I already downloaded. &lt;strong&gt;The inference cost is 0.&lt;/strong&gt; The laptop and the electricity are real tho.&lt;/p&gt;

&lt;p&gt;That matters for a student tool (I don't wanna spend my money). A free tier with a hard quota teaches you to ration your own inbox. &lt;strong&gt;A local model does not.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  It still works when Wi-Fi is not available
&lt;/h3&gt;

&lt;p&gt;The last sync sits in SQLite, so the radar, watchlist, and briefing script are still there when the Wi-Fi dies and local inference keeps running. If the network drops halfway through a sync, the ledger doesn't get half-written because the ledger row, the task, and the opportunity all go in one transaction, so a retry starts from a clean state instead of some weird half-synced mess. If llama-server is down, the heuristic fallback takes over instead of throwing a 500 at my face.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wi-Fi always dies exactly when a submission window is closing. That's not a hypothetical. That's a law.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Open weights are inspectable
&lt;/h3&gt;

&lt;p&gt;The other thing I like about local weights is that I can see what the thing is doing. The prompt is in the repo. The GBNF grammar is in &lt;code&gt;app/grammars&lt;/code&gt;. The date filter is a few lines of Python, and the SQLite schema is right there, so if something looks wrong I can follow the whole path from email to radar without opening a vendor dashboard. Swapping the model is one env var. &lt;strong&gt;No vendor call, no pricing page, no "contact sales".&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I can run the whole thing with no account, no card, and no analytics leaving the box. For a tool that touches grade emails and faculty stuff, that matters more than whatever benchmark is trending this week.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture &amp;amp; Deterministic Safeguards
&lt;/h2&gt;

&lt;p&gt;Here's the whole loop. I'm not going to pretend it's the prettiest diagram, but it's the actual flow.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    A[Google Classroom&amp;lt;br/&amp;gt;Gmail&amp;lt;br/&amp;gt;Opportunity feeds] --&amp;gt; B[Stage-1 deterministic filter]
    B --&amp;gt;|OTP, receipt, flyer| L[(SQLite ledger&amp;lt;br/&amp;gt;seen IDs only)]
    B --&amp;gt;|candidate| C[(SQLite state ledger)]
    C --&amp;gt; D[Local Gemma 3 4B&amp;lt;br/&amp;gt;Q4_K_M + GBNF grammar]
    D --&amp;gt; E{Verified deadline?}
    E --&amp;gt;|yes| F[(SQLite WAL&amp;lt;br/&amp;gt;tasks + opportunities)]
    E --&amp;gt;|no| G[Autonomous search&amp;lt;br/&amp;gt;SerpApi -&amp;gt; DuckDuckGo]
    G --&amp;gt; H{Date found?}
    H --&amp;gt;|yes| F
    H --&amp;gt;|no| I[Watchlist&amp;lt;br/&amp;gt;no invented cutoff]
    F --&amp;gt; J[Urgency radar&amp;lt;br/&amp;gt;Panic / Approaching / Chill]
    F --&amp;gt; K[English briefing script]
    K --&amp;gt; M[ElevenLabs TTS&amp;lt;br/&amp;gt;content-hash cache]
    J --&amp;gt; N[FastAPI dashboard]
    M --&amp;gt; N
    I --&amp;gt; N&lt;/code&gt;&lt;/pre&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The rule under all of it:&lt;/strong&gt; a missing date is data, not an invitation to invent one.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Layer 1: the deterministic pre-filter
&lt;/h3&gt;

&lt;p&gt;The first pass is sender and keyword matching. OTPs, receipts, password resets, bookstore sales, club flyers, and lost-and-found stuff are all gone before they touch the GPU, because &lt;strong&gt;a bookstore email does not deserve three seconds of my laptop's life&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If a sale email also says "assignment due", it gets a second look and the academic signal wins. The model is good at pulling one deadline out of a clean email, but &lt;em&gt;it's not good at deciding whether a hoodie coupon is urgent&lt;/em&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 2: constrained Gemma extraction
&lt;/h3&gt;

&lt;p&gt;Gemma gets the email body, sender, subject, and the received time in &lt;code&gt;Asia/Kolkata&lt;/code&gt;. The output is forced through a GBNF grammar checked into &lt;code&gt;app/grammars/deadline_extraction.gbnf&lt;/code&gt;, and then Pydantic validates it.&lt;/p&gt;

&lt;p&gt;A small slice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;category ::= "\"ACADEMIC_DEADLINE\"" | "\"OPPORTUNITY_DEADLINE\"" | "\"INFORMATIONAL_ALERT\"" | "\"SPAM_NOISE\""
number ::= "-"? [0-9]+ ("." [0-9]+)?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Every rule is on one line because llama-server threw a 400 at me when I formatted them nicely. &lt;em&gt;Cool.&lt;/em&gt; &lt;strong&gt;The grammar is not decoration.&lt;/strong&gt; It stops the model from replying with a prose apology where the JSON should be.&lt;/p&gt;

&lt;p&gt;Dates are where I trust the model the least, so "tonight at 11:59 PM" gets resolved against the email's timestamp in IST, then converted to UTC, and the model's own &lt;code&gt;exact_deadline_iso&lt;/code&gt; is only a suggestion. It gets accepted if it falls between 30 days before the email and 365 days after it. If it looks like a hallucination, it goes to the Watchlist. &lt;strong&gt;No Panic lane, no fake timer.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Layer 3: autonomous search, with a negative cache
&lt;/h3&gt;

&lt;p&gt;When an opportunity has no date, the tool hits SerpApi, falls back to DuckDuckGo's HTML endpoint, and asks Gemma to pull a date out of the snippets, then caches a positive result for 30 days and a negative one for 2. The short negative TTL is on purpose, because a conference that hasn't announced its cutoff today might announce it tomorrow, and I don't want a stale "no date" sitting in cache when the real date finally drops.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Search is a tool, not god.&lt;/strong&gt; If no future deadline comes back, the item stays on the Watchlist. &lt;strong&gt;It doesn't get a default seven days.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Bugs I Hit Along the Way
&lt;/h2&gt;

&lt;p&gt;These five are the ones that actually changed the design. Some were my fault, some were the model's, and one was just me not closing database connections. The fixes are more useful than the feature list, so here they are.&lt;/p&gt;
&lt;h3&gt;
  
  
  1. The seven-day deadline that did not exist
&lt;/h3&gt;

&lt;p&gt;The first version of the opportunity fetcher had a fallback. If a page didn't expose a deadline, I set one seven days out. The dashboard looked complete. &lt;strong&gt;It was also lying.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I removed every default timestamp. Devpost now handles ISO dates with a &lt;code&gt;Z&lt;/code&gt; suffix and cross-year ranges. Unstop and Classroom parsers return &lt;code&gt;None&lt;/code&gt; for missing or partial dates, and those items go to the Watchlist with "cutoff unconfirmed" in a separate lane with no timer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A missing date isn't a UX problem to hide. It's a fact to display.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  2. The 384-byte silent MP3
&lt;/h3&gt;

&lt;p&gt;ElevenLabs failed once and the old audio path wrote a 384-byte MPEG frame into &lt;code&gt;data/audio_cache/&lt;/code&gt;. Keyed by content hash. Every later request found a "cache hit" and played silence. &lt;strong&gt;The dashboard looked like it had audio. It didn't.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Now the service rejects anything under 2 KB, checks for an ID3 tag or a valid MPEG frame header, writes to a temp file, fsyncs it, and moves it into place with &lt;code&gt;os.replace&lt;/code&gt;. A failure leaves no cache entry. The route returns &lt;code&gt;503&lt;/code&gt;, and the browser speech fallback takes over.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Silence with a play button is a lie.&lt;/strong&gt; &lt;em&gt;I'd rather hear the Chrome robot voice.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  3. The OAuth scope shuffle
&lt;/h3&gt;

&lt;p&gt;Google's OAuth flow compares requested scopes with the scopes in the token. If the order changes, &lt;code&gt;oauthlib&lt;/code&gt; can throw &lt;code&gt;ScopeChanged&lt;/code&gt; even when the permission set is identical. Multi-account auth made that easy to reproduce.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;scripts/auth_google.py&lt;/code&gt; sets &lt;code&gt;OAUTHLIB_RELAX_TOKEN_SCOPE=1&lt;/code&gt; before importing the flow. That relaxes the order check, not the consent screen. The active manifest is ignored by git, and &lt;code&gt;config/accounts.example.json&lt;/code&gt; contains placeholders only. &lt;em&gt;This one cost me an evening, and I still don't fully forgive oauthlib.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  4. The midnight IST drift
&lt;/h3&gt;

&lt;p&gt;A 20:00 IST email is 14:30 UTC. The old code took that UTC datetime and called &lt;code&gt;.replace(hour=23, minute=59)&lt;/code&gt;, which made it 23:59 UTC. That's 05:29 IST the next morning. A deadline due in four hours moved to the next day.&lt;/p&gt;

&lt;p&gt;The resolver now converts to &lt;code&gt;ZoneInfo("Asia/Kolkata")&lt;/code&gt; first:&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;email_ist&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;to_utc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email_dt&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;astimezone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ZoneInfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Asia/Kolkata&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;tonight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;email_ist&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hour&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;minute&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;59&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;second&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;microsecond&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;tonight&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;astimezone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Test coverage includes explicit times, midnight, noon, and a 20:00 IST anchor. The model still sometimes spits out &lt;code&gt;23:59:00Z&lt;/code&gt; for "tonight at 11:59 PM". The code doesn't trust it. &lt;strong&gt;Good. Neither should you.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  5. The SQLite leak under rapid polling
&lt;/h3&gt;

&lt;p&gt;A stress run with repeated syncs gave me 167 &lt;code&gt;ResourceWarning&lt;/code&gt; messages. Connections were being opened in helper methods and left for the garbage collector. Under WAL and concurrent reads, that's a slow way to build a file-handle leak.&lt;/p&gt;

&lt;p&gt;Now every connection goes through a context manager. The DB sets &lt;code&gt;PRAGMA journal_mode=WAL&lt;/code&gt;, &lt;code&gt;PRAGMA busy_timeout=5000&lt;/code&gt;, and &lt;code&gt;PRAGMA synchronous=NORMAL&lt;/code&gt;, and the ledger and extracted tasks go in one transaction, so a failed sync doesn't leave a row claiming an email was handled when the task never got saved. The full suite runs with &lt;code&gt;-W error::ResourceWarning&lt;/code&gt; and passes.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;NORMAL&lt;/code&gt; is a deliberate trade. &lt;strong&gt;This is a cache and a task ledger, not a bank.&lt;/strong&gt; WAL keeps readers from blocking writers. A power loss can cost the last few transactions, but it won't corrupt the database. The next sync rebuilds what it can.&lt;/p&gt;

&lt;p&gt;63 tests pass in about half a second. &lt;em&gt;I'm not saying that makes me a good engineer.&lt;/em&gt; &lt;strong&gt;I'm saying the suite runs before I push.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The Lazy Payoff &amp;amp; Partner Prize Mappings
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Slow and Steady&lt;/strong&gt; was the Week 3 theme, and I read the hidden clause as &lt;strong&gt;Rewards Laziness&lt;/strong&gt;, so here's the payoff.&lt;/p&gt;

&lt;p&gt;Before Hermes, my morning was the Classroom tab, the Gmail tab, an internship board, a hackathon feed, a calendar, and the feeling that I had forgotten something. &lt;strong&gt;That was 25 minutes of scanning before I wrote a line of code.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Now it's one page. Background sync refreshes the connectors. Extraction runs locally and lands in SQLite. The radar gives me three lanes and a Watchlist. If I want audio, the briefing reads the important part in about 60 seconds while I put my shoes on.&lt;/p&gt;

&lt;p&gt;The time balance sheet is not complicated:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;25 minutes of manual tab scanning&lt;/td&gt;
&lt;td&gt;3-second status check; extraction runs in the background&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deadlines found by memory&lt;/td&gt;
&lt;td&gt;SQLite ledger plus radar bands&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unknown dates turned into fake dates&lt;/td&gt;
&lt;td&gt;Explicit Watchlist with no timer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Silent MP3 dead ends&lt;/td&gt;
&lt;td&gt;ElevenLabs audio or browser speech, never fake silence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Repeated LLM calls for the same email&lt;/td&gt;
&lt;td&gt;Persistent ledger skip, plus content-hash audio cache&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I still do the assignments. Hermes isn't writing the Raft implementation for me. It just makes sure I know it's due before the upload window closes. &lt;em&gt;And I can go back to Black Clover without feeling guilty.&lt;/em&gt; &lt;strong&gt;That's the real KPI.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Best Use of Gemma
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Gemma 3 4B is doing the extraction, not sitting in a chat bubble.&lt;/strong&gt; It runs locally through llama.cpp at Q4_K_M, gets a GBNF grammar, returns a validated schema, and falls back to deterministic parsing when the server is offline, so the no-GPU path still works. On the RTX 4050, a full email extraction takes about 3 seconds and generation stays around 48-60 tok/s.&lt;/p&gt;

&lt;p&gt;The part I care about is the boundary. &lt;strong&gt;The model reads the messy text, and Python owns the clock.&lt;/strong&gt; That's how a 4B model can sit in a real path without inventing deadlines.&lt;/p&gt;
&lt;h3&gt;
  
  
  Best Use of ElevenLabs
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The briefing is the actual lazy part.&lt;/strong&gt; I can listen to a 60-second English summary while I put my shoes on, and ElevenLabs gets the generated script, not the raw inbox. The audio cache is keyed by a SHA-256 hash of voice ID plus script text. The service checks MP3 headers and minimum size, writes atomically, and returns &lt;code&gt;503&lt;/code&gt; when synthesis is unavailable, so the failure path is as useful as the happy path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A missing key should give me browser speech, not a silent file with a play button.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Best Use of Render
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;render.yaml&lt;/code&gt; defines the judge sandbox as a Render web service. The live demo is &lt;a href="https://athena-hermes.onrender.com" rel="noopener noreferrer"&gt;athena-hermes.onrender.com&lt;/a&gt;. Because the free instance has no GPU, &lt;code&gt;llama-server&lt;/code&gt; telemetry reports offline and the deterministic paths handle extraction. &lt;em&gt;That's on purpose.&lt;/em&gt; A judge can open the dashboard, hit sync, inspect the radar, and test the Watchlist without a Google login or a local model.&lt;/p&gt;

&lt;p&gt;The blueprint keeps &lt;code&gt;ELEVENLABS_API_KEY&lt;/code&gt; in the dashboard with &lt;code&gt;sync: false&lt;/code&gt;, binds Uvicorn to &lt;code&gt;$PORT&lt;/code&gt;, uses the same FastAPI app as local mode, and ships the mock dataset, so a judge can reproduce the demo without a local setup.&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/P1yansh" rel="noopener noreferrer"&gt;
        P1yansh
      &lt;/a&gt; / &lt;a href="https://github.com/P1yansh/hermes" rel="noopener noreferrer"&gt;
        hermes
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &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;Hermes&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;Autonomous academic triage and opportunity radar for university students and developers, built as an extension layer for the Athena workspace.&lt;/p&gt;

&lt;p&gt;Built for the &lt;strong&gt;Hacktoberfest 2026 Week 3: Slow and Steady&lt;/strong&gt; challenge.&lt;/p&gt;




&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Why I Built This&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;I have a recurring problem every semester: I miss assignment deadlines and hackathon cutoffs because my inbox is flooded with university spam, society announcements, and course portal notifications. Important submission cutoffs sit buried beneath bookstore promotions and club flyers until someone in a group chat mentions they're finishing their submission.&lt;/p&gt;

&lt;p&gt;Hermes runs in the background and takes care of this:&lt;/p&gt;


&lt;ol&gt;

&lt;li&gt;

&lt;strong&gt;Academic Triage Agent&lt;/strong&gt;: Pulls Google Classroom coursework and unread emails, drops marketing noise through a fast Stage-1 header/keyword pre-filter, and runs local Gemma 3 4B to extract the course, title, and exact deadline timestamp. Relative phrases such as "tonight" are anchored to the email's IST timestamp, not the current wall clock.&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Opportunity Scout&lt;/strong&gt;…&lt;/li&gt;

&lt;/ol&gt;&lt;/div&gt;
&lt;br&gt;
  &lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/P1yansh/hermes" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;





&lt;p&gt;&lt;strong&gt;The best tool is the one that lets me be bored.&lt;/strong&gt; I want a morning that is coffee, one look at three lanes, one tap on a briefing, and then work. &lt;em&gt;Hermes isn't a second brain. It's a bouncer for my inbox, running on the laptop I already own.&lt;/em&gt; &lt;strong&gt;That's the kind of laziness I can defend.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Anyway, the marker is still on my hand. At least now the inbox checks itself. I'm going to watch mentalist now, bye bye.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Shoutout to my teammate &lt;a class="mentioned-user" href="https://dev.to/attlar"&gt;@attlar&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>gemma</category>
      <category>elevenlabs</category>
    </item>
  </channel>
</rss>
