<?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: Cian</title>
    <description>The latest articles on DEV Community by Cian (@mutaician).</description>
    <link>https://dev.to/mutaician</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%2F1071931%2F29ff7a98-a20e-4902-884d-00465e4c3e24.png</url>
      <title>DEV Community: Cian</title>
      <link>https://dev.to/mutaician</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mutaician"/>
    <language>en</language>
    <item>
      <title>KEMT: Benchmarking AI Translation Across Five Kenyan Languages</title>
      <dc:creator>Cian</dc:creator>
      <pubDate>Fri, 09 Oct 2026 17:54:52 +0000</pubDate>
      <link>https://dev.to/mutaician/kemt-benchmarking-ai-translation-across-five-kenyan-languages-92o</link>
      <guid>https://dev.to/mutaician/kemt-benchmarking-ai-translation-across-five-kenyan-languages-92o</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/kaggle-2026-09-23"&gt;Kaggle Benchmarking Challenge&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This would probably needs its own research paper to explain everything, but here's a shorter version? 😅&lt;/p&gt;

&lt;p&gt;It started with the conversations about pacing the frontier. I was reading posts from the people building these models, talking about how far AI had come and what needed to happen next. Meanwhile, I was thinking about the languages we speak here in Kenya.&lt;/p&gt;

&lt;p&gt;If these systems are already at the frontier, where does that leave Kalenjin?&lt;/p&gt;

&lt;p&gt;Or Kikuyu, Dholuo, Somali and Maasai?&lt;/p&gt;

&lt;p&gt;That question became &lt;strong&gt;“Frontier for Whom?”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;iframe class="tweet-embed" id="tweet-2098773920774074715-878" src="https://platform.twitter.com/embed/Tweet.html?id=2098773920774074715"&gt;
&lt;/iframe&gt;

  // Detect dark theme
  var iframe = document.getElementById('tweet-2098773920774074715-878');
  if (document.body.className.includes('dark-theme')) {
    iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=2098773920774074715&amp;amp;theme=dark"
  }



&lt;/p&gt;

&lt;p&gt;&lt;em&gt;One of the conversations that got me thinking. &lt;a href="https://x.com/DarioAmodei/status/2098773920774074715" rel="noopener noreferrer"&gt;Original post&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I even wrote a joke in my notes: if frontier AI goes rogue here, perhaps speaking our local languages will buy us some time 😂. The thought behind the joke was something I wanted to investigate: how much of the progress people celebrate reaches the languages around me?&lt;/p&gt;

&lt;h3&gt;
  
  
  The first idea did not survive the first experiments
&lt;/h3&gt;

&lt;p&gt;My initial plan was broader: reading, mathematics and intent recognition across &lt;strong&gt;14 African languages&lt;/strong&gt;—Amharic, Hausa, Igbo, Kinyarwanda, Lingala, Luganda, Oromo, Shona, Southern Sotho, Swahili, Wolof, Xhosa, Yoruba and Zulu—with English as a reference. I expected to find obvious weaknesses.&lt;/p&gt;

&lt;p&gt;What really pushed me to reconsider was &lt;strong&gt;Swahili&lt;/strong&gt;. From my own interactions and judgment, current models were already doing well at the things I wanted to test. I was looking for a gap that was more visible in languages I knew were still underserved.&lt;/p&gt;

&lt;p&gt;The development pilots helped too. Several models got all nine examples right after I corrected an overly strict output parser. That small hosted pilot covered English, Amharic and Yoruba; my Swahili observation came from my own experience. I had not run the whole 14-language plan.&lt;/p&gt;

&lt;p&gt;I had started with a conclusion I wanted to justify. The early results challenged it. I needed to let the question evolve instead of making the benchmark produce the answer I expected.&lt;/p&gt;

&lt;p&gt;So I came back to a problem I knew: translation in Kenyan languages.&lt;/p&gt;

&lt;p&gt;Initially, I had worried that Kalenjin was too niche. But I speak it. I have worked on it. I could look at an output and have a personal view of whether it made sense. That experience was useful.&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%2F7te78rjsyak32is7p7cb.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%2F7te78rjsyak32is7p7cb.png" alt="Four stages of the project: broad African-language tasks, Kenyan translation, harness experiments, and fresh native Kaggle runs." width="800" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Choosing data I could actually work with
&lt;/h3&gt;

&lt;p&gt;I chose &lt;a href="https://huggingface.co/Anv-ke" rel="noopener noreferrer"&gt;African Next Voices Kenya&lt;/a&gt;, a dataset I was familiar with. There were other options, but I knew more about this one and how its text was collected.&lt;/p&gt;

&lt;p&gt;KEMT—the &lt;strong&gt;Kenyan Machine Translation Benchmark&lt;/strong&gt;—evaluates general-purpose models translating between English and five languages, in both directions:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;English ↔ Kalenjin, Kikuyu, Dholuo, Somali and Maasai.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The initial translation cohort contained &lt;strong&gt;548 English prompts&lt;/strong&gt; shared across all five languages. Once I included the distinct local variants and both directions, that became &lt;strong&gt;5,637 translation items per model&lt;/strong&gt;. The cost calculations made me scale it down: I selected &lt;strong&gt;162 English prompts&lt;/strong&gt;, retaining their distinct local variants, for &lt;strong&gt;1,757 items per model&lt;/strong&gt;. The references came from the dataset; I did not ask another model to invent the “correct” translations.&lt;/p&gt;

&lt;p&gt;Those variants mattered. These language labels can cover different speech varieties: the &lt;a href="https://huggingface.co/Anv-ke" rel="noopener noreferrer"&gt;dataset documents&lt;/a&gt; Nandi and Kipsigis under Kalenjin, for example, and several Kikuyu dialects. I retained the supplied alternative wording, although I did not classify every variant by dialect or claim dialect-specific results.&lt;/p&gt;

&lt;h3&gt;
  
  
  Kikuyu characters replaced by &lt;code&gt;?&lt;/code&gt;: check the file history
&lt;/h3&gt;

&lt;p&gt;The Kikuyu export had a problem before I could even evaluate a model: some &lt;code&gt;ĩ&lt;/code&gt; and &lt;code&gt;ũ&lt;/code&gt; characters had become question marks. Here is one actual example for the English prompt “How do you eat this?”:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Damaged export:  ?r?aga at?a irio ici?
Older export:   Å©rÄ©aga atÄ©a irio ici?
Recovered text: ũrĩaga atĩa irio ici?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The AI suggestions at this point were to contact the dataset creators. I wanted to try something simpler before abandoning the Kikuyu part: &lt;strong&gt;what if an earlier version still had the characters?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I went back through the Hugging Face file history. An &lt;a href="https://huggingface.co/datasets/Anv-ke/kikuyu/blob/30ad575238928448713f45bb5b92a2679b27bd41/train/unscripted/files/transcripts.csv" rel="noopener noreferrer"&gt;older pinned export&lt;/a&gt; had garbled characters, but they were still recoverable. In this file, encoding that older text as Windows-1252 and decoding it as UTF-8 restored them. We checked that the conversion was reversible and that the recovered strings matched the damaged versions when the lost characters were replaced by &lt;code&gt;?&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That recovered &lt;strong&gt;174 selected variants&lt;/strong&gt;. I inspected samples before using them; the English selection and other four languages stayed the same.&lt;/p&gt;

&lt;p&gt;If you find a similar problem, check the repository's file history before dropping the language. A literal &lt;code&gt;?&lt;/code&gt; has already lost information, so changing the encoding of the damaged file alone will not bring it back. The useful part here was finding an older copy that still preserved it. If the dataset maintainers are reading this, a corrected export would save the next researcher this detour.&lt;/p&gt;

&lt;p&gt;That was one of the early reminders that preparing a benchmark is also data work. A leaderboard cannot tell you whether the text you fed it was damaged.&lt;/p&gt;

&lt;p&gt;The cohort is deliberately small and curated. It uses public training-export text, so I cannot claim the models have never seen it. It is a starting point for comparison, not a representative survey of everything spoken in these languages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Models Tested
&lt;/h2&gt;

&lt;p&gt;I wanted broad coverage: different labs, different sizes, and both expensive and cheaper models. Ideally, I wanted at least one top model from each of the seven labs available in Kaggle's catalogue.&lt;/p&gt;

