<?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: Kamelyoul</title>
    <description>The latest articles on DEV Community by Kamelyoul (@kamelyoul).</description>
    <link>https://dev.to/kamelyoul</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%2F4167487%2F62bf62ed-845c-4e8c-9419-e8f903c7a9e5.png</url>
      <title>DEV Community: Kamelyoul</title>
      <link>https://dev.to/kamelyoul</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kamelyoul"/>
    <language>en</language>
    <item>
      <title>Look Up: an open-weight model on your phone reads the clouds, then tells you to put the phone away</title>
      <dc:creator>Kamelyoul</dc:creator>
      <pubDate>Tue, 06 Oct 2026 23:13:36 +0000</pubDate>
      <link>https://dev.to/kamelyoul/look-up-an-open-weight-model-on-your-phone-reads-the-clouds-then-tells-you-to-put-the-phone-away-10og</link>
      <guid>https://dev.to/kamelyoul/look-up-an-open-weight-model-on-your-phone-reads-the-clouds-then-tells-you-to-put-the-phone-away-10og</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;&lt;strong&gt;Look Up&lt;/strong&gt; is a tiny web app for cloud spotting.&lt;/p&gt;

&lt;p&gt;You point your phone at the sky and take one photo. An open-weight vision model, running &lt;strong&gt;on the phone itself&lt;/strong&gt;, tells you which of the ten cloud families you're looking at, what that cloud usually means for the next few hours, and gives you one small thing to watch for. Then it asks you to put the phone away.&lt;/p&gt;

&lt;p&gt;That last part is the whole point. The best thing about clouds is that they &lt;em&gt;move&lt;/em&gt;: a cumulus grows or evaporates in five minutes, a contrail vanishes or spreads, a halo appears around the sun. You can only see that if you stop looking at a screen. So every reading ends with a mission:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Cumulus&lt;/strong&gt;: &lt;em&gt;Pick one cloud and watch it for five minutes. Is it growing upward, or fraying at the edges and evaporating?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cirrus&lt;/strong&gt;: &lt;em&gt;Pick one wisp and watch which way its tail is combed. That is the wind direction kilometres above you.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You tap &lt;strong&gt;Put the phone away&lt;/strong&gt;. The screen goes black and counts the time until you come back (you can lock the phone). When you return, you tap what the cloud did ("It grew", "It faded", "It moved on"), and the app keeps a small &lt;strong&gt;sky log&lt;/strong&gt; with one number I care about: &lt;strong&gt;how many seconds you spent looking up for every second you spent looking at the screen.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It's for anyone who has ever looked up and wondered "what &lt;em&gt;is&lt;/em&gt; that?", but especially for people who reach for the phone out of habit. This time, the phone sends you back to the sky.&lt;/p&gt;

&lt;p&gt;And when it sees a &lt;strong&gt;cumulonimbus&lt;/strong&gt;, there is no timer. The mission is "go indoors".&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%2Fg1f66042dg655qbuzdo8.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%2Fg1f66042dg655qbuzdo8.jpg" alt="Look Up home screen" width="800" height="366"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://look-up-sky-smoky.vercel.app" rel="noopener noreferrer"&gt;https://look-up-sky-smoky.vercel.app&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Open it on your phone, tap &lt;strong&gt;Get ready for no signal&lt;/strong&gt; once on Wi-Fi (it downloads the model, about 90 MB), then go outside and tap &lt;strong&gt;Read the sky&lt;/strong&gt;. No account and no install (you can add it to your home screen if you like).&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/Kamelyoul" rel="noopener noreferrer"&gt;
        Kamelyoul
      &lt;/a&gt; / &lt;a href="https://github.com/Kamelyoul/look-up" rel="noopener noreferrer"&gt;
        look-up
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Look Up — point your phone at the sky, learn the cloud, then put the phone away. On-device CLIP, works offline.
    &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;Look Up&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Point your phone at the sky. An open-weight model running on your phone names the clouds, tells you what they usually mean, and gives you a small thing to watch for. Then you put the phone away.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Built for the &lt;a href="https://dev.to/challenges/hacktoberfest-week1-2026-10-05" rel="nofollow"&gt;DEV Hacktoberfest Open-Source AI Challenge, Week 1: Touch Grass&lt;/a&gt; (October 5 to 11, 2026). The project and this repository were created during the challenge window.&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;ol&gt;
