<?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: pielouNW</title>
    <description>The latest articles on DEV Community by pielouNW (@pielounw).</description>
    <link>https://dev.to/pielounw</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%2F3829247%2F74724489-a661-4b45-9359-d82b4f28c1f3.png</url>
      <title>DEV Community: pielouNW</title>
      <link>https://dev.to/pielounw</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pielounw"/>
    <language>en</language>
    <item>
      <title>Triage support tickets locally with a 449 MB decision model</title>
      <dc:creator>pielouNW</dc:creator>
      <pubDate>Thu, 29 Oct 2026 16:25:54 +0000</pubDate>
      <link>https://dev.to/pielounw/triage-support-tickets-locally-with-a-449-mb-decision-model-3525</link>
      <guid>https://dev.to/pielounw/triage-support-tickets-locally-with-a-449-mb-decision-model-3525</guid>
      <description>&lt;p&gt;Every support inbox asks the same three questions of every ticket: who should handle it, does the customer want their money back, and how much is on fire. The usual fix is to ask an LLM, write a parser for whatever it decides to say, and then write a second parser for the day it answers "Billing (probably)" instead of "billing".&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/nobodywho-ooo/NobodyWho" rel="noopener noreferrer"&gt;NobodyWho's&lt;/a&gt; new decision model support for Flutter, React Native, Swift, Kotlin and Godot bindings, remove the parser from that story. A decision model scores answers you have already written. You give it the options up front, it returns a probability for each one, and it never generates a single token, so it has no way to invent a fourth department.&lt;/p&gt;

&lt;p&gt;TypeSafe calls this kind of model a System One model, after the fast, intuitive half of Kahneman's &lt;em&gt;Thinking, Fast and Slow&lt;/em&gt;, and describes its own model, Jev, as "unstructured state in, typed probabilistic decisions out". NobodyWho runs the same kind of decision on your own hardware, using Laya, an Apache-2.0 decision model whose &lt;code&gt;Q8_0&lt;/code&gt; GGUF file is 449 MB.&lt;/p&gt;

&lt;h2&gt;
  
  
  Load the model
&lt;/h2&gt;

&lt;p&gt;Install NobodyWho with &lt;code&gt;pip install nobodywho&lt;/code&gt;, then point a &lt;code&gt;Model&lt;/code&gt; at the Laya file on Hugging Face, or at a local GGUF path if you already have it.&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;nobodywho&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Decision&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Model&lt;/span&gt;

&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model_path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hf://ggml-org/Laya-GGUF/Laya-Q8_0.gguf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;decisions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Decision&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="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n_ctx&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Ask the ticket three questions
&lt;/h2&gt;

&lt;p&gt;Each question has an ID, a type and plain-English instructions. A choice question picks one option, a noul question returns the probability that the answer is yes, and a score question places the ticket on a scale you define, from two to ten levels.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;I was charged twice. Please refund the duplicate payment.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;questions&lt;/span&gt; &lt;span class="o"&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;department&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;type&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;choice&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;instructions&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;Which team should handle this?&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;criteria&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;billing&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;Payments and refunds&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;technical&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;Bugs and login problems&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="p"&gt;},&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;refund&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;type&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;noul&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;instructions&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;Is a refund requested?&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;urgency&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;type&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;score&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;instructions&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;How urgent is this?&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;criteria&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;can wait&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;soon&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;urgent&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="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;decisions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&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="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;questions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;questions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;team&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;answers&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;department&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;choice&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;probabilities&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;answers&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;department&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;probabilities&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;refund_probability&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;answers&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;refund&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;noul&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;urgency&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;answers&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;urgency&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;score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;usage&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;output_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last line is the whole pitch in one assert. The model read the ticket, answered three questions and wrote nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually comes back
&lt;/h2&gt;

&lt;p&gt;NobodyWho's test suite runs a very similar ticket through Laya: "I was charged twice for my order last week and nobody has replied." It asks which of billing, shipping or technical should take it, whether the customer is angry, and how urgent it is on a four-step scale from "can wait" to "right now". These are the reference answers the suite checks against on CPU, rounded:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"route"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"choice"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"billing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"probabilities"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"billing"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.975&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"shipping"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.014&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"technical"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.011&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"confidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.962&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"angry"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"noul"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.784&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"urgency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.914&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"legend"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"0"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"can wait"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"1"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"this week"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"2"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"today"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"3"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"right now"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"probabilities"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"0"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.040&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"1"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.458&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"2"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.050&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"3"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.452&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"confidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.006&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"usage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"input_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;166&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"output_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The routing answer is as clean as it gets: billing at 97.5%, with a confidence of 0.96. The model also puts the chance that this customer is angry at 78%, which anyone who has been charged twice will find conservative.&lt;/p&gt;

&lt;p&gt;The urgency answer is where it gets interesting. A score is a weighted average of the levels, and 1.914 rounds neatly to "today", but the model only gives "today" 5%. What it actually believes is that this ticket is either a "this week" problem (45.8%) or a "right now" problem (45.2%), and the confidence of 0.006 is its way of telling you it can't pick. If your code only read the score, it would file the ticket under an answer the model gave a 5% chance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Send the unsure ones to a person
&lt;/h2&gt;

&lt;p&gt;Choice and score answers both carry that confidence value, from 0 to 1, which measures how concentrated the probabilities are. That gives you a simple rule for triage: route confident answers automatically and put the rest in front of a human.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;answers&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;department&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;queue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;choice&lt;/span&gt;&lt;span class="sh"&gt;"&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;answer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;confidence&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.6&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;human-review&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Applied to the reference output above, the route goes straight to billing at 0.96 and the urgency goes to a person at 0.006, which is exactly the split you want. Treat 0.6 as a starting point to tune against your own tickets, and keep an eye on the queues for the first week, because the docs are clear that high confidence does not guarantee a correct answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running it in your app
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;DecisionAsync&lt;/code&gt; gives you the same API with &lt;code&gt;await&lt;/code&gt;, which suits a web handler that triages tickets as they arrive. Decision models are also in the &lt;strong&gt;Flutter&lt;/strong&gt;, &lt;strong&gt;React Native/Expo&lt;/strong&gt;, &lt;strong&gt;Swift&lt;/strong&gt;, &lt;strong&gt;Kotlin&lt;/strong&gt; and &lt;strong&gt;Godot&lt;/strong&gt; bindings, so the same questions can run inside a mobile or desktop app, and the ticket text never leaves the device.&lt;/p&gt;

&lt;p&gt;The full Python guide can be found &lt;a href="https://docs.nobodywho.ai/python/decision-models" rel="noopener noreferrer"&gt;here&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;If this saved you a parser, &lt;a href="https://github.com/nobodywho-ooo/nobodywho" rel="noopener noreferrer"&gt;a star on the repo&lt;/a&gt; is the easiest way to tell the team to keep going ❤️&lt;/p&gt;

</description>
      <category>python</category>
      <category>ai</category>
      <category>opensource</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>(How) Can LLMs run in the browser?</title>
      <dc:creator>pielouNW</dc:creator>
      <pubDate>Wed, 28 Oct 2026 09:45:51 +0000</pubDate>
      <link>https://dev.to/pielounw/how-can-llms-run-in-the-browser-3ia</link>
      <guid>https://dev.to/pielounw/how-can-llms-run-in-the-browser-3ia</guid>
      <description>&lt;p&gt;A big push here at &lt;a href="https://www.nobodywho.ai" rel="noopener noreferrer"&gt;NobodyWho&lt;/a&gt; is to get language models running in the browser using WebAssembly.&lt;/p&gt;

&lt;p&gt;After working on it for a few weeks we've gotten a good idea of the landscape. One thing that's very clear, and this will come as a surprise, is that browsers are actually kinda complicated.&lt;/p&gt;

&lt;p&gt;First, we use Rust at NobodyWho, and there are (at least) three different compilation targets for Rust for WASM: Emscripten, WASI, and &lt;code&gt;wasm32-unknown-unknown&lt;/code&gt; with &lt;code&gt;wasm-bindgen&lt;/code&gt;. All with different levels of support in various dependencies and within Rust itself.&lt;/p&gt;

&lt;p&gt;Second, there are (at least) three different hardware backends: CPU, WebGPU, and WebNN, with the last two being the only reasonably fast ones and both being very new or experimental.&lt;/p&gt;

&lt;p&gt;Third, there's the question of whether the LLM will be running in a website or a browser extension.&lt;/p&gt;

&lt;p&gt;Fourth, and importantly, there are many different browsers and many different browser versions. Many of the features needed for running LLMs in a browser are relatively new, and browser support can be spotty, especially if your users haven't updated for a while.&lt;/p&gt;

&lt;h2&gt;
  
  
  Spoiled for choice
&lt;/h2&gt;

&lt;p&gt;Fortunately, many of these dimensions can get flattened by technical constraints or a specific use case.&lt;/p&gt;

&lt;p&gt;At NobodyWho we use both llama.cpp and the ONNX Runtime as inference engines, and they both assume a system library. That's an issue for &lt;code&gt;wasm-bindgen&lt;/code&gt;, since the second &lt;code&gt;unknown&lt;/code&gt; in &lt;code&gt;wasm32-unknown-unknown&lt;/code&gt; means "unknown OS", i.e. no system library. This leaves WASI and Emscripten, with Emscripten being the obvious choice given that WASI in the browser is… experimental.¹&lt;/p&gt;

&lt;p&gt;The story for hardware backends is just as clear: run with WebGPU when available and fall back to CPU when WebGPU isn't available (or when on Firefox, since apparently GPU is &lt;a href="https://github.com/ngxson/wllama/issues/296" rel="noopener noreferrer"&gt;slower&lt;/a&gt; there). Why not WebNN? Again because it's… experimental.²&lt;/p&gt;

&lt;p&gt;At NobodyWho we're focusing on running LLMs in a website rather than a browser extension, since that's what we have a use case for, but there are some interesting tradeoffs between the two. A big issue with running an LLM in the browser is that useful models are &lt;em&gt;big&lt;/em&gt;. Even the small models are typically a few hundred MB, which is much more than a typical website. Given how important load times are, that can be a huge blocker. For an extension, you download the model once, deliberately, when you install the extension, so it taking some time is less of an issue,³ while website use is generally more ephemeral. This is less true for websites that users typically return to often, since the site can then cache the model so the download cost is only paid once.&lt;/p&gt;

&lt;p&gt;Lastly we have browsers, and it's a mess. The most relevant features are WebGPU, Memory64 (see my &lt;a href="https://www.nobodywho.ai/posts/letting-rust-support-models-bigger-than-4gb-in-the-browser/" rel="noopener noreferrer"&gt;previous post&lt;/a&gt;), and SharedArrayBuffer (for multi-threading). All of these have patchy support across browsers and platforms. Often, support just isn't implemented yet. Other times it is, but requires a feature flag to be turned on in the browser (good luck getting your non-technical users to do that). Sometimes, it is officially supported, but just works really poorly. The best example of this is Firefox's WebGPU support mentioned above.&lt;/p&gt;

&lt;p&gt;Multi-threading and SharedArrayBuffer is its own can of worms because it requires strict cross-origin headers (COOP/COEP) which can be a real problem for some sites. And of course the easiest way to meet these requirements are different for each browser. On top of that, browsers vary wildly in how much storage they allow for websites, what their eviction policies are, how aggressive they are in killing workers, etc. The only good news is that of the large number of different browsers, in practice &lt;a href="https://gs.statcounter.com/browser-market-share" rel="noopener noreferrer"&gt;most people&lt;/a&gt; use Chromium-based browsers like Chrome and Edge,⁴ and that is where support is by far the best.&lt;/p&gt;

&lt;h2&gt;
  
  
  LLMs do run in the browser
&lt;/h2&gt;

&lt;p&gt;But not cleanly. There are a lot of options and target platforms/browsers/use cases all requiring you to handle stuff differently. Until browser support for the necessary features becomes much more mature, a library for browser LLM inference that just works in all browsers, on all platforms, for most use cases, will require a &lt;em&gt;lot&lt;/em&gt; of work on top of the basic browser APIs. A bit ironic given that part of the dream of WASM is to &lt;em&gt;not&lt;/em&gt; depend on specific platforms so heavily.&lt;/p&gt;

&lt;p&gt;I think incorporating such a general web LLM library into NobodyWho as part of our mission to support all platforms would be super cool, but for now we're exploring a few specific use cases where Emscripten and WebGPU with CPU fallback are the right choice. We haven't figured out how to handle the spotty browser support yet, but we'll get to that in time. Hopefully you'll see a release announcement right here within the next few weeks!&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Notes&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;¹ &lt;a href="https://www.npmjs.com/package/@bytecodealliance/preview2-shim" rel="noopener noreferrer"&gt;Everything is shimmed ATM&lt;/a&gt;, which makes it pretty damn slow.&lt;/p&gt;

&lt;p&gt;² It's supported &lt;a href="https://web-platform-dx.github.io/web-features-explorer/features/webnn/" rel="noopener noreferrer"&gt;nowhere&lt;/a&gt;. But at least there's a &lt;a href="https://issues.chromium.org/issues/40206287" rel="noopener noreferrer"&gt;bug report&lt;/a&gt; on that for Chromium!&lt;/p&gt;

&lt;p&gt;³ Extensions have other issues though. Browsers generally &lt;a href="https://developer.chrome.com/docs/extensions/develop/concepts/service-workers/lifecycle" rel="noopener noreferrer"&gt;aggressively kill&lt;/a&gt; background processes like those an extension would hold an LLM in, so without some hacky solutions (e.g. &lt;a href="https://developer.chrome.com/docs/extensions/reference/api/offscreen" rel="noopener noreferrer"&gt;offscreen documents&lt;/a&gt;) to keep them alive you'd need to constantly reload the multi-GB model from disk.&lt;/p&gt;

