<?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: SRG-Cy</title>
    <description>The latest articles on DEV Community by SRG-Cy (@srgcy).</description>
    <link>https://dev.to/srgcy</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%2F4159871%2Fffcc056f-cae3-443f-a951-1d01cc9e47fb.png</url>
      <title>DEV Community: SRG-Cy</title>
      <link>https://dev.to/srgcy</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/srgcy"/>
    <language>en</language>
    <item>
      <title>Halloween started in Ireland. I put a century of Irish ghost stories back on the map, and code wouldn't let the AI invent a single place</title>
      <dc:creator>SRG-Cy</dc:creator>
      <pubDate>Sat, 31 Oct 2026 20:17:47 +0000</pubDate>
      <link>https://dev.to/srgcy/halloween-started-in-ireland-i-put-a-century-of-irish-ghost-stories-back-on-the-map-and-code-18m1</link>
      <guid>https://dev.to/srgcy/halloween-started-in-ireland-i-put-a-century-of-irish-ghost-stories-back-on-the-map-and-code-18m1</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;Halloween started here. Long before plastic pumpkins, Samhain was the night at the end of October when the Irish said the wall between this world and the other one wore thin, and the dead walked the roads.&lt;/p&gt;

&lt;p&gt;I live in Ireland. In the 1890s an American folklorist, Jeremiah Curtin, sat by firesides in Kerry and wrote down what people told him: a man walking to Sneem at night who meets a ghost and fights it off with a steel sword, a midwife carried away to a fairy fort on a horse that turns out to be a plough beam, a dead mother walking a strand at night, perishing with the cold, asking for a pair of shoes to be given to the poor in her name. He printed thirty of them in 1895 as &lt;em&gt;Tales of the Fairies and of the Ghost World&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Every one of those tales happens somewhere real. The road to Sneem. Blackwater bridge. Ventry strand. You can still walk most of them. But in the book they're just words on a page, and the old spellings (Lochlein, Rahonain) don't match any modern map.&lt;/p&gt;

&lt;p&gt;Kerry is only one corner, though. So I added a second public-domain book: &lt;em&gt;True Irish Ghost Stories&lt;/em&gt; (1914), compiled by a Tipperary rector, St John D. Seymour, and a police district inspector, Harry L. Neligan, from accounts people sent in from all over the country: haunted houses in Dublin squares, banshees, poltergeists, RIC barracks with something in them besides the police.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;Samhain Map&lt;/strong&gt;: click a place anywhere in Ireland and read the ghost stories that happened there.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&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%2Fti66bbkfi4pc62bljbrj.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%2Fti66bbkfi4pc62bljbrj.jpg" alt="Samhain Map: all of Ireland with glowing marks where the stories happen, filters by ghost, banshee, poltergeist and fairy, and a list of stories" width="800" height="600"&gt;&lt;/a&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%2Fd6tj2xetalbzdsx5egyr.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%2Fd6tj2xetalbzdsx5egyr.jpg" alt="A story open on a phone: The Ghost of Sneem, set in old-print type, with a link to the scanned 1895 page" width="750" height="1624"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A story on a phone: Curtin's own words, in type close to how they were first printed, with the scanned 1895 page one tap away.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A map of Ireland&lt;/strong&gt; with a glowing mark at every place the stories happen: 156 stories, 82 places in 22 of the 32 counties, north and south. Click one and you get the stories set there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Each story&lt;/strong&gt; opens with the old text itself, then a one-line summary, the line that shows its ghost or fairy, every place it names with a ✓ or ✗ and the reason, and a link to the scanned page or the book on Project Gutenberg.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Famous haunted sites&lt;/strong&gt;, as pale ghosts on the map: Leap Castle, Loftus Hall, Charles Fort, Kilkea Castle, Dunluce and Dunseverick castles, Clare Abbey, and Lough Leane, where O'Donoghue's ghost rides the lake on May mornings. Each one is there only because a public-domain book (1825 to 1914) tells its story, and code checked the quote word for word. Famous places none of these books tells, like the Hellfire Club, aren't on the map.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stories you can listen to.&lt;/strong&gt; The haunted sites' stories and a few of the best short ones are read aloud by an ElevenLabs storyteller voice: slow, low, a little unsteady, the way they were first told by a fire.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filters&lt;/strong&gt; by what haunts the story: ghosts, banshees, poltergeists, death warnings, fairies, the dead returning, a seal woman.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A count of what the checker refused&lt;/strong&gt;, right on the front page. That number matters to me as much as the pins.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rule behind all of it: &lt;strong&gt;Gemma reads. Code decides.&lt;/strong&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://samhain-map.onrender.com" rel="noopener noreferrer"&gt;samhain-map.onrender.com&lt;/a&gt; (no sign-up, works on a phone)&lt;/p&gt;

&lt;p&gt;Try: filter by &lt;strong&gt;Ghost&lt;/strong&gt;, click &lt;strong&gt;Sneem&lt;/strong&gt;, open &lt;em&gt;The Ghost of Sneem&lt;/em&gt;, then scroll to "Where it happened". Then zoom into Dublin Bay and click &lt;strong&gt;Dún Laoghaire&lt;/strong&gt;: the 1914 story calls it Kingstown. Or click any white ghost, like &lt;strong&gt;Leap Castle&lt;/strong&gt;, open its story and press play under "Listen to the story". Headphones, lights off.&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/SRG-Cy" rel="noopener noreferrer"&gt;
        SRG-Cy
      &lt;/a&gt; / &lt;a href="https://github.com/SRG-Cy/samhain-map" rel="noopener noreferrer"&gt;
        samhain-map
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Kerry's 1890s ghost and fairy tales, put back on the map. Gemma reads, code decides.
    &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;Samhain Map&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;Irish ghost and fairy stories from two public-domain books, put back on the map.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Jeremiah Curtin's &lt;em&gt;Tales of the Fairies and of the Ghost World&lt;/em&gt; (1895): 30 tales told to him by
firesides in south-west Munster.&lt;/li&gt;
&lt;li&gt;St John D. Seymour and Harry L. Neligan's &lt;em&gt;True Irish Ghost Stories&lt;/em&gt; (1914): ghost accounts sent in
from all over Ireland.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;An open-weight model (Gemma 4) reads each story: what haunts it, a one-line summary, and which places it
names. Plain code then checks every answer before anything is pinned on the map.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Gemma reads. Code decides.&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Every place must come with an exact quote, and the quote must be in the story.&lt;/li&gt;
&lt;li&gt;The name must be a real place on the island of Ireland: a Tailte Éireann or OSNI townland, or a GeoNames
town, hill, lake or bridge.&lt;/li&gt;
&lt;li&gt;Curtin spelled names as he heard them (Lochlein, Rahonain). Gemma may suggest a…&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/SRG-Cy/samhain-map" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1895 scan (archive.org) ─▶ code splits it into 30 tales
1914 book (Gutenberg)   ─▶ Gemma says which paragraph each account starts at ─▶ code cuts 124 accounts
each story ─▶ Gemma 4 26B (JSON mode): beings, summary, every place + the exact words it came from
           ─▶ code: is the quote really in the story? is the name in the quote?
           ─▶ code: is it a real place? (59,618 townlands from Tailte Éireann and OSNI + 31,329 GeoNames places, all 32 counties)
           ─▶ code: several places share the name? nearest the story's other places, or in the county the story names, or refuse
           ─▶ old spelling not found? Gemma suggests a modern one; code accepts it only if it exists near the story's other places
           ─▶ map
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Gemma only points at the text
&lt;/h3&gt;