&lt;p&gt;Access and successful completion turned out to be two different things.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;9 October snapshot has 15 complete native evaluations&lt;/strong&gt;, each covering all 1,757 items:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;Completed models&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Google&lt;/td&gt;
&lt;td&gt;Gemini 3.1 Pro Preview; Gemini 3.7 Flash; Gemini 3.8 Flash; Gemini 3.6 Flash; Gemini 3.5 Flash-Lite&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI&lt;/td&gt;
&lt;td&gt;GPT-6 Astra; GPT-6.1 Sol; GPT-5.6 Sol; GPT-6 Luna; GPT-OSS 20B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anthropic&lt;/td&gt;
&lt;td&gt;Claude Opus 5.5; Claude Opus 5; Claude Sonnet 5.5; Claude Haiku 4.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Z.ai&lt;/td&gt;
&lt;td&gt;GLM-5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Qwen 3 Next 80B Instruct completed four languages, but its Kalenjin run timed out after saving 280 of 384 translations. I have kept it outside the complete-model ranking. Earlier Qwen 3 235B attempts encountered heavy-load errors, while Grok returned a model-access error. Those are missing evaluations, not zero-quality translations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Before all this, there were the subscription harnesses
&lt;/h3&gt;

&lt;p&gt;The first practical obstacle was budget. My initial Kaggle allowance was $10 a day and $100 a month. After projecting the cost of the larger cohort, I started looking for cheaper ways to do the work.&lt;/p&gt;

&lt;p&gt;Come on, a fellow working with a limited budget here in Kenya has to try something with what he has 😅.&lt;/p&gt;

&lt;p&gt;I normally use &lt;strong&gt;Codex&lt;/strong&gt; for development. My $20 subscription already gave me access to models such as Astra through that tool, using subscription allowances rather than paying separately for every API token. That made it a practical route to explore, within its usage limits.&lt;/p&gt;

&lt;p&gt;I also had to sacrifice some cash for &lt;strong&gt;Claude Code&lt;/strong&gt; to access the Anthropic models, and &lt;strong&gt;CommandCode&lt;/strong&gt; for some open models. I tried &lt;strong&gt;OpenCode&lt;/strong&gt; routes too. For Google models, &lt;strong&gt;Antigravity&lt;/strong&gt; helped because I already had a student Pro account. I wanted to try Grok models through Cursor, but my student subscription had ended and the budget did not stretch to another subscription.&lt;/p&gt;

&lt;p&gt;I ran translations through these harnesses, saved the responses and scored them locally. Between the subscriptions, API estimates, batches and retries, I began to appreciate why running large evaluations—the kinds associated with benchmarks such as ARC-AGI—can become expensive. Even my much smaller experiment needed compromises.&lt;/p&gt;

&lt;p&gt;Those experiments were useful. They also taught me that a model name alone does not describe an evaluation. The harness, its instructions, available controls and the way it returns output can change the result.&lt;/p&gt;

&lt;p&gt;I wanted those runs on the native Kaggle leaderboard, so I emailed the Kaggle Benchmarks team. Their reply clarified that native results had to come through Kaggle's managed inference pipeline. External harness results could not simply be imported. I paused that exploration and focused on Kaggle models so I could complete the native benchmark.&lt;/p&gt;

&lt;h3&gt;
  
  
  A reply that gave me more than quota
&lt;/h3&gt;

&lt;p&gt;I did not expect to receive a reply from &lt;strong&gt;Addison Howard&lt;/strong&gt;, whose signature read &lt;strong&gt;Head of Kaggle Competitions Program Management&lt;/strong&gt;. Seeing someone in that position take the benchmark seriously gave me even more motivation to finish it.&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%2F5ai9rysgjl0mv572srqc.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%2F5ai9rysgjl0mv572srqc.png" alt="Addison Howard's reply confirming the quota increase and explaining that the team was investigating temporary Grok and Qwen issues." width="800" height="326"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This was more than additional credits to me. It was encouragement to keep going.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;My allowance was raised to &lt;strong&gt;$50 a day and $500 a month&lt;/strong&gt;. Addison also explained that the team was debugging temporary Grok and Qwen issues on the backend. That helped put the access failures in context.&lt;/p&gt;

&lt;p&gt;I had originally aimed for one frontier model from each lab. With the extra quota, I could explore more of the available models and pushed the completed native roster to &lt;strong&gt;15&lt;/strong&gt;. By my rough estimate, the Kaggle side of the experiment used &lt;strong&gt;around $100 in inference credit&lt;/strong&gt; overall; the subscriptions were separate.&lt;/p&gt;

&lt;p&gt;I still plan to continue the local harness runs and compare them with the Kaggle results later, keeping each route clearly labelled. For this submission, the results come from the fresh native Kaggle runs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Batching was clever… until it became more work
&lt;/h3&gt;

&lt;p&gt;At first, I packed multiple translations into a request to save on prompt overhead. That was a reasonable idea, and some models handled it well.&lt;/p&gt;

&lt;p&gt;Others did not. There were formatting problems, output-ID problems, provider configuration errors, rate limits, and one particularly memorable GLM response:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I apologize, but I cannot provide translations for these specific texts as they appear to be part of a translation evaluation or test set.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I was, indeed, trying to run a translation evaluation. The model had correctly identified the occasion and then declined the invitation 😅.&lt;/p&gt;

&lt;p&gt;I needed a rule that worked consistently across models. Explicit refusals would receive empty translations and be scored accordingly. Provider or transport failures would leave the run incomplete, rather than being silently counted as wrong answers.&lt;/p&gt;

&lt;p&gt;Eventually, I switched the native tasks to &lt;strong&gt;one sentence per request&lt;/strong&gt;, with the model asked to return only its translation. I scored the returned text without requiring a JSON envelope or model-generated IDs.&lt;/p&gt;

&lt;p&gt;That removed a whole category of formatting failures. It also meant 1,757 requests for each new complete model evaluation. The waiting became a real part of the project. At one point my notes for the day were basically: launch a run, then wait.&lt;/p&gt;

&lt;p&gt;Single requests did not make provider limits disappear. Running several language jobs together still produced heavy-load problems. I went back to launching one language at a time.&lt;/p&gt;

&lt;p&gt;For development, preserving accepted responses helped me continue interrupted work. For the final comparison, I removed the automatic reuse of old answers and ran the complete models afresh under the common single-sentence protocol. Otherwise, pressing “run” could just rescore saved answers—which was not what I wanted readers to assume had happened.&lt;/p&gt;

&lt;h3&gt;
  
  
  Even presenting the results needed a few attempts
&lt;/h3&gt;

&lt;p&gt;The first leaderboard showed a score around 44.97 as &lt;strong&gt;4497%&lt;/strong&gt;. That was a display-scale mistake, not a sudden breakthrough in translation.&lt;/p&gt;

&lt;p&gt;The fix was to return chrF++ divided by 100 to Kaggle. The header then shows about &lt;strong&gt;0.45&lt;/strong&gt;, while a task cell shows about &lt;strong&gt;45%&lt;/strong&gt;. Both represent roughly &lt;strong&gt;45 chrF++ points&lt;/strong&gt;, not 45% of translations being correct.&lt;/p&gt;

&lt;p&gt;Kaggle's interface also proved challenging when I wanted a different layout or a more interactive view. I had a local table with model rows, several metrics and filters. The native collection used its own layout. Even the collapsible section I tried was flattened by the editor. There was quite a bit of experimenting just to make the page readable.&lt;/p&gt;

&lt;p&gt;I split the collection into five language tasks so language differences were visible. BLEU, chrF and TER went into a separate results section. I placed both directions on the same row to use the empty horizontal space, then added a divider between them. The screenshot below is where that work ended up:&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%2F41a6xgkama8pcgeuixct.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%2F41a6xgkama8pcgeuixct.png" alt="KEMT's supporting results table showing both translation directions side by side for Kalenjin." width="800" height="631"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Getting the benchmark to run was one job. Making its results understandable was another.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Findings
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Gemini 3.1 Pro surprised me
&lt;/h3&gt;