&lt;p&gt;⁴ I'm not saying this is a &lt;em&gt;good&lt;/em&gt; thing, just that it is the case, unless you're targeting some specific demographics. E.g. iOS browsers are all WebKit based (like Safari) while the proportion of Firefox users is higher among Google &lt;del&gt;hating&lt;/del&gt; skeptical tech-nerds.&lt;/p&gt;

</description>
      <category>webassembly</category>
      <category>rust</category>
      <category>llm</category>
      <category>webgpu</category>
    </item>
    <item>
      <title>How to build an offline transcription app with Whisper</title>
      <dc:creator>pielouNW</dc:creator>
      <pubDate>Mon, 26 Oct 2026 14:25:05 +0000</pubDate>
      <link>https://dev.to/pielounw/how-to-build-an-offline-transcription-app-with-whisper-1pa3</link>
      <guid>https://dev.to/pielounw/how-to-build-an-offline-transcription-app-with-whisper-1pa3</guid>
      <description>&lt;p&gt;At &lt;a href="https://github.com/nobodywho-ooo/nobodywho" rel="noopener noreferrer"&gt;NobodyWho&lt;/a&gt;, we've spent the last months shipping new features, like Speech-to-Text or Voice Activity Detection, to complete our local AI stack, and now felt like a good time to run a few experiments with our software.&lt;/p&gt;

&lt;p&gt;I decided to build a simple transcription app with React Native that you can use fully offline once you have downloaded the whisper model (small, int8 quantization). With this app, you can easily record 10+ minutes of English speech and save either the entire transcript or just the parts you are interested in. Let's take a closer look at the implementation and see how it compares to a cloud transcription service.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recording and transcription
&lt;/h2&gt;

&lt;p&gt;The interface is straightforward: a button to start the recording and a timer, with the first sentences appearing as soon as they are processed. Let's have a look at what happens when the user presses the recording button:&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%2Fn9olsu0v3hj7y41bml55.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%2Fn9olsu0v3hj7y41bml55.png" alt="Record screen" width="282" height="612"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;First the OS asks permission for microphone access.&lt;/li&gt;
&lt;li&gt;Then the audio streaming starts and the stream of audio buffers arrives.&lt;/li&gt;
&lt;li&gt;Each buffer is processed by &lt;code&gt;transcribeChunk&lt;/code&gt; (via &lt;code&gt;onChunk&lt;/code&gt;), which calls NobodyWho's &lt;code&gt;.transcribePcm&lt;/code&gt; method and returns the corresponding text. Since Whisper by default, returns non-verbal human sounds like coughs, laughs, yawning and so on, we remove them from the transcription (see &lt;code&gt;cleanText&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;When the user presses the stop button, we stop the recording and wait for any queued chunks to be processed thanks to &lt;code&gt;pendingRef&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here is what the simplified streaming process with buffers looks like in the app:&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useAudioStream&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;sampleRate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SAMPLE_RATE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;channels&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;encoding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;int16&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;onBuffer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;chunkerRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;chunkerRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Chunker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sampleRate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onChunk&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;chunkerRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;toMonoInt16&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// .push() also calls onChunk&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;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;requestRecordingPermissionsAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&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;onChunk&lt;/span&gt; &lt;span class="o"&gt;=&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="nx"&gt;AudioChunk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;chunksRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&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="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;live&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;LiveChunk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;start&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="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;end&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="nx"&gt;end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nx"&gt;chunksRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;chunksRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;live&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;job&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;transcribeChunk&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;chunksRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;chunksRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&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;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;index&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="nf"&gt;setChunks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunksRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;pendingRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;job&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;transcribeChunk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &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="nx"&gt;AudioChunk&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;whisperModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transcribePcm&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="nx"&gt;samples&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="nx"&gt;sampleRate&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;completed&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;cleanText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&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;Once the job queue is empty, the user is redirected to the Transcript screen where they can see the full transcript, export or listen to each chunk individually or compact them into one audio and text file:&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%2Flf1vwsb2phhxd9gwoa1k.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%2Flf1vwsb2phhxd9gwoa1k.png" alt="Transcript screen" width="282" height="612"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Results and comparison
&lt;/h2&gt;

&lt;p&gt;Alright, but how does it compare to other transcription tools? Since I'm French and my English is well, not the best as you can imagine, I often use subtitles on YouTube videos. Therefore I did a little comparison, using latest &lt;a href="https://www.youtube.com/watch?v=ESABTN4VtrA" rel="noopener noreferrer"&gt;MKBHD tech review&lt;/a&gt; of the iPhone Duo:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;YouTube transcript&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;First-generation devices are always the most fun to review. And for the past, like, month, everyone I talk to has been asking me, "What do you think of the iPhone Duo? Like, should I get a foldable iPhone?? Is that for me? Who is it even really for?? You've been testing it for the past week, right?? Yeah... So most new pieces of tech are fundamentally like a solid idea at their core. And then they can be a little bit rough around the edges, but that'll get better over time. The iPhone Duo specifically, as the first folding iPhone, is very polished around the edges, but kind of rough in the middle, if that makes sense.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;App transcript&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;First generation devices are always the most fun to review. And for the past like month everyone I've talked to has been asking me, "What do you think of the iPhone Duo? Should I get a foldable iPhone? Is that for me? Who is it even really for?" You've been testing it for the past week, right? Yeah. So most new pieces of tech are fundamentally like a solid idea at their core and then they can be a little bit rough around the edges but that'll get better over time. The iPhone Duo specifically as the first folding iPhone is very polished around the edges but kind of rough in the middle if that makes sense.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The differences are minor. A "like" has been omitted by the Transcriber app, "I've talked" isn't correct in the Youtube transcript and punctuation is sometimes better in one version and sometimes in the other one, that's it!&lt;/p&gt;

&lt;p&gt;This app has been vibe-coded and manually adjusted in half a day. It obviously needs more polishing, but it clearly shows potential for being a solid alternative to paid-per-token, cloud based solutions.&lt;/p&gt;

&lt;p&gt;Adding Voice Activity Detection (&lt;a href="https://docs.nobodywho.ooo/js/voice-activity-detection" rel="noopener noreferrer"&gt;VAD&lt;/a&gt;) would be a nice way to visually separate chunks of text, and using another more powerful whisper model (medium, large... with less quantization) would also improve results.&lt;/p&gt;

&lt;p&gt;For more details, you can find the &lt;a href="https://github.com/pielouNW/transcribe-app" rel="noopener noreferrer"&gt;app’s repo on GitHub&lt;/a&gt; where you can download the Android apk to test it on your phone. &lt;/p&gt;

&lt;p&gt;Want to build your app with on-device AI features? Star &lt;a href="https://github.com/nobodywho-ooo/nobodywho" rel="noopener noreferrer"&gt;NobodyWho on GitHub&lt;/a&gt; and dive into the &lt;a href="https://docs.nobodywho.ai" rel="noopener noreferrer"&gt;docs&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>whisper</category>
      <category>reactnative</category>
      <category>programming</category>
    </item>
    <item>
      <title>Letting Rust support models bigger than 4GB in the browser</title>
      <dc:creator>pielouNW</dc:creator>
      <pubDate>Wed, 21 Oct 2026 09:40:44 +0000</pubDate>
      <link>https://dev.to/pielounw/letting-rust-support-models-bigger-than-4gb-in-the-browser-22mf</link>
      <guid>https://dev.to/pielounw/letting-rust-support-models-bigger-than-4gb-in-the-browser-22mf</guid>
      <description>&lt;p&gt;At NobodyWho we aim to make language models run everywhere. We already have support for mobile and desktop apps, the Godot game engine, and even the Apple Watch. Nowadays though, most of the software people use isn't desktop apps running directly on their architecture of choice, but instead running in a browser on some website. It's therefore a huge hole in our promise that we don't support running language models in the browser. We know that, which is part of why we recently started work on adding WASM support to our library.&lt;/p&gt;

