<?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: Vytas</title>
    <description>The latest articles on DEV Community by Vytas (@vytasgavelis).</description>
    <link>https://dev.to/vytasgavelis</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%2F4148747%2F39cec85b-e701-4409-8a61-a6a47a29b5ca.jpg</url>
      <title>DEV Community: Vytas</title>
      <link>https://dev.to/vytasgavelis</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vytasgavelis"/>
    <language>en</language>
    <item>
      <title>Hue Hunt: a daily colour hunt for friends, judged by Gemma</title>
      <dc:creator>Vytas</dc:creator>
      <pubDate>Sun, 11 Oct 2026 06:18:15 +0000</pubDate>
      <link>https://dev.to/vytasgavelis/hue-hunt-a-daily-colour-hunt-for-friends-judged-by-gemma-4h78</link>
      <guid>https://dev.to/vytasgavelis/hue-hunt-a-daily-colour-hunt-for-friends-judged-by-gemma-4h78</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week1-2026-10-05"&gt;Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;I built Hue Hunt, a daily colour hunt for a group of friends.&lt;/p&gt;

&lt;p&gt;Every morning the group gets a colour and a bonus twist, for example "Yellow. Bonus: something that moves." During the day everyone goes out and photographs what they find. A yellow car, a bin lid, a sunflower. Nobody sees anyone else's photos until the evening reveal, when all of them appear side by side with points, a winner and a short, playful recap. A season runs for a week, with a leaderboard at the end.&lt;/p&gt;

&lt;p&gt;The judge is an AI model. It looks at each photo and answers a few questions: is the thing in the photo really yellow, does it move, what is it, and is it a photo of a screen or a poster rather than a real thing. It also writes a one-line comment on every photo, which turned out to be the part people react to most. "That tiny yellow logo is a bit lost in all that gorgeous red!" is a fair verdict on a red Ferrari submitted on a yellow day.&lt;/p&gt;

&lt;p&gt;The screen is the shortest part of the game. You read the colour in the morning, which takes five seconds. Taking a photo takes another five. Then you look at the reveal in the evening. Everything in between happens outside, and the game is a reason to look at things you walk past every day.&lt;/p&gt;

&lt;p&gt;It's for a group of friends, the kind who already have a chat group. There are no accounts. You share a link, people type a name, and their phone remembers them. The person who creates the group is the host and can set the reveal time, press "reveal now", or start a new season.&lt;/p&gt;

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

&lt;p&gt;Hue Hunt runs on a small private server for my friends and me, so there's no public link yet. Running it yourself takes five minutes, and the steps are in the next section.&lt;/p&gt;

&lt;p&gt;Here is a demo season, screenshotted from the real app. I seeded it with a folder of photos and let the real judge score them, so every point and comment below is Gemma's.&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%2Fe718m04ehvfzp1906ju9.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%2Fe718m04ehvfzp1906ju9.png" alt="Today's challenge: orange, bonus something with a face. The host's orange cat scored 15 points with the bonus." width="780" height="2186"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Day 3: orange, "something with a face". The cat has been judged: 15 points, bonus earned, and a comment.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F231wjbfqmrk0aycyyf9t.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%2F231wjbfqmrk0aycyyf9t.png" alt="The yellow day reveal: Maja wins with 35 points, the recap, and everyone's photos with points and comments." width="760" height="3556"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The reveal for the yellow day. Maja wins with a taxi, a raincoat and a sunflower. Every photo has its points and the judge's comment. Tom's sunlit grass got 0: "That golden sunlight is beautiful, but we're hunting for actual yellow objects!"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3dr2fi4qwivj6y5lzib3.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%2F3dr2fi4qwivj6y5lzib3.png" alt="The season leaderboard after two days." width="780" height="1688"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The season leaderboard after two days.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9msidhqfm2xnyie4n0fp.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%2F9msidhqfm2xnyie4n0fp.png" alt="The invite page: You're invited to Sunday Walkers, 4 players, type your name and join." width="780" height="1688"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The invite page. No account, no password. The small print says photos go to a third-party API and that location data is stripped.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsgw49dajnxtpbjd3dube.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%2Fsgw49dajnxtpbjd3dube.png" alt="The reveal share card: six photos in a grid under " width="800" height="420"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The share links carry preview cards, so a reveal unfurls in WhatsApp or iMessage as a collage of the day's best photos.&lt;/em&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/vytasgavelis" rel="noopener noreferrer"&gt;
        vytasgavelis
      &lt;/a&gt; / &lt;a href="https://github.com/vytasgavelis/hue-hunt" rel="noopener noreferrer"&gt;
        hue-hunt
      &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;Hue Hunt&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;A daily colour hunt for a group of friends, judged by an open-weight vision model.&lt;/p&gt;