&lt;p&gt;I expected the more recent models to have an advantage. Instead, &lt;strong&gt;Gemini 3.1 Pro Preview led this snapshot at 44.49 chrF++&lt;/strong&gt;, followed by &lt;strong&gt;GPT-6 Astra at 43.33&lt;/strong&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%2Fvwryqyv1u6vsqogoy11e.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%2Fvwryqyv1u6vsqogoy11e.png" alt="Overall chrF++ results for all 15 complete native models, with Gemini 3.1 Pro Preview first and GPT-6 Astra second." width="800" height="704"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Scores use the equal-weight mean of ten language/direction corpus scores. All models shown completed the same frozen cohort.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Gemini 3.7 Flash followed at &lt;strong&gt;42.74&lt;/strong&gt;, then GPT-6.1 Sol at &lt;strong&gt;42.61&lt;/strong&gt; and Gemini 3.8 Flash at &lt;strong&gt;42.17&lt;/strong&gt;. The top two were separated by about &lt;strong&gt;1.16 points&lt;/strong&gt;. I would want repeated runs and uncertainty estimates before treating a small gap as a settled ranking.&lt;/p&gt;

&lt;p&gt;The Claude models I tested scored lower overall: Opus 5.5 at &lt;strong&gt;41.35&lt;/strong&gt;, Opus 5 at &lt;strong&gt;40.83&lt;/strong&gt;, Sonnet 5.5 at &lt;strong&gt;32.31&lt;/strong&gt;, and Haiku 4.5 at &lt;strong&gt;23.76&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;My initial impression was that these languages might be less well represented in those models. But I cannot establish their training-data representation from these results. There is also an important behavioural difference: the fresh Sonnet run contained &lt;strong&gt;67 explicit refusals&lt;/strong&gt;, and Haiku contained &lt;strong&gt;213&lt;/strong&gt;. Their scores include those empty translations. So this comparison measures what the models returned under this prompt and protocol, including refusals.&lt;/p&gt;

&lt;p&gt;Even the overall ranking hides exceptions. For English → Kalenjin, Opus 5 scored &lt;strong&gt;27.72&lt;/strong&gt;, above Gemini 3.1 Pro's &lt;strong&gt;26.97&lt;/strong&gt;, despite ranking lower overall. One headline cannot describe every language and direction.&lt;/p&gt;

&lt;h3&gt;
  
  
  Going into English was generally easier than coming back
&lt;/h3&gt;

&lt;p&gt;For Gemini 3.1 Pro, the mean across the five local → English cells was &lt;strong&gt;49.47&lt;/strong&gt;, compared with &lt;strong&gt;39.52&lt;/strong&gt; for English → local. Astra showed a similar difference: &lt;strong&gt;48.91&lt;/strong&gt; versus &lt;strong&gt;37.76&lt;/strong&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%2F71plkb7t5lv9c8jppvpr.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%2F71plkb7t5lv9c8jppvpr.png" alt="Heatmap of all ten language/direction scores for each complete model, showing the differences between translating into English and into local languages." width="800" height="555"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Each cell is a chrF++ corpus score. The two directions use different source sets and reference structures&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Somali had substantially higher overlap scores than Kalenjin and Maasai for these three models:&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%2Fqf3rhzdzh63em0dkem8p.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%2Fqf3rhzdzh63em0dkem8p.png" alt="Language-level chrF++ comparison for Gemini 3.1 Pro Preview, GPT-6 Astra and Claude Opus 5." width="800" height="472"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That pattern is useful to investigate. It does not, by itself, prove how much training data any language received. The references, spelling, morphology and source variants also matter.&lt;/p&gt;

&lt;h3&gt;
  
  
  The most important finding was outside the score
&lt;/h3&gt;

&lt;p&gt;Here is where being a speaker of one of the languages came back into the experiment.&lt;/p&gt;

&lt;p&gt;When I read some of the Kalenjin outputs, I recognised Kalenjin words. But I could not make sense of the sentence as natural Kalenjin. Some text looked like the language without communicating what the source was meant to say.&lt;/p&gt;

&lt;p&gt;That is a personal observation from reading outputs, not a formal human audit of the whole cohort. Still, it changed how I looked at the leaderboard.&lt;/p&gt;

&lt;p&gt;Recognisable words can coexist with incorrect meaning, unnatural combinations or hallucinated content. A higher overlap score does not settle whether a speaker would understand or trust the translation.&lt;/p&gt;

&lt;p&gt;KEMT's headline metric is &lt;strong&gt;chrF++&lt;/strong&gt;. It compares character and word-sequence overlap with supplied references. &lt;strong&gt;BLEU, chrF and TER&lt;/strong&gt; provide supporting views; I do not average these different metrics into the leaderboard score. The implementation uses fixed &lt;a href="https://github.com/mjpost/sacrebleu" rel="noopener noreferrer"&gt;SacreBLEU 2.5.1 settings&lt;/a&gt;. Useful as those measurements are, they do not directly establish understanding or fluency.&lt;/p&gt;

&lt;p&gt;This is also why I cannot conclude that Gemini “understands Kalenjin” simply because it won the aggregate. It performed best on this particular automatic comparison. That is encouraging, and it gives me a model worth investigating more closely.&lt;/p&gt;

&lt;p&gt;I looked into learned evaluation metrics too. Researchers have already worked on this through &lt;strong&gt;&lt;a href="https://aclanthology.org/2024.naacl-long.334/" rel="noopener noreferrer"&gt;AfriCOMET&lt;/a&gt;&lt;/strong&gt;. Its &lt;a href="https://huggingface.co/masakhane/africomet-stl-1.1#languages-covered" rel="noopener noreferrer"&gt;1.1 checkpoint documents coverage&lt;/a&gt; for Kikuyu, Luo and Somali, but not Kalenjin or Maasai. I did not use it as a common metric across all five languages because reliable coverage for the full set was not established. That is different from saying it is technically impossible to obtain a score.&lt;/p&gt;

&lt;p&gt;For me, this points to a research direction: evaluation grounded in speakers' judgments of meaning, fluency and adequacy for these languages, and metrics validated against those judgments. It may involve extending existing work rather than inventing an entirely new metric.&lt;/p&gt;

&lt;p&gt;The next version of KEMT should include a human-reviewed subset, clearer error categories, dialect-aware review and uncertainty estimates. I would also like fresh held-out material, larger cohorts and more models. There is plenty to do.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I learned from building it
&lt;/h3&gt;

&lt;p&gt;I used Codex and Claude to help with the implementation, debugging and analysis. This article was also drafted with AI assistance from my notes and reviewed results.&lt;/p&gt;

&lt;p&gt;But I still had to decide what problem mattered, which data I trusted, what a failure meant, and whether the outputs made sense to me as a speaker. The domain knowledge kept coming back into the work.&lt;/p&gt;

&lt;p&gt;I started wanting to demonstrate a gap. I finished with a benchmark that gives me a more specific way to investigate it—and with more questions about evaluation itself.&lt;/p&gt;

&lt;p&gt;Preparing it was harder than I expected. It was also exciting. I did not want to build something that ended when the challenge did.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Benchmark
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.kaggle.com/benchmarks/cypriankiplangat/kemt" rel="noopener noreferrer"&gt;Explore KEMT on Kaggle&lt;/a&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The public benchmark contains the five language tasks, native model results, a short explanation of the scoring, and supporting BLEU, chrF, chrF++ and TER tables. The backing notebooks remain private; the benchmark is the public entry point.&lt;/p&gt;

&lt;p&gt;Data credit: &lt;strong&gt;African Next Voices Kenya / AfriVoices-KE&lt;/strong&gt;, Lilian Wanzare et al. (2026), under &lt;a href="https://creativecommons.org/licenses/by/4.0/" rel="noopener noreferrer"&gt;CC BY 4.0&lt;/a&gt;. My preparation includes prompt selection, exact-pair deduplication, reference grouping and the documented Kikuyu character recovery. Thank you to the dataset contributors and the Kaggle Benchmarks team for the quota support.&lt;/p&gt;

&lt;p&gt;I want this to become a continuing research project. As soon as I get access to another model, I now have a reason to be excited beyond its general benchmark scores: I can ask what changed for the languages here.&lt;/p&gt;

&lt;p&gt;I hope future models will speak my local language well. I am also one of the people working towards that.&lt;/p&gt;