&lt;li&gt;
&lt;strong&gt;Read the sky&lt;/strong&gt;: take a photo of the sky (or pick one you already took).&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://huggingface.co/openai/clip-vit-base-patch16" rel="nofollow noopener noreferrer"&gt;CLIP ViT-B/16&lt;/a&gt; (open weights, MIT) runs &lt;strong&gt;in the browser&lt;/strong&gt; through &lt;a href="https://github.com/huggingface/transformers.js" rel="noopener noreferrer"&gt;Transformers.js&lt;/a&gt; and ONNX Runtime Web. It classifies the photo zero-shot into the 10 WMO cloud genera, plus &lt;em&gt;contrail&lt;/em&gt;, &lt;em&gt;clear sky&lt;/em&gt; and &lt;em&gt;not the sky&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;You get a card with what to check with your own eyes, what that cloud usually means for the next hours (hedged weather lore), and a &lt;strong&gt;mission&lt;/strong&gt;…&lt;/li&gt;
&lt;/ol&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/Kamelyoul/look-up" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Vanilla TypeScript and Vite, with no framework and no backend. MIT licensed. &lt;code&gt;npm test&lt;/code&gt; runs the 34 unit tests; &lt;code&gt;npm run eval&lt;/code&gt; reproduces the accuracy numbers below.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  The model: CLIP, zero-shot, in the browser
&lt;/h3&gt;

&lt;p&gt;The core is &lt;a href="https://huggingface.co/openai/clip-vit-base-patch16" rel="noopener noreferrer"&gt;OpenAI's CLIP ViT-B/16&lt;/a&gt;, an open-weight (MIT) image–text model. I run it with &lt;a href="https://github.com/huggingface/transformers.js" rel="noopener noreferrer"&gt;Transformers.js&lt;/a&gt; on top of ONNX Runtime Web (WebAssembly), using &lt;a href="https://huggingface.co/Xenova/clip-vit-base-patch16" rel="noopener noreferrer"&gt;Xenova's ONNX export&lt;/a&gt; at 8-bit.&lt;/p&gt;

&lt;p&gt;CLIP was never trained to classify clouds. It doesn't need to be: it maps images and sentences into the same space, so I describe each class in plain English and pick the closest description. Each of the 13 classes (the ten &lt;a href="https://cloudatlas.wmo.int/en/clouds-genera.html" rel="noopener noreferrer"&gt;WMO cloud genera&lt;/a&gt;, plus &lt;em&gt;contrail&lt;/em&gt;, &lt;em&gt;clear sky&lt;/em&gt; and &lt;em&gt;not the sky&lt;/em&gt;) gets 3 to 6 prompts like:&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="nx"&gt;prompts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a photo of cumulus clouds&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fluffy white cotton-ball cumulus clouds with flat bases in a blue sky&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;puffy cauliflower-shaped white clouds on a sunny day&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The descriptive prompts matter. CLIP has seen "fluffy white clouds" millions of times and "stratocumulus" far less.&lt;/p&gt;

&lt;h3&gt;
  
  
  The trick: the text half never ships
&lt;/h3&gt;

&lt;p&gt;CLIP has two towers: one for text and one for images. The prompts are fixed, so I run the &lt;strong&gt;text tower once, at build time, in Node&lt;/strong&gt;, and ship only the 43 resulting vectors: a 120 KB JSON file. The phone downloads only the &lt;strong&gt;vision&lt;/strong&gt; tower. That saves a download of tens of MB and makes classification a dot product:&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="c1"&gt;// score of a class = its best-matching prompt (max, not mean)&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;bank&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;dot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;dim&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;dot&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;bank&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;matrix&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;dim&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="nx"&gt;best&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&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;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;best&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dot&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// then softmax(100 * score) over the 13 classes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few design decisions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Max, not mean.&lt;/strong&gt; The "not the sky" class mixes very different things (grass, a room, a screen, a street). Averaging over them gives a blurry "average non-sky" that matches nothing well. So a class scores as its &lt;em&gt;best-matching&lt;/em&gt; prompt. On my test set, that rejected 22 of 24 non-sky photos, against 18 with averaging. Point it at your feet and it tells you to point it up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's allowed to be unsure.&lt;/strong&gt; If the top class is under 45%, the card says &lt;em&gt;"Altocumulus, or maybe Cirrocumulus — I'm torn"&lt;/em&gt;. Real skies often contain several genera at once, and I'd rather the app say so than bluff.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storms get special treatment.&lt;/strong&gt; If "cumulonimbus" is the top guess, the runner-up, or simply above 10%, the card adds a red line: &lt;em&gt;"This could also be a thunderstorm cloud. If it is tall and dark, or you hear thunder, go indoors instead."&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Did it actually work? I measured it.
&lt;/h3&gt;