&lt;p&gt;For every place, Gemma has to return the place &lt;strong&gt;and the exact words it came from&lt;/strong&gt;. Code then checks two things: the quote is really in the tale, and the name is really in the quote. If either fails, the place is refused, and the map says so.&lt;/p&gt;

&lt;p&gt;Across Curtin's 30 tales, Gemma reported 95 place names. Two failed the quote check, and they surprised me: &lt;strong&gt;neither was invented.&lt;/strong&gt; Both places are really in the tales. Gemma just changed one word while copying:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tale says&lt;/th&gt;
&lt;th&gt;Gemma quoted&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"…the fort &lt;strong&gt;on&lt;/strong&gt; the place where I am living now, in Lismore."&lt;/td&gt;
&lt;td&gt;"&lt;strong&gt;in&lt;/strong&gt; the place where I am living now, in Lismore."&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"…the eldest son to marry &lt;strong&gt;and&lt;/strong&gt; Gort na Leachtan (the field of the stone heaps)…"&lt;/td&gt;
&lt;td&gt;"&lt;strong&gt;is&lt;/strong&gt; Gort na Leachtan (the field of the stone heaps) as fortune."&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I kept the rule strict anyway. A checker that lets "close enough" through is a checker that will one day let a made-up townland through. Losing two pins is the cheaper mistake. (The 1914 book added 223 more place names; three more failed the same check.)&lt;/p&gt;

&lt;h3&gt;
  
  
  A real place, or nothing
&lt;/h3&gt;

&lt;p&gt;A name that passes the quote check still isn't pinned until it matches a real place: one of 59,618 townlands (Tailte Éireann for the 26 counties, OSNI Open Data for the six in the North), or a town, hill, lake or bridge from GeoNames. "Munster", "Co. Kerry" or "the North of Ireland" are listed but never pinned: too broad to be a place you could stand.&lt;/p&gt;

&lt;p&gt;Many names exist more than once. Ireland has 41 places called Derry and 49 called Curragh. Code decides, in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Nearest the story's other places.&lt;/strong&gt; A place with only one match anywhere is a reference point.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In the county the story names.&lt;/strong&gt; "A haunted house in Kingstown, Co. Dublin" only looks in Co. Dublin.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The only town of that name&lt;/strong&gt;, if nothing else in the story helps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Otherwise, refuse.&lt;/strong&gt; "41 places in Ireland are called 'Derry', and the story gives nothing to choose by."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That Kingstown is my favourite fix. Co. Dublin has three townlands called Kingstown, so code refused, until Gemma's modern name for it, Dún Laoghaire, turned out to be exactly one town in the county the story names. Kingstown &lt;em&gt;is&lt;/em&gt; the old name for Dún Laoghaire.&lt;/p&gt;

&lt;h3&gt;
  
  
  The 1914 book has no story headings
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;True Irish Ghost Stories&lt;/em&gt; runs 124 accounts together inside ten chapters. I didn't let Gemma rewrite or split the text. It sees each chapter with numbered paragraphs and answers only "account starts at paragraph 4, call it &lt;em&gt;The Spectral Charwoman&lt;/em&gt;". Code checks the numbers exist and does the cutting, so every word on the map is the book's. The titles of those stories are Gemma's, and each one says so.&lt;/p&gt;

&lt;h3&gt;
  
  
  Famous haunted sites, but only with a source
&lt;/h3&gt;

&lt;p&gt;Leap Castle is the one everyone in Ireland names. The 1914 book has it, but under a made-up name: "Kilman" Castle, "perhaps the worst haunted mansion in the British Isles". Seymour gives the clue himself: its owner wrote about it in the &lt;em&gt;Occult Review&lt;/em&gt; in December 1908. That was Mildred Darby of Leap Castle, and the site page says so and links the source.&lt;/p&gt;

&lt;p&gt;The sites are a short hand-picked list, but nothing on it is taken on trust. For each one, code checks that its quote is word for word in exactly one story, and takes the location from the same gazetteer as everything else. Only Charles Fort and Dunseverick Castle, which the gazetteer doesn't have, use Wikipedia's coordinates, and say so. Gill Hall, home of the famous Beresford ghost, is in the 1914 book, but I couldn't find a source for exactly where the house stood, so it isn't a ghost on the map.&lt;/p&gt;

&lt;p&gt;Each site has an image from Wikimedia Commons, taken only if its licence is free (all turned out CC BY-SA) and credited on the page. Kilkea Castle has none: Commons tags its postcard CC BY-SA, but the postcard's own credit line says CC BY-NC, and when two licences disagree I leave the image out.&lt;/p&gt;

&lt;h3&gt;
  
  
  Told aloud again
&lt;/h3&gt;

&lt;p&gt;These stories were told by a fire long before anyone wrote them down, so reading them silently felt like half of them. Twelve, about 69 minutes in all, are now read aloud by an ElevenLabs voice ("George", an older storyteller), set slow and slightly unsteady: lower stability so the voice can drop and linger, speed at 0.9.&lt;/p&gt;

&lt;p&gt;The same rule holds: the voice reads only the book's own words and the story's title, nothing generated. The audio is made once by a script and served as small static files (32 kbps speech, about a quarter of a megabyte a minute), so the API key never reaches the page and the site still costs nothing to host. Long stories go up in 4,000-character pieces cut at paragraph ends, each sent with the end of the piece before it, so the voice keeps its tone across the joins.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mentioned is not where it happened
&lt;/h3&gt;

&lt;p&gt;My first 1914 map pinned Waterloo, in Co. Cavan. The story says the widow's husband "fought at Waterloo". It also pinned the town of Shannon for "the estuary of the Shannon", and Cashel for "the diocese of Cashel". A review of every 1914 pin found them.&lt;/p&gt;

&lt;p&gt;Gemma had already marked each place as &lt;code&gt;setting&lt;/code&gt; or &lt;code&gt;mentioned&lt;/code&gt;, and every one of those bad pins was &lt;code&gt;mentioned&lt;/code&gt;. The 1914 contributors constantly say where they live ("the Archdeacon of Limerick sends…"), which isn't where the ghost is. So for that book only settings are pinned, and the rest are listed with the reason. A story whose only place is a county (&lt;em&gt;The Pig-Headed Ghost&lt;/em&gt;, somewhere in Co. Cavan) is pinned on the county, marked as county-level.&lt;/p&gt;

&lt;h3&gt;
  
  
  The 1890s spellings
&lt;/h3&gt;

&lt;p&gt;Curtin wrote names as he heard them. &lt;em&gt;Lochlein&lt;/em&gt; is Lough Leane. &lt;em&gt;Rahonain&lt;/em&gt; is probably Rahinnane. Modern maps have never heard of the first spelling.&lt;/p&gt;