&lt;p&gt;There is still a lot to research. At least now, the next model release comes with a task waiting for it 😅.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>kagglechallenge</category>
      <category>ai</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Floor: the number you meant to hold</title>
      <dc:creator>Cian</dc:creator>
      <pubDate>Mon, 05 Oct 2026 06:56:33 +0000</pubDate>
      <link>https://dev.to/mutaician/floor-the-number-you-meant-to-hold-31kn</link>
      <guid>https://dev.to/mutaician/floor-the-number-you-meant-to-hold-31kn</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;p&gt;I did not start this weekend building Floor.&lt;/p&gt;

&lt;p&gt;I started with another project, Pause. I had a plan, a model to train, and the hope that the next run would make the idea come together. I put time into it. Then I put more time into it.&lt;/p&gt;

&lt;p&gt;Eventually, I had to admit something frustrating: I was trying to force a solution.&lt;/p&gt;

&lt;p&gt;There is a particular disappointment in looking at work you have already done and deciding it should not be the thing you ship. You want the effort to count. You want one more experiment to justify the hours before it.&lt;/p&gt;

&lt;p&gt;But the deadline was getting closer, and I wanted to build something someone could actually find useful. I archived Pause.&lt;/p&gt;

&lt;p&gt;After that, I met a friend over the weekend. Our conversation turned to the online marketplace they run, and the work of dealing with buyer inquiries.&lt;/p&gt;

&lt;p&gt;They were getting plenty of messages. The difficult part was negotiating well enough for those conversations to become clear agreements. Someone still had to answer the questions, understand the offers, and decide how far to move on price.&lt;/p&gt;

&lt;p&gt;I had been so caught up in getting my own idea to work that it was a relief to have a specific person's problem in front of me. Their situation gave me somewhere to start again.&lt;/p&gt;

&lt;p&gt;I kept thinking about the person on the other side of that inbox. How many times can you explain the same thing, reconsider your price, and try to keep a conversation alive before it begins to wear on you?&lt;/p&gt;

&lt;p&gt;I left that conversation with a clearer idea of what I wanted to build for my friend: a little support between the buyer's message and the seller's reply.&lt;/p&gt;

&lt;p&gt;That became Floor.&lt;/p&gt;

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

&lt;p&gt;“What's your best price?”&lt;/p&gt;

&lt;p&gt;It is such a small message. Answering it can take more thought than writing the listing.&lt;/p&gt;

&lt;p&gt;The seller already has an asking price. Somewhere behind it is another number: the amount they can afford to accept. But knowing that number and holding it through a conversation are different things.&lt;/p&gt;

&lt;p&gt;One buyer wants delivery included. Another wants to pay half now and the rest next week. Someone else comes back to an old conversation, and the seller has to remember what they already offered.&lt;/p&gt;

&lt;p&gt;That was the part of my friend's work I wanted to help with. Every inquiry creates more work before it creates a sale. And although a buyer sees one conversation, the person answering may be keeping track of several.&lt;/p&gt;

&lt;p&gt;I kept coming back to a simple question: could I help my friend answer the next message without making them rethink the whole deal?&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;Floor&lt;/strong&gt;, a seller inbox agent that keeps the listing, the seller's terms, and separate buyer conversations together. It reads a message, checks the context, and prepares a next move: answer, accept, counter, clarify, or close.&lt;/p&gt;

&lt;p&gt;The seller sees the interpreted terms, corrects anything misunderstood, and edits the reply before copying it to their marketplace or chat. After sending it there, they record it as sent in Floor.&lt;/p&gt;

&lt;p&gt;I liked the name because it points to something the seller already knows. Floor helps them carry that decision into the next conversation. For my friend, I wanted it to mean a little less second-guessing and a little more room to get on with the rest of the work.&lt;/p&gt;

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

&lt;p&gt;After the first project, I wanted a more concrete reason to use fine-tuning. I needed to be able to point to a message and say: this is the distinction the model should learn.&lt;/p&gt;

&lt;p&gt;An offer of KES 11,000 sounds promising. But KES 11,000 including delivery leaves the seller with a different amount. KES 6,000 today and KES 5,000 later is a different payment arrangement again.&lt;/p&gt;

&lt;p&gt;A fluent reply would mean very little if the assistant misunderstood the deal.&lt;/p&gt;

&lt;p&gt;I used &lt;strong&gt;Qwen3-8B&lt;/strong&gt;, fine-tuned through &lt;strong&gt;Tinker&lt;/strong&gt;, and gave it a narrow job: extract the total offered, the payment type, the costs the seller would pay, and the buyer's intent.&lt;/p&gt;

&lt;p&gt;Training combined human buyer messages from &lt;a href="https://stanfordnlp.github.io/cocoa/" rel="noopener noreferrer"&gt;CraigslistBargains&lt;/a&gt; with generated scenarios covering delivery ambiguity, payment conditions, and different ways of making offers. The human messages have explicit task-specific seller-policy assumptions; the generated examples are marked as generated. Preparation produced &lt;strong&gt;396 training scenarios&lt;/strong&gt;, rendered with two prompt variants, and &lt;strong&gt;100 validation scenarios&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The application then uses those interpreted terms to calculate prices. It also checks saved listing facts and previous seller quotes. The suggested wording comes from code, and the interpreted terms remain visible for review. FastAPI serves the application; SQLite keeps the desk persistent.&lt;/p&gt;

&lt;h3&gt;
  
  
  The next run was better. The last one wasn't the best.
&lt;/h3&gt;

&lt;p&gt;I compared the base model and fine-tuned checkpoints on the same 20-case development probe:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model and prompt&lt;/th&gt;
&lt;th&gt;Exact interpretation&lt;/th&gt;
&lt;th&gt;Correct action and price with the shared pricing function&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Base Qwen3-8B, detailed prompt&lt;/td&gt;
&lt;td&gt;5/20&lt;/td&gt;
&lt;td&gt;12/20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Base Qwen3-8B, compact prompt&lt;/td&gt;
&lt;td&gt;9/20&lt;/td&gt;
&lt;td&gt;16/20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fine-tuned Qwen3-8B, detailed prompt, three passes&lt;/td&gt;
&lt;td&gt;15/20&lt;/td&gt;
&lt;td&gt;19/20&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The pricing function was identical for every model. Better interpretations gave that same function better inputs.&lt;/p&gt;

&lt;p&gt;Seeing the improvement was encouraging. It gave me a reason to keep building the product around this task.&lt;/p&gt;

&lt;p&gt;I tried six training passes, hoping more training would help. The three-pass checkpoint was the best on this probe. Six passes finished at 18/20 decisions, so I selected the earlier checkpoint. The six-pass run cost approximately &lt;strong&gt;$0.80 in estimated training-token charges&lt;/strong&gt;, with roughly &lt;strong&gt;$0.01&lt;/strong&gt; for its checkpoint comparisons, excluding storage.&lt;/p&gt;

&lt;p&gt;There was still a mistake I could not ignore: an installment offer interpreted as full payment. These are small development results used to select the checkpoint, not an independent accuracy benchmark. They do not tell me how many sales Floor will help someone make. The &lt;a href="https://github.com/mutaician/floor/blob/main/docs/training.md" rel="noopener noreferrer"&gt;training notes&lt;/a&gt; document the fuller comparison.&lt;/p&gt;

&lt;p&gt;That remaining error helped shape the interface. The seller needs to see what Floor heard before trusting what it suggests.&lt;/p&gt;

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

&lt;p&gt;The moment in the demo that made me think back to my friend's inbox was a buyer agreeing to an earlier quote.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://raw.githubusercontent.com/mutaician/floor/main/docs/demo.mp4" rel="noopener noreferrer"&gt;Watch the 28-second interface walkthrough&lt;/a&gt;. It shows saved sample conversations and analyses produced by the selected Tinker model during the demo checks.&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%2Fgkmhnhn6frnf6uyqqj3t.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%2Fgkmhnhn6frnf6uyqqj3t.png" alt="Floor's seller inbox, showing a sample conversation and a suggested reply" width="800" height="611"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the sample, the headphones are listed at &lt;strong&gt;KES 12,000&lt;/strong&gt;, with a private floor of &lt;strong&gt;KES 10,000&lt;/strong&gt;. Kevin offers &lt;strong&gt;KES 10,000&lt;/strong&gt; in full and will collect. Floor prepares a &lt;strong&gt;KES 11,000&lt;/strong&gt; counteroffer.&lt;/p&gt;