&lt;p&gt;Every morning the group gets a colour and a bonus twist, for example &lt;em&gt;"Yellow. Bonus: something
alive."&lt;/em&gt; During the day everyone photographs what they find outside, straight from the phone's
camera in the browser. Nobody sees anyone else's photos until the evening reveal, when all the
photos appear side by side with scores, a winner and a short, playful recap. Seasons run a week
with a leaderboard at the end.&lt;/p&gt;
&lt;p&gt;Built for the DEV Hacktoberfest 2026 "Touch Grass" challenge. The screen is the shortest part of
the game: a few seconds to take a photo, one look at the reveal in the evening.&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;ul&gt;
&lt;li&gt;
&lt;strong&gt;Groups, no accounts.&lt;/strong&gt; Create a group, share the join link. Players pick a display name and the
phone remembers them with a random token in a cookie…&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/vytasgavelis/hue-hunt" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;The README has the full setup. In short, you need Node.js 24 and pnpm, and then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git clone https://github.com/vytasgavelis/hue-hunt.git
cd hue-hunt
cp .env.example .env     # add a free Gemini API key from Google AI Studio
pnpm i
pnpm db:migrate
pnpm dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then open &lt;a href="http://localhost:5201" rel="noopener noreferrer"&gt;http://localhost:5201&lt;/a&gt;. If you want to look at a few files, these are the interesting ones:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;apps/server/src/ai/tasks.ts&lt;/code&gt; has the four prompts: judge a photo, invent the day's challenge, write the evening recap, write the season recap.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;apps/server/src/ai/client.ts&lt;/code&gt; is the model client. It asks for JSON, validates it, retries once, and logs every call.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;packages/shared/src/scoring.ts&lt;/code&gt; turns the judge's answers into points. No AI in this file.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;apps/server/src/scripts/eval-judge.ts&lt;/code&gt; is the judging eval, which is how I chose the model and tuned the prompt.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;apps/server/src/lib/images.ts&lt;/code&gt; strips the metadata from every photo before it is stored.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Hue Hunt is a TypeScript app: a React web app, a Hono API, a SQLite database and a small job queue, all in one Node process on a $6 server. The AI side is one open-weight model, Gemma 4 26B-A4B from Google, called through the Vercel AI SDK. I call it on the Gemini API's free tier, so running Hue Hunt needs no GPU and no paid account, but the model is a published set of weights and nothing in the code depends on where it runs.&lt;/p&gt;

&lt;h3&gt;
  
  
  One model, four jobs
&lt;/h3&gt;

&lt;p&gt;Gemma does four things, and they're all single prompt-and-reply calls. No agents, no tools, no streaming.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Judging a photo&lt;/strong&gt; is the only task that sees images. The prompt gives the colour and the twist, and asks for exactly this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "colour_match": 0.0,          // number 0 to 1 on the scale above: how clearly the thing photographed is "yellow"
  "bonus_match": false,         // true only if the yellow thing in the photo itself satisfies the bonus twist "something that moves"
  "subject": "...",             // short noun phrase naming the main subject, e.g. "yellow taxi"
  "category": "...",            // one of: vehicle, plant, animal, building, sign, clothing, food, object, other
  "is_screen_or_print": false,  // true if this is a photo of a screen, a poster, a printed image or a drawing rather than a real thing
  "comment": "..."              // one playful line about the photo, at most 15 words, friendly never mean
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the part a pixel histogram can't do. "Yellow and alive" is a question about what the thing is, not what colour the pixels are. So is "it's just a tiny sign in the corner of a grey street", and so is "that's a photo of a laptop screen".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inventing the challenge&lt;/strong&gt; runs at 7 am in the group's timezone. The model gets the colours and twists already used this season and returns a new colour, a twist and a tagline. It likes "terracotta" and "cobalt blue" more than I expected.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The recaps&lt;/strong&gt; never see the photos. The evening recap gets the judge's notes for each player, the subject, points and comment of every photo, and writes three to five sentences naming the winner and the funniest find. The season recap does the same across the week.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scoring is code, not the model
&lt;/h3&gt;

&lt;p&gt;The model answers questions about a photo. Points are worked out by plain code that I can test:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;photoPoints&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Judged&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;is_screen_or_print&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;SCORING&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;screenOrPrintPoints&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bonus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bonus_match&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;colour_match&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;SCORING&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bonusMinColourMatch&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;colour_match&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;SCORING&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;colourPoints&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bonus&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;SCORING&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bonusPoints&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Colour match times 10, plus 5 for the twist, 0 for screens. Your best three photos count for the day, and a second photo in the same category counts half, so three photos of three yellow cars don't beat one car, one flower and one bin. The season adds 3 points for each daily win.&lt;/p&gt;