&lt;p&gt;So there's a second, small Gemma pass: for every name the map couldn't find, Gemma suggests up to three modern spellings. &lt;strong&gt;Code still decides.&lt;/strong&gt; A suggestion is accepted only if that place exists &lt;em&gt;and&lt;/em&gt; lies within 30 km of the tale's other places. Those pins are labelled "via modern spelling, suggested by Gemma", so you can see which ones the model helped with.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Curtin wrote&lt;/th&gt;
&lt;th&gt;Gemma suggested&lt;/th&gt;
&lt;th&gt;Code said&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Lochlein&lt;/td&gt;
&lt;td&gt;Lough Leane&lt;/td&gt;
&lt;td&gt;✓ pinned: the big lake at Killarney (two tales)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Skneem&lt;/td&gt;
&lt;td&gt;Sneem&lt;/td&gt;
&lt;td&gt;✓ pinned&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Awnascawil&lt;/td&gt;
&lt;td&gt;Annascaul&lt;/td&gt;
&lt;td&gt;✓ pinned: a real village in the right corner of Kerry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Castlemain&lt;/td&gt;
&lt;td&gt;Castlemaine&lt;/td&gt;
&lt;td&gt;✓ pinned (two tales)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kilshanig&lt;/td&gt;
&lt;td&gt;Kilshannig&lt;/td&gt;
&lt;td&gt;✓ pinned&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rahin&lt;/td&gt;
&lt;td&gt;Rahan&lt;/td&gt;
&lt;td&gt;✗ refused: Rahan exists, but 124 km from the tale's other places (and once the map covered all Ireland, a real &lt;em&gt;Rahin&lt;/em&gt; turned up in Co. Laois, refused at 244 km)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dun Lean&lt;/td&gt;
&lt;td&gt;Dunlea&lt;/td&gt;
&lt;td&gt;not pinned: no such place&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Drumfada&lt;/td&gt;
&lt;td&gt;Drumfada&lt;/td&gt;
&lt;td&gt;not pinned: no such place on modern maps&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The Rahan one is my favourite row in the project. Gemma's suggestion was a real place. It just wasn't &lt;em&gt;this&lt;/em&gt; place, and only the distance check could tell.&lt;/p&gt;

&lt;p&gt;This pass was also the flakiest part of the build. Asked about 29 names at once, Gemma reasoned past every time limit I gave it. Even 5 at a time timed out. One name per call, with a hard 150-second limit, got answers for 15 of 26 names after two runs; the rest (Rahonain among them) simply stay unpinned. In the end 8 place names are on the map because of this pass, and every one of them is labelled as a suggestion.&lt;/p&gt;

&lt;h3&gt;
  
  
  Measuring it
&lt;/h3&gt;

&lt;p&gt;For 7 of the shorter tales, every real place in Ireland they name was labelled by hand: 22 places. Then I compared two versions of the map: one that pins everything Gemma reported, and the real one.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tale&lt;/th&gt;
&lt;th&gt;Places in the tale&lt;/th&gt;
&lt;th&gt;Gemma found&lt;/th&gt;
&lt;th&gt;Gemma alone would also pin&lt;/th&gt;
&lt;th&gt;Pinned (correct / wrong)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;The Midwife of Listowel&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;1 / 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tom Connors and the Dead Girl&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;3 / 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;James Murray and Saint Martin&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;"Kerry"&lt;/td&gt;
&lt;td&gt;3 / 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The Ghost of Sneem&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;6 / 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The Dead Mother&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;"America"&lt;/td&gt;
&lt;td&gt;2 / 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tim Sheehy Sent Back…&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;1 / 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The Four-leafed Shamrock&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;2 / 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;22&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;22&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;18 / 0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gemma's reading was excellent.&lt;/strong&gt; It found all 22 places and invented none.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every pin on the map in these tales is right.&lt;/strong&gt; The 4 it couldn't pin are 1890s spellings that no modern map has: Rahin, Rahonain, Drumfada and "Ventry Strand".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The honest limits:&lt;/strong&gt; it's 7 tales and 22 places, not a benchmark. The labels were drafted with my coding agent rather than by an independent annotator, so read them as a sanity check, not ground truth. The 1914 book has no hand labels yet; its pins were checked by reading through the checker's decision list, which is how Waterloo was caught.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Things that went wrong
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gemma 4 31B returned server errors all afternoon&lt;/strong&gt; on AI Studio, so everything ran on Gemma 4 26B (the mixture-of-experts one). Because the weights are open, switching was one line.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;At temperature 0, Gemma 26B got stuck in its own reasoning:&lt;/strong&gt; "Wait, 'Sneem' is a setting. Wait, 'Blackwater bridge' is a setting." for 26,000 characters. JSON mode fixed it: about 11 seconds a tale.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cork city wasn't on the map.&lt;/strong&gt; City centres have no townlands, so the city sits in a hole in the county outline. Points just outside the outline now count.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The free tier rate-limited me&lt;/strong&gt; halfway through. Every reply is cached per tale, so the run picked up where it stopped.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A Drummond in Tipperary dragged Waterville to Limerick.&lt;/strong&gt; In &lt;em&gt;Murderous Ghosts&lt;/em&gt;, the only "Drummond" in Munster is 178 km away in Tipperary. My first checker trusted a single match as a reference point, and then picked the Waterville in Limerick because it was "nearest". Now only places in Kerry and Cork count as reference points, and a lone match that far from the rest of the tale is refused with the distance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Then my fix was too strict.&lt;/strong&gt; Tales where someone travels to Cork city lost Dingle and Brandon Mountain, because Cork pulled the centre of the tale east. A trip to the city isn't where a tale is set, so cities no longer count as reference points.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every line of the 1914 book became its own paragraph.&lt;/strong&gt; Saving the download on Windows doubled the line endings, and Gemma happily split 491 "paragraphs". The chapter titles came out blank, which gave it away.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Waterford North Station" and "Larne River".&lt;/strong&gt; When a town, its station and its river sit within 4 km, they count as one place, and code first picked the wrong one to show. Now the town wins.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Still wrong: the Curragh.&lt;/strong&gt; A story where "they gallop round the Curragh", near Athy and Kilkea in Kildare, is pinned on a townland called Curragh in Co. Laois, because the River Greese, matched in Laois, pulls the story's centre that way. It's the nearest-place rule doing exactly what it says. I left it in rather than add a special case.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limits and 500s from the free tier&lt;/strong&gt; for most of an evening. Every reply is cached, so two workers ran the 124 stories from both ends.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On a phone, the stories didn't show at all.&lt;/strong&gt; The page was locked to one screen height, and the header, map and footer left the story panel 33 pixels tall, hidden behind the footer. Now a phone scrolls like a page: map first, then the stories.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;strong&gt;Archives don't want their collections fed to AI companies, and I understand why.&lt;/strong&gt; Ireland's largest folklore archive, duchas.ie, blocks AI crawlers by name in its robots.txt. Those are people's grandparents' stories.&lt;/p&gt;

&lt;p&gt;That's exactly why the model being open matters. Gemma's weights are public, so an archive can run this same pipeline on its own server, on its own collection, and nothing ever leaves the building. I ran it on a hosted Gemma for the demo, using public-domain books. The National Folklore Collection could run it in-house without asking anyone's permission.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's checkable.&lt;/strong&gt; Same open model, same prompts, same public-domain books, same public maps: anyone can re-run it and argue with any pin. Every Gemma reply is committed in &lt;code&gt;data/extract/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It isn't locked inside a service that might disappear.&lt;/strong&gt; When the bigger Gemma was down, I changed one line. If the hosted API went away tomorrow, the same weights run on a laptop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The one closed piece is the voice.&lt;/strong&gt; ElevenLabs reads the stories aloud because I didn't want to run a voice model on my own laptop, and its Hacktoberfest credits gave me a storyteller voice in an evening. An open voice model could take its place. It sits at the very end of the pipeline, though: it reads text the open model and the checker have already handled, it decides nothing, and if it disappeared the map would lose its voice but not one pin.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where a closed model might do better:&lt;/strong&gt; probably the old spellings. A bigger model might know more 1890s Kerry place names. It would still need the same checker, because a confident model inventing a townland is the one thing a map of real places can't have.&lt;/p&gt;

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