&lt;p&gt;The reply is edited and recorded as sent. Then Kevin agrees to &lt;strong&gt;KES 11,000&lt;/strong&gt; in full.&lt;/p&gt;

&lt;p&gt;Floor recognizes the earlier quote and recommends accepting. The conversation can move forward without another counteroffer.&lt;/p&gt;

&lt;p&gt;That is a small piece of memory, but it matters. A seller should not have to reconstruct the whole conversation every time a buyer comes back.&lt;/p&gt;

&lt;p&gt;Other sample inquiries ask about condition and availability, include delivery costs, or propose installments. All are labeled examples, not actual customer conversations.&lt;/p&gt;

&lt;p&gt;The desk saves listing details, message histories, edited drafts, and inquiry states. I checked that they survived browser refreshes and server restarts. It also works on mobile. I wanted the reply someone had spent time editing to still be there when they returned.&lt;/p&gt;

&lt;p&gt;The current version handles one seller and one listing. Messages are pasted in manually, and replies are sent through the seller's existing chat. My friend's handover and feedback are still ahead; I have not measured actual sales or conversion.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://github.com/mutaician/floor" rel="noopener noreferrer"&gt;Explore the Floor repository&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It includes the application, prompts, generated scenarios, data preparation and training code, attribution, and deployment instructions. Credentials, seller conversations, and private checkpoint identifiers stay outside the repository.&lt;/p&gt;

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

&lt;p&gt;This weekend, being able to change the model mattered to me.&lt;/p&gt;

&lt;p&gt;When a message was misunderstood, I could inspect the failure, change the examples, fine-tune the open-weight model through Tinker, and compare the resulting checkpoints with the base model. I had something concrete to work on when an interpretation was wrong.&lt;/p&gt;

&lt;p&gt;Tinker made remote training and sampling practical without setting up a local GPU training system. The application keeps readable rules for prices, listing facts, and approved quotes. Those decisions remain inspectable alongside the model's interpretation, and the model can be changed as the project develops.&lt;/p&gt;

&lt;p&gt;Inference runs on Tinker, so buyer messages and listing terms are sent there. The local database keeps conversation history, and credentials stay on the server.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Best Use of Tinker:&lt;/strong&gt; task-specific fine-tuning of Qwen3-8B, with a recorded improvement over the baseline using the same pricing policy and development cases.&lt;/p&gt;

&lt;p&gt;I began the weekend hoping a model would make my first idea work. A conversation with a friend gave me a clearer reason to keep building.&lt;/p&gt;

&lt;p&gt;Now I want to put Floor in their hands and find out whether it helps during an ordinary afternoon of selling. Do the replies sound like them? Are the terms easy to check? Does holding a price become a little less tiring?&lt;/p&gt;

&lt;p&gt;I hope it gives them a moment to breathe before answering. A place to remember what they already decided. A reply they feel comfortable standing behind.&lt;/p&gt;

&lt;p&gt;Archiving Pause felt disappointing. Building something with my friend in mind gave the rest of the weekend a direction.&lt;/p&gt;

&lt;p&gt;For now, Floor is a place to keep the number you meant to hold—and a little backup for the next message.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>hf26challenge</category>
      <category>tinker</category>
    </item>
    <item>
      <title>Beside: generosity can be a little of your time</title>
      <dc:creator>Cian</dc:creator>
      <pubDate>Sat, 05 Sep 2026 17:02:36 +0000</pubDate>
      <link>https://dev.to/mutaician/beside-generosity-can-be-a-little-of-your-time-5d2j</link>
      <guid>https://dev.to/mutaician/beside-generosity-can-be-a-little-of-your-time-5d2j</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/challenges/weekend-2026-09-03"&gt;Weekend Challenge: Generosity Edition&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;“I’m here for you” is easy to say. Knowing what to offer next can be harder.&lt;/p&gt;

&lt;p&gt;I built &lt;strong&gt;Beside&lt;/strong&gt; to help turn that intention into something small and specific: a listening ear, quiet company, a helping hand, or a gentle hello after some time apart.&lt;/p&gt;

&lt;p&gt;With September being Suicide Prevention Month, I wanted to explore how generosity could look like everyday connection. Giving someone a little of your time and attention can be an act of generosity, too.&lt;/p&gt;

&lt;p&gt;Beside helps you find an offer you can follow through on. It has 42+ gestures, simple steps, and messages you can make your own. There’s always room for the other person to say “not today.”&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://beside.cyprianmutai5900.workers.dev" rel="noopener noreferrer"&gt;Try Beside&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Start with what you know.&lt;/strong&gt; In this walkthrough, I chose “I’m not sure where to start” and “I’m open to ideas.” No names or personal stories are needed.&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%2Fpee7pmq2sa6pfhszeol2.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%2Fpee7pmq2sa6pfhszeol2.png" alt="Choosing a situation and staying open to different kinds of care" width="799" height="662"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Choose what you can offer.&lt;/strong&gt; I set ten minutes, from a distance, with no purchase. Snowflake returned these suggestions based on those choices.&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%2Fipqp22naqqtmzyld2c14.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%2Fipqp22naqqtmzyld2c14.png" alt="Three suggestions returned by Snowflake semantic search" width="800" height="611"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Make the offer yours.&lt;/strong&gt; I opened “Ask one open question, then listen.” Beside gives me a few steps and a message I can edit, then copy into whichever app I normally use to talk.&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%2Fqee8fpnop5ejjpsp23bn.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%2Fqee8fpnop5ejjpsp23bn.png" alt="A listening gesture with practical steps and an editable message" width="800" height="729"&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/mutaician" rel="noopener noreferrer"&gt;
        mutaician
      &lt;/a&gt; / &lt;a href="https://github.com/mutaician/Beside" rel="noopener noreferrer"&gt;
        Beside
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Small, thoughtful ways to show up for someone you care about.
    &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;Beside&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;Small ways to show up for someone you care about.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://beside.cyprianmutai5900.workers.dev" rel="nofollow noopener noreferrer"&gt;Try Beside&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Beside turns “I’m here for you” into a thoughtful offer of listening, company, practical help, or reconnection. Choose what feels manageable, explore 42+ gestures, and personalize a message before sharing it.&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/mutaician/Beside/docs/screenshots/home-user.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%2Fmutaician%2FBeside%2FHEAD%2Fdocs%2Fscreenshots%2Fhome-user.png" alt="Beside homepage: a warm space for finding small gestures of care"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;How I built it&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;I used &lt;strong&gt;Snowflake&lt;/strong&gt; to store the gestures, message templates, and source references. I created embeddings with &lt;code&gt;AI_EMBED&lt;/code&gt; and used &lt;code&gt;VECTOR_COSINE_SIMILARITY&lt;/code&gt; to find gestures relevant to the selected situation. SQL filters keep the suggestions within the time, distance, budget, and kind of care someone can offer.&lt;/p&gt;
&lt;p&gt;I built the interface with &lt;strong&gt;React, TypeScript, Vite, and pnpm&lt;/strong&gt;, and deployed it on &lt;strong&gt;Cloudflare Workers&lt;/strong&gt;. The Worker connects to Snowflake using credentials stored in Cloudflare secrets. Message edits stay in the browser.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Run the app&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Use Node.js 24 and pnpm. Copy &lt;code&gt;.dev.vars.example&lt;/code&gt; to &lt;code&gt;.dev.vars&lt;/code&gt; and add your Snowflake service-account settings for live matching.&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;pnpm&lt;/pre&gt;…
&lt;/div&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/mutaician/Beside" 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;I used &lt;strong&gt;Snowflake&lt;/strong&gt; to store the gestures, message templates, and source references. I created embeddings with &lt;code&gt;AI_EMBED&lt;/code&gt; using &lt;code&gt;snowflake-arctic-embed-l-v2.0&lt;/code&gt;, then used &lt;code&gt;VECTOR_COSINE_SIMILARITY&lt;/code&gt; to match each situation with relevant gestures. SQL filters account for time, distance, budget, and the kind of care someone wants to offer.&lt;/p&gt;