&lt;p&gt;One thing that most people know about large language models is that they are, well, large, with even the "small" language models that NobodyWho specializes in providing easily taking many GB. This is a problem when wasm32 has a hard cap of 4 GB memory . Luckily we have wasm64 to save us. Great news is that Rust does support a &lt;code&gt;wasm64&lt;/code&gt; target. Bad news is, it doesn't support a &lt;code&gt;wasm64&lt;/code&gt;+ Emscripten target.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;unknown&lt;/code&gt; (no C interop)&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;emscripten&lt;/code&gt; (C/C++ interop)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;wasm32&lt;/strong&gt; (4 GB cap)&lt;/td&gt;
&lt;td&gt;✅ &lt;code&gt;wasm32-unknown-unknown&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;✅ &lt;code&gt;wasm32-unknown-emscripten&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;wasm64&lt;/strong&gt; (Memory64)&lt;/td&gt;
&lt;td&gt;✅ &lt;code&gt;wasm64-unknown-unknown&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;❌ missing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The most straightforward way to fix this would be to create a custom target and use that, but of course Rust is open source, so we can fix it at the source and others can benefit as well. One issue was that a lot of the Rust standard library assumes that the only non-&lt;code&gt;unknown&lt;/code&gt; wasm target was 32-bit. The Rust compiler has a fair bit of conditional compilation conditional on whether the target is wasm or not, but most of it just checked for &lt;code&gt;target_arch="wasm32"&lt;/code&gt; instead of wasm targets in general so first step was fixing that by replacing &lt;code&gt;target_arch="wasm32"&lt;/code&gt; with &lt;code&gt;target_family="wasm"&lt;/code&gt; (&lt;a href="https://github.com/rust-lang/rust/pull/163794" rel="noopener noreferrer"&gt;PR&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Next step is actually creating the target. We have a &lt;a href="https://github.com/rust-lang/rust/pull/163808" rel="noopener noreferrer"&gt;PR&lt;/a&gt; for this active in the Rust repo, but it'll probably take some time for this to get merged, and even longer before it lands in stable.&lt;/p&gt;

&lt;p&gt;Of course all of this still requires us to get NobodyWho to run in WASM, which both requires large changes in our own library and more upstream work, which we are making &lt;a href="https://github.com/pykeio/ort/pull/663" rel="noopener noreferrer"&gt;progress&lt;/a&gt; on. Even after that there's the exciting problem of figuring out how to make this &lt;em&gt;useful&lt;/em&gt;. Figuring out problems like getting the models to the user's machine (even a small half GB model can be a lot to download while on a website), and how to make them run reasonably fast.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Want to run LLMs in the browser?&lt;/strong&gt; Star and watch &lt;a href="https://github.com/nobodywho-ooo/nobodywho" rel="noopener noreferrer"&gt;NobodyWho on GitHub&lt;/a&gt; to get notified when WebAssembly support drops!&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>webassembly</category>
      <category>rust</category>
    </item>
    <item>
      <title>You don’t need [insert expensive code review agent here]. All you need is Pi.</title>
      <dc:creator>pielouNW</dc:creator>
      <pubDate>Tue, 20 Oct 2026 09:32:57 +0000</pubDate>
      <link>https://dev.to/pielounw/you-dont-need-insert-expensive-code-review-agent-here-all-you-need-is-pi-4mom</link>
      <guid>https://dev.to/pielounw/you-dont-need-insert-expensive-code-review-agent-here-all-you-need-is-pi-4mom</guid>
      <description>&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%2Fqp3q5qijdz0v9luygs6a.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%2Fqp3q5qijdz0v9luygs6a.png" alt="Our code review action powered by Pi and GLM 5.3" width="800" height="702"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Our code review action powered by Pi and GLM 5.3&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;As we’re shipping out more and more code, it’s becoming increasingly hard to keep up. There’s a whole economy of code-review-as-a-service companies that are tackling that space. &lt;a href="https://www.coderabbit.ai" rel="noopener noreferrer"&gt;CodeRabbit&lt;/a&gt;, &lt;a href="https://www.greptile.com" rel="noopener noreferrer"&gt;Greptile&lt;/a&gt;, &lt;a href="https://macroscope.io" rel="noopener noreferrer"&gt;Macroscope&lt;/a&gt;, are just some of them. These are great, but they are also expensive. Additionally - in most of them, you don’t actually pay per review or lines of code, or even tokens, you pay per seat!&lt;/p&gt;

&lt;p&gt;At &lt;a href="https://www.nobodywho.ai" rel="noopener noreferrer"&gt;NobodyWho&lt;/a&gt;, we are all about keeping control. And if we can avoid a fat bill while we are at it, we will. Instead of adopting an expensive and fancy code review service, we just use &lt;a href="https://github.com/earendil-works/pi-coding-agent" rel="noopener noreferrer"&gt;Pi&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;Need a review in your PR? Just comment &lt;code&gt;/ai-review&lt;/code&gt; and an agent will review your code. We control the model, we control the prompt, and we control when and how it runs. &lt;/p&gt;

&lt;p&gt;Here’s how it works. &lt;/p&gt;

&lt;h2&gt;
  
  
  The core: Code review skill
&lt;/h2&gt;

&lt;p&gt;We stole the original review skill from the &lt;a href="https://github.com/earendil-works/pi-review" rel="noopener noreferrer"&gt;Earendil folks and their pi-review extension&lt;/a&gt; (we are big fans!). In our repo it sits right under &lt;a href="https://github.com/nobodywho-ooo/nobodywho/blob/main/.agents/skills/ai-review/SKILL.md" rel="noopener noreferrer"&gt;&lt;code&gt;.agents/skills/ai-review/SKILL.md&lt;/code&gt;&lt;/a&gt;. Anyone can run that skill locally before they open a Pull Request. &lt;/p&gt;

&lt;p&gt;It’s a single file with code review instructions that our team can iterate on. One of the things we like the most about it is that it categorizes issues according to priority: P0 (fix this now), P1 (you should fix it), etc.  &lt;/p&gt;

&lt;p&gt;We are completely in control of what the agent reviews, and how it reviews it. Great - now how do we run it?&lt;/p&gt;

&lt;h2&gt;
  
  
  The engine: GitHub Actions
&lt;/h2&gt;

&lt;p&gt;Our &lt;a href="https://github.com/nobodywho-ooo/nobodywho/blob/main/.github/workflows/ai-review.yml" rel="noopener noreferrer"&gt;GitHub Action&lt;/a&gt; is also straightforward: &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It runs whenever someone posts &lt;code&gt;/ai-review&lt;/code&gt; as a comment in a PR&lt;/li&gt;
&lt;li&gt;Adds an &lt;code&gt;ai-review&lt;/code&gt; label and acknowledges that it started reviewing the code&lt;/li&gt;
&lt;li&gt;It gets the details and diff of the PR&lt;/li&gt;
&lt;li&gt;It installs Pi&lt;/li&gt;
&lt;li&gt;The code review launches using the Pi CLI, our selected model from &lt;a href="https://openrouter.ai" rel="noopener noreferrer"&gt;OpenRouter&lt;/a&gt; (we like &lt;a href="https://openrouter.ai/z-ai/glm-5.3" rel="noopener noreferrer"&gt;GLM 5.3&lt;/a&gt;!) and using the code review skill from the main branch (to avoid prompt injection shenanigans)&lt;/li&gt;
&lt;li&gt;When it’s done it &lt;a href="https://github.com/nobodywho-ooo/nobodywho/pull/810#issuecomment-6145687451" rel="noopener noreferrer"&gt;posts the review to the PR&lt;/a&gt; as a comment&lt;/li&gt;
&lt;li&gt;Finally, it &lt;a href="https://github.com/nobodywho-ooo/nobodywho/actions/runs/38407626876" rel="noopener noreferrer"&gt;saves the review as a GitHub Action artifact we can inspect and debug&lt;/a&gt; - which uses Pi’s export functionality &lt;/li&gt;
&lt;/ol&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%2Fmely0jzs8tstkjvyqw9s.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%2Fmely0jzs8tstkjvyqw9s.png" alt="Inspecting the transcript from an AI code review run" width="800" height="571"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Inspecting the transcript from an AI code review run&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/nobodywho-ooo/nobodywho/blob/main/.github/workflows/ai-review.yml" rel="noopener noreferrer"&gt;Check our action here!&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why do we prefer this?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Control&lt;/strong&gt;: We know what model ran, we know what prompt ran, and we can change those whenever we want according to what we need.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Price:&lt;/strong&gt; We pay per review or per token. We don’t pay per seat. No subscriptions, no surprises, no rug pulls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Customizability:&lt;/strong&gt; Want to test that new model? Go for it. Too verbose? Adjust the skill. Swarm of agents? Go nuts. Want to make it more fancy and have it comment the diff and suggest? Possible!&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debuggability:&lt;/strong&gt; Thanks to GitHub Actions and Pi, we save a full transcript every time we run it. This means we can see exactly what the agent inspected and how it reasoned about it. We could even train our own code review agent in the future given the team’s feedback.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simple:&lt;/strong&gt; One YAML file, that’s all you need.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We hope some day that these can even run on local models in the future. Some argue we are almost there. And we are all about local models at &lt;a href="https://github.com/nobodywho-ooo/nobodywho" rel="noopener noreferrer"&gt;NobodyWho&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>agents</category>
      <category>cicd</category>
    </item>
    <item>
      <title>Why isn't Pi written in Lisp? Pi Durable in Erlang?</title>
      <dc:creator>pielouNW</dc:creator>
      <pubDate>Mon, 19 Oct 2026 13:33:55 +0000</pubDate>
      <link>https://dev.to/pielounw/why-isnt-pi-written-in-lisp-pi-durable-in-erlang-gh7</link>
      <guid>https://dev.to/pielounw/why-isnt-pi-written-in-lisp-pi-durable-in-erlang-gh7</guid>
      <description>&lt;p&gt;I'm a big fan of pi-coding-agent. I'm also a big fan of thinking about programming languages and their properties. When I combine the two, something feels dissonant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pi feels lisp-y
&lt;/h2&gt;

&lt;p&gt;Since I first started using pi, I haven't been able to shake the hunch that it &lt;em&gt;feels&lt;/em&gt; like something best written in lisp.&lt;/p&gt;

&lt;p&gt;Why? Its goals align with the goals of lisp programs: minimal, radically extensible, self-modifiable and dynamic.&lt;/p&gt;

&lt;p&gt;If you squint a little, emacs stans start to sound like pi stans.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Yeah you can do that in emacs. Emacs even includes the tools to change to emacs in that way! Just whip up the elisp repl and there you go!&lt;/p&gt;

&lt;p&gt;Yeah you can do that in pi. Pi even includes the tools to change pi in that way! Just yeet a prompt like this and there you to!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I did hit a concrete snag when messing around with pi at one point: I was toying with the idea of storing the JSONL conversation history in the git tree (not as files, but as notes attached to the history). Eventually I convinced myself that this was not possible to do well without forking pi itself. Maybe this would have gone different in a homoiconic language?&lt;/p&gt;

&lt;h2&gt;
  
  
  Pi Durable feels BEAM-y
&lt;/h2&gt;

&lt;p&gt;I had this deja-vu feeling again when reading the &lt;a href="https://earendil.com/posts/pi-durable/" rel="noopener noreferrer"&gt;Pi Durable blogpost&lt;/a&gt;, but for a different technology this time.&lt;/p&gt;

&lt;p&gt;It highlights a number of properties that come straight from the mouth of a Erlang/Elixir/Gleam/BEAM/OTP die-hard.&lt;/p&gt;

&lt;p&gt;Earendil writes:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;We want to change the code of a running agent without stopping it.&lt;/p&gt;

&lt;p&gt;A tool call that is already running finishes on the code it started with&lt;/p&gt;

&lt;p&gt;That allows the harness to run on one machine while its tools run on another. Your env function builds the environment for every tool call, from the conversation's working directory, so each conversation can run in a different place.&lt;/p&gt;

&lt;p&gt;Installing an extension under a name that is already installed replaces it in one step.&lt;/p&gt;

&lt;p&gt;If the process dies, a new process opens the same storage, finds the unfinished tasks, and continues each one from its last checkpoint.&lt;/p&gt;

&lt;p&gt;Aborting a task aborts what it owns, bottom-up, so every task cleans up its own effects first, and a task only finishes once the work it owns has finished.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;All of these sound like exactly features that are already implemented and battle-tested in BEAM/OTP. Is this an instance of &lt;a href="https://en.wikipedia.org/wiki/Greenspun%27s_tenth_rule" rel="noopener noreferrer"&gt;Greenspun's Tenth Rule&lt;/a&gt; for Erlang?&lt;/p&gt;

&lt;h2&gt;
  
  
  Should LLMs write in mainstream languages?
&lt;/h2&gt;

&lt;p&gt;A common argument is that "LLMs are just better at writing typescript". Does the &lt;em&gt;agentic era&lt;/em&gt; imply that we should double-down on the most mainstream tech stacks?&lt;/p&gt;

&lt;p&gt;I often think about &lt;a href="https://youtu.be/ZsBHc-J9f8o" rel="noopener noreferrer"&gt;Richard Feldman coping&lt;/a&gt; with having put years of his life into making Roc, a new programming language, just as LLMs were getting good.&lt;/p&gt;

&lt;p&gt;At first he was distraught: Roc code is not well-represented in the LLMs' training dataset, and since LLMs are better at writing stuff they'd seen before, Roc would never grow popular enough to be represented in the dataset. Thus all new languages are doomed.&lt;/p&gt;

&lt;p&gt;But then he did a 180: LLMs are &lt;em&gt;better&lt;/em&gt; at writing in new languages than humans are. Just throw the docs and a few examples at the model, and it outputs good and valid code- accomplishing in minutes what would take an unassisted human days or weeks. LLMs are a great boon to niche technologies.&lt;/p&gt;

&lt;p&gt;The Roc project then released a text file to throw at your coding agent and kept trucking.&lt;/p&gt;

&lt;p&gt;It &lt;a href="https://paulgraham.com/avg.html" rel="noopener noreferrer"&gt;does matter&lt;/a&gt; which programming language your program is written in. A programming language carries with it some properties that transfer onto the program artifact. A program written in Rust simply is more likely to be memory safe than a program written in C, even if you tell your agent to make no mistakes.&lt;/p&gt;

&lt;p&gt;While it is possible to write your program in an arbitrary language, in such a way that it implements the properties you want, this is exactly how you end up with &lt;a href="https://en.wikipedia.org/wiki/Greenspun%27s_tenth_rule" rel="noopener noreferrer"&gt;Greenspun's Tenth Rule&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The point being: yes, while LLMs are better at writing typescript than other languages, they're also very good at writing in a language they've never seen before. Because your technological foundation does have a significant impact on the result, I believe the advantage of the softened learning curve far outweighs the LLM-familiarty advantage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Back to Pi
&lt;/h2&gt;

&lt;p&gt;Earendil does address this in their single-entry FAQ for Pi Durable:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Why TypeScript again?&lt;/p&gt;

&lt;p&gt;Because it is the easiest way to bootstrap this. But as everybody knows by now, it's very easy to port everything to Rust or assembler. We're not ruling this out in the future, but at the moment we are focusing on TypeScript&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I read this as "It's not an informed choice. We just went with the tool that we had at hand", and that's fine by me.&lt;/p&gt;

&lt;h2&gt;
  
  
  ...and what about our code?
&lt;/h2&gt;

&lt;p&gt;We write &lt;a href="https://github.com/nobodywho-ooo/nobodywho" rel="noopener noreferrer"&gt;NobodyWho&lt;/a&gt; in Rust because it gives us the properties we want. It gives us confidence in&amp;nbsp;&amp;nbsp;correctness and reliability. It's highly portable with good cross-compilation for many targets. It has excellent FFI support (made legible via a strong macro system) which we use in two directions: binding downwards into llama.cpp for the juicy GGML acceleration, and binding upwards to the seven different programming languages you can use NobodyWho from.&lt;/p&gt;

&lt;p&gt;People sometimes ask me if I had trouble finding people with sufficient Rust experience to recruit for this project. My answer is usually: "Why would they need Rust experience?"&lt;/p&gt;

</description>
      <category>agents</category>
      <category>programming</category>
      <category>tools</category>
    </item>
    <item>
      <title>NobodyWho vs RunAnywhere: On-Device Inference Engine Comparison</title>
      <dc:creator>pielouNW</dc:creator>
      <pubDate>Fri, 09 Oct 2026 08:21:27 +0000</pubDate>
      <link>https://dev.to/pielounw/nobodywho-vs-runanywhere-on-device-inference-engine-comparison-nae</link>
      <guid>https://dev.to/pielounw/nobodywho-vs-runanywhere-on-device-inference-engine-comparison-nae</guid>
      <description>&lt;p&gt;On paper, NobodyWho and RunAnywhere look almost identical. They run LLMs locally on consumer devices such as laptops and phones, are built on llama.cpp, and both list the same features across Kotlin, Swift, Python, Flutter and React Native. The differences only show up once you put them in the same app and start a real conversation. That's exactly what I did, side by side on iPhone and Android.&lt;/p&gt;

&lt;p&gt;Here is a brief summary of the technical findings:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Speed:&lt;/strong&gt; NobodyWho starts answering sooner on a single prompt, and RunAnywhere gets slower with every turn of a conversation, up to 33× slower after 20 turns on Android.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multimodal:&lt;/strong&gt; RunAnywhere accepts one image per request and no audio. NobodyWho lets you mix several images and audio files in one prompt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool calling:&lt;/strong&gt; RunAnywhere forgets previous tool results, so it can't answer follow-up questions about them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every result can be reproduced with my &lt;a href="https://github.com/pielouNW/runanywhere-react-native-starter-app" rel="noopener noreferrer"&gt;test app on GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Before getting to the benchmarks, let's start with an overview of both libraries: engine, model format and features, platform support and licensing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Engine, model format and features
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/nobodywho-ooo/nobodywho" rel="noopener noreferrer"&gt;NobodyWho&lt;/a&gt; runs any GGUF model through llama.cpp, loaded straight from Hugging Face, a URL or a local path, with no conversion step.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/RunanywhereAI/runanywhere-sdks" rel="noopener noreferrer"&gt;RunAnywhere&lt;/a&gt; also uses llama.cpp, and can additionally register MLX and QHexRT (Qualcomm Hexagon NPU) backends. Registering them did not change any of my results below.&lt;/p&gt;

&lt;p&gt;Both libraries offer hardware acceleration and a similar feature set: text generation, multimodal input, embeddings, RAG, speech-to-text, text-to-speech, structured output, voice activity detection and tool calling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Platform support
&lt;/h2&gt;

&lt;p&gt;NobodyWho and RunAnywhere both support Kotlin, Swift, Python, Flutter, React Native and Expo.&lt;/p&gt;

&lt;p&gt;RunAnywhere also supports Electron and WebAssembly. NobodyWho has &lt;a href="https://github.com/nobodywho-ooo/nobodywho/pull/755" rel="noopener noreferrer"&gt;started work&lt;/a&gt; on WebAssembly, which will be available soon. NobodyWho also ships for Godot, and runs on &lt;a href="https://apps.apple.com/us/app/nobodywho-eyes/id6771770762" rel="noopener noreferrer"&gt;Apple Vision Pro&lt;/a&gt; and &lt;a href="https://apps.apple.com/us/app/nobodywho-wrist/id6762020355?platform=watch" rel="noopener noreferrer"&gt;Apple Watch&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Licensing
&lt;/h2&gt;

&lt;p&gt;NobodyWho uses &lt;a href="https://github.com/nobodywho-ooo/nobodywho/blob/main/LICENSE" rel="noopener noreferrer"&gt;EUPL-1.2&lt;/a&gt;, an OSI-approved open-source licence, allowing proprietary and commercial projects to use the engine free of charge. However, if you distribute a modified version of NobodyWho itself, those engine changes must be open sourced.&lt;/p&gt;

&lt;p&gt;RunAnywhere describes its licence as &lt;a href="https://github.com/RunanywhereAI/runanywhere-sdks/blob/main/LICENSE" rel="noopener noreferrer"&gt;"RunAnywhere License (Apache 2.0 based, with additional commercial-use terms)."&lt;/a&gt; For companies, the free grant only applies to organizations with both "Less than $1,000,000 USD in total funding" and "Less than $1,000,000 USD in gross annual revenue." Organizations outside the listed criteria "must obtain a separate commercial license." If either threshold is exceeded later on, "a commercial license must be obtained within thirty (30) days."&lt;/p&gt;

&lt;p&gt;In practice, NobodyWho stays free no matter how much funding your company raises or how much revenue it earns. RunAnywhere is free for a company only while it stays under $1M in total funding and $1M in annual revenue. Once it passes either figure, a paid commercial licence is required.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technical comparison
&lt;/h2&gt;

&lt;p&gt;To compare both engines under the same conditions, I added NobodyWho to the &lt;a href="https://github.com/RunanywhereAI/react-native-starter-app" rel="noopener noreferrer"&gt;RunAnywhere React Native Starter App&lt;/a&gt; and ran both side by side on an iPhone Air and a Samsung S25. The comparison covers &lt;strong&gt;speed&lt;/strong&gt;, &lt;strong&gt;multimodal input&lt;/strong&gt; and &lt;strong&gt;tool calling&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I used the React Native SDKs, but none of these issues are specific to React Native. They come from RunAnywhere's inference engine and APIs, so they affect every platform and language it supports. You can &lt;a href="https://github.com/pielouNW/runanywhere-react-native-starter-app" rel="noopener noreferrer"&gt;check out the test app&lt;/a&gt; and run it on your own device to reproduce every result. Note that the RunAnywhere starter app needed a few fixes before it would build with Xcode 27, which you can find listed in the appendix at the end of this article.&lt;/p&gt;

&lt;h3&gt;
  
  
  Speed
&lt;/h3&gt;

&lt;p&gt;The speed tests were done on both phones with the Qwen3 0.6B model and the same configuration.&lt;/p&gt;

&lt;p&gt;On a single prompt, NobodyWho starts answering sooner on both phones, but RunAnywhere generates faster on Android:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;iPhone Air:&lt;/strong&gt; RunAnywhere generates 70.5 tokens per second (tok/s), with a time to first token (TTFT) of 201 ms. NobodyWho generates 70.8 tok/s, with a TTFT of 46 ms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Samsung S25:&lt;/strong&gt; RunAnywhere generates 52.4 tok/s, with a TTFT of 1268 ms (fluctuate between 300 and 1300 ms in my tests). NobodyWho generates 32.2 tok/s, with a TTFT of 230 ms.&lt;/li&gt;
&lt;/ul&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%2Fr8spzkcg1dh559zjn145.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%2Fr8spzkcg1dh559zjn145.png" alt="single turn inference speed" width="800" height="643"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The real problem appears in a conversation, where &lt;strong&gt;RunAnywhere's TTFT grows with every turn&lt;/strong&gt;. Over 20 turns, it climbs from 147 ms to 471 ms on the iPhone Air (3× slower), and from 333 ms to over 11 seconds on the S25 (33× slower). NobodyWho stays between 33 ms and 60 ms on the iPhone Air, and between 218 ms and 896 ms on the S25. It means that after a few prompts, &lt;strong&gt;NobodyWho is answering faster&lt;/strong&gt;, even on Android.&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%2Fb9l3vaqgrucaa8atfb1n.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%2Fb9l3vaqgrucaa8atfb1n.png" alt="multi turn inference speed" width="800" height="882"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This happens because on every turn, RunAnywhere starts from an empty cache and re-processes the system prompt, the entire chat history and the new message. NobodyWho keeps the conversation in its KV cache and only processes the new message. With RunAnywhere, &lt;strong&gt;the longer the conversation, the slower the response&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;NobodyWho currently shows the same growing TTFT issue with hybrid models like Qwen3.5, since the cache can't yet be reused between turns in the same way. A &lt;a href="https://github.com/nobodywho-ooo/nobodywho/pull/803" rel="noopener noreferrer"&gt;fix&lt;/a&gt; is in progress.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multimodal input
&lt;/h3&gt;

&lt;p&gt;Multimodal LLMs like Gemma 4 can take images and/or audio files along with a prompt. Let's see how each library handles them.&lt;/p&gt;

&lt;p&gt;With RunAnywhere, you cannot send an audio file to the model, and you can only send &lt;strong&gt;one image at a time&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// node_modules/@runanywhere/core/src/Public/Api/Vlm.ts&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;vlm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ImageInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;LlmOptions&lt;/span&gt;
  &lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;GenerationResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;

  &lt;span class="nf"&gt;generateStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ImageInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;LlmOptions&lt;/span&gt;
  &lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;AsyncIterable&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;GenerationEvent&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In both &lt;code&gt;generate&lt;/code&gt; and &lt;code&gt;generateStream&lt;/code&gt;, the image is a required argument. Every follow-up question about the same image means sending it again and having the model process it again, instead of continuing the conversation naturally.&lt;/p&gt;

&lt;p&gt;It is also &lt;strong&gt;not possible to interleave&lt;/strong&gt; several images and audio files in a single prompt, which NobodyWho allows:&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;chat&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Prompt&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
      &lt;span class="nx"&gt;Prompt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Tell me what you see in the images and what you hear in the audio.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nx"&gt;Prompt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/path/to/dog.png&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nx"&gt;Prompt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/path/to/cat.png&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nx"&gt;Prompt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Audio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/path/to/sound.mp3&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;]),&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;completed&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Tool calling
&lt;/h3&gt;

&lt;p&gt;Tool calling lets your LLM call predefined functions when needed. For example, if you give the LLM a &lt;code&gt;get_weather&lt;/code&gt; function, it will call it whenever the user asks about the weather.&lt;/p&gt;

&lt;p&gt;Both libraries handle tool calling properly, but RunAnywhere does not keep previous tool calls and their results in the conversation history, so follow-up questions get answered incorrectly.&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%2Farid9u2v9aqlqr6y8h1r.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%2Farid9u2v9aqlqr6y8h1r.png" alt="tool calling" width="800" height="858"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The screenshot above compares the official NobodyWho and RunAnywhere apps, both running Qwen3 4B. After a &lt;code&gt;get_weather&lt;/code&gt; call, NobodyWho answers "What is the humidity?" from the earlier result, while RunAnywhere has forgotten it and asks for the location again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing between NobodyWho and RunAnywhere
&lt;/h2&gt;

&lt;p&gt;The two libraries share the same feature list, but the differences become obvious once you use them. With RunAnywhere, the chat slows down with every message, the assistant forgets what its tools just returned, and you can only send one image at a time.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Requirement&lt;/th&gt;
&lt;th&gt;Engine&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OSI open-source licence with no revenue ceiling&lt;/td&gt;
&lt;td&gt;NobodyWho&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wide platform and language support&lt;/td&gt;
&lt;td&gt;Both&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fast multi-turn conversations&lt;/td&gt;
&lt;td&gt;NobodyWho&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multimodal support&lt;/td&gt;
&lt;td&gt;NobodyWho&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool calling&lt;/td&gt;
&lt;td&gt;NobodyWho&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;RunAnywhere is a good fit if you need Electron or WebAssembly support right now. For everything else, &lt;strong&gt;&lt;a href="https://github.com/nobodywho-ooo/nobodywho" rel="noopener noreferrer"&gt;NobodyWho&lt;/a&gt; is the better choice for on-device AI&lt;/strong&gt;: answers stay fast as conversations grow, tool results carry over to follow-up questions, and the licence stays free however big your company gets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Appendix: building the RunAnywhere starter app with Xcode 27
&lt;/h2&gt;

&lt;p&gt;The RunAnywhere starter app doesn't build out of the box with Xcode 27 on macOS 27. Both the starter app and the latest RunAnywhere SDK use older versions of React Native (&lt;a href="https://github.com/RunanywhereAI/react-native-starter-app/blob/e1117fe0e506f1d5edbb148f0d179b75b7f6c7b7/package.json#L25" rel="noopener noreferrer"&gt;0.83&lt;/a&gt; and &lt;a href="https://github.com/RunanywhereAI/runanywhere-sdks/blob/acc341c8eae9078a5ab99102bad0ca8bb0377fc7/bindings/react-native/package.json#L63" rel="noopener noreferrer"&gt;0.85&lt;/a&gt;) instead of the current &lt;a href="https://reactnative.dev/versions" rel="noopener noreferrer"&gt;0.87&lt;/a&gt;, and they conflict with Xcode 27's stricter compiler. To get it running, I &lt;a href="https://github.com/pielouNW/runanywhere-react-native-starter-app/blob/f70c06dc344792f9320ee42dad6974dda5c69126/ios/Podfile#L44-L90" rel="noopener noreferrer"&gt;fixed four issues in the Podfile&lt;/a&gt; (an outdated fmt C++ library, Swift 6 concurrency errors, an Objective-C class hidden from Swift and an undeclared static library), and a method called by the wrong name with a &lt;a href="https://github.com/pielouNW/runanywhere-react-native-starter-app/blob/f70c06dc344792f9320ee42dad6974dda5c69126/.yarn/patches/@runanywhere-core-npm-0.20.19-f4d947d688.patch" rel="noopener noreferrer"&gt;Yarn patch&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Info&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;This article was written on October 9, 2026. Results may have changed since then, on both the NobodyWho and RunAnywhere sides.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;@runanywhere&lt;/code&gt; dependencies in &lt;code&gt;package.json&lt;/code&gt; are set to v0.20.19. &lt;a href="https://github.com/pielouNW/runanywhere-react-native-starter-app/tree/feat/ra-v0.20.27" rel="noopener noreferrer"&gt;Updating them&lt;/a&gt; to the latest published version, v0.20.27, did not change the results.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>mobile</category>
      <category>llm</category>
    </item>
    <item>
      <title>When Chat Templates Go Wrong</title>
      <dc:creator>pielouNW</dc:creator>
      <pubDate>Thu, 01 Oct 2026 08:59:33 +0000</pubDate>
      <link>https://dev.to/pielounw/when-chat-templates-go-wrong-4k3k</link>
      <guid>https://dev.to/pielounw/when-chat-templates-go-wrong-4k3k</guid>
      <description>&lt;p&gt;A model running locally loads cleanly and answers correctly, then keeps generating past its answer, stops following the system prompt, or drops an instruction it followed a turn earlier. The same model served through a hosted API behaves normally.&lt;/p&gt;

&lt;p&gt;This behavior usually traces back to the chat template or the token metadata stored in the GGUF file.&lt;/p&gt;

&lt;p&gt;An instruction-tuned model is trained on conversations written in one exact format. The chat template reproduces that format at inference, so the model receives the same layout it saw during training. When the rendered prompt deviates from it, the model is working from input it was never trained on, and its behavior degrades.&lt;/p&gt;

&lt;p&gt;Most models ship a correct template, so the fault is usually in a specific file or setup, and it can be fixed without touching the weights.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Chat Template
&lt;/h2&gt;

&lt;p&gt;In that training format, control tokens mark where each turn begins and ends and which role is speaking. The wording inside a turn can vary, because that varies in training, but the control tokens and the structure around them are what the model keys on, and behavior degrades as that structure drifts from what the model saw in training.&lt;/p&gt;

&lt;p&gt;The chat template defines that structure. It is a Jinja program that renders a list of messages into the single string the model receives, special tokens included, and it runs every time a message is added. These programs are not trivial, and the template Gemma ships with runs to roughly 250 lines. In a GGUF file the template is stored in the metadata under the &lt;code&gt;tokenizer.chat_template&lt;/code&gt; key. (The wider tour of what else lives in GGUF metadata is in &lt;a href="https://www.nobodywho.ai/posts/whats-in-a-gguf/" rel="noopener noreferrer"&gt;What's in a GGUF, besides the weights&lt;/a&gt;.)&lt;/p&gt;

&lt;p&gt;A common layout, ChatML, structures a turn like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;|im_start|&amp;gt;system
You are a helpful assistant.&amp;lt;|im_end|&amp;gt;
&amp;lt;|im_start|&amp;gt;user
What is the capital of Denmark?&amp;lt;|im_end|&amp;gt;
&amp;lt;|im_start|&amp;gt;assistant
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;&amp;lt;|im_end|&amp;gt;&lt;/code&gt; is a single token with its own vocabulary id, which the model was trained to emit at the end of a turn and which the engine watches for to stop generation. The trailing &lt;code&gt;&amp;lt;|im_start|&amp;gt;assistant&lt;/code&gt; line, with no content after it, is the generation prompt, the signal that the assistant's turn has begun, and without it the model has no indication that it should respond instead of continuing the preceding text.&lt;/p&gt;

&lt;p&gt;Control tokens differ across model families, so Gemma marks turns one way and a ChatML-style model such as LFM2 or Qwen another, and a template written for one family does not align with the tokens another was trained on. Some models also ship more than one template, typically a plain one and a separate one for tool calling, and rendering the wrong variant changes the string with nothing to signal the substitution.&lt;/p&gt;

&lt;p&gt;The template is a program that has to be run on every message, and the engine that runs it is not the same across tools. Hugging Face transformers uses jinja2, llama.cpp's server and CLI use their own Jinja implementation, and &lt;a href="https://github.com/nobodywho-ooo/nobodywho" rel="noopener noreferrer"&gt;NobodyWho&lt;/a&gt; uses minijinja, a pure-Rust reimplementation.&lt;/p&gt;

&lt;p&gt;Model authors write and test their templates against transformers, so Python's jinja2 is the reference. Templates often call Python string methods such as &lt;code&gt;.strip()&lt;/code&gt;, or helpers that transformers adds such as &lt;code&gt;raise_exception&lt;/code&gt;, and the other engines have to reproduce them. When they do, every engine renders the same string and the difference between them is speed. When an engine is missing something a template uses, the template fails to render or produces a different string, and the model is back to input it was never trained on. NobodyWho defines &lt;code&gt;raise_exception&lt;/code&gt; itself and turns on minijinja's Python compatibility layer, which supplies string and dictionary methods such as &lt;code&gt;.split()&lt;/code&gt; and &lt;code&gt;.get()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A separate case is an older libllama call, &lt;code&gt;llama_chat_apply_template&lt;/code&gt;, which ignores the model's embedded template and applies one of a small set of formats hardcoded in C++. This means that the template format in use is not the one the model shipped with, and a model outside that hardcoded set is rendered with a format that does not match it. The model can then ignore instructions or run past the end of a turn, even though the template stored in the file is correct.&lt;/p&gt;

&lt;p&gt;This is also why the same model behaves correctly through a hosted API, where the serving stack applies the model's registered template and tokenizer and that layer is never exposed. Run locally, that layer is the caller's responsibility.&lt;/p&gt;

&lt;h2&gt;
  
  
  Invalid Token IDs
&lt;/h2&gt;

&lt;p&gt;Special tokens are referenced by id, as indices into the token list at &lt;code&gt;tokenizer.ggml.tokens&lt;/code&gt;, and the metadata records which entry is beginning-of-sequence, which is end-of-sequence, which ends a turn, and so on. When one of those ids is negative or points past the end of that list, the file still opens and passes surface inspection. Current llama.cpp catches it at load, logs a bad special token warning, and falls back to a default id, so the model runs with a special token the metadata never intended.&lt;/p&gt;

&lt;h2&gt;
  
  
  Silent Failures
&lt;/h2&gt;

&lt;p&gt;When the token the model ends a turn on is not the token the engine treats as the stop signal, the end of the turn goes unrecognized and generation continues into the next one, with the model producing both sides of the conversation. Current llama.cpp recognizes common end-of-turn tokens such as &lt;code&gt;&amp;lt;|im_end|&amp;gt;&lt;/code&gt; by name, even when the metadata names a different stop token. The run-on shows up with older engines, wrappers that stop only on the end-of-sequence id in the metadata, end-of-turn tokens outside the names llama.cpp checks, or files where the special tokens did not survive conversion.&lt;/p&gt;

&lt;p&gt;A chat model's end-of-turn token often differs from the base end-of-sequence token, so a ChatML model stops on &lt;code&gt;&amp;lt;|im_end|&amp;gt;&lt;/code&gt; rather than the model's plain &lt;code&gt;&amp;lt;|endoftext|&amp;gt;&lt;/code&gt; token, and that end-of-turn token has to be recorded in the GGUF metadata as the token to stop on. When it is absent or wrong, an engine that stops only on the recorded token watches for a token the model does not emit at the end of a turn, and generation runs past the assistant's response. This was reported in llama.cpp &lt;a href="https://github.com/ggml-org/llama.cpp/issues/5040" rel="noopener noreferrer"&gt;issue #5040&lt;/a&gt;. A related conversion fault produces the same result: when the special tokens are not preserved as single tokens, the template emits the text &lt;code&gt;&amp;lt;|im_end|&amp;gt;&lt;/code&gt; while the tokenizer splits that string into ordinary sub-word tokens, and the trained stop token never reaches the engine.&lt;/p&gt;

&lt;p&gt;A missing generation prompt causes the model to continue the user's text instead of answering it. Incorrect whitespace around a role header tokenizes differently from the training format and lowers quality with no visible failure. Some fields do not survive conversion: the &lt;code&gt;think_token&lt;/code&gt; separating a model's reasoning from its answer is frequently absent from GGUF conversions even when the upstream model defines it, and tool-call formats vary between families, so a template rendering them incorrectly produces calls the parser cannot read.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking a Template
&lt;/h2&gt;

&lt;p&gt;To check a template, run three checks:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Render the final string the template produces immediately before tokenization, with the special tokens in place, and compare it against the format on the model card character for character.&lt;/li&gt;
&lt;li&gt;Tokenize that string and confirm each special token resolves to a single dedicated id. A special token that splits into several ordinary tokens is why the model fails to stop.&lt;/li&gt;
&lt;li&gt;Check what the run stopped on. A generation that ends at the token limit without emitting a stop token means the stop token is wrong or unregistered.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Diagnosing a Stop Token Mismatch
&lt;/h2&gt;

&lt;p&gt;Take a ChatML model whose end-of-turn token, &lt;code&gt;&amp;lt;|im_end|&amp;gt;&lt;/code&gt;, is not the model's base end-of-sequence token and is not recorded in the GGUF metadata as the token to stop on, running on an engine that stops only on the token recorded in the metadata. A single question returns a correct answer, then generation continues: the model writes a follow-up as the user, answers it, and halts only at the token limit. No error is raised and the transcript reads fluently.&lt;/p&gt;

&lt;p&gt;The rendered prompt matches the model card, so the template text is correct. The turn markers tokenize to single ids, so the vocabulary is intact. Generation stopped on the length limit without the engine recognizing a stop token, and the stop token recorded in the metadata is the base end-of-sequence token rather than &lt;code&gt;&amp;lt;|im_end|&amp;gt;&lt;/code&gt;. The weights are correct, so the fix is a metadata edit: set the stop token to &lt;code&gt;&amp;lt;|im_end|&amp;gt;&lt;/code&gt;, and the same prompt answers once and stops.&lt;/p&gt;

&lt;h2&gt;
  
  
  Correcting the Metadata
&lt;/h2&gt;

&lt;p&gt;Each of these faults is a small edit, often a single field, yet correcting one has until recently required either re-quantizing the model or running ad-hoc Python scripts against the file, a change of a few bytes in an otherwise correct header. &lt;a href="https://github.com/nobodywho-ooo/gguf-surgeon" rel="noopener noreferrer"&gt;gguf-surgeon&lt;/a&gt; opens the metadata block in a GGUF, reads and rewrites fields in place including the chat template and the token ids, and writes the file back with the tensor data byte-identical, so the model loads exactly as before.&lt;/p&gt;

&lt;p&gt;An out-of-range id can be caught before publication with a single check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gguf check model.gguf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It reports the offending key and that its id is out of range, in the wording of its validator: token id N is out of range (tokens array has M elements). Correcting a token id is one command, because set parses the value as the existing key's type. Replace 106 with the id of the correct token in the model's own vocabulary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gguf &lt;span class="nb"&gt;set &lt;/span&gt;model.gguf tokenizer.ggml.eos_token_id 106 &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Editing the template is a matter of reading it out, changing it, and writing it back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gguf get model.gguf tokenizer.chat_template &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; template.j2
&lt;span class="nv"&gt;$EDITOR&lt;/span&gt; template.j2
gguf &lt;span class="nb"&gt;set &lt;/span&gt;model.gguf tokenizer.chat_template @template.j2 &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In each case the tensor data is untouched and the file continues to load in llama.cpp.&lt;/p&gt;

</description>
      <category>llm</category>
      <category>opensource</category>
      <category>ai</category>
      <category>llamacpp</category>
    </item>
    <item>
      <title>NobodyWho vs Ollama: When to Use Each</title>
      <dc:creator>pielouNW</dc:creator>
      <pubDate>Mon, 28 Sep 2026 09:31:42 +0000</pubDate>
      <link>https://dev.to/pielounw/nobodywho-vs-ollama-when-to-use-each-f35</link>
      <guid>https://dev.to/pielounw/nobodywho-vs-ollama-when-to-use-each-f35</guid>
      <description>&lt;p&gt;NobodyWho and Ollama both run language models locally, without a cloud API, so they are often compared as alternatives. They are built for different jobs. Ollama runs a model on a machine you control. NobodyWho embeds a model inside an application that runs on your users' devices. Both build on the same foundation, so the decision comes down to where the model runs and who runs it.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Built on llama.cpp&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Both trace back to llama.cpp, the open-source engine that made it practical to run language models on ordinary hardware, and both read the GGUF model format. Ollama began as a Go server wrapping llama.cpp and now runs its own ggml-based engine for multimodal and other newer models. NobodyWho builds on llama.cpp directly and contributes its fixes upstream. Because the inference layer under both is the same, the choice between them is about deployment: where the model needs to run, and who runs it.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Ollama&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Ollama runs a model on a machine you administer, your own workstation or a server.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Installs as a background service exposing an HTTP API on &lt;code&gt;127.0.0.1:11434&lt;/code&gt;, with a CLI for pulling and running models.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Manages the model lifecycle: downloading, loading, and keeping a model resident between requests.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Offers an OpenAI-compatible API route, so existing OpenAI client code works against a local model with almost no change.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Pulls from its own model registry, and imports a GGUF file directly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Runs on macOS, Linux, and Windows, with GPU acceleration on Nvidia, AMD, Apple, and Intel hardware.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It fits work where the model and its caller sit on infrastructure you control: evaluating several models locally, backing a script or notebook with a local endpoint instead of a paid API, or self-hosting inference for tools on your own network.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;NobodyWho&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;NobodyWho embeds a model inside an application you ship to other people, so the model runs on the user's own hardware.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Compiles into the app as a library, with no separate service or daemon to install.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Runs in the app's own process, on the end user's device, fully offline.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Ships as a package for Godot, Flutter, React Native, Swift, Kotlin, and Python.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Loads plain GGUF files from Hugging Face or any URL.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Runs on Android, iOS, and desktop, down to hardware as small as an Apple Watch.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Keeps user data on the device, since inference never leaves it.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You cannot bundle a daemon like Ollama inside a mobile app or an app-store game and require every user to install and run it, which is the gap NobodyWho fills. An on-device assistant in a mobile app, an NPC in a Godot game, or a desktop tool that works with no network all fit here.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Using Them Together&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The two commonly appear in one project at different stages. Ollama is a fast way to evaluate models during development, comparing candidates from your own machine. Once a model is chosen and the work turns to shipping it inside an application that runs on your users' devices, NobodyWho is what embeds it. Running a model for yourself and shipping one to your users are different questions, and a project often answers both.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>gguf</category>
      <category>ollama</category>
    </item>
    <item>
      <title>NobodyWho vs Cactus compared on engine design, model format, hardware, platforms, cloud, and licensing.</title>
      <dc:creator>pielouNW</dc:creator>
      <pubDate>Wed, 16 Sep 2026 18:35:15 +0000</pubDate>
      <link>https://dev.to/pielounw/nobodywho-vs-cactus-compared-on-engine-design-model-format-hardware-platforms-cloud-and-1518</link>
      <guid>https://dev.to/pielounw/nobodywho-vs-cactus-compared-on-engine-design-model-format-hardware-platforms-cloud-and-1518</guid>
      <description>&lt;p&gt;Choosing an on-device inference engine sets where the model runs and under what terms. NobodyWho and Cactus both run models on the user's device, with no API key, no per-request cost, and no data leaving the hardware once the model is downloaded. Underneath, they are different engines. NobodyWho Edge is built on &lt;a href="https://github.com/ggerganov/llama.cpp" rel="noopener noreferrer"&gt;llama.cpp&lt;/a&gt; and runs GGUF models. Cactus is a from-scratch engine with its own quantization format, and positions itself as a llama.cpp alternative. This is a technical comparison of NobodyWho vs Cactus across engine and model format, hardware, installation, platform coverage, cloud behaviour, and licensing. The better fit depends on the target device and the commercial model of the product.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Engine and model format&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/nobodywho-ooo" rel="noopener noreferrer"&gt;NobodyWho&lt;/a&gt; runs GGUF models through llama.cpp. It loads any GGUF file from Hugging Face or a URL directly, with no conversion step, so any model already published in GGUF, at any of the quantization levels the ecosystem provides, runs as-is.&lt;/p&gt;

&lt;p&gt;Cactus runs its own format. Cactus Quants (CQ) is a rotation-and-codebook quantization applied to every weight tensor, from 4-bit down to 1-bit, and the engine runs from CQ bundles. You get a bundle by downloading one Cactus has pre-built for its own model catalog (cactus download) or by converting a source model yourself (cactus convert), which &lt;a href="https://github.com/cactus-compute/cactus" rel="noopener noreferrer"&gt;Cactus&lt;/a&gt; documents as experimental for models it has not pre-built.&lt;/p&gt;

&lt;p&gt;NobodyWho runs the existing GGUF catalog with no conversion step. Cactus's CQ is tuned in-house for on-device size and quality, with published accuracy tables per bit-width, at the cost of depending on its own bundles or an experimental conversion for anything outside its catalog.&lt;/p&gt;

&lt;p&gt;Both also cover the parts of an app beyond chat: embeddings, speech-to-text, text-to-speech, and tool calling. NobodyWho generates the tool-calling grammar from your function signatures and constrains generation to it, so you pass plain functions and the output conforms to the expected structure without writing schemas by hand. Cactus takes OpenAI and MCP-style tool definitions, ships a vector index for retrieval, and provides Needle, a 26M-parameter model dedicated to tool calling.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Hardware acceleration&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Cactus is built around the mobile processor. Its kernels are hand-written in ARM NEON SIMD for the CPU, with Metal on Apple GPUs and the Apple Neural Engine for some vision models. Qualcomm, MediaTek, and Exynos NPU support is on its roadmap, not shipped. Cactus publishes per-device benchmarks in its README, listing tokens per second and peak RAM for named iPhone and Mac hardware.&lt;/p&gt;

&lt;p&gt;NobodyWho takes a hardware-aware approach to acceleration, using Vulkan and Metal for GPU execution rather than targeting the NPU.  This is the sharpest hardware split between them. On older or low-end phones that lean on the CPU, Cactus's hand-written kernels are the advantage. On devices with a capable GPU, NobodyWho runs on the accelerator most platforms already expose.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Installation and first call&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;NobodyWho installs through each ecosystem's own package manager: pip install nobodywho for Python, flutter pub add nobodywho for Flutter, npm install react-native-nobodywho for React Native, ai.nobodywho:nobodywho from Maven Central for Kotlin, Swift Package Manager for Swift, and the in-editor asset library for Godot. Each binding is a thin wrapper over one shared Rust core.&lt;/p&gt;

&lt;p&gt;Cactus installs its engine by cloning the repository and running a setup script, then building the binding and downloading a model bundle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/cactus-compute/cactus &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;cactus &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;source&lt;/span&gt; ./setup 
cactus build &lt;span class="nt"&gt;--python&lt;/span&gt; 
cactus download LiquidAI/LFM2-VL-450M
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cactus also publishes per-platform packages on pip, Maven, pub, and npm.&lt;/p&gt;

&lt;p&gt;The difference carries into the first call. NobodyWho's Python API handles model loading and lifecycle for you, and returns the reply as a string:&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;nobodywho&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Chat&lt;/span&gt;

&lt;span class="n"&gt;chat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hf:NobodyWho/Qwen_Qwen3-0.6B-GGUF/Qwen_Qwen3-0.6B-Q4_K_M.gguf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;What is the capital of Denmark?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;completed&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; 
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cactus's Python binding is a ctypes FFI over its C engine, with explicit model lifecycle and JSON message payloads:&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;cactus&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ensure_model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cactus_init&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cactus_complete&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cactus_destroy&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;

&lt;span class="n"&gt;bundle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ensure_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LiquidAI/LFM2-VL-450M&lt;/span&gt;&lt;span class="sh"&gt;"&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="nf"&gt;cactus_init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bundle&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="bp"&gt;None&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="n"&gt;messages&lt;/span&gt; &lt;span class="o"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&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;user&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;content&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;What is the capital of Denmark?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}])&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;cactus_complete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;response&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="nf"&gt;cactus_destroy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both snippets are each project's own documented quick-start.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Platform support&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Cactus targets mobile and embedded. Its C and C++ core runs on wearables, smart-home devices, robots, and Raspberry Pi, with bindings for Flutter, React Native, Kotlin Multiplatform, Swift, Python, and Rust.&lt;/p&gt;

&lt;p&gt;NobodyWho targets mobile, desktop (Linux, macOS, Windows), Python, and the JVM, and ships a binding for the &lt;a href="https://docs.nobodywho.ooo/godot/" rel="noopener noreferrer"&gt;Godot game engine&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Both engines cover phones and wearables, and Cactus reaches further into embedded hardware like robots and Raspberry Pi. Cactus also supports desktop deployment on macOS and ARM Linux. NobodyWho ships a Godot binding and targets desktop app runtimes across Linux, macOS, and Windows through its JVM and Python bindings. Cactus has no game-engine binding. Neither ships a browser or WebAssembly target. NobodyWho has an open GitHub issue tracking WASM export, and Cactus does not target the browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Cloud behaviour&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Both run inference locally by default. Cactus adds an optional cloud handoff that routes low-confidence queries to a hosted model. NobodyWho has no cloud router in the engine. In deployments that prohibit outbound network calls, NobodyWho has nothing to disable, and Cactus's handoff must be turned off and verified.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Licensing&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;NobodyWho uses &lt;a href="https://github.com/nobodywho-ooo/nobodywho" rel="noopener noreferrer"&gt;EUPL-1.2&lt;/a&gt;, an OSI-approved open-source licence. It permits proprietary and commercial use with no revenue limit. The only obligation is that redistributing a modified version of the engine requires publishing those engine changes.&lt;/p&gt;

&lt;p&gt;Cactus is source-available under its own licence. That &lt;a href="https://github.com/cactus-compute/cactus/blob/main/LICENSE" rel="noopener noreferrer"&gt;licence&lt;/a&gt; grants free use to individuals, students, non-profits, and organizations with "Less than $2,000,000 USD in total funding" and "Less than $2,000,000 USD in gross annual revenue." An organization that does not meet those criteria "must obtain a separate commercial license," and a qualifying organization that later crosses either threshold has "thirty (30) days" to obtain one. Both licences were verified on 9 September 2026. Cactus has revised its terms before, so check the current file before shipping a commercial product.&lt;/p&gt;

&lt;p&gt;Both engines are free under those thresholds. Once a company crosses either one, Cactus requires a paid commercial licence and NobodyWho stays free.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Choosing between NobodyWho and Cactus&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The decision follows target hardware and commercial model.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Requirement&lt;/th&gt;
&lt;th&gt;Engine&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Run any existing GGUF model with no conversion step&lt;/td&gt;
&lt;td&gt;NobodyWho&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;In-house quantization tuned for on-device size and quality&lt;/td&gt;
&lt;td&gt;Cactus&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hand-optimized CPU performance on ARM without a capable GPU&lt;/td&gt;
&lt;td&gt;Cactus&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Robotics, smart-home, or Raspberry Pi&lt;/td&gt;
&lt;td&gt;Cactus&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Desktop applications alongside mobile&lt;/td&gt;
&lt;td&gt;NobodyWho&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A model running locally inside a Godot game&lt;/td&gt;
&lt;td&gt;NobodyWho&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OSI open-source licence with no revenue ceiling&lt;/td&gt;
&lt;td&gt;NobodyWho&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The two are different engines with different model formats, so the choice is set by where the model has to run and how the licence treats the product, not by a shared core. The &lt;a href="https://github.com/nobodywho-ooo/nobodywho" rel="noopener noreferrer"&gt;NobodyWho source and per-binding docs&lt;/a&gt; list current platform and format support.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>mobile</category>
    </item>
    <item>
      <title>On-Device AI in Flutter</title>
      <dc:creator>pielouNW</dc:creator>
      <pubDate>Wed, 02 Sep 2026 13:50:47 +0000</pubDate>
      <link>https://dev.to/pielounw/on-device-ai-in-flutter-3np1</link>
      <guid>https://dev.to/pielounw/on-device-ai-in-flutter-3np1</guid>
      <description>&lt;p&gt;In this Flutter tutorial, you'll learn how to run a large language model (LLM) directly on a user's device: no server, no API key needed. We'll start from scratch with a simple chat exchange, and progressively introduce more advanced features: multimodal input, speech-to-text, text-to-speech, voice activity detection, tool calling and RAG.&lt;/p&gt;

&lt;p&gt;Each concept is explained before the code, so you can follow along whether you're new to on-device AI.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why run AI On-Device?
&lt;/h2&gt;

&lt;p&gt;Most AI features rely on a cloud API: you send a request to a remote server, it runs the model, and sends a response back. That works well, but it comes with tradeoffs.&lt;/p&gt;

&lt;p&gt;Running the model directly on the device avoids all of them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Works offline&lt;/strong&gt; — no internet connection required&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privacy by design&lt;/strong&gt; — user data never leaves the device&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Low latency&lt;/strong&gt; — no network round-trip&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No cloud costs&lt;/strong&gt; — inference is free&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tradeoff is raw capability: on-device models are smaller and less powerful than frontier cloud models. But for many use cases like summarization, chatbots, or local search, they're more than good enough.&lt;/p&gt;




&lt;h2&gt;
  
  
  About NobodyWho
&lt;/h2&gt;

&lt;p&gt;We'll use the &lt;a href="https://github.com/nobodywho-ooo/nobodywho" rel="noopener noreferrer"&gt;NobodyWho&lt;/a&gt; library throughout this tutorial. It wraps &lt;a href="https://github.com/ggerganov/llama.cpp" rel="noopener noreferrer"&gt;llama.cpp&lt;/a&gt; in Rust and ships bindings for several languages and frameworks: Kotlin, Python, Expo/React Native, Swift, Flutter, and Godot. It exposes a clean Dart API for running any model locally in &lt;code&gt;.gguf&lt;/code&gt; format, on iOS and Android.&lt;/p&gt;

&lt;p&gt;Add it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;flutter pub add nobodywho
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Import it under the &lt;code&gt;nobodywho&lt;/code&gt; namespace, since the package uses common names like &lt;code&gt;Model&lt;/code&gt; and &lt;code&gt;Chat&lt;/code&gt; that could otherwise collide with your own code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:nobodywho/nobodywho.dart'&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;nobodywho&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before calling anything else, initialize the native bindings exactly once in main before &lt;code&gt;runApp()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;nobodywho&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;NobodyWho&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;init&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Loading a Model
&lt;/h2&gt;

&lt;p&gt;NobodyWho can download a GGUF model for you directly from Hugging Face, cache it, and reuse it on every subsequent launch. That means you don't need to bundle anything into your app or manage downloads yourself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;chat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;nobodywho&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Chat&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;modelPath:&lt;/span&gt; &lt;span class="s"&gt;'huggingface:NobodyWho/Qwen_Qwen3-0.6B-GGUF/Qwen_Qwen3-0.6B-Q4_K_M.gguf'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first time this runs, the model is downloaded to the platform cache directory. Every call after that loads the model directly.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;modelPath&lt;/code&gt; accepts a few different forms:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Form&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;HuggingFace reference&lt;/td&gt;
&lt;td&gt;&lt;code&gt;hf:owner/repo/file.gguf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Downloaded and cached on first use&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTPS URL&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://example.com/model.gguf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Downloaded and cached on first use&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Local path&lt;/td&gt;
&lt;td&gt;&lt;code&gt;./model.gguf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Used as-is, no download&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The HuggingFace prefix is case-insensitive and the &lt;code&gt;//&lt;/code&gt; is optional, so &lt;code&gt;hf:&lt;/code&gt;, &lt;code&gt;hf://&lt;/code&gt;, &lt;code&gt;huggingface:&lt;/code&gt;, and &lt;code&gt;huggingface://&lt;/code&gt; are all equivalent. You can also pass &lt;code&gt;"auto"&lt;/code&gt; to let NobodyWho pick a chat model based on the device's available memory, which is a handy default if you don't want to think about model selection at all.&lt;/p&gt;

&lt;p&gt;You can track a remote download by passing &lt;code&gt;onDownloadProgress&lt;/code&gt; to &lt;code&gt;Chat.fromPath&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;chat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;nobodywho&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Chat&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;modelPath:&lt;/span&gt; &lt;span class="s"&gt;'huggingface:NobodyWho/Qwen_Qwen3-0.6B-GGUF/Qwen_Qwen3-0.6B-Q4_K_M.gguf'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;onDownloadProgress:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;downloaded&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="si"&gt;$downloaded&lt;/span&gt;&lt;span class="s"&gt; / &lt;/span&gt;&lt;span class="si"&gt;$total&lt;/span&gt;&lt;span class="s"&gt; bytes'&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;You can find thousands of LLMs in &lt;code&gt;.gguf&lt;/code&gt; format on Hugging Face &lt;a href="https://huggingface.co/models?library=gguf&amp;amp;sort=trending" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Basic Chat
&lt;/h2&gt;

&lt;p&gt;With a model loaded, you're ready to start a conversation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;chat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;nobodywho&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Chat&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;modelPath:&lt;/span&gt; &lt;span class="s"&gt;'huggingface:NobodyWho/Qwen_Qwen3-0.6B-GGUF/Qwen_Qwen3-0.6B-Q4_K_M.gguf'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'Is water wet?'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;completed&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Yes, indeed, water is wet!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;chat.ask()&lt;/code&gt; sends your message and returns a &lt;code&gt;TokenStream&lt;/code&gt;. Calling &lt;code&gt;.completed()&lt;/code&gt; waits for the whole response and gives you back the final string, which is fine for a one-off question. But a real chat interface needs to stream tokens as they arrive, otherwise users stare at a blank screen until generation finishes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Streaming Tokens
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'What is the capital of Denmark?'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&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;A &lt;em&gt;token&lt;/em&gt; is the smallest unit a model generates, typically a word, or a fragment of a word.&lt;/p&gt;




&lt;h2&gt;
  
  
  Multimodal Models
&lt;/h2&gt;

&lt;p&gt;Some models can natively ingest images and audio. To use them, you need two things: a multimodal LLM, and its projection model that converts images and/or audio into tokens the LLM can consume (usually named with &lt;code&gt;mmproj&lt;/code&gt; in it). A solid default that handles both image and audio is Gemma 4 with its BF16 projection model.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;nobodywho&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;modelPath:&lt;/span&gt; &lt;span class="s"&gt;"./vision-model.gguf"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;projectionModelPath:&lt;/span&gt; &lt;span class="s"&gt;"./mmproj.gguf"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;chat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nobodywho&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;model:&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To actually send image or audio content, build a &lt;code&gt;Prompt&lt;/code&gt; mixing text, images, and audio, and pass it to &lt;code&gt;chat.askWithPrompt()&lt;/code&gt; instead of a plain string:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;askWithPrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nobodywho&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Prompt&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="n"&gt;nobodywho&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;TextPart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Tell me what you see in the image and what you hear in the audio."&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;nobodywho&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ImagePart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"./dog.png"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;nobodywho&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;AudioPart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"./sound.mp3"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;]))&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;completed&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep in mind that images and audio consume context fast, so you'll likely want a bigger &lt;code&gt;contextSize&lt;/code&gt; than you'd use for text-only chat. Also note that the language model and its projection model have to be trained together — you can't mix an LLM and a projection model you happen to like and expect them to work.&lt;/p&gt;




&lt;h2&gt;
  
  
  Speech to Text
&lt;/h2&gt;

&lt;p&gt;If you'd rather transcribe spoken audio into text than have the model listen to it directly, NobodyWho integrates Whisper models in ONNX format through &lt;code&gt;SpeechToText&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;stt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;nobodywho&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;SpeechToText&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kn"&gt;source&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;'hf://onnx-community/whisper-base'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;stt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;transcribeFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'recording.mp3'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;completed&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;source&lt;/code&gt; is a Hugging Face repo (&lt;code&gt;hf://owner/repo&lt;/code&gt;) or a local directory laid out the same way. Browse the &lt;a href="https://huggingface.co/models?library=onnx&amp;amp;search=whisper" rel="noopener noreferrer"&gt;Whisper ONNX models on Hugging Face&lt;/a&gt; to find one that fits your accuracy and speed needs.&lt;/p&gt;

&lt;p&gt;If your audio comes from a buffer rather than a file, use &lt;code&gt;transcribePcm&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;stt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;transcribePcm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;samples&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;16000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;completed&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The buffer needs to be mono i16 PCM samples. The sample rate can be anything, NobodyWho resamples internally to what Whisper expects. And just like chat, transcription can be streamed piece by piece instead of waiting for the full result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'dart:io'&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;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;piece&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;stt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;transcribeFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'recording.mp3'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;piece&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;h2&gt;
  
  
  Text to Speech
&lt;/h2&gt;

&lt;p&gt;Going the other direction, &lt;code&gt;TextToSpeech&lt;/code&gt; turns text into WAV audio you can play back or save.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'dart:io'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;tts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;nobodywho&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;TextToSpeech&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="kn"&gt;source&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;'hf://NobodyWho/Kokoro-82M'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;voice:&lt;/span&gt; &lt;span class="s"&gt;'bf_emma'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;language:&lt;/span&gt; &lt;span class="s"&gt;'en-gb'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;wav&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;tts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;synthesize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;text:&lt;/span&gt; &lt;span class="s"&gt;'Hello from NobodyWho!'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;File&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'out.wav'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;writeAsBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wav&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three architectures are supported, all ONNX-based: &lt;a href="https://github.com/hexgrad/kokoro" rel="noopener noreferrer"&gt;Kokoro&lt;/a&gt;, &lt;a href="https://github.com/kyutai-labs/pocket-tts" rel="noopener noreferrer"&gt;Pocket TTS&lt;/a&gt;, and &lt;a href="https://github.com/supertone-inc/supertonic" rel="noopener noreferrer"&gt;Supertonic&lt;/a&gt;. NobodyWho infers which one you're using from the &lt;code&gt;source&lt;/code&gt; string, so you only need to set &lt;code&gt;architecture&lt;/code&gt; explicitly when loading from a custom local folder.&lt;/p&gt;

&lt;p&gt;Each architecture has its own &lt;code&gt;voice&lt;/code&gt; and &lt;code&gt;language&lt;/code&gt; options that need to agree with what the model supports.&lt;/p&gt;




&lt;h2&gt;
  
  
  Voice Activity Detection
&lt;/h2&gt;

&lt;p&gt;Before transcribing audio, it helps to know when someone is actually speaking rather than relying on a fixed silence timeout. &lt;code&gt;VoiceActivityDetection&lt;/code&gt; uses a small model to reliably tell speech and silence apart, and pairs naturally with &lt;code&gt;SpeechToText&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For streaming microphone input, push chunks in as they arrive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;vad&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;nobodywho&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;VoiceActivityDetection&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;sampleRate:&lt;/span&gt; &lt;span class="mi"&gt;16000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="kn"&gt;source&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;'hf://onnx-community/silero-vad'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;stt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;nobodywho&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;SpeechToText&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kn"&gt;source&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;'hf://onnx-community/whisper-base'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;readMic&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vad&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;nobodywho&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;VoiceActivityDetectionEvent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;speechEnded&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;speech&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;vad&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;finish&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;transcription&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;stt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;transcribePcm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;speech&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;16000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;completed&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transcription&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each &lt;code&gt;push()&lt;/code&gt; call reports the current state (&lt;code&gt;speechStarted&lt;/code&gt;, &lt;code&gt;speechEnded&lt;/code&gt;, &lt;code&gt;speech&lt;/code&gt;, or &lt;code&gt;silence&lt;/code&gt;), and &lt;code&gt;finish()&lt;/code&gt; hands you back the buffered speech segment while resetting internal state for the next turn.&lt;/p&gt;

&lt;p&gt;If you already have a full recording and just want to pull out the speech segments from it, &lt;code&gt;segment()&lt;/code&gt; does that in one pass:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;audio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;readWavPcm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'recording.wav'&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;final&lt;/span&gt; &lt;span class="n"&gt;speech&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;vad&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;segment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;transcription&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;stt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;transcribePcm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;speech&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;16000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;completed&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="n"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transcription&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;Sensitivity is tunable via &lt;code&gt;threshold&lt;/code&gt;, &lt;code&gt;minSpeechDurationMs&lt;/code&gt;, &lt;code&gt;minSilenceDurationMs&lt;/code&gt;, and &lt;code&gt;prerollDurationMs&lt;/code&gt; (how much audio to keep before the detected start, so you don't clip the beginning of a sentence). The defaults are a reasonable starting point, but VAD is one of those things that usually benefits from tuning to your actual environment.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tool Calling
&lt;/h2&gt;

&lt;p&gt;Tools let the model call out to real functions in your app rather than just generating text. Any Dart function that returns a &lt;code&gt;String&lt;/code&gt; or &lt;code&gt;Future&amp;lt;String&amp;gt;&lt;/code&gt; becomes a tool by wrapping it in &lt;code&gt;Tool()&lt;/code&gt; with a name and description:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'dart:math'&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;circleAreaTool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nobodywho&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;name:&lt;/span&gt; &lt;span class="s"&gt;"circle_area"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;description:&lt;/span&gt; &lt;span class="s"&gt;"Calculates the area of a circle given its radius"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;function:&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="kd"&gt;required&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;radius&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;area&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;pi&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;radius&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;radius&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"Circle with radius &lt;/span&gt;&lt;span class="si"&gt;$radius&lt;/span&gt;&lt;span class="s"&gt; has area &lt;/span&gt;&lt;span class="si"&gt;${area.toStringAsFixed(2)}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;chat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;nobodywho&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Chat&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;modelPath:&lt;/span&gt; &lt;span class="s"&gt;'huggingface:NobodyWho/Qwen_Qwen3-0.6B-GGUF/Qwen_Qwen3-0.6B-Q4_K_M.gguf'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;tools:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;circleAreaTool&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;NobodyWho also ships two general-purpose tools out of the box, a &lt;a href="https://github.com/pydantic/monty" rel="noopener noreferrer"&gt;Python interpreter&lt;/a&gt; and a &lt;a href="https://github.com/everruns/bashkit" rel="noopener noreferrer"&gt;Bash interpreter&lt;/a&gt;, for models that need to reason precisely or compute something:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;chat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;nobodywho&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Chat&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;modelPath:&lt;/span&gt; &lt;span class="s"&gt;'huggingface:NobodyWho/Qwen_Qwen3-0.6B-GGUF/Qwen_Qwen3-0.6B-Q4_K_M.gguf'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;tools:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;nobodywho&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Tool&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;python&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;nobodywho&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Tool&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;bash&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;Not every model supports tool calling well, the &lt;a href="https://huggingface.co/Qwen/models" rel="noopener noreferrer"&gt;Qwen&lt;/a&gt; family is a solid choice if you need it to be reliable. See the &lt;a href="https://docs.nobodywho.ooo/flutter/tool-calling/" rel="noopener noreferrer"&gt;Tool Calling documentation&lt;/a&gt; for more.&lt;/p&gt;




&lt;h2&gt;
  
  
  RAG
&lt;/h2&gt;

&lt;p&gt;Retrieval-Augmented Generation combines document search with LLM generation, so the model grounds its answers in your own knowledge base instead of what it happened to learn during training. NobodyWho provides an &lt;code&gt;Encoder&lt;/code&gt; for embeddings and a &lt;code&gt;CrossEncoder&lt;/code&gt; for reranking:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;encoder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;nobodywho&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Encoder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;modelPath:&lt;/span&gt; &lt;span class="s"&gt;'./embedding-model.gguf'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;queryEmbedding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;encoder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;text:&lt;/span&gt; &lt;span class="s"&gt;"What is the return policy?"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;docEmbeddings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;encoder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;encodeBatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;texts:&lt;/span&gt; &lt;span class="n"&gt;knowledge&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;similarities&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;docEmbeddings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;nobodywho&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cosineSimilarity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nl"&gt;a:&lt;/span&gt; &lt;span class="n"&gt;queryEmbedding&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toList&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="nl"&gt;b:&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toList&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toList&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For better precision, rerank the top candidates with a &lt;code&gt;CrossEncoder&lt;/code&gt; before handing them to the model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;crossencoder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;nobodywho&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;CrossEncoder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;modelPath:&lt;/span&gt; &lt;span class="s"&gt;'./reranker-model.gguf'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;ranked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;crossencoder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;rankAndSort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;query:&lt;/span&gt; &lt;span class="s"&gt;"What is the return policy?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;documents:&lt;/span&gt; &lt;span class="n"&gt;topDocs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See the &lt;a href="https://docs.nobodywho.ooo/flutter/embeddings-and-rag/" rel="noopener noreferrer"&gt;Embeddings &amp;amp; RAG documentation&lt;/a&gt; for the full walkthrough, including how to wire this up as a tool the model calls automatically.&lt;/p&gt;




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

&lt;p&gt;You now have a complete foundation for building on-device AI features in Flutter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Download and run a GGUF model&lt;/li&gt;
&lt;li&gt;Send messages and get streamed tokens back&lt;/li&gt;
&lt;li&gt;Feed images and audio directly into a multimodal model&lt;/li&gt;
&lt;li&gt;Transcribe speech, synthesize it back, and detect when someone's actually talking&lt;/li&gt;
&lt;li&gt;Extend the model with tool calling and perform search with RAG&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can also have a look at the &lt;a href="https://github.com/nobodywho-ooo/flutter-starter-example" rel="noopener noreferrer"&gt;Flutter starter example&lt;/a&gt; to see a full implementation.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://github.com/nobodywho-ooo/nobodywho" rel="noopener noreferrer"&gt;Github&lt;/a&gt; - &lt;a href="https://pub.dev/packages/nobodywho" rel="noopener noreferrer"&gt;pub.dev&lt;/a&gt; - &lt;a href="https://docs.nobodywho.ooo/flutter/" rel="noopener noreferrer"&gt;Docs&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>flutter</category>
      <category>rag</category>
    </item>
    <item>
      <title>On-Device AI in Python</title>
      <dc:creator>pielouNW</dc:creator>
      <pubDate>Wed, 02 Sep 2026 11:26:56 +0000</pubDate>
      <link>https://dev.to/pielounw/on-device-ai-in-python-5ig</link>
      <guid>https://dev.to/pielounw/on-device-ai-in-python-5ig</guid>
      <description>&lt;p&gt;In this Python tutorial, you'll learn how to run a large language model (LLM) directly on a user's device: no server, no API key needed. We'll start from scratch with a simple chat exchange, and progressively introduce more advanced features: multimodal input, speech-to-text, text-to-speech, voice activity detection, tool calling and RAG.&lt;/p&gt;

&lt;p&gt;Each concept is explained before the code, so you can follow along whether you're new to on-device AI.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why run AI On-Device?
&lt;/h2&gt;

&lt;p&gt;Most AI features rely on a cloud API: you send a request to a remote server, it runs the model, and sends a response back. That works well, but it comes with tradeoffs.&lt;/p&gt;

&lt;p&gt;Running the model directly on the device avoids all of them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Works offline&lt;/strong&gt; — no internet connection required&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privacy by design&lt;/strong&gt; — user data never leaves the device&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Low latency&lt;/strong&gt; — no network round-trip&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No cloud costs&lt;/strong&gt; — inference is free&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tradeoff is raw capability: on-device models are smaller and less powerful than frontier cloud models. But for many use cases like summarization, chatbots, or local search, they're more than good enough.&lt;/p&gt;




&lt;h2&gt;
  
  
  About NobodyWho
&lt;/h2&gt;

&lt;p&gt;We'll use the &lt;a href="https://github.com/nobodywho-ooo/nobodywho" rel="noopener noreferrer"&gt;NobodyWho&lt;/a&gt; library throughout this tutorial. It wraps &lt;a href="https://github.com/ggerganov/llama.cpp" rel="noopener noreferrer"&gt;llama.cpp&lt;/a&gt; in Rust and ships bindings for several languages and frameworks: Kotlin, Python, Expo/React Native, Swift, Flutter &amp;amp; Godot. It exposes a clean API for running any model locally in &lt;code&gt;.gguf&lt;/code&gt; format.&lt;/p&gt;

&lt;p&gt;Install it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;nobodywho
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or, preferably:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv add nobodywho
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Loading a Model
&lt;/h2&gt;

&lt;p&gt;NobodyWho can download a GGUF model for you directly from Hugging Face, cache it, and reuse it on every subsequent launch. That means you don't need to manage downloads yourself:&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;nobodywho&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Chat&lt;/span&gt;

&lt;span class="n"&gt;chat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;huggingface:NobodyWho/Qwen_Qwen3-0.6B-GGUF/Qwen_Qwen3-0.6B-Q4_K_M.gguf&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;The first time this runs, the model is downloaded to the platform cache directory. Every call after that loads the model directly.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;model_path&lt;/code&gt; accepts a few different forms:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Form&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;HuggingFace reference&lt;/td&gt;
&lt;td&gt;&lt;code&gt;hf:owner/repo/file.gguf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Downloaded and cached on first use&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTPS URL&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://example.com/model.gguf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Downloaded and cached on first use&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Local path&lt;/td&gt;
&lt;td&gt;&lt;code&gt;./model.gguf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Used as-is, no download&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The HuggingFace prefix is case-insensitive and the &lt;code&gt;//&lt;/code&gt; is optional, so &lt;code&gt;hf:&lt;/code&gt;, &lt;code&gt;hf://&lt;/code&gt;, &lt;code&gt;huggingface:&lt;/code&gt;, and &lt;code&gt;huggingface://&lt;/code&gt; are all equivalent.&lt;/p&gt;

&lt;p&gt;You can track a remote download by calling &lt;code&gt;download_model&lt;/code&gt; directly and passing an &lt;code&gt;on_download_progress&lt;/code&gt; callback. It receives &lt;code&gt;(downloaded_bytes, total_bytes)&lt;/code&gt; and is skipped for cached or local files — if you don't pass anything, NobodyWho prints a default terminal progress bar:&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;nobodywho&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;download_model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Chat&lt;/span&gt;

&lt;span class="n"&gt;model_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;download_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;huggingface:NobodyWho/Qwen_Qwen3-0.6B-GGUF/Qwen_Qwen3-0.6B-Q4_K_M.gguf&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;on_download_progress&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;downloaded&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;print&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="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;downloaded&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;total&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; bytes&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="n"&gt;chat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can find thousands of LLMs in &lt;code&gt;.gguf&lt;/code&gt; format on Hugging Face &lt;a href="https://huggingface.co/models?library=gguf&amp;amp;sort=trending" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Basic Chat
&lt;/h2&gt;

&lt;p&gt;With a model loaded, you're ready to start a 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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;nobodywho&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Chat&lt;/span&gt;

&lt;span class="n"&gt;chat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;huggingface:NobodyWho/Qwen_Qwen3-0.6B-GGUF/Qwen_Qwen3-0.6B-Q4_K_M.gguf&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Is water wet?&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;completed&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# Yes, indeed, water is wet!
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;chat.ask()&lt;/code&gt; sends your message and returns a &lt;code&gt;TokenStream&lt;/code&gt;. Calling &lt;code&gt;.completed()&lt;/code&gt; blocks until the model is done generating and gives you back the final string, which is fine for a one-off question. But a real chat interface needs to stream tokens as they arrive, otherwise users stare at a blank screen until generation finishes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Streaming Tokens
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;What is the capital of Denmark?&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="o"&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;flush&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;em&gt;token&lt;/em&gt; is the smallest unit a model generates, typically a word, or a fragment of a word.&lt;/p&gt;

&lt;p&gt;If you'd rather not block synchronously while a model loads or generates, swap &lt;code&gt;Chat&lt;/code&gt; for &lt;code&gt;ChatAsync&lt;/code&gt; — the API stays the same, so &lt;code&gt;await chat.ask(...).completed()&lt;/code&gt; or &lt;code&gt;async for token in chat.ask(...)&lt;/code&gt; both work.&lt;/p&gt;




&lt;h2&gt;
  
  
  Multimodal Models
&lt;/h2&gt;

&lt;p&gt;Some models can natively ingest images and audio. To use them, you need two things: a multimodal LLM, and its projection model that converts images and/or audio into tokens the LLM can consume (usually named with &lt;code&gt;mmproj&lt;/code&gt; in it). A solid default that handles both image and audio is Gemma 4 with its BF16 projection model.&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;nobodywho&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Chat&lt;/span&gt;

&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;./vision-model.gguf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;projection_model_path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;./projection_model.gguf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;chat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;system_prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You are a helpful assistant, that can hear and see stuff!&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;To actually send image or audio content, build a &lt;code&gt;Prompt&lt;/code&gt; mixing text, images, and audio, and pass it to &lt;code&gt;chat.ask()&lt;/code&gt; instead of a plain string:&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;nobodywho&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Audio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Text&lt;/span&gt;

&lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Prompt&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Tell me what you see in the image and what you hear in the audio.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nc"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;./dog.png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nc"&gt;Audio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;./sound.mp3&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="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;completed&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep in mind that images and audio consume context fast, so you'll likely want a bigger &lt;code&gt;n_ctx&lt;/code&gt; than you'd use for text-only chat. Also note that the language model and its projection model have to be trained together — you can't mix an LLM and a projection model you happen to like and expect them to work.&lt;/p&gt;




&lt;h2&gt;
  
  
  Speech to Text
&lt;/h2&gt;

&lt;p&gt;If you'd rather transcribe spoken audio into text than have the model listen to it directly, NobodyWho integrates Whisper models in ONNX format through &lt;code&gt;SpeechToText&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;nobodywho&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SpeechToText&lt;/span&gt;

&lt;span class="n"&gt;stt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SpeechToText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hf://onnx-community/whisper-base&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transcribe_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;recording.mp3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;completed&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;source&lt;/code&gt; is a Hugging Face repo (&lt;code&gt;hf://owner/repo&lt;/code&gt;) or a local directory laid out the same way. Browse the &lt;a href="https://huggingface.co/models?library=onnx&amp;amp;search=whisper" rel="noopener noreferrer"&gt;Whisper ONNX models on Hugging Face&lt;/a&gt; to find one that fits your accuracy and speed needs.&lt;/p&gt;

&lt;p&gt;If your audio comes from a buffer rather than a file, use &lt;code&gt;transcribe_pcm&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="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transcribe_pcm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;samples&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sample_rate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;16000&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;completed&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The buffer needs to be mono i16 PCM samples. The sample rate can be anything, NobodyWho resamples internally to what Whisper expects. And just like chat, transcription can be streamed piece by piece instead of waiting for the full result:&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;piece&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;stt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transcribe_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;recording.mp3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;piece&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="o"&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;flush&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Text to Speech
&lt;/h2&gt;

&lt;p&gt;Going the other direction, &lt;code&gt;TextToSpeech&lt;/code&gt; turns text into WAV audio you can play back or save.&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;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;nobodywho&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TextToSpeech&lt;/span&gt;

&lt;span class="n"&gt;tts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TextToSpeech&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hf://NobodyWho/Kokoro-82M&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;bf_emma&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;language&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;en-gb&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="n"&gt;wav&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;synthesize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hello from NobodyWho!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;out.wav&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wav&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three architectures are supported, all ONNX-based: &lt;a href="https://github.com/hexgrad/kokoro" rel="noopener noreferrer"&gt;Kokoro&lt;/a&gt;, &lt;a href="https://github.com/kyutai-labs/pocket-tts" rel="noopener noreferrer"&gt;Pocket TTS&lt;/a&gt;, and &lt;a href="https://github.com/supertone-inc/supertonic" rel="noopener noreferrer"&gt;Supertonic&lt;/a&gt;. NobodyWho infers which one you're using from the &lt;code&gt;source&lt;/code&gt; string, so you only need to set &lt;code&gt;architecture&lt;/code&gt; explicitly when loading from a custom local folder.&lt;/p&gt;

&lt;p&gt;Each architecture has its own &lt;code&gt;voice&lt;/code&gt; and &lt;code&gt;language&lt;/code&gt; options that need to agree with what the model supports.&lt;/p&gt;




&lt;h2&gt;
  
  
  Voice Activity Detection
&lt;/h2&gt;

&lt;p&gt;Before transcribing audio, it helps to know when someone is actually speaking rather than relying on a fixed silence timeout. &lt;code&gt;VoiceActivityDetection&lt;/code&gt; uses a small model to reliably tell speech and silence apart, and pairs naturally with &lt;code&gt;SpeechToText&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For streaming microphone input, push chunks in as they arrive:&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;nobodywho&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;VoiceActivityDetection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;VoiceActivityDetectionEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SpeechToText&lt;/span&gt;

&lt;span class="n"&gt;vad&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;VoiceActivityDetection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hf://onnx-community/silero-vad&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sample_rate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;16000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;stt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SpeechToText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hf://onnx-community/whisper-base&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;read_mic&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;vad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;VoiceActivityDetectionEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SpeechEnded&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;

&lt;span class="n"&gt;speech&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;vad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;finish&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;transcription&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transcribe_pcm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;speech&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sample_rate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;16000&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;completed&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transcription&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each &lt;code&gt;.push()&lt;/code&gt; call reports the current state (&lt;code&gt;SpeechStarted&lt;/code&gt;, &lt;code&gt;SpeechEnded&lt;/code&gt;, &lt;code&gt;Speech&lt;/code&gt;, or &lt;code&gt;Silence&lt;/code&gt;), and &lt;code&gt;.finish()&lt;/code&gt; hands you back the buffered speech segment while resetting internal state for the next turn.&lt;/p&gt;

&lt;p&gt;If you already have a full recording and just want to pull out the speech segments from it, &lt;code&gt;.segment()&lt;/code&gt; does that in one pass:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;audio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;read_wav_pcm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;recording.wav&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;speech&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;vad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;segment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;transcription&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transcribe_pcm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;speech&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sample_rate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;16000&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;completed&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transcription&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sensitivity is tunable via &lt;code&gt;threshold&lt;/code&gt;, &lt;code&gt;min_speech_duration_ms&lt;/code&gt;, &lt;code&gt;min_silence_duration_ms&lt;/code&gt;, and &lt;code&gt;preroll_duration_ms&lt;/code&gt; (how much audio to keep before the detected start, so you don't clip the beginning of a sentence). The defaults are a reasonable starting point, but VAD is one of those things that usually benefits from tuning to your actual environment.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tool Calling
&lt;/h2&gt;

&lt;p&gt;Tools let the model call out to real functions in your app rather than just generating text. Any synchronous function that returns a string becomes a tool with the &lt;code&gt;@tool&lt;/code&gt; decorator:&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;import&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;nobodywho&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Chat&lt;/span&gt;

&lt;span class="nd"&gt;@tool&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;Calculates the area of a circle given its radius&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;circle_area&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&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;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;area&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pi&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;radius&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
    &lt;span class="k"&gt;return&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;Circle with radius &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;radius&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; has area &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;area&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;chat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;./model.gguf&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;circle_area&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;NobodyWho inspects the function's name, parameter names, and types to describe the tool to the model — add a &lt;code&gt;params&lt;/code&gt; dict to &lt;code&gt;@tool&lt;/code&gt; if a parameter needs more explanation than its name alone gives.&lt;/p&gt;

&lt;p&gt;NobodyWho also ships two general-purpose tools out of the box, a &lt;a href="https://github.com/pydantic/monty" rel="noopener noreferrer"&gt;Python interpreter&lt;/a&gt; and a &lt;a href="https://github.com/everruns/bashkit" rel="noopener noreferrer"&gt;Bash interpreter&lt;/a&gt;, for models that need to reason precisely or compute something:&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;nobodywho&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;python_tool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bash_tool&lt;/span&gt;

&lt;span class="n"&gt;chat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;./model.gguf&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;python_tool&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nf"&gt;bash_tool&lt;/span&gt;&lt;span class="p"&gt;()])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not every model supports tool calling well, the &lt;a href="https://huggingface.co/Qwen/models" rel="noopener noreferrer"&gt;Qwen&lt;/a&gt; family is a solid choice if you need it to be reliable. See the &lt;a href="https://docs.nobodywho.ooo/python/tool-calling/" rel="noopener noreferrer"&gt;Tool Calling documentation&lt;/a&gt; for more.&lt;/p&gt;




&lt;h2&gt;
  
  
  RAG
&lt;/h2&gt;

&lt;p&gt;Retrieval-Augmented Generation combines document search with LLM generation, so the model grounds its answers in your own knowledge base instead of what it happened to learn during training. NobodyWho provides an &lt;code&gt;Encoder&lt;/code&gt; for embeddings and a &lt;code&gt;CrossEncoder&lt;/code&gt; for reranking:&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;nobodywho&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Encoder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cosine_similarity&lt;/span&gt;

&lt;span class="n"&gt;encoder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Encoder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;./embedding-model.gguf&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;query_embedding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;encoder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;What is the return policy?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;doc_embeddings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;encoder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode_batch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;knowledge&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;similarities&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;cosine_similarity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query_embedding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;doc_emb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;doc_emb&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;doc_embeddings&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For better precision, rerank the top candidates with a &lt;code&gt;CrossEncoder&lt;/code&gt; before handing them to the model:&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;nobodywho&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;CrossEncoder&lt;/span&gt;

&lt;span class="n"&gt;crossencoder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CrossEncoder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;./reranker-model.gguf&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ranked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;crossencoder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rank_and_sort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;What is the return policy?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;top_docs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See the &lt;a href="https://docs.nobodywho.ooo/python/embeddings-and-rag/" rel="noopener noreferrer"&gt;Embeddings &amp;amp; RAG documentation&lt;/a&gt; for the full walkthrough, including how to wire this up as a tool the model calls automatically.&lt;/p&gt;




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

&lt;p&gt;You now have a complete foundation for building on-device AI features in Python:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Download and run a GGUF model&lt;/li&gt;
&lt;li&gt;Send messages and get streamed tokens back, synchronously or with &lt;code&gt;ChatAsync&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Feed images and audio directly into a multimodal model&lt;/li&gt;
&lt;li&gt;Transcribe speech, synthesize it back, and detect when someone's actually talking&lt;/li&gt;
&lt;li&gt;Extend the model with tool calling and perform search with RAG&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;a href="https://github.com/nobodywho-ooo/nobodywho" rel="noopener noreferrer"&gt;Github&lt;/a&gt; - &lt;a href="https://pypi.org/project/nobodywho/" rel="noopener noreferrer"&gt;PyPI&lt;/a&gt; - &lt;a href="https://docs.nobodywho.ooo/python/" rel="noopener noreferrer"&gt;Docs&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>kotlin</category>
    </item>
  </channel>
</rss>