&lt;p&gt;The Samhain stories in these books are less about monsters than about the dead being close, and close to particular places: a bend in the road, an old fort in a field, a lake at sunrise. Most of those places are still there, under the same names or close to them.&lt;/p&gt;

&lt;p&gt;So that's what I'd ask you to do with it: find your own place on the map. If a story happened there, it has been waiting about a hundred years for someone from home to read it.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Gemma:&lt;/strong&gt; Gemma 4 26B finds where each of 124 untitled accounts starts, reads all 156 stories for the beings, the summary and every place with its exact quote and role, and suggests modern spellings for old names. Every answer is checked in code before it reaches the map.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of ElevenLabs:&lt;/strong&gt; twelve stories narrated with &lt;code&gt;eleven_multilingual_v2&lt;/code&gt; and voice settings tuned for a fireside ghost story (stability 0.35, style 0.45, speed 0.9), chunked at paragraph ends with &lt;code&gt;previous_text&lt;/code&gt;/&lt;code&gt;next_text&lt;/code&gt; for continuity, cached once as static audio.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Render:&lt;/strong&gt; the whole map is a static site deployed from a &lt;code&gt;render.yaml&lt;/code&gt; Blueprint in the repo. Every push to &lt;code&gt;main&lt;/code&gt; redeploys it, so each re-run of the checker goes live on its own.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Tales: Jeremiah Curtin (1895), public domain, scan by Cornell University Library via archive.org. Stories: St John D. Seymour and Harry L. Neligan (1914), public domain, Project Gutenberg #14099. Haunted sites also from T. Crofton Croker (1825) and D. R. McAnally (1888), public domain, Project Gutenberg. Townland boundaries © Tailte Éireann, CC BY 4.0; Northern Ireland townlands and counties from OSNI Open Data, OGL v3. Places: GeoNames, CC BY 4.0. Site images: Wikimedia Commons, CC BY-SA, credited on each page. Narration: ElevenLabs. Built with Claude Code; I directed it and checked the results.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>gemma</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Google's free Gemma failed 35% of my calls. My sloth-paced reminder app didn't lose one.</title>
      <dc:creator>SRG-Cy</dc:creator>
      <pubDate>Sun, 25 Oct 2026 11:27:23 +0000</pubDate>
      <link>https://dev.to/srgcy/googles-free-gemma-failed-35-of-my-calls-my-sloth-paced-reminder-app-didnt-lose-one-2ip4</link>
      <guid>https://dev.to/srgcy/googles-free-gemma-failed-35-of-my-calls-my-sloth-paced-reminder-app-didnt-lose-one-2ip4</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;Return windows, warranty end dates, "come back in six months", the morning tickets go on sale, the bill that's due on the 5th. None of these are hard. They're just things I'm supposed to &lt;em&gt;remember&lt;/em&gt;, and I don't.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;Later&lt;/strong&gt;: you throw something at it, it works out the last sensible day to act, and then it does the most sloth thing possible. It waits. Quietly. For weeks or months. Then it nudges you once.&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%2Fz4hxq8myc1mt8lxjl46r.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%2Fz4hxq8myc1mt8lxjl46r.png" alt="Later on a phone: a timeline of held items grouped into Needs a look, Reading, Today, This week, This month and Later on, with a big Throw something in button at the bottom" width="500" height="1080"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;Later is a phone-friendly web app with one big button, &lt;strong&gt;Throw something in&lt;/strong&gt;. It opens a sheet with three ways in: &lt;strong&gt;Snap&lt;/strong&gt; (a photo of a receipt, letter or poster), &lt;strong&gt;Say&lt;/strong&gt; (a voice note, transcribed by ElevenLabs) and &lt;strong&gt;Type&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You capture something and it answers instantly: &lt;em&gt;"Got it. I'm reading it now, so you can close this page."&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;In the background, an open-weight model (&lt;strong&gt;Gemma 4 31B&lt;/strong&gt;) reads it and reports the facts: what kind of thing it is, the dates it saw, any window like "refunds within 28 days".&lt;/li&gt;
&lt;li&gt;Plain code turns those facts into a deadline and a nudge time in Irish time: 3 days before a return window closes, 30 days before a warranty ends, 2 weeks before you should book that check-up.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;Temporal&lt;/strong&gt; workflow sleeps until then. It survives restarts and deploys.&lt;/li&gt;
&lt;li&gt;You get &lt;strong&gt;one&lt;/strong&gt; push on your phone, with &lt;strong&gt;Done&lt;/strong&gt; and &lt;strong&gt;Snooze 3 days&lt;/strong&gt; buttons. That's the only time Later ever talks to you.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything Later is holding sits on one timeline, grouped by when you'll hear about it: &lt;em&gt;Today&lt;/em&gt;, &lt;em&gt;This week&lt;/em&gt;, &lt;em&gt;This month&lt;/em&gt;, &lt;em&gt;Later on&lt;/em&gt;. Anything that needs you (no date found) floats to the top. Tap an item to see the due date and Done / Snooze / Forget.&lt;/p&gt;

&lt;p&gt;The lazy part is the whole point: you don't type a calendar entry, you don't work out what "28 days from the 20th" is, and you don't check an app to see what's coming. You throw it in and forget it.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Try it:&lt;/strong&gt; &lt;a href="https://later-h5nw.onrender.com" rel="noopener noreferrer"&gt;https://later-h5nw.onrender.com&lt;/a&gt; (no sign-up). Tap &lt;strong&gt;Throw something in&lt;/strong&gt;, then &lt;strong&gt;Type&lt;/strong&gt;, paste something like &lt;em&gt;"Penneys receipt 20/10/2026. Refunds within 28 days"&lt;/em&gt;, and watch it move from &lt;em&gt;Reading&lt;/em&gt; to &lt;em&gt;This month&lt;/em&gt;. Tap the 🔔 to subscribe to your private push topic in the free &lt;a href="https://ntfy.sh" rel="noopener noreferrer"&gt;ntfy&lt;/a&gt; app.&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/SRG-Cy" rel="noopener noreferrer"&gt;
        SRG-Cy
      &lt;/a&gt; / &lt;a href="https://github.com/SRG-Cy/later" rel="noopener noreferrer"&gt;
        later
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Snap it, say it, paste it. Later reminds you at the last sensible moment. Hacktoberfest 2026 Week 3.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/SRG-Cy/later/static/brand/wordmark.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FSRG-Cy%2Flater%2FHEAD%2Fstatic%2Fbrand%2Fwordmark.png" alt="Later" width="300"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Later 🦥&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;Snap it, say it or type it. Later works out what you'll need to do and the last sensible day to do it, then stays quiet until then and nudges you once.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Live:&lt;/strong&gt; &lt;a href="https://later-h5nw.onrender.com" rel="nofollow noopener noreferrer"&gt;https://later-h5nw.onrender.com&lt;/a&gt; · Built for the Hacktoberfest 2026 Open-Source AI Challenge, Week 3: Slow and Steady.&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/SRG-Cy/later/docs/post/app-light.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FSRG-Cy%2Flater%2FHEAD%2Fdocs%2Fpost%2Fapp-light.png" alt="Later on a phone: a timeline of held items grouped by when you will be nudged, with a big Throw something in button" width="320"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;How it works&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt;phone ──capture──▶ FastAPI ──store──▶ MongoDB Atlas
                      │
                      └─start──▶ Temporal workflow (one per item)
                                   1. read_item   ── Gemma 4 31B (open weights) reads the text or photo
                                   │                  retried patiently: the free endpoint fails ~1 call in 3
                                   2. resolve     ── plain code turns facts into a deadline (the model never does date maths)
                                   3. sleep until the last sensible moment (days or months; survives restarts)
                                   4. send_reminder ── one ntfy push with signed Done / Snooze buttons
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The model reports facts, code does the maths.&lt;/strong&gt; Gemma copies dates and windows ("30 day returns") exactly as written; &lt;code&gt;later/resolve.py&lt;/code&gt;…&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/SRG-Cy/later" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;MIT licensed, with 113 tests. Temporal workflows are tested with Temporal's time-skipping test server, so "wait 30 days" runs in a second.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;phone ──capture──▶ FastAPI ──store──▶ MongoDB Atlas
                      │
                      └─start──▶ Temporal workflow (one per item)
                                   1. read_item       Gemma 4 31B reads the text or photo
                                   2. resolve         plain code computes the deadline
                                   3. sleep until the last sensible moment
                                   4. send_reminder   one ntfy push, signed Done / Snooze links
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The model reports facts. Code does the maths.
&lt;/h3&gt;