&lt;p&gt;I used &lt;strong&gt;React, TypeScript, Vite, and pnpm&lt;/strong&gt; for the interface, and &lt;strong&gt;Cloudflare Workers&lt;/strong&gt; for hosting and the API connecting to Snowflake. Message editing happens in the browser, so personal wording isn’t sent to Snowflake.&lt;/p&gt;

&lt;p&gt;I wanted Beside to feel warm and approachable. I used rounded fonts, soft colors, and 3D icons from &lt;a href="https://3dicons.co/" rel="noopener noreferrer"&gt;3dicons&lt;/a&gt;. AI adapted source guidance into message templates. Each gesture includes links to the guidance behind it.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Best Use of Snowflake&lt;/strong&gt; for storing the content and using embeddings, semantic ranking, and practical filters to find a small way to show up.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>snowflake</category>
    </item>
    <item>
      <title>I built Persite because I was tired of guessing GPU costs in my head</title>
      <dc:creator>Cian</dc:creator>
      <pubDate>Fri, 13 Mar 2026 19:51:10 +0000</pubDate>
      <link>https://dev.to/mutaician/i-built-persite-because-i-was-tired-of-guessing-gpu-costs-in-my-head-5837</link>
      <guid>https://dev.to/mutaician/i-built-persite-because-i-was-tired-of-guessing-gpu-costs-in-my-head-5837</guid>
      <description>&lt;p&gt;When this started, I was not trying to build a hackathon project.&lt;/p&gt;

&lt;p&gt;I was trying to rent GPUs for my ML project.&lt;/p&gt;

&lt;p&gt;What I wanted was simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;price per hour&lt;/li&gt;
&lt;li&gt;in my local currency&lt;/li&gt;
&lt;li&gt;visible immediately&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What I got on many sites was the opposite: generic hero copy, too many sections, and pricing buried somewhere I had to hunt for. Then I had to mentally convert currency and estimate actual cost.&lt;/p&gt;

&lt;p&gt;That friction became the core idea behind Persite.&lt;/p&gt;

&lt;p&gt;Persite is a locale-aware and intent-aware personalization system. It tries to answer this:&lt;/p&gt;

&lt;p&gt;If someone arrives with clear intent, why are we still forcing everyone through the same static page?&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem I wanted to solve
&lt;/h2&gt;

&lt;p&gt;Most websites treat a buyer in Kenya the same as a buyer in the US or Germany. Same message, same CTA, same layout priority.&lt;/p&gt;

&lt;p&gt;I think that is a product problem, not just a translation problem.&lt;/p&gt;

&lt;p&gt;The key point for me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;translation alone is not enough&lt;/li&gt;
&lt;li&gt;intent alone is not enough&lt;/li&gt;
&lt;li&gt;locale plus intent is where it starts making sense&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also wanted a privacy-friendly approach. I did not want profile tracking or long-term behavior graphs. I wanted lightweight signals:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;locale&lt;/li&gt;
&lt;li&gt;URL params&lt;/li&gt;
&lt;li&gt;UTM data&lt;/li&gt;
&lt;li&gt;referrer&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;I built two surfaces:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A landing page that adapts full-page content based on landing intent (&lt;code&gt;judge&lt;/code&gt;, &lt;code&gt;github&lt;/code&gt;, &lt;code&gt;investor&lt;/code&gt;, &lt;code&gt;browse&lt;/code&gt;) and locale.&lt;/li&gt;
&lt;li&gt;A demo e-commerce store (&lt;code&gt;/demo&lt;/code&gt;) that adapts hero copy, product ordering behavior, and localized product description content.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both surfaces have a draggable control panel so you can switch locale and intent quickly and see why a variant was selected.&lt;/p&gt;

&lt;h2&gt;
  
  
  High-level architecture
&lt;/h2&gt;

&lt;p&gt;The flow is straightforward:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Detect signals (locale + intent)&lt;/li&gt;
&lt;li&gt;Choose a variant with deterministic rules&lt;/li&gt;
&lt;li&gt;Send selected content to Lingo API for localization&lt;/li&gt;
&lt;li&gt;Render localized result&lt;/li&gt;
&lt;li&gt;Expose decision metadata in panel&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I kept the logic explicit and finite on purpose. I wanted a demo that can be explained under time pressure.&lt;/p&gt;

&lt;h2&gt;
  
  
  The code that made the project real
&lt;/h2&gt;

&lt;p&gt;This is the core hero localization flow in my API route:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;step3Decision&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;buildStep3Decision&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;intentDetection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;intent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;intentDetection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&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;localizablePayload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;LocalizablePayload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;headline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;step3Decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;baseContent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;subheadline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;step3Decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;baseContent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subheadline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;ctaLabel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;step3Decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;baseContent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ctaLabel&lt;/span&gt;&lt;span class="p"&gt;,&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;localizationResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;LINGO_LOCALIZE_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&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="s2"&gt;X-API-Key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&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="s2"&gt;application/json&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="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;engineId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;sourceLocale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;targetLocale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;localeDetection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;localizablePayload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;cache&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-store&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;That block is where static template content becomes locale-adapted output.&lt;/p&gt;

&lt;p&gt;And this is the part that saved me when product localization became too slow:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;chunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;chunkProducts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;toTranslate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;CHUNK_SIZE&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;chunks&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;i&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;MAX_PARALLEL_CHUNKS&lt;/span&gt;&lt;span class="p"&gt;)&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;group&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&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="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;MAX_PARALLEL_CHUNKS&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;groupResults&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;chunk&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;localizeChunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&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;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;groupResults&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;translatedByKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;result&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That was a practical turning point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the clean design broke
&lt;/h2&gt;

&lt;p&gt;My initial clean idea was bigger: make this portable as a script that works on any website.&lt;/p&gt;

&lt;p&gt;Reality check: every website has a different structure, different content ownership, and different component boundaries.&lt;/p&gt;

&lt;p&gt;So I narrowed scope to a controlled environment where I could show the value clearly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;deterministic variant model&lt;/li&gt;
&lt;li&gt;explainable decisions&lt;/li&gt;
&lt;li&gt;real localization behavior&lt;/li&gt;
&lt;li&gt;fast enough demo interactions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That scope cut is honestly what made it shippable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Biggest pain point and messy workaround
&lt;/h2&gt;

&lt;p&gt;The biggest pain was localization latency when payloads got large.&lt;/p&gt;

&lt;p&gt;I hit situations where requests were taking far too long. I ended up doing a mix of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;parallel chunked localization&lt;/li&gt;
&lt;li&gt;selective localization (only high-impact copy)&lt;/li&gt;
&lt;li&gt;caching by locale + intent + content key&lt;/li&gt;
&lt;li&gt;fallback paths for missing translations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is not the purest architecture, but it crossed the finish line and stayed understandable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trade-offs I accepted
&lt;/h2&gt;

&lt;p&gt;I made deliberate trade-offs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I did not localize everything through the API.&lt;/li&gt;
&lt;li&gt;I used hybrid content strategy:

&lt;ul&gt;
&lt;li&gt;dynamic/high-impact copy through API&lt;/li&gt;
&lt;li&gt;repeated UI labels via locale dictionaries&lt;/li&gt;
&lt;li&gt;technical product names/spec tokens kept unchanged&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;I optimized for demo clarity over maximum abstraction.&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;I think this was the right call for a hackathon MVP.&lt;/p&gt;

&lt;h2&gt;
  
  
  One extra thing I intentionally modeled
&lt;/h2&gt;

&lt;p&gt;In the demo store data, I also reflected a real market situation: GPU and RAM prices being elevated due to AI-era supply pressure.&lt;/p&gt;

&lt;p&gt;That was intentional. I wanted the demo to feel like it understands real buyer context, not just UI translation.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to run it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/mutaician/persite
&lt;span class="nb"&gt;cd &lt;/span&gt;persite
pnpm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create &lt;code&gt;.env&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LINGO_API_KEY=your_lingo_api_key
LINGO_ENGINE_ID=your_lingo_engine_id
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run dev server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Build check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm run build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Useful routes to test:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;http://localhost:3000/?intent=judge&amp;amp;locale=de-DE&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;http://localhost:3000/?intent=investor&amp;amp;locale=sw-KE&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;http://localhost:3000/demo?intent=compare&amp;amp;locale=fr-FR&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;http://localhost:3000/demo?intent=budget&amp;amp;locale=pt-BR&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I would build next
&lt;/h2&gt;