&lt;p&gt;The bonus only counts when the colour match is at least 0.5. That rule came from the eval. On a yellow day with "something alive", the model correctly called a green forest "alive", and without the rule it would have scored 5 points for a photo with no yellow in it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Getting JSON out of Gemma
&lt;/h3&gt;

&lt;p&gt;Gemma's native JSON schema mode let stray tokens into values in my last project, so this time I never turned it on. The prompt shows a filled-in template of the reply, with a comment on each field, and the server validates the reply with a Zod schema that the web app shares. The schema is forgiving where it can be: a colour match of "0.8" as a string is coerced to a number and clamped to 0 to 1, a category the model made up becomes "other". If the reply still doesn't validate, the call is retried once with a reminder. Of the 57 model calls logged while I built and tested it, none needed the retry.&lt;/p&gt;

&lt;p&gt;Setting Gemma's thinking to "minimal" matters a lot here. Judging a photo takes 3 to 5 seconds. The recaps are slower, around 30 seconds, because they are longer and I let the model be more creative.&lt;/p&gt;

&lt;h3&gt;
  
  
  Choosing the model with an eval
&lt;/h3&gt;

&lt;p&gt;I didn't want to guess, so I wrote &lt;code&gt;pnpm eval:judge&lt;/code&gt;. It runs the judge over a folder of photos for a given colour and twist and prints a table. The photos are named for what I expect, so the table reads like a test report.&lt;/p&gt;

&lt;p&gt;I ran it on nine photos for "yellow, something alive": a taxi, a raincoat, a sunflower, bananas, a duckling, a green forest, a red Ferrari, a photo of a smartphone screen, and a yellow mosaic on a wall.&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;Gemma 4 26B-A4B&lt;/th&gt;
&lt;th&gt;Gemma 4 31B&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Photos judged&lt;/td&gt;
&lt;td&gt;9 / 9&lt;/td&gt;
&lt;td&gt;5 / 9 (4 timeouts at 60 s)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Judgements I agreed with&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Median time per photo&lt;/td&gt;
&lt;td&gt;3.8 s&lt;/td&gt;
&lt;td&gt;40.8 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both models judged the photos the way I would have. The 31B is dense, the 26B is a mixture-of-experts model with about 4 billion active parameters, and it's ten times faster. That's the difference between a comment appearing while you're still looking at your photo and a comment appearing after you've put the phone away, so the 26B is the default.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tuning the prompt on real photos
&lt;/h3&gt;

&lt;p&gt;The first prompt was two sentences. Then I ran it on a second set of photos, the kind of thing you'd actually find on a walk, and the results were uneven:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Photo&lt;/th&gt;
&lt;th&gt;First prompt&lt;/th&gt;
&lt;th&gt;Final prompt&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Orange cat, "something alive"&lt;/td&gt;
&lt;td&gt;0.70 + bonus = 12 pts&lt;/td&gt;
&lt;td&gt;0.30 = 3 pts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Orange traffic cone&lt;/td&gt;
&lt;td&gt;0.10&lt;/td&gt;
&lt;td&gt;0.30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Yellow lid on a green bin&lt;/td&gt;
&lt;td&gt;0.40&lt;/td&gt;
&lt;td&gt;0.80&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sunlight on grass&lt;/td&gt;
&lt;td&gt;0.40 + bonus&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Field of sunflowers&lt;/td&gt;
&lt;td&gt;1.00 + bonus&lt;/td&gt;
&lt;td&gt;1.00 + bonus&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;An orange cat scoring 12 points on a yellow day was the problem. The model knew it was orange, its comment said so, but it gave 0.7 anyway. The cone, which is just as orange, got 0.1. And the bin lid, which is unmistakably yellow and the obvious point of the photo, got 0.4 because the bin around it is green.&lt;/p&gt;

&lt;p&gt;The fix was to tell the judge what I actually meant. The final prompt judges the hue strictly and the size leniently: any shade of the colour counts fully, a neighbouring hue caps at 0.3, a coloured part of a bigger object counts if it's clearly the point of the photo, and light cast by the sun doesn't count. It also gives an anchored scale, 1.0, 0.8, 0.5, 0.3, 0, with a sentence for each.&lt;/p&gt;

&lt;p&gt;One lesson from the iteration. My first version of that rule said "orange, gold or ginger is not yellow". Gemma took "gold" literally and dropped a golden-orange sunflower from 1.0 to 0.3. The rule became "lemon, mustard and golden yellow are all yellow" instead, and the sunflower came back. With a model this fast, the loop of change the prompt, run the eval, read the table takes a minute.&lt;/p&gt;