&lt;p&gt;LLMs are not reliable at date arithmetic (&lt;a href="https://dev.to/maverickyadav/-llms-cant-reliably-do-date-math-and-now-theres-data-4hm2"&gt;there's data on this&lt;/a&gt;), and a reminder app that's a day out is worse than none. So Gemma is told to &lt;strong&gt;copy&lt;/strong&gt; dates exactly as written and report lengths of time separately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"return_window"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"org"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Penneys"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"anchor_date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-20"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"window_days"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"evidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Refunds within 28 days with receipt"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then &lt;code&gt;resolve()&lt;/code&gt; does the rest in plain Python: month ends (31 Aug + 6 months = 28 Feb), Irish dates (day/month), and the clock change. Ireland's clocks went back on Sunday 25 October, the last day of this challenge, and 09:00 nudges stay at 09:00 local.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reading slowly on purpose
&lt;/h3&gt;

&lt;p&gt;I first ran Gemma 4 on Google AI Studio's free tier as a normal request: you capture, you wait for the answer. It was &lt;strong&gt;right every time it answered&lt;/strong&gt;, but it failed a lot and was slow. In a 24-call comparison, Gemma 4 31B succeeded 4 times out of 6, with each answer taking ~30 seconds. OpenRouter's free Gemma was rate-limited on all 12 calls.&lt;/p&gt;

&lt;p&gt;Nobody should stare at a spinner for a minute, so reading moved into the background:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/capture&lt;/code&gt; stores the raw item as &lt;strong&gt;reading&lt;/strong&gt; and starts its workflow. It returns in milliseconds.&lt;/li&gt;
&lt;li&gt;The workflow's first step is a &lt;code&gt;read_item&lt;/code&gt; activity with a patient retry policy: 5 s, then doubling up to every 2 minutes, for up to 2 hours.&lt;/li&gt;
&lt;li&gt;A model error raises, so Temporal retries it. A reply that's understood but has no date becomes &lt;strong&gt;no date found&lt;/strong&gt;, with no endless retries.&lt;/li&gt;
&lt;li&gt;If the model is down for 2 hours, the item is marked &lt;strong&gt;failed&lt;/strong&gt; instead of quietly vanishing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's this week's theme taken literally: the sloth takes its time so you don't have to.&lt;/p&gt;

&lt;h3&gt;
  
  
  No accounts, still private
&lt;/h3&gt;

&lt;p&gt;The public demo has no sign-up, so each visitor gets an anonymous cookie and only sees their own items. Each visitor gets a private ntfy topic derived with an HMAC. The push buttons carry a signed per-item token, because the ntfy app has no cookie, and a forged token gets a 404. Daily capture limits protect the free model quota.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deploying
&lt;/h3&gt;

&lt;p&gt;One Docker container on Render runs the Temporal dev server (state on a 1 GB persistent disk), a self-restarting worker and the web app. MongoDB Atlas holds the items and photos.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;I ran 35 captures through the real pipeline (Gemma extraction, then the resolver), with Temporal-style retries:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;30 hand-written captures&lt;/th&gt;
&lt;th&gt;5 real Galway event listings&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;27 typed, 3 drawn images&lt;/td&gt;
&lt;td&gt;3 typed, 2 screenshots of the live listing pages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Due date exactly right&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;30/30&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5/5&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kind of item right&lt;/td&gt;
&lt;td&gt;28/30&lt;/td&gt;
&lt;td&gt;5/5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model calls that failed&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;16 of 46 (35%)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1 of 6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Captures lost&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time to read, median&lt;/td&gt;
&lt;td&gt;60 s&lt;/td&gt;
&lt;td&gt;55 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The hand-written set includes an Irish mix: Penneys and Zalando returns, an ESB bill, motor tax, the NCT, a Revenue Form 11 deadline, a Panda bin top-up, a passport expiry. One gig poster has a decoy date: the gig is in March 2027, but tickets go on sale 13 Nov 2026, and that's the date that matters. The Galway set uses real listings: Paul Brady at Leisureland, The Frank And Walters and Pa Sheehy at Róisín Dubh, and the Eyre Square Christmas Market.&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%2Fs2jk3wa6mw34bch3id5g.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%2Fs2jk3wa6mw34bch3id5g.png" alt="A real Songkick listing screenshot used as a test capture: Paul Brady, Saturday 28 November 2026, Leisureland, Galway" width="430" height="900"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The number I care about is the third row.&lt;/strong&gt; Google's free Gemma endpoint failed &lt;strong&gt;35%&lt;/strong&gt; of calls. The worst capture, an ESB bill, failed 4 times and was read on the 5th try, 8½ minutes later. Without the retries, a third of everything you threw at Later would simply be gone. With them, nothing was lost. (The standard advice on &lt;a href="https://dev.to/draganristicrsjpg/retry-backoff-and-circuit-breakers-for-llm-api-calls-h3k"&gt;retrying LLM calls with backoff&lt;/a&gt; applies; Temporal just makes it durable.)&lt;/p&gt;

&lt;h3&gt;
  
  
  What went wrong
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gigs were nudged after the gig.&lt;/strong&gt; The real Galway listings caught this one. Gemma labels gigs as "ticket" events, which nudge 30 minutes before a known time. With only a date, that came out as &lt;strong&gt;23:29 on the night&lt;/strong&gt;. Now a date-only event nudges at 09:00 that morning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemma 4 thinks out loud.&lt;/strong&gt; On Google AI Studio it always writes a &lt;code&gt;&amp;lt;thought&amp;gt;&lt;/code&gt; block before its answer, sometimes wraps the JSON in code fences, and JSON mode returned 500 errors. The parser now skips the thinking and takes the last complete JSON object.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Date-only dates became midnight.&lt;/strong&gt; Pydantic happily parses &lt;code&gt;"2026-11-05"&lt;/code&gt; into a &lt;em&gt;datetime&lt;/em&gt; at 00:00, which would have fired reminders a day early. Dates without a time now stay dates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two kind mix-ups&lt;/strong&gt; in the test, both with the right date: a Hozier ticket sale labelled a &lt;em&gt;deadline&lt;/em&gt; (nudges 2 days early instead of 30 minutes before), and a flight labelled a &lt;em&gt;ticket event&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The weights are open, so the exact same model can run anywhere.&lt;/strong&gt; I used Google AI Studio's hosted Gemma for the demo, but Gemma is open-weight: anyone who doesn't want their receipts and letters leaving their machine can point &lt;code&gt;LLM_BASE_URL&lt;/code&gt; at their own server and run the identical model. The code only needs an OpenAI-compatible endpoint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No lock-in.&lt;/strong&gt; Switching provider or model is one environment variable. I did exactly that mid-build: I benchmarked two providers and two Gemma sizes, then moved, without touching the code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The free tier made it possible, and open tools made it reliable.&lt;/strong&gt; A free, flaky endpoint was fine because open-source Temporal absorbs the flakiness. The whole stack is open source or has a free tier: Gemma, Temporal, ntfy, FastAPI.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Here's what one capture saves you:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;By hand&lt;/th&gt;
&lt;th&gt;With Later&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Work out "28 days from the 20th"&lt;/td&gt;
&lt;td&gt;Snap the receipt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Open the calendar, make an event, pick a reminder time&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Remember to check it&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Notice when you've missed it&lt;/td&gt;
&lt;td&gt;One push, the morning it matters&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Capturing takes one tap and a photo, a few seconds. After that the effort is zero. Across the 35 test captures, Later worked out every deadline correctly, and the person capturing never had to look at a date.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Gemma:&lt;/strong&gt; Gemma 4 31B reads every capture, text and photos, and returns structured facts. It's evaluated on 35 captures, with 35/35 due dates correct.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Temporal:&lt;/strong&gt; one durable workflow per item. It reads in the background with patient retries (recovering from a 35% failure rate with 0 lost captures), sleeps for days or months until the nudge, and handles Done/Snooze signals. Tested with the time-skipping test server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of ElevenLabs:&lt;/strong&gt; the &lt;strong&gt;Say&lt;/strong&gt; button. ElevenLabs speech-to-text (&lt;code&gt;scribe_v2&lt;/code&gt;) turns a voice note into text in about 2 seconds, and Gemma reads the transcript like anything typed. Live test: &lt;em&gt;"Dentist said come back for a checkup in six months"&lt;/em&gt; was transcribed word for word and scheduled for 25 April 2027. If transcription fails, the person gets a clear message instead of a lost capture.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of MongoDB Atlas:&lt;/strong&gt; stores every item and photo, with per-visitor queries and daily limit counts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Render:&lt;/strong&gt; one Docker web service from a &lt;code&gt;render.yaml&lt;/code&gt; Blueprint runs Temporal, the worker and the app, with a persistent disk for Temporal's state. Push links fall back to Render's own &lt;code&gt;RENDER_EXTERNAL_URL&lt;/code&gt;, so they work without extra setup.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;This project and post were built with the help of AI (Claude Code). I reviewed and tested everything here, and every number comes from runs in the repo's &lt;code&gt;eval/results&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>gemma</category>
      <category>temporal</category>
    </item>
    <item>
      <title>StyleCast: an AI outfit planner that runs on your phone</title>
      <dc:creator>SRG-Cy</dc:creator>
      <pubDate>Sat, 17 Oct 2026 13:59:36 +0000</pubDate>
      <link>https://dev.to/srgcy/stylecast-an-ai-outfit-planner-that-runs-on-your-phone-414g</link>
      <guid>https://dev.to/srgcy/stylecast-an-ai-outfit-planner-that-runs-on-your-phone-414g</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week2-2026-10-12"&gt;Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;Every morning I check the weather app, open the wardrobe, and guess. Then it's 22 degrees and I'm in a hoodie, or it's pouring and my jacket isn't waterproof.&lt;/p&gt;

&lt;p&gt;StyleCast does the guessing for me:&lt;/p&gt;

&lt;p&gt;📸 Snap your wardrobe once. An open vision model running in the browser tags each item: type, colour, warmth, waterproof, formality. Every tag is one tap to edit.&lt;br&gt;
🌦️ Get today's outfit. It reads the forecast for where you are (rain, wind, temperature) and your plan for the day (casual, work or gym), then picks from your clothes.&lt;br&gt;
🧺 It remembers. No wearing the same shirt three days running, and anything marked "in the wash" stays out.&lt;br&gt;
🕳️ It spots gaps. "Nothing waterproof for days like today."&lt;br&gt;
🛍️ Budget chat. Ask "I have €50, what should I buy?" and Gemma, running on your device, plans searches from your gaps and shows real products that fit the budget.&lt;/p&gt;
&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;Try it: &lt;a href="https://srg-cy.github.io/StyleCast/" rel="noopener noreferrer"&gt;https://srg-cy.github.io/StyleCast/&lt;/a&gt; (works best in Chrome on a laptop or Android; it installs to your home screen like an app).&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%2Fuztqizmvjeem52ml3upn.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%2Fuztqizmvjeem52ml3upn.png" alt=" " width="780" height="1688"&gt;&lt;/a&gt;&lt;br&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%2Ff2ohuuegnwq8gnmv6ncm.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%2Ff2ohuuegnwq8gnmv6ncm.png" alt=" " width="780" height="1688"&gt;&lt;/a&gt;&lt;br&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%2F2mabhgu1i4p1mowvxcwb.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%2F2mabhgu1i4p1mowvxcwb.png" alt=" " width="780" height="1688"&gt;&lt;/a&gt;&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/SRG-Cy" rel="noopener noreferrer"&gt;
        SRG-Cy
      &lt;/a&gt; / &lt;a href="https://github.com/SRG-Cy/StyleCast" rel="noopener noreferrer"&gt;
        StyleCast
      &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;StyleCast&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;Photograph your wardrobe once. Every morning, get an outfit from your own clothes that fits the real forecast.&lt;/p&gt;
&lt;p&gt;Built for the DEV Hacktoberfest 2026 Open-Source AI Challenge, Week 2 ("Side Hustle").&lt;/p&gt;
&lt;p&gt;
  &lt;a rel="noopener noreferrer" href="https://github.com/SRG-Cy/StyleCast/docs/screenshots/today-rain.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FSRG-Cy%2FStyleCast%2FHEAD%2Fdocs%2Fscreenshots%2Ftoday-rain.png" width="200" alt="Today on a rainy day: 13 degrees under the curtain-red header, and an outfit of denim jacket, hoodie, jeans and trainers in an arched mirror"&gt;&lt;/a&gt;
  &lt;a rel="noopener noreferrer" href="https://github.com/SRG-Cy/StyleCast/docs/screenshots/today-sunny.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FSRG-Cy%2FStyleCast%2FHEAD%2Fdocs%2Fscreenshots%2Ftoday-sunny.png" width="200" alt="Today on a sunny day: 22 degrees, and a t-shirt, shorts and trainers in the mirror"&gt;&lt;/a&gt;
  &lt;a rel="noopener noreferrer" href="https://github.com/SRG-Cy/StyleCast/docs/screenshots/wardrobe.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FSRG-Cy%2FStyleCast%2FHEAD%2Fdocs%2Fscreenshots%2Fwardrobe.png" width="200" alt="The wardrobe, with each category hanging on its own brass rail"&gt;&lt;/a&gt;
  &lt;a rel="noopener noreferrer" href="https://github.com/SRG-Cy/StyleCast/docs/screenshots/shop.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FSRG-Cy%2FStyleCast%2FHEAD%2Fdocs%2Fscreenshots%2Fshop.png" width="200" alt="The Shop chat with country and who-for settings"&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;Today's outfit stands in a fitting-room mirror under the forecast; the wardrobe hangs on rails, one per category.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;What it does&lt;/h2&gt;
&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Wardrobe scan:&lt;/strong&gt; snap each item. An open model running in your browser tags type, colour, warmth, waterproofing and formality. Every tag is editable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Daily outfit:&lt;/strong&gt; checks today's forecast (rain, wind, temperature) and your occasion (casual, work or gym), then picks an outfit from your wardrobe.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wear history:&lt;/strong&gt; remembers what you wore recently so it doesn't repeat, and lets you mark items "in the wash".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gap spotting:&lt;/strong&gt; tells you what your wardrobe is missing for the weather you actually get ("nothing waterproof for days like today").&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Budget chat (Shop tab):&lt;/strong&gt; say "I have €50 today, what should I…&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/SRG-Cy/StyleCast" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;It's a static site with no build step, MIT licensed. npm test covers the outfit rules, the budget chat and the shopping server, and CI runs it on every pull request.&lt;/p&gt;
&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Version 1 was a dead end&lt;/strong&gt;&lt;br&gt;
On day one, StyleCast was a Python server calling Qwen2.5-VL through Ollama on my PC. The outfits were good. The setup was terrible: every user would need to install Ollama and pull a 6 GB model before seeing a single outfit. Nobody pays for that.&lt;/p&gt;

&lt;p&gt;So on day two I threw it away and moved the AI into the browser:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tagging clothes from photos:&lt;/strong&gt; CLIP ViT-B/32 via Transformers.js, on your device (WebGPU on laptops, WebAssembly on phones).
Budget chat: Gemma 3 1B, or Gemma 3 270M without WebGPU, on your device in a web worker.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weather:&lt;/strong&gt; Open-Meteo, free with no key, called from the browser.
Shopping results: SerpApi Google Shopping, through a tiny Node server on Render.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wardrobe and photos:&lt;/strong&gt; IndexedDB, in your browser.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;CLIP doesn't describe photos the way a chat model does. It compares a photo with a list of descriptions and picks the closest. For "what kind of clothing is this?" that's enough, and it's small enough to run on a phone. The outfit itself is chosen by plain JavaScript rules: weather first, then occasion, then what you haven't worn lately. I don't need a model to tell me it's raining.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Then my jeans became a jacket&lt;/strong&gt;&lt;br&gt;
The first thing I photographed on my own phone was a pair of jeans. StyleCast filed them under jackets.&lt;/p&gt;

&lt;p&gt;I could have tweaked a prompt and hoped. Instead I wrote a scoring script that runs the tagger over 272 labelled product photos, and it now runs in CI on every pull request that touches the tagger. It found three problems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Lazy labels&lt;/strong&gt;. CLIP was matching my photo against "a photo of a jeans". With nothing else to go on, it went by colour and fabric, and blue denim looks like a denim jacket. Each type now gets a few plain descriptions: "a pair of jeans", "blue denim jeans", "denim trousers with two legs".&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The centre crop&lt;/strong&gt;. CLIP crops to a square, which cut tall photos like jeans in half. Photos are now padded to a square instead.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A shortcut that cost accuracy&lt;/strong&gt;. I'd put phones on an 8-bit model to save memory. Now they use the 16-bit model, loaded in two halves (text first, then image) so it fits in phone memory.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With all three fixed, on the same 272 photos:&lt;/p&gt;

&lt;p&gt;📱 Phones: right clothing type went from 55% to 77%, and right category (top, bottom, shoes...) from 90% to 93%.&lt;br&gt;
💻 Laptops: right clothing type went from 72% to 77%, and right category from 92% to 93%.&lt;br&gt;
The cost: phones now download about 300 MB the first time instead of about 150 MB. The browser keeps it after that.&lt;/p&gt;

&lt;p&gt;The cost: phones now download about 300 MB the first time instead of about 150 MB. The browser keeps it after that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The budget chat: Gemma, Render and SerpApi&lt;/strong&gt;&lt;br&gt;
Ask "I have €50, what should I buy?" and this happens:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Gemma&lt;/strong&gt;(on your device) reads your wardrobe and the gaps the rules found, and plans up to three searches.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Only the short query ("rain jacket") and a price cap go to a Render server. It holds the SerpApi key, caches each search for six hours, and caps searches per IP and per day.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Rules pick one product per search so the total stays within budget, and Gemma writes a sentence on why each pick fits.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you name the item ("shoes under 20"), it skips planning and searches for that. If Gemma can't load on your device, the rules plan the searches and write a plain explanation, so the chat still works.&lt;/p&gt;

&lt;p&gt;One lesson for anyone running models in the browser: a failed first load can leave ONNX Runtime broken for the rest of the page. Now each model attempt gets a fresh web worker, so a failed WebGPU attempt doesn't break the WebAssembly fallback.&lt;/p&gt;

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

&lt;p&gt;StyleCast is only a viable side hustle because the models are open.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;🔒 Privacy. Nobody wants photos of their bedroom wardrobe on a stranger's server. An open model running in the browser means the photos never leave the phone. A hosted vision API can't promise that.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;💸 €0 per user. Closed APIs bill per photo and per message. Open weights on the user's device cost me nothing, which is what makes a €2 subscription work.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;✈️ Offline. Once the model is downloaded, tagging and outfit picks work without a connection. The last forecast is saved for mornings with bad signal.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🔧 Fixable. When CLIP got my jeans wrong, I could look inside: change the labels, change the preprocessing, compare precisions, and measure every change. A closed API would have left me rewording prompts.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🔁 Swappable. Gemma 1B drops to 270M on weaker devices, and the repo has a test page comparing CLIP with SmolVLM. No vendor decides that for me.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Transformers.js, ONNX Runtime Web, CLIP, Gemma and Open-Meteo did the heavy lifting. My part was gluing them together for one specific morning problem.&lt;/p&gt;

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

&lt;p&gt;My first users were two friends. They photographed their own clothes on their Android phones, and StyleCast got them right. Their verdict was "good, but it can be improved", which is fair. StyleCast is only a few days old, and their feedback is what goes into the next version.&lt;/p&gt;

&lt;p&gt;Before that, my first real test was my own wardrobe, and that's how the jeans bug turned up. That one wrong tag sent me from "it works on the demo photos" to measuring accuracy properly, and it was the most useful thing that happened all week. I've also run the full shopping flow live, with the app on GitHub Pages, the server on Render and real Google Shopping results from SerpApi.&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 runs entirely in the browser and powers the budget chat. It plans searches from the user's wardrobe gaps and explains each pick.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best use of Render&lt;/strong&gt;: the shopping server runs on Render from a one-click blueprint (render.yaml) and keeps the SerpApi key off users' devices.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best use of SerpApi&lt;/strong&gt;: Google Shopping results with a price cap turn wardrobe gaps into real products within budget.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>ai</category>
    </item>
    <item>
      <title>LevelOut: an anime-style System that levels you up at a real skill, outside</title>
      <dc:creator>SRG-Cy</dc:creator>
      <pubDate>Sun, 11 Oct 2026 12:13:23 +0000</pubDate>
      <link>https://dev.to/srgcy/levelout-an-anime-style-system-that-levels-you-up-at-a-real-skill-outside-3abj</link>
      <guid>https://dev.to/srgcy/levelout-an-anime-style-system-that-levels-you-up-at-a-real-skill-outside-3abj</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;p&gt;&lt;strong&gt;LevelOut&lt;/strong&gt; turns learning a real skill into a leveling anime.&lt;/p&gt;

&lt;p&gt;You know the trope: a weak hero gets chosen by a mysterious &lt;strong&gt;System&lt;/strong&gt; that hands out daily quests, and every completed quest makes them stronger. I wanted that feeling for real life, with the quests happening &lt;strong&gt;outside&lt;/strong&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pick a skill.&lt;/strong&gt; Football, cricket bowling, badminton footwork, skipping, a 5K, or type anything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tell the System what you have&lt;/strong&gt;: a wall, stairs, water bottles, a ball, or nothing at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Take the trial.&lt;/strong&gt; The open-weight model writes 6 questions for &lt;em&gt;your&lt;/em&gt; skill, including an injury check.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Awaken.&lt;/strong&gt; You get a rank (E to S), a title like &lt;em&gt;E-Rank Striker&lt;/em&gt;, five stats (STR / AGI / VIT / SKL / FOC), a 30-day goal, a rank-up test and a weekly plan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Daily Quest.&lt;/strong&gt; Warm-up, drills, cool-down, bonus. Every drill works &lt;strong&gt;alone, outdoors, with only the things you said you have&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pocket your phone.&lt;/strong&gt; Accepting the quest starts a timer and tells you to go.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Report back.&lt;/strong&gt; Rate each drill &lt;em&gt;Easy / Just right / Hard / Hurt / Skipped&lt;/em&gt;. You earn XP, stats and levels, and &lt;strong&gt;the next quest adapts&lt;/strong&gt;. Drills that hurt are replaced with gentler ones for a different body part.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To rank up you have to pass a &lt;strong&gt;real-world test&lt;/strong&gt; the System set for you, such as "juggle 25 times in a row". The game only moves when you do.&lt;/p&gt;

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

&lt;p&gt;🔗 &lt;strong&gt;Try it live:&lt;/strong&gt; &lt;a href="https://levelout.onrender.com" rel="noopener noreferrer"&gt;https://levelout.onrender.com&lt;/a&gt;&lt;/p&gt;


&lt;div&gt;
    &lt;iframe src="https://www.youtube.com/embed/8Lo6s_KjuCU" width="315" height="560"&gt;
    &lt;/iframe&gt;
  &lt;/div&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/SRG-Cy" rel="noopener noreferrer"&gt;
        SRG-Cy
      &lt;/a&gt; / &lt;a href="https://github.com/SRG-Cy/LevelOut" rel="noopener noreferrer"&gt;
        LevelOut
      &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;⚔️ LevelOut&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Train outside. Level up for real.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;LevelOut turns learning a real skill into a leveling anime. An open-weight AI model plays &lt;strong&gt;the System&lt;/strong&gt;: it tests you, assigns you a rank, and gives you a &lt;strong&gt;Daily Quest&lt;/strong&gt; of outdoor drills you can do &lt;strong&gt;alone, with little or no equipment&lt;/strong&gt;. Complete it, report back, and you gain XP, stats and levels, while the System adapts tomorrow's quest to how you actually did.&lt;/p&gt;
&lt;p&gt;Built for the &lt;strong&gt;MLH × DEV Hacktoberfest Open-Source AI Challenge, Week 1: "Touch Grass"&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/SRG-Cy/LevelOut/docs/quest.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FSRG-Cy%2FLevelOut%2FHEAD%2Fdocs%2Fquest.png" alt="Daily quest"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;How it plays&lt;/h2&gt;
&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Accept the System.&lt;/strong&gt; &lt;em&gt;"You have met the conditions to become a Player."&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Choose your path.&lt;/strong&gt; Pick any skill (football, cricket bowling, badminton footwork, skipping, a 5K, parkour, anything) and tick what you have: a wall, stairs, water bottles, a ball, a bench, or nothing at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The trial.&lt;/strong&gt; The model writes 6 assessment questions for &lt;em&gt;your&lt;/em&gt; skill, including…&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/SRG-Cy/LevelOut" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


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

&lt;p&gt;&lt;strong&gt;LevelOut runs an open-weight model, and it can run that model locally too.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Open-weight model:&lt;/strong&gt; the System runs on &lt;strong&gt;gpt-oss-120b&lt;/strong&gt;, an open-weight model (Apache 2.0), served by &lt;strong&gt;Groq&lt;/strong&gt; through its OpenAI-compatible API and called with the open-source &lt;code&gt;huggingface_hub&lt;/code&gt; client. Any open model works: Qwen through Hugging Face Inference Providers is built in, and Llama, Gemma or Mistral are one environment variable away.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local inference (optional):&lt;/strong&gt; set &lt;code&gt;LLM_BASE_URL&lt;/code&gt; to an &lt;strong&gt;Ollama&lt;/strong&gt;, &lt;strong&gt;llama.cpp&lt;/strong&gt; or &lt;strong&gt;vLLM&lt;/strong&gt; server and the same app runs entirely on weights you host yourself, with no code changes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The open model isn't decoration. &lt;strong&gt;It is the game master.&lt;/strong&gt; Without it there is no trial, no rank, no quest and no adaptation. It does three jobs, each with a strict JSON contract:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;What the open model does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Trial&lt;/td&gt;
&lt;td&gt;Writes level-check questions specific to the chosen skill&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Awakening&lt;/td&gt;
&lt;td&gt;Rates the Player honestly (most people start at E or D), sets stats, a 30-day goal, a rank-up test and a 7-day plan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Daily Quest&lt;/td&gt;
&lt;td&gt;Designs drills from the Player's own equipment, then &lt;strong&gt;adapts&lt;/strong&gt; them using the last report. Drills that hurt are replaced.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The game maths (XP curve, levels, rank gates, streaks, penalty quests) is deterministic JavaScript in the browser, so the model can't "inflate" your progress. You only level up by doing the drills.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Your training data stays yours.&lt;/strong&gt; Fitness and injury answers are personal. Because the System is an open-weight model, you can run the exact same app on your own machine or server with &lt;code&gt;LLM_BASE_URL&lt;/code&gt;, and nothing leaves it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No lock-in.&lt;/strong&gt; The model is a setting, not a dependency: gpt-oss today, Qwen, Llama or Gemma tomorrow, on any host or none.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anyone can audit the coach.&lt;/strong&gt; The prompts (&lt;code&gt;prompts.py&lt;/code&gt;) and the model weights are both public, so you can see exactly why the System gave you a drill, and change it if it's wrong for you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free to run.&lt;/strong&gt; Open models on free hosts, or on your own hardware, mean a student can use a personal coach without a subscription.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;strong&gt;Render.&lt;/strong&gt; LevelOut is deployed on Render's free tier from a &lt;code&gt;render.yaml&lt;/code&gt; Blueprint in the repo, so anyone can fork it and get their own System running in a few clicks: Render reads the build and start commands, pins Python, sets the open-model settings, and only asks you to paste your own API key, which never touches the repo. Two choices were shaped by running it as a public Render service: a 120-second gunicorn timeout, because reasoning models like gpt-oss can think for a while before answering, and a per-visitor limit on AI calls so a public link can't drain the key's free quota.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Built with Flask, vanilla JS, and the open-weight gpt-oss-120b model served by Groq. MIT licensed.&lt;/em&gt;&lt;/p&gt;

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