&lt;p&gt;The missing piece is portability.&lt;/p&gt;

&lt;p&gt;I want a reusable integration layer that can plug into arbitrary websites and personalize key surfaces (especially pricing and plan-selection pages) based on intent and locale, without requiring each team to rewrite their whole frontend.&lt;/p&gt;

&lt;p&gt;That is where this can move from a strong demo to a deployable product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Live demo: &lt;a href="https://persite-seven.vercel.app/" rel="noopener noreferrer"&gt;https://persite-seven.vercel.app/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Repository: &lt;a href="https://github.com/mutaician/persite" rel="noopener noreferrer"&gt;https://github.com/mutaician/persite&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>hackathon</category>
      <category>lingo</category>
      <category>ai</category>
      <category>localization</category>
    </item>
    <item>
      <title>Gemini Became My Entire Hackathon Team — How a Solo Dev in Kenya Won His First MLH Prize Building RepoX</title>
      <dc:creator>Cian</dc:creator>
      <pubDate>Tue, 03 Mar 2026 14:46:48 +0000</pubDate>
      <link>https://dev.to/mutaician/gemini-became-my-entire-hackathon-team-how-a-solo-dev-in-kenya-won-his-first-mlh-prize-building-2k27</link>
      <guid>https://dev.to/mutaician/gemini-became-my-entire-hackathon-team-how-a-solo-dev-in-kenya-won-his-first-mlh-prize-building-2k27</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/mlh-built-with-google-gemini-02-25-26"&gt;Built with Google Gemini: Writing Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built with Google Gemini
&lt;/h2&gt;

&lt;p&gt;Picture this: It’s 3 a.m.. My first-ever MLH hackathon. I’m staring at a blank screen, heart racing, knowing I’m completely outmatched by teams with years of experience.&lt;/p&gt;

&lt;p&gt;Then I opened &lt;strong&gt;Google Antigravity&lt;/strong&gt; — and everything changed.&lt;/p&gt;

&lt;p&gt;I built &lt;strong&gt;RepoX&lt;/strong&gt;: an interactive platform that turns any public GitHub repository into a living, breathing learning adventure.&lt;/p&gt;

&lt;p&gt;No more getting lost in massive codebases. RepoX gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A stunning D3.js force-directed graph that maps every file and its relationships like a neural network&lt;/li&gt;
&lt;li&gt;Instant AI-powered explanations for any file (including “Explain Like I’m 5” mode that actually makes sense)&lt;/li&gt;
&lt;li&gt;Smart personalized learning paths — the AI reads the entire repo and tells you the exact smartest order to explore it&lt;/li&gt;
&lt;li&gt;Progress checklists and history so you never lose momentum&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The crazy part? The app itself runs &lt;strong&gt;on Gemini&lt;/strong&gt;. Every explanation and learning path is generated live by the Gemini API (securely routed through Cloudflare Workers).&lt;/p&gt;

&lt;p&gt;And yes — this project won &lt;strong&gt;Best AI Application Built with Cloudflare&lt;/strong&gt; at Hacks for Hackers 2026. My very first hackathon… and I took home a prize. I still can’t believe it.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Live app&lt;/strong&gt; (paste any GitHub repo and watch the magic): &lt;a href="https://main.repox.pages.dev" rel="noopener noreferrer"&gt;https://main.repox.pages.dev&lt;/a&gt;  &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Full YouTube demo&lt;/strong&gt;: &lt;br&gt;


  &lt;iframe src="https://www.youtube.com/embed/8m2kGGZEJTw"&gt;
  &lt;/iframe&gt;


  &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Devpost&lt;/strong&gt;: &lt;a href="https://devpost.com/software/repox" rel="noopener noreferrer"&gt;https://devpost.com/software/repox&lt;/a&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/mutaician/RepoX" rel="noopener noreferrer"&gt;https://github.com/mutaician/RepoX&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;This wasn’t just a hackathon project — it was my crash course in what happens when you stop coding &lt;em&gt;alone&lt;/em&gt; and start coding &lt;em&gt;with&lt;/em&gt; an AI teammate.&lt;/p&gt;

&lt;p&gt;I went from zero D3.js experience to building a smooth, responsive graph that handles thousands of nodes. I learned secure API proxying on Cloudflare Workers under extreme time pressure. I mastered prompt engineering at a level I never thought possible — crafting system prompts so precise that Gemini would output perfectly formatted learning paths every single time.&lt;/p&gt;

&lt;p&gt;Most importantly, I learned that one determined developer + the right Gemini workflow can outpace entire traditional teams. The confidence this gave me is something no tutorial could ever provide.&lt;/p&gt;

&lt;h2&gt;
  
  
  Google Gemini Feedback
&lt;/h2&gt;

&lt;p&gt;Gemini wasn’t a tool. It was my co-founder, my senior dev, my QA tester, and my creative director — all in one.&lt;/p&gt;

&lt;p&gt;I used &lt;strong&gt;Antigravity&lt;/strong&gt; (Google’s agentic IDE) the entire time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gemini 3 Pro&lt;/strong&gt; handled the heavy lifting — autonomously designing the learning-path algorithm, reasoning through complex repo analysis, and even suggesting UI tweaks that made the graph feel alive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemini 3 Flash&lt;/strong&gt; was my speed demon — instantly generating UI components, ELI5 explanations, and quick fixes while I kept momentum.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemini 2.5&lt;/strong&gt; was the reliable fallback when context got too big on massive repos.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What blew me away:&lt;/strong&gt;&lt;br&gt;
The agentic flow was unreal. I’d describe a feature once, and Antigravity would plan, code, debug, and iterate — often better than I would have done myself. The personalized learning paths Gemini 3.1 created were scarily good — logical, educational, and genuinely helpful.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where it got messy (keeping it real):&lt;/strong&gt;&lt;br&gt;
Larger repos sometimes overwhelmed the context window and the agent would start hallucinating relationships or going off on wild creative tangents. I had to get surgical with my prompts and occasionally switch models. Response formatting could be inconsistent (markdown breaking in weird places), and yes, the token costs added up during heavy 3.1 sessions.&lt;/p&gt;

&lt;p&gt;But here’s the truth: Without this exact multi-model + Antigravity setup, RepoX would still be a half-finished idea on my laptop. Gemini didn’t just help me finish — it helped me win my first hackathon.&lt;/p&gt;

&lt;p&gt;From a nervous solo dev in Kenya to MLH prize winner in 48 hours. That’s the power of Google Gemini.&lt;/p&gt;

&lt;p&gt;Thanks for reading my story — can’t wait to see what we build next. 🚀&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>geminireflections</category>
      <category>gemini</category>
      <category>hackathon</category>
    </item>
    <item>
      <title>I Built an AI That Can See Your Arduino and Write the Code For It</title>
      <dc:creator>Cian</dc:creator>
      <pubDate>Fri, 27 Feb 2026 17:49:51 +0000</pubDate>
      <link>https://dev.to/mutaician/i-built-an-ai-that-can-see-your-arduino-and-write-the-code-for-it-558l</link>
      <guid>https://dev.to/mutaician/i-built-an-ai-that-can-see-your-arduino-and-write-the-code-for-it-558l</guid>
      <description>&lt;p&gt;There is a specific frustration anyone who has worked with Arduino knows well.&lt;/p&gt;

&lt;p&gt;You have a breadboard in front of you. Components are wired up. You open a chat window, describe your setup in text — "I have an LED on pin 8 with a 220 ohm resistor" — copy the code the AI gives you, paste it into the Arduino IDE, hit upload, and watch the LED do nothing. You go back to the chat window. You describe what happened. You get a revised version. You copy it again.&lt;/p&gt;

&lt;p&gt;You do this five times before realizing the AI gave you code for pin 9 because you told it pin 8 and it added a one-line comment that said "change this to match your wiring" which you missed.&lt;/p&gt;

&lt;p&gt;Every AI coding assistant has this problem: they are blind to your physical setup.&lt;/p&gt;

&lt;p&gt;ArduinoVision is my attempt to fix that.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Idea
&lt;/h2&gt;