&lt;h3&gt;
  
  
  Photos and privacy
&lt;/h3&gt;

&lt;p&gt;Photos are sent to a third-party API for judging. The join page says so. Before a photo is stored, the server decodes it, rotates it the right way up, resizes it to 1280 px and writes a fresh JPEG. All metadata is gone, including the GPS location phones embed in every shot. There's a test that uploads a photo with GPS coordinates and asserts they are not in the file on disk. Photos are only visible to the group, and other people's photos are hidden until the reveal.&lt;/p&gt;

&lt;h3&gt;
  
  
  How I worked
&lt;/h3&gt;

&lt;p&gt;I wrote a spec first, then built it with Claude Code as my coding assistant over two days. The spec was the useful part. It settled the scoring rules, the data model and the job queue before any code existed, and it meant the first version came out working end to end, with 40 tests. I then spent my time on the eval and the prompt, which is where the quality of the game actually lives.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;The judge can be argued with.&lt;/strong&gt; A game with an AI referee only works if people trust the referee. Everything the judge is told is in one file, the prompt is plain text, and anyone can read it, run the eval on their own photos, and see the table. When my friends disagree with a score, I can show them the rule, and if they are right I change it. A closed, hosted judging feature would be a black box with a score coming out of it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The model I tested is the model I ship.&lt;/strong&gt; Gemma 4 26B-A4B is a set of published weights. The eval results above stay true, because the model can't be silently updated or retired under me. With a closed API, the provider can swap the model next month and the orange cat might be worth 12 points again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It can run anywhere, including at home.&lt;/strong&gt; Today the calls go to Google's free tier because it was the easiest. The provider is one environment variable, and the code has a second provider for any OpenAI-compatible server. A group that doesn't want its photos leaving the house can run the same model on a machine at home with Ollama or vLLM and get the same judge. That's a real option for a game where people photograph their street, their garden and their kids' bikes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost makes the game possible at all.&lt;/strong&gt; A group of five taking five photos a day is 25 model calls a day, plus a challenge and a recap. A small open model on a free tier makes that free. A frontier vision model at API prices would make a daily game for friends an expense nobody would pay.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The frameworks are open too.&lt;/strong&gt; The Vercel AI SDK is a library inside my app. When I wanted the image passed one way rather than another, or the thinking level set, I read the source instead of waiting for a feature.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Gemma.&lt;/strong&gt; Hue Hunt runs on Gemma 4 26B-A4B for every model call: judging photos, inventing the daily challenge and writing the recaps. It was chosen over Gemma 4 31B by the eval above.&lt;/p&gt;

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

&lt;p&gt;Hue Hunt is a first version, and it has gaps I know about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It isn't public yet. Anyone who can reach the server can create a group and upload photos, so before I open it up it needs limits on groups and uploads, and a job that deletes photos when a season ends.&lt;/li&gt;
&lt;li&gt;iPhones save photos as HEIC, and the server can't decode those yet. Safari hands the page a JPEG when you use the camera button, but a HEIC picked from the photo library is refused with a message.&lt;/li&gt;
&lt;li&gt;The recap takes 30 seconds. That's fine for an evening reveal, and too slow for anything else.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What I want to build next:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reactions on the reveal, so the group can disagree with the judge.&lt;/li&gt;
&lt;li&gt;A "judge's appeal": tap a score, write one line, and the model re-judges with your argument in the prompt.&lt;/li&gt;
&lt;li&gt;Running the eval on a few hundred real photos from a season, with the group's own verdicts as labels. With open weights, that's the dataset for a fine-tune.&lt;/li&gt;
&lt;li&gt;A self-hosted mode in the README with Ollama, for groups who want their photos to stay home.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
    </item>
    <item>
      <title>Enzyme: breaking research papers into facts you can check</title>
      <dc:creator>Vytas</dc:creator>
      <pubDate>Sun, 04 Oct 2026 21:13:08 +0000</pubDate>
      <link>https://dev.to/vytasgavelis/enzyme-breaking-research-papers-into-facts-you-can-check-43pc</link>
      <guid>https://dev.to/vytasgavelis/enzyme-breaking-research-papers-into-facts-you-can-check-43pc</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-weekend-2026-10-01"&gt;Hacktoberfest Weekend Challenge: Build for a Friend&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;I built Enzyme for a friend. They're a scientist who wants to create healthy-lifestyle content based on real, published evidence.&lt;/p&gt;

&lt;p&gt;That means reading a lot of papers. New studies come out every week on the topics they follow, like gut health, omega-3 and inflammation, or vitamin D and autoimmune thyroid disease. Most aren't worth their time. Some are only loosely related to the search, some were done in mice, and some are too small to tell you much. The ones that do matter are dense abstracts that take a while to read properly. Sorting through all of this by hand is slow, and it's easy to fall behind or miss a good paper.&lt;/p&gt;