&lt;p&gt;Zero-shot demos are easy to cherry-pick, so I built a small, reproducible test set from Wikimedia Commons: the first 12 JPEG photos in each cloud category whose file name names only that genus (132 sky photos), plus 24 photos of lawns and living rooms that the app should refuse. The evaluation script uses &lt;strong&gt;the exact code path the browser uses&lt;/strong&gt;: same weights, same prompts, same scoring.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric (CLIP ViT-B/16, 8-bit)&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Top-1 accuracy, 13 classes (chance ≈ 7.7%)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;43%&lt;/strong&gt; (57/132)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Correct class in the top 3&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;68%&lt;/strong&gt; (90/132)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Accuracy when the app says it's confident&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;69%&lt;/strong&gt; (25/36)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Contrails&lt;/td&gt;
&lt;td&gt;11/12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Thunderstorm photos that get the storm warning&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;9/12&lt;/strong&gt; (10 false alarms on 120 other skies)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sky photos wrongly rejected as "not the sky"&lt;/td&gt;
&lt;td&gt;0/132&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lawns and living rooms correctly rejected&lt;/td&gt;
&lt;td&gt;22/24&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;It's good at the distinctive skies (contrails, cumulus, the altocumulus "sheep"). It's honestly bad at the subtle ones: cirrostratus (2/12) and stratocumulus (1/12) mostly come back as something else. That's why the app shows its top guesses and phrases everything as "check with your own eyes". It's a field guide that nudges you to look, not an oracle. (Caveat: Commons labels are crowd-sourced, and many photos contain more than one genus.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Choosing the model with the numbers.&lt;/strong&gt; I also ran the smaller-patch sibling, CLIP ViT-B/32. It's twice as fast and &lt;em&gt;more&lt;/em&gt; precise when confident (84% vs 69%). But it caught only 5 of 12 thunderstorm photos where B/16 caught 9, and it raised more false alarms (16 vs 10). For an app that tells people when to go indoors, that settled it. I shipped B/16 and accepted the extra ~200 ms. The full per-image tables are in the repo (&lt;code&gt;eval/&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  The rest
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Offline&lt;/strong&gt;: a small service worker caches the app shell. Transformers.js stores the weights and the WASM runtime in the browser's Cache Storage, so after the first load it works in airplane mode.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Phone away" screen&lt;/strong&gt;: it's black, has a big counter and nothing else. Time is measured from timestamps, so it keeps counting if you lock the phone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sky log&lt;/strong&gt;: localStorage only. Nothing leaves the device.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tests&lt;/strong&gt;: 34 unit tests (Vitest) cover the scoring and the base64 embedding format. A test fails if someone edits the prompts without regenerating the embeddings. Another checks that the thunderstorm card can never offer a "watch it" timer. A GitHub Actions workflow runs the tests and the build on every push.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;For this app, open weights aren't a nice-to-have. Three things only work because of them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The sky is where the signal isn't.&lt;/strong&gt; Hills, beaches, fields and trails are exactly where you have one bar or none. A cloud app that calls a hosted vision API fails in the very place it's meant to be used. With open weights, the model is a file. The browser caches it and the app works in airplane mode.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Your photos stay yours.&lt;/strong&gt; Sky photos contain more than sky: rooftops, your street, sometimes your kids at the edge of the frame. Here, nothing is uploaded, because there's no server to upload to. I didn't have to write a privacy policy for a server I'd have to trust; there just isn't one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. It costs nothing to run, so it can stay free.&lt;/strong&gt; There is no API key, no per-request bill and no rate limit. The whole thing is a static site. If a thousand people use it on a sunny Saturday, my cost doesn't change.&lt;/p&gt;

&lt;p&gt;There's a fourth, more personal reason: &lt;strong&gt;I could open it up and change it.&lt;/strong&gt; Because the model is open, I could split it in two, run half of it at build time, and ship only the half the phone needs. You can't do that with a closed endpoint. The classes are just sentences in a TypeScript file too. Want to add &lt;em&gt;mammatus&lt;/em&gt; or &lt;em&gt;noctilucent clouds&lt;/em&gt;? Add a line, run &lt;code&gt;npm run embeddings&lt;/code&gt;, and redeploy. No retraining, no permission needed.&lt;/p&gt;

&lt;p&gt;Where closed models would win: a large hosted multimodal model would likely be more accurate on the hard pairs (altocumulus vs. cirrocumulus). But it would need a signal, see your photos, and cost money per look. For an app whose job is to get you &lt;em&gt;off&lt;/em&gt; the screen and &lt;em&gt;out&lt;/em&gt; where there's no Wi-Fi, a small model you can carry beats a big one you can't.&lt;/p&gt;

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

&lt;p&gt;Full disclosure: I built this with an AI coding agent (&lt;strong&gt;Claude Code&lt;/strong&gt;). It wrote most of the code, the tests and the evaluation scripts, and helped draft this post. Every number above comes from scripts in the repo that you can rerun (&lt;code&gt;npm test&lt;/code&gt;, &lt;code&gt;npm run eval&lt;/code&gt;), not from the agent's say-so. I don't have a DevRelay recording of the session.&lt;/p&gt;

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