&lt;p&gt;The concept is simple enough to state in one sentence: an AI agent that can see your breadboard through a camera, write the correct Arduino code based on what it actually observes, and upload it directly to your board.&lt;/p&gt;

&lt;p&gt;No copy-paste. No IDE switching. No describing your wiring in text. You connect the components. The AI handles everything else.&lt;/p&gt;

&lt;p&gt;I built this for the Vision Possible: Agent Protocol hackathon by WeMakeDevs, and the core of it runs on the VisionAgents SDK by Stream.&lt;/p&gt;




&lt;h2&gt;
  
  
  What VisionAgents Makes Possible
&lt;/h2&gt;

&lt;p&gt;Before I get into the build, I want to explain why this project needed VisionAgents specifically — because that is not an obvious answer.&lt;/p&gt;

&lt;p&gt;The challenge with building a hardware coding agent is that it needs three things happening simultaneously and tightly integrated: it needs to see video (your camera), hear audio (your voice), reason about both together (the LLM), and take external actions (compile, upload). Wiring all of that together manually — WebRTC for the camera feed, a separate STT service, a separate LLM call, a separate TTS for the response — is a significant amount of infrastructure before you write a single line of the actual agent logic.&lt;/p&gt;

&lt;p&gt;VisionAgents collapses all of that into a few lines of Python.&lt;/p&gt;

&lt;p&gt;The relevant part of the agent setup looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;vision_agents.core&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AgentLauncher&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Runner&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;vision_agents.plugins&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;getstream&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt;

&lt;span class="n"&gt;llm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Realtime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-realtime&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;voice&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cedar&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fps&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;edge&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;getstream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Edge&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;agent_user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ArduinoVision&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;arduino-vision-agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;instructions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;SYSTEM_PROMPT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;llm&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;That is the entire transport and LLM setup. &lt;code&gt;getstream.Edge()&lt;/code&gt; handles the WebRTC infrastructure — video/audio in and out, connection management, reconnection logic. &lt;code&gt;openai.Realtime()&lt;/code&gt; handles speech-to-speech natively — no separate STT or TTS services, no intermediate text conversion, just audio in and audio out with video frames attached. Stream's edge network keeps the latency under 30ms, which matters when someone is physically holding a component in front of the camera.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;fps=1&lt;/code&gt; setting deserves a note. I initially had it at &lt;code&gt;fps=3&lt;/code&gt; and the audio quality was noticeably degraded — cutting out, pitch shifts mid-sentence. Dropping to one frame per second freed up the audio pipeline entirely. For identifying breadboard wiring, one frame per second is more than sufficient.&lt;/p&gt;




&lt;h2&gt;
  
  
  Registering Arduino Tools
&lt;/h2&gt;

&lt;p&gt;The agent's practical capability comes from tool registration. VisionAgents uses &lt;code&gt;@llm.register_function()&lt;/code&gt; to make Python functions callable by the model during conversation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@llm.register_function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;List all connected Arduino boards. Returns port, board name, and FQBN. ALWAYS call this first to find the port needed for upload.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;list_boards&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;boards&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list_arduino_boards&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;boards&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;found&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;boards&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;boards&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Found &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;boards&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; board(s). Use the &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;port&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; for upload operations.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;found&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;boards&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No Arduino boards detected.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I registered six tools in total: &lt;code&gt;list_boards&lt;/code&gt;, &lt;code&gt;write_code&lt;/code&gt;, &lt;code&gt;compile_code&lt;/code&gt;, &lt;code&gt;upload_code&lt;/code&gt;, &lt;code&gt;serial_monitor&lt;/code&gt;, and &lt;code&gt;deploy_code&lt;/code&gt; (which chains the previous three). Each one wraps a call to &lt;code&gt;arduino-cli&lt;/code&gt; on the system.&lt;/p&gt;

&lt;p&gt;What makes this work well in practice is that the model chains these calls naturally based on the conversation. The user says "make the LED blink." The model calls &lt;code&gt;list_boards&lt;/code&gt; to find the port, calls &lt;code&gt;write_code&lt;/code&gt; to save the sketch, then &lt;code&gt;deploy_code&lt;/code&gt; to compile and upload. The user did not ask it to do those steps in that order — the model inferred the sequence from context and tool descriptions.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Event System
&lt;/h2&gt;

&lt;p&gt;One thing I found genuinely useful during development was the event subscription API. Every tool call emits a &lt;code&gt;ToolStartEvent&lt;/code&gt; and &lt;code&gt;ToolEndEvent&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@agent.events.subscribe&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_tool_start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ToolStartEvent&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TOOL START: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Args: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@agent.events.subscribe&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;on_tool_end&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ToolEndEvent&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TOOL END: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;execution_time_ms&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;ms)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TOOL FAILED: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; - &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you are building a hardware-in-the-loop agent where failures are physical (LED does not blink, board does not respond), having a structured log of every tool call with arguments and timing is essential. It is also how I caught that the model was calling &lt;code&gt;deploy_code&lt;/code&gt; before the port permissions were correctly set — the error message was clear in the log instantly.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Learned Building This
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Real-time video AI has a different failure mode than text AI.&lt;/strong&gt; With text AI, wrong output is obvious — you read it and fix the prompt. With video AI, wrong output means the board does not respond and you are staring at a stationary LED trying to figure out if the model misidentified the pin, or the code is wrong, or the upload failed, or the LED is wired backwards. Good observability (the event system) is not optional.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tool descriptions are more important than I expected.&lt;/strong&gt; The model's behaviour changed significantly based on how I phrased the tool descriptions. "Detect connected Arduino boards" caused the model to call it inconsistently. "List all connected Arduino boards. ALWAYS call this first to find the port needed for upload." made it call the tool reliably every time, in the right order.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hardware-in-the-loop iteration is slow.&lt;/strong&gt; Software agents can iterate in milliseconds. Hardware agents have a four-second compile-upload cycle. This changes how you design the system — you want the model to be confident before it acts, not to try-and-retry. Good visual grounding (making sure the agent can clearly see the wiring before generating code) matters more than in pure software contexts.&lt;/p&gt;




&lt;h2&gt;
  
  
  What It Is Not (Yet)
&lt;/h2&gt;

&lt;p&gt;ArduinoVision is a hackathon prototype. Its scope right now is: AVR boards (Uno, Nano), basic GPIO (digital pins, LEDs, buttons), one board connected at a time. It does not handle I2C sensors, servo control, ESP32/ESP8266, or multi-board setups. These are natural extensions but they are not in this version.&lt;/p&gt;

&lt;p&gt;The interface also relies on the VisionAgents demo UI at demo.visionagents.ai rather than a custom frontend. For a prototype this is fine — building a custom WebRTC client is significant work that would have added nothing to the core idea.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Bigger Picture
&lt;/h2&gt;

&lt;p&gt;The thing that strikes me about this project is how little code it took to get something genuinely useful working. The Arduino tooling (list boards, write, compile, upload) is maybe 300 lines of Python. The agent setup is another 150. The entire relevant surface area is small.&lt;/p&gt;

&lt;p&gt;What VisionAgents provides is the hard part: real-time video transport, speech-to-speech latency that feels natural, and a clean function calling interface that the model uses reliably. Without that infrastructure being pre-built, this project would have been two weeks of WebRTC work before a single Arduino command got called.&lt;/p&gt;

&lt;p&gt;There is a real category of applications that becomes possible when AI agents can see physical environments and take actions based on what they observe. Hardware debugging is one. Lab automation is another. Physical quality control. Teaching environments where a student shows their circuit and gets immediate, accurate feedback.&lt;/p&gt;

&lt;p&gt;ArduinoVision is a small example of what that category looks like when the infrastructure is available.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;The code is on GitHub: &lt;a href="https://github.com/mutaician/arduino-vision" rel="noopener noreferrer"&gt;github.com/mutaician/arduino-vision&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You need a Stream account (free tier works), an OpenAI API key, Python 3.12, and arduino-cli. The README has full setup instructions. If you are on Windows, there are notes on forwarding the USB serial port to WSL.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built for the Vision Possible: Agent Protocol hackathon by WeMakeDevs. Powered by VisionAgents SDK by Stream.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>arduino</category>
      <category>visionagents</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