&lt;p&gt;The name comes from what enzymes do: they break big molecules down into pieces the body can use. Enzyme does the same with research papers.&lt;/p&gt;

&lt;p&gt;My friend saves the topics they care about. They can describe a topic in plain English and Enzyme writes the search query for them. Enzyme then pulls in matching papers from Europe PMC, which also covers PubMed and preprints, and marks the ones that are new since their last visit.&lt;/p&gt;

&lt;p&gt;To cut the noise, they can filter by the information that comes with each paper. For example, they can show only meta-analyses and randomised trials, show only studies in humans, or hide preprints. Retracted papers are hidden by default.&lt;/p&gt;

&lt;p&gt;For any paper, Enzyme can write a short plain-language summary and a one-line takeaway, and pull out the key facts: who was studied, how many people, what dose, for how long, what was measured and what happened. Each fact comes with the exact sentence from the abstract it was taken from, and hovering over a fact highlights that sentence. If the abstract doesn't mention something, like the dose or who funded the study, Enzyme says "Not stated" instead of guessing.&lt;/p&gt;

&lt;p&gt;That last part matters most for what my friend wants to do. If you tell people something about their health, you should be able to show where it came from. With Enzyme, checking a fact means hovering over it instead of rereading the whole abstract.&lt;/p&gt;

&lt;p&gt;Tools like Elicit and Consensus already exist, and they're good. But they're built for everyone. Enzyme is built around my friend's topics, and it's the first step toward turning the papers they read into the content they make.&lt;/p&gt;

&lt;p&gt;When I showed it to them, their verdict was that scientists can now research and summarise papers fast, and at scale.&lt;/p&gt;

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

&lt;p&gt;Enzyme is a tool for one person, so for now it runs on my laptop and there's no public link. Here's a walkthrough using my friend's real searches:&lt;/p&gt;

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

&lt;p&gt;In the video:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I describe a topic in plain English and Enzyme writes the search query. For "gut health" it narrowed the results from about 121,000 papers to under 1,000.&lt;/li&gt;
&lt;li&gt;I pull the newest papers for a search.&lt;/li&gt;
&lt;li&gt;I filter the feed down to randomised trials and meta-analyses in humans.&lt;/li&gt;
&lt;li&gt;I summarise a paper and hover over its key facts to show the sentence each one came from.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I sped up the waits in the video. A summary takes about 15 seconds.&lt;/p&gt;

&lt;p&gt;The feed for one of my friend's searches, filtered to randomised trials and meta-analyses in humans. Summarised papers show their key numbers and a one-line takeaway right in the list.&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%2Fhv01gn9byn3aw7rq55f8.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%2Fhv01gn9byn3aw7rq55f8.png" alt="The feed for one of my friend's searches, filtered to human studies" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;An expanded paper. Hovering over "Dose / regimen" highlights the sentence it came from in the abstract: "1320 mg/day curcumin".&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%2Fiz6rnqhv7ofd8enjd8sw.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%2Fiz6rnqhv7ofd8enjd8sw.png" alt="Key facts for a curcumin trial. Hovering over the dose highlights " width="800" height="991"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Writing a search in plain English. Enzyme suggests a query and shows how many papers it finds before you save it.&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%2Fcpv769m7wh9u2vjse06c.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%2Fcpv769m7wh9u2vjse06c.png" alt="Writing a search from a plain-English description" width="528" height="685"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you want to run it yourself, the setup steps are in the README, linked in the next section.&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/vytasgavelis" rel="noopener noreferrer"&gt;
        vytasgavelis
      &lt;/a&gt; / &lt;a href="https://github.com/vytasgavelis/enzyme" rel="noopener noreferrer"&gt;
        enzyme
      &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;Enzyme&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;An enzyme breaks big molecules down into pieces the body can use. Enzyme the app does the same for
research papers: it gathers the new studies on the topics you follow and breaks each one down into
a short, checkable set of key facts.&lt;/p&gt;
&lt;p&gt;Built in one weekend for the DEV Hacktoberfest 2026 Weekend Challenge, "Build for a Friend".&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/vytasgavelis/enzyme/docs/feed.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%2Fvytasgavelis%2Fenzyme%2FHEAD%2Fdocs%2Ffeed.png" alt="The feed for &amp;quot;Anti-inflammatory diet&amp;quot;, filtered to human RCTs and meta-analyses, with key numbers and a one-line takeaway on summarised papers"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Who it's for&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;My friend has a master's in biochemistry, works in the field, and wants to create
healthy-lifestyle content based on real, published evidence. Their problem is keeping up: every week
brings new papers on the topics they care about, many of them off-topic, animal studies or weak
designs, and each one a dense abstract to read before you know if it matters.&lt;/p&gt;
&lt;p&gt;General tools for this already exist. Enzyme is built for them: their topics (anti-inflammatory diet
autoimmune disease, gut health), their filters (human studies, RCTs and meta-analyses…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/vytasgavelis/enzyme" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;The README has the full setup. In short, you need Node.js 24 and pnpm, and then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/vytasgavelis/enzyme.git
&lt;span class="nb"&gt;cd &lt;/span&gt;enzyme
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
pnpm i
pnpm db:migrate
pnpm seed
pnpm dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then open &lt;a href="http://localhost:5173" rel="noopener noreferrer"&gt;http://localhost:5173&lt;/a&gt;. The seed adds my friend's six saved searches. Searches, pulls and filters work without an API key. The AI features need a free key from Google AI Studio, which serves Gemma.&lt;/p&gt;

&lt;p&gt;If you only want to look at a few files, these are the interesting ones:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;packages/shared/src/card.ts&lt;/code&gt; checks every quote the model returns against the abstract.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;apps/server/src/ai/card-agent.ts&lt;/code&gt; is the agent that writes the key facts. Its comments explain the Gemma quirks I had to work around.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;apps/server/src/ai/query-agent.ts&lt;/code&gt; is the agent that turns plain English into a search query and checks the hit count with a tool.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;packages/shared/src/evidence.ts&lt;/code&gt; works out study type, species and retractions from the paper's metadata, without AI.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Enzyme is a TypeScript app: a React web app, a Hono API and a SQLite database. The AI side uses two open pieces. &lt;strong&gt;Gemma 4 26B-A4B&lt;/strong&gt; is an open-weight model from Google. &lt;strong&gt;Mastra&lt;/strong&gt; is an open-source TypeScript framework for building agents. I call Gemma through the Gemini API's free tier, so running Enzyme needs no GPU and no paid account.&lt;/p&gt;

&lt;h3&gt;
  
  
  Metadata first, AI second
&lt;/h3&gt;

&lt;p&gt;I decided early that the model shouldn't do anything the data can already do. Papers from Europe PMC come with publication types and MeSH terms, the subject tags that PubMed indexers add. Study type, humans or animals, and retractions are all worked out from those fields, so the filters are exact, instant and free. The model only does two jobs that metadata can't: reading an abstract, and turning a plain-English topic into a search query.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key facts you can check
&lt;/h3&gt;

&lt;p&gt;The study card comes from a Mastra agent. It gets a paper's title and abstract and returns 12 facts, a one-line takeaway and a short summary. For every fact, it has to copy the exact words from the abstract that state it. The reply is validated against a Zod schema that the server and the web app share.&lt;/p&gt;

&lt;p&gt;Then the server checks every quote against the abstract. If a quote isn't there, the fact is flagged with "check" in the UI. If the model says the abstract doesn't state something, the fact shows as "Not stated". Whether a fact is backed up is decided by plain code, not by the model.&lt;/p&gt;

&lt;p&gt;Getting clean output from Gemma took some work. Its native JSON schema mode let stray tokens into the values (one dose came back as "2.emannine 2.5 g"). When I gave it Mastra's generated schema description, it copied the description's structure instead of filling it in. Mastra's structured output can also work through the prompt, with your own format instructions, so I gave Gemma a filled-in template of the reply and kept the Zod validation. Setting Gemma's thinking to "minimal" brought a card down from over a minute to about 15 seconds.&lt;/p&gt;

&lt;h3&gt;
  
  
  An agent that tests its own search queries
&lt;/h3&gt;

&lt;p&gt;Europe PMC's query syntax is powerful, but my friend shouldn't have to learn it. A second Mastra agent turns a plain-English description into a query. It has one tool, &lt;code&gt;count_hits&lt;/code&gt;, which counts the papers the query finds and says whether that's too many, too few or about right (50 to 5,000). The agent revises the query and checks again, up to three times. For "gut health", it went from about 121,000 papers to 975.&lt;/p&gt;

&lt;p&gt;The tool doesn't hold any logic of its own. Each request passes its own checker to the tool through Mastra's request context. That checker counts the hits, limits how many checks the agent gets and records every query it tried. If the agent's final answer is worse than a query it checked earlier, the app uses the earlier one. So the agent chooses what to try, but my code counts the hits and picks the final answer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Choosing the model with a benchmark
&lt;/h3&gt;

&lt;p&gt;I didn't want to guess which model was best, so I wrote a benchmark with Mastra's scorers. It runs each model on the same 12 papers and scores the cards in three ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How many quotes are really in the abstract.&lt;/li&gt;
&lt;li&gt;How often the model says "unknown" for the fields abstracts usually leave out, like dose and funding, instead of guessing.&lt;/li&gt;
&lt;li&gt;How many claims in the summary are backed by the extracted facts. A second model judges this, and each model judged the other's cards, so neither marked its own work.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each score is a Mastra scorer. The simplest one, the quote check, looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;quoteCheckScorer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;createScorer&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ScoredPaper&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ScoredCard&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;quote-check-rate&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Share of the values the model gave whose quote is found in the title or abstract&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preprocess&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;run&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;quoteCheck&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;card&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generateScore&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;preprocessStepResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rate&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nf"&gt;notScorable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;no values&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generateReason&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;preprocessStepResult&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stated&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; of &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;given&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; values verified, &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;suspect&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; suspect, &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; unknown`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The faithfulness scorer has the same shape, but its &lt;code&gt;analyze&lt;/code&gt; step calls the judge model. Here are the results:&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;Gemma 4 26B-A4B&lt;/th&gt;
&lt;th&gt;Gemma 4 31B&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cards produced&lt;/td&gt;
&lt;td&gt;11 / 12&lt;/td&gt;
&lt;td&gt;10 / 12 (2 timeouts)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quotes found word for word in the abstract&lt;/td&gt;
&lt;td&gt;99%&lt;/td&gt;
&lt;td&gt;99%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Said "unknown" instead of guessing&lt;/td&gt;
&lt;td&gt;98%&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Summary claims supported by the facts&lt;/td&gt;
&lt;td&gt;84%&lt;/td&gt;
&lt;td&gt;69%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Median time per card&lt;/td&gt;
&lt;td&gt;14.2 s&lt;/td&gt;
&lt;td&gt;49.4 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both models extract facts equally well. The summary scores are a softer signal, since the judges are different models and most flagged claims were true but went beyond the extracted facts. The smaller model, a mixture-of-experts model with about 4 billion active parameters, is 3.5 times faster and never timed out, so it's the default. Switching models means changing one environment variable, because Mastra's model router takes a plain &lt;code&gt;provider/model&lt;/code&gt; string.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tracing
&lt;/h3&gt;

&lt;p&gt;Every agent run is traced with Mastra's observability: each model call with its tokens and timing, and each &lt;code&gt;count_hits&lt;/code&gt; call with its query and result. The traces go into a local SQLite file, and &lt;code&gt;pnpm traces&lt;/code&gt; prints them. Here is the query agent narrowing "supplements that reduce inflammation" in three checks (I shortened the queries):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ pnpm traces --agent query-writer

agent run: 'query-writer'  15518 ms
  input:  "supplements that reduce inflammation"
  model llm: 'gemma-4-26b-a4b-it'  in=3127 out=379
  tool tool: 'count_hits'  1427 ms
    in:  (supplement* OR vitamin* OR mineral* OR herb* ...) AND (inflammation OR inflammatory)
    out: {"hitCount":40748,"verdict":"Too many (aim for 50–5000). Narrow it."}
  tool tool: 'count_hits'  428 ms
    in:  ... AND (PUB_TYPE:"Review" OR PUB_TYPE:"Systematic Review" OR PUB_TYPE:"Meta-Analysis" OR PUB_TYPE:"Randomized Controlled Trial")
    out: {"hitCount":10912,"verdict":"Too many (aim for 50–5000). Narrow it."}
  tool tool: 'count_hits'  378 ms
    in:  ... AND (PUB_TYPE:"Systematic Review" OR PUB_TYPE:"Meta-Analysis")
    out: {"hitCount":911,"verdict":"Good: in the useful range."}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Traces also show when the problem isn't the model. When one suggestion failed, its trace showed every &lt;code&gt;count_hits&lt;/code&gt; call getting a 503 from Europe PMC, and the agent being told it had no checks left.&lt;/p&gt;

&lt;p&gt;Study card runs are simpler: one model call each, with the facts and their quotes in the output (shortened here too):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ pnpm traces --limit 3 --agent study-card

agent run: 'study-card'  16824 ms
  input:  "Title: Effect of 21-Day Omega-3 Polyunsaturated Fatty Acid Supplementation on Exercise-Induced Secretory Factors ..."
  output: {"design":{"quote":"randomized double-blind study","value":"randomized double-blind study","status":"stated"},
           "population":{"quote":"24 physically active men","value":"24 physically active men","status":"stated"}, ...}
  model llm: 'gemma-4-26b-a4b-it'  16822 ms  in=1268 out=749

agent run: 'study-card'  16906 ms
  input:  "Title: Role of the EPA: DHA dosing ratio in omega-3 supplements on blood fatty acid profiles and inflammation ..."
  output: {"design":{"quote":"systematic review and meta-analysis","value":"systematic review and meta-analysis","status":"stated"},
           "population":{"quote":"across 96 clinical trials","value":"96 clinical trials","status":"stated"}, ...}
  model llm: 'gemma-4-26b-a4b-it'  16903 ms  in=1319 out=730

agent run: 'study-card'  16034 ms
  input:  "Title: Omega-3 Fatty Acids Supplementation Improves Pulmonary Function and Clinical Outcomes in Critically Ill Patients ..."
  output: {"design":{"quote":"meta-analysis","value":"meta-analysis","status":"stated"},
           "sampleSize":{"quote":"29 studies involving 2551 critically ill patients", ...}, ...}
  model llm: 'gemma-4-26b-a4b-it'  16031 ms  in=1412 out=704
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How I worked
&lt;/h3&gt;

&lt;p&gt;I built Enzyme over the weekend with Claude Code as my coding assistant. I built the UI first against mock data, to settle how it should work before writing the API.&lt;/p&gt;

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

&lt;p&gt;My friend works in science, where a result counts when someone else can check it. A tool that helps them read research should follow the same rule, and open models and tools make that possible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The model I tested is the model I ship.&lt;/strong&gt; Gemma 4 26B-A4B is a set of published weights. The version I benchmarked is a fixed file that anyone can download and inspect, and it will still exist next year. With a closed model behind an API, the provider can update or retire it at any time, and you have no way to see what changed. For a tool whose job is getting facts right, I want my benchmark results to stay true. One honest caveat: different providers can serve the same open model in different ways, for example at lower precision. Real consistency means pinning the provider too, but with open weights that choice is mine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No lock-in.&lt;/strong&gt; Today the model calls go to Google's Gemini API, because the free tier made it easy. Nothing ties Enzyme to it. Any host that serves Gemma will do, and so will my friend's own hardware if they ever want their reading to stay private. Because Mastra's model router takes a plain &lt;code&gt;provider/model&lt;/code&gt; string, moving is a one-line change in &lt;code&gt;.env&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It can learn from them.&lt;/strong&gt; The next step is letting my friend correct and verify the key facts. Every corrected card is an example of exactly the output they want. With open weights, those examples can be used to fine-tune the model for their topics. A closed model offers that only if its provider decides to.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost.&lt;/strong&gt; Keeping up with research means hundreds of papers a week. A small open model that writes a card in 15 seconds on a free tier makes that affordable for one person.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The framework is open too.&lt;/strong&gt; Mastra runs inside my app, so the traces and benchmark scores stay on my machine as local files. When Gemma struggled with structured output, I could change how Mastra asked for JSON instead of waiting for a fix from someone else.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Mastra.&lt;/strong&gt; Mastra is the AI layer of Enzyme, and I used more of it than plain model calls:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Agents:&lt;/strong&gt; one writes the study card, one writes search queries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tools:&lt;/strong&gt; the query agent checks its own queries with &lt;code&gt;count_hits&lt;/code&gt;, and the request context gives the tool the current request's checker.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structured output:&lt;/strong&gt; the reply is validated with Zod, using prompt-based JSON with my own format instructions to work around Gemma's quirks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scorers:&lt;/strong&gt; the model benchmark is built from them, including an LLM judge where each model judges the other.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observability:&lt;/strong&gt; every agent run is traced to a local file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The model router:&lt;/strong&gt; switching models is one environment variable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Gemma.&lt;/strong&gt; Enzyme runs on Gemma 4 26B-A4B, chosen over Gemma 4 31B by a benchmark on my friend's own topics.&lt;/p&gt;

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

&lt;p&gt;Enzyme is a first version, and it has gaps I know about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It only reads abstracts, so details that appear only in the full paper show as "Not stated".&lt;/li&gt;
&lt;li&gt;About 1 card in 12 fails validation and has to be generated again.&lt;/li&gt;
&lt;li&gt;Recent papers often don't have MeSH terms yet, so the "Humans only" filter hides them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What I want to build next, roughly in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Corrections and verification.&lt;/strong&gt; My friend can edit any fact and mark a card as checked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drafting posts from verified cards,&lt;/strong&gt; the step toward their content plans.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A weekly digest&lt;/strong&gt; of new papers on their topics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full text&lt;/strong&gt; for open-access papers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fine-tuning Gemma&lt;/strong&gt; on their verified cards.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A private deployment&lt;/strong&gt; on a small server behind Tailscale, so they can use it without my laptop.&lt;/li&gt;
&lt;/ol&gt;